cclaw-cli 6.9.0 → 6.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-linter/plan.js +37 -0
- package/dist/artifact-linter/shared.d.ts +48 -2
- package/dist/artifact-linter/shared.js +52 -4
- package/dist/artifact-linter/tdd.d.ts +20 -0
- package/dist/artifact-linter/tdd.js +187 -14
- package/dist/artifact-linter.js +87 -2
- package/dist/content/examples.js +9 -9
- package/dist/content/hooks.js +135 -2
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/skills.js +1 -1
- package/dist/content/stages/tdd.js +6 -8
- package/dist/content/subagents.js +9 -1
- package/dist/content/templates.js +5 -15
- package/dist/delegation.d.ts +92 -0
- package/dist/delegation.js +159 -10
- package/dist/internal/advance-stage.js +19 -3
- package/dist/internal/plan-split-waves.d.ts +66 -0
- package/dist/internal/plan-split-waves.js +249 -0
- package/dist/tdd-slices.d.ts +90 -0
- package/dist/tdd-slices.js +375 -0
- package/package.json +1 -1
package/dist/content/hooks.js
CHANGED
|
@@ -326,7 +326,7 @@ function hasPriorAck(events, args, runId) {
|
|
|
326
326
|
function usage() {
|
|
327
327
|
process.stderr.write([
|
|
328
328
|
"Usage:",
|
|
329
|
-
" 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] [--json]",
|
|
329
|
+
" 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]",
|
|
330
330
|
" 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]",
|
|
331
331
|
" node .cclaw/hooks/delegation-record.mjs --repair --span-id=<id> --repair-reason=\"<why>\" [--json]",
|
|
332
332
|
"",
|
|
@@ -339,6 +339,10 @@ function usage() {
|
|
|
339
339
|
"Dispatch dedup (v6.8.0):",
|
|
340
340
|
" --supersede=<prevSpanId> close the previous active span on this (stage, agent) as 'stale' before recording the new scheduled row",
|
|
341
341
|
" --allow-parallel record both spans as concurrent; new row is tagged allowParallel: true",
|
|
342
|
+
"",
|
|
343
|
+
"TDD parallel scheduler (v6.10.0):",
|
|
344
|
+
" --paths=<a,b,c> repo-relative paths the slice-implementer will edit; disjoint sets auto-promote to allowParallel, overlap throws DispatchOverlapError",
|
|
345
|
+
" --override-cap=<int> raise the slice-implementer fan-out cap once for this dispatch (default cap " + String(5) + ", env CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS overrides globally)",
|
|
342
346
|
""
|
|
343
347
|
].join("\\n") + "\\n");
|
|
344
348
|
}
|
|
@@ -440,6 +444,13 @@ function buildRow(args, status, runId, now, options) {
|
|
|
440
444
|
// Inherit the span's startTs from prior rows so monotonic validation
|
|
441
445
|
// can compare against the original schedule, not the row write time.
|
|
442
446
|
const startTs = (options && options.spanStartTs) || now;
|
|
447
|
+
// v6.10.0 (P1): claimedPaths from --paths=<comma-separated>. Empty
|
|
448
|
+
// arrays are dropped so the row stays compatible with v6.9 readers.
|
|
449
|
+
const claimedPathsRaw = typeof args.paths === "string" ? args.paths : "";
|
|
450
|
+
const claimedPaths = claimedPathsRaw
|
|
451
|
+
.split(",")
|
|
452
|
+
.map((value) => value.trim())
|
|
453
|
+
.filter((value) => value.length > 0);
|
|
443
454
|
return {
|
|
444
455
|
stage: args.stage,
|
|
445
456
|
agent: args.agent,
|
|
@@ -461,7 +472,8 @@ function buildRow(args, status, runId, now, options) {
|
|
|
461
472
|
completedTs: args["completed-ts"] || (status === "completed" ? now : undefined),
|
|
462
473
|
endTs: TERMINAL.has(status) ? now : undefined,
|
|
463
474
|
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
464
|
-
allowParallel: args["allow-parallel"] === true ? true : undefined
|
|
475
|
+
allowParallel: args["allow-parallel"] === true ? true : undefined,
|
|
476
|
+
claimedPaths: claimedPaths.length > 0 ? claimedPaths : undefined
|
|
465
477
|
};
|
|
466
478
|
}
|
|
467
479
|
|
|
@@ -502,6 +514,102 @@ function findActiveSpanForPairInline(stage, agent, runId, entries) {
|
|
|
502
514
|
return null;
|
|
503
515
|
}
|
|
504
516
|
|
|
517
|
+
// keep in sync with computeActiveSubagents in src/delegation.ts
|
|
518
|
+
function computeActiveSubagentsInline(entries) {
|
|
519
|
+
const ACTIVE_STATUSES = new Set(["scheduled", "launched", "acknowledged"]);
|
|
520
|
+
const effectiveTs = (entry) =>
|
|
521
|
+
entry.completedTs || entry.ackTs || entry.launchedTs || entry.endTs || entry.startTs || entry.ts || "";
|
|
522
|
+
const latestBySpan = new Map();
|
|
523
|
+
for (const entry of entries) {
|
|
524
|
+
if (!entry || typeof entry !== "object") continue;
|
|
525
|
+
if (typeof entry.spanId !== "string" || entry.spanId.length === 0) continue;
|
|
526
|
+
const existing = latestBySpan.get(entry.spanId);
|
|
527
|
+
if (!existing || effectiveTs(entry) >= effectiveTs(existing)) {
|
|
528
|
+
latestBySpan.set(entry.spanId, entry);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const active = [];
|
|
532
|
+
for (const entry of latestBySpan.values()) {
|
|
533
|
+
if (ACTIVE_STATUSES.has(entry.status)) active.push(entry);
|
|
534
|
+
}
|
|
535
|
+
return active;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// keep in sync with validateFileOverlap in src/delegation.ts
|
|
539
|
+
function validateFileOverlapInline(stamped, activeEntries) {
|
|
540
|
+
if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") {
|
|
541
|
+
return { autoParallel: false, conflict: null };
|
|
542
|
+
}
|
|
543
|
+
const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
|
|
544
|
+
if (newPaths.length === 0) {
|
|
545
|
+
return { autoParallel: false, conflict: null };
|
|
546
|
+
}
|
|
547
|
+
const sameLane = activeEntries.filter(
|
|
548
|
+
(entry) =>
|
|
549
|
+
entry.stage === stamped.stage &&
|
|
550
|
+
entry.agent === stamped.agent &&
|
|
551
|
+
entry.spanId !== stamped.spanId
|
|
552
|
+
);
|
|
553
|
+
if (sameLane.length === 0) {
|
|
554
|
+
return { autoParallel: true, conflict: null };
|
|
555
|
+
}
|
|
556
|
+
for (const existing of sameLane) {
|
|
557
|
+
const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
|
|
558
|
+
if (existingPaths.length === 0) {
|
|
559
|
+
return { autoParallel: false, conflict: null };
|
|
560
|
+
}
|
|
561
|
+
const overlap = newPaths.filter((p) => existingPaths.includes(p));
|
|
562
|
+
if (overlap.length > 0) {
|
|
563
|
+
return {
|
|
564
|
+
autoParallel: false,
|
|
565
|
+
conflict: {
|
|
566
|
+
existingSpanId: existing.spanId || "unknown",
|
|
567
|
+
newSpanId: stamped.spanId || "unknown",
|
|
568
|
+
pair: { stage: stamped.stage, agent: stamped.agent },
|
|
569
|
+
conflictingPaths: overlap
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return { autoParallel: true, conflict: null };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const MAX_PARALLEL_SLICE_IMPLEMENTERS_INLINE = 5;
|
|
578
|
+
|
|
579
|
+
function readMaxParallelOverrideFromEnvInline() {
|
|
580
|
+
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
|
|
581
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return null;
|
|
582
|
+
const parsed = Number(raw);
|
|
583
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1) return null;
|
|
584
|
+
return parsed;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// keep in sync with validateFanOutCap in src/delegation.ts
|
|
588
|
+
function validateFanOutCapInline(stamped, activeEntries, override) {
|
|
589
|
+
if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") return null;
|
|
590
|
+
if (stamped.status !== "scheduled") return null;
|
|
591
|
+
let cap;
|
|
592
|
+
if (override !== null && override !== undefined && Number.isInteger(override) && override >= 1) {
|
|
593
|
+
cap = override;
|
|
594
|
+
} else {
|
|
595
|
+
cap = readMaxParallelOverrideFromEnvInline() || MAX_PARALLEL_SLICE_IMPLEMENTERS_INLINE;
|
|
596
|
+
}
|
|
597
|
+
const sameLaneActive = activeEntries.filter(
|
|
598
|
+
(entry) =>
|
|
599
|
+
entry.stage === stamped.stage &&
|
|
600
|
+
entry.agent === stamped.agent &&
|
|
601
|
+
entry.spanId !== stamped.spanId
|
|
602
|
+
);
|
|
603
|
+
if (sameLaneActive.length + 1 > cap) {
|
|
604
|
+
return {
|
|
605
|
+
cap,
|
|
606
|
+
active: sameLaneActive.length,
|
|
607
|
+
pair: { stage: stamped.stage, agent: stamped.agent }
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
505
613
|
function enforceDispatchDedupInline(stamped, priorEntries, args) {
|
|
506
614
|
if (stamped.status !== "scheduled") return null;
|
|
507
615
|
if (args["allow-parallel"] === true) return null;
|
|
@@ -992,6 +1100,31 @@ async function main() {
|
|
|
992
1100
|
emitErrorJson("delegation_timestamp_non_monotonic", violation, json);
|
|
993
1101
|
return;
|
|
994
1102
|
}
|
|
1103
|
+
|
|
1104
|
+
// v6.10.0 (P1+P2): file-overlap scheduler + fan-out cap. Run before
|
|
1105
|
+
// the legacy dispatch dedup so disjoint claimedPaths can auto-promote
|
|
1106
|
+
// to allowParallel and bypass the duplicate guard.
|
|
1107
|
+
if (status === "scheduled") {
|
|
1108
|
+
const sameRunPrior = priorLedger.filter((entry) => entry.runId === runId);
|
|
1109
|
+
const activeForRun = computeActiveSubagentsInline(sameRunPrior);
|
|
1110
|
+
const overlap = validateFileOverlapInline(clean, activeForRun);
|
|
1111
|
+
if (overlap.conflict) {
|
|
1112
|
+
emitErrorJson("dispatch_overlap", overlap.conflict, json);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (overlap.autoParallel && clean.allowParallel !== true) {
|
|
1116
|
+
clean.allowParallel = true;
|
|
1117
|
+
args["allow-parallel"] = true;
|
|
1118
|
+
event.allowParallel = true;
|
|
1119
|
+
}
|
|
1120
|
+
const overrideRaw = typeof args["override-cap"] === "string" ? args["override-cap"] : null;
|
|
1121
|
+
const override = overrideRaw !== null ? Number(overrideRaw) : null;
|
|
1122
|
+
const capViolation = validateFanOutCapInline(clean, activeForRun, override);
|
|
1123
|
+
if (capViolation) {
|
|
1124
|
+
emitErrorJson("dispatch_cap", capViolation, json);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
995
1128
|
const dedupViolation = enforceDispatchDedupInline(clean, priorLedger, args);
|
|
996
1129
|
if (dedupViolation) {
|
|
997
1130
|
if (dedupViolation.kind === "supersede") {
|
|
@@ -36,7 +36,7 @@ export const REFERENCE_PATTERNS = [
|
|
|
36
36
|
"Discover tests and affected contracts before opening a RED vertical slice.",
|
|
37
37
|
"Map the slice to the active source item before editing production code."
|
|
38
38
|
],
|
|
39
|
-
artifactSections: ["Test Discovery", "System-Wide Impact Check", "Acceptance
|
|
39
|
+
artifactSections: ["Test Discovery", "System-Wide Impact Check", "Acceptance & Failure Map"]
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
stage: "review",
|
|
@@ -143,7 +143,7 @@ export const REFERENCE_PATTERNS = [
|
|
|
143
143
|
"Open one packet as one vertical slice; do not mix unrelated packet evidence.",
|
|
144
144
|
"Close packet only when RED, GREEN, REFACTOR, and verification evidence align."
|
|
145
145
|
],
|
|
146
|
-
artifactSections: ["Acceptance
|
|
146
|
+
artifactSections: ["Acceptance & Failure Map", "RED Evidence", "GREEN Evidence", "REFACTOR Notes"]
|
|
147
147
|
}
|
|
148
148
|
]
|
|
149
149
|
},
|
package/dist/content/skills.js
CHANGED
|
@@ -102,7 +102,7 @@ Any "the failure is real" claim (failing test, broken build, regression catch, d
|
|
|
102
102
|
|
|
103
103
|
\`proof: <iso-ts> | <observed snippet — first 200 chars> | source: <command or log path>\`
|
|
104
104
|
|
|
105
|
-
For TDD specifically, this is the watched-RED proof and is required per new test before \`stage-complete\` accepts the stage.
|
|
105
|
+
For TDD specifically, this is the watched-RED proof and is required per new test before \`stage-complete\` accepts the stage. From v6.10.0 onward, record TDD slice transitions through the sidecar CLI \`cclaw-cli internal tdd-slice-record --slice <id> --status red|green|refactor-done|refactor-deferred ...\` rather than hand-editing the \`Watched-RED Proof\` or \`Vertical Slice Cycle\` markdown tables; the linter reads \`.cclaw/artifacts/06-tdd-slices.jsonl\` when present and treats the markdown as an auto-derived view.
|
|
106
106
|
`;
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
@@ -42,14 +42,14 @@ export const TDD = {
|
|
|
42
42
|
"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.",
|
|
43
43
|
"Run a system-wide impact check — name callbacks, state transitions, interfaces, schemas, CLI/config/API contracts, persistence, or event boundaries that this slice can affect. Add RED coverage for each affected public contract or record why it is out of scope.",
|
|
44
44
|
"Source/test preflight — before production edits, classify planned paths using test-path patterns; verify the RED touches a test path and the GREEN touches only source paths needed for the failing behavior.",
|
|
45
|
-
"Set execution posture — record whether this slice is sequential, batch-safe, or blocked; when the existing git workflow permits small commits, checkpoint after RED, GREEN, and REFACTOR (or record why commits are deferred).",
|
|
46
45
|
"Use the mandatory `test-author` delegation for RED — after discovery and impact check, produce failing behavior tests and RED evidence only (no production edits). Set `CCLAW_ACTIVE_AGENT=tdd-red` when the harness supports phase labels.",
|
|
47
|
-
"RED: Capture failure output — copy the exact failure output as RED evidence. Record in
|
|
46
|
+
"RED: Capture failure output — copy the exact failure output as RED evidence. Record the slice in `.cclaw/artifacts/06-tdd-slices.jsonl` via `cclaw-cli internal tdd-slice-record --slice <id> --status red --test-file <path> --command <cmd> --paths <comma-separated>` (the markdown `Watched-RED Proof` table is now auto-derived from this sidecar).",
|
|
48
47
|
"Continue the same `test-author` delegation intent for GREEN — minimal implementation plus full-suite GREEN evidence. Set `CCLAW_ACTIVE_AGENT=tdd-green` when the harness supports phase labels.",
|
|
49
48
|
"GREEN: Run full suite — execute ALL tests, not just the ones you wrote. The full suite must be GREEN.",
|
|
50
49
|
"GREEN: Verify no regressions — if any existing test breaks, fix the regression before proceeding.",
|
|
51
50
|
"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
|
-
"
|
|
51
|
+
"GREEN: append a `green` row to `.cclaw/artifacts/06-tdd-slices.jsonl` via `cclaw-cli internal tdd-slice-record --slice <id> --status green [--green-output-ref <path|spanId:...>]` so the Vertical Slice Cycle linter validates the sidecar instead of a hand-edited table.",
|
|
52
|
+
"REFACTOR: continue the `test-author` evidence cycle (or a dedicated refactor mode when available) to improve code quality without behavior changes, then record `--status refactor-done` (or `--status refactor-deferred --refactor-rationale \"<why>\"`) via the same `tdd-slice-record` CLI. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
|
|
53
53
|
"Record evidence — capture test discovery, system-wide impact check, RED failure, GREEN output, and REFACTOR notes in the TDD artifact. When logging a `green` row, attach the closed acceptance-criterion IDs in `acIds` so Ralph Loop status counts them.",
|
|
54
54
|
"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.",
|
|
55
55
|
"**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.",
|
|
@@ -158,10 +158,8 @@ export const TDD = {
|
|
|
158
158
|
{ section: "Upstream Handoff", required: false, validationRule: "Summarizes plan/spec/design decisions, constraints, open questions, and explicit drift before RED work." },
|
|
159
159
|
{ section: "Test Discovery", required: true, validationRule: "Before RED: lists existing tests, fixtures/helpers, exact commands, and the chosen local pattern to extend." },
|
|
160
160
|
{ section: "System-Wide Impact Check", required: true, validationRule: "Before implementation: names affected callbacks, state transitions, interfaces, schemas, public APIs/config/CLI, persistence, or event contracts, with coverage or explicit out-of-scope notes." },
|
|
161
|
-
{ section: "Execution Posture", required: false, validationRule: "Records sequential/batch/blocked posture and vertical-slice RED/GREEN/REFACTOR checkpoint plan, including incremental commit boundaries when consistent with the repository git workflow." },
|
|
162
161
|
{ section: "RED Evidence", required: true, validationRule: "Failing test output captured per slice." },
|
|
163
|
-
{ section: "Acceptance
|
|
164
|
-
{ section: "Failure Analysis", required: false, validationRule: "Failure reason matches expected missing behavior." },
|
|
162
|
+
{ section: "Acceptance & Failure Map", required: false, validationRule: "Each slice row carries Source ID, AC ID, expected behavior, and a RED-link (delegation spanId, evidence path, or sidecar redOutputRef)." },
|
|
165
163
|
{ section: "GREEN Evidence", required: true, validationRule: "Full suite pass output captured." },
|
|
166
164
|
{ section: "REFACTOR Notes", required: true, validationRule: "What changed, why, behavior preservation confirmed." },
|
|
167
165
|
{ section: "Traceability", required: true, validationRule: "Plan task ID and spec criterion linked." },
|
|
@@ -302,11 +300,11 @@ function tddStageVariantForTrack(track) {
|
|
|
302
300
|
traceabilityRule: "Every RED test traces to an acceptance criterion. Every GREEN change traces to a RED test. Evidence chain must be unbroken."
|
|
303
301
|
},
|
|
304
302
|
artifactValidation: TDD.artifactRules.artifactValidation.map((row) => {
|
|
305
|
-
if (row.section === "Acceptance
|
|
303
|
+
if (row.section === "Acceptance & Failure Map") {
|
|
306
304
|
return {
|
|
307
305
|
...row,
|
|
308
306
|
required: true,
|
|
309
|
-
validationRule: "Each
|
|
307
|
+
validationRule: "Each slice row carries Source ID, AC ID (spec acceptance criterion ID, for example AC-1), expected behavior, and a RED-link (delegation spanId, evidence path, or sidecar redOutputRef)."
|
|
310
308
|
};
|
|
311
309
|
}
|
|
312
310
|
if (row.section === "Traceability") {
|
|
@@ -678,7 +678,7 @@ You are a slice-implementer subagent.
|
|
|
678
678
|
|
|
679
679
|
SLICE: {single vertical slice}
|
|
680
680
|
RED_EVIDENCE: {failing test and expected failure}
|
|
681
|
-
ALLOWED_FILES: {explicit file boundaries}
|
|
681
|
+
ALLOWED_FILES: {explicit file boundaries — surfaced to scheduler as Files: <paths>}
|
|
682
682
|
FORBIDDEN_CHANGES: {scope/compatibility limits}
|
|
683
683
|
VERIFICATION: {commands expected}
|
|
684
684
|
|
|
@@ -686,6 +686,12 @@ Rules:
|
|
|
686
686
|
- Implement only the minimal GREEN change for the existing RED evidence.
|
|
687
687
|
- Keep REFACTOR behavior-preserving.
|
|
688
688
|
- Return the strict worker JSON schema first.
|
|
689
|
+
|
|
690
|
+
Slice ledger contract (v6.10.0):
|
|
691
|
+
- After observing the failing test, run \`cclaw-cli internal tdd-slice-record --slice <id> --status red --test-file <path> --command <cmd> --paths <comma-separated> [--ac <id>] [--plan-unit <id>]\`. The command appends to \`.cclaw/artifacts/06-tdd-slices.jsonl\`.
|
|
692
|
+
- After the same test passes, run \`cclaw-cli internal tdd-slice-record --slice <id> --status green [--green-output-ref <path|spanId:...>]\`.
|
|
693
|
+
- After REFACTOR, run \`cclaw-cli internal tdd-slice-record --slice <id> --status refactor-done\` or \`--status refactor-deferred --refactor-rationale "<why>"\`.
|
|
694
|
+
- Do NOT hand-edit the Watched-RED Proof or Vertical Slice Cycle markdown tables; the linter reads the JSONL sidecar when present and the markdown becomes an auto-derived view.
|
|
689
695
|
${MARKDOWN_CODE_FENCE}
|
|
690
696
|
|
|
691
697
|
`;
|
|
@@ -931,10 +937,12 @@ Process (mandatory):
|
|
|
931
937
|
1) If STAGE_MODE=TEST_RED_ONLY:
|
|
932
938
|
- RED only — add failing tests proving the gap (show failing output excerpt).
|
|
933
939
|
- Do NOT edit production code.
|
|
940
|
+
- Append the slice to the sidecar via \`cclaw-cli internal tdd-slice-record --slice <id> --status red --test-file <path> --command <cmd> --paths <comma-separated>\` instead of editing the Watched-RED Proof markdown table.
|
|
934
941
|
- Report: TESTS_ADDED, RED_COMMAND_RUN, RED_EVIDENCE, STATUS: DONE|BLOCKED.
|
|
935
942
|
2) If STAGE_MODE=BUILD_GREEN_REFACTOR:
|
|
936
943
|
- GREEN — minimal production code to satisfy existing RED tests, rerun full suite.
|
|
937
944
|
- REFACTOR — only after full suite is green; preserve behavior.
|
|
945
|
+
- Append \`--status green\` (and \`--status refactor-done\` or \`--status refactor-deferred --refactor-rationale "<why>"\` after refactor) via \`cclaw-cli internal tdd-slice-record\`. The Vertical Slice Cycle markdown stays auto-derived from this sidecar.
|
|
938
946
|
- Report: FILES_EDITED, GREEN_COMMAND_RUN, REFACTOR_NOTES, STATUS: DONE|BLOCKED.
|
|
939
947
|
${MARKDOWN_CODE_FENCE}
|
|
940
948
|
|
|
@@ -986,27 +986,17 @@ ${renderBehaviorAnchorTemplateLine("tdd")}
|
|
|
986
986
|
|---|---|---|
|
|
987
987
|
| S-1 | | covered/out-of-scope because |
|
|
988
988
|
|
|
989
|
-
## Execution Posture
|
|
990
|
-
- Posture: sequential | dependency-batched | blocked
|
|
991
|
-
- Vertical-slice RED/GREEN/REFACTOR checkpoint plan:
|
|
992
|
-
- Incremental commits: yes/no/deferred because
|
|
993
|
-
|
|
994
989
|
## RED Evidence
|
|
995
990
|
| Slice | Test name | Command | Failure output summary |
|
|
996
991
|
|---|---|---|---|
|
|
997
992
|
| S-1 | | | |
|
|
998
993
|
|
|
999
|
-
## Acceptance
|
|
1000
|
-
|
|
|
1001
|
-
|
|
1002
|
-
| S-1 | SRC-1 | AC-1 |
|
|
1003
|
-
|
|
1004
|
-
> Map each slice to the active track's source item: plan slice on standard/medium, or the \`Quick Reproduction Contract\` bug slice / spec acceptance item on quick.
|
|
994
|
+
## Acceptance & Failure Map
|
|
995
|
+
| Slice | Source ID | AC ID | Expected behavior | RED-link |
|
|
996
|
+
|---|---|---|---|---|
|
|
997
|
+
| S-1 | SRC-1 | AC-1 | | |
|
|
1005
998
|
|
|
1006
|
-
|
|
1007
|
-
| Slice | Expected missing behavior | Actual failure reason |
|
|
1008
|
-
|---|---|---|
|
|
1009
|
-
| S-1 | | |
|
|
999
|
+
> Each slice maps to the active track's source item (plan slice on standard/medium, or the \`Quick Reproduction Contract\` bug slice / spec acceptance item on quick) and to a spec criterion. The RED-link column is satisfied by either a \`spanId:<id>\` from the delegation ledger, an \`<artifacts-dir>/<file>\` evidence pointer, or a \`redOutputRef\` recorded via \`cclaw-cli internal tdd-slice-record\` in the sidecar ledger.
|
|
1010
1000
|
|
|
1011
1001
|
## GREEN Evidence
|
|
1012
1002
|
- Full suite command:
|
package/dist/delegation.d.ts
CHANGED
|
@@ -129,6 +129,17 @@ export type DelegationEntry = {
|
|
|
129
129
|
* coherent successor chain.
|
|
130
130
|
*/
|
|
131
131
|
supersededBy?: string;
|
|
132
|
+
/**
|
|
133
|
+
* v6.10.0 (P1) — repo-relative paths the delegated unit will edit.
|
|
134
|
+
* Used by the slice-implementer file-overlap scheduler to either
|
|
135
|
+
* auto-allow parallel dispatch (disjoint paths) or block the row
|
|
136
|
+
* with `DispatchOverlapError` (overlapping paths). For agents
|
|
137
|
+
* other than slice-implementer the field is advisory.
|
|
138
|
+
*
|
|
139
|
+
* keep in sync with the inline copy in
|
|
140
|
+
* `src/content/hooks.ts::delegationRecordScript`.
|
|
141
|
+
*/
|
|
142
|
+
claimedPaths?: string[];
|
|
132
143
|
};
|
|
133
144
|
export declare const DELEGATION_LEDGER_SCHEMA_VERSION: 3;
|
|
134
145
|
export type DelegationLedger = {
|
|
@@ -231,6 +242,87 @@ export declare class DispatchDuplicateError extends Error {
|
|
|
231
242
|
};
|
|
232
243
|
});
|
|
233
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* v6.10.0 (P1) — thrown by `validateFileOverlap` when a new
|
|
247
|
+
* `slice-implementer` is scheduled on a TDD stage with at least one
|
|
248
|
+
* `claimedPaths` entry that overlaps an active span. The cclaw scheduler
|
|
249
|
+
* auto-allows parallel dispatch when paths are disjoint, so an explicit
|
|
250
|
+
* overlap is treated as a serialization signal: the operator must wait
|
|
251
|
+
* for the existing span to terminate or pass `--allow-parallel`
|
|
252
|
+
* deliberately to acknowledge the conflict.
|
|
253
|
+
*/
|
|
254
|
+
export declare class DispatchOverlapError extends Error {
|
|
255
|
+
readonly existingSpanId: string;
|
|
256
|
+
readonly newSpanId: string;
|
|
257
|
+
readonly pair: {
|
|
258
|
+
stage: string;
|
|
259
|
+
agent: string;
|
|
260
|
+
};
|
|
261
|
+
readonly conflictingPaths: string[];
|
|
262
|
+
constructor(params: {
|
|
263
|
+
existingSpanId: string;
|
|
264
|
+
newSpanId: string;
|
|
265
|
+
pair: {
|
|
266
|
+
stage: string;
|
|
267
|
+
agent: string;
|
|
268
|
+
};
|
|
269
|
+
conflictingPaths: string[];
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* v6.10.0 (P2) — thrown when the count of active `slice-implementer`
|
|
274
|
+
* spans (after fold) reaches `MAX_PARALLEL_SLICE_IMPLEMENTERS` and a new
|
|
275
|
+
* scheduled row would push it past the cap. Cap can be overridden once
|
|
276
|
+
* via `--override-cap=N` on the hook flag or globally via
|
|
277
|
+
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
|
|
278
|
+
*/
|
|
279
|
+
export declare class DispatchCapError extends Error {
|
|
280
|
+
readonly cap: number;
|
|
281
|
+
readonly active: number;
|
|
282
|
+
readonly pair: {
|
|
283
|
+
stage: string;
|
|
284
|
+
agent: string;
|
|
285
|
+
};
|
|
286
|
+
constructor(params: {
|
|
287
|
+
cap: number;
|
|
288
|
+
active: number;
|
|
289
|
+
pair: {
|
|
290
|
+
stage: string;
|
|
291
|
+
agent: string;
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
|
|
297
|
+
* single TDD run. Aligned with evanflow's parallel cap. Override via
|
|
298
|
+
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
|
|
299
|
+
*/
|
|
300
|
+
export declare const MAX_PARALLEL_SLICE_IMPLEMENTERS: 5;
|
|
301
|
+
/**
|
|
302
|
+
* v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
|
|
303
|
+
* compare `claimedPaths` against every currently active span on the
|
|
304
|
+
* same `(stage, agent)` pair. Overlap → throw `DispatchOverlapError`;
|
|
305
|
+
* disjoint paths → return `{ autoParallel: true }` so the caller can
|
|
306
|
+
* mark the new entry `allowParallel = true` without explicit operator
|
|
307
|
+
* intent. When the agent is not a slice-implementer or no
|
|
308
|
+
* `claimedPaths` are supplied, the function returns
|
|
309
|
+
* `{ autoParallel: false }` and the legacy dedup path takes over.
|
|
310
|
+
*/
|
|
311
|
+
export declare function validateFileOverlap(stamped: DelegationEntry, activeEntries: DelegationEntry[]): {
|
|
312
|
+
autoParallel: boolean;
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* v6.10.0 (P2) — enforce the slice-implementer fan-out cap. The new
|
|
316
|
+
* scheduled row pushes the active count from N to N+1; if that would
|
|
317
|
+
* exceed the cap (default 5, env-overridable), throw `DispatchCapError`.
|
|
318
|
+
*
|
|
319
|
+
* Caller passes the already-folded list of active entries (latest row
|
|
320
|
+
* per spanId, ACTIVE statuses only). The function counts entries that
|
|
321
|
+
* match the agent on the same `stage`. The new row's own spanId is
|
|
322
|
+
* excluded so re-recording a `scheduled` doesn't trip the cap on a
|
|
323
|
+
* span that's already counted.
|
|
324
|
+
*/
|
|
325
|
+
export declare function validateFanOutCap(stamped: DelegationEntry, activeEntries: DelegationEntry[], override?: number | null): void;
|
|
234
326
|
/**
|
|
235
327
|
* v6.9.0 — find the latest active span for a given `(stage, agent)`
|
|
236
328
|
* pair in the supplied ledger entries. Returns the row whose latest
|
package/dist/delegation.js
CHANGED
|
@@ -224,7 +224,9 @@ function isDelegationEntry(value) {
|
|
|
224
224
|
(o.skill === undefined || typeof o.skill === "string") &&
|
|
225
225
|
(o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3) &&
|
|
226
226
|
(o.allowParallel === undefined || typeof o.allowParallel === "boolean") &&
|
|
227
|
-
(o.supersededBy === undefined || typeof o.supersededBy === "string")
|
|
227
|
+
(o.supersededBy === undefined || typeof o.supersededBy === "string") &&
|
|
228
|
+
(o.claimedPaths === undefined ||
|
|
229
|
+
(Array.isArray(o.claimedPaths) && o.claimedPaths.every((item) => typeof item === "string"))));
|
|
228
230
|
}
|
|
229
231
|
function isDelegationDispatchSurface(value) {
|
|
230
232
|
return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
|
|
@@ -547,6 +549,138 @@ export class DispatchDuplicateError extends Error {
|
|
|
547
549
|
this.pair = params.pair;
|
|
548
550
|
}
|
|
549
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* v6.10.0 (P1) — thrown by `validateFileOverlap` when a new
|
|
554
|
+
* `slice-implementer` is scheduled on a TDD stage with at least one
|
|
555
|
+
* `claimedPaths` entry that overlaps an active span. The cclaw scheduler
|
|
556
|
+
* auto-allows parallel dispatch when paths are disjoint, so an explicit
|
|
557
|
+
* overlap is treated as a serialization signal: the operator must wait
|
|
558
|
+
* for the existing span to terminate or pass `--allow-parallel`
|
|
559
|
+
* deliberately to acknowledge the conflict.
|
|
560
|
+
*/
|
|
561
|
+
export class DispatchOverlapError extends Error {
|
|
562
|
+
existingSpanId;
|
|
563
|
+
newSpanId;
|
|
564
|
+
pair;
|
|
565
|
+
conflictingPaths;
|
|
566
|
+
constructor(params) {
|
|
567
|
+
super(`dispatch_overlap — slice-implementer span ${params.newSpanId} claims path(s) ${params.conflictingPaths.join(", ")} already held by active spanId=${params.existingSpanId} on stage=${params.pair.stage}. ` +
|
|
568
|
+
`Wait for ${params.existingSpanId} to finish, dispatch a non-overlapping slice, or pass --allow-parallel to acknowledge the conflict.`);
|
|
569
|
+
this.name = "DispatchOverlapError";
|
|
570
|
+
this.existingSpanId = params.existingSpanId;
|
|
571
|
+
this.newSpanId = params.newSpanId;
|
|
572
|
+
this.pair = params.pair;
|
|
573
|
+
this.conflictingPaths = params.conflictingPaths;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* v6.10.0 (P2) — thrown when the count of active `slice-implementer`
|
|
578
|
+
* spans (after fold) reaches `MAX_PARALLEL_SLICE_IMPLEMENTERS` and a new
|
|
579
|
+
* scheduled row would push it past the cap. Cap can be overridden once
|
|
580
|
+
* via `--override-cap=N` on the hook flag or globally via
|
|
581
|
+
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
|
|
582
|
+
*/
|
|
583
|
+
export class DispatchCapError extends Error {
|
|
584
|
+
cap;
|
|
585
|
+
active;
|
|
586
|
+
pair;
|
|
587
|
+
constructor(params) {
|
|
588
|
+
super(`dispatch_cap — ${params.active} active ${params.pair.agent}(s) at the cap of ${params.cap}. ` +
|
|
589
|
+
`Complete one before scheduling another, or pass --override-cap=N (or CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=N) to lift the cap for this run.`);
|
|
590
|
+
this.name = "DispatchCapError";
|
|
591
|
+
this.cap = params.cap;
|
|
592
|
+
this.active = params.active;
|
|
593
|
+
this.pair = params.pair;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
|
|
598
|
+
* single TDD run. Aligned with evanflow's parallel cap. Override via
|
|
599
|
+
* `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
|
|
600
|
+
*/
|
|
601
|
+
export const MAX_PARALLEL_SLICE_IMPLEMENTERS = 5;
|
|
602
|
+
function readMaxParallelOverrideFromEnv() {
|
|
603
|
+
const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
|
|
604
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
605
|
+
return null;
|
|
606
|
+
const parsed = Number(raw);
|
|
607
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
|
|
608
|
+
return null;
|
|
609
|
+
return parsed;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
|
|
613
|
+
* compare `claimedPaths` against every currently active span on the
|
|
614
|
+
* same `(stage, agent)` pair. Overlap → throw `DispatchOverlapError`;
|
|
615
|
+
* disjoint paths → return `{ autoParallel: true }` so the caller can
|
|
616
|
+
* mark the new entry `allowParallel = true` without explicit operator
|
|
617
|
+
* intent. When the agent is not a slice-implementer or no
|
|
618
|
+
* `claimedPaths` are supplied, the function returns
|
|
619
|
+
* `{ autoParallel: false }` and the legacy dedup path takes over.
|
|
620
|
+
*/
|
|
621
|
+
export function validateFileOverlap(stamped, activeEntries) {
|
|
622
|
+
if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") {
|
|
623
|
+
return { autoParallel: false };
|
|
624
|
+
}
|
|
625
|
+
const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
|
|
626
|
+
if (newPaths.length === 0) {
|
|
627
|
+
return { autoParallel: false };
|
|
628
|
+
}
|
|
629
|
+
const sameLane = activeEntries.filter((entry) => entry.stage === stamped.stage &&
|
|
630
|
+
entry.agent === stamped.agent &&
|
|
631
|
+
entry.spanId !== stamped.spanId);
|
|
632
|
+
if (sameLane.length === 0) {
|
|
633
|
+
return { autoParallel: true };
|
|
634
|
+
}
|
|
635
|
+
for (const existing of sameLane) {
|
|
636
|
+
const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
|
|
637
|
+
if (existingPaths.length === 0) {
|
|
638
|
+
// We can't prove disjoint without the other side declaring paths;
|
|
639
|
+
// be conservative and let the legacy dedup error path fire.
|
|
640
|
+
return { autoParallel: false };
|
|
641
|
+
}
|
|
642
|
+
const overlap = newPaths.filter((p) => existingPaths.includes(p));
|
|
643
|
+
if (overlap.length > 0) {
|
|
644
|
+
throw new DispatchOverlapError({
|
|
645
|
+
existingSpanId: existing.spanId ?? "unknown",
|
|
646
|
+
newSpanId: stamped.spanId ?? "unknown",
|
|
647
|
+
pair: { stage: stamped.stage, agent: stamped.agent },
|
|
648
|
+
conflictingPaths: overlap
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { autoParallel: true };
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* v6.10.0 (P2) — enforce the slice-implementer fan-out cap. The new
|
|
656
|
+
* scheduled row pushes the active count from N to N+1; if that would
|
|
657
|
+
* exceed the cap (default 5, env-overridable), throw `DispatchCapError`.
|
|
658
|
+
*
|
|
659
|
+
* Caller passes the already-folded list of active entries (latest row
|
|
660
|
+
* per spanId, ACTIVE statuses only). The function counts entries that
|
|
661
|
+
* match the agent on the same `stage`. The new row's own spanId is
|
|
662
|
+
* excluded so re-recording a `scheduled` doesn't trip the cap on a
|
|
663
|
+
* span that's already counted.
|
|
664
|
+
*/
|
|
665
|
+
export function validateFanOutCap(stamped, activeEntries, override) {
|
|
666
|
+
if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd")
|
|
667
|
+
return;
|
|
668
|
+
if (stamped.status !== "scheduled")
|
|
669
|
+
return;
|
|
670
|
+
const cap = (override !== null && override !== undefined && Number.isInteger(override) && override >= 1)
|
|
671
|
+
? override
|
|
672
|
+
: (readMaxParallelOverrideFromEnv() ?? MAX_PARALLEL_SLICE_IMPLEMENTERS);
|
|
673
|
+
const sameLaneActive = activeEntries.filter((entry) => entry.stage === stamped.stage &&
|
|
674
|
+
entry.agent === stamped.agent &&
|
|
675
|
+
entry.spanId !== stamped.spanId);
|
|
676
|
+
if (sameLaneActive.length + 1 > cap) {
|
|
677
|
+
throw new DispatchCapError({
|
|
678
|
+
cap,
|
|
679
|
+
active: sameLaneActive.length,
|
|
680
|
+
pair: { stage: stamped.stage, agent: stamped.agent }
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
550
684
|
/**
|
|
551
685
|
* v6.9.0 — find the latest active span for a given `(stage, agent)`
|
|
552
686
|
* pair in the supplied ledger entries. Returns the row whose latest
|
|
@@ -657,15 +791,30 @@ export async function appendDelegation(projectRoot, entry) {
|
|
|
657
791
|
return;
|
|
658
792
|
}
|
|
659
793
|
validateMonotonicTimestamps(stamped, prior.entries);
|
|
660
|
-
if (stamped.status === "scheduled"
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
794
|
+
if (stamped.status === "scheduled") {
|
|
795
|
+
// v6.10.0 (P1+P2): for slice-implementer rows with declared
|
|
796
|
+
// claimedPaths, the file-overlap scheduler runs first. Disjoint
|
|
797
|
+
// paths auto-promote the row to allowParallel so the legacy
|
|
798
|
+
// dispatch_duplicate guard does not fire. Overlapping paths
|
|
799
|
+
// throw DispatchOverlapError. The fan-out cap then runs against
|
|
800
|
+
// the active set (excluding the new row's spanId).
|
|
801
|
+
const sameRunPrior = prior.entries.filter((entry) => entry.runId === activeRunId);
|
|
802
|
+
const activeForRun = computeActiveSubagents(sameRunPrior);
|
|
803
|
+
const overlap = validateFileOverlap(stamped, activeForRun);
|
|
804
|
+
if (overlap.autoParallel && stamped.allowParallel !== true) {
|
|
805
|
+
stamped.allowParallel = true;
|
|
806
|
+
}
|
|
807
|
+
validateFanOutCap(stamped, activeForRun);
|
|
808
|
+
if (stamped.allowParallel !== true) {
|
|
809
|
+
const existing = findActiveSpanForPair(stamped.stage, stamped.agent, activeRunId, prior);
|
|
810
|
+
if (existing && existing.spanId && existing.spanId !== stamped.spanId) {
|
|
811
|
+
throw new DispatchDuplicateError({
|
|
812
|
+
existingSpanId: existing.spanId,
|
|
813
|
+
existingStatus: existing.status,
|
|
814
|
+
newSpanId: stamped.spanId,
|
|
815
|
+
pair: { stage: stamped.stage, agent: stamped.agent }
|
|
816
|
+
});
|
|
817
|
+
}
|
|
669
818
|
}
|
|
670
819
|
}
|
|
671
820
|
await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
|