cclaw-cli 6.14.3 → 7.0.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 (86) hide show
  1. package/README.md +0 -2
  2. package/dist/artifact-linter/brainstorm.js +1 -1
  3. package/dist/artifact-linter/design.js +2 -2
  4. package/dist/artifact-linter/findings-dedup.js +1 -1
  5. package/dist/artifact-linter/plan.js +6 -6
  6. package/dist/artifact-linter/review-army.d.ts +1 -1
  7. package/dist/artifact-linter/review-army.js +1 -1
  8. package/dist/artifact-linter/scope.js +6 -6
  9. package/dist/artifact-linter/shared.d.ts +37 -73
  10. package/dist/artifact-linter/shared.js +30 -37
  11. package/dist/artifact-linter/spec.js +1 -1
  12. package/dist/artifact-linter/tdd.d.ts +20 -33
  13. package/dist/artifact-linter/tdd.js +89 -617
  14. package/dist/artifact-linter.js +11 -32
  15. package/dist/cli.js +1 -1
  16. package/dist/config.js +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/content/core-agents.d.ts +8 -26
  19. package/dist/content/core-agents.js +48 -94
  20. package/dist/content/examples.d.ts +1 -1
  21. package/dist/content/examples.js +4 -4
  22. package/dist/content/hooks.js +62 -149
  23. package/dist/content/idea.js +2 -2
  24. package/dist/content/iron-laws.js +1 -1
  25. package/dist/content/node-hooks.js +2 -2
  26. package/dist/content/skills-elicitation.js +2 -2
  27. package/dist/content/skills.d.ts +4 -6
  28. package/dist/content/skills.js +14 -53
  29. package/dist/content/stage-schema.d.ts +3 -3
  30. package/dist/content/stage-schema.js +8 -46
  31. package/dist/content/stages/brainstorm.js +5 -5
  32. package/dist/content/stages/plan.js +2 -2
  33. package/dist/content/stages/review.js +1 -1
  34. package/dist/content/stages/schema-types.d.ts +1 -1
  35. package/dist/content/stages/scope.js +1 -1
  36. package/dist/content/stages/spec.js +2 -2
  37. package/dist/content/stages/tdd.js +43 -108
  38. package/dist/content/start-command.js +3 -3
  39. package/dist/content/subagent-context-skills.js +5 -3
  40. package/dist/content/subagents.js +13 -74
  41. package/dist/content/templates.d.ts +6 -6
  42. package/dist/content/templates.js +23 -24
  43. package/dist/content/utility-skills.d.ts +1 -1
  44. package/dist/content/utility-skills.js +1 -1
  45. package/dist/delegation.d.ts +79 -139
  46. package/dist/delegation.js +83 -215
  47. package/dist/early-loop.js +1 -1
  48. package/dist/flow-state.d.ts +24 -129
  49. package/dist/flow-state.js +5 -30
  50. package/dist/gate-evidence.d.ts +2 -7
  51. package/dist/gate-evidence.js +2 -59
  52. package/dist/harness-adapters.d.ts +1 -1
  53. package/dist/harness-adapters.js +11 -10
  54. package/dist/install.js +24 -459
  55. package/dist/internal/advance-stage/advance.d.ts +5 -5
  56. package/dist/internal/advance-stage/advance.js +9 -24
  57. package/dist/internal/advance-stage/parsers.d.ts +1 -1
  58. package/dist/internal/advance-stage/review-loop.d.ts +1 -1
  59. package/dist/internal/advance-stage/review-loop.js +3 -3
  60. package/dist/internal/advance-stage/start-flow.js +1 -3
  61. package/dist/internal/advance-stage.js +4 -23
  62. package/dist/internal/cohesion-contract-stub.d.ts +8 -13
  63. package/dist/internal/cohesion-contract-stub.js +18 -24
  64. package/dist/internal/flow-state-repair.d.ts +1 -1
  65. package/dist/internal/plan-split-waves.d.ts +44 -7
  66. package/dist/internal/plan-split-waves.js +113 -12
  67. package/dist/internal/wave-status.d.ts +3 -6
  68. package/dist/internal/wave-status.js +5 -27
  69. package/dist/policy.js +1 -1
  70. package/dist/run-persistence.js +10 -44
  71. package/dist/runtime/run-hook.mjs +3 -3
  72. package/dist/track-heuristics.js +1 -1
  73. package/dist/types.d.ts +2 -2
  74. package/package.json +1 -1
  75. package/dist/integration-fanin.d.ts +0 -44
  76. package/dist/integration-fanin.js +0 -180
  77. package/dist/internal/set-checkpoint-mode.d.ts +0 -16
  78. package/dist/internal/set-checkpoint-mode.js +0 -72
  79. package/dist/internal/set-integration-overseer-mode.d.ts +0 -14
  80. package/dist/internal/set-integration-overseer-mode.js +0 -69
  81. package/dist/internal/set-worktree-mode.d.ts +0 -10
  82. package/dist/internal/set-worktree-mode.js +0 -28
  83. package/dist/worktree-manager.d.ts +0 -50
  84. package/dist/worktree-manager.js +0 -136
  85. package/dist/worktree-types.d.ts +0 -36
  86. package/dist/worktree-types.js +0 -6
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { integrationCheckRequired, loadTddReadySlicePool, readDelegationLedger, readDelegationEvents, selectReadySlices } from "../delegation.js";
3
+ import { loadTddReadySlicePool, readDelegationLedger, readDelegationEvents, selectReadySlices } from "../delegation.js";
4
4
  import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "../internal/plan-split-waves.js";
5
5
  import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
6
6
  const SLICE_SUMMARY_START = "<!-- auto-start: tdd-slice-summary -->";
@@ -8,7 +8,7 @@ const SLICE_SUMMARY_END = "<!-- auto-end: tdd-slice-summary -->";
8
8
  const SLICES_INDEX_START = "<!-- auto-start: slices-index -->";
9
9
  const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
10
10
  /**
11
- * v6.11.0 — TDD stage linter.
11
+ * TDD stage linter.
12
12
  *
13
13
  * Source-of-truth ladder, in order of precedence:
14
14
  *
@@ -18,26 +18,16 @@ const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
18
18
  * auto-derives Watched-RED / Vertical Slice Cycle from the events
19
19
  * and writes a rendered summary block between auto-render markers
20
20
  * in `06-tdd.md`. Markdown table content is no longer required.
21
- * 2. **Legacy markdown tables** (Watched-RED Proof + Vertical Slice
22
- * Cycle) — used as a fallback when the events ledger has no slice
23
- * phase rows for the active run. Existing v6.10 and earlier
24
- * artifacts continue to validate via this path.
21
+ * 2. **Hand-authored markdown tables** (Watched-RED Proof + Vertical
22
+ * Slice Cycle) — used as a fallback when the events ledger has no
23
+ * slice phase rows for the active run.
25
24
  * 3. **Sharded slice files** under `<artifacts-dir>/tdd-slices/S-*.md`.
26
25
  * Per-slice prose lives there. The main `06-tdd.md` is auto-indexed
27
26
  * via `## Slices Index`.
28
27
  */
29
28
  export async function lintTddStage(ctx) {
30
- const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode, legacyContinuation, tddCheckpointMode, integrationOverseerMode, tddCutoverSliceId, tddWorktreeCutoverSliceId } = ctx;
29
+ const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter } = ctx;
31
30
  void parsedFrontmatter;
