Merge repair/audit-s5-ux (S5 UX surfaces)

This commit is contained in:
John Smith
2026-10-10 21:09:16 -04:00
16 changed files with 1335 additions and 343 deletions
+19 -14
View File
@@ -520,25 +520,30 @@ export function runHeadless(options: HeadlessOptions): AsyncGenerator<HeadlessEv
agent: "factory",
prompt: async (state, factory) =>
`${await factory.summary(state)}\n${headline(state, await readLiveness(options.root, state))}\nContinue the factory run from its current stage. This is a headless run: no human will answer questions.`,
finished: async (state) => {
if (state.stage === "DONE")
return { type: "done", ...(state.release?.prUrl ? { prUrl: state.release.prUrl } : {}) }
const pending = await pendingApprovals(options.root)
if (pending.length)
return {
type: "waiting",
reason: "a human must approve (es approve <stage>)",
stages: pending.map((item) => item.stage),
}
if (state.stage === "GRILL")
return { type: "waiting", reason: "the grill needs a human; run /grill interactively" }
return undefined
},
finished: (state) => headlessFinished(options.root, state),
// Research artifacts and seat outcomes count as progress, not just the stage machine.
progress: async (state) => progressPrint(state, await readLiveness(options.root, state)),
})
}
/**
* One headless end condition (#247.1): DONE carries the release PR url when
* the run released one, a pending approval waits on a human, and the grill
* needs a human. Anything else keeps driving.
*/
export async function headlessFinished(root: string, state: FactoryState): Promise<HeadlessEvent | undefined> {
if (state.stage === "DONE") return { type: "done", ...(state.release?.prUrl ? { prUrl: state.release.prUrl } : {}) }
const pending = await pendingApprovals(root)
if (pending.length)
return {
type: "waiting",
reason: "a human must approve (es approve <stage>)",
stages: pending.map((item) => item.stage),
}
if (state.stage === "GRILL") return { type: "waiting", reason: "the grill needs a human; run /grill interactively" }
return undefined
}
export const LOG_LEVELS = ["quiet", "info", "debug"] as const
export type LogLevel = (typeof LOG_LEVELS)[number]
+13 -8
View File
@@ -2,10 +2,13 @@
// harness-neutral factory operations to harnesses without a native plugin and
// to headless runs. It has no approve, trust, waive or resume tool: those are
// human-only. Caller identity is PINNED, never piped: the harness adapter sets
// one agent id per server in its environment (ES_MCP_AGENT, else the legacy
// ES_AGENT/defaultAgent, both written by the human in the adapter config), and
// the per-call `agent` argument is ignored. Without an adapter identity the
// caller is "mcp", which maps to no seat, so every seat-checked tool refuses.
// one agent id per server in its environment (`ES_MCP_AGENT`, else the legacy
// `ES_AGENT`, else the `defaultAgent` option — all written by the human in the
// adapter config), and the per-call `agent` argument is ignored. Without an
// adapter identity the caller is "mcp", which maps to no seat, so every
// seat-checked tool refuses. Only a registry agent id (`factory`,
// `es-manager`, `es-programmer`, …) holds a seat: any other pinned string is
// still the caller, but a seat-less one, refused the same way as "mcp".
import { stateDir as defaultStateDir, esTools, Factory, gateRunner } from "@heretek-ai/es-core"
import { VERSION } from "./version.ts"
@@ -26,7 +29,9 @@ export function createMcpServer(options: McpOptions) {
const env = options.env ?? process.env
// Pinned at construction: the adapter's environment, never the caller's
// arguments. An agent piping JSON-RPC into this server cannot claim a seat.
const caller = env.ES_MCP_AGENT || options.defaultAgent || "mcp"
// Library consumers get the same order as the CLI wrapper: the adapter's
// ES_MCP_AGENT wins, then the legacy ES_AGENT, then the defaultAgent option.
const caller = env.ES_MCP_AGENT || env.ES_AGENT || options.defaultAgent || "mcp"
const factory = new Factory(options.root, {
gates: gateRunner(options.stateDir ? { stateDir: options.stateDir } : {}),
...(options.stateDir ? { stateDir: options.stateDir } : {}),
@@ -35,7 +40,7 @@ export function createMcpServer(options: McpOptions) {
const byName = new Map(tools.map((tool) => [tool.name.replace(/^es_/, ""), tool]))
const listed = [...byName].map(([name, tool]) => ({
name,
description: `${tool.description}${name === "status" || name === "spec_validate" ? "" : " Caller identity is pinned by the adapter (ES_MCP_AGENT); a per-call `agent` argument is accepted but ignored."}`,
description: `${tool.description}${name === "status" || name === "spec_validate" ? "" : " Caller identity is pinned by the adapter (ES_MCP_AGENT, else ES_AGENT, else the default agent); a per-call `agent` argument is accepted but ignored."}`,
inputSchema: {
...tool.input,
properties: {
@@ -43,7 +48,7 @@ export function createMcpServer(options: McpOptions) {
agent: {
type: "string",
description:
"Ignored: the caller is the adapter's pinned ES_MCP_AGENT (or the default agent), never this argument.",
"Ignored: the caller is the adapter's pinned ES_MCP_AGENT (else ES_AGENT, else the default agent), never this argument.",
},
},
},
@@ -61,7 +66,7 @@ export function createMcpServer(options: McpOptions) {
capabilities: { tools: { listChanged: false } },
serverInfo: { name: "epistemic-swarm", version: options.version ?? VERSION },
instructions:
"Epistemic Swarm factory operations. Approvals, trust, waivers and resume are human-only (TUI or `es` CLI) and have no MCP tool. Caller identity is pinned: the adapter sets ES_MCP_AGENT in this server's environment (one server per agent); without it the caller is `mcp`, which holds no seat, so seat-checked tools refuse. A per-call `agent` argument is ignored.",
"Epistemic Swarm factory operations. Approvals, trust, waivers and resume are human-only (TUI or `es` CLI) and have no MCP tool. Caller identity is pinned: the adapter sets ES_MCP_AGENT in this server's environment (one server per agent), else the legacy ES_AGENT, else the default agent; without any of them the caller is `mcp`, which holds no seat, so seat-checked tools refuse. A per-call `agent` argument is ignored.",
})
case "ping":
return reply({})
+192 -2
View File
@@ -1,16 +1,19 @@
// Headless fleet probes (#120): per-turn tool metrics carry duration and seat,
// and stay out of the default human output.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { Factory, gateRunner } from "@heretek-ai/es-core"
import { Factory, type FactoryState, factoryLayout, gateRunner } from "@heretek-ai/es-core"
import {
driveHeadless,
type HarnessDriver,
type HeadlessEvent,
HeadlessJsonlSchema,
headlessFinished,
opencodeDriver,
presentEvent,
runHeadless,
toJsonl,
} from "../src/headless.ts"
@@ -117,3 +120,190 @@ describe("turn-metrics human output (#120)", () => {
expect(presentEvent(metrics, "debug")).toEqual(metrics)
})
})
describe("runHeadless end conditions (#247)", () => {
test("a research run drives to done, and done carries no prUrl without a release", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "es-headless-done-"))
const dirState = await mkdtemp(path.join(tmpdir(), "es-headless-done-state-"))
try {
const factory = new Factory(dir, { gates: gateRunner({ stateDir: dirState }), stateDir: dirState })
await factory.beginResearchRun({ objective: "what queue", ceilingUSD: 5 })
const seen: Array<{ agent: string; prompt: string }> = []
const driver: HarnessDriver = {
id: "fake",
available: async () => true,
async *turn(input) {
seen.push({ agent: input.agent, prompt: input.prompt })
yield { text: "working" }
// The coordinator flow, played against the real tools and gate.
const { researchTools } = await import("@heretek-ai/es-core")
const stubFetch = (async () =>
new Response("<p>Queues decouple workers from producers durably.</p>", {
headers: { "content-type": "text/html" },
})) as unknown as typeof fetch
const tools = researchTools({
root: dir,
fetch: stubFetch,
stateDir: dirState,
policy: async () => ({ root: dir }),
})
const fetched = await tools
.find((tool) => tool.name === "es_research_fetch")!
.execute({ url: "https://192.0.2.1/queues" }, { agent: "es-research-alpha" })
const sha = /sha256:([0-9a-f]{64})/.exec(fetched)?.[1]
await writeFile(
path.join(dir, ".factory/research/REPORT.md"),
`# Report\n\n- Queues decouple workers [VERIFIED: sha256:${sha} "decouple workers from producers"]\n`,
)
await new Factory(dir, { gates: gateRunner({ stateDir: dirState }), stateDir: dirState }).completeResearch(
"deep-researcher",
)
return "ses_fake"
},
}
const events: HeadlessEvent[] = []
for await (const event of runHeadless({ root: dir, driver, maxTurns: 5, stallTurns: 99, stateDir: dirState }))
events.push(event)
// The driver saw the factory seat identity and the headless prompt (#247.2).
expect(seen).toHaveLength(1)
expect(seen[0]?.agent).toBe("factory")
expect(seen[0]?.prompt).toContain("headless run")
expect(events[0]).toMatchObject({ type: "start", driver: "fake" })
expect(events.filter((event) => event.type === "turn")).toHaveLength(1)
const done = events.at(-1)!
expect(done).toEqual({ type: "done" })
expect("prUrl" in done).toBe(false)
} finally {
await rm(dir, { recursive: true, force: true })
await rm(dirState, { recursive: true, force: true })
}
})
test("headlessFinished maps DONE (with prUrl spread), pending approvals and the grill", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "es-headless-finished-"))
try {
const done = (release?: FactoryState["release"]) =>
({ stage: "DONE", ...(release ? { release } : {}) }) as unknown as FactoryState
// No release: no prUrl key. With one: the url spreads onto done (#247.1).
expect(await headlessFinished(dir, done())).toEqual({ type: "done" })
expect(await headlessFinished(dir, done({ prUrl: "https://example.test/pr/9" }))).toEqual({
type: "done",
prUrl: "https://example.test/pr/9",
})
// A pending approval waits on a human, with its stages.
await mkdir(path.dirname(factoryLayout(dir).pending), { recursive: true })
await writeFile(factoryLayout(dir).pending, JSON.stringify([{ stage: "spec", requestedBy: "factory", at: "x" }]))
expect(await headlessFinished(dir, { stage: "RESEARCH" } as unknown as FactoryState)).toEqual({
type: "waiting",
reason: "a human must approve (es approve <stage>)",
stages: ["spec"],
})
await writeFile(factoryLayout(dir).pending, "[]")
// The grill (no pending) waits on a human; a mid-build stage keeps driving.
expect(await headlessFinished(dir, { stage: "GRILL" } as unknown as FactoryState)).toMatchObject({
type: "waiting",
reason: expect.stringContaining("grill"),
})
expect(await headlessFinished(dir, { stage: "RESEARCH" } as unknown as FactoryState)).toBeUndefined()
} finally {
await rm(dir, { recursive: true, force: true })
}
})
test("a STOP file halts at headless level without granting anything", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "es-headless-stop-"))
const dirState = await mkdtemp(path.join(tmpdir(), "es-headless-stop-state-"))
try {
await writeFile(path.join(dir, "README.md"), "# x\n")
await new Factory(dir, { gates: gateRunner({ stateDir: dirState }), stateDir: dirState }).provisionRun(
"tester",
5,
)
await mkdir(path.dirname(factoryLayout(dir).stop), { recursive: true })
await writeFile(factoryLayout(dir).stop, "lunch\n")
const driver: HarnessDriver = {
id: "fake",
available: async () => true,
async *turn() {
yield { text: "unused" }
return "ses_fake"
},
}
const halted: HeadlessEvent[] = []
for await (const event of runHeadless({ root: dir, driver, maxTurns: 3, stateDir: dirState })) halted.push(event)
expect(halted[0]).toMatchObject({ type: "start" })
expect(halted.at(-1)).toMatchObject({ type: "halted", reason: expect.stringContaining("STOP file") })
expect(halted.some((event) => event.type === "turn")).toBe(false)
} finally {
await rm(dir, { recursive: true, force: true })
await rm(dirState, { recursive: true, force: true })
}
})
test("an unknown --driver is refused before anything launches", async () => {
const { main } = await import("../src/main.ts")
const printed: string[] = []
const code = await main(["factory", "run", "--headless", "--driver", "nope"], {
print: (text) => printed.push(text),
confirm: { interactive: false, write: () => {}, readLine: async () => "y", readSecret: async () => "" },
cwd: root,
stateDir: state,
})
expect(code).toBe(2)
expect(printed.join("\n")).toContain("Unknown driver")
})
})
describe("opencode driver argv shape (#247.2)", () => {
test("run --standalone --format json --agent, with model/session/prompt passthrough", async () => {
const { chmod } = await import("node:fs/promises")
const bin = await mkdtemp(path.join(tmpdir(), "es-driver-argv-"))
const previous = process.env.PATH
try {
await writeFile(
path.join(bin, "opencode"),
`#!/bin/sh\nprintf '%s\\n' "$@" > "$ES_ARGV_LOG"\necho '{"type":"text","sessionID":"ses_argv","part":{"text":"hi"}}'\n`,
)
await chmod(path.join(bin, "opencode"), 0o755)
process.env.PATH = `${bin}${path.delimiter}${previous}`
const argvOf = async (input: Parameters<typeof opencodeDriver.turn>[0]): Promise<string[]> => {
const log = path.join(bin, `argv-${Math.random().toString(36).slice(2)}.txt`)
process.env.ES_ARGV_LOG = log
try {
const turn = opencodeDriver.turn(input)
let step = await turn.next()
while (!step.done) step = await turn.next()
expect(step.value).toBe("ses_argv")
return (await Bun.file(log).text()).split("\n").filter(Boolean)
} finally {
delete process.env.ES_ARGV_LOG
}
}
expect(await argvOf({ root, agent: "factory", prompt: "go" })).toEqual([
"run",
"--standalone",
"--format",
"json",
"--agent",
"factory",
"go",
])
expect(await argvOf({ root, agent: "factory", prompt: "again", model: "prov/model", session: "ses_1" })).toEqual([
"run",
"--standalone",
"--format",
"json",
"--agent",
"factory",
"--model",
"prov/model",
"--session",
"ses_1",
"again",
])
} finally {
process.env.PATH = previous
await rm(bin, { recursive: true, force: true })
}
})
})
+191
View File
@@ -0,0 +1,191 @@
// MCP caller-identity pinning (#230, #232): the adapter pins one agent id per
// server in the server environment — `ES_MCP_AGENT`, else the legacy
// `ES_AGENT`, else the `defaultAgent` option — and the per-call `agent`
// argument is ignored. Without an adapter identity the caller is "mcp", which
// maps to no seat, so every seat-checked tool refuses. A pinned string that is
// not a registry agent id (e.g. `programmer`) pins to a seat-less identity:
// the caller string is still pinned, it just holds no seat.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { Factory, gateRunner, HUMAN_VERBS, readApproval } from "@heretek-ai/es-core"
import { createMcpServer } from "../src/mcp.ts"
let root: string
let state: string
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-mcp-id-"))
state = await mkdtemp(path.join(tmpdir(), "es-mcp-id-state-"))
const proc = Bun.spawn(["git", "init", "-q", "-b", "main"], { cwd: root, stdout: "pipe", stderr: "pipe" })
await proc.exited
await writeFile(path.join(root, "README.md"), "# x\n")
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
await rm(state, { recursive: true, force: true })
})
const call = (server: ReturnType<typeof createMcpServer>, name: string, args: Record<string, unknown> = {}, id = 1) =>
server.handle({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } })
describe("mcp identity pin order (mcp.ts:29)", () => {
test("the legacy ES_AGENT pins the caller when ES_MCP_AGENT is absent", async () => {
const server = createMcpServer({ root, stateDir: state, env: { ES_AGENT: "factory" } })
const refused = await call(server, "complete", {})
expect(refused?.result.isError).toBe(true)
expect(refused?.result.content[0].text).toContain("called by factory")
})
test("ES_MCP_AGENT beats ES_AGENT beats defaultAgent", async () => {
const top = createMcpServer({
root,
stateDir: state,
env: { ES_MCP_AGENT: "es-qa-functional", ES_AGENT: "factory" },
defaultAgent: "factory",
})
const pinned = await call(top, "complete", { agent: "factory" })
expect(pinned?.result.isError).toBe(true)
expect(pinned?.result.content[0].text).toContain("called by es-qa-functional")
const legacy = createMcpServer({
root,
stateDir: state,
env: { ES_AGENT: "factory" },
defaultAgent: "es-qa-functional",
})
const viaLegacy = await call(legacy, "complete", {})
expect(viaLegacy?.result.isError).toBe(true)
expect(viaLegacy?.result.content[0].text).toContain("called by factory")
const fallback = createMcpServer({ root, stateDir: state, env: {}, defaultAgent: "factory" })
const viaDefault = await call(fallback, "complete", {})
expect(viaDefault?.result.isError).toBe(true)
expect(viaDefault?.result.content[0].text).toContain("called by factory")
})
test("a pinned string without a registry seat stays seat-less but pinned", async () => {
const server = createMcpServer({ root, stateDir: state, env: { ES_MCP_AGENT: "programmer" } })
const refused = await call(server, "complete", {})
expect(refused?.result.isError).toBe(true)
// Registry ids are `es-programmer`/`es-manager`/`factory`: the literal
// `programmer` holds no seat, yet the caller string is still the pin.
expect(refused?.result.content[0].text).toContain("called by programmer")
})
})
describe("as-mcp refusal across every seat-gated tool", () => {
// Every tool whose execute path opens with requireSeat, called with an
// empty adapter environment (caller "mcp", no seat).
const seatGated: Array<{ tool: string; args: Record<string, unknown> }> = [
{ tool: "frontier_write", args: { frontier: {} } },
{ tool: "research_complete", args: {} },
{ tool: "build_start", args: {} },
{ tool: "qa_verdict", args: { verdict: "pass", notes: "n" } },
{ tool: "tiebreak", args: { verdict: "pass", notes: "n" } },
{ tool: "replan", args: { notes: "n" } },
{ tool: "release", args: {} },
{ tool: "complete", args: {} },
]
for (const { tool, args } of seatGated) {
test(`${tool} refuses the unpinned caller as mcp`, async () => {
const server = createMcpServer({ root, stateDir: state, env: {} })
const refused = await call(server, tool, args)
expect(refused?.result.isError).toBe(true)
expect(refused?.result.content[0].text).toContain("called by mcp")
})
}
})
describe("no MCP tool grants a human-only verb (derived from HUMAN_VERBS)", () => {
test("the coarse tool list is exactly the 12 read/request/seat tools", async () => {
const server = createMcpServer({ root, stateDir: state })
const list = await server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" })
const names = list?.result.tools.map((tool: { name: string }) => tool.name).sort()
// A future human-only verb gaining an MCP tool changes this list and fails loudly here.
expect(names).toEqual(
[
"build_start",
"complete",
"frontier_write",
"gates_run",
"qa_verdict",
"release",
"replan",
"request_approval",
"research_complete",
"spec_validate",
"status",
"tiebreak",
].sort(),
)
})
test("no listed tool (except the request-only approval) matches a HUMAN_VERBS rule", async () => {
const server = createMcpServer({ root, stateDir: state })
const list = await server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" })
const names = list?.result.tools.map((tool: { name: string }) => tool.name) as string[]
const humanOnly = (verb: string, sub: string | undefined): boolean => {
const rule = HUMAN_VERBS.find(([name]) => name === verb)
if (!rule) return false
const [, predicate] = rule
return predicate === undefined ? true : predicate(sub)
}
for (const name of names) {
if (name === "request_approval") continue // request-only: proven below, never grants.
const tokens = name.split(/[_-]/)
const matched = HUMAN_VERBS.some(([verb, predicate]) => {
if (!tokens.includes(verb)) return false
if (predicate === undefined) return true
return tokens.some((token) => token !== verb && predicate(token))
})
expect([name, matched, humanOnly("never-a-verb", undefined)]).toEqual([name, false, false])
}
})
test("request_approval records a request without granting anything", async () => {
const factory = new Factory(root, { gates: gateRunner({ stateDir: state }), stateDir: state })
await factory.writeFrontier("grill", {
version: "1.1",
idea: "mcp test",
spendCeiling: { currency: "USD", maxAmount: 3 },
settled: true,
nodes: [{ id: "a", question: "q?", answer: "yes", status: "settled" }],
})
const server = createMcpServer({ root, stateDir: state, env: { ES_MCP_AGENT: "factory" } })
const requested = await call(server, "request_approval", { stage: "frontier" })
expect(requested?.result.isError).toBeUndefined()
expect(requested?.result.content[0].text).toContain("Requested human approval")
// Request-only: the approval file is still absent (a human grants it elsewhere).
expect(await readApproval(root, "frontier")).toBeUndefined()
})
})
describe("mcp identity over real stdio", () => {
test("serveStdio pins ES_MCP_AGENT: a piped agent claim is refused as the pin", async () => {
const proc = Bun.spawn(["bun", path.join(import.meta.dir, "../src/main.ts"), "mcp", "--cwd", root], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
// The literal `programmer` holds no registry seat (#232.1), so the
// seat check itself names the pin — the strongest stdio proof.
env: { ...process.env, ES_MCP_AGENT: "programmer" },
})
proc.stdin.write(
`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "complete", arguments: { agent: "factory" } } })}\n`,
)
proc.stdin.end()
const lines = (await new Response(proc.stdout).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line))
await proc.exited
expect(lines.map((line) => line.id)).toEqual([1])
expect(lines[0].result.isError).toBe(true)
// The per-call `agent: "factory"` is ignored: the refusal names the pin.
expect(lines[0].result.content[0].text).toContain("called by programmer")
expect(await readFile(path.join(root, "README.md"), "utf8")).toContain("# x")
})
})
+23 -11
View File
@@ -285,7 +285,11 @@ interface HttpResponder {
const jsonBody = (res: HttpResponder, code: number, value: unknown): void => {
const text = JSON.stringify(value)
res.writeHead(code, { "content-type": "application/json", "content-length": Buffer.byteLength(text) })
res.writeHead(code, {
...WEB_SECURITY_HEADERS,
"content-type": "application/json",
"content-length": Buffer.byteLength(text),
})
res.end(text)
}
@@ -430,7 +434,7 @@ export class TelemetryServer {
if ((field(headers, "upgrade") ?? "").toLowerCase() === "websocket" && target === "/fleet/ws")
return this.handleUpgrade(socket, headers, res)
if (!this.hostOk(field(headers, "host"))) {
res.writeHead(403, { "content-length": 0, connection: "close" })
res.writeHead(403, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -440,7 +444,7 @@ export class TelemetryServer {
// (bearer or session), submit is browser-only (cookie + Origin + CSRF).
if (method === "POST" && target === "/fleet/approve/preview") return this.approvePreview(headers, rest, res)
if (method === "POST" && target === "/fleet/approve") return this.approveSubmit(headers, rest, res)
res.writeHead(404, { "content-length": 0, connection: "close" })
res.writeHead(404, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -448,7 +452,7 @@ export class TelemetryServer {
// A presented session cookie that cannot authorize is a forbidden
// CSRF/origin failure (403); wholly missing credentials are 401.
const code = parseSessionCookie(field(headers, "cookie")) !== undefined ? 403 : 401
res.writeHead(code, { "content-length": 0, connection: "close" })
res.writeHead(code, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -509,7 +513,7 @@ export class TelemetryServer {
res.end()
}
if (this.options.webRoot === undefined) {
res.writeHead(404, { "content-length": 0, connection: "close" })
res.writeHead(404, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -580,7 +584,7 @@ export class TelemetryServer {
// Same convention as read-only RPC: a presented session cookie that
// cannot authorize is a forbidden CSRF/origin failure (403).
const code = parseSessionCookie(field(headers, "cookie")) !== undefined ? 403 : 401
res.writeHead(code, { "content-length": 0, connection: "close" })
res.writeHead(code, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -637,12 +641,12 @@ export class TelemetryServer {
private async approveSubmit(headers: Map<string, string>, rest: Buffer, res: HttpResponder): Promise<void> {
const session = parseSessionCookie(field(headers, "cookie")) ?? ""
if (!this.sessions.valid(session)) {
res.writeHead(401, { "content-length": 0, connection: "close" })
res.writeHead(401, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
if (!loopbackOriginOk(field(headers, "origin")) || !this.csrf.check(session, field(headers, "x-csrf"))) {
res.writeHead(403, { "content-length": 0, connection: "close" })
res.writeHead(403, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
@@ -730,13 +734,21 @@ export class TelemetryServer {
private ok(res: HttpResponder, value: unknown): void {
const text = JSON.stringify(value)
res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) })
res.writeHead(200, {
...WEB_SECURITY_HEADERS,
"content-type": "application/json",
"content-length": Buffer.byteLength(text),
})
res.end(text)
}
private fail(res: HttpResponder, code: number, message: string, extra: Record<string, unknown> = {}): void {
const text = JSON.stringify({ error: message, ...extra })
res.writeHead(code, { "content-type": "application/json", "content-length": Buffer.byteLength(text) })
res.writeHead(code, {
...WEB_SECURITY_HEADERS,
"content-type": "application/json",
"content-length": Buffer.byteLength(text),
})
res.end(text)
}
@@ -818,7 +830,7 @@ export class TelemetryServer {
(!this.bearerOk(field(headers, "authorization")) && !cookieOk)
) {
const code = !this.hostOk(field(headers, "host")) || !this.originOk(field(headers, "origin")) ? 403 : 401
res.writeHead(code, { "content-length": 0, connection: "close" })
res.writeHead(code, { ...WEB_SECURITY_HEADERS, "content-length": 0, connection: "close" })
res.end()
return
}
+43 -1
View File
@@ -22,7 +22,7 @@ const frontier = {
nodes: [{ id: "lang", question: "Language?", answer: "TypeScript", status: "settled" }],
}
async function rig() {
async function rig(extra: { approveTicketTtlMs?: number } = {}) {
const state = await mkdtemp(path.join(tmpdir(), "es-approve-"))
const repo = await mkdtemp(path.join(tmpdir(), "es-approve-repo-"))
await mkdir(path.join(repo, ".factory"), { recursive: true })
@@ -56,6 +56,7 @@ async function rig() {
stateRoot: state,
port: 0,
token: TOKEN,
...extra,
getSnapshot: (): FleetSnapshot => ({
daemon: { running: true, pid: 1, maxUsd: 10, concurrency: 1 },
tasks: [],
@@ -301,6 +302,47 @@ describe("approve submit", () => {
}
})
test("checklist 4b: an expired preview ticket fails the submit (#283.2)", async () => {
const { state, repo, server } = await rig({ approveTicketTtlMs: 0 })
try {
const cookie = await session(server.port, state)
const origin = `http://127.0.0.1:${server.port}`
const preview = json(
(
await post(
server.port,
"/fleet/approve/preview",
{ runId: "t1", stage: "frontier" },
{ Cookie: cookie, Origin: origin },
)
).body,
)
// A zero TTL expires the ticket before the submit even runs: no sleep needed.
const res = await approveAs(server.port, cookie, preview.csrf as string, {
ticket: preview.ticket,
passphrase: PASS,
})
expect(res.status).toBe(403)
expect(res.body).toContain("expired")
const { readApproval } = await import("@heretek-ai/es-core")
expect(await readApproval(repo, "frontier")).toBeUndefined()
} finally {
await server.stop()
await rm(state, { recursive: true, force: true })
await rm(repo, { recursive: true, force: true })
}
})
test("approve tickets expire by TTL at unit level (#283.2)", async () => {
const { ApproveTickets } = await import("../src/approve.ts")
const expired = new ApproveTickets(0)
expect(expired.redeem(expired.issue({ repoDir: "r", stage: "frontier", subjectHash: "h" }))).toBeUndefined()
const live = new ApproveTickets()
const ticket = live.issue({ repoDir: "r", stage: "frontier", subjectHash: "h" })
expect(live.redeem(ticket)).toEqual({ repoDir: "r", stage: "frontier", subjectHash: "h" })
expect(live.redeem(ticket)).toBeUndefined()
})
test("checklist 5: an artifact changed after the preview refuses the sign", async () => {
const { state, repo, server } = await rig()
try {
+4 -1
View File
@@ -1,6 +1,9 @@
// S5.4 (CodeQL #84) remote property injection (repair-151): request headers
// parse into a Map (never a plain `{}` keyed by remote names), non-token
// names are rejected, and `field()` is actually case-insensitive.
// names are rejected, and `field()` is actually case-insensitive. This file
// probes request-header parser hygiene only — web security headers (CSP) are
// proven in `web.test.ts` ("every web response carries the strict security
// headers", #283.1).
import { describe, expect, test } from "bun:test"
import { field, parseHeaders } from "../src/telemetry.ts"
+75 -4
View File
@@ -36,9 +36,13 @@ async function rig() {
return { state, webRoot, server, token }
}
const raw = (port: number, head: string): Promise<{ status: number; headers: Record<string, string>; body: string }> =>
const raw = (
port: number,
head: string,
body = "",
): Promise<{ status: number; headers: Record<string, string>; body: string }> =>
new Promise((resolve, reject) => {
const socket = createConnection(port, "127.0.0.1", () => socket.write(head))
const socket = createConnection(port, "127.0.0.1", () => socket.write(head + body))
let buffer = Buffer.alloc(0)
socket.on("data", (chunk: Buffer) => {
buffer = Buffer.concat([buffer, chunk])
@@ -67,6 +71,24 @@ const get = (port: number, target: string, headers: Record<string, string> = {})
.join("")}\r\n`,
)
const postRpc = (port: number, payload: unknown, extra: Record<string, string> = {}) => {
const body = JSON.stringify(payload)
return raw(
port,
`POST /fleet/rpc HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n${Object.entries(
extra,
)
.map(([k, v]) => `${k}: ${v}\r\n`)
.join("")}\r\n`,
body,
)
}
const expectSecurityHeaders = (headers: Record<string, string>, where: string): void => {
for (const [name, value] of Object.entries(WEB_SECURITY_HEADERS))
expect(headers[name], `${where}: ${name}`).toBe(value)
}
describe("web tickets", () => {
test("a ticket redeems once, then is dead", async () => {
const state = await mkdtemp(path.join(tmpdir(), "es-fleet-web-"))
@@ -91,7 +113,7 @@ describe("web tickets", () => {
}
})
test("malformed and expired tickets are refused", async () => {
test("malformed and missing tickets are refused", async () => {
const state = await mkdtemp(path.join(tmpdir(), "es-fleet-web-"))
try {
expect(await redeemWebTicket(state, "not-hex")).toBe(false)
@@ -102,6 +124,35 @@ describe("web tickets", () => {
}
})
test("a minted-then-expired ticket is refused and consumed (#283.2)", async () => {
const state = await mkdtemp(path.join(tmpdir(), "es-fleet-web-"))
try {
const ticket = await mintWebTicket(state)
const { fleetDir } = await import("../src/state.ts")
// Age the ticket file past its TTL instead of sleeping for it.
await writeFile(path.join(fleetDir(state), "web-tickets", ticket), `${Date.now() - 1_000}\n`)
expect(await redeemWebTicket(state, ticket)).toBe(false)
expect(await redeemWebTicket(state, ticket)).toBe(false)
} finally {
await rm(state, { recursive: true, force: true })
}
})
test("an expired ticket redeems to 403 without a cookie at HTTP level (#283.2)", async () => {
const { state, server } = await rig()
try {
const ticket = await mintWebTicket(state)
const { fleetDir } = await import("../src/state.ts")
await writeFile(path.join(fleetDir(state), "web-tickets", ticket), `${Date.now() - 1_000}\n`)
const res = await get(server.port, `/?t=${ticket}`)
expect(res.status).toBe(403)
expect(res.headers["set-cookie"]).toBeUndefined()
} finally {
await server.stop()
await rm(state, { recursive: true, force: true })
}
})
test("cookie parsing extracts the session id", () => {
const id = "ab".repeat(32)
expect(parseSessionCookie(`a=1; ${WEB_SESSION_COOKIE}=${id}; b=2`)).toBe(id)
@@ -164,7 +215,7 @@ describe("web serving", () => {
})
test("every web response carries the strict security headers", async () => {
const { state, server } = await rig()
const { state, server, token } = await rig()
try {
const ticket = await mintWebTicket(state)
const login = await get(server.port, `/?t=${ticket}`)
@@ -174,6 +225,26 @@ describe("web serving", () => {
expect(login.headers["x-content-type-options"]).toBe("nosniff")
expect(login.headers["referrer-policy"]).toBe("no-referrer")
expect(WEB_SECURITY_HEADERS["content-security-policy"]).toContain("connect-src 'self'")
// JSON responders carry them too (#282): success, error envelopes, and
// the bare 401/403/404 paths.
const authed = { Authorization: `Bearer ${token}` }
const ok = await postRpc(server.port, { method: "fleet.status", params: {} }, authed)
expect(ok.status).toBe(200)
expect(ok.headers["content-type"]).toContain("application/json")
expectSecurityHeaders(ok.headers, "fleet.status 200")
const unknown = await postRpc(server.port, { method: "nope", params: {} }, authed)
expect(unknown.status).toBe(200)
expect(unknown.body).toContain("-32601")
expectSecurityHeaders(unknown.headers, "unknown method")
const anon = await postRpc(server.port, { method: "fleet.status", params: {} })
expect(anon.status).toBe(401)
expectSecurityHeaders(anon.headers, "anonymous rpc 401")
const missing = await raw(
server.port,
`GET /fleet/rpc HTTP/1.1\r\nHost: 127.0.0.1:${server.port}\r\nConnection: close\r\n\r\n`,
)
expect(missing.status).toBe(404)
expectSecurityHeaders(missing.headers, "wrong path 404")
} finally {
await server.stop()
await rm(state, { recursive: true, force: true })
+4 -1
View File
@@ -174,7 +174,10 @@ export function createPolicyHooks(runtime: Runtime, seats?: SeatTracker) {
? patchTargets(input.patchText)
: []
if (!files.length) return
if (runtime.options.lspAfterEdit !== false) {
// The post-edit diagnostics are a language-server channel (#277.2): seats
// scoped to `lsp: "none"` never get them (their tool rules already hide
// `lsp_*`). Navigating seats and the user's own agents keep the channel.
if (runtime.options.lspAfterEdit !== false && agentSpec(event.agent)?.lsp !== "none") {
const lines: string[] = []
for (const file of files) {
const absolute = path.resolve(runtime.root, file)
+8 -4
View File
@@ -1,8 +1,9 @@
// Per-location runtime shared by the plugin's hooks, tools, commands and RPC.
// Options are layered: the canonical config (global file → project file) with
// plugin options as the most specific override. Plugin options are config
// keys validated by the same strict schema; unknown keys are ignored with a
// warning the TUI shows once.
// The server only wires option layering (parseOptions → createRuntime); the
// merge itself lives in core (loadEsConfig/mergeConfig): the canonical config
// (global file → project file) with plugin options as the most specific
// override. Plugin options are config keys validated by the same strict
// schema; unknown keys are ignored with a warning the TUI shows once.
import {
stateDir as defaultStateDir,
type EsConfig,
@@ -68,6 +69,9 @@ export async function createRuntime(root: string, parsed: ParsedOptions): Promis
...(config.searchProvider ? { searchProvider: config.searchProvider } : {}),
}
const stateDir = options.stateDir ?? defaultStateDir()
// One manager per project (#275.1): servers start lazily per (server, root)
// and are reaped by the idle sweeper between sessions, stopping only on
// plugin unload (see server.ts) — long-lived servers outlive sessions.
const lsp = new LspManager({ root, stateDir })
const factory = new Factory(root, {
gates: gateRunner({ stateDir }),
+356 -297
View File
@@ -1,6 +1,9 @@
// Epistemic Swarm for OpenCode v2 (server plugin). Additive: registers our
// agents, tools, skills, commands, hooks and RPC through runtime transforms;
// writes no config files and never touches built-in agents or defaults.
// setup writes no project-tree files and no config files (the engine key goes
// to the masked state dir; spend and seat tracking later write
// .factory/runtime/* while sessions run). It never touches built-in agents or
// defaults.
import { readdir, readFile } from "node:fs/promises"
import path from "node:path"
import {
@@ -27,7 +30,7 @@ import { createPolicyHooks } from "./policy.ts"
import { createWebCache, PendingSearches, registerWebsearch } from "./research.ts"
import { createRpcHandlers } from "./rpc.ts"
import { EsRpc } from "./rpc-def.ts"
import { createRuntime, parseOptions } from "./runtime.ts"
import { createRuntime, parseOptions, type Runtime } from "./runtime.ts"
import { createSeatTracker } from "./seats.ts"
import { createSessionHooks, createSpendTracker } from "./session.ts"
import { registerTools } from "./tools.ts"
@@ -47,6 +50,346 @@ export function parseDeepResearchArgs(text: string): DeepResearchArgs {
return { query, ceiling }
}
/** The session surface slash commands use (the host's `ctx.session`; stubbed in tests). */
export interface CommandSession {
switchAgent(sessionID: string, agent: string): Promise<unknown>
sendPrompt(sessionID: string, prompt: Record<string, unknown>, text: string, delivery: unknown): Promise<unknown>
synthetic(sessionID: string, text: string): Promise<unknown>
}
/** One slash command: its name, description and host-independent body. */
export interface CommandSpec {
readonly name: string
readonly description: string
readonly execute: (args: {
readonly sessionID: string
readonly prompt: { readonly text?: string } & Record<string, unknown>
readonly delivery?: unknown
}) => Promise<void>
}
/** The hook-bridge surface slash commands read (status for `/hooks`). */
export interface CommandBridge {
readonly engine: {
refresh(): Promise<void>
status(): {
readonly handlers: number
readonly projectHandlers: number
readonly trusted: boolean
readonly projectLines: readonly string[]
readonly diagnostics: readonly string[]
}
readonly recent: readonly string[]
}
}
/**
* Every slash command the plugin registers, as data: the host loop below
* adds each one, and tests invoke each body against a stub session to prove
* routing (seat/verb per command) and name uniqueness (one `research`).
*/
export function esCommandSpecs(
io: CommandSession,
runtime: Runtime,
bridge: CommandBridge,
runners: { readonly runGates?: typeof runGates; readonly git?: typeof git } = {},
): CommandSpec[] {
const specs: CommandSpec[] = []
const gates = runners.runGates ?? runGates
const gitStatus = runners.git ?? git
specs.push({
name: "grill",
description: "Grill an idea until the design and spend ceiling are settled (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await io.switchAgent(sessionID, "grill")
await io.sendPrompt(
sessionID,
prompt,
prompt.text?.trim() ? `Grill me on: ${prompt.text}` : "Start grilling me on my idea.",
delivery,
)
},
})
specs.push({
name: "brainstorm",
description: "Fan out divergent lenses on an idea and return a diversified shortlist (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await io.switchAgent(sessionID, "brainstormer")
await io.sendPrompt(
sessionID,
prompt,
prompt.text?.trim()
? `Brainstorm: ${prompt.text}`
: "Ask me for the idea to brainstorm (one sentence), then run the lens fan-out.",
delivery,
)
},
})
specs.push({
name: "harvest",
description: "Tear down competitor projects: licences, feature matrix, clean-room specs (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await io.switchAgent(sessionID, "harvester")
await io.sendPrompt(
sessionID,
prompt,
prompt.text?.trim()
? `Darkharvest: ${prompt.text}`
: "Ask me for the teardown objective and the candidate projects, then run the harvest.",
delivery,
)
},
})
specs.push({
name: "design",
description: "Interview me into a design system: tokens, contrast gates, style guide (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await io.switchAgent(sessionID, "designer")
await io.sendPrompt(
sessionID,
prompt,
prompt.text?.trim()
? `Run the design interview. Product: ${prompt.text}`
: "Ask me for the product name and start the design interview, one question at a time.",
delivery,
)
},
})
specs.push({
name: "scout",
description: "Find and vet open-source candidates for a feature: licenses, maintenance, CVEs (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await io.switchAgent(sessionID, "scout")
await io.sendPrompt(
sessionID,
prompt,
prompt.text?.trim()
? `Scout: ${prompt.text}`
: "Ask me for the feature to scout and our license posture, then plan the candidates.",
delivery,
)
},
})
specs.push({
name: "research",
// One name, one body (#274): a leading `deep` runs the adversarial deep
// flow (switching to the deep-researcher); anything else reads the
// research stage (report, audit coverage, cached sources).
description:
"Deep research to a grounded report, or the research stage: report, audit coverage and cached sources (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const say = async (text: string) => {
await io.synthetic(sessionID, text)
}
const text = prompt.text?.trim() ?? ""
if (text.split(/\s+/)[0]?.toLowerCase() !== "deep") {
const layout = factoryLayout(runtime.root)
const report = await readFile(layout.researchReport, "utf8").catch(() => undefined)
const coverage = await readJson<{
claims?: number
grounded?: number
untagged?: number
ungrounded?: number
malformed?: number
sources?: number
passed?: boolean
}>(path.join(layout.research, "coverage.json")).catch(() => undefined)
const sources = (await readdir(researchSourcesDir(runtime.root)).catch(() => [])).filter((file) =>
file.endsWith(".md"),
)
const lines = [
report === undefined
? "No research report yet (.factory/research/REPORT.md)."
: `Report: ${report.length} characters in .factory/research/REPORT.md.`,
coverage
? `Audit: ${coverage.grounded ?? 0}/${coverage.claims ?? 0} claims grounded (untagged ${coverage.untagged ?? 0}, ungrounded ${coverage.ungrounded ?? 0}, malformed ${coverage.malformed ?? 0}); cited sources ${coverage.sources ?? 0}; passed ${coverage.passed ? "yes" : "no"}.`
: "No coverage.json yet (es_research_complete writes it).",
`Cached sources: ${sources.length}.`,
]
return say(lines.join("\n"))
}
const parsed = parseDeepResearchArgs(text)
if ("error" in parsed && parsed.error === "usage") {
return say("Usage: /research deep <question> --max-usd N")
}
if ("error" in parsed) {
return say(
"Deep research needs a spend ceiling you set: /research deep <question> --max-usd N. Seats halt when the spend reaches it.",
)
}
const { query, ceiling } = parsed
const existing = await runtime.factory.read().catch(() => undefined)
if (existing && existing.mode !== "research") {
return say(
"This project holds a build run. Start deep research in a directory without one (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
)
}
if (existing?.stage === "HALTED") {
return say(
"This research run is halted; resume it first (`es factory resume` at a terminal, or /es-resume to preview).",
)
}
if (existing?.stage === "DONE") {
return say(
"This research run is done. For a new question, start one in a fresh directory (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
)
}
if (!existing) {
try {
// The human typed /research deep: beginning the run is their action.
await runtime.factory.beginResearchRun({ objective: query, ceilingUSD: Number(ceiling) }, "human:tui")
} catch (error) {
return say(`Could not start the research run: ${error instanceof Error ? error.message : String(error)}`)
}
}
await io.switchAgent(sessionID, "deep-researcher")
await io.sendPrompt(
sessionID,
prompt,
`${await runtime.factory.summary()}\nDeep research runs adversarially, with no human answering questions mid-flow; follow your Flow through es_research_complete.`,
delivery,
)
},
})
specs.push({
name: "audit",
description: "Open a code audit of the active phase or a path and run the auditor pair (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const target = prompt.text?.trim()
const say = async (text: string) => {
await io.synthetic(sessionID, text)
}
if (!target) return say("Usage: /audit <active phase id | path in the project>")
const state = await runtime.factory.read().catch(() => undefined)
if (state?.spendCeilingUSD === undefined)
return say(
"Audits run inside a factory run with a spend ceiling. Start one at a terminal with `es audit <target> --max-usd N`, or run /grill first.",
)
let opened: Awaited<ReturnType<typeof runtime.factory.openAudit>>
try {
// The human typed /audit: opening it is their action.
opened = await runtime.factory.openAudit(
"human:tui",
parseAuditTarget(
target,
state.phases.map((phase) => phase.id),
),
)
} catch (error) {
return say(`Could not open the audit: ${error instanceof Error ? error.message : String(error)}`)
}
const { audit } = opened
await io.switchAgent(sessionID, "factory")
await io.sendPrompt(
sessionID,
prompt,
`${await runtime.factory.summary()}\nThe human opened code audit ${audit.id} (round ${audit.round}) on ${describeTarget(audit.target)}. Launch es-auditor-thesis and es-auditor-antithesis in parallel with the audit id and target; if they split, have es-manager break the tie (es_tiebreak with audit). Report the verdicts and the report path when the audit settles.`,
delivery,
)
},
})
specs.push({
name: "factory",
description: "Drive the build factory from its current stage (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const state = await runtime.factory.read()
if (!state) {
await io.synthetic(sessionID, "No factory run here yet — run /grill first.")
return
}
await io.switchAgent(sessionID, "factory")
await io.sendPrompt(
sessionID,
prompt,
`${await runtime.factory.summary(state)}\nContinue the factory run from its current stage.${prompt.text?.trim() ? `\nHuman note: ${prompt.text}` : ""}`,
delivery,
)
},
})
specs.push({
name: "gates",
description: "Run the mechanical gates on changed files (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const changed = (await gitStatus(runtime.root, ["status", "--porcelain=v1", "-uall"], { allowFail: true })).stdout
.split("\n")
.filter(Boolean)
.map((line) => line.slice(3).split(" -> ").at(-1)!)
const report = await gates({
root: runtime.root,
scope: changed.length ? "touched" : "full",
touched: changed,
stateDir: runtime.stateDir,
})
await io.synthetic(sessionID, formatReport(report))
},
})
specs.push({
name: "lsp",
description: "Language servers: configured, available and running (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const status = await runtime.lsp.status()
const settings = await runtime.lsp.settings()
const lines = [status.enabled ? "LSP is on." : "LSP is disabled (lsp: false)."]
for (const server of settings.servers) {
const command = await runtime.lsp.command(server, runtime.root)
const running = status.running.filter((item) => item.id === server.id)
lines.push(
`${server.id} [${server.extensions.join(" ")}]: ${Array.isArray(command) ? `available (${command.join(" ")})` : command.unavailable}${running.length ? ` · running for ${running.length} root(s)` : ""}`,
)
}
lines.push(...status.diagnostics.map((item) => `! ${item}`))
await io.synthetic(sessionID, lines.join("\n"))
},
})
specs.push({
name: "hooks",
description: "Show the hook bridge: sources, handlers, trust and diagnostics (Epistemic Swarm)",
execute: async ({ sessionID }) => {
await bridge.engine.refresh()
const status = bridge.engine.status()
const trust =
status.projectHandlers === 0
? "nothing to trust"
: status.trusted
? "trusted"
: "NOT trusted: run /es-trust or `es trust`"
const lines = [
`Hook bridge: ${status.handlers} handler(s), ${status.projectHandlers} from the project (${trust}).`,
...status.projectLines,
...status.diagnostics.map((item) => `! ${item}`),
...bridge.engine.recent.slice(-10).map((item) => `recent error: ${item}`),
"Stop hooks are emulated after the turn ends (advisory for plain chat).",
]
await io.synthetic(sessionID, lines.join("\n"))
},
})
specs.push({
name: "status",
description: "Show the factory state (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const state = await runtime.factory.read()
const liveness = await readLiveness(runtime.root, state)
const text = [await runtime.factory.summary(state), ...livenessLines(state, liveness)].join("\n")
await io.synthetic(sessionID, text)
},
})
specs.push({
name: "config",
description: "Show the effective Epistemic Swarm config and where it came from (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const lines = [
runtime.configSources.length
? `Layers: ${runtime.configSources.join(" → ")} → plugin options`
: "Layers: defaults only (no config files)",
...runtime.warnings.map((warning) => `Warning: ${warning}`),
JSON.stringify(runtime.config, null, 2),
]
await io.synthetic(sessionID, lines.join("\n"))
},
})
return specs
}
export default Plugin.define({
id: PLUGIN_ID,
async setup(ctx) {
@@ -183,301 +526,13 @@ export default Plugin.define({
}) as any)
await ctx.command.transform((editor) => {
editor.add({
name: "grill",
description: "Grill an idea until the design and spend ceiling are settled (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await ctx.session.switchAgent({ sessionID, agent: "grill" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: prompt.text?.trim() ? `Grill me on: ${prompt.text}` : "Start grilling me on my idea.",
delivery,
} as any)
},
})
editor.add({
name: "brainstorm",
description: "Fan out divergent lenses on an idea and return a diversified shortlist (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await ctx.session.switchAgent({ sessionID, agent: "brainstormer" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: prompt.text?.trim()
? `Brainstorm: ${prompt.text}`
: "Ask me for the idea to brainstorm (one sentence), then run the lens fan-out.",
delivery,
} as any)
},
})
editor.add({
name: "harvest",
description: "Tear down competitor projects: licences, feature matrix, clean-room specs (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await ctx.session.switchAgent({ sessionID, agent: "harvester" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: prompt.text?.trim()
? `Darkharvest: ${prompt.text}`
: "Ask me for the teardown objective and the candidate projects, then run the harvest.",
delivery,
} as any)
},
})
editor.add({
name: "design",
description: "Interview me into a design system: tokens, contrast gates, style guide (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await ctx.session.switchAgent({ sessionID, agent: "designer" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: prompt.text?.trim()
? `Run the design interview. Product: ${prompt.text}`
: "Ask me for the product name and start the design interview, one question at a time.",
delivery,
} as any)
},
})
editor.add({
name: "scout",
description: "Find and vet open-source candidates for a feature: licenses, maintenance, CVEs (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
await ctx.session.switchAgent({ sessionID, agent: "scout" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: prompt.text?.trim()
? `Scout: ${prompt.text}`
: "Ask me for the feature to scout and our license posture, then plan the candidates.",
delivery,
} as any)
},
})
editor.add({
name: "research",
description: "Adversarial deep research to a grounded report: thesis, antithesis, synthesis (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const say = async (text: string) => {
await ctx.session.synthetic({ sessionID, text } as any)
}
const text = prompt.text?.trim() ?? ""
const parsed = parseDeepResearchArgs(text)
if ("error" in parsed && parsed.error === "usage") {
return say("Usage: /research deep <question> --max-usd N")
}
if ("error" in parsed) {
return say(
"Deep research needs a spend ceiling you set: /research deep <question> --max-usd N. Seats halt when the spend reaches it.",
)
}
const { query, ceiling } = parsed
const existing = await runtime.factory.read().catch(() => undefined)
if (existing && existing.mode !== "research") {
return say(
"This project holds a build run. Start deep research in a directory without one (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
)
}
if (existing?.stage === "HALTED") {
return say(
"This research run is halted; resume it first (`es factory resume` at a terminal, or /es-resume to preview).",
)
}
if (existing?.stage === "DONE") {
return say(
"This research run is done. For a new question, start one in a fresh directory (`es research deep <question> --output <dir> --max-usd N` at a terminal).",
)
}
if (!existing) {
try {
// The human typed /research deep: beginning the run is their action.
await runtime.factory.beginResearchRun({ objective: query, ceilingUSD: Number(ceiling) }, "human:tui")
} catch (error) {
return say(`Could not start the research run: ${error instanceof Error ? error.message : String(error)}`)
}
}
await ctx.session.switchAgent({ sessionID, agent: "deep-researcher" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
// The coordinator flow is the deep-researcher seat's system prompt
// (single-sourced in core); this only adds the run state.
text: `${await runtime.factory.summary()}\nDeep research runs adversarially, with no human answering questions mid-flow; follow your Flow through es_research_complete.`,
delivery,
} as any)
},
})
editor.add({
name: "audit",
description: "Open a code audit of the active phase or a path and run the auditor pair (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const target = prompt.text?.trim()
const say = async (text: string) => {
await ctx.session.synthetic({ sessionID, text } as any)
}
if (!target) return say("Usage: /audit <active phase id | path in the project>")
const state = await runtime.factory.read().catch(() => undefined)
if (state?.spendCeilingUSD === undefined)
return say(
"Audits run inside a factory run with a spend ceiling. Start one at a terminal with `es audit <target> --max-usd N`, or run /grill first.",
)
let opened: Awaited<ReturnType<typeof runtime.factory.openAudit>>
try {
// The human typed /audit: opening it is their action.
opened = await runtime.factory.openAudit(
"human:tui",
parseAuditTarget(
target,
state.phases.map((phase) => phase.id),
),
)
} catch (error) {
return say(`Could not open the audit: ${error instanceof Error ? error.message : String(error)}`)
}
const { audit } = opened
await ctx.session.switchAgent({ sessionID, agent: "factory" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: `${await runtime.factory.summary()}\nThe human opened code audit ${audit.id} (round ${audit.round}) on ${describeTarget(audit.target)}. Launch es-auditor-thesis and es-auditor-antithesis in parallel with the audit id and target; if they split, have es-manager break the tie (es_tiebreak with audit). Report the verdicts and the report path when the audit settles.`,
delivery,
} as any)
},
})
editor.add({
name: "factory",
description: "Drive the build factory from its current stage (Epistemic Swarm)",
execute: async ({ sessionID, prompt, delivery }) => {
const state = await runtime.factory.read()
if (!state) {
await ctx.session.synthetic({
sessionID,
text: "No factory run here yet — run /grill first.",
} as any)
return
}
await ctx.session.switchAgent({ sessionID, agent: "factory" } as any)
await ctx.session.prompt({
...prompt,
sessionID,
text: `${await runtime.factory.summary(state)}\nContinue the factory run from its current stage.${prompt.text?.trim() ? `\nHuman note: ${prompt.text}` : ""}`,
delivery,
} as any)
},
})
editor.add({
name: "gates",
description: "Run the mechanical gates on changed files (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const changed = (await git(runtime.root, ["status", "--porcelain=v1", "-uall"], { allowFail: true })).stdout
.split("\n")
.filter(Boolean)
.map((line) => line.slice(3).split(" -> ").at(-1)!)
const report = await runGates({
root: runtime.root,
scope: changed.length ? "touched" : "full",
touched: changed,
stateDir: runtime.stateDir,
})
await ctx.session.synthetic({ sessionID, text: formatReport(report) } as any)
},
})
editor.add({
name: "lsp",
description: "Language servers: configured, available and running (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const status = await runtime.lsp.status()
const settings = await runtime.lsp.settings()
const lines = [status.enabled ? "LSP is on." : "LSP is disabled (lsp: false)."]
for (const server of settings.servers) {
const command = await runtime.lsp.command(server, runtime.root)
const running = status.running.filter((item) => item.id === server.id)
lines.push(
`${server.id} [${server.extensions.join(" ")}]: ${Array.isArray(command) ? `available (${command.join(" ")})` : command.unavailable}${running.length ? ` · running for ${running.length} root(s)` : ""}`,
)
}
lines.push(...status.diagnostics.map((item) => `! ${item}`))
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
},
})
editor.add({
name: "hooks",
description: "Show the hook bridge: sources, handlers, trust and diagnostics (Epistemic Swarm)",
execute: async ({ sessionID }) => {
await bridge.engine.refresh()
const status = bridge.engine.status()
const trust =
status.projectHandlers === 0
? "nothing to trust"
: status.trusted
? "trusted"
: "NOT trusted: run /es-trust or `es trust`"
const lines = [
`Hook bridge: ${status.handlers} handler(s), ${status.projectHandlers} from the project (${trust}).`,
...status.projectLines,
...status.diagnostics.map((item) => `! ${item}`),
...bridge.engine.recent.slice(-10).map((item) => `recent error: ${item}`),
"Stop hooks are emulated after the turn ends (advisory for plain chat).",
]
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
},
})
editor.add({
name: "status",
description: "Show the factory state (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const state = await runtime.factory.read()
const liveness = await readLiveness(runtime.root, state)
const text = [await runtime.factory.summary(state), ...livenessLines(state, liveness)].join("\n")
await ctx.session.synthetic({ sessionID, text } as any)
},
})
editor.add({
name: "config",
description: "Show the effective Epistemic Swarm config and where it came from (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const lines = [
runtime.configSources.length
? `Layers: ${runtime.configSources.join(" → ")} → plugin options`
: "Layers: defaults only (no config files)",
...runtime.warnings.map((warning) => `Warning: ${warning}`),
JSON.stringify(runtime.config, null, 2),
]
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
},
})
editor.add({
name: "research",
description: "Research stage: report, audit coverage and cached sources (Epistemic Swarm)",
execute: async ({ sessionID }) => {
const layout = factoryLayout(runtime.root)
const report = await readFile(layout.researchReport, "utf8").catch(() => undefined)
const coverage = await readJson<{
claims?: number
grounded?: number
untagged?: number
ungrounded?: number
malformed?: number
sources?: number
passed?: boolean
}>(path.join(layout.research, "coverage.json")).catch(() => undefined)
const sources = (await readdir(researchSourcesDir(runtime.root)).catch(() => [])).filter((file) =>
file.endsWith(".md"),
)
const lines = [
report === undefined
? "No research report yet (.factory/research/REPORT.md)."
: `Report: ${report.length} characters in .factory/research/REPORT.md.`,
coverage
? `Audit: ${coverage.grounded ?? 0}/${coverage.claims ?? 0} claims grounded (untagged ${coverage.untagged ?? 0}, ungrounded ${coverage.ungrounded ?? 0}, malformed ${coverage.malformed ?? 0}); cited sources ${coverage.sources ?? 0}; passed ${coverage.passed ? "yes" : "no"}.`
: "No coverage.json yet (es_research_complete writes it).",
`Cached sources: ${sources.length}.`,
]
await ctx.session.synthetic({ sessionID, text: lines.join("\n") } as any)
},
})
const io: CommandSession = {
switchAgent: (sessionID, agent) => ctx.session.switchAgent({ sessionID, agent } as any),
sendPrompt: (sessionID, prompt, text, delivery) =>
ctx.session.prompt({ ...prompt, sessionID, text, delivery } as any),
synthetic: (sessionID, text) => ctx.session.synthetic({ sessionID, text } as any),
}
for (const spec of esCommandSpecs(io, runtime, bridge as unknown as CommandBridge)) editor.add(spec as any)
})
// Spend: per-step host cost for factory-agent sessions (estimated from tokens when cost is 0).
@@ -495,6 +550,10 @@ export default Plugin.define({
// stream closed on unload
}
})()
// Language-server lifecycle (#275.1): servers stop here on plugin unload;
// between sessions only the manager's idle sweeper (600 s default) reaps
// them, so a long-lived server outlives its session by design. The running
// set stays visible through `/lsp` and the LSP panel.
return async () => {
abort.abort()
if (queued) clearTimeout(queued)
+46
View File
@@ -0,0 +1,46 @@
// Compaction (#276.2): only factory seats get the factory-state block
// injected; any other agent compacts without it.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { createRuntime, parseOptions, type Runtime } from "../src/runtime.ts"
import { createSessionHooks } from "../src/session.ts"
let root: string
let state: string
let runtime: Runtime
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-compaction-"))
state = await mkdtemp(path.join(tmpdir(), "es-compaction-state-"))
runtime = await createRuntime(root, parseOptions({ stateDir: state }))
})
afterEach(async () => {
await runtime.lsp.stopAll().catch(() => undefined)
await rm(root, { recursive: true, force: true })
await rm(state, { recursive: true, force: true })
})
describe("compaction is factory-only", () => {
test("a factory seat compacts with the verbatim state block", async () => {
const hooks = createSessionHooks(runtime)
const event = { agent: "factory", system: [] as unknown[] }
await hooks.compaction(event as never)
expect(event.system).toHaveLength(1)
const text = (event.system[0] as { text: string }).text
expect(text).toContain("Preserve this factory state verbatim")
expect(text).toContain("<factory-state>")
})
test("a non-registry agent compacts without the state block", async () => {
const hooks = createSessionHooks(runtime)
for (const agent of ["build", "es-programmer", "grill"]) {
const event = { agent, system: [] as unknown[] }
await hooks.compaction(event as never)
if (agent === "build") expect(event.system).toEqual([])
else expect(event.system).toHaveLength(1)
}
})
})
@@ -0,0 +1,20 @@
// Legacy option wording (#240.1): 0.7-era options are ignored, not loaded —
// plugin-options only, while unknown keys in config files are
// strict-rejected; the unknowns collapse into one warning the TUI shows once.
import { describe, expect, test } from "bun:test"
import { parseOptions, unknownOptionsWarning } from "../src/runtime.ts"
describe("legacy option wording", () => {
test("unknown plugin options are ignored (never loaded), naming the 0.7 removal", () => {
const warning = unknownOptionsWarning(["search_engine", "max_iterations", "mode"])
expect(warning).toContain("Ignored unknown plugin option")
expect(warning).toContain("were removed in 1.0")
expect(warning).not.toMatch(/load/i)
})
test("unknowns collapse into a single warning string", () => {
const parsed = parseOptions({ search_engine: "x", max_iterations: 2, pr: "off" })
expect(parsed.unknown).toEqual(["max_iterations", "search_engine"])
expect(parsed.overrides).toEqual({ pr: "off" })
})
})
@@ -0,0 +1,62 @@
// After-edit diagnostics scoping (#277.2): the `<diagnostics>` injection is a
// language-server channel, so seats with `lsp: "none"` (harvester, designer,
// scout, brainstormer, …) must not receive it; seats that may navigate (and
// the user's own agents) keep it.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { createPolicyHooks } from "../src/policy.ts"
import type { Runtime } from "../src/runtime.ts"
let root: string
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-lsp-scope-"))
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
const diagnostic = {
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
severity: 1,
message: "boom",
}
const runtime = (root: string) =>
({
root,
options: { lspAfterEdit: true, afterEdit: "off" },
lsp: { diagnostics: async () => [diagnostic] },
policy: async () => ({ root }),
}) as unknown as Runtime
const edited = (agent: string) => ({
tool: "write",
agent,
id: "1",
input: { path: "src/a.ts" },
status: "completed",
result: {},
})
describe("after-edit diagnostics respect lsp scoping", () => {
test("an lsp:none seat (harvester) gets no diagnostics channel", async () => {
const { after } = createPolicyHooks(runtime(root))
const event = edited("harvester")
await after(event as never)
expect(event.result).toEqual({})
})
test("a navigating seat (QA) and the user's own agent keep the injection", async () => {
const { after } = createPolicyHooks(runtime(root))
const qa = edited("es-qa-functional")
await after(qa as never)
expect(JSON.stringify((qa.result as { content: unknown }).content)).toContain("<diagnostics>")
const user = edited("build")
await after(user as never)
expect(JSON.stringify((user.result as { content: unknown }).content)).toContain("<diagnostics>")
})
})
+184
View File
@@ -0,0 +1,184 @@
// Slash commands (#274 duplicate `/research`, #276 routing gaps, #277 `/lsp`
// coverage): one name per command, and per-command routing (seat/verb) proven
// by invoking each body against a stub session — no host needed.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { formatReport } from "@heretek-ai/es-core"
import { createRuntime, parseOptions, type Runtime } from "../src/runtime.ts"
import { type CommandSession, esCommandSpecs } from "../src/server.ts"
let root: string
let state: string
let runtime: Runtime
const stubSession = () => {
const switched: Array<{ sessionID: string; agent: string }> = []
const prompted: Array<{ sessionID: string; prompt: Record<string, unknown>; text: string; delivery: unknown }> = []
const said: Array<{ sessionID: string; text: string }> = []
const io: CommandSession = {
switchAgent: async (sessionID, agent) => {
switched.push({ sessionID, agent })
},
sendPrompt: async (sessionID, prompt, text, delivery) => {
prompted.push({ sessionID, prompt, text, delivery })
},
synthetic: async (sessionID, text) => {
said.push({ sessionID, text })
},
}
return { io, switched, prompted, said }
}
const stubBridge = () => ({
engine: {
refresh: async () => {},
status: () => ({
handlers: 2,
projectHandlers: 1,
trusted: false,
projectLines: ["project hook line"],
diagnostics: ["bridge diagnostic"],
}),
recent: ["recent failure"],
},
})
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-slash-"))
state = await mkdtemp(path.join(tmpdir(), "es-slash-state-"))
runtime = await createRuntime(root, parseOptions({ stateDir: state }))
})
afterEach(async () => {
await runtime.lsp.stopAll().catch(() => undefined)
await rm(root, { recursive: true, force: true })
await rm(state, { recursive: true, force: true })
})
const specs = () => esCommandSpecs(stubSession().io, runtime, stubBridge())
const byName = () => new Map(specs().map((spec) => [spec.name, spec]))
describe("slash command registration (#274)", () => {
test("13 unique names, with a single research", () => {
const names = specs().map((spec) => spec.name)
expect(names.filter((name) => name === "research")).toHaveLength(1)
expect([...new Set(names)].sort()).toEqual(
[
"audit",
"brainstorm",
"config",
"design",
"factory",
"gates",
"grill",
"harvest",
"hooks",
"lsp",
"research",
"scout",
"status",
].sort(),
)
})
})
describe("per-command routing: seat and verb (#276.2)", () => {
test("grill / brainstorm / harvest / design / scout switch to their seat with a verb", async () => {
const routes: Array<[string, string, string]> = [
["grill", "grill", "Grill me on: q"],
["brainstorm", "brainstormer", "Brainstorm: q"],
["harvest", "harvester", "Darkharvest: q"],
["design", "designer", "Run the design interview. Product: q"],
["scout", "scout", "Scout: q"],
]
for (const [name, seat, verb] of routes) {
const stub = stubSession()
const spec = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item])).get(name)!
await spec.execute({ sessionID: "ses_1", prompt: { text: "q" }, delivery: undefined })
expect([name, stub.switched]).toEqual([name, [{ sessionID: "ses_1", agent: seat }]])
expect([name, stub.prompted[0]?.text]).toEqual([name, verb])
}
})
test("research deep starts a research run and routes to the deep-researcher", async () => {
const stub = stubSession()
const spec = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item])).get(
"research",
)!
await spec.execute({ sessionID: "ses_1", prompt: { text: "deep what queue --max-usd 5" }, delivery: undefined })
const begun = await runtime.factory.read()
expect(begun).toMatchObject({ mode: "research", stage: "RESEARCH" })
expect(stub.switched).toEqual([{ sessionID: "ses_1", agent: "deep-researcher" }])
expect(stub.prompted[0]?.text).toContain("es_research_complete")
})
test("research without deep shows the report reader, not usage", async () => {
const stub = stubSession()
const spec = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item])).get(
"research",
)!
await spec.execute({ sessionID: "ses_1", prompt: { text: "" }, delivery: undefined })
expect(stub.switched).toEqual([])
expect(stub.said[0]?.text).toContain("No research report yet")
// A bare `deep` still gets the deep flow's usage, not the report.
const usage = stubSession()
const usageSpec = new Map(esCommandSpecs(usage.io, runtime, stubBridge()).map((item) => [item.name, item])).get(
"research",
)!
await usageSpec.execute({ sessionID: "ses_1", prompt: { text: "deep" }, delivery: undefined })
expect(usage.said[0]?.text).toContain("Usage: /research deep")
})
test("audit usage and ceiling refusals stay synthetic", async () => {
const stub = stubSession()
const spec = byName().get("audit")!
// Rebind the spec to this stub's session: rebuild with the same stub.
const bound = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item])).get("audit")!
expect(spec.name).toBe(bound.name)
await bound.execute({ sessionID: "ses_1", prompt: { text: "" }, delivery: undefined })
expect(stub.said[0]?.text).toContain("Usage: /audit")
await bound.execute({ sessionID: "ses_1", prompt: { text: "src/x.ts" }, delivery: undefined })
expect(stub.said[1]?.text).toContain("spend ceiling")
expect(stub.switched).toEqual([])
})
test("factory with no run is synthetic; gates renders the gate report", async () => {
const stub = stubSession()
const commands = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item]))
await commands.get("factory")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
expect(stub.said[0]?.text).toContain("No factory run here yet")
const canned = { summary: "Gates passed (stubbed).", findings: [], totalFindings: 0, logDir: "stub" }
const gated = stubSession()
const gatedCommands = new Map(
esCommandSpecs(gated.io, runtime, stubBridge(), {
git: (async () => ({ stdout: "" })) as never,
runGates: (async () => canned) as never,
}).map((item) => [item.name, item]),
)
await gatedCommands.get("gates")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
expect(gated.said[0]?.text).toBe(formatReport(canned as never))
})
test("lsp reports status and availability lines (#277.1)", async () => {
const stub = stubSession()
const commands = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item]))
await commands.get("lsp")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
const text = stub.said[0]?.text ?? ""
expect(text).toMatch(/LSP is (on|disabled)/)
expect(text).toContain("typescript")
})
test("hooks / status / config render server content synthetically", async () => {
const stub = stubSession()
const commands = new Map(esCommandSpecs(stub.io, runtime, stubBridge()).map((item) => [item.name, item]))
await commands.get("hooks")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
expect(stub.said[0]?.text).toContain("Hook bridge: 2 handler(s)")
expect(stub.said[0]?.text).toContain("NOT trusted")
await commands.get("status")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
expect(stub.said[1]?.text).toContain("<factory-state>")
await commands.get("config")!.execute({ sessionID: "ses_1", prompt: {}, delivery: undefined })
expect(stub.said[2]?.text).toContain("Layers:")
})
})
+95
View File
@@ -0,0 +1,95 @@
// Visibility proofs (#276): the plugin registers additively (built-in tools
// keep working; only invisible es_* tools are pruned), the status footer is
// server-computed content, and setup writes no project-tree files.
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import { mkdtemp, readdir, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import path from "node:path"
import { AGENTS, ensureEngineKey, footerLine, loadSkills, readLiveness } from "@heretek-ai/es-core"
import { compileAgents } from "../src/agents.ts"
import { createRpcHandlers } from "../src/rpc.ts"
import { createRuntime, parseOptions, type Runtime } from "../src/runtime.ts"
import { createSessionHooks } from "../src/session.ts"
let root: string
let state: string
let runtime: Runtime
beforeEach(async () => {
root = await mkdtemp(path.join(tmpdir(), "es-visibility-"))
state = await mkdtemp(path.join(tmpdir(), "es-visibility-state-"))
runtime = await createRuntime(root, parseOptions({ stateDir: state }))
})
afterEach(async () => {
await runtime.lsp.stopAll().catch(() => undefined)
await rm(root, { recursive: true, force: true })
await rm(state, { recursive: true, force: true })
})
describe("additive registration", () => {
test("we only add registry agents (never built-ins or defaults)", async () => {
const ids = new Set(AGENTS.map((agent) => agent.id))
for (const compiled of await compileAgents(runtime.options, [])) expect(ids.has(compiled.spec.id)).toBe(true)
})
test("context prunes only invisible es_* tools; built-ins and state blocks are additive", async () => {
const hooks = createSessionHooks(runtime)
const tools = () => ({
read: {},
write: {},
shell: {},
question: {},
es_status: {},
es_gates_run: {},
es_complete: {},
es_build_start: {},
})
const user = { agent: "build", system: [] as unknown[], tools: tools() }
await hooks.context(user as never)
// Built-ins are untouched; only the read-only es tools stay visible.
expect(Object.keys(user.tools).sort()).toEqual(["es_gates_run", "es_status", "question", "read", "shell", "write"])
expect(user.system).toEqual([])
const factory = { agent: "factory", system: [] as unknown[], tools: tools() }
await hooks.context(factory as never)
expect(Object.keys(factory.tools)).toContain("es_build_start")
expect(factory.system).toHaveLength(1)
expect((factory.system[0] as { text: string }).text).toContain("<factory-state>")
})
})
describe("footer server-content wiring", () => {
test("status().footer is footerLine(state, liveness, paused)", async () => {
const handlers = createRpcHandlers(runtime, async () => {})
const seen = await handlers.status()
const state = await runtime.factory.read()
const liveness = await readLiveness(root, state)
expect(seen.footer ?? null).toBe(footerLine(state, liveness, undefined) ?? null)
expect(seen.headline).toContain("No factory run")
})
})
describe("setup writes no project-tree files (#276.1)", () => {
test("runtime creation, engine key, agents and skills leave the project tree alone", async () => {
const fresh = await mkdtemp(path.join(tmpdir(), "es-setup-diff-"))
const freshState = await mkdtemp(path.join(tmpdir(), "es-setup-diff-state-"))
try {
const before = await readdir(fresh)
const staged = await createRuntime(fresh, parseOptions({ stateDir: freshState }))
try {
// The engine key seals cached sources: created at setup in the masked
// state dir, never in the project tree.
await ensureEngineKey(staged.stateDir)
await compileAgents(staged.options, [])
await loadSkills()
} finally {
await staged.lsp.stopAll().catch(() => undefined)
}
expect(await readdir(fresh)).toEqual(before)
} finally {
await rm(fresh, { recursive: true, force: true })
await rm(freshState, { recursive: true, force: true })
}
})
})