cclaw-cli 6.9.0 → 6.11.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.
@@ -976,41 +976,32 @@ ${renderBehaviorAnchorTemplateLine("tdd")}
976
976
  - Open questions:
977
977
  - Drift from upstream (or \`None\`):
978
978
 
979
+ <!-- auto-start: slices-index -->
980
+ ## Slices Index
981
+
982
+ _Auto-rendered from \`tdd-slices/S-*.md\` once slice-documenter or controller writes per-slice files. Do not edit by hand._
983
+ <!-- auto-end: slices-index -->
984
+
979
985
  ## Test Discovery
980
- | Slice | Existing tests / helpers / fixtures | Exact command(s) | Pattern to extend |
981
- |---|---|---|---|
982
- | S-1 | | | |
986
+ > Overall narrative for how this stage discovered the existing test surface. Per-slice details live in \`tdd-slices/S-<id>.md\`.
983
987
 
984
988
  ## System-Wide Impact Check
985
989
  | Slice | Callbacks/state/interfaces/contracts affected | Coverage decision |
986
990
  |---|---|---|
987
991
  | S-1 | | covered/out-of-scope because |
988
992
 
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
993
  ## RED Evidence
995
- | Slice | Test name | Command | Failure output summary |
996
- |---|---|---|---|
997
- | S-1 | | | |
994
+ > From v6.11.0 the per-slice RED rows are auto-satisfied by \`phase=red\` events in \`delegation-events.jsonl\` (controller dispatches \`test-author --slice S-<id> --phase red\`). Legacy hand-filled tables continue to validate as a fallback. Use \`Evidence: <path>\` or \`Evidence: spanId:<id>\` pointers if you prefer a manual reference.
998
995
 
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.
996
+ ## Acceptance & Failure Map
997
+ | Slice | Source ID | AC ID | Expected behavior | RED-link |
998
+ |---|---|---|---|---|
999
+ | S-1 | SRC-1 | AC-1 | | |
1005
1000
 
1006
- ## Failure Analysis
1007
- | Slice | Expected missing behavior | Actual failure reason |
1008
- |---|---|---|
1009
- | S-1 | | |
1001
+ > 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 or an \`<artifacts-dir>/<file>\` evidence pointer. From v6.11.0 the column is auto-derivable: a \`phase=red\` event in \`delegation-events.jsonl\` with non-empty evidenceRefs auto-satisfies the row.
1010
1002
 
1011
1003
  ## GREEN Evidence
1012
- - Full suite command:
1013
- - Full suite result:
1004
+ > From v6.11.0 GREEN rows are auto-satisfied by \`phase=green\` events in \`delegation-events.jsonl\` (controller dispatches \`slice-implementer --slice S-<id> --phase green\`). Legacy hand-filled tables continue to validate as a fallback. Use \`Evidence: <path>\` or \`Evidence: spanId:<id>\` pointers if you prefer a manual reference.
1014
1005
 
1015
1006
  ## REFACTOR Notes
1016
1007
  - What changed:
@@ -1027,19 +1018,11 @@ ${renderBehaviorAnchorTemplateLine("tdd")}
1027
1018
  - Acknowledged: yes — code that landed before its test will be deleted and rewritten from the test.
1028
1019
  - Exceptions invoked (or \`- None.\`):
1029
1020
 
1030
- ## Watched-RED Proof
1031
- > Required for every new test in this stage. Each row proves the test was *observed* failing before any production code was written.
1032
-
1033
- | Slice | Test name | Observed at (ISO ts) | Failure reason snippet | Source command/log |
1034
- |---|---|---|---|---|
1035
- | S-1 | | | | |
1036
-
1021
+ <!-- auto-start: tdd-slice-summary -->
1037
1022
  ## Vertical Slice Cycle
1038
- > Per slice: RED -> GREEN -> REFACTOR within the same cycle (refactor not deferred). The linter checks structural presence of all three phases.
1039
1023
 
1040
- | Slice | RED ts | GREEN ts | REFACTOR ts (or \`deferred because <reason>\`) |
1041
- |---|---|---|---|
1042
- | S-1 | | | |
1024
+ _Auto-rendered from \`delegation-events.jsonl\` once \`test-author\` and \`slice-implementer\` are dispatched with \`--slice <id> --phase red|green|refactor|refactor-deferred\`. Do not edit by hand._
1025
+ <!-- auto-end: tdd-slice-summary -->
1043
1026
 
1044
1027
  ## Assertion Correctness Notes
1045
1028
  > For each new test assertion, name a *plausible subtle bug* that would still pass it (mental mutation test). If you cannot, the assertion is too coarse — strengthen it.
@@ -1633,6 +1616,31 @@ Track-specific skips are allowed only when \`flow-state.track\` + \`skippedStage
1633
1616
  - Preamble budget: keep role/status announcements brief and avoid repeating
1634
1617
  them unless the stage or role changes.
1635
1618
  `;
1619
+ /**
1620
+ * v6.11.0 (S2) — per-slice prose file written by `slice-documenter`
1621
+ * (or the controller) to `<artifacts-dir>/tdd-slices/S-<id>.md`. The
1622
+ * main `06-tdd.md` is auto-indexed via `## Slices Index`.
1623
+ */
1624
+ export function tddSliceFileTemplate(sliceId) {
1625
+ return `# Slice ${sliceId}
1626
+
1627
+ ## Plan unit
1628
+ T-...
1629
+
1630
+ ## Acceptance criteria
1631
+ AC-...
1632
+
1633
+ ## Why this slice
1634
+
1635
+ ## What was tested
1636
+
1637
+ ## What was implemented
1638
+
1639
+ ## REFACTOR notes
1640
+
1641
+ ## Learnings
1642
+ `;
1643
+ }
1636
1644
  export function buildRulesJson() {
1637
1645
  return {
1638
1646
  version: 1,
@@ -129,7 +129,45 @@ 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[];
143
+ /**
144
+ * v6.11.0 (D1) — TDD slice identifier, e.g. `"S-1"`. Recorded by the
145
+ * controller when dispatching `test-author` / `slice-implementer` /
146
+ * `slice-documenter` so the artifact linter can auto-derive the
147
+ * Watched-RED Proof + Vertical Slice Cycle tables from
148
+ * `delegation-events.jsonl` instead of requiring agents to maintain
149
+ * the markdown by hand. Optional: legacy and non-TDD rows omit it.
150
+ *
151
+ * keep in sync with the inline copy in
152
+ * `src/content/hooks.ts::delegationRecordScript`.
153
+ */
154
+ sliceId?: string;
155
+ /**
156
+ * v6.11.0 (D1) — explicit phase tag for TDD slice events. Combined
157
+ * with `sliceId`, the linter validates RED -> GREEN -> REFACTOR
158
+ * monotonicity per slice. `refactor-deferred` requires a rationale
159
+ * either via `--refactor-rationale` (recorded into evidenceRefs[0])
160
+ * or an `evidenceRefs` entry that contains the rationale text.
161
+ * `doc` is reserved for the parallel `slice-documenter` subagent
162
+ * (Phase C). Optional: legacy and non-TDD rows omit it.
163
+ *
164
+ * keep in sync with the inline copy in
165
+ * `src/content/hooks.ts::delegationRecordScript`.
166
+ */
167
+ phase?: "red" | "green" | "refactor" | "refactor-deferred" | "doc";
132
168
  };
169
+ export declare const DELEGATION_PHASES: readonly ["red", "green", "refactor", "refactor-deferred", "doc"];
170
+ export type DelegationPhase = (typeof DELEGATION_PHASES)[number];
133
171
  export declare const DELEGATION_LEDGER_SCHEMA_VERSION: 3;
134
172
  export type DelegationLedger = {
135
173
  runId: string;
@@ -231,6 +269,87 @@ export declare class DispatchDuplicateError extends Error {
231
269
  };
232
270
  });
233
271
  }
