cclaw-cli 0.51.29 → 0.55.2

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 (151) hide show
  1. package/README.md +22 -16
  2. package/dist/artifact-linter/brainstorm.d.ts +2 -0
  3. package/dist/artifact-linter/brainstorm.js +245 -0
  4. package/dist/artifact-linter/design.d.ts +2 -0
  5. package/dist/artifact-linter/design.js +323 -0
  6. package/dist/artifact-linter/plan.d.ts +2 -0
  7. package/dist/artifact-linter/plan.js +162 -0
  8. package/dist/artifact-linter/review-army.d.ts +24 -0
  9. package/dist/artifact-linter/review-army.js +365 -0
  10. package/dist/artifact-linter/review.d.ts +2 -0
  11. package/dist/artifact-linter/review.js +65 -0
  12. package/dist/artifact-linter/scope.d.ts +2 -0
  13. package/dist/artifact-linter/scope.js +115 -0
  14. package/dist/artifact-linter/shared.d.ts +246 -0
  15. package/dist/artifact-linter/shared.js +1488 -0
  16. package/dist/artifact-linter/ship.d.ts +2 -0
  17. package/dist/artifact-linter/ship.js +46 -0
  18. package/dist/artifact-linter/spec.d.ts +2 -0
  19. package/dist/artifact-linter/spec.js +108 -0
  20. package/dist/artifact-linter/tdd.d.ts +2 -0
  21. package/dist/artifact-linter/tdd.js +124 -0
  22. package/dist/artifact-linter.d.ts +4 -76
  23. package/dist/artifact-linter.js +56 -2949
  24. package/dist/cli.d.ts +2 -18
  25. package/dist/cli.js +8 -246
  26. package/dist/codex-feature-flag.d.ts +1 -1
  27. package/dist/codex-feature-flag.js +1 -1
  28. package/dist/config.d.ts +3 -2
  29. package/dist/config.js +67 -3
  30. package/dist/constants.d.ts +1 -7
  31. package/dist/constants.js +9 -15
  32. package/dist/content/cancel-command.js +2 -2
  33. package/dist/content/closeout-guidance.js +13 -10
  34. package/dist/content/core-agents.d.ts +18 -0
  35. package/dist/content/core-agents.js +51 -7
  36. package/dist/content/decision-protocol.d.ts +1 -1
  37. package/dist/content/decision-protocol.js +1 -1
  38. package/dist/content/examples.js +6 -6
  39. package/dist/content/harness-doc.js +20 -2
  40. package/dist/content/hook-inline-snippets.d.ts +17 -4
  41. package/dist/content/hook-inline-snippets.js +218 -5
  42. package/dist/content/hook-manifest.d.ts +2 -2
  43. package/dist/content/hook-manifest.js +2 -2
  44. package/dist/content/hooks.d.ts +1 -0
  45. package/dist/content/hooks.js +32 -137
  46. package/dist/content/idea-command.d.ts +8 -0
  47. package/dist/content/{ideate-command.js → idea-command.js} +57 -50
  48. package/dist/content/idea-frames.d.ts +31 -0
  49. package/dist/content/{ideate-frames.js → idea-frames.js} +9 -9
  50. package/dist/content/idea-ranking.d.ts +25 -0
  51. package/dist/content/{ideate-ranking.js → idea-ranking.js} +5 -5
  52. package/dist/content/iron-laws.d.ts +0 -1
  53. package/dist/content/iron-laws.js +31 -16
  54. package/dist/content/learnings.js +1 -1
  55. package/dist/content/meta-skill.js +11 -13
  56. package/dist/content/node-hooks.d.ts +10 -0
  57. package/dist/content/node-hooks.js +45 -11
  58. package/dist/content/opencode-plugin.js +3 -3
  59. package/dist/content/session-hooks.js +1 -1
  60. package/dist/content/skills.js +19 -7
  61. package/dist/content/stage-command.js +1 -1
  62. package/dist/content/stage-schema.js +44 -2
  63. package/dist/content/stages/_lint-metadata/index.js +26 -2
  64. package/dist/content/stages/brainstorm.js +13 -7
  65. package/dist/content/stages/design.js +16 -11
  66. package/dist/content/stages/plan.js +9 -6
  67. package/dist/content/stages/review.js +4 -4
  68. package/dist/content/stages/schema-types.d.ts +1 -1
  69. package/dist/content/stages/scope.js +15 -12
  70. package/dist/content/stages/ship.js +2 -2
  71. package/dist/content/stages/spec.js +9 -3
  72. package/dist/content/stages/tdd.js +14 -4
  73. package/dist/content/start-command.d.ts +2 -2
  74. package/dist/content/start-command.js +24 -21
  75. package/dist/content/status-command.js +8 -8
  76. package/dist/content/subagents.js +61 -7
  77. package/dist/content/templates.d.ts +1 -1
  78. package/dist/content/templates.js +104 -152
  79. package/dist/content/tree-command.js +2 -2
  80. package/dist/content/utility-skills.d.ts +2 -2
  81. package/dist/content/utility-skills.js +2 -2
  82. package/dist/content/view-command.js +4 -2
  83. package/dist/delegation.d.ts +2 -0
  84. package/dist/delegation.js +2 -1
  85. package/dist/early-loop.d.ts +66 -0
  86. package/dist/early-loop.js +275 -0
  87. package/dist/flow-state.d.ts +1 -1
  88. package/dist/flow-state.js +1 -1
  89. package/dist/gate-evidence.d.ts +8 -0
  90. package/dist/gate-evidence.js +141 -5
  91. package/dist/harness-adapters.d.ts +2 -2
  92. package/dist/harness-adapters.js +54 -122
  93. package/dist/harness-selection.d.ts +31 -0
  94. package/dist/harness-selection.js +214 -0
  95. package/dist/install.js +166 -38
  96. package/dist/internal/advance-stage/advance.d.ts +50 -0
  97. package/dist/internal/advance-stage/advance.js +480 -0
  98. package/dist/internal/advance-stage/cancel-run.d.ts +8 -0
  99. package/dist/internal/advance-stage/cancel-run.js +19 -0
  100. package/dist/internal/advance-stage/flow-state-coercion.d.ts +3 -0
  101. package/dist/internal/advance-stage/flow-state-coercion.js +81 -0
  102. package/dist/internal/advance-stage/helpers.d.ts +14 -0
  103. package/dist/internal/advance-stage/helpers.js +145 -0
  104. package/dist/internal/advance-stage/hook.d.ts +8 -0
  105. package/dist/internal/advance-stage/hook.js +40 -0
  106. package/dist/internal/advance-stage/parsers.d.ts +54 -0
  107. package/dist/internal/advance-stage/parsers.js +307 -0
  108. package/dist/internal/advance-stage/review-loop.d.ts +7 -0
  109. package/dist/internal/advance-stage/review-loop.js +170 -0
  110. package/dist/internal/advance-stage/rewind.d.ts +14 -0
  111. package/dist/internal/advance-stage/rewind.js +108 -0
  112. package/dist/internal/advance-stage/start-flow.d.ts +11 -0
  113. package/dist/internal/advance-stage/start-flow.js +136 -0
  114. package/dist/internal/advance-stage/verify.d.ts +29 -0
  115. package/dist/internal/advance-stage/verify.js +225 -0
  116. package/dist/internal/advance-stage.js +21 -1470
  117. package/dist/internal/compound-readiness.d.ts +1 -1
  118. package/dist/internal/compound-readiness.js +2 -2
  119. package/dist/internal/early-loop-status.d.ts +7 -0
  120. package/dist/internal/early-loop-status.js +90 -0
  121. package/dist/internal/runtime-integrity.d.ts +7 -0
  122. package/dist/internal/runtime-integrity.js +288 -0
  123. package/dist/internal/tdd-red-evidence.js +1 -1
  124. package/dist/knowledge-store.d.ts +3 -8
  125. package/dist/knowledge-store.js +16 -29
  126. package/dist/managed-resources.js +24 -2
  127. package/dist/policy.js +5 -7
  128. package/dist/run-archive.d.ts +1 -1
  129. package/dist/run-archive.js +16 -16
  130. package/dist/run-persistence.js +112 -12
  131. package/dist/tdd-cycle.d.ts +3 -3
  132. package/dist/tdd-cycle.js +1 -1
  133. package/dist/types.d.ts +18 -10
  134. package/package.json +1 -1
  135. package/dist/content/finish-command.d.ts +0 -2
  136. package/dist/content/finish-command.js +0 -26
  137. package/dist/content/ideate-command.d.ts +0 -8
  138. package/dist/content/ideate-frames.d.ts +0 -31
  139. package/dist/content/ideate-ranking.d.ts +0 -25
  140. package/dist/content/next-command.d.ts +0 -20
  141. package/dist/content/next-command.js +0 -298
  142. package/dist/content/seed-shelf.d.ts +0 -36
  143. package/dist/content/seed-shelf.js +0 -301
  144. package/dist/content/stage-common-guidance.d.ts +0 -1
  145. package/dist/content/stage-common-guidance.js +0 -106
  146. package/dist/doctor-registry.d.ts +0 -10
  147. package/dist/doctor-registry.js +0 -186
  148. package/dist/doctor.d.ts +0 -17
  149. package/dist/doctor.js +0 -2206
  150. package/dist/internal/hook-manifest.d.ts +0 -16
  151. package/dist/internal/hook-manifest.js +0 -77