32
- // v6.14.2 — boundary slice for the "any-metadata" exemption applied
33
- // to worktree-first findings. Falls back to the v6.12 cutover marker
34
- // when the boundary is absent (sync hasn't run, or `cclaw-cli sync`
35
- // detected no auto-detectable boundary). The exemption only kicks in
36
- // for `legacyContinuation: true` projects — fresh worktree-first
37
- // projects continue to enforce all three rules globally.
38
- const worktreeCutoverBoundary = legacyContinuation
39
- ? parseSliceNumber(tddWorktreeCutoverSliceId || tddCutoverSliceId || "")
40
- : null;
41
31
  const artifactsDir = path.dirname(absFile);
42
32
  const planPath = path.join(artifactsDir, "05-plan.md");
43
33
  let planRaw = "";
@@ -153,118 +143,58 @@ export async function lintTddStage(ctx) {
153
143
  });
154
144
  }
155
145
  }
156
- // v6.14.0 Phase 4 slice-documenter coverage is mandatory only on
157
- // `discoveryMode === "deep"` runs. lean/guided still emit the finding
158
- // but as advisory (`required: false`) so the controller can choose to
159
- // run a tighter inline-doc pass instead. The DOC role still exists;
160
- // the linter just stops blocking the gate on lean/guided. Reference
161
- // research report Section 4: "soften slice-documenter mandate".
146
+ // slice-builder owns DOC inline. For every slice with a phase=green
147
+ // row, require a matching phase=doc event whose evidenceRefs reference
148
+ // `<artifacts-dir>/tdd-slices/S-<id>.md`. Mandatory only on deep
149
+ // discoveryMode; advisory otherwise.
162
150
  if (eventsActive) {
163
- const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
151
+ const docResult = evaluateSliceDocCoverage(slicesByEvents);
164
152
  if (docResult.missing.length > 0) {
165
153
  const required = discoveryMode === "deep";
166
154
  findings.push({
167
- section: "tdd_slice_documenter_missing",
155
+ section: "tdd_slice_doc_missing",
168
156
  required,
169
157
  rule: required
170
- ? "deep mode: 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`."
171
- : "lean/guided modes (v6.14.0): the slice-documenter `phase=doc` event is advisory; controllers may use slice-implementer --finalize-doc inline instead. Required only for deep mode.",
158
+ ? "deep mode: every TDD slice with a phase=green event must also carry a slice-builder `phase=doc` event whose evidenceRefs reference `<artifacts-dir>/tdd-slices/S-<id>.md`."
159
+ : "lean/guided modes: the slice-builder `phase=doc` event is advisory; the doc step may be folded into the GREEN span. Required only for deep mode.",
172
160
  found: false,
173
- details: `Slices missing slice-documenter coverage: ${docResult.missing.join(", ")}. ` +
161
+ details: `Slices missing per-slice DOC coverage: ${docResult.missing.join(", ")}. ` +
174
162
  (required
175
- ? "Dispatch slice-documenter --slice <id> --phase doc in parallel with slice-implementer --phase green for each slice."
176
- : "Either dispatch slice-documenter --phase doc or call slice-implementer --finalize-doc inline at GREEN-completion.")
163
+ ? "Have the slice-builder emit a `--phase doc` row referencing `tdd-slices/S-<id>.md` after GREEN."
164
+ : "Either emit a `--phase doc` row referencing `tdd-slices/S-<id>.md` or fold the doc write into GREEN.")
177
165
  });
178
166
  }
179
167
  }
180
- // v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
181
- // with a phase=red row carrying non-empty evidenceRefs, require a
182
- // matching phase=green event whose `agent === "slice-implementer"`.
183
- // This catches "controller wrote GREEN itself" — the most common
184
- // backslide we have observed in fresh runs (hox S-11).
168
+ // slice-builder must own GREEN. For each slice with a phase=red row
169
+ // carrying non-empty evidenceRefs, require a matching phase=green event
170
+ // whose `agent === "slice-builder"`. Catches "controller wrote GREEN
171
+ // itself" backslides.
185
172
  if (eventsActive) {
186
- const implResult = evaluateSliceImplementerCoverage(slicesByEvents);
173
+ const implResult = evaluateSliceBuilderCoverage(slicesByEvents);
187
174
  if (implResult.missing.length > 0) {
188
175
  findings.push({
189
- section: "tdd_slice_implementer_missing",
176
+ section: "tdd_slice_builder_missing",
190
177
  required: true,
191
- 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).",
178
+ rule: "Every TDD slice that recorded a phase=red event with non-empty evidenceRefs must reach phase=green via `slice-builder`. Controller writing GREEN production code itself is forbidden.",
192
179
  found: false,
193
- details: `Slices missing slice-implementer GREEN coverage: ${implResult.missing.join(", ")}. Dispatch slice-implementer --slice <id> --phase green --paths <comma-separated production paths>.`
180
+ details: `Slices missing slice-builder-owned GREEN coverage: ${implResult.missing.join(", ")}. Dispatch slice-builder --slice <id> --phase green --paths <comma-separated production paths>.`
194
181
  });
195
182
  }
196
183
  }
