cclaw-cli 0.51.25 → 0.51.26

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.
@@ -30,6 +30,24 @@ const DOC_RETURN_SCHEMA = {
30
30
  requiredFields: ["status", "filesUpdated", "summary", "evidenceRefs", "openQuestions"],
31
31
  evidenceFields: ["filesUpdated", "evidenceRefs"]
32
32
  };
33
+ function workerAckContract() {
34
+ return `## Worker ACK Contract
35
+
36
+ Before doing substantive work, return an ACK object that the parent can record:
37
+
38
+ \`\`\`json
39
+ {
40
+ "status": "ACK",
41
+ "spanId": "<parent spanId>",
42
+ "dispatchId": "<parent dispatchId or workerRunId>",
43
+ "dispatchSurface": "claude-task|cursor-task|opencode-agent|codex-agent|generic-task|role-switch",
44
+ "agentDefinitionPath": ".cclaw/agents/<agent>.md or harness generated path",
45
+ "ackTs": "<ISO timestamp>"
46
+ }
47
+ \`\`\`
48
+
49
+ Finish with the required return schema plus the same \`spanId\` and \`dispatchId\`. The parent must not claim isolated completion unless ACK/result proof matches the ledger/event span.`;
50
+ }
33
51
  function formatReturnSchema(schema) {
34
52
  return [
35
53
  `- Status field: \`${schema.statusField}\``,
@@ -446,9 +464,11 @@ ${agent.body}
446
464
  - Mode: ${agent.activation}
447
465
  - Related stages: ${relatedStages}
448
466
 
467
+ ${workerAckContract()}
468
+
449
469
  ## Required Return Schema
450
470
 
451
- STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated.
471
+ STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated. Include \`spanId\`, \`dispatchId\` or \`workerRunId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and lifecycle timestamps when provided by the parent.
452
472
 
453
473
  ${formatReturnSchema(agent.returnSchema)}
454
474
 
@@ -0,0 +1 @@
1
+ export declare function harnessIntegrationDocMarkdown(): string;
@@ -0,0 +1,3 @@
1
+ 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";
3
+ }
@@ -1,5 +1,6 @@
1
1
  export declare function startFlowScript(): string;
2
2
  export declare function stageCompleteScript(): string;
3
+ export declare function delegationRecordScript(): string;
3
4
  export declare function runHookCmdScript(): string;
4
5
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
5
6
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
@@ -260,6 +260,195 @@ async function main() {
260
260
  });
261
261
  }
262
262
 
263
+ void main();
264
+ `;
265
+ }
266
+ export function delegationRecordScript() {
267
+ return `#!/usr/bin/env node
268
+ import fs from "node:fs/promises";
269
+ import path from "node:path";
270
+ import process from "node:process";
271
+
272
+ const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
273
+ const VALID_STATUSES = new Set(["scheduled", "launched", "acknowledged", "completed", "failed", "waived", "stale"]);
274
+ const TERMINAL = new Set(["completed", "failed", "waived", "stale"]);
275
+
276
+ function parseArgs(argv) {
277
+ const args = {};
278
+ for (const raw of argv) {
279
+ const valueMatch = /^--([^=]+)=(.*)$/u.exec(raw);
280
+ if (valueMatch) {
281
+ args[valueMatch[1]] = valueMatch[2];
282
+ continue;
283
+ }
284
+ const flagMatch = /^--([^=]+)$/u.exec(raw);
285
+ if (flagMatch) args[flagMatch[1]] = true;
286
+ }
287
+ return args;
288
+ }
289
+
290
+ async function exists(filePath) {
291
+ try {
292
+ await fs.access(filePath);
293
+ return true;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+
299
+ async function detectRoot() {
300
+ const candidates = [
301
+ process.env.CCLAW_PROJECT_ROOT,
302
+ process.env.CLAUDE_PROJECT_DIR,
303
+ process.env.CURSOR_PROJECT_DIR,
304
+ process.env.CURSOR_PROJECT_ROOT,
305
+ process.env.OPENCODE_PROJECT_DIR,
306
+ process.env.OPENCODE_PROJECT_ROOT,
307
+ process.cwd()
308
+ ].filter((value) => typeof value === "string" && value.length > 0);
309
+ for (const candidate of candidates) {
310
+ if (await exists(path.join(candidate, RUNTIME_ROOT))) return candidate;
311
+ }
312
+ return candidates[0] || process.cwd();
313
+ }
314
+
315
+ async function readRunId(root) {
316
+ try {
317
+ const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
318
+ const parsed = JSON.parse(raw);
319
+ return typeof parsed.activeRunId === "string" ? parsed.activeRunId : "unknown-run";
320
+ } catch {
321
+ return "unknown-run";
322
+ }
323
+ }
324
+
325
+ async function readDelegationEvents(root) {
326
+ try {
327
+ const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-events.jsonl"), "utf8");
328
+ return raw
329
+ .split(/\\r?\\n/u)
330
+ .filter((line) => line.trim().length > 0)
331
+ .map((line) => {
332
+ try {
333
+ return JSON.parse(line);
334
+ } catch {
335
+ return null;
336
+ }
337
+ })
338
+ .filter((event) => event && typeof event === "object");
339
+ } catch {
340
+ return [];
341
+ }
342
+ }
343
+
344
+ function hasPriorAck(events, args, runId) {
345
+ return events.some((event) =>
346
+ event.runId === runId &&
347
+ event.stage === args.stage &&
348
+ event.agent === args.agent &&
349
+ event.spanId === args["span-id"] &&
350
+ event.event === "acknowledged" &&
351
+ typeof event.ackTs === "string" &&
352
+ event.ackTs.length > 0
353
+ );
354
+ }
355
+
356
+ 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");
358
+ }
359
+
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;
386
+ }
387
+
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
+ }
403
+ }
404
+ const status = args.status;
405
+ const row = {
406
+ stage: args.stage,
407
+ agent: args.agent,
408
+ mode: args.mode,
409
+ status,
410
+ spanId: args["span-id"],
411
+ dispatchId: args["dispatch-id"],
412
+ workerRunId: args["worker-run-id"],
413
+ dispatchSurface: args["dispatch-surface"],
414
+ 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",
416
+ waiverReason: args["waiver-reason"],
417
+ evidenceRefs: args["evidence-ref"] ? [args["evidence-ref"]] : [],
418
+ runId,
419
+ startTs: now,
420
+ ts: now,
421
+ launchedTs: args["launched-ts"] || (status === "launched" ? now : undefined),
422
+ ackTs: args["ack-ts"] || (status === "acknowledged" ? now : undefined),
423
+ completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
424
+ endTs: TERMINAL.has(status) ? now : undefined,
425
+ schemaVersion: 1
426
+ };
427
+ const clean = Object.fromEntries(Object.entries(row).filter(([, value]) => value !== undefined));
428
+ const event = { ...clean, event: status, eventTs: now };
429
+ const stateDir = path.join(root, RUNTIME_ROOT, "state");
430
+ await fs.mkdir(stateDir, { recursive: true });
431
+ await fs.appendFile(path.join(stateDir, "delegation-events.jsonl"), JSON.stringify(event) + "\\n", { encoding: "utf8", mode: 0o600 });
432
+
433
+ const ledgerPath = path.join(stateDir, "delegation-log.json");
434
+ let ledger = { runId, entries: [] };
435
+ try {
436
+ ledger = JSON.parse(await fs.readFile(ledgerPath, "utf8"));
437
+ if (!Array.isArray(ledger.entries)) ledger.entries = [];
438
+ } catch {
439
+ ledger = { runId, entries: [] };
440
+ }
441
+ if (!ledger.entries.some((entry) => entry.spanId === clean.spanId && entry.status === clean.status)) {
442
+ ledger.entries.push(clean);
443
+ ledger.runId = runId;
444
+ await fs.writeFile(ledgerPath, JSON.stringify(ledger, null, 2) + "\\n", { encoding: "utf8", mode: 0o600 });
445
+ }
446
+
447
+ 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 });
449
+ process.stdout.write(JSON.stringify({ ok: true, event }, null, 2) + "\\n");
450
+ }
451
+
263
452
  void main();
264
453
  `;
265
454
  }
@@ -67,14 +67,14 @@ function autoSubagentDispatchBlock(stage, track) {
67
67
  const mandatory = schema.mandatoryDelegations;
68
68
  const mandatoryList = mandatory.length > 0 ? mandatory.map((a) => `\`${a}\``).join(", ") : "none";
69
69
  const delegationLogRel = `${RUNTIME_ROOT}/state/delegation-log.json`;
70
+ const delegationEventsRel = `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
70
71
  const artifactRef = `${RUNTIME_ROOT}/artifacts/${schema.artifactRules.artifactFile}`;
71
72
  return `## Automatic Subagent Dispatch
72
73
  | Agent | Mode | Class | Return Schema | User Gate | Trigger | Purpose |
73
74
  |---|---|---|---|---|---|---|
74
75
  ${rows}
75
- Mandatory: ${mandatoryList}. Record scheduled/completed/waived lifecycle rows in \`${delegationLogRel}\` before completion.
76
- ### Harness Dispatch Contract
77
- Use true harness dispatch: Claude native Task, Cursor generic dispatch, OpenCode \`.opencode/agents/<agent>.md\`, Codex \`.codex/agents/<agent>.toml\`. Run independent read-only/review agents in parallel where safe, write evidence into \`${artifactRef}\`, then append \`${delegationLogRel}\` rows with matching \`fulfillmentMode: "isolated"\` or \`"generic-dispatch"\`. Each dispatched worker should have a scheduled row and a terminal row sharing \`spanId\`; stale scheduled spans block completion. Do not collapse OpenCode or Codex to role-switch by default; role-switch is degraded fallback and must carry non-empty \`evidenceRefs\`. Missing evidence blocks completion.
76
+ Mandatory: ${mandatoryList}. Record lifecycle rows in \`${delegationLogRel}\` and append-only \`${delegationEventsRel}\` before completion.
77
+ ### Harness Dispatch Contract — use true harness dispatch: Claude Task, Cursor generic dispatch, OpenCode \`.opencode/agents/<agent>.md\` via Task/@agent, Codex \`.codex/agents/<agent>.toml\`. Do not collapse OpenCode or Codex to role-switch by default. Worker ACK Contract: ACK must include \`spanId\`, \`dispatchId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and \`ackTs\`; never claim \`fulfillmentMode: "isolated"\` without matching lifecycle proof. Helper: \`.cclaw/hooks/delegation-record.mjs --status=<status> --span-id=<spanId> --dispatch-id=<dispatchId> --dispatch-surface=<surface> --agent-definition-path=<path> --json\`. Exact recipe: scheduled -> launched -> acknowledged -> completed with the same span; completed isolated/generic rows require a prior ACK event for that span or \`--ack-ts=<iso>\`.
78
78
  `;
79
79
  }
80
80
  function researchPlaybooksBlock(playbooks) {
@@ -248,8 +248,9 @@ function completionParametersBlock(schema, track) {
248
248
  - \`completion helper\`: \`node .cclaw/hooks/stage-complete.mjs ${schema.stage}\`
249
249
  - \`completion helper with evidence\`: \`node .cclaw/hooks/stage-complete.mjs ${schema.stage} --evidence-json '{"<gate_id>":"<evidence note>"}' --passed=<gate_id>[,<gate_id>]\`
250
250
  - \`completion helper JSON diagnostics\`: append \`--json\` to receive a machine-readable validation failure summary.
251
+ - \`delegation record helper\`: \`node .cclaw/hooks/delegation-record.mjs --stage=${schema.stage} --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<spanId> --dispatch-id=<dispatchId> --dispatch-surface=<surface> --agent-definition-path=<path> --json\`. \`delegation helper recipe\`: call \`--status=scheduled\`, then \`--status=launched\`, then \`--status=acknowledged\`, then \`--status=completed\` with the same \`--span-id\`, \`--dispatch-id\`, \`--dispatch-surface\`, and \`--agent-definition-path\`; completed isolated/generic rows fail unless that same span already has an acknowledged event or the completed call includes \`--ack-ts=<iso>\`. For role-switch fallback, use \`--dispatch-surface=role-switch --evidence-ref=<artifact#anchor>\` instead of pretending isolated completion.
251
252
  - Fill \`## Learnings\` before closeout: either \`- None this stage.\` or JSON bullets with required keys \`type\`, \`trigger\`, \`action\`, \`confidence\` (knowledge-schema compatible).
252
- - Record mandatory delegation completion/waiver in \`${RUNTIME_ROOT}/state/delegation-log.json\` with rationale as needed.${mandatoryAgents.length > 0 ? ` If a mandatory delegation cannot run in this harness, use \`--waive-delegation=${mandatoryAgents.join(",")} --waiver-reason="<why safe>"\` on the completion helper.` : ""}
253
+ - Record mandatory delegation lifecycle in \`${RUNTIME_ROOT}/state/delegation-log.json\` and append proof events to \`${RUNTIME_ROOT}/state/delegation-events.jsonl\`; the ledger is current state, the event log is audit proof.${mandatoryAgents.length > 0 ? ` If a mandatory delegation cannot run in this harness, use \`--waive-delegation=${mandatoryAgents.join(",")} --waiver-reason="<why safe>"\` on the completion helper.` : ""}
253
254
  - Never edit raw \`flow-state.json\` to complete a stage, even in advisory mode; that bypasses validation, gate evidence, and Learnings harvest. If the helper fails, stop and report the exact command/output instead of applying a manual state workaround.
254
255
  - Completion protocol: verify required gates, update the artifact, then use the completion helper with \`--evidence-json\` and \`--passed\` for every satisfied gate.
255
256
  `;
@@ -5,6 +5,12 @@ function flowStatePath() {
5
5
  function delegationLogPath() {
6
6
  return `${RUNTIME_ROOT}/state/delegation-log.json`;
7
7
  }
8
+ function delegationEventsPath() {
9
+ return `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
10
+ }
11
+ function subagentsPath() {
12
+ return `${RUNTIME_ROOT}/state/subagents.json`;
13
+ }
8
14
  function knowledgePath() {
9
15
  return `${RUNTIME_ROOT}/knowledge.jsonl`;
10
16
  }
@@ -27,7 +33,7 @@ advancing or mutating anything. Safe to run at any point. The snapshot reflects:
27
33
  - progress across stages with per-stage markers,
28
34
  - gate coverage,
29
35
  - mandatory delegations with **fulfillmentMode** (isolated / generic-dispatch /
30
- role-switch) plus explicit waived status and evidence gate,
36
+ role-switch), dispatch proof fields, explicit waived status, and evidence gate,
31
37
  - **closeout substate** after ship (retro → compound → archive),
32
38
  - **harness parity row** (tier + fallback) for the active harness set.
33
39
 
@@ -85,7 +91,7 @@ a read-only command.
85
91
  \`Current\`, \`Stage\`, \`Gates\`, \`Delegations\`, \`Blocked by\`, \`Next\`, \`Evidence needed\`.
86
92
  - When blocked, include a plain-English action block:
87
93
  \`Current: <stage or closeout substate>\`; \`Blocked by: <gate/delegation/blocker code>\`; \`Next: <exact command or managed remediation>\`; \`Evidence needed: <artifact/test/review/delegation evidence>\`.
88
- - Report counts, not full artifact contents.
94
+ - Report counts, not full artifact contents. Include active subagent count from \`${subagentsPath()}\` and proof gaps from \`${delegationEventsPath()}\` when present.
89
95
  - If any data source is missing or corrupt, say so explicitly rather than guessing.
90
96
  - Include \`/cc-view tree\` for deep structure and \`/cc-view diff\` for before/after map in the final line.
91
97
 
@@ -175,6 +175,10 @@ Borrow the good part of Team/Ruflo-style orchestration without adding a swarm ru
175
175
  - **Checkpoint before synthesis.** Each agent returns status, files inspected/changed, evidence, and blockers before the parent acts.
176
176
  - **Consensus is for hard calls only.** Use two reviewers when severity or architecture is disputed; otherwise one evidence-backed reviewer is enough.
177
177
 
178
+ ## Parallelization Decision Gate
179
+
180
+ Before parallel dispatch, answer yes to all gates: tasks are independent, write sets do not overlap, outputs can be reconciled by evidence, and failure in one lane will not invalidate hidden assumptions in another. If any answer is no, serialize. Coder/overseer work is contract-first: the coder implements only the pasted contract, the overseer reads code and verifies acceptance evidence before the controller marks work complete.
181
+
178
182
  ## When to Use
179
183
 
180
184
  - Mid/large plans with multiple discrete tasks, dependencies, or risky overlap.
@@ -1013,9 +1017,9 @@ Two patterns (skills under \`.cclaw/skills/\`):
1013
1017
  - **SDD** (subagent-driven-development): sequential implementer→reviewer loops. Paste self-contained task text; never point subagents at plan files.
1014
1018
  - **Parallel Agents** (dispatching-parallel-agents): parallel review/analysis lenses. Never parallelize implementers on same codebase.
1015
1019
 
1016
- Status contract: DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED. Worker returns must use the strict JSON schemas in \`subagent-driven-development\`.
1020
+ Status contract: ACK first, then DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED. Worker returns must use the strict JSON schemas in \`subagent-driven-development\` and include matching spanId+dispatchId proof.
1017
1021
 
1018
- - Controller sequentially dispatches **implementer → reviewer** loops per task.
1022
+ - Controller sequentially dispatches **implementer → reviewer** loops per task and records lifecycle events in \`.cclaw/state/delegation-events.jsonl\`.
1019
1023
  - HARD-GATE: paste **self-contained task text**; never point subagents at plan files to “discover” scope.
1020
1024
  - **Review fixers** are **fresh agents** after failed review passes — avoids parent-context pollution.
1021
1025
  - **Machine-only flow checks auto-dispatch** by stage (design/plan/tdd/review/ship) without asking the user to trigger each specialist manually.
@@ -5,6 +5,12 @@ function flowStatePath() {
5
5
  function delegationLogPath() {
6
6
  return `${RUNTIME_ROOT}/state/delegation-log.json`;
7
7
  }
8
+ function delegationEventsPath() {
9
+ return `${RUNTIME_ROOT}/state/delegation-events.jsonl`;
10
+ }
11
+ function subagentsPath() {
12
+ return `${RUNTIME_ROOT}/state/subagents.json`;
13
+ }
8
14
  function artifactsPath() {
9
15
  return `${RUNTIME_ROOT}/artifacts`;
10
16
  }
@@ -30,7 +36,7 @@ Do not modify state in this command. It is a pure read/render operation.
30
36
  - stage marker: passed/current/pending/skipped/stale,
31
37
  - gates summary,
32
38
  - artifact summary,
33
- - delegation branch for current stage with fulfillmentMode labels,
39
+ - delegation branch for current stage with fulfillmentMode, dispatchSurface, proof status, and active tracker labels,
34
40
  6. When \`closeout.shipSubstate !== "idle"\` or \`currentStage === "ship"\`, add
35
41
  a closeout sub-tree:
36
42
  - \`retro:\` line derived from \`closeout.retroDraftedAt\` /
@@ -1,7 +1,9 @@
1
1
  import { type SubagentFallback } from "./harness-adapters.js";
2
2
  import type { FlowStage } from "./types.js";
3
3
  export type DelegationMode = "mandatory" | "proactive";
4
- export type DelegationStatus = "scheduled" | "completed" | "failed" | "waived";
4
+ export type DelegationStatus = "scheduled" | "launched" | "acknowledged" | "completed" | "failed" | "waived" | "stale";
5
+ export type DelegationDispatchSurface = "claude-task" | "cursor-task" | "opencode-agent" | "codex-agent" | "generic-task" | "role-switch" | "manual";
6
+ export type DelegationEventType = DelegationStatus;
5
7
  /**
6
8
  * How a delegation was actually fulfilled. Advisory — mirrors the harness
7
9
  * `subagentFallback` that was in effect when the entry was recorded.
@@ -53,6 +55,20 @@ export type DelegationEntry = {
53
55
  retryCount?: number;
54
56
  /** Optional references to evidence anchors in artifacts. */
55
57
  evidenceRefs?: string[];
58
+ /** Dispatch proof id from the parent/controller side. */
59
+ dispatchId?: string;
60
+ /** Worker-reported run id or task id returned by the harness. */
61
+ workerRunId?: string;
62
+ /** Concrete runtime surface used to launch the worker. */
63
+ dispatchSurface?: DelegationDispatchSurface;
64
+ /** Path to the generated or canonical agent definition used for dispatch. */
65
+ agentDefinitionPath?: string;
66
+ /** ISO timestamp when the worker was acknowledged by the harness/worker. */
67
+ ackTs?: string;
68
+ /** ISO timestamp when the worker was launched. */
69
+ launchedTs?: string;
70
+ /** ISO timestamp when the worker completed. */
71
+ completedTs?: string;
56
72
  /** Optional skill marker used for role-specific mandatory checks. */
57
73
  skill?: string;
58
74
  /**
@@ -68,6 +84,11 @@ export type DelegationLedger = {
68
84
  runId: string;
69
85
  entries: DelegationEntry[];
70
86
  };
87
+ export type DelegationEvent = DelegationEntry & {
88
+ event: DelegationEventType;
89
+ eventTs: string;
90
+ schemaVersion: 1;
91
+ };
71
92
  /**
72
93
  * Heuristic: does a changed file path strongly imply a trust-boundary
73
94
  * surface? Used by tests and prompt guidance for risk-triggered review.
@@ -79,6 +100,10 @@ export type DelegationLedger = {
79
100
  */
80
101
  export declare function isTrustBoundaryPath(filePath: string): boolean;
81
102
  export declare function readDelegationLedger(projectRoot: string): Promise<DelegationLedger>;
103
+ export declare function readDelegationEvents(projectRoot: string): Promise<{
104
+ events: DelegationEvent[];
105
+ corruptLines: number[];
106
+ }>;
82
107
  export declare function appendDelegation(projectRoot: string, entry: DelegationEntry): Promise<void>;
83
108
  /**
84
109
  * Aggregate the fulfillment mode cclaw expects for the active harness set.
@@ -96,6 +121,12 @@ export declare function checkMandatoryDelegations(projectRoot: string, stage: Fl
96
121
  staleIgnored: string[];
97
122
  /** Delegation rows missing required evidence under a role-switch fallback. */
98
123
  missingEvidence: string[];
124
+ /** Native isolated completion rows that lack dispatch proof. */
125
+ missingDispatchProof: string[];
126
+ /** Legacy inferred isolated completions accepted only as migration warnings. */
127
+ legacyInferredCompletions: string[];
128
+ /** Current-run event log lines that could not be parsed. */
129
+ corruptEventLines: number[];
99
130
  /** Current-run scheduled rows with no terminal row sharing the same spanId. */
100
131
  staleWorkers: string[];
101
132
  /** Expected fulfillment mode for the active harness set. */
@@ -9,13 +9,19 @@ import { HARNESS_ADAPTERS } from "./harness-adapters.js";
9
9
  import { readFlowState } from "./runs.js";
10
10
  import { stageSchema } from "./content/stage-schema.js";
11
11
  const execFileAsync = promisify(execFile);
12
- const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived"]);
12
+ const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
13
13
  function delegationLogPath(projectRoot) {
14
14
  return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
15
15
  }
16
16
  function delegationLockPath(projectRoot) {
17
17
  return path.join(projectRoot, RUNTIME_ROOT, "state", ".delegation.lock");
18
18
  }
19
+ function delegationEventsPath(projectRoot) {
20
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-events.jsonl");
21
+ }
22
+ function subagentsStatePath(projectRoot) {
23
+ return path.join(projectRoot, RUNTIME_ROOT, "state", "subagents.json");
24
+ }
19
25
  function createSpanId() {
20
26
  return `dspan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
21
27
  }
@@ -131,13 +137,16 @@ function isDelegationEntry(value) {
131
137
  const o = value;
132
138
  const modeOk = o.mode === "mandatory" || o.mode === "proactive";
133
139
  const statusOk = o.status === "scheduled" ||
140
+ o.status === "launched" ||
141
+ o.status === "acknowledged" ||
134
142
  o.status === "completed" ||
135
143
  o.status === "failed" ||
136
- o.status === "waived";
144
+ o.status === "waived" ||
145
+ o.status === "stale";
137
146
  const timestampOk = typeof o.ts === "string" ||
138
147
  typeof o.startTs === "string";
139
- const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived";
140
- const lifecycleOk = o.status !== "scheduled" || o.endTs === undefined;
148
+ const terminalStatus = o.status === "completed" || o.status === "failed" || o.status === "waived" || o.status === "stale";
149
+ const lifecycleOk = (o.status !== "scheduled" && o.status !== "launched" && o.status !== "acknowledged") || o.endTs === undefined;
141
150
  const terminalLifecycleOk = !terminalStatus ||
142
151
  o.endTs === undefined ||
143
152
  typeof o.endTs === "string";
@@ -168,12 +177,53 @@ function isDelegationEntry(value) {
168
177
  o.fulfillmentMode === "role-switch" ||
169
178
  o.fulfillmentMode === "harness-waiver") &&
170
179
  (o.conditionTrigger === undefined || typeof o.conditionTrigger === "string") &&
180
+ (o.dispatchId === undefined || typeof o.dispatchId === "string") &&
181
+ (o.workerRunId === undefined || typeof o.workerRunId === "string") &&
182
+ (o.dispatchSurface === undefined || isDelegationDispatchSurface(o.dispatchSurface)) &&
183
+ (o.agentDefinitionPath === undefined || typeof o.agentDefinitionPath === "string") &&
184
+ (o.ackTs === undefined || typeof o.ackTs === "string") &&
185
+ (o.launchedTs === undefined || typeof o.launchedTs === "string") &&
186
+ (o.completedTs === undefined || typeof o.completedTs === "string") &&
171
187
  (o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
172
188
  retryOk &&
173
189
  (o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
174
190
  (o.skill === undefined || typeof o.skill === "string") &&
175
191
  (o.schemaVersion === undefined || o.schemaVersion === 1));
176
192
  }
193
+ function isDelegationDispatchSurface(value) {
194
+ return (value === "claude-task" ||
195
+ value === "cursor-task" ||
196
+ value === "opencode-agent" ||
197
+ value === "codex-agent" ||
198
+ value === "generic-task" ||
199
+ value === "role-switch" ||
200
+ value === "manual");
201
+ }
202
+ function statusTimestampPatch(entry, ts) {
203
+ const patch = { ...entry };
204
+ if (patch.status === "launched")
205
+ patch.launchedTs = patch.launchedTs ?? ts;
206
+ if (patch.status === "acknowledged")
207
+ patch.ackTs = patch.ackTs ?? ts;
208
+ if (patch.status === "completed")
209
+ patch.completedTs = patch.completedTs ?? patch.endTs ?? ts;
210
+ return patch;
211
+ }
212
+ function eventFromEntry(entry) {
213
+ const eventTs = entry.completedTs ?? entry.ackTs ?? entry.launchedTs ?? entry.endTs ?? entry.startTs ?? entry.ts ?? new Date().toISOString();
214
+ return {
215
+ ...entry,
216
+ event: entry.status,
217
+ eventTs,
218
+ schemaVersion: 1
219
+ };
220
+ }
221
+ function isDelegationEvent(value) {
222
+ if (!isDelegationEntry(value))
223
+ return false;
224
+ const o = value;
225
+ return o.event === o.status && typeof o.eventTs === "string";
226
+ }
177
227
  function parseLedger(raw, runId) {
178
228
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
179
229
  return { runId, entries: [] };
@@ -195,6 +245,9 @@ function parseLedger(raw, runId) {
195
245
  startTs: ts,
196
246
  endTs: TERMINAL_DELEGATION_STATUSES.has(item.status) ? (item.endTs ?? ts) : undefined,
197
247
  ts,
248
+ launchedTs: item.launchedTs ?? (item.status === "launched" ? ts : undefined),
249
+ ackTs: item.ackTs ?? (item.status === "acknowledged" ? ts : undefined),
250
+ completedTs: item.completedTs ?? (item.status === "completed" ? (item.endTs ?? ts) : undefined),
198
251
  retryCount: typeof item.retryCount === "number" && Number.isInteger(item.retryCount) && item.retryCount >= 0
199
252
  ? item.retryCount
200
253
  : 0,
@@ -222,6 +275,57 @@ export async function readDelegationLedger(projectRoot) {
222
275
  return { runId: activeRunId, entries: [] };
223
276
  }
224
277
  }
278
+ export async function readDelegationEvents(projectRoot) {
279
+ const filePath = delegationEventsPath(projectRoot);
280
+ if (!(await exists(filePath))) {
281
+ return { events: [], corruptLines: [] };
282
+ }
283
+ const events = [];
284
+ const corruptLines = [];
285
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
286
+ const lines = text.split(/\r?\n/gu);
287
+ for (let index = 0; index < lines.length; index += 1) {
288
+ const line = lines[index]?.trim() ?? "";
289
+ if (line.length === 0)
290
+ continue;
291
+ try {
292
+ const parsed = JSON.parse(line);
293
+ if (isDelegationEvent(parsed)) {
294
+ events.push(parsed);
295
+ }
296
+ else {
297
+ corruptLines.push(index + 1);
298
+ }
299
+ }
300
+ catch {
301
+ corruptLines.push(index + 1);
302
+ }
303
+ }
304
+ return { events, corruptLines };
305
+ }
306
+ async function appendDelegationEvent(projectRoot, event) {
307
+ const filePath = delegationEventsPath(projectRoot);
308
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
309
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, { encoding: "utf8", mode: 0o600 });
310
+ }
311
+ async function writeSubagentTracker(projectRoot, entries) {
312
+ const active = entries
313
+ .filter((entry) => entry.status === "scheduled" || entry.status === "launched" || entry.status === "acknowledged")
314
+ .map((entry) => ({
315
+ spanId: entry.spanId,
316
+ dispatchId: entry.dispatchId,
317
+ workerRunId: entry.workerRunId,
318
+ stage: entry.stage,
319
+ agent: entry.agent,
320
+ status: entry.status,
321
+ dispatchSurface: entry.dispatchSurface,
322
+ agentDefinitionPath: entry.agentDefinitionPath,
323
+ startedAt: entry.startTs,
324
+ launchedAt: entry.launchedTs,
325
+ acknowledgedAt: entry.ackTs
326
+ }));
327
+ await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
328
+ }
225
329
  export async function appendDelegation(projectRoot, entry) {
226
330
  const { activeRunId } = await readFlowState(projectRoot);
227
331
  await withDirectoryLock(delegationLockPath(projectRoot), async () => {
@@ -231,13 +335,16 @@ export async function appendDelegation(projectRoot, entry) {
231
335
  if (entry.status === "waived" && !hasValidWaiverReason(entry.waiverReason)) {
232
336
  throw new Error("waived delegation entries require a non-empty waiverReason");
233
337
  }
234
- const stamped = { ...entry, runId: entry.runId ?? activeRunId };
338
+ const stamped = statusTimestampPatch({ ...entry, runId: entry.runId ?? activeRunId }, startTs);
235
339
  stamped.spanId = entry.spanId ?? createSpanId();
236
340
  stamped.startTs = startTs;
237
341
  stamped.ts = startTs;
238
342
  if (TERMINAL_DELEGATION_STATUSES.has(stamped.status) && !stamped.endTs) {
239
343
  stamped.endTs = new Date().toISOString();
240
344
  }
345
+ if (stamped.status === "completed") {
346
+ stamped.completedTs = stamped.completedTs ?? stamped.endTs ?? new Date().toISOString();
347
+ }
241
348
  if (stamped.status === "scheduled") {
242
349
  delete stamped.endTs;
243
350
  }
@@ -268,11 +375,13 @@ export async function appendDelegation(projectRoot, entry) {
268
375
  if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
269
376
  return;
270
377
  }
378
+ await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
271
379
  const ledger = {
272
380
  runId: activeRunId,
273
381
  entries: [...prior.entries, stamped]
274
382
  };
275
383
  await writeFileSafe(filePath, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600 });
384
+ await writeSubagentTracker(projectRoot, ledger.entries);
276
385
  });
277
386
  }
278
387
  /**
@@ -299,6 +408,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
299
408
  const mandatory = stageSchema(stage, flowState.track).mandatoryDelegations;
300
409
  const { activeRunId } = flowState;
301
410
  const ledger = await readDelegationLedger(projectRoot);
411
+ const events = await readDelegationEvents(projectRoot);
302
412
  const forStage = ledger.entries.filter((e) => e.stage === stage);
303
413
  const forRun = forStage.filter((e) => e.runId === activeRunId);
304
414
  const staleIgnored = forStage
@@ -307,6 +417,8 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
307
417
  const missing = [];
308
418
  const waived = [];
309
419
  const missingEvidence = [];
420
+ const missingDispatchProof = [];
421
+ const legacyInferredCompletions = [];
310
422
  const terminalSpanIds = new Set(forRun
311
423
  .filter((entry) => TERMINAL_DELEGATION_STATUSES.has(entry.status) && entry.spanId)
312
424
  .map((entry) => entry.spanId));
@@ -342,13 +454,40 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
342
454
  !completedRows.some((e) => Array.isArray(e.evidenceRefs) && e.evidenceRefs.length > 0)) {
343
455
  missingEvidence.push(agent);
344
456
  }
457
+ for (const row of completedRows) {
458
+ const mode = row.fulfillmentMode ?? "isolated";
459
+ if (mode === "isolated") {
460
+ const spanEvents = events.events.filter((event) => event.runId === activeRunId &&
461
+ event.stage === stage &&
462
+ event.agent === agent &&
463
+ event.spanId === row.spanId);
464
+ const dispatchId = row.dispatchId ?? row.workerRunId ?? spanEvents.find((event) => event.dispatchId || event.workerRunId)?.dispatchId ?? spanEvents.find((event) => event.workerRunId)?.workerRunId;
465
+ const dispatchSurface = row.dispatchSurface ?? spanEvents.find((event) => event.dispatchSurface)?.dispatchSurface;
466
+ const agentDefinitionPath = row.agentDefinitionPath ?? spanEvents.find((event) => event.agentDefinitionPath)?.agentDefinitionPath;
467
+ const hasAck = Boolean(row.ackTs || spanEvents.some((event) => event.event === "acknowledged" && event.ackTs));
468
+ const hasCompleted = Boolean(row.completedTs || spanEvents.some((event) => event.event === "completed" && event.completedTs));
469
+ const hasDispatchProof = Boolean(row.spanId && dispatchId && dispatchSurface && agentDefinitionPath && hasAck && hasCompleted);
470
+ if (!hasDispatchProof) {
471
+ const proofEraSignal = Boolean(row.dispatchId || row.workerRunId || row.dispatchSurface || row.agentDefinitionPath || spanEvents.some((event) => event.dispatchId || event.workerRunId || event.dispatchSurface || event.agentDefinitionPath || event.event === "acknowledged" || event.event === "launched"));
472
+ if (proofEraSignal) {
473
+ missingDispatchProof.push(agent);
474
+ }
475
+ else {
476
+ legacyInferredCompletions.push(`${agent}(spanId=${row.spanId ?? "unknown"})`);
477
+ }
478
+ }
479
+ }
480
+ }
345
481
  }
346
482
  return {
347
- satisfied: missing.length === 0 && missingEvidence.length === 0 && staleWorkers.length === 0,
483
+ satisfied: missing.length === 0 && missingEvidence.length === 0 && missingDispatchProof.length === 0 && staleWorkers.length === 0 && events.corruptLines.length === 0,
348
484
  missing,
349
485
  waived,
350
486
  staleIgnored,
351
487
  missingEvidence,
488
+ missingDispatchProof,
489
+ legacyInferredCompletions,
490
+ corruptEventLines: events.corruptLines,
352
491
  staleWorkers,
353
492
  expectedMode
354
493
  };
@@ -107,6 +107,15 @@ const RULES = [
107
107
  docRef: ref("harnesses.md")
108
108
  }
109
109
  },
110
+ {
111
+ test: /^harness:reality:/,
112
+ metadata: {
113
+ severity: "info",
114
+ summary: "Harness reality label for dispatch/proof support.",
115
+ fix: "No action required; use this label to interpret native/generic/role-switch proof requirements.",
116
+ docRef: ref("harnesses.md")
117
+ }
118
+ },
110
119
  {
111
120
  test: /^delegation:/,
112
121
  metadata: {
package/dist/doctor.js CHANGED
@@ -13,7 +13,7 @@ import { policyChecks } from "./policy.js";
13
13
  import { CorruptFlowStateError, readFlowState } from "./runs.js";
14
14
  import { createInitialFlowState, skippedStagesForTrack } from "./flow-state.js";
15
15
  import { FLOW_STAGES, TRACK_STAGES } from "./types.js";
16
- import { checkMandatoryDelegations } from "./delegation.js";
16
+ import { checkMandatoryDelegations, readDelegationEvents } from "./delegation.js";
17
17
  import { buildTraceMatrix } from "./trace-matrix.js";
18
18
  import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
19
19
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
@@ -305,6 +305,20 @@ function normalizeOpenCodePluginEntry(entry) {
305
305
  }
306
306
  return null;
307
307
  }
308
+ function generatedAgentShape(content, kind, agentName) {
309
+ if (kind === "opencode") {
310
+ return content.includes("mode: subagent") && content.includes(`# ${agentName}`) && content.includes("STRICT_RETURN_SCHEMA");
311
+ }
312
+ return content.includes(`name = "${agentName}"`) && content.includes("developer_instructions") && content.includes("STRICT_RETURN_SCHEMA");
313
+ }
314
+ function harnessRealityLabel(harness) {
315
+ const adapter = HARNESS_ADAPTERS[harness];
316
+ const declaredSupport = adapter.capabilities.nativeSubagentDispatch;
317
+ const runtimeLaunch = harness === "opencode" || harness === "codex" ? "prompt-level launch" : declaredSupport === "generic" ? "generic Task launch" : "native tool launch";
318
+ const proofRequired = adapter.capabilities.subagentFallback === "native" ? "dispatchId+spanId+ack" : "evidenceRefs";
319
+ const proofSource = harness === "opencode" ? ".opencode/agents + delegation-events.jsonl" : harness === "codex" ? ".codex/agents + delegation-events.jsonl" : ".cclaw/state/delegation-log.json";
320
+ return `declaredSupport=${declaredSupport}; runtimeLaunch=${runtimeLaunch}; proofRequired=${proofRequired}; proofSource=${proofSource}`;
321
+ }
308
322
  const OPENCODE_PLUGIN_REL_PATH = ".opencode/plugins/cclaw-plugin.mjs";
309
323
  function opencodeConfigCandidates(projectRoot) {
310
324
  return [
@@ -780,6 +794,14 @@ export async function doctorChecks(projectRoot, options = {}) {
780
794
  }
781
795
  }
782
796
  }
797
+ for (const harness of configuredHarnesses) {
798
+ checks.push({
799
+ name: `harness:reality:${harness}`,
800
+ ok: true,
801
+ severity: "info",
802
+ details: harnessRealityLabel(harness)
803
+ });
804
+ }
783
805
  const agentsFile = path.join(projectRoot, "AGENTS.md");
784
806
  let agentsBlockOk = false;
785
807
  if (await exists(agentsFile)) {
@@ -884,12 +906,39 @@ export async function doctorChecks(projectRoot, options = {}) {
884
906
  details: agentPath
885
907
  });
886
908
  }
909
+ for (const agent of CCLAW_AGENTS) {
910
+ if (configuredHarnesses.includes("opencode")) {
911
+ const agentPath = path.join(projectRoot, ".opencode", "agents", `${agent.name}.md`);
912
+ let ok = false;
913
+ if (await exists(agentPath)) {
914
+ ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "opencode", agent.name);
915
+ }
916
+ checks.push({
917
+ name: `agent:opencode:${agent.name}:shape`,
918
+ ok,
919
+ details: `${agentPath} must be a generated OpenCode subagent with mode: subagent and strict return schema`
920
+ });
921
+ }
922
+ if (configuredHarnesses.includes("codex")) {
923
+ const agentPath = path.join(projectRoot, ".codex", "agents", `${agent.name}.toml`);
924
+ let ok = false;
925
+ if (await exists(agentPath)) {
926
+ ok = generatedAgentShape(await fs.readFile(agentPath, "utf8"), "codex", agent.name);
927
+ }
928
+ checks.push({
929
+ name: `agent:codex:${agent.name}:shape`,
930
+ ok,
931
+ details: `${agentPath} must be a generated Codex custom agent TOML with developer_instructions and strict return schema`
932
+ });
933
+ }
934
+ }
887
935
  // Hook scripts
888
936
  for (const script of [
889
937
  "run-hook.mjs",
890
938
  "run-hook.cmd",
891
939
  "stage-complete.mjs",
892
940
  "start-flow.mjs",
941
+ "delegation-record.mjs",
893
942
  "opencode-plugin.mjs"
894
943
  ]) {
895
944
  const scriptPath = path.join(projectRoot, RUNTIME_ROOT, "hooks", script);
@@ -1774,6 +1823,7 @@ export async function doctorChecks(projectRoot, options = {}) {
1774
1823
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
1775
1824
  repairFeatureSystem: false
1776
1825
  });
1826
+ const delegationEvents = await readDelegationEvents(projectRoot);
1777
1827
  const delegationSatisfiedForDoctor = currentStageUntouched || delegation.satisfied;
1778
1828
  const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
1779
1829
  ? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
@@ -1785,7 +1835,30 @@ export async function doctorChecks(projectRoot, options = {}) {
1785
1835
  ? `mandatory delegation check deferred for untouched stage "${flowState.currentStage}"; stage-complete enforces it when work begins`
1786
1836
  : delegation.satisfied
1787
1837
  ? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
1788
- : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
1838
+ : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}; missingDispatchProof=${delegation.missingDispatchProof.join(", ")}; staleWorkers=${delegation.staleWorkers.join(", ")}; corruptEventLines=${delegation.corruptEventLines.join(", ")}`
1839
+ });
1840
+ checks.push({
1841
+ name: "delegation:events:parse",
1842
+ ok: delegationEvents.corruptLines.length === 0,
1843
+ details: delegationEvents.corruptLines.length === 0
1844
+ ? `${RUNTIME_ROOT}/state/delegation-events.jsonl parsed successfully (${delegationEvents.events.length} event(s))`
1845
+ : `corrupt delegation event line(s): ${delegationEvents.corruptLines.join(", ")}`
1846
+ });
1847
+ checks.push({
1848
+ name: "delegation:proof:current_stage",
1849
+ ok: currentStageUntouched || delegation.missingDispatchProof.length === 0,
1850
+ details: currentStageUntouched
1851
+ ? `dispatch proof check deferred for untouched stage "${flowState.currentStage}"`
1852
+ : delegation.missingDispatchProof.length === 0
1853
+ ? `no dispatch proof gaps for current stage "${flowState.currentStage}"`
1854
+ : `isolated completions missing dispatchId/dispatchSurface/agentDefinitionPath/ackTs/completedTs: ${delegation.missingDispatchProof.join(", ")}`
1855
+ });
1856
+ checks.push({
1857
+ name: "warning:delegation:legacy_inferred_completions",
1858
+ ok: true,
1859
+ details: delegation.legacyInferredCompletions.length > 0
1860
+ ? `warning: legacy inferred isolated completion rows lack event-log proof: ${delegation.legacyInferredCompletions.join(", ")}`
1861
+ : "no legacy inferred isolated completions for current stage"
1789
1862
  });
1790
1863
  checks.push({
1791
1864
  name: "warning:delegation:waived",
@@ -37,6 +37,12 @@ export type SubagentFallback =
37
37
  export type ShimKind = "command" | "skill";
38
38
  export interface HarnessAdapter {
39
39
  id: HarnessId;
40
+ reality: {
41
+ declaredSupport: "full" | "generic" | "partial" | "none";
42
+ runtimeLaunch: string;
43
+ proofRequired: string;
44
+ proofSource: string;
45
+ };
40
46
  /**
41
47
  * Root directory where cclaw writes `/cc*` entry points.
42
48
  *
@@ -71,6 +71,12 @@ export function harnessShimSkillNames() {
71
71
  export const HARNESS_ADAPTERS = {
72
72
  claude: {
73
73
  id: "claude",
74
+ reality: {
75
+ declaredSupport: "full",
76
+ runtimeLaunch: "native Task launch",
77
+ proofRequired: "spanId+dispatchId or workerRunId+ACK for isolated completion",
78
+ proofSource: ".cclaw/state/delegation-events.jsonl plus delegation-log.json"
79
+ },
74
80
  commandDir: ".claude/commands",
75
81
  shimKind: "command",
76
82
  capabilities: {
@@ -82,6 +88,12 @@ export const HARNESS_ADAPTERS = {
82
88
  },
83
89
  cursor: {
84
90
  id: "cursor",
91
+ reality: {
92
+ declaredSupport: "generic",
93
+ runtimeLaunch: "generic Task/Subagent launch with cclaw role prompt",
94
+ proofRequired: "spanId+dispatchId/evidenceRefs for generic-dispatch completion",
95
+ proofSource: ".cclaw/state/delegation-events.jsonl plus artifact evidenceRefs"
96
+ },
85
97
  commandDir: ".cursor/commands",
86
98
  shimKind: "command",
87
99
  capabilities: {
@@ -97,6 +109,12 @@ export const HARNESS_ADAPTERS = {
97
109
  },
98
110
  opencode: {
99
111
  id: "opencode",
112
+ reality: {
113
+ declaredSupport: "full",
114
+ runtimeLaunch: "prompt-level launch via Task or @agent against generated .opencode/agents",
115
+ proofRequired: "spanId+dispatchId+ackTs+completedTs before isolated completion",
116
+ proofSource: ".opencode/agents/<agent>.md and .cclaw/state/delegation-events.jsonl"
117
+ },
100
118
  commandDir: ".opencode/commands",
101
119
  shimKind: "command",
102
120
  capabilities: {
@@ -119,6 +137,12 @@ export const HARNESS_ADAPTERS = {
119
137
  },
120
138
  codex: {
121
139
  id: "codex",
140
+ reality: {
141
+ declaredSupport: "full",
142
+ runtimeLaunch: "prompt-level launch by asking Codex to spawn generated custom agents",
143
+ proofRequired: "spanId+dispatchId+ackTs+completedTs before isolated completion",
144
+ proofSource: ".codex/agents/<agent>.toml and .cclaw/state/delegation-events.jsonl"
145
+ },
122
146
  // Codex CLI reads skills from the universal `.agents/skills/` path
123
147
  // (OpenAI Codex 0.89, Jan 2026). It does NOT have a native
124
148
  // `.codex/commands/*` slash-command discovery — cclaw installs
@@ -155,9 +179,9 @@ export function harnessDispatchSurface(harnessId) {
155
179
  case "cursor":
156
180
  return "Use Cursor Subagent/Task with a generic subagent_type (explore for read-only mapping, generalPurpose for broader work, shell/browser-use when specifically needed) and paste the cclaw role prompt; record fulfillmentMode: \"generic-dispatch\" with evidenceRefs.";
157
181
  case "opencode":
158
- return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>, run independent agents in parallel when safe, then record fulfillmentMode: \"isolated\".";
182
+ return "Use OpenCode subagents: invoke the generated .opencode/agents/<agent>.md agent via Task or @<agent>; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
159
183
  case "codex":
160
- return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name, wait for all results, then record fulfillmentMode: \"isolated\".";
184
+ return "Use Codex native subagents: ask Codex to spawn the generated .codex/agents/<agent>.toml agent(s) by name; record scheduled/launched/acknowledged/completed events with spanId+dispatchId before claiming fulfillmentMode: \"isolated\".";
161
185
  }
162
186
  }
163
187
  export function harnessDispatchFallback(harnessId) {
@@ -597,7 +621,7 @@ async function cleanupLegacyCodexSurfaces(projectRoot) {
597
621
  }
598
622
  }
599
623
  function codexAgentToml(agent) {
600
- const instructions = `${agent.body}\n\n${enhancedAgentInstruction(agent.name)}`.trim();
624
+ const instructions = `${agentMarkdown(agent)}\n\n${enhancedAgentInstruction(agent.name)}`.trim();
601
625
  const sandboxMode = agent.tools.some((tool) => ["Write", "Edit", "Bash"].includes(tool))
602
626
  ? "workspace-write"
603
627
  : "read-only";
@@ -625,7 +649,7 @@ permission:
625
649
  ${agentMarkdown(agent)}`;
626
650
  }
627
651
  function enhancedAgentInstruction(agentName) {
628
- return `You are the cclaw ${agentName} subagent. Follow the parent prompt as the task boundary, produce evidence suitable for .cclaw/state/delegation-log.json, and do not recursively orchestrate other agents unless the parent explicitly asks.`;
652
+ return `## Worker ACK Contract\n\nYou are the cclaw ${agentName} subagent. Follow the parent prompt as the task boundary. ACK first with JSON containing spanId, dispatchId or workerRunId, dispatchSurface, agentDefinitionPath, ackTs, and status: "ACK". Finish with the strict return schema plus the same spanId+dispatchId proof so the parent can append .cclaw/state/delegation-events.jsonl and .cclaw/state/delegation-log.json. Do not let the parent claim isolated completion without matching ACK/result proof. Do not recursively orchestrate other agents unless the parent explicitly asks.`;
629
653
  }
630
654
  async function syncAgentFiles(projectRoot, harnesses) {
631
655
  const agentsDir = path.join(projectRoot, RUNTIME_ROOT, "agents");
package/dist/install.js CHANGED
@@ -13,7 +13,7 @@ import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-co
13
13
  import { subagentDrivenDevSkill, parallelAgentsSkill } from "./content/subagents.js";
14
14
  import { sessionHooksSkillMarkdown } from "./content/session-hooks.js";
15
15
  import { ironLawRuntimeDocument, ironLawsSkillMarkdown } from "./content/iron-laws.js";
16
- import { stageCompleteScript, startFlowScript, runHookCmdScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
16
+ import { stageCompleteScript, startFlowScript, runHookCmdScript, delegationRecordScript, opencodePluginJs, claudeHooksJson, codexHooksJson, cursorHooksJson } from "./content/hooks.js";
17
17
  import { nodeHookRuntimeScript } from "./content/node-hooks.js";
18
18
  import { META_SKILL_NAME, usingCclawSkillMarkdown } from "./content/meta-skill.js";
19
19
  import { ARTIFACT_TEMPLATES, CURSOR_WORKFLOW_RULE_MDC, RULEBOOK_MARKDOWN, buildRulesJson } from "./content/templates.js";
@@ -884,6 +884,7 @@ async function writeHooks(projectRoot, config) {
884
884
  compoundRecurrenceThreshold: config.compound?.recurrenceThreshold
885
885
  }));
886
886
  await writeFileSafe(path.join(hooksDir, "run-hook.cmd"), runHookCmdScript());
887
+ await writeFileSafe(path.join(hooksDir, "delegation-record.mjs"), delegationRecordScript());
887
888
  const opencodePluginSource = opencodePluginJs();
888
889
  await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
889
890
  try {
@@ -892,6 +893,7 @@ async function writeHooks(projectRoot, config) {
892
893
  "start-flow.mjs",
893
894
  "run-hook.mjs",
894
895
  "run-hook.cmd",
896
+ "delegation-record.mjs",
895
897
  "opencode-plugin.mjs"
896
898
  ]) {
897
899
  await fs.chmod(path.join(hooksDir, script), 0o755);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.25",
3
+ "version": "0.51.26",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {