cclaw-cli 6.11.0 → 6.13.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 (41) hide show
  1. package/dist/artifact-linter/plan.js +60 -2
  2. package/dist/artifact-linter/shared.d.ts +9 -0
  3. package/dist/artifact-linter/spec.js +14 -0
  4. package/dist/artifact-linter/tdd.d.ts +29 -0
  5. package/dist/artifact-linter/tdd.js +398 -11
  6. package/dist/artifact-linter.js +10 -1
  7. package/dist/content/core-agents.d.ts +2 -2
  8. package/dist/content/core-agents.js +3 -3
  9. package/dist/content/examples.js +4 -4
  10. package/dist/content/hooks.js +48 -1
  11. package/dist/content/skills.d.ts +10 -0
  12. package/dist/content/skills.js +64 -2
  13. package/dist/content/stage-schema.js +13 -4
  14. package/dist/content/stages/plan.js +2 -1
  15. package/dist/content/stages/schema-types.d.ts +1 -1
  16. package/dist/content/stages/spec.js +2 -2
  17. package/dist/content/stages/tdd.js +8 -7
  18. package/dist/content/templates.js +10 -4
  19. package/dist/delegation.d.ts +73 -3
  20. package/dist/delegation.js +196 -6
  21. package/dist/flow-state.d.ts +35 -0
  22. package/dist/flow-state.js +7 -0
  23. package/dist/gate-evidence.d.ts +5 -0
  24. package/dist/gate-evidence.js +58 -1
  25. package/dist/install.js +173 -1
  26. package/dist/integration-fanin.d.ts +44 -0
  27. package/dist/integration-fanin.js +180 -0
  28. package/dist/internal/advance-stage/advance.js +16 -1
  29. package/dist/internal/advance-stage/start-flow.js +3 -1
  30. package/dist/internal/advance-stage.js +13 -4
  31. package/dist/internal/plan-split-waves.d.ts +39 -1
  32. package/dist/internal/plan-split-waves.js +190 -6
  33. package/dist/internal/set-worktree-mode.d.ts +10 -0
  34. package/dist/internal/set-worktree-mode.js +28 -0
  35. package/dist/managed-resources.js +2 -0
  36. package/dist/run-persistence.js +22 -0
  37. package/dist/worktree-manager.d.ts +50 -0
  38. package/dist/worktree-manager.js +136 -0
  39. package/dist/worktree-types.d.ts +36 -0
  40. package/dist/worktree-types.js +6 -0
  41. package/package.json +1 -1
@@ -4,9 +4,9 @@ import { exists } from "../fs-utils.js";
4
4
  import { FORBIDDEN_PLACEHOLDER_TOKENS, CONFIDENCE_FINDING_REGEX_SOURCE } from "../content/skills.js";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
- import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits } from "../internal/plan-split-waves.js";
7
+ import { PLAN_SPLIT_SMALL_PLAN_THRESHOLD, parseImplementationUnits, parseImplementationUnitParallelFields } from "../internal/plan-split-waves.js";
8
8
  export async function lintPlanStage(ctx) {
9
- const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride } = ctx;
9
+ const { projectRoot, track, raw, absFile, sections, findings, parsedFrontmatter, brainstormShortCircuitBody, brainstormShortCircuitActivated, staleDiagramAuditEnabled, isTrivialOverride, legacyContinuation } = ctx;
10
10
  evaluateInvestigationTrace(ctx, "Implementation Units");
11
11
  const strictPlanGuards = parsedFrontmatter.hasFrontmatter ||
12
12
  headingPresent(sections, "Plan Quality Scan") ||
@@ -219,4 +219,62 @@ export async function lintPlanStage(ctx) {
219
219
  : `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
220
220
  });
221
221
  }
222
+ const planUnits = parseImplementationUnits(raw);
223
+ const parallelMetaApplies = strictPlanGuards && planUnits.length > 0;
224
+ if (parallelMetaApplies) {
225
+ const metaRulesRequired = !legacyContinuation;
226
+ const missingDepends = [];
227
+ const missingPaths = [];
228
+ const missingParallelMeta = [];
229
+ for (const unit of planUnits) {
230
+ const id = unit.id;
231
+ if (!/\bdependsOn\s*:/iu.test(unit.body)) {
232
+ missingDepends.push(id);
233
+ }
234
+ if (!/\bclaimedPaths\s*:/iu.test(unit.body)) {
235
+ missingPaths.push(id);
236
+ }
237
+ if (!/\bparallelizable\s*:/iu.test(unit.body) || !/\briskTier\s*:/iu.test(unit.body)) {
238
+ missingParallelMeta.push(id);
239
+ }
240
+ }
241
+ findings.push({
242
+ section: "plan_units_missing_dependsOn",
243
+ required: metaRulesRequired,
244
+ rule: "Every implementation unit must declare `dependsOn:` (v6.13.0) — use comma-separated unit ids or `none`.",
245
+ found: missingDepends.length === 0,
246
+ details: missingDepends.length === 0
247
+ ? "All implementation units declare dependsOn."
248
+ : `Missing dependsOn on: ${missingDepends.join(", ")}. Remediation: add a bullet \`- **dependsOn:** U-2, U-3\` or \`- **dependsOn:** none\`.`
249
+ });
250
+ findings.push({
251
+ section: "plan_units_missing_claimedPaths",
252
+ required: metaRulesRequired,
253
+ rule: "Every implementation unit must declare explicit `claimedPaths:` predictions for parallel scheduling (v6.13.0).",
254
+ found: missingPaths.length === 0,
255
+ details: missingPaths.length === 0
256
+ ? "All implementation units declare claimedPaths."
257
+ : `Missing claimedPaths on: ${missingPaths.join(", ")}. Remediation: add \`- **claimedPaths:** path/a, path/b\` (repo-relative globs or files).`
258
+ });
259
+ findings.push({
260
+ section: "plan_units_missing_parallel_metadata",
261
+ required: metaRulesRequired,
262
+ rule: "Every implementation unit must declare `parallelizable:` and `riskTier:` (low|standard|high) (v6.13.0).",
263
+ found: missingParallelMeta.length === 0,
264
+ details: missingParallelMeta.length === 0
265
+ ? "All implementation units carry parallelizable + riskTier."
266
+ : `Missing parallel metadata on: ${missingParallelMeta.join(", ")}. Remediation: add \`- **parallelizable:** true|false\` and \`- **riskTier:** low|standard|high\`.`
267
+ });
268
+ const parallelizableCount = planUnits.filter((u) => parseImplementationUnitParallelFields(u).parallelizable).length;
269
+ const advisorySerial = parallelizableCount === 0 && planUnits.length > 1;
270
+ findings.push({
271
+ section: "plan_no_parallel_lanes_detected",
272
+ required: false,
273
+ rule: "When multiple independent units exist, consider marking at least one `parallelizable: true` with disjoint claimedPaths.",
274
+ found: !advisorySerial,
275
+ details: advisorySerial
276
+ ? "All units are marked parallelizable false; scheduler will serialize. If surfaces are independent, opt units into parallelism explicitly."
277
+ : "Parallel-ready units detected or plan is single-unit."
278
+ });
279
+ }
222
280
  }
@@ -630,4 +630,13 @@ export interface StageLintContext {
630
630
  * expansion-strategist delegation) from required → advisory.
631
631
  */
632
632
  taskClass: "software-standard" | "software-trivial" | "software-bugfix" | null;
633
+ /**
634
+ * v6.13.0 — when true, plan parallel-metadata rules downgrade to advisory
635
+ * for legacy continuation projects (hox-style).
636
+ */
637
+ legacyContinuation: boolean;
638
+ /**
639
+ * v6.13.0 — effective worktree execution mode for TDD linters.
640
+ */
641
+ worktreeExecutionMode: "single-tree" | "worktree-first";
633
642
  }
@@ -127,4 +127,18 @@ export async function lintSpecStage(ctx) {
127
127
  : `Unwaived FAIL/PARTIAL statuses: ${layeredDocumentReview.failOrPartialWithoutWaiver.join(", ")}.`
128
128
  });
129
129
  }
130
+ const acceptanceCriteriaBody = sectionBodyByName(sections, "Acceptance Criteria");
131
+ if (acceptanceCriteriaBody !== null && /\|/u.test(acceptanceCriteriaBody)) {
132
+ const hasParallel = /\bparallelSafe\b/iu.test(acceptanceCriteriaBody);
133
+ const hasTouch = /\btouchSurface\b/iu.test(acceptanceCriteriaBody);
134
+ findings.push({
135
+ section: "spec_acs_not_sliceable",
136
+ required: false,
137
+ rule: "Acceptance criteria should declare `parallelSafe` and `touchSurface` per row (v6.13.0) so plan/TDD can schedule slices safely.",
138
+ found: hasParallel && hasTouch,
139
+ details: hasParallel && hasTouch
140
+ ? "Acceptance Criteria mentions parallelSafe and touchSurface."
141
+ : "Add columns or inline markers for parallelSafe (true|false) and touchSurface (short area description) for each AC."
142
+ });
143
+ }
130
144
  }