@@ -13,15 +13,17 @@ export function closeoutSubstateInline() {
13
13
  return `\`${CLOSEOUT_SUBSTATE_KEY}\``;
14
14
  }
15
15
  export function closeoutNextCommandGuidance() {
16
- return `After ship completes, the closeout chain ${closeoutChainInline()} runs automatically, driven by ${closeoutSubstateInline()}. Continue through \`/cc-next\`; do not branch into \`ce:compound\`, a separate operations router, or a one-off closeout command. Ralph Loop may be mentioned only as tdd carry-forward context when it explains the next \`/cc-next\` move; it is not part of compound/archive routing.`;
16
+ return `After ship completes, the closeout chain ${closeoutChainInline()} runs automatically, driven by ${closeoutSubstateInline()}. Continue through \`/cc\`; do not branch into \`ce:compound\`, a separate operations router, or a one-off closeout command. Ralph Loop may be mentioned only as tdd carry-forward context when it explains the next \`/cc\` move; it is not part of compound/archive routing.`;
17
17
  }
18
18
  export function closeoutSubstateProtocolBullets() {
19
19
  return `When \`currentStage === "ship"\`, route by **${closeoutSubstateInline()}**:
20
- - \`"idle"\` or missing -> set ${closeoutSubstateInline()} = \`"retro_review"\`, then
21
- execute the in-stage retro protocol (draft + one structured accept/edit/skip ask).
22
- - \`"retro_review"\` -> continue the retro protocol (re-ask the structured
23
- question; the draft already exists do not regenerate it).
24
- - \`"compound_review"\` -> execute the in-stage compound closeout scan (not \`ce:compound\`):
20
+ - \`"idle"\` or missing -> outcome: initialize closeout by setting
21
+ ${closeoutSubstateInline()} = \`"retro_review"\`, then continue \`/cc\`
22
+ into the in-stage retro protocol (draft + one structured accept/edit/skip ask).
23
+ - \`"retro_review"\` -> outcome: finish retro acceptance/edit/skip and advance
24
+ closeout; the draft already exists, so continue it and do not regenerate it.
25
+ - \`"compound_review"\` -> outcome: execute the in-stage compound closeout scan
26
+ (not \`ce:compound\`) and advance toward archive readiness:
25
27
  read \`.cclaw/state/compound-readiness.json\` plus the relevant tail of
26
28
  \`.cclaw/knowledge.jsonl\`, assess overlap before adding duplicate knowledge,
27
29
  separate bug-track learnings (turn into rules/tests/remediation) from
@@ -31,12 +33,13 @@ export function closeoutSubstateProtocolBullets() {
31
33
  session transcripts for matching historical learnings; only do it after opt-in.
32
34
  Ask **one** structured question (apply / skip) per candidate cluster or a
33
35
  single accept-all / skip choice, then advance substate.
34
- - \`"ready_to_archive"\` -> run \`npx cclaw-cli archive\` (or \`npx cclaw-cli archive --name=<slug>\`) and reset state.
35
- - \`"archived"\` (transient) -> report "run archived" and stop.`;
36
+ - \`"ready_to_archive"\` -> outcome: continue \`/cc\` so the runtime archive step
37
+ executes, snapshots, and resets active state.
38
+ - \`"archived"\` (transient) -> outcome: report "run archived" and stop (flow complete).`;
36
39
  }
37
40
  export function closeoutFlowMapSentence() {
38
- return `The first stage names are the critical path. \`retro\`, \`compound\`, and \`archive\` are post-ship closeout substates under ${closeoutSubstateInline()}, not separate stage schemas or commands. Continue them with \`/cc-next\`; do not route compound closeout through \`ce:compound\`.`;
41
+ return `The first stage names are the critical path. \`retro\`, \`compound\`, and \`archive\` are post-ship closeout substates under ${closeoutSubstateInline()}, not separate stage schemas or commands. Continue them with \`/cc\`; do not route compound closeout through \`ce:compound\`.`;
39
42
  }
40
43
  export function closeoutProtocolBehaviorSentence() {
41
- return `Keep decision, completion, and preamble discipline inline: ask only decision-changing questions, verify gates before advancing, and keep context compact. After \`ship\`, keep using \`/cc-next\` through ${closeoutChainInline()}; do not route normal closeout through \`ce:compound\` or a separate operations command. In compound closeout, assess overlap before appending knowledge: refresh recurring bug-track learnings as actionable rules/tests, keep knowledge-track learnings as durable process/project guidance, and mark outdated entries with lightweight \`supersedes\` / \`superseded_by\` fields instead of building a new doc system.`;
44
+ return `Keep decision, completion, and preamble discipline inline: ask only decision-changing questions, verify gates before advancing, and keep context compact. After \`ship\`, keep using \`/cc\` through ${closeoutChainInline()}; do not route normal closeout through \`ce:compound\` or a separate operations command. In compound closeout, assess overlap before appending knowledge: refresh recurring bug-track learnings as actionable rules/tests, keep knowledge-track learnings as durable process/project guidance, and mark outdated entries with lightweight \`supersedes\` / \`superseded_by\` fields instead of building a new doc system.`;
42
45
  }
@@ -70,6 +70,15 @@ export declare const CCLAW_AGENTS: readonly [{
70
70
  readonly relatedStages: ["brainstorm", "scope"];
71
71
  readonly returnSchema: AgentReturnSchema;
72
72
  readonly body: string;
73
+ }, {
74
+ readonly name: "product-strategist";
75
+ readonly description: "PROACTIVE during scope. MUST BE USED when selected scope mode is SCOPE EXPANSION or SELECTIVE EXPANSION to pressure-test 10x vision, strategic upside, and long-term trajectory before lock.";
76
+ readonly tools: ["Read", "Grep", "Glob", "WebSearch"];
77
+ readonly model: "deep";
78
+ readonly activation: "proactive";
79
+ readonly relatedStages: ["scope"];
80
+ readonly returnSchema: AgentReturnSchema;
81
+ readonly body: string;
73
82
  }, {
74
83
  readonly name: "critic";
75
84
  readonly description: "PROACTIVE during brainstorm/scope/design when premises, alternatives, cost, rollback, or hidden assumptions need adversarial pressure.";
@@ -97,6 +106,15 @@ export declare const CCLAW_AGENTS: readonly [{
97
106
  readonly relatedStages: ["spec"];
98
107
  readonly returnSchema: AgentReturnSchema;
99
108
  readonly body: string;
109
+ }, {
110
+ readonly name: "spec-document-reviewer";
111
+ readonly description: "PROACTIVE during spec when self-review surfaces issues, subsystem boundaries feel broad, or the artifact needs a final plan-readiness pass.";
112
+ readonly tools: ["Read", "Grep", "Glob"];
113
+ readonly model: "balanced";
114
+ readonly activation: "proactive";
115
+ readonly relatedStages: ["spec"];
116
+ readonly returnSchema: AgentReturnSchema;
117
+ readonly body: string;
100
118
  }, {
101
119
  readonly name: "reviewer";
102
120
  readonly description: "MANDATORY during review. MUST BE USED to run a two-pass audit: spec compliance first, then correctness/maintainability/performance/architecture.";
@@ -162,6 +162,28 @@ export const CCLAW_AGENTS = [
162
162
  "**Role boundary:** frame value and problem fit. Do NOT choose implementation architecture."
163
163
  ].join("\n")
164
164
  },
165
+ {
166
+ name: "product-strategist",
167
+ description: "PROACTIVE during scope. MUST BE USED when selected scope mode is SCOPE EXPANSION or SELECTIVE EXPANSION to pressure-test 10x vision, strategic upside, and long-term trajectory before lock.",
168
+ tools: ["Read", "Grep", "Glob", "WebSearch"],
169
+ model: "deep",
170
+ activation: "proactive",
171
+ relatedStages: ["scope"],
172
+ returnSchema: ADVISORY_RETURN_SCHEMA,
173
+ body: [
174
+ "You are a **product strategy specialist** focused on expansion decisions.",
175
+ "",
176
+ "Produce concise evidence for:",
177
+ "- 10x vision and ideal outcome versus baseline scope",
178
+ "- concrete expansion proposals (not cosmetic variants)",
179
+ "- expected upside, reversibility, and trajectory impact",
180
+ "- explicit add/defer/skip recommendation per proposal",
181
+ "",
182
+ "Operate only when scope mode is SCOPE EXPANSION or SELECTIVE EXPANSION; otherwise return `None - mode does not require strategist pass`.",
183
+ "",
184
+ "**Role boundary:** challenge strategic scope and trajectory; do NOT choose implementation architecture."
185
+ ].join("\n")
186
+ },
165
187
  {
166
188
  name: "critic",
167
189
  description: "PROACTIVE during brainstorm/scope/design when premises, alternatives, cost, rollback, or hidden assumptions need adversarial pressure.",
@@ -216,6 +238,28 @@ export const CCLAW_AGENTS = [
216
238
  "**Role boundary:** validate the spec; do NOT write plan tasks or implementation."
217
239
  ].join("\n")
218
240
  },
241
+ {
242
+ name: "spec-document-reviewer",
243
+ description: "PROACTIVE during spec when self-review surfaces issues, subsystem boundaries feel broad, or the artifact needs a final plan-readiness pass.",
244
+ tools: ["Read", "Grep", "Glob"],
245
+ model: "balanced",
246
+ activation: "proactive",
247
+ relatedStages: ["spec"],
248
+ returnSchema: REVIEW_RETURN_SCHEMA,
249
+ body: [
250
+ "You are a **spec document reviewer** focused on plan-readiness.",
251
+ "",
252
+ "Run a concise pass over:",
253
+ "- completeness of required spec sections",
254
+ "- consistency across acceptance criteria, assumptions, and mapping",
255
+ "- clarity / ambiguity / placeholder drift",
256
+ "- single-subsystem scope fit and YAGNI pressure",
257
+ "",
258
+ "Return `PASS`, `PASS_WITH_GAPS`, `FAIL`, or `BLOCKED` with concrete evidence refs and minimal corrective actions.",
259
+ "",
260
+ "**Role boundary:** review the spec artifact only; do NOT write plan tasks or implementation."
261
+ ].join("\n")
262
+ },
219
263
  {
220
264
  name: "reviewer",
221
265
  description: "MANDATORY during review. MUST BE USED to run a two-pass audit: spec compliance first, then correctness/maintainability/performance/architecture.",
@@ -498,11 +542,11 @@ export function agentRoutingTable() {
498
542
  return `| Stage Entry | Primary Agent(s) | Supporting guidance |
499
543
  |---|---|---|
500
544
  | Brainstorm (start with \`/cc <idea>\`) | ${brainstormPrimary} | Run in-thread research playbooks: \`research/repo-scan.md\`, \`research/learnings-lookup.md\` |
501
- | Scope / Design / Plan (via \`/cc-next\`) | ${scopeDesignPlanPrimary} | Use \`research/git-history.md\` (scope) and \`research/framework-docs-lookup.md\` + \`research/best-practices-lookup.md\` (design) as needed |
502
- | Spec (via \`/cc-next\`) | ${specPrimary} | planner (if ambiguity or conflicts remain) |
503
- | TDD (via \`/cc-next\`) | ${tddPrimary} | doc-updater on public behavior/config changes |
504
- | Review (via \`/cc-next\`) | ${reviewPrimary} | conditional second reviewer for high blast-radius diffs |
505
- | Ship (via \`/cc-next\`) | ${shipPrimary} | security-reviewer when release risk is elevated |
545
+ | Scope / Design / Plan (via \`/cc\`) | ${scopeDesignPlanPrimary} | Use \`research/git-history.md\` (scope) and \`research/framework-docs-lookup.md\` + \`research/best-practices-lookup.md\` (design) as needed |
546
+ | Spec (via \`/cc\`) | ${specPrimary} | planner (if ambiguity or conflicts remain) |
547
+ | TDD (via \`/cc\`) | ${tddPrimary} | doc-updater on public behavior/config changes |
548
+ | Review (via \`/cc\`) | ${reviewPrimary} | conditional second reviewer for high blast-radius diffs |
549
+ | Ship (via \`/cc\`) | ${shipPrimary} | security-reviewer when release risk is elevated |
506
550
  `;
507
551
  }
508
552
  /**
@@ -511,8 +555,8 @@ export function agentRoutingTable() {
511
555
  export function agentCostTierTable() {
512
556
  return `| Tier | Use for | Example agents |
513
557
  |---|---|---|
514
- | \`deep\` | one heavy planning pass per stage | planner |
515
- | \`balanced\` | discovery, criticism, review, TDD, and bounded worker execution | product-manager, critic, reviewer, security-reviewer, test-author, implementer, fixer |
558
+ | \`deep\` | one heavy planning/strategy pass per stage | planner, product-strategist |
559
+ | \`balanced\` | discovery, criticism, review, TDD, and bounded worker execution | product-manager, critic, spec-document-reviewer, reviewer, security-reviewer, test-author, implementer, fixer |
516
560
  | \`fast\` | bounded maintenance updates with limited blast radius | doc-updater |
517
561
  `;
518
562
  }
@@ -9,4 +9,4 @@ export declare const STRUCTURED_ASK_TOOL_LIST_IDEATE = "`AskUserQuestion` on Cla
9
9
  export declare function structuredAskFallbackSentence(toolList?: string): string;
10
10
  export declare function decisionProtocolInstruction(subject: string, optionsClause: string, recommendationClause: string, toolList?: string): string;
11
11
  export declare function structuredAskSingleChoiceInstruction(subject: string, choicesClause: string, toolList?: string): string;
12
- export declare function ideateStructuredAskToolsWithFallback(): string;
12
+ export declare function ideaStructuredAskToolsWithFallback(): string;
@@ -15,6 +15,6 @@ export function decisionProtocolInstruction(subject, optionsClause, recommendati
15
15
  export function structuredAskSingleChoiceInstruction(subject, choicesClause, toolList = STRUCTURED_ASK_TOOL_LIST_GENERIC) {
16
16
  return `For ${subject}: use the native structured-ask tool (${toolList}) only if runtime schema is confirmed; otherwise collect ${choicesClause} with a plain-text single-choice prompt.`;
17
17
  }
18
- export function ideateStructuredAskToolsWithFallback() {
18
+ export function ideaStructuredAskToolsWithFallback() {
19
19
  return `${STRUCTURED_ASK_TOOL_LIST_IDEATE}; fall back to a plain-text lettered list when the tool is hidden or errors`;
20
20
  }
@@ -753,7 +753,7 @@ const STAGE_DOMAIN_SAMPLES = {
753
753
  {
754
754
  domain: "cli",
755
755
  label: "Direction",
756
- body: "Problem: `cclaw archive` silently deletes 30+ day runs with no preview. Success: a `--dry-run` flag prints would-be-archived run IDs to stdout and exits 0; current behavior is unchanged without the flag. Anti-success: adding an interactive confirmation prompt that breaks CI scripts."
756
+ body: "Problem: `npx cclaw-cli archive` silently deletes 30+ day runs with no preview. Success: a `--dry-run` flag prints would-be-archived run IDs to stdout and exits 0; current behavior is unchanged without the flag. Anti-success: adding an interactive confirmation prompt that breaks CI scripts."
757
757
  },
758
758
  {
759
759
  domain: "library",
@@ -775,7 +775,7 @@ const STAGE_DOMAIN_SAMPLES = {
775
775
  {
776
776
  domain: "cli",
777
777
  label: "Scope line",
778
- body: "In: add `--dry-run` to `cclaw archive`; out: redesigning archive formats, adding retention flags, or changing the default. Discretion: exact wording of stdout lines. NOT in scope: touching `init` / `sync` / `doctor` subcommands."
778
+ body: "In: add `--dry-run` to `npx cclaw-cli archive`; out: redesigning archive formats, adding retention flags, or changing the default. Discretion: exact wording of stdout lines. NOT in scope: touching `init` / `sync` / `sync` subcommands."
779
779
  },
780
780
  {
781
781
  domain: "library",
@@ -824,7 +824,7 @@ const STAGE_DOMAIN_SAMPLES = {
824
824
  {
825
825
  domain: "cli",
826
826
  label: "AC",
827
- body: "AC-C1: Given `cclaw init --claude` run in an empty directory, exit code is `0`, `.cclaw/config.yaml` is created with `harnesses: [claude]`, and stderr contains no warnings (asserted by `tests/integration/init-sync-doctor.test.ts`)."
827
+ body: "AC-C1: Given `npx cclaw-cli init --harnesses=claude` run in an empty directory, exit code is `0`, `.cclaw/config.yaml` is created with `harnesses: [claude]`, and stderr contains no warnings (asserted by `tests/integration/init-sync-runtime.test.ts`)."
828
828
  },
829
829
  {
830
830
  domain: "library",
@@ -846,7 +846,7 @@ const STAGE_DOMAIN_SAMPLES = {
846
846
  {
847
847
  domain: "cli",
848
848
  label: "Task",
849
- body: "T-C-2 `[~3m]`: Add `--dry-run` flag to `cclaw archive` that prints the would-be-archived run IDs to stdout and exits 0. AC-C3. Verify: `node dist/cli.js archive --dry-run` + `tests/unit/cli-parse.test.ts`."
849
+ body: "T-C-2 `[~3m]`: Add `--dry-run` flag to `npx cclaw-cli archive` that prints the would-be-archived run IDs to stdout and exits 0. AC-C3. Verify: `node dist/cli.js archive --dry-run` + `tests/unit/cli-parse.test.ts`."
850
850
  },
851
851
  {
852
852
  domain: "library",
@@ -890,7 +890,7 @@ const STAGE_DOMAIN_SAMPLES = {
890
890
  {
891
891
  domain: "cli",
892
892
  label: "Finding",
893
- body: "R-C-2 (Suggestion, UX): `cclaw archive --dry-run` prints run IDs without a trailing newline, breaking downstream `xargs` pipelines. Evidence: `echo '' | xargs -I{} printf '%s\\n' {}` contrast. Fix owner: CLI; non-blocking."
893
+ body: "R-C-2 (Suggestion, UX): `npx cclaw-cli archive --dry-run` prints run IDs without a trailing newline, breaking downstream `xargs` pipelines. Evidence: `echo '' | xargs -I{} printf '%s\\n' {}` contrast. Fix owner: CLI; non-blocking."
894
894
  },
895
895
  {
896
896
  domain: "library",
@@ -912,7 +912,7 @@ const STAGE_DOMAIN_SAMPLES = {
912
912
  {
913
913
  domain: "cli",
914
914
  label: "Rollback",
915
- body: "Trigger: `cclaw init --claude` exits non-zero on a fresh tmp dir, OR `cclaw doctor` regresses (FAIL count increases) on the smoke matrix. Steps: `npm unpublish cclaw-cli@<version>` (within the 72h window) or `npm deprecate cclaw-cli@<version> '<reason>'`; publish the previous patch. Verify: `npx cclaw-cli@latest --version` prints the previous version."
915
+ body: "Trigger: `npx cclaw-cli init --harnesses=claude` exits non-zero on a fresh tmp dir, OR `npx cclaw-cli sync` regresses (FAIL count increases) on the smoke matrix. Steps: `npm unpublish cclaw-cli@<version>` (within the 72h window) or `npm deprecate cclaw-cli@<version> '<reason>'`; publish the previous patch. Verify: `npx cclaw-cli@latest --version` prints the previous version."
916
916
  },
917
917
  {
918
918
  domain: "library",
@@ -35,13 +35,31 @@ function perHarnessRecipeMarkdown() {
35
35
  const examples = recipes
36
36
  .map((recipe) => `**${recipe.harnessId}**:\n\n` + recipe.lifecycleCommands.map((cmd) => ` ${cmd}`).join("\n"))
37
37
  .join("\n\n");
38
- return `\n\n## Per-Harness Lifecycle Recipe\n\n| Harness | Surface | Agent definition path | fulfillmentMode | Lifecycle |\n|---|---|---|---|---|\n${rows}\n\nNeutral placeholder tokens only: \`<agent-name>\`, \`<stage>\`, \`<run-id>\`, \`<span-id>\`, \`<dispatch-id>\`, \`<agent-def-path>\`, \`<iso-ts>\`, \`<artifact-anchor>\`. See \`docs/quality-gates.md\` for stage-by-stage gate mapping.\n\nThe four shipped harnesses (\`claude\`, \`cursor\`, \`opencode\`, \`codex\`) each ship with a canonical primary surface in the table above. Repair hints: \`cclaw sync\` safely regenerates shims/plugins/agents; Codex also needs \`[features] codex_hooks = true\`; OpenCode needs \`opencode.json(.c)\` plugin registration; role-switch completions require evidenceRefs. The remaining enum values \`generic-task\`, \`role-switch\`, and \`manual\` are documented in the dispatch-surface table below and are available to any harness as fallback paths when the primary surface is unavailable.\n\n${examples}\n\n${dispatchSurfaceTableMarkdown()}\n\n### Legacy ledger upgrade\n\nPre-v3 ledger entries that lack a recorded \`dispatchSurface\` are tagged \`fulfillmentMode: "legacy-inferred"\` on read. Stage-complete blocks completion until those rows are re-recorded with the v3 helper:\n\n node .cclaw/hooks/delegation-record.mjs \\\n --rerecord \\\n --span-id=<span-id> \\\n --dispatch-id=<dispatch-id> \\\n --dispatch-surface=<surface> \\\n --agent-definition-path=<agent-def-path> \\\n --ack-ts=<iso-ts> \\\n --completed-ts=<iso-ts> \\\n --json\n\n\`--dispatch-surface\` must be one of the values listed in the dispatch-surface table above (the enum is generated verbatim from \`src/delegation.ts::DELEGATION_DISPATCH_SURFACES\`). Surfaces must align with the allowed agent-definition-path prefixes shown alongside each surface; \`role-switch\` and \`manual\` accept any path. The deprecated \`task\` surface is rejected.\n\n`;
38
+ return `\n\n## Per-Harness Lifecycle Recipe\n\n| Harness | Surface | Agent definition path | fulfillmentMode | Lifecycle |\n|---|---|---|---|---|\n${rows}\n\nNeutral placeholder tokens only: \`<agent-name>\`, \`<stage>\`, \`<run-id>\`, \`<span-id>\`, \`<dispatch-id>\`, \`<agent-def-path>\`, \`<iso-ts>\`, \`<artifact-anchor>\`. See \`docs/quality-gates.md\` for stage-by-stage gate mapping.\n\nThe four shipped harnesses (\`claude\`, \`cursor\`, \`opencode\`, \`codex\`) each ship with a canonical primary surface in the table above. Repair hints: \`npx cclaw-cli sync\` safely regenerates shims/plugins/agents; Codex also needs \`[features] codex_hooks = true\`; OpenCode needs \`opencode.json(.c)\` plugin registration; role-switch completions require evidenceRefs. The remaining enum values \`generic-task\`, \`role-switch\`, and \`manual\` are documented in the dispatch-surface table below and are available to any harness as fallback paths when the primary surface is unavailable.\n\n${examples}\n\n${dispatchSurfaceTableMarkdown()}\n\n### Legacy ledger upgrade\n\nPre-v3 ledger entries that lack a recorded \`dispatchSurface\` are tagged \`fulfillmentMode: "legacy-inferred"\` on read. Stage-complete blocks completion until those rows are re-recorded with the v3 helper:\n\n node .cclaw/hooks/delegation-record.mjs \\\n --rerecord \\\n --span-id=<span-id> \\\n --dispatch-id=<dispatch-id> \\\n --dispatch-surface=<surface> \\\n --agent-definition-path=<agent-def-path> \\\n --ack-ts=<iso-ts> \\\n --completed-ts=<iso-ts> \\\n --json\n\n\`--dispatch-surface\` must be one of the values listed in the dispatch-surface table above (the enum is generated verbatim from \`src/delegation.ts::DELEGATION_DISPATCH_SURFACES\`). Surfaces must align with the allowed agent-definition-path prefixes shown alongside each surface; \`role-switch\` and \`manual\` accept any path. The deprecated \`task\` surface is rejected.\n\n`;
39
+ }
40
+ function hookLayeringSectionMarkdown() {
41
+ return `## Hook layering
42
+
43
+ Hook behavior is intentionally split into three layers so docs, generation, and runtime checks stay in sync:
44
+
45
+ | Layer | Source of truth | Responsibility |
46
+ |---|---|---|
47
+ | 1) Manifest projection | \`src/content/hook-manifest.ts\` | Canonical handler/event map per harness. This is the authoring surface for new handlers or reroutes. |
48
+ | 2) JSON schema descriptors | \`src/hook-schemas/*.json\` + \`src/hook-schema.ts\` descriptor map | Declares required harness-native event arrays and schema version for each harness document. |
49
+ | 3) Runtime TS validation | \`src/hook-schema.ts::validateHookDocument\` + sync hook checks | Validates generated hook JSON shape/required events and reports actionable diagnostics. |
50
+
51
+ Flow:
52
+ 1. Manifest defines handler bindings.
53
+ 2. Hook documents are generated from manifest projections.
54
+ 3. Schema descriptors + TS validators enforce structure at sync time.
55
+ `;
39
56
  }
40
57
  export function harnessIntegrationDocMarkdown() {
41
58
  const head = "# Harness Integration Matrix\n\nGenerated from `src/harness-adapters.ts` capabilities and hook event mappings.";
42
59
  return [
43
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",
44
61
  perHarnessRecipeMarkdown(),
45
- "\nFallback legend:\n\n- `native` \u2014 first-class named subagent dispatch (Claude).\n- `generic-dispatch` \u2014 generic Task dispatcher mapped to cclaw roles (Cursor).\n- `role-switch` \u2014 degraded fallback for a runtime where declared native/generic dispatch is unavailable; explicit role headers, artifact outputs, and non-empty delegation-log evidenceRefs are required.\n- `waiver` \u2014 no parity path; reserved for harnesses that cannot role-switch (none shipped).\n\n## Stage-Aware Native Dispatch Workflow\n\nOpenCode and Codex receive generated native isolated subagents. Use them before considering role-switch fallback:\n\n1. Use the active stage skill's generated dispatch table as the source of truth.\n2. OpenCode: invoke `.opencode/agents/<agent>.md` via Task or `@<agent>`; Codex: ask Codex to spawn `.codex/agents/<agent>.toml` by name, in parallel when lanes are independent.\n3. Load `.cclaw/agents/<agent>.md`, execute only that role's stage task, and write outputs into the active stage artifact.\n4. Append `.cclaw/state/delegation-events.jsonl` for scheduled/launched/acknowledged/completed/failed/waived/stale, then mirror current state in `.cclaw/state/delegation-log.json`. The ledger is current state; the event log is proof/audit.\n5. Treat completed role-switch rows without `evidenceRefs` as unresolved; treat native isolated completion without matching `spanId` + `dispatchId`/`workerRunId` + `ackTs` + `completedTs` as fake isolated completion. Native isolated rows are not a role-switch substitute and should reflect a real dispatched worker.\n\nThis is staged agent work backed by the harness-native subagent surfaces. Role-switch remains only a degraded fallback when that surface is unavailable in the active runtime.\n\n## Parallel research dispatch semantics\n\nDesign-stage research fleet uses the same parity model:\n\n- **Claude / Cursor**: dispatch all four research lenses in one turn\n (stack, features, architecture, pitfalls) and synthesize into\n `.cclaw/artifacts/02a-research.md`.\n- **OpenCode / Codex**: dispatch generated native subagents for the same\n four lenses and run independent lanes in parallel where the active runtime\n permits. Use role-switch with evidence only as a degraded fallback.\n\n## Semantic hook event coverage\n\n| Event | Claude | Cursor | OpenCode | Codex |\n|---|---|---|---|---|\n| `session_rehydrate` | SessionStart matcher startup|resume|clear|compact | sessionStart/sessionResume/sessionClear/sessionCompact | plugin event handlers + transform rehydration | SessionStart matcher startup|resume |\n| `pre_tool_prompt_guard` | PreToolUse -> prompt-guard | preToolUse -> prompt-guard | plugin tool.execute.before -> prompt-guard | PreToolUse matcher Bash -> prompt-guard (plus UserPromptSubmit for non-Bash prompts) |\n| `pre_tool_workflow_guard` | PreToolUse -> workflow-guard | preToolUse -> workflow-guard | plugin tool.execute.before -> workflow-guard | PreToolUse matcher Bash -> workflow-guard (Bash-only) |\n| `post_tool_context_monitor` | PostToolUse -> context-monitor | postToolUse -> context-monitor | plugin tool.execute.after -> context-monitor | PostToolUse matcher Bash -> context-monitor (Bash-only) |\n| `stop_handoff` | Stop -> stop-handoff | stop -> stop-handoff | plugin session.idle -> stop-handoff | Stop -> stop-handoff |\n| `precompact_compat` | PreCompact -> pre-compact | sessionCompact -> pre-compact | plugin session.compacted -> pre-compact | missing |\n| `strict_state_verify` | missing | missing | missing | UserPromptSubmit -> verify-current-state (blocks only in strict mode) |\n\n## Hook lifecycle aliases\n\nThe generated Node dispatcher accepts a small compatibility alias set for lifecycle names: `stop` and `stop-checkpoint` route to `stop-handoff`, `precompact` routes to `pre-compact`, and `session-rehydrate` routes to `session-start`. The `pre-compact` handler is intentionally a no-op compatibility marker; rehydration remains the `session-start` responsibility after compact events. Harness JSON should still emit the canonical handler names from `src/content/hook-manifest.ts`.\n\n## Hook event casing\n\nHook keys are intentionally harness-native and must not be normalized:\n\n| Harness | ID | Event key casing |\n|---|---|---|\n| Claude Code | `claude` | PascalCase (`SessionStart`, `PreToolUse`) |\n| Cursor | `cursor` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenCode | `opencode` | camelCase (`sessionStart`, `preToolUse`) |\n| OpenAI Codex | `codex` | PascalCase (`SessionStart`, `PreToolUse`) |\n\nUse the exact event names from each harness schema. Treating all hooks as one\nshared casing silently breaks generated wiring.\n\n## Interpretation\n\n- `tier1`: full native delegation + structured asks + full hook surface.\n- `tier2`: usable flow with capability gaps; mandatory delegation can require waivers.\n- Codex-specific ceiling: `PreToolUse` can only intercept `Bash`. Direct\n `Write`/`Edit` to `.cclaw/state/flow-state.json` cannot be hard-blocked\n at hook level, so the canonical path is\n `node .cclaw/hooks/stage-complete.mjs <stage>` plus the non-blocking\n `UserPromptSubmit` state nudge.\n- In `strict` mode, Codex additionally runs the generated Node/runtime `verify-current-state` path on `UserPromptSubmit` as a fail-closed check. Advisory mode remains non-blocking, including when the generated local Node entrypoint is missing; doctor reports that install drift separately. This strict-only coverage is represented explicitly by the `strict_state_verify` semantic row above.\n\n## Shared command contract\n\nAll harnesses receive the same utility commands:\n\n- `/cc` - flow entry and resume\n- `/cc-next` - stage progression and post-ship closeout\n- `/cc-ideate` - ideate mode for ranked repo-improvement backlog\n- `/cc-view` - read-only router for status/tree/diff\n\nRead-only subcommands:\n- `/cc-view status` - visual flow snapshot\n- `/cc-view tree` - deep flow tree (stages, artifacts, stale markers)\n- `/cc-view diff` - before/after flow-state diff map\n\nOperational work is handled by `/cc`, `/cc-next`, `/cc-ideate`, `/cc-view`, and `node .cclaw/hooks/stage-complete.mjs <stage>` inside the installed harness runtime. `npx cclaw-cli` is the installer/support surface for init, sync, upgrade, doctor, and explicit/manual archive; the normal stage flow must not depend on a runtime `cclaw` binary in PATH.\n\nCritical-path stage order remains canonical:\n`brainstorm -> scope -> design -> spec -> plan -> tdd -> review -> ship`\n\nEvery track then closes out through:\n`retro -> compound -> archive`\n\n## Stage -> skill folder mapping\n\n| Stage | Skill folder |\n|---|---|\n| `brainstorm` | `brainstorming` |\n| `scope` | `scope-shaping` |\n| `design` | `engineering-design-lock` |\n| `spec` | `specification-authoring` |\n| `plan` | `planning-and-task-breakdown` |\n| `tdd` | `test-driven-development` |\n| `review` | `two-layer-review` |\n| `ship` | `shipping-and-handoff` |\n\nThis map is generated from `src/constants.ts::STAGE_TO_SKILL_FOLDER` so\nskill-path naming stays explicit and stable even when stage ids differ from\nfolder names.\n\n## Install surfaces\n\nAlways generated:\n\n- `.cclaw/commands/*.md`\n- `.cclaw/skills/*/SKILL.md`\n- `.cclaw/state/*.json|*.jsonl`\n- `AGENTS.md` managed block\n\nHarness-specific additions:\n\n- `claude`: `.claude/commands/cc*.md`, `.claude/hooks/hooks.json`\n- `cursor`: `.cursor/commands/cc*.md`, `.cursor/hooks.json`, `.cursor/rules/cclaw-workflow.mdc`\n- `opencode`: `.opencode/commands/cc*.md`, `.opencode/plugins/cclaw-plugin.mjs`, opencode plugin registration with `permission.question: \"allow\"`; set `OPENCODE_ENABLE_QUESTION_TOOL=1` for ACP clients so structured asks can route through question tooling. Doctor validates the config permission and warns when the environment hint is absent.\n- `codex`: `.agents/skills/cc/SKILL.md`, `.agents/skills/cc-next/SKILL.md`, `.agents/skills/cc-ideate/SKILL.md`, `.agents/skills/cc-view/SKILL.md`, `.codex/hooks.json` (Codex CLI reads `.agents/skills/` for custom skills and consumes `.codex/hooks.json` on v0.114+ when `[features] codex_hooks = true` is set in `~/.codex/config.toml`. `.codex/commands/` and the legacy `.agents/skills/cclaw-cc*/` layout from v0.39.x are auto-cleaned on sync.)\n\n## Runtime observability\n\n- `npx cclaw-cli doctor` validates shim, hook, and lifecycle surfaces against this capability model.\n- `/cc-view status` and `/cc-view tree` surface the same harness tier/fallback facts from the generated runtime metadata.\n\n## Delegation Proof Model\n\nRuntime state is split deliberately:\n\n- `.cclaw/state/delegation-log.json` is the compact current ledger used by stage gates and `/cc-view` summaries.\n- `.cclaw/state/delegation-events.jsonl` is append-only audit proof for `scheduled`, `launched`, `acknowledged`, `completed`, `failed`, `waived`, and `stale` lifecycle transitions.\n- `.cclaw/state/subagents.json` is a lightweight active-worker tracker for status/tree/doctor surfaces.\n- `.cclaw/hooks/delegation-record.mjs` is the generated helper for lifecycle rows/events. It validates required fields and emits JSON diagnostics with `--json`.\n\nIsolated completion requires `spanId`, `dispatchId` or `workerRunId`, `dispatchSurface`, `agentDefinitionPath`, `ackTs`, `launchedTs`, and `completedTs`. Cursor/generic dispatch and role-switch also require evidence refs when artifact evidence is the proof source. Legacy inferred completions remain readable, but doctor reports them as warnings because they predate event-log proof.\n\n## Reference Audit Appendix\n\nStatus meanings: `deep` = read for transferable implementation contract; `targeted` = inspected the relevant files only; `skimmed` = searched/read enough to classify; `not relevant` = intentionally excluded from implementation influence.\n\n| Reference path under `/Users/zuevrs/Downloads/references` | Status | Findings preserved |\n|---|---|---|\n| `evanklem-evanflow/skills/evanflow-coder-overseer/SKILL.md` | deep | Contract-first coder/overseer loop, reviewer reads code rather than worker narrative, and integration overseer pattern map cleanly onto cclaw subagent guidance. |\n| `evanklem-evanflow/agents/evanflow-coder.md` | targeted | Worker role is narrow: implement the pasted contract, avoid broad orchestration, and return evidence for overseer verification. |\n| `evanklem-evanflow/agents/evanflow-overseer.md` | targeted | Overseer validates actual code and acceptance evidence before controller marks work complete. |\n| `oh-my-codex/src/agents/native-config.ts` | deep | Native agent config shape supports explicit metadata/model/tool posture; cclaw should validate generated `.codex/agents/*.toml` shape instead of trusting file presence. |\n| `oh-my-codex/src/team/state/events.ts` and `src/team/state/workers.ts` | targeted | Append-only events plus worker state are useful as separate audit/current-state layers; cclaw mirrors that with `delegation-events.jsonl` and `subagents.json`. |\n| `oh-my-openagent/src/tools/delegate-task/tools.ts` | deep | Delegation should have an explicit dispatch surface and mode instead of relying on a prose claim that an agent was launched. |\n| `oh-my-openagent/src/tools/delegate-task/subagent-resolver.ts` | targeted | Agent discovery should be checked by doctor so missing/corrupt generated agent definitions are visible before dispatch. |\n| `oh-my-openagent/src/tools/delegate-task/prompt-builder.ts` | targeted | Prompt builders should include exact invocation/return contracts; cclaw generated worker prompts now carry ACK/result schemas. |\n| `giancarloerra-socraticode/**` | skimmed | Useful for workflow/e2e and graph-oriented contract testing, but not a subagent dispatch implementation reference; no runtime pattern imported. |\n| unrelated large reference trees not named above | not relevant | Searched/skipped because they did not contain flow/subagent/harness dispatch patterns relevant to this plan. |\n"
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"
46
64
  ].join("");
47
65
  }
@@ -10,6 +10,8 @@
10
10
  * `src/knowledge-store.ts::computeCompoundReadiness`.
11
11
  * 2. `computeRalphLoopStatusInline` mirrors
12
12
  * `src/tdd-cycle.ts::computeRalphLoopStatus`.
13
+ * 3. `computeEarlyLoopStatusInline` mirrors
14
+ * `src/early-loop.ts::computeEarlyLoopStatus`.
13
15
  *
14
16
  * Previously those bodies lived inline in `src/content/node-hooks.ts` — a
15
17
  * ~2000-line file — next to unrelated hook-handler code. Any silent drift
@@ -25,11 +27,12 @@
25
27
  * longer owns their source code.
26
28
  *
27
29
  * Parity with the TypeScript canonical implementations is enforced by
28
- * `tests/unit/ralph-loop-parity.test.ts`. Any structural change to the
30
+ * `tests/unit/ralph-loop-parity.test.ts` and
31
+ * `tests/unit/early-loop-parity.test.ts`. Any structural change to the
29
32
  * canonical TS code MUST:
30
33
  *
31
34
  * 1. Update the matching snippet below.
32
- * 2. Re-run `npm test tests/unit/ralph-loop-parity.test.ts`.
35
+ * 2. Re-run parity tests for the touched snippet.
33
36
  *
34
37
  * DO NOT inline tests here — keep the parity check in its dedicated test
35
38
  * file.
@@ -43,13 +46,13 @@
43
46
  * timestamp so the hook-written `compound-readiness.json` is byte-equal
44
47
  * to the CLI-written version for the same input.
45
48
  * - `countArchivedRunsInline` counts immediate subdirectories of
46
- * `<root>/.cclaw/runs/` so both the hook and the CLI see the same
49
+ * `<root>/.cclaw/archive/` so both the hook and the CLI see the same
47
50
  * `archivedRunsCount` for the small-project relaxation.
48
51
  * - `formatCompoundReadinessLineInline` mirrors the one-line summary shape
49
52
  * used by `src/internal/compound-readiness.ts::formatCompoundReadinessLine`
50
53
  * so session-start and internal CLI command stay wording-compatible.
51
54
  */
52
- export declare const HOOK_INLINE_SHARED_HELPERS = "\nfunction normalizeCompoundLastUpdatedAt(date) {\n return date.toISOString().replace(/\\.\\d{3}Z$/u, \"Z\");\n}\n\n// Count archived runs as sub-directories under `.cclaw/runs/`. Missing\n// dir returns 0; unexpected errors return undefined so the caller can\n// skip the small-project relaxation rather than guess.\nasync function countArchivedRunsInline(root) {\n const dir = path.join(root, RUNTIME_ROOT, \"runs\");\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n return entries.filter((entry) => entry.isDirectory()).length;\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? error.code : null;\n if (code === \"ENOENT\") return 0;\n return undefined;\n }\n}\n\nfunction formatCompoundReadinessLineInline(readiness) {\n if (!readiness || typeof readiness !== \"object\") {\n return \"\";\n }\n const ready = Array.isArray(readiness.ready) ? readiness.ready : [];\n const readyCount =\n typeof readiness.readyCount === \"number\" && Number.isFinite(readiness.readyCount)\n ? Math.trunc(readiness.readyCount)\n : ready.length;\n const clusterCount =\n typeof readiness.clusterCount === \"number\" && Number.isFinite(readiness.clusterCount)\n ? Math.trunc(readiness.clusterCount)\n : 0;\n const threshold =\n typeof readiness.threshold === \"number\" && Number.isFinite(readiness.threshold)\n ? Math.trunc(readiness.threshold)\n : COMPOUND_RECURRENCE_THRESHOLD;\n if (readyCount === 0) {\n return \"Compound readiness: no candidates (clusters=\" +\n String(clusterCount) + \", threshold=\" + String(threshold) + \")\";\n }\n const critical = ready.filter(\n (entry) => entry && typeof entry === \"object\" && entry.severity === \"critical\"\n ).length;\n const criticalSuffix = critical > 0 ? \" (critical=\" + String(critical) + \")\" : \"\";\n return \"Compound readiness: clusters=\" + String(clusterCount) +\n \", ready=\" + String(readyCount) + criticalSuffix;\n}\n";
55
+ export declare const HOOK_INLINE_SHARED_HELPERS = "\nfunction normalizeCompoundLastUpdatedAt(date) {\n return date.toISOString().replace(/\\.\\d{3}Z$/u, \"Z\");\n}\n\n// Count archived runs as sub-directories under `.cclaw/archive/`. Missing\n// dir returns 0; unexpected errors return undefined so the caller can\n// skip the small-project relaxation rather than guess.\nasync function countArchivedRunsInline(root) {\n const dir = path.join(root, RUNTIME_ROOT, \"archive\");\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n return entries.filter((entry) => entry.isDirectory()).length;\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? error.code : null;\n if (code === \"ENOENT\") return 0;\n return undefined;\n }\n}\n\nfunction formatCompoundReadinessLineInline(readiness) {\n if (!readiness || typeof readiness !== \"object\") {\n return \"\";\n }\n const ready = Array.isArray(readiness.ready) ? readiness.ready : [];\n const readyCount =\n typeof readiness.readyCount === \"number\" && Number.isFinite(readiness.readyCount)\n ? Math.trunc(readiness.readyCount)\n : ready.length;\n const clusterCount =\n typeof readiness.clusterCount === \"number\" && Number.isFinite(readiness.clusterCount)\n ? Math.trunc(readiness.clusterCount)\n : 0;\n const threshold =\n typeof readiness.threshold === \"number\" && Number.isFinite(readiness.threshold)\n ? Math.trunc(readiness.threshold)\n : COMPOUND_RECURRENCE_THRESHOLD;\n if (readyCount === 0) {\n return \"Compound readiness: no candidates (clusters=\" +\n String(clusterCount) + \", threshold=\" + String(threshold) + \")\";\n }\n const critical = ready.filter(\n (entry) => entry && typeof entry === \"object\" && entry.severity === \"critical\"\n ).length;\n const criticalSuffix = critical > 0 ? \" (critical=\" + String(critical) + \")\" : \"\";\n return \"Compound readiness: clusters=\" + String(clusterCount) +\n \", ready=\" + String(readyCount) + criticalSuffix;\n}\n";
53
56
  /**
54
57
  * Inline mirror of `src/knowledge-store.ts::computeCompoundReadiness`.
55
58
  *
@@ -81,3 +84,13 @@ export declare const COMPOUND_READINESS_INLINE_SOURCE = "\nasync function comput
81
84
  * async function computeRalphLoopStatusInline(stateDir, runId) -> RalphLoopStatus
82
85
  */
83
86
  export declare const RALPH_LOOP_INLINE_SOURCE = "\nasync function computeRalphLoopStatusInline(stateDir, runId) {\n const filePath = path.join(stateDir, \"tdd-cycle-log.jsonl\");\n const raw = await readTextFile(filePath, \"\");\n const sliceMap = new Map();\n const acClosed = new Set();\n const redOpenSlices = [];\n let loopIteration = 0;\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let row;\n try { row = JSON.parse(line); } catch { continue; }\n if (!row || typeof row !== \"object\" || Array.isArray(row)) continue;\n const rowRun = typeof row.runId === \"string\" && row.runId.length > 0 ? row.runId : runId;\n if (rowRun !== runId) continue;\n const slice = typeof row.slice === \"string\" && row.slice.length > 0 ? row.slice : \"S-unknown\";\n let state = sliceMap.get(slice);\n if (!state) {\n state = { slice, redCount: 0, greenCount: 0, refactorCount: 0, redOpen: false, acIds: [] };\n sliceMap.set(slice, state);\n }\n const exitCode = typeof row.exitCode === \"number\" ? row.exitCode : undefined;\n if (row.phase === \"red\") {\n state.redCount += 1;\n if (exitCode !== undefined && exitCode !== 0) state.redOpen = true;\n } else if (row.phase === \"green\") {\n state.greenCount += 1;\n state.redOpen = false;\n loopIteration += 1;\n if (Array.isArray(row.acIds)) {\n for (const acId of row.acIds) {\n if (typeof acId !== \"string\" || acId.length === 0) continue;\n acClosed.add(acId);\n if (!state.acIds.includes(acId)) state.acIds.push(acId);\n }\n }\n } else if (row.phase === \"refactor\") {\n state.refactorCount += 1;\n }\n }\n for (const state of sliceMap.values()) {\n if (state.redOpen) redOpenSlices.push(state.slice);\n }\n const slices = Array.from(sliceMap.values()).sort((a, b) => a.slice.localeCompare(b.slice, \"en\"));\n return {\n schemaVersion: 1,\n runId,\n loopIteration,\n redOpen: redOpenSlices.length > 0,\n redOpenSlices,\n acClosed: Array.from(acClosed).sort(),\n sliceCount: slices.length,\n slices,\n lastUpdatedAt: new Date().toISOString()\n };\n}\n";
87
+ /**
88
+ * Inline mirror of `src/early-loop.ts::computeEarlyLoopStatus`.
89
+ *
90
+ * Parity enforced by
91
+ * `tests/unit/early-loop-parity.test.ts::early-loop parity`.
92
+ *
93
+ * Signature contract:
94
+ * async function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) -> EarlyLoopStatus
95
+ */
96
+ export declare const EARLY_LOOP_INLINE_SOURCE = "\nfunction normalizeEarlyLoopSeverityInline(value) {\n if (value === \"critical\" || value === \"important\" || value === \"suggestion\") {\n return value;\n }\n return \"important\";\n}\n\nfunction normalizeEarlyLoopTextInline(value, fallback) {\n if (typeof value !== \"string\") return fallback;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\nfunction stableConcernFallbackIdInline(locator, summary) {\n const seed = (String(locator) + \"::\" + String(summary)).trim().toLowerCase();\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (Math.imul(31, hash) + seed.charCodeAt(index)) >>> 0;\n }\n return \"C-\" + hash.toString(16).padStart(8, \"0\");\n}\n\nfunction normalizeEarlyLoopConcernInline(row) {\n if (!row || typeof row !== \"object\" || Array.isArray(row)) return null;\n const locator = normalizeEarlyLoopTextInline(row.locator, \"unknown-location\");\n const summary = normalizeEarlyLoopTextInline(row.summary, \"missing-summary\");\n const id = typeof row.id === \"string\" && row.id.trim().length > 0\n ? row.id.trim()\n : stableConcernFallbackIdInline(locator, summary);\n return {\n id,\n severity: normalizeEarlyLoopSeverityInline(row.severity),\n locator,\n summary\n };\n}\n\nfunction normalizeEarlyLoopMaxIterationsInline(value) {\n return Number.isInteger(value) && value >= 1 ? value : 3;\n}\n\nfunction earlyLoopSeverityWeightInline(value) {\n if (value === \"critical\") return 3;\n if (value === \"important\") return 2;\n return 1;\n}\n\nfunction sortEarlyLoopConcernsInline(a, b) {\n const severityDiff = earlyLoopSeverityWeightInline(b.severity) - earlyLoopSeverityWeightInline(a.severity);\n if (severityDiff !== 0) return severityDiff;\n if (a.firstSeenIteration !== b.firstSeenIteration) {\n return a.firstSeenIteration - b.firstSeenIteration;\n }\n if (a.lastSeenIteration !== b.lastSeenIteration) {\n return a.lastSeenIteration - b.lastSeenIteration;\n }\n return String(a.id).localeCompare(String(b.id), \"en\");\n}\n\nfunction formatEarlyLoopStatusLineInline(status) {\n if (!status || typeof status !== \"object\") return \"\";\n const convergence = status.convergenceTripped ? \"tripped\" : \"clear\";\n return \"Early Loop: stage=\" + String(status.stage) +\n \", iter=\" + String(status.iteration) + \"/\" + String(status.maxIterations) +\n \", open=\" + String(Array.isArray(status.openConcerns) ? status.openConcerns.length : 0) +\n \", convergence=\" + convergence;\n}\n\nasync function computeEarlyLoopStatusInline(stateDir, stageId, runId, maxIterations) {\n const filePath = path.join(stateDir, \"early-loop-log.jsonl\");\n const raw = await readTextFile(filePath, \"\");\n const maxIters = normalizeEarlyLoopMaxIterationsInline(maxIterations);\n const concernsMap = new Map();\n let previousSnapshotKey = \"\";\n let sameConcernStreak = 0;\n let convergenceTripped = false;\n let escalationReason = undefined;\n let currentIteration = 0;\n let lastSeenConcernIds = [];\n\n for (const rawLine of raw.split(/\\r?\\n/gu)) {\n const line = rawLine.trim();\n if (line.length === 0) continue;\n let parsed;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n const rowRunId = typeof parsed.runId === \"string\" && parsed.runId.trim().length > 0\n ? parsed.runId.trim()\n : \"active\";\n const rowStage = typeof parsed.stage === \"string\" && parsed.stage.trim().length > 0\n ? parsed.stage.trim()\n : \"brainstorm\";\n if (rowRunId !== runId || rowStage !== stageId) continue;\n\n currentIteration += 1;\n const iteration = Number.isInteger(parsed.iteration) && parsed.iteration >= 1\n ? parsed.iteration\n : currentIteration;\n const seenThisIteration = new Set();\n const concerns = Array.isArray(parsed.concerns) ? parsed.concerns : [];\n for (const rawConcern of concerns) {\n const concern = normalizeEarlyLoopConcernInline(rawConcern);\n if (!concern) continue;\n seenThisIteration.add(concern.id);\n const existing = concernsMap.get(concern.id);\n if (!existing) {\n concernsMap.set(concern.id, {\n id: concern.id,\n severity: concern.severity,\n locator: concern.locator,\n summary: concern.summary,\n firstSeenIteration: iteration,\n lastSeenIteration: iteration\n });\n continue;\n }\n existing.lastSeenIteration = iteration;\n existing.locator = concern.locator;\n existing.summary = concern.summary;\n if (earlyLoopSeverityWeightInline(concern.severity) >= earlyLoopSeverityWeightInline(existing.severity)) {\n existing.severity = concern.severity;\n }\n delete existing.resolvedAtIteration;\n }\n\n const resolvedConcernIds = Array.isArray(parsed.resolvedConcernIds)\n ? parsed.resolvedConcernIds\n .filter((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim())\n : [];\n for (const concernId of resolvedConcernIds) {\n const existing = concernsMap.get(concernId);\n if (!existing) continue;\n if (seenThisIteration.has(concernId)) continue;\n if (existing.resolvedAtIteration === undefined) {\n existing.resolvedAtIteration = iteration;\n }\n }\n\n for (const concern of concernsMap.values()) {\n if (concern.resolvedAtIteration !== undefined) continue;\n if (seenThisIteration.has(concern.id)) continue;\n concern.resolvedAtIteration = iteration;\n }\n\n const openConcernIds = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration === undefined)\n .map((concern) => concern.id)\n .sort((a, b) => String(a).localeCompare(String(b), \"en\"));\n lastSeenConcernIds = openConcernIds;\n const snapshotKey = openConcernIds.join(\"|\");\n if (snapshotKey.length > 0 && snapshotKey === previousSnapshotKey) {\n sameConcernStreak += 1;\n if (!convergenceTripped && sameConcernStreak >= 2) {\n convergenceTripped = true;\n escalationReason = \"same concerns \" + String(sameConcernStreak) + \" iterations in a row\";\n }\n } else {\n sameConcernStreak = snapshotKey.length > 0 ? 1 : 0;\n }\n previousSnapshotKey = snapshotKey;\n }\n\n const openConcerns = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration === undefined)\n .sort(sortEarlyLoopConcernsInline);\n const resolvedConcerns = Array.from(concernsMap.values())\n .filter((concern) => concern.resolvedAtIteration !== undefined)\n .sort((a, b) => {\n if (a.resolvedAtIteration !== b.resolvedAtIteration) {\n return a.resolvedAtIteration - b.resolvedAtIteration;\n }\n return sortEarlyLoopConcernsInline(a, b);\n });\n\n if (!convergenceTripped && openConcerns.length > 0 && currentIteration >= maxIters) {\n convergenceTripped = true;\n escalationReason = \"max iterations \" + String(maxIters) +\n \" reached with \" + String(openConcerns.length) + \" open concern(s)\";\n }\n\n return {\n schemaVersion: 1,\n stage: stageId,\n runId,\n iteration: currentIteration,\n maxIterations: maxIters,\n openConcerns,\n resolvedConcerns,\n lastSeenConcernIds,\n convergenceTripped,\n ...(escalationReason ? { escalationReason } : {}),\n lastUpdatedAt: new Date().toISOString()\n };\n}\n";