cclaw-cli 6.14.1 → 6.14.3
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.js +237 -8
- package/dist/artifact-linter.js +9 -1
- package/dist/content/core-agents.js +2 -2
- package/dist/content/hooks.js +190 -0
- package/dist/content/stages/tdd.js +3 -2
- package/dist/flow-state.d.ts +46 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +175 -14
- package/dist/internal/advance-stage.js +21 -3
- package/dist/internal/cohesion-contract-stub.d.ts +29 -0
- package/dist/internal/cohesion-contract-stub.js +166 -0
- package/dist/internal/set-checkpoint-mode.d.ts +16 -0
- package/dist/internal/set-checkpoint-mode.js +72 -0
- package/dist/internal/set-integration-overseer-mode.d.ts +14 -0
- package/dist/internal/set-integration-overseer-mode.js +69 -0
- package/dist/internal/wave-status.d.ts +51 -0
- package/dist/internal/wave-status.js +285 -0
- package/dist/run-persistence.js +20 -0
- package/package.json +1 -1
|
@@ -654,4 +654,19 @@ export interface StageLintContext {
|
|
|
654
654
|
* wave.
|
|
655
655
|
*/
|
|
656
656
|
integrationOverseerMode: "conditional" | "always";
|
|
657
|
+
/**
|
|
658
|
+
* v6.14.2 — historical cutover marker (`flow-state.json::tddCutoverSliceId`).
|
|
659
|
+
* Empty string when not set. Used by the `tdd_cutover_misread_warning`
|
|
660
|
+
* advisory rule to detect controllers that mistake the historical
|
|
661
|
+
* marker for an active-slice pointer.
|
|
662
|
+
*/
|
|
663
|
+
tddCutoverSliceId: string;
|
|
664
|
+
/**
|
|
665
|
+
* v6.14.2 — worktree-first boundary
|
|
666
|
+
* (`flow-state.json::tddWorktreeCutoverSliceId`). Empty string when
|
|
667
|
+
* not set. Linters that fire on closed worktree slices use this
|
|
668
|
+
* boundary (with a fallback to `tddCutoverSliceId`) to exempt
|
|
669
|
+
* pre-flip closed slices on `legacyContinuation: true` projects.
|
|
670
|
+
*/
|
|
671
|
+
tddWorktreeCutoverSliceId: string;
|
|
657
672
|
}
|
|
@@ -27,8 +27,17 @@ 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, tddCheckpointMode, integrationOverseerMode } = ctx;
|
|
30
|
+
const { projectRoot, discoveryMode, raw, absFile, sections, findings, parsedFrontmatter, worktreeExecutionMode, legacyContinuation, tddCheckpointMode, integrationOverseerMode, tddCutoverSliceId, tddWorktreeCutoverSliceId } = ctx;
|
|
31
31
|
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;
|
|
32
41
|
const artifactsDir = path.dirname(absFile);
|
|
33
42
|
const planPath = path.join(artifactsDir, "05-plan.md");
|
|
34
43
|
let planRaw = "";
|
|
@@ -236,6 +245,25 @@ export async function lintTddStage(ctx) {
|
|
|
236
245
|
if (cutoverFinding) {
|
|
237
246
|
findings.push(cutoverFinding);
|
|
238
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);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
239
267
|
const { events: jsonlEvents, fanInAudits } = await readDelegationEvents(projectRoot);
|
|
240
268
|
const runEvents = jsonlEvents.filter((e) => e.runId === delegationLedger.runId);
|
|
241
269
|
if (eventsActive && planRaw.length > 0) {
|
|
@@ -257,7 +285,37 @@ export async function lintTddStage(ctx) {
|
|
|
257
285
|
"refactor-deferred",
|
|
258
286
|
"resolve-conflict"
|
|
259
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
|
+
};
|
|
260
317
|
const missingGreenMeta = new Set();
|
|
318
|
+
const exemptedGreenMeta = new Set();
|
|
261
319
|
for (const ev of runEvents) {
|
|
262
320
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
263
321
|
continue;
|
|
@@ -269,7 +327,12 @@ export async function lintTddStage(ctx) {
|
|
|
269
327
|
const lane = ev.ownerLaneId?.trim() ?? "";
|
|
270
328
|
const lease = ev.leasedUntil?.trim() ?? "";
|
|
271
329
|
if (tok.length === 0 || lane.length === 0 || lease.length === 0) {
|
|
272
|
-
|
|
330
|
+
if (isExemptLegacySlice(ev.sliceId)) {
|
|
331
|
+
exemptedGreenMeta.add(ev.sliceId);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
missingGreenMeta.add(ev.sliceId);
|
|
335
|
+
}
|
|
273
336
|
}
|
|
274
337
|
}
|
|
275
338
|
if (missingGreenMeta.size > 0) {
|
|
@@ -281,7 +344,17 @@ export async function lintTddStage(ctx) {
|
|
|
281
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.`
|
|
282
345
|
});
|
|
283
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
|
+
}
|
|
284
356
|
const missingClaim = new Set();
|
|
357
|
+
const exemptedClaim = new Set();
|
|
285
358
|
for (const ev of runEvents) {
|
|
286
359
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
287
360
|
continue;
|
|
@@ -291,7 +364,12 @@ export async function lintTddStage(ctx) {
|
|
|
291
364
|
continue;
|
|
292
365
|
const tok = ev.claimToken?.trim() ?? "";
|
|
293
366
|
if (tok.length === 0 && typeof ev.sliceId === "string") {
|
|
294
|
-
|
|
367
|
+
if (isExemptLegacySlice(ev.sliceId)) {
|
|
368
|
+
exemptedClaim.add(ev.sliceId);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
missingClaim.add(ev.sliceId);
|
|
372
|
+
}
|
|
295
373
|
}
|
|
296
374
|
}
|
|
297
375
|
if (missingClaim.size > 0) {
|
|
@@ -303,6 +381,15 @@ export async function lintTddStage(ctx) {
|
|
|
303
381
|
details: `Slices missing claim token on non-GREEN terminal rows: ${[...missingClaim].join(", ")}.`
|
|
304
382
|
});
|
|
305
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
|
+
}
|
|
306
393
|
const conflictSlices = [
|
|
307
394
|
...new Set([
|
|
308
395
|
...runEvents
|
|
@@ -327,6 +414,12 @@ export async function lintTddStage(ctx) {
|
|
|
327
414
|
}
|
|
328
415
|
const now = Date.now();
|
|
329
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);
|
|
330
423
|
for (const ev of runEvents) {
|
|
331
424
|
if (typeof ev.leasedUntil !== "string")
|
|
332
425
|
continue;
|
|
@@ -335,8 +428,18 @@ export async function lintTddStage(ctx) {
|
|
|
335
428
|
continue;
|
|
336
429
|
if (ev.leaseState === "reclaimed" || ev.leaseState === "released")
|
|
337
430
|
continue;
|
|
338
|
-
if (typeof ev.sliceId
|
|
339
|
-
|
|
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);
|
|
340
443
|
}
|
|
341
444
|
if (leaseStale.size > 0) {
|
|
342
445
|
findings.push({
|
|
@@ -347,6 +450,15 @@ export async function lintTddStage(ctx) {
|
|
|
347
450
|
details: `Expired leases not reclaimed for slice(s): ${[...leaseStale].join(", ")}.`
|
|
348
451
|
});
|
|
349
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
|
+
}
|
|
350
462
|
}
|
|
351
463
|
const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
|
|
352
464
|
if (assertionBody !== null) {
|
|
@@ -426,14 +538,27 @@ export async function lintTddStage(ctx) {
|
|
|
426
538
|
cohesionContractFound = false;
|
|
427
539
|
cohesionErrors.push("cohesion-contract.json is missing or invalid JSON.");
|
|
428
540
|
}
|
|
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.";
|
|
429
552
|
findings.push({
|
|
430
553
|
section: "tdd.cohesion_contract_missing",
|
|
431
|
-
required:
|
|
432
|
-
rule:
|
|
554
|
+
required: cohesionRequired,
|
|
555
|
+
rule: cohesionRequired
|
|
556
|
+
? "When delegation ledger has >1 completed slice-implementer rows for active TDD run, require `.cclaw/artifacts/cohesion-contract.md` and parseable `.cclaw/artifacts/cohesion-contract.json` sidecar."
|
|
557
|
+
: "v6.14.2 advisory under legacyContinuation: cohesion contract is recommended, not required. Use `cclaw-cli internal cohesion-contract --stub` to write a baseline.",
|
|
433
558
|
found: cohesionContractFound,
|
|
434
559
|
details: cohesionContractFound
|
|
435
560
|
? `Fan-out detected (${completedSliceImplementers.length} completed slice-implementer rows); cohesion contract markdown+JSON sidecar are present and parseable.`
|
|
436
|
-
:
|
|
561
|
+
: advisoryNote
|
|
437
562
|
});
|
|
438
563
|
const completedOverseerRows = activeRunEntries.filter((entry) => entry.agent === "integration-overseer" && entry.status === "completed");
|
|
439
564
|
const overseerStatusInEvidence = completedOverseerRows.some((entry) => {
|
|
@@ -1213,6 +1338,110 @@ function pickEventTs(rows) {
|
|
|
1213
1338
|
}
|
|
1214
1339
|
return undefined;
|
|
1215
1340
|
}
|
|
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
|
+
}
|
|
1216
1445
|
export function parseVerticalSliceCycle(body) {
|
|
1217
1446
|
const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
|
|
1218
1447
|
if (tableLines.length < 3) {
|
package/dist/artifact-linter.js
CHANGED
|
@@ -126,6 +126,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
126
126
|
let worktreeExecutionMode = "single-tree";
|
|
127
127
|
let tddCheckpointMode = "per-slice";
|
|
128
128
|
let integrationOverseerMode = "always";
|
|
129
|
+
let tddCutoverSliceId = "";
|
|
130
|
+
let tddWorktreeCutoverSliceId = "";
|
|
129
131
|
try {
|
|
130
132
|
const flowState = await readFlowState(projectRoot);
|
|
131
133
|
const hint = flowState.interactionHints?.[stage];
|
|
@@ -140,6 +142,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
140
142
|
worktreeExecutionMode = effectiveWorktreeExecutionMode(flowState);
|
|
141
143
|
tddCheckpointMode = effectiveTddCheckpointMode(flowState);
|
|
142
144
|
integrationOverseerMode = effectiveIntegrationOverseerMode(flowState);
|
|
145
|
+
tddCutoverSliceId = flowState.tddCutoverSliceId ?? "";
|
|
146
|
+
tddWorktreeCutoverSliceId = flowState.tddWorktreeCutoverSliceId ?? "";
|
|
143
147
|
}
|
|
144
148
|
catch {
|
|
145
149
|
activeStageFlags = [];
|
|
@@ -152,6 +156,8 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
152
156
|
worktreeExecutionMode = "single-tree";
|
|
153
157
|
tddCheckpointMode = "per-slice";
|
|
154
158
|
integrationOverseerMode = "always";
|
|
159
|
+
tddCutoverSliceId = "";
|
|
160
|
+
tddWorktreeCutoverSliceId = "";
|
|
155
161
|
}
|
|
156
162
|
for (const extra of options.extraStageFlags ?? []) {
|
|
157
163
|
if (typeof extra === "string" && extra.length > 0 && !activeStageFlags.includes(extra)) {
|
|
@@ -291,7 +297,9 @@ export async function lintArtifact(projectRoot, stage, track = "standard", optio
|
|
|
291
297
|
legacyContinuation,
|
|
292
298
|
worktreeExecutionMode,
|
|
293
299
|
tddCheckpointMode,
|
|
294
|
-
integrationOverseerMode
|
|
300
|
+
integrationOverseerMode,
|
|
301
|
+
tddCutoverSliceId,
|
|
302
|
+
tddWorktreeCutoverSliceId
|
|
295
303
|
};
|
|
296
304
|
switch (stage) {
|
|
297
305
|
case "brainstorm":
|
|
@@ -68,7 +68,7 @@ function tddWorkerSelfRecordContract(agentName) {
|
|
|
68
68
|
const laneFlags = isImplementer
|
|
69
69
|
? " [--claim-token=<t>] [--lane-id=<lane>] [--lease-until=<iso>]"
|
|
70
70
|
: "";
|
|
71
|
-
return `## TDD Worker Self-Record Contract (v6.14.
|
|
71
|
+
return `## TDD Worker Self-Record Contract (v6.14.2)
|
|
72
72
|
|
|
73
73
|
You are a TDD worker dispatched via \`Task\`. The parent already wrote your \`scheduled\` and \`launched\` ledger rows BEFORE invoking you. **Your responsibility is to self-record \`acknowledged\` on entry and \`completed\` on exit** by invoking \`.cclaw/hooks/delegation-record.mjs\` directly. Do NOT skip these — the controller depends on them, the linter validates them, and back-fill via \`--repair\` is reserved for recovery only.
|
|
74
74
|
|
|
@@ -100,7 +100,7 @@ node .cclaw/hooks/delegation-record.mjs \\
|
|
|
100
100
|
--json
|
|
101
101
|
\`\`\`
|
|
102
102
|
|
|
103
|
-
Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. \`--ack-ts\` and \`--completed-ts\` must be monotonic on the span (\`startTs ≤ launchedTs ≤ ackTs ≤ completedTs\`); the helper rejects out-of-order writes with \`delegation_timestamp_non_monotonic\`. If the helper rejects with \`dispatch_active_span_collision\` against a stale span, surface the conflicting \`spanId\` to the parent — do NOT silently retry with \`--allow-parallel\`.`;
|
|
103
|
+
Reuse the same \`<spanId>\` and \`<dispatchId>\` across both rows. **v6.14.2 evidence-freshness contract** (slice-implementer GREEN only): the FIRST \`--evidence-ref\` MUST (1) reference the same test the matching \`phase=red\` row cited (basename/stem substring; reject \`green_evidence_red_test_mismatch\`), (2) include a recognized passing-runner line such as \`=> N passed; 0 failed\`, \`N passed in 0.42s\`, or \`ok pkg 0.12s\` (reject \`green_evidence_passing_assertion_missing\`), AND (3) be captured AFTER \`ackTs\` of this span — \`completedTs - ackTs\` must be ≥ \`flow-state.json::tddGreenMinElapsedMs\` (default 4000ms; reject \`green_evidence_too_fresh\`). Escape clause for legitimate observational GREEN: pass BOTH \`--allow-fast-green --green-mode=observational\`. \`--ack-ts\` and \`--completed-ts\` must be monotonic on the span (\`startTs ≤ launchedTs ≤ ackTs ≤ completedTs\`); the helper rejects out-of-order writes with \`delegation_timestamp_non_monotonic\`. If the helper rejects with \`dispatch_active_span_collision\` against a stale span, surface the conflicting \`spanId\` to the parent — do NOT silently retry with \`--allow-parallel\`.`;
|
|
104
104
|
}
|
|
105
105
|
function formatReturnSchema(schema) {
|
|
106
106
|
const lines = [
|
package/dist/content/hooks.js
CHANGED
|
@@ -307,6 +307,65 @@ async function readWorktreeExecutionModeInline(root) {
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// v6.14.2 — read \`tddGreenMinElapsedMs\` from flow-state.json. Defaults to
|
|
311
|
+
// 4000ms when missing or invalid. Operators set 0 to disable the freshness
|
|
312
|
+
// floor while keeping RED-test-name and passing-assertion checks active.
|
|
313
|
+
async function readTddGreenMinElapsedMsInline(root) {
|
|
314
|
+
try {
|
|
315
|
+
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "flow-state.json"), "utf8");
|
|
316
|
+
const parsed = JSON.parse(raw);
|
|
317
|
+
if (parsed && typeof parsed.tddGreenMinElapsedMs === "number" && parsed.tddGreenMinElapsedMs >= 0) {
|
|
318
|
+
return Math.floor(parsed.tddGreenMinElapsedMs);
|
|
319
|
+
}
|
|
320
|
+
return 4000;
|
|
321
|
+
} catch {
|
|
322
|
+
return 4000;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// v6.14.2 Fix 4 — match the RED test name into the GREEN evidenceRef.
|
|
327
|
+
// Returns the basename or stem (without extension) of the most-specific
|
|
328
|
+
// path token in the RED row's first evidenceRef. We deliberately use a
|
|
329
|
+
// substring match, not equality, so callers can include richer text
|
|
330
|
+
// like "REGRESSION: cargo test --test foo => 8 passed; 0 failed".
|
|
331
|
+
function extractRedTestNameInline(redEvidenceRef) {
|
|
332
|
+
if (typeof redEvidenceRef !== "string") return null;
|
|
333
|
+
const trimmed = redEvidenceRef.trim();
|
|
334
|
+
if (trimmed.length === 0) return null;
|
|
335
|
+
// Path-shaped token (foo/bar/baz_test.rs or src/foo.test.ts).
|
|
336
|
+
const pathMatch = /[A-Za-z0-9_./-]+/u.exec(trimmed);
|
|
337
|
+
if (pathMatch) {
|
|
338
|
+
const token = pathMatch[0];
|
|
339
|
+
const slashIdx = token.lastIndexOf("/");
|
|
340
|
+
const base = slashIdx >= 0 ? token.slice(slashIdx + 1) : token;
|
|
341
|
+
const dotIdx = base.indexOf(".");
|
|
342
|
+
const stem = dotIdx > 0 ? base.slice(0, dotIdx) : base;
|
|
343
|
+
if (stem.length >= 4) return stem;
|
|
344
|
+
return base;
|
|
345
|
+
}
|
|
346
|
+
return trimmed;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Match canonical runner pass lines:
|
|
350
|
+
// cargo: "test result: ok. N passed; 0 failed"
|
|
351
|
+
// pytest: "===== N passed in 0.42s ====="
|
|
352
|
+
// go test: "ok pkg 0.123s"
|
|
353
|
+
// npm/jest/vitest: "Tests: N passed"
|
|
354
|
+
// We accept a generic shape: "=> N passed; 0 failed" (the example in
|
|
355
|
+
// the v6.14.2 worker contract) plus four runner-specific patterns.
|
|
356
|
+
const GREEN_PASS_PATTERNS = [
|
|
357
|
+
/=>\\s*\\d+\\s+passed/iu,
|
|
358
|
+
/\\b\\d+\\s+passed[;,]\\s*0\\s+failed\\b/iu,
|
|
359
|
+
/\\btest\\s+result:\\s*ok\\b/iu,
|
|
360
|
+
/\\b\\d+\\s+passed\\s+in\\s+\\d+(?:\\.\\d+)?\\s*s\\b/iu,
|
|
361
|
+
/^ok\\s+\\S+\\s+\\d+(?:\\.\\d+)?s\\b/imu
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
function matchesPassingAssertionInline(value) {
|
|
365
|
+
if (typeof value !== "string") return false;
|
|
366
|
+
return GREEN_PASS_PATTERNS.some((re) => re.test(value));
|
|
367
|
+
}
|
|
368
|
+
|
|
310
369
|
async function readDelegationEvents(root) {
|
|
311
370
|
try {
|
|
312
371
|
const raw = await fs.readFile(path.join(root, RUNTIME_ROOT, "state", "delegation-events.jsonl"), "utf8");
|
|
@@ -1435,6 +1494,137 @@ async function main() {
|
|
|
1435
1494
|
}
|
|
1436
1495
|
}
|
|
1437
1496
|
|
|
1497
|
+
// v6.14.2 Fix 4 — GREEN evidence freshness contract for
|
|
1498
|
+
// \`slice-implementer --phase green --status=completed\`. Three checks:
|
|
1499
|
+
// 1. green_evidence_red_test_mismatch — evidenceRefs[0] must contain
|
|
1500
|
+
// the basename/stem of the RED span's first evidenceRef.
|
|
1501
|
+
// 2. green_evidence_passing_assertion_missing — evidenceRefs[0]
|
|
1502
|
+
// must carry a recognized passing-assertion line ("=> N passed;
|
|
1503
|
+
// 0 failed" or runner-specific equivalents).
|
|
1504
|
+
// 3. green_evidence_too_fresh — completedTs minus ackTs must be
|
|
1505
|
+
// >= flow-state.json::tddGreenMinElapsedMs (default 4000ms).
|
|
1506
|
+
// Escape hatch for legitimate observational GREENs (cross-slice
|
|
1507
|
+
// handoff, no-op verification): pair --allow-fast-green with
|
|
1508
|
+
// --green-mode=observational. Both flags are required.
|
|
1509
|
+
if (
|
|
1510
|
+
clean.stage === "tdd" &&
|
|
1511
|
+
clean.agent === "slice-implementer" &&
|
|
1512
|
+
clean.phase === "green" &&
|
|
1513
|
+
clean.status === "completed"
|
|
1514
|
+
) {
|
|
1515
|
+
const isObservational =
|
|
1516
|
+
typeof args["green-mode"] === "string" &&
|
|
1517
|
+
args["green-mode"].trim().toLowerCase() === "observational";
|
|
1518
|
+
const allowFastGreen = args["allow-fast-green"] === true;
|
|
1519
|
+
const greenEvidenceFirst =
|
|
1520
|
+
Array.isArray(clean.evidenceRefs) && clean.evidenceRefs.length > 0
|
|
1521
|
+
? String(clean.evidenceRefs[0])
|
|
1522
|
+
: "";
|
|
1523
|
+
|
|
1524
|
+
// Locate the matching RED row's first evidenceRef in the events log.
|
|
1525
|
+
const priorEvents = await readDelegationEvents(root);
|
|
1526
|
+
let redEvidenceRef = null;
|
|
1527
|
+
for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
|
|
1528
|
+
const ev = priorEvents[i];
|
|
1529
|
+
if (!ev) continue;
|
|
1530
|
+
if (ev.runId !== runId) continue;
|
|
1531
|
+
if (ev.stage !== "tdd") continue;
|
|
1532
|
+
if (ev.sliceId !== clean.sliceId) continue;
|
|
1533
|
+
if (ev.phase !== "red") continue;
|
|
1534
|
+
if (Array.isArray(ev.evidenceRefs) && ev.evidenceRefs.length > 0) {
|
|
1535
|
+
redEvidenceRef = String(ev.evidenceRefs[0] || "");
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// The freshness contract only fires when there's a matching RED row
|
|
1541
|
+
// for this slice in the active run. Without RED context we have
|
|
1542
|
+
// nothing to verify GREEN against (legacy ledger imports, RED
|
|
1543
|
+
// happened outside cclaw harness, or test fixtures that bypass
|
|
1544
|
+
// RED). Once a RED row is present, the contract becomes
|
|
1545
|
+
// mandatory unless explicitly waived via --allow-fast-green
|
|
1546
|
+
// --green-mode=observational.
|
|
1547
|
+
const hasRedContext = redEvidenceRef !== null;
|
|
1548
|
+
const escapeFastGreen = allowFastGreen && isObservational;
|
|
1549
|
+
|
|
1550
|
+
if (hasRedContext && !escapeFastGreen) {
|
|
1551
|
+
// Check 1: RED test name match.
|
|
1552
|
+
const stem = extractRedTestNameInline(redEvidenceRef);
|
|
1553
|
+
if (stem && greenEvidenceFirst.length > 0 && !greenEvidenceFirst.toLowerCase().includes(stem.toLowerCase())) {
|
|
1554
|
+
emitErrorJson(
|
|
1555
|
+
"green_evidence_red_test_mismatch",
|
|
1556
|
+
{
|
|
1557
|
+
sliceId: clean.sliceId,
|
|
1558
|
+
redEvidenceFirst: redEvidenceRef,
|
|
1559
|
+
greenEvidenceFirst,
|
|
1560
|
+
expectedSubstring: stem,
|
|
1561
|
+
remediation:
|
|
1562
|
+
"evidenceRefs[0] on the GREEN row must reference the same test the RED row cited. Re-run the matching RED test, capture its passing output, and pass it as --evidence-ref."
|
|
1563
|
+
},
|
|
1564
|
+
json
|
|
1565
|
+
);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Check 2: passing-assertion line.
|
|
1570
|
+
if (greenEvidenceFirst.length > 0 && !matchesPassingAssertionInline(greenEvidenceFirst)) {
|
|
1571
|
+
emitErrorJson(
|
|
1572
|
+
"green_evidence_passing_assertion_missing",
|
|
1573
|
+
{
|
|
1574
|
+
sliceId: clean.sliceId,
|
|
1575
|
+
greenEvidenceFirst,
|
|
1576
|
+
remediation:
|
|
1577
|
+
"evidenceRefs[0] on the GREEN row must contain a passing-assertion line such as \\"=> N passed; 0 failed\\" (cargo/jest/vitest), \\"N passed in 0.42s\\" (pytest), \\"ok pkg 0.12s\\" (go test), or equivalent runner output. Re-run the test and paste a fresh runner line."
|
|
1578
|
+
},
|
|
1579
|
+
json
|
|
1580
|
+
);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Check 3: fast-green floor. ackTs is required upstream; we use
|
|
1585
|
+
// the persisted ackTs from prior events when not provided on this
|
|
1586
|
+
// row.
|
|
1587
|
+
const minMs = await readTddGreenMinElapsedMsInline(root);
|
|
1588
|
+
if (minMs > 0 && clean.completedTs) {
|
|
1589
|
+
let ackTs = clean.ackTs;
|
|
1590
|
+
if (!ackTs) {
|
|
1591
|
+
for (let i = priorEvents.length - 1; i >= 0; i -= 1) {
|
|
1592
|
+
const ev = priorEvents[i];
|
|
1593
|
+
if (!ev) continue;
|
|
1594
|
+
if (ev.spanId !== clean.spanId) continue;
|
|
1595
|
+
if (typeof ev.ackTs === "string" && ev.ackTs.length > 0) {
|
|
1596
|
+
ackTs = ev.ackTs;
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
if (ackTs) {
|
|
1602
|
+
const completedMs = Date.parse(clean.completedTs);
|
|
1603
|
+
const ackMs = Date.parse(ackTs);
|
|
1604
|
+
if (Number.isFinite(completedMs) && Number.isFinite(ackMs)) {
|
|
1605
|
+
const elapsed = completedMs - ackMs;
|
|
1606
|
+
if (elapsed < minMs) {
|
|
1607
|
+
emitErrorJson(
|
|
1608
|
+
"green_evidence_too_fresh",
|
|
1609
|
+
{
|
|
1610
|
+
sliceId: clean.sliceId,
|
|
1611
|
+
ackTs,
|
|
1612
|
+
completedTs: clean.completedTs,
|
|
1613
|
+
elapsedMs: elapsed,
|
|
1614
|
+
minMs,
|
|
1615
|
+
remediation:
|
|
1616
|
+
"GREEN completedTs - ackTs is below the freshness floor. Either run the verification test for real and re-record, or pass --allow-fast-green --green-mode=observational for legitimate no-op verification spans."
|
|
1617
|
+
},
|
|
1618
|
+
json
|
|
1619
|
+
);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1438
1628
|
if (
|
|
1439
1629
|
clean.stage === "tdd" &&
|
|
1440
1630
|
clean.agent === "slice-implementer" &&
|
|
@@ -37,7 +37,8 @@ export const TDD = {
|
|
|
37
37
|
},
|
|
38
38
|
executionModel: {
|
|
39
39
|
checklist: [
|
|
40
|
-
"**
|
|
40
|
+
"**Wave dispatch — discovery hardened (v6.14.2):** Before routing, your FIRST tool call after entering TDD MUST be `node .cclaw/cli.mjs internal wave-status --json` (or the harness equivalent `npx cclaw-cli internal wave-status --json`). Do NOT page through `05-plan.md` to find the managed block — the helper reads the managed `<!-- parallel-exec-managed-start -->` block deterministically and prints `{ waves, nextDispatch.readyToDispatch, warnings }`. Open `05-plan.md` only AFTER `wave-status` names a slice that needs context. Multi-ready waves: one AskQuestion (launch wave vs single-slice); then RED checkpoint (when `tddCheckpointMode: \"global-red\"`) or per-lane stream (when `tddCheckpointMode: \"per-slice\"`, the v6.14+ default), parallel GREEN+DOC with worktree-first flags, per-lane REFACTOR. Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
|
|
41
|
+
"**Stream-style wave dispatch (v6.14.0):** After `wave-status` resolves the next dispatch, route accordingly. 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).",
|
|
41
42
|
"**Controller dispatch ordering (v6.14.1 — record BEFORE dispatch).** For every `Task` subagent the controller spawns, record `scheduled` then `launched` ledger events via `node .cclaw/hooks/delegation-record.mjs --status=scheduled ...` and `--status=launched ...` **BEFORE** the `Task(...)` call (one message: ledger writes first, then the matching `Task` calls). Workers self-record `acknowledged` and `completed`; controller back-fill is reserved for `--repair` recovery only. Pass `--span-id`, `--lane-id`, `--claim-token`, `--lease-until` through to the worker so its own helper invocations reuse them.",
|
|
42
43
|
"**Wave closure — integration-overseer decision (v6.14.1).** When every dispatched lane has a `phase=green status=completed` event AND per-lane REFACTOR coverage is satisfied (separate phase event OR `refactorOutcome` folded into GREEN), call `integrationCheckRequired(events, fanInAudits)` from `src/delegation.ts`. (1) `required: true` → dispatch `integration-overseer` as before. (2) `required: false` → emit the audit row via `node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped --audit-reason=\"<reasons>\" --slice-ids=\"S-1,S-2\" --json` and SKIP the dispatch. Linter advisory `tdd_integration_overseer_skipped_audit_missing` flags a wave that closes without either an overseer dispatch or this audit row.",
|
|
43
44
|
"**Inline DOC opt-in (v6.14.1 — single-slice non-deep waves).** Default remains parallel `slice-documenter --phase doc` dispatched alongside `slice-implementer --phase green`. For single-slice waves where `flow-state.json::discoveryMode != \"deep\"`, the controller MAY skip the parallel documenter and instead invoke `slice-implementer --finalize-doc --slice S-<id> --paths <artifacts-dir>/tdd-slices/S-<id>.md` synchronously after GREEN. Multi-slice waves and any `discoveryMode=deep` run keep parallel slice-documenter mandatory.",
|
|
@@ -114,7 +115,7 @@ export const TDD = {
|
|
|
114
115
|
"Relevant existing test files, helpers, fixtures, and exact commands identified before RED.",
|
|
115
116
|
"Callbacks, state transitions, interfaces, schemas, and contracts checked for impact before implementation.",
|
|
116
117
|
"Execution posture and vertical-slice RED/GREEN/REFACTOR checkpoint plan recorded, including commit boundaries when the repo workflow supports them.",
|
|
117
|
-
"RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer). Slices created
|
|
118
|
+
"RED observability: a `phase=red` event in `delegation-events.jsonl` for each slice with non-empty evidenceRefs (test path, span ref, or pasted-output pointer). **`flow-state.json::tddCutoverSliceId` is a HISTORICAL boundary set by `cclaw-cli sync` at upgrade time; it is NOT a pointer to the active slice and the controller MUST NOT dispatch new work for that slice id on its basis.** Slices created at or before the cutover marker may retain legacy `## Watched-RED Proof` / `## RED Evidence` markdown tables; slices created after the cutover marker MUST use phase events + slice-documenter doc, and legacy table writes are surfaced by the advisory `tdd_legacy_section_writes_after_cutover` rule. To find the ACTIVE slice, run `cclaw-cli internal wave-status --json` (Fix 1, v6.14.2) — never derive it from `tddCutoverSliceId`.",
|
|
118
119
|
"GREEN observability: a `phase=green` event in `delegation-events.jsonl` per slice whose `completedTs` >= the matching `phase=red` `completedTs`, authored by `slice-implementer` (linter rule `tdd_slice_implementer_missing` blocks the gate otherwise), and whose evidenceRefs name the failing-now-passing test. Pre-cutover slices may keep legacy `## GREEN Evidence` markdown.",
|
|
119
120
|
"REFACTOR observability: per slice, a `phase=refactor` event OR a `phase=refactor-deferred` event whose evidenceRefs / refactor rationale captures why refactor was deferred.",
|
|
120
121
|
"Per slice, a `phase=doc` event from `slice-documenter` whose evidenceRefs name `<artifacts-dir>/tdd-slices/S-<id>.md`. Mandatory regardless of `discoveryMode` (v6.12.0 Phase R). Linter rule `tdd_slice_documenter_missing` blocks the gate when missing.",
|