@@ -37,6 +37,35 @@ interface DocCoverageResult {
37
37
  missing: string[];
38
38
  }
39
39
  export declare function evaluateSliceDocumenterCoverage(slices: Map<string, DelegationEntry[]>): DocCoverageResult;
40
+ interface ImplementerCoverageResult {
41
+ missing: string[];
42
+ }
43
+ /**
44
+ * v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
45
+ * that recorded a phase=red event with non-empty evidenceRefs, require a
46
+ * phase=green event whose `agent === "slice-implementer"`. Slices whose
47
+ * GREEN event came from a different agent (e.g. controller wrote GREEN
48
+ * itself and recorded a green row under another agent name) are flagged.
49
+ */
50
+ export declare function evaluateSliceImplementerCoverage(slices: Map<string, DelegationEntry[]>): ImplementerCoverageResult;
51
+ interface RedCheckpointResult {
52
+ ok: boolean;
53
+ details: string;
54
+ }
55
+ /**
56
+ * v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
57
+ * requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
58
+ * The rule is enforced on a per-wave basis, where a wave is defined by
59
+ * `<artifacts-dir>/wave-plans/wave-NN.md` files (when present) listing
60
+ * slice ids. When no wave manifest exists, the linter falls back to a
61
+ * conservative implicit detection: a wave is a contiguous run of
62
+ * `phase=red` events with no other-phase events between them; the rule
63
+ * fires only when the implicit wave has 2+ members.
64
+ *
65
+ * @param waveMembers Optional explicit wave manifest. Map key is wave
66
+ * name (e.g. `"W-01"`); value is the set of slice ids in that wave.
67
+ */
68
+ export declare function evaluateRedCheckpoint(slices: Map<string, DelegationEntry[]>, waveMembers?: Map<string, Set<string>> | null): RedCheckpointResult;
40
69
  export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
41
70
  interface VerificationLadderResult {
42
71
  ok: boolean;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { readDelegationLedger } from "../delegation.js";
3
+ import { readDelegationLedger, readDelegationEvents } from "../delegation.js";
4
4
  import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
5
5
  const SLICE_SUMMARY_START = "<!-- auto-start: tdd-slice-summary -->";
6
6
  const SLICE_SUMMARY_END = "<!-- auto-end: tdd-slice-summary -->";
@@ -26,8 +26,7 @@ const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
26
26
  * via `## Slices Index`.
27
27
  */
28
28
  export async function lintTddStage(ctx) {
29
- const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter } = ctx;
30
- void projectRoot;
29
+ const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode } = ctx;
31
30
  void parsedFrontmatter;
32
31
  evaluateInvestigationTrace(ctx, "Watched-RED Proof");
33
32
  const delegationLedger = await readDelegationLedger(ctx.projectRoot);
@@ -135,19 +134,165 @@ export async function lintTddStage(ctx) {
135
134
  });
136
135
  }
137
136
  }
