cclaw-cli 0.48.1 → 0.48.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/README.md +10 -3
- package/dist/artifact-linter.js +2 -8
- package/dist/cli.js +8 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -3
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +11 -0
- package/dist/content/contracts.d.ts +2 -2
- package/dist/content/contracts.js +2 -2
- package/dist/content/core-agents.d.ts +1 -1
- package/dist/content/core-agents.js +1 -1
- package/dist/content/hooks.js +16 -15
- package/dist/content/next-command.js +4 -2
- package/dist/content/observe.d.ts +2 -2
- package/dist/content/observe.js +83 -13
- package/dist/content/opencode-plugin.js +227 -45
- package/dist/content/stage-schema.js +1 -1
- package/dist/content/stages/ship.js +2 -5
- package/dist/content/templates.js +3 -6
- package/dist/delegation.d.ts +5 -1
- package/dist/delegation.js +12 -8
- package/dist/doctor.js +132 -15
- package/dist/eval/runner.js +36 -4
- package/dist/feature-system.d.ts +11 -4
- package/dist/feature-system.js +54 -10
- package/dist/flow-state.d.ts +2 -0
- package/dist/flow-state.js +19 -2
- package/dist/fs-utils.d.ts +4 -1
- package/dist/fs-utils.js +20 -4
- package/dist/gate-evidence.d.ts +2 -0
- package/dist/gate-evidence.js +13 -4
- package/dist/install.js +25 -23
- package/dist/internal/advance-stage.js +49 -10
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +113 -33
- package/dist/retro-gate.js +33 -23
- package/dist/run-archive.js +166 -128
- package/dist/run-persistence.d.ts +8 -1
- package/dist/run-persistence.js +7 -6
- package/dist/trace-matrix.js +7 -7
- package/package.json +1 -1
package/dist/run-archive.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { RUNTIME_ROOT } from "./constants.js";
|
|
4
4
|
import { createInitialFlowState } from "./flow-state.js";
|
|
5
5
|
import { readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
|
|
6
|
-
import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
|
|
6
|
+
import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
|
|
7
|
+
import { readKnowledgeSafely } from "./knowledge-store.js";
|
|
7
8
|
import { evaluateRetroGate } from "./retro-gate.js";
|
|
8
9
|
import { ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
|
|
9
10
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
@@ -17,6 +18,12 @@ const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
|
17
18
|
const DELEGATION_LOG_FILE = "delegation-log.json";
|
|
18
19
|
const TDD_CYCLE_LOG_FILE = "tdd-cycle-log.jsonl";
|
|
19
20
|
const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
|
|
21
|
+
const CRITICAL_STATE_SNAPSHOT_FILES = new Set([
|
|
22
|
+
"flow-state.json",
|
|
23
|
+
DELEGATION_LOG_FILE,
|
|
24
|
+
TDD_CYCLE_LOG_FILE,
|
|
25
|
+
RECONCILIATION_NOTICES_FILE
|
|
26
|
+
]);
|
|
20
27
|
function runsRoot(projectRoot) {
|
|
21
28
|
return path.join(projectRoot, RUNS_DIR_REL_PATH);
|
|
22
29
|
}
|
|
@@ -26,6 +33,9 @@ function activeArtifactsPath(projectRoot) {
|
|
|
26
33
|
function stateDirPath(projectRoot) {
|
|
27
34
|
return path.join(projectRoot, STATE_DIR_REL_PATH);
|
|
28
35
|
}
|
|
36
|
+
function archiveLockPath(projectRoot) {
|
|
37
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", ".archive.lock");
|
|
38
|
+
}
|
|
29
39
|
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
30
40
|
const sourceDir = stateDirPath(projectRoot);
|
|
31
41
|
if (!(await exists(sourceDir))) {
|
|
@@ -57,8 +67,12 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
|
57
67
|
copied.push(entry.name);
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
|
-
catch {
|
|
61
|
-
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (CRITICAL_STATE_SNAPSHOT_FILES.has(entry.name)) {
|
|
72
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(`Archive snapshot failed for critical state file "${entry.name}" (${details}).`);
|
|
74
|
+
}
|
|
75
|
+
// Non-critical snapshot files are best-effort and may be skipped.
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
78
|
return copied.sort((a, b) => a.localeCompare(b));
|
|
@@ -66,9 +80,9 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
|
66
80
|
async function resetCarryoverStateFiles(projectRoot, activeRunId) {
|
|
67
81
|
const stateDir = stateDirPath(projectRoot);
|
|
68
82
|
await ensureDir(stateDir);
|
|
69
|
-
await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n
|
|
70
|
-
await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "");
|
|
71
|
-
await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n
|
|
83
|
+
await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`, { mode: 0o600 });
|
|
84
|
+
await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "", { mode: 0o600 });
|
|
85
|
+
await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
|
|
72
86
|
}
|
|
73
87
|
function toArchiveDate(date = new Date()) {
|
|
74
88
|
const yyyy = date.getFullYear().toString();
|
|
@@ -147,136 +161,160 @@ export async function listRuns(projectRoot) {
|
|
|
147
161
|
}
|
|
148
162
|
export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
149
163
|
await ensureRunSystem(projectRoot);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const feature = (featureName?.trim() && featureName.trim().length > 0)
|
|
156
|
-
? featureName.trim()
|
|
157
|
-
: await inferFeatureNameFromArtifacts(projectRoot);
|
|
158
|
-
const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
|
|
159
|
-
const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
|
|
160
|
-
const archivePath = path.join(runsDir, archiveId);
|
|
161
|
-
const archiveArtifactsPath = path.join(archivePath, "artifacts");
|
|
162
|
-
let sourceState = await readFlowState(projectRoot);
|
|
163
|
-
const retroGate = await evaluateRetroGate(projectRoot, sourceState);
|
|
164
|
-
const shipCompleted = sourceState.completedStages.includes("ship");
|
|
165
|
-
const skipRetro = options.skipRetro === true;
|
|
166
|
-
const skipRetroReason = options.skipRetroReason?.trim();
|
|
167
|
-
if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
|
|
168
|
-
throw new Error("archive --skip-retro requires --retro-reason=<text>.");
|
|
169
|
-
}
|
|
170
|
-
const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
|
|
171
|
-
typeof sourceState.closeout.retroSkipReason === "string" &&
|
|
172
|
-
sourceState.closeout.retroSkipReason.trim().length > 0;
|
|
173
|
-
const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
|
|
174
|
-
if (shipCompleted && !readyForArchive && !skipRetro) {
|
|
175
|
-
throw new Error("Archive blocked: closeout is not ready_to_archive. " +
|
|
176
|
-
"Resume /cc-next until closeout reaches ready_to_archive, " +
|
|
177
|
-
"or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
|
|
178
|
-
}
|
|
179
|
-
if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
|
|
180
|
-
throw new Error("Archive blocked: retro gate is required after ship completion. " +
|
|
181
|
-
"Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
|
|
182
|
-
}
|
|
183
|
-
if (retroGate.completed) {
|
|
184
|
-
const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
|
|
185
|
-
sourceState = {
|
|
186
|
-
...sourceState,
|
|
187
|
-
retro: {
|
|
188
|
-
required: retroGate.required,
|
|
189
|
-
completedAt,
|
|
190
|
-
compoundEntries: retroGate.compoundEntries
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
await writeFlowState(projectRoot, sourceState, { allowReset: true });
|
|
194
|
-
}
|
|
195
|
-
const retroSummary = {
|
|
196
|
-
required: retroGate.required,
|
|
197
|
-
completed: retroGate.completed,
|
|
198
|
-
skipped: skipRetro || retroSkippedInCloseout,
|
|
199
|
-
skipReason: skipRetro
|
|
200
|
-
? skipRetroReason
|
|
201
|
-
: retroSkippedInCloseout
|
|
202
|
-
? sourceState.closeout.retroSkipReason
|
|
203
|
-
: undefined,
|
|
204
|
-
compoundEntries: retroGate.compoundEntries
|
|
205
|
-
};
|
|
206
|
-
await ensureDir(archivePath);
|
|
207
|
-
// Drop an `.archive-in-progress` sentinel immediately so that a crash
|
|
208
|
-
// between the artifact rename and the final manifest write leaves a
|
|
209
|
-
// recoverable marker (doctor surfaces these; re-running archive on an
|
|
210
|
-
// orphan attempts to complete or roll back). The sentinel is removed
|
|
211
|
-
// only after the manifest lands successfully.
|
|
212
|
-
const sentinelPath = path.join(archivePath, ".archive-in-progress");
|
|
213
|
-
const archivedAt = new Date().toISOString();
|
|
214
|
-
await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
|
|
215
|
-
let artifactsMoved = false;
|
|
216
|
-
try {
|
|
217
|
-
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
218
|
-
artifactsMoved = true;
|
|
164
|
+
return withDirectoryLock(archiveLockPath(projectRoot), async () => {
|
|
165
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
166
|
+
const artifactsDir = activeArtifactsPath(projectRoot);
|
|
167
|
+
const runsDir = runsRoot(projectRoot);
|
|
168
|
+
await ensureDir(runsDir);
|
|
219
169
|
await ensureDir(artifactsDir);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
170
|
+
const feature = (featureName?.trim() && featureName.trim().length > 0)
|
|
171
|
+
? featureName.trim()
|
|
172
|
+
: await inferFeatureNameFromArtifacts(projectRoot);
|
|
173
|
+
const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
|
|
174
|
+
const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
|
|
175
|
+
const archivePath = path.join(runsDir, archiveId);
|
|
176
|
+
const archiveArtifactsPath = path.join(archivePath, "artifacts");
|
|
177
|
+
let sourceState = await readFlowState(projectRoot);
|
|
178
|
+
const retroGate = await evaluateRetroGate(projectRoot, sourceState);
|
|
179
|
+
const shipCompleted = sourceState.completedStages.includes("ship");
|
|
180
|
+
const skipRetro = options.skipRetro === true;
|
|
181
|
+
const skipRetroReason = options.skipRetroReason?.trim();
|
|
182
|
+
if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
|
|
183
|
+
throw new Error("archive --skip-retro requires --retro-reason=<text>.");
|
|
184
|
+
}
|
|
185
|
+
const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
|
|
186
|
+
typeof sourceState.closeout.retroSkipReason === "string" &&
|
|
187
|
+
sourceState.closeout.retroSkipReason.trim().length > 0;
|
|
188
|
+
const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
|
|
189
|
+
const inShipCloseout = sourceState.currentStage === "ship";
|
|
190
|
+
if (inShipCloseout && skipRetro) {
|
|
191
|
+
throw new Error("Archive blocked: --skip-retro is not allowed while current stage is ship. " +
|
|
192
|
+
"Complete closeout to ready_to_archive via /cc-next.");
|
|
193
|
+
}
|
|
194
|
+
if (inShipCloseout && !readyForArchive) {
|
|
195
|
+
throw new Error("Archive blocked: closeout is not ready_to_archive. " +
|
|
196
|
+
"Resume /cc-next until closeout reaches ready_to_archive.");
|
|
197
|
+
}
|
|
198
|
+
if (shipCompleted && !readyForArchive && !skipRetro) {
|
|
199
|
+
throw new Error("Archive blocked: closeout is not ready_to_archive. " +
|
|
200
|
+
"Resume /cc-next until closeout reaches ready_to_archive, " +
|
|
201
|
+
"or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
|
|
202
|
+
}
|
|
203
|
+
if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
|
|
204
|
+
throw new Error("Archive blocked: retro gate is required after ship completion. " +
|
|
205
|
+
"Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
|
|
206
|
+
}
|
|
207
|
+
if (retroGate.completed) {
|
|
208
|
+
const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
|
|
209
|
+
sourceState = {
|
|
210
|
+
...sourceState,
|
|
211
|
+
retro: {
|
|
212
|
+
required: retroGate.required,
|
|
213
|
+
completedAt,
|
|
214
|
+
compoundEntries: retroGate.compoundEntries
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
await writeFlowState(projectRoot, sourceState, { allowReset: true });
|
|
218
|
+
}
|
|
219
|
+
const retroSummary = {
|
|
220
|
+
required: retroGate.required,
|
|
221
|
+
completed: retroGate.completed,
|
|
222
|
+
skipped: skipRetro || retroSkippedInCloseout,
|
|
223
|
+
skipReason: skipRetro
|
|
224
|
+
? skipRetroReason
|
|
225
|
+
: retroSkippedInCloseout
|
|
226
|
+
? sourceState.closeout.retroSkipReason
|
|
227
|
+
: undefined,
|
|
228
|
+
compoundEntries: retroGate.compoundEntries
|
|
252
229
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
230
|
+
await ensureDir(archivePath);
|
|
231
|
+
// Drop an `.archive-in-progress` sentinel immediately so that a crash
|
|
232
|
+
// between the artifact rename and the final manifest write leaves a
|
|
233
|
+
// recoverable marker (doctor surfaces these; re-running archive on an
|
|
234
|
+
// orphan attempts to complete or roll back). The sentinel is removed
|
|
235
|
+
// only after the manifest lands successfully.
|
|
236
|
+
const sentinelPath = path.join(archivePath, ".archive-in-progress");
|
|
237
|
+
const archivedAt = new Date().toISOString();
|
|
238
|
+
await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
|
|
239
|
+
const stateBeforeReset = sourceState;
|
|
240
|
+
let artifactsMoved = false;
|
|
241
|
+
let stateReset = false;
|
|
242
|
+
try {
|
|
243
|
+
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
244
|
+
artifactsMoved = true;
|
|
245
|
+
await ensureDir(artifactsDir);
|
|
246
|
+
const archiveStatePath = path.join(archivePath, "state");
|
|
247
|
+
const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
|
|
248
|
+
const resetState = createInitialFlowState();
|
|
249
|
+
await writeFlowState(projectRoot, resetState, { allowReset: true });
|
|
250
|
+
stateReset = true;
|
|
251
|
+
await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
|
|
252
|
+
const manifest = {
|
|
253
|
+
version: 1,
|
|
254
|
+
archiveId,
|
|
255
|
+
archivedAt,
|
|
256
|
+
featureName: feature,
|
|
257
|
+
activeFeature,
|
|
258
|
+
sourceRunId: sourceState.activeRunId,
|
|
259
|
+
sourceCurrentStage: sourceState.currentStage,
|
|
260
|
+
sourceCompletedStages: sourceState.completedStages,
|
|
261
|
+
snapshottedStateFiles,
|
|
262
|
+
retro: retroSummary
|
|
263
|
+
};
|
|
264
|
+
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
265
|
+
// Manifest landed — sentinel is no longer needed.
|
|
266
|
+
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
267
|
+
const knowledgeStats = await readKnowledgeStats(projectRoot);
|
|
268
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
269
|
+
return {
|
|
270
|
+
archiveId,
|
|
271
|
+
archivePath,
|
|
272
|
+
archivedAt,
|
|
273
|
+
featureName: feature,
|
|
274
|
+
activeFeature,
|
|
275
|
+
resetState,
|
|
276
|
+
snapshottedStateFiles,
|
|
277
|
+
knowledge: knowledgeStats,
|
|
278
|
+
retro: retroSummary
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
// Best-effort rollback: if artifacts were moved but the subsequent
|
|
283
|
+
// steps failed, put artifacts back so the user is not left without
|
|
284
|
+
// a working run. The sentinel is intentionally left behind for
|
|
285
|
+
// inspection; doctor surfaces it.
|
|
286
|
+
if (artifactsMoved) {
|
|
287
|
+
try {
|
|
288
|
+
await fs.rm(artifactsDir, { recursive: true, force: true });
|
|
289
|
+
await fs.rename(archiveArtifactsPath, artifactsDir);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Rollback failed — sentinel + orphaned archive dir will be
|
|
293
|
+
// surfaced by doctor and can be reconciled manually.
|
|
294
|
+
}
|
|
263
295
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
296
|
+
if (stateReset) {
|
|
297
|
+
try {
|
|
298
|
+
await writeFlowState(projectRoot, stateBeforeReset, { allowReset: true });
|
|
299
|
+
await resetCarryoverStateFiles(projectRoot, stateBeforeReset.activeRunId);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// If rollback of state fails, keep sentinel + archive remnants for
|
|
303
|
+
// manual reconciliation.
|
|
304
|
+
}
|
|
267
305
|
}
|
|
306
|
+
throw err;
|
|
268
307
|
}
|
|
269
|
-
|
|
270
|
-
|
|
308
|
+
}, {
|
|
309
|
+
retries: 400,
|
|
310
|
+
retryDelayMs: 25,
|
|
311
|
+
staleAfterMs: 120_000
|
|
312
|
+
});
|
|
271
313
|
}
|
|
272
314
|
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
273
315
|
async function readKnowledgeStats(projectRoot) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
if (await exists(knowledgePath)) {
|
|
277
|
-
const text = await fs.readFile(knowledgePath, "utf8");
|
|
278
|
-
activeEntryCount = countActiveKnowledgeEntries(text);
|
|
279
|
-
}
|
|
316
|
+
const { entries } = await readKnowledgeSafely(projectRoot);
|
|
317
|
+
const activeEntryCount = entries.length;
|
|
280
318
|
return {
|
|
281
319
|
activeEntryCount,
|
|
282
320
|
softThreshold: KNOWLEDGE_SOFT_THRESHOLD,
|
|
@@ -12,12 +12,19 @@ export interface WriteFlowStateOptions {
|
|
|
12
12
|
*/
|
|
13
13
|
allowReset?: boolean;
|
|
14
14
|
}
|
|
15
|
+
export interface ReadFlowStateOptions {
|
|
16
|
+
/**
|
|
17
|
+
* When false, skip feature-system auto-repair writes and read flow-state in
|
|
18
|
+
* pure diagnostic mode.
|
|
19
|
+
*/
|
|
20
|
+
repairFeatureSystem?: boolean;
|
|
21
|
+
}
|
|
15
22
|
export declare class CorruptFlowStateError extends Error {
|
|
16
23
|
readonly statePath: string;
|
|
17
24
|
readonly quarantinedPath: string;
|
|
18
25
|
constructor(statePath: string, quarantinedPath: string, cause: unknown);
|
|
19
26
|
}
|
|
20
|
-
export declare function readFlowState(projectRoot: string): Promise<FlowState>;
|
|
27
|
+
export declare function readFlowState(projectRoot: string, options?: ReadFlowStateOptions): Promise<FlowState>;
|
|
21
28
|
export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
|
|
22
29
|
interface EnsureRunSystemOptions {
|
|
23
30
|
createIfMissing?: boolean;
|
package/dist/run-persistence.js
CHANGED
|
@@ -277,7 +277,7 @@ function sanitizeCloseoutState(value) {
|
|
|
277
277
|
}
|
|
278
278
|
function coerceFlowState(parsed) {
|
|
279
279
|
const track = coerceTrack(parsed.track);
|
|
280
|
-
const next = createInitialFlowState(
|
|
280
|
+
const next = createInitialFlowState({ track });
|
|
281
281
|
const activeRunIdRaw = parsed.activeRunId;
|
|
282
282
|
const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
|
|
283
283
|
? activeRunIdRaw.trim()
|
|
@@ -333,8 +333,10 @@ async function quarantineCorruptState(statePath, cause) {
|
|
|
333
333
|
}
|
|
334
334
|
throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
|
|
335
335
|
}
|
|
336
|
-
export async function readFlowState(projectRoot) {
|
|
337
|
-
|
|
336
|
+
export async function readFlowState(projectRoot, options = {}) {
|
|
337
|
+
if (options.repairFeatureSystem !== false) {
|
|
338
|
+
await ensureFeatureSystem(projectRoot);
|
|
339
|
+
}
|
|
338
340
|
const statePath = flowStatePath(projectRoot);
|
|
339
341
|
if (!(await exists(statePath))) {
|
|
340
342
|
return createInitialFlowState();
|
|
@@ -375,12 +377,11 @@ export async function writeFlowState(projectRoot, state, options = {}) {
|
|
|
375
377
|
if (err instanceof InvalidStageTransitionError) {
|
|
376
378
|
throw err;
|
|
377
379
|
}
|
|
378
|
-
|
|
379
|
-
// block a legitimate write attempt on parse errors here.
|
|
380
|
+
throw new Error(`cannot validate flow-state transition because ${FLOW_STATE_REL_PATH} is unreadable or corrupt (${err instanceof Error ? err.message : String(err)}). Run \`cclaw doctor\` and reconcile the state before retrying.`);
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
const safe = coerceFlowState({ ...state });
|
|
383
|
-
await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n
|
|
384
|
+
await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`, { mode: 0o600 });
|
|
384
385
|
});
|
|
385
386
|
await syncActiveFeatureSnapshot(projectRoot);
|
|
386
387
|
}
|
package/dist/trace-matrix.js
CHANGED
|
@@ -124,17 +124,17 @@ export async function buildTraceMatrix(projectRoot) {
|
|
|
124
124
|
const acToTasks = new Map();
|
|
125
125
|
for (const [task, acs] of taskToAcs) {
|
|
126
126
|
for (const ac of acs) {
|
|
127
|
-
const prev = acToTasks.get(ac) ??
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
127
|
+
const prev = acToTasks.get(ac) ?? new Set();
|
|
128
|
+
prev.add(task);
|
|
129
|
+
acToTasks.set(ac, prev);
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
132
|
const entries = criterionIds.map((criterionId) => {
|
|
134
|
-
const taskIds = acToTasks.get(criterionId) ??
|
|
133
|
+
const taskIds = [...(acToTasks.get(criterionId) ?? new Set())];
|
|
134
|
+
const taskIdSet = new Set(taskIds);
|
|
135
135
|
const testSlices = [];
|
|
136
136
|
for (const [slice, tasks] of sliceToTasks) {
|
|
137
|
-
if (tasks.some((t) =>
|
|
137
|
+
if (tasks.some((t) => taskIdSet.has(t))) {
|
|
138
138
|
testSlices.push(slice);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -145,7 +145,7 @@ export async function buildTraceMatrix(projectRoot) {
|
|
|
145
145
|
reviewFindings: layer1LinesForCriterion(layer1, criterionId)
|
|
146
146
|
};
|
|
147
147
|
});
|
|
148
|
-
const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac) ??
|
|
148
|
+
const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac)?.size ?? 0) === 0);
|
|
149
149
|
const tasksWithSlice = new Set();
|
|
150
150
|
for (const tasks of sliceToTasks.values()) {
|
|
151
151
|
for (const t of tasks) {
|