cclaw-cli 7.7.0 → 7.7.1

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.
@@ -153,6 +153,8 @@ export function sliceBuilderProtocol() {
153
153
  "",
154
154
  "**slice-builder** is the canonical worker for **one feature-atomic implementation unit/slice** end-to-end: **RED → GREEN → REFACTOR → inline DOC** in **one** delegated span. The unit may contain internal 2-5 minute TDD steps; do not split those into separate agents unless the parent selected `strict-micro`. Multiple slice-builder spans run in parallel only when topology is `parallel-builders` and the wave plan declares disjoint `claimedPaths`.",
155
155
  "",
156
+ "**Multi-slice batch under `single-builder`:** the controller may dispatch ONE slice-builder span that owns MORE than one ready slice (e.g. when the router collapses a wave into single-builder mode). Treat each slice in the batch as its own RED → GREEN → REFACTOR → DOC mini-cycle: emit a separate `phase=red|green|refactor|doc` row per `--slice` value (use the same `spanId`/`dispatchId` for the whole batch), keep `claimedPaths` disjoint per-slice within the batch, and author one `tdd-slices/S-<id>.md` per slice. Phase-status rules from the table below stay unchanged — never collapse multiple slices into one phase row.",
157
+ "",
156
158
  "### Invariants",
157
159
  "- Produce failing RED evidence (or cite the delegated RED artifact) **before** production edits.",
158
160
  "- Stay inside the slice contract: `claimedPaths`, acceptance mapping, and forbidden-change lists from the parent.",
@@ -57,15 +57,17 @@ If you think any of these, stop and follow the routing flow:
57
57
  - "I can answer from memory without loading the active stage skill." -> No. Load the skill first.
58
58
  - "Hook guard warned, but I can ignore it." -> No. Resolve the warning before continuing.
59
59
  - "I'll edit \`.cclaw/state\` directly to move faster." -> No. Use managed commands only.
60
- - "I'll just do the worker's job inline so we move faster." -> Only if the active TDD topology is explicitly \`inline\`; otherwise see the Controller dispatch discipline below.
60
+ - "I'll just do the worker's job inline so we move faster." -> Only if the active TDD topology is explicitly \`inline\` (\`nextDispatch.mode = controller-inline\` from \`wave-status\`); otherwise see the Controller dispatch discipline below.
61
61
 
62
62
  ## Controller dispatch discipline (applies to every stage)
63
63
 
64
- cclaw stages have **mandatory delegations** (TDD normally routes through \`slice-builder\`; review: \`reviewer\` + \`security-reviewer\`; design: \`architect\`; scope: \`planner\`; etc.). The controller is the **orchestrator**, not the worker, except when TDD topology is explicitly \`inline\` for a low-risk unit. When a stage declares a mandatory delegation:
64
+ cclaw stages have **mandatory delegations** (TDD normally routes through \`slice-builder\`; review: \`reviewer\` + \`security-reviewer\`; design: \`architect\`; scope: \`planner\`; etc.). The controller is the **orchestrator**, not the worker, except when the TDD router selects \`inline\` (a non-high-risk discovery/scaffold/docs ready set with no path conflicts). When a stage declares a mandatory delegation:
65
65
 
66
- - **Dispatch via the harness Task tool unless topology says inline.** Do NOT write the worker's output (slice code, review findings, architect notes) into the artifact yourself as a substitute for delegating. For TDD \`inline\`, record the routing reason and satisfy the same RED/GREEN/REFACTOR, AC traceability, path containment, managed commit/worktree, lockfile twin, and orphan-change gates.
67
- - **Parallel only when topology says parallel-builders.** TDD fan-out requires genuinely independent substantial units with disjoint \`claimedPaths\`; review-army (independent reviewer lenses) MUST emit all parallel \`Task\` calls in a SINGLE controller message not sequentially over multiple turns. The controller waits for all spans to return before reconciling.
66
+ - **Dispatch via the harness Task tool unless topology says inline.** Do NOT write the worker's output (slice code, review findings, architect notes) into the artifact yourself as a substitute for delegating. For TDD \`inline\`, record \`delegation-record\` lifecycle rows with \`--dispatch-surface=role-switch\` and \`--agent-definition-path=.cclaw/skills/tdd/SKILL.md\` (scheduled → completed with \`--evidence-ref\`) and satisfy the same RED/GREEN/REFACTOR, AC traceability, path containment, managed commit/worktree, lockfile twin, and orphan-change gates.
67
+ - **\`single-builder\`: one Task covers the wave.** When the router chooses \`single-builder\` and the ready set has more than one slice, issue exactly ONE \`Task\` dispatch and let that single \`slice-builder\` span own multi-slice TDD for every currently ready member. Do not split the ready set into multiple Task calls when the router chose single-builder.
68
+ - **Parallel only when topology says parallel-builders.** TDD fan-out requires genuinely independent substantial units with disjoint \`claimedPaths\` and at least one non-discovery ready unit (pure discovery-only sets are collapsed by the router); review-army (independent reviewer lenses) MUST emit all parallel \`Task\` calls in a SINGLE controller message — not sequentially over multiple turns. The controller waits for all spans to return before reconciling.
68
69
  - **Record lifecycle on the same span** via \`delegation-record --status=scheduled|launched|acknowledged|completed\`; the worker emits its own \`--phase=…\` and evidence rows. A \`completed\` row without a matching ACK or dispatch surface is a forgery.
70
+ - **Trust the router; skip routing AskQuestions.** \`wave-status\` already returns \`nextDispatch.topology\`, \`nextDispatch.mode\`, and (when present) \`nextDispatch.controllerHint\`. Act on them; do not wrap an extra "launch wave or single builder?" confirmation around routing.
69
71
  - **Auto-advance when stage-complete returns ok.** When the helper reports a new \`currentStage\`, immediately load the next stage skill and continue. Announce \`Stage <prev> complete → entering <next>. Continuing.\` Do NOT pause for the user to retype \`/cc\` or say \"продолжай\" — that pause is the failure mode 7.0.2 explicitly removed. The only legitimate stop is a real blocker (missing user input, ambiguous decision, hook fail).
70
72
 
71
73
  ## Routing flow
@@ -37,21 +37,21 @@ export const TDD = {
37
37
  },
38
38
  executionModel: {
39
39
  checklist: [
40
- "**Wave discovery:** Entering TDD, first call `node .cclaw/cli.mjs internal wave-status --json`. It parses the managed `<!-- parallel-exec-managed-start -->` block and reports the adaptive `topology`; read `05-plan.md`/`wave-plans/` only once `wave-status` names work. Restore partial waves by resuming remaining units.",
41
- "**Topology routing:** Default `auto` + `balanced` means cheapest safe route: inline for one low-risk inline-safe unit, single-builder for one feature-atomic unit or conflicts, parallel-builders only for genuinely independent substantial units, strict-micro for high-risk/configured micro-slice work.",
42
- "**Routing AskQuestion:** Two or more ready units with `topology=parallel-builders` ⇒ exactly one AskQuestion (“launch wave …” vs “single builder …”, default wave). Otherwise do not ask “which slice next?” when the plan already resolves it.",
40
+ "**Wave discovery:** Entering TDD, first call `node .cclaw/cli.mjs internal wave-status --json`. It parses the managed `<!-- parallel-exec-managed-start -->` block and reports `nextDispatch.topology`, `nextDispatch.mode`, and (when present) `nextDispatch.controllerHint`; read `05-plan.md`/`wave-plans/` only once `wave-status` names work. Restore partial waves by resuming remaining units.",
41
+ "**Topology routing:** Default `auto` + `balanced` means cheapest safe route: lane-aware inline collapse for non-high-risk discovery/scaffold/docs ready sets, single-builder for one feature-atomic unit or large discovery-only batches, parallel-builders only when at least one ready unit is non-discovery and the wave is genuinely independent + substantial, strict-micro for high-risk/configured micro-slice work. The router decides; the controller acts on its output without re-asking the user which slice to take next.",
43
42
  "**Record before dispatch:** For every `Task`, write `delegation-record` `--status=scheduled` then `--status=launched` before the tool call. Workers self-record `acknowledged` and `completed`; back-fill is `--repair` only.",
44
- "**One worker per feature-atomic unit when delegated:** Dispatch `slice-builder` with `--slice S-<id>` and explicit `--paths` from the plan. The worker owns the unit's internal 2-5 minute RED/GREEN/REFACTOR steps. Parallel builders are allowed only when topology says `parallel-builders` and paths are disjoint; honor any lane/lease flags the hook requires today.",
45
- "**Inline topology:** When the router/guidance chooses `inline`, the controller may execute the unit without a builder dispatch, but must still keep RED-before-GREEN, AC traceability, path containment, verification-before-completion, lockfile twin handling, managed commit/worktree policy, and orphan-change gates intact.",
46
- "**Single span owns the delegated unit:** `slice-builder` runs RED GREEN REFACTOR (separate phase rows or `--refactor-outcome` on GREEN) and authors `<artifacts-dir>/tdd-slices/S-<id>.md`. Follow the agent body and `delegation-record` snippets it embeds.",
43
+ "**Honor `topology=inline` (controller-inline):** Do NOT use `Task` to dispatch `slice-builder` for inline slices. Write each ready slice's `claimedPaths` and any small file edits yourself, then record lifecycle rows with `--dispatch-surface=role-switch --agent-definition-path=.cclaw/skills/tdd/SKILL.md` (scheduled → completed, with `--evidence-ref` on completion). RED-before-GREEN, AC traceability, path containment, verification-before-completion, lockfile twin handling, managed commit/worktree policy, and orphan-change gates remain intact the router only changes who does the work, not what evidence is required.",
44
+ "**Honor `topology=single-builder`:** Issue exactly ONE `Task` dispatch that covers ALL currently ready members of the wave. The single `slice-builder` span owns multi-slice TDD: RED GREEN REFACTOR (and inline DOC card) for every slice it received. Do not split that ready set into multiple separate Task calls when the router chose `single-builder`.",
45
+ "**Honor `topology=parallel-builders`:** Fan out only when at least one ready unit is non-discovery (router collapses pure discovery-only sets into inline/single-builder per the previous bullets) and `claimedPaths` are disjoint. Emit all parallel `Task` calls in a single controller message and wait for all spans before reconciling.",
46
+ "**Single span owns the delegated unit:** Dispatched `slice-builder` runs RED → GREEN → REFACTOR (separate phase rows or `--refactor-outcome` on GREEN) and authors `<artifacts-dir>/tdd-slices/S-<id>.md`. Follow the agent body and `delegation-record` snippets it embeds.",
47
47
  "**Wave closure:** When every slice/unit in the wave has GREEN + REFACTOR coverage, call `integrationCheckRequired`. Dispatch `integration-overseer` when required; otherwise emit `cclaw_integration_overseer_skipped` via `delegation-record --audit-kind=...`.",
48
48
  "**Plan triggers:** If the unit row demands extra scrutiny (`touchCount >= filesChangedThreshold`, matching `touchPaths`, or `highRisk`), capture that review posture in `tdd-slices/S-<id>.md` or via a reviewer dispatch before closing the slice.",
49
49
  "**Auto-render tables:** Do not hand-edit content between `auto-start: tdd-slice-summary` markers; the linter overwrites them from `delegation-events.jsonl`.",
50
50
  "**Active-span collisions:** If scheduling fails with `dispatch_duplicate` / `dispatch_active_span_collision`, identify the live span; use `--allow-parallel` or `--supersede` deliberately. Do not silence errors blindly.",
51
51
  ],
52
52
  interactionProtocol: [
53
- "Parallel `slice-builder` tasks are allowed only when topology is `parallel-builders` and `claimedPaths` are disjoint; remain serial for dependencies, conflicts, high-risk units, or strict-micro chains.",
54
- "Controller normally orchestrates delegated builders. In `inline` topology it may execute directly, but must record the routing decision and satisfy the same RED/GREEN/REFACTOR evidence gates before completion.",
53
+ "Parallel `slice-builder` tasks are allowed only when topology is `parallel-builders` and `claimedPaths` are disjoint; remain serial for dependencies, conflicts, high-risk units, or strict-micro chains. Pure discovery/scaffold/docs ready sets collapse to inline (or single-builder for large batches) — never one worker per markdown spike.",
54
+ "Controller normally orchestrates delegated builders. When topology is `inline` (mode `controller-inline`) it executes the wave's ready slices itself with `--dispatch-surface=role-switch` lifecycle rows, but must satisfy the same RED/GREEN/REFACTOR evidence gates before completion.",
55
55
  "Discover existing tests and commands before RED; run a system-wide impact check (callbacks, state, interfaces, contracts) before GREEN.",
56
56
  "RED must fail for the right reason; capture logs. GREEN must run the full relevant suite, not a narrow subset.",
57
57
  "Before calling a slice done, run verification-before-completion (command + PASS/FAIL + durable commit evidence: managed-per-slice git commits when `.git` is present, or explicit no-VCS attestation + hash).",
@@ -61,7 +61,7 @@ export const TDD = {
61
61
  process: [
62
62
  "Map the slice to acceptance criteria; read `ralph-loop.json` for open RED cycles before starting new work.",
63
63
  "Discover tests, fixtures, helpers, and commands; record impact on public surfaces.",
64
- "Route the active unit via topology (`inline`, `single-builder`, `parallel-builders`, or `strict-micro`) before RED.",
64
+ "Read the topology + mode + controllerHint reported by `wave-status --json` and act on them (inline = controller fulfils ready set; single-builder = one Task covers all ready members; parallel-builders = one Task per disjoint ready unit; strict-micro = micro-slice fan-out).",
65
65
  "For delegated units, dispatch `slice-builder` for RED (failing tests, no production edits beyond test files).",
66
66
  "For delegated units, dispatch the same builder for GREEN with minimal production changes and full-suite evidence.",
67
67
  "Close REFACTOR inline, via deferred phase, or `--refactor-outcome` on GREEN — match what `delegation-record` expects.",
@@ -14,6 +14,14 @@ export interface ExecutionTopologyShape {
14
14
  requiresStrictMicro?: boolean;
15
15
  /** True when the controller can safely execute the unit inline in the current harness. */
16
16
  inlineSafe?: boolean;
17
+ /**
18
+ * 7.7.1 — count of ready units classified as discovery-only/scaffold/docs
19
+ * lanes that are not high-risk and have no path conflicts. When this equals
20
+ * `unitCount`, the auto router collapses the wave into `inline` (small
21
+ * batch) or `single-builder` (large batch) so trivial markdown spikes never
22
+ * fan out into one slice-builder span per file.
23
+ */
24
+ discoveryOnlyUnits?: number;
17
25
  }
18
26
  export interface ExecutionTopologyDecision {
19
27
  topology: Exclude<ExecutionTopology, "auto">;
@@ -47,6 +47,28 @@ export function routeExecutionTopology(options) {
47
47
  reason: "path conflicts require serialized execution"
48
48
  };
49
49
  }
50
+ // 7.7.1 — lane-aware short-circuit. When every ready unit is a non-high-risk
51
+ // discovery/scaffold/docs lane with no path conflicts, never fan out: the
52
+ // controller should fulfil a small batch inline, or hand the whole wave to a
53
+ // single builder when the batch is large enough that one inline turn would
54
+ // bloat the controller transcript.
55
+ const discoveryOnly = shape.discoveryOnlyUnits ?? 0;
56
+ if (!shape.highRisk &&
57
+ discoveryOnly === unitCount &&
58
+ unitCount >= 1) {
59
+ if (unitCount <= 3) {
60
+ return {
61
+ topology: "inline",
62
+ maxBuilders,
63
+ reason: "discovery-only ready set; controller can fulfill inline"
64
+ };
65
+ }
66
+ return {
67
+ topology: "single-builder",
68
+ maxBuilders,
69
+ reason: "discovery-only ready set is large; one builder owns the wave"
70
+ };
71
+ }
50
72
  const independent = shape.independentUnitCount ?? unitCount;
51
73
  const substantial = shape.substantialUnitCount ?? unitCount;
52
74
  if (maxBuilders > 1 && independent >= 2 && substantial >= 2) {
@@ -13,14 +13,25 @@ export interface WaveStatusWaveSummary {
13
13
  blockedMembers: string[];
14
14
  status: "closed" | "open" | "partial" | "empty";
15
15
  }
16
+ /**
17
+ * 7.7.1 — `controller-inline` is added so the controller knows it must
18
+ * fulfil the ready slices in this turn (no slice-builder dispatch). The
19
+ * remaining values (`single-slice`, `wave-fanout`, `blocked`, `none`) keep
20
+ * their pre-7.7.1 contract.
21
+ */
16
22
  export interface WaveStatusNextDispatch {
17
23
  waveId: string | null;
18
24
  readyToDispatch: string[];
19
25
  pathConflicts: string[];
20
- mode: "single-slice" | "wave-fanout" | "blocked" | "none";
26
+ mode: "single-slice" | "wave-fanout" | "blocked" | "none" | "controller-inline";
21
27
  topology: Exclude<ExecutionTopology, "auto"> | "none";
22
28
  topologyReason: string;
23
29
  maxBuilders: number;
30
+ /**
31
+ * Optional natural-language hint for the controller, populated when the
32
+ * router selects a non-default mode (currently only `inline`).
33
+ */
34
+ controllerHint?: string;
24
35
  }
25
36
  export interface WaveStatusReport {
26
37
  activeRunId: string;
@@ -57,7 +57,7 @@ function parsePipeRow(trimmedLine) {
57
57
  function normalizePathToken(raw) {
58
58
  return raw.trim().replace(/^`|`$/gu, "").replace(/^\.\/+/u, "");
59
59
  }
60
- function parseManagedWaveClaimedPaths(planMarkdown) {
60
+ function parseManagedWaveSliceMeta(planMarkdown) {
61
61
  const out = new Map();
62
62
  const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
63
63
  const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
@@ -115,10 +115,52 @@ function parseManagedWaveClaimedPaths(planMarkdown) {
115
115
  .split(",")
116
116
  .map((p) => normalizePathToken(p))
117
117
  .filter((p) => p.length > 0);
118
- out.get(currentWaveId).set(sliceId, claimedPaths);
118
+ const laneIdx = headerIdx.get("lane");
119
+ const rawLane = laneIdx !== undefined ? (cells[laneIdx] ?? "") : "";
120
+ const lane = rawLane.replace(/^`|`$/gu, "").trim().toLowerCase();
121
+ const riskIdx = headerIdx.get("risktier");
122
+ const rawRisk = riskIdx !== undefined ? (cells[riskIdx] ?? "") : "";
123
+ const riskTier = rawRisk.replace(/^`|`$/gu, "").trim().toLowerCase();
124
+ out.get(currentWaveId).set(sliceId, {
125
+ claimedPaths,
126
+ lane: lane.length > 0 ? lane : undefined,
127
+ riskTier: riskTier.length > 0 ? riskTier : undefined
128
+ });
119
129
  }
120
130
  return out;
121
131
  }
132
+ /**
133
+ * Backward-compatible projection: callers that only need claimed paths
134
+ * keep getting `Map<sliceId, paths[]>` shapes.
135
+ */
136
+ function projectClaimedPaths(meta) {
137
+ const out = new Map();
138
+ for (const [sliceId, info] of meta) {
139
+ out.set(sliceId, info.claimedPaths);
140
+ }
141
+ return out;
142
+ }
143
+ const DISCOVERY_LANES = new Set(["scaffold", "docs"]);
144
+ /**
145
+ * 7.7.1 — count ready members whose lane is `scaffold` or `docs` and whose
146
+ * `riskTier` is not `high`. Members with no recorded lane fall back to
147
+ * non-discovery (so the controller never collapses substantive work into
148
+ * inline by accident).
149
+ */
150
+ function countDiscoveryOnlyUnits(readySlices, meta) {
151
+ let count = 0;
152
+ for (const sliceId of readySlices) {
153
+ const info = meta.get(sliceId);
154
+ if (!info)
155
+ continue;
156
+ if (!info.lane || !DISCOVERY_LANES.has(info.lane))
157
+ continue;
158
+ if (info.riskTier === "high")
159
+ continue;
160
+ count += 1;
161
+ }
162
+ return count;
163
+ }
122
164
  function detectPathConflicts(readySlices, bySlice) {
123
165
  const conflicts = new Set();
124
166
  const ordered = [...readySlices].sort(compareSliceIds);
@@ -362,16 +404,18 @@ export async function runWaveStatus(projectRoot, options = {}) {
362
404
  }
363
405
  else {
364
406
  const readyToDispatch = [...firstOpenWave.readyMembers].sort(compareSliceIds);
365
- const claimedPathsByWave = parseManagedWaveClaimedPaths(planRaw);
366
- const conflicts = detectPathConflicts(readyToDispatch, claimedPathsByWave.get(firstOpenWave.waveId) ?? new Map());
367
- const mode = conflicts.length > 0
407
+ const sliceMetaByWave = parseManagedWaveSliceMeta(planRaw);
408
+ const sliceMeta = sliceMetaByWave.get(firstOpenWave.waveId) ?? new Map();
409
+ const conflicts = detectPathConflicts(readyToDispatch, projectClaimedPaths(sliceMeta));
410
+ const discoveryOnlyUnits = countDiscoveryOnlyUnits(readyToDispatch, sliceMeta);
411
+ const baseMode = conflicts.length > 0
368
412
  ? "blocked"
369
413
  : readyToDispatch.length > 1
370
414
  ? "wave-fanout"
371
415
  : readyToDispatch.length === 1
372
416
  ? "single-slice"
373
417
  : "none";
374
- const topologyDecision = mode === "none"
418
+ const topologyDecision = baseMode === "none"
375
419
  ? null
376
420
  : routeExecutionTopology({
377
421
  configuredTopology,
@@ -382,9 +426,20 @@ export async function runWaveStatus(projectRoot, options = {}) {
382
426
  independentUnitCount: conflicts.length > 0 ? 0 : readyToDispatch.length,
383
427
  substantialUnitCount: readyToDispatch.length,
384
428
  hasPathConflicts: conflicts.length > 0,
385
- inlineSafe: false
429
+ inlineSafe: false,
430
+ discoveryOnlyUnits
386
431
  }
387
432
  });
433
+ // 7.7.1 — when the router collapses a discovery-only ready set into
434
+ // `inline`, surface `controller-inline` mode + a controllerHint so the
435
+ // controller knows it must fulfil the slices this turn instead of
436
+ // dispatching slice-builder per file.
437
+ const mode = topologyDecision?.topology === "inline"
438
+ ? "controller-inline"
439
+ : baseMode;
440
+ const controllerHint = topologyDecision?.topology === "inline"
441
+ ? "Fulfill ready slices in this turn without dispatching slice-builder. Record delegation rows with role=controller (scheduled→completed) per slice."
442
+ : undefined;
388
443
  nextDispatch = {
389
444
  waveId: firstOpenWave.waveId,
390
445
  readyToDispatch,
@@ -392,7 +447,8 @@ export async function runWaveStatus(projectRoot, options = {}) {
392
447
  mode,
393
448
  topology: topologyDecision?.topology ?? "none",
394
449
  topologyReason: topologyDecision?.reason ?? "no ready units",
395
- maxBuilders: topologyDecision?.maxBuilders ?? maxBuilders
450
+ maxBuilders: topologyDecision?.maxBuilders ?? maxBuilders,
451
+ ...(controllerHint ? { controllerHint } : {})
396
452
  };
397
453
  }
398
454
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "7.7.0",
3
+ "version": "7.7.1",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {