cclaw-cli 6.14.0 → 6.14.2
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 +333 -12
- package/dist/artifact-linter.js +9 -1
- package/dist/content/core-agents.js +66 -1
- package/dist/content/hooks.js +271 -0
- package/dist/content/stages/tdd.js +11 -6
- package/dist/flow-state.d.ts +46 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +144 -10
- 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,32 @@ export async function lintTddStage(ctx) {
|
|
|
257
285
|
"refactor-deferred",
|
|
258
286
|
"resolve-conflict"
|
|
259
287
|
]);
|
|
288
|
+
// v6.14.2 — under `legacyContinuation: true` AND a stamped
|
|
289
|
+
// boundary, exempt closed slices that NEVER recorded ANY of the
|
|
290
|
+
// three worktree-first metadata fields. This is the "all-or-
|
|
291
|
+
// nothing legacy" rule from v6.14.2 Fix 3: partial-metadata
|
|
292
|
+
// slices stay flagged (a real bug), but slices that pre-date
|
|
293
|
+
// the worktree-first flip get amnesty.
|
|
294
|
+
const sliceWorktreeMetaState = computeSliceWorktreeMetaState(runEvents);
|
|
295
|
+
const isExemptLegacySlice = (sliceId) => {
|
|
296
|
+
if (!legacyContinuation)
|
|
297
|
+
return false;
|
|
298
|
+
if (worktreeCutoverBoundary === null)
|
|
299
|
+
return false;
|
|
300
|
+
const n = parseSliceNumber(sliceId);
|
|
301
|
+
if (n === null)
|
|
302
|
+
return false;
|
|
303
|
+
if (n > worktreeCutoverBoundary)
|
|
304
|
+
return false;
|
|
305
|
+
const meta = sliceWorktreeMetaState.get(sliceId);
|
|
306
|
+
if (!meta)
|
|
307
|
+
return true; // no slice-implementer rows at all → fully legacy
|
|
308
|
+
// Exempt only when the slice carries ZERO worktree fields across
|
|
309
|
+
// all rows. Partial metadata stays flagged.
|
|
310
|
+
return !meta.anyMeta;
|
|
311
|
+
};
|
|
260
312
|
const missingGreenMeta = new Set();
|
|
313
|
+
const exemptedGreenMeta = new Set();
|
|
261
314
|
for (const ev of runEvents) {
|
|
262
315
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
263
316
|
continue;
|
|
@@ -269,7 +322,12 @@ export async function lintTddStage(ctx) {
|
|
|
269
322
|
const lane = ev.ownerLaneId?.trim() ?? "";
|
|
270
323
|
const lease = ev.leasedUntil?.trim() ?? "";
|
|
271
324
|
if (tok.length === 0 || lane.length === 0 || lease.length === 0) {
|
|
272
|
-
|
|
325
|
+
if (isExemptLegacySlice(ev.sliceId)) {
|
|
326
|
+
exemptedGreenMeta.add(ev.sliceId);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
missingGreenMeta.add(ev.sliceId);
|
|
330
|
+
}
|
|
273
331
|
}
|
|
274
332
|
}
|
|
275
333
|
if (missingGreenMeta.size > 0) {
|
|
@@ -281,7 +339,17 @@ export async function lintTddStage(ctx) {
|
|
|
281
339
|
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
340
|
});
|
|
283
341
|
}
|
|
342
|
+
else if (exemptedGreenMeta.size > 0) {
|
|
343
|
+
findings.push({
|
|
344
|
+
section: "tdd_slice_lane_metadata_legacy_exempt",
|
|
345
|
+
required: false,
|
|
346
|
+
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`.",
|
|
347
|
+
found: true,
|
|
348
|
+
details: `Legacy-exempt slices (no claimToken/ownerLaneId/leasedUntil recorded; all closed before worktree-first flip): ${[...exemptedGreenMeta].sort().join(", ")}.`
|
|
349
|
+
});
|
|
350
|
+
}
|
|
284
351
|
const missingClaim = new Set();
|
|
352
|
+
const exemptedClaim = new Set();
|
|
285
353
|
for (const ev of runEvents) {
|
|
286
354
|
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
287
355
|
continue;
|
|
@@ -291,7 +359,12 @@ export async function lintTddStage(ctx) {
|
|
|
291
359
|
continue;
|
|
292
360
|
const tok = ev.claimToken?.trim() ?? "";
|
|
293
361
|
if (tok.length === 0 && typeof ev.sliceId === "string") {
|
|
294
|
-
|
|
362
|
+
if (isExemptLegacySlice(ev.sliceId)) {
|
|
363
|
+
exemptedClaim.add(ev.sliceId);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
missingClaim.add(ev.sliceId);
|
|
367
|
+
}
|
|
295
368
|
}
|
|
296
369
|
}
|
|
297
370
|
if (missingClaim.size > 0) {
|
|
@@ -303,6 +376,15 @@ export async function lintTddStage(ctx) {
|
|
|
303
376
|
details: `Slices missing claim token on non-GREEN terminal rows: ${[...missingClaim].join(", ")}.`
|
|
304
377
|
});
|
|
305
378
|
}
|
|
379
|
+
else if (exemptedClaim.size > 0) {
|
|
380
|
+
findings.push({
|
|
381
|
+
section: "tdd_slice_claim_token_legacy_exempt",
|
|
382
|
+
required: false,
|
|
383
|
+
rule: "v6.14.2 legacyContinuation amnesty: closed pre-cutover slices without claim tokens on terminal rows are exempt from `tdd_slice_claim_token_missing`.",
|
|
384
|
+
found: true,
|
|
385
|
+
details: `Legacy-exempt slices: ${[...exemptedClaim].sort().join(", ")}.`
|
|
386
|
+
});
|
|
387
|
+
}
|
|
306
388
|
const conflictSlices = [
|
|
307
389
|
...new Set([
|
|
308
390
|
...runEvents
|
|
@@ -327,6 +409,12 @@ export async function lintTddStage(ctx) {
|
|
|
327
409
|
}
|
|
328
410
|
const now = Date.now();
|
|
329
411
|
const leaseStale = new Set();
|
|
412
|
+
const leaseStaleExempted = new Set();
|
|
413
|
+
// v6.14.2 — also exempt slices whose lease has expired but the
|
|
414
|
+
// slice was already closed (terminal row recorded) before the
|
|
415
|
+
// expiry. The reclaim audit row was just never written —
|
|
416
|
+
// bookkeeping advisory, not a blocker.
|
|
417
|
+
const closedBeforeLeaseExpiry = computeClosedBeforeLeaseExpiry(runEvents);
|
|
330
418
|
for (const ev of runEvents) {
|
|
331
419
|
if (typeof ev.leasedUntil !== "string")
|
|
332
420
|
continue;
|
|
@@ -335,8 +423,18 @@ export async function lintTddStage(ctx) {
|
|
|
335
423
|
continue;
|
|
336
424
|
if (ev.leaseState === "reclaimed" || ev.leaseState === "released")
|
|
337
425
|
continue;
|
|
338
|
-
if (typeof ev.sliceId
|
|
339
|
-
|
|
426
|
+
if (typeof ev.sliceId !== "string")
|
|
427
|
+
continue;
|
|
428
|
+
const sliceId = ev.sliceId;
|
|
429
|
+
if (isExemptLegacySlice(sliceId)) {
|
|
430
|
+
leaseStaleExempted.add(sliceId);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (closedBeforeLeaseExpiry.has(sliceId)) {
|
|
434
|
+
leaseStaleExempted.add(sliceId);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
leaseStale.add(sliceId);
|
|
340
438
|
}
|
|
341
439
|
if (leaseStale.size > 0) {
|
|
342
440
|
findings.push({
|
|
@@ -347,6 +445,15 @@ export async function lintTddStage(ctx) {
|
|
|
347
445
|
details: `Expired leases not reclaimed for slice(s): ${[...leaseStale].join(", ")}.`
|
|
348
446
|
});
|
|
349
447
|
}
|
|
448
|
+
else if (leaseStaleExempted.size > 0) {
|
|
449
|
+
findings.push({
|
|
450
|
+
section: "tdd_lease_expired_legacy_exempt",
|
|
451
|
+
required: false,
|
|
452
|
+
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.",
|
|
453
|
+
found: true,
|
|
454
|
+
details: `Lease-expiry-exempt slices: ${[...leaseStaleExempted].sort().join(", ")}.`
|
|
455
|
+
});
|
|
456
|
+
}
|
|
350
457
|
}
|
|
351
458
|
const assertionBody = sectionBodyByName(sections, "Assertion Correctness Notes");
|
|
352
459
|
if (assertionBody !== null) {
|
|
@@ -426,14 +533,27 @@ export async function lintTddStage(ctx) {
|
|
|
426
533
|
cohesionContractFound = false;
|
|
427
534
|
cohesionErrors.push("cohesion-contract.json is missing or invalid JSON.");
|
|
428
535
|
}
|
|
536
|
+
// v6.14.2 — soften cohesion-contract under `legacyContinuation: true`.
|
|
537
|
+
// Pre-flip projects (hox) carry many closed implementer rows but
|
|
538
|
+
// never recorded cross-slice cohesion data because that schema
|
|
539
|
+
// didn't exist when the slices closed. Flag advisory + suggest the
|
|
540
|
+
// auto-stub helper instead of blocking the gate.
|
|
541
|
+
const cohesionRequired = legacyContinuation === true ? false : true;
|
|
542
|
+
const advisoryNote = cohesionRequired
|
|
543
|
+
? cohesionErrors.join(" ")
|
|
544
|
+
: `${cohesionErrors.join(" ")} ` +
|
|
545
|
+
"Cohesion contract is advisory under legacyContinuation: true — emit a stub via " +
|
|
546
|
+
"`cclaw-cli internal cohesion-contract --stub` to silence this finding.";
|
|
429
547
|
findings.push({
|
|
430
548
|
section: "tdd.cohesion_contract_missing",
|
|
431
|
-
required:
|
|
432
|
-
rule:
|
|
549
|
+
required: cohesionRequired,
|
|
550
|
+
rule: cohesionRequired
|
|
551
|
+
? "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."
|
|
552
|
+
: "v6.14.2 advisory under legacyContinuation: cohesion contract is recommended, not required. Use `cclaw-cli internal cohesion-contract --stub` to write a baseline.",
|
|
433
553
|
found: cohesionContractFound,
|
|
434
554
|
details: cohesionContractFound
|
|
435
555
|
? `Fan-out detected (${completedSliceImplementers.length} completed slice-implementer rows); cohesion contract markdown+JSON sidecar are present and parseable.`
|
|
436
|
-
:
|
|
556
|
+
: advisoryNote
|
|
437
557
|
});
|
|
438
558
|
const completedOverseerRows = activeRunEntries.filter((entry) => entry.agent === "integration-overseer" && entry.status === "completed");
|
|
439
559
|
const overseerStatusInEvidence = completedOverseerRows.some((entry) => {
|
|
@@ -448,36 +568,69 @@ export async function lintTddStage(ctx) {
|
|
|
448
568
|
// `integrationCheckRequired()` returns required=false, the gate is
|
|
449
569
|
// soft (advisory) and an audit-only finding is emitted so the
|
|
450
570
|
// controller can record the deliberate skip in artifacts.
|
|
571
|
+
//
|
|
572
|
+
// v6.14.1 — also surface the audit row presence. When the controller
|
|
573
|
+
// skips `integration-overseer` dispatch (or the heuristic returns
|
|
574
|
+
// false), the run log MUST contain a
|
|
575
|
+
// `cclaw_integration_overseer_skipped` audit row for traceability.
|
|
576
|
+
// The advisory `tdd_integration_overseer_skipped_audit_missing`
|
|
577
|
+
// surfaces a missing audit row when 2+ closed slices closed without
|
|
578
|
+
// any overseer dispatch AND no audit was recorded.
|
|
451
579
|
let overseerVerdict = null;
|
|
452
580
|
let overseerRequired = true;
|
|
581
|
+
const skippedAuditRowCount = await countIntegrationOverseerSkippedAudits(projectRoot, delegationLedger.runId);
|
|
582
|
+
const skippedAuditRowFound = skippedAuditRowCount > 0;
|
|
453
583
|
if (integrationOverseerMode === "conditional") {
|
|
454
584
|
const eventsForVerdict = runEvents.length > 0 ? runEvents : [];
|
|
455
585
|
const auditsForVerdict = fanInAudits.filter((a) => a.runId === delegationLedger.runId);
|
|
456
586
|
overseerVerdict = integrationCheckRequired(eventsForVerdict, auditsForVerdict);
|
|
457
587
|
overseerRequired = overseerVerdict.required;
|
|
458
588
|
if (!overseerVerdict.required) {
|
|
589
|
+
const auditRowSuffix = skippedAuditRowFound
|
|
590
|
+
? "audit row recorded — skip is fully traceable."
|
|
591
|
+
: "audit row MISSING — controller should append `cclaw_integration_overseer_skipped` for traceability (see `tdd_integration_overseer_skipped_audit_missing`).";
|
|
459
592
|
findings.push({
|
|
460
593
|
section: "tdd_integration_overseer_skipped_by_disjoint_paths",
|
|
461
594
|
required: false,
|
|
462
|
-
rule: "v6.14.0 conditional integration-overseer mode: the heuristic returned `required: false` (disjoint claimedPaths, no high-risk slices, no fan-in conflicts). The controller may skip dispatching `integration-overseer` and emit a `cclaw_integration_overseer_skipped` audit row instead.",
|
|
595
|
+
rule: "v6.14.0+ conditional integration-overseer mode: the heuristic returned `required: false` (disjoint claimedPaths, no high-risk slices, no fan-in conflicts). The controller may skip dispatching `integration-overseer` and emit a `cclaw_integration_overseer_skipped` audit row instead.",
|
|
463
596
|
found: true,
|
|
464
|
-
details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe —
|
|
597
|
+
details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe — ${auditRowSuffix}`
|
|
465
598
|
});
|
|
466
599
|
}
|
|
467
600
|
}
|
|
601
|
+
// v6.14.1 — `tdd_integration_overseer_skipped_audit_missing` (advisory).
|
|
602
|
+
// Fires when fan-out is detected (2+ completed slice-implementers),
|
|
603
|
+
// no `integration-overseer` was dispatched at all (no scheduled or
|
|
604
|
+
// completed row for the active run), AND no
|
|
605
|
+
// `cclaw_integration_overseer_skipped` audit row exists. This pairs
|
|
606
|
+
// with the controller skill text rule that the wave-closure decision
|
|
607
|
+
// ("dispatch overseer or skip") MUST leave a trail.
|
|
608
|
+
const overseerDispatched = activeRunEntries.some((entry) => entry.agent === "integration-overseer");
|
|
609
|
+
if (!overseerDispatched && !skippedAuditRowFound) {
|
|
610
|
+
findings.push({
|
|
611
|
+
section: "tdd_integration_overseer_skipped_audit_missing",
|
|
612
|
+
required: false,
|
|
613
|
+
rule: "v6.14.1: 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.",
|
|
614
|
+
found: false,
|
|
615
|
+
details: `Fan-out detected (${completedSliceImplementers.length} completed slice-implementer rows) but no integration-overseer dispatch row OR cclaw_integration_overseer_skipped audit row exists for active run. ` +
|
|
616
|
+
"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."
|
|
617
|
+
});
|
|
618
|
+
}
|
|
468
619
|
findings.push({
|
|
469
620
|
section: "tdd.integration_overseer_missing",
|
|
470
621
|
required: overseerRequired,
|
|
471
622
|
rule: overseerRequired
|
|
472
623
|
? "When fan-out is detected, require completed `integration-overseer` evidence with PASS or PASS_WITH_GAPS."
|
|
473
|
-
: "v6.14.0 conditional integration-overseer mode: integration-overseer dispatch is advisory because `integrationCheckRequired()` returned required=false. Run it anyway if the run touches new boundaries.",
|
|
624
|
+
: "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.",
|
|
474
625
|
found: integrationOverseerFound,
|
|
475
626
|
details: integrationOverseerFound
|
|
476
627
|
? "integration-overseer completion recorded with PASS/PASS_WITH_GAPS evidence."
|
|
477
628
|
: completedOverseerRows.length === 0
|
|
478
629
|
? overseerRequired
|
|
479
630
|
? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
|
|
480
|
-
:
|
|
631
|
+
: skippedAuditRowFound
|
|
632
|
+
? "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths) and `cclaw_integration_overseer_skipped` audit row recorded. Audit-only."
|
|
633
|
+
: "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths). Audit-only."
|
|
481
634
|
: "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
|
|
482
635
|
});
|
|
483
636
|
}
|
|
@@ -551,6 +704,46 @@ export async function lintTddStage(ctx) {
|
|
|
551
704
|
}
|
|
552
705
|
}
|
|
553
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* v6.14.1 — count `cclaw_integration_overseer_skipped` audit rows in
|
|
709
|
+
* `delegation-events.jsonl` for a given runId. The audit row is not a
|
|
710
|
+
* `DelegationEvent` (no agent/status), so `readDelegationEvents`
|
|
711
|
+
* filters it out; we re-scan the raw file with a narrow JSON match.
|
|
712
|
+
*
|
|
713
|
+
* Best-effort: missing file or parse errors return 0.
|
|
714
|
+
*/
|
|
715
|
+
async function countIntegrationOverseerSkippedAudits(projectRoot, runId) {
|
|
716
|
+
const filePath = path.join(projectRoot, ".cclaw/state/delegation-events.jsonl");
|
|
717
|
+
let raw = "";
|
|
718
|
+
try {
|
|
719
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return 0;
|
|
723
|
+
}
|
|
724
|
+
let count = 0;
|
|
725
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
726
|
+
const trimmed = line.trim();
|
|
727
|
+
if (trimmed.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
let parsed;
|
|
730
|
+
try {
|
|
731
|
+
parsed = JSON.parse(trimmed);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
737
|
+
continue;
|
|
738
|
+
const obj = parsed;
|
|
739
|
+
if (obj.event !== "cclaw_integration_overseer_skipped")
|
|
740
|
+
continue;
|
|
741
|
+
if (typeof obj.runId === "string" && obj.runId !== runId)
|
|
742
|
+
continue;
|
|
743
|
+
count += 1;
|
|
744
|
+
}
|
|
745
|
+
return count;
|
|
746
|
+
}
|
|
554
747
|
async function listSliceFiles(slicesDir) {
|
|
555
748
|
let entries = [];
|
|
556
749
|
try {
|
|
@@ -1140,6 +1333,134 @@ function pickEventTs(rows) {
|
|
|
1140
1333
|
}
|
|
1141
1334
|
return undefined;
|
|
1142
1335
|
}
|
|
1336
|
+
/**
|
|
1337
|
+
* v6.14.2 — for each slice id appearing in `slice-implementer` rows of
|
|
1338
|
+
* the active run, record whether ANY row carried at least one of the
|
|
1339
|
+
* three worktree-first metadata fields (`claimToken`, `ownerLaneId`,
|
|
1340
|
+
* `leasedUntil`). Used by `isExemptLegacySlice` to enforce the "all-or-
|
|
1341
|
+
* nothing legacy" rule: only slices with NO worktree fields anywhere
|
|
1342
|
+
* in their rows qualify for the legacyContinuation amnesty.
|
|
1343
|
+
*/
|
|
1344
|
+
function computeSliceWorktreeMetaState(events) {
|
|
1345
|
+
const out = new Map();
|
|
1346
|
+
for (const ev of events) {
|
|
1347
|
+
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
1348
|
+
continue;
|
|
1349
|
+
if (typeof ev.sliceId !== "string")
|
|
1350
|
+
continue;
|
|
1351
|
+
const tok = ev.claimToken?.trim() ?? "";
|
|
1352
|
+
const lane = ev.ownerLaneId?.trim() ?? "";
|
|
1353
|
+
const lease = ev.leasedUntil?.trim() ?? "";
|
|
1354
|
+
const anyHere = tok.length > 0 || lane.length > 0 || lease.length > 0;
|
|
1355
|
+
const prev = out.get(ev.sliceId) ?? { anyMeta: false };
|
|
1356
|
+
out.set(ev.sliceId, { anyMeta: prev.anyMeta || anyHere });
|
|
1357
|
+
}
|
|
1358
|
+
return out;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* v6.14.2 — slices whose terminal `refactor` / `refactor-deferred` /
|
|
1362
|
+
* `resolve-conflict` row recorded a `completedTs` that PRECEDES the
|
|
1363
|
+
* latest `leasedUntil` for the same slice. The lease was never
|
|
1364
|
+
* reclaimed but the wave closed in time; the missing audit row is
|
|
1365
|
+
* advisory bookkeeping, not a correctness failure.
|
|
1366
|
+
*/
|
|
1367
|
+
function computeClosedBeforeLeaseExpiry(events) {
|
|
1368
|
+
const terminalPhases = new Set([
|
|
1369
|
+
"refactor",
|
|
1370
|
+
"refactor-deferred",
|
|
1371
|
+
"resolve-conflict"
|
|
1372
|
+
]);
|
|
1373
|
+
const lastLease = new Map();
|
|
1374
|
+
const earliestTerminal = new Map();
|
|
1375
|
+
for (const ev of events) {
|
|
1376
|
+
if (ev.stage !== "tdd" || ev.agent !== "slice-implementer")
|
|
1377
|
+
continue;
|
|
1378
|
+
if (typeof ev.sliceId !== "string")
|
|
1379
|
+
continue;
|
|
1380
|
+
if (typeof ev.leasedUntil === "string") {
|
|
1381
|
+
const until = Date.parse(ev.leasedUntil);
|
|
1382
|
+
if (Number.isFinite(until)) {
|
|
1383
|
+
const prev = lastLease.get(ev.sliceId);
|
|
1384
|
+
if (prev === undefined || until > prev) {
|
|
1385
|
+
lastLease.set(ev.sliceId, until);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (ev.status === "completed" &&
|
|
1390
|
+
typeof ev.phase === "string" &&
|
|
1391
|
+
terminalPhases.has(ev.phase) &&
|
|
1392
|
+
typeof ev.completedTs === "string") {
|
|
1393
|
+
const ts = Date.parse(ev.completedTs);
|
|
1394
|
+
if (Number.isFinite(ts)) {
|
|
1395
|
+
const prev = earliestTerminal.get(ev.sliceId);
|
|
1396
|
+
if (prev === undefined || ts < prev) {
|
|
1397
|
+
earliestTerminal.set(ev.sliceId, ts);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const out = new Set();
|
|
1403
|
+
for (const [sliceId, terminalTs] of earliestTerminal.entries()) {
|
|
1404
|
+
const leaseTs = lastLease.get(sliceId);
|
|
1405
|
+
if (leaseTs === undefined)
|
|
1406
|
+
continue;
|
|
1407
|
+
if (terminalTs < leaseTs) {
|
|
1408
|
+
out.add(sliceId);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return out;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* v6.14.2 Fix 2 — advisory linter rule.
|
|
1415
|
+
*
|
|
1416
|
+
* Fires when:
|
|
1417
|
+
* (a) `tddCutoverSliceId` is set on the active flow state, AND
|
|
1418
|
+
* (b) the active run has a `scheduled` row whose `sliceId === tddCutoverSliceId`
|
|
1419
|
+
* AND `phase ∈ {red, green, doc}`, AND
|
|
1420
|
+
* (c) that slice already has a terminal `refactor` / `refactor-deferred` /
|
|
1421
|
+
* `resolve-conflict` event recorded for it (under any run) — i.e.
|
|
1422
|
+
* it's already closed.
|
|
1423
|
+
*
|
|
1424
|
+
* This is the diagnostic hox surfaced on S-17/W-03: the controller
|
|
1425
|
+
* read `tddCutoverSliceId: "S-11"` and treated it as the active slice
|
|
1426
|
+
* pointer, then dispatched new work for S-11 (already closed under
|
|
1427
|
+
* v6.12 markdown). Advisory — never blocks stage-complete.
|
|
1428
|
+
*/
|
|
1429
|
+
function evaluateCutoverMisread(input) {
|
|
1430
|
+
const { tddCutoverSliceId, activeRunEntries, ledgerEntries } = input;
|
|
1431
|
+
const cutoverPhases = new Set(["red", "green", "doc"]);
|
|
1432
|
+
const newWork = activeRunEntries.find((entry) => entry.sliceId === tddCutoverSliceId &&
|
|
1433
|
+
typeof entry.phase === "string" &&
|
|
1434
|
+
cutoverPhases.has(entry.phase) &&
|
|
1435
|
+
// any schedule/launch/ack/completed for the cutover slice in this run
|
|
1436
|
+
(entry.status === "scheduled" ||
|
|
1437
|
+
entry.status === "launched" ||
|
|
1438
|
+
entry.status === "acknowledged" ||
|
|
1439
|
+
entry.status === "completed"));
|
|
1440
|
+
if (!newWork)
|
|
1441
|
+
return null;
|
|
1442
|
+
const terminalPhases = new Set([
|
|
1443
|
+
"refactor",
|
|
1444
|
+
"refactor-deferred",
|
|
1445
|
+
"resolve-conflict"
|
|
1446
|
+
]);
|
|
1447
|
+
const closure = ledgerEntries.find((entry) => entry.sliceId === tddCutoverSliceId &&
|
|
1448
|
+
entry.status === "completed" &&
|
|
1449
|
+
typeof entry.phase === "string" &&
|
|
1450
|
+
terminalPhases.has(entry.phase));
|
|
1451
|
+
if (!closure)
|
|
1452
|
+
return null;
|
|
1453
|
+
const closedTs = closure.completedTs ?? closure.endTs ?? closure.ts ?? "(unknown)";
|
|
1454
|
+
const closedRunId = closure.runId ?? "(unknown-run)";
|
|
1455
|
+
return {
|
|
1456
|
+
section: "tdd_cutover_misread_warning",
|
|
1457
|
+
required: false,
|
|
1458
|
+
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.",
|
|
1459
|
+
found: false,
|
|
1460
|
+
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. ` +
|
|
1461
|
+
"Use `cclaw-cli internal wave-status --json` to find the next ready slice."
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1143
1464
|
export function parseVerticalSliceCycle(body) {
|
|
1144
1465
|
const tableLines = body.split("\n").filter((line) => /^\|/u.test(line));
|
|
1145
1466
|
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":
|
|
@@ -52,6 +52,56 @@ Before doing substantive work, return an ACK object that the parent can record:
|
|
|
52
52
|
|
|
53
53
|
Finish with the required return schema plus the same \`spanId\` and \`dispatchId\`. The parent must not claim isolated completion unless ACK/result proof matches the ledger/event span.`;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* v6.14.1 — TDD worker self-record contract. The parent records
|
|
57
|
+
* `scheduled` and `launched` rows BEFORE dispatching the Task; the
|
|
58
|
+
* worker is responsible for `acknowledged` (on entry) and `completed`
|
|
59
|
+
* (on exit). This contract restores the v6.13.1 discipline that
|
|
60
|
+
* v6.14.0 dropped — the controller-side fix in v6.14.1's TDD skill
|
|
61
|
+
* text is paired with this worker-side self-record helper template.
|
|
62
|
+
*/
|
|
63
|
+
function tddWorkerSelfRecordContract(agentName) {
|
|
64
|
+
const isImplementer = agentName === "slice-implementer";
|
|
65
|
+
const refactorOutcomeFlag = isImplementer
|
|
66
|
+
? " --refactor-outcome=inline|deferred [--refactor-rationale=\"<why>\"]"
|
|
67
|
+
: "";
|
|
68
|
+
const laneFlags = isImplementer
|
|
69
|
+
? " [--claim-token=<t>] [--lane-id=<lane>] [--lease-until=<iso>]"
|
|
70
|
+
: "";
|
|
71
|
+
return `## TDD Worker Self-Record Contract (v6.14.2)
|
|
72
|
+
|
|
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
|
+
|
|
75
|
+
**On entry — record acknowledgement (BEFORE doing work):**
|
|
76
|
+
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
ACK_TS="$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
79
|
+
node .cclaw/hooks/delegation-record.mjs \\
|
|
80
|
+
--stage=tdd --agent=${agentName} --mode=mandatory \\
|
|
81
|
+
--status=acknowledged \\
|
|
82
|
+
--span-id=<spanId from controller dispatch> \\
|
|
83
|
+
--dispatch-id=<dispatchId from controller dispatch> \\
|
|
84
|
+
--dispatch-surface=<surface from controller dispatch> \\
|
|
85
|
+
--agent-definition-path=.cclaw/agents/${agentName}.md \\
|
|
86
|
+
--ack-ts="$ACK_TS" \\
|
|
87
|
+
--json
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
**On exit — record completion (AFTER work + verification):**
|
|
91
|
+
|
|
92
|
+
\`\`\`bash
|
|
93
|
+
COMPLETED_TS="$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
94
|
+
node .cclaw/hooks/delegation-record.mjs \\
|
|
95
|
+
--stage=tdd --agent=${agentName} --mode=mandatory \\
|
|
96
|
+
--status=completed \\
|
|
97
|
+
--span-id=<same spanId> \\
|
|
98
|
+
--completed-ts="$COMPLETED_TS" \\
|
|
99
|
+
--evidence-ref="<test-path-or-artifact-ref>"${refactorOutcomeFlag}${laneFlags} \\
|
|
100
|
+
--json
|
|
101
|
+
\`\`\`
|
|
102
|
+
|
|
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
|
+
}
|
|
55
105
|
function formatReturnSchema(schema) {
|
|
56
106
|
const lines = [
|
|
57
107
|
`- Status field: \`${schema.statusField}\``,
|
|
@@ -600,6 +650,18 @@ export const CCLAW_AGENTS = [
|
|
|
600
650
|
].join("\n")
|
|
601
651
|
}
|
|
602
652
|
];
|
|
653
|
+
/**
|
|
654
|
+
* v6.14.1 — agents whose rendered `.cclaw/agents/<name>.md` file gets the
|
|
655
|
+
* TDD worker self-record helper template. These agents are the ones the
|
|
656
|
+
* controller dispatches via `Task` during a TDD wave; they are
|
|
657
|
+
* responsible for `acknowledged` and `completed` ledger writes.
|
|
658
|
+
*/
|
|
659
|
+
const TDD_WORKER_SELF_RECORD_AGENTS = new Set([
|
|
660
|
+
"test-author",
|
|
661
|
+
"slice-implementer",
|
|
662
|
+
"slice-documenter",
|
|
663
|
+
"integration-overseer"
|
|
664
|
+
]);
|
|
603
665
|
import { stageDelegationSummary } from "./stage-schema.js";
|
|
604
666
|
/**
|
|
605
667
|
* Render a complete cclaw agent markdown file (YAML frontmatter + body).
|
|
@@ -627,6 +689,9 @@ export function agentMarkdown(agent) {
|
|
|
627
689
|
].join("\n");
|
|
628
690
|
const relatedStages = agent.relatedStages.length > 0 ? agent.relatedStages.join(", ") : "(none)";
|
|
629
691
|
const taskDelegation = defaultTaskDelegationSection(agent.name);
|
|
692
|
+
const tddWorkerSelfRecordSection = TDD_WORKER_SELF_RECORD_AGENTS.has(agent.name)
|
|
693
|
+
? `\n${tddWorkerSelfRecordContract(agent.name)}\n`
|
|
694
|
+
: "";
|
|
630
695
|
return `${frontmatter}
|
|
631
696
|
|
|
632
697
|
# ${agent.name}
|
|
@@ -639,7 +704,7 @@ ${agent.body}
|
|
|
639
704
|
- Related stages: ${relatedStages}
|
|
640
705
|
|
|
641
706
|
${workerAckContract()}
|
|
642
|
-
|
|
707
|
+
${tddWorkerSelfRecordSection}
|
|
643
708
|
## Required Return Schema
|
|
644
709
|
|
|
645
710
|
STRICT_RETURN_SCHEMA: return a structured object matching this contract before any narrative when delegated. Include \`spanId\`, \`dispatchId\` or \`workerRunId\`, \`dispatchSurface\`, \`agentDefinitionPath\`, and lifecycle timestamps when provided by the parent.
|