#!/usr/bin/env bun import { existsSync, writeSync } from "node:fs"; import { basename, dirname, isAbsolute, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { dispatcherWorkspaceUtilityArgv, launcherRouteUsesPin, parseDispatcherPluginCommand, parseDispatcherWorkspaceCommand, type RouteNamespaceName, } from "./aidlc-command.ts"; import { cmd, configureColor, dim, errorLabel, fixLabel, heading, tipLabel, } from "./aidlc-color.ts"; import { AIDLC_VERSION } from "./aidlc-version.ts"; import { aidlcInvocation, discoverProjectHarnesses, isCompiledExecutable, packagedDistributionRoot, runtimeHarnessDir, runtimeHarnessName, } from "./aidlc-runtime-paths.ts"; type Classification = "passthrough" | "translation" | "stub" | "routing-only" | "help"; type RouteKind = | "top-passthrough" | "top-prefix" | "top-stub" | "top-help" | "noun-passthrough" | "noun-map" | "custom" | "routing-only"; type HelpLine = { command: string; summary: string; }; type HelpSection = { label: string; forms: readonly string[]; }; type CustomRoute = "workspace" | "config" | "plugin" | "gen"; type RouteOnly = "hook" | "statusline" | "adapter" | "tool-passthrough"; type Visibility = "public" | "hidden" | "legacy"; type RouteNamespace = RouteNamespaceName; type ProjectRequirement = "none" | "optional" | "required"; type PinPolicy = "active" | "inspect" | "pinned"; type NetworkPolicy = "forbidden" | "explicit-only" | "interactive-bounded" | "required"; type MutationScope = | "none" | "project" | "machine" | "project-and-machine" | "user-home"; export type Route = { id: string; namespace: RouteNamespace; group: string; kind: RouteKind; classification: Classification; verbs: readonly string[]; tool?: string; prefix?: readonly string[]; targets?: Readonly>; custom?: CustomRoute; routeOnly?: RouteOnly; visibility: Visibility; projectRequirement: ProjectRequirement; pinPolicy: PinPolicy; networkPolicy: NetworkPolicy; mutationScope: MutationScope; outputModes: readonly ("human" | "quiet" | "json")[]; human?: readonly HelpLine[]; all?: readonly string[]; helpSummary?: string; helpSections?: readonly HelpSection[]; }; export type NamespaceHelpOptions = { namespace: Exclude; usage: string; header: string; excludedRouteIds?: ReadonlySet; }; const PUBLIC_ENGINE = { namespace: "engine", visibility: "hidden", projectRequirement: "required", pinPolicy: "pinned", networkPolicy: "forbidden", mutationScope: "project", outputModes: ["human", "json"], } as const; const HIDDEN_ENGINE = { ...PUBLIC_ENGINE, visibility: "hidden", } as const; type Alias = { from: string; to: string; irregular?: boolean; }; export const TOOLS = { audit: "aidlc-audit.ts", bolt: "aidlc-bolt.ts", graph: "aidlc-graph.ts", doctor: "aidlc-doctor.ts", init: "aidlc-init.ts", jump: "aidlc-jump.ts", knowledge: "aidlc-knowledge.ts", testingPosture: "aidlc-testing-posture.ts", learnings: "aidlc-learnings.ts", log: "aidlc-log.ts", lifecycle: "aidlc-lifecycle.ts", machineConfig: "aidlc-machine-config.ts", completions: "aidlc-completions.ts", orchestrate: "aidlc-orchestrate.ts", plugin: "aidlc-plugin.ts", runnerGen: "aidlc-runner-gen.ts", runtime: "aidlc-runtime.ts", reviewBrief: "aidlc-review-brief.ts", sensor: "aidlc-sensor.ts", sensorClaimSources: "aidlc-sensor-claim-sources.ts", sensorLinter: "aidlc-sensor-linter.ts", sensorRequiredSections: "aidlc-sensor-required-sections.ts", sensorTraceability: "aidlc-sensor-traceability.ts", sensorTypeCheck: "aidlc-sensor-type-check.ts", sensorUpstreamCoverage: "aidlc-sensor-upstream-coverage.ts", state: "aidlc-state.ts", swarm: "aidlc-swarm.ts", unit: "aidlc-unit.ts", utility: "aidlc-utility.ts", validate: "aidlc-validate.ts", worktree: "aidlc-worktree.ts", workspaceSync: "aidlc-workspace-sync.ts", } as const; const SENSOR_WORKERS = [ ["claim-sources", TOOLS.sensorClaimSources], ["linter", TOOLS.sensorLinter], ["required-sections", TOOLS.sensorRequiredSections], ["traceability", TOOLS.sensorTraceability], ["type-check", TOOLS.sensorTypeCheck], ["upstream-coverage", TOOLS.sensorUpstreamCoverage], ] as const; export const SLASH_FLAG_ALIASES: readonly Alias[] = [ { from: "--claim", to: "unit claim", irregular: true }, { from: "--release", to: "unit release", irregular: true }, { from: "--status", to: "status" }, { from: "--doctor", to: "doctor" }, { from: "--help", to: "help" }, { from: "--version", to: "version" }, { from: "--resume", to: "next --resume", irregular: true }, { from: "--scope", to: "next --scope", irregular: true }, { from: "config-change", to: "config set", irregular: true }, { from: "space-create", to: "space create", irregular: true }, ]; export const PUBLIC_COMMANDS = [ "config", "doctor", "version", "update", "use", "uninstall", ] as const; type PublicCommand = (typeof PUBLIC_COMMANDS)[number]; const HUMAN_TOP_ROUTE_IDS = new Set([ "top-doctor", "top-version", "top-help", "top-config", "top-update", "top-use", "top-uninstall", ]); export const ENGINE_NAMESPACE_HELP: NamespaceHelpOptions = { namespace: "engine", usage: "aidlc engine [args]", header: "Engine machinery - generated harness surfaces only; not for human scripts:", excludedRouteIds: HUMAN_TOP_ROUTE_IDS, }; export const SYSTEM_NAMESPACE_HELP: NamespaceHelpOptions = { namespace: "system", usage: "aidlc system [args]", header: "Operations on this user's aidlc installation; never a system-wide or root install:", }; // ROUTES_TABLE_START export const ROUTES: readonly Route[] = [ { id: "top-orchestrate", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["next", "continue", "report", "park", "team-board"], tool: TOOLS.orchestrate, ...PUBLIC_ENGINE, namespace: "public", human: [ { command: "next [args]", summary: "run the next orchestrator action" }, { command: "report [args]", summary: "render the orchestrator report" }, { command: "park [args]", summary: "park the current workflow" }, ], all: [ "next [args]", "continue ", "report [args]", "park [args]", "team-board [--snapshot]", ], }, { id: "unit", group: "unit", kind: "noun-passthrough", classification: "passthrough", verbs: [ "adopt", "claim", "gate", "land", "merge-status", "participate", "pin", "publish", "release", "status", ], tool: TOOLS.unit, ...PUBLIC_ENGINE, namespace: "public", mutationScope: "project", all: [ "unit adopt ", "unit claim ", "unit gate --decision --user-input ", "unit land [--step git|state|audit|all] [--target ] [--accept-released-attempt --user-input ]", "unit merge-status ", "unit participate", "unit pin ", "unit publish ", "unit release ", "unit status", ], }, { id: "top-compose", group: "top", kind: "top-prefix", classification: "translation", verbs: ["compose"], tool: TOOLS.orchestrate, prefix: ["next", "compose"], ...PUBLIC_ENGINE, namespace: "public", human: [{ command: "compose [args]", summary: "start composition through orchestrate next compose" }], all: ["compose [args]"], }, { id: "top-status", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["status"], tool: TOOLS.utility, ...PUBLIC_ENGINE, namespace: "engine", mutationScope: "none", human: [ { command: "status [args]", summary: "show the current AIDLC status" }, ], all: ["status [args]"], }, { id: "top-recompose", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["recompose"], tool: TOOLS.utility, ...PUBLIC_ENGINE, namespace: "engine", human: [ { command: "recompose [args]", summary: "rerun composition through the utility handler" }, ], all: ["recompose [args]"], }, { id: "top-doctor", namespace: "public", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["doctor"], tool: TOOLS.doctor, visibility: "public", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "interactive-bounded", mutationScope: "project-and-machine", outputModes: ["human", "quiet", "json"], human: [ { command: "doctor [--check-updates]", summary: "run environment diagnostics" }, ], all: [ "doctor [--verbose] [--json] [--quiet] [--check-updates] [--release-base-url ] [--ca-bundle ] [--offline] [--export] [--output ]", ], }, { id: "top-version", namespace: "public", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["version"], tool: TOOLS.utility, visibility: "public", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "none", outputModes: ["human", "json"], human: [{ command: "version [--json]", summary: "print the installed AIDLC version" }], all: ["version [--json]"], }, { id: "top-help", namespace: "public", group: "top", kind: "top-help", classification: "help", verbs: ["help"], visibility: "public", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "none", outputModes: ["human"], all: ["help [--all]"], }, { id: "top-config", namespace: "public", group: "top", kind: "top-passthrough", classification: "translation", verbs: ["config"], tool: TOOLS.init, visibility: "public", projectRequirement: "none", pinPolicy: "active", networkPolicy: "explicit-only", mutationScope: "project-and-machine", outputModes: ["human", "quiet", "json"], human: [{ command: "config [args]", summary: "configure, pin, or refresh this project" }], all: [ "config [--harness ] [--from ] [--mcp ] [--pin |--unpin] [--dry-run] [--yes] [--json] [--quiet] [--force] [--plan-token ] [--project-dir ]", "config models [--show [--json]|--check|--reset|--preset |--from --save-as ] [--local|--project|--global]", "config models [--deciding-effort ] [--reviewing-effort ] [--writing-up-effort ] [--agent --effort [--model ]] [--local|--project|--global] [--dry-run] [--yes]", "config runtime [--show [--json]|--check|--record-paths|--reset] [--dry-run] [--yes]", "config providers [--show [--json]|--check|--reset|--provider ] [--region ] [--profile ] [--opencode-default ] [--acknowledge] [--mark-done ] [--dry-run] [--yes]", "config trust [--show [--json]|--check|--acknowledge|--reset] [--dry-run] [--yes]", "config flags [--show [--json]|--check|--reset] [--default-scope ] [--swarm ] [--hook-debug ] [--sensor-timeout-ms ] [--bypass ] [--clear-bypass ] [--local|--project|--global] [--dry-run] [--yes]", "config project [--show [--json]|--check|--reset] [--plugins ] [--mcp ] [--completions ] [--dry-run] [--yes]", "config --pin [--from ] [--release-base-url ] [--ca-bundle ] [--offline]", "config --unpin", "config --channel [stable|preview]", ], }, { id: "top-update", namespace: "public", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["update"], tool: TOOLS.lifecycle, visibility: "public", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "explicit-only", mutationScope: "machine", outputModes: ["human", "quiet", "json"], human: [ { command: "update [args]", summary: "install and activate a framework release" }, ], all: [ "update [--version ] [--channel ] [--from ] [--release-base-url ] [--release-api-url ] [--ca-bundle ] [--offline] [--check|--dry-run] [--json|--quiet]", ], }, { id: "top-rollback", namespace: "system", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["rollback"], tool: TOOLS.lifecycle, visibility: "hidden", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "machine", outputModes: ["human", "quiet", "json"], all: ["rollback [--version |--list]"], }, { id: "top-use", namespace: "public", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["use"], tool: TOOLS.lifecycle, visibility: "public", projectRequirement: "none", pinPolicy: "active", networkPolicy: "explicit-only", mutationScope: "machine", outputModes: ["human", "quiet", "json"], human: [{ command: "use ", summary: "select an exact machine release" }], all: [ "use [--from ] [--release-base-url ] [--ca-bundle ] [--offline] [--json|--quiet]", ], }, { id: "top-uninstall", namespace: "public", group: "top", kind: "top-passthrough", classification: "passthrough", verbs: ["uninstall"], tool: TOOLS.lifecycle, visibility: "public", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "machine", outputModes: ["human", "quiet", "json"], human: [{ command: "uninstall [--purge]", summary: "remove the machine installation" }], all: ["uninstall [--purge] [--yes] [--json|--quiet]"], }, { id: "top-completions", namespace: "system", group: "completions", kind: "routing-only", classification: "routing-only", verbs: [""], tool: TOOLS.completions, routeOnly: "tool-passthrough", visibility: "hidden", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "none", outputModes: ["human"], all: ["completions "], }, { id: "versions-list", namespace: "system", group: "versions", kind: "noun-passthrough", classification: "passthrough", verbs: ["list"], tool: TOOLS.lifecycle, prefix: ["versions"], visibility: "hidden", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "none", outputModes: ["human", "quiet", "json"], human: [{ command: "versions list", summary: "inspect retained releases" }], all: ["list"], }, { id: "versions-install", namespace: "system", group: "versions", kind: "noun-passthrough", classification: "passthrough", verbs: ["install"], tool: TOOLS.lifecycle, prefix: ["versions"], visibility: "hidden", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "explicit-only", mutationScope: "machine", outputModes: ["human", "quiet", "json"], human: [{ command: "versions install", summary: "install a retained release" }], all: [ "install [--from ] [--release-base-url ] [--ca-bundle ]", ], }, { id: "versions-prune", namespace: "system", group: "versions", kind: "noun-passthrough", classification: "passthrough", verbs: ["prune"], tool: TOOLS.lifecycle, prefix: ["versions"], visibility: "hidden", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "machine", outputModes: ["human", "quiet", "json"], all: ["prune [--yes]"], }, { id: "state-passthrough", group: "state", kind: "noun-passthrough", classification: "passthrough", tool: TOOLS.state, ...HIDDEN_ENGINE, verbs: [ "get", "set", "set-skeleton-stance", "set-construction-iteration", "checkbox", "count", "advance", "finalize", "complete-workflow", "gate-start", "approve", "reject", "revise", "skip", "resume", "acknowledge-compaction", "reuse-artifact", "lookup", "practices-event", "practices-promote", "set-unit-ownership", "set-unit-gate-rhythm", "fork", "merge", "park", "unpark", "unit", ], helpSections: [ { label: "records", forms: [ "get", "set", "set-status", "set-skeleton-stance", "set-construction-iteration", "checkbox", "count", "lookup", ], }, { label: "lifecycle", forms: [ "init", "advance", "finalize", "complete-workflow", "skip", "resume", "acknowledge-compaction", "reuse-artifact", ], }, { label: "gates", forms: ["gate-start", "approve", "reject", "revise"], }, { label: "practices", forms: ["practices-event", "practices-promote"], }, { label: "fork/merge", forms: ["fork", "merge"], }, { label: "parking", forms: ["park", "unpark"], }, ], }, { id: "state-utility", group: "state", kind: "noun-map", classification: "translation", verbs: ["set-status", "init"], tool: TOOLS.utility, ...HIDDEN_ENGINE, targets: { "set-status": "set-status", init: "state-init" }, }, { id: "audit-passthrough", group: "audit", kind: "noun-passthrough", classification: "passthrough", verbs: ["append", "append-batch", "append-raw"], tool: TOOLS.audit, ...HIDDEN_ENGINE, }, { id: "audit-renames", group: "audit", kind: "noun-map", classification: "translation", verbs: ["fork", "merge"], tool: TOOLS.audit, ...HIDDEN_ENGINE, targets: { fork: "audit-fork", merge: "audit-merge" }, }, { id: "graph", group: "graph", kind: "noun-passthrough", classification: "passthrough", verbs: [ "artifacts", "producers", "consumers", "topo", "cycles", "scope", "validate-scope", "validate-grid", "ars", "compile", "resolve", "export", ], tool: TOOLS.graph, ...HIDDEN_ENGINE, }, { id: "runtime", group: "runtime", kind: "noun-passthrough", classification: "passthrough", verbs: ["compile", "read", "summary", "fragment-fork", "fragment-merge"], tool: TOOLS.runtime, ...HIDDEN_ENGINE, }, { id: "sensor", group: "sensor", kind: "noun-passthrough", classification: "passthrough", verbs: ["list", "describe", "fire"], tool: TOOLS.sensor, ...HIDDEN_ENGINE, }, { id: "swarm", group: "swarm", kind: "noun-passthrough", classification: "passthrough", verbs: ["prepare", "check", "finalize"], tool: TOOLS.swarm, ...HIDDEN_ENGINE, }, { id: "bolt", group: "bolt", kind: "noun-passthrough", classification: "passthrough", verbs: ["start", "complete", "fail", "abort", "set-autonomy", "dispatch-event", "hold-merge", "release-merge"], tool: TOOLS.bolt, ...HIDDEN_ENGINE, }, { id: "worktree", group: "worktree", kind: "noun-passthrough", classification: "passthrough", verbs: ["create", "merge", "discard", "list", "verify", "info"], tool: TOOLS.worktree, ...HIDDEN_ENGINE, }, { id: "jump", group: "jump", kind: "noun-passthrough", classification: "passthrough", verbs: ["resolve", "execute"], tool: TOOLS.jump, ...HIDDEN_ENGINE, }, { id: "log", group: "log", kind: "noun-passthrough", classification: "passthrough", verbs: ["decision", "answer", "review", "link"], tool: TOOLS.log, ...HIDDEN_ENGINE, }, { id: "learnings", group: "learnings", kind: "noun-passthrough", classification: "passthrough", verbs: ["surface", "persist"], tool: TOOLS.learnings, ...HIDDEN_ENGINE, }, { id: "testing-posture", group: "testing-posture", kind: "noun-passthrough", classification: "passthrough", verbs: ["resolve", "render", "fingerprint", "verify", "begin", "brief"], tool: TOOLS.testingPosture, ...HIDDEN_ENGINE, }, { id: "validate", group: "validate", kind: "noun-passthrough", classification: "passthrough", verbs: ["outputs"], tool: TOOLS.validate, ...HIDDEN_ENGINE, }, { id: "intent", group: "intent", kind: "custom", classification: "translation", verbs: ["list", "switch", "", "create"], custom: "workspace", ...PUBLIC_ENGINE, human: [{ command: "intent [list|switch|create]", summary: "list, switch, or create intent context" }], all: ["list [--json]", "switch ", "", "create [args]"], }, { id: "space", group: "space", kind: "custom", classification: "translation", verbs: ["list", "switch", "", "create"], custom: "workspace", ...PUBLIC_ENGINE, human: [{ command: "space [list|switch|create]", summary: "list, switch, or create a space" }], all: ["list", "switch ", "", "create "], }, { id: "scope", group: "scope", kind: "noun-map", classification: "translation", verbs: ["change", "detect", "resolve-env"], tool: TOOLS.utility, ...PUBLIC_ENGINE, targets: { change: "scope-change", detect: "detect-scope", "resolve-env": "resolve-env-scope", }, }, { id: "config-global", namespace: "system", group: "config", kind: "noun-passthrough", classification: "passthrough", verbs: ["global"], tool: TOOLS.machineConfig, visibility: "hidden", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "machine", outputModes: ["human", "quiet", "json"], all: ["global "], }, { id: "config", group: "config", kind: "custom", classification: "translation", verbs: ["set depth", "set test-strategy", "set review", "set change-control", "get", "list"], custom: "config", ...PUBLIC_ENGINE, visibility: "hidden", targets: { "set depth": "config-change", "set test-strategy": "config-change", "set review": "config-change", "set change-control": "change-control", get: "config-get", list: "config-list", }, human: [ { command: "config get ", summary: "print supported project configuration" }, { command: "config set ", summary: "change supported project configuration" }, { command: "config list", summary: "list supported project configuration" }, ], all: ["set depth ", "set test-strategy ", "set review ", "set change-control ", "get ", "list"], }, { id: "plugin", group: "plugin", kind: "custom", classification: "translation", verbs: ["select", "sync", "list", "validate", "build"], custom: "plugin", ...PUBLIC_ENGINE, visibility: "hidden", targets: { select: "select-plugins", sync: "sync", list: "list", validate: "plugin-validate", build: "plugin-build", }, human: [ { command: "plugin select [names]", summary: "set enabled plugins" }, { command: "plugin list [--verbose|--json]", summary: "compare installed and composed plugins" }, { command: "plugin sync [--prune-missing]", summary: "transactionally compose installed plugins" }, { command: "plugin validate [path]", summary: "validate an authored plugin" }, { command: "plugin build [outDir]", summary: "build an authored plugin for a harness" }, ], all: ["select [names]", "sync [--prune-missing] [--yes]", "list [--verbose] [--json]", "validate [path]", "build [outDir]"], }, { // The plugin AUTHORING surface (v2): `aidlc plugin validate` / `aidlc plugin // build` run against a plugin checkout, which is usually NOT an installed // project, so this route is public, unpinned, and carries no project // requirement. The installed-plugin lifecycle (select/sync/list) stays on // the engine `plugin` noun above. id: "plugin-author", group: "plugin", kind: "noun-map", classification: "translation", verbs: ["validate", "build"], tool: TOOLS.utility, targets: { validate: "plugin-validate", build: "plugin-build" }, namespace: "public", visibility: "hidden", projectRequirement: "none", pinPolicy: "active", networkPolicy: "forbidden", mutationScope: "none", outputModes: ["human", "json"], all: ["validate [path]", "build [outDir] [--plugin-root ]"], }, { // The DocumentKB noun. Unlike `plugin`, the verb IS the subcommand -- these // verbs live in their own tool -- so there is no `targets` translation table // to keep in step. Only the verbs the tool actually implements are listed: // registering a verb the tool would reject turns a clean "unknown verb" // error into a confusing one from a layer down. id: "knowledge", group: "knowledge", // `noun-passthrough`, NOT `top-passthrough`: this route's group is // "knowledge", and the two resolvers split on group. `resolveTop` only // iterates `group === "top"` routes, so it never saw this one; `resolveNoun` // did see it but handles only `noun-passthrough`/`noun-map`/`custom`/ // `routing-only`, so it fell through to "unknown verb". The result was that // NO knowledge verb ran through the compiled dispatcher while the tool // itself worked perfectly when invoked directly -- which is why the defect // survived a review round in which it was reported, claimed fixed, and never // executed. The dispatcher test below runs every verb rather than asserting // this literal, because reading the route is exactly what missed it. kind: "noun-passthrough", classification: "passthrough", verbs: ["onboard", "sync", "list", "show", "associate", "dissociate", "rebind", "summarize"], tool: TOOLS.knowledge, ...PUBLIC_ENGINE, // ONE line in the human help, which is capped at 20 lines: it is a summary // for a person deciding what to type, not the surface. Every verb still // appears in `help --all` via `all` below. human: [ { command: "knowledge ", summary: "index and read customer documents" }, ], all: [ "onboard [path]", "sync", "list", "show ", "associate --intent [slug]", "dissociate --intent [slug]", "rebind --to ", "summarize --text-file --source-revision ", ], }, { id: "gen", group: "gen", kind: "custom", classification: "translation", verbs: ["runners", "runners --check", "runner-list", "runner-scopes", "stage-table", "scope-table"], custom: "gen", tool: TOOLS.runnerGen, ...HIDDEN_ENGINE, all: ["runners [args]", "runners --check", "runner-list", "runner-scopes", "stage-table [args]", "scope-table [args]"], }, { id: "workspace", group: "workspace", kind: "noun-map", classification: "translation", verbs: [ "detect", "codekb", "codekb-scope-diff", "codekb-snapshot", "codekb-publish", "project-description", "document-input", ], tool: TOOLS.utility, ...HIDDEN_ENGINE, targets: { detect: "detect", codekb: "codekb-path", "codekb-scope-diff": "codekb-scope-diff", "codekb-snapshot": "codekb-snapshot", "codekb-publish": "codekb-publish", "project-description": "project-description", "document-input": "document-input", }, }, { id: "review-brief", group: "review-brief", kind: "noun-passthrough", classification: "passthrough", verbs: ["review", "context", "summary"], tool: TOOLS.reviewBrief, ...HIDDEN_ENGINE, }, { id: "workspace-sync", namespace: "system", group: "workspace-sync", kind: "routing-only", classification: "routing-only", verbs: [""], tool: TOOLS.workspaceSync, routeOnly: "tool-passthrough", visibility: "hidden", projectRequirement: "required", pinPolicy: "pinned", networkPolicy: "required", mutationScope: "project", outputModes: ["human", "quiet", "json"], all: ["[--force] [--project-dir ]"], }, { id: "engine-orchestrate", group: "orchestrate", kind: "noun-passthrough", classification: "passthrough", verbs: ["next", "continue", "report", "park"], tool: TOOLS.orchestrate, ...HIDDEN_ENGINE, all: ["next [args]", "continue ", "report [args]", "park [args]"], }, { id: "engine-orchestrate-help", group: "orchestrate", kind: "noun-map", classification: "translation", verbs: ["help"], tool: TOOLS.utility, targets: { help: "help" }, ...HIDDEN_ENGINE, all: ["help"], }, ...SENSOR_WORKERS.map(([sensorId, tool]): Route => { const group = `sensor-${sensorId}`; return { id: `engine-${group}`, namespace: "engine", group, kind: "routing-only", classification: "routing-only", verbs: [""], tool, routeOnly: "tool-passthrough", visibility: "hidden", projectRequirement: "required", pinPolicy: "pinned", networkPolicy: "forbidden", mutationScope: "project", outputModes: ["human", "quiet", "json"], }; }), { id: "system-lifecycle", namespace: "system", group: "lifecycle", kind: "routing-only", classification: "routing-only", verbs: [""], tool: TOOLS.lifecycle, routeOnly: "tool-passthrough", visibility: "hidden", projectRequirement: "optional", pinPolicy: "active", networkPolicy: "explicit-only", mutationScope: "machine", outputModes: ["human", "quiet", "json"], all: [ "install-apply --from --version [args]", "install-profile --profile --bin-dir [args]", ], }, { id: "hook", group: "hook", kind: "routing-only", classification: "routing-only", verbs: [""], routeOnly: "hook", ...HIDDEN_ENGINE, all: [""], }, { id: "statusline", group: "top", kind: "routing-only", classification: "routing-only", verbs: ["statusline"], routeOnly: "statusline", ...HIDDEN_ENGINE, all: ["statusline"], }, { id: "adapter", group: "top", kind: "routing-only", classification: "routing-only", verbs: ["adapter"], routeOnly: "adapter", ...HIDDEN_ENGINE, all: ["adapter [args]"], }, ]; // ROUTES_TABLE_END export type Action = | { type: "delegate"; tool: string; args: string[] } | { type: "hook"; name: string; path: string; projectDir?: string } | { type: "statusline"; path: string; projectDir?: string } | { type: "adapter"; harness: AdapterHarness; target: string; extraArgs: string[]; path: string; projectDir?: string } | { type: "sensor-script-file"; id: string; args: string[]; projectDir?: string } | { type: "version"; json: boolean } | { type: "stub"; message: string; code: number } | { type: "help"; scope: "human" | "engine" | "system" | "all" } | { type: "error"; message: string; humanMessage?: string; code: number }; function text(fd: number, value: string | Uint8Array): void { if (typeof value === "string") { writeSync(fd, value); return; } writeSync(fd, value); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function dispatcherDir(): string { return dirname(fileURLToPath(import.meta.url)); } function toolsDir(): string { // Test seam for parity suites that need one dispatcher copy to delegate to a // different generated tools tree. Production resolves sibling tools beside // this dispatcher. const fromEnv = process.env.AIDLC_DISPATCH_TOOLS_DIR; if (fromEnv) return isAbsolute(fromEnv) ? fromEnv : resolve(process.cwd(), fromEnv); return dispatcherDir(); } type AdapterHarness = "codex" | "copilot" | "cursor" | "kiro" | "kiro-ide"; const ADAPTER_HARNESS_LEAF: Record = { codex: ".codex", copilot: ".aidlc", cursor: ".cursor", kiro: ".kiro", "kiro-ide": ".kiro", }; function isAdapterHarness(value: string): value is AdapterHarness { return Object.hasOwn(ADAPTER_HARNESS_LEAF, value); } function adapterFile(harness: AdapterHarness): string { if (harness === "codex") return "aidlc-codex-adapter.ts"; if (harness === "copilot") return "aidlc-copilot-adapter.ts"; if (harness === "cursor") return "aidlc-cursor-adapter.ts"; return "aidlc-kiro-adapter.ts"; } function resolveHookPath( file: string, harness?: AdapterHarness, projectDir = process.cwd(), ): string { const moduleRelative = join(dispatcherDir(), "..", "hooks", file); const runtimeLeaf = harness ? ADAPTER_HARNESS_LEAF[harness] : runtimeHarnessDir(projectDir); const leaves = harness ? [ADAPTER_HARNESS_LEAF[harness]] : [ runtimeLeaf, ...discoverProjectHarnesses(projectDir).map((candidate) => candidate.harnessDir), ".claude", ".kiro", ".codex", ".cursor", ].filter((value, index, values): value is string => typeof value === "string" && value.length > 0 && values.indexOf(value) === index ); const installed = leaves.map((leaf) => join(projectDir, leaf, "hooks", file)); const executableRelative = join( packagedDistributionRoot(runtimeLeaf, harness), runtimeLeaf, "hooks", file, ); const candidates = [moduleRelative, ...installed, executableRelative]; return candidates.find((candidate) => existsSync(candidate)) ?? moduleRelative; } function routeForms(route: Route): string[] { if (route.all) return [...route.all]; return [...route.verbs]; } export function listRoutes(): readonly Route[] { return ROUTES; } export function renderHumanHelp(): string { const invoke = aidlcInvocation(); const out = process.stdout; const row = (name: string, summary: string): string => ` ${cmd(name, out)}${" ".repeat(12 - name.length)}${summary}`; return [ "aidlc - structured AI-driven development for your coding agent", "", heading("USAGE", out), ` ${cmd(`${invoke} [flags]`, out)}`, "", heading("SET UP A PROJECT", out), row("config", "Set up, refresh, or pin this project (run bare for the guided setup)"), row("doctor", "Check this machine and project, with fixes for anything wrong"), "", heading("MANAGE THE MACHINE INSTALL", out), row("update", "Install and switch to a newer release"), row("use", "Switch to an exact installed release"), row("version", "Print binary and runtime versions"), row("uninstall", "Remove aidlc from this machine"), "", heading("EXAMPLES", out), ` ${cmd(`${invoke} config`, out)} guided setup in the current project`, ` ${cmd(`${invoke} config models --show`, out)} see which model each agent uses, and why`, ` ${cmd(`${invoke} doctor`, out)} find out why something isn't working`, "", heading("LEARN MORE", out), dim(` Use '${invoke} --help' for more about a command.`, out), dim(" Docs: https://awslabs.github.io/aidlc-workflows/", out), dim(` Agents: workflows run through '${invoke} engine' - see '${invoke} engine --help'.`, out), "", ].join("\n"); } const COMMAND_HELP_USAGE: Record = { config: "aidlc config [options]", doctor: "aidlc doctor [options]", version: "aidlc version [--json]", update: "aidlc update [options]", use: "aidlc use [options]", uninstall: "aidlc uninstall [options]", }; const ROOT_CONFIG_HELP_VALUE_FLAGS = new Set([ "--ca-bundle", "--channel", "--from", "--harness", "--mcp", "--pin", "--plan-token", "--project-dir", "--release-base-url", ]); function publicCommandFromToken(token: string | undefined): PublicCommand | null { if ((PUBLIC_COMMANDS as readonly (string | undefined)[]).includes(token)) { return token as PublicCommand; } if (token === "--doctor") return "doctor"; if (token === "--version") return "version"; return null; } export function renderCommandHelp(command: PublicCommand): string { const route = ROUTES.find((candidate) => candidate.namespace === "public" && candidate.group === "top" && candidate.verbs.includes(command) ); if (!route) throw new Error(`dispatcher route registry is missing public command ${command}`); const invoke = aidlcInvocation(); const out = process.stdout; if (command === "config") { const sectionRow = (name: string, summary: string): string => ` ${cmd(name, out)}${" ".repeat(12 - name.length)}${summary}`; return [ "Set up, refresh, or change AI-DLC project configuration", "", heading("USAGE", out), ` ${cmd(`${invoke} config`, out)}`, ` ${cmd(`${invoke} config
[flags]`, out)}`, "", heading("SECTIONS", out), sectionRow("models", "Which model and effort each agent uses (presets: thorough, balanced, minimal)"), sectionRow("runtime", "Whether hooks can find bun, aidlc, and the selected harness"), sectionRow("providers", "Provider, AWS region/profile, and manual provider actions"), sectionRow("trust", "Host trust and command allowlist acknowledgement"), sectionRow("flags", "Default scope, swarm, hook debug, sensor timeout, and bypasses"), sectionRow("project", "Plugins, MCP servers, and shell completions"), "", heading("COMMON FLAGS", out), " --pin Pin this project to an installed release", " --channel [name] Show or set the machine release channel (stable, preview)", " --show Show the selected section without changing it", " --dry-run Print the transaction plan without writing", " --yes Confirm explicit choices; it never chooses values", "", heading("EXAMPLES", out), ` ${cmd(`${invoke} config`, out)}`, ` ${cmd(`${invoke} config models --show`, out)}`, ` ${cmd(`${invoke} config models --preset thorough --project --yes`, out)}`, "", dim("Every interactive question has an equivalent flag for non-interactive use.", out), dim(`Full flag reference: ${invoke} config
--help`, out), "", ].join("\n"); } const descriptions: Record, string> = { doctor: "Check this machine and project and show a fix for every problem", version: "Print binary and selected runtime versions", update: "Install and switch to a newer aidlc release", use: "Switch to an exact installed aidlc release", uninstall: "Remove aidlc from this machine", }; const examples: Partial, string[]>> = { doctor: [` ${invoke} doctor`, ` ${invoke} doctor --verbose`], update: [ ` ${invoke} update --check`, ` ${invoke} update --dry-run`, ` ${invoke} update --channel preview`, ], use: [` ${invoke} use 2.6.2`], uninstall: [` ${invoke} uninstall`, ` ${invoke} uninstall --purge`], }; return [ descriptions[command], "", heading("USAGE", out), ` ${cmd(`${invoke} ${COMMAND_HELP_USAGE[command].replace(/^aidlc\s+/, "")}`, out)}`, ...(examples[command]?.length ? [ "", heading("EXAMPLES", out), ...(examples[command] ?? []).map((line) => ` ${cmd(line.trimStart(), out)}` ), ] : []), "", dim(`Run '${invoke} --help' for the public command list.`, out), "", ].join("\n"); } function commandHelpRequest(argv: readonly string[]): PublicCommand | null { const clean = withoutProjectDirFlag(argv); const invocation = clean[0]; const command = publicCommandFromToken(invocation); const delimiter = clean.indexOf("--"); const commandArgs = delimiter < 0 ? clean : clean.slice(0, delimiter); if ( !command || !commandArgs.slice(1).some((token) => token === "--help" || token === "-h") ) { return null; } if (command === "config") { const normalized = normalizePublicCommandArgv(argv, command, invocation); for (let index = 1; index < normalized.length; index++) { const token = normalized[index]; if (ROOT_CONFIG_HELP_VALUE_FLAGS.has(token)) { const value = normalized[index + 1]; if (value && !value.startsWith("-")) index++; continue; } if (token === "--help" || token === "-h") return command; if (!token.startsWith("-")) return null; } return null; } return command; } function stripHelpGroupPrefix(group: string, form: string): string { if (form === group) return ""; return form.startsWith(`${group} `) ? form.slice(group.length + 1) : form; } export function renderNamespaceHelp( options: NamespaceHelpOptions, summaryOverrides: ReadonlyMap = new Map(), ): string { const lines: string[] = [options.usage, "", options.header]; const grouped = new Map(); const summaries = new Map(); const sections = new Map(); for (const route of ROUTES) { if ( route.namespace !== options.namespace || options.excludedRouteIds?.has(route.id) ) { continue; } const group = route.group === "top" ? route.id.replace(/^top-/, "") : route.group; grouped.set(group, [...(grouped.get(group) ?? []), ...routeForms(route)]); const summary = summaryOverrides.get(group) ?? route.helpSummary; if (summary) summaries.set(group, summary); if (route.helpSections) sections.set(group, route.helpSections); } for (const [group, forms] of grouped) { const summary = summaries.get(group); if (summary) { lines.push(` ${group}: ${summary}`); continue; } const helpSections = sections.get(group); if (helpSections) { lines.push(` ${group}:`); for (const section of helpSections) { lines.push(` ${section.label}: ${section.forms.join(", ")}`); } continue; } const rendered = forms.map((form) => stripHelpGroupPrefix(group, form)); if (rendered.length === 1 && rendered[0] === "") { lines.push(` ${group}`); continue; } lines.push(` ${group}: ${rendered.filter(Boolean).join(", ")}`); } return `${lines.join("\n")}\n`; } export async function renderEngineHelp(): Promise { const { sensorHelpSummaries } = await import("./aidlc-sensor.ts"); return renderNamespaceHelp(ENGINE_NAMESPACE_HELP, sensorHelpSummaries()); } export function renderAllHelp(): string { return [ renderHumanHelp().trimEnd(), "", "Hidden namespaces:", " engine Generated harness surfaces only; not for human scripts. Run: aidlc engine --help", " system This user's installation; unsupported interface. Run: aidlc system --help", "", ].join("\n"); } function topLevelError(command: string): Action { return { type: "error", code: 2, message: `aidlc: unknown command or noun '${command}'; try 'aidlc --help'\n`, }; } function editDistance(left: string, right: string): number { const row = Array.from({ length: right.length + 1 }, (_, index) => index); for (let i = 1; i <= left.length; i++) { let diagonal = row[0]; row[0] = i; for (let j = 1; j <= right.length; j++) { const prior = row[j]; row[j] = Math.min( row[j] + 1, row[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1), ); diagonal = prior; } } return row[right.length]; } function publicCommandError(command: string): Action { const invoke = aidlcInvocation(); const nearest = [...PUBLIC_COMMANDS] .map((candidate) => ({ candidate, distance: editDistance(command, candidate) })) .sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate) )[0]; const tip = nearest && nearest.distance <= 2 ? `\n tip: did you mean '${nearest.candidate}'?\n` : ""; return { type: "error", code: 2, message: `aidlc: unknown command or noun '${command}'; try 'aidlc --help'\n`, humanMessage: `${errorLabel("error:", process.stderr)} unknown command '${command}'\n${ tip ? `\n ${tipLabel("tip:", process.stderr)} did you mean '${nearest?.candidate}'?\n` : "" }\n` + `${heading("usage:", process.stderr)} ${invoke} [flags]\n` + `For the full list, run '${invoke} --help'.\n`, }; } function nounError(noun: string, verb: string | undefined): Action { const detail = verb ? `unknown verb '${verb}'` : "missing verb"; return { type: "error", code: 2, message: `aidlc: ${detail} for engine noun '${noun}'; try 'aidlc engine --help'\n`, }; } function requireValue(noun: string, verb: string, value: string | undefined): Action | undefined { if (value) return undefined; return nounError(noun, verb); } function isSafeName(value: string): boolean { return /^[a-z0-9][a-z0-9-]*$/i.test(value); } // TRANSLATION_LOGIC_START function handleWorkspace(argv: string[]): Action { const command = parseDispatcherWorkspaceCommand(argv); if (command.kind === "not-workspace") return nounError(argv[0], argv[1]); if (command.kind === "error") { return { type: "error", code: 2, message: `${command.message}\n` }; } const utilityArgv = dispatcherWorkspaceUtilityArgv(command); if (utilityArgv === null) return nounError(argv[0], argv[1]); return { type: "delegate", tool: TOOLS.utility, args: utilityArgv }; } function handleConfig(route: Route, argv: string[]): Action { const verb = argv[1]; if (verb === "get" || verb === "list") { const target = route.targets?.[verb]; if (target) return { type: "delegate", tool: TOOLS.utility, args: [target, ...argv.slice(2)] }; } if (verb !== "set") return nounError("config", verb); const key = argv[2]; const value = argv[3]; if (key === "depth") { const missing = requireValue("config", "set depth", value); if (missing) return missing; return { type: "delegate", tool: TOOLS.utility, args: ["config-change", "--depth", value, ...argv.slice(4)] }; } if (key === "test-strategy") { const missing = requireValue("config", "set test-strategy", value); if (missing) return missing; return { type: "delegate", tool: TOOLS.utility, args: ["config-change", "--test-strategy", value, ...argv.slice(4)] }; } if (key === "review") { const missing = requireValue("config", "set review", value); if (missing) return missing; return { type: "delegate", tool: TOOLS.utility, args: ["config-change", "--review", value, ...argv.slice(4)] }; } if (key === "change-control") { // The per-intent Change Control flip is its own utility verb (it rewrites // the state line and logs CHANGE_CONTROL_SET), not a config-change field. const missing = requireValue("config", "set change-control", value); if (missing) return missing; return { type: "delegate", tool: TOOLS.utility, args: ["change-control", value, ...argv.slice(4)] }; } return nounError("config", key ? `set ${key}` : "set"); } function handlePlugin(argv: string[]): Action { const command = parseDispatcherPluginCommand(argv); if (command.kind === "help") { return { type: "help", scope: "engine" }; } if (command.kind === "error") { return { type: "error", code: 1, message: `${command.message}\n` }; } if (command.kind === "run") { if (argv[1] === "list" || argv[1] === "sync") { return { type: "delegate", tool: TOOLS.plugin, args: [argv[1], ...argv.slice(2)] }; } return { type: "delegate", tool: TOOLS.utility, args: command.argv }; } return nounError("plugin", argv[1]); } function handleGen(argv: string[]): Action { const verb = argv[1]; if (verb === "runners") { // Keep --check as a flag-shaped route. Other runner flags are passed // through unchanged, including Windows callers that avoid shell redirects. if (argv[2] === "--check") { return { type: "delegate", tool: TOOLS.runnerGen, args: ["check", ...argv.slice(3)] }; } return { type: "delegate", tool: TOOLS.runnerGen, args: ["write", ...argv.slice(2)] }; } if (verb === "runner-list") { return { type: "delegate", tool: TOOLS.runnerGen, args: ["list", ...argv.slice(2)] }; } if (verb === "runner-scopes") { return { type: "delegate", tool: TOOLS.runnerGen, args: ["scopes", ...argv.slice(2)] }; } if (verb === "stage-table" || verb === "scope-table") { return { type: "delegate", tool: TOOLS.utility, args: [verb, ...argv.slice(2)] }; } return nounError("gen", verb); } function handleCustom(route: Route, argv: string[]): Action { if (route.custom === "workspace") return handleWorkspace(argv); if (route.custom === "config") return handleConfig(route, argv); if (route.custom === "plugin") return handlePlugin(argv); if (route.custom === "gen") return handleGen(argv); return nounError(argv[0], argv[1]); } function handleRouteOnly(route: Route, argv: string[]): Action { if (route.routeOnly === "tool-passthrough") { if (!route.tool) return nounError(argv[0], argv[1]); return { type: "delegate", tool: route.tool, args: argv.slice(1) }; } if (route.routeOnly === "hook") { const name = argv[1]; if (!name) return nounError("hook", undefined); if (!isSafeName(name)) return nounError("hook", name); // 2.8.0 projected the Cursor and Copilot adapters onto this one-argument // route (`aidlc engine hook cursor-adapter `), and that wiring is // project-owned, so `aidlc update` alone cannot rewrite it. Resolve those // two shipped spellings to the adapter action they meant. if (name === "cursor-adapter" || name === "copilot-adapter") { const harness: AdapterHarness = name === "cursor-adapter" ? "cursor" : "copilot"; const target = argv[2]; if (!target) return nounError("adapter", undefined); if (!isSafeName(target)) return nounError("adapter", target); return { type: "adapter", harness, target, extraArgs: argv.slice(3), path: resolveHookPath(adapterFile(harness), harness), }; } return { type: "hook", name, path: resolveHookPath(`aidlc-${name}.ts`) }; } if (route.routeOnly === "statusline") { return { type: "statusline", path: resolveHookPath("aidlc-statusline.ts") }; } if (route.routeOnly === "adapter") { const harness = argv[1]; const target = argv[2]; if (!harness) return nounError("adapter", undefined); if (!isAdapterHarness(harness)) return nounError("adapter", harness); if (!target) return nounError("adapter", undefined); if (!isSafeName(target)) return nounError("adapter", target); const file = adapterFile(harness); return { type: "adapter", harness, target, extraArgs: argv.slice(3), path: resolveHookPath(file, harness), }; } return nounError(argv[0], argv[1]); } function resolveAlias(argv: string[], engineNamespace = false): Action | undefined { const head = argv[0]; if (engineNamespace && head === "__sensor-script-file") { const id = argv[1]; if (!id || !isSafeName(id)) { return topLevelError(argv.slice(0, 2).join(" ")); } return { type: "sensor-script-file", id, args: argv.slice(2), }; } if (engineNamespace && head === "__sensor-script") { const scripts: Record = { "claim-sources": TOOLS.sensorClaimSources, linter: TOOLS.sensorLinter, "required-sections": TOOLS.sensorRequiredSections, traceability: TOOLS.sensorTraceability, "type-check": TOOLS.sensorTypeCheck, "upstream-coverage": TOOLS.sensorUpstreamCoverage, }; const tool = scripts[argv[1] ?? ""]; return tool ? { type: "delegate", tool, args: argv.slice(2) } : topLevelError(argv.slice(0, 2).join(" ")); } if (head === "--status") return { type: "delegate", tool: TOOLS.utility, args: ["status", ...argv.slice(1)] }; if (head === "--claim") return { type: "delegate", tool: TOOLS.utility, args: ["claim", ...argv.slice(1)] }; if (head === "--release") return { type: "delegate", tool: TOOLS.utility, args: ["release", ...argv.slice(1)] }; if (head === "--doctor") return { type: "delegate", tool: TOOLS.doctor, args: ["doctor", ...argv.slice(1)] }; if (head === "--version") return { type: "version", json: false }; if (head === "--resume") return { type: "delegate", tool: TOOLS.orchestrate, args: ["next", "--resume", ...argv.slice(1)] }; if (head === "--scope") return { type: "delegate", tool: TOOLS.orchestrate, args: ["next", "--scope", ...argv.slice(1)] }; return undefined; } function resolveTop(argv: string[]): Action | undefined { const verb = argv[0]; for (const route of ROUTES.filter((item) => item.group === "top" && item.namespace === "public" )) { if (!route.verbs.includes(verb)) continue; if (route.kind === "top-passthrough" && route.tool) { if (verb === "version") return { type: "version", json: false }; return { type: "delegate", tool: route.tool, args: [verb, ...argv.slice(1)] }; } if (route.kind === "top-prefix" && route.tool && route.prefix) { return { type: "delegate", tool: route.tool, args: [...route.prefix, ...argv.slice(1)] }; } if (route.kind === "top-stub") { return { type: "stub", code: 3, message: "reserved; not available in this install\n", }; } if (route.kind === "top-help") return { type: "help", scope: "human" }; if (route.kind === "routing-only") return handleRouteOnly(route, argv); } return undefined; } function resolveNoun(argv: string[], namespace: Exclude): Action | undefined { const noun = argv[0]; const routes = ROUTES.filter((item) => item.namespace === namespace && item.group === noun ); if (routes.length === 0) return undefined; for (const route of routes) { if (route.kind === "routing-only") return handleRouteOnly(route, argv); const verb = argv[1]; if (!verb || !route.verbs.includes(verb)) continue; if (route.kind === "top-stub") { return { type: "stub", code: 3, message: `${noun} ${verb} is reserved for the next lifecycle release\n`, }; } if (route.kind === "noun-passthrough" && route.tool) { return { type: "delegate", tool: route.tool, args: [...(route.prefix ?? []), verb, ...argv.slice(2)], }; } if (route.kind === "noun-map" && route.tool && route.targets) { return { type: "delegate", tool: route.tool, args: [route.targets[verb], ...argv.slice(2)] }; } } const custom = routes.find((route) => route.kind === "custom"); if (custom) return handleCustom(custom, argv); return nounError(noun, argv[1]); } function resolveEngine(argv: string[]): Action { if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") { return { type: "help", scope: "engine" }; } const alias = resolveAlias(argv, true); if (alias) return alias; const noun = resolveNoun(argv, "engine"); if (noun) return noun; const top = ROUTES.find((route) => route.namespace === "engine" && route.group === "top" && route.verbs.includes(argv[0]) ); if (top?.kind === "top-passthrough" && top.tool) { return { type: "delegate", tool: top.tool, args: [argv[0], ...argv.slice(1)] }; } const routeOnly = ROUTES.find((route) => route.namespace === "engine" && route.group === "top" && route.kind === "routing-only" && route.verbs.includes(argv[0]) ); if (routeOnly) return handleRouteOnly(routeOnly, argv); return topLevelError(`engine ${argv[0]}`); } function resolveSystem(argv: string[]): Action { if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") { return { type: "help", scope: "system" }; } const noun = resolveNoun(argv, "system"); if (noun) return noun; const top = ROUTES.find((route) => route.namespace === "system" && route.group === "top" && route.verbs.includes(argv[0]) ); if (top?.kind === "top-passthrough" && top.tool) { return { type: "delegate", tool: top.tool, args: [argv[0], ...argv.slice(1)] }; } return topLevelError(`system ${argv[0]}`); } // Public noun routes (the v2 team/authoring surface): `aidlc unit ` and // `aidlc plugin ` resolve at the top level without the engine // prefix. A head token whose verb does not match falls through to the public // unknown-command error rather than a noun error, so `aidlc plugin list` // remains "unknown command 'plugin'" (that engine surface is `aidlc engine // plugin list`). function resolvePublicNoun(argv: string[]): Action | undefined { const noun = argv[0]; const verb = argv[1]; for (const route of ROUTES.filter((item) => item.namespace === "public" && item.group !== "top" && item.group === noun )) { if (!verb || !route.verbs.includes(verb)) continue; if (route.kind === "noun-passthrough" && route.tool) { return { type: "delegate", tool: route.tool, args: [...(route.prefix ?? []), verb, ...argv.slice(2)], }; } if (route.kind === "noun-map" && route.tool && route.targets) { return { type: "delegate", tool: route.tool, args: [route.targets[verb], ...argv.slice(2)] }; } } return undefined; } function resolveActionWithoutGlobalFlags(argv: string[]): Action { if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") { return { type: "help", scope: "human" }; } if (argv[0] === "help") { return { type: "help", scope: argv[1] === "--all" ? "all" : "human", }; } if (argv[0] === "engine") return resolveEngine(argv.slice(1)); if (argv[0] === "system") return resolveSystem(argv.slice(1)); const alias = resolveAlias(argv); if (alias) return alias; const top = resolveTop(argv); if (top) return top; const publicNoun = resolvePublicNoun(argv); if (publicNoun) return publicNoun; return publicCommandError(argv[0]); } // The 2.8.0 Copilot adapter spawned its core hooks as `aidlc hook ` (no // `engine` namespace). That adapter lives in every native Copilot project // configured by 2.8.0, is project-owned, and is preferred by resolveHookPath() // over the packaged one, so `aidlc update` alone cannot replace it. Accept the // spelling ONLY in the context that adapter's children run in: runAdapter() // pins AIDLC_HARNESS_NAME=copilot and, under the compiled binary, exports // AIDLC_COMPILED_EXECUTABLE, and the adapter forwards both. Any other caller // keeps getting `unknown command 'hook'`; `aidlc config` installs the adapter // that uses the canonical `engine hook` route. function canonicalizeLegacyCopilotHookArgv(argv: string[]): string[] { return argv[0] === "hook" && process.env.AIDLC_HARNESS_NAME === "copilot" && (process.env.AIDLC_COMPILED_EXECUTABLE ?? "") !== "" ? ["engine", ...argv] : argv; } export function resolveAction(rawArgv: string[]): Action { const argv = canonicalizeLegacyCopilotHookArgv(rawArgv); const clean: string[] = []; const globalFlags: string[] = []; let projectDir: string | undefined; let literalArgs = false; for (let i = 0; i < argv.length; i++) { if (argv[i] === "--") { literalArgs = true; clean.push(argv[i]); continue; } if ( !literalArgs && ["--json", "--quiet", "--no-color", "--yes", "--offline", "--verbose"].includes(argv[i]) ) { globalFlags.push(argv[i]); continue; } if (!literalArgs && argv[i].startsWith("--project-dir=")) { return { type: "error", code: 2, message: "aidlc: --project-dir takes a separate path value; use --project-dir \n", }; } if (literalArgs || argv[i] !== "--project-dir") { clean.push(argv[i]); continue; } const value = argv[++i]; if (!value || value.startsWith("--")) { return { type: "error", code: 2, message: "aidlc: --project-dir requires a path value\n", }; } if (projectDir !== undefined) { return { type: "error", code: 2, message: "aidlc: --project-dir may be specified only once\n", }; } projectDir = value; } const action = resolveActionWithoutGlobalFlags(clean); if (action.type === "version" && globalFlags.includes("--json")) { action.json = true; } if (projectDir) { const absoluteProjectDir = isAbsolute(projectDir) ? projectDir : resolve(process.cwd(), projectDir); if (action.type === "delegate") { const delimiter = action.args.indexOf("--"); if (delimiter >= 0) action.args.splice(delimiter, 0, "--project-dir", absoluteProjectDir); else action.args.push("--project-dir", absoluteProjectDir); } else if (action.type === "hook") { action.projectDir = absoluteProjectDir; action.path = resolveHookPath(`aidlc-${action.name}.ts`, undefined, absoluteProjectDir); } else if (action.type === "statusline") { action.projectDir = absoluteProjectDir; action.path = resolveHookPath("aidlc-statusline.ts", undefined, absoluteProjectDir); } else if (action.type === "adapter") { action.projectDir = absoluteProjectDir; const file = adapterFile(action.harness); action.path = resolveHookPath(file, action.harness, absoluteProjectDir); } else if (action.type === "sensor-script-file") { action.projectDir = absoluteProjectDir; } } if (action.type === "delegate") { const delimiter = action.args.indexOf("--"); // Literal text after `--` may itself contain `--project-dir`. Delegates // that scan argv for that flag must find the dispatcher's directory first, // so when the caller gave none the resolved directory is pinned ahead of // the delimiter. The literal text stays byte-for-byte intact. if (delimiter >= 0 && !projectDir) { action.args.splice(delimiter, 0, "--project-dir", dispatcherProjectDirFrom(argv)); } if (delimiter >= 0) action.args.splice(action.args.indexOf("--"), 0, ...globalFlags); else action.args.push(...globalFlags); } return action; } // TRANSLATION_LOGIC_END function toolPath(tool: string): string { return join(toolsDir(), tool); } function bunExecutable(): string { return basename(process.execPath).startsWith("bun") ? process.execPath : "bun"; } function delegatedProjectDir(args: readonly string[]): string | undefined { return projectDirFlag(args).value; } function runDelegateDev(tool: string, args: string[]): number { try { const child = Bun.spawnSync([bunExecutable(), toolPath(tool), ...args], { /* dev-mode bun spawn */ cwd: process.cwd(), stdout: "inherit", stderr: "inherit", env: { ...process.env, ...(delegatedProjectDir(args) ? { AIDLC_PROJECT_DIR: delegatedProjectDir(args) } : {}), }, }); return child.exitCode ?? 1; } catch (error) { text(2, `aidlc: failed to spawn ${tool}: ${String(error)}\n`); return 1; } } type DelegateModule = { main(argv: string[]): void | Promise; }; async function loadDelegate(tool: string): Promise { switch (tool) { 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.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; } } async function runDelegateInProcess(tool: string, args: string[]): Promise { const previousProjectDir = process.env.AIDLC_PROJECT_DIR; const projectDir = delegatedProjectDir(args); if (projectDir) process.env.AIDLC_PROJECT_DIR = projectDir; try { const mod = await loadDelegate(tool); if (mod === null || typeof mod.main !== "function") { text(2, `${JSON.stringify({ error: `${tool} does not export main(argv)` })}\n`); return 1; } await mod.main(args); const code = process.exitCode; if (typeof code === "number") return code; return code ? Number(code) || 1 : 0; } catch (error) { text(2, `${JSON.stringify({ error: errorMessage(error) })}\n`); return 1; } finally { if (previousProjectDir === undefined) delete process.env.AIDLC_PROJECT_DIR; else process.env.AIDLC_PROJECT_DIR = previousProjectDir; } } let bufferedStdin: string | null = null; async function readStdin(): Promise { if (bufferedStdin === null) bufferedStdin = await Bun.stdin.text(); return bufferedStdin; } async function readStdinWithTimeout(timeoutMs: number): Promise { return await new Promise((resolve) => { const chunks: Buffer[] = []; let settled = false; let timeout: ReturnType; const cleanup = () => { clearTimeout(timeout); process.stdin.off("data", onData); process.stdin.off("end", onEnd); process.stdin.off("error", onError); }; const finish = (value: string) => { if (settled) return; settled = true; cleanup(); resolve(value); }; const onData = (chunk: Buffer | string) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }; const onEnd = () => finish(Buffer.concat(chunks).toString("utf-8")); const onError = () => finish(""); timeout = setTimeout(() => { process.stdin.pause(); finish(""); }, timeoutMs); process.stdin.on("data", onData); process.stdin.once("end", onEnd); process.stdin.once("error", onError); process.stdin.resume(); }); } async function withProjectDir( projectDir: string | undefined, run: () => Promise, ): Promise { if (!projectDir) return await run(); const previousAidlc = process.env.AIDLC_PROJECT_DIR; const previousClaude = process.env.CLAUDE_PROJECT_DIR; process.env.AIDLC_PROJECT_DIR = projectDir; process.env.CLAUDE_PROJECT_DIR = projectDir; try { return await run(); } finally { if (previousAidlc === undefined) delete process.env.AIDLC_PROJECT_DIR; else process.env.AIDLC_PROJECT_DIR = previousAidlc; if (previousClaude === undefined) delete process.env.CLAUDE_PROJECT_DIR; else process.env.CLAUDE_PROJECT_DIR = previousClaude; } } async function runHook(action: Extract): Promise { if (!existsSync(action.path)) { text(2, `aidlc engine hook ${action.name}: not available in this install\n`); return 1; } const mod = await import(pathToFileURL(action.path).href); if (typeof mod.run !== "function") { text(2, `aidlc engine hook ${action.name}: hook does not export run(input)\n`); return 1; } return await mod.run(await readStdin()); } async function runStatusline(action: Extract): Promise { if (!existsSync(action.path)) { text(2, "aidlc engine statusline: not available in this install\n"); return 1; } const mod = await import(pathToFileURL(action.path).href); if (typeof mod.run !== "function") { text(2, "aidlc engine statusline: hook does not export run(input)\n"); return 1; } return await mod.run(await readStdin()); } async function runAdapter(action: Extract): Promise { if (!existsSync(action.path)) { text(2, `aidlc engine adapter ${action.harness} ${action.target}: not available in this install\n`); return 1; } // Dispatcher startup may already have pinned AIDLC_HARNESS_DIR/NAME from // the cwd's metadata (compiled mode does so before routing). The adapter's // harness is authoritative here: pin both so the core hooks it spawns // resolve the matching packaged runtime even when the hook cwd carries no // harness metadata (.aidlc is shared by copilot and opencode; the // metadata-free fallback names opencode). const previousHarness = process.env.AIDLC_HARNESS_DIR; const previousHarnessName = process.env.AIDLC_HARNESS_NAME; const previousExecutable = process.env.AIDLC_COMPILED_EXECUTABLE; process.env.AIDLC_HARNESS_DIR = ADAPTER_HARNESS_LEAF[action.harness]; process.env.AIDLC_HARNESS_NAME = action.harness; if (isCompiledExecutable()) { process.env.AIDLC_COMPILED_EXECUTABLE = process.execPath; } try { const mod = await import(pathToFileURL(action.path).href); if (typeof mod.run !== "function") { text(2, `aidlc engine adapter ${action.harness} ${action.target}: adapter does not export run(target, input, extraArgs)\n`); return 1; } let input = ""; if (action.harness !== "kiro-ide") { input = await readStdin(); } else if ( action.target === "audit-and-sensors" || action.target === "log-subagent" || action.target === "plan-approval-guard" || action.target === "record-human-turn" || action.target === "rebuild-stage-graph" || action.target === "session-start" || action.target === "continue-workflow" || action.target === "verb-intercept" || action.target === "terminal-command-guard" ) { // Mirror the adapter entry point's dual-generation channel contract. // IDE 0.12 provides USER_PROMPT and leaves stdin open forever, so consume // a non-empty env payload immediately. IDE 1.x leaves USER_PROMPT empty // and writes+closes stdin; the timeout is only a broken-channel ceiling. const legacyPayload = process.env.USER_PROMPT ?? ""; if (legacyPayload.trim().length > 0) { input = legacyPayload; } else if (!process.stdin.isTTY) { // AIDLC_IDE_STDIN_TIMEOUT_MS mirrors the adapter's test seam so both // entry points share one contract. const override = Number(process.env.AIDLC_IDE_STDIN_TIMEOUT_MS ?? ""); const ceiling = Number.isFinite(override) && override > 0 ? override : 2000; input = await readStdinWithTimeout(ceiling); } } return await mod.run(action.target, input, action.extraArgs); } finally { if (previousHarness === undefined) delete process.env.AIDLC_HARNESS_DIR; else process.env.AIDLC_HARNESS_DIR = previousHarness; if (previousHarnessName === undefined) delete process.env.AIDLC_HARNESS_NAME; else process.env.AIDLC_HARNESS_NAME = previousHarnessName; if (previousExecutable === undefined) delete process.env.AIDLC_COMPILED_EXECUTABLE; else process.env.AIDLC_COMPILED_EXECUTABLE = previousExecutable; } } async function runSensorScriptFile( action: Extract, ): Promise { const sensorModule = await import("./aidlc-sensor.ts") as { resolveSensorScriptPath?: (id: string) => string; }; if (typeof sensorModule.resolveSensorScriptPath !== "function") { text(2, "aidlc sensor worker: resolver unavailable\n"); return 1; } let path: string; try { path = sensorModule.resolveSensorScriptPath(action.id); } catch (error) { text(2, `aidlc sensor worker: ${errorMessage(error)}\n`); return 1; } if (!existsSync(path)) { text(2, `aidlc sensor worker: not found: ${path}\n`); return 1; } const mod = await import(pathToFileURL(path).href) as { main?: (argv: string[]) => void | Promise; }; if (typeof mod.main !== "function") { text(2, `aidlc sensor worker: ${path} does not export main(argv)\n`); return 1; } await mod.main(action.args); return typeof process.exitCode === "number" ? process.exitCode : 0; } async function execute(action: Action): Promise { const isCompiled = isCompiledExecutable(); if (action.type === "delegate") { // Tool modules are imported lazily in compiled mode to keep dev-mode startup // fast and to avoid loading every tool for help/version calls. return isCompiled ? await runDelegateInProcess(action.tool, action.args) : runDelegateDev(action.tool, action.args); } if (action.type === "help") { const rendered = action.scope === "engine" ? await renderEngineHelp() : action.scope === "system" ? renderNamespaceHelp(SYSTEM_NAMESPACE_HELP) : action.scope === "all" ? renderAllHelp() : renderHumanHelp(); text( 1, rendered, ); if (action.scope === "human" && process.stdout.isTTY) { try { const { cachedUpdateNotice } = await import("./aidlc-update.ts"); const notice = cachedUpdateNotice(); if (notice) text(1, `\n${notice}\n`); } catch { // Ambient discovery never makes help fail. } } return 0; } if (action.type === "version") { text( 1, action.json ? `${JSON.stringify({ schemaVersion: 1, binaryVersion: AIDLC_VERSION, runtimeVersion: AIDLC_VERSION, })}\n` : `aidlc ${AIDLC_VERSION} (runtime ${AIDLC_VERSION})\n`, ); return 0; } if (action.type === "hook") { return await withProjectDir(action.projectDir, () => runHook(action)); } if (action.type === "statusline") { return await withProjectDir(action.projectDir, () => runStatusline(action)); } if (action.type === "adapter") { return await withProjectDir(action.projectDir, () => runAdapter(action)); } if (action.type === "sensor-script-file") { return await withProjectDir(action.projectDir, () => runSensorScriptFile(action)); } if (action.type === "stub" || action.type === "error") { text(2, action.type === "error" ? action.humanMessage ?? action.message : action.message); return action.code; } return 1; } function withoutProjectDirFlag(argv: readonly string[]): string[] { const clean: string[] = []; let literalArgs = false; for (let index = 0; index < argv.length; index++) { if (argv[index] === "--") { literalArgs = true; clean.push(argv[index]); continue; } if ( !literalArgs && ["--json", "--quiet", "--no-color", "--yes", "--offline", "--verbose"].includes(argv[index]) ) { continue; } if (!literalArgs && argv[index] === "--project-dir") { index++; continue; } clean.push(argv[index]); } return clean; } function normalizePublicCommandArgv( argv: readonly string[], command: PublicCommand, invocation: string = command, ): string[] { const normalized: string[] = [command]; let removedCommand = false; let literalArgs = false; for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (token === "--") { literalArgs = true; normalized.push(token); continue; } if (!literalArgs && token === "--project-dir") { normalized.push(token); if (argv[index + 1] !== undefined) normalized.push(argv[++index]); continue; } if (!removedCommand && token === invocation) { removedCommand = true; continue; } normalized.push(token); } return normalized; } function routeById(id: string): Route { const route = ROUTES.find((candidate) => candidate.id === id); if (!route) throw new Error(`dispatcher route registry is missing ${id}`); return route; } export function routePolicyFor(argv: readonly string[]): Route | null { const clean = withoutProjectDirFlag(argv); const head = clean[0]; if (!head || head === "--help" || head === "-h" || head === "help") { return routeById("top-help"); } const aliasRoutes: Readonly> = { "--claim": "unit", "--release": "unit", "--status": "top-status", "--doctor": "top-doctor", "--version": "top-version", "--resume": "top-orchestrate", "--scope": "top-orchestrate", }; if (aliasRoutes[head]) return routeById(aliasRoutes[head]); if (head === "engine" || head === "system") { const namespace = head; const delegate = clean[1]; const command = clean[2]; if (!delegate || delegate === "--help" || delegate === "-h" || delegate === "help") { return head === "engine" ? routeById("top-help") : null; } if ( namespace === "engine" && (delegate === "__sensor-script" || delegate === "__sensor-script-file") ) { return routeById("sensor"); } const nounRoutes = ROUTES.filter((route) => route.namespace === namespace && route.group === delegate ); if (nounRoutes.length > 0) { const matched = nounRoutes.find((route) => route.verbs.includes(command ?? "")) ?? nounRoutes.find((route) => route.kind === "custom" && route.verbs.includes(`${command ?? ""} ${clean[3] ?? ""}`) ) ?? nounRoutes.find((route) => route.kind === "custom" && route.verbs.includes("") && Boolean(command && isSafeName(command)) ) ?? nounRoutes.find((route) => route.kind === "routing-only") ?? undefined; if (matched) return matched; } return ROUTES.find((route) => route.namespace === namespace && route.group === "top" && route.verbs.includes(delegate) ) ?? null; } const top = ROUTES.find((route) => route.namespace === "public" && route.group === "top" && route.verbs.includes(head) ); if (top) return top; const publicNoun = ROUTES.find((route) => route.namespace === "public" && route.group !== "top" && route.group === head && route.verbs.includes(clean[1] ?? "") ); if (publicNoun) return publicNoun; return null; } function routePinPolicy(argv: readonly string[]): PinPolicy { const route = routePolicyFor(argv); if ( route && (route.pinPolicy === "pinned") !== launcherRouteUsesPin(argv) ) { throw new Error(`launcher pin policy drift for route ${route.id}`); } return route?.pinPolicy ?? "active"; } function dispatcherProjectDirFrom(argv: readonly string[]): string { const explicit = projectDirFlag(argv).value; const value = explicit || process.env.AIDLC_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || process.env.KIRO_PROJECT_DIR; return value ? (isAbsolute(value) ? value : resolve(process.cwd(), value)) : process.cwd(); } // The dispatcher owns the --project-dir grammar before the `--` delimiter: // only the two-token form is accepted, because delegates that also parse // `--project-dir=` would otherwise mutate a directory the dispatcher's // policy never inspected. Tokens after `--` are literal task text and are never // interpreted here; resolveAction() pins the dispatcher's own project // directory ahead of that delimiter so no delegate can route on the literal. function projectDirFlag(argv: readonly string[]): { value?: string; error?: string; } { let value: string | undefined; for (let index = 0; index < argv.length; index++) { const token = argv[index]; if (token === "--") break; if (token.startsWith("--project-dir=")) { return { error: "--project-dir takes a separate path value; use --project-dir " }; } if (token !== "--project-dir") continue; const candidate = argv[++index]; if (!candidate || candidate.startsWith("--")) { return { error: "--project-dir requires a path value" }; } if (value !== undefined) { return { error: "--project-dir may be specified only once" }; } value = candidate; } return { value }; } async function dispatchPinnedVersion( argv: string[], input: string | null, ): Promise { if (routePinPolicy(argv) !== "pinned") return null; const projectDir = dispatcherProjectDirFrom(argv); const pinPath = join(projectDir, ".aidlc-version"); if (!existsSync(pinPath)) return null; const { reserveDispatchedVersion, resolvePinnedDispatch, } = await import("./aidlc-lifecycle.ts"); const result = resolvePinnedDispatch(argv, projectDir); if (result.kind === "none") return null; if (result.kind === "failure") { return renderDispatcherFailure( argv, result.code, result.message, result.remediation, ); } const releaseReservation = reserveDispatchedVersion(result.version); try { const child = Bun.spawnSync([result.executable, ...argv], { cwd: process.cwd(), stdin: input === null ? "inherit" : new TextEncoder().encode(input), stdout: "inherit", stderr: "inherit", env: { ...process.env, AIDLC_PIN_DISPATCHED: result.version, AIDLC_ACTIVE_VERSION: AIDLC_VERSION, }, }); return child.exitCode ?? 1; } finally { releaseReservation(); } } function refuseUnpinnedMajorSkew(argv: readonly string[]): number | null { if (routePinPolicy(argv) !== "pinned") return null; const projectDir = dispatcherProjectDirFrom(argv); if (existsSync(join(projectDir, ".aidlc-version"))) return null; for (const harness of discoverProjectHarnesses(projectDir)) { if ( harness.frameworkVersion && harness.frameworkVersion.split(".")[0] !== AIDLC_VERSION.split(".")[0] ) { return renderDispatcherFailure( argv, 1, `project runtime ${harness.frameworkVersion} is incompatible with selected engine ${AIDLC_VERSION}`, "aidlc use or aidlc config", ); } } return null; } function requestedOutputMode(argv: readonly string[]): "human" | "quiet" | "json" { const delimiter = argv.indexOf("--"); const globalArgs = delimiter < 0 ? argv : argv.slice(0, delimiter); if (globalArgs.includes("--json")) return "json"; if (globalArgs.includes("--quiet")) return "quiet"; return "human"; } function renderDispatcherFailure( argv: readonly string[], code: number, message: string, remediation?: string, ): number { const cleanMessage = message.replace(/^aidlc:\s*/, "").trim(); const mode = requestedOutputMode(argv); if (mode === "json") { text(1, `${ JSON.stringify({ schemaVersion: 1, ok: false, code, status: code === 2 ? "usage" : code === 3 ? "unavailable" : "failed", message: cleanMessage, ...(remediation ? { remediation } : {}), }) }\n`); } else if (mode === "quiet") { text(1, `${remediation ?? cleanMessage}\n`); } else { text(2, `${errorLabel("aidlc:", process.stderr)} ${cleanMessage}\n`); if (remediation) { text(2, `${fixLabel("fix:", process.stderr)} ${remediation}\n`); } } return code; } function basicPolicyError(route: Route, argv: readonly string[]): string | null { const output = requestedOutputMode(argv); if (!route.outputModes.includes(output)) { return `${route.id} does not support --${output}`; } const delimiter = argv.indexOf("--"); const globalArgs = delimiter < 0 ? argv : argv.slice(0, delimiter); if (route.networkPolicy === "required" && globalArgs.includes("--offline")) { return `${route.id} requires network access and cannot run with --offline`; } return null; } type SimplePublicGrammar = { values: ReadonlySet; bare: ReadonlySet; }; const SIMPLE_PUBLIC_GRAMMARS: Readonly< Partial> > = { doctor: { values: new Set([ "--ca-bundle", "--output", "--project-dir", "--release-base-url", ]), bare: new Set([ "--check-updates", "--export", "--json", "--no-color", "--offline", "--quiet", "--verbose", ]), }, version: { values: new Set(["--project-dir"]), bare: new Set(["--json", "--no-color"]), }, }; function validateSimplePublicGrammar( command: PublicCommand, argv: readonly string[], ): string | null { const grammar = SIMPLE_PUBLIC_GRAMMARS[command]; if (!grammar) return null; const seen = new Set(); for (let index = 1; index < argv.length; index++) { const token = argv[index]; if (!token.startsWith("--")) { return `unexpected ${command} positional ${JSON.stringify(token)}`; } if (grammar.values.has(token)) { if (seen.has(token)) return `${token} may be specified only once`; const value = argv[index + 1]; if (!value || value.startsWith("--")) return `${token} requires a value`; seen.add(token); index++; continue; } if (!grammar.bare.has(token)) return `unknown ${command} option ${token}`; if (seen.has(token)) return `${token} may be specified only once`; seen.add(token); } if (argv.includes("--json") && argv.includes("--quiet")) { return "--json and --quiet are mutually exclusive"; } if ( command === "doctor" && argv.includes("--output") && !argv.includes("--export") ) { return "--output requires --export"; } if ( command === "doctor" && argv.includes("--export") && (argv.includes("--json") || argv.includes("--quiet")) ) { return "--export cannot be combined with --json or --quiet"; } return null; } async function publicCommandGrammarError( route: Route | null, argv: readonly string[], ): Promise { if ( route?.namespace !== "public" || route.group !== "top" ) { return null; } const command = route.verbs[0] as PublicCommand | undefined; const invocation = withoutProjectDirFlag(argv)[0]; if ( !command || !(PUBLIC_COMMANDS as readonly string[]).includes(command) || publicCommandFromToken(invocation) !== command ) { return null; } const normalized = normalizePublicCommandArgv(argv, command, invocation); if (command === "config") { const { validatePublicConfigArgs } = await import("./aidlc-init.ts"); return validatePublicConfigArgs(normalized); } if (command === "update" || command === "use" || command === "uninstall") { const { validatePublicLifecycleArgs } = await import("./aidlc-lifecycle.ts"); return validatePublicLifecycleArgs(normalized); } return validateSimplePublicGrammar(command, normalized); } function renderPublicGrammarFailure( argv: readonly string[], message: string, ): number { const configSection = /^unknown config section "([^"]+)";/.exec(message)?.[1]; if ( configSection && requestedOutputMode(argv) === "human" && (process.stdin.isTTY || process.env.AIDLC_TEST_CONFIG_TTY === "1") ) { const invoke = aidlcInvocation(); const nearest = [ "models", "runtime", "providers", "trust", "flags", "project", ] .map((candidate) => ({ candidate, distance: editDistance(configSection, candidate), })) .sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate) )[0]; text( 2, `${errorLabel("error:", process.stderr)} unknown config section '${configSection}'\n`, ); if (nearest && nearest.distance <= 2) { text( 2, `\n ${tipLabel("tip:", process.stderr)} did you mean '${nearest.candidate}'?\n`, ); } text( 2, `\n${heading("usage:", process.stderr)} ${invoke} config
[flags]\n`, ); text(2, `For the full list, run '${invoke} config --help'.\n`); return 2; } return renderDispatcherFailure(argv, 2, message); } function projectPolicyError(route: Route, argv: readonly string[]): string | null { if (route.projectRequirement !== "required") return null; const projectDir = dispatcherProjectDirFrom(argv); const recognized = [ ".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", "aidlc", ].some((entry) => existsSync(join(projectDir, entry))) || discoverProjectHarnesses(projectDir).length > 0; return recognized ? null : `${route.id} requires an installed project harness or recognized project directory; run aidlc config`; } // A project inside a machine root is never acceptable. The reverse (a machine // root inside the project) matters only for the project-required engine and // unit routes, whose tools write `/aidlc` and the harness directory // directly. Optional-project public commands (doctor, config) are commonly // run from `$HOME`, which contains the default install and command roots; // their writes go through the transaction engine, which refuses machine-owned // targets in a project-rooted plan, so the reverse check would only refuse // read-only diagnostics there. async function projectMachineOverlapError( route: Route, argv: readonly string[], ): Promise { const scope = effectiveMutationScope(route, argv); const required = route.projectRequirement === "required"; if (!required && scope !== "project" && scope !== "project-and-machine") { return null; } const projectDir = dispatcherProjectDirFrom(argv); const { isMachineOwnedPath, projectPathOverlapsMachineRoots } = await import( "./aidlc-install-paths.ts" ); const overlaps = required ? projectPathOverlapsMachineRoots(projectDir) : isMachineOwnedPath(projectDir); return overlaps ? `${route.id} cannot use an AI-DLC machine install or command directory as its project directory` : null; } function effectiveMutationScope( route: Route, argv: readonly string[], ): MutationScope { const clean = withoutProjectDirFlag(argv); if ( route.id === "system-lifecycle" && clean[2] === "install-profile" ) { return "user-home"; } return route.mutationScope; } async function withRoutePolicy(route: Route, argv: readonly string[], run: () => Promise): Promise { const values: Record = { AIDLC_ROUTE_ID: route.id, AIDLC_ROUTE_NETWORK_POLICY: route.networkPolicy, AIDLC_ROUTE_MUTATION_SCOPE: effectiveMutationScope(route, argv), AIDLC_ROUTE_PROJECT_DIR: dispatcherProjectDirFrom(argv), AIDLC_ROUTE_OUTPUT_MODE: requestedOutputMode(argv), }; const prior = Object.fromEntries( Object.keys(values).map((key) => [key, process.env[key]]), ) as Record; Object.assign(process.env, values); try { return await run(); } finally { for (const [key, value] of Object.entries(prior)) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } } } export async function main(rawArgv: string[]): Promise { // Canonicalized before route policy so stdin buffering, pinning, and // dispatch see `engine hook`. const argv = canonicalizeLegacyCopilotHookArgv(rawArgv); process.exitCode = 0; bufferedStdin = null; configureColor(argv); const projectDirOption = projectDirFlag(argv); if (projectDirOption.error) { process.exitCode = renderDispatcherFailure(argv, 2, projectDirOption.error); return; } if (argv.length === 1 && argv[0] === "--internal-metrics-send") { const metrics = await import("./aidlc-metrics.ts"); await metrics.sendMetricFromStdin(); return; } const commandHelp = commandHelpRequest(argv); if (commandHelp) { text(1, renderCommandHelp(commandHelp)); return; } const route = routePolicyFor(argv); if (route) { const error = basicPolicyError(route, argv); if (error) { process.exitCode = renderDispatcherFailure(argv, 2, error); return; } } const grammarError = await publicCommandGrammarError(route, argv); if (grammarError) { process.exitCode = renderPublicGrammarFailure(argv, grammarError); return; } if (route) { const overlapError = await projectMachineOverlapError(route, argv); if (overlapError) { process.exitCode = renderDispatcherFailure(argv, 1, overlapError); return; } } if (isCompiledExecutable()) { // Compiled, no explicit harness: discover the project install from its // shipped stamp/harness metadata. Module-relative derivation cannot work // from $bunfs, and embedded data may be Claude-flavoured. Every delegate // and sibling tool reads these envs, so pin both identifiers once here, // before lazy delegate imports, so same-directory harnesses retain // identity. Falls back to .claude when no install is present. if (!process.env.AIDLC_HARNESS_DIR) { process.env.AIDLC_HARNESS_DIR = runtimeHarnessDir(); } if (!process.env.AIDLC_HARNESS_NAME) { process.env.AIDLC_HARNESS_NAME = runtimeHarnessName(); } } if ( process.platform === "win32" && !["doctor", "--doctor", "uninstall"].includes(argv[0] ?? "") ) { try { const { recoverWindowsUninstallContinuations } = await import( "./aidlc-windows-uninstall.ts" ); const recovered = recoverWindowsUninstallContinuations(); if (recovered > 0) { process.exitCode = renderDispatcherFailure( argv, 3, `resumed ${recovered} pending Windows uninstall continuation(s); this command was not run`, ); return; } } catch (error) { process.exitCode = renderDispatcherFailure( argv, 1, `Windows uninstall recovery failed: ${errorMessage(error)}`, "aidlc doctor", ); return; } } if ( route?.routeOnly === "hook" || route?.routeOnly === "statusline" || (route?.routeOnly === "adapter" && withoutProjectDirFlag(argv)[2] !== "kiro-ide") ) { await readStdin(); } if ( route?.id === "top-config" && process.env.AIDLC_TEST_CONFIG_TTY === "1" && !process.stdin.isTTY ) { process.env.AIDLC_TEST_CONFIG_INPUT = await readStdin(); } const pinnedCode = await dispatchPinnedVersion(argv, bufferedStdin); if (pinnedCode !== null) { process.exitCode = pinnedCode; return; } const skewCode = refuseUnpinnedMajorSkew(argv); if (skewCode !== null) { process.exitCode = skewCode; return; } if (route) { const error = projectPolicyError(route, argv); if (error) { process.exitCode = renderDispatcherFailure(argv, 1, error); return; } } const action = resolveAction(argv); if ( (action.type === "error" || action.type === "stub") && requestedOutputMode(argv) !== "human" ) { process.exitCode = renderDispatcherFailure(argv, action.code, action.message); return; } const code = route ? await withRoutePolicy(route, argv, () => execute(action)) : await execute(action); process.exitCode = code; } if (import.meta.main) { main(process.argv.slice(2)).catch((error) => { process.exitCode = renderDispatcherFailure( process.argv.slice(2), 1, errorMessage(error), ); }); }