cclaw-cli 6.8.0 → 6.10.0

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.
Files changed (40) hide show
  1. package/dist/artifact-linter/design.js +1 -1
  2. package/dist/artifact-linter/plan.js +37 -0
  3. package/dist/artifact-linter/shared.d.ts +48 -2
  4. package/dist/artifact-linter/shared.js +54 -5
  5. package/dist/artifact-linter/tdd.d.ts +31 -0
  6. package/dist/artifact-linter/tdd.js +357 -17
  7. package/dist/artifact-linter.js +87 -2
  8. package/dist/content/examples.js +9 -9
  9. package/dist/content/harness-doc.js +1 -1
  10. package/dist/content/hooks.js +140 -3
  11. package/dist/content/iron-laws.js +6 -2
  12. package/dist/content/node-hooks.js +15 -1308
  13. package/dist/content/reference-patterns.js +2 -2
  14. package/dist/content/skills-elicitation.js +2 -2
  15. package/dist/content/skills.js +1 -1
  16. package/dist/content/stages/brainstorm.js +2 -2
  17. package/dist/content/stages/design.js +2 -2
  18. package/dist/content/stages/scope.js +2 -2
  19. package/dist/content/stages/tdd.js +7 -8
  20. package/dist/content/subagents.js +20 -2
  21. package/dist/content/templates.js +5 -15
  22. package/dist/delegation.d.ts +102 -3
  23. package/dist/delegation.js +172 -14
  24. package/dist/early-loop.js +15 -1
  25. package/dist/gate-evidence.js +15 -23
  26. package/dist/harness-adapters.js +4 -2
  27. package/dist/install.js +37 -221
  28. package/dist/internal/advance-stage.js +19 -3
  29. package/dist/internal/detect-supply-chain-changes.d.ts +6 -0
  30. package/dist/internal/detect-supply-chain-changes.js +138 -0
  31. package/dist/internal/flow-state-repair.d.ts +7 -0
  32. package/dist/internal/flow-state-repair.js +57 -18
  33. package/dist/internal/plan-split-waves.d.ts +66 -0
  34. package/dist/internal/plan-split-waves.js +249 -0
  35. package/dist/run-persistence.d.ts +2 -0
  36. package/dist/run-persistence.js +62 -3
  37. package/dist/runtime/run-hook.mjs +44 -8729
  38. package/dist/tdd-slices.d.ts +90 -0
  39. package/dist/tdd-slices.js +375 -0
  40. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  import fs from "node:fs/promises";
2
+ import path from "node:path";
2
3
  import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
3
4
  import { exists } from "./fs-utils.js";
4
5
  import { stageSchema } from "./content/stage-schema.js";
5
6
  import { readFlowState } from "./run-persistence.js";
6
- import { duplicateH2Headings, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
7
+ import { duplicateH2Headings, extractEvidencePointers, extractH2Sections, extractRequirementIdsFromMarkdown, isShortCircuitActivated, normalizeHeadingTitle, parseFrontmatter, parseLearningsSection, sectionBodyByAnyName, sectionBodyByHeadingPrefix, sectionBodyByName, validateSectionBody, formatLearningsErrorsBullets } from "./artifact-linter/shared.js";
8
+ import { foldTddSliceLedger, readTddSliceLedger } from "./tdd-slices.js";
7
9
  import { shouldDemoteArtifactValidationByTrack } from "./content/stage-schema.js";
8
10
  import { readDelegationLedger, recordArtifactValidationDemotedByTrack } from "./delegation.js";
9
11
  import { classifyAndPersistFindings } from "./artifact-linter/findings-dedup.js";
@@ -145,6 +147,17 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
145
147
  }
146
148
  }
147
149
  const liteTierForValidators = shouldDemoteArtifactValidationByTrack(track, taskClass);
150
+ // v6.10.0 (T3) — pre-resolve RED/GREEN Evidence pointers and sidecar
151
+ // auto-satisfy state once for the whole TDD loop, then thread the
152
+ // booleans through `validateSectionBody`. We do the async resolution
153
+ // here (path existence + delegation spanId match) so the validators
154
+ // themselves stay sync.
155
+ const tddEvidenceContext = stage === "tdd"
156
+ ? await resolveTddEvidencePointerContext({
157
+ projectRoot,
158
+ sections
159
+ })
160
+ : { red: {}, green: {} };
148
161
  for (const v of schema.artifactValidation) {
149
162
  const sectionKey = normalizeHeadingTitle(v.section).toLowerCase();
150
163
  const scopeBoundaryAlias = stage === "scope" && sectionKey === "in scope / out of scope";
@@ -164,7 +177,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
164
177
  ? { ok: false, details: `No ## heading matching required section "${v.section}".` }
165
178
  : validateSectionBody(body, v.validationRule, v.section, {
166
179
  sections,
167
- liteTier: liteTierForValidators
180
+ liteTier: liteTierForValidators,
181
+ tddEvidence: stage === "tdd" ? tddEvidenceContext : undefined
168
182
  });
169
183
  const found = hasHeading && validation.ok;
170
184
  findings.push({
@@ -420,3 +434,74 @@ const ARTIFACT_VALIDATION_LITE_DEMOTE_SECTIONS = new Set([
420
434
  "Stale Diagram Drift Check",
421
435
  "Product Discovery Delegation (Strategist Mode)"
422
436
  ]);
437
+ /**
438
+ * v6.10.0 (T3) — pre-resolve `Evidence:` pointers and sidecar
439
+ * auto-satisfy state for the TDD stage's RED/GREEN Evidence rows so
440
+ * `validateSectionBody` (sync) can short-circuit. A pointer of the form
441
+ * `<path>` is satisfied when the path exists on disk relative to the
442
+ * project root; `spanId:<id>` is satisfied when any delegation ledger
443
+ * row carries that span id. Sidecar auto-satisfy fires when
444
+ * `06-tdd-slices.jsonl` carries at least one slice with a non-empty
445
+ * `redOutputRef` / `greenOutputRef`.
446
+ */
447
+ async function resolveTddEvidencePointerContext(input) {
448
+ const { projectRoot, sections } = input;
449
+ const redSection = sectionBodyByName(sections, "RED Evidence") ?? "";
450
+ const greenSection = sectionBodyByName(sections, "GREEN Evidence") ?? "";
451
+ const redPointers = extractEvidencePointers(redSection);
452
+ const greenPointers = extractEvidencePointers(greenSection);
453
+ let knownSpanIds = new Set();
454
+ if (redPointers.length > 0 || greenPointers.length > 0) {
455
+ try {
456
+ const ledger = await readDelegationLedger(projectRoot);
457
+ knownSpanIds = new Set(ledger.entries
458
+ .map((entry) => entry.spanId)
459
+ .filter((id) => typeof id === "string" && id.length > 0));
460
+ }
461
+ catch {
462
+ knownSpanIds = new Set();
463
+ }
464
+ }
465
+ async function pointerResolves(value) {
466
+ const trimmed = value.replace(/[`*_]/gu, "").trim();
467
+ if (trimmed.length === 0)
468
+ return false;
469
+ if (/^spanid\s*:/iu.test(trimmed)) {
470
+ const id = trimmed.replace(/^spanid\s*:\s*/iu, "").trim();
471
+ return id.length > 0 && knownSpanIds.has(id);
472
+ }
473
+ const candidate = path.isAbsolute(trimmed) ? trimmed : path.join(projectRoot, trimmed);
474
+ return exists(candidate);
475
+ }
476
+ async function anyResolved(values) {
477
+ for (const value of values) {
478
+ if (await pointerResolves(value))
479
+ return true;
480
+ }
481
+ return false;
482
+ }
483
+ let redSidecarAutoSatisfy = false;
484
+ let greenSidecarAutoSatisfy = false;
485
+ try {
486
+ const sidecar = await readTddSliceLedger(projectRoot);
487
+ if (sidecar.entries.length > 0) {
488
+ const folded = foldTddSliceLedger(sidecar.entries);
489
+ redSidecarAutoSatisfy = folded.some((entry) => typeof entry.redOutputRef === "string" && entry.redOutputRef.length > 0);
490
+ greenSidecarAutoSatisfy = folded.some((entry) => typeof entry.greenOutputRef === "string" && entry.greenOutputRef.length > 0);
491
+ }
492
+ }
493
+ catch {
494
+ redSidecarAutoSatisfy = false;
495
+ greenSidecarAutoSatisfy = false;
496
+ }
497
+ return {
498
+ red: {
499
+ pointerSatisfied: await anyResolved(redPointers),
500
+ sidecarAutoSatisfy: redSidecarAutoSatisfy
501
+ },
502
+ green: {
503
+ pointerSatisfied: await anyResolved(greenPointers),
504
+ sidecarAutoSatisfy: greenSidecarAutoSatisfy
505
+ }
506
+ };
507
+ }
@@ -36,10 +36,10 @@ export const BEHAVIOR_ANCHORS = [
36
36
  },
37
37
  {
38
38
  stage: "tdd",
39
- section: "RED Evidence",
40
- bad: "RED: `expect(true).toBe(true)` then \"failing test observed\" the assertion can never have caught the bug it claims to prove.",
41
- good: "RED: `expect(api.fetchFeed()).rejects.toThrow(AuthError)`; the failure output names the missing guard and ties to AC-3.",
42
- ruleHint: "Mental mutation test: name a plausible bug that would still pass the assertion. If you can, the assertion is too coarse."
39
+ section: "Watched-RED Proof",
40
+ bad: "Hand-edit `S-1 | 2026-04-15T10:00 | observed RED` into the markdown table; nothing lands in the JSONL sidecar, so retries silently overwrite the row.",
41
+ good: "Run `cclaw-cli internal tdd-slice-record --slice S-1 --status red --test-file tests/feed.spec.ts --command \"npm test\" --paths src/api/feed.ts --ac AC-3`; the linter reads the sidecar.",
42
+ ruleHint: "RED/GREEN/REFACTOR transitions are recorded by `cclaw-cli internal tdd-slice-record`; the markdown tables are an auto-derived view from v6.10.0 onward."
43
43
  },
44
44
  {
45
45
  stage: "review",
@@ -247,12 +247,12 @@ Plan is ready to execute after user confirmation.
247
247
  | S-1 feed window | expected 30d window, got 7d |
248
248
  | S-2 degraded banner | banner absent after forced disconnect |
249
249
 
250
- ## Acceptance Mapping
250
+ ## Acceptance & Failure Map
251
251
 
252
- | Slice | AC IDs |
253
- | --- | --- |
254
- | S-1 | AC-1 |
255
- | S-2 | AC-3 |
252
+ | Slice | Source ID | AC ID | Expected behavior | RED-link |
253
+ | --- | --- | --- | --- | --- |
254
+ | S-1 | SRC-1 | AC-1 | feed window honors 30d cap | spanId:tdd-feed-window-red |
255
+ | S-2 | SRC-2 | AC-3 | degraded banner appears on disconnect | .cclaw/artifacts/06-tdd-slices.jsonl |
256
256
 
257
257
  ## GREEN
258
258
 
@@ -60,6 +60,6 @@ export function harnessIntegrationDocMarkdown() {
60
60
  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",
61
61
  perHarnessRecipeMarkdown(),
62
62
  hookLayeringSectionMarkdown(),
63
- "\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; sync 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` - stage progression and post-ship closeout\n- `/cc-idea` - idea 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-idea`, `/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, 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` | `brainstorm` |\n| `scope` | `scope` |\n| `design` | `design` |\n| `spec` | `spec` |\n| `plan` | `plan` |\n| `tdd` | `tdd` |\n| `review` | `review` |\n| `ship` | `ship` |\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. Sync/runtime checks validate the config permission and warn when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-idea/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 sync` 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/sync reports.\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 sync 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 `<repo-relative references dir>` | 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 sync 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"
63
+ "\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\nRuntime Honesty 6.9.0 reduced the runtime to two dispatched handlers:\n`session-start` (rehydrate) and `stop-handoff` (clean handoff). Earlier\nreleases also generated `prompt-guard`, `workflow-guard`,\n`context-monitor`, `pre-compact`, and `verify-current-state` handlers, but\nthose entry points were unreachable via the dispatch table they shipped\nwith and have been removed.\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| `stop_handoff` | Stop -> stop-handoff | stop -> stop-handoff | plugin session.idle -> stop-handoff | Stop -> stop-handoff |\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`, and `session-rehydrate` routes to `session-start`. 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`, `Stop`) |\n| Cursor | `cursor` | camelCase (`sessionStart`, `stop`) |\n| OpenCode | `opencode` | camelCase (`sessionStart`, `stop`) |\n| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `Stop`) |\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- Hook-level pre-tool blocking (prompt-guard / workflow-guard) was removed\n in 6.9.0. Stage transitions are owned by the canonical CLI path\n `node .cclaw/hooks/stage-complete.mjs <stage>`; harness sessions enforce\n workflow discipline via the iron-laws block surfaced at session-start\n rather than via per-tool hook interception.\n\n## Shared command contract\n\nAll harnesses receive the same utility commands:\n\n- `/cc` - flow entry and resume\n- `/cc` - stage progression and post-ship closeout\n- `/cc-idea` - idea 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-idea`, `/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, 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`post_ship_review -> archive`\n\n## Stage -> skill folder mapping\n\n| Stage | Skill folder |\n|---|---|\n| `brainstorm` | `brainstorm` |\n| `scope` | `scope` |\n| `design` | `design` |\n| `spec` | `spec` |\n| `plan` | `plan` |\n| `tdd` | `tdd` |\n| `review` | `review` |\n| `ship` | `ship` |\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. Sync/runtime checks validate the config permission and warn when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-idea/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 sync` 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/sync reports.\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 sync 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 `<repo-relative references dir>` | 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 sync 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"
64
64
  ].join("");
65
65
  }
@@ -326,7 +326,7 @@ function hasPriorAck(events, args, runId) {
326
326
  function usage() {
327
327
  process.stderr.write([
328
328
  "Usage:",
329
- " 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>] [--supersede=<prevSpanId>] [--allow-parallel] [--json]",
329
+ " 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>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--json]",
330
330
  " 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>] [--evidence-ref=<ref>] [--json]",
331
331
  " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
332
332
  "",
@@ -339,6 +339,10 @@ function usage() {
339
339
  "Dispatch dedup (v6.8.0):",
340
340
  " --supersede=<prevSpanId> close the previous active span on this (stage, agent) as 'stale' before recording the new scheduled row",
341
341
  " --allow-parallel record both spans as concurrent; new row is tagged allowParallel: true",
342
+ "",
343
+ "TDD parallel scheduler (v6.10.0):",
344
+ " --paths=<a,b,c> repo-relative paths the slice-implementer will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
345
+ " --override-cap=<int> raise the slice-implementer fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS overrides globally)",
342
346
  ""
343
347
  ].join("\\n") + "\\n");
344
348
  }
@@ -440,6 +444,13 @@ function buildRow(args, status, runId, now, options) {
440
444
  // Inherit the span's startTs from prior rows so monotonic validation
441
445
  // can compare against the original schedule, not the row write time.
442
446
  const startTs = (options && options.spanStartTs) || now;
447
+ // v6.10.0 (P1): claimedPaths from --paths=<comma-separated>. Empty
448
+ // arrays are dropped so the row stays compatible with v6.9 readers.
449
+ const claimedPathsRaw = typeof args.paths === "string" ? args.paths : "";
450
+ const claimedPaths = claimedPathsRaw
451
+ .split(",")
452
+ .map((value) => value.trim())
453
+ .filter((value) => value.length > 0);
443
454
  return {
444
455
  stage: args.stage,
445
456
  agent: args.agent,
@@ -461,7 +472,8 @@ function buildRow(args, status, runId, now, options) {
461
472
  completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
462
473
  endTs: TERMINAL.has(status) ? now : undefined,
463
474
  schemaVersion: LEDGER_SCHEMA_VERSION,
464
- allowParallel: args["allow-parallel"] === true ? true : undefined
475
+ allowParallel: args["allow-parallel"] === true ? true : undefined,
476
+ claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined
465
477
  };
466
478
  }
467
479
 
@@ -485,7 +497,11 @@ function findActiveSpanForPairInline(stage, agent, runId, entries) {
485
497
  for (const entry of entries) {
486
498
  if (!entry || typeof entry !== "object") continue;
487
499
  if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
488
- if (entry.runId && entry.runId !== runId) continue;
500
+ // Strict run-scope (v6.9.0 R7 fix): legacy entries without a runId
501
+ // are treated as foreign so they cannot keep an old span "active"
502
+ // across runs and trip dispatch_duplicate on a fresh dispatch.
503
+ if (typeof entry.runId !== "string" || entry.runId.length === 0) continue;
504
+ if (entry.runId !== runId) continue;
489
505
  if (entry.stage !== stage || entry.agent !== agent) continue;
490
506
  const existing = latestBySpan.get(entry.spanId);
491
507
  if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
@@ -498,6 +514,102 @@ function findActiveSpanForPairInline(stage, agent, runId, entries) {
498
514
  return null;
499
515
  }
500
516
 
517
+ // keep in sync with computeActiveSubagents in src/delegation.ts
518
+ function computeActiveSubagentsInline(entries) {
519
+ const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
520
+ const effectiveTs = (entry) =>
521
+ entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
522
+ const latestBySpan = new Map();
523
+ for (const entry of entries) {
524
+ if (!entry || typeof entry !== "object") continue;
525
+ if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
526
+ const existing = latestBySpan.get(entry.spanId);
527
+ if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
528
+ latestBySpan.set(entry.spanId, entry);
529
+ }
530
+ }
531
+ const active = [];
532
+ for (const entry of latestBySpan.values()) {
533
+ if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
534
+ }
535
+ return active;
536
+ }
537
+
538
+ // keep in sync with validateFileOverlap in src/delegation.ts
539
+ function validateFileOverlapInline(stamped, activeEntries) {
540
+ if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") {
541
+ return { autoParallel: false, conflict: null };
542
+ }
543
+ const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
544
+ if (newPaths.length === 0) {
545
+ return { autoParallel: false, conflict: null };
546
+ }
547
+ const sameLane = activeEntries.filter(
548
+ (entry) =>
549
+ entry.stage === stamped.stage &&
550
+ entry.agent === stamped.agent &&
551
+ entry.spanId !== stamped.spanId
552
+ );
553
+ if (sameLane.length === 0) {
554
+ return { autoParallel: true, conflict: null };
555
+ }
556
+ for (const existing of sameLane) {
557
+ const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
558
+ if (existingPaths.length === 0) {
559
+ return { autoParallel: false, conflict: null };
560
+ }
561
+ const overlap = newPaths.filter((p) => existingPaths.includes(p));
562
+ if (overlap.length > 0) {
563
+ return {
564
+ autoParallel: false,
565
+ conflict: {
566
+ existingSpanId: existing.spanId || "unknown",
567
+ newSpanId: stamped.spanId || "unknown",
568
+ pair: { stage: stamped.stage, agent: stamped.agent },
569
+ conflictingPaths: overlap
570
+ }
571
+ };
572
+ }
573
+ }
574
+ return { autoParallel: true, conflict: null };
575
+ }
576
+
577
+ const MAX_PARALLEL_SLICE_IMPLEMENTERS_INLINE = 5;
578
+
579
+ function readMaxParallelOverrideFromEnvInline() {
580
+ const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
581
+ if (typeof raw !== "string" || raw.trim().length === 0) return null;
582
+ const parsed = Number(raw);
583
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) return null;
584
+ return parsed;
585
+ }
586
+
587
+ // keep in sync with validateFanOutCap in src/delegation.ts
588
+ function validateFanOutCapInline(stamped, activeEntries, override) {
589
+ if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") return null;
590
+ if (stamped.status !== "scheduled") return null;
591
+ let cap;
592
+ if (override !== null && override !== undefined && Number.isInteger(override) && override >= 1) {
593
+ cap = override;
594
+ } else {
595
+ cap = readMaxParallelOverrideFromEnvInline() || MAX_PARALLEL_SLICE_IMPLEMENTERS_INLINE;
596
+ }
597
+ const sameLaneActive = activeEntries.filter(
598
+ (entry) =>
599
+ entry.stage === stamped.stage &&
600
+ entry.agent === stamped.agent &&
601
+ entry.spanId !== stamped.spanId
602
+ );
603
+ if (sameLaneActive.length + 1 > cap) {
604
+ return {
605
+ cap,
606
+ active: sameLaneActive.length,
607
+ pair: { stage: stamped.stage, agent: stamped.agent }
608
+ };
609
+ }
610
+ return null;
611
+ }
612
+
501
613
  function enforceDispatchDedupInline(stamped, priorEntries, args) {
502
614
  if (stamped.status !== "scheduled") return null;
503
615
  if (args["allow-parallel"] === true) return null;
@@ -988,6 +1100,31 @@ async function main() {
988
1100
  emitErrorJson("delegation_timestamp_non_monotonic", violation, json);
989
1101
  return;
990
1102
  }
1103
+
1104
+ // v6.10.0 (P1+P2): file-overlap scheduler + fan-out cap. Run before
1105
+ // the legacy dispatch dedup so disjoint claimedPaths can auto-promote
1106
+ // to allowParallel and bypass the duplicate guard.
1107
+ if (status === "scheduled") {
1108
+ const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
1109
+ const activeForRun = computeActiveSubagentsInline(sameRunPrior);
1110
+ const overlap = validateFileOverlapInline(clean, activeForRun);
1111
+ if (overlap.conflict) {
1112
+ emitErrorJson("dispatch_overlap", overlap.conflict, json);
1113
+ return;
1114
+ }
1115
+ if (overlap.autoParallel && clean.allowParallel !== true) {
1116
+ clean.allowParallel = true;
1117
+ args["allow-parallel"] = true;
1118
+ event.allowParallel = true;
1119
+ }
1120
+ const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
1121
+ const override = overrideRaw !== null ? Number(overrideRaw) : null;
1122
+ const capViolation = validateFanOutCapInline(clean, activeForRun, override);
1123
+ if (capViolation) {
1124
+ emitErrorJson("dispatch_cap", capViolation, json);
1125
+ return;
1126
+ }
1127
+ }
991
1128
  const dedupViolation = enforceDispatchDedupInline(clean, priorLedger, args);
992
1129
  if (dedupViolation) {
993
1130
  if (dedupViolation.kind === "supersede") {
@@ -158,9 +158,13 @@ function hardGateReference(law) {
158
158
  .join(", ");
159
159
  }
160
160
  export function ironLawsSkillMarkdown() {
161
+ // v6.9.0: Phase A purged the `PreToolUse` / `PostToolUse` / pre-tool
162
+ // pipeline handlers, so `review-coverage-complete-before-ship` is no
163
+ // longer hook-enforced — it now lives in the ship stage HARD-GATE.
164
+ // Only `stop-clean-or-handoff` (Stop hook) is still hook-enforced;
165
+ // everything else is stage-owned advisory.
161
166
  const enforcedLawIds = new Set([
162
- "stop-clean-or-handoff",
163
- "review-coverage-complete-before-ship"
167
+ "stop-clean-or-handoff"
164
168
  ]);
165
169
  const enforced = IRON_LAWS.filter((law) => enforcedLawIds.has(law.id));
166
170
  const advisory = IRON_LAWS.filter((law) => !enforcedLawIds.has(law.id));