197
- // v6.14.0 Phase 1 RED checkpoint enforcement. The mode is selected
198
- // by `flow-state.json::tddCheckpointMode`:
199
- //
200
- // - `per-slice` (default for new projects): enforce RED-before-GREEN
201
- // per slice only. No global wave barrier; lanes run RED→GREEN as
202
- // soon as their dependsOn closes. Rule id:
203
- // `tdd_slice_red_completed_before_green`.
204
- // - `global-red` (auto-applied for legacyContinuation): enforce the
205
- // v6.12 wave-batch barrier — every slice in a wave must complete
206
- // phase=red before any slice in the same wave starts phase=green.
207
- // Rule id: `tdd_red_checkpoint_violation` (legacy).
184
+ // Per-slice RED-before-GREEN only (no global-red wave barrier in the linter).
208
185
  if (eventsActive) {
209
- if (tddCheckpointMode === "global-red") {
210
- const waveManifest = await readMergedWaveManifestForCheckpoint(artifactsDir, planRaw);
211
- const checkpointResult = evaluateGlobalRedCheckpoint(slicesByEvents, waveManifest);
212
- if (!checkpointResult.ok) {
213
- findings.push({
214
- section: "tdd_red_checkpoint_violation",
215
- required: true,
216
- rule: "Wave Batch Mode (legacy global-red 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.",
217
- found: false,
218
- details: checkpointResult.details
219
- });
220
- }
221
- }
222
- else {
223
- const perSliceResult = evaluatePerSliceRedBeforeGreen(slicesByEvents);
224
- if (!perSliceResult.ok) {
225
- findings.push({
226
- section: "tdd_slice_red_completed_before_green",
227
- required: true,
228
- rule: "Stream-style TDD (v6.14.0): each slice's phase=green completedTs must be >= the same slice's last phase=red completedTs. No global wave barrier — lanes run independently.",
229
- found: false,
230
- details: perSliceResult.details
231
- });
232
- }
233
- }
234
- }
235
- // v6.12.0 Phase L — advisory backslide detection. When a cutover is
236
- // recorded in flow-state, slice-id rows in the legacy per-slice
237
- // sections of `06-tdd.md` that exceed the cutover boundary should
238
- // migrate to `tdd-slices/S-<id>.md`. Surface as advisory so it does
239
- // not block the gate but does keep the controller honest.
240
- const cutoverFinding = await evaluateLegacySectionBackslide({
241
- projectRoot,
242
- raw,
243
- sections
244
- });
245
- if (cutoverFinding) {
246
- findings.push(cutoverFinding);
247
- }
248
- // v6.14.2 Fix 2 — advisory cutover-misread detection. Fires when the
249
- // active run scheduled NEW work for the slice id stored in
250
- // `tddCutoverSliceId` AND that slice has already closed (terminal
251
- // refactor* row recorded for the same id, possibly under a prior
252
- // run). This is the "controller mistook the historical marker for an
253
- // active-slice pointer" pattern observed in hox W-03/S-17. Advisory
254
- // only — clears as soon as the controller pivots to a different
255
- // slice, and never blocks stage-complete.
256
- if (tddCutoverSliceId) {
257
- const misreadFinding = evaluateCutoverMisread({
258
- projectRoot,
259
- tddCutoverSliceId,
260
- activeRunEntries,
261
- ledgerEntries: delegationLedger.entries
262
- });
263
- if (misreadFinding) {
264
- findings.push(misreadFinding);
186
+ const perSliceResult = evaluatePerSliceRedBeforeGreen(slicesByEvents);
187
+ if (!perSliceResult.ok) {
188
+ findings.push({
189
+ section: "tdd_slice_red_completed_before_green",
190
+ required: true,
191
+ rule: "Each slice's phase=green completedTs must be >= the same slice's last phase=red completedTs. Lanes run independently within a wave.",
192
+ found: false,
193
+ details: perSliceResult.details
194
+ });
265
195
  }
266
196
  }
267
- const { events: jsonlEvents, fanInAudits } = await readDelegationEvents(projectRoot);
197
+ const { events: jsonlEvents } = await readDelegationEvents(projectRoot);
268
198
  const runEvents = jsonlEvents.filter((e) => e.runId === delegationLedger.runId);
269
199
  if (eventsActive && planRaw.length > 0) {
270
200
  const ignoredWave = await evaluateWavePlanDispatchIgnored({
@@ -272,194 +202,12 @@ export async function lintTddStage(ctx) {
272
202
  planMarkdown: planRaw,
273
203
  runEvents,
274
204
  runId: delegationLedger.runId,
275
- slices: slicesByEvents,
276
- legacyContinuation
205
+ slices: slicesByEvents
277
206
  });
278
207
  if (ignoredWave) {
279
208
  findings.push(ignoredWave);
280
209
  }
281
210
  }
282
- if (eventsActive && worktreeExecutionMode === "worktree-first") {
283
- const terminalPhases = new Set([
284
- "refactor",
285
- "refactor-deferred",
286
- "resolve-conflict"
287
- ]);
288
- // v6.14.3 — under `legacyContinuation: true` AND a stamped
289
- // boundary, exempt every slice closed at or before
290
- // `tddWorktreeCutoverSliceId`. The cutover boundary itself is the
291
- // contract: slices ≤ boundary were closed before the
292
- // worktree-first metadata mandate took effect, so we trust the
293
- // boundary as authoritative and do not require the slice to have
294
- // recorded zero metadata across all rows.
295
- //
296
- // The earlier v6.14.2 "all-or-nothing" rule rejected the common
297
- // hox-shape pattern where the GREEN row carries claim/lane/lease
298
- // (added on the v6.14.x worktree-first flip) but a later
299
- // `refactor-deferred` terminal row does not. That partial-
300
- // metadata layout is the operator-visible signature of the
301
- // failure mode this exemption was introduced to fix; flagging it
302
- // again under a different code defeated the entire migration.
303
- //
304
- // Operators who want a strict gate can opt out by clearing
305
- // `legacyContinuation` (or omitting `tddWorktreeCutoverSliceId`)
306
- // — both fields are explicit, persisted, and operator-editable.
307
- const isExemptLegacySlice = (sliceId) => {
308
- if (!legacyContinuation)
309
- return false;
310
- if (worktreeCutoverBoundary === null)
311
- return false;
312
- const n = parseSliceNumber(sliceId);
313
- if (n === null)
314
- return false;
315
- return n <= worktreeCutoverBoundary;
316
- };
317
- const missingGreenMeta = new Set();
318
- const exemptedGreenMeta = new Set();
319
- for (const ev of runEvents) {
320
- if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
321
- continue;
322
- if (ev.status !== "completed" || ev.phase !== "green")
323
- continue;
324
- if (typeof ev.sliceId !== "string")
325
- continue;
326
- const tok = ev.claimToken?.trim() ?? "";
327
- const lane = ev.ownerLaneId?.trim() ?? "";
328
- const lease = ev.leasedUntil?.trim() ?? "";
329
- if (tok.length === 0 || lane.length === 0 || lease.length === 0) {
330
- if (isExemptLegacySlice(ev.sliceId)) {
331
- exemptedGreenMeta.add(ev.sliceId);
332
- }
333
- else {
334
- missingGreenMeta.add(ev.sliceId);
335
- }
336
- }
337
- }
338
- if (missingGreenMeta.size > 0) {
339
- findings.push({
340
- section: "tdd_slice_lane_metadata_missing",
341
- required: true,
342
- rule: "Worktree-first: every completed slice-implementer phase=green row must record claimToken, ownerLaneId (--lane-id), and leasedUntil (--lease-until).",
343
- found: false,
344
- details: `Slices missing one or more lane fields on GREEN: ${[...missingGreenMeta].sort().join(", ")}. Remediation: include --claim-token, --lane-id, and --lease-until on every slice-implementer --phase green delegation-record write (schedule through completion); the hook fails fast with dispatch_lane_metadata_missing when they are omitted.`
345
- });
346
- }
347
- else if (exemptedGreenMeta.size > 0) {
348
- findings.push({
349
- section: "tdd_slice_lane_metadata_legacy_exempt",
350
- required: false,
351
- rule: "v6.14.2 legacyContinuation amnesty: closed slices ≤ tddWorktreeCutoverSliceId whose slice-implementer rows lack ALL worktree-first metadata are exempt from `tdd_slice_lane_metadata_missing`.",
352
- found: true,
353
- details: `Legacy-exempt slices (no claimToken/ownerLaneId/leasedUntil recorded; all closed before worktree-first flip): ${[...exemptedGreenMeta].sort().join(", ")}.`
354
- });
355
- }
356
- const missingClaim = new Set();
357
- const exemptedClaim = new Set();
358
- for (const ev of runEvents) {
359
- if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
360
- continue;
361
- if (ev.status !== "completed" && ev.status !== "failed")
362
- continue;
363
- if (!ev.phase || !terminalPhases.has(ev.phase))
364
- continue;
365
- const tok = ev.claimToken?.trim() ?? "";
366
- if (tok.length === 0 && typeof ev.sliceId === "string") {
367
- if (isExemptLegacySlice(ev.sliceId)) {
368
- exemptedClaim.add(ev.sliceId);
369
- }
370
- else {
371
- missingClaim.add(ev.sliceId);
372
- }
373
- }
374
- }
375
- if (missingClaim.size > 0) {
376
- findings.push({
377
- section: "tdd_slice_claim_token_missing",
378
- required: true,
379
- rule: "Worktree-first: terminal slice-implementer rows (refactor / refactor-deferred / resolve-conflict) must echo --claim-token. Remediation: pass the same --claim-token used on the scheduled row for every completed/failed terminal phase.",
380
- found: false,
381
- details: `Slices missing claim token on non-GREEN terminal rows: ${[...missingClaim].join(", ")}.`
382
- });
383
- }
384
- else if (exemptedClaim.size > 0) {
385
- findings.push({
386
- section: "tdd_slice_claim_token_legacy_exempt",
387
- required: false,
388
- rule: "v6.14.2 legacyContinuation amnesty: closed pre-cutover slices without claim tokens on terminal rows are exempt from `tdd_slice_claim_token_missing`.",
389
- found: true,
390
- details: `Legacy-exempt slices: ${[...exemptedClaim].sort().join(", ")}.`
391
- });
392
- }
393
- const conflictSlices = [
394
- ...new Set([
395
- ...runEvents
396
- .filter((e) => e.integrationState === "conflict")
397
- .map((e) => e.sliceId)
398
- .filter((s) => typeof s === "string"),
399
- ...fanInAudits
400
- .filter((a) => a.runId === delegationLedger.runId &&
401
- a.event === "cclaw_fanin_conflict" &&
402
- Array.isArray(a.sliceIds))
403
- .flatMap((a) => a.sliceIds ?? [])
404
- ].filter((s) => typeof s === "string" && s.length > 0))
405
- ];
406
- if (conflictSlices.length > 0) {
407
- findings.push({
408
- section: "tdd_fanin_conflict_unresolved",
409
- required: true,
410
- rule: "Resolve fan-in conflicts before stage-complete: dispatch slice-implementer --phase resolve-conflict or abandon the slice explicitly.",
411
- found: false,
412
- details: `integrationState=conflict for slice(s): ${conflictSlices.join(", ")}. Remediation: finish deterministic fan-in or mark integrationState=resolved after manual merge evidence.`
413
- });
414
- }
415
- const now = Date.now();
416
- const leaseStale = new Set();
417
- const leaseStaleExempted = new Set();
418
- // v6.14.2 — also exempt slices whose lease has expired but the
419
- // slice was already closed (terminal row recorded) before the
420
- // expiry. The reclaim audit row was just never written —
421
- // bookkeeping advisory, not a blocker.
422
- const closedBeforeLeaseExpiry = computeClosedBeforeLeaseExpiry(runEvents);
423
- for (const ev of runEvents) {
424
- if (typeof ev.leasedUntil !== "string")
425
- continue;
426
- const until = Date.parse(ev.leasedUntil);
427
- if (!Number.isFinite(until) || until >= now)
428
- continue;
429
- if (ev.leaseState === "reclaimed" || ev.leaseState === "released")
430
- continue;
431
- if (typeof ev.sliceId !== "string")
432
- continue;
433
- const sliceId = ev.sliceId;
434
- if (isExemptLegacySlice(sliceId)) {
435
- leaseStaleExempted.add(sliceId);
436
- continue;
437
- }
438
- if (closedBeforeLeaseExpiry.has(sliceId)) {
439
- leaseStaleExempted.add(sliceId);
440
- continue;
441
- }
442
- leaseStale.add(sliceId);
443
- }
444
- if (leaseStale.size > 0) {
445
- findings.push({
446
- section: "tdd_lease_expired_unreclaimed",
447
- required: true,
448
- rule: "Leases past leasedUntil must be reclaimed or released. Remediation: run scheduler reclaim or emit leaseState=reclaimed audit rows after controller action.",
449
- found: false,
450
- details: `Expired leases not reclaimed for slice(s): ${[...leaseStale].join(", ")}.`
451
- });
452
- }
453
- else if (leaseStaleExempted.size > 0) {
454
- findings.push({
455
- section: "tdd_lease_expired_legacy_exempt",
456
- required: false,
457
- rule: "v6.14.2 amnesty: expired leases are exempt when the slice closed before the expiry timestamp (reclaim audit just never recorded) OR when the slice predates the worktree-first cutover under legacyContinuation.",
458
- found: true,
459
- details: `Lease-expiry-exempt slices: ${[...leaseStaleExempted].sort().join(", ")}.`
460
- });
461
- }
462
- }
463
211
  const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
464
212
  if (assertionBody !== null) {
465
213
  const tableRows = assertionBody.split("\n").filter((line) => /^\|/u.test(line));
@@ -499,8 +247,8 @@ export async function lintTddStage(ctx) {
499
247
  : "Mocks/spies detected without boundary justification; add explicit trust-boundary rationale or replace with real/fake/stub coverage."
500
248
  });
501
249
  }
502
- const completedSliceImplementers = activeRunEntries.filter((entry) => entry.agent === "slice-implementer" && entry.status === "completed");
503
- const fanOutDetected = completedSliceImplementers.length > 1;
250
+ const completedSliceBuilders = activeRunEntries.filter((entry) => entry.agent === "slice-builder" && entry.status === "completed");
251
+ const fanOutDetected = completedSliceBuilders.length > 1;
504
252
  if (fanOutDetected) {
505
253
  const cohesionContractMarkdownPath = path.join(artifactsDir, "cohesion-contract.md");
506
254
  const cohesionContractJsonPath = path.join(artifactsDir, "cohesion-contract.json");
@@ -538,27 +286,14 @@ export async function lintTddStage(ctx) {
538
286
  cohesionContractFound = false;
539
287
  cohesionErrors.push("cohesion-contract.json is missing or invalid JSON.");
540
288
  }
541
- // v6.14.2 — soften cohesion-contract under `legacyContinuation: true`.
542
- // Pre-flip projects (hox) carry many closed implementer rows but
543
- // never recorded cross-slice cohesion data because that schema
544
- // didn't exist when the slices closed. Flag advisory + suggest the
545
- // auto-stub helper instead of blocking the gate.
546
- const cohesionRequired = legacyContinuation === true ? false : true;
547
- const advisoryNote = cohesionRequired
548
- ? cohesionErrors.join(" ")
549
- : `${cohesionErrors.join(" ")} ` +
550
- "Cohesion contract is advisory under legacyContinuation: true — emit a stub via " +
551
- "`cclaw-cli internal cohesion-contract --stub` to silence this finding.";
552
289
  findings.push({
553
290
  section: "tdd.cohesion_contract_missing",
554
- required: cohesionRequired,
555
- rule: cohesionRequired
556
- ? "When delegation ledger has >1 completed slice-implementer rows for active TDD run, require `.cclaw/artifacts/cohesion-contract.md` and parseable `.cclaw/artifacts/cohesion-contract.json` sidecar."
557
- : "v6.14.2 advisory under legacyContinuation: cohesion contract is recommended, not required. Use `cclaw-cli internal cohesion-contract --stub` to write a baseline.",
291
+ required: true,
292
+ rule: "When the delegation ledger has >1 completed slice-builder rows for the active TDD run, require `.cclaw/artifacts/cohesion-contract.md` and a parseable `.cclaw/artifacts/cohesion-contract.json` sidecar.",
558
293
  found: cohesionContractFound,
559
294
  details: cohesionContractFound
560
- ? `Fan-out detected (${completedSliceImplementers.length} completed slice-implementer rows); cohesion contract markdown+JSON sidecar are present and parseable.`
561
- : advisoryNote
295
+ ? `Fan-out detected (${completedSliceBuilders.length} completed slice-builder rows); cohesion contract markdown+JSON sidecar are present and parseable.`
296
+ : `${cohesionErrors.join(" ")} Use \`cclaw-cli internal cohesion-contract --stub\` only as a scaffold; the gate expects real cohesion data for fan-out waves.`
562
297
  });
563
298
  const completedOverseerRows = activeRunEntries.filter((entry) => entry.agent === "integration-overseer" && entry.status === "completed");
564
299
  const overseerStatusInEvidence = completedOverseerRows.some((entry) => {
@@ -568,74 +303,35 @@ export async function lintTddStage(ctx) {
568
303
  const overseerStatusInArtifact = /\bintegration-overseer\b[\s\S]{0,200}\b(?:PASS_WITH_GAPS|PASS)\b/iu.test(raw);
569
304
  const integrationOverseerFound = completedOverseerRows.length > 0 &&
570
305
  (overseerStatusInEvidence || overseerStatusInArtifact);
571
- // v6.14.0 Phase 3 — conditional integration-overseer dispatch. When
572
- // `integrationOverseerMode === "conditional"` and
573
- // `integrationCheckRequired()` returns required=false, the gate is
574
- // soft (advisory) and an audit-only finding is emitted so the
575
- // controller can record the deliberate skip in artifacts.
576
- //
577
- // v6.14.1 — also surface the audit row presence. When the controller
578
- // skips `integration-overseer` dispatch (or the heuristic returns
579
- // false), the run log MUST contain a
580
- // `cclaw_integration_overseer_skipped` audit row for traceability.
581
- // The advisory `tdd_integration_overseer_skipped_audit_missing`
582
- // surfaces a missing audit row when 2+ closed slices closed without
583
- // any overseer dispatch AND no audit was recorded.
584
- let overseerVerdict = null;
585
- let overseerRequired = true;
586
306
  const skippedAuditRowCount = await countIntegrationOverseerSkippedAudits(projectRoot, delegationLedger.runId);
587
307
  const skippedAuditRowFound = skippedAuditRowCount > 0;
588
- if (integrationOverseerMode === "conditional") {
589
- const eventsForVerdict = runEvents.length > 0 ? runEvents : [];
590
- const auditsForVerdict = fanInAudits.filter((a) => a.runId === delegationLedger.runId);
591
- overseerVerdict = integrationCheckRequired(eventsForVerdict, auditsForVerdict);
592
- overseerRequired = overseerVerdict.required;
593
- if (!overseerVerdict.required) {
594
- const auditRowSuffix = skippedAuditRowFound
595
- ? "audit row recorded — skip is fully traceable."
596
- : "audit row MISSING — controller should append `cclaw_integration_overseer_skipped` for traceability (see `tdd_integration_overseer_skipped_audit_missing`).";
597
- findings.push({
598
- section: "tdd_integration_overseer_skipped_by_disjoint_paths",
599
- required: false,
600
- rule: "v6.14.0+ conditional integration-overseer mode: the heuristic returned `required: false` (disjoint claimedPaths, no high-risk slices, no fan-in conflicts). The controller may skip dispatching `integration-overseer` and emit a `cclaw_integration_overseer_skipped` audit row instead.",
601
- found: true,
602
- details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe — ${auditRowSuffix}`
603
- });
604
- }
605
- }
606
- // v6.14.1 — `tdd_integration_overseer_skipped_audit_missing` (advisory).
607
- // Fires when fan-out is detected (2+ completed slice-implementers),
308
+ // Advisory: when fan-out is detected (2+ completed slice-builders) and
608
309
  // no `integration-overseer` was dispatched at all (no scheduled or
609
310
  // completed row for the active run), AND no
610
- // `cclaw_integration_overseer_skipped` audit row exists. This pairs
611
- // with the controller skill text rule that the wave-closure decision
612
- // ("dispatch overseer or skip") MUST leave a trail.
311
+ // `cclaw_integration_overseer_skipped` audit row exists, the controller
312
+ // should call `integrationCheckRequired()` and emit a
313
+ // `cclaw_integration_overseer_skipped` audit row so the decision stays
314
+ // traceable.
613
315
  const overseerDispatched = activeRunEntries.some((entry) => entry.agent === "integration-overseer");
614
316
  if (!overseerDispatched && !skippedAuditRowFound) {
615
317
  findings.push({
616
318
  section: "tdd_integration_overseer_skipped_audit_missing",
617
319
  required: false,
618
- rule: "v6.14.1: when a wave with 2+ closed slices closes without any integration-overseer dispatch, the controller should call `integrationCheckRequired()` and emit a `cclaw_integration_overseer_skipped` audit row so the decision is traceable. Advisory — never blocks stage-complete.",
320
+ rule: "When a wave with 2+ closed slices closes without any integration-overseer dispatch, the controller should call `integrationCheckRequired()` and emit a `cclaw_integration_overseer_skipped` audit row so the decision is traceable. Advisory — never blocks stage-complete.",
619
321
  found: false,
620
- details: `Fan-out detected (${completedSliceImplementers.length} completed slice-implementer rows) but no integration-overseer dispatch row OR cclaw_integration_overseer_skipped audit row exists for active run. ` +
322
+ details: `Fan-out detected (${completedSliceBuilders.length} completed slice-builder rows) but no integration-overseer dispatch row OR cclaw_integration_overseer_skipped audit row exists for active run. ` +
621
323
  "Remediation: emit `node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped --audit-reason=\"<reasons>\" --slice-ids=\"<S-1,S-2,...>\"` after wave closure."
622
324
  });
623
325
  }
624
326
  findings.push({
625
327
  section: "tdd.integration_overseer_missing",
626
- required: overseerRequired,
627
- rule: overseerRequired
628
- ? "When fan-out is detected, require completed `integration-overseer` evidence with PASS or PASS_WITH_GAPS."
629
- : "v6.14.0+ conditional integration-overseer mode: integration-overseer dispatch is advisory because `integrationCheckRequired()` returned required=false. Run it anyway if the run touches new boundaries.",
328
+ required: true,
329
+ rule: "When fan-out is detected, require completed `integration-overseer` evidence with PASS or PASS_WITH_GAPS.",
630
330
  found: integrationOverseerFound,
631
331
  details: integrationOverseerFound
632
332
  ? "integration-overseer completion recorded with PASS/PASS_WITH_GAPS evidence."
633
333
  : completedOverseerRows.length === 0
634
- ? overseerRequired
635
- ? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
636
- : skippedAuditRowFound
637
- ? "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths) and `cclaw_integration_overseer_skipped` audit row recorded. Audit-only."
638
- : "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths). Audit-only."
334
+ ? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
639
335
  : "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
640
336
  });
641
337
  }
@@ -710,7 +406,7 @@ export async function lintTddStage(ctx) {
710
406
  }
711
407
  }
712
408
  /**
713
- * v6.14.1 — count `cclaw_integration_overseer_skipped` audit rows in
409
+ * count `cclaw_integration_overseer_skipped` audit rows in
714
410
  * `delegation-events.jsonl` for a given runId. The audit row is not a
715
411
  * `DelegationEvent` (no agent/status), so `readDelegationEvents`
716
412
  * filters it out; we re-scan the raw file with a narrow JSON match.
@@ -811,7 +507,7 @@ export function evaluateEventsWatchedRed(slices) {
811
507
  if (redCount === 0) {
812
508
  return {
813
509
  ok: false,
814
- details: "Watched-RED Proof: events ledger has slice phase rows but none with phase=red. Dispatch test-author --slice <id> --phase red so RED is observable in delegation-events.jsonl."
510
+ details: "Watched-RED Proof: events ledger has slice phase rows but none with phase=red. Dispatch slice-builder --slice <id> --phase red so RED is observable in delegation-events.jsonl."
815
511
  };
816
512
  }
817
513
  if (errors.length > 0) {
@@ -881,7 +577,7 @@ export function evaluateEventsSliceCycle(slices) {
881
577
  });
882
578
  continue;
883
579
  }
884
- // v6.14.0 — refactorOutcome on phase=green satisfies REFACTOR coverage
580
+ // refactorOutcome on phase=green satisfies REFACTOR coverage
885
581
  // without a separate phase=refactor / phase=refactor-deferred row.
886
582
  // - mode: "inline" → REFACTOR ran inline as part of GREEN.
887
583
  // - mode: "deferred" → rationale required (carried in evidenceRefs[0]
@@ -893,7 +589,7 @@ export function evaluateEventsSliceCycle(slices) {
893
589
  findings.push({
894
590
  section: `tdd_slice_refactor_missing:${sliceId}`,
895
591
  required: true,
896
- rule: "Each TDD slice must close with a `phase=refactor` event, a `phase=refactor-deferred` event whose evidenceRefs / refactorRationale captures why refactor was deferred, OR a `phase=green` event carrying `refactorOutcome` (v6.14.0).",
592
+ rule: "Each TDD slice must close with a `phase=refactor` event, a `phase=refactor-deferred` event whose evidenceRefs / refactorRationale captures why refactor was deferred, OR a `phase=green` event carrying `refactorOutcome`.",
897
593
  found: false,
898
594
  details: `${sliceId}: no phase=refactor / phase=refactor-deferred event and no refactorOutcome on phase=green.`
899
595
  });
@@ -946,19 +642,14 @@ export function evaluateEventsSliceCycle(slices) {
946
642
  findings: []
947
643
  };
948
644
  }
949
- export function evaluateSliceDocumenterCoverage(slices) {
645
+ export function evaluateSliceDocCoverage(slices) {
950
646
  const missing = [];
951
647
  for (const [sliceId, rows] of slices.entries()) {
952
648
  const hasGreen = rows.some((entry) => entry.phase === "green");
953
649
  if (!hasGreen)
954
650
  continue;
955
- const docRow = rows.find((entry) => entry.phase === "doc");
956
- if (!docRow) {
957
- missing.push(sliceId);
958
- continue;
959
- }
960
- const refs = Array.isArray(docRow.evidenceRefs) ? docRow.evidenceRefs : [];
961
- const hasSliceFileRef = refs.some((ref) => typeof ref === "string" && /tdd-slices\/S-[^/]+\.md/u.test(ref));
651
+ const refsAcrossPhases = rows.flatMap((entry) => Array.isArray(entry.evidenceRefs) ? entry.evidenceRefs : []);
652
+ const hasSliceFileRef = refsAcrossPhases.some((ref) => typeof ref === "string" && /tdd-slices\/S-[^/]+\.md/u.test(ref));
962
653
  if (!hasSliceFileRef) {
963
654
  missing.push(sliceId);
964
655
  }
@@ -966,13 +657,11 @@ export function evaluateSliceDocumenterCoverage(slices) {
966
657
  return { missing };
967
658
  }
968
659
  /**
969
- * v6.12.0 Phase M — slice-implementer must own GREEN. For each slice
970
- * that recorded a phase=red event with non-empty evidenceRefs, require a
971
- * phase=green event whose `agent === "slice-implementer"`. Slices whose
972
- * GREEN event came from a different agent (e.g. controller wrote GREEN
973
- * itself and recorded a green row under another agent name) are flagged.
660
+ * `slice-builder` must own GREEN. For each slice that recorded a phase=red
661
+ * event with non-empty evidenceRefs, require a phase=green whose agent is
662
+ * `slice-builder`.
974
663
  */
975
- export function evaluateSliceImplementerCoverage(slices) {
664
+ export function evaluateSliceBuilderCoverage(slices) {
976
665
  const missing = [];
977
666
  for (const [sliceId, rows] of slices.entries()) {
978
667
  const reds = rows.filter((entry) => entry.phase === "red");
@@ -985,39 +674,24 @@ export function evaluateSliceImplementerCoverage(slices) {
985
674
  if (!hasRedEvidence)
986
675
  continue;
987
676
  const greens = rows.filter((entry) => entry.phase === "green");
988
- const ownedByImplementer = greens.some((entry) => entry.agent === "slice-implementer");
989
- if (!ownedByImplementer) {
677
+ const ownedByBuilder = greens.some((entry) => entry.agent === "slice-builder");
678
+ if (!ownedByBuilder) {
990
679
  missing.push(sliceId);
991
680
  }
992
681
  }
993
682
  return { missing };
994
683
  }
995
- async function readMergedWaveManifestForCheckpoint(artifactsDir, planMarkdown) {
996
- try {
997
- const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
998
- if (merged.length === 0)
999
- return null;
1000
- const map = new Map();
1001
- for (const w of merged) {
1002
- map.set(w.waveId, new Set(w.members.map((m) => m.sliceId)));
1003
- }
1004
- return map.size > 0 ? map : null;
1005
- }
1006
- catch {
1007
- return null;
1008
- }
1009
- }
1010
684
  function sliceRefactorTerminal(sliceId, slices) {
1011
685
  const rows = slices.get(sliceId);
1012
686
  if (!rows)
1013
687
  return false;
1014
- return rows.some((e) => e.agent === "slice-implementer" &&
688
+ return rows.some((e) => e.agent === "slice-builder" &&
1015
689
  (e.phase === "refactor" || e.phase === "refactor-deferred") &&
1016
690
  (e.status === "completed" || e.status === "failed"));
1017
691
  }
1018
692
  /**
1019
- * v6.13.1 — detect single-slice dispatch when the merged wave plan
1020
- * requires parallel ready slice-implementer fan-out.
693
+ * Detect single-slice dispatch when the merged wave plan requires parallel
694
+ * ready slice-builder fan-out.
1021
695
  */
1022
696
  export async function evaluateWavePlanDispatchIgnored(params) {
1023
697
  let merged;
@@ -1032,7 +706,7 @@ export async function evaluateWavePlanDispatchIgnored(params) {
1032
706
  let pool;
1033
707
  try {
1034
708
  pool = await loadTddReadySlicePool(params.planMarkdown, params.artifactsDir, {
1035
- legacyParallelDefaultSerial: params.legacyContinuation
709
+ legacyParallelDefaultSerial: false
1036
710
  });
1037
711
  }
1038
712
  catch {
@@ -1048,13 +722,15 @@ export async function evaluateWavePlanDispatchIgnored(params) {
1048
722
  }
1049
723
  const scoped = params.runEvents.filter((e) => e.runId === params.runId);
1050
724
  const tail = scoped.slice(-20);
1051
- const implInTail = new Set();
725
+ const builderInTail = new Set();
1052
726
  for (const e of tail) {
1053
- if (e.agent === "slice-implementer" && typeof e.sliceId === "string" && e.sliceId.length > 0) {
1054
- implInTail.add(e.sliceId);
727
+ if (e.agent === "slice-builder" &&
728
+ typeof e.sliceId === "string" &&
729
+ e.sliceId.length > 0) {
730
+ builderInTail.add(e.sliceId);
1055
731
  }
1056
732
  }
1057
- if (implInTail.size !== 1)
733
+ if (builderInTail.size !== 1)
1058
734
  return null;
1059
735
  for (const wave of merged) {
1060
736
  const waveSliceSet = new Set(wave.members.map((m) => m.sliceId));
@@ -1067,27 +743,27 @@ export async function evaluateWavePlanDispatchIgnored(params) {
1067
743
  const ready = selectReadySlices(wavePool, {
1068
744
  cap: Math.max(32, wavePool.length),
1069
745
  completedUnitIds,
1070
- activePathHolders: [],
1071
- legacyContinuation: params.legacyContinuation
746
+ activePathHolders: []
1072
747
  });
1073
748
  if (ready.length < 2)
1074
749
  continue;
1075
- const only = [...implInTail][0];
750
+ const only = [...builderInTail][0];
1076
751
  const missed = ready.map((r) => r.sliceId).filter((s) => s !== only);
1077
752
  if (missed.length === 0)
1078
753
  continue;
1079
754
  return {
1080
755
  section: "tdd_wave_plan_ignored",
1081
756
  required: true,
1082
- rule: "When the Parallel Execution Plan (or wave-plans/) defines an open wave with two or more ready parallelizable slices, the controller must fan out slice-implementer work for each ready slice instead of serializing to one slice only.",
757
+ rule: "When the Parallel Execution Plan (or wave-plans/) defines an open wave with two or more ready parallelizable slices, the controller must fan out slice-builder Tasks for each ready slice instead of serializing to one slice only.",
1083
758
  found: false,
1084
- details: `Wave ${wave.waveId}: scheduler-ready members ${ready.map((r) => r.sliceId).join(", ")}; last 20 delegation events show slice-implementer only for ${only}. Missed parallel dispatch: ${missed.join(", ")}. Remediation: load \`05-plan.md\` (Parallel Execution Plan) and \`wave-plans/\` before routing, launch the wave (AskQuestion only when waveCount>=2 and single-slice is a real alternative), then dispatch GREEN+DOC for every ready slice with mandatory worktree-first flags on GREEN.`
759
+ details: `Wave ${wave.waveId}: scheduler-ready members ${ready.map((r) => r.sliceId).join(", ")}; last 20 delegation events show slice workers only for ${only}. Missed parallel dispatch: ${missed.join(", ")}. Remediation: load \`05-plan.md\` (Parallel Execution Plan) and \`wave-plans/\` before routing, launch the wave (AskQuestion only when waveCount>=2 and single-slice is a real alternative), then dispatch workers for every ready slice.`
1085
760
  };
1086
761
  }
1087
762
  return null;
1088
763
  }
1089
764
  /**
1090
- * v6.12.0 Phase W (legacy `global-red` mode) — RED checkpoint enforcement.
765
+ * Global RED checkpoint enforcement (`global-red` mode).
766
+ *
1091
767
  * The wave protocol requires ALL Phase A REDs to land before ANY Phase B
1092
768
  * GREEN starts. The rule is enforced on a per-wave basis, where a wave is
1093
769
  * defined by the managed `## Parallel Execution Plan` block in
@@ -1097,10 +773,9 @@ export async function evaluateWavePlanDispatchIgnored(params) {
1097
773
  * with no other-phase events between them; the rule fires only when the
1098
774
  * implicit wave has 2+ members.
1099
775
  *
1100
- * v6.14.0: this function powers the `global-red` checkpoint mode. New
1101
- * projects default to `per-slice` mode (see
1102
- * `evaluatePerSliceRedBeforeGreen`); `legacyContinuation: true` projects
1103
- * auto-keep this behavior. Exported under both `evaluateGlobalRedCheckpoint`
776
+ * Default mode is `per-slice` (see `evaluatePerSliceRedBeforeGreen`);
777
+ * this checkpoint applies when a project explicitly opts into
778
+ * `global-red`. Exported under both `evaluateGlobalRedCheckpoint`
1104
779
  * (canonical name) and `evaluateRedCheckpoint` (back-compat alias for
1105
780
  * existing tests/consumers).
1106
781
  *
@@ -1180,30 +855,21 @@ export function evaluateGlobalRedCheckpoint(slices, waveMembers = null) {
1180
855
  return {
1181
856
  ok: false,
1182
857
  details: `RED checkpoint violation: ${violations.join("; ")}. ` +
1183
- "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."
858
+ "When using the global wave barrier, dispatch ALL slice-builder --phase red calls in one message, verify every phase=red event lands with non-empty evidenceRefs, and only then dispatch the GREEN/REFACTOR/DOC fan-out."
1184
859
  };
1185
860
  }
1186
861
  /**
1187
- * Back-compat alias for `evaluateGlobalRedCheckpoint` (v6.12.0 Phase W
1188
- * behavior). Existing tests/consumers can keep importing
1189
- * `evaluateRedCheckpoint`. The v6.14.0 stream-style mode uses
1190
- * `evaluatePerSliceRedBeforeGreen` instead.
862
+ * Back-compat alias for `evaluateGlobalRedCheckpoint`. The default mode
863
+ * uses `evaluatePerSliceRedBeforeGreen` instead.
1191
864
  */
1192
865
  export const evaluateRedCheckpoint = evaluateGlobalRedCheckpoint;
1193
866
  /**
1194
- * v6.14.0 — per-slice RED-before-GREEN enforcement (default mode).
867
+ * Per-slice RED-before-GREEN enforcement (default mode).
1195
868
  *
1196
869
  * For each slice with both phase=red and phase=green completed events,
1197
870
  * fail if any green completedTs precedes the slice's last red completedTs.
1198
871
  * No global wave barrier — different slices may freely interleave their
1199
872
  * RED/GREEN/REFACTOR phases.
1200
- *
1201
- * Note: this is intentionally weaker than `evaluateGlobalRedCheckpoint`
1202
- * because the W-02 measurement on hox showed ~6 minutes of barrier
1203
- * overhead when slices were already disjoint (file-overlap scheduler did
1204
- * the parallelism job). The per-slice rule retains the only invariant
1205
- * that mattered for correctness: no slice goes GREEN before its own
1206
- * RED is observed failing.
1207
873
  */
1208
874
  export function evaluatePerSliceRedBeforeGreen(slices) {
1209
875
  const violations = [];
@@ -1240,96 +906,6 @@ export function evaluatePerSliceRedBeforeGreen(slices) {
1240
906
  "Stream-style TDD requires each slice's RED to land before its own GREEN, but cross-lane interleaving is allowed."
1241
907
  };
1242
908
  }
1243
- const LEGACY_PER_SLICE_SECTIONS = [
1244
- "Test Discovery",
1245
- "RED Evidence",
1246
- "GREEN Evidence",
1247
- "Watched-RED Proof",
1248
- "Vertical Slice Cycle",
1249
- "Per-Slice Review",
1250
- "Failure Analysis",
1251
- "Acceptance Mapping"
1252
- ];
1253
- /**
1254
- * v6.12.0 Phase L — advisory finding when post-cutover slice ids appear
1255
- * in legacy per-slice sections of `06-tdd.md`. Reads
1256
- * `flow-state.json::tddCutoverSliceId` (e.g. `"S-10"`) and scans each
1257
- * legacy section for `S-<N>` references with N > cutover.
1258
- */
1259
- async function evaluateLegacySectionBackslide(ctx) {
1260
- const cutover = await readTddCutoverSliceId(ctx.projectRoot);
1261
- if (cutover === null)
1262
- return null;
1263
- const cutoverNum = parseSliceNumber(cutover);
1264
- if (cutoverNum === null)
1265
- return null;
1266
- const offenders = [];
1267
- for (const sectionName of LEGACY_PER_SLICE_SECTIONS) {
1268
- const body = sectionBodyByName(ctx.sections, sectionName);
1269
- if (body === null)
1270
- continue;
1271
- const ids = extractSliceIdsFromBody(body);
1272
- for (const id of ids) {
1273
- const num = parseSliceNumber(id);
1274
- if (num === null)
1275
- continue;
1276
- if (num > cutoverNum) {
1277
- offenders.push({ section: sectionName, sliceId: id });
1278
- }
1279
- }
1280
- }
1281
- if (offenders.length === 0)
1282
- return null;
1283
- const summary = offenders
1284
- .map((row) => `${row.sliceId} appears in legacy section \`## ${row.section}\``)
1285
- .join("; ");
1286
- return {
1287
- section: "tdd_legacy_section_writes_after_cutover",
1288
- required: false,
1289
- 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).",
1290
- found: false,
1291
- details: `${summary}. Move post-cutover slice prose into \`tdd-slices/<id>.md\` and let slice-documenter own the writes.`
1292
- };
1293
- }
1294
- async function readTddCutoverSliceId(projectRoot) {
1295
- const flowStatePath = path.join(projectRoot, ".cclaw/state/flow-state.json");
1296
- let raw;
1297
- try {
1298
- raw = await fs.readFile(flowStatePath, "utf8");
1299
- }
1300
- catch {
1301
- return null;
1302
- }
1303
- let parsed;
1304
- try {
1305
- parsed = JSON.parse(raw);
1306
- }
1307
- catch {
1308
- return null;
1309
- }
1310
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1311
- return null;
1312
- const value = parsed.tddCutoverSliceId;
1313
- if (typeof value !== "string" || value.length === 0)
1314
- return null;
1315
- return value;
1316
- }
1317
- function parseSliceNumber(sliceId) {
1318
- const match = /^S-(\d+)\b/u.exec(sliceId);
1319
- if (!match)
1320
- return null;
1321
- const num = Number.parseInt(match[1], 10);
1322
- return Number.isFinite(num) ? num : null;
1323
- }
1324
- function extractSliceIdsFromBody(body) {
1325
- const ids = new Set();
1326
- const regex = /\bS-(\d+)\b/gu;
1327
- let match;
1328
- while ((match = regex.exec(body)) !== null) {
1329
- ids.add(`S-${match[1]}`);
1330
- }
1331
- return [...ids];
1332
- }
1333
909
  function pickEventTs(rows) {
1334
910
  for (const entry of rows) {
1335
911
  const ts = entry.completedTs ?? entry.endTs ?? entry.ts;
@@ -1338,110 +914,6 @@ function pickEventTs(rows) {
1338
914
  }
1339
915
  return undefined;
1340
916
  }
1341
- /**
1342
- * v6.14.2 — slices whose terminal `refactor` / `refactor-deferred` /
1343
- * `resolve-conflict` row recorded a `completedTs` that PRECEDES the
1344
- * latest `leasedUntil` for the same slice. The lease was never
1345
- * reclaimed but the wave closed in time; the missing audit row is
1346
- * advisory bookkeeping, not a correctness failure.
1347
- */
1348
- function computeClosedBeforeLeaseExpiry(events) {
1349
- const terminalPhases = new Set([
1350
- "refactor",
1351
- "refactor-deferred",
1352
- "resolve-conflict"
1353
- ]);
1354
- const lastLease = new Map();
1355
- const earliestTerminal = new Map();
1356
- for (const ev of events) {
1357
- if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
1358
- continue;
1359
- if (typeof ev.sliceId !== "string")
1360
- continue;
1361
- if (typeof ev.leasedUntil === "string") {
1362
- const until = Date.parse(ev.leasedUntil);
1363
- if (Number.isFinite(until)) {
1364
- const prev = lastLease.get(ev.sliceId);
1365
- if (prev === undefined || until > prev) {
1366
- lastLease.set(ev.sliceId, until);
1367
- }
1368
- }
1369
- }
1370
- if (ev.status === "completed" &&
1371
- typeof ev.phase === "string" &&
1372
- terminalPhases.has(ev.phase) &&
1373
- typeof ev.completedTs === "string") {
1374
- const ts = Date.parse(ev.completedTs);
1375
- if (Number.isFinite(ts)) {
1376
- const prev = earliestTerminal.get(ev.sliceId);
1377
- if (prev === undefined || ts < prev) {
1378
- earliestTerminal.set(ev.sliceId, ts);
1379
- }
1380
- }
1381
- }
1382
- }
1383
- const out = new Set();
1384
- for (const [sliceId, terminalTs] of earliestTerminal.entries()) {
1385
- const leaseTs = lastLease.get(sliceId);
1386
- if (leaseTs === undefined)
1387
- continue;
1388
- if (terminalTs < leaseTs) {
1389
- out.add(sliceId);
1390
- }
1391
- }
1392
- return out;
1393
- }
1394
- /**
1395
- * v6.14.2 Fix 2 — advisory linter rule.
1396
- *
1397
- * Fires when:
1398
- * (a) `tddCutoverSliceId` is set on the active flow state, AND
1399
- * (b) the active run has a `scheduled` row whose `sliceId === tddCutoverSliceId`
1400
- * AND `phase ∈ {red, green, doc}`, AND
1401
- * (c) that slice already has a terminal `refactor` / `refactor-deferred` /
1402
- * `resolve-conflict` event recorded for it (under any run) — i.e.
1403
- * it's already closed.
1404
- *
1405
- * This is the diagnostic hox surfaced on S-17/W-03: the controller
1406
- * read `tddCutoverSliceId: "S-11"` and treated it as the active slice
1407
- * pointer, then dispatched new work for S-11 (already closed under
1408
- * v6.12 markdown). Advisory — never blocks stage-complete.
1409
- */
1410
- function evaluateCutoverMisread(input) {
1411
- const { tddCutoverSliceId, activeRunEntries, ledgerEntries } = input;
1412
- const cutoverPhases = new Set(["red", "green", "doc"]);
1413
- const newWork = activeRunEntries.find((entry) => entry.sliceId === tddCutoverSliceId &&
1414
- typeof entry.phase === "string" &&
1415
- cutoverPhases.has(entry.phase) &&
1416
- // any schedule/launch/ack/completed for the cutover slice in this run
1417
- (entry.status === "scheduled" ||
1418
- entry.status === "launched" ||
1419
- entry.status === "acknowledged" ||
1420
- entry.status === "completed"));
1421
- if (!newWork)
1422
- return null;
1423
- const terminalPhases = new Set([
1424
- "refactor",
1425
- "refactor-deferred",
1426
- "resolve-conflict"
1427
- ]);
1428
- const closure = ledgerEntries.find((entry) => entry.sliceId === tddCutoverSliceId &&
1429
- entry.status === "completed" &&
1430
- typeof entry.phase === "string" &&
1431
- terminalPhases.has(entry.phase));
1432
- if (!closure)
1433
- return null;
1434
- const closedTs = closure.completedTs ?? closure.endTs ?? closure.ts ?? "(unknown)";
1435
- const closedRunId = closure.runId ?? "(unknown-run)";
1436
- return {
1437
- section: "tdd_cutover_misread_warning",
1438
- required: false,
1439
- rule: "v6.14.2 Fix 2 advisory: `tddCutoverSliceId` is a HISTORICAL boundary set by sync, NOT a pointer to the active slice. The controller appears to have scheduled new work on the cutover slice id while that slice already closed.",
1440
- found: false,
1441
- details: `Active run scheduled new ${newWork.phase} work for slice ${tddCutoverSliceId} but that slice closed at ${closedTs} (run ${closedRunId}) — confirm this is intentional re-work, not a misread of tddCutoverSliceId. ` +
1442
- "Use `cclaw-cli internal wave-status --json` to find the next ready slice."
1443
- };
1444
- }
1445
917
  export function parseVerticalSliceCycle(body) {
1446
918
  const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
1447
919
  if (tableLines.length < 3) {