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