cclaw-cli 6.13.0 → 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 +53 -10
- package/dist/artifact-linter/tdd.js +315 -92
- package/dist/artifact-linter.js +10 -2
- package/dist/content/hooks.js +119 -3
- package/dist/content/skills.js +15 -12
- package/dist/content/stages/tdd.js +8 -8
- package/dist/content/start-command.js +6 -3
- package/dist/delegation.d.ts +88 -0
- package/dist/delegation.js +171 -3
- package/dist/flow-state.d.ts +45 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +115 -2
- package/dist/internal/plan-split-waves.d.ts +46 -0
- package/dist/internal/plan-split-waves.js +225 -6
- 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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DelegationEntry } from "../delegation.js";
|
|
1
|
+
import type { DelegationEntry, DelegationEvent } from "../delegation.js";
|
|
2
2
|
import { type LintFinding, type StageLintContext } from "./shared.js";
|
|
3
3
|
/**
|
|
4
4
|
* v6.11.0 — TDD stage linter.
|
|
@@ -53,19 +53,62 @@ interface RedCheckpointResult {
|
|
|
53
53
|
details: string;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* v6.
|
|
57
|
-
* requires
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
* v6.13.1 — detect single-slice dispatch when the merged wave plan
|
|
57
|
+
* requires parallel ready slice-implementer fan-out.
|
|
58
|
+
*/
|
|
59
|
+
export declare function evaluateWavePlanDispatchIgnored(params: {
|
|
60
|
+
artifactsDir: string;
|
|
61
|
+
planMarkdown: string;
|
|
62
|
+
runEvents: DelegationEvent[];
|
|
63
|
+
runId: string;
|
|
64
|
+
slices: Map<string, DelegationEntry[]>;
|
|
65
|
+
legacyContinuation: boolean;
|
|
66
|
+
}): Promise<LintFinding | null>;
|
|
67
|
+
/**
|
|
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).
|
|
64
84
|
*
|
|
65
85
|
* @param waveMembers Optional explicit wave manifest. Map key is wave
|
|
66
86
|
* name (e.g. `"W-01"`); value is the set of slice ids in that wave.
|
|
67
87
|
*/
|
|
68
|
-
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;
|
|
69
112
|
export declare function parseVerticalSliceCycle(body: string): ParsedSliceCycleResult;
|
|
70
113
|
interface VerificationLadderResult {
|
|
71
114
|
ok: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readDelegationLedger, readDelegationEvents } from "../delegation.js";
|
|
3
|
+
import { integrationCheckRequired, loadTddReadySlicePool, readDelegationLedger, readDelegationEvents, selectReadySlices } from "../delegation.js";
|
|
4
|
+
import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "../internal/plan-split-waves.js";
|
|
4
5
|
import { evaluateInvestigationTrace, sectionBodyByName } from "./shared.js";
|
|
5
6
|
const SLICE_SUMMARY_START = "<!-- auto-start: tdd-slice-summary -->";
|
|
6
7
|
const SLICE_SUMMARY_END = "<!-- auto-end: tdd-slice-summary -->";
|
|
@@ -26,8 +27,17 @@ const SLICES_INDEX_END = "<!-- auto-end: slices-index -->";
|
|
|
26
27
|
* via `## Slices Index`.
|
|
27
28
|
*/
|
|
28
29
|
export async function lintTddStage(ctx) {
|
|
29
|
-
const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode } = ctx;
|
|
30
|
+
const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode, legacyContinuation, tddCheckpointMode, integrationOverseerMode } = ctx;
|
|
30
31
|
void parsedFrontmatter;
|
|
32
|
+
const artifactsDir = path.dirname(absFile);
|
|
33
|
+
const planPath = path.join(artifactsDir, "05-plan.md");
|
|
34
|
+
let planRaw = "";
|
|
35
|
+
try {
|
|
36
|
+
planRaw = await fs.readFile(planPath, "utf8");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
planRaw = "";
|
|
40
|
+
}
|
|
31
41
|
evaluateInvestigationTrace(ctx, "Watched-RED Proof");
|
|
32
42
|
const delegationLedger = await readDelegationLedger(ctx.projectRoot);
|
|
33
43
|
const activeRunEntries = delegationLedger.entries.filter((entry) => entry.stage === "tdd" && entry.runId === delegationLedger.runId);
|
|
@@ -134,21 +144,27 @@ export async function lintTddStage(ctx) {
|
|
|
134
144
|
});
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
|
-
// v6.
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
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".
|
|
143
153
|
if (eventsActive) {
|
|
144
154
|
const docResult = evaluateSliceDocumenterCoverage(slicesByEvents);
|
|
145
155
|
if (docResult.missing.length > 0) {
|
|
156
|
+
const required = discoveryMode === "deep";
|
|
146
157
|
findings.push({
|
|
147
158
|
section: "tdd_slice_documenter_missing",
|
|
148
|
-
required
|
|
149
|
-
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.",
|
|
150
163
|
found: false,
|
|
151
|
-
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.")
|
|
152
168
|
});
|
|
153
169
|
}
|
|
154
170
|
}
|
|
@@ -169,23 +185,42 @@ export async function lintTddStage(ctx) {
|
|
|
169
185
|
});
|
|
170
186
|
}
|
|
171
187
|
}
|
|
172
|
-
// v6.
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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).
|
|
178
199
|
if (eventsActive) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
189
224
|
}
|
|
190
225
|
}
|
|
191
226
|
// v6.12.0 Phase L — advisory backslide detection. When a cutover is
|
|
@@ -203,52 +238,69 @@ export async function lintTddStage(ctx) {
|
|
|
203
238
|
}
|
|
204
239
|
const { events: jsonlEvents, fanInAudits } = await readDelegationEvents(projectRoot);
|
|
205
240
|
const runEvents = jsonlEvents.filter((e) => e.runId === delegationLedger.runId);
|
|
241
|
+
if (eventsActive && planRaw.length > 0) {
|
|
242
|
+
const ignoredWave = await evaluateWavePlanDispatchIgnored({
|
|
243
|
+
artifactsDir,
|
|
244
|
+
planMarkdown: planRaw,
|
|
245
|
+
runEvents,
|
|
246
|
+
runId: delegationLedger.runId,
|
|
247
|
+
slices: slicesByEvents,
|
|
248
|
+
legacyContinuation
|
|
249
|
+
});
|
|
250
|
+
if (ignoredWave) {
|
|
251
|
+
findings.push(ignoredWave);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
206
254
|
if (eventsActive && worktreeExecutionMode === "worktree-first") {
|
|
207
255
|
const terminalPhases = new Set([
|
|
208
|
-
"green",
|
|
209
256
|
"refactor",
|
|
210
257
|
"refactor-deferred",
|
|
211
258
|
"resolve-conflict"
|
|
212
259
|
]);
|
|
213
|
-
const
|
|
260
|
+
const missingGreenMeta = new Set();
|
|
214
261
|
for (const ev of runEvents) {
|
|
215
262
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
216
263
|
continue;
|
|
217
|
-
if (ev.status !== "completed"
|
|
264
|
+
if (ev.status !== "completed" || ev.phase !== "green")
|
|
218
265
|
continue;
|
|
219
|
-
if (
|
|
266
|
+
if (typeof ev.sliceId !== "string")
|
|
220
267
|
continue;
|
|
221
268
|
const tok = ev.claimToken?.trim() ?? "";
|
|
222
|
-
|
|
223
|
-
|
|
269
|
+
const lane = ev.ownerLaneId?.trim() ?? "";
|
|
270
|
+
const lease = ev.leasedUntil?.trim() ?? "";
|
|
271
|
+
if (tok.length === 0 || lane.length === 0 || lease.length === 0) {
|
|
272
|
+
missingGreenMeta.add(ev.sliceId);
|
|
224
273
|
}
|
|
225
274
|
}
|
|
226
|
-
if (
|
|
275
|
+
if (missingGreenMeta.size > 0) {
|
|
227
276
|
findings.push({
|
|
228
|
-
section: "
|
|
277
|
+
section: "tdd_slice_lane_metadata_missing",
|
|
229
278
|
required: true,
|
|
230
|
-
rule: "Worktree-first:
|
|
279
|
+
rule: "Worktree-first: every completed slice-implementer phase=green row must record claimToken, ownerLaneId (--lane-id), and leasedUntil (--lease-until).",
|
|
231
280
|
found: false,
|
|
232
|
-
details: `Slices missing
|
|
281
|
+
details: `Slices missing one or more lane fields on GREEN: ${[...missingGreenMeta].sort().join(", ")}. Remediation: include --claim-token, --lane-id, and --lease-until on every slice-implementer --phase green delegation-record write (schedule through completion); the hook fails fast with dispatch_lane_metadata_missing when they are omitted.`
|
|
233
282
|
});
|
|
234
283
|
}
|
|
235
|
-
const
|
|
284
|
+
const missingClaim = new Set();
|
|
236
285
|
for (const ev of runEvents) {
|
|
237
286
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
238
287
|
continue;
|
|
239
|
-
if (ev.status !== "completed"
|
|
288
|
+
if (ev.status !== "completed" && ev.status !== "failed")
|
|
240
289
|
continue;
|
|
241
|
-
if (!ev.
|
|
242
|
-
|
|
290
|
+
if (!ev.phase || !terminalPhases.has(ev.phase))
|
|
291
|
+
continue;
|
|
292
|
+
const tok = ev.claimToken?.trim() ?? "";
|
|
293
|
+
if (tok.length === 0 && typeof ev.sliceId === "string") {
|
|
294
|
+
missingClaim.add(ev.sliceId);
|
|
243
295
|
}
|
|
244
296
|
}
|
|
245
|
-
if (
|
|
297
|
+
if (missingClaim.size > 0) {
|
|
246
298
|
findings.push({
|
|
247
|
-
section: "
|
|
299
|
+
section: "tdd_slice_claim_token_missing",
|
|
248
300
|
required: true,
|
|
249
|
-
rule: "Worktree-first:
|
|
301
|
+
rule: "Worktree-first: terminal slice-implementer rows (refactor / refactor-deferred / resolve-conflict) must echo --claim-token. Remediation: pass the same --claim-token used on the scheduled row for every completed/failed terminal phase.",
|
|
250
302
|
found: false,
|
|
251
|
-
details: `Slices missing
|
|
303
|
+
details: `Slices missing claim token on non-GREEN terminal rows: ${[...missingClaim].join(", ")}.`
|
|
252
304
|
});
|
|
253
305
|
}
|
|
254
306
|
const conflictSlices = [
|
|
@@ -338,7 +390,6 @@ export async function lintTddStage(ctx) {
|
|
|
338
390
|
const completedSliceImplementers = activeRunEntries.filter((entry) => entry.agent === "slice-implementer" && entry.status === "completed");
|
|
339
391
|
const fanOutDetected = completedSliceImplementers.length > 1;
|
|
340
392
|
if (fanOutDetected) {
|
|
341
|
-
const artifactsDir = path.dirname(absFile);
|
|
342
393
|
const cohesionContractMarkdownPath = path.join(artifactsDir, "cohesion-contract.md");
|
|
343
394
|
const cohesionContractJsonPath = path.join(artifactsDir, "cohesion-contract.json");
|
|
344
395
|
let cohesionContractFound = true;
|
|
@@ -392,15 +443,41 @@ export async function lintTddStage(ctx) {
|
|
|
392
443
|
const overseerStatusInArtifact = /\bintegration-overseer\b[\s\S]{0,200}\b(?:PASS_WITH_GAPS|PASS)\b/iu.test(raw);
|
|
393
444
|
const integrationOverseerFound = completedOverseerRows.length > 0 &&
|
|
394
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
|
+
}
|
|
395
468
|
findings.push({
|
|
396
469
|
section: "tdd.integration_overseer_missing",
|
|
397
|
-
required:
|
|
398
|
-
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.",
|
|
399
474
|
found: integrationOverseerFound,
|
|
400
475
|
details: integrationOverseerFound
|
|
401
476
|
? "integration-overseer completion recorded with PASS/PASS_WITH_GAPS evidence."
|
|
402
477
|
: completedOverseerRows.length === 0
|
|
403
|
-
?
|
|
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."
|
|
404
481
|
: "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
|
|
405
482
|
});
|
|
406
483
|
}
|
|
@@ -418,7 +495,6 @@ export async function lintTddStage(ctx) {
|
|
|
418
495
|
// Phase S — sharded slice files. Validate per-slice file presence
|
|
419
496
|
// and required headings. `tdd-slices/` is optional; missing folder
|
|
420
497
|
// simply means main-only mode (legacy fallback).
|
|
421
|
-
const artifactsDir = path.dirname(absFile);
|
|
422
498
|
const slicesDir = path.join(artifactsDir, "tdd-slices");
|
|
423
499
|
const sliceFiles = await listSliceFiles(slicesDir);
|
|
424
500
|
for (const sliceFile of sliceFiles) {
|
|
@@ -607,19 +683,43 @@ export function evaluateEventsSliceCycle(slices) {
|
|
|
607
683
|
});
|
|
608
684
|
continue;
|
|
609
685
|
}
|
|
610
|
-
|
|
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) {
|
|
611
694
|
errors.push(`${sliceId}: phase=refactor or phase=refactor-deferred event missing.`);
|
|
612
695
|
findings.push({
|
|
613
696
|
section: `tdd_slice_refactor_missing:${sliceId}`,
|
|
614
697
|
required: true,
|
|
615
|
-
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).",
|
|
699
|
+
found: false,
|
|
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).",
|
|
616
714
|
found: false,
|
|
617
|
-
details: `${sliceId}:
|
|
715
|
+
details: `${sliceId}: phase=green refactorOutcome.mode=deferred recorded without rationale.`
|
|
618
716
|
});
|
|
619
717
|
continue;
|
|
620
718
|
}
|
|
621
719
|
const deferred = refactors.find((entry) => entry.phase === "refactor-deferred");
|
|
622
|
-
if (
|
|
720
|
+
if (refactors.length > 0 &&
|
|
721
|
+
deferred &&
|
|
722
|
+
refactors.every((entry) => entry.phase === "refactor-deferred")) {
|
|
623
723
|
const refs = Array.isArray(deferred.evidenceRefs) ? deferred.evidenceRefs : [];
|
|
624
724
|
const hasRationale = refs.some((ref) => typeof ref === "string" && ref.trim().length > 0);
|
|
625
725
|
if (!hasRationale) {
|
|
@@ -694,20 +794,122 @@ export function evaluateSliceImplementerCoverage(slices) {
|
|
|
694
794
|
}
|
|
695
795
|
return { missing };
|
|
696
796
|
}
|
|
797
|
+
async function readMergedWaveManifestForCheckpoint(artifactsDir, planMarkdown) {
|
|
798
|
+
try {
|
|
799
|
+
const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
|
|
800
|
+
if (merged.length === 0)
|
|
801
|
+
return null;
|
|
802
|
+
const map = new Map();
|
|
803
|
+
for (const w of merged) {
|
|
804
|
+
map.set(w.waveId, new Set(w.members.map((m) => m.sliceId)));
|
|
805
|
+
}
|
|
806
|
+
return map.size > 0 ? map : null;
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function sliceRefactorTerminal(sliceId, slices) {
|
|
813
|
+
const rows = slices.get(sliceId);
|
|
814
|
+
if (!rows)
|
|
815
|
+
return false;
|
|
816
|
+
return rows.some((e) => e.agent === "slice-implementer" &&
|
|
817
|
+
(e.phase === "refactor" || e.phase === "refactor-deferred") &&
|
|
818
|
+
(e.status === "completed" || e.status === "failed"));
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* v6.13.1 — detect single-slice dispatch when the merged wave plan
|
|
822
|
+
* requires parallel ready slice-implementer fan-out.
|
|
823
|
+
*/
|
|
824
|
+
export async function evaluateWavePlanDispatchIgnored(params) {
|
|
825
|
+
let merged;
|
|
826
|
+
try {
|
|
827
|
+
merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(params.planMarkdown), await parseWavePlanDirectory(params.artifactsDir));
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
if (merged.length === 0)
|
|
833
|
+
return null;
|
|
834
|
+
let pool;
|
|
835
|
+
try {
|
|
836
|
+
pool = await loadTddReadySlicePool(params.planMarkdown, params.artifactsDir, {
|
|
837
|
+
legacyParallelDefaultSerial: params.legacyContinuation
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
if (pool.length === 0)
|
|
844
|
+
return null;
|
|
845
|
+
const completedUnitIds = new Set();
|
|
846
|
+
for (const u of pool) {
|
|
847
|
+
if (sliceRefactorTerminal(u.sliceId, params.slices)) {
|
|
848
|
+
completedUnitIds.add(u.unitId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const scoped = params.runEvents.filter((e) => e.runId === params.runId);
|
|
852
|
+
const tail = scoped.slice(-20);
|
|
853
|
+
const implInTail = new Set();
|
|
854
|
+
for (const e of tail) {
|
|
855
|
+
if (e.agent === "slice-implementer" && typeof e.sliceId === "string" && e.sliceId.length > 0) {
|
|
856
|
+
implInTail.add(e.sliceId);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (implInTail.size !== 1)
|
|
860
|
+
return null;
|
|
861
|
+
for (const wave of merged) {
|
|
862
|
+
const waveSliceSet = new Set(wave.members.map((m) => m.sliceId));
|
|
863
|
+
const wavePool = pool.filter((u) => waveSliceSet.has(u.sliceId));
|
|
864
|
+
if (wavePool.length < 2)
|
|
865
|
+
continue;
|
|
866
|
+
const waveIncomplete = wave.members.some((m) => !sliceRefactorTerminal(m.sliceId, params.slices));
|
|
867
|
+
if (!waveIncomplete)
|
|
868
|
+
continue;
|
|
869
|
+
const ready = selectReadySlices(wavePool, {
|
|
870
|
+
cap: Math.max(32, wavePool.length),
|
|
871
|
+
completedUnitIds,
|
|
872
|
+
activePathHolders: [],
|
|
873
|
+
legacyContinuation: params.legacyContinuation
|
|
874
|
+
});
|
|
875
|
+
if (ready.length < 2)
|
|
876
|
+
continue;
|
|
877
|
+
const only = [...implInTail][0];
|
|
878
|
+
const missed = ready.map((r) => r.sliceId).filter((s) => s !== only);
|
|
879
|
+
if (missed.length === 0)
|
|
880
|
+
continue;
|
|
881
|
+
return {
|
|
882
|
+
section: "tdd_wave_plan_ignored",
|
|
883
|
+
required: true,
|
|
884
|
+
rule: "When the Parallel Execution Plan (or wave-plans/) defines an open wave with two or more ready parallelizable slices, the controller must fan out slice-implementer work for each ready slice instead of serializing to one slice only.",
|
|
885
|
+
found: false,
|
|
886
|
+
details: `Wave ${wave.waveId}: scheduler-ready members ${ready.map((r) => r.sliceId).join(", ")}; last 20 delegation events show slice-implementer only for ${only}. Missed parallel dispatch: ${missed.join(", ")}. Remediation: load \`05-plan.md\` (Parallel Execution Plan) and \`wave-plans/\` before routing, launch the wave (AskQuestion only when waveCount>=2 and single-slice is a real alternative), then dispatch GREEN+DOC for every ready slice with mandatory worktree-first flags on GREEN.`
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
697
891
|
/**
|
|
698
|
-
* v6.12.0 Phase W — RED checkpoint enforcement.
|
|
699
|
-
* requires ALL Phase A REDs to land before ANY Phase B
|
|
700
|
-
* The rule is enforced on a per-wave basis, where a wave is
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
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).
|
|
706
908
|
*
|
|
707
909
|
* @param waveMembers Optional explicit wave manifest. Map key is wave
|
|
708
910
|
* name (e.g. `"W-01"`); value is the set of slice ids in that wave.
|
|
709
911
|
*/
|
|
710
|
-
export function
|
|
912
|
+
export function evaluateGlobalRedCheckpoint(slices, waveMembers = null) {
|
|
711
913
|
const events = [];
|
|
712
914
|
for (const [sliceId, rows] of slices.entries()) {
|
|
713
915
|
for (const entry of rows) {
|
|
@@ -784,40 +986,61 @@ export function evaluateRedCheckpoint(slices, waveMembers = null) {
|
|
|
784
986
|
};
|
|
785
987
|
}
|
|
786
988
|
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
* empty/unparseable.
|
|
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.
|
|
792
993
|
*/
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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)
|
|
806
1016
|
continue;
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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)
|
|
813
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})`);
|
|
814
1031
|
}
|
|
815
|
-
const ids = extractSliceIdsFromBody(body);
|
|
816
|
-
if (ids.length === 0)
|
|
817
|
-
continue;
|
|
818
|
-
waves.set(`W-${match[1]}`, new Set(ids));
|
|
819
1032
|
}
|
|
820
|
-
|
|
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
|
+
};
|
|
821
1044
|
}
|
|
822
1045
|
const LEGACY_PER_SLICE_SECTIONS = [
|
|
823
1046
|
"Test Discovery",
|