cclaw-cli 6.14.3 → 7.0.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.
Files changed (86) hide show
  1. package/README.md +0 -2
  2. package/dist/artifact-linter/brainstorm.js +1 -1
  3. package/dist/artifact-linter/design.js +2 -2
  4. package/dist/artifact-linter/findings-dedup.js +1 -1
  5. package/dist/artifact-linter/plan.js +6 -6
  6. package/dist/artifact-linter/review-army.d.ts +1 -1
  7. package/dist/artifact-linter/review-army.js +1 -1
  8. package/dist/artifact-linter/scope.js +6 -6
  9. package/dist/artifact-linter/shared.d.ts +37 -73
  10. package/dist/artifact-linter/shared.js +30 -37
  11. package/dist/artifact-linter/spec.js +1 -1
  12. package/dist/artifact-linter/tdd.d.ts +20 -33
  13. package/dist/artifact-linter/tdd.js +89 -617
  14. package/dist/artifact-linter.js +11 -32
  15. package/dist/cli.js +1 -1
  16. package/dist/config.js +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/content/core-agents.d.ts +8 -26
  19. package/dist/content/core-agents.js +48 -94
  20. package/dist/content/examples.d.ts +1 -1
  21. package/dist/content/examples.js +4 -4
  22. package/dist/content/hooks.js +62 -149
  23. package/dist/content/idea.js +2 -2
  24. package/dist/content/iron-laws.js +1 -1
  25. package/dist/content/node-hooks.js +2 -2
  26. package/dist/content/skills-elicitation.js +2 -2
  27. package/dist/content/skills.d.ts +4 -6
  28. package/dist/content/skills.js +14 -53
  29. package/dist/content/stage-schema.d.ts +3 -3
  30. package/dist/content/stage-schema.js +8 -46
  31. package/dist/content/stages/brainstorm.js +5 -5
  32. package/dist/content/stages/plan.js +2 -2
  33. package/dist/content/stages/review.js +1 -1
  34. package/dist/content/stages/schema-types.d.ts +1 -1
  35. package/dist/content/stages/scope.js +1 -1
  36. package/dist/content/stages/spec.js +2 -2
  37. package/dist/content/stages/tdd.js +43 -108
  38. package/dist/content/start-command.js +3 -3
  39. package/dist/content/subagent-context-skills.js +5 -3
  40. package/dist/content/subagents.js +13 -74
  41. package/dist/content/templates.d.ts +6 -6
  42. package/dist/content/templates.js +23 -24
  43. package/dist/content/utility-skills.d.ts +1 -1
  44. package/dist/content/utility-skills.js +1 -1
  45. package/dist/delegation.d.ts +79 -139
  46. package/dist/delegation.js +83 -215
  47. package/dist/early-loop.js +1 -1
  48. package/dist/flow-state.d.ts +24 -129
  49. package/dist/flow-state.js +5 -30
  50. package/dist/gate-evidence.d.ts +2 -7
  51. package/dist/gate-evidence.js +2 -59
  52. package/dist/harness-adapters.d.ts +1 -1
  53. package/dist/harness-adapters.js +11 -10
  54. package/dist/install.js +24 -459
  55. package/dist/internal/advance-stage/advance.d.ts +5 -5
  56. package/dist/internal/advance-stage/advance.js +9 -24
  57. package/dist/internal/advance-stage/parsers.d.ts +1 -1
  58. package/dist/internal/advance-stage/review-loop.d.ts +1 -1
  59. package/dist/internal/advance-stage/review-loop.js +3 -3
  60. package/dist/internal/advance-stage/start-flow.js +1 -3
  61. package/dist/internal/advance-stage.js +4 -23
  62. package/dist/internal/cohesion-contract-stub.d.ts +8 -13
  63. package/dist/internal/cohesion-contract-stub.js +18 -24
  64. package/dist/internal/flow-state-repair.d.ts +1 -1
  65. package/dist/internal/plan-split-waves.d.ts +44 -7
  66. package/dist/internal/plan-split-waves.js +113 -12
  67. package/dist/internal/wave-status.d.ts +3 -6
  68. package/dist/internal/wave-status.js +5 -27
  69. package/dist/policy.js +1 -1
  70. package/dist/run-persistence.js +10 -44
  71. package/dist/runtime/run-hook.mjs +3 -3
  72. package/dist/track-heuristics.js +1 -1
  73. package/dist/types.d.ts +2 -2
  74. package/package.json +1 -1
  75. package/dist/integration-fanin.d.ts +0 -44
  76. package/dist/integration-fanin.js +0 -180
  77. package/dist/internal/set-checkpoint-mode.d.ts +0 -16
  78. package/dist/internal/set-checkpoint-mode.js +0 -72
  79. package/dist/internal/set-integration-overseer-mode.d.ts +0 -14
  80. package/dist/internal/set-integration-overseer-mode.js +0 -69
  81. package/dist/internal/set-worktree-mode.d.ts +0 -10
  82. package/dist/internal/set-worktree-mode.js +0 -28
  83. package/dist/worktree-manager.d.ts +0 -50
  84. package/dist/worktree-manager.js +0 -136
  85. package/dist/worktree-types.d.ts +0 -36
  86. package/dist/worktree-types.js +0 -6
@@ -8,7 +8,6 @@ import { exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
8
8
  import { HARNESS_ADAPTERS } from "./harness-adapters.js";
9
9
  import { readFlowState } from "./runs.js";
10
10
  import { mandatoryAgentsFor, stageSchema } from "./content/stage-schema.js";
11
- import { effectiveWorktreeExecutionMode } from "./flow-state.js";
12
11
  import { compareCanonicalUnitIds, mergeParallelWaveDefinitions, parseImplementationUnitParallelFields, parseImplementationUnits, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./internal/plan-split-waves.js";
13
12
  const execFileAsync = promisify(execFile);
14
13
  const TERMINAL_DELEGATION_STATUSES = new Set(["completed", "failed", "waived", "stale"]);
@@ -21,6 +20,10 @@ export const DELEGATION_DISPATCH_SURFACES = [
21
20
  "role-switch",
22
21
  "manual"
23
22
  ];
23
+ /** Agents that declare `claimedPaths` for parallel/disjoint scheduling and fan-out caps. */
24
+ export function isParallelTddSliceWorker(agent) {
25
+ return agent === "slice-builder";
26
+ }
24
27
  /**
25
28
  * Per-surface allowed agent-definition path prefixes. Used by the generated
26
29
  * `.cclaw/hooks/delegation-record.mjs` helper to reject mismatched
@@ -237,27 +240,10 @@ function isDelegationEntry(value) {
237
240
  (o.supersededBy === undefined || typeof o.supersededBy === "string") &&
238
241
  (o.claimedPaths === undefined ||
239
242
  (Array.isArray(o.claimedPaths) && o.claimedPaths.every((item) => typeof item === "string"))) &&
240
- (o.sliceId === undefined ||
241
- (typeof o.sliceId === "string" && o.sliceId.length > 0)) &&
243
+ (o.sliceId === undefined || typeof o.sliceId === "string") &&
242
244
  (o.phase === undefined ||
243
245
  (typeof o.phase === "string" &&
244
246
  DELEGATION_PHASES.includes(o.phase))) &&
245
- (o.claimToken === undefined || typeof o.claimToken === "string") &&
246
- (o.ownerLaneId === undefined || typeof o.ownerLaneId === "string") &&
247
- (o.leasedUntil === undefined || typeof o.leasedUntil === "string") &&
248
- (o.leaseState === undefined ||
249
- o.leaseState === "claimed" ||
250
- o.leaseState === "expired" ||
251
- o.leaseState === "released" ||
252
- o.leaseState === "reclaimed") &&
253
- (o.dependsOn === undefined ||
254
- (Array.isArray(o.dependsOn) && o.dependsOn.every((item) => typeof item === "string"))) &&
255
- (o.integrationState === undefined ||
256
- o.integrationState === "pending" ||
257
- o.integrationState === "applied" ||
258
- o.integrationState === "conflict" ||
259
- o.integrationState === "resolved" ||
260
- o.integrationState === "abandoned") &&
261
247
  (o.refactorOutcome === undefined || isRefactorOutcomeShape(o.refactorOutcome)) &&
262
248
  (o.riskTier === undefined ||
263
249
  o.riskTier === "low" ||
@@ -374,7 +360,7 @@ export async function readDelegationLedger(projectRoot) {
374
360
  }
375
361
  }
376
362
  /**
377
- * Wave 24 (v6.0.0) audit-only event types that live in
363
+ * Audit-only event types that live in
378
364
  * `delegation-events.jsonl` but do NOT carry a delegation lifecycle
379
365
  * payload (no agent/spanId). The parser must accept them so they
380
366
  * don't show up as corrupt lines.
@@ -388,7 +374,8 @@ const NON_DELEGATION_AUDIT_EVENTS = new Set([
388
374
  "cclaw_fanin_conflict",
389
375
  "cclaw_fanin_resolved",
390
376
  "cclaw_fanin_abandoned",
391
- "cclaw_integration_overseer_skipped"
377
+ "cclaw_integration_overseer_skipped",
378
+ "slice-completed"
392
379
  ]);
393
380
  function isAuditEventLine(parsed) {
394
381
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
@@ -396,47 +383,13 @@ function isAuditEventLine(parsed) {
396
383
  const evt = parsed.event;
397
384
  return typeof evt === "string" && NON_DELEGATION_AUDIT_EVENTS.has(evt);
398
385
  }
399
- const FAN_IN_AUDIT_EVENT_KINDS = new Set([
400
- "cclaw_fanin_applied",
401
- "cclaw_fanin_conflict",
402
- "cclaw_fanin_resolved",
403
- "cclaw_fanin_abandoned"
404
- ]);
405
- function isFanInAuditRecord(value) {
406
- if (!value || typeof value !== "object" || Array.isArray(value))
407
- return false;
408
- const o = value;
409
- const evt = o.event;
410
- if (typeof evt !== "string" || !FAN_IN_AUDIT_EVENT_KINDS.has(evt)) {
411
- return false;
412
- }
413
- return typeof o.ts === "string" && o.ts.length > 0;
414
- }
415
- /**
416
- * Append a deterministic fan-in audit row (not a delegation lifecycle event).
417
- */
418
- export async function recordCclawFanInAudit(projectRoot, params) {
419
- const filePath = delegationEventsPath(projectRoot);
420
- const payload = {
421
- event: params.kind,
422
- runId: params.runId,
423
- laneId: params.laneId,
424
- sliceIds: params.sliceIds,
425
- integrationBranch: params.integrationBranch,
426
- details: params.details,
427
- ts: new Date().toISOString()
428
- };
429
- await fs.mkdir(path.dirname(filePath), { recursive: true });
430
- await fs.appendFile(filePath, `${JSON.stringify(payload)}\n`, { encoding: "utf8", mode: 0o600 });
431
- }
432
386
  export async function readDelegationEvents(projectRoot) {
433
387
  const filePath = delegationEventsPath(projectRoot);
434
388
  if (!(await exists(filePath))) {
435
- return { events: [], corruptLines: [], fanInAudits: [] };
389
+ return { events: [], corruptLines: [] };
436
390
  }
437
391
  const events = [];
438
392
  const corruptLines = [];
439
- const fanInAudits = [];
440
393
  const text = await fs.readFile(filePath, "utf8").catch(() => "");
441
394
  const lines = text.split(/\r?\n/gu);
442
395
  for (let index = 0; index < lines.length; index += 1) {
@@ -448,11 +401,8 @@ export async function readDelegationEvents(projectRoot) {
448
401
  if (isDelegationEvent(parsed)) {
449
402
  events.push(parsed);
450
403
  }
451
- else if (isFanInAuditRecord(parsed)) {
452
- fanInAudits.push(parsed);
453
- }
454
404
  else if (isAuditEventLine(parsed)) {
455
- // Wave 24 audit-only row (e.g. mandatory_delegations_skipped_by_track).
405
+ // Audit-only row (e.g. mandatory_delegations_skipped_by_track).
456
406
  // Not a delegation lifecycle event but valid audit content.
457
407
  continue;
458
408
  }
@@ -464,7 +414,7 @@ export async function readDelegationEvents(projectRoot) {
464
414
  corruptLines.push(index + 1);
465
415
  }
466
416
  }
467
- return { events, corruptLines, fanInAudits };
417
+ return { events, corruptLines };
468
418
  }
469
419
  async function appendDelegationEvent(projectRoot, event) {
470
420
  const filePath = delegationEventsPath(projectRoot);
@@ -537,7 +487,7 @@ export function computeActiveSubagents(entries) {
537
487
  return folded;
538
488
  }
539
489
  /**
540
- * v6.8.0 — thrown by `validateMonotonicTimestamps` when an incoming row
490
+ * Thrown by `validateMonotonicTimestamps` when an incoming row
541
491
  * would push a span's timeline backwards. Carries enough context that
542
492
  * the CLI / hook surface can format a `delegation_timestamp_non_monotonic`
543
493
  * JSON payload without re-deriving the offending field.
@@ -558,7 +508,7 @@ export class DelegationTimestampError extends Error {
558
508
  }
559
509
  }
560
510
  /**
561
- * v6.8.0 — enforce that lifecycle timestamps on a delegation span move
511
+ * Enforce that lifecycle timestamps on a delegation span move
562
512
  * forward (or stay equal). Validates both per-row invariants
563
513
  * (`startTs ≤ launchedTs ≤ ackTs ≤ completedTs`) and a cross-row
564
514
  * invariant: the union of prior rows for this `spanId` plus the
@@ -616,7 +566,7 @@ export function validateMonotonicTimestamps(stamped, prior) {
616
566
  }
617
567
  }
618
568
  /**
619
- * v6.8.0 — thrown by `appendDelegation` when the operator opens a
569
+ * Thrown by `appendDelegation` when the operator opens a
620
570
  * second `scheduled` span on the same `(stage, agent)` pair while an
621
571
  * earlier span on the same pair is still active. Callers can catch and
622
572
  * either pass the existing span id via `--supersede=<id>` (which
@@ -639,13 +589,12 @@ export class DispatchDuplicateError extends Error {
639
589
  }
640
590
  }
641
591
  /**
642
- * v6.10.0 (P1) — thrown by `validateFileOverlap` when a new
643
- * `slice-implementer` is scheduled on a TDD stage with at least one
644
- * `claimedPaths` entry that overlaps an active span. The cclaw scheduler
645
- * auto-allows parallel dispatch when paths are disjoint, so an explicit
646
- * overlap is treated as a serialization signal: the operator must wait
647
- * for the existing span to terminate or pass `--allow-parallel`
648
- * deliberately to acknowledge the conflict.
592
+ * Thrown by `validateFileOverlap` when a new `slice-builder` is scheduled
593
+ * on a TDD stage with at least one `claimedPaths` entry that overlaps an
594
+ * active span. The scheduler auto-allows parallel dispatch when paths are
595
+ * disjoint, so an explicit overlap is treated as a serialization signal:
596
+ * the operator must wait for the existing span to terminate or pass
597
+ * `--allow-parallel` deliberately to acknowledge the conflict.
649
598
  */
650
599
  export class DispatchOverlapError extends Error {
651
600
  existingSpanId;
@@ -653,7 +602,7 @@ export class DispatchOverlapError extends Error {
653
602
  pair;
654
603
  conflictingPaths;
655
604
  constructor(params) {
656
- 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}. ` +
605
+ super(`dispatch_overlap — slice-builder span ${params.newSpanId} claims path(s) ${params.conflictingPaths.join(", ")} already held by active spanId=${params.existingSpanId} on stage=${params.pair.stage}. ` +
657
606
  `Wait for ${params.existingSpanId} to finish, dispatch a non-overlapping slice, or pass --allow-parallel to acknowledge the conflict.`);
658
607
  this.name = "DispatchOverlapError";
659
608
  this.existingSpanId = params.existingSpanId;
@@ -663,11 +612,10 @@ export class DispatchOverlapError extends Error {
663
612
  }
664
613
  }
665
614
  /**
666
- * v6.10.0 (P2) — thrown when the count of active `slice-implementer`
667
- * spans (after fold) reaches `MAX_PARALLEL_SLICE_IMPLEMENTERS` and a new
668
- * scheduled row would push it past the cap. Cap can be overridden once
669
- * via `--override-cap=N` on the hook flag or globally via
670
- * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
615
+ * Thrown when the count of active `slice-builder` spans reaches
616
+ * `MAX_PARALLEL_SLICE_BUILDERS` and a new scheduled row would push it past
617
+ * the cap. Cap can be overridden once via `--override-cap=N` on the hook
618
+ * flag or globally via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS=<N>` env.
671
619
  */
672
620
  export class DispatchCapError extends Error {
673
621
  cap;
@@ -675,7 +623,7 @@ export class DispatchCapError extends Error {
675
623
  pair;
676
624
  constructor(params) {
677
625
  super(`dispatch_cap — ${params.active} active ${params.pair.agent}(s) at the cap of ${params.cap}. ` +
678
- `Complete one before scheduling another, or pass --override-cap=N (or CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=N) to lift the cap for this run.`);
626
+ `Complete one before scheduling another, or pass --override-cap=N (or CCLAW_MAX_PARALLEL_SLICE_BUILDERS=N) to lift the cap for this run.`);
679
627
  this.name = "DispatchCapError";
680
628
  this.cap = params.cap;
681
629
  this.active = params.active;
@@ -683,27 +631,16 @@ export class DispatchCapError extends Error {
683
631
  }
684
632
  }
685
633
  /**
686
- * v6.13.0 claim / lease contract violation for worktree-first TDD rows.
634
+ * Default cap on active `slice-builder` spans in a single TDD run. Override
635
+ * via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS=<int>` (validated `>=1`).
687
636
  */
688
- export class DispatchClaimInvalidError extends Error {
689
- constructor(message) {
690
- super(message);
691
- this.name = "DispatchClaimInvalidError";
692
- }
693
- }
694
- /**
695
- * v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
696
- * single TDD run. Aligned with evanflow's parallel cap. Override via
697
- * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
698
- */
699
- export const MAX_PARALLEL_SLICE_IMPLEMENTERS = 5;
637
+ export const MAX_PARALLEL_SLICE_BUILDERS = 5;
700
638
  /**
701
639
  * Return up to `cap` slice units whose dependsOn are satisfied, avoiding
702
640
  * `claimedPaths` intersections with already-selected units and active holders.
703
641
  */
704
642
  export function selectReadySlices(units, opts) {
705
- const pool = opts.legacyContinuation ? units.filter((u) => u.parallelizable) : units;
706
- const ordered = [...pool].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
643
+ const ordered = [...units].sort((a, b) => compareCanonicalUnitIds(a.unitId, b.unitId));
707
644
  const selected = [];
708
645
  const blockedPaths = new Set();
709
646
  for (const holder of opts.activePathHolders) {
@@ -747,7 +684,7 @@ export function selectReadySlices(units, opts) {
747
684
  return selected;
748
685
  }
749
686
  /**
750
- * v6.13.1 — build scheduler rows from merged parallel wave definitions + plan units.
687
+ * Build scheduler rows from merged parallel wave definitions + plan units.
751
688
  */
752
689
  export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, options) {
753
690
  const units = parseImplementationUnits(planMarkdown);
@@ -787,7 +724,25 @@ export function readySliceUnitsFromMergedWaves(mergedWaves, planMarkdown, option
787
724
  }
788
725
  return out;
789
726
  }
790
- export function integrationCheckRequired(events, fanInAudits) {
727
+ /**
728
+ * Heuristic helper deciding whether a multi-slice wave needs
729
+ * the `integration-overseer` dispatch.
730
+ *
731
+ * Triggers (any one):
732
+ * - **two or more closed slices share import boundaries** (heuristic:
733
+ * two slices declare a `claimedPaths` whose first 2 path segments
734
+ * match — same package/module directory);
735
+ * - any slice has `riskTier === "high"`.
736
+ *
737
+ * When none fire, the verdict is `{ required: false, reasons: ["disjoint-paths"] }`
738
+ * and the caller should record a `cclaw_integration_overseer_skipped`
739
+ * audit before bypassing the dispatch.
740
+ *
741
+ * Note on inputs: this function reads from the supplied delegation
742
+ * events list directly so callers can inject synthetic data in tests.
743
+ * Use `readDelegationEvents(projectRoot)` in production paths.
744
+ */
745
+ export function integrationCheckRequired(events) {
791
746
  const reasons = [];
792
747
  // Closed slices = ones whose phase=green or phase=refactor row is
793
748
  // completed. We collect each unique sliceId's representative paths
@@ -858,18 +813,13 @@ export function integrationCheckRequired(events, fanInAudits) {
858
813
  }
859
814
  if (sharedFound)
860
815
  reasons.push("shared-import-boundary");
861
- // Fan-in conflict trigger — any `cclaw_fanin_conflict` in the supplied
862
- // audits forces the overseer regardless of paths/risk.
863
- if (Array.isArray(fanInAudits) && fanInAudits.some((a) => a.event === "cclaw_fanin_conflict")) {
864
- reasons.push("fanin-conflict");
865
- }
866
816
  if (reasons.length > 0) {
867
817
  return { required: true, reasons };
868
818
  }
869
819
  return { required: false, reasons: ["disjoint-paths"] };
870
820
  }
871
821
  /**
872
- * v6.14.0 — append a non-delegation audit event recording that the
822
+ * Append a non-delegation audit event recording that the
873
823
  * integration-overseer dispatch was skipped because
874
824
  * `integrationCheckRequired()` returned `required: false`. Best-effort;
875
825
  * never throws.
@@ -892,14 +842,14 @@ export async function recordIntegrationOverseerSkipped(projectRoot, params) {
892
842
  }
893
843
  }
894
844
  /**
895
- * v6.13.1 — load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
845
+ * Load merged wave plan (Parallel Execution Plan block + wave-plans/) and map to `ReadySliceUnit[]`.
896
846
  */
897
847
  export async function loadTddReadySlicePool(planMarkdown, artifactsDir, options) {
898
848
  const merged = mergeParallelWaveDefinitions(parseParallelExecutionPlanWaves(planMarkdown), await parseWavePlanDirectory(artifactsDir));
899
849
  return readySliceUnitsFromMergedWaves(merged, planMarkdown, options);
900
850
  }
901
851
  function readMaxParallelOverrideFromEnv() {
902
- const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
852
+ const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_BUILDERS;
903
853
  if (typeof raw !== "string" || raw.trim().length === 0)
904
854
  return null;
905
855
  const parsed = Number(raw);
@@ -908,17 +858,16 @@ function readMaxParallelOverrideFromEnv() {
908
858
  return parsed;
909
859
  }
910
860
  /**
911
- * v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
912
- * compare `claimedPaths` against every currently active span on the
913
- * same `(stage, agent)` pair. Overlap → throw `DispatchOverlapError`;
914
- * disjoint paths → return `{ autoParallel: true }` so the caller can
915
- * mark the new entry `allowParallel = true` without explicit operator
916
- * intent. When the agent is not a slice-implementer or no
917
- * `claimedPaths` are supplied, the function returns
918
- * `{ autoParallel: false }` and the legacy dedup path takes over.
861
+ * When scheduling a `slice-builder` on a TDD stage, compare `claimedPaths`
862
+ * against every currently active span on the same `(stage, agent)` pair.
863
+ * Overlap → throw `DispatchOverlapError`; disjoint paths → return
864
+ * `{ autoParallel: true }` so the caller can mark the new entry
865
+ * `allowParallel = true` without explicit operator intent. When the agent
866
+ * is not a slice-builder or no `claimedPaths` are supplied, the function
867
+ * returns `{ autoParallel: false }` and the standard dedup path takes over.
919
868
  */
920
869
  export function validateFileOverlap(stamped, activeEntries) {
921
- if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") {
870
+ if (!isParallelTddSliceWorker(stamped.agent) || stamped.stage !== "tdd") {
922
871
  return { autoParallel: false };
923
872
  }
924
873
  const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
@@ -935,7 +884,7 @@ export function validateFileOverlap(stamped, activeEntries) {
935
884
  const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
936
885
  if (existingPaths.length === 0) {
937
886
  // We can't prove disjoint without the other side declaring paths;
938
- // be conservative and let the legacy dedup error path fire.
887
+ // be conservative and let the standard dedup error path fire.
939
888
  return { autoParallel: false };
940
889
  }
941
890
  const overlap = newPaths.filter((p) => existingPaths.includes(p));
@@ -951,24 +900,25 @@ export function validateFileOverlap(stamped, activeEntries) {
951
900
  return { autoParallel: true };
952
901
  }
953
902
  /**
954
- * v6.10.0 (P2) — enforce the slice-implementer fan-out cap. The new
955
- * scheduled row pushes the active count from N to N+1; if that would
956
- * exceed the cap (default 5, env-overridable), throw `DispatchCapError`.
903
+ * Enforce the slice-builder fan-out cap. The new scheduled row pushes the
904
+ * active count from N to N+1; if that would exceed the cap (default 5,
905
+ * env-overridable via `CCLAW_MAX_PARALLEL_SLICE_BUILDERS`), throw
906
+ * `DispatchCapError`.
957
907
  *
958
- * Caller passes the already-folded list of active entries (latest row
959
- * per spanId, ACTIVE statuses only). The function counts entries that
960
- * match the agent on the same `stage`. The new row's own spanId is
961
- * excluded so re-recording a `scheduled` doesn't trip the cap on a
962
- * span that's already counted.
908
+ * Caller passes the already-folded list of active entries (latest row per
909
+ * spanId, ACTIVE statuses only). The function counts entries that match
910
+ * the agent on the same `stage`. The new row's own spanId is excluded so
911
+ * re-recording a `scheduled` doesn't trip the cap on a span that's already
912
+ * counted.
963
913
  */
964
914
  export function validateFanOutCap(stamped, activeEntries, override) {
965
- if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd")
915
+ if (!isParallelTddSliceWorker(stamped.agent) || stamped.stage !== "tdd")
966
916
  return;
967
917
  if (stamped.status !== "scheduled")
968
918
  return;
969
919
  const cap = (override !== null && override !== undefined && Number.isInteger(override) && override >= 1)
970
920
  ? override
971
- : (readMaxParallelOverrideFromEnv() ?? MAX_PARALLEL_SLICE_IMPLEMENTERS);
921
+ : (readMaxParallelOverrideFromEnv() ?? MAX_PARALLEL_SLICE_BUILDERS);
972
922
  const sameLaneActive = activeEntries.filter((entry) => entry.stage === stamped.stage &&
973
923
  entry.agent === stamped.agent &&
974
924
  entry.spanId !== stamped.spanId);
@@ -981,18 +931,18 @@ export function validateFanOutCap(stamped, activeEntries, override) {
981
931
  }
982
932
  }
983
933
  /**
984
- * v6.9.0 — find the latest active span for a given `(stage, agent)`
934
+ * Find the latest active span for a given `(stage, agent)`
985
935
  * pair in the supplied ledger entries. Returns the row whose latest
986
936
  * status (after the latest-by-spanId fold) is still in the active set
987
937
  * (`scheduled | launched | acknowledged`).
988
938
  *
989
939
  * Run-scope is **strict**: only entries whose `runId` matches the
990
940
  * supplied `runId` are folded. Entries with empty/missing `runId`
991
- * (legacy ledgers from v6.8 and earlier) are treated as NOT belonging
941
+ * (older ledgers without explicit run scoping) are treated as NOT belonging
992
942
  * to the current run, so they cannot keep an old span "active" across
993
943
  * a fresh dispatch and trip a spurious `dispatch_duplicate`. This
994
- * fixes R7: a slice-implementer that ran in run-1 must not block a
995
- * slice-implementer scheduled in run-2.
944
+ * Ensures a slice-builder that ran in run-1 does not block a
945
+ * slice-builder scheduled in run-2.
996
946
  *
997
947
  * keep in sync with the inline copy in
998
948
  * `src/content/hooks.ts::delegationRecordScript`.
@@ -1027,51 +977,12 @@ async function writeSubagentTracker(projectRoot, entries) {
1027
977
  }));
1028
978
  await writeFileSafe(subagentsStatePath(projectRoot), `${JSON.stringify({ active, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
1029
979
  }
1030
- function latestClaimTokenForSpan(entries, spanId) {
1031
- if (!spanId)
1032
- return null;
1033
- let latest = null;
1034
- for (const e of entries) {
1035
- if (e.spanId !== spanId)
1036
- continue;
1037
- if (typeof e.claimToken === "string" && e.claimToken.trim().length > 0) {
1038
- latest = e.claimToken.trim();
1039
- }
1040
- }
1041
- return latest;
1042
- }
1043
- function assertSliceClaimInvariant(flow, stamped, prior) {
1044
- if (stamped.stage !== "tdd" || stamped.agent !== "slice-implementer")
1045
- return;
1046
- if (effectiveWorktreeExecutionMode(flow) !== "worktree-first")
1047
- return;
1048
- if (stamped.status === "scheduled" && typeof stamped.sliceId === "string" && stamped.sliceId.length > 0) {
1049
- const tok = stamped.claimToken?.trim() ?? "";
1050
- if (tok.length === 0) {
1051
- throw new DispatchClaimInvalidError("dispatch_claim_invalid — worktree-first requires --claim-token when scheduling slice-implementer with --slice");
1052
- }
1053
- }
1054
- if (!TERMINAL_DELEGATION_STATUSES.has(stamped.status))
1055
- return;
1056
- if (stamped.status === "waived" || stamped.status === "stale")
1057
- return;
1058
- const expected = latestClaimTokenForSpan(prior, stamped.spanId);
1059
- if (!expected)
1060
- return;
1061
- const got = stamped.claimToken?.trim() ?? "";
1062
- if (got !== expected) {
1063
- throw new DispatchClaimInvalidError("dispatch_claim_invalid — claimToken must match the scheduled claim for this span");
1064
- }
1065
- }
1066
980
  export async function appendDelegation(projectRoot, entry) {
1067
981
  const flowState = await readFlowState(projectRoot);
1068
982
  const { activeRunId } = flowState;
1069
983
  await withDirectoryLock(delegationLockPath(projectRoot), async () => {
1070
984
  const filePath = delegationLogPath(projectRoot);
1071
985
  const prior = await readDelegationLedger(projectRoot);
1072
- // Span start anchor: prefer explicit `startTs`; otherwise fall back to
1073
- // the earliest provided lifecycle marker so the monotonic validator
1074
- // never sees a synthetic `now` overshoot a real event timestamp.
1075
986
  const lifecycleCandidates = [
1076
987
  entry.startTs,
1077
988
  entry.launchedTs,
@@ -1120,21 +1031,11 @@ export async function appendDelegation(projectRoot, entry) {
1120
1031
  stamped.fulfillmentMode = expectedFulfillmentMode(fallbacks);
1121
1032
  }
1122
1033
  }
1123
- // Idempotency: a retried hook may replay the same lifecycle row. Allow a
1124
- // terminal row to close an existing scheduled span, but drop exact same
1125
- // span/status duplicates so checks do not mis-count repeated writes.
1126
1034
  if (prior.entries.some((existing) => existing.spanId === stamped.spanId && existing.status === stamped.status)) {
1127
1035
  return;
1128
1036
  }
1129
- assertSliceClaimInvariant(flowState, stamped, prior.entries);
1130
1037
  validateMonotonicTimestamps(stamped, prior.entries);
1131
1038
  if (stamped.status === "scheduled") {
1132
- // v6.10.0 (P1+P2): for slice-implementer rows with declared
1133
- // claimedPaths, the file-overlap scheduler runs first. Disjoint
1134
- // paths auto-promote the row to allowParallel so the legacy
1135
- // dispatch_duplicate guard does not fire. Overlapping paths
1136
- // throw DispatchOverlapError. The fan-out cap then runs against
1137
- // the active set (excluding the new row's spanId).
1138
1039
  const sameRunPrior = prior.entries.filter((entry) => entry.runId === activeRunId);
1139
1040
  const activeForRun = computeActiveSubagents(sameRunPrior);
1140
1041
  const overlap = validateFileOverlap(stamped, activeForRun);
@@ -1164,39 +1065,6 @@ export async function appendDelegation(projectRoot, entry) {
1164
1065
  await writeSubagentTracker(projectRoot, ledger.entries);
1165
1066
  });
1166
1067
  }
1167
- /**
1168
- * Scan delegation events for expired `leasedUntil` timestamps and append
1169
- * best-effort `cclaw_slice_lease_expired` audit rows (one per span/slice key).
1170
- */
1171
- export async function reclaimExpiredDelegationClaims(projectRoot, now = new Date()) {
1172
- const { events } = await readDelegationEvents(projectRoot);
1173
- const seen = new Set();
1174
- let count = 0;
1175
- const ts = now.toISOString();
1176
- const filePath = delegationEventsPath(projectRoot);
1177
- const cutoff = Date.parse(ts);
1178
- for (const e of events) {
1179
- if (e.leaseState !== "claimed")
1180
- continue;
1181
- if (typeof e.leasedUntil !== "string" || e.leasedUntil.length === 0)
1182
- continue;
1183
- if (Date.parse(e.leasedUntil) > cutoff)
1184
- continue;
1185
- const key = `${e.spanId ?? ""}|${e.sliceId ?? ""}`;
1186
- if (seen.has(key))
1187
- continue;
1188
- seen.add(key);
1189
- await fs.appendFile(filePath, `${JSON.stringify({
1190
- event: "cclaw_slice_lease_expired",
1191
- spanId: e.spanId,
1192
- sliceId: e.sliceId,
1193
- leasedUntil: e.leasedUntil,
1194
- eventTs: ts
1195
- })}\n`, { encoding: "utf8", mode: 0o600 });
1196
- count += 1;
1197
- }
1198
- return count;
1199
- }
1200
1068
  /**
1201
1069
  * Aggregate the fulfillment mode cclaw expects for the active harness set.
1202
1070
  * Priority native > generic-dispatch > role-switch > waiver — the best
@@ -1218,7 +1086,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
1218
1086
  const flowState = await readFlowState(projectRoot, {
1219
1087
  repairFeatureSystem: options.repairFeatureSystem
1220
1088
  });
1221
- // Wave 24 follow-up (v6.1.1): read `flowState.taskClass` as a fallback
1089
+ // Read `flowState.taskClass` as a fallback
1222
1090
  // when the caller doesn't pass an explicit override. The
1223
1091
  // `cclaw advance-stage` path (`buildValidationReport` →
1224
1092
  // `checkMandatoryDelegations`) never forwarded `taskClass`, which left
@@ -1343,7 +1211,7 @@ export async function checkMandatoryDelegations(projectRoot, stage, options = {}
1343
1211
  };
1344
1212
  }
1345
1213
  /**
1346
- * Wave 24 (v6.0.0) — append a non-delegation audit event to
1214
+ * Append a non-delegation audit event to
1347
1215
  * `delegation-events.jsonl` recording that the mandatory delegation
1348
1216
  * gate was skipped because of the active track / task class. Plays the
1349
1217
  * same audit role as a `waived` row but does NOT carry an agent —
@@ -1373,12 +1241,12 @@ async function recordMandatorySkippedByTrack(projectRoot, params) {
1373
1241
  }
1374
1242
  }
1375
1243
  /**
1376
- * Wave 25 (v6.1.0) — append a non-delegation audit event recording
1244
+ * Append a non-delegation audit event recording
1377
1245
  * that one or more required artifact-validation findings were
1378
1246
  * demoted from blocking to advisory because the active run is on a
1379
1247
  * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
1380
1248
  *
1381
- * The event mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
1249
+ * The event mirrors `mandatory_delegations_skipped_by_track`
1382
1250
  * audit pattern: best-effort write to `delegation-events.jsonl`, no
1383
1251
  * agent payload, recognized by `readDelegationEvents` so it does not
1384
1252
  * corrupt downstream parsers. Failures are swallowed.
@@ -1405,12 +1273,12 @@ export async function recordArtifactValidationDemotedByTrack(projectRoot, params
1405
1273
  }
1406
1274
  }
1407
1275
  /**
1408
- * Wave 25 (v6.1.0) — append a non-delegation audit event recording
1276
+ * Append a non-delegation audit event recording
1409
1277
  * that the scope-stage Expansion Strategist (`product-discovery`)
1410
1278
  * delegation requirement was skipped because the active run is on a
1411
1279
  * small-fix lane (`track === "quick"` or `taskClass === "software-bugfix"`).
1412
1280
  *
1413
- * Mirrors the Wave 24 `mandatory_delegations_skipped_by_track`
1281
+ * Mirrors the `mandatory_delegations_skipped_by_track`
1414
1282
  * audit pattern: best-effort write to `delegation-events.jsonl`, no
1415
1283
  * agent payload, recognized by `readDelegationEvents` so it does not
1416
1284
  * corrupt downstream parsers. Failures are swallowed.
@@ -137,7 +137,7 @@ export function parseEarlyLoopLog(text, options = {}) {
137
137
  continue;
138
138
  }
139
139
  }
140
- // v6.9.0 schema repair: legacy logs may carry rows with no runId
140
+ // schema repair: legacy logs may carry rows with no runId
141
141
  // (the prior parser silently coerced them to "active", which then
142
142
  // collided across runs). Surface a structured warning on read but
143
143
  // skip the row so derived status doesn't fold cross-run state.