cclaw-cli 6.14.0 → 6.14.1
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.
|
@@ -448,36 +448,69 @@ export async function lintTddStage(ctx) {
|
|
|
448
448
|
// `integrationCheckRequired()` returns required=false, the gate is
|
|
449
449
|
// soft (advisory) and an audit-only finding is emitted so the
|
|
450
450
|
// controller can record the deliberate skip in artifacts.
|
|
451
|
+
//
|
|
452
|
+
// v6.14.1 — also surface the audit row presence. When the controller
|
|
453
|
+
// skips `integration-overseer` dispatch (or the heuristic returns
|
|
454
|
+
// false), the run log MUST contain a
|
|
455
|
+
// `cclaw_integration_overseer_skipped` audit row for traceability.
|
|
456
|
+
// The advisory `tdd_integration_overseer_skipped_audit_missing`
|
|
457
|
+
// surfaces a missing audit row when 2+ closed slices closed without
|
|
458
|
+
// any overseer dispatch AND no audit was recorded.
|
|
451
459
|
let overseerVerdict = null;
|
|
452
460
|
let overseerRequired = true;
|
|
461
|
+
const skippedAuditRowCount = await countIntegrationOverseerSkippedAudits(projectRoot, delegationLedger.runId);
|
|
462
|
+
const skippedAuditRowFound = skippedAuditRowCount > 0;
|
|
453
463
|
if (integrationOverseerMode === "conditional") {
|
|
454
464
|
const eventsForVerdict = runEvents.length > 0 ? runEvents : [];
|
|
455
465
|
const auditsForVerdict = fanInAudits.filter((a) => a.runId === delegationLedger.runId);
|
|
456
466
|
overseerVerdict = integrationCheckRequired(eventsForVerdict, auditsForVerdict);
|
|
457
467
|
overseerRequired = overseerVerdict.required;
|
|
458
468
|
if (!overseerVerdict.required) {
|
|
469
|
+
const auditRowSuffix = skippedAuditRowFound
|
|
470
|
+
? "audit row recorded — skip is fully traceable."
|
|
471
|
+
: "audit row MISSING — controller should append `cclaw_integration_overseer_skipped` for traceability (see `tdd_integration_overseer_skipped_audit_missing`).";
|
|
459
472
|
findings.push({
|
|
460
473
|
section: "tdd_integration_overseer_skipped_by_disjoint_paths",
|
|
461
474
|
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.",
|
|
475
|
+
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
476
|
found: true,
|
|
464
|
-
details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe —
|
|
477
|
+
details: `integrationCheckRequired() reasons: ${overseerVerdict.reasons.join(", ")}. Skip is safe — ${auditRowSuffix}`
|
|
465
478
|
});
|
|
466
479
|
}
|
|
467
480
|
}
|
|
481
|
+
// v6.14.1 — `tdd_integration_overseer_skipped_audit_missing` (advisory).
|
|
482
|
+
// Fires when fan-out is detected (2+ completed slice-implementers),
|
|
483
|
+
// no `integration-overseer` was dispatched at all (no scheduled or
|
|
484
|
+
// completed row for the active run), AND no
|
|
485
|
+
// `cclaw_integration_overseer_skipped` audit row exists. This pairs
|
|
486
|
+
// with the controller skill text rule that the wave-closure decision
|
|
487
|
+
// ("dispatch overseer or skip") MUST leave a trail.
|
|
488
|
+
const overseerDispatched = activeRunEntries.some((entry) => entry.agent === "integration-overseer");
|
|
489
|
+
if (!overseerDispatched && !skippedAuditRowFound) {
|
|
490
|
+
findings.push({
|
|
491
|
+
section: "tdd_integration_overseer_skipped_audit_missing",
|
|
492
|
+
required: false,
|
|
493
|
+
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.",
|
|
494
|
+
found: false,
|
|
495
|
+
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. ` +
|
|
496
|
+
"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."
|
|
497
|
+
});
|
|
498
|
+
}
|
|
468
499
|
findings.push({
|
|
469
500
|
section: "tdd.integration_overseer_missing",
|
|
470
501
|
required: overseerRequired,
|
|
471
502
|
rule: overseerRequired
|
|
472
503
|
? "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.",
|
|
504
|
+
: "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
505
|
found: integrationOverseerFound,
|
|
475
506
|
details: integrationOverseerFound
|
|
476
507
|
? "integration-overseer completion recorded with PASS/PASS_WITH_GAPS evidence."
|
|
477
508
|
: completedOverseerRows.length === 0
|
|
478
509
|
? overseerRequired
|
|
479
510
|
? "Fan-out detected but no completed integration-overseer delegation row exists for active run."
|
|
480
|
-
:
|
|
511
|
+
: skippedAuditRowFound
|
|
512
|
+
? "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths) and `cclaw_integration_overseer_skipped` audit row recorded. Audit-only."
|
|
513
|
+
: "Fan-out detected; integration-overseer not dispatched (conditional mode skipped on disjoint paths). Audit-only."
|
|
481
514
|
: "integration-overseer completion exists, but PASS/PASS_WITH_GAPS evidence is missing in delegation evidenceRefs and artifact text."
|
|
482
515
|
});
|
|
483
516
|
}
|
|
@@ -551,6 +584,46 @@ export async function lintTddStage(ctx) {
|
|
|
551
584
|
}
|
|
552
585
|
}
|
|
553
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* v6.14.1 — count `cclaw_integration_overseer_skipped` audit rows in
|
|
589
|
+
* `delegation-events.jsonl` for a given runId. The audit row is not a
|
|
590
|
+
* `DelegationEvent` (no agent/status), so `readDelegationEvents`
|
|
591
|
+
* filters it out; we re-scan the raw file with a narrow JSON match.
|
|
592
|
+
*
|
|
593
|
+
* Best-effort: missing file or parse errors return 0.
|
|
594
|
+
*/
|
|
595
|
+
async function countIntegrationOverseerSkippedAudits(projectRoot, runId) {
|
|
596
|
+
const filePath = path.join(projectRoot, ".cclaw/state/delegation-events.jsonl");
|
|
597
|
+
let raw = "";
|
|
598
|
+
try {
|
|
599
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
let count = 0;
|
|
605
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
606
|
+
const trimmed = line.trim();
|
|
607
|
+
if (trimmed.length === 0)
|
|
608
|
+
continue;
|
|
609
|
+
let parsed;
|
|
610
|
+
try {
|
|
611
|
+
parsed = JSON.parse(trimmed);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
617
|
+
continue;
|
|
618
|
+
const obj = parsed;
|
|
619
|
+
if (obj.event !== "cclaw_integration_overseer_skipped")
|
|
620
|
+
continue;
|
|
621
|
+
if (typeof obj.runId === "string" && obj.runId !== runId)
|
|
622
|
+
continue;
|
|
623
|
+
count += 1;
|
|
624
|
+
}
|
|
625
|
+
return count;
|
|
626
|
+
}
|
|
554
627
|
async function listSliceFiles(slicesDir) {
|
|
555
628
|
let entries = [];
|
|
556
629
|
try {
|
|
@@ -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.1)
|
|
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. \`--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.
|
package/dist/content/hooks.js
CHANGED
|
@@ -344,6 +344,7 @@ function usage() {
|
|
|
344
344
|
" 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
345
|
" 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
346
|
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\\\"<why>\\\" [--json]",
|
|
347
|
+
" 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
348
|
"",
|
|
348
349
|
"Allowed --dispatch-surface values:",
|
|
349
350
|
" " + VALID_DISPATCH_SURFACES.join(", "),
|
|
@@ -898,6 +899,74 @@ async function findLegacyEntry(root, spanId) {
|
|
|
898
899
|
return ledger.entries.find((entry) => entry && entry.spanId === spanId) || null;
|
|
899
900
|
}
|
|
900
901
|
|
|
902
|
+
// v6.14.1 — allow-list of non-delegation audit events the controller
|
|
903
|
+
// can emit via the helper. Keep in sync with NON_DELEGATION_AUDIT_EVENTS
|
|
904
|
+
// in src/delegation.ts.
|
|
905
|
+
const VALID_AUDIT_KINDS = new Set([
|
|
906
|
+
"cclaw_integration_overseer_skipped"
|
|
907
|
+
]);
|
|
908
|
+
|
|
909
|
+
async function runAuditEmit(args, json) {
|
|
910
|
+
const kind = String(args["audit-kind"]).trim();
|
|
911
|
+
if (!VALID_AUDIT_KINDS.has(kind)) {
|
|
912
|
+
emitProblems([
|
|
913
|
+
"invalid --audit-kind: " + kind +
|
|
914
|
+
" (allowed: " + [...VALID_AUDIT_KINDS].join(", ") + ")"
|
|
915
|
+
], json, 2);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const root = await detectRoot();
|
|
919
|
+
const runId = await readRunId(root);
|
|
920
|
+
const reason = typeof args["audit-reason"] === "string"
|
|
921
|
+
? args["audit-reason"].trim()
|
|
922
|
+
: "";
|
|
923
|
+
const sliceIdsRaw = typeof args["slice-ids"] === "string"
|
|
924
|
+
? args["slice-ids"].trim()
|
|
925
|
+
: "";
|
|
926
|
+
const sliceIds = sliceIdsRaw.length > 0
|
|
927
|
+
? sliceIdsRaw
|
|
928
|
+
.split(",")
|
|
929
|
+
.map((value) => value.trim())
|
|
930
|
+
.filter((value) => value.length > 0)
|
|
931
|
+
: [];
|
|
932
|
+
const ts = new Date().toISOString();
|
|
933
|
+
const payload = {
|
|
934
|
+
event: kind,
|
|
935
|
+
runId,
|
|
936
|
+
ts,
|
|
937
|
+
eventTs: ts,
|
|
938
|
+
...(reason.length > 0 ? { reasons: reason.split(",").map((r) => r.trim()).filter((r) => r.length > 0) } : {}),
|
|
939
|
+
...(sliceIds.length > 0 ? { sliceIds } : {})
|
|
940
|
+
};
|
|
941
|
+
const stateDir = path.join(root, RUNTIME_ROOT, "state");
|
|
942
|
+
try {
|
|
943
|
+
await fs.mkdir(stateDir, { recursive: true });
|
|
944
|
+
await fs.appendFile(
|
|
945
|
+
path.join(stateDir, "delegation-events.jsonl"),
|
|
946
|
+
JSON.stringify(payload) + "\\n",
|
|
947
|
+
{ encoding: "utf8", mode: 0o600 }
|
|
948
|
+
);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
const message = error && typeof error === "object" && "message" in error
|
|
951
|
+
? String(error.message)
|
|
952
|
+
: String(error);
|
|
953
|
+
emitErrorJson("audit_emit_failed", { kind, message }, json);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (json) {
|
|
957
|
+
process.stdout.write(JSON.stringify({
|
|
958
|
+
ok: true,
|
|
959
|
+
command: "audit-emit",
|
|
960
|
+
auditKind: kind,
|
|
961
|
+
runId,
|
|
962
|
+
sliceIds,
|
|
963
|
+
ts
|
|
964
|
+
}, null, 2) + "\\n");
|
|
965
|
+
} else {
|
|
966
|
+
process.stdout.write("[cclaw] audit emitted: " + kind + " (run=" + runId + ", ts=" + ts + ")\\n");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
901
970
|
async function runRerecord(args, json) {
|
|
902
971
|
const problems = [];
|
|
903
972
|
for (const key of ["span-id", "dispatch-id", "dispatch-surface", "agent-definition-path"]) {
|
|
@@ -1130,6 +1199,18 @@ async function main() {
|
|
|
1130
1199
|
return;
|
|
1131
1200
|
}
|
|
1132
1201
|
|
|
1202
|
+
// v6.14.1 — audit-only emit path. When the controller wants to record
|
|
1203
|
+
// a non-delegation audit row (e.g. \`cclaw_integration_overseer_skipped\`
|
|
1204
|
+
// when the wave heuristic chose to skip the overseer dispatch), pass
|
|
1205
|
+
// --audit-kind=<event-name> [--audit-reason=<text>] [--slice-ids=<csv>]
|
|
1206
|
+
// and the helper appends a single line to delegation-events.jsonl
|
|
1207
|
+
// without touching the lifecycle ledger. The kind must be in the
|
|
1208
|
+
// canonical allow-list so a typo cannot inject an unrecognized event.
|
|
1209
|
+
if (typeof args["audit-kind"] === "string" && args["audit-kind"].trim().length > 0) {
|
|
1210
|
+
await runAuditEmit(args, json);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1133
1214
|
const problems = [];
|
|
1134
1215
|
if (!args.stage) problems.push("missing --stage");
|
|
1135
1216
|
if (!args.agent) problems.push("missing --agent");
|
|
@@ -38,6 +38,10 @@ export const TDD = {
|
|
|
38
38
|
executionModel: {
|
|
39
39
|
checklist: [
|
|
40
40
|
"**Stream-style wave dispatch (v6.14.0):** Before routing, read the Parallel Execution Plan (managed block in the track planning artifact) and `<artifacts-dir>/wave-plans/`. 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). Resume partial waves by parallelizing remaining members only (see top-of-skill `## Wave Batch Mode`).",
|
|
41
|
+
"**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
|
+
"**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
|
+
"**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.",
|
|
44
|
+
"**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
45
|
"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
46
|
"Map to acceptance criterion — identify the specific spec criterion this test proves.",
|
|
43
47
|
"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 +53,8 @@ export const TDD = {
|
|
|
49
53
|
"GREEN: Run full suite — execute ALL tests, not just the ones you wrote. The full suite must be GREEN.",
|
|
50
54
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
51
55
|
"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
|
|
56
|
+
"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.",
|
|
57
|
+
"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
58
|
"**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
59
|
"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
60
|
"**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 +62,9 @@ export const TDD = {
|
|
|
58
62
|
"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
63
|
],
|
|
60
64
|
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.",
|
|
65
|
+
"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
66
|
"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
|
|
67
|
+
"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
68
|
"Before writing RED tests, discover relevant existing tests and commands so the new test extends the suite instead of fighting it.",
|
|
65
69
|
"Before implementation, perform a system-wide impact check across callbacks, state, interfaces, schemas, and external contracts touched by the slice.",
|
|
66
70
|
"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`).",
|