272
+ /**
273
+ * v6.10.0 (P1) — thrown by `validateFileOverlap` when a new
274
+ * `slice-implementer` is scheduled on a TDD stage with at least one
275
+ * `claimedPaths` entry that overlaps an active span. The cclaw scheduler
276
+ * auto-allows parallel dispatch when paths are disjoint, so an explicit
277
+ * overlap is treated as a serialization signal: the operator must wait
278
+ * for the existing span to terminate or pass `--allow-parallel`
279
+ * deliberately to acknowledge the conflict.
280
+ */
281
+ export declare class DispatchOverlapError extends Error {
282
+ readonly existingSpanId: string;
283
+ readonly newSpanId: string;
284
+ readonly pair: {
285
+ stage: string;
286
+ agent: string;
287
+ };
288
+ readonly conflictingPaths: string[];
289
+ constructor(params: {
290
+ existingSpanId: string;
291
+ newSpanId: string;
292
+ pair: {
293
+ stage: string;
294
+ agent: string;
295
+ };
296
+ conflictingPaths: string[];
297
+ });
298
+ }
299
+ /**
300
+ * v6.10.0 (P2) — thrown when the count of active `slice-implementer`
301
+ * spans (after fold) reaches `MAX_PARALLEL_SLICE_IMPLEMENTERS` and a new
302
+ * scheduled row would push it past the cap. Cap can be overridden once
303
+ * via `--override-cap=N` on the hook flag or globally via
304
+ * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
305
+ */
306
+ export declare class DispatchCapError extends Error {
307
+ readonly cap: number;
308
+ readonly active: number;
309
+ readonly pair: {
310
+ stage: string;
311
+ agent: string;
312
+ };
313
+ constructor(params: {
314
+ cap: number;
315
+ active: number;
316
+ pair: {
317
+ stage: string;
318
+ agent: string;
319
+ };
320
+ });
321
+ }
322
+ /**
323
+ * v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
324
+ * single TDD run. Aligned with evanflow's parallel cap. Override via
325
+ * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
326
+ */
327
+ export declare const MAX_PARALLEL_SLICE_IMPLEMENTERS: 5;
328
+ /**
329
+ * v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
330
+ * compare `claimedPaths` against every currently active span on the
331
+ * same `(stage, agent)` pair. Overlap → throw `DispatchOverlapError`;
332
+ * disjoint paths → return `{ autoParallel: true }` so the caller can
333
+ * mark the new entry `allowParallel = true` without explicit operator
334
+ * intent. When the agent is not a slice-implementer or no
335
+ * `claimedPaths` are supplied, the function returns
336
+ * `{ autoParallel: false }` and the legacy dedup path takes over.
337
+ */
338
+ export declare function validateFileOverlap(stamped: DelegationEntry, activeEntries: DelegationEntry[]): {
339
+ autoParallel: boolean;
340
+ };
341
+ /**
342
+ * v6.10.0 (P2) — enforce the slice-implementer fan-out cap. The new
343
+ * scheduled row pushes the active count from N to N+1; if that would
344
+ * exceed the cap (default 5, env-overridable), throw `DispatchCapError`.
345
+ *
346
+ * Caller passes the already-folded list of active entries (latest row
347
+ * per spanId, ACTIVE statuses only). The function counts entries that
348
+ * match the agent on the same `stage`. The new row's own spanId is
349
+ * excluded so re-recording a `scheduled` doesn't trip the cap on a
350
+ * span that's already counted.
351
+ */
352
+ export declare function validateFanOutCap(stamped: DelegationEntry, activeEntries: DelegationEntry[], override?: number | null): void;
234
353
  /**
235
354
  * v6.9.0 — find the latest active span for a given `(stage, agent)`
236
355
  * pair in the supplied ledger entries. Returns the row whose latest
@@ -38,6 +38,13 @@ export const DELEGATION_DISPATCH_SURFACE_PATH_PREFIXES = {
38
38
  "role-switch": [],
39
39
  "manual": []
40
40
  };
41
+ export const DELEGATION_PHASES = [
42
+ "red",
43
+ "green",
44
+ "refactor",
45
+ "refactor-deferred",
46
+ "doc"
47
+ ];
41
48
  export const DELEGATION_LEDGER_SCHEMA_VERSION = 3;
42
49
  function delegationLogPath(projectRoot) {
43
50
  return path.join(projectRoot, RUNTIME_ROOT, "state", "delegation-log.json");
@@ -224,7 +231,14 @@ function isDelegationEntry(value) {
224
231
  (o.skill === undefined || typeof o.skill === "string") &&
225
232
  (o.schemaVersion === undefined || o.schemaVersion === 1 || o.schemaVersion === 2 || o.schemaVersion === 3) &&
226
233
  (o.allowParallel === undefined || typeof o.allowParallel === "boolean") &&
227
- (o.supersededBy === undefined || typeof o.supersededBy === "string"));
234
+ (o.supersededBy === undefined || typeof o.supersededBy === "string") &&
235
+ (o.claimedPaths === undefined ||
236
+ (Array.isArray(o.claimedPaths) && o.claimedPaths.every((item) => typeof item === "string"))) &&
237
+ (o.sliceId === undefined ||
238
+ (typeof o.sliceId === "string" && o.sliceId.length > 0)) &&
239
+ (o.phase === undefined ||
240
+ (typeof o.phase === "string" &&
241
+ DELEGATION_PHASES.includes(o.phase))));
228
242
  }
229
243
  function isDelegationDispatchSurface(value) {
230
244
  return typeof value === "string" && DELEGATION_DISPATCH_SURFACES.includes(value);
@@ -547,6 +561,138 @@ export class DispatchDuplicateError extends Error {
547
561
  this.pair = params.pair;
548
562
  }
549
563
  }
564
+ /**
565
+ * v6.10.0 (P1) — thrown by `validateFileOverlap` when a new
566
+ * `slice-implementer` is scheduled on a TDD stage with at least one
567
+ * `claimedPaths` entry that overlaps an active span. The cclaw scheduler
568
+ * auto-allows parallel dispatch when paths are disjoint, so an explicit
569
+ * overlap is treated as a serialization signal: the operator must wait
570
+ * for the existing span to terminate or pass `--allow-parallel`
571
+ * deliberately to acknowledge the conflict.
572
+ */
573
+ export class DispatchOverlapError extends Error {
574
+ existingSpanId;
575
+ newSpanId;
576
+ pair;
577
+ conflictingPaths;
578
+ constructor(params) {
579
+ 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}. ` +
580
+ `Wait for ${params.existingSpanId} to finish, dispatch a non-overlapping slice, or pass --allow-parallel to acknowledge the conflict.`);
581
+ this.name = "DispatchOverlapError";
582
+ this.existingSpanId = params.existingSpanId;
583
+ this.newSpanId = params.newSpanId;
584
+ this.pair = params.pair;
585
+ this.conflictingPaths = params.conflictingPaths;
586
+ }
587
+ }
588
+ /**
589
+ * v6.10.0 (P2) — thrown when the count of active `slice-implementer`
590
+ * spans (after fold) reaches `MAX_PARALLEL_SLICE_IMPLEMENTERS` and a new
591
+ * scheduled row would push it past the cap. Cap can be overridden once
592
+ * via `--override-cap=N` on the hook flag or globally via
593
+ * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<N>` env.
594
+ */
595
+ export class DispatchCapError extends Error {
596
+ cap;
597
+ active;
598
+ pair;
599
+ constructor(params) {
600
+ super(`dispatch_cap — ${params.active} active ${params.pair.agent}(s) at the cap of ${params.cap}. ` +
601
+ `Complete one before scheduling another, or pass --override-cap=N (or CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=N) to lift the cap for this run.`);
602
+ this.name = "DispatchCapError";
603
+ this.cap = params.cap;
604
+ this.active = params.active;
605
+ this.pair = params.pair;
606
+ }
607
+ }
608
+ /**
609
+ * v6.10.0 (P2) — default cap on active `slice-implementer` spans in a
610
+ * single TDD run. Aligned with evanflow's parallel cap. Override via
611
+ * `CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS=<int>` (validated `>=1`).
612
+ */
613
+ export const MAX_PARALLEL_SLICE_IMPLEMENTERS = 5;
614
+ function readMaxParallelOverrideFromEnv() {
615
+ const raw = process.env.CCLAW_MAX_PARALLEL_SLICE_IMPLEMENTERS;
616
+ if (typeof raw !== "string" || raw.trim().length === 0)
617
+ return null;
618
+ const parsed = Number(raw);
619
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
620
+ return null;
621
+ return parsed;
622
+ }
623
+ /**
624
+ * v6.10.0 (P1) — when scheduling a `slice-implementer` on a TDD stage,
625
+ * compare `claimedPaths` against every currently active span on the
626
+ * same `(stage, agent)` pair. Overlap → throw `DispatchOverlapError`;
627
+ * disjoint paths → return `{ autoParallel: true }` so the caller can
628
+ * mark the new entry `allowParallel = true` without explicit operator
629
+ * intent. When the agent is not a slice-implementer or no
630
+ * `claimedPaths` are supplied, the function returns
631
+ * `{ autoParallel: false }` and the legacy dedup path takes over.
632
+ */
633
+ export function validateFileOverlap(stamped, activeEntries) {
634
+ if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd") {
635
+ return { autoParallel: false };
636
+ }
637
+ const newPaths = Array.isArray(stamped.claimedPaths) ? stamped.claimedPaths : [];
638
+ if (newPaths.length === 0) {
639
+ return { autoParallel: false };
640
+ }
641
+ const sameLane = activeEntries.filter((entry) => entry.stage === stamped.stage &&
642
+ entry.agent === stamped.agent &&
643
+ entry.spanId !== stamped.spanId);
644
+ if (sameLane.length === 0) {
645
+ return { autoParallel: true };
646
+ }
647
+ for (const existing of sameLane) {
648
+ const existingPaths = Array.isArray(existing.claimedPaths) ? existing.claimedPaths : [];
649
+ if (existingPaths.length === 0) {
650
+ // We can't prove disjoint without the other side declaring paths;
651
+ // be conservative and let the legacy dedup error path fire.
652
+ return { autoParallel: false };
653
+ }
654
+ const overlap = newPaths.filter((p) => existingPaths.includes(p));
655
+ if (overlap.length > 0) {
656
+ throw new DispatchOverlapError({
657
+ existingSpanId: existing.spanId ?? "unknown",
658
+ newSpanId: stamped.spanId ?? "unknown",
659
+ pair: { stage: stamped.stage, agent: stamped.agent },
660
+ conflictingPaths: overlap
661
+ });
662
+ }
663
+ }
664
+ return { autoParallel: true };
665
+ }
666
+ /**
667
+ * v6.10.0 (P2) — enforce the slice-implementer fan-out cap. The new
668
+ * scheduled row pushes the active count from N to N+1; if that would
669
+ * exceed the cap (default 5, env-overridable), throw `DispatchCapError`.
670
+ *
671
+ * Caller passes the already-folded list of active entries (latest row
672
+ * per spanId, ACTIVE statuses only). The function counts entries that
673
+ * match the agent on the same `stage`. The new row's own spanId is
674
+ * excluded so re-recording a `scheduled` doesn't trip the cap on a
675
+ * span that's already counted.
676
+ */
677
+ export function validateFanOutCap(stamped, activeEntries, override) {
678
+ if (stamped.agent !== "slice-implementer" || stamped.stage !== "tdd")
679
+ return;
680
+ if (stamped.status !== "scheduled")
681
+ return;
682
+ const cap = (override !== null && override !== undefined && Number.isInteger(override) && override >= 1)
683
+ ? override
684
+ : (readMaxParallelOverrideFromEnv() ?? MAX_PARALLEL_SLICE_IMPLEMENTERS);
685
+ const sameLaneActive = activeEntries.filter((entry) => entry.stage === stamped.stage &&
686
+ entry.agent === stamped.agent &&
687
+ entry.spanId !== stamped.spanId);
688
+ if (sameLaneActive.length + 1 > cap) {
689
+ throw new DispatchCapError({
690
+ cap,
691
+ active: sameLaneActive.length,
692
+ pair: { stage: stamped.stage, agent: stamped.agent }
693
+ });
694
+ }
695
+ }
550
696
  /**
551
697
  * v6.9.0 — find the latest active span for a given `(stage, agent)`
552
698
  * pair in the supplied ledger entries. Returns the row whose latest
@@ -657,15 +803,30 @@ export async function appendDelegation(projectRoot, entry) {
657
803
  return;
658
804
  }
659
805
  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
- });
806
+ if (stamped.status === "scheduled") {
807
+ // v6.10.0 (P1+P2): for slice-implementer rows with declared
808
+ // claimedPaths, the file-overlap scheduler runs first. Disjoint
809
+ // paths auto-promote the row to allowParallel so the legacy
810
+ // dispatch_duplicate guard does not fire. Overlapping paths
811
+ // throw DispatchOverlapError. The fan-out cap then runs against
812
+ // the active set (excluding the new row's spanId).
813
+ const sameRunPrior = prior.entries.filter((entry) => entry.runId === activeRunId);
814
+ const activeForRun = computeActiveSubagents(sameRunPrior);
815
+ const overlap = validateFileOverlap(stamped, activeForRun);
816
+ if (overlap.autoParallel && stamped.allowParallel !== true) {
817
+ stamped.allowParallel = true;
818
+ }
819
+ validateFanOutCap(stamped, activeForRun);
820
+ if (stamped.allowParallel !== true) {
821
+ const existing = findActiveSpanForPair(stamped.stage, stamped.agent, activeRunId, prior);
822
+ if (existing && existing.spanId && existing.spanId !== stamped.spanId) {
823
+ throw new DispatchDuplicateError({
824
+ existingSpanId: existing.spanId,
825
+ existingStatus: existing.status,
826
+ newSpanId: stamped.spanId,
827
+ pair: { stage: stamped.stage, agent: stamped.agent }
828
+ });
829
+ }
669
830
  }
670
831
  }
671
832
  await appendDelegationEvent(projectRoot, eventFromEntry(stamped));
package/dist/install.js CHANGED
@@ -168,6 +168,14 @@ const DEPRECATED_STATE_FILES = [
168
168
  // now reads cycle phase progression directly from the artifact table.
169
169
  "tdd-cycle-log.jsonl"
170
170
  ];
171
+ // v6.11.0 (R5): files under `<runtime>/artifacts/` that previous releases
172
+ // generated and v6.11.0 removed. `cclaw-cli sync` deletes each so existing
173
+ // installs lose the obsolete sidecar without requiring manual cleanup.
174
+ const DEPRECATED_ARTIFACT_FILES = [
175
+ // v6.10.0 sidecar — replaced in v6.11.0 by phase events in
176
+ // delegation-events.jsonl + auto-rendered tables in 06-tdd.md.
177
+ "06-tdd-slices.jsonl"
178
+ ];
171
179
  const DEPRECATED_HOOK_FILES = [
172
180
  "observe.sh",
173
181
  "summarize-observations.sh",
@@ -851,6 +859,7 @@ async function cleanLegacyArtifacts(projectRoot) {
851
859
  ...DEPRECATED_COMMAND_FILES.map((file) => runtimePath(projectRoot, "commands", file)),
852
860
  ...DEPRECATED_SKILL_FILES.map((segments) => runtimePath(projectRoot, "skills", ...segments)),
853
861
  ...DEPRECATED_STATE_FILES.map((file) => runtimePath(projectRoot, "state", file)),
862
+ ...DEPRECATED_ARTIFACT_FILES.map((file) => runtimePath(projectRoot, "artifacts", file)),
854
863
  ...DEPRECATED_RUNTIME_ROOT_FILES.map((file) => runtimePath(projectRoot, file)),
855
864
  ...DEPRECATED_HOOK_FILES.map((file) => runtimePath(projectRoot, "hooks", file))
856
865
  ]) {
@@ -14,7 +14,8 @@ import { parseAdvanceStageArgs, parseCancelRunArgs, parseHookArgs, parseRewindAr
14
14
  import { parseFlowStateRepairArgs, runFlowStateRepair } from "./flow-state-repair.js";
15
15
  import { parseWaiverGrantArgs, runWaiverGrant } from "./waiver-grant.js";
16
16
  import { FlowStateGuardMismatchError, verifyFlowStateGuard } from "../run-persistence.js";
17
- import { DelegationTimestampError, DispatchDuplicateError } from "../delegation.js";
17
+ import { DelegationTimestampError, DispatchCapError, DispatchDuplicateError, DispatchOverlapError } from "../delegation.js";
18
+ import { parsePlanSplitWavesArgs, runPlanSplitWaves } from "./plan-split-waves.js";
18
19
  /**
19
20
  * Subcommands that mutate or consult flow-state.json via the CLI runtime.
20
21
  * They all require the sha256 sidecar to match before continuing so a
@@ -32,7 +33,7 @@ const GUARD_ENFORCED_SUBCOMMANDS = new Set([
32
33
  export async function runInternalCommand(projectRoot, argv, io) {
33
34
  const [subcommand, ...tokens] = argv;
34
35
  if (!subcommand) {
35
- io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n");
36
+ io.stderr.write("cclaw internal requires a subcommand: advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves\n");
36
37
  return 1;
37
38
  }
38
39
  try {
@@ -84,7 +85,10 @@ export async function runInternalCommand(projectRoot, argv, io) {
84
85
  if (subcommand === "waiver-grant") {
85
86
  return await runWaiverGrant(projectRoot, parseWaiverGrantArgs(tokens), io);
86
87
  }
87
- io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant\n`);
88
+ if (subcommand === "plan-split-waves") {
89
+ return await runPlanSplitWaves(projectRoot, parsePlanSplitWavesArgs(tokens), io);
90
+ }
91
+ io.stderr.write(`Unknown internal subcommand: ${subcommand}. Expected advance-stage | start-flow | cancel-run | rewind | verify-flow-state-diff | verify-current-state | envelope-validate | tdd-red-evidence | tdd-loop-status | early-loop-status | compound-readiness | runtime-integrity | hook | flow-state-repair | waiver-grant | plan-split-waves\n`);
88
92
  return 1;
89
93
  }
90
94
  catch (err) {
@@ -100,6 +104,14 @@ export async function runInternalCommand(projectRoot, argv, io) {
100
104
  io.stderr.write(`error: dispatch_duplicate — ${err.message}\n`);
101
105
  return 2;
102
106
  }
107
+ if (err instanceof DispatchOverlapError) {
108
+ io.stderr.write(`error: dispatch_overlap — ${err.message}\n`);
109
+ return 2;
110
+ }
111
+ if (err instanceof DispatchCapError) {
112
+ io.stderr.write(`error: dispatch_cap — ${err.message}\n`);
113
+ return 2;
114
+ }
103
115
  io.stderr.write(`cclaw internal ${subcommand} failed: ${err instanceof Error ? err.message : String(err)}\n`);
104
116
  return 1;
105
117
  }
@@ -0,0 +1,66 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ /**
7
+ * v6.10.0 (P3) — split a large `05-plan.md` Implementation Units section
8
+ * into wave-NN.md sub-files so an executor can carry one wave at a time
9
+ * without re-reading the whole plan.
10
+ *
11
+ * Threshold contract:
12
+ * - total units < SMALL_PLAN_THRESHOLD → no-op, exit 0.
13
+ * - total units >= SMALL_PLAN_THRESHOLD → split into waves of `--wave-size`
14
+ * (default 25).
15
+ *
16
+ * Files written:
17
+ * - `<artifacts-dir>/wave-plans/wave-NN.md` per wave (1-indexed).
18
+ * - In-place update to `05-plan.md` adding (or refreshing) a
19
+ * `## Wave Plans` section between
20
+ * `<!-- wave-split-managed-start -->` and `<!-- wave-split-managed-end -->`
21
+ * markers. Outside-marker content is preserved verbatim.
22
+ *
23
+ * `--dry-run` prints the plan but does not write. `--force` overwrites
24
+ * existing wave files; without it, the command refuses to clobber.
25
+ */
26
+ export interface PlanSplitWavesArgs {
27
+ waveSize: number;
28
+ dryRun: boolean;
29
+ force: boolean;
30
+ json: boolean;
31
+ }
32
+ export declare const PLAN_SPLIT_DEFAULT_WAVE_SIZE = 25;
33
+ export declare const PLAN_SPLIT_SMALL_PLAN_THRESHOLD = 50;
34
+ export interface ParsedImplementationUnit {
35
+ id: string;
36
+ /**
37
+ * The full markdown body of this unit, starting at the
38
+ * `### Implementation Unit U-N` heading and ending right before the
39
+ * next unit heading (or the next `## ` H2, or end of file).
40
+ */
41
+ body: string;
42
+ /** Repo-relative path declarations from the optional `Files:` line. */
43
+ paths: string[];
44
+ }
45
+ /**
46
+ * Parse `## Implementation Units` section into individual unit blocks.
47
+ * Recognizes the canonical heading shape in the TDD-velocity plan template
48
+ * (`### Implementation Unit U-<n>`). Tolerant of `Files:` listed either
49
+ * inline or as a `- **Files (...):**` bullet block.
50
+ */
51
+ export declare function parseImplementationUnits(planMarkdown: string): ParsedImplementationUnit[];
52
+ /**
53
+ * Pull repo-relative paths from a `Files:` line or the `Files (...)` bullet
54
+ * block. Both shapes appear in the wild; the parser extracts after the colon
55
+ * and splits on commas. Empty/whitespace items are dropped.
56
+ */
57
+ export declare function extractPathsLine(unitBody: string): string[];
58
+ export declare function parsePlanSplitWavesArgs(tokens: string[]): PlanSplitWavesArgs;
59
+ /**
60
+ * Replace any existing managed Wave Plans block with the new one, or append
61
+ * it at the end of the file when no markers are present yet. The helper
62
+ * never touches text outside the markers.
63
+ */
64
+ export declare function upsertWavePlansSection(planMarkdown: string, managedBlock: string): string;
65
+ export declare function runPlanSplitWaves(projectRoot: string, args: PlanSplitWavesArgs, io: InternalIo): Promise<number>;
66
+ export {};