cclaw-cli 7.3.0 → 7.4.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.
|
@@ -165,6 +165,11 @@ export function sliceBuilderProtocol() {
|
|
|
165
165
|
"- Honor every `delegation-record`/`delegation-record.mjs` row shape the controller requests so artifact linters keep passing.",
|
|
166
166
|
"- The umbrella `slice-completed` row ties RED/GREEN/REFACTOR/DOC timestamps to your builder span.",
|
|
167
167
|
"",
|
|
168
|
+
"### Streaming output contract",
|
|
169
|
+
"- Emit one JSON line to stdout per completed phase: `{\"event\":\"phase-completed\",\"stage\":\"tdd\",\"sliceId\":\"S-<n>\",\"phase\":\"<red|green|refactor|refactor-deferred|doc>\",\"spanId\":\"<span>\",\"runId\":\"<run>\",\"ts\":\"<iso>\"}`.",
|
|
170
|
+
"- For `phase=green` with inline/deferred refactor folding, include `refactorOutcome.mode` in the same JSON line so live controllers can close the slice without waiting for file sync.",
|
|
171
|
+
"- If streaming is unavailable in your harness/runtime, keep writing canonical `delegation-record` rows; controller-side live mode will fall back to file-based events.",
|
|
172
|
+
"",
|
|
168
173
|
"**Role boundary:** do not widen scope, do not self-approve ship-level review, and do not recurse into other agents unless the parent explicitly directs it."
|
|
169
174
|
].join("\n");
|
|
170
175
|
}
|
|
@@ -32,6 +32,17 @@ export interface RunWaveStatusOptions {
|
|
|
32
32
|
* `<projectRoot>/.cclaw/artifacts`.
|
|
33
33
|
*/
|
|
34
34
|
artifactsDir?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Event ingestion mode:
|
|
37
|
+
* - auto: prefer live stream file; fallback to delegation-events.jsonl.
|
|
38
|
+
* - live: force live stream first; fallback to delegation-events.jsonl with warning.
|
|
39
|
+
* - file: skip stream file and use delegation-events.jsonl only.
|
|
40
|
+
*/
|
|
41
|
+
streamMode?: "auto" | "live" | "file";
|
|
42
|
+
/**
|
|
43
|
+
* Optional absolute stream path override (tests).
|
|
44
|
+
*/
|
|
45
|
+
streamPath?: string;
|
|
35
46
|
}
|
|
36
47
|
/**
|
|
37
48
|
* Deterministic helper for the TDD controller. Reads the managed
|
|
@@ -3,16 +3,21 @@ import path from "node:path";
|
|
|
3
3
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
4
4
|
import { readDelegationEvents, readDelegationLedger } from "../delegation.js";
|
|
5
5
|
import { readFlowState } from "../runs.js";
|
|
6
|
+
import { DEFAULT_SLICE_STREAM_REL_PATH, readEventStreamFile } from "../streaming/event-stream.js";
|
|
6
7
|
import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./plan-split-waves.js";
|
|
7
8
|
const PARALLEL_EXEC_MANAGED_START = "<!-- parallel-exec-managed-start -->";
|
|
8
9
|
const PARALLEL_EXEC_MANAGED_END = "<!-- parallel-exec-managed-end -->";
|
|
9
10
|
function parseArgs(tokens) {
|
|
10
|
-
const args = { format: "json" };
|
|
11
|
+
const args = { format: "json", streamMode: "auto" };
|
|
11
12
|
for (const token of tokens) {
|
|
12
13
|
if (token === "--json")
|
|
13
14
|
args.format = "json";
|
|
14
15
|
else if (token === "--human" || token === "--text")
|
|
15
16
|
args.format = "human";
|
|
17
|
+
else if (token === "--live")
|
|
18
|
+
args.streamMode = "live";
|
|
19
|
+
else if (token === "--file-only")
|
|
20
|
+
args.streamMode = "file";
|
|
16
21
|
else if (token.startsWith("--format=")) {
|
|
17
22
|
const raw = token.slice("--format=".length).trim();
|
|
18
23
|
if (raw === "json" || raw === "human")
|
|
@@ -20,6 +25,13 @@ function parseArgs(tokens) {
|
|
|
20
25
|
else
|
|
21
26
|
throw new Error(`Unknown wave-status --format value: ${raw}`);
|
|
22
27
|
}
|
|
28
|
+
else if (token.startsWith("--stream-mode=")) {
|
|
29
|
+
const raw = token.slice("--stream-mode=".length).trim();
|
|
30
|
+
if (raw === "auto" || raw === "live" || raw === "file")
|
|
31
|
+
args.streamMode = raw;
|
|
32
|
+
else
|
|
33
|
+
throw new Error(`Unknown wave-status --stream-mode value: ${raw}`);
|
|
34
|
+
}
|
|
23
35
|
else {
|
|
24
36
|
throw new Error(`Unknown wave-status flag: ${token}`);
|
|
25
37
|
}
|
|
@@ -228,34 +240,72 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
228
240
|
}
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
if (ev.phase === "green" && ev.refactorOutcome) {
|
|
250
|
-
const mode = ev.refactorOutcome.mode;
|
|
251
|
-
if (mode === "inline" || mode === "deferred") {
|
|
243
|
+
const warnings = [];
|
|
244
|
+
const streamMode = options.streamMode ?? "auto";
|
|
245
|
+
const streamPath = options.streamPath ?? path.join(projectRoot, DEFAULT_SLICE_STREAM_REL_PATH);
|
|
246
|
+
let shouldReadJsonlFallback = streamMode === "file";
|
|
247
|
+
// Prefer live stream events when requested/available.
|
|
248
|
+
if (streamMode !== "file") {
|
|
249
|
+
const streamResult = await readEventStreamFile(streamPath);
|
|
250
|
+
if (streamResult.droppedLines > 0) {
|
|
251
|
+
warnings.push(`wave_stream_dropped_lines: ${streamResult.droppedLines}`);
|
|
252
|
+
}
|
|
253
|
+
if (streamResult.events.length > 0) {
|
|
254
|
+
for (const ev of streamResult.events) {
|
|
255
|
+
if (ev.runId && ev.runId !== activeRunId)
|
|
256
|
+
continue;
|
|
257
|
+
if (ev.stage && ev.stage !== "tdd")
|
|
258
|
+
continue;
|
|
259
|
+
if (TERMINAL_PHASES.has(ev.phase)) {
|
|
252
260
|
closedSlices.add(ev.sliceId);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (ev.phase === "green" && ev.refactorOutcome) {
|
|
264
|
+
const mode = ev.refactorOutcome.mode;
|
|
265
|
+
if (mode === "inline" || mode === "deferred") {
|
|
266
|
+
closedSlices.add(ev.sliceId);
|
|
267
|
+
}
|
|
253
268
|
}
|
|
254
269
|
}
|
|
270
|
+
shouldReadJsonlFallback = false;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
shouldReadJsonlFallback = true;
|
|
274
|
+
if (streamMode === "auto" || streamMode === "live") {
|
|
275
|
+
warnings.push("wave_status_live_fallback_to_file: no parseable stream events");
|
|
276
|
+
}
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
|
-
|
|
258
|
-
|
|
279
|
+
// Fallback to JSONL events when live stream is absent or disabled.
|
|
280
|
+
if (shouldReadJsonlFallback) {
|
|
281
|
+
try {
|
|
282
|
+
const { events } = await readDelegationEvents(projectRoot);
|
|
283
|
+
for (const ev of events) {
|
|
284
|
+
if (ev.event !== "completed")
|
|
285
|
+
continue;
|
|
286
|
+
if (ev.runId !== activeRunId)
|
|
287
|
+
continue;
|
|
288
|
+
if (ev.stage !== "tdd")
|
|
289
|
+
continue;
|
|
290
|
+
if (typeof ev.sliceId !== "string")
|
|
291
|
+
continue;
|
|
292
|
+
if (typeof ev.phase !== "string")
|
|
293
|
+
continue;
|
|
294
|
+
if (TERMINAL_PHASES.has(ev.phase)) {
|
|
295
|
+
closedSlices.add(ev.sliceId);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (ev.phase === "green" && ev.refactorOutcome) {
|
|
299
|
+
const mode = ev.refactorOutcome.mode;
|
|
300
|
+
if (mode === "inline" || mode === "deferred") {
|
|
301
|
+
closedSlices.add(ev.sliceId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// best-effort; ledger already covers the canonical case.
|
|
308
|
+
}
|
|
259
309
|
}
|
|
260
310
|
const waves = merged.map((wave) => {
|
|
261
311
|
const members = wave.members.map((m) => m.sliceId);
|
|
@@ -272,7 +322,6 @@ export async function runWaveStatus(projectRoot, options = {}) {
|
|
|
272
322
|
};
|
|
273
323
|
});
|
|
274
324
|
const firstOpenWave = waves.find((w) => w.status === "open" || w.status === "partial") ?? null;
|
|
275
|
-
const warnings = [];
|
|
276
325
|
if (merged.length === 0 && planRaw.length === 0) {
|
|
277
326
|
warnings.push("wave_plan_missing: 05-plan.md not found or empty under <artifacts-dir>.");
|
|
278
327
|
}
|
|
@@ -349,7 +398,7 @@ export async function runWaveStatusCommand(projectRoot, argv, io) {
|
|
|
349
398
|
io.stderr.write(`cclaw internal wave-status: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
350
399
|
return 1;
|
|
351
400
|
}
|
|
352
|
-
const report = await runWaveStatus(projectRoot);
|
|
401
|
+
const report = await runWaveStatus(projectRoot, { streamMode: parsed.streamMode });
|
|
353
402
|
if (parsed.format === "json") {
|
|
354
403
|
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
355
404
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const DEFAULT_SLICE_STREAM_REL_PATH = ".cclaw/state/slice-builder-stream.jsonl";
|
|
2
|
+
export interface SliceBuilderPhaseEvent {
|
|
3
|
+
event: "phase-completed";
|
|
4
|
+
runId?: string;
|
|
5
|
+
stage?: string;
|
|
6
|
+
sliceId: string;
|
|
7
|
+
phase: string;
|
|
8
|
+
spanId?: string;
|
|
9
|
+
refactorOutcome?: {
|
|
10
|
+
mode?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface EventStreamParseResult {
|
|
14
|
+
events: SliceBuilderPhaseEvent[];
|
|
15
|
+
droppedLines: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Incremental JSONL parser with bounded in-memory buffer. If chunks arrive
|
|
19
|
+
* faster than the consumer drains complete lines, we trim the oldest partial
|
|
20
|
+
* payload once maxBufferBytes is exceeded instead of letting memory grow
|
|
21
|
+
* unbounded.
|
|
22
|
+
*/
|
|
23
|
+
export declare class EventStreamLineBuffer {
|
|
24
|
+
private readonly maxBufferBytes;
|
|
25
|
+
private buffer;
|
|
26
|
+
constructor(maxBufferBytes?: number);
|
|
27
|
+
push(chunk: string | Buffer): EventStreamParseResult;
|
|
28
|
+
flush(): EventStreamParseResult;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseEventStreamText(raw: string): EventStreamParseResult;
|
|
31
|
+
export declare function readEventStreamFile(absPath: string): Promise<EventStreamParseResult>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export const DEFAULT_SLICE_STREAM_REL_PATH = ".cclaw/state/slice-builder-stream.jsonl";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function asString(value) {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
8
|
+
}
|
|
9
|
+
function parseEventLine(rawLine) {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = JSON.parse(rawLine);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!isRecord(parsed))
|
|
18
|
+
return null;
|
|
19
|
+
if (parsed.event !== "phase-completed")
|
|
20
|
+
return null;
|
|
21
|
+
const sliceId = asString(parsed.sliceId);
|
|
22
|
+
const phase = asString(parsed.phase);
|
|
23
|
+
if (!sliceId || !phase)
|
|
24
|
+
return null;
|
|
25
|
+
const stage = asString(parsed.stage);
|
|
26
|
+
const runId = asString(parsed.runId);
|
|
27
|
+
const spanId = asString(parsed.spanId);
|
|
28
|
+
const refactorOutcome = isRecord(parsed.refactorOutcome)
|
|
29
|
+
? { mode: asString(parsed.refactorOutcome.mode) }
|
|
30
|
+
: undefined;
|
|
31
|
+
return {
|
|
32
|
+
event: "phase-completed",
|
|
33
|
+
...(runId ? { runId } : {}),
|
|
34
|
+
...(stage ? { stage } : {}),
|
|
35
|
+
sliceId,
|
|
36
|
+
phase,
|
|
37
|
+
...(spanId ? { spanId } : {}),
|
|
38
|
+
...(refactorOutcome ? { refactorOutcome } : {})
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Incremental JSONL parser with bounded in-memory buffer. If chunks arrive
|
|
43
|
+
* faster than the consumer drains complete lines, we trim the oldest partial
|
|
44
|
+
* payload once maxBufferBytes is exceeded instead of letting memory grow
|
|
45
|
+
* unbounded.
|
|
46
|
+
*/
|
|
47
|
+
export class EventStreamLineBuffer {
|
|
48
|
+
maxBufferBytes;
|
|
49
|
+
buffer = "";
|
|
50
|
+
constructor(maxBufferBytes = 256 * 1024) {
|
|
51
|
+
this.maxBufferBytes = maxBufferBytes;
|
|
52
|
+
}
|
|
53
|
+
push(chunk) {
|
|
54
|
+
this.buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
55
|
+
let droppedLines = 0;
|
|
56
|
+
if (this.buffer.length > this.maxBufferBytes) {
|
|
57
|
+
const overflowStart = this.buffer.length - this.maxBufferBytes;
|
|
58
|
+
const nextBreak = this.buffer.indexOf("\n", overflowStart);
|
|
59
|
+
if (nextBreak >= 0) {
|
|
60
|
+
this.buffer = this.buffer.slice(nextBreak + 1);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.buffer = "";
|
|
64
|
+
}
|
|
65
|
+
droppedLines += 1;
|
|
66
|
+
}
|
|
67
|
+
const events = [];
|
|
68
|
+
let newlineIdx = this.buffer.indexOf("\n");
|
|
69
|
+
while (newlineIdx >= 0) {
|
|
70
|
+
const line = this.buffer.slice(0, newlineIdx).trim();
|
|
71
|
+
this.buffer = this.buffer.slice(newlineIdx + 1);
|
|
72
|
+
if (line.length > 0) {
|
|
73
|
+
const parsed = parseEventLine(line);
|
|
74
|
+
if (parsed)
|
|
75
|
+
events.push(parsed);
|
|
76
|
+
else
|
|
77
|
+
droppedLines += 1;
|
|
78
|
+
}
|
|
79
|
+
newlineIdx = this.buffer.indexOf("\n");
|
|
80
|
+
}
|
|
81
|
+
return { events, droppedLines };
|
|
82
|
+
}
|
|
83
|
+
flush() {
|
|
84
|
+
if (this.buffer.trim().length === 0) {
|
|
85
|
+
this.buffer = "";
|
|
86
|
+
return { events: [], droppedLines: 0 };
|
|
87
|
+
}
|
|
88
|
+
const parsed = parseEventLine(this.buffer.trim());
|
|
89
|
+
this.buffer = "";
|
|
90
|
+
if (parsed) {
|
|
91
|
+
return { events: [parsed], droppedLines: 0 };
|
|
92
|
+
}
|
|
93
|
+
return { events: [], droppedLines: 1 };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function parseEventStreamText(raw) {
|
|
97
|
+
const buffer = new EventStreamLineBuffer();
|
|
98
|
+
const pushed = buffer.push(raw);
|
|
99
|
+
const flushed = buffer.flush();
|
|
100
|
+
return {
|
|
101
|
+
events: [...pushed.events, ...flushed.events],
|
|
102
|
+
droppedLines: pushed.droppedLines + flushed.droppedLines
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function readEventStreamFile(absPath) {
|
|
106
|
+
let raw = "";
|
|
107
|
+
try {
|
|
108
|
+
raw = await fs.readFile(absPath, "utf8");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { events: [], droppedLines: 0 };
|
|
112
|
+
}
|
|
113
|
+
return parseEventStreamText(raw);
|
|
114
|
+
}
|