From 2b5eb2788b75125d6a2f811dc097d7fd95afecd6 Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Thu, 10 Sep 2026 18:30:24 +1000 Subject: [PATCH 1/8] fix: route native review briefs to their renderer --- core/tools/aidlc.ts | 2 + docs/reference/06-hooks-and-tools.md | 3 + tests/unit/t230-dispatcher-routes.test.ts | 119 +++++++++++++++++++++- 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/core/tools/aidlc.ts b/core/tools/aidlc.ts index d31f9e0be..8a74c2b48 100644 --- a/core/tools/aidlc.ts +++ b/core/tools/aidlc.ts @@ -2043,6 +2043,8 @@ async function loadDelegate(tool: string): Promise { return import("./aidlc-orchestrate.ts"); case TOOLS.plugin: return import("./aidlc-plugin.ts"); + case TOOLS.reviewBrief: + return import("./aidlc-review-brief.ts"); case TOOLS.runnerGen: return import("./aidlc-runner-gen.ts"); case TOOLS.runtime: diff --git a/docs/reference/06-hooks-and-tools.md b/docs/reference/06-hooks-and-tools.md index 3d65382c1..dff5459fa 100644 --- a/docs/reference/06-hooks-and-tools.md +++ b/docs/reference/06-hooks-and-tools.md @@ -848,6 +848,9 @@ for debugging. ### `aidlc-review-brief.ts` — Decision-context renderer +Native installs invoke this tool through `aidlc engine review-brief`, followed +by `summary`, `review`, or `context` and the mode's flags, including `--stage `. + `summary` renders the pre-generation confirmation context from the stage graph and questions-file path. `review` renders a reviewer-backed gate with hydrated finding dispositions and optional stale-path detail. `context` emits only the diff --git a/tests/unit/t230-dispatcher-routes.test.ts b/tests/unit/t230-dispatcher-routes.test.ts index f2b2ddf21..328ba3177 100644 --- a/tests/unit/t230-dispatcher-routes.test.ts +++ b/tests/unit/t230-dispatcher-routes.test.ts @@ -1,5 +1,5 @@ // covers: tool:aidlc, function:renderCommandHelp, tool:aidlc-sensor, tool:aidlc-swarm, hook:aidlc-validate-state, hook:aidlc-review-freeze, hook:aidlc-statusline -import { afterAll, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { @@ -49,8 +49,10 @@ import { AIDLC_VERSION } from "../../core/tools/aidlc-version.ts"; import { cleanupTestProject, createTestProject, + seedAidlcMemory, seededRecordDir, seededStateFile, + seedStateFile, } from "../harness/fixtures.ts"; import { setupTuiProject } from "../harness/tui-fixtures.ts"; @@ -1598,6 +1600,121 @@ describe("t230 dispatcher dev and compiled in-process modes", () => { }); }); +describe("t230 native review-brief dispatch", () => { + let executable: string; + let projectDir: string; + let otherCwd: string; + let artifact: string; + let questions: string; + let finding: string; + let env: NodeJS.ProcessEnv; + + beforeAll(() => { + const root = mkdtempSync(join(tmpdir(), "aidlc-t230-native-")); + tempProjects.add(root); + executable = join(root, process.platform === "win32" ? "aidlc.exe" : "aidlc"); + // Compile the actual dispatcher once; the $bunfs import fixture above + // cannot establish that the native executable reaches its bundled delegate. + const built = spawnSync( + BUN, + ["build", "--compile", join(DIST_TOOLS_DIR, "aidlc.ts"), "--outfile", executable], + { cwd: REPO_ROOT, encoding: "utf-8", timeout: 60_000 }, + ); + if (built.error) throw built.error; + expect(built.status, `${built.stdout}\n${built.stderr}`).toBe(0); + + projectDir = makeProject(); + seedAidlcMemory(projectDir); + seedStateFile(projectDir, "state-mid-inception.md"); + const stageDir = join(seededRecordDir(projectDir), "inception", "requirements-analysis"); + mkdirSync(stageDir, { recursive: true }); + const artifactPath = join(stageDir, "requirements.md"); + const questionsPath = join(stageDir, "requirements-analysis-questions.md"); + artifact = relative(projectDir, artifactPath).replaceAll("\\", "/"); + questions = relative(projectDir, questionsPath).replaceAll("\\", "/"); + finding = `| R-01 | Minor | ${artifact} > FR-1 | Deadline is missing | Add a delivery date | New |`; + writeFileSync(artifactPath, [ + "# Requirements", + "", + "## Review", + "", + "**Verdict:** NOT-READY", + "**Reviewer:** aidlc-product-lead-agent", + "**Iteration:** 1", + "", + "### Findings", + "", + "| ID | Severity | Location | Finding | Required action | Status |", + "|---|---|---|---|---|---|", + finding, + "", + ].join("\n")); + writeFileSync(questionsPath, "# Questions\n\n## Q1: Delivery scope\n[Answer]: A command-line application.\n"); + + otherCwd = join(root, "unrelated-cwd"); + mkdirSync(otherCwd); + env = { + // Both forms read the same generated graph; project content must still + // resolve from --project-dir despite both project envs pointing at cwd. + AIDLC_RUNTIME_HARNESS_ROOT: dirname(DIST_TOOLS_DIR), + AIDLC_PROJECT_DIR: otherCwd, + PATH: "", + }; + }, 65_000); + + function native(args: string[]): RunResult { + return run( + [executable, "engine", "review-brief", ...args, "--project-dir", projectDir], + otherCwd, + { ...env, AIDLC_DISPATCH_TOOLS_DIR: "" }, + ); + } + + for (const command of ["review", "context", "summary"]) { + test(`${command} matches Bun output from another cwd without Bun on PATH`, () => { + const args = [command, "--stage", "requirements-analysis"]; + if (command === "review") args.push("--why", "first"); + if (command === "summary") args.push("--questions-file", questions); + const source = viaDispatcher( + ["engine", "review-brief", ...args, "--project-dir", projectDir], + otherCwd, + env, + ); + expect(source.exitCode, source.stderr.toString()).toBe(0); + expect(source.stderr.toString()).toBe(""); + const result = native(args); + expectSameRun(result, source, `native review-brief ${command}`); + const output = result.stdout.toString(); + if (command === "summary") { + expect(output).toContain("**Stage:** Requirements Analysis"); + expect(output).toContain(`**Confirming:** Consolidated answers in \`${questions}\``); + expect(output).toContain(`before generating \`${artifact}\``); + expect(output).toContain("**Looks correct**"); + expect(output).toContain("**Request changes**"); + } else { + expect(output).toContain(`**Review artifact:** \`${artifact}\``); + expect(output).toContain(finding); + if (command === "review") { + expect(output).toContain("**Stage:** Requirements Analysis"); + expect(output).toContain("**Review outcome:** Concerns remain for your decision."); + expect(output).toContain("**Why now:** First review completed."); + expect(output).toContain("**Approve**"); + expect(output).toContain("**Request Changes**"); + } + } + }, 35_000); + } + + test("missing --stage reaches the native renderer's argument error", () => { + const result = native(["review", "--why", "first"]); + expect(result.exitCode, result.stderr.toString()).toBe(1); + expect(result.stdout.toString()).toBe(""); + expect(JSON.parse(result.stderr.toString())).toEqual({ + error: "Missing --stage .", + }); + }, 20_000); +}); + describe("t230 dispatcher route completeness", () => { test("every route declares the complete normative execution policy", () => { for (const route of ROUTES) { From 2e92ebe3638a17a579dde9e1ca592a60f9af0762 Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:41:54 +1000 Subject: [PATCH 2/8] fix: make the compiled delegate table exhaustive over TOOLS Type the compiled delegate table as Record Promise> so adding a TOOLS entry without a loader or a delegate without export main fails typecheck under bun run check. Keep every import specifier literal for native bundling and reject unknown names before awaiting a delegate. --- core/tools/aidlc.ts | 124 ++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/core/tools/aidlc.ts b/core/tools/aidlc.ts index 8a74c2b48..4dd0400fa 100644 --- a/core/tools/aidlc.ts +++ b/core/tools/aidlc.ts @@ -2005,81 +2005,56 @@ function runDelegateDev(tool: string, args: string[]): number { } } +type ToolFile = (typeof TOOLS)[keyof typeof TOOLS]; + type DelegateModule = { main(argv: string[]): void | Promise; }; -async function loadDelegate(tool: string): Promise { - switch (tool) { - case TOOLS.attest: - return import("./aidlc-attest.ts"); - case TOOLS.audit: - return import("./aidlc-audit.ts"); - case TOOLS.bolt: - return import("./aidlc-bolt.ts"); - case TOOLS.graph: - return import("./aidlc-graph.ts"); - case TOOLS.doctor: - return import("./aidlc-doctor.ts"); - case TOOLS.init: - return import("./aidlc-init.ts"); - case TOOLS.jump: - return import("./aidlc-jump.ts"); - case TOOLS.knowledge: - return import("./aidlc-knowledge.ts"); - case TOOLS.testingPosture: - return import("./aidlc-testing-posture.ts"); - case TOOLS.learnings: - return import("./aidlc-learnings.ts"); - case TOOLS.log: - return import("./aidlc-log.ts"); - case TOOLS.lifecycle: - return import("./aidlc-lifecycle.ts"); - case TOOLS.machineConfig: - return import("./aidlc-machine-config.ts"); - case TOOLS.completions: - return import("./aidlc-completions.ts"); - case TOOLS.orchestrate: - return import("./aidlc-orchestrate.ts"); - case TOOLS.plugin: - return import("./aidlc-plugin.ts"); - case TOOLS.reviewBrief: - return import("./aidlc-review-brief.ts"); - case TOOLS.runnerGen: - return import("./aidlc-runner-gen.ts"); - case TOOLS.runtime: - return import("./aidlc-runtime.ts"); - case TOOLS.sensor: - return import("./aidlc-sensor.ts"); - case TOOLS.sensorClaimSources: - return import("./aidlc-sensor-claim-sources.ts"); - case TOOLS.sensorLinter: - return import("./aidlc-sensor-linter.ts"); - case TOOLS.sensorRequiredSections: - return import("./aidlc-sensor-required-sections.ts"); - case TOOLS.sensorTraceability: - return import("./aidlc-sensor-traceability.ts"); - case TOOLS.sensorTypeCheck: - return import("./aidlc-sensor-type-check.ts"); - case TOOLS.sensorUpstreamCoverage: - return import("./aidlc-sensor-upstream-coverage.ts"); - case TOOLS.state: - return import("./aidlc-state.ts"); - case TOOLS.unit: - return import("./aidlc-unit.ts"); - case TOOLS.swarm: - return import("./aidlc-swarm.ts"); - case TOOLS.utility: - return import("./aidlc-utility.ts"); - case TOOLS.validate: - return import("./aidlc-validate.ts"); - case TOOLS.worktree: - return import("./aidlc-worktree.ts"); - case TOOLS.workspaceSync: - return import("./aidlc-workspace-sync.ts"); - default: - return null; - } +// Issue #1070 shipped in 2.8.0-2.8.2 because reviewBrief was added to TOOLS and +// routed, but this loader's switch never got its arm. Dev mode spawns +// `bun `, so every test that ran the tool passed; only the compiled binary +// walks this table. The Record annotation makes a missing loader +// OR a delegate without `export main` a tsc error under `bun run check`. Keep +// import specifiers literal so `bun build --compile` bundles every delegate. +const DELEGATES: Record Promise> = { + "aidlc-attest.ts": () => import("./aidlc-attest.ts"), + "aidlc-audit.ts": () => import("./aidlc-audit.ts"), + "aidlc-bolt.ts": () => import("./aidlc-bolt.ts"), + "aidlc-completions.ts": () => import("./aidlc-completions.ts"), + "aidlc-doctor.ts": () => import("./aidlc-doctor.ts"), + "aidlc-graph.ts": () => import("./aidlc-graph.ts"), + "aidlc-init.ts": () => import("./aidlc-init.ts"), + "aidlc-jump.ts": () => import("./aidlc-jump.ts"), + "aidlc-knowledge.ts": () => import("./aidlc-knowledge.ts"), + "aidlc-learnings.ts": () => import("./aidlc-learnings.ts"), + "aidlc-lifecycle.ts": () => import("./aidlc-lifecycle.ts"), + "aidlc-log.ts": () => import("./aidlc-log.ts"), + "aidlc-machine-config.ts": () => import("./aidlc-machine-config.ts"), + "aidlc-orchestrate.ts": () => import("./aidlc-orchestrate.ts"), + "aidlc-plugin.ts": () => import("./aidlc-plugin.ts"), + "aidlc-review-brief.ts": () => import("./aidlc-review-brief.ts"), + "aidlc-runner-gen.ts": () => import("./aidlc-runner-gen.ts"), + "aidlc-runtime.ts": () => import("./aidlc-runtime.ts"), + "aidlc-sensor-claim-sources.ts": () => import("./aidlc-sensor-claim-sources.ts"), + "aidlc-sensor-linter.ts": () => import("./aidlc-sensor-linter.ts"), + "aidlc-sensor-required-sections.ts": () => import("./aidlc-sensor-required-sections.ts"), + "aidlc-sensor-traceability.ts": () => import("./aidlc-sensor-traceability.ts"), + "aidlc-sensor-type-check.ts": () => import("./aidlc-sensor-type-check.ts"), + "aidlc-sensor-upstream-coverage.ts": () => import("./aidlc-sensor-upstream-coverage.ts"), + "aidlc-sensor.ts": () => import("./aidlc-sensor.ts"), + "aidlc-state.ts": () => import("./aidlc-state.ts"), + "aidlc-swarm.ts": () => import("./aidlc-swarm.ts"), + "aidlc-testing-posture.ts": () => import("./aidlc-testing-posture.ts"), + "aidlc-unit.ts": () => import("./aidlc-unit.ts"), + "aidlc-utility.ts": () => import("./aidlc-utility.ts"), + "aidlc-validate.ts": () => import("./aidlc-validate.ts"), + "aidlc-workspace-sync.ts": () => import("./aidlc-workspace-sync.ts"), + "aidlc-worktree.ts": () => import("./aidlc-worktree.ts"), +}; + +function loadDelegate(tool: string): Promise | null { + return Object.hasOwn(DELEGATES, tool) ? DELEGATES[tool as ToolFile]() : null; } async function runDelegateInProcess(tool: string, args: string[]): Promise { @@ -2087,11 +2062,12 @@ async function runDelegateInProcess(tool: string, args: string[]): Promise Date: Sun, 13 Sep 2026 06:43:05 +1000 Subject: [PATCH 3/8] test: compile the native review-brief guard from the release projection --- tests/unit/t230-dispatcher-routes.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/t230-dispatcher-routes.test.ts b/tests/unit/t230-dispatcher-routes.test.ts index 328ba3177..3997a22b1 100644 --- a/tests/unit/t230-dispatcher-routes.test.ts +++ b/tests/unit/t230-dispatcher-routes.test.ts @@ -60,6 +60,7 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const BUN = process.execPath; const CORE_TOOLS_DIR = join(REPO_ROOT, "core", "tools"); const DIST_TOOLS_DIR = join(REPO_ROOT, "dist", "claude", ".claude", "tools"); +const RELEASE_TOOLS_DIR = join(REPO_ROOT, "dist-release", "claude", ".claude", "tools"); const DISPATCHER = join(CORE_TOOLS_DIR, "aidlc.ts"); type RunResult = { @@ -1613,11 +1614,12 @@ describe("t230 native review-brief dispatch", () => { const root = mkdtempSync(join(tmpdir(), "aidlc-t230-native-")); tempProjects.add(root); executable = join(root, process.platform === "win32" ? "aidlc.exe" : "aidlc"); - // Compile the actual dispatcher once; the $bunfs import fixture above - // cannot establish that the native executable reaches its bundled delegate. + // Compile the release projection that build-binaries.ts ships once; the + // $bunfs import fixture above cannot establish that the native executable + // reaches its bundled delegate. const built = spawnSync( BUN, - ["build", "--compile", join(DIST_TOOLS_DIR, "aidlc.ts"), "--outfile", executable], + ["build", "--compile", join(RELEASE_TOOLS_DIR, "aidlc.ts"), "--outfile", executable], { cwd: REPO_ROOT, encoding: "utf-8", timeout: 60_000 }, ); if (built.error) throw built.error; From ffdeaa721ac60a9360ea9428e5bf7d0baddc2a1e Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:56:35 +1000 Subject: [PATCH 4/8] fix: emit review-brief argument errors as JSON in the copy channel The compiled dispatcher and aidlc-lib die() already emit JSON errors, and the generated parity test exposed the copy-channel mismatch. --- core/tools/aidlc-review-brief.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tools/aidlc-review-brief.ts b/core/tools/aidlc-review-brief.ts index 7ab3ad4db..03e08a5fd 100644 --- a/core/tools/aidlc-review-brief.ts +++ b/core/tools/aidlc-review-brief.ts @@ -991,7 +991,7 @@ if (import.meta.main) { try { main(process.argv.slice(2)); } catch (error) { - process.stderr.write(`aidlc-review-brief: ${String(error)}\n`); + process.stderr.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`); process.exit(1); } } From 717504f71ae79815255ec3301d86ab1347481a14 Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:56:38 +1000 Subject: [PATCH 5/8] test: run every compiled delegate through the simulated native dispatcher Generate sandboxed dev/compiled parity cases from TOOLS and ROUTES, requiring every delegate to execute or carry an explicit network-policy exception. Assert delegate resolution and reject missing in-process loaders so a routed-but-unbundled tool like review-brief fails at runtime. --- tests/unit/t230-dispatcher-routes.test.ts | 73 +++++++++++++++++++---- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/tests/unit/t230-dispatcher-routes.test.ts b/tests/unit/t230-dispatcher-routes.test.ts index 3997a22b1..e8beb15f8 100644 --- a/tests/unit/t230-dispatcher-routes.test.ts +++ b/tests/unit/t230-dispatcher-routes.test.ts @@ -1469,6 +1469,43 @@ describe("t230 dispatcher global flag translation", () => { }); }); +const UNEXERCISED_DELEGATES: Partial> = { + "aidlc-doctor.ts": "The only route has networkPolicy 'interactive-bounded'.", + "aidlc-init.ts": "The only route has networkPolicy 'explicit-only'.", + "aidlc-workspace-sync.ts": "The only route has networkPolicy 'required'.", +}; + +// Generalize the former four-case list: #1070's review-brief was routed and tested +// directly, yet unreachable through the compiled dispatcher. Per-command lists +// only guard what someone remembered to list. +function compiledParityCases(): Array<{ tool: string; routeId: string; argv: string[] }> { + const cases: Array<{ tool: string; routeId: string; argv: string[] }> = []; + tools: for (const tool of Object.values(TOOLS)) { + for (const route of ROUTES) { + if ( + (route.tool !== tool && route.kind !== "custom") || + route.networkPolicy !== "forbidden" + ) continue; + + const nsPrefix = route.namespace === "public" ? [] : [route.namespace]; + const top = route.kind === "top-passthrough" || + route.kind === "top-prefix" || route.kind === "top-help"; + for (const verb of route.verbs) { + const argv = [ + ...nsPrefix, + ...(top ? [] : [route.group]), + ...(verb.startsWith("<") ? [] : verb.split(" ")), + ]; + const action = resolveAction(argv); + if (action.type !== "delegate" || action.tool !== tool) continue; + cases.push({ tool, routeId: route.id, argv }); + continue tools; + } + } + } + return cases; +} + describe("t230 dispatcher dev and compiled in-process modes", () => { test("compiled URL detection recognizes Bun virtual roots on Unix and Windows", () => { expect(isCompiledModuleUrl("file:///$bunfs/root/aidlc.ts")).toBe(true); @@ -1477,19 +1514,33 @@ describe("t230 dispatcher dev and compiled in-process modes", () => { expect(isCompiledModuleUrl("file:///workspace/core/tools/aidlc.ts")).toBe(false); }); - const cases = [ - { name: "version", args: ["version"] }, - { name: "graph artifacts", args: ["engine", "graph", "artifacts", "--help"] }, - { name: "sensor list", args: ["engine", "sensor", "list"] }, - { name: "state get", args: ["engine", "state", "get"] }, - ]; + const cases = compiledParityCases(); - for (const item of cases) { - test(`${item.name} imported compiled main matches spawned dev dispatcher`, () => { + test("every delegate is exercised or explicitly excused", () => { + expect(new Set([ + ...cases.map((item) => item.tool), + ...Object.keys(UNEXERCISED_DELEGATES), + ])).toEqual(new Set(Object.values(TOOLS))); + expect(cases.filter((item) => UNEXERCISED_DELEGATES[item.tool])).toEqual([]); + }); + + for (const { tool, routeId, argv } of cases) { + const title = `${tool} via ${routeId}: imported compiled main matches spawned dev dispatcher`; + test(title, () => { + expect(resolveAction(argv)).toMatchObject({ type: "delegate", tool }); const projectDir = makeProject(); - const dev = viaDispatcher(item.args, projectDir, { AIDLC_DISPATCH_TOOLS_DIR: DIST_TOOLS_DIR }); - const compiled = viaImportedCompiledMain(item.args, projectDir); - expectSameRun(compiled, dev, item.name); + const root = mkdtempSync(join(tmpdir(), "aidlc-t230-compiled-sandbox-")); + tempProjects.add(root); + const env = { + AIDLC_INSTALL_ROOT: join(root, "install"), + AIDLC_BIN_DIR: join(root, "bin"), + AIDLC_OFFLINE: "1", + }; + const dev = viaDispatcher(argv, projectDir, { AIDLC_DISPATCH_TOOLS_DIR: DIST_TOOLS_DIR, ...env }); + const compiled = viaImportedCompiledMain(argv, projectDir, env); + expect(compiled.stderr.toString()).not.toContain("has no in-process delegate"); + expect(compiled.stderr.toString()).not.toContain("does not export main"); + expectSameRun(compiled, dev, title); }); } From 446dd4d7275040f62de1c1324a3afba496aad214 Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:20:45 +1000 Subject: [PATCH 6/8] build: derive the bare tool rewrite list from TOOLS The hand-maintained list had drifted to 23 of 33 delegates; deriving it from TOOLS covers every bare invocation while the leftover checks still reject surviving invocations and non-route rewrites. --- scripts/package.ts | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/scripts/package.ts b/scripts/package.ts index de6ac5fa6..03b9102e2 100644 --- a/scripts/package.ts +++ b/scripts/package.ts @@ -93,7 +93,7 @@ import { TRUSTED_ROUTE_NAMESPACE, trustedCommand, } from "../core/tools/aidlc-command.ts"; -import { ROUTES } from "../core/tools/aidlc.ts"; +import { ROUTES, TOOLS } from "../core/tools/aidlc.ts"; import { AIDLC_VERSION } from "../core/tools/aidlc-version.ts"; import { BUILD_VERSION_ENV, releaseBuildVersion } from "../core/tools/aidlc-channel.ts"; import { sha256Bytes } from "../core/tools/aidlc-distribution.ts"; @@ -1096,31 +1096,13 @@ function rewriteNativeInvocations( ): void { projectNativeRootIntegrations(outRoot, m); const harnessDir = escapeRegExp(m.harnessDir); - const delegateNames = [ - "audit", - "bolt", - "graph", - "init", - "jump", - "learnings", - "lifecycle", - "log", - "orchestrate", - "runner-gen", - "runtime", - "sensor", - "sensor-claim-sources", - "sensor-linter", - "sensor-required-sections", - "sensor-type-check", - "sensor-upstream-coverage", - "state", - "swarm", - "utility", - "validate", - "worktree", - "workspace-sync", - ].join("|"); + // The hand-maintained list had drifted to 23 of 33 tools, omitting review-brief. + // Deriving it from TOOLS keeps new delegates' bare bun aidlc-.ts forms + // covered by both the rewrite and bareToolCheck. The leftover checks below + // still reject a rewrite to a non-route. + const delegateNames = Object.values(TOOLS) + .map((file) => escapeRegExp(file.slice("aidlc-".length, -".ts".length))) + .join("|"); // The authored subprocess adapters, each backed by an `aidlc engine adapter // ` dispatcher route. Only these project onto that route; any other // hook file keeps the generic one-argument `engine hook ` rewrite. From 30b5303d0170c38d66ec7f89376a90ab1832a95b Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:22:57 +1000 Subject: [PATCH 7/8] fix: emit uncaught tool errors as JSON in every copy-channel CLI wrapper Following the review-brief precedent, the remaining copy-channel wrappers emit the compiled dispatcher JSON error shape while preserving their exit semantics. --- core/tools/aidlc-init.ts | 2 +- core/tools/aidlc-lifecycle.ts | 2 +- core/tools/aidlc-orchestrate.ts | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/tools/aidlc-init.ts b/core/tools/aidlc-init.ts index c5c0f7f27..a42cb300a 100644 --- a/core/tools/aidlc-init.ts +++ b/core/tools/aidlc-init.ts @@ -6582,7 +6582,7 @@ export async function main( if (import.meta.main) { main(process.argv.slice(2)).catch((error) => { - process.stderr.write(`aidlc config: ${error instanceof Error ? error.message : String(error)}\n`); + process.stderr.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`); process.exitCode = EXIT.failure; }); } diff --git a/core/tools/aidlc-lifecycle.ts b/core/tools/aidlc-lifecycle.ts index 907592646..ea81c8ccd 100644 --- a/core/tools/aidlc-lifecycle.ts +++ b/core/tools/aidlc-lifecycle.ts @@ -2143,7 +2143,7 @@ export async function main(input: string[]): Promise { if (import.meta.main) { main(process.argv.slice(2)).catch((error) => { - process.stderr.write(`aidlc lifecycle: ${error instanceof Error ? error.message : String(error)}\n`); + process.stderr.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`); process.exitCode = EXIT.failure; }); } diff --git a/core/tools/aidlc-orchestrate.ts b/core/tools/aidlc-orchestrate.ts index 5eca76255..1dfa0dc3c 100644 --- a/core/tools/aidlc-orchestrate.ts +++ b/core/tools/aidlc-orchestrate.ts @@ -9392,9 +9392,10 @@ if (import.meta.main) { main(process.argv.slice(2)); } catch (e) { // Any uncaught read error (missing graph, malformed state) surfaces as a - // non-zero exit with the message on stderr — never a half-emitted - // directive on stdout. - console.error(`aidlc-orchestrate: ${errorMessage(e)}`); + // non-zero exit with JSON on stderr — never a half-emitted directive on + // stdout. The shape matches the compiled dispatcher when main throws + // in-process, so the copy and native channels agree. + process.stderr.write(`${JSON.stringify({ error: errorMessage(e) })}\n`); process.exit(1); } } From e1bee08b03fb015bcad11385c1ef32c55a79c0b8 Mon Sep 17 00:00:00 2001 From: Arden Packeer <2102737+apackeer@users.noreply.github.com> Date: Sun, 13 Sep 2026 10:00:32 +1000 Subject: [PATCH 8/8] test: decode wrapper errors as JSON in t299 and t312 Decode wrapper error messages before checking wizard recovery paths and session conflicts while preserving all state and audit assertions. --- tests/unit/t299-first-run-wizard.test.ts | 15 ++++++++++++--- .../unit/t312-orchestrate-session-binding.test.ts | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/unit/t299-first-run-wizard.test.ts b/tests/unit/t299-first-run-wizard.test.ts index 73fa8be11..0d157f6ba 100644 --- a/tests/unit/t299-first-run-wizard.test.ts +++ b/tests/unit/t299-first-run-wizard.test.ts @@ -178,6 +178,15 @@ function runWizard( }; } +function wizardStderrMessage(stderr: string): string { + try { + const parsed = JSON.parse(stderr) as { error?: unknown }; + return typeof parsed.error === "string" ? parsed.error : stderr; + } catch { + return stderr; + } +} + describe("t299 first-run setup wizard", () => { test("recommended defaults render detection, trichotomy, receipts, blocker, and next commands", () => { const result = runWizard("\n", { aidlc: false, runtimeIssue: true }); @@ -344,7 +353,7 @@ describe("t299 first-run setup wizard", () => { }); expect(result.status).toBe(1); expect(readFileSync(join(result.project, "aidlc.settings.json"), "utf-8")).toBe(newer); - const output = `${result.stdout}${result.stderr}`; + const output = `${result.stdout}${wizardStderrMessage(result.stderr)}`; expect(output).toContain("rollback was incomplete"); const recovery = /recovery snapshot preserved at ([^\r\n]+)/.exec(output)?.[1]; expect(recovery).toBeDefined(); @@ -394,7 +403,7 @@ describe("t299 first-run setup wizard", () => { }); expect(result.status, result.stdout + result.stderr).toBe(1); expect(readFileSync(settings, "utf-8")).toBe(newer); - const output = `${result.stdout}${result.stderr}`; + const output = `${result.stdout}${wizardStderrMessage(result.stderr)}`; expect(output).toContain("rollback was incomplete"); const recovery = /recovery snapshot preserved at ([^\r\n]+)/.exec(output)?.[1]; expect(recovery).toBeDefined(); @@ -464,7 +473,7 @@ describe("t299 first-run setup wizard", () => { timeout: 60_000, }, ); - const output = `${result.stdout}${result.stderr}`; + const output = `${result.stdout}${wizardStderrMessage(result.stderr)}`; expect(result.status, output).toBe(0); expect(output).toContain("Choice [1]:"); expect(output).not.toContain("Nothing written."); diff --git a/tests/unit/t312-orchestrate-session-binding.test.ts b/tests/unit/t312-orchestrate-session-binding.test.ts index 8dfb92a43..5026f5d09 100644 --- a/tests/unit/t312-orchestrate-session-binding.test.ts +++ b/tests/unit/t312-orchestrate-session-binding.test.ts @@ -187,7 +187,8 @@ describe("t312 orchestrate session binding", () => { }); expect(result.exitCode).not.toBe(0); - expect(result.stderr.toString()).toContain( + const diagnostic = JSON.parse(result.stderr.toString()) as { error: string }; + expect(diagnostic.error).toContain( 'Session override "session-b" conflicts with the owning conversation "session-a"', ); expect(readFileSync(statePath(firstDir), "utf-8")).toBe(firstBefore);