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
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");
|
|
@@ -344,6 +403,7 @@ function usage() {
|
|
|
344
403
|
" node .cclaw/hooks/delegation-record.mjs --stage=<stage> --agent=<agent> --mode=<mandatory|proactive> --status=<scheduled|launched|acknowledged|completed|failed|waived|stale> --span-id=<id> [--dispatch-id=<id>] [--worker-run-id=<id>] [--dispatch-surface=<surface>] [--agent-definition-path=<path>] [--ack-ts=<iso>] [--launched-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--waiver-reason=<text>] [--supersede=<prevSpanId>] [--allow-parallel] [--paths=<comma-separated>] [--override-cap=<int>] [--json]",
|
|
345
404
|
" node .cclaw/hooks/delegation-record.mjs --rerecord --span-id=<id> --dispatch-id=<id> --dispatch-surface=<surface> --agent-definition-path=<path> [--ack-ts=<iso>] [--completed-ts=<iso>] [--evidence-ref=<ref>] [--json]",
|
|
346
405
|
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
|
|
406
|
+
" node .cclaw/hooks/delegation-record.mjs --audit-kind=cclaw_integration_overseer_skipped [--audit-reason=\\\"<comma-separated reasons>\\\"] [--slice-ids=\\\"S-1,S-2\\\"] [--json] # v6.14.1: emit non-delegation audit row only",
|
|
347
407
|
"",
|
|
348
408
|
"Allowed --dispatch-surface values:",
|
|
349
409
|
" " + VALID_DISPATCH_SURFACES.join(", "),
|
|
@@ -898,6 +958,74 @@ async function findLegacyEntry(root, spanId) {
|
|
|
898
958
|
return ledger.entries.find((entry) => entry && entry.spanId === spanId) || null;
|
|
899
959
|
}
|
|
900
960
|
|
|
961
|
+
// v6.14.1 — allow-list of non-delegation audit events the controller
|
|
962
|
+
// can emit via the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS
|
|
963
|
+
// in src/delegation.ts.
|
|
964
|
+
const VALID_AUDIT_KINDS = new Set([
|
|
965
|
+
"cclaw_integration_overseer_skipped"
|
|
966
|
+
]);
|
|
967
|
+
|
|
968
|
+
async function runAuditEmit(args, json) {
|
|
969
|
+
const kind = String(args["audit-kind"]).trim();
|
|
970
|
+
if (!VALID_AUDIT_KINDS.has(kind)) {
|
|
971
|
+
emitProblems([
|
|
972
|
+
"invalid --audit-kind: " + kind +
|
|
973
|
+
" (allowed: " + [...VALID_AUDIT_KINDS].join(", ") + ")"
|
|
974
|
+
], json, 2);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const root = await detectRoot();
|
|
978
|
+
const runId = await readRunId(root);
|
|
979
|
+
const reason = typeof args["audit-reason"] === "string"
|
|
980
|
+
? args["audit-reason"].trim()
|
|
981
|
+
: "";
|
|
982
|
+
const sliceIdsRaw = typeof args["slice-ids"] === "string"
|
|
983
|
+
? args["slice-ids"].trim()
|
|
984
|
+
: "";
|
|
985
|
+
const sliceIds = sliceIdsRaw.length > 0
|
|
986
|
+
? sliceIdsRaw
|
|
987
|
+
.split(",")
|
|
988
|
+
.map((value) => value.trim())
|
|
989
|
+
.filter((value) => value.length > 0)
|
|
990
|
+
: [];
|
|
991
|
+
const ts = new Date().toISOString();
|
|
992
|
+
const payload = {
|
|
993
|
+
event: kind,
|
|
994
|
+
runId,
|
|
995
|
+
ts,
|
|
996
|
+
eventTs: ts,
|
|
997
|
+
...(reason.length > 0 ? { reasons: reason.split(",").map((r) => r.trim()).filter((r) => r.length > 0) } : {}),
|
|
998
|
+
...(sliceIds.length > 0 ? { sliceIds } : {})
|
|
999
|
+
};
|
|
1000
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
1001
|
+
try {
|
|
1002
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
1003
|
+
await fs.appendFile(
|
|
1004
|
+
path.join(stateDir, "delegation-events.jsonl"),
|
|
1005
|
+
JSON.stringify(payload) + "\\n",
|
|
1006
|
+
{ encoding: "utf8", mode: 0o600 }
|
|
1007
|
+
);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
const message = error && typeof error === "object" && "message" in error
|
|
1010
|
+
? String(error.message)
|
|
1011
|
+
: String(error);
|
|
1012
|
+
emitErrorJson("audit_emit_failed", { kind, message }, json);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (json) {
|
|
1016
|
+
process.stdout.write(JSON.stringify({
|
|
1017
|
+
ok: true,
|
|
1018
|
+
command: "audit-emit",
|
|
1019
|
+
auditKind: kind,
|
|
1020
|
+
runId,
|
|
1021
|
+
sliceIds,
|
|
1022
|
+
ts
|
|
1023
|
+
}, null, 2) + "\\n");
|
|
1024
|
+
} else {
|
|
1025
|
+
process.stdout.write("[cclaw] audit emitted: " + kind + " (run=" + runId + ", ts=" + ts + ")\\n");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
901
1029
|
async function runRerecord(args, json) {
|
|
902
1030
|
const problems = [];
|
|
903
1031
|
for (const key of ["span-id", "dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
@@ -1130,6 +1258,18 @@ async function main() {
|
|
|
1130
1258
|
return;
|
|
1131
1259
|
}
|
|
1132
1260
|
|
|
1261
|
+
// v6.14.1 — audit-only emit path. When the controller wants to record
|
|
1262
|
+
// a non-delegation audit row (e.g. \`cclaw_integration_overseer_skipped\`
|
|
1263
|
+
// when the wave heuristic chose to skip the overseer dispatch), pass
|
|
1264
|
+
// --audit-kind=<event-name> [--audit-reason=<text>] [--slice-ids=<csv>]
|
|
1265
|
+
// and the helper appends a single line to delegation-events.jsonl
|
|
1266
|
+
// without touching the lifecycle ledger. The kind must be in the
|
|
1267
|
+
// canonical allow-list so a typo cannot inject an unrecognized event.
|
|
1268
|
+
if (typeof args["audit-kind"] === "string" && args["audit-kind"].trim().length > 0) {
|
|
1269
|
+
await runAuditEmit(args, json);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1133
1273
|
const problems = [];
|
|
1134
1274
|
if (!args.stage) problems.push("missing --stage");
|
|
1135
1275
|
if (!args.agent) problems.push("missing --agent");
|
|
@@ -1354,6 +1494,137 @@ async function main() {
|
|
|
1354
1494
|
}
|
|
1355
1495
|
}
|
|
1356
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
|
+
|
|
1357
1628
|
if (
|
|
1358
1629
|
clean.stage === "tdd" &&
|
|
1359
1630
|
clean.agent === "slice-implementer" &&
|
|
@@ -37,7 +37,12 @@ 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).",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
45
|
+
"**Stale active-span recovery (v6.14.1).** If `delegation-record` rejects a new `--status=scheduled` with `dispatch_active_span_collision` or `dispatch_duplicate` and the conflicting span has a `completed` event in `delegation-events.jsonl`, the fold is correct (`computeActiveSubagents` excludes terminal spans) and the rejection is from a different live span on the same `(stage, agent)` pair — pass `--allow-parallel` deliberately, quote the conflicting `spanId` in the turn output, and proceed. If you cannot identify the conflicting active span, STOP and report — do not blanket-add `--allow-parallel` to silence the helper.",
|
|
41
46
|
"Select vertical slice — the active wave plan (or single ready slice) defines work. Do not ask \"which slice next?\" when the plan already resolves it. Before starting, read `.cclaw/state/ralph-loop.json` (`loopIteration`, `acClosed[]`, `redOpenSlices[]`) so you skip cycles already closed. If `redOpenSlices[]` is non-empty, repair or explicitly park those slices before opening a new RED.",
|
|
42
47
|
"Map to acceptance criterion — identify the specific spec criterion this test proves.",
|
|
43
48
|
"Discover the test surface — inspect existing tests, fixtures, helpers, test commands, and nearby assertions before authoring RED. Reuse the local test style unless the slice genuinely needs a new pattern.",
|
|
@@ -49,8 +54,8 @@ export const TDD = {
|
|
|
49
54
|
"GREEN: Run full suite — execute ALL tests, not just the ones you wrote. The full suite must be GREEN.",
|
|
50
55
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
51
56
|
"Run verification-before-completion discipline for the slice — capture a fresh test command, explicit PASS/FAIL status, and a config-aware ref (commit SHA when VCS is present/required, or no-vcs attestation when allowed).",
|
|
52
|
-
"REFACTOR (v6.14.0 — three forms): (1) re-dispatch `slice-implementer` with `--phase refactor` after GREEN; (2) re-dispatch with `--phase refactor-deferred --refactor-rationale \"<why>\"` to close without a separate pass; (3) **fold REFACTOR into GREEN** by adding `--refactor-outcome=inline|deferred [--refactor-rationale=\"<why>\"]` on the same `slice-implementer --phase green`
|
|
53
|
-
"DOC (v6.14.0
|
|
57
|
+
"REFACTOR (v6.14.0+ — three forms): (1) re-dispatch `slice-implementer` with `--phase refactor` after GREEN; (2) re-dispatch with `--phase refactor-deferred --refactor-rationale \"<why>\"` to close without a separate pass; (3) **fold REFACTOR into GREEN** by adding `--refactor-outcome=inline|deferred [--refactor-rationale=\"<why>\"]` on the same `slice-implementer --phase green` `--status=completed` write. Form (3) is the v6.14.0 default for new projects; the linter accepts all three as REFACTOR coverage. Form (1) is the only legal form when BOTH `legacyContinuation: true` AND `flow-state.json::tddCheckpointMode: \"global-red\"` are set (legacy hox-shape projects); other projects may use any form. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
|
|
58
|
+
"DOC (v6.14.0+ softened, v6.14.1 inline-opt-in): in `discoveryMode=deep` runs DOC remains mandatory — dispatch `slice-documenter --slice S-<id> --phase doc --paths <artifacts-dir>/tdd-slices/S-<id>.md` IN PARALLEL with `slice-implementer --phase green` for the same slice (ONE message with TWO concurrent Task calls). The documenter only writes `tdd-slices/S-<id>.md`, so its `--paths` are disjoint from the implementer's production paths and the file-overlap scheduler auto-allows parallel dispatch. **In `lean` and `guided` modes DOC is advisory** (linter `tdd_slice_documenter_missing` becomes `required: false`); the controller MAY either keep parallel `slice-documenter` dispatch (default — preserves the documenter's isolated context) OR, **for single-slice non-deep waves**, call `slice-implementer --finalize-doc --slice S-<id> --paths <artifacts-dir>/tdd-slices/S-<id>.md` inline after GREEN completes. Multi-slice waves keep parallel `slice-documenter` regardless of mode. **Provisional-then-finalize still applies for parallel dispatch:** append a provisional row/section in `tdd-slices/S-<id>.md` at dispatch time, then finalize after the matching `phase=green` event records evidence.",
|
|
54
59
|
"**slice-documenter writes per-slice prose** (test discovery, system-wide impact check, RED/GREEN/REFACTOR notes, acceptance mapping, failure analysis) into `tdd-slices/S-<id>.md`. Controller does NOT touch this content. When logging a `green` row, attach the closed acceptance-criterion IDs in `acIds` so Ralph Loop status counts them.",
|
|
55
60
|
"Annotate traceability — link to the active track's source: plan task ID + spec criterion on standard/medium, or spec acceptance item / bug reproduction slice on quick.",
|
|
56
61
|
"**Boundary with review (do NOT escalate single-slice findings to whole-diff review).** `tdd.Per-Slice Review` OWNS severity-classified findings WITHIN one slice (correctness, edge cases, regression). `review` OWNS whole-diff Layer 1 (spec compliance) plus Layer 2 (cross-slice integration, security sweep, dependency/version audit, observability). When a single-slice finding genuinely needs whole-diff escalation, surface it in `06-tdd.md > Per-Slice Review` first; review will cite it (not re-classify) and the cross-artifact-duplication linter requires matching severity/disposition.",
|
|
@@ -58,9 +63,9 @@ export const TDD = {
|
|
|
58
63
|
"Repeat for each slice — when not in multi-slice wave mode, return to wave-plan discovery; otherwise continue the active wave until members close.",
|
|
59
64
|
],
|
|
60
65
|
interactionProtocol: [
|
|
61
|
-
"Pick one vertical slice at a time **only when** the merged wave plan leaves a single scheduler-ready slice or the operator chose single-slice mode. Parallel implementers are allowed when lanes touch non-overlapping files (the file-overlap scheduler auto-allows parallel when `--paths` are disjoint). **Integration-overseer is conditional in v6.14.0** (see `flow-state.json::integrationOverseerMode`): with the default `\"conditional\"` it dispatches only when `integrationCheckRequired()` returns `required: true` (shared import boundaries between closed slices, any slice with `riskTier=high`, or a recorded `cclaw_fanin_conflict`). When the heuristic returns `required: false`, record an audit `cclaw_integration_overseer_skipped` and let the linter emit advisory `tdd_integration_overseer_skipped_by_disjoint_paths`. Projects with `legacyContinuation: true` or explicit `\"always\"` keep the v6.13.x mandatory dispatch.",
|
|
66
|
+
"Pick one vertical slice at a time **only when** the merged wave plan leaves a single scheduler-ready slice or the operator chose single-slice mode. Parallel implementers are allowed when lanes touch non-overlapping files (the file-overlap scheduler auto-allows parallel when `--paths` are disjoint). **Integration-overseer is conditional in v6.14.0** (see `flow-state.json::integrationOverseerMode`): with the default `\"conditional\"` it dispatches only when `integrationCheckRequired()` returns `required: true` (shared import boundaries between closed slices, any slice with `riskTier=high`, or a recorded `cclaw_fanin_conflict`). When the heuristic returns `required: false`, record an audit `cclaw_integration_overseer_skipped` (via `delegation-record --audit-kind=cclaw_integration_overseer_skipped --audit-reason=\"<reasons>\"`) and let the linter emit advisory `tdd_integration_overseer_skipped_by_disjoint_paths`. Projects with `legacyContinuation: true` or explicit `\"always\"` keep the v6.13.x mandatory dispatch.",
|
|
62
67
|
"Slice implementers are sequential only when the plan serializes work; prefer wave-parallel GREEN+DOC when the Parallel Execution Plan marks multiple ready members.",
|
|
63
|
-
"Controller owns orchestration. For each slice S-<id>, dispatch in
|
|
68
|
+
"Controller owns orchestration. **v6.14.1 — record BEFORE dispatch:** every controller `Task` dispatch is preceded by two `delegation-record` writes (`--status=scheduled` then `--status=launched`); workers self-record `--status=acknowledged` on entry and `--status=completed` on exit. Never dispatch first and back-fill — that order breaks the active-span check and forces `--allow-parallel` workarounds. For each slice S-<id>, dispatch in order: (1) `test-author --slice S-<id> --phase red` (RED-only, no production edits), (2) `slice-implementer --slice S-<id> --phase green --paths <comma-separated>` for GREEN, (3) re-dispatch `--phase refactor` or `--phase refactor-deferred --refactor-rationale \"<why>\"` to close REFACTOR. Each dispatch records a row in `delegation-events.jsonl` and the linter auto-derives the Watched-RED + Vertical Slice Cycle tables — do NOT hand-edit those tables.",
|
|
64
69
|
"Before writing RED tests, discover relevant existing tests and commands so the new test extends the suite instead of fighting it.",
|
|
65
70
|
"Before implementation, perform a system-wide impact check across callbacks, state, interfaces, schemas, and external contracts touched by the slice.",
|
|
66
71
|
"Slice-documenter (mandatory v6.12.0, regardless of `discoveryMode`): in PARALLEL with `slice-implementer --phase green`, dispatch `slice-documenter --slice S-<id> --phase doc` whose only `claimedPaths` is `<artifacts-dir>/tdd-slices/S-<id>.md`. The two dispatches run concurrently because their paths are disjoint. The documenter writes per-slice prose so the main `06-tdd.md` stays thin. Controller MUST NOT author per-slice prose; controller MUST NOT author GREEN production code (use `slice-implementer`).",
|
|
@@ -110,7 +115,7 @@ export const TDD = {
|
|
|
110
115
|
"Relevant existing test files, helpers, fixtures, and exact commands identified before RED.",
|
|
111
116
|
"Callbacks, state transitions, interfaces, schemas, and contracts checked for impact before implementation.",
|
|
112
117
|
"Execution posture and vertical-slice RED/GREEN/REFACTOR checkpoint plan recorded, including commit boundaries when the repo workflow supports them.",
|
|
113
|
-
"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`.",
|
|
114
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.",
|
|
115
120
|
"REFACTOR observability: per slice, a `phase=refactor` event OR a `phase=refactor-deferred` event whose evidenceRefs / refactor rationale captures why refactor was deferred.",
|
|
116
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.",
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -136,6 +136,29 @@ export interface FlowState {
|
|
|
136
136
|
* sync hasn't visited yet.
|
|
137
137
|
*/
|
|
138
138
|
tddCutoverSliceId?: string;
|
|
139
|
+
/**
|
|
140
|
+
* v6.14.2 — boundary slice id at which worktree-first protocol began
|
|
141
|
+
* applying. `cclaw-cli sync` auto-stamps this when
|
|
142
|
+
* `legacyContinuation: true` AND `worktreeExecutionMode: "worktree-first"`
|
|
143
|
+
* AND the value is not already set.
|
|
144
|
+
*
|
|
145
|
+
* Detection rule (v6.14.2): the highest `S-N` among slices with at
|
|
146
|
+
* least one completed `slice-implementer` row in the active run that
|
|
147
|
+
* carries NONE of the worktree-first metadata fields (`claimToken`,
|
|
148
|
+
* `ownerLaneId`, `leasedUntil`). When no such slice exists, sync
|
|
149
|
+
* falls back to `tddCutoverSliceId` so legacy v6.12 cutover marks
|
|
150
|
+
* still confer the exemption.
|
|
151
|
+
*
|
|
152
|
+
* Effect: closed slices whose numeric id is `<= tddWorktreeCutoverSliceId`
|
|
153
|
+
* AND whose `slice-implementer` rows in the active run lack ALL
|
|
154
|
+
* three worktree fields are exempt from `tdd_slice_lane_metadata_missing`,
|
|
155
|
+
* `tdd_slice_claim_token_missing`, and `tdd_lease_expired_unreclaimed`.
|
|
156
|
+
*
|
|
157
|
+
* One-shot: subsequent sync runs leave the value untouched. Operators
|
|
158
|
+
* may pin it earlier/later by direct edit + `cclaw-cli internal
|
|
159
|
+
* flow-state-repair --reason=<slug>`.
|
|
160
|
+
*/
|
|
161
|
+
tddWorktreeCutoverSliceId?: string;
|
|
139
162
|
/**
|
|
140
163
|
* v6.13.0 — when `worktree-first` (default for newly initialized runs),
|
|
141
164
|
* slice-implementer work happens in isolated git worktrees with explicit
|
|
@@ -185,6 +208,20 @@ export interface FlowState {
|
|
|
185
208
|
* Omitted on legacy state files (treated as `"always"`).
|
|
186
209
|
*/
|
|
187
210
|
integrationOverseerMode?: "conditional" | "always";
|
|
211
|
+
/**
|
|
212
|
+
* v6.14.2 — minimum elapsed milliseconds between `acknowledged` and
|
|
213
|
+
* `completed` for a `slice-implementer --phase green` row. The hook
|
|
214
|
+
* helper rejects fast-greens (`completedTs - ackTs < this`) with
|
|
215
|
+
* `green_evidence_too_fresh` unless the dispatch carries
|
|
216
|
+
* `--allow-fast-green --green-mode=observational`.
|
|
217
|
+
*
|
|
218
|
+
* Default 4000ms when omitted (see `effectiveTddGreenMinElapsedMs`).
|
|
219
|
+
* Operators tuning the floor for very fast suites may set it lower
|
|
220
|
+
* (e.g. `1500`) or set it to `0` to disable the check entirely while
|
|
221
|
+
* keeping the other Fix 4 contracts (RED test name match, passing
|
|
222
|
+
* assertion line) active.
|
|
223
|
+
*/
|
|
224
|
+
tddGreenMinElapsedMs?: number;
|
|
188
225
|
}
|
|
189
226
|
/**
|
|
190
227
|
* Effective worktree mode: legacy state files without the field keep
|
|
@@ -202,6 +239,15 @@ export declare function effectiveTddCheckpointMode(state: FlowState): "per-slice
|
|
|
202
239
|
* the field default to `always` (matches v6.13 behavior).
|
|
203
240
|
*/
|
|
204
241
|
export declare function effectiveIntegrationOverseerMode(state: FlowState): "conditional" | "always";
|
|
242
|
+
export declare const DEFAULT_TDD_GREEN_MIN_ELAPSED_MS = 4000;
|
|
243
|
+
/**
|
|
244
|
+
* v6.14.2 — effective minimum GREEN elapsed window in milliseconds.
|
|
245
|
+
* Returns the per-project override when present and finite; otherwise
|
|
246
|
+
* the documented 4000ms default. Negative values or NaN fall through
|
|
247
|
+
* to the default so a hand-edited `flow-state.json` cannot accidentally
|
|
248
|
+
* disable the check via `-1` or `"oops"`.
|
|
249
|
+
*/
|
|
250
|
+
export declare function effectiveTddGreenMinElapsedMs(state: FlowState): number;
|
|
205
251
|
export interface StageInteractionHint {
|
|
206
252
|
skipQuestions?: boolean;
|
|
207
253
|
sourceStage?: FlowStage;
|
package/dist/flow-state.js
CHANGED
|
@@ -69,6 +69,24 @@ export function effectiveTddCheckpointMode(state) {
|
|
|
69
69
|
export function effectiveIntegrationOverseerMode(state) {
|
|
70
70
|
return state.integrationOverseerMode === "conditional" ? "conditional" : "always";
|
|
71
71
|
}
|
|
72
|
+
export const DEFAULT_TDD_GREEN_MIN_ELAPSED_MS = 4000;
|
|
73
|
+
/**
|
|
74
|
+
* v6.14.2 — effective minimum GREEN elapsed window in milliseconds.
|
|
75
|
+
* Returns the per-project override when present and finite; otherwise
|
|
76
|
+
* the documented 4000ms default. Negative values or NaN fall through
|
|
77
|
+
* to the default so a hand-edited `flow-state.json` cannot accidentally
|
|
78
|
+
* disable the check via `-1` or `"oops"`.
|
|
79
|
+
*/
|
|
80
|
+
export function effectiveTddGreenMinElapsedMs(state) {
|
|
81
|
+
const raw = state.tddGreenMinElapsedMs;
|
|
82
|
+
if (typeof raw !== "number")
|
|
83
|
+
return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
|
|
84
|
+
if (!Number.isFinite(raw))
|
|
85
|
+
return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
|
|
86
|
+
if (raw < 0)
|
|
87
|
+
return DEFAULT_TDD_GREEN_MIN_ELAPSED_MS;
|
|
88
|
+
return Math.floor(raw);
|
|
89
|
+
}
|
|
72
90
|
export function isFlowTrack(value) {
|
|
73
91
|
return typeof value === "string" && FLOW_TRACKS.includes(value);
|
|
74
92
|
}
|
package/dist/install.js
CHANGED
|
@@ -1005,12 +1005,25 @@ async function applyPlanLegacyContinuationIfNeeded(projectRoot) {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
/**
|
|
1007
1007
|
* v6.14.0 — set stream-style defaults on `cclaw-cli sync` and print a
|
|
1008
|
-
* one-line hint when defaults change.
|
|
1008
|
+
* one-line hint when defaults change.
|
|
1009
1009
|
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1010
|
+
* v6.14.2 update — flip the legacyContinuation defaults from
|
|
1011
|
+
* `global-red`/`always` to `per-slice`/`conditional`. Rationale: hox-shape
|
|
1012
|
+
* projects ran into S-17 misroutes precisely because the default
|
|
1013
|
+
* preserved the v6.12 wave barrier even after the project itself had
|
|
1014
|
+
* moved to stream-mode. Existing flow-state values are NEVER overwritten
|
|
1015
|
+
* — operators who want to pin `global-red`/`always` may set them
|
|
1016
|
+
* explicitly via `cclaw-cli internal set-checkpoint-mode global-red` and
|
|
1017
|
+
* `set-integration-overseer-mode always`.
|
|
1018
|
+
*
|
|
1019
|
+
* Strategy:
|
|
1020
|
+
*
|
|
1021
|
+
* - When `legacyContinuation: true` and `tddCheckpointMode` is unset,
|
|
1022
|
+
* default to `tddCheckpointMode: "per-slice"` (v6.14.2 — was
|
|
1023
|
+
* `global-red` in v6.14.0/v6.14.1).
|
|
1024
|
+
* - When `legacyContinuation: true` and `integrationOverseerMode` is
|
|
1025
|
+
* unset, default to `integrationOverseerMode: "conditional"` (v6.14.2
|
|
1026
|
+
* — was `always` in v6.14.0/v6.14.1).
|
|
1014
1027
|
* - When `legacyContinuation` is NOT true (new / standard projects) and
|
|
1015
1028
|
* neither field is set, default to `tddCheckpointMode: "per-slice"`,
|
|
1016
1029
|
* `integrationOverseerMode: "conditional"`. Also default
|
|
@@ -1053,12 +1066,12 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
|
|
|
1053
1066
|
const legacyContinuation = obj.legacyContinuation === true;
|
|
1054
1067
|
if (legacyContinuation) {
|
|
1055
1068
|
if (!tddCheckpointModeSet) {
|
|
1056
|
-
updates.tddCheckpointMode = "
|
|
1057
|
-
summary.push("tddCheckpointMode=
|
|
1069
|
+
updates.tddCheckpointMode = "per-slice";
|
|
1070
|
+
summary.push("tddCheckpointMode=per-slice (legacyContinuation, v6.14.2 default flip)");
|
|
1058
1071
|
}
|
|
1059
1072
|
if (!integrationOverseerModeSet) {
|
|
1060
|
-
updates.integrationOverseerMode = "
|
|
1061
|
-
summary.push("integrationOverseerMode=
|
|
1073
|
+
updates.integrationOverseerMode = "conditional";
|
|
1074
|
+
summary.push("integrationOverseerMode=conditional (legacyContinuation, v6.14.2 default flip)");
|
|
1062
1075
|
}
|
|
1063
1076
|
}
|
|
1064
1077
|
else {
|
|
@@ -1085,7 +1098,124 @@ async function applyV614DefaultsIfNeeded(projectRoot) {
|
|
|
1085
1098
|
catch {
|
|
1086
1099
|
return null;
|
|
1087
1100
|
}
|
|
1088
|
-
return `v6.14.
|
|
1101
|
+
return `v6.14.2 stream-style defaults applied: ${summary.join(", ")}. To opt out, run cclaw-cli internal set-checkpoint-mode global-red --reason="..." and/or cclaw-cli internal set-integration-overseer-mode always --reason="...".`;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* v6.14.2 — auto-stamp `tddWorktreeCutoverSliceId` for legacyContinuation
|
|
1105
|
+
* projects in worktree-first mode that haven't yet recorded a boundary.
|
|
1106
|
+
*
|
|
1107
|
+
* Detection ("any-metadata" rule): scan the active run's
|
|
1108
|
+
* `slice-implementer` rows. The boundary is the highest `S-N` whose
|
|
1109
|
+
* rows for the active run lack ALL of `claimToken`, `ownerLaneId`, and
|
|
1110
|
+
* `leasedUntil`. When no such slice exists (every slice carries at
|
|
1111
|
+
* least one worktree field), fall back to `tddCutoverSliceId` so the
|
|
1112
|
+
* v6.12 cutover marker still confers exemption.
|
|
1113
|
+
*
|
|
1114
|
+
* Idempotent: when the field is already set, returns null without
|
|
1115
|
+
* writing. Best-effort: read failures, missing ledger, or unparseable
|
|
1116
|
+
* rows all short-circuit silently — the existing flow-state.json is
|
|
1117
|
+
* never corrupted.
|
|
1118
|
+
*
|
|
1119
|
+
* Returns a one-line hint string (or `null` if nothing changed).
|
|
1120
|
+
*/
|
|
1121
|
+
async function applyV6142WorktreeCutoverIfNeeded(projectRoot) {
|
|
1122
|
+
const flowStatePath = runtimePath(projectRoot, "state", "flow-state.json");
|
|
1123
|
+
let flowStateRaw;
|
|
1124
|
+
try {
|
|
1125
|
+
flowStateRaw = await fs.readFile(flowStatePath, "utf8");
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
let parsed;
|
|
1131
|
+
try {
|
|
1132
|
+
parsed = JSON.parse(flowStateRaw);
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
const obj = parsed;
|
|
1141
|
+
if (obj.legacyContinuation !== true)
|
|
1142
|
+
return null;
|
|
1143
|
+
if (obj.worktreeExecutionMode !== "worktree-first")
|
|
1144
|
+
return null;
|
|
1145
|
+
if (typeof obj.tddWorktreeCutoverSliceId === "string" &&
|
|
1146
|
+
obj.tddWorktreeCutoverSliceId.trim().length > 0) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
const ledgerPath = runtimePath(projectRoot, "state", "delegation-log.json");
|
|
1150
|
+
const activeRunId = typeof obj.activeRunId === "string" ? obj.activeRunId : "";
|
|
1151
|
+
let ledgerRaw;
|
|
1152
|
+
try {
|
|
1153
|
+
ledgerRaw = await fs.readFile(ledgerPath, "utf8");
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
ledgerRaw = "";
|
|
1157
|
+
}
|
|
1158
|
+
let ledgerParsed = null;
|
|
1159
|
+
if (ledgerRaw.length > 0) {
|
|
1160
|
+
try {
|
|
1161
|
+
ledgerParsed = JSON.parse(ledgerRaw);
|
|
1162
|
+
}
|
|
1163
|
+
catch {
|
|
1164
|
+
ledgerParsed = null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const entries = ledgerParsed &&
|
|
1168
|
+
typeof ledgerParsed === "object" &&
|
|
1169
|
+
!Array.isArray(ledgerParsed) &&
|
|
1170
|
+
Array.isArray(ledgerParsed.entries)
|
|
1171
|
+
? ledgerParsed.entries
|
|
1172
|
+
: [];
|
|
1173
|
+
let boundary = -1;
|
|
1174
|
+
for (const entry of entries) {
|
|
1175
|
+
if (entry.agent !== "slice-implementer")
|
|
1176
|
+
continue;
|
|
1177
|
+
if (entry.status !== "completed")
|
|
1178
|
+
continue;
|
|
1179
|
+
if (typeof entry.sliceId !== "string")
|
|
1180
|
+
continue;
|
|
1181
|
+
if (activeRunId && entry.runId && entry.runId !== activeRunId)
|
|
1182
|
+
continue;
|
|
1183
|
+
const tok = typeof entry.claimToken === "string" ? entry.claimToken.trim() : "";
|
|
1184
|
+
const lane = typeof entry.ownerLaneId === "string" ? entry.ownerLaneId.trim() : "";
|
|
1185
|
+
const lease = typeof entry.leasedUntil === "string" ? entry.leasedUntil.trim() : "";
|
|
1186
|
+
if (tok.length > 0 || lane.length > 0 || lease.length > 0)
|
|
1187
|
+
continue;
|
|
1188
|
+
const m = /^S-(\d+)$/u.exec(entry.sliceId);
|
|
1189
|
+
if (!m)
|
|
1190
|
+
continue;
|
|
1191
|
+
const n = Number.parseInt(m[1], 10);
|
|
1192
|
+
if (!Number.isFinite(n))
|
|
1193
|
+
continue;
|
|
1194
|
+
if (n > boundary)
|
|
1195
|
+
boundary = n;
|
|
1196
|
+
}
|
|
1197
|
+
let stamped = null;
|
|
1198
|
+
if (boundary >= 0) {
|
|
1199
|
+
stamped = `S-${boundary}`;
|
|
1200
|
+
}
|
|
1201
|
+
else if (typeof obj.tddCutoverSliceId === "string" &&
|
|
1202
|
+
/^S-\d+$/u.test(obj.tddCutoverSliceId)) {
|
|
1203
|
+
stamped = obj.tddCutoverSliceId;
|
|
1204
|
+
}
|
|
1205
|
+
if (!stamped)
|
|
1206
|
+
return null;
|
|
1207
|
+
const merged = { ...obj, tddWorktreeCutoverSliceId: stamped };
|
|
1208
|
+
try {
|
|
1209
|
+
await writeFileSafe(flowStatePath, `${JSON.stringify(merged, null, 2)}\n`, {
|
|
1210
|
+
mode: 0o600
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
return (`v6.14.2 stamped tddWorktreeCutoverSliceId=${stamped}; closed slices ≤ ${stamped} ` +
|
|
1217
|
+
"are exempt from worktree-first findings under legacyContinuation. " +
|
|
1218
|
+
"Edit .cclaw/state/flow-state.json to override (advisory).");
|
|
1089
1219
|
}
|
|
1090
1220
|
async function cleanLegacyArtifacts(projectRoot) {
|
|
1091
1221
|
for (const legacyFolder of DEPRECATED_UTILITY_SKILL_FOLDERS) {
|
|
@@ -1266,6 +1396,10 @@ async function materializeRuntime(projectRoot, config, forceStateReset, operatio
|
|
|
1266
1396
|
if (v614Hint) {
|
|
1267
1397
|
process.stdout.write(`cclaw: ${v614Hint}\n`);
|
|
1268
1398
|
}
|
|
1399
|
+
const v6142Hint = await applyV6142WorktreeCutoverIfNeeded(projectRoot);
|
|
1400
|
+
if (v6142Hint) {
|
|
1401
|
+
process.stdout.write(`cclaw: ${v6142Hint}\n`);
|
|
1402
|
+
}
|
|
1269
1403
|
}
|
|
1270
1404
|
try {
|
|
1271
1405
|
await ensureRunSystem(projectRoot, { createIfMissing: false });
|