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.
- package/dist/artifact-linter/shared.d.ts +15 -0
- package/dist/artifact-linter/tdd.d.ts +40 -10
- package/dist/artifact-linter/tdd.js +183 -44
- package/dist/artifact-linter.js +10 -2
- package/dist/content/hooks.js +79 -3
- package/dist/content/stages/tdd.js +4 -4
- package/dist/content/start-command.js +3 -3
- package/dist/delegation.d.ts +79 -0
- package/dist/delegation.js +122 -2
- package/dist/flow-state.d.ts +45 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +88 -0
- package/dist/run-persistence.js +14 -0
- package/package.json +1 -1
|
@@ -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.
|
|
69
|
-
* requires ALL Phase A REDs to land before ANY Phase B
|
|
70
|
-
* The rule is enforced on a per-wave basis, where a wave is
|
|
71
|
-
* the managed `## Parallel Execution Plan` block in
|
|
72
|
-
* `<artifacts-dir>/wave-plans/wave-NN.md` files. When
|
|
73
|
-
* exists, the linter falls back to a conservative
|
|
74
|
-
* wave is a contiguous run of `phase=red` events
|
|
75
|
-
* events between them; the rule fires only when the
|
|
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
|
|
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.
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
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
|
|
159
|
-
rule:
|
|
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(", ")}.
|
|
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.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
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 RED→GREEN 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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
424
|
-
rule:
|
|
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
|
-
?
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
818
|
-
* requires ALL Phase A REDs to land before ANY Phase B
|
|
819
|
-
* The rule is enforced on a per-wave basis, where a wave is
|
|
820
|
-
* the managed `## Parallel Execution Plan` block in
|
|
821
|
-
* `<artifacts-dir>/wave-plans/wave-NN.md` files. When
|
|
822
|
-
* exists, the linter falls back to a conservative
|
|
823
|
-
* wave is a contiguous run of `phase=red` events
|
|
824
|
-
* events between them; the rule fires only when the
|
|
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
|
|
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",
|
package/dist/artifact-linter.js
CHANGED
|
@@ -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":
|
package/dist/content/hooks.js
CHANGED
|
@@ -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
|
|
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
|
-
"**
|
|
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
|
|
53
|
-
"DOC (
|
|
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
|
|
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.
|
|
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.
|
|
212
|
-
6. **Wave dispatch resume:** If a wave is partially closed (some members already past GREEN/REFACTOR), continue with the remaining members in parallel
|
|
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.
|
package/dist/delegation.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/delegation.js
CHANGED
|
@@ -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
|
*/
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-state.js
CHANGED
|
@@ -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 });
|
package/dist/run-persistence.js
CHANGED
|
@@ -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;
|