diff --git a/src/agent/codex-read-raw-file.ts b/src/agent/codex-read-raw-file.ts index 348f5cc92..85623ab0b 100644 --- a/src/agent/codex-read-raw-file.ts +++ b/src/agent/codex-read-raw-file.ts @@ -32,6 +32,7 @@ import { } from "../plugins/secret-guard-plugin.js"; import type { PermissionGate } from "../permission/gate.js"; import type { CodexReadRawFile } from "./codex-tool-proxies.js"; +import { errorMessage } from "./error-message.js"; /** `path` is workspace-relative (apply_patch's parser rejects absolute paths). */ export function createCodexReadRawFile( @@ -96,7 +97,7 @@ export function createCodexReadRawFile( return { content: `path is a directory: ${path}`, isError: true }; } return { - content: err instanceof Error ? err.message : String(err), + content: errorMessage(err), isError: true, }; } diff --git a/src/agent/director.ts b/src/agent/director.ts index 6c0507575..7687723cb 100644 --- a/src/agent/director.ts +++ b/src/agent/director.ts @@ -23,6 +23,7 @@ import { } from "./compaction.js"; import { onTurnBoundary } from "./reactor-events.js"; import { isOperatorOriginated } from "./message-provenance.js"; +import { errorMessage } from "./error-message.js"; import { type } from "arktype"; import { applyManageTasks, @@ -656,79 +657,89 @@ class ChatDirectorImpl extends DefaultDirector { // is the exception: its sole call site runs mid-turn (tool.done, never the // turn boundary), so it always degrades to plain inference on a // coordinator throw and takes no rethrow parameter. - private coordinatorIsActive(rethrowCoordinatorError: boolean): boolean { + // + // The four rethrow-capable consults below share one guard: on a coordinator + // throw, either rethrow (noting it so the turn drops queued notifications) + // or log under the consult's label and resolve the consult's fallback. + private withCoordinatorGuard( + label: string, + fallback: T, + rethrowCoordinatorError: boolean, + consult: () => T, + ): T { try { - return this.workflowCoordinator?.isActive() === true; + return consult(); } catch (err) { if (rethrowCoordinatorError) { this.coordinatorRethrowNoted = true; throw err; } - logger.warn`workflow-coordinator-isActive-threw error=${err instanceof Error ? err.message : String(err)}`; - return false; + logger.warn`${label} error=${errorMessage(err)}`; + return fallback; } } + private coordinatorIsActive(rethrowCoordinatorError: boolean): boolean { + return this.withCoordinatorGuard( + "workflow-coordinator-isActive-threw", + false, + rethrowCoordinatorError, + () => this.workflowCoordinator?.isActive() === true, + ); + } + private coordinatorDirective( rethrowCoordinatorError: boolean, ): string | null { - try { - const directive = this.workflowCoordinator?.directive() ?? null; - if (directive === null) return null; - if (typeof directive !== "string") { - logger.warn`workflow-coordinator-directive-not-string`; - return null; - } - if (directive.length === 0) return null; - if (directive.length > MAX_WORKFLOW_DIRECTIVE_CHARS) { - logger.warn`workflow-coordinator-directive-truncated chars=${String(directive.length)} max=${String(MAX_WORKFLOW_DIRECTIVE_CHARS)}`; - return `${directive.slice(0, MAX_WORKFLOW_DIRECTIVE_CHARS)}\n…[truncated]`; - } - return directive; - } catch (err) { - if (rethrowCoordinatorError) { - this.coordinatorRethrowNoted = true; - throw err; - } - logger.warn`workflow-coordinator-directive-threw error=${err instanceof Error ? err.message : String(err)}`; - return null; - } + return this.withCoordinatorGuard( + "workflow-coordinator-directive-threw", + null, + rethrowCoordinatorError, + () => { + const directive = this.workflowCoordinator?.directive() ?? null; + if (directive === null) return null; + if (typeof directive !== "string") { + logger.warn`workflow-coordinator-directive-not-string`; + return null; + } + if (directive.length === 0) return null; + if (directive.length > MAX_WORKFLOW_DIRECTIVE_CHARS) { + logger.warn`workflow-coordinator-directive-truncated chars=${String(directive.length)} max=${String(MAX_WORKFLOW_DIRECTIVE_CHARS)}`; + return `${directive.slice(0, MAX_WORKFLOW_DIRECTIVE_CHARS)}\n…[truncated]`; + } + return directive; + }, + ); } private coordinatorCurrentStepIsGate( rethrowCoordinatorError: boolean, ): boolean { - try { - return this.workflowCoordinator?.currentStepIsGate() === true; - } catch (err) { - if (rethrowCoordinatorError) { - this.coordinatorRethrowNoted = true; - throw err; - } - logger.warn`workflow-coordinator-gate-threw error=${err instanceof Error ? err.message : String(err)}`; - return false; - } + return this.withCoordinatorGuard( + "workflow-coordinator-gate-threw", + false, + rethrowCoordinatorError, + () => this.workflowCoordinator?.currentStepIsGate() === true, + ); } private coordinatorCurrentStepId( rethrowCoordinatorError: boolean, ): string | null { - try { - const stepId = this.workflowCoordinator?.currentStepId() ?? null; - if (stepId === null) return null; - if (typeof stepId !== "string" || stepId.length === 0) { - logger.warn`workflow-coordinator-step-id-not-string`; - return null; - } - return stepId; - } catch (err) { - if (rethrowCoordinatorError) { - this.coordinatorRethrowNoted = true; - throw err; - } - logger.warn`workflow-coordinator-step-id-threw error=${err instanceof Error ? err.message : String(err)}`; - return null; - } + return this.withCoordinatorGuard( + "workflow-coordinator-step-id-threw", + null, + rethrowCoordinatorError, + () => { + const stepId = this.workflowCoordinator?.currentStepId() ?? null; + if (stepId === null) return null; + if (typeof stepId !== "string" || stepId.length === 0) { + logger.warn`workflow-coordinator-step-id-not-string`; + return null; + } + return stepId; + }, + ); } private coordinatorHandleToolDone( @@ -743,7 +754,7 @@ class ChatDirectorImpl extends DefaultDirector { } catch (err) { // Mid-turn only (tool.done): a throwing coordinator degrades to plain // inference rather than failing the turn. - logger.warn`workflow-coordinator-handleToolDone-threw error=${err instanceof Error ? err.message : String(err)}`; + logger.warn`workflow-coordinator-handleToolDone-threw error=${errorMessage(err)}`; return false; } } diff --git a/src/agent/directors/bruckheimer/package.test.ts b/src/agent/directors/bruckheimer/package.test.ts index 805dec1be..bed10faed 100644 --- a/src/agent/directors/bruckheimer/package.test.ts +++ b/src/agent/directors/bruckheimer/package.test.ts @@ -2,21 +2,6 @@ import { describe, expect, test } from "bun:test"; import { bruckheimerPackage } from "./package.js"; describe("bruckheimerPackage", () => { - test("id matches directory (keep bruckheimer path; identity is Bruckheimer)", () => { - expect(bruckheimerPackage.id).toBe("bruckheimer"); - }); - - test("systemPrompt is real (not Placeholder)", () => { - expect(bruckheimerPackage.systemPrompt.length).toBeGreaterThan(0); - expect(bruckheimerPackage.systemPrompt.startsWith("Placeholder")).toBe( - false, - ); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(bruckheimerPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Bruckheimer / BruckheimerDirector", () => { const p = bruckheimerPackage.systemPrompt; expect(p).toMatch(/BruckheimerDirector \(Bruckheimer\)/); @@ -101,15 +86,8 @@ describe("bruckheimerPackage", () => { expect(p).not.toMatch(/via run_shell/i); }); - test("spawn.maySpawn is false", () => { - expect(bruckheimerPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is DOCS_TOOLS surface (writes, no shell)", () => { + test("tools.allow has no shell", () => { const allow = bruckheimerPackage.tools?.allow ?? []; - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); expect(allow).not.toContain("run_shell"); }); @@ -117,10 +95,6 @@ describe("bruckheimerPackage", () => { expect(bruckheimerPackage.modelRole).toBe("docs"); }); - test("tier is leaf", () => { - expect(bruckheimerPackage.tier).toBe("leaf"); - }); - test("primaryIntent and outOfLane match discovery lane", () => { expect(bruckheimerPackage.primaryIntent).toMatch(/product discovery/i); expect(bruckheimerPackage.outOfLane).toContain("shipping product code"); diff --git a/src/agent/directors/builder/package.test.ts b/src/agent/directors/builder/package.test.ts index b39ccd09a..7f68c2339 100644 --- a/src/agent/directors/builder/package.test.ts +++ b/src/agent/directors/builder/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { builderPackage } from "./package.js"; describe("builderPackage", () => { - test("id matches directory / registry id", () => { - expect(builderPackage.id).toBe("builder"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(builderPackage.systemPrompt.length).toBeGreaterThan(0); - expect(builderPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt mentions PRIMARY INTENT", () => { - expect(builderPackage.systemPrompt).toContain("PRIMARY INTENT"); - }); - test("systemPrompt identity is Builder / BuilderDirector (not job-title language)", () => { const p = builderPackage.systemPrompt; expect(p).toMatch(/BuilderDirector \(Builder\)/); @@ -108,18 +95,6 @@ describe("builderPackage", () => { expect(p).not.toMatch(/scheduler/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(builderPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow includes product write tools", () => { - const allow = builderPackage.tools?.allow ?? []; - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); - expect(allow).toContain("apply_patch"); - }); - test("modelRole is implement", () => { expect(builderPackage.modelRole).toBe("implement"); }); diff --git a/src/agent/directors/counsel/package.test.ts b/src/agent/directors/counsel/package.test.ts index 8c05b6240..03fc781dd 100644 --- a/src/agent/directors/counsel/package.test.ts +++ b/src/agent/directors/counsel/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { counselPackage } from "./package.js"; describe("counselPackage", () => { - test("id matches directory", () => { - expect(counselPackage.id).toBe("counsel"); - }); - - test("systemPrompt is real (not Placeholder)", () => { - expect(counselPackage.systemPrompt.length).toBeGreaterThan(0); - expect(counselPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(counselPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Counsel / CounselDirector (not PlanDirector)", () => { const p = counselPackage.systemPrompt; expect(p).toMatch(/CounselDirector \(Counsel\)/); @@ -63,16 +50,9 @@ describe("counselPackage", () => { expect(p).toContain("Headings-only Findings is not done"); }); - test("spawn.maySpawn is false", () => { - expect(counselPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is review surface with product writes", () => { + test("tools.allow mounts read (plan lane)", () => { const allow = counselPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is plan", () => { diff --git a/src/agent/directors/critic/package.test.ts b/src/agent/directors/critic/package.test.ts index 7597345c2..77d24b946 100644 --- a/src/agent/directors/critic/package.test.ts +++ b/src/agent/directors/critic/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { criticPackage } from "./package.js"; describe("criticPackage", () => { - test("id matches directory", () => { - expect(criticPackage.id).toBe("critic"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(criticPackage.systemPrompt.length).toBeGreaterThan(0); - expect(criticPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(criticPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Critic / CriticDirector", () => { const p = criticPackage.systemPrompt; expect(p).toMatch(/CriticDirector \(Critic\)/); @@ -103,18 +90,11 @@ describe("criticPackage", () => { expect(p).not.toMatch(/via run_shell/i); }); - test("spawn.maySpawn is false", () => { - expect(criticPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is review surface with product writes", () => { + test("tools.allow mounts read plus skill discovery", () => { const allow = criticPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("skill_search"); expect(allow).toContain("use_skill"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is review", () => { diff --git a/src/agent/directors/draper/package.test.ts b/src/agent/directors/draper/package.test.ts index afd9d7ca0..0b3f55dc7 100644 --- a/src/agent/directors/draper/package.test.ts +++ b/src/agent/directors/draper/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { draperPackage } from "./package.js"; describe("draperPackage", () => { - test("id matches directory", () => { - expect(draperPackage.id).toBe("draper"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(draperPackage.systemPrompt.length).toBeGreaterThan(0); - expect(draperPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(draperPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Draper / DraperDirector (package id stays draper)", () => { const p = draperPackage.systemPrompt; expect(p).toMatch(/DraperDirector \(Draper\)/); @@ -126,23 +113,15 @@ describe("draperPackage", () => { expect(p).not.toMatch(/## Paths/); }); - test("spawn.maySpawn is false", () => { - expect(draperPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is review surface with file writes for evidence tests", () => { + test("tools.allow mounts read plus skill discovery", () => { const allow = draperPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("skill_search"); expect(allow).toContain("use_skill"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); - test("modelRole is review and tier is leaf", () => { + test("modelRole is review", () => { expect(draperPackage.modelRole).toBe("review"); - expect(draperPackage.tier).toBe("leaf"); }); test("primaryIntent and outOfLane match the restored full-critique lane", () => { diff --git a/src/agent/directors/emil/package.test.ts b/src/agent/directors/emil/package.test.ts index 41170b046..8ecfabba6 100644 --- a/src/agent/directors/emil/package.test.ts +++ b/src/agent/directors/emil/package.test.ts @@ -2,23 +2,13 @@ import { describe, expect, test } from "bun:test"; import { emilPackage } from "./package.js"; describe("emilPackage", () => { - test("id matches directory", () => { - expect(emilPackage.id).toBe("emil"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(emilPackage.systemPrompt.length).toBeGreaterThan(0); - expect(emilPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Emil / EmilDirector (package id stays emil)", () => { const p = emilPackage.systemPrompt; expect(p).toMatch(/EmilDirector \(Emil\)/); expect(p).toMatch(/design-eng critique lane only/i); }); - test("systemPrompt states PRIMARY INTENT", () => { - expect(emilPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); + test("systemPrompt routes to builder", () => { expect(emilPackage.systemPrompt).toContain("route to builder"); }); @@ -135,18 +125,11 @@ describe("emilPackage", () => { expect(p).not.toMatch(/Never spawn/); }); - test("spawn.maySpawn is false", () => { - expect(emilPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is review surface with product writes", () => { + test("tools.allow mounts read plus skill discovery", () => { const allow = emilPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("skill_search"); expect(allow).toContain("use_skill"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is review", () => { diff --git a/src/agent/directors/explorer/package.test.ts b/src/agent/directors/explorer/package.test.ts index 28860d25f..8e56273b6 100644 --- a/src/agent/directors/explorer/package.test.ts +++ b/src/agent/directors/explorer/package.test.ts @@ -2,17 +2,7 @@ import { describe, expect, test } from "bun:test"; import { explorerPackage } from "./package.js"; describe("explorerPackage", () => { - test("id matches directory", () => { - expect(explorerPackage.id).toBe("explorer"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(explorerPackage.systemPrompt.length).toBeGreaterThan(0); - expect(explorerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(explorerPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); + test("systemPrompt states the map-and-read intent", () => { expect(explorerPackage.systemPrompt).toMatch(/map and read/i); }); @@ -78,17 +68,10 @@ describe("explorerPackage", () => { expect(explorerPackage.systemPrompt).toMatch(/call flow/i); }); - test("spawn.maySpawn is false", () => { - expect(explorerPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow mounts product writes (lane: no product edits)", () => { + test("tools.allow mounts read tools (lane: no product edits)", () => { const allow = explorerPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("grep"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is explore", () => { diff --git a/src/agent/directors/gaasbot/package.test.ts b/src/agent/directors/gaasbot/package.test.ts index 2d99a02ee..521845884 100644 --- a/src/agent/directors/gaasbot/package.test.ts +++ b/src/agent/directors/gaasbot/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { gaasbotPackage } from "./package.js"; describe("gaasbotPackage", () => { - test("id matches directory (keep gaasbot path; identity is Gaasbot)", () => { - expect(gaasbotPackage.id).toBe("gaasbot"); - }); - - test("systemPrompt is real (not Placeholder)", () => { - expect(gaasbotPackage.systemPrompt.length).toBeGreaterThan(0); - expect(gaasbotPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(gaasbotPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Gaasbot / GaasbotDirector (risk counsel)", () => { const p = gaasbotPackage.systemPrompt; expect(p).toMatch(/GaasbotDirector \(Gaasbot\)/); @@ -63,16 +50,9 @@ describe("gaasbotPackage", () => { expect(p).not.toMatch(/via run_shell/i); }); - test("spawn.maySpawn is false", () => { - expect(gaasbotPackage.spawn.maySpawn).toBe(false); - }); - - test("mounts product write tools (lane discipline in prompts)", () => { + test("tools.allow mounts read", () => { const allow = gaasbotPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is plan", () => { diff --git a/src/agent/directors/gauntlet/package.test.ts b/src/agent/directors/gauntlet/package.test.ts index d95d7d71e..a206b818c 100644 --- a/src/agent/directors/gauntlet/package.test.ts +++ b/src/agent/directors/gauntlet/package.test.ts @@ -2,19 +2,9 @@ import { describe, expect, test } from "bun:test"; import { gauntletPackage } from "./package.js"; describe("gauntletPackage", () => { - test("id matches directory / registry id", () => { - expect(gauntletPackage.id).toBe("gauntlet"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(gauntletPackage.systemPrompt.length).toBeGreaterThan(0); - expect(gauntletPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Gauntlet / GauntletDirector", () => { const p = gauntletPackage.systemPrompt; expect(p).toMatch(/GauntletDirector \(Gauntlet\)/); - expect(p).toContain("PRIMARY INTENT"); expect(p).toMatch(/mutation\/vacuity lane/i); }); @@ -73,21 +63,10 @@ describe("gauntletPackage", () => { expect(p).not.toMatch(/scheduler/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(gauntletPackage.spawn.maySpawn).toBe(false); - }); - - test("tier is leaf", () => { - expect(gauntletPackage.tier).toBe("leaf"); - }); - - test("tools.allow mounts the review surface (lane discipline in prompt)", () => { + test("tools.allow mounts read and shell (lane discipline in prompt)", () => { const allow = gauntletPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("run_shell"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is test", () => { diff --git a/src/agent/directors/greybeard/package.test.ts b/src/agent/directors/greybeard/package.test.ts index f57f2b63a..f89cd87eb 100644 --- a/src/agent/directors/greybeard/package.test.ts +++ b/src/agent/directors/greybeard/package.test.ts @@ -3,19 +3,6 @@ import { REVIEW_TOOLS } from "../tool-sets.js"; import { greybeardPackage } from "./package.js"; describe("greybeardPackage", () => { - test("id matches directory", () => { - expect(greybeardPackage.id).toBe("greybeard"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(greybeardPackage.systemPrompt.length).toBeGreaterThan(0); - expect(greybeardPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt mentions PRIMARY INTENT", () => { - expect(greybeardPackage.systemPrompt).toContain("PRIMARY INTENT"); - }); - test("systemPrompt identity is Greybeard / GreybeardDirector (not job-title language)", () => { const p = greybeardPackage.systemPrompt; expect(p).toMatch(/GreybeardDirector \(Greybeard\)/); @@ -127,15 +114,6 @@ describe("greybeardPackage", () => { expect(greybeardPackage.systemPrompt).not.toMatch(/\bspawn implement\b/); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(greybeardPackage.spawn.maySpawn).toBe(false); - expect(greybeardPackage.spawn.allowlist).toBeUndefined(); - }); - - test("no spawn allowlist survives the leaf conversion", () => { - expect(greybeardPackage.spawn.allowlist ?? []).toHaveLength(0); - }); - test("tools.allow is the review surface without fleet verbs", () => { const allow = greybeardPackage.tools?.allow ?? []; expect([...allow]).toEqual([...REVIEW_TOOLS]); @@ -148,13 +126,6 @@ describe("greybeardPackage", () => { expect(allow).not.toContain("send_input"); expect(allow).toContain("read_file"); expect(allow).toContain("grep"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); - }); - - test("tier is leaf", () => { - expect(greybeardPackage.tier).toBe("leaf"); }); test("modelRole is review", () => { diff --git a/src/agent/directors/identity.test.ts b/src/agent/directors/identity.test.ts index 106454a05..1be9d2a19 100644 --- a/src/agent/directors/identity.test.ts +++ b/src/agent/directors/identity.test.ts @@ -7,15 +7,19 @@ import { } from "./identity.js"; import { DIRECTOR_REGISTRY } from "./registry.js"; +const SKILL_LIST_BY_DIRECTOR = { + builder: "style, philosophy, native-runtime, idiot-proof, ponytail", + counsel: "style, philosophy, native-integration", + skywalker: "style, philosophy, native-integration, interview", +} as const; + describe("formatDirectorSystemPrompt", () => { test("prefixes agent id, model role, and optional skills", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.builder); expect(text.startsWith("Identity: agent id `builder`")).toBe(true); expect(text).toContain('spawn_agent(agent="builder")'); expect(text).toContain("Model role: implement."); - expect(text).toContain( - "style, philosophy, native-runtime, idiot-proof, ponytail", - ); + expect(text).toContain(SKILL_LIST_BY_DIRECTOR.builder); expect(text).toContain(DIRECTOR_REGISTRY.builder.systemPrompt); }); @@ -28,7 +32,7 @@ describe("formatDirectorSystemPrompt", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.builder); expect(text).not.toContain("# Baked skill guidance"); expect(text).toContain( - "Optional skills (names for awareness; load brief-named skills straight through use_skill, skill_search for discovery when mounted): style, philosophy, native-runtime, idiot-proof, ponytail.", + `Optional skills (names for awareness; load brief-named skills straight through use_skill, skill_search for discovery when mounted): ${SKILL_LIST_BY_DIRECTOR.builder}.`, ); expect(text).toContain(WORKER_SKILL_SCOPING); expect(text).toContain( @@ -77,23 +81,18 @@ describe("formatDirectorSystemPrompt", () => { expect(text).not.toContain("use_skill is not mounted on workers"); expect(text).not.toMatch(/guidance is baked/i); expect(text).toContain("use_skill is primary-mounted"); - expect(text).toContain("style, philosophy, native-integration, interview"); + expect(text).toContain(SKILL_LIST_BY_DIRECTOR.skywalker); }); test("counsel lists skill names with the scoping rule and no bodies (CL-6803)", () => { const text = formatDirectorSystemPrompt(DIRECTOR_REGISTRY.counsel); - expect(DIRECTOR_REGISTRY.counsel.optionalSkills).toEqual([ - "style", - "philosophy", - "native-integration", - ]); expect(text).not.toContain("# Baked skill guidance"); expect(text).not.toContain("### interview"); // interview recipe centers on ask_operator batches; counsel must not embed it expect(text).not.toMatch( /multiple-choice questions in batches via `ask_operator`/, ); - expect(text).toContain("style, philosophy"); + expect(text).toContain(SKILL_LIST_BY_DIRECTOR.counsel); expect(text).toContain(WORKER_SKILL_SCOPING); }); }); diff --git a/src/agent/directors/intern/package.test.ts b/src/agent/directors/intern/package.test.ts index a661c9138..7dd66ed53 100644 --- a/src/agent/directors/intern/package.test.ts +++ b/src/agent/directors/intern/package.test.ts @@ -3,16 +3,6 @@ import { describe, expect, test } from "bun:test"; import { internPackage } from "./package.js"; describe("internPackage", () => { - test("id matches directory", () => { - expect(internPackage.id).toBe("intern"); - }); - - test("systemPrompt is real (not placeholder)", () => { - expect(internPackage.systemPrompt.length).toBeGreaterThan(0); - expect(internPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - expect(internPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt is mechanical executor (gaas intern port)", () => { const p = internPackage.systemPrompt; expect(p).toMatch(/execute clear (mechanical )?instructions/i); @@ -30,22 +20,13 @@ describe("internPackage", () => { test("fail-closes without shell_collect rather than pinning prompt copy", () => { const allow = internPackage.tools?.allow ?? []; - expect(allow).toContain("run_shell"); expect(allow).not.toContain("shell_collect"); }); - test("spawn.maySpawn is false", () => { - expect(internPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow is shell-first with path writes", () => { + test("tools.allow has path writes with a narrow deny set", () => { const allow = internPackage.tools?.allow ?? []; - expect(allow).toContain("run_shell"); expect(allow).toContain("read_file"); expect(allow).toContain("list_dir"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); expect(allow).not.toContain("shell_collect"); for (const name of [ "grep", diff --git a/src/agent/directors/migrator/package.test.ts b/src/agent/directors/migrator/package.test.ts index c6b6ee339..ee0f07ee5 100644 --- a/src/agent/directors/migrator/package.test.ts +++ b/src/agent/directors/migrator/package.test.ts @@ -2,20 +2,10 @@ import { describe, expect, test } from "bun:test"; import { migratorPackage } from "./package.js"; describe("migratorPackage", () => { - test("id matches directory / registry id", () => { - expect(migratorPackage.id).toBe("migrator"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(migratorPackage.systemPrompt.length).toBeGreaterThan(0); - expect(migratorPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is the reversible-migration leaf", () => { const p = migratorPackage.systemPrompt; expect(p).toContain("You are MigratorDirector (Migrator)"); expect(p).toMatch(/reversible-migration leaf/); - expect(p).toContain("PRIMARY INTENT"); }); test("systemPrompt owns only settings/config/session-state data changes", () => { @@ -73,15 +63,6 @@ describe("migratorPackage", () => { ); }); - test("tools.allow is exactly read_file/grep/lsp/run_shell in order", () => { - expect(migratorPackage.tools?.allow).toEqual([ - "read_file", - "grep", - "lsp", - "run_shell", - ]); - }); - test("tools.allow carries no fleet verbs and no path writes", () => { const allow = migratorPackage.tools?.allow ?? []; for (const verb of [ @@ -99,10 +80,8 @@ describe("migratorPackage", () => { } }); - test("spawn.maySpawn is false with no allowlist (leaf)", () => { - expect(migratorPackage.spawn.maySpawn).toBe(false); + test("spawn has no allowlist (leaf)", () => { expect(migratorPackage.spawn.allowlist).toBeUndefined(); - expect(migratorPackage.tier).toBe("leaf"); }); test("modelRole is plan", () => { diff --git a/src/agent/directors/neckbeard/package.test.ts b/src/agent/directors/neckbeard/package.test.ts index dc153e676..9df962c62 100644 --- a/src/agent/directors/neckbeard/package.test.ts +++ b/src/agent/directors/neckbeard/package.test.ts @@ -2,24 +2,11 @@ import { describe, expect, test } from "bun:test"; import { neckbeardPackage } from "./package.js"; describe("neckbeardPackage", () => { - test("id matches directory", () => { - expect(neckbeardPackage.id).toBe("neckbeard"); - }); - - test("systemPrompt is real (not Placeholder)", () => { - expect(neckbeardPackage.systemPrompt.length).toBeGreaterThan(0); - expect(neckbeardPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt is a substantial near-literal port", () => { // gaas neckbeard.md is ~504 lines; Corbits adaptations still keep a long prompt. expect(neckbeardPackage.systemPrompt.length).toBeGreaterThan(8_000); }); - test("systemPrompt states PRIMARY INTENT", () => { - expect(neckbeardPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt names NeckbeardDirector and never-fix stance", () => { expect(neckbeardPackage.systemPrompt).toMatch(/NeckbeardDirector/); expect(neckbeardPackage.systemPrompt).toMatch(/never fix/i); @@ -62,16 +49,9 @@ describe("neckbeardPackage", () => { expect(p).toMatch(/ranked nits with evidence|evidence paths/i); }); - test("spawn.maySpawn is false", () => { - expect(neckbeardPackage.spawn.maySpawn).toBe(false); - }); - - test("mounts product write tools", () => { + test("tools.allow mounts read", () => { const allow = neckbeardPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is review", () => { diff --git a/src/agent/directors/prober/package.test.ts b/src/agent/directors/prober/package.test.ts index 4f5ee089b..4fe200d11 100644 --- a/src/agent/directors/prober/package.test.ts +++ b/src/agent/directors/prober/package.test.ts @@ -2,19 +2,9 @@ import { describe, expect, test } from "bun:test"; import { proberPackage } from "./package.js"; describe("proberPackage", () => { - test("id matches directory / registry id", () => { - expect(proberPackage.id).toBe("prober"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(proberPackage.systemPrompt.length).toBeGreaterThan(0); - expect(proberPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Prober / ProberDirector", () => { const p = proberPackage.systemPrompt; expect(p).toMatch(/ProberDirector \(Prober\)/); - expect(p).toContain("PRIMARY INTENT"); expect(p).toMatch(/measure-only lane/i); }); @@ -79,21 +69,10 @@ describe("proberPackage", () => { expect(p).not.toMatch(/scheduler/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(proberPackage.spawn.maySpawn).toBe(false); - }); - - test("tier is leaf", () => { - expect(proberPackage.tier).toBe("leaf"); - }); - - test("tools.allow mounts the review surface (lane discipline in prompt)", () => { + test("tools.allow mounts read and shell (lane discipline in prompt)", () => { const allow = proberPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); expect(allow).toContain("run_shell"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is test", () => { diff --git a/src/agent/directors/rand/package.test.ts b/src/agent/directors/rand/package.test.ts index f05442e9e..ad6a4b7bc 100644 --- a/src/agent/directors/rand/package.test.ts +++ b/src/agent/directors/rand/package.test.ts @@ -2,15 +2,6 @@ import { describe, expect, test } from "bun:test"; import { randPackage } from "./package.js"; describe("randPackage", () => { - test("id matches directory", () => { - expect(randPackage.id).toBe("rand"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(randPackage.systemPrompt.length).toBeGreaterThan(0); - expect(randPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Rand / RandDirector", () => { const p = randPackage.systemPrompt; expect(p).toMatch(/RandDirector \(Rand\)/); @@ -18,8 +9,7 @@ describe("randPackage", () => { expect(p).not.toMatch(/BrandReviewerDirector/); }); - test("systemPrompt states PRIMARY INTENT", () => { - expect(randPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); + test("systemPrompt names builder as the implement lane", () => { expect(randPackage.systemPrompt).toContain("name builder"); expect(randPackage.systemPrompt).not.toContain("name implement"); }); @@ -65,17 +55,6 @@ describe("randPackage", () => { expect(p).not.toMatch(/## Summary/); }); - test("spawn.maySpawn is false", () => { - expect(randPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow includes write tools", () => { - const allow = randPackage.tools?.allow ?? []; - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); - }); - test("systemPrompt mentions DESIGN.md", () => { expect(randPackage.systemPrompt).toMatch(/DESIGN\.md/); expect(randPackage.systemPrompt).not.toMatch(/authz/i); diff --git a/src/agent/directors/shakespeare/package.test.ts b/src/agent/directors/shakespeare/package.test.ts index acfcdaa32..35bd67da2 100644 --- a/src/agent/directors/shakespeare/package.test.ts +++ b/src/agent/directors/shakespeare/package.test.ts @@ -2,21 +2,9 @@ import { describe, expect, test } from "bun:test"; import { shakespearePackage } from "./package.js"; describe("shakespearePackage", () => { - test("id matches directory / registry id", () => { - expect(shakespearePackage.id).toBe("shakespeare"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(shakespearePackage.systemPrompt.length).toBeGreaterThan(0); - expect(shakespearePackage.systemPrompt.startsWith("Placeholder")).toBe( - false, - ); - }); - test("systemPrompt identity is Shakespeare / ShakespeareDirector", () => { const p = shakespearePackage.systemPrompt; expect(p).toMatch(/ShakespeareDirector \(Shakespeare\)/); - expect(p).toContain("PRIMARY INTENT"); expect(p).toMatch(/docs lane only/i); expect(p).toMatch(/PRODUCT\.md/); expect(p).toMatch(/ARCHITECTURE\.md/); @@ -70,17 +58,6 @@ describe("shakespearePackage", () => { expect(p).toMatch(/do not become Builder, Critic, or Rand/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(shakespearePackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow includes write tools", () => { - const allow = shakespearePackage.tools?.allow ?? []; - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); - }); - test("modelRole is docs", () => { expect(shakespearePackage.modelRole).toBe("docs"); }); diff --git a/src/agent/directors/skywalker/package.test.ts b/src/agent/directors/skywalker/package.test.ts index e0c6e0699..4c1098385 100644 --- a/src/agent/directors/skywalker/package.test.ts +++ b/src/agent/directors/skywalker/package.test.ts @@ -2,18 +2,11 @@ import { describe, expect, test } from "bun:test"; import { createSkywalkerSystemPrompt, skywalkerPackage } from "./package.js"; describe("skywalkerPackage", () => { - test("id matches directory", () => { - expect(skywalkerPackage.id).toBe("skywalker"); - }); - - test("systemPrompt is real, not placeholder", () => { - expect(skywalkerPackage.systemPrompt.length).toBeGreaterThan(0); - expect(skywalkerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); + test("systemPrompt names Skywalker and the DIY lane", () => { expect(skywalkerPackage.systemPrompt).toContain("You are Skywalker"); expect(skywalkerPackage.systemPrompt).toContain( "When asked your name, answer: Skywalker", ); - expect(skywalkerPackage.systemPrompt).toContain("PRIMARY INTENT"); expect(skywalkerPackage.systemPrompt).toContain( "write_file/edit_file/delete_file", ); @@ -26,8 +19,7 @@ describe("skywalkerPackage", () => { expect(createSkywalkerSystemPrompt()).toBe(skywalkerPackage.systemPrompt); }); - test("maySpawn true with full closed allowlist", () => { - expect(skywalkerPackage.spawn.maySpawn).toBe(true); + test("spawn allowlist is the full closed set", () => { expect(skywalkerPackage.spawn.allowlist).toHaveLength(19); expect(skywalkerPackage.spawn.allowlist).toEqual([ "builder", @@ -52,17 +44,9 @@ describe("skywalkerPackage", () => { ]); }); - test("tools.allow mounts orchestrator surface plus product writes for DIY", () => { + test("tools.allow mounts agent search for DIY delegation", () => { const allow = skywalkerPackage.tools?.allow ?? []; - expect(allow).not.toContain("task"); - expect(allow).toContain("spawn_agent"); - // CL-7678: TUI primary collects through mailbox mail; wait_agents is - // exec-primary opt-in, so it stays off the Skywalker allow. - expect(allow).not.toContain("wait_agents"); expect(allow).toContain("search_agents"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is orchestrator", () => { diff --git a/src/agent/directors/tester/package.test.ts b/src/agent/directors/tester/package.test.ts index 02af2efd9..cf3f9fa3a 100644 --- a/src/agent/directors/tester/package.test.ts +++ b/src/agent/directors/tester/package.test.ts @@ -2,15 +2,6 @@ import { describe, expect, test } from "bun:test"; import { testerPackage } from "./package.js"; describe("testerPackage", () => { - test("id matches directory / registry id", () => { - expect(testerPackage.id).toBe("tester"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(testerPackage.systemPrompt.length).toBeGreaterThan(0); - expect(testerPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Tester / TesterDirector (named entity)", () => { const p = testerPackage.systemPrompt; expect(p).toMatch(/TesterDirector \(Tester\)/); @@ -18,9 +9,8 @@ describe("testerPackage", () => { expect(p).not.toMatch(/test director/i); }); - test("systemPrompt states PRIMARY INTENT to run suite/repro and never fix", () => { + test("systemPrompt runs suite/repro and never fixes", () => { const p = testerPackage.systemPrompt; - expect(p).toContain("PRIMARY INTENT"); expect(p).toMatch(/suite\s*\/\s*repro|suite \/ repro/i); expect(p).toMatch(/pass\/fail evidence|evidence/i); expect(p).toMatch(/never fix|Never fix|do not patch/i); @@ -56,17 +46,10 @@ describe("testerPackage", () => { expect(p).not.toMatch(/harness-allowed tools/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(testerPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow mounts product writes (lane: never fix)", () => { + test("tools.allow mounts shell and read (lane: never fix)", () => { const allow = testerPackage.tools?.allow ?? []; expect(allow).toContain("run_shell"); expect(allow).toContain("read_file"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is test", () => { diff --git a/src/agent/directors/testsmith/package.test.ts b/src/agent/directors/testsmith/package.test.ts index 2f3c63272..33b97319e 100644 --- a/src/agent/directors/testsmith/package.test.ts +++ b/src/agent/directors/testsmith/package.test.ts @@ -2,19 +2,9 @@ import { describe, expect, test } from "bun:test"; import { testsmithPackage } from "./package.js"; describe("testsmithPackage", () => { - test("id matches directory / registry id", () => { - expect(testsmithPackage.id).toBe("testsmith"); - }); - - test("systemPrompt is non-empty and not a Placeholder", () => { - expect(testsmithPackage.systemPrompt.length).toBeGreaterThan(0); - expect(testsmithPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - test("systemPrompt identity is Testsmith / TestsmithDirector", () => { const p = testsmithPackage.systemPrompt; expect(p).toMatch(/TestsmithDirector \(Testsmith\)/); - expect(p).toContain("PRIMARY INTENT"); expect(p).toMatch(/permanent test/i); }); @@ -76,16 +66,9 @@ describe("testsmithPackage", () => { expect(p).not.toMatch(/filed-for-later/i); }); - test("spawn.maySpawn is false (leaf)", () => { - expect(testsmithPackage.spawn.maySpawn).toBe(false); - }); - - test("tools.allow mounts product writes (lane: design only)", () => { + test("tools.allow mounts read (lane: design only)", () => { const allow = testsmithPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is test", () => { diff --git a/src/agent/directors/warden/package.test.ts b/src/agent/directors/warden/package.test.ts index 038bb3c7d..e804e517a 100644 --- a/src/agent/directors/warden/package.test.ts +++ b/src/agent/directors/warden/package.test.ts @@ -2,19 +2,6 @@ import { describe, expect, test } from "bun:test"; import { wardenPackage } from "./package.js"; describe("wardenPackage", () => { - test("id matches directory", () => { - expect(wardenPackage.id).toBe("warden"); - }); - - test("systemPrompt is real, not a placeholder", () => { - expect(wardenPackage.systemPrompt.length).toBeGreaterThan(0); - expect(wardenPackage.systemPrompt.startsWith("Placeholder")).toBe(false); - }); - - test("systemPrompt states PRIMARY INTENT", () => { - expect(wardenPackage.systemPrompt).toMatch(/PRIMARY INTENT/i); - }); - test("systemPrompt identity is Warden / WardenDirector", () => { const p = wardenPackage.systemPrompt; expect(p).toMatch(/WardenDirector \(Warden\)/); @@ -71,24 +58,13 @@ describe("wardenPackage", () => { expect(p).not.toMatch(/via run_shell/i); }); - test("spawn.maySpawn is false", () => { - expect(wardenPackage.spawn.maySpawn).toBe(false); - }); - - test("tier is leaf", () => { - expect(wardenPackage.tier).toBe("leaf"); - }); - - test("tools.allow is review surface with product writes", () => { + test("tools.allow mounts read plus skill discovery", () => { const allow = wardenPackage.tools?.allow ?? []; expect(allow).toContain("read_file"); // Skill tools ride REVIEW_TOOLS via READ_TOOLS (scoped at mount to // optionalSkills) so warden loads its skills on demand like critic. expect(allow).toContain("skill_search"); expect(allow).toContain("use_skill"); - expect(allow).toContain("write_file"); - expect(allow).toContain("edit_file"); - expect(allow).toContain("delete_file"); }); test("modelRole is review", () => { diff --git a/src/agent/error-message.ts b/src/agent/error-message.ts new file mode 100644 index 000000000..dfb1489b7 --- /dev/null +++ b/src/agent/error-message.ts @@ -0,0 +1,8 @@ +/** + * Shared unknown-throw coercion for the agent loop: an Error's message, + * otherwise String(err). Consolidates the repeated inline + * `err instanceof Error ? err.message : String(err)` expression. + */ +export function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/src/agent/tools.ts b/src/agent/tools.ts index a8dbf5261..125df2db3 100644 --- a/src/agent/tools.ts +++ b/src/agent/tools.ts @@ -122,6 +122,7 @@ import { type CodexRunTool, } from "./codex-tool-proxies.js"; import { createCodexReadRawFile } from "./codex-read-raw-file.js"; +import { errorMessage } from "./error-message.js"; import type { ReactorEmittedEvent } from "@intx/inference"; const AskOperatorArgs = type({ @@ -1317,7 +1318,7 @@ export async function createAgentToolset( }); } catch (err) { if (staleOrDisabled()) return; - const error = err instanceof Error ? err.message : String(err); + const error = errorMessage(err); if (isBuiltinExaMCPServer(config)) { resolveBuiltinExaConnection?.({ ok: false, @@ -1387,7 +1388,7 @@ export async function createAgentToolset( permissionGate.unregisterMcpServer(config.name); await result.client.close().catch(() => undefined); if (staleOrDisabled()) return; - const error = err instanceof Error ? err.message : String(err); + const error = errorMessage(err); if (isBuiltinExaMCPServer(config)) { resolveBuiltinExaConnection?.({ ok: false, diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index f80965396..d883b0574 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -107,6 +107,7 @@ import { import { formatSubAgentSpawnAuthFailureMessage } from "./inference-auth-failure.js"; import { isResolvedProviderFailureError } from "../inference-error-message.js"; +import { errorMessage } from "../agent/error-message.js"; import { isSubAgentCancelError } from "./dispose.js"; import { createInterventionLog, @@ -1206,7 +1207,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { }); } catch (err: unknown) { log.error("spawn_agent worktree cleanup failed: {error}", { - error: err instanceof Error ? err.message : String(err), + error: errorMessage(err), }); } }; @@ -1256,7 +1257,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { const message = err instanceof WorktreeError ? err.message - : `sub-agent worktree setup failed: ${err instanceof Error ? err.message : String(err)}`; + : `sub-agent worktree setup failed: ${errorMessage(err)}`; log.error("spawn_agent worktree setup failed: {error}", { error: message, }); @@ -1510,8 +1511,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { return; } const isProviderFailure = isResolvedProviderFailureError(err); - const diagnosticMessage = - err instanceof Error ? err.message : String(err); + const diagnosticMessage = errorMessage(err); const authMessage = formatSubAgentSpawnAuthFailureMessage( description, err, diff --git a/src/subagent/director-test-harness.ts b/src/subagent/director-test-harness.ts new file mode 100644 index 000000000..a111e310a --- /dev/null +++ b/src/subagent/director-test-harness.ts @@ -0,0 +1,34 @@ +import type { ReactorAction, ReactorCapabilities } from "@intx/types/runtime"; + +/** + * Shared ReactorCapabilities stub for director tests. The action builders + * return protocol-shaped objects; tests only assert on the resulting + * decide() actions, never on inference behavior. + */ +export function createTestCapabilities(): ReactorCapabilities { + return { + infer: (options) => + ({ + type: "infer", + ...(options !== undefined ? { options } : {}), + }) as ReactorAction, + executeTools: (calls, parallel, addToHistory) => + ({ + type: "execute_tools", + calls, + parallel, + addToHistory, + }) as ReactorAction, + suspend: (gate) => ({ type: "suspend", gate }) as ReactorAction, + fork: (mode, forkId) => ({ type: "fork", mode, forkId }) as ReactorAction, + emit: (eventType, data) => + ({ type: "emit", eventType, data }) as ReactorAction, + reply: (content) => ({ type: "reply", content }) as ReactorAction, + checkpoint: (message = "") => + ({ type: "checkpoint", message }) as ReactorAction, + compact: (compactor, reason) => + ({ type: "compact", compactor, reason }) as ReactorAction, + wait: () => ({ type: "wait" }) as ReactorAction, + done: () => ({ type: "done" }) as ReactorAction, + }; +} diff --git a/src/subagent/dispose.ts b/src/subagent/dispose.ts index 413bc1c46..b746a9fb0 100644 --- a/src/subagent/dispose.ts +++ b/src/subagent/dispose.ts @@ -175,21 +175,15 @@ export async function disposeSubAgentSession( } catch (err: unknown) { posixError = err; } - try { - await awaitCloseWithoutHidingLeftover( - input.agent?.close() ?? Promise.resolve(), - posixError, - ); - } catch { - // ignore - } - try { - await awaitCloseWithoutHidingLeftover( - input.streamPromise ?? Promise.resolve(), - posixError, - ); - } catch { - // ignore + for (const teardown of [ + input.agent?.close() ?? Promise.resolve(), + input.streamPromise ?? Promise.resolve(), + ]) { + try { + await awaitCloseWithoutHidingLeftover(teardown, posixError); + } catch { + // ignore + } } if (posixError !== undefined) throw posixError; } diff --git a/src/subagent/followup-live-agent.test.ts b/src/subagent/followup-live-agent.test.ts index 7251977f7..670515c0b 100644 --- a/src/subagent/followup-live-agent.test.ts +++ b/src/subagent/followup-live-agent.test.ts @@ -25,6 +25,7 @@ import { withMockedModuleDuring } from "../../tests/helpers/mock-module.js"; import { defined } from "../../tests/helpers/defined.js"; import { createPermissionGate } from "../permission/gate.js"; import type { RunSubAgentParams } from "./types.js"; +import { errorMessage } from "../agent/error-message.js"; const testPermissionGate = createPermissionGate({ approvals: [], @@ -313,7 +314,7 @@ describe("interrupt_agent / resume_agent reuse the same live agent", () => { (reply) => ({ ok: true as const, reply }), (err: unknown) => ({ ok: false as const, - message: err instanceof Error ? err.message : String(err), + message: errorMessage(err), }), ); return { first, followup }; diff --git a/src/subagent/inference-auth-failure.ts b/src/subagent/inference-auth-failure.ts index ba0d693c2..4002d7394 100644 --- a/src/subagent/inference-auth-failure.ts +++ b/src/subagent/inference-auth-failure.ts @@ -1,5 +1,6 @@ import { CodexAuthError } from "../auth/codex/session.js"; import { XaiAuthError } from "../auth/xai/session.js"; +import { errorMessage } from "../agent/error-message.js"; export type SubAgentAuthFailureKind = "codex" | "xai"; @@ -22,7 +23,7 @@ export function formatSubAgentSpawnAuthFailureMessage( err instanceof CodexAuthError || err instanceof XaiAuthError ? err.profile : "default"; - const detail = err instanceof Error ? err.message : String(err); + const detail = errorMessage(err); const detailSentence = detail.endsWith(".") ? detail : `${detail}.`; const providerLabel = kind === "codex" ? "Codex" : "xAI"; // No "Error:" prefix — SessionStore.fail and tool-result surfaces add their own. diff --git a/src/subagent/lifecycle-tools.ts b/src/subagent/lifecycle-tools.ts index f3b73d169..b7e482d5c 100644 --- a/src/subagent/lifecycle-tools.ts +++ b/src/subagent/lifecycle-tools.ts @@ -14,6 +14,7 @@ import { type } from "arktype"; import type { ToolDefinition, ToolResult } from "@intx/types/runtime"; import { DEFAULT_CLOSE_DEADLINE_MS } from "./dispose.js"; +import { errorMessage } from "../agent/error-message.js"; import type { FleetMailboxHandle } from "./agent-fleet.js"; import { DEFAULT_MAX_ENTRY_CHARS, @@ -284,10 +285,7 @@ export function createResumeAgentTool(deps: ResumeAgentToolDeps): AgentTool { deps.sessions.complete(target, reply); }, onFail: (err) => { - deps.sessions.fail( - target, - err instanceof Error ? err.message : String(err), - ); + deps.sessions.fail(target, errorMessage(err)); }, }); if (!outcome.ok) { diff --git a/src/subagent/nudge-director.test.ts b/src/subagent/nudge-director.test.ts index 26261c63d..95295bbda 100644 --- a/src/subagent/nudge-director.test.ts +++ b/src/subagent/nudge-director.test.ts @@ -12,6 +12,7 @@ import { } from "../session/compactor.js"; import { SubAgentDirector } from "./nudge-director.js"; import type { AdmissionQueue } from "./admission.js"; +import { createTestCapabilities } from "./director-test-harness.js"; import { defined } from "../../tests/helpers/defined.js"; const state = { turns: [] } as unknown as ReactorState; @@ -32,34 +33,6 @@ const longState = { // pings, so freeze time instead. const frozenNow = () => 0; -function capabilities(): ReactorCapabilities { - return { - infer: (options) => - ({ - type: "infer", - ...(options !== undefined ? { options } : {}), - }) as ReactorAction, - executeTools: (calls, parallel, addToHistory) => - ({ - type: "execute_tools", - calls, - parallel, - addToHistory, - }) as ReactorAction, - suspend: (gate) => ({ type: "suspend", gate }) as ReactorAction, - fork: (mode, forkId) => ({ type: "fork", mode, forkId }) as ReactorAction, - emit: (eventType, data) => - ({ type: "emit", eventType, data }) as ReactorAction, - reply: (content) => ({ type: "reply", content }) as ReactorAction, - checkpoint: (message = "") => - ({ type: "checkpoint", message }) as ReactorAction, - compact: (compactor, reason) => - ({ type: "compact", compactor, reason }) as ReactorAction, - wait: () => ({ type: "wait" }) as ReactorAction, - done: () => ({ type: "done" }) as ReactorAction, - }; -} - function inferenceDone( callIds: string[], inputTokens = 0, @@ -174,7 +147,7 @@ function ephemeralTexts( describe("SubAgentDirector tool failure recovery", () => { test("failed tool result adds one actionable ephemeral recovery nudge", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["failed-call"]), state, caps); const texts = ephemeralTexts( @@ -194,7 +167,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("coalesces consecutive failed tool audits into one counted intervention", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const records: { id: string; count?: number }[] = []; director.observeInterventions((event) => { records.push( @@ -224,7 +197,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("a single failed tool audit omits the count field", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const records: { id: string; count: number | null }[] = []; director.observeInterventions((event) => { records.push({ id: event.id, count: event.count ?? null }); @@ -239,7 +212,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("flushes an undelivered recovery burst when the run goes terminal", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const records: { id: string; count?: number }[] = []; director.observeInterventions((event) => { records.push( @@ -271,7 +244,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("successful tool result has no ephemeral recovery turn", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["successful-call"]), state, caps); const infer = inferAction( @@ -283,7 +256,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("waits for all pending results and carries one recovery nudge on the normal infer", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide( inferenceDone(["failed-first", "successful-last"]), @@ -306,7 +279,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("a later successful cycle has no stale recovery nudge", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["failed-cycle"]), state, caps); await director.decide(toolDone("failed-cycle", true), state, caps); @@ -329,7 +302,7 @@ describe("SubAgentDirector tool failure recovery", () => { 30, frozenNow, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide( inferenceDone(["failed-at-threshold"], 999_999), @@ -370,7 +343,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("recovery nudge appends to ephemeral turns already on the infer", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); // Seed an ephemeral turn only when the caller did not supply any, so the // infer action applyPendingNudge rewrites already carries ephemeral turns. const seeding: ReactorCapabilities = { @@ -409,7 +382,7 @@ describe("SubAgentDirector tool failure recovery", () => { test("failed-tool recovery supersedes soft re-read guidance", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const callIds = [ "shared-1", "shared-2", @@ -453,7 +426,7 @@ describe("SubAgentDirector tool failure recovery", () => { 30, frozenNow, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["failed-then-overflow"]), state, caps); const texts = ephemeralTexts( @@ -506,7 +479,7 @@ describe("SubAgentDirector tool failure recovery", () => { 30, frozenNow, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["failed-then-done"]), state, caps); const recovered = ephemeralTexts( @@ -564,7 +537,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { test("nudges once for explicit tool-call wrapper text before report policy", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const correction = actions( await director.decide(inferenceDoneText(verbatimToolCall), state, caps), @@ -595,7 +568,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { }); test("does not treat arbitrary XML or thinking as verbatim tool calls", async () => { - const caps = capabilities(); + const caps = createTestCapabilities(); const arbitraryXML = new SubAgentDirector("system", [], undefined, 30); const arbitraryResult = actions( await arbitraryXML.decide( @@ -627,7 +600,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { test("resets correction only after genuine tool activity or parent follow-up", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText(verbatimToolCall), state, caps); const narration = actions( @@ -660,7 +633,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { test("after the verbatim nudge a real tool call executes", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText(verbatimToolCall), state, caps); const result = actions( @@ -672,7 +645,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { test("after the verbatim nudge a four-heading envelope completes", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText(verbatimToolCall), state, caps); const result = actions( @@ -686,7 +659,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { test("a complete envelope that quotes tool-call markup still completes", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const reportQuotingMarkup = `${REPORT_ENVELOPE}\n\nThe model emitted ${verbatimToolCall} as text.`; const result = actions( @@ -710,7 +683,7 @@ describe("SubAgentDirector verbatim tool markup recovery", () => { describe("SubAgentDirector incomplete-report wiring", () => { test("tool-less narration after tools gets one wrap-up nudge, not a complete", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -739,7 +712,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { test("Summary-only mid-run narration gets a wrap-up nudge, not done", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -772,7 +745,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { test("second tool-less narration after the wrap-up nudge salvages incomplete-report", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -818,7 +791,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { 30, frozenNow, ); - const caps = capabilities(); + const caps = createTestCapabilities(); const records: { id: string; class: string }[] = []; director.observeInterventions((event) => { records.push({ id: event.id, class: event.class }); @@ -869,7 +842,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { test("tool-using turns reset the tool-less narration count (CL-7788)", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide( inferenceDoneText("Still looking at the files..."), @@ -898,7 +871,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { test("tool-less turn with the four headings completes normally", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -923,7 +896,7 @@ describe("SubAgentDirector incomplete-report wiring", () => { test("zero-tool first turn without a report envelope nudges for one, not a hard stop", async () => { const director = new SubAgentDirector("system", [], undefined, 30); - const caps = capabilities(); + const caps = createTestCapabilities(); const result = actions( await director.decide( @@ -992,7 +965,7 @@ describe("SubAgentDirector plan-substance wiring", () => { false, true, ); - const caps = capabilities(); + const caps = createTestCapabilities(); const result = actions( await director.decide(inferenceDoneText(STUB_PLAN_ENVELOPE), state, caps), @@ -1024,7 +997,7 @@ describe("SubAgentDirector plan-substance wiring", () => { false, true, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText(STUB_PLAN_ENVELOPE), state, caps); @@ -1054,7 +1027,7 @@ describe("SubAgentDirector plan-substance wiring", () => { false, true, ); - const caps = capabilities(); + const caps = createTestCapabilities(); const result = actions( await director.decide(inferenceDoneText(PASS_PLAN_ENVELOPE), state, caps), @@ -1080,7 +1053,7 @@ describe("SubAgentDirector plan-substance wiring", () => { false, true, ); - const caps = capabilities(); + const caps = createTestCapabilities(); const wrapPlan = [ "## Summary", "Plan after reading the gate.", @@ -1117,7 +1090,7 @@ describe("SubAgentDirector plan-substance wiring", () => { describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { test("empty continuation after a valid report reply waits instead of re-inferring", async () => { const director = new SubAgentDirector("system", [], undefined, 1000); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -1147,7 +1120,7 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { 1000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -1166,7 +1139,7 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { test("a non-empty parent follow-up re-opens inference after a report reply", async () => { const director = new SubAgentDirector("system", [], undefined, 1000); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -1185,7 +1158,7 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { test("empty continuation after incomplete-report-stop salvage waits instead of re-inferring", async () => { const director = new SubAgentDirector("system", [], undefined, 1000); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -1225,7 +1198,7 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { }, 1000, ); - const caps = capabilities(); + const caps = createTestCapabilities(); // Under-threshold tooling so tool.done does not compact before the report. await director.decide(inferenceDone(["read-1"]), longState, caps); @@ -1269,7 +1242,7 @@ describe("SubAgentDirector post-complete terminalization (CL-7068)", () => { test("repeated empty continuations after a report reply keep waiting", async () => { const director = new SubAgentDirector("system", [], undefined, 1000); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["read-1"]), state, caps); await director.decide(toolDone("read-1"), state, caps); @@ -1300,7 +1273,7 @@ describe("SubAgentDirector stall nudge grace", () => { 1_000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["slow-1"]), state, caps); @@ -1330,7 +1303,7 @@ describe("SubAgentDirector stall nudge grace", () => { 1_000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDone(["parked-1"]), state, caps); now += 60_000; @@ -1357,7 +1330,7 @@ describe("SubAgentDirector stall nudge grace", () => { 1_000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText("working"), state, caps); @@ -1386,7 +1359,7 @@ describe("SubAgentDirector stall nudge grace", () => { 1_000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText("working"), state, caps); @@ -1431,7 +1404,7 @@ describe("SubAgentDirector stall nudge grace", () => { 1_000, () => now, ); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText("working"), state, caps); now += 1_000; @@ -1470,7 +1443,7 @@ describe("SubAgentDirector ask_director park wait-guard", () => { () => now, ); director.observeAskPending(() => parked); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText("which file?"), state, caps); now += 60_000; @@ -1497,7 +1470,7 @@ describe("SubAgentDirector ask_director park wait-guard", () => { frozenNow, ); director.observeAskPending(() => parked); - const caps = capabilities(); + const caps = createTestCapabilities(); const compact = actions( await director.decide(overflowError(), state, caps), @@ -1536,7 +1509,7 @@ describe("SubAgentDirector ask_director park wait-guard", () => { () => now, ); director.observeAskPending(() => parked); - const caps = capabilities(); + const caps = createTestCapabilities(); await director.decide(inferenceDoneText("which file?"), state, caps); now += 60_000; @@ -1590,7 +1563,11 @@ describe("SubAgentDirector infer retryPolicy", () => { retryPolicy, ); const infer = inferAction( - await director.decide(messageReceived("go"), state, capabilities()), + await director.decide( + messageReceived("go"), + state, + createTestCapabilities(), + ), ); const stamped = infer.options?.retryPolicy; expect(stamped).toBeDefined(); diff --git a/src/subagent/run-ask-director-continue.test.ts b/src/subagent/run-ask-director-continue.test.ts index 65ce4db1c..3a4f551d7 100644 --- a/src/subagent/run-ask-director-continue.test.ts +++ b/src/subagent/run-ask-director-continue.test.ts @@ -11,7 +11,6 @@ import { join } from "node:path"; import type { DirectorFactory } from "@intx/agent"; import type { ReactorAction, - ReactorCapabilities, ReactorInboundEvent, ReactorState, } from "@intx/types/runtime"; @@ -22,6 +21,7 @@ import { createPermissionGate } from "../permission/gate.js"; import type { RunSubAgentParams } from "./types.js"; import type { AskDirectorState } from "./ask-director.js"; import { SubAgentDirector } from "./nudge-director.js"; +import { createTestCapabilities } from "./director-test-harness.js"; const testPermissionGate = createPermissionGate({ approvals: [], @@ -69,34 +69,6 @@ function createHangingStubAgent(deliverLog: unknown[]) { }; } -function capabilities(): ReactorCapabilities { - return { - infer: (options) => - ({ - type: "infer", - ...(options !== undefined ? { options } : {}), - }) as ReactorAction, - executeTools: (calls, parallel, addToHistory) => - ({ - type: "execute_tools", - calls, - parallel, - addToHistory, - }) as ReactorAction, - suspend: (gate) => ({ type: "suspend", gate }) as ReactorAction, - fork: (mode, forkId) => ({ type: "fork", mode, forkId }) as ReactorAction, - emit: (eventType, data) => - ({ type: "emit", eventType, data }) as ReactorAction, - reply: (content) => ({ type: "reply", content }) as ReactorAction, - checkpoint: (message = "") => - ({ type: "checkpoint", message }) as ReactorAction, - compact: (compactor, reason) => - ({ type: "compact", compactor, reason }) as ReactorAction, - wait: () => ({ type: "wait" }) as ReactorAction, - done: () => ({ type: "done" }) as ReactorAction, - }; -} - function emptyContinuation(): ReactorInboundEvent { return { type: "message.received", @@ -263,7 +235,7 @@ describe("runSubAgent ask_director compact-continue wiring", () => { await director.decide( emptyContinuation(), state, - capabilities(), + createTestCapabilities(), ), ); expect(parkedPing).toEqual([{ type: "wait" }]); diff --git a/src/subagent/session-store.ts b/src/subagent/session-store.ts index 31da425f3..0542ec481 100644 --- a/src/subagent/session-store.ts +++ b/src/subagent/session-store.ts @@ -9,6 +9,7 @@ import { AgentClosedError } from "@intx/agent"; import { getLogger } from "@intx/log"; import { LOG_NAMESPACE_ROOT } from "../branding.js"; import { awaitBoundedTeardown, DEFAULT_CLOSE_DEADLINE_MS } from "./dispose.js"; +import { errorMessage } from "../agent/error-message.js"; import { isAlreadyClosed, isLiveStrip, @@ -31,7 +32,7 @@ async function invokeCloseBounded( await awaitBoundedTeardown(close(deadlineMs), deadlineMs); } catch (err: unknown) { log.warn("session close raced deadline: {error}", { - error: err instanceof Error ? err.message : String(err), + error: errorMessage(err), }); throw err; } @@ -810,7 +811,7 @@ export function createSubAgentSessionStore( closeHandles.delete(id); void close(DEFAULT_CLOSE_DEADLINE_MS).catch((err: unknown) => { log.warn("session close during handle release failed: {error}", { - error: err instanceof Error ? err.message : String(err), + error: errorMessage(err), }); }); } @@ -1098,7 +1099,7 @@ export function createSubAgentSessionStore( } log.error("followup turn failed for {id}: {error}", { id, - error: err instanceof Error ? err.message : String(err), + error: errorMessage(err), }); launchNextStashedFollowup(id); return; @@ -1124,7 +1125,7 @@ export function createSubAgentSessionStore( endFollowupTurn(id, failLifecycle); log.error("followup turn failed for {id}: {error}", { id, - error: err instanceof Error ? err.message : String(err), + error: errorMessage(err), }); }; const queueFollowupTurn = ( diff --git a/src/subagent/worktree.ts b/src/subagent/worktree.ts index 37748dcda..61548f782 100644 --- a/src/subagent/worktree.ts +++ b/src/subagent/worktree.ts @@ -7,6 +7,7 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; +import { errorMessage } from "../agent/error-message.js"; const execFileAsync = promisify(execFile); @@ -71,7 +72,7 @@ export async function createSubAgentWorktree( await exec(["worktree", "add", "--detach", path, "HEAD"], { cwd: repoCwd }); } catch (err) { throw new WorktreeError( - `Failed to create sub-agent worktree at "${path}": ${err instanceof Error ? err.message : String(err)}`, + `Failed to create sub-agent worktree at "${path}": ${errorMessage(err)}`, { cause: err }, ); } @@ -81,7 +82,7 @@ export async function createSubAgentWorktree( headAtCreate = stdout.trim(); } catch (err) { throw new WorktreeError( - `Failed to record HEAD for sub-agent worktree at "${path}": ${err instanceof Error ? err.message : String(err)}`, + `Failed to record HEAD for sub-agent worktree at "${path}": ${errorMessage(err)}`, { cause: err }, ); } @@ -194,9 +195,9 @@ export async function cleanupSubAgentWorktree( return { status: "preserved", path, - notice: `Sub-agent worktree at ${path} could not be removed automatically: ${ - err instanceof Error ? err.message : String(err) - }`, + notice: `Sub-agent worktree at ${path} could not be removed automatically: ${errorMessage( + err, + )}`, }; } return { status: "removed", path }; diff --git a/src/workflows/host.ts b/src/workflows/host.ts index 05e3d1a86..dd694f055 100644 --- a/src/workflows/host.ts +++ b/src/workflows/host.ts @@ -2,6 +2,7 @@ import { join } from "node:path"; import type { ToolDefinition } from "@intx/types/runtime"; import { sessionDir } from "../session/index.js"; +import { errorMessage } from "../agent/error-message.js"; import { CAPABILITIES, detectCapabilities, @@ -136,7 +137,7 @@ export class WorkflowHost { runtime.state(), this.args.home, ).catch((err: unknown) => { - const reason = err instanceof Error ? err.message : String(err); + const reason = errorMessage(err); warnWorkflowPersistenceFailure( join( sessionDir(this.args.cwd, sessionId, this.args.home),