138
- // Phase C — slice-documenter coverage. When discoveryMode=deep, every
139
- // slice with a green phase event must also carry a phase=doc event with
140
- // non-empty evidenceRefs pointing at `tdd-slices/S-<id>.md`.
137
+ // v6.12.0 Phase R — slice-documenter coverage is mandatory on every
138
+ // TDD run regardless of discoveryMode. `discoveryMode` is now strictly
139
+ // an early-stage knob (brainstorm/scope/design); TDD parallelism must
140
+ // be uniform across lean/guided/deep so the controller cannot quietly
141
+ // skip per-slice prose by picking a non-deep mode.
142
+ void discoveryMode;
141
143
  if (eventsActive) {
142
144
  const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
143
145
  if (docResult.missing.length > 0) {
144
- const isDeep = discoveryMode === "deep";
145
146
  findings.push({
146
- section: "tdd_slice_documenter_missing_for_deep",
147
- required: isDeep,
148
- rule: "On discoveryMode=deep, every TDD slice with a phase=green event must also carry a slice-documenter `phase=doc` event whose evidenceRefs reference `<artifacts-dir>/tdd-slices/S-<id>.md`. On other discovery modes the requirement is advisory.",
147
+ section: "tdd_slice_documenter_missing",
148
+ required: true,
149
+ rule: "Every TDD slice with a phase=green event must also carry a slice-documenter `phase=doc` event whose evidenceRefs reference `<artifacts-dir>/tdd-slices/S-<id>.md`. The requirement is independent of discoveryMode (v6.12.0 Phase R).",
150
+ found: false,
151
+ details: `Slices missing slice-documenter coverage: ${docResult.missing.join(", ")}. Dispatch slice-documenter --slice <id> --phase doc in parallel with slice-implementer --phase green for each slice.`
152
+ });
153
+ }
154
+ }
155
+ // v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
156
+ // with a phase=red row carrying non-empty evidenceRefs, require a
157
+ // matching phase=green event whose `agent === "slice-implementer"`.
158
+ // This catches "controller wrote GREEN itself" — the most common
159
+ // backslide we have observed in fresh runs (hox S-11).
160
+ if (eventsActive) {
161
+ const implResult = evaluateSliceImplementerCoverage(slicesByEvents);
162
+ if (implResult.missing.length > 0) {
163
+ findings.push({
164
+ section: "tdd_slice_implementer_missing",
165
+ required: true,
166
+ rule: "Every TDD slice that recorded a phase=red event with non-empty evidenceRefs must reach phase=green via the `slice-implementer` agent. Controller writing GREEN production code itself is forbidden (v6.12.0 Phase M).",
149
167
  found: false,
150
- details: `Slices missing slice-documenter coverage: ${docResult.missing.join(", ")}.`
168
+ details: `Slices missing slice-implementer GREEN coverage: ${implResult.missing.join(", ")}. Dispatch slice-implementer --slice <id> --phase green --paths <comma-separated production paths>.`
169
+ });
170
+ }
171
+ }
172
+ // v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
173
+ // requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
174
+ // Enforced per-wave: explicit `wave-plans/wave-NN.md` manifest if
175
+ // present, otherwise implicit detection via contiguous red blocks
176
+ // (size >= 2). Sequential per-slice runs (red→green→refactor in a
177
+ // tight loop) form size-1 implicit waves and are unaffected.
178
+ if (eventsActive) {
179
+ const waveManifest = await readWaveManifest(path.dirname(absFile));
180
+ const checkpointResult = evaluateRedCheckpoint(slicesByEvents, waveManifest);
181
+ if (!checkpointResult.ok) {
182
+ findings.push({
183
+ section: "tdd_red_checkpoint_violation",
184
+ required: true,
185
+ rule: "Wave Batch Mode (v6.12.0 Phase W): every slice in a wave must complete phase=red before any slice in the same wave starts phase=green. Detected: a phase=green completedTs precedes the last phase=red completedTs of the same wave.",
186
+ found: false,
187
+ details: checkpointResult.details
188
+ });
189
+ }
190
+ }
191
+ // v6.12.0 Phase L — advisory backslide detection. When a cutover is
192
+ // recorded in flow-state, slice-id rows in the legacy per-slice
193
+ // sections of `06-tdd.md` that exceed the cutover boundary should
194
+ // migrate to `tdd-slices/S-<id>.md`. Surface as advisory so it does
195
+ // not block the gate but does keep the controller honest.
196
+ const cutoverFinding = await evaluateLegacySectionBackslide({
197
+ projectRoot,
198
+ raw,
199
+ sections
200
+ });
201
+ if (cutoverFinding) {
202
+ findings.push(cutoverFinding);
203
+ }
204
+ const { events: jsonlEvents, fanInAudits } = await readDelegationEvents(projectRoot);
205
+ const runEvents = jsonlEvents.filter((e) => e.runId === delegationLedger.runId);
206
+ if (eventsActive && worktreeExecutionMode === "worktree-first") {
207
+ const terminalPhases = new Set([
208
+ "green",
209
+ "refactor",
210
+ "refactor-deferred",
211
+ "resolve-conflict"
212
+ ]);
213
+ const missingClaim = new Set();
214
+ for (const ev of runEvents) {
215
+ if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
216
+ continue;
217
+ if (ev.status !== "completed" && ev.status !== "failed")
218
+ continue;
219
+ if (!ev.phase || !terminalPhases.has(ev.phase))
220
+ continue;
221
+ const tok = ev.claimToken?.trim() ?? "";
222
+ if (tok.length === 0 && typeof ev.sliceId === "string") {
223
+ missingClaim.add(ev.sliceId);
224
+ }
225
+ }
226
+ if (missingClaim.size > 0) {
227
+ findings.push({
228
+ section: "tdd_slice_claim_token_missing",
229
+ required: true,
230
+ rule: "Worktree-first: terminal slice-implementer rows must echo --claim-token. Remediation: pass the same --claim-token used on the scheduled row for every completed/failed terminal phase.",
231
+ found: false,
232
+ details: `Slices missing claim token on terminal rows: ${[...missingClaim].join(", ")}.`
233
+ });
234
+ }
235
+ const missingLane = new Set();
236
+ for (const ev of runEvents) {
237
+ if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
238
+ continue;
239
+ if (ev.status !== "completed" || ev.phase !== "green")
240
+ continue;
241
+ if (!ev.ownerLaneId?.trim() && typeof ev.sliceId === "string") {
242
+ missingLane.add(ev.sliceId);
243
+ }
244
+ }
245
+ if (missingLane.size > 0) {
246
+ findings.push({
247
+ section: "tdd_slice_worktree_metadata_missing",
248
+ required: true,
249
+ rule: "Worktree-first: completed GREEN rows must record --lane-id (ownerLaneId) for the lane worktree.",
250
+ found: false,
251
+ details: `Slices missing ownerLaneId on GREEN completion: ${[...missingLane].join(", ")}.`
252
+ });
253
+ }
254
+ const conflictSlices = [
255
+ ...new Set([
256
+ ...runEvents
257
+ .filter((e) => e.integrationState === "conflict")
258
+ .map((e) => e.sliceId)
259
+ .filter((s) => typeof s === "string"),
260
+ ...fanInAudits
261
+ .filter((a) => a.runId === delegationLedger.runId &&
262
+ a.event === "cclaw_fanin_conflict" &&
263
+ Array.isArray(a.sliceIds))
264
+ .flatMap((a) => a.sliceIds ?? [])
265
+ ].filter((s) => typeof s === "string" && s.length > 0))
266
+ ];
267
+ if (conflictSlices.length > 0) {
268
+ findings.push({
269
+ section: "tdd_fanin_conflict_unresolved",
270
+ required: true,
271
+ rule: "Resolve fan-in conflicts before stage-complete: dispatch slice-implementer --phase resolve-conflict or abandon the slice explicitly.",
272
+ found: false,
273
+ details: `integrationState=conflict for slice(s): ${conflictSlices.join(", ")}. Remediation: finish deterministic fan-in or mark integrationState=resolved after manual merge evidence.`
274
+ });
275
+ }
276
+ const now = Date.now();
277
+ const leaseStale = new Set();
278
+ for (const ev of runEvents) {
279
+ if (typeof ev.leasedUntil !== "string")
280
+ continue;
281
+ const until = Date.parse(ev.leasedUntil);
282
+ if (!Number.isFinite(until) || until >= now)
283
+ continue;
284
+ if (ev.leaseState === "reclaimed" || ev.leaseState === "released")
285
+ continue;
286
+ if (typeof ev.sliceId === "string")
287
+ leaseStale.add(ev.sliceId);
288
+ }
289
+ if (leaseStale.size > 0) {
290
+ findings.push({
291
+ section: "tdd_lease_expired_unreclaimed",
292
+ required: true,
293
+ rule: "Leases past leasedUntil must be reclaimed or released. Remediation: run scheduler reclaim or emit leaseState=reclaimed audit rows after controller action.",
294
+ found: false,
295
+ details: `Expired leases not reclaimed for slice(s): ${[...leaseStale].join(", ")}.`
151
296
  });
152
297
  }
153
298
  }
