cclaw-cli 6.14.1 → 6.14.3
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/shared.d.ts +15 -0
- package/dist/artifact-linter/tdd.js +237 -8
- package/dist/artifact-linter.js +9 -1
- package/dist/content/core-agents.js +2 -2
- package/dist/content/hooks.js +190 -0
- package/dist/content/stages/tdd.js +3 -2
- package/dist/flow-state.d.ts +46 -0
- package/dist/flow-state.js +18 -0
- package/dist/install.js +175 -14
- package/dist/internal/advance-stage.js +21 -3
- package/dist/internal/cohesion-contract-stub.d.ts +29 -0
- package/dist/internal/cohesion-contract-stub.js +166 -0
- package/dist/internal/set-checkpoint-mode.d.ts +16 -0
- package/dist/internal/set-checkpoint-mode.js +72 -0
- package/dist/internal/set-integration-overseer-mode.d.ts +14 -0
- package/dist/internal/set-integration-overseer-mode.js +69 -0
- package/dist/internal/wave-status.d.ts +51 -0
- package/dist/internal/wave-status.js +285 -0
- package/dist/run-persistence.js +20 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/run-persistence.js
CHANGED
|
@@ -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;
|