cclaw-cli 6.13.1 → 6.14.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.
@@ -639,4 +639,19 @@ export interface StageLintContext {
639
639
  * v6.13.0 — effective worktree execution mode for TDD linters.
640
640
  */
641
641
  worktreeExecutionMode: "single-tree" | "worktree-first";
642
+ /**
643
+ * v6.14.0 — effective TDD checkpoint mode. `per-slice` enforces
644
+ * RED-before-GREEN per slice (the default for new projects);
645
+ * `global-red` keeps the v6.12/v6.13 wave-batch barrier (auto-applied
646
+ * for `legacyContinuation: true` projects on `cclaw-cli sync`).
647
+ */
648
+ tddCheckpointMode: "per-slice" | "global-red";
649
+ /**
650
+ * v6.14.0 — effective integration-overseer dispatch mode.
651
+ * `conditional` runs the overseer only when
652
+ * `integrationCheckRequired()` returns `required: true`; `always`
653
+ * preserves the v6.13 behavior of running it on every multi-slice
654
+ * wave.
655
+ */
656
+ integrationOverseerMode: "conditional" | "always";
642
657
  }
@@ -65,20 +65,50 @@ export declare function evaluateWavePlanDispatchIgnored(params: {
65
65
  legacyContinuation: boolean;
66
66
  }): Promise<LintFinding | null>;
67
67
  /**
68
- * v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
69
- * requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
70
- * The rule is enforced on a per-wave basis, where a wave is defined by
71
- * the managed `## Parallel Execution Plan` block in `05-plan.md` and/or
72
- * `<artifacts-dir>/wave-plans/wave-NN.md` files. When no wave manifest
73
- * exists, the linter falls back to a conservative implicit detection: a
74
- * wave is a contiguous run of `phase=red` events with no other-phase
75
- * events between them; the rule fires only when the implicit wave has
76
- * 2+ members.
68
+ * v6.12.0 Phase W (legacy `global-red` mode) — RED checkpoint enforcement.
69
+ * The wave protocol requires ALL Phase A REDs to land before ANY Phase B
70
+ * GREEN starts. The rule is enforced on a per-wave basis, where a wave is
71
+ * defined by the managed `## Parallel Execution Plan` block in
72
+ * `05-plan.md` and/or `<artifacts-dir>/wave-plans/wave-NN.md` files. When
73
+ * no wave manifest exists, the linter falls back to a conservative
74
+ * implicit detection: a wave is a contiguous run of `phase=red` events
75
+ * with no other-phase events between them; the rule fires only when the
76
+ * implicit wave has 2+ members.
77
+ *
78
+ * v6.14.0: this function powers the `global-red` checkpoint mode. New
79
+ * projects default to `per-slice` mode (see
80
+ * `evaluatePerSliceRedBeforeGreen`); `legacyContinuation: true` projects
81
+ * auto-keep this behavior. Exported under both `evaluateGlobalRedCheckpoint`
82
+ * (canonical name) and `evaluateRedCheckpoint` (back-compat alias for
83
+ * existing tests/consumers).
77
84
  *
78
85
  * @param waveMembers Optional explicit wave manifest. Map key is wave
79
86
  * name (e.g. `"W-01"`); value is the set of slice ids in that wave.
80
87
  */
81
- export declare function evaluateRedCheckpoint(slices: Map<string, DelegationEntry[]>, waveMembers?: Map<string, Set<string>> | null): RedCheckpointResult;
88
+ export declare function evaluateGlobalRedCheckpoint(slices: Map<string, DelegationEntry[]>, waveMembers?: Map<string, Set<string>> | null): RedCheckpointResult;
89
+ /**
90
+ * Back-compat alias for `evaluateGlobalRedCheckpoint` (v6.12.0 Phase W
91
+ * behavior). Existing tests/consumers can keep importing
92
+ * `evaluateRedCheckpoint`. The v6.14.0 stream-style mode uses
93
+ * `evaluatePerSliceRedBeforeGreen` instead.
94
+ */
95
+ export declare const evaluateRedCheckpoint: typeof evaluateGlobalRedCheckpoint;
96
+ /**
97
+ * v6.14.0 — per-slice RED-before-GREEN enforcement (default mode).
98
+ *
99
+ * For each slice with both phase=red and phase=green completed events,
100
+ * fail if any green completedTs precedes the slice's last red completedTs.
101
+ * No global wave barrier — different slices may freely interleave their
102
+ * RED/GREEN/REFACTOR phases.
103
+ *
104
+ * Note: this is intentionally weaker than `evaluateGlobalRedCheckpoint`
105
+ * because the W-02 measurement on hox showed ~6 minutes of barrier
106
+ * overhead when slices were already disjoint (file-overlap scheduler did
107
+ * the parallelism job). The per-slice rule retains the only invariant
108
+ * that mattered for correctness: no slice goes GREEN before its own
109
+ * RED is observed failing.
110
+ */
111
+ export declare function evaluatePerSliceRedBeforeGreen(slices: Map<string, DelegationEntry[]>): RedCheckpointResult;
82
112
  export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
83
113
  interface VerificationLadderResult {
84
114
  ok: boolean;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadTddReadySlicePool, readDelegationLedger, readDelegationEvents, selectReadySlices } from "../delegation.js";
3
+ import { integrationCheckRequired, 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 -->";
@@ -27,7 +27,7 @@ const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
27
27
  * via `## Slices Index`.
28
28
  */
29
29
  export async function lintTddStage(ctx) {
30
- const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode, legacyContinuation } = ctx;
30
+ const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode, legacyContinuation, tddCheckpointMode, integrationOverseerMode } = ctx;
31
31
  void parsedFrontmatter;
32
32
  const artifactsDir = path.dirname(absFile);
33
33
  const planPath = path.join(artifactsDir, "05-plan.md");
@@ -144,21 +144,27 @@ export async function lintTddStage(ctx) {
144
144
  });
145
145
  }
146
146
  }
147
- // v6.12.0 Phase R — slice-documenter coverage is mandatory on every
148
- // TDD run regardless of discoveryMode. `discoveryMode` is now strictly
149
- // an early-stage knob (brainstorm/scope/design); TDD parallelism must
150
- // be uniform across lean/guided/deep so the controller cannot quietly
151
- // skip per-slice prose by picking a non-deep mode.
152
- void discoveryMode;
147
+ // v6.14.0 Phase 4 — slice-documenter coverage is mandatory only on
148
+ // `discoveryMode === "deep"` runs. lean/guided still emit the finding
149
+ // but as advisory (`required: false`) so the controller can choose to
150
+ // run a tighter inline-doc pass instead. The DOC role still exists;
151
+ // the linter just stops blocking the gate on lean/guided. Reference
152
+ // research report Section 4: "soften slice-documenter mandate".
153
153
  if (eventsActive) {
154
154
  const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
155
155
  if (docResult.missing.length > 0) {
156
+ const required = discoveryMode === "deep";
156
157
  findings.push({
157
158
  section: "tdd_slice_documenter_missing",
158
- required: true,
159
- 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).",
159
+ required,
160
+ rule: required
161
+ ? "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`."
162
+ : "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.",
160
163
  found: false,
161
- 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.`
164
+ details: `Slices missing slice-documenter coverage: ${docResult.missing.join(", ")}. ` +
165
+ (required
166
+ ? "Dispatch slice-documenter --slice <id> --phase doc in parallel with slice-implementer --phase green for each slice."
167
+ : "Either dispatch slice-documenter --phase doc or call slice-implementer --finalize-doc inline at GREEN-completion.")
162
168
  });
163
169
  }
164
170
  }
@@ -179,23 +185,42 @@ export async function lintTddStage(ctx) {
179
185
  });
180
186
  }
181
187
  }
182
- // v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
183
- // requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
184
- // Enforced per-wave: explicit `wave-plans/wave-NN.md` manifest if
185
- // present, otherwise implicit detection via contiguous red blocks
186
- // (size >= 2). Sequential per-slice runs (redgreen→refactor in a
187
- // tight loop) form size-1 implicit waves and are unaffected.
188
+ // v6.14.0 Phase 1 — RED checkpoint enforcement. The mode is selected
189
+ // by `flow-state.json::tddCheckpointMode`:
190
+ //
191
+ // - `per-slice` (default for new projects): enforce RED-before-GREEN
192
+ // per slice only. No global wave barrier; lanes run REDGREEN as
193
+ // soon as their dependsOn closes. Rule id:
194
+ // `tdd_slice_red_completed_before_green`.
195
+ // - `global-red` (auto-applied for legacyContinuation): enforce the
196
+ // v6.12 wave-batch barrier — every slice in a wave must complete
197
+ // phase=red before any slice in the same wave starts phase=green.
198
+ // Rule id: `tdd_red_checkpoint_violation` (legacy).
188
199
  if (eventsActive) {
189
- const waveManifest = await readMergedWaveManifestForCheckpoint(artifactsDir, planRaw);
190
- const checkpointResult = evaluateRedCheckpoint(slicesByEvents, waveManifest);
191
- if (!checkpointResult.ok) {
192
- findings.push({
193
- section: "tdd_red_checkpoint_violation",
194
- required: true,
195
- 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.",
196
- found: false,
197
- details: checkpointResult.details
198
- });
200
+ if (tddCheckpointMode === "global-red") {
201
+ const waveManifest = await readMergedWaveManifestForCheckpoint(artifactsDir, planRaw);
202
+ const checkpointResult = evaluateGlobalRedCheckpoint(slicesByEvents, waveManifest);
203
+ if (!checkpointResult.ok) {
204
+ findings.push({
205
+ section: "tdd_red_checkpoint_violation",
206
+ required: true,
207
+ 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.",
208
+ found: false,
209
+ details: checkpointResult.details
210
+ });
211
+ }
212
+ }
213
+ else {
214
+ const perSliceResult = evaluatePerSliceRedBeforeGreen(slicesByEvents);
215
+ if (!perSliceResult.ok) {
216
+ findings.push({
217
+ section: "tdd_slice_red_completed_before_green",
218
+ required: true,
219
+ 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.",
220
+ found: false,
221
+ details: perSliceResult.details
222
+ });
223
+ }
199
224
  }
200
225
  }
201
226
  // v6.12.0 Phase L — advisory backslide detection. When a cutover is
@@ -418,15 +443,41 @@ export async function lintTddStage(ctx) {
418
443
  const overseerStatusInArtifact = /\bintegration-overseer\b[\s\S]{0,200}\b(?:PASS_WITH_GAPS|PASS)\b/iu.test(raw);
419
444
  const integrationOverseerFound = completedOverseerRows.length > 0 &&
420
445
  (overseerStatusInEvidence || overseerStatusInArtifact);
446
+ // v6.14.0 Phase 3 — conditional integration-overseer dispatch. When
447
+ // `integrationOverseerMode === "conditional"` and
448
+ // `integrationCheckRequired()` returns required=false, the gate is
449
+ // soft (advisory) and an audit-only finding is emitted so the
450
+ // controller can record the deliberate skip in artifacts.
451
+ let overseerVerdict = null;
452
+ let overseerRequired = true;
453
+ if (integrationOverseerMode === "conditional") {
454
+ const eventsForVerdict = runEvents.length > 0 ? runEvents : [];
455
+ const auditsForVerdict = fanInAudits.filter((a) => a.runId === delegationLedger.runId);
456
+ overseerVerdict = integrationCheckRequired(eventsForVerdict, auditsForVerdict);
457
+ overseerRequired = overseerVerdict.required;
458
+ if (!overseerVerdict.required) {
459
+ findings.push({
460
+ section: "tdd_integration_overseer_skipped_by_disjoint_paths",
461
+ required: false,
462
+ 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.",
463
+ found: true,
464
+ details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe — record an audit row via delegation events for traceability.`
465
+ });
466
+ }
467
+ }
421
468
  findings.push({
422
469
  section: "tdd.integration_overseer_missing",
423
- required: true,
424
- rule: "When fan-out is detected, require completed `integration-overseer` evidence with PASS or PASS_WITH_GAPS.",
470
+ required: overseerRequired,
471
+ rule: overseerRequired
472
+ ? "When fan-out is detected, require completed `integration-overseer` evidence with PASS or PASS_WITH_GAPS."
473
+ : "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.",
425
474
  found: integrationOverseerFound,
426
475
  details: integrationOverseerFound
427
476
  ? "integration-overseer completion recorded with PASS/PASS_WITH_GAPS evidence."
428
477
  : completedOverseerRows.length === 0
429
- ? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
478
+ ? overseerRequired
479
+ ? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
480
+ : "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths). Audit-only."
430
481
  : "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
431
482
  });
432
483
  }
@@ -632,19 +683,43 @@ export function evaluateEventsSliceCycle(slices) {
632
683
  });
633
684
  continue;
634
685
  }
635
- if (refactors.length === 0) {
686
+ // v6.14.0 refactorOutcome on phase=green satisfies REFACTOR coverage
687
+ // without a separate phase=refactor / phase=refactor-deferred row.
688
+ // - mode: "inline" → REFACTOR ran inline as part of GREEN.
689
+ // - mode: "deferred" → rationale required (carried in evidenceRefs[0]
690
+ // by the hook helper so legacy linters keep working).
691
+ const greenWithOutcome = greens.find((entry) => entry.refactorOutcome &&
692
+ (entry.refactorOutcome.mode === "inline" || entry.refactorOutcome.mode === "deferred"));
693
+ if (refactors.length === 0 && !greenWithOutcome) {
636
694
  errors.push(`${sliceId}: phase=refactor or phase=refactor-deferred event missing.`);
637
695
  findings.push({
638
696
  section: `tdd_slice_refactor_missing:${sliceId}`,
639
697
  required: true,
640
- rule: "Each TDD slice must close with a `phase=refactor` event or a `phase=refactor-deferred` event whose evidenceRefs / refactorRationale captures why refactor was deferred.",
698
+ 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).",
641
699
  found: false,
642
- details: `${sliceId}: no phase=refactor or phase=refactor-deferred event.`
700
+ details: `${sliceId}: no phase=refactor / phase=refactor-deferred event and no refactorOutcome on phase=green.`
701
+ });
702
+ continue;
703
+ }
704
+ if (greenWithOutcome &&
705
+ greenWithOutcome.refactorOutcome?.mode === "deferred" &&
706
+ !greenWithOutcome.refactorOutcome.rationale &&
707
+ !(Array.isArray(greenWithOutcome.evidenceRefs) &&
708
+ greenWithOutcome.evidenceRefs.some((ref) => typeof ref === "string" && ref.trim().length > 0))) {
709
+ errors.push(`${sliceId}: phase=green refactorOutcome=deferred missing rationale.`);
710
+ findings.push({
711
+ section: `tdd_slice_refactor_missing:${sliceId}`,
712
+ required: true,
713
+ rule: "phase=green refactorOutcome=deferred requires a rationale (via --refactor-rationale or --evidence-ref).",
714
+ found: false,
715
+ details: `${sliceId}: phase=green refactorOutcome.mode=deferred recorded without rationale.`
643
716
  });
644
717
  continue;
645
718
  }
646
719
  const deferred = refactors.find((entry) => entry.phase === "refactor-deferred");
647
- if (deferred && refactors.every((entry) => entry.phase === "refactor-deferred")) {
720
+ if (refactors.length > 0 &&
721
+ deferred &&
722
+ refactors.every((entry) => entry.phase === "refactor-deferred")) {
648
723
  const refs = Array.isArray(deferred.evidenceRefs) ? deferred.evidenceRefs : [];
649
724
  const hasRationale = refs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
650
725
  if (!hasRationale) {
@@ -814,20 +889,27 @@ export async function evaluateWavePlanDispatchIgnored(params) {
814
889
  return null;
815
890
  }
816
891
  /**
817
- * v6.12.0 Phase W — RED checkpoint enforcement. The wave protocol
818
- * requires ALL Phase A REDs to land before ANY Phase B GREEN starts.
819
- * The rule is enforced on a per-wave basis, where a wave is defined by
820
- * the managed `## Parallel Execution Plan` block in `05-plan.md` and/or
821
- * `<artifacts-dir>/wave-plans/wave-NN.md` files. When no wave manifest
822
- * exists, the linter falls back to a conservative implicit detection: a
823
- * wave is a contiguous run of `phase=red` events with no other-phase
824
- * events between them; the rule fires only when the implicit wave has
825
- * 2+ members.
892
+ * v6.12.0 Phase W (legacy `global-red` mode) — RED checkpoint enforcement.
893
+ * The wave protocol requires ALL Phase A REDs to land before ANY Phase B
894
+ * GREEN starts. The rule is enforced on a per-wave basis, where a wave is
895
+ * defined by the managed `## Parallel Execution Plan` block in
896
+ * `05-plan.md` and/or `<artifacts-dir>/wave-plans/wave-NN.md` files. When
897
+ * no wave manifest exists, the linter falls back to a conservative
898
+ * implicit detection: a wave is a contiguous run of `phase=red` events
899
+ * with no other-phase events between them; the rule fires only when the
900
+ * implicit wave has 2+ members.
901
+ *
902
+ * v6.14.0: this function powers the `global-red` checkpoint mode. New
903
+ * projects default to `per-slice` mode (see
904
+ * `evaluatePerSliceRedBeforeGreen`); `legacyContinuation: true` projects
905
+ * auto-keep this behavior. Exported under both `evaluateGlobalRedCheckpoint`
906
+ * (canonical name) and `evaluateRedCheckpoint` (back-compat alias for
907
+ * existing tests/consumers).
826
908
  *
827
909
  * @param waveMembers Optional explicit wave manifest. Map key is wave
828
910
  * name (e.g. `"W-01"`); value is the set of slice ids in that wave.
829
911
  */
830
- export function evaluateRedCheckpoint(slices, waveMembers = null) {
912
+ export function evaluateGlobalRedCheckpoint(slices, waveMembers = null) {
831
913
  const events = [];
832
914
  for (const [sliceId, rows] of slices.entries()) {
833
915
  for (const entry of rows) {
@@ -903,6 +985,63 @@ export function evaluateRedCheckpoint(slices, waveMembers = null) {
903
985
  "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."
904
986
  };
905
987
  }
988
+ /**
989
+ * Back-compat alias for `evaluateGlobalRedCheckpoint` (v6.12.0 Phase W
990
+ * behavior). Existing tests/consumers can keep importing
991
+ * `evaluateRedCheckpoint`. The v6.14.0 stream-style mode uses
992
+ * `evaluatePerSliceRedBeforeGreen` instead.
993
+ */
994
+ export const evaluateRedCheckpoint = evaluateGlobalRedCheckpoint;
995
+ /**
996
+ * v6.14.0 — per-slice RED-before-GREEN enforcement (default mode).
997
+ *
998
+ * For each slice with both phase=red and phase=green completed events,
999
+ * fail if any green completedTs precedes the slice's last red completedTs.
1000
+ * No global wave barrier — different slices may freely interleave their
1001
+ * RED/GREEN/REFACTOR phases.
1002
+ *
1003
+ * Note: this is intentionally weaker than `evaluateGlobalRedCheckpoint`
1004
+ * because the W-02 measurement on hox showed ~6 minutes of barrier
1005
+ * overhead when slices were already disjoint (file-overlap scheduler did
1006
+ * the parallelism job). The per-slice rule retains the only invariant
1007
+ * that mattered for correctness: no slice goes GREEN before its own
1008
+ * RED is observed failing.
1009
+ */
1010
+ export function evaluatePerSliceRedBeforeGreen(slices) {
1011
+ const violations = [];
1012
+ for (const [sliceId, rows] of slices.entries()) {
1013
+ const reds = rows.filter((entry) => entry.phase === "red");
1014
+ const greens = rows.filter((entry) => entry.phase === "green");
1015
+ if (reds.length === 0 || greens.length === 0)
1016
+ continue;
1017
+ const redTs = reds
1018
+ .map((entry) => entry.completedTs ?? entry.endTs ?? entry.ts ?? "")
1019
+ .filter((ts) => ts.length > 0)
1020
+ .sort();
1021
+ const greenTs = greens
1022
+ .map((entry) => entry.completedTs ?? entry.endTs ?? entry.ts ?? "")
1023
+ .filter((ts) => ts.length > 0)
1024
+ .sort();
1025
+ if (redTs.length === 0 || greenTs.length === 0)
1026
+ continue;
1027
+ const lastRed = redTs[redTs.length - 1];
1028
+ const earliestGreen = greenTs[0];
1029
+ if (earliestGreen < lastRed) {
1030
+ violations.push(`${sliceId}: phase=green completedTs (${earliestGreen}) precedes the slice's last phase=red completedTs (${lastRed})`);
1031
+ }
1032
+ }
1033
+ if (violations.length === 0) {
1034
+ return {
1035
+ ok: true,
1036
+ details: `Per-slice RED-before-GREEN holds: ${slices.size} slice(s) checked.`
1037
+ };
1038
+ }
1039
+ return {
1040
+ ok: false,
1041
+ details: `Per-slice RED-before-GREEN violation: ${violations.join("; ")}. ` +
1042
+ "Stream-style TDD requires each slice's RED to land before its own GREEN, but cross-lane interleaving is allowed."
1043
+ };
1044
+ }
906
1045
  const LEGACY_PER_SLICE_SECTIONS = [
907
1046
  "Test Discovery",
908
1047
  "RED Evidence",
@@ -1,7 +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
+ import { effectiveIntegrationOverseerMode, effectiveTddCheckpointMode, effectiveWorktreeExecutionMode } from "./flow-state.js";
5
5
  import { exists } from "./fs-utils.js";
6
6
  import { stageSchema } from "./content/stage-schema.js";
7
7
  import { readFlowState } from "./run-persistence.js";
@@ -124,6 +124,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
124
124
  let completedStageMetaForAudit;
125
125
  let legacyContinuation = false;
126
126
  let worktreeExecutionMode = "single-tree";
127
+ let tddCheckpointMode = "per-slice";
128
+ let integrationOverseerMode = "always";
127
129
  try {
128
130
  const flowState = await readFlowState(projectRoot);
129
131
  const hint = flowState.interactionHints?.[stage];
@@ -136,6 +138,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
136
138
  completedStageMetaForAudit = flowState.completedStageMeta;
137
139
  legacyContinuation = flowState.legacyContinuation === true;
138
140
  worktreeExecutionMode = effectiveWorktreeExecutionMode(flowState);
141
+ tddCheckpointMode = effectiveTddCheckpointMode(flowState);
142
+ integrationOverseerMode = effectiveIntegrationOverseerMode(flowState);
139
143
  }
140
144
  catch {
141
145
  activeStageFlags = [];
@@ -146,6 +150,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
146
150
  completedStageMetaForAudit = undefined;
147
151
  legacyContinuation = false;
148
152
  worktreeExecutionMode = "single-tree";
153
+ tddCheckpointMode = "per-slice";
154
+ integrationOverseerMode = "always";
149
155
  }
150
156
  for (const extra of options.extraStageFlags ?? []) {
151
157
  if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
@@ -283,7 +289,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
283
289
  activeStageFlags,
284
290
  taskClass,
285
291
  legacyContinuation,
286
- worktreeExecutionMode
292
+ worktreeExecutionMode,
293
+ tddCheckpointMode,
294
+ integrationOverseerMode
287
295
  };
288
296
  switch (stage) {
289
297
  case "brainstorm":
@@ -343,7 +343,7 @@ function usage() {
343
343
  "Usage:",
344
344
  " node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--json]",
345
345
  " node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
346
- " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
346
+ " node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
347
347
  "",
348
348
  "Allowed --dispatch-surface values:",
349
349
  " " + VALID_DISPATCH_SURFACES.join(", "),
@@ -362,12 +362,14 @@ function usage() {
362
362
  "TDD slice phase tagging (v6.11.0):",
363
363
  " --slice=<id> TDD slice identifier (e.g. S-1) used by the linter to auto-derive the Watched-RED + Vertical Slice Cycle tables.",
364
364
  " --phase=<phase> one of " + VALID_DELEGATION_PHASES.join(", ") + ". Pair with --slice to record a TDD slice phase event.",
365
- " --refactor-rationale=<t> required when --phase=refactor-deferred unless --evidence-ref carries the rationale text.",
365
+ " --refactor-rationale=<t> required when --phase=refactor-deferred unless --evidence-ref carries the rationale text. v6.14.0: also paired with --refactor-outcome on phase=green.",
366
366
  " --claim-token=<opaque> v6.13 — required for worktree-first slice-implementer schedules with --slice (echo on all terminal rows for the span).",
367
367
  " --lane-id=<id> v6.13 — worktree lane id (ownerLaneId metadata).",
368
368
  " --lease-until=<iso> v6.13 — ISO8601 lease expiry for reclaim tooling.",
369
369
  " --depends-on=<a,b> v6.13 — comma-separated plan unit ids for scheduler diagnostics.",
370
370
  " --integration-state=<s> v6.13 — one of pending|applied|conflict|resolved|abandoned.",
371
+ " --refactor-outcome=<m> v6.14.0 — one of inline|deferred. Folds REFACTOR into the phase=green event so a single row can close RED→GREEN→REFACTOR. Pair --refactor-outcome=deferred with --refactor-rationale.",
372
+ " --risk-tier=<t> v6.14.0 — one of low|medium|high. high triggers integration-overseer in conditional mode.",
371
373
  ""
372
374
  ].join("\\n") + "\\n");
373
375
  }
@@ -538,6 +540,40 @@ function buildRow(args, status, runId, now, options) {
538
540
  : undefined;
539
541
  const leaseState =
540
542
  leasedUntil && status === "scheduled" ? "claimed" : undefined;
543
+ // v6.14.0: refactorOutcome folds REFACTOR into a phase=green event. We
544
+ // also accept it on phase=refactor / phase=refactor-deferred for forward
545
+ // compatibility with controllers that emit it on the legacy lifecycle.
546
+ // When mode=deferred and a --refactor-rationale is supplied we also
547
+ // mirror the rationale into evidenceRefs[0] so legacy linters keep
548
+ // reading evidence (matches the v6.11.0 refactor-deferred behavior).
549
+ const refactorOutcomeMode =
550
+ typeof args["refactor-outcome"] === "string"
551
+ ? args["refactor-outcome"].trim()
552
+ : "";
553
+ let refactorOutcome;
554
+ if (refactorOutcomeMode === "inline" || refactorOutcomeMode === "deferred") {
555
+ const rationaleRaw =
556
+ typeof args["refactor-rationale"] === "string"
557
+ ? args["refactor-rationale"].trim()
558
+ : "";
559
+ refactorOutcome = {
560
+ mode: refactorOutcomeMode,
561
+ ...(rationaleRaw.length > 0 ? { rationale: rationaleRaw } : {})
562
+ };
563
+ if (
564
+ refactorOutcomeMode === "deferred" &&
565
+ rationaleRaw.length > 0 &&
566
+ !resolvedEvidenceRefs.includes(rationaleRaw)
567
+ ) {
568
+ resolvedEvidenceRefs = [rationaleRaw, ...resolvedEvidenceRefs];
569
+ }
570
+ }
571
+ const riskTierRaw =
572
+ typeof args["risk-tier"] === "string" ? args["risk-tier"].trim() : "";
573
+ const riskTier =
574
+ riskTierRaw === "low" || riskTierRaw === "medium" || riskTierRaw === "high"
575
+ ? riskTierRaw
576
+ : undefined;
541
577
  return {
542
578
  stage: args.stage,
543
579
  agent: args.agent,
@@ -568,7 +604,9 @@ function buildRow(args, status, runId, now, options) {
568
604
  leasedUntil,
569
605
  leaseState,
570
606
  dependsOn,
571
- integrationState
607
+ integrationState,
608
+ refactorOutcome,
609
+ riskTier
572
610
  };
573
611
  }
574
612
 
@@ -1139,6 +1177,44 @@ async function main() {
1139
1177
  }
1140
1178
  }
1141
1179
 
1180
+ // v6.14.0 — --refactor-outcome must be one of inline|deferred. When
1181
+ // mode=deferred a rationale is required (either --refactor-rationale or
1182
+ // --evidence-ref carrying the rationale text). --risk-tier must be one of
1183
+ // low|medium|high if provided.
1184
+ if (
1185
+ args["refactor-outcome"] !== undefined &&
1186
+ args["refactor-outcome"] !== "inline" &&
1187
+ args["refactor-outcome"] !== "deferred"
1188
+ ) {
1189
+ problems.push("invalid --refactor-outcome (allowed: inline, deferred)");
1190
+ emitProblems(problems, json, 2);
1191
+ return;
1192
+ }
1193
+ if (args["refactor-outcome"] === "deferred") {
1194
+ const rationaleProvided =
1195
+ typeof args["refactor-rationale"] === "string" && args["refactor-rationale"].trim().length > 0;
1196
+ const evidenceProvided =
1197
+ (typeof args["evidence-ref"] === "string" && args["evidence-ref"].trim().length > 0) ||
1198
+ (Array.isArray(args["evidence-refs"]) && args["evidence-refs"].some(
1199
+ (ref) => typeof ref === "string" && ref.trim().length > 0
1200
+ ));
1201
+ if (!rationaleProvided && !evidenceProvided) {
1202
+ problems.push("--refactor-outcome=deferred requires --refactor-rationale=<text> or --evidence-ref=<text>");
1203
+ emitProblems(problems, json, 2);
1204
+ return;
1205
+ }
1206
+ }
1207
+ if (
1208
+ args["risk-tier"] !== undefined &&
1209
+ args["risk-tier"] !== "low" &&
1210
+ args["risk-tier"] !== "medium" &&
1211
+ args["risk-tier"] !== "high"
1212
+ ) {
1213
+ problems.push("invalid --risk-tier (allowed: low, medium, high)");
1214
+ emitProblems(problems, json, 2);
1215
+ return;
1216
+ }
1217
+
1142
1218
  if (args.status === "completed" && args["dispatch-surface"] !== "role-switch") {
1143
1219
  for (const key of ["dispatch-id", "dispatch-surface", "agent-definition-path"]) {
1144
1220
  if (!args[key]) problems.push("completed isolated/generic status requires --" + key);
@@ -37,7 +37,7 @@ export const TDD = {
37
37
  },
38
38
  executionModel: {
39
39
  checklist: [
40
- "**Wave dispatch (v6.13.1):** Before routing, read the Parallel Execution Plan (managed block in the track planning artifact) and `<artifacts-dir>/wave-plans/`. Multi-ready waves: one AskQuestion (launch wave vs single-slice); then RED checkpoint, parallel GREEN+DOC with worktree-first flags, per-lane REFACTOR. Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
40
+ "**Stream-style wave dispatch (v6.14.0):** Before routing, read the Parallel Execution Plan (managed block in the track planning artifact) and `<artifacts-dir>/wave-plans/`. Per-lane stream: each lane runs RED→GREEN→REFACTOR independently as soon as its `dependsOn` closes — no global RED checkpoint between Phase A and Phase B. The linter enforces RED-before-GREEN per slice via `tdd_slice_red_completed_before_green`; cross-lane interleaving is allowed. **Legacy `global-red` mode** is preserved for projects with `legacyContinuation: true` and any project that explicitly sets `flow-state.json::tddCheckpointMode: \"global-red\"` (rule `tdd_red_checkpoint_violation` still fires there). Multi-ready waves still get one AskQuestion (launch wave vs single-slice); then per-lane GREEN+DOC dispatch with worktree-first flags. Integration-overseer fires only on cross-slice trigger (see `integrationCheckRequired()` heuristic). Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
41
41
  "Select vertical slice — the active wave plan (or single ready slice) defines work. Do not ask \"which slice next?\" when the plan already resolves it. Before starting, read `.cclaw/state/ralph-loop.json` (`loopIteration`, `acClosed[]`, `redOpenSlices[]`) so you skip cycles already closed. If `redOpenSlices[]` is non-empty, repair or explicitly park those slices before opening a new RED.",
42
42
  "Map to acceptance criterion — identify the specific spec criterion this test proves.",
43
43
  "Discover the test surface — inspect existing tests, fixtures, helpers, test commands, and nearby assertions before authoring RED. Reuse the local test style unless the slice genuinely needs a new pattern.",
@@ -49,8 +49,8 @@ export const TDD = {
49
49
  "GREEN: Run full suite — execute ALL tests, not just the ones you wrote. The full suite must be GREEN.",
50
50
  "GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
51
51
  "Run verification-before-completion discipline for the slice — capture a fresh test command, explicit PASS/FAIL status, and a config-aware ref (commit SHA when VCS is present/required, or no-vcs attestation when allowed).",
52
- "REFACTOR: re-dispatch the `slice-implementer` (or `test-author`) with `--phase refactor` once GREEN holds, OR `--phase refactor-deferred --refactor-rationale \"<why>\"` to close the slice without a refactor pass. Both options are recorded as a delegation event; the linter accepts either as REFACTOR coverage. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
53
- "DOC (parallel, mandatory v6.12.0): dispatch `slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md` IN PARALLEL with `slice-implementer --phase green` for the same slice ONE message with TWO concurrent Task calls. The documenter only writes `tdd-slices/S-<id>.md`, so its `--paths` are disjoint from the implementer's production paths and the file-overlap scheduler auto-allows the parallel dispatch. **Provisional-then-finalize:** append a provisional row/section in `tdd-slices/S-<id>.md` at dispatch time, then finalize that artifact after the matching `phase=green` event records evidence (never treat guesses as final before GREEN lands). Linter rule `tdd_slice_documenter_missing` blocks the gate when the `phase=doc` event is absent (regardless of `discoveryMode`).",
52
+ "REFACTOR (v6.14.0 — three forms): (1) re-dispatch `slice-implementer` with `--phase refactor` after GREEN; (2) re-dispatch with `--phase refactor-deferred --refactor-rationale \"<why>\"` to close without a separate pass; (3) **fold REFACTOR into GREEN** by adding `--refactor-outcome=inline|deferred [--refactor-rationale=\"<why>\"]` on the same `slice-implementer --phase green` dispatch. Form (3) is the v6.14.0 default; the linter accepts all three as REFACTOR coverage. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
53
+ "DOC (v6.14.0 — softened): in `discoveryMode=deep` runs DOC remains mandatory — dispatch `slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md` IN PARALLEL with `slice-implementer --phase green` for the same slice (ONE message with TWO concurrent Task calls). The documenter only writes `tdd-slices/S-<id>.md`, so its `--paths` are disjoint from the implementer's production paths and the file-overlap scheduler auto-allows parallel dispatch. **In `lean` and `guided` modes DOC is advisory** (linter `tdd_slice_documenter_missing` becomes `required: false`); controllers may either keep parallel `slice-documenter` dispatch or call `slice-implementer --finalize-doc` inline at GREEN-completion. **Provisional-then-finalize still applies for parallel dispatch:** append a provisional row/section in `tdd-slices/S-<id>.md` at dispatch time, then finalize after the matching `phase=green` event records evidence.",
54
54
  "**slice-documenter writes per-slice prose** (test discovery, system-wide impact check, RED/GREEN/REFACTOR notes, acceptance mapping, failure analysis) into `tdd-slices/S-<id>.md`. Controller does NOT touch this content. When logging a `green` row, attach the closed acceptance-criterion IDs in `acIds` so Ralph Loop status counts them.",
55
55
  "Annotate traceability — link to the active track's source: plan task ID + spec criterion on standard/medium, or spec acceptance item / bug reproduction slice on quick.",
56
56
  "**Boundary with review (do NOT escalate single-slice findings to whole-diff review).** `tdd.Per-Slice Review` OWNS severity-classified findings WITHIN one slice (correctness, edge cases, regression). `review` OWNS whole-diff Layer 1 (spec compliance) plus Layer 2 (cross-slice integration, security sweep, dependency/version audit, observability). When a single-slice finding genuinely needs whole-diff escalation, surface it in `06-tdd.md > Per-Slice Review` first; review will cite it (not re-classify) and the cross-artifact-duplication linter requires matching severity/disposition.",
@@ -58,7 +58,7 @@ export const TDD = {
58
58
  "Repeat for each slice — when not in multi-slice wave mode, return to wave-plan discovery; otherwise continue the active wave until members close.",
59
59
  ],
60
60
  interactionProtocol: [
61
- "Pick one vertical slice at a time **only when** the merged wave plan leaves a single scheduler-ready slice or the operator chose single-slice mode. Parallel implementers are allowed when (a) lanes touch non-overlapping files (the file-overlap scheduler auto-allows parallel when `--paths` are disjoint), and (b) an `integration-overseer` is dispatched after the parallel lanes and writes cohesion-evidence into the artifact before the gate is marked passed.",
61
+ "Pick one vertical slice at a time **only when** the merged wave plan leaves a single scheduler-ready slice or the operator chose single-slice mode. Parallel implementers are allowed when lanes touch non-overlapping files (the file-overlap scheduler auto-allows parallel when `--paths` are disjoint). **Integration-overseer is conditional in v6.14.0** (see `flow-state.json::integrationOverseerMode`): with the default `\"conditional\"` it dispatches only when `integrationCheckRequired()` returns `required: true` (shared import boundaries between closed slices, any slice with `riskTier=high`, or a recorded `cclaw_fanin_conflict`). When the heuristic returns `required: false`, record an audit `cclaw_integration_overseer_skipped` and let the linter emit advisory `tdd_integration_overseer_skipped_by_disjoint_paths`. Projects with `legacyContinuation: true` or explicit `\"always\"` keep the v6.13.x mandatory dispatch.",
62
62
  "Slice implementers are sequential only when the plan serializes work; prefer wave-parallel GREEN+DOC when the Parallel Execution Plan marks multiple ready members.",
63
63
  "Controller owns orchestration. For each slice S-<id>, dispatch in this order: (1) `test-author --slice S-<id> --phase red` (RED-only, no production edits), (2) `slice-implementer --slice S-<id> --phase green --paths <comma-separated>` for GREEN, (3) re-dispatch `--phase refactor` or `--phase refactor-deferred --refactor-rationale \"<why>\"` to close REFACTOR. Each dispatch records a row in `delegation-events.jsonl` and the linter auto-derives the Watched-RED + Vertical Slice Cycle tables from those rows. Do NOT hand-edit those tables.",
64
64
  "Before writing RED tests, discover relevant existing tests and commands so the new test extends the suite instead of fighting it.",
@@ -115,7 +115,7 @@ If during any stage the agent discovers evidence that contradicts the initial Ph
115
115
  2. If flow state is missing → guide the user to run \`npx cclaw-cli init\` and stop.
116
116
  3. If flow state is only a fresh init placeholder (\`completedStages: []\`, all \`passed\` arrays empty, and no \`00-idea.md\`) → stop and ask for \`/cc <prompt>\` to start a tracked run. Do not create a brainstorm state implicitly.
117
117
  4. Otherwise check current stage gates, resume if incomplete, and advance if complete.
118
- 5. **TDD wave dispatch (v6.13.1):** When \`currentStage\` is \`tdd\`, read \`${RUNTIME_ROOT}/artifacts/05-plan.md\` Parallel Execution Plan block and \`${RUNTIME_ROOT}/artifacts/wave-plans/\` **before** any slice-routing question. If an open wave still has multiple ready slices, resume parallel dispatch for the **remaining** members only (do not restart completed slices).
118
+ 5. **TDD wave dispatch (v6.14.0 — stream-style):** When \`currentStage\` is \`tdd\`, read \`${RUNTIME_ROOT}/artifacts/05-plan.md\` Parallel Execution Plan block and \`${RUNTIME_ROOT}/artifacts/wave-plans/\` **before** any slice-routing question. If an open wave still has multiple ready slices, resume per-lane stream dispatch for the **remaining** members only (do not restart completed slices). **No global RED checkpoint barrier** in default \`tddCheckpointMode: "per-slice"\` — each lane advances RED→GREEN→REFACTOR as soon as its \`dependsOn\` closes; the linter enforces RED-before-GREEN per slice. Projects with \`legacyContinuation: true\` or explicit \`tddCheckpointMode: "global-red"\` retain the v6.13.x global barrier.
119
119
 
120
120
  ## Headless mode (CI/automation only)
121
121
 
@@ -208,8 +208,8 @@ Progress the tracked flow only when one exists:
208
208
  2. If missing, guide the user to run \`npx cclaw-cli init\` and stop.
209
209
  3. If it is only a fresh init placeholder (\`completedStages: []\`, no passed gates, and no \`${RUNTIME_ROOT}/artifacts/00-idea.md\`), stop and ask for \`/cc <prompt>\` to start a tracked run. Do not silently create a brainstorm run.
210
210
  4. Check gates for \`currentStage\`.
211
- 5. **TDD (v6.13.1):** When \`currentStage\` is \`tdd\`, read \`${RUNTIME_ROOT}/artifacts/05-plan.md\` (managed \`## Parallel Execution Plan\` between \`parallel-exec-managed\` markers) and scan \`${RUNTIME_ROOT}/artifacts/wave-plans/wave-NN.md\` **before** asking which slice runs next. Merge sources in controller memory: Parallel Execution Plan first, wave files second; the same slice must not disagree across sources.
212
- 6. **Wave dispatch resume:** If a wave is partially closed (some members already past GREEN/REFACTOR), continue with the remaining members in parallel; never redo finished lanes.
211
+ 5. **TDD (v6.14.0 — stream-style):** When \`currentStage\` is \`tdd\`, read \`${RUNTIME_ROOT}/artifacts/05-plan.md\` (managed \`## Parallel Execution Plan\` between \`parallel-exec-managed\` markers) and scan \`${RUNTIME_ROOT}/artifacts/wave-plans/wave-NN.md\` **before** asking which slice runs next. Merge sources in controller memory: Parallel Execution Plan first, wave files second; the same slice must not disagree across sources.
212
+ 6. **Wave dispatch resume (per-lane stream):** If a wave is partially closed (some members already past GREEN/REFACTOR), continue with the remaining members in parallel as soon as their \`dependsOn\` closes — do **not** wait for a global RED-completion barrier in default \`tddCheckpointMode: "per-slice"\`. Never redo finished lanes. Integration-overseer fires only on cross-slice trigger (default \`integrationOverseerMode: "conditional"\`); skipped dispatches must record audit \`cclaw_integration_overseer_skipped\`.
213
213
  7. If incomplete → load current stage skill and execute.
214
214
  8. If complete → advance to next stage and execute.
215
215
  9. If flow is done → report completion.
@@ -183,6 +183,35 @@ export type DelegationEntry = {
183
183
  * v6.13.0 — integration branch merge status after deterministic fan-in.
184
184
  */
185
185
  integrationState?: "pending" | "applied" | "conflict" | "resolved" | "abandoned";
186
+ /**
187
+ * v6.14.0 — refactor outcome folded into `phase=green` events so a single
188
+ * row can close RED→GREEN→REFACTOR for the slice without a separate
189
+ * `phase=refactor` / `phase=refactor-deferred` lifecycle pass.
190
+ *
191
+ * - `mode: "inline"` — refactor pass ran inline as part of the GREEN
192
+ * delegation (rationale optional but recommended for traceability).
193
+ * - `mode: "deferred"` — refactor was intentionally deferred; rationale
194
+ * is required (carried in `rationale` and mirrored into
195
+ * `evidenceRefs[0]` so legacy linters that read evidence still work).
196
+ *
197
+ * `phase=refactor` and `phase=refactor-deferred` events remain valid
198
+ * for backward compatibility; the linter accepts either form for
199
+ * REFACTOR coverage.
200
+ *
201
+ * keep in sync with the inline copy in
202
+ * `src/content/hooks.ts::delegationRecordScript`.
203
+ */
204
+ refactorOutcome?: {
205
+ mode: "inline" | "deferred";
206
+ rationale?: string;
207
+ };
208
+ /**
209
+ * v6.14.0 — risk tier hint copied from the plan slice. Used by
210
+ * `integrationCheckRequired()` to decide whether the
211
+ * integration-overseer must run. `low` and `medium` are advisory;
212
+ * `high` always triggers the overseer. Optional on every row.
213
+ */
214
+ riskTier?: "low" | "medium" | "high";
186
215
  };
187
216
  export declare const DELEGATION_PHASES: readonly ["red", "green", "refactor", "refactor-deferred", "doc", "resolve-conflict"];
188
217
  export type DelegationPhase = (typeof DELEGATION_PHASES)[number];
@@ -395,6 +424,56 @@ export declare function selectReadySlices(units: ReadySliceUnit[], opts: SelectR
395
424
  * v6.13.1 — build scheduler rows from merged parallel wave definitions + plan units.
396
425
  */
397
426
  export declare function readySliceUnitsFromMergedWaves(mergedWaves: ParsedParallelWave[], planMarkdown: string, options?: ParseImplementationUnitParallelOptions): ReadySliceUnit[];
427
+ /**
428
+ * v6.14.0 — verdict from `integrationCheckRequired()`.
429
+ *
430
+ * `required: true` means the controller MUST dispatch
431
+ * `integration-overseer` before stage-complete; `reasons[]` lists the
432
+ * triggers that fired so the controller can quote them in artifacts.
433
+ *
434
+ * `required: false` means the integration check can be safely skipped
435
+ * (disjoint paths, no high-risk slices, no fan-in conflicts). Callers
436
+ * that skip dispatch should append a `cclaw_integration_overseer_skipped`
437
+ * audit row to `delegation-events.jsonl` so the run log stays honest
438
+ * about the decision.
439
+ */
440
+ export interface IntegrationCheckVerdict {
441
+ required: boolean;
442
+ reasons: string[];
443
+ }
444
+ /**
445
+ * v6.14.0 — heuristic helper deciding whether a multi-slice wave needs
446
+ * the `integration-overseer` dispatch.
447
+ *
448
+ * Triggers (any one):
449
+ * - **two or more closed slices share import boundaries** (heuristic:
450
+ * two slices declare a `claimedPaths` whose first 2 path segments
451
+ * match — same package/module directory);
452
+ * - any slice has `riskTier === "high"`;
453
+ * - any `cclaw_fanin_conflict` audit row exists in the supplied
454
+ * events list (regardless of slice).
455
+ *
456
+ * When none fire, the verdict is `{ required: false, reasons: ["disjoint-paths"] }`
457
+ * and the caller should record a `cclaw_integration_overseer_skipped`
458
+ * audit before bypassing the dispatch.
459
+ *
460
+ * Note on inputs: this function reads from the supplied delegation
461
+ * events list directly so callers can inject synthetic data in tests.
462
+ * Use `readDelegationEvents(projectRoot)` in production paths.
463
+ */
464
+ export declare function integrationCheckRequired(events: DelegationEvent[]): IntegrationCheckVerdict;
465
+ export declare function integrationCheckRequired(events: DelegationEvent[], fanInAudits: FanInAuditRecord[]): IntegrationCheckVerdict;
466
+ /**
467
+ * v6.14.0 — append a non-delegation audit event recording that the
468
+ * integration-overseer dispatch was skipped because
469
+ * `integrationCheckRequired()` returned `required: false`. Best-effort;
470
+ * never throws.
471
+ */
472
+ export declare function recordIntegrationOverseerSkipped(projectRoot: string, params: {
473
+ runId: string;
474
+ reasons: string[];
475
+ sliceIds: string[];
476
+ }): Promise<void>;
398
477
  /**
399
478
  * v6.13.1 — load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
400
479
  */
@@ -257,7 +257,22 @@ function isDelegationEntry(value) {
257
257
  o.integrationState === "applied" ||
258
258
  o.integrationState === "conflict" ||
259
259
  o.integrationState === "resolved" ||
260
- o.integrationState === "abandoned"));
260
+ o.integrationState === "abandoned") &&
261
+ (o.refactorOutcome === undefined || isRefactorOutcomeShape(o.refactorOutcome)) &&
262
+ (o.riskTier === undefined ||
263
+ o.riskTier === "low" ||
264
+ o.riskTier === "medium" ||
265
+ o.riskTier === "high"));
266
+ }
267
+ function isRefactorOutcomeShape(value) {
268
+ if (!value || typeof value !== "object" || Array.isArray(value))
269
+ return false;
270
+ const o = value;
271
+ if (o.mode !== "inline" && o.mode !== "deferred")
272
+ return false;
273
+ if (o.rationale !== undefined && typeof o.rationale !== "string")
274
+ return false;
275
+ return true;
261
276
  }
262
277
  function isDelegationDispatchSurface(value) {
263
278
  return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
@@ -372,7 +387,8 @@ const NON_DELEGATION_AUDIT_EVENTS = new Set([
372
387
  "cclaw_fanin_applied",
373
388
  "cclaw_fanin_conflict",
374
389
  "cclaw_fanin_resolved",
375
- "cclaw_fanin_abandoned"
390
+ "cclaw_fanin_abandoned",
391
+ "cclaw_integration_overseer_skipped"
376
392
  ]);
377
393
  function isAuditEventLine(parsed) {
378
394
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
@@ -771,6 +787,110 @@ export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, option
771
787
  }
772
788
  return out;
773
789
  }
790
+ export function integrationCheckRequired(events, fanInAudits) {
791
+ const reasons = [];
792
+ // Closed slices = ones whose phase=green or phase=refactor row is
793
+ // completed. We collect each unique sliceId's representative paths
794
+ // and risk tier so the heuristic looks at terminal state only.
795
+ const sliceState = new Map();
796
+ for (const evt of events) {
797
+ if (evt.stage !== "tdd")
798
+ continue;
799
+ if (typeof evt.sliceId !== "string" || evt.sliceId.length === 0)
800
+ continue;
801
+ if (evt.status !== "completed")
802
+ continue;
803
+ if (evt.phase !== "green" && evt.phase !== "refactor" && evt.phase !== "refactor-deferred") {
804
+ continue;
805
+ }
806
+ const existing = sliceState.get(evt.sliceId) ?? { sliceId: evt.sliceId };
807
+ if (Array.isArray(evt.claimedPaths) && evt.claimedPaths.length > 0) {
808
+ const merged = new Set(existing.claimedPaths ?? []);
809
+ for (const p of evt.claimedPaths)
810
+ merged.add(p);
811
+ existing.claimedPaths = [...merged];
812
+ }
813
+ if (evt.riskTier === "low" || evt.riskTier === "medium" || evt.riskTier === "high") {
814
+ // Highest-wins so the verdict is conservative.
815
+ const order = { low: 0, medium: 1, high: 2 };
816
+ const prev = existing.riskTier ?? "low";
817
+ if (order[evt.riskTier] >= order[prev]) {
818
+ existing.riskTier = evt.riskTier;
819
+ }
820
+ }
821
+ sliceState.set(evt.sliceId, existing);
822
+ }
823
+ const slices = [...sliceState.values()];
824
+ if (slices.some((s) => s.riskTier === "high")) {
825
+ reasons.push("high-risk-slice");
826
+ }
827
+ // Shared-directory heuristic — two distinct slices with overlapping
828
+ // first-2-segment directory prefixes count as shared boundary.
829
+ const sliceDirs = new Map();
830
+ for (const s of slices) {
831
+ const dirs = new Set();
832
+ for (const raw of s.claimedPaths ?? []) {
833
+ const segments = raw.split("/").filter((seg) => seg.length > 0);
834
+ if (segments.length === 0)
835
+ continue;
836
+ // For top-level files like `package.json`, fall back to the
837
+ // first segment so single-segment paths still count as a shared
838
+ // directory when two slices both claim the file.
839
+ const prefix = segments.slice(0, Math.max(1, Math.min(2, segments.length))).join("/");
840
+ dirs.add(prefix);
841
+ }
842
+ if (dirs.size > 0)
843
+ sliceDirs.set(s.sliceId, dirs);
844
+ }
845
+ let sharedFound = false;
846
+ const ids = [...sliceDirs.keys()];
847
+ outer: for (let i = 0; i < ids.length; i += 1) {
848
+ const a = sliceDirs.get(ids[i]);
849
+ for (let j = i + 1; j < ids.length; j += 1) {
850
+ const b = sliceDirs.get(ids[j]);
851
+ for (const dir of a) {
852
+ if (b.has(dir)) {
853
+ sharedFound = true;
854
+ break outer;
855
+ }
856
+ }
857
+ }
858
+ }
859
+ if (sharedFound)
860
+ reasons.push("shared-import-boundary");
861
+ // Fan-in conflict trigger — any `cclaw_fanin_conflict` in the supplied
862
+ // audits forces the overseer regardless of paths/risk.
863
+ if (Array.isArray(fanInAudits) && fanInAudits.some((a) => a.event === "cclaw_fanin_conflict")) {
864
+ reasons.push("fanin-conflict");
865
+ }
866
+ if (reasons.length > 0) {
867
+ return { required: true, reasons };
868
+ }
869
+ return { required: false, reasons: ["disjoint-paths"] };
870
+ }
871
+ /**
872
+ * v6.14.0 — append a non-delegation audit event recording that the
873
+ * integration-overseer dispatch was skipped because
874
+ * `integrationCheckRequired()` returned `required: false`. Best-effort;
875
+ * never throws.
876
+ */
877
+ export async function recordIntegrationOverseerSkipped(projectRoot, params) {
878
+ const eventsPath = delegationEventsPath(projectRoot);
879
+ const payload = {
880
+ event: "cclaw_integration_overseer_skipped",
881
+ runId: params.runId,
882
+ reasons: params.reasons,
883
+ sliceIds: params.sliceIds,
884
+ ts: new Date().toISOString()
885
+ };
886
+ try {
887
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true });
888
+ await fs.appendFile(eventsPath, `${JSON.stringify(payload)}\n`, "utf8");
889
+ }
890
+ catch {
891
+ // best-effort audit; never block stage advance.
892
+ }
893
+ }
774
894
  /**
775
895
  * v6.13.1 — load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
776
896
  */
@@ -151,12 +151,57 @@ export interface FlowState {
151
151
  * defaults scheduler parallelism to opt-in only for those units.
152
152
  */
153
153
  legacyContinuation?: boolean;
154
+ /**
155
+ * v6.14.0 — TDD wave checkpoint mode (stream-style parallel TDD).
156
+ *
157
+ * - `per-slice` — default for new projects. Each lane runs RED→GREEN as
158
+ * soon as its `dependsOn` closes; the linter enforces RED-before-GREEN
159
+ * per slice only (`tdd_slice_red_completed_before_green`). No global
160
+ * barrier between Phase A REDs and Phase B GREENs.
161
+ * - `global-red` — legacy v6.12/v6.13 behavior. ALL Phase A REDs in a
162
+ * wave must complete before ANY Phase B GREEN starts. Auto-applied
163
+ * for projects with `legacyContinuation: true` so hox-style runs
164
+ * continue to enforce the wave barrier.
165
+ *
166
+ * Omitted on legacy state files (treated as `"global-red"` for
167
+ * `legacyContinuation: true` and `"per-slice"` otherwise via
168
+ * `effectiveTddCheckpointMode`).
169
+ */
170
+ tddCheckpointMode?: "per-slice" | "global-red";
171
+ /**
172
+ * v6.14.0 — integration-overseer dispatch mode.
173
+ *
174
+ * - `conditional` — default for new projects. The controller calls
175
+ * `integrationCheckRequired(events)` after wave closeout; the
176
+ * integration-overseer is dispatched only when (a) two or more
177
+ * closed slices share import boundaries (heuristic: shared
178
+ * directory in `evidenceRefs`/`claimedPaths`), (b) any slice has
179
+ * `riskTier === "high"`, or (c) deterministic fan-in reported a
180
+ * `cclaw_fanin_conflict`. Otherwise the linter emits the audit
181
+ * row `cclaw_integration_overseer_skipped` and skips dispatch.
182
+ * - `always` — legacy v6.13 behavior. Run integration-overseer
183
+ * after every multi-slice wave regardless of trigger.
184
+ *
185
+ * Omitted on legacy state files (treated as `"always"`).
186
+ */
187
+ integrationOverseerMode?: "conditional" | "always";
154
188
  }
155
189
  /**
156
190
  * Effective worktree mode: legacy state files without the field keep
157
191
  * single-tree scheduling to avoid breaking existing runs on upgrade.
158
192
  */
159
193
  export declare function effectiveWorktreeExecutionMode(state: FlowState): "single-tree" | "worktree-first";
194
+ /**
195
+ * Effective v6.14 TDD checkpoint mode: legacy state files without the
196
+ * field default to `global-red` when `legacyContinuation: true` (hox)
197
+ * and `per-slice` otherwise. Explicit values always win.
198
+ */
199
+ export declare function effectiveTddCheckpointMode(state: FlowState): "per-slice" | "global-red";
200
+ /**
201
+ * Effective v6.14 integration-overseer mode: legacy state files without
202
+ * the field default to `always` (matches v6.13 behavior).
203
+ */
204
+ export declare function effectiveIntegrationOverseerMode(state: FlowState): "conditional" | "always";
160
205
  export interface StageInteractionHint {
161
206
  skipQuestions?: boolean;
162
207
  sourceStage?: FlowStage;
@@ -51,6 +51,24 @@ export function createInitialCloseoutState() {
51
51
  export function effectiveWorktreeExecutionMode(state) {
52
52
  return state.worktreeExecutionMode ?? "single-tree";
53
53
  }
54
+ /**
55
+ * Effective v6.14 TDD checkpoint mode: legacy state files without the
56
+ * field default to `global-red` when `legacyContinuation: true` (hox)
57
+ * and `per-slice` otherwise. Explicit values always win.
58
+ */
59
+ export function effectiveTddCheckpointMode(state) {
60
+ if (state.tddCheckpointMode === "per-slice" || state.tddCheckpointMode === "global-red") {
61
+ return state.tddCheckpointMode;
62
+ }
63
+ return state.legacyContinuation === true ? "global-red" : "per-slice";
64
+ }
65
+ /**
66
+ * Effective v6.14 integration-overseer mode: legacy state files without
67
+ * the field default to `always` (matches v6.13 behavior).
68
+ */
69
+ export function effectiveIntegrationOverseerMode(state) {
70
+ return state.integrationOverseerMode === "conditional" ? "conditional" : "always";
71
+ }
54
72
  export function isFlowTrack(value) {
55
73
  return typeof value === "string" && FLOW_TRACKS.includes(value);
56
74
  }
package/dist/install.js CHANGED
@@ -1003,6 +1003,90 @@ async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
1003
1003
  // Best-effort: corrupt/missing state is handled elsewhere on sync.
1004
1004
  }
1005
1005
  }
1006
+ /**
1007
+ * v6.14.0 — set stream-style defaults on `cclaw-cli sync` and print a
1008
+ * one-line hint when defaults change. Strategy:
1009
+ *
1010
+ * - When `legacyContinuation: true` and `tddCheckpointMode` is unset, force
1011
+ * `tddCheckpointMode: "global-red"` (preserves hox wave protocol).
1012
+ * - When `legacyContinuation: true` and `integrationOverseerMode` is unset,
1013
+ * force `integrationOverseerMode: "always"` (preserves v6.13 behavior).
1014
+ * - When `legacyContinuation` is NOT true (new / standard projects) and
1015
+ * neither field is set, default to `tddCheckpointMode: "per-slice"`,
1016
+ * `integrationOverseerMode: "conditional"`. Also default
1017
+ * `worktreeExecutionMode: "worktree-first"` if unset.
1018
+ *
1019
+ * Returns a one-line hint string (or `null` if nothing changed) so callers
1020
+ * can print it through the standard sync hint surface.
1021
+ */
1022
+ async function applyV614DefaultsIfNeeded(projectRoot) {
1023
+ // Defensive read — match `applyTddCutoverIfNeeded`'s pattern (raw +
1024
+ // JSON.parse) so corrupt state is left untouched for the downstream
1025
+ // fail-fast check in `materializeRuntime` (which expects to see the
1026
+ // CorruptFlowStateError surfaced via `ensureRunSystem`). Calling
1027
+ // `readFlowState` directly would quarantine the corrupt file and hide
1028
+ // the failure from the caller.
1029
+ const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
1030
+ let flowStateRaw;
1031
+ try {
1032
+ flowStateRaw = await fs.readFile(flowStatePath, "utf8");
1033
+ }
1034
+ catch {
1035
+ return null;
1036
+ }
1037
+ let parsed;
1038
+ try {
1039
+ parsed = JSON.parse(flowStateRaw);
1040
+ }
1041
+ catch {
1042
+ return null;
1043
+ }
1044
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1045
+ return null;
1046
+ }
1047
+ const obj = parsed;
1048
+ const updates = {};
1049
+ const summary = [];
1050
+ const tddCheckpointModeSet = obj.tddCheckpointMode === "per-slice" || obj.tddCheckpointMode === "global-red";
1051
+ const integrationOverseerModeSet = obj.integrationOverseerMode === "conditional" || obj.integrationOverseerMode === "always";
1052
+ const worktreeExecutionModeSet = obj.worktreeExecutionMode === "worktree-first" || obj.worktreeExecutionMode === "single-tree";
1053
+ const legacyContinuation = obj.legacyContinuation === true;
1054
+ if (legacyContinuation) {
1055
+ if (!tddCheckpointModeSet) {
1056
+ updates.tddCheckpointMode = "global-red";
1057
+ summary.push("tddCheckpointMode=global-red (legacyContinuation)");
1058
+ }
1059
+ if (!integrationOverseerModeSet) {
1060
+ updates.integrationOverseerMode = "always";
1061
+ summary.push("integrationOverseerMode=always (legacyContinuation)");
1062
+ }
1063
+ }
1064
+ else {
1065
+ if (!tddCheckpointModeSet) {
1066
+ updates.tddCheckpointMode = "per-slice";
1067
+ summary.push("tddCheckpointMode=per-slice");
1068
+ }
1069
+ if (!integrationOverseerModeSet) {
1070
+ updates.integrationOverseerMode = "conditional";
1071
+ summary.push("integrationOverseerMode=conditional");
1072
+ }
1073
+ if (!worktreeExecutionModeSet) {
1074
+ updates.worktreeExecutionMode = "worktree-first";
1075
+ summary.push("worktreeExecutionMode=worktree-first");
1076
+ }
1077
+ }
1078
+ if (summary.length === 0) {
1079
+ return null;
1080
+ }
1081
+ const merged = { ...obj, ...updates };
1082
+ try {
1083
+ await writeFileSafe(flowStatePath, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
1084
+ }
1085
+ catch {
1086
+ return null;
1087
+ }
1088
+ return `v6.14.0 stream-style defaults applied: ${summary.join(", ")}. To opt out, edit .cclaw/state/flow-state.json directly or pin the legacy mode (tddCheckpointMode="global-red", integrationOverseerMode="always").`;
1089
+ }
1006
1090
  async function cleanLegacyArtifacts(projectRoot) {
1007
1091
  for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
1008
1092
  await removeBestEffort(runtimePath(projectRoot, "skills", legacyFolder), true);
@@ -1178,6 +1262,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
1178
1262
  if (operation === "sync" || operation === "upgrade") {
1179
1263
  await applyTddCutoverIfNeeded(projectRoot);
1180
1264
  await applyPlanLegacyContinuationIfNeeded(projectRoot);
1265
+ const v614Hint = await applyV614DefaultsIfNeeded(projectRoot);
1266
+ if (v614Hint) {
1267
+ process.stdout.write(`cclaw: ${v614Hint}\n`);
1268
+ }
1181
1269
  }
1182
1270
  try {
1183
1271
  await ensureRunSystem(projectRoot, { createIfMissing: false });
@@ -473,6 +473,8 @@ function coerceFlowState(parsed) {
473
473
  const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
474
474
  const tddCutoverSliceId = coerceTddCutoverSliceId(parsed.tddCutoverSliceId);
475
475
  const worktreeExecutionMode = coerceWorktreeExecutionMode(parsed.worktreeExecutionMode);
476
+ const tddCheckpointMode = coerceTddCheckpointMode(parsed.tddCheckpointMode);
477
+ const integrationOverseerMode = coerceIntegrationOverseerMode(parsed.integrationOverseerMode);
476
478
  const legacyContinuation = typeof parsed.legacyContinuation === "boolean" ? parsed.legacyContinuation : undefined;
477
479
  const state = {
478
480
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
@@ -488,6 +490,8 @@ function coerceFlowState(parsed) {
488
490
  ...(completedStageMeta ? { completedStageMeta } : {}),
489
491
  ...(tddCutoverSliceId ? { tddCutoverSliceId } : {}),
490
492
  ...(worktreeExecutionMode !== undefined ? { worktreeExecutionMode } : {}),
493
+ ...(tddCheckpointMode !== undefined ? { tddCheckpointMode } : {}),
494
+ ...(integrationOverseerMode !== undefined ? { integrationOverseerMode } : {}),
491
495
  ...(legacyContinuation !== undefined ? { legacyContinuation } : {}),
492
496
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
493
497
  staleStages: sanitizeStaleStages(parsed.staleStages),
@@ -514,6 +518,16 @@ function coerceWorktreeExecutionMode(value) {
514
518
  return value;
515
519
  return undefined;
516
520
  }
521
+ function coerceTddCheckpointMode(value) {
522
+ if (value === "per-slice" || value === "global-red")
523
+ return value;
524
+ return undefined;
525
+ }
526
+ function coerceIntegrationOverseerMode(value) {
527
+ if (value === "conditional" || value === "always")
528
+ return value;
529
+ return undefined;
530
+ }
517
531
  export class CorruptFlowStateError extends Error {
518
532
  statePath;
519
533
  quarantinedPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.13.1",
3
+ "version": "6.14.0",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {