cclaw-cli 0.51.26 → 0.51.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,15 +7,16 @@ const STAGE_EXAMPLES = {
7
7
  ## Problem Decision Record
8
8
 
9
9
  - **Depth:** standard
10
- - **Frame type:** technical-maintenance
10
+ - **Frame type:** \`technical-maintenance\` (free-form label; pick whatever fits — see commentary in the template for example labels)
11
11
 
12
- ### Technical-maintenance framing
12
+ ### Framing fields (universal — keep field names; fill in whatever is meaningful for this work)
13
13
 
14
- - **Affected operator/developer:** release operator and package maintainer.
15
- - **Current failure mode:** release checks are fragile and inconsistent between CI and local runs; invalid metadata sometimes reaches npm publish.
16
- - **Expected operational improvement:** invalid release preconditions are caught before publish with explicit operator feedback in CI and local workflows.
17
- - **Verification signal:** shared release validation tests and CI release-check command fail on invalid metadata.
18
- - **Do-nothing cost:** continued publish risk and duplicated local/CI fixes.
14
+ - **Affected user / role / operator:** release operator and package maintainer.
15
+ - **Current state / failure mode / opportunity:** release checks are fragile and inconsistent between CI and local runs; invalid metadata sometimes reaches npm publish.
16
+ - **Desired outcome (observable):** invalid release preconditions are caught before publish; \`pnpm release:check\` exits non-zero with explicit operator feedback in CI and local workflows.
17
+ - **Evidence / signal supporting this framing:** prior incident postmortems referencing release-metadata drift; \`scripts/pre-publish.sh\` already partially encodes the rules.
18
+ - **Why now (urgency / cost of waiting):** every additional release reinforces the divergent CI/local behavior and burns operator trust.
19
+ - **Do-nothing consequence:** continued publish risk and duplicated local/CI fixes.
19
20
  - **Non-goals:** no new runtime dependencies; no release-framework rewrite.
20
21
 
21
22
  ## Clarifying Questions
@@ -675,7 +676,7 @@ function exampleSummaryBullets(stage) {
675
676
  // sample in STAGE_EXAMPLES gains or loses a top-level section.
676
677
  const STAGE_EXAMPLE_SECTION_HEADINGS = {
677
678
  brainstorm: [
678
- "Problem Decision Record (product or technical-maintenance framing)",
679
+ "Problem Decision Record (free-form Frame type label + universal framing fields)",
679
680
  "Reference Pattern Candidates and approaches with trade-offs",
680
681
  "Recommended direction + open questions",
681
682
  "Clarification log and decision record"
@@ -1,3 +1,47 @@
1
+ import { DELEGATION_DISPATCH_SURFACES, DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES } from "../delegation.js";
2
+ import { harnessDelegationRecipes } from "../harness-adapters.js";
3
+ /**
4
+ * One-line description per `--dispatch-surface` enum value. Sourced from
5
+ * `src/delegation.ts::DELEGATION_DISPATCH_SURFACES` so the generated doc
6
+ * stays in lockstep with the runtime enum. Add a new key here whenever a
7
+ * new surface is introduced upstream — TypeScript's `Record` enforces it.
8
+ */
9
+ const DISPATCH_SURFACE_DESCRIPTIONS = {
10
+ "claude-task": "Claude Code native Task launch against `.claude/agents/<agent-name>.md`; fulfillmentMode: `isolated`.",
11
+ "cursor-task": "Cursor generic Task/Subagent dispatch against `.cclaw/agents/<agent-name>.md` with a role prompt; fulfillmentMode: `generic-dispatch`. Requires non-empty `evidenceRefs` on completion.",
12
+ "opencode-agent": "OpenCode native subagent (`@<agent-name>` or Task) against `.opencode/agents/<agent-name>.md`; fulfillmentMode: `isolated`.",
13
+ "codex-agent": "OpenAI Codex CLI native custom agent against `.codex/agents/<agent-name>.toml`; fulfillmentMode: `isolated`.",
14
+ "generic-task": "Generic Task dispatch against `.cclaw/agents/<agent-name>.md` for harnesses without a vendor-specific surface; fulfillmentMode: `generic-dispatch`.",
15
+ "role-switch": "In-session role-switch fallback when no isolated dispatch surface is available. No agent-definition-path prefix is enforced; completion requires non-empty `evidenceRefs`. fulfillmentMode: `role-switch`.",
16
+ "manual": "Out-of-band manual dispatch (e.g. operator hand-off, external ticketing system). The agent-definition-path is intentionally free-form; recorded for audit only."
17
+ };
18
+ function dispatchSurfaceTableMarkdown() {
19
+ const rows = DELEGATION_DISPATCH_SURFACES
20
+ .map((surface) => {
21
+ const prefixes = DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES[surface];
22
+ const prefixCell = prefixes.length === 0
23
+ ? "(any)"
24
+ : prefixes.map((prefix) => `\`${prefix}\``).join(", ");
25
+ return `| \`${surface}\` | ${DISPATCH_SURFACE_DESCRIPTIONS[surface]} | ${prefixCell} |`;
26
+ })
27
+ .join("\n");
28
+ return `### Dispatch surfaces (\`--dispatch-surface\` enum)\n\nGenerated from \`src/delegation.ts::DELEGATION_DISPATCH_SURFACES\` and \`DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES\`. Any surface not in this table is rejected by \`.cclaw/hooks/delegation-record.mjs\` with a non-zero exit. The deprecated \`task\` surface is **not** in this enum.\n\n| Surface | Purpose | Allowed agent-definition-path prefixes |\n|---|---|---|\n${rows}\n`;
29
+ }
30
+ function perHarnessRecipeMarkdown() {
31
+ const recipes = harnessDelegationRecipes();
32
+ const rows = recipes
33
+ .map((recipe) => `| \`${recipe.harnessId}\` | \`${recipe.dispatchSurface}\` | \`${recipe.agentDefinitionExample}\` | ${recipe.fulfillmentMode} | scheduled -> launched -> acknowledged -> completed (reuse \`<span-id>\` + \`<dispatch-id>\`; \`--ack-ts=<iso-ts>\` for completed isolated/generic) |`)
34
+ .join("\n");
35
+ const examples = recipes
36
+ .map((recipe) => `**${recipe.harnessId}**:\n\n` + recipe.lifecycleCommands.map((cmd) => ` ${cmd}`).join("\n"))
37
+ .join("\n\n");
38
+ return `\n\n## Per-Harness Lifecycle Recipe\n\n| Harness | Surface | Agent definition path | fulfillmentMode | Lifecycle |\n|---|---|---|---|---|\n${rows}\n\nNeutral placeholder tokens only: \`<agent-name>\`, \`<stage>\`, \`<run-id>\`, \`<span-id>\`, \`<dispatch-id>\`, \`<agent-def-path>\`, \`<iso-ts>\`, \`<artifact-anchor>\`. See \`docs/quality-gates.md\` for stage-by-stage gate mapping.\n\nThe four shipped harnesses (\`claude\`, \`cursor\`, \`opencode\`, \`codex\`) each ship with a canonical primary surface in the table above. The remaining enum values \`generic-task\`, \`role-switch\`, and \`manual\` are documented in the dispatch-surface table below and are available to any harness as fallback paths when the primary surface is unavailable.\n\n${examples}\n\n${dispatchSurfaceTableMarkdown()}\n\n### Legacy ledger upgrade\n\nPre-v3 ledger entries that lack a recorded \`dispatchSurface\` are tagged \`fulfillmentMode: "legacy-inferred"\` on read. Stage-complete blocks completion until those rows are re-recorded with the v3 helper:\n\n node .cclaw/hooks/delegation-record.mjs \\\n --rerecord \\\n --span-id=<span-id> \\\n --dispatch-id=<dispatch-id> \\\n --dispatch-surface=<surface> \\\n --agent-definition-path=<agent-def-path> \\\n --ack-ts=<iso-ts> \\\n --completed-ts=<iso-ts> \\\n --json\n\n\`--dispatch-surface\` must be one of the values listed in the dispatch-surface table above (the enum is generated verbatim from \`src/delegation.ts::DELEGATION_DISPATCH_SURFACES\`). Surfaces must align with the allowed agent-definition-path prefixes shown alongside each surface; \`role-switch\` and \`manual\` accept any path. The deprecated \`task\` surface is rejected.\n\n`;
39
+ }
1
40
  export function harnessIntegrationDocMarkdown() {
2
- return "# Harness Integration Matrix\n\nGenerated from `src/harness-adapters.ts` capabilities and hook event mappings. For the end-to-end subagent dispatch model, proof sequence, controller/worker responsibilities, and future roadmap, see [`docs/subagent-flow.md`](./subagent-flow.md).\n\n## Capability tiers\n\n| Harness | ID | Tier | declaredSupport | runtimeLaunch | Fallback | proofRequired | proofSource | Hook surface | Structured ask |\n|---|---|---|---|---|---|---|---|---|---|\n| Claude Code | `claude` | `tier1` (full native automation) | full | native Task launch | native | spanId+dispatchId or workerRunId+ACK | `.cclaw/state/delegation-events.jsonl` + ledger | full | AskUserQuestion |\n| Cursor | `cursor` | `tier2` (supported with fallback paths) | generic | generic Task/Subagent role prompt | generic-dispatch | spanId+dispatchId/evidenceRefs | events + artifact evidenceRefs | full | AskQuestion |\n| OpenCode | `opencode` | `tier2` hooks, native dispatch declared | full | prompt-level launch via Task / `@agent` against `.opencode/agents` | native | spanId+dispatchId+ackTs+completedTs | `.opencode/agents/<agent>.md` + events | plugin | question |\n| OpenAI Codex | `codex` | `tier2` hooks, native dispatch declared | full | prompt-level request to spawn `.codex/agents` custom agents | native | spanId+dispatchId+ackTs+completedTs | `.codex/agents/<agent>.toml` + events | limited | request_user_input |\n\nFallback legend:\n\n- `native` \u2014 first-class named subagent dispatch (Claude).\n- `generic-dispatch` \u2014 generic Task dispatcher mapped to cclaw roles (Cursor).\n- `role-switch` \u2014 degraded fallback for a runtime where declared native/generic dispatch is unavailable; explicit role headers, artifact outputs, and non-empty delegation-log evidenceRefs are required.\n- `waiver` \u2014 no parity path; reserved for harnesses that cannot role-switch (none shipped).\n\n## Stage-Aware Native Dispatch Workflow\n\nOpenCode and Codex receive generated native isolated subagents. Use them before considering role-switch fallback:\n\n1. Use the active stage skill's generated dispatch table as the source of truth.\n2. OpenCode: invoke `.opencode/agents/<agent>.md` via Task or `@<agent>`; Codex: ask Codex to spawn `.codex/agents/<agent>.toml` by name, in parallel when lanes are independent.\n3. Load `.cclaw/agents/<agent>.md`, execute only that role's stage task, and write outputs into the active stage artifact.\n4. Append `.cclaw/state/delegation-events.jsonl` for scheduled/launched/acknowledged/completed/failed/waived/stale, then mirror current state in `.cclaw/state/delegation-log.json`. The ledger is current state; the event log is proof/audit.\n5. Treat completed role-switch rows without `evidenceRefs` as unresolved; treat native isolated completion without matching `spanId` + `dispatchId`/`workerRunId` + `ackTs` + `completedTs` as fake isolated completion. Native isolated rows are not a role-switch substitute and should reflect a real dispatched worker.\n\nThis is staged agent work backed by the harness-native subagent surfaces. Role-switch remains only a degraded fallback when that surface is unavailable in the active runtime.\n\n## Parallel research dispatch semantics\n\nDesign-stage research fleet uses the same parity model:\n\n- **Claude / Cursor**: dispatch all four research lenses in one turn\n (stack, features, architecture, pitfalls) and synthesize into\n `.cclaw/artifacts/02a-research.md`.\n- **OpenCode / Codex**: dispatch generated native subagents for the same\n four lenses and run independent lanes in parallel where the active runtime\n permits. Use role-switch with evidence only as a degraded fallback.\n\n## Semantic hook event coverage\n\n| Event | Claude | Cursor | OpenCode | Codex |\n|---|---|---|---|---|\n| `session_rehydrate` | SessionStart matcher startup|resume|clear|compact | sessionStart/sessionResume/sessionClear/sessionCompact | plugin event handlers + transform rehydration | SessionStart matcher startup|resume |\n| `pre_tool_prompt_guard` | PreToolUse -> prompt-guard | preToolUse -> prompt-guard | plugin tool.execute.before -> prompt-guard | PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts) |\n| `pre_tool_workflow_guard` | PreToolUse -> workflow-guard | preToolUse -> workflow-guard | plugin tool.execute.before -> workflow-guard | PreToolUse matcher Bash -> workflow-guard (Bash-only) |\n| `post_tool_context_monitor` | PostToolUse -> context-monitor | postToolUse -> context-monitor | plugin tool.execute.after -> context-monitor | PostToolUse matcher Bash -> context-monitor (Bash-only) |\n| `stop_handoff` | Stop -> stop-handoff | stop -> stop-handoff | plugin session.idle -> stop-handoff | Stop -> stop-handoff |\n| `precompact_compat` | PreCompact -> pre-compact | sessionCompact -> pre-compact | plugin session.compacted -> pre-compact | missing |\n| `strict_state_verify` | missing | missing | missing | UserPromptSubmit -> verify-current-state (blocks only in strict mode) |\n\n## Hook lifecycle aliases\n\nThe generated Node dispatcher accepts a small compatibility alias set for lifecycle names: `stop` and `stop-checkpoint` route to `stop-handoff`, `precompact` routes to `pre-compact`, and `session-rehydrate` routes to `session-start`. The `pre-compact` handler is intentionally a no-op compatibility marker; rehydration remains the `session-start` responsibility after compact events. Harness JSON should still emit the canonical handler names from `src/content/hook-manifest.ts`.\n\n## Hook event casing\n\nHook keys are intentionally harness-native and must not be normalized:\n\n| Harness | ID | Event key casing |\n|---|---|---|\n| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |\n| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |\n\nUse the exact event names from each harness schema. Treating all hooks as one\nshared casing silently breaks generated wiring.\n\n## Interpretation\n\n- `tier1`: full native delegation + structured asks + full hook surface.\n- `tier2`: usable flow with capability gaps; mandatory delegation can require waivers.\n- Codex-specific ceiling: `PreToolUse` can only intercept `Bash`. Direct\n `Write`/`Edit` to `.cclaw/state/flow-state.json` cannot be hard-blocked\n at hook level, so the canonical path is\n `node .cclaw/hooks/stage-complete.mjs <stage>` plus the non-blocking\n `UserPromptSubmit` state nudge.\n- In `strict` mode, Codex additionally runs the generated Node/runtime `verify-current-state` path on `UserPromptSubmit` as a fail-closed check. Advisory mode remains non-blocking, including when the generated local Node entrypoint is missing; doctor reports that install drift separately. This strict-only coverage is represented explicitly by the `strict_state_verify` semantic row above.\n\n## Shared command contract\n\nAll harnesses receive the same utility commands:\n\n- `/cc` - flow entry and resume\n- `/cc-next` - stage progression and post-ship closeout\n- `/cc-ideate` - ideate mode for ranked repo-improvement backlog\n- `/cc-view` - read-only router for status/tree/diff\n\nRead-only subcommands:\n- `/cc-view status` - visual flow snapshot\n- `/cc-view tree` - deep flow tree (stages, artifacts, stale markers)\n- `/cc-view diff` - before/after flow-state diff map\n\nOperational work is handled by `/cc`, `/cc-next`, `/cc-ideate`, `/cc-view`, and `node .cclaw/hooks/stage-complete.mjs <stage>` inside the installed harness runtime. `npx cclaw-cli` is the installer/support surface for init, sync, upgrade, doctor, and explicit/manual archive; the normal stage flow must not depend on a runtime `cclaw` binary in PATH.\n\nCritical-path stage order remains canonical:\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nEvery track then closes out through:\n`retro -> compound -> archive`\n\n## Stage -> skill folder mapping\n\n| Stage | Skill folder |\n|---|---|\n| `brainstorm` | `brainstorming` |\n| `scope` | `scope-shaping` |\n| `design` | `engineering-design-lock` |\n| `spec` | `specification-authoring` |\n| `plan` | `planning-and-task-breakdown` |\n| `tdd` | `test-driven-development` |\n| `review` | `two-layer-review` |\n| `ship` | `shipping-and-handoff` |\n\nThis map is generated from `src/constants.ts::STAGE_TO_SKILL_FOLDER` so\nskill-path naming stays explicit and stable even when stage ids differ from\nfolder names.\n\n## Install surfaces\n\nAlways generated:\n\n- `.cclaw/commands/*.md`\n- `.cclaw/skills/*/SKILL.md`\n- `.cclaw/state/*.json|*.jsonl`\n- `AGENTS.md` managed block\n\nHarness-specific additions:\n\n- `claude`: `.claude/commands/cc*.md`, `.claude/hooks/hooks.json`\n- `cursor`: `.cursor/commands/cc*.md`, `.cursor/hooks.json`, `.cursor/rules/cclaw-workflow.mdc`\n- `opencode`: `.opencode/commands/cc*.md`, `.opencode/plugins/cclaw-plugin.mjs`, opencode plugin registration with `permission.question: \"allow\"`; set `OPENCODE_ENABLE_QUESTION_TOOL=1` for ACP clients so structured asks can route through question tooling. Doctor validates the config permission and warns when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-next/SKILL.md`, `.agents/skills/cc-ideate/SKILL.md`, `.agents/skills/cc-view/SKILL.md`, `.codex/hooks.json` (Codex CLI reads `.agents/skills/` for custom skills and consumes `.codex/hooks.json` on v0.114+ when `[features] codex_hooks = true` is set in `~/.codex/config.toml`. `.codex/commands/` and the legacy `.agents/skills/cclaw-cc*/` layout from v0.39.x are auto-cleaned on sync.)\n\n## Runtime observability\n\n- `npx cclaw-cli doctor` validates shim, hook, and lifecycle surfaces against this capability model.\n- `/cc-view status` and `/cc-view tree` surface the same harness tier/fallback facts from the generated runtime metadata.\n\n## Delegation Proof Model\n\nRuntime state is split deliberately:\n\n- `.cclaw/state/delegation-log.json` is the compact current ledger used by stage gates and `/cc-view` summaries.\n- `.cclaw/state/delegation-events.jsonl` is append-only audit proof for `scheduled`, `launched`, `acknowledged`, `completed`, `failed`, `waived`, and `stale` lifecycle transitions.\n- `.cclaw/state/subagents.json` is a lightweight active-worker tracker for status/tree/doctor surfaces.\n- `.cclaw/hooks/delegation-record.mjs` is the generated helper for lifecycle rows/events. It validates required fields and emits JSON diagnostics with `--json`.\n\nIsolated completion requires `spanId`, `dispatchId` or `workerRunId`, `dispatchSurface`, `agentDefinitionPath`, `ackTs`, `launchedTs`, and `completedTs`. Cursor/generic dispatch and role-switch also require evidence refs when artifact evidence is the proof source. Legacy inferred completions remain readable, but doctor reports them as warnings because they predate event-log proof.\n\n## Reference Audit Appendix\n\nStatus meanings: `deep` = read for transferable implementation contract; `targeted` = inspected the relevant files only; `skimmed` = searched/read enough to classify; `not relevant` = intentionally excluded from implementation influence.\n\n| Reference path under `/Users/zuevrs/Downloads/references` | Status | Findings preserved |\n|---|---|---|\n| `evanklem-evanflow/skills/evanflow-coder-overseer/SKILL.md` | deep | Contract-first coder/overseer loop, reviewer reads code rather than worker narrative, and integration overseer pattern map cleanly onto cclaw subagent guidance. |\n| `evanklem-evanflow/agents/evanflow-coder.md` | targeted | Worker role is narrow: implement the pasted contract, avoid broad orchestration, and return evidence for overseer verification. |\n| `evanklem-evanflow/agents/evanflow-overseer.md` | targeted | Overseer validates actual code and acceptance evidence before controller marks work complete. |\n| `oh-my-codex/src/agents/native-config.ts` | deep | Native agent config shape supports explicit metadata/model/tool posture; cclaw should validate generated `.codex/agents/*.toml` shape instead of trusting file presence. |\n| `oh-my-codex/src/team/state/events.ts` and `src/team/state/workers.ts` | targeted | Append-only events plus worker state are useful as separate audit/current-state layers; cclaw mirrors that with `delegation-events.jsonl` and `subagents.json`. |\n| `oh-my-openagent/src/tools/delegate-task/tools.ts` | deep | Delegation should have an explicit dispatch surface and mode instead of relying on a prose claim that an agent was launched. |\n| `oh-my-openagent/src/tools/delegate-task/subagent-resolver.ts` | targeted | Agent discovery should be checked by doctor so missing/corrupt generated agent definitions are visible before dispatch. |\n| `oh-my-openagent/src/tools/delegate-task/prompt-builder.ts` | targeted | Prompt builders should include exact invocation/return contracts; cclaw generated worker prompts now carry ACK/result schemas. |\n| `giancarloerra-socraticode/**` | skimmed | Useful for workflow/e2e and graph-oriented contract testing, but not a subagent dispatch implementation reference; no runtime pattern imported. |\n| unrelated large reference trees not named above | not relevant | Searched/skipped because they did not contain flow/subagent/harness dispatch patterns relevant to this plan. |\n";
41
+ const head = "# Harness Integration Matrix\n\nGenerated from `src/harness-adapters.ts` capabilities and hook event mappings.";
42
+ return [
43
+ head + " For the end-to-end subagent dispatch model, proof sequence, controller/worker responsibilities, and future roadmap, see [`docs/subagent-flow.md`](./subagent-flow.md).\n\n## Capability tiers\n\n| Harness | ID | Tier | declaredSupport | runtimeLaunch | Fallback | proofRequired | proofSource | Hook surface | Structured ask |\n|---|---|---|---|---|---|---|---|---|---|\n| Claude Code | `claude` | `tier1` (full native automation) | full | native Task launch | native | spanId+dispatchId or workerRunId+ACK | `.cclaw/state/delegation-events.jsonl` + ledger | full | AskUserQuestion |\n| Cursor | `cursor` | `tier2` (supported with fallback paths) | generic | generic Task/Subagent role prompt | generic-dispatch | spanId+dispatchId/evidenceRefs | events + artifact evidenceRefs | full | AskQuestion |\n| OpenCode | `opencode` | `tier2` hooks, native dispatch declared | full | prompt-level launch via Task / `@agent` against `.opencode/agents` | native | spanId+dispatchId+ackTs+completedTs | `.opencode/agents/<agent>.md` + events | plugin | question |\n| OpenAI Codex | `codex` | `tier2` hooks, native dispatch declared | full | prompt-level request to spawn `.codex/agents` custom agents | native | spanId+dispatchId+ackTs+completedTs | `.codex/agents/<agent>.toml` + events | limited | request_user_input |\n",
44
+ perHarnessRecipeMarkdown(),
45
+ "\nFallback legend:\n\n- `native` \u2014 first-class named subagent dispatch (Claude).\n- `generic-dispatch` \u2014 generic Task dispatcher mapped to cclaw roles (Cursor).\n- `role-switch` \u2014 degraded fallback for a runtime where declared native/generic dispatch is unavailable; explicit role headers, artifact outputs, and non-empty delegation-log evidenceRefs are required.\n- `waiver` \u2014 no parity path; reserved for harnesses that cannot role-switch (none shipped).\n\n## Stage-Aware Native Dispatch Workflow\n\nOpenCode and Codex receive generated native isolated subagents. Use them before considering role-switch fallback:\n\n1. Use the active stage skill's generated dispatch table as the source of truth.\n2. OpenCode: invoke `.opencode/agents/<agent>.md` via Task or `@<agent>`; Codex: ask Codex to spawn `.codex/agents/<agent>.toml` by name, in parallel when lanes are independent.\n3. Load `.cclaw/agents/<agent>.md`, execute only that role's stage task, and write outputs into the active stage artifact.\n4. Append `.cclaw/state/delegation-events.jsonl` for scheduled/launched/acknowledged/completed/failed/waived/stale, then mirror current state in `.cclaw/state/delegation-log.json`. The ledger is current state; the event log is proof/audit.\n5. Treat completed role-switch rows without `evidenceRefs` as unresolved; treat native isolated completion without matching `spanId` + `dispatchId`/`workerRunId` + `ackTs` + `completedTs` as fake isolated completion. Native isolated rows are not a role-switch substitute and should reflect a real dispatched worker.\n\nThis is staged agent work backed by the harness-native subagent surfaces. Role-switch remains only a degraded fallback when that surface is unavailable in the active runtime.\n\n## Parallel research dispatch semantics\n\nDesign-stage research fleet uses the same parity model:\n\n- **Claude / Cursor**: dispatch all four research lenses in one turn\n (stack, features, architecture, pitfalls) and synthesize into\n `.cclaw/artifacts/02a-research.md`.\n- **OpenCode / Codex**: dispatch generated native subagents for the same\n four lenses and run independent lanes in parallel where the active runtime\n permits. Use role-switch with evidence only as a degraded fallback.\n\n## Semantic hook event coverage\n\n| Event | Claude | Cursor | OpenCode | Codex |\n|---|---|---|---|---|\n| `session_rehydrate` | SessionStart matcher startup|resume|clear|compact | sessionStart/sessionResume/sessionClear/sessionCompact | plugin event handlers + transform rehydration | SessionStart matcher startup|resume |\n| `pre_tool_prompt_guard` | PreToolUse -> prompt-guard | preToolUse -> prompt-guard | plugin tool.execute.before -> prompt-guard | PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts) |\n| `pre_tool_workflow_guard` | PreToolUse -> workflow-guard | preToolUse -> workflow-guard | plugin tool.execute.before -> workflow-guard | PreToolUse matcher Bash -> workflow-guard (Bash-only) |\n| `post_tool_context_monitor` | PostToolUse -> context-monitor | postToolUse -> context-monitor | plugin tool.execute.after -> context-monitor | PostToolUse matcher Bash -> context-monitor (Bash-only) |\n| `stop_handoff` | Stop -> stop-handoff | stop -> stop-handoff | plugin session.idle -> stop-handoff | Stop -> stop-handoff |\n| `precompact_compat` | PreCompact -> pre-compact | sessionCompact -> pre-compact | plugin session.compacted -> pre-compact | missing |\n| `strict_state_verify` | missing | missing | missing | UserPromptSubmit -> verify-current-state (blocks only in strict mode) |\n\n## Hook lifecycle aliases\n\nThe generated Node dispatcher accepts a small compatibility alias set for lifecycle names: `stop` and `stop-checkpoint` route to `stop-handoff`, `precompact` routes to `pre-compact`, and `session-rehydrate` routes to `session-start`. The `pre-compact` handler is intentionally a no-op compatibility marker; rehydration remains the `session-start` responsibility after compact events. Harness JSON should still emit the canonical handler names from `src/content/hook-manifest.ts`.\n\n## Hook event casing\n\nHook keys are intentionally harness-native and must not be normalized:\n\n| Harness | ID | Event key casing |\n|---|---|---|\n| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |\n| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |\n\nUse the exact event names from each harness schema. Treating all hooks as one\nshared casing silently breaks generated wiring.\n\n## Interpretation\n\n- `tier1`: full native delegation + structured asks + full hook surface.\n- `tier2`: usable flow with capability gaps; mandatory delegation can require waivers.\n- Codex-specific ceiling: `PreToolUse` can only intercept `Bash`. Direct\n `Write`/`Edit` to `.cclaw/state/flow-state.json` cannot be hard-blocked\n at hook level, so the canonical path is\n `node .cclaw/hooks/stage-complete.mjs <stage>` plus the non-blocking\n `UserPromptSubmit` state nudge.\n- In `strict` mode, Codex additionally runs the generated Node/runtime `verify-current-state` path on `UserPromptSubmit` as a fail-closed check. Advisory mode remains non-blocking, including when the generated local Node entrypoint is missing; doctor reports that install drift separately. This strict-only coverage is represented explicitly by the `strict_state_verify` semantic row above.\n\n## Shared command contract\n\nAll harnesses receive the same utility commands:\n\n- `/cc` - flow entry and resume\n- `/cc-next` - stage progression and post-ship closeout\n- `/cc-ideate` - ideate mode for ranked repo-improvement backlog\n- `/cc-view` - read-only router for status/tree/diff\n\nRead-only subcommands:\n- `/cc-view status` - visual flow snapshot\n- `/cc-view tree` - deep flow tree (stages, artifacts, stale markers)\n- `/cc-view diff` - before/after flow-state diff map\n\nOperational work is handled by `/cc`, `/cc-next`, `/cc-ideate`, `/cc-view`, and `node .cclaw/hooks/stage-complete.mjs <stage>` inside the installed harness runtime. `npx cclaw-cli` is the installer/support surface for init, sync, upgrade, doctor, and explicit/manual archive; the normal stage flow must not depend on a runtime `cclaw` binary in PATH.\n\nCritical-path stage order remains canonical:\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nEvery track then closes out through:\n`retro -> compound -> archive`\n\n## Stage -> skill folder mapping\n\n| Stage | Skill folder |\n|---|---|\n| `brainstorm` | `brainstorming` |\n| `scope` | `scope-shaping` |\n| `design` | `engineering-design-lock` |\n| `spec` | `specification-authoring` |\n| `plan` | `planning-and-task-breakdown` |\n| `tdd` | `test-driven-development` |\n| `review` | `two-layer-review` |\n| `ship` | `shipping-and-handoff` |\n\nThis map is generated from `src/constants.ts::STAGE_TO_SKILL_FOLDER` so\nskill-path naming stays explicit and stable even when stage ids differ from\nfolder names.\n\n## Install surfaces\n\nAlways generated:\n\n- `.cclaw/commands/*.md`\n- `.cclaw/skills/*/SKILL.md`\n- `.cclaw/state/*.json|*.jsonl`\n- `AGENTS.md` managed block\n\nHarness-specific additions:\n\n- `claude`: `.claude/commands/cc*.md`, `.claude/hooks/hooks.json`\n- `cursor`: `.cursor/commands/cc*.md`, `.cursor/hooks.json`, `.cursor/rules/cclaw-workflow.mdc`\n- `opencode`: `.opencode/commands/cc*.md`, `.opencode/plugins/cclaw-plugin.mjs`, opencode plugin registration with `permission.question: \"allow\"`; set `OPENCODE_ENABLE_QUESTION_TOOL=1` for ACP clients so structured asks can route through question tooling. Doctor validates the config permission and warns when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-next/SKILL.md`, `.agents/skills/cc-ideate/SKILL.md`, `.agents/skills/cc-view/SKILL.md`, `.codex/hooks.json` (Codex CLI reads `.agents/skills/` for custom skills and consumes `.codex/hooks.json` on v0.114+ when `[features] codex_hooks = true` is set in `~/.codex/config.toml`. `.codex/commands/` and the legacy `.agents/skills/cclaw-cc*/` layout from v0.39.x are auto-cleaned on sync.)\n\n## Runtime observability\n\n- `npx cclaw-cli doctor` validates shim, hook, and lifecycle surfaces against this capability model.\n- `/cc-view status` and `/cc-view tree` surface the same harness tier/fallback facts from the generated runtime metadata.\n\n## Delegation Proof Model\n\nRuntime state is split deliberately:\n\n- `.cclaw/state/delegation-log.json` is the compact current ledger used by stage gates and `/cc-view` summaries.\n- `.cclaw/state/delegation-events.jsonl` is append-only audit proof for `scheduled`, `launched`, `acknowledged`, `completed`, `failed`, `waived`, and `stale` lifecycle transitions.\n- `.cclaw/state/subagents.json` is a lightweight active-worker tracker for status/tree/doctor surfaces.\n- `.cclaw/hooks/delegation-record.mjs` is the generated helper for lifecycle rows/events. It validates required fields and emits JSON diagnostics with `--json`.\n\nIsolated completion requires `spanId`, `dispatchId` or `workerRunId`, `dispatchSurface`, `agentDefinitionPath`, `ackTs`, `launchedTs`, and `completedTs`. Cursor/generic dispatch and role-switch also require evidence refs when artifact evidence is the proof source. Legacy inferred completions remain readable, but doctor reports them as warnings because they predate event-log proof.\n\n## Reference Audit Appendix\n\nStatus meanings: `deep` = read for transferable implementation contract; `targeted` = inspected the relevant files only; `skimmed` = searched/read enough to classify; `not relevant` = intentionally excluded from implementation influence.\n\n| Reference path under `/Users/zuevrs/Downloads/references` | Status | Findings preserved |\n|---|---|---|\n| `evanklem-evanflow/skills/evanflow-coder-overseer/SKILL.md` | deep | Contract-first coder/overseer loop, reviewer reads code rather than worker narrative, and integration overseer pattern map cleanly onto cclaw subagent guidance. |\n| `evanklem-evanflow/agents/evanflow-coder.md` | targeted | Worker role is narrow: implement the pasted contract, avoid broad orchestration, and return evidence for overseer verification. |\n| `evanklem-evanflow/agents/evanflow-overseer.md` | targeted | Overseer validates actual code and acceptance evidence before controller marks work complete. |\n| `oh-my-codex/src/agents/native-config.ts` | deep | Native agent config shape supports explicit metadata/model/tool posture; cclaw should validate generated `.codex/agents/*.toml` shape instead of trusting file presence. |\n| `oh-my-codex/src/team/state/events.ts` and `src/team/state/workers.ts` | targeted | Append-only events plus worker state are useful as separate audit/current-state layers; cclaw mirrors that with `delegation-events.jsonl` and `subagents.json`. |\n| `oh-my-openagent/src/tools/delegate-task/tools.ts` | deep | Delegation should have an explicit dispatch surface and mode instead of relying on a prose claim that an agent was launched. |\n| `oh-my-openagent/src/tools/delegate-task/subagent-resolver.ts` | targeted | Agent discovery should be checked by doctor so missing/corrupt generated agent definitions are visible before dispatch. |\n| `oh-my-openagent/src/tools/delegate-task/prompt-builder.ts` | targeted | Prompt builders should include exact invocation/return contracts; cclaw generated worker prompts now carry ACK/result schemas. |\n| `giancarloerra-socraticode/**` | skimmed | Useful for workflow/e2e and graph-oriented contract testing, but not a subagent dispatch implementation reference; no runtime pattern imported. |\n| unrelated large reference trees not named above | not relevant | Searched/skipped because they did not contain flow/subagent/harness dispatch patterns relevant to this plan. |\n"
46
+ ].join("");
3
47
  }
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { RUNTIME_ROOT } from "../constants.js";
5
+ import { DELEGATION_DISPATCH_SURFACES, DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES } from "../delegation.js";
5
6
  function resolveCliEntrypointForGeneratedHook() {
6
7
  const here = fileURLToPath(import.meta.url);
7
8
  const candidates = [
@@ -272,6 +273,10 @@ import process from "node:process";
272
273
  const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
273
274
  const VALID_STATUSES = new Set(["scheduled", "launched", "acknowledged", "completed", "failed", "waived", "stale"]);
274
275
  const TERMINAL = new Set(["completed", "failed", "waived", "stale"]);
276
+ const VALID_DISPATCH_SURFACES = ${JSON.stringify([...DELEGATION_DISPATCH_SURFACES])};
277
+ const VALID_DISPATCH_SURFACES_SET = new Set(VALID_DISPATCH_SURFACES);
278
+ const SURFACE_PATH_PREFIXES = ${JSON.stringify(DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES)};
279
+ const LEDGER_SCHEMA_VERSION = 3;
275
280
 
276
281
  function parseArgs(argv) {
277
282
  const args = {};
@@ -354,55 +359,58 @@ function hasPriorAck(events, args, runId) {
354
359
  }
355
360
 
356
361
  function usage() {
357
- process.stderr.write("Usage: node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--json]\\n");
362
+ process.stderr.write([
363
+ "Usage:",
364
+ " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--json]",
365
+ " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--json]",
366
+ "",
367
+ "Allowed --dispatch-surface values:",
368
+ " " + VALID_DISPATCH_SURFACES.join(", "),
369
+ "",
370
+ "Per-surface allowed --agent-definition-path prefixes:",
371
+ ...VALID_DISPATCH_SURFACES.map((surface) => " " + surface + ": " + (SURFACE_PATH_PREFIXES[surface].length === 0 ? "(any)" : SURFACE_PATH_PREFIXES[surface].join(", "))),
372
+ ""
373
+ ].join("\\n") + "\\n");
358
374
  }
359
375
 
360
- async function main() {
361
- const args = parseArgs(process.argv.slice(2));
362
- const json = args.json !== undefined;
363
- const problems = [];
364
- if (!args.stage) problems.push("missing --stage");
365
- if (!args.agent) problems.push("missing --agent");
366
- if (args.mode !== "mandatory" && args.mode !== "proactive") problems.push("--mode must be mandatory or proactive");
367
- if (!VALID_STATUSES.has(args.status)) problems.push("invalid --status");
368
- if (!args["span-id"]) problems.push("missing --span-id");
369
- if (args.status === "waived" && !args["waiver-reason"]) problems.push("waived status requires --waiver-reason");
370
- if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
371
- for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
372
- if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
373
- }
374
- }
375
- if (args.status === "completed" && args["dispatch-surface"] === "role-switch" && !args["evidence-ref"]) {
376
- problems.push("completed role-switch status requires --evidence-ref");
377
- }
378
- if (problems.length > 0) {
379
- if (json) process.stdout.write(JSON.stringify({ ok: false, problems }, null, 2) + "\\n");
380
- else {
381
- usage();
382
- process.stderr.write("[cclaw] delegation-record: " + problems.join("; ") + "\\n");
383
- }
384
- process.exitCode = 1;
385
- return;
376
+ function emitProblems(problems, json, code) {
377
+ const exitCode = typeof code === "number" ? code : 1;
378
+ if (json) {
379
+ process.stdout.write(JSON.stringify({ ok: false, problems, allowedDispatchSurfaces: VALID_DISPATCH_SURFACES }, null, 2) + "\\n");
380
+ } else {
381
+ usage();
382
+ process.stderr.write("[cclaw] delegation-record: " + problems.join("; ") + "\\n");
386
383
  }
384
+ process.exitCode = exitCode;
385
+ }
387
386
 
388
- const root = await detectRoot();
389
- const now = new Date().toISOString();
390
- const runId = await readRunId(root);
391
- if (args.status === "completed" && args["dispatch-surface"] !== "role-switch" && !args["ack-ts"]) {
392
- const priorEvents = await readDelegationEvents(root);
393
- if (!hasPriorAck(priorEvents, args, runId)) {
394
- const ackProblem = "completed isolated/generic status requires prior acknowledged event for same span or --ack-ts";
395
- if (json) process.stdout.write(JSON.stringify({ ok: false, problems: [ackProblem] }, null, 2) + "\\n");
396
- else {
397
- usage();
398
- process.stderr.write("[cclaw] delegation-record: " + ackProblem + "\\n");
399
- }
400
- process.exitCode = 1;
401
- return;
402
- }
387
+ function normalizeRelPath(value) {
388
+ return String(value || "").replace(/\\\\/gu, "/").replace(/^\\.\\//u, "");
389
+ }
390
+
391
+ function dispatchSurfaceMatchesPath(surface, agentDefinitionPath) {
392
+ const allowed = SURFACE_PATH_PREFIXES[surface] || [];
393
+ if (allowed.length === 0) return true;
394
+ const normalized = normalizeRelPath(agentDefinitionPath);
395
+ return allowed.some((prefix) => normalized === prefix.replace(/\\/$/u, "") || normalized.startsWith(prefix));
396
+ }
397
+
398
+ async function pathExists(filePath) {
399
+ try {
400
+ const stat = await fs.stat(filePath);
401
+ return stat.isFile() || stat.isDirectory();
402
+ } catch {
403
+ return false;
403
404
  }
404
- const status = args.status;
405
- const row = {
405
+ }
406
+
407
+ function buildRow(args, status, runId, now) {
408
+ const fulfillmentMode = args["dispatch-surface"] === "role-switch"
409
+ ? "role-switch"
410
+ : args["dispatch-surface"] === "cursor-task" || args["dispatch-surface"] === "generic-task"
411
+ ? "generic-dispatch"
412
+ : "isolated";
413
+ return {
406
414
  stage: args.stage,
407
415
  agent: args.agent,
408
416
  mode: args.mode,
@@ -412,7 +420,7 @@ async function main() {
412
420
  workerRunId: args["worker-run-id"],
413
421
  dispatchSurface: args["dispatch-surface"],
414
422
  agentDefinitionPath: args["agent-definition-path"],
415
- fulfillmentMode: args["dispatch-surface"] === "role-switch" ? "role-switch" : args["dispatch-surface"] === "cursor-task" || args["dispatch-surface"] === "generic-task" ? "generic-dispatch" : "isolated",
423
+ fulfillmentMode,
416
424
  waiverReason: args["waiver-reason"],
417
425
  evidenceRefs: args["evidence-ref"] ? [args["evidence-ref"]] : [],
418
426
  runId,
@@ -422,30 +430,202 @@ async function main() {
422
430
  ackTs: args["ack-ts"] || (status === "acknowledged" ? now : undefined),
423
431
  completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
424
432
  endTs: TERMINAL.has(status) ? now : undefined,
425
- schemaVersion: 1
433
+ schemaVersion: LEDGER_SCHEMA_VERSION
426
434
  };
427
- const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
428
- const event = { ...clean, event: status, eventTs: now };
435
+ }
436
+
437
+ async function persistEntry(root, runId, clean, event, options = {}) {
429
438
  const stateDir = path.join(root, RUNTIME_ROOT, "state");
430
439
  await fs.mkdir(stateDir, { recursive: true });
431
440
  await fs.appendFile(path.join(stateDir, "delegation-events.jsonl"), JSON.stringify(event) + "\\n", { encoding: "utf8", mode: 0o600 });
432
441
 
433
442
  const ledgerPath = path.join(stateDir, "delegation-log.json");
434
- let ledger = { runId, entries: [] };
443
+ let ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
435
444
  try {
436
445
  ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
437
446
  if (!Array.isArray(ledger.entries)) ledger.entries = [];
438
447
  } catch {
439
- ledger = { runId, entries: [] };
448
+ ledger = { runId, entries: [], schemaVersion: LEDGER_SCHEMA_VERSION };
440
449
  }
441
- if (!ledger.entries.some((entry) => entry.spanId === clean.spanId && entry.status === clean.status)) {
450
+
451
+ // Rerecord semantics: replace any pre-existing row with the same spanId
452
+ // (regardless of its status) so the legacy v1/v2 row is upgraded to v3
453
+ // shape on disk. The append path keeps the historical dedup semantics:
454
+ // an exact (spanId, status) duplicate is dropped to keep retried hooks
455
+ // idempotent.
456
+ if (options.replaceBySpanId) {
457
+ ledger.entries = ledger.entries.filter((entry) => entry.spanId !== clean.spanId);
442
458
  ledger.entries.push(clean);
443
459
  ledger.runId = runId;
460
+ ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
461
+ await fs.writeFile(ledgerPath, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
462
+ } else if (!ledger.entries.some((entry) => entry.spanId === clean.spanId && entry.status === clean.status)) {
463
+ ledger.entries.push(clean);
464
+ ledger.runId = runId;
465
+ ledger.schemaVersion = LEDGER_SCHEMA_VERSION;
444
466
  await fs.writeFile(ledgerPath, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
445
467
  }
446
468
 
447
469
  const active = ledger.entries.filter((entry) => ["scheduled", "launched", "acknowledged"].includes(entry.status));
448
- await fs.writeFile(path.join(stateDir, "subagents.json"), JSON.stringify({ active, updatedAt: now }, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
470
+ await fs.writeFile(path.join(stateDir, "subagents.json"), JSON.stringify({ active, updatedAt: event.eventTs }, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
471
+ }
472
+
473
+ async function findLegacyEntry(root, spanId) {
474
+ const ledgerPath = path.join(root, RUNTIME_ROOT, "state", "delegation-log.json");
475
+ let ledger;
476
+ try {
477
+ ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
478
+ } catch {
479
+ return null;
480
+ }
481
+ if (!ledger || !Array.isArray(ledger.entries)) return null;
482
+ return ledger.entries.find((entry) => entry && entry.spanId === spanId) || null;
483
+ }
484
+
485
+ async function runRerecord(args, json) {
486
+ const problems = [];
487
+ for (const key of ["span-id", "dispatch-id", "dispatch-surface", "agent-definition-path"]) {
488
+ if (!args[key]) problems.push("missing --" + key);
489
+ }
490
+ if (args["dispatch-surface"] && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
491
+ problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
492
+ }
493
+ if (problems.length > 0) {
494
+ emitProblems(problems, json, 2);
495
+ return;
496
+ }
497
+ const root = await detectRoot();
498
+ const now = new Date().toISOString();
499
+ const runId = await readRunId(root);
500
+ const legacyEntry = await findLegacyEntry(root, args["span-id"]);
501
+ if (!legacyEntry) {
502
+ emitProblems(["no legacy ledger entry found for --span-id=" + args["span-id"]], json, 1);
503
+ return;
504
+ }
505
+ if (args["dispatch-surface"] !== "role-switch") {
506
+ if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
507
+ const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
508
+ emitProblems([
509
+ "--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")"
510
+ ], json, 2);
511
+ return;
512
+ }
513
+ const exists = await pathExists(path.join(root, args["agent-definition-path"]));
514
+ if (!exists) {
515
+ emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
516
+ return;
517
+ }
518
+ }
519
+ const merged = {
520
+ stage: legacyEntry.stage,
521
+ agent: legacyEntry.agent,
522
+ mode: legacyEntry.mode || "mandatory",
523
+ "span-id": args["span-id"],
524
+ "dispatch-id": args["dispatch-id"],
525
+ "worker-run-id": args["worker-run-id"] || legacyEntry.workerRunId,
526
+ "dispatch-surface": args["dispatch-surface"],
527
+ "agent-definition-path": args["agent-definition-path"],
528
+ "ack-ts": args["ack-ts"] || legacyEntry.ackTs || now,
529
+ "completed-ts": args["completed-ts"] || legacyEntry.completedTs || now,
530
+ "launched-ts": args["launched-ts"] || legacyEntry.launchedTs || now
531
+ };
532
+ const status = "completed";
533
+ const clean = Object.fromEntries(Object.entries(buildRow(merged, status, runId, now)).filter(([, value]) => value !== undefined));
534
+ clean.fulfillmentMode = clean.dispatchSurface === "role-switch" ? "role-switch" : (clean.dispatchSurface === "cursor-task" || clean.dispatchSurface === "generic-task" ? "generic-dispatch" : "isolated");
535
+ const event = { ...clean, event: status, eventTs: now, rerecord: true };
536
+ await persistEntry(root, runId, clean, event, { replaceBySpanId: true });
537
+ process.stdout.write(JSON.stringify({ ok: true, event, rerecord: true }, null, 2) + "\\n");
538
+ }
539
+
540
+ async function main() {
541
+ const args = parseArgs(process.argv.slice(2));
542
+ const json = args.json !== undefined;
543
+
544
+ if (args.rerecord) {
545
+ await runRerecord(args, json);
546
+ return;
547
+ }
548
+
549
+ const problems = [];
550
+ if (!args.stage) problems.push("missing --stage");
551
+ if (!args.agent) problems.push("missing --agent");
552
+ if (args.mode !== "mandatory" && args.mode !== "proactive") problems.push("--mode must be mandatory or proactive");
553
+ if (!VALID_STATUSES.has(args.status)) problems.push("invalid --status");
554
+ if (!args["span-id"]) problems.push("missing --span-id");
555
+ if (args.status === "waived" && !args["waiver-reason"]) problems.push("waived status requires --waiver-reason");
556
+
557
+ // Strict --dispatch-surface enum validation: any provided surface must be
558
+ // in the canonical allow-list. Do this BEFORE we use the value to gate
559
+ // completed/role-switch fields.
560
+ if (args["dispatch-surface"] !== undefined && !VALID_DISPATCH_SURFACES_SET.has(args["dispatch-surface"])) {
561
+ problems.push("invalid --dispatch-surface (allowed: " + VALID_DISPATCH_SURFACES.join(", ") + ")");
562
+ emitProblems(problems, json, 2);
563
+ return;
564
+ }
565
+
566
+ if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
567
+ for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
568
+ if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
569
+ }
570
+ }
571
+ if (args.status === "completed" && args["dispatch-surface"] === "role-switch" && !args["evidence-ref"]) {
572
+ problems.push("completed role-switch status requires --evidence-ref");
573
+ }
574
+
575
+ // Validate --agent-definition-path against the surface and on-disk
576
+ // existence whenever both are provided.
577
+ if (args["dispatch-surface"] && args["agent-definition-path"] && args["dispatch-surface"] !== "role-switch" && args["dispatch-surface"] !== "manual") {
578
+ if (!dispatchSurfaceMatchesPath(args["dispatch-surface"], args["agent-definition-path"])) {
579
+ const allowedPrefixes = SURFACE_PATH_PREFIXES[args["dispatch-surface"]];
580
+ problems.push("--agent-definition-path does not lie under any allowed prefix for --dispatch-surface=" + args["dispatch-surface"] + " (expected one of: " + (allowedPrefixes.join(", ") || "(any)") + ")");
581
+ }
582
+ }
583
+
584
+ if (problems.length > 0) {
585
+ emitProblems(problems, json, 2);
586
+ return;
587
+ }
588
+
589
+ const root = await detectRoot();
590
+ const now = new Date().toISOString();
591
+ const runId = await readRunId(root);
592
+
593
+ // For completed isolated/generic rows, --agent-definition-path must
594
+ // resolve to an existing file or directory inside the project. This
595
+ // catches typos and stale generated agent paths before they enter the
596
+ // ledger. Skipped for role-switch (no agent file is generated) and
597
+ // manual (intentionally free-form).
598
+ if (
599
+ args.status === "completed" &&
600
+ args["dispatch-surface"] &&
601
+ args["dispatch-surface"] !== "role-switch" &&
602
+ args["dispatch-surface"] !== "manual" &&
603
+ args["agent-definition-path"]
604
+ ) {
605
+ const exists = await pathExists(path.join(root, args["agent-definition-path"]));
606
+ if (!exists) {
607
+ emitProblems(["--agent-definition-path does not exist on disk: " + args["agent-definition-path"]], json, 2);
608
+ return;
609
+ }
610
+ }
611
+
612
+ // Completed isolated/generic rows require explicit --ack-ts OR a prior
613
+ // acknowledged event for the same span. fulfillmentMode=isolated cannot
614
+ // be claimed without an ACK timestamp anchor.
615
+ if (args.status === "completed" && args["dispatch-surface"] !== "role-switch" && !args["ack-ts"]) {
616
+ const priorEvents = await readDelegationEvents(root);
617
+ if (!hasPriorAck(priorEvents, args, runId)) {
618
+ const ackProblem = "completed isolated/generic status requires prior acknowledged event for same span or --ack-ts";
619
+ emitProblems([ackProblem], json, 2);
620
+ return;
621
+ }
622
+ }
623
+
624
+ const status = args.status;
625
+ const row = buildRow(args, status, runId, now);
626
+ const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
627
+ const event = { ...clean, event: status, eventTs: now };
628
+ await persistEntry(root, runId, clean, event);
449
629
  process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
450
630
  }
451
631
 
@@ -1,3 +1,12 @@
1
1
  import { type FlowStage, type FlowTrack } from "../types.js";
2
+ export declare const FORBIDDEN_SYCOPHANCY_PHRASES: string[];
3
+ export declare const FORBIDDEN_PLACEHOLDER_TOKENS: string[];
4
+ export declare const CONFIDENCE_FINDING_REGEX_SOURCE = "\\[P[123]\\]\\s*\\(confidence:\\s*\\d{1,2}/10\\)\\s+[^\\s]+(?::\\d+)?\\s+\u2014";
5
+ export declare function stopPerIssueBlock(): string;
6
+ export declare function confidenceCalibrationBlock(): string;
7
+ export declare function outsideVoiceSlotBlock(): string;
8
+ export declare function antiSycophancyBlock(): string;
9
+ export declare function noPlaceholdersBlock(): string;
10
+ export declare function watchedFailProofBlock(): string;
2
11
  export declare function stageSkillFolder(stage: FlowStage): string;
3
12
  export declare function stageSkillMarkdown(stage: FlowStage, track?: FlowTrack): string;