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.
@@ -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 Mapping"]
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 Mapping", "RED Evidence", "GREEN Evidence", "REFACTOR Notes"]
146
+ artifactSections: ["Acceptance & Failure Map", "RED Evidence", "GREEN Evidence", "REFACTOR Notes"]
147
147
  }
148
148
  ]
149
149
  },
@@ -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 artifact.",
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
- "REFACTOR: continue the `test-author` evidence cycle (or a dedicated refactor mode when available) to improve code quality without behavior changes. Set `CCLAW_ACTIVE_AGENT=tdd-refactor` when the harness supports phase labels.",
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 Mapping", required: false, validationRule: "Each RED test links to a plan task and spec criterion." },
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 Mapping") {
303
+ if (row.section === "Acceptance & Failure Map") {
306
304
  return {
307
305
  ...row,
308
306
  required: true,
309
- validationRule: "Each RED test links to a spec acceptance criterion ID (for example AC-1)."
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 Mapping
1000
- | Vertical slice | Source item ID | Spec criterion ID |
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
- ## Failure Analysis
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:
@@ -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
@@ -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" && stamped.allowParallel !== true) {
661
- const existing = findActiveSpanForPair(stamped.stage, stamped.agent, activeRunId, prior);
662
- if (existing && existing.spanId && existing.spanId !== stamped.spanId) {
663
- throw new DispatchDuplicateError({
664
- existingSpanId: existing.spanId,
665
- existingStatus: existing.status,
666
- newSpanId: stamped.spanId,
667
- pair: { stage: stamped.stage, agent: stamped.agent }
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));