cclaw-cli 7.6.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.
- package/README.md +2 -1
- package/dist/artifact-linter/plan.js +98 -25
- package/dist/artifact-linter/tdd.js +2 -2
- package/dist/config.d.ts +15 -1
- package/dist/config.js +152 -2
- package/dist/content/core-agents.d.ts +1 -1
- package/dist/content/core-agents.js +4 -2
- package/dist/content/meta-skill.js +6 -4
- package/dist/content/skills.js +12 -8
- package/dist/content/stage-schema.js +2 -2
- package/dist/content/stages/plan.js +17 -17
- package/dist/content/stages/tdd.js +13 -10
- package/dist/content/start-command.js +3 -3
- package/dist/content/subagent-context-skills.js +2 -2
- package/dist/content/subagents.js +6 -6
- package/dist/content/templates.js +12 -7
- package/dist/delegation.d.ts +4 -3
- package/dist/delegation.js +14 -8
- package/dist/execution-topology.d.ts +44 -0
- package/dist/execution-topology.js +95 -0
- package/dist/internal/plan-split-waves.d.ts +5 -2
- package/dist/internal/plan-split-waves.js +14 -8
- package/dist/internal/wave-status.d.ts +16 -1
- package/dist/internal/wave-status.js +108 -13
- package/dist/types.d.ts +45 -0
- package/package.json +1 -1
|
@@ -71,8 +71,11 @@ export function extractMembersListFromLine(trimmedLine) {
|
|
|
71
71
|
*
|
|
72
72
|
* Rules:
|
|
73
73
|
* - The line must start with `|` (after trimming).
|
|
74
|
-
* - Column 1 (after stripping markdown noise)
|
|
75
|
-
*
|
|
74
|
+
* - Column 1 (after stripping markdown noise) may be either a slice id
|
|
75
|
+
* (`S-N`) or an implementation-unit id (`U-N`). Unit ids derive their
|
|
76
|
+
* execution slice as `S-N`, which lets 7.7+ plans schedule feature-atomic
|
|
77
|
+
* units without inventing a tiny `T-NNN` row per dispatch lane. Header rows
|
|
78
|
+
* (`| sliceId | …`, `| unit | …`) and separator rows (`|---|---|…`) are
|
|
76
79
|
* silently skipped.
|
|
77
80
|
* - Column 2, when present and non-empty, becomes the `unitId`
|
|
78
81
|
* verbatim (after stripping whitespace + backticks/quotes/brackets).
|
|
@@ -94,13 +97,16 @@ export function parseTableRowMember(trimmedLine) {
|
|
|
94
97
|
const stripDecorations = (raw) => raw.replace(/^[`"'[\]()]+|[`"'[\]()]+$/gu, "").trim();
|
|
95
98
|
const col1 = stripDecorations(cells[0]);
|
|
96
99
|
const parsedSlice = parseSliceId(col1);
|
|
97
|
-
|
|
100
|
+
const parsedUnit = tokenToSliceAndUnit(col1);
|
|
101
|
+
if (!parsedSlice && !parsedUnit)
|
|
98
102
|
return null;
|
|
99
|
-
const sliceTail = parsedSlice
|
|
100
|
-
?
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
const sliceTail = parsedSlice
|
|
104
|
+
? parsedSlice.suffix.length > 0
|
|
105
|
+
? `${parsedSlice.numeric}${parsedSlice.suffix}`
|
|
106
|
+
: `${parsedSlice.numeric}`
|
|
107
|
+
: "";
|
|
108
|
+
const sliceId = parsedSlice ? parsedSlice.id : parsedUnit.sliceId;
|
|
109
|
+
let unitId = parsedSlice ? `U-${sliceTail}` : parsedUnit.unitId;
|
|
104
110
|
if (cells.length >= 2) {
|
|
105
111
|
const col2 = stripDecorations(cells[1]);
|
|
106
112
|
if (col2.length > 0) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Writable } from "node:stream";
|
|
2
|
+
import type { ExecutionTopology } from "../types.js";
|
|
2
3
|
interface InternalIo {
|
|
3
4
|
stdout: Writable;
|
|
4
5
|
stderr: Writable;
|
|
@@ -12,11 +13,25 @@ export interface WaveStatusWaveSummary {
|
|
|
12
13
|
blockedMembers: string[];
|
|
13
14
|
status: "closed" | "open" | "partial" | "empty";
|
|
14
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
|
+
*/
|
|
15
22
|
export interface WaveStatusNextDispatch {
|
|
16
23
|
waveId: string | null;
|
|
17
24
|
readyToDispatch: string[];
|
|
18
25
|
pathConflicts: string[];
|
|
19
|
-
mode: "single-slice" | "wave-fanout" | "blocked" | "none";
|
|
26
|
+
mode: "single-slice" | "wave-fanout" | "blocked" | "none" | "controller-inline";
|
|
27
|
+
topology: Exclude<ExecutionTopology, "auto"> | "none";
|
|
28
|
+
topologyReason: string;
|
|
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;
|
|
20
35
|
}
|
|
21
36
|
export interface WaveStatusReport {
|
|
22
37
|
activeRunId: string;
|
|
@@ -4,6 +4,8 @@ import { RUNTIME_ROOT } from "../constants.js";
|
|
|
4
4
|
import { readDelegationEvents, readDelegationLedger } from "../delegation.js";
|
|
5
5
|
import { readFlowState } from "../runs.js";
|
|
6
6
|
import { DEFAULT_SLICE_STREAM_REL_PATH, readEventStreamFile } from "../streaming/event-stream.js";
|
|
7
|
+
import { readConfig, resolveExecutionStrictness, resolveExecutionTopology, resolveMaxBuilders } from "../config.js";
|
|
8
|
+
import { routeExecutionTopology } from "../execution-topology.js";
|
|
7
9
|
import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./plan-split-waves.js";
|
|
8
10
|
import { compareSliceIds, parseSliceId } from "../util/slice-id.js";
|
|
9
11
|
const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
|
|
@@ -55,7 +57,7 @@ function parsePipeRow(trimmedLine) {
|
|
|
55
57
|
function normalizePathToken(raw) {
|
|
56
58
|
return raw.trim().replace(/^`|`$/gu, "").replace(/^\.\/+/u, "");
|
|
57
59
|
}
|
|
58
|
-
function
|
|
60
|
+
function parseManagedWaveSliceMeta(planMarkdown) {
|
|
59
61
|
const out = new Map();
|
|
60
62
|
const startIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_START);
|
|
61
63
|
const endIdx = planMarkdown.indexOf(PARALLEL_EXEC_MANAGED_END);
|
|
@@ -82,7 +84,11 @@ function parseManagedWaveClaimedPaths(planMarkdown) {
|
|
|
82
84
|
if (cells.length === 0)
|
|
83
85
|
continue;
|
|
84
86
|
const first = cells[0].toLowerCase();
|
|
85
|
-
if (first === "sliceid" ||
|
|
87
|
+
if (first === "sliceid" ||
|
|
88
|
+
first === "slice id" ||
|
|
89
|
+
first === "unitid" ||
|
|
90
|
+
first === "unit id" ||
|
|
91
|
+
first === "unit") {
|
|
86
92
|
headerIdx = new Map();
|
|
87
93
|
for (let i = 0; i < cells.length; i += 1) {
|
|
88
94
|
const key = cells[i].toLowerCase().replace(/[^a-z0-9]/gu, "");
|
|
@@ -95,10 +101,12 @@ function parseManagedWaveClaimedPaths(planMarkdown) {
|
|
|
95
101
|
if (cells.every((cell) => /^:?-{3,}:?$/u.test(cell))) {
|
|
96
102
|
continue;
|
|
97
103
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
104
|
+
const firstCell = (cells[0] ?? "").replace(/^`|`$/gu, "").trim();
|
|
105
|
+
const parsedSlice = parseSliceId(firstCell);
|
|
106
|
+
const parsedUnit = /^U-(\d+(?:[a-z][a-z0-9]*)?)$/iu.exec(firstCell);
|
|
107
|
+
if (!parsedSlice && !parsedUnit)
|
|
100
108
|
continue;
|
|
101
|
-
const sliceId = parsedSlice.
|
|
109
|
+
const sliceId = parsedSlice?.id ?? `S-${parsedUnit[1].toLowerCase()}`;
|
|
102
110
|
const pathsIdx = headerIdx.get("claimedpaths");
|
|
103
111
|
const rawPaths = pathsIdx !== undefined ? (cells[pathsIdx] ?? "") : "";
|
|
104
112
|
const claimedPaths = rawPaths.length === 0
|
|
@@ -107,10 +115,52 @@ function parseManagedWaveClaimedPaths(planMarkdown) {
|
|
|
107
115
|
.split(",")
|
|
108
116
|
.map((p) => normalizePathToken(p))
|
|
109
117
|
.filter((p) => p.length > 0);
|
|
110
|
-
|
|
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
|
+
});
|
|
111
129
|
}
|
|
112
130
|
return out;
|
|
113
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
|
+
}
|
|
114
164
|
function detectPathConflicts(readySlices, bySlice) {
|
|
115
165
|
const conflicts = new Set();
|
|
116
166
|
const ordered = [...readySlices].sort(compareSliceIds);
|
|
@@ -175,7 +225,10 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
175
225
|
waveId: null,
|
|
176
226
|
readyToDispatch: [],
|
|
177
227
|
pathConflicts: [],
|
|
178
|
-
mode: "none"
|
|
228
|
+
mode: "none",
|
|
229
|
+
topology: "none",
|
|
230
|
+
topologyReason: "wave plan could not be parsed",
|
|
231
|
+
maxBuilders: 0
|
|
179
232
|
},
|
|
180
233
|
warnings: [
|
|
181
234
|
`wave_plan_parse_error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -203,7 +256,10 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
203
256
|
waveId: null,
|
|
204
257
|
readyToDispatch: [],
|
|
205
258
|
pathConflicts: [],
|
|
206
|
-
mode: "none"
|
|
259
|
+
mode: "none",
|
|
260
|
+
topology: "none",
|
|
261
|
+
topologyReason: "wave plan sources conflict",
|
|
262
|
+
maxBuilders: 0
|
|
207
263
|
},
|
|
208
264
|
warnings: [
|
|
209
265
|
`wave_plan_merge_conflict: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -331,30 +387,68 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
331
387
|
warnings.push("wave_plan_managed_block_missing: <!-- parallel-exec-managed-start --> block not found in 05-plan.md and wave-plans/ has no parseable wave files.");
|
|
332
388
|
}
|
|
333
389
|
let nextDispatch;
|
|
390
|
+
const config = await readConfig(projectRoot).catch(() => null);
|
|
391
|
+
const configuredTopology = resolveExecutionTopology(config);
|
|
392
|
+
const strictness = resolveExecutionStrictness(config);
|
|
393
|
+
const maxBuilders = resolveMaxBuilders(config);
|
|
334
394
|
if (firstOpenWave === null) {
|
|
335
395
|
nextDispatch = {
|
|
336
396
|
waveId: null,
|
|
337
397
|
readyToDispatch: [],
|
|
338
398
|
pathConflicts: [],
|
|
339
|
-
mode: "none"
|
|
399
|
+
mode: "none",
|
|
400
|
+
topology: "none",
|
|
401
|
+
topologyReason: "no open wave has ready units",
|
|
402
|
+
maxBuilders
|
|
340
403
|
};
|
|
341
404
|
}
|
|
342
405
|
else {
|
|
343
406
|
const readyToDispatch = [...firstOpenWave.readyMembers].sort(compareSliceIds);
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
const
|
|
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
|
|
347
412
|
? "blocked"
|
|
348
413
|
: readyToDispatch.length > 1
|
|
349
414
|
? "wave-fanout"
|
|
350
415
|
: readyToDispatch.length === 1
|
|
351
416
|
? "single-slice"
|
|
352
417
|
: "none";
|
|
418
|
+
const topologyDecision = baseMode === "none"
|
|
419
|
+
? null
|
|
420
|
+
: routeExecutionTopology({
|
|
421
|
+
configuredTopology,
|
|
422
|
+
strictness,
|
|
423
|
+
maxBuilders,
|
|
424
|
+
shape: {
|
|
425
|
+
unitCount: readyToDispatch.length,
|
|
426
|
+
independentUnitCount: conflicts.length > 0 ? 0 : readyToDispatch.length,
|
|
427
|
+
substantialUnitCount: readyToDispatch.length,
|
|
428
|
+
hasPathConflicts: conflicts.length > 0,
|
|
429
|
+
inlineSafe: false,
|
|
430
|
+
discoveryOnlyUnits
|
|
431
|
+
}
|
|
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;
|
|
353
443
|
nextDispatch = {
|
|
354
444
|
waveId: firstOpenWave.waveId,
|
|
355
445
|
readyToDispatch,
|
|
356
446
|
pathConflicts: conflicts,
|
|
357
|
-
mode
|
|
447
|
+
mode,
|
|
448
|
+
topology: topologyDecision?.topology ?? "none",
|
|
449
|
+
topologyReason: topologyDecision?.reason ?? "no ready units",
|
|
450
|
+
maxBuilders: topologyDecision?.maxBuilders ?? maxBuilders,
|
|
451
|
+
...(controllerHint ? { controllerHint } : {})
|
|
358
452
|
};
|
|
359
453
|
}
|
|
360
454
|
return {
|
|
@@ -382,6 +476,7 @@ function formatHumanReport(report) {
|
|
|
382
476
|
}
|
|
383
477
|
lines.push(`nextDispatch: wave=${report.nextDispatch.waveId ?? "(none)"} ` +
|
|
384
478
|
`mode=${report.nextDispatch.mode} ` +
|
|
479
|
+
`topology=${report.nextDispatch.topology} ` +
|
|
385
480
|
`ready=[${report.nextDispatch.readyToDispatch.join(",")}]`);
|
|
386
481
|
if (report.warnings.length > 0) {
|
|
387
482
|
lines.push("warnings:");
|
package/dist/types.d.ts
CHANGED
|
@@ -163,6 +163,10 @@ export interface ReviewLoopConfig {
|
|
|
163
163
|
export type VcsMode = "git-with-remote" | "git-local-only" | "none";
|
|
164
164
|
export type TddCommitMode = "managed-per-slice" | "agent-required" | "checkpoint-only" | "off";
|
|
165
165
|
export type TddIsolationMode = "worktree" | "in-place" | "auto";
|
|
166
|
+
export type ExecutionTopology = "auto" | "inline" | "single-builder" | "parallel-builders" | "strict-micro";
|
|
167
|
+
export type ExecutionStrictnessProfile = "fast" | "balanced" | "strict";
|
|
168
|
+
export type PlanSliceGranularity = "feature-atomic" | "strict-micro";
|
|
169
|
+
export type PlanMicroTaskPolicy = "advisory" | "strict";
|
|
166
170
|
/**
|
|
167
171
|
* 7.6.0 — what slice-commit does when a manifest in the slice's
|
|
168
172
|
* claim drifts its corresponding lockfile twin (e.g. Cargo.toml +
|
|
@@ -206,11 +210,52 @@ export interface TddConfig {
|
|
|
206
210
|
*/
|
|
207
211
|
lockfileTwinPolicy?: LockfileTwinPolicy;
|
|
208
212
|
}
|
|
213
|
+
export interface ExecutionConfig {
|
|
214
|
+
/**
|
|
215
|
+
* 7.7.0 — adaptive execution topology for implementation units.
|
|
216
|
+
*
|
|
217
|
+
* - `auto` (default): choose the cheapest safe route from plan shape.
|
|
218
|
+
* - `inline`: controller may execute a low-risk unit inline while still
|
|
219
|
+
* recording RED/GREEN/REFACTOR evidence.
|
|
220
|
+
* - `single-builder`: one slice-builder owns a feature-atomic unit.
|
|
221
|
+
* - `parallel-builders`: fan out independent substantial units only.
|
|
222
|
+
* - `strict-micro`: preserve the pre-7.7 posture where each tiny task is
|
|
223
|
+
* its own schedulable slice.
|
|
224
|
+
*/
|
|
225
|
+
topology?: ExecutionTopology;
|
|
226
|
+
/**
|
|
227
|
+
* 7.7.0 — default calibration for topology and plan-shape advisories.
|
|
228
|
+
* `balanced` is the default: prefer feature-atomic units with internal
|
|
229
|
+
* 2-5 minute TDD steps, warning on microtask-only plans without blocking.
|
|
230
|
+
*/
|
|
231
|
+
strictness?: ExecutionStrictnessProfile;
|
|
232
|
+
/**
|
|
233
|
+
* 7.7.0 — upper bound for simultaneous slice-builder workers when the
|
|
234
|
+
* router selects `parallel-builders`.
|
|
235
|
+
*/
|
|
236
|
+
maxBuilders?: number;
|
|
237
|
+
}
|
|
238
|
+
export interface PlanConfig {
|
|
239
|
+
/**
|
|
240
|
+
* 7.7.0 — default schedulable surface for plan authoring.
|
|
241
|
+
* - feature-atomic: U-* slices contain internal 2-5 minute TDD steps.
|
|
242
|
+
* - strict-micro: one tiny task can remain one schedulable slice.
|
|
243
|
+
*/
|
|
244
|
+
sliceGranularity?: PlanSliceGranularity;
|
|
245
|
+
/**
|
|
246
|
+
* 7.7.0 — how strongly the plan linter reacts to microtask-only plans.
|
|
247
|
+
* `advisory` warns in fast/balanced execution; `strict` treats strict
|
|
248
|
+
* microtask planning as intentional.
|
|
249
|
+
*/
|
|
250
|
+
microTaskPolicy?: PlanMicroTaskPolicy;
|
|
251
|
+
}
|
|
209
252
|
export interface CclawConfig {
|
|
210
253
|
version: string;
|
|
211
254
|
flowVersion: string;
|
|
212
255
|
harnesses: HarnessId[];
|
|
213
256
|
tdd?: TddConfig;
|
|
257
|
+
execution?: ExecutionConfig;
|
|
258
|
+
plan?: PlanConfig;
|
|
214
259
|
}
|
|
215
260
|
export interface TransitionRule {
|
|
216
261
|
from: FlowStage;
|