@@ -522,6 +667,248 @@ export function evaluateSliceDocumenterCoverage(slices) {
522
667
  }
523
668
  return { missing };
524
669
  }
670
+ /**
671
+ * v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
672
+ * that recorded a phase=red event with non-empty evidenceRefs, require a
673
+ * phase=green event whose `agent === "slice-implementer"`. Slices whose
674
+ * GREEN event came from a different agent (e.g. controller wrote GREEN
675
+ * itself and recorded a green row under another agent name) are flagged.
676
+ */
677
+ export function evaluateSliceImplementerCoverage(slices) {
678
+ const missing = [];
679
+ for (const [sliceId, rows] of slices.entries()) {
680
+ const reds = rows.filter((entry) => entry.phase === "red");
681
+ if (reds.length === 0)
682
+ continue;
683
+ const hasRedEvidence = reds.some((red) => {
684
+ const refs = Array.isArray(red.evidenceRefs) ? red.evidenceRefs : [];
685
+ return refs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
686
+ });
687
+ if (!hasRedEvidence)
688
+ continue;
689
+ const greens = rows.filter((entry) => entry.phase === "green");
690
+ const ownedByImplementer = greens.some((entry) => entry.agent === "slice-implementer");
691
+ if (!ownedByImplementer) {
692
+ missing.push(sliceId);
693
+ }
694
+ }
695
+ return { missing };
696
+ }
697
+ /**
698
+ * v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
699
+ * requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
700
+ * The rule is enforced on a per-wave basis, where a wave is defined by
701
+ * `<artifacts-dir>/wave-plans/wave-NN.md` files (when present) listing
702
+ * slice ids. When no wave manifest exists, the linter falls back to a
703
+ * conservative implicit detection: a wave is a contiguous run of
704
+ * `phase=red` events with no other-phase events between them; the rule
705
+ * fires only when the implicit wave has 2+ members.
706
+ *
707
+ * @param waveMembers Optional explicit wave manifest. Map key is wave
708
+ * name (e.g. `"W-01"`); value is the set of slice ids in that wave.
709
+ */
710
+ export function evaluateRedCheckpoint(slices, waveMembers = null) {
711
+ const events = [];
712
+ for (const [sliceId, rows] of slices.entries()) {
713
+ for (const entry of rows) {
714
+ const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
715
+ if (typeof ts !== "string" || ts.length === 0)
716
+ continue;
717
+ if (typeof entry.phase !== "string")
718
+ continue;
719
+ events.push({ sliceId, phase: entry.phase, ts });
720
+ }
721
+ }
722
+ events.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
723
+ // Build the canonical wave list. Explicit manifest wins; otherwise
724
+ // derive implicit waves from contiguous red event blocks.
725
+ const waves = [];
726
+ if (waveMembers && waveMembers.size > 0) {
727
+ for (const [name, members] of waveMembers.entries()) {
728
+ if (members.size === 0)
729
+ continue;
730
+ waves.push({ name, members });
731
+ }
732
+ }
733
+ else {
734
+ let current = null;
735
+ let waveIdx = 0;
736
+ for (const evt of events) {
737
+ if (evt.phase === "red") {
738
+ if (current === null)
739
+ current = new Set();
740
+ current.add(evt.sliceId);
741
+ }
742
+ else if (current !== null) {
743
+ if (current.size >= 2) {
744
+ waveIdx += 1;
745
+ waves.push({ name: `implicit-${waveIdx}`, members: current });
746
+ }
747
+ current = null;
748
+ }
749
+ }
750
+ if (current !== null && current.size >= 2) {
751
+ waveIdx += 1;
752
+ waves.push({ name: `implicit-${waveIdx}`, members: current });
753
+ }
754
+ }
755
+ if (waves.length === 0) {
756
+ return {
757
+ ok: true,
758
+ details: "RED checkpoint inactive: no wave manifest detected and no implicit wave (2+ contiguous reds) found."
759
+ };
760
+ }
761
+ const violations = [];
762
+ for (const wave of waves) {
763
+ const memberReds = events.filter((e) => e.phase === "red" && wave.members.has(e.sliceId));
764
+ const memberGreens = events.filter((e) => e.phase === "green" && wave.members.has(e.sliceId));
765
+ if (memberReds.length === 0 || memberGreens.length === 0)
766
+ continue;
767
+ const lastRedTs = memberReds.reduce((acc, e) => (e.ts > acc ? e.ts : acc), memberReds[0].ts);
768
+ for (const g of memberGreens) {
769
+ if (g.ts < lastRedTs) {
770
+ violations.push(`${wave.name}: ${g.sliceId} phase=green at ${g.ts} precedes wave's last phase=red completedTs at ${lastRedTs}`);
771
+ }
772
+ }
773
+ }
774
+ if (violations.length === 0) {
775
+ return {
776
+ ok: true,
777
+ details: `RED checkpoint holds across ${waves.length} wave(s): all phase=green events follow the last phase=red of their wave.`
778
+ };
779
+ }
780
+ return {
781
+ ok: false,
782
+ details: `RED checkpoint violation: ${violations.join("; ")}. ` +
783
+ "Dispatch ALL Phase A test-author --phase red calls in one message, verify every phase=red event lands with non-empty evidenceRefs, and only then dispatch Phase B slice-implementer --phase green + slice-documenter --phase doc fan-out."
784
+ };
785
+ }
786
+ /**
787
+ * Read explicit wave manifest from `<artifacts-dir>/wave-plans/wave-NN.md`
788
+ * files. Returns a map from wave name to the set of slice ids it
789
+ * contains. Slice ids are extracted via `S-<digits>` regex matches in
790
+ * each wave file. Returns null when no wave files exist or all are
791
+ * empty/unparseable.
792
+ */
793
+ async function readWaveManifest(artifactsDir) {
794
+ const wavePlansDir = path.join(artifactsDir, "wave-plans");
795
+ let entries = [];
796
+ try {
797
+ entries = await fs.readdir(wavePlansDir);
798
+ }
799
+ catch {
800
+ return null;
801
+ }
802
+ const waves = new Map();
803
+ for (const name of entries) {
804
+ const match = /^wave-(\d+)\.md$/u.exec(name);
805
+ if (!match)
806
+ continue;
807
+ const wavePath = path.join(wavePlansDir, name);
808
+ let body = "";
809
+ try {
810
+ body = await fs.readFile(wavePath, "utf8");
811
+ }
812
+ catch {
813
+ continue;
814
+ }
815
+ const ids = extractSliceIdsFromBody(body);
816
+ if (ids.length === 0)
817
+ continue;
818
+ waves.set(`W-${match[1]}`, new Set(ids));
819
+ }
820
+ return waves.size > 0 ? waves : null;
821
+ }
822
+ const LEGACY_PER_SLICE_SECTIONS = [
823
+ "Test Discovery",
824
+ "RED Evidence",
825
+ "GREEN Evidence",
826
+ "Watched-RED Proof",
827
+ "Vertical Slice Cycle",
828
+ "Per-Slice Review",
829
+ "Failure Analysis",
830
+ "Acceptance Mapping"
831
+ ];
832
+ /**
833
+ * v6.12.0 Phase L — advisory finding when post-cutover slice ids appear
834
+ * in legacy per-slice sections of `06-tdd.md`. Reads
835
+ * `flow-state.json::tddCutoverSliceId` (e.g. `"S-10"`) and scans each
836
+ * legacy section for `S-<N>` references with N > cutover.
837
+ */
838
+ async function evaluateLegacySectionBackslide(ctx) {
839
+ const cutover = await readTddCutoverSliceId(ctx.projectRoot);
840
+ if (cutover === null)
841
+ return null;
842
+ const cutoverNum = parseSliceNumber(cutover);
843
+ if (cutoverNum === null)
844
+ return null;
845
+ const offenders = [];
846
+ for (const sectionName of LEGACY_PER_SLICE_SECTIONS) {
847
+ const body = sectionBodyByName(ctx.sections, sectionName);
848
+ if (body === null)
849
+ continue;
850
+ const ids = extractSliceIdsFromBody(body);
851
+ for (const id of ids) {
852
+ const num = parseSliceNumber(id);
853
+ if (num === null)
854
+ continue;
855
+ if (num > cutoverNum) {
856
+ offenders.push({ section: sectionName, sliceId: id });
857
+ }
858
+ }
859
+ }
860
+ if (offenders.length === 0)
861
+ return null;
862
+ const summary = offenders
863
+ .map((row) => `${row.sliceId} appears in legacy section \`## ${row.section}\``)
864
+ .join("; ");
865
+ return {
866
+ section: "tdd_legacy_section_writes_after_cutover",
867
+ required: false,
868
+ rule: "After v6.12.0 cutover, per-slice prose for slices > cutoverSliceId must live in `tdd-slices/S-<id>.md`, not in legacy `06-tdd.md` sections (Test Discovery, RED Evidence, GREEN Evidence, Watched-RED Proof, Vertical Slice Cycle, Per-Slice Review, Failure Analysis, Acceptance Mapping).",
869
+ found: false,
870
+ details: `${summary}. Move post-cutover slice prose into \`tdd-slices/<id>.md\` and let slice-documenter own the writes.`
871
+ };
872
+ }
873
+ async function readTddCutoverSliceId(projectRoot) {
874
+ const flowStatePath = path.join(projectRoot, ".cclaw/state/flow-state.json");
875
+ let raw;
876
+ try {
877
+ raw = await fs.readFile(flowStatePath, "utf8");
878
+ }
879
+ catch {
880
+ return null;
881
+ }
882
+ let parsed;
883
+ try {
884
+ parsed = JSON.parse(raw);
885
+ }
886
+ catch {
887
+ return null;
888
+ }
889
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
890
+ return null;
891
+ const value = parsed.tddCutoverSliceId;
892
+ if (typeof value !== "string" || value.length === 0)
893
+ return null;
894
+ return value;
895
+ }
896
+ function parseSliceNumber(sliceId) {
897
+ const match = /^S-(\d+)\b/u.exec(sliceId);
898
+ if (!match)
899
+ return null;
900
+ const num = Number.parseInt(match[1], 10);
901
+ return Number.isFinite(num) ? num : null;
902
+ }
903
+ function extractSliceIdsFromBody(body) {
904
+ const ids = new Set();
905
+ const regex = /\bS-(\d+)\b/gu;
906
+ let match;
907
+ while ((match = regex.exec(body)) !== null) {
908
+ ids.add(`S-${match[1]}`);
909
+ }
910
+ return [...ids];
911
+ }
525
912
  function pickEventTs(rows) {
526
913
  for (const entry of rows) {
527
914
  const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveArtifactPath as resolveStageArtifactPath } from "./artifact-paths.js";
4
+ import { effectiveWorktreeExecutionMode } from "./flow-state.js";
4
5
  import { exists } from "./fs-utils.js";
5
6
  import { stageSchema } from "./content/stage-schema.js";
6
7
  import { readFlowState } from "./run-persistence.js";
@@ -121,6 +122,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
121
122
  let activeRunId = null;
122
123
  let completedStagesForAudit = [];
123
124
  let completedStageMetaForAudit;
125
+ let legacyContinuation = false;
126
+ let worktreeExecutionMode = "single-tree";
124
127
  try {
125
128
  const flowState = await readFlowState(projectRoot);
126
129
  const hint = flowState.interactionHints?.[stage];
@@ -131,6 +134,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
131
134
  activeRunId = flowState.activeRunId ?? null;
132
135
  completedStagesForAudit = flowState.completedStages;
133
136
  completedStageMetaForAudit = flowState.completedStageMeta;
137
+ legacyContinuation = flowState.legacyContinuation === true;
138
+ worktreeExecutionMode = effectiveWorktreeExecutionMode(flowState);
134
139
  }
135
140
  catch {
136
141
  activeStageFlags = [];
@@ -139,6 +144,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
139
144
  activeRunId = null;
140
145
  completedStagesForAudit = [];
141
146
  completedStageMetaForAudit = undefined;
147
+ legacyContinuation = false;
148
+ worktreeExecutionMode = "single-tree";
142
149
  }
143
150
  for (const extra of options.extraStageFlags ?? []) {
144
151
  if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
@@ -274,7 +281,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
274
281
  isTrivialOverride,
275
282
  overrideSet,
276
283
  activeStageFlags,
277
- taskClass
284
+ taskClass,
285
+ legacyContinuation,
286
+ worktreeExecutionMode
278
287
  };
279
288
  switch (stage) {
280
289
  case "brainstorm":
@@ -209,10 +209,10 @@ export declare const CCLAW_AGENTS: readonly [{
209
209
  readonly body: string;
210
210
  }, {
211
211
  readonly name: "slice-documenter";
212
- readonly description: "PARALLEL with slice-implementer in TDD GREEN. Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Mandatory on discoveryMode=deep, opt-in elsewhere.";
212
+ readonly description: "MANDATORY in PARALLEL with slice-implementer for every TDD slice (regardless of discoveryMode, v6.12.0 Phase R). Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Linter rule `tdd_slice_documenter_missing` blocks the gate when a `phase=doc` event is missing for a green slice.";
213
213
  readonly tools: ["Read", "Write", "Edit", "Grep", "Glob"];
214
214
  readonly model: "fast";
215
- readonly activation: "on-demand";
215
+ readonly activation: "mandatory";
216
216
  readonly relatedStages: ["tdd"];
217
217
  readonly returnSchema: {
218
218
  readonly statusField: "status";
@@ -546,10 +546,10 @@ export const CCLAW_AGENTS = [
546
546
  },
547
547
  {
548
548
  name: "slice-documenter",
549
- description: "PARALLEL with slice-implementer in TDD GREEN. Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Mandatory on discoveryMode=deep, opt-in elsewhere.",
549
+ description: "MANDATORY in PARALLEL with slice-implementer for every TDD slice (regardless of discoveryMode, v6.12.0 Phase R). Writes per-slice prose summary to `<artifacts-dir>/tdd-slices/S-<id>.md`. Does NOT implement, does NOT write tests. Linter rule `tdd_slice_documenter_missing` blocks the gate when a `phase=doc` event is missing for a green slice.",
550
550
  tools: ["Read", "Write", "Edit", "Grep", "Glob"],
551
551
  model: "fast",
552
- activation: "on-demand",
552
+ activation: "mandatory",
553
553
  relatedStages: ["tdd"],
554
554
  returnSchema: {
555
555
  statusField: "status",
@@ -729,7 +729,7 @@ ${(() => {
729
729
  const mode = activationModeSummary();
730
730
  return `- **Mandatory:** ${mode.mandatory}.
731
731
  - **Proactive:** ${mode.proactive}.
732
- - **On-demand:** slice-implementer, fixer. Research playbooks are in-thread procedures.`;
732
+ - **On-demand:** fixer. Research playbooks are in-thread procedures.`;
733
733
  })()}
734
734
 
735
735
  ### Cost-aware routing