cclaw-cli 6.14.1 → 6.14.2

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.
@@ -0,0 +1,69 @@
1
+ import { readFlowState, writeFlowState } from "../runs.js";
2
+ export function parseSetIntegrationOverseerModeArgs(tokens) {
3
+ let mode = null;
4
+ let reason = null;
5
+ let positional = null;
6
+ for (const token of tokens) {
7
+ if (token.startsWith("--mode=")) {
8
+ const raw = token.slice("--mode=".length).trim();
9
+ if (raw === "conditional" || raw === "always") {
10
+ mode = raw;
11
+ }
12
+ else {
13
+ return null;
14
+ }
15
+ continue;
16
+ }
17
+ if (token.startsWith("--reason=")) {
18
+ const raw = token.slice("--reason=".length).trim();
19
+ if (raw.length > 0)
20
+ reason = raw;
21
+ continue;
22
+ }
23
+ if (token.startsWith("--")) {
24
+ return null;
25
+ }
26
+ if (positional === null) {
27
+ positional = token.trim();
28
+ continue;
29
+ }
30
+ return null;
31
+ }
32
+ if (mode === null && positional !== null) {
33
+ if (positional === "conditional" || positional === "always") {
34
+ mode = positional;
35
+ }
36
+ else {
37
+ return null;
38
+ }
39
+ }
40
+ if (mode === null)
41
+ return null;
42
+ return { mode, reason };
43
+ }
44
+ /**
45
+ * v6.14.2 — set `flow-state.json::integrationOverseerMode` without
46
+ * advancing the stage DAG. Mirrors `set-worktree-mode` and
47
+ * `set-checkpoint-mode`.
48
+ */
49
+ export async function runSetIntegrationOverseerMode(projectRoot, tokens, io) {
50
+ const parsed = parseSetIntegrationOverseerModeArgs(tokens);
51
+ if (!parsed) {
52
+ io.stderr.write("cclaw internal set-integration-overseer-mode: usage: <conditional|always> [--reason=\"<short>\"] " +
53
+ "(or --mode=<conditional|always>)\n");
54
+ return 1;
55
+ }
56
+ const state = await readFlowState(projectRoot);
57
+ const writerSubsystem = parsed.reason
58
+ ? `set-integration-overseer-mode:${slugifyReason(parsed.reason)}`
59
+ : "set-integration-overseer-mode";
60
+ await writeFlowState(projectRoot, { ...state, integrationOverseerMode: parsed.mode }, { writerSubsystem });
61
+ return 0;
62
+ }
63
+ function slugifyReason(reason) {
64
+ return (reason
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9_-]+/gu, "-")
67
+ .replace(/^-+|-+$/gu, "")
68
+ .slice(0, 60) || "unspecified");
69
+ }
@@ -0,0 +1,51 @@
1
+ import type { Writable } from "node:stream";
2
+ interface InternalIo {
3
+ stdout: Writable;
4
+ stderr: Writable;
5
+ }
6
+ export interface WaveStatusWaveSummary {
7
+ waveId: string;
8
+ members: string[];
9
+ closedMembers: string[];
10
+ openMembers: string[];
11
+ readyMembers: string[];
12
+ blockedMembers: string[];
13
+ status: "closed" | "open" | "partial" | "empty";
14
+ }
15
+ export interface WaveStatusNextDispatch {
16
+ waveId: string | null;
17
+ readyToDispatch: string[];
18
+ pathConflicts: string[];
19
+ mode: "single-slice" | "wave-fanout" | "none";
20
+ }
21
+ export interface WaveStatusReport {
22
+ activeRunId: string;
23
+ currentStage: string;
24
+ tddCutoverSliceId: string | null;
25
+ tddWorktreeCutoverSliceId: string | null;
26
+ legacyContinuation: boolean;
27
+ waves: WaveStatusWaveSummary[];
28
+ nextDispatch: WaveStatusNextDispatch;
29
+ warnings: string[];
30
+ }
31
+ export interface RunWaveStatusOptions {
32
+ /**
33
+ * Override the artifacts directory; useful for fixture tests that
34
+ * place an `05-plan.md` outside `.cclaw/artifacts`. Defaults to
35
+ * `<projectRoot>/.cclaw/artifacts`.
36
+ */
37
+ artifactsDir?: string;
38
+ }
39
+ /**
40
+ * v6.14.2 — deterministic helper for the TDD controller. Reads the
41
+ * managed `<!-- parallel-exec-managed-start -->` block from
42
+ * `<artifacts-dir>/05-plan.md` plus the `wave-plans/` directory and
43
+ * reports waves + the next dispatchable members so the controller does
44
+ * NOT have to page through a 1400-line plan to find the active wave.
45
+ *
46
+ * Always exits 0 unless the plan is malformed (no managed block AND no
47
+ * wave-plans directory), in which case exit 2 with a structured error.
48
+ */
49
+ export declare function runWaveStatus(projectRoot: string, options?: RunWaveStatusOptions): Promise<WaveStatusReport>;
50
+ export declare function runWaveStatusCommand(projectRoot: string, argv: string[], io: InternalIo): Promise<number>;
51
+ export {};
@@ -0,0 +1,285 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RUNTIME_ROOT } from "../constants.js";
4
+ import { readDelegationEvents, readDelegationLedger } from "../delegation.js";
5
+ import { readFlowState } from "../runs.js";
6
+ import { mergeParallelWaveDefinitions, parseParallelExecutionPlanWaves, parseWavePlanDirectory } from "./plan-split-waves.js";
7
+ function parseArgs(tokens) {
8
+ const args = { format: "json" };
9
+ for (const token of tokens) {
10
+ if (token === "--json")
11
+ args.format = "json";
12
+ else if (token === "--human" || token === "--text")
13
+ args.format = "human";
14
+ else if (token.startsWith("--format=")) {
15
+ const raw = token.slice("--format=".length).trim();
16
+ if (raw === "json" || raw === "human")
17
+ args.format = raw;
18
+ else
19
+ throw new Error(`Unknown wave-status --format value: ${raw}`);
20
+ }
21
+ else {
22
+ throw new Error(`Unknown wave-status flag: ${token}`);
23
+ }
24
+ }
25
+ return args;
26
+ }
27
+ function classifyWaveStatus(total, closedCount) {
28
+ if (total === 0)
29
+ return "empty";
30
+ if (closedCount === 0)
31
+ return "open";
32
+ if (closedCount >= total)
33
+ return "closed";
34
+ return "partial";
35
+ }
36
+ const TERMINAL_PHASES = new Set([
37
+ "refactor",
38
+ "refactor-deferred",
39
+ "resolve-conflict"
40
+ ]);
41
+ /**
42
+ * v6.14.2 — deterministic helper for the TDD controller. Reads the
43
+ * managed `<!-- parallel-exec-managed-start -->` block from
44
+ * `<artifacts-dir>/05-plan.md` plus the `wave-plans/` directory and
45
+ * reports waves + the next dispatchable members so the controller does
46
+ * NOT have to page through a 1400-line plan to find the active wave.
47
+ *
48
+ * Always exits 0 unless the plan is malformed (no managed block AND no
49
+ * wave-plans directory), in which case exit 2 with a structured error.
50
+ */
51
+ export async function runWaveStatus(projectRoot, options = {}) {
52
+ const artifactsDir = options.artifactsDir ?? path.join(projectRoot, RUNTIME_ROOT, "artifacts");
53
+ const flowState = await readFlowState(projectRoot).catch(() => null);
54
+ const activeRunId = flowState?.activeRunId ?? "unknown-run";
55
+ const currentStage = flowState?.currentStage ?? "tdd";
56
+ const tddCutoverSliceId = flowState?.tddCutoverSliceId ?? null;
57
+ const tddWorktreeCutoverSliceId = flowState?.tddWorktreeCutoverSliceId ?? null;
58
+ const legacyContinuation = flowState?.legacyContinuation === true;
59
+ let planRaw = "";
60
+ try {
61
+ planRaw = await fs.readFile(path.join(artifactsDir, "05-plan.md"), "utf8");
62
+ }
63
+ catch {
64
+ planRaw = "";
65
+ }
66
+ let primaryWaves = [];
67
+ try {
68
+ primaryWaves = parseParallelExecutionPlanWaves(planRaw);
69
+ }
70
+ catch (err) {
71
+ return {
72
+ activeRunId,
73
+ currentStage,
74
+ tddCutoverSliceId,
75
+ tddWorktreeCutoverSliceId,
76
+ legacyContinuation,
77
+ waves: [],
78
+ nextDispatch: {
79
+ waveId: null,
80
+ readyToDispatch: [],
81
+ pathConflicts: [],
82
+ mode: "none"
83
+ },
84
+ warnings: [
85
+ `wave_plan_parse_error: ${err instanceof Error ? err.message : String(err)}`
86
+ ]
87
+ };
88
+ }
89
+ let secondaryWaves = [];
90
+ try {
91
+ secondaryWaves = await parseWavePlanDirectory(artifactsDir);
92
+ }
93
+ catch (err) {
94
+ secondaryWaves = [];
95
+ void err;
96
+ }
97
+ let merged = [];
98
+ try {
99
+ merged = mergeParallelWaveDefinitions(primaryWaves, secondaryWaves);
100
+ }
101
+ catch (err) {
102
+ return {
103
+ activeRunId,
104
+ currentStage,
105
+ tddCutoverSliceId,
106
+ tddWorktreeCutoverSliceId,
107
+ legacyContinuation,
108
+ waves: [],
109
+ nextDispatch: {
110
+ waveId: null,
111
+ readyToDispatch: [],
112
+ pathConflicts: [],
113
+ mode: "none"
114
+ },
115
+ warnings: [
116
+ `wave_plan_merge_conflict: ${err instanceof Error ? err.message : String(err)}`
117
+ ]
118
+ };
119
+ }
120
+ // Collect closed slice ids from the active run delegation ledger +
121
+ // events. A slice is "closed" once it carries a terminal phase
122
+ // (refactor, refactor-deferred, resolve-conflict) OR a phase=green
123
+ // event with refactorOutcome (v6.14.0 fold-inline path). Anything else
124
+ // we treat as still open so the helper never falsely advances.
125
+ const closedSlices = new Set();
126
+ let ledgerEntries = [];
127
+ try {
128
+ const ledger = await readDelegationLedger(projectRoot);
129
+ ledgerEntries = ledger.entries.filter((entry) => entry.runId === ledger.runId && entry.stage === "tdd");
130
+ }
131
+ catch {
132
+ ledgerEntries = [];
133
+ }
134
+ for (const entry of ledgerEntries) {
135
+ if (entry.status !== "completed")
136
+ continue;
137
+ if (typeof entry.sliceId !== "string")
138
+ continue;
139
+ if (typeof entry.phase !== "string")
140
+ continue;
141
+ if (TERMINAL_PHASES.has(entry.phase)) {
142
+ closedSlices.add(entry.sliceId);
143
+ continue;
144
+ }
145
+ if (entry.phase === "green" && entry.refactorOutcome) {
146
+ const mode = entry.refactorOutcome.mode;
147
+ if (mode === "inline" || mode === "deferred") {
148
+ closedSlices.add(entry.sliceId);
149
+ }
150
+ }
151
+ }
152
+ // Also consult the JSONL events in case the ledger projection lags.
153
+ try {
154
+ const { events } = await readDelegationEvents(projectRoot);
155
+ for (const ev of events) {
156
+ if (ev.event !== "completed")
157
+ continue;
158
+ if (ev.runId !== activeRunId)
159
+ continue;
160
+ if (ev.stage !== "tdd")
161
+ continue;
162
+ if (typeof ev.sliceId !== "string")
163
+ continue;
164
+ if (typeof ev.phase !== "string")
165
+ continue;
166
+ if (TERMINAL_PHASES.has(ev.phase)) {
167
+ closedSlices.add(ev.sliceId);
168
+ continue;
169
+ }
170
+ if (ev.phase === "green" && ev.refactorOutcome) {
171
+ const mode = ev.refactorOutcome.mode;
172
+ if (mode === "inline" || mode === "deferred") {
173
+ closedSlices.add(ev.sliceId);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ catch {
179
+ // best-effort; ledger already covers the canonical case.
180
+ }
181
+ const waves = merged.map((wave) => {
182
+ const members = wave.members.map((m) => m.sliceId);
183
+ const closedMembers = members.filter((id) => closedSlices.has(id));
184
+ const openMembers = members.filter((id) => !closedSlices.has(id));
185
+ return {
186
+ waveId: wave.waveId,
187
+ members,
188
+ closedMembers,
189
+ openMembers,
190
+ readyMembers: openMembers,
191
+ blockedMembers: [],
192
+ status: classifyWaveStatus(members.length, closedMembers.length)
193
+ };
194
+ });
195
+ const firstOpenWave = waves.find((w) => w.status === "open" || w.status === "partial") ?? null;
196
+ const warnings = [];
197
+ if (tddCutoverSliceId) {
198
+ warnings.push("tddCutoverSliceId is a historical boundary; do not use it to find the active slice.");
199
+ }
200
+ if (merged.length === 0 && planRaw.length === 0) {
201
+ warnings.push("wave_plan_missing: 05-plan.md not found or empty under <artifacts-dir>.");
202
+ }
203
+ else if (merged.length === 0) {
204
+ 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.");
205
+ }
206
+ let nextDispatch;
207
+ if (firstOpenWave === null) {
208
+ nextDispatch = {
209
+ waveId: null,
210
+ readyToDispatch: [],
211
+ pathConflicts: [],
212
+ mode: "none"
213
+ };
214
+ }
215
+ else {
216
+ const readyToDispatch = [...firstOpenWave.readyMembers].sort();
217
+ nextDispatch = {
218
+ waveId: firstOpenWave.waveId,
219
+ readyToDispatch,
220
+ pathConflicts: [],
221
+ mode: readyToDispatch.length > 1 ? "wave-fanout" : "single-slice"
222
+ };
223
+ }
224
+ return {
225
+ activeRunId,
226
+ currentStage,
227
+ tddCutoverSliceId,
228
+ tddWorktreeCutoverSliceId,
229
+ legacyContinuation,
230
+ waves,
231
+ nextDispatch,
232
+ warnings
233
+ };
234
+ }
235
+ function formatHumanReport(report) {
236
+ const lines = [];
237
+ lines.push(`activeRunId: ${report.activeRunId}`);
238
+ lines.push(`currentStage: ${report.currentStage}`);
239
+ if (report.tddCutoverSliceId) {
240
+ lines.push(`tddCutoverSliceId: ${report.tddCutoverSliceId} (HISTORICAL)`);
241
+ }
242
+ if (report.tddWorktreeCutoverSliceId) {
243
+ lines.push(`tddWorktreeCutoverSliceId: ${report.tddWorktreeCutoverSliceId}`);
244
+ }
245
+ lines.push(`legacyContinuation: ${report.legacyContinuation}`);
246
+ lines.push("waves:");
247
+ if (report.waves.length === 0) {
248
+ lines.push(" (no waves discovered)");
249
+ }
250
+ else {
251
+ for (const wave of report.waves) {
252
+ lines.push(` ${wave.waveId} [${wave.status}]: ` +
253
+ `closed=[${wave.closedMembers.join(",")}] ` +
254
+ `open=[${wave.openMembers.join(",")}]`);
255
+ }
256
+ }
257
+ lines.push(`nextDispatch: wave=${report.nextDispatch.waveId ?? "(none)"} ` +
258
+ `mode=${report.nextDispatch.mode} ` +
259
+ `ready=[${report.nextDispatch.readyToDispatch.join(",")}]`);
260
+ if (report.warnings.length > 0) {
261
+ lines.push("warnings:");
262
+ for (const warn of report.warnings) {
263
+ lines.push(` - ${warn}`);
264
+ }
265
+ }
266
+ return lines.join("\n") + "\n";
267
+ }
268
+ export async function runWaveStatusCommand(projectRoot, argv, io) {
269
+ let parsed;
270
+ try {
271
+ parsed = parseArgs(argv);
272
+ }
273
+ catch (err) {
274
+ io.stderr.write(`cclaw internal wave-status: ${err instanceof Error ? err.message : String(err)}\n`);
275
+ return 1;
276
+ }
277
+ const report = await runWaveStatus(projectRoot);
278
+ if (parsed.format === "json") {
279
+ io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
280
+ }
281
+ else {
282
+ io.stdout.write(formatHumanReport(report));
283
+ }
284
+ return 0;
285
+ }
@@ -472,10 +472,12 @@ function coerceFlowState(parsed) {
472
472
  const repoSignals = coerceRepoSignals(parsed.repoSignals);
473
473
  const completedStageMeta = sanitizeCompletedStageMeta(parsed.completedStageMeta);
474
474
  const tddCutoverSliceId = coerceTddCutoverSliceId(parsed.tddCutoverSliceId);
475
+ const tddWorktreeCutoverSliceId = coerceTddCutoverSliceId(parsed.tddWorktreeCutoverSliceId);
475
476
  const worktreeExecutionMode = coerceWorktreeExecutionMode(parsed.worktreeExecutionMode);
476
477
  const tddCheckpointMode = coerceTddCheckpointMode(parsed.tddCheckpointMode);
477
478
  const integrationOverseerMode = coerceIntegrationOverseerMode(parsed.integrationOverseerMode);
478
479
  const legacyContinuation = typeof parsed.legacyContinuation === "boolean" ? parsed.legacyContinuation : undefined;
480
+ const tddGreenMinElapsedMs = coerceTddGreenMinElapsedMs(parsed.tddGreenMinElapsedMs);
479
481
  const state = {
480
482
  schemaVersion: FLOW_STATE_SCHEMA_VERSION,
481
483
  activeRunId,
@@ -489,10 +491,12 @@ function coerceFlowState(parsed) {
489
491
  ...(repoSignals ? { repoSignals } : {}),
490
492
  ...(completedStageMeta ? { completedStageMeta } : {}),
491
493
  ...(tddCutoverSliceId ? { tddCutoverSliceId } : {}),
494
+ ...(tddWorktreeCutoverSliceId ? { tddWorktreeCutoverSliceId } : {}),
492
495
  ...(worktreeExecutionMode !== undefined ? { worktreeExecutionMode } : {}),
493
496
  ...(tddCheckpointMode !== undefined ? { tddCheckpointMode } : {}),
494
497
  ...(integrationOverseerMode !== undefined ? { integrationOverseerMode } : {}),
495
498
  ...(legacyContinuation !== undefined ? { legacyContinuation } : {}),
499
+ ...(tddGreenMinElapsedMs !== undefined ? { tddGreenMinElapsedMs } : {}),
496
500
  skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
497
501
  staleStages: sanitizeStaleStages(parsed.staleStages),
498
502
  rewinds: sanitizeRewinds(parsed.rewinds),
@@ -528,6 +532,22 @@ function coerceIntegrationOverseerMode(value) {
528
532
  return value;
529
533
  return undefined;
530
534
  }
535
+ /**
536
+ * v6.14.2 — coerce `tddGreenMinElapsedMs` from disk. Mirrors the
537
+ * defensive read in `effectiveTddGreenMinElapsedMs`: numbers ≥ 0 round
538
+ * down to integers; everything else (NaN, strings, negatives) returns
539
+ * undefined so the field is omitted from the rehydrated state and the
540
+ * effective getter falls back to the documented 4000ms default.
541
+ */
542
+ function coerceTddGreenMinElapsedMs(value) {
543
+ if (typeof value !== "number")
544
+ return undefined;
545
+ if (!Number.isFinite(value))
546
+ return undefined;
547
+ if (value < 0)
548
+ return undefined;
549
+ return Math.floor(value);
550
+ }
531
551
  export class CorruptFlowStateError extends Error {
532
552
  statePath;
533
553
  quarantinedPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "6.14.1",
3
+ "version": "6.14.2",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {