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.
- package/dist/artifact-linter/plan.js +37 -0
- package/dist/artifact-linter/shared.d.ts +51 -2
- package/dist/artifact-linter/shared.js +52 -4
- package/dist/artifact-linter/tdd.d.ts +42 -1
- package/dist/artifact-linter/tdd.js +443 -31
- package/dist/artifact-linter.js +93 -2
- package/dist/content/core-agents.d.ts +14 -0
- package/dist/content/core-agents.js +35 -0
- package/dist/content/examples.js +9 -9
- package/dist/content/hooks.js +202 -4
- package/dist/content/reference-patterns.js +2 -2
- package/dist/content/skills.js +1 -1
- package/dist/content/stage-schema.js +0 -3
- package/dist/content/stages/tdd.js +22 -23
- package/dist/content/subagents.js +12 -3
- package/dist/content/templates.d.ts +6 -0
- package/dist/content/templates.js +42 -34
- package/dist/delegation.d.ts +119 -0
- package/dist/delegation.js +171 -10
- package/dist/install.js +9 -0
- package/dist/internal/advance-stage.js +15 -3
- package/dist/internal/plan-split-waves.d.ts +66 -0
- package/dist/internal/plan-split-waves.js +249 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
1000
|
-
|
|
|
1001
|
-
|
|
1002
|
-
| S-1 | SRC-1 | AC-1 |
|
|
1003
|
-
|
|
1004
|
-
> Map each slice to the active track's source item: plan slice on standard/medium, or the \`Quick Reproduction Contract\` bug slice / spec acceptance item on quick.
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/delegation.d.ts
CHANGED
|
@@ -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
|
package/dist/delegation.js
CHANGED
|
@@ -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"
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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 {};
|