cclaw-cli 0.48.1 → 0.48.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.
- package/dist/artifact-linter.js +2 -8
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +11 -0
- 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 +11 -7
- package/dist/doctor.js +97 -14
- package/dist/feature-system.d.ts +11 -4
- package/dist/feature-system.js +52 -8
- package/dist/flow-state.d.ts +2 -0
- package/dist/flow-state.js +19 -2
- package/dist/fs-utils.js +11 -2
- package/dist/gate-evidence.d.ts +2 -0
- package/dist/gate-evidence.js +12 -3
- package/dist/install.js +1 -1
- package/dist/internal/advance-stage.js +45 -8
- package/dist/run-archive.js +160 -119
- package/dist/run-persistence.d.ts +8 -1
- package/dist/run-persistence.js +6 -5
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { RUNTIME_ROOT } from "./constants.js";
|
|
3
|
+
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "./constants.js";
|
|
4
4
|
import { exists } from "./fs-utils.js";
|
|
5
5
|
import { stageSchema } from "./content/stage-schema.js";
|
|
6
6
|
import { FLOW_STAGES } from "./types.js";
|
|
@@ -137,13 +137,7 @@ function tokensFromRule(rule) {
|
|
|
137
137
|
return [...new Set(allCaps)];
|
|
138
138
|
}
|
|
139
139
|
if (/finalization enum token/iu.test(rule)) {
|
|
140
|
-
return [
|
|
141
|
-
"FINALIZE_MERGE_LOCAL",
|
|
142
|
-
"FINALIZE_OPEN_PR",
|
|
143
|
-
"FINALIZE_KEEP_BRANCH",
|
|
144
|
-
"FINALIZE_DISCARD_BRANCH",
|
|
145
|
-
"FINALIZE_NO_VCS"
|
|
146
|
-
];
|
|
140
|
+
return [...SHIP_FINALIZATION_MODES];
|
|
147
141
|
}
|
|
148
142
|
if (/final verdict/iu.test(rule)) {
|
|
149
143
|
return ["APPROVED", "APPROVED_WITH_CONCERNS", "BLOCKED"];
|
package/dist/constants.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { FlowStage, HarnessId } from "./types.js";
|
|
|
3
3
|
export declare const RUNTIME_ROOT = ".cclaw";
|
|
4
4
|
export declare const CCLAW_VERSION: string;
|
|
5
5
|
export declare const FLOW_VERSION = "1.0.0";
|
|
6
|
+
/**
|
|
7
|
+
* Canonical ship finalization enums used across stage schema, linting, and
|
|
8
|
+
* runtime gate evidence checks.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SHIP_FINALIZATION_MODES: readonly ["FINALIZE_MERGE_LOCAL", "FINALIZE_OPEN_PR", "FINALIZE_KEEP_BRANCH", "FINALIZE_DISCARD_BRANCH", "FINALIZE_NO_VCS"];
|
|
11
|
+
export type ShipFinalizationMode = (typeof SHIP_FINALIZATION_MODES)[number];
|
|
6
12
|
export declare const DEFAULT_HARNESSES: HarnessId[];
|
|
7
13
|
/**
|
|
8
14
|
* Evals subtree. Scaffolds the directory layout and a default config.yaml; the
|
package/dist/constants.js
CHANGED
|
@@ -35,6 +35,17 @@ function readPackageVersion() {
|
|
|
35
35
|
}
|
|
36
36
|
export const CCLAW_VERSION = readPackageVersion();
|
|
37
37
|
export const FLOW_VERSION = "1.0.0";
|
|
38
|
+
/**
|
|
39
|
+
* Canonical ship finalization enums used across stage schema, linting, and
|
|
40
|
+
* runtime gate evidence checks.
|
|
41
|
+
*/
|
|
42
|
+
export const SHIP_FINALIZATION_MODES = [
|
|
43
|
+
"FINALIZE_MERGE_LOCAL",
|
|
44
|
+
"FINALIZE_OPEN_PR",
|
|
45
|
+
"FINALIZE_KEEP_BRANCH",
|
|
46
|
+
"FINALIZE_DISCARD_BRANCH",
|
|
47
|
+
"FINALIZE_NO_VCS"
|
|
48
|
+
];
|
|
38
49
|
export const DEFAULT_HARNESSES = [
|
|
39
50
|
"claude",
|
|
40
51
|
"cursor",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SHIP_FINALIZATION_MODES } from "../../constants.js";
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// SHIP — reference: superpowers finishing-a-development-branch + gstack /ship
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -93,11 +94,7 @@ export const SHIP = {
|
|
|
93
94
|
"Pre-Ship Checks",
|
|
94
95
|
"Release Notes",
|
|
95
96
|
"Rollback Plan",
|
|
96
|
-
|
|
97
|
-
"FINALIZE_OPEN_PR",
|
|
98
|
-
"FINALIZE_KEEP_BRANCH",
|
|
99
|
-
"FINALIZE_DISCARD_BRANCH",
|
|
100
|
-
"FINALIZE_NO_VCS"
|
|
97
|
+
...SHIP_FINALIZATION_MODES
|
|
101
98
|
],
|
|
102
99
|
artifactFile: "08-ship.md",
|
|
103
100
|
// `done` exits the stage pipeline. Archive semantics are handled by the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { CCLAW_VERSION } from "../constants.js";
|
|
1
|
+
import { CCLAW_VERSION, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
2
2
|
import { orderedStageSchemas } from "./stage-schema.js";
|
|
3
3
|
import { FLOW_STAGES } from "../types.js";
|
|
4
|
+
const SHIP_FINALIZATION_ENUM_LINES = SHIP_FINALIZATION_MODES.map((mode) => ` - ${mode}`).join("\n");
|
|
4
5
|
export const ARTIFACT_TEMPLATES = {
|
|
5
6
|
"01-brainstorm.md": `---
|
|
6
7
|
stage: brainstorm
|
|
@@ -645,11 +646,7 @@ inputs_hash: sha256:pending
|
|
|
645
646
|
|
|
646
647
|
## Finalization
|
|
647
648
|
- Selected enum (exactly one):
|
|
648
|
-
|
|
649
|
-
- FINALIZE_OPEN_PR
|
|
650
|
-
- FINALIZE_KEEP_BRANCH
|
|
651
|
-
- FINALIZE_DISCARD_BRANCH
|
|
652
|
-
- FINALIZE_NO_VCS
|
|
649
|
+
${SHIP_FINALIZATION_ENUM_LINES}
|
|
653
650
|
- Selected label (A/B/C/D/E):
|
|
654
651
|
- Execution result:
|
|
655
652
|
- PR URL / merge commit / kept branch / discard confirmation:
|
package/dist/delegation.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type DelegationEntry = {
|
|
|
53
53
|
retryCount?: number;
|
|
54
54
|
/** Optional references to evidence anchors in artifacts. */
|
|
55
55
|
evidenceRefs?: string[];
|
|
56
|
+
/** Optional skill marker used for role-specific mandatory checks. */
|
|
57
|
+
skill?: string;
|
|
56
58
|
/**
|
|
57
59
|
* Fulfillment mode this entry was executed under. Omitted on legacy rows
|
|
58
60
|
* (treated as `"isolated"` for Claude, otherwise inferred from the active
|
|
@@ -85,7 +87,9 @@ export declare function appendDelegation(projectRoot: string, entry: DelegationE
|
|
|
85
87
|
* strongest guarantee.
|
|
86
88
|
*/
|
|
87
89
|
export declare function expectedFulfillmentMode(fallbacks: SubagentFallback[]): DelegationFulfillmentMode;
|
|
88
|
-
export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage
|
|
90
|
+
export declare function checkMandatoryDelegations(projectRoot: string, stage: FlowStage, options?: {
|
|
91
|
+
repairFeatureSystem?: boolean;
|
|
92
|
+
}): Promise<{
|
|
89
93
|
satisfied: boolean;
|
|
90
94
|
missing: string[];
|
|
91
95
|
waived: string[];
|
package/dist/delegation.js
CHANGED
|
@@ -154,6 +154,7 @@ function isDelegationEntry(value) {
|
|
|
154
154
|
(o.tokens === undefined || isDelegationTokenUsage(o.tokens)) &&
|
|
155
155
|
retryOk &&
|
|
156
156
|
(o.evidenceRefs === undefined || (Array.isArray(o.evidenceRefs) && o.evidenceRefs.every((item) => typeof item === "string"))) &&
|
|
157
|
+
(o.skill === undefined || typeof o.skill === "string") &&
|
|
157
158
|
(o.schemaVersion === undefined || o.schemaVersion === 1));
|
|
158
159
|
}
|
|
159
160
|
function parseLedger(raw, runId) {
|
|
@@ -257,9 +258,11 @@ export function expectedFulfillmentMode(fallbacks) {
|
|
|
257
258
|
return "role-switch";
|
|
258
259
|
return "harness-waiver";
|
|
259
260
|
}
|
|
260
|
-
export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
261
|
+
export async function checkMandatoryDelegations(projectRoot, stage, options = {}) {
|
|
261
262
|
const mandatory = stageSchema(stage).mandatoryDelegations;
|
|
262
|
-
const { activeRunId } = await readFlowState(projectRoot
|
|
263
|
+
const { activeRunId } = await readFlowState(projectRoot, {
|
|
264
|
+
repairFeatureSystem: options.repairFeatureSystem
|
|
265
|
+
});
|
|
263
266
|
const ledger = await readDelegationLedger(projectRoot);
|
|
264
267
|
const forStage = ledger.entries.filter((e) => e.stage === stage);
|
|
265
268
|
const forRun = forStage.filter((e) => e.runId === activeRunId);
|
|
@@ -279,14 +282,15 @@ export async function checkMandatoryDelegations(projectRoot, stage) {
|
|
|
279
282
|
const rows = forRun.filter((e) => e.agent === agent);
|
|
280
283
|
const completedRows = rows.filter((e) => e.status === "completed");
|
|
281
284
|
const waivedRows = rows.filter((e) => e.status === "waived");
|
|
282
|
-
const
|
|
285
|
+
const adversarialReviewerRequired = stage === "review" &&
|
|
283
286
|
agent === "reviewer" &&
|
|
284
|
-
reviewTriggers?.requireAdversarialReviewer
|
|
285
|
-
|
|
286
|
-
: 1;
|
|
287
|
+
reviewTriggers?.requireAdversarialReviewer === true;
|
|
288
|
+
const requiredCompletedCount = adversarialReviewerRequired ? 2 : 1;
|
|
287
289
|
const hasCompleted = completedRows.length >= requiredCompletedCount;
|
|
288
290
|
const hasWaived = waivedRows.length > 0;
|
|
289
|
-
const
|
|
291
|
+
const hasAdversarialSkill = !adversarialReviewerRequired ||
|
|
292
|
+
completedRows.some((row) => row.skill === "adversarial-review");
|
|
293
|
+
const ok = hasWaived || (hasCompleted && hasAdversarialSkill);
|
|
290
294
|
if (!ok) {
|
|
291
295
|
missing.push(agent);
|
|
292
296
|
continue;
|
package/dist/doctor.js
CHANGED
|
@@ -10,11 +10,11 @@ import { exists } from "./fs-utils.js";
|
|
|
10
10
|
import { gitignoreHasRequiredPatterns } from "./gitignore.js";
|
|
11
11
|
import { HARNESS_ADAPTERS, CCLAW_MARKER_START, CCLAW_MARKER_END, harnessShimFileNames, harnessShimSkillNames } from "./harness-adapters.js";
|
|
12
12
|
import { policyChecks } from "./policy.js";
|
|
13
|
-
import { readFlowState } from "./runs.js";
|
|
14
|
-
import { skippedStagesForTrack } from "./flow-state.js";
|
|
13
|
+
import { CorruptFlowStateError, readFlowState } from "./runs.js";
|
|
14
|
+
import { createInitialFlowState, skippedStagesForTrack } from "./flow-state.js";
|
|
15
15
|
import { FLOW_STAGES, TRACK_STAGES } from "./types.js";
|
|
16
16
|
import { checkMandatoryDelegations } from "./delegation.js";
|
|
17
|
-
import { ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
|
|
17
|
+
import { activeFeatureMetaPath, ensureFeatureSystem, listFeatures, readActiveFeature, readFeatureWorktreeRegistry, resolveFeatureWorkspacePath, worktreeRegistryPath } from "./feature-system.js";
|
|
18
18
|
import { buildTraceMatrix } from "./trace-matrix.js";
|
|
19
19
|
import { classifyReconciliationNotices, reconcileAndWriteCurrentStageGateCatalog, readReconciliationNotices, RECONCILIATION_NOTICES_REL_PATH, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
|
|
20
20
|
import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
|
|
@@ -190,6 +190,25 @@ async function readHookDocument(filePath) {
|
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
+
async function readJsonObjectStatus(filePath) {
|
|
194
|
+
if (!(await exists(filePath))) {
|
|
195
|
+
return { exists: false, ok: false, error: "file is missing" };
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
199
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
200
|
+
return { exists: true, ok: false, error: "JSON root must be an object" };
|
|
201
|
+
}
|
|
202
|
+
return { exists: true, ok: true };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
exists: true,
|
|
207
|
+
ok: false,
|
|
208
|
+
error: error instanceof Error ? error.message : String(error)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
193
212
|
function normalizeOpenCodePluginEntry(entry) {
|
|
194
213
|
if (typeof entry === "string" && entry.trim().length > 0)
|
|
195
214
|
return entry.trim();
|
|
@@ -212,12 +231,16 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
212
231
|
path.join(projectRoot, ".opencode/opencode.json"),
|
|
213
232
|
path.join(projectRoot, ".opencode/opencode.jsonc")
|
|
214
233
|
];
|
|
234
|
+
const mismatches = [];
|
|
235
|
+
let foundAnyConfig = false;
|
|
215
236
|
for (const configPath of candidates) {
|
|
216
237
|
if (!(await exists(configPath))) {
|
|
217
238
|
continue;
|
|
218
239
|
}
|
|
240
|
+
foundAnyConfig = true;
|
|
219
241
|
const parsed = await readHookDocument(configPath);
|
|
220
242
|
if (!parsed) {
|
|
243
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} is unreadable or invalid JSON`);
|
|
221
244
|
continue;
|
|
222
245
|
}
|
|
223
246
|
const plugins = Array.isArray(parsed.plugin) ? parsed.plugin : [];
|
|
@@ -225,7 +248,10 @@ async function opencodeRegistrationCheck(projectRoot) {
|
|
|
225
248
|
if (registered) {
|
|
226
249
|
return { ok: true, details: `${path.relative(projectRoot, configPath)} registers ${expected}` };
|
|
227
250
|
}
|
|
228
|
-
|
|
251
|
+
mismatches.push(`${path.relative(projectRoot, configPath)} missing plugin ${expected}`);
|
|
252
|
+
}
|
|
253
|
+
if (foundAnyConfig) {
|
|
254
|
+
return { ok: false, details: mismatches.join(" | ") };
|
|
229
255
|
}
|
|
230
256
|
return { ok: false, details: `No opencode.json/opencode.jsonc found with plugin ${expected}` };
|
|
231
257
|
}
|
|
@@ -244,6 +270,12 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
244
270
|
};
|
|
245
271
|
}
|
|
246
272
|
const plugin = imported.default({ directory: projectRoot });
|
|
273
|
+
if (!plugin || typeof plugin !== "object" || Array.isArray(plugin)) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
details: `${path.relative(projectRoot, pluginPath)} factory must return a plugin object`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
247
279
|
const requiredHandlers = [
|
|
248
280
|
"event",
|
|
249
281
|
"tool.execute.before",
|
|
@@ -257,7 +289,6 @@ async function opencodePluginRuntimeShapeCheck(projectRoot) {
|
|
|
257
289
|
details: `${path.relative(projectRoot, pluginPath)} missing runtime handlers: ${missing.join(", ")}`
|
|
258
290
|
};
|
|
259
291
|
}
|
|
260
|
-
await plugin.event({ event: { type: "session.updated", data: {} } });
|
|
261
292
|
return {
|
|
262
293
|
ok: true,
|
|
263
294
|
details: `${path.relative(projectRoot, pluginPath)} exports compatible runtime handler shape`
|
|
@@ -1289,10 +1320,28 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1289
1320
|
details: modePath
|
|
1290
1321
|
});
|
|
1291
1322
|
}
|
|
1292
|
-
await ensureFeatureSystem(projectRoot);
|
|
1293
|
-
const activeFeature = await readActiveFeature(projectRoot);
|
|
1294
|
-
let flowState =
|
|
1295
|
-
|
|
1323
|
+
await ensureFeatureSystem(projectRoot, { repair: false });
|
|
1324
|
+
const activeFeature = await readActiveFeature(projectRoot, { repair: false });
|
|
1325
|
+
let flowState = createInitialFlowState();
|
|
1326
|
+
let flowStateCorruptError = null;
|
|
1327
|
+
try {
|
|
1328
|
+
flowState = await readFlowState(projectRoot, { repairFeatureSystem: false });
|
|
1329
|
+
}
|
|
1330
|
+
catch (error) {
|
|
1331
|
+
if (error instanceof CorruptFlowStateError) {
|
|
1332
|
+
flowStateCorruptError = error;
|
|
1333
|
+
checks.push({
|
|
1334
|
+
name: "flow_state:readable",
|
|
1335
|
+
ok: false,
|
|
1336
|
+
severity: "error",
|
|
1337
|
+
details: error.message
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (options.reconcileCurrentStageGates === true && !flowStateCorruptError) {
|
|
1296
1345
|
const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
|
|
1297
1346
|
if (reconciliation.wrote) {
|
|
1298
1347
|
flowState = {
|
|
@@ -1311,6 +1360,13 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1311
1360
|
: `no gate reconciliation changes needed for stage "${reconciliation.stage}"`
|
|
1312
1361
|
});
|
|
1313
1362
|
}
|
|
1363
|
+
else if (options.reconcileCurrentStageGates === true && flowStateCorruptError) {
|
|
1364
|
+
checks.push({
|
|
1365
|
+
name: "gates:reconcile:writeback",
|
|
1366
|
+
ok: false,
|
|
1367
|
+
details: "skipped gate reconciliation because flow-state.json is corrupt"
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1314
1370
|
const activeRunId = typeof flowState.activeRunId === "string" ? flowState.activeRunId.trim() : "";
|
|
1315
1371
|
checks.push({
|
|
1316
1372
|
name: "flow_state:active_run_id",
|
|
@@ -1318,6 +1374,15 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1318
1374
|
details: `${RUNTIME_ROOT}/state/flow-state.json must include activeRunId`
|
|
1319
1375
|
});
|
|
1320
1376
|
const reconciliationNotices = await readReconciliationNotices(projectRoot);
|
|
1377
|
+
checks.push({
|
|
1378
|
+
name: "state:reconciliation_notices_parse",
|
|
1379
|
+
ok: reconciliationNotices.parseOk && reconciliationNotices.schemaOk,
|
|
1380
|
+
details: !reconciliationNotices.parseOk
|
|
1381
|
+
? `unable to parse ${RECONCILIATION_NOTICES_REL_PATH}; reset with \`cclaw sync\` or repair JSON by hand`
|
|
1382
|
+
: !reconciliationNotices.schemaOk
|
|
1383
|
+
? `${RECONCILIATION_NOTICES_REL_PATH} schemaVersion mismatch; expected ${reconciliationNotices.schemaVersion}`
|
|
1384
|
+
: `${RECONCILIATION_NOTICES_REL_PATH} parsed successfully`
|
|
1385
|
+
});
|
|
1321
1386
|
const noticeBuckets = classifyReconciliationNotices(flowState, reconciliationNotices.notices);
|
|
1322
1387
|
const formatNoticeList = (items) => items
|
|
1323
1388
|
.slice(0, 8)
|
|
@@ -1425,22 +1490,38 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1425
1490
|
? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
|
|
1426
1491
|
: `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
|
|
1427
1492
|
});
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1493
|
+
const activeMetaStatus = await readJsonObjectStatus(activeFeatureMetaPath(projectRoot));
|
|
1494
|
+
const worktreeRegistryStatus = await readJsonObjectStatus(worktreeRegistryPath(projectRoot));
|
|
1495
|
+
const features = await listFeatures(projectRoot, { repair: false });
|
|
1496
|
+
const worktreeRegistry = await readFeatureWorktreeRegistry(projectRoot, { repair: false });
|
|
1430
1497
|
const activeFeatureEntry = worktreeRegistry.entries.find((entry) => entry.featureId === activeFeature);
|
|
1431
1498
|
const activeFeatureWorkspacePath = activeFeatureEntry
|
|
1432
1499
|
? resolveFeatureWorkspacePath(projectRoot, activeFeatureEntry)
|
|
1433
1500
|
: "";
|
|
1434
1501
|
checks.push({
|
|
1435
1502
|
name: "state:active_feature_meta",
|
|
1436
|
-
ok:
|
|
1503
|
+
ok: activeMetaStatus.exists,
|
|
1437
1504
|
details: `${RUNTIME_ROOT}/state/active-feature.json must exist`
|
|
1438
1505
|
});
|
|
1506
|
+
checks.push({
|
|
1507
|
+
name: "state:active_feature_meta_valid_json",
|
|
1508
|
+
ok: activeMetaStatus.ok,
|
|
1509
|
+
details: activeMetaStatus.ok
|
|
1510
|
+
? `${RUNTIME_ROOT}/state/active-feature.json parsed successfully`
|
|
1511
|
+
: `${RUNTIME_ROOT}/state/active-feature.json is invalid: ${activeMetaStatus.error ?? "unknown error"}`
|
|
1512
|
+
});
|
|
1439
1513
|
checks.push({
|
|
1440
1514
|
name: "state:worktree_registry_exists",
|
|
1441
|
-
ok:
|
|
1515
|
+
ok: worktreeRegistryStatus.exists,
|
|
1442
1516
|
details: `${RUNTIME_ROOT}/state/worktrees.json must exist and track feature->worktree mapping`
|
|
1443
1517
|
});
|
|
1518
|
+
checks.push({
|
|
1519
|
+
name: "state:worktree_registry_valid_json",
|
|
1520
|
+
ok: worktreeRegistryStatus.ok,
|
|
1521
|
+
details: worktreeRegistryStatus.ok
|
|
1522
|
+
? `${RUNTIME_ROOT}/state/worktrees.json parsed successfully`
|
|
1523
|
+
: `${RUNTIME_ROOT}/state/worktrees.json is invalid: ${worktreeRegistryStatus.error ?? "unknown error"}`
|
|
1524
|
+
});
|
|
1444
1525
|
checks.push({
|
|
1445
1526
|
name: "state:active_feature_exists",
|
|
1446
1527
|
ok: features.includes(activeFeature),
|
|
@@ -1590,7 +1671,9 @@ export async function doctorChecks(projectRoot, options = {}) {
|
|
|
1590
1671
|
ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "runs")),
|
|
1591
1672
|
details: `${RUNTIME_ROOT}/runs must exist for archived feature snapshots`
|
|
1592
1673
|
});
|
|
1593
|
-
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage
|
|
1674
|
+
const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
|
|
1675
|
+
repairFeatureSystem: false
|
|
1676
|
+
});
|
|
1594
1677
|
const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
|
|
1595
1678
|
? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
|
|
1596
1679
|
: "";
|
package/dist/feature-system.d.ts
CHANGED
|
@@ -19,16 +19,23 @@ export interface CreateFeatureOptions {
|
|
|
19
19
|
cloneActive?: boolean;
|
|
20
20
|
switchTo?: boolean;
|
|
21
21
|
}
|
|
22
|
+
export interface FeatureSystemAccessOptions {
|
|
23
|
+
/**
|
|
24
|
+
* When false, read metadata without auto-repair writes. Useful for pure
|
|
25
|
+
* diagnostics (doctor) that should not mutate state as a side effect.
|
|
26
|
+
*/
|
|
27
|
+
repair?: boolean;
|
|
28
|
+
}
|
|
22
29
|
export declare function activeFeatureMetaPath(projectRoot: string): string;
|
|
23
30
|
export declare function worktreeRegistryPath(projectRoot: string): string;
|
|
24
31
|
export declare function featureRootPath(projectRoot: string, featureId: string): string;
|
|
25
32
|
export declare function featureArtifactsPath(projectRoot: string, featureId: string): string;
|
|
26
33
|
export declare function featureStatePath(projectRoot: string, featureId: string): string;
|
|
27
34
|
export declare function resolveFeatureWorkspacePath(projectRoot: string, entry: FeatureWorkspaceEntry): string;
|
|
28
|
-
export declare function ensureFeatureSystem(projectRoot: string): Promise<ActiveFeatureMeta>;
|
|
29
|
-
export declare function readFeatureWorktreeRegistry(projectRoot: string): Promise<FeatureWorktreeRegistry>;
|
|
30
|
-
export declare function readActiveFeature(projectRoot: string): Promise<string>;
|
|
31
|
-
export declare function listFeatures(projectRoot: string): Promise<string[]>;
|
|
35
|
+
export declare function ensureFeatureSystem(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<ActiveFeatureMeta>;
|
|
36
|
+
export declare function readFeatureWorktreeRegistry(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<FeatureWorktreeRegistry>;
|
|
37
|
+
export declare function readActiveFeature(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<string>;
|
|
38
|
+
export declare function listFeatures(projectRoot: string, options?: FeatureSystemAccessOptions): Promise<string[]>;
|
|
32
39
|
export declare function syncActiveFeatureSnapshot(projectRoot: string): Promise<void>;
|
|
33
40
|
export declare function switchActiveFeature(projectRoot: string, featureId: string): Promise<ActiveFeatureMeta>;
|
|
34
41
|
export declare function createFeature(projectRoot: string, rawFeatureId: string, options?: CreateFeatureOptions): Promise<string>;
|
package/dist/feature-system.js
CHANGED
|
@@ -278,20 +278,64 @@ async function ensureRegistryState(projectRoot) {
|
|
|
278
278
|
await writeActiveFeatureMeta(projectRoot, activeMeta);
|
|
279
279
|
return { registry, activeMeta };
|
|
280
280
|
}
|
|
281
|
-
|
|
282
|
-
const
|
|
281
|
+
async function readRegistryStateReadonly(projectRoot) {
|
|
282
|
+
const currentRegistry = await readRegistry(projectRoot);
|
|
283
|
+
const entries = [...currentRegistry.entries];
|
|
284
|
+
const gitRepo = await isGitRepository(projectRoot);
|
|
285
|
+
const source = gitRepo ? "git-worktree" : "workspace";
|
|
286
|
+
const branch = gitRepo ? await currentBranch(projectRoot) : "workspace/default";
|
|
287
|
+
if (!entries.some((entry) => entry.featureId === DEFAULT_FEATURE_ID)) {
|
|
288
|
+
entries.push(buildDefaultEntry(source, branch));
|
|
289
|
+
}
|
|
290
|
+
const legacyFeatureIds = await listLegacySnapshotIds(projectRoot);
|
|
291
|
+
for (const legacyId of legacyFeatureIds) {
|
|
292
|
+
if (entries.some((entry) => entry.featureId === legacyId)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
entries.push({
|
|
296
|
+
featureId: legacyId,
|
|
297
|
+
branch: `legacy/${legacyId}`,
|
|
298
|
+
path: `${LEGACY_FEATURES_DIR_REL_PATH}/${legacyId}`,
|
|
299
|
+
source: "legacy-snapshot",
|
|
300
|
+
createdAt: new Date().toISOString()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const registry = {
|
|
304
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA_VERSION,
|
|
305
|
+
updatedAt: currentRegistry.updatedAt,
|
|
306
|
+
entries: dedupeEntries(entries)
|
|
307
|
+
};
|
|
308
|
+
const active = await readActiveFeatureMetaInternal(projectRoot);
|
|
309
|
+
return {
|
|
310
|
+
registry,
|
|
311
|
+
activeMeta: {
|
|
312
|
+
activeFeature: registryHasFeature(registry, active.activeFeature)
|
|
313
|
+
? active.activeFeature
|
|
314
|
+
: DEFAULT_FEATURE_ID,
|
|
315
|
+
updatedAt: active.updatedAt
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function resolveFeatureSystemState(projectRoot, options = {}) {
|
|
320
|
+
if (options.repair === false) {
|
|
321
|
+
return readRegistryStateReadonly(projectRoot);
|
|
322
|
+
}
|
|
323
|
+
return ensureRegistryState(projectRoot);
|
|
324
|
+
}
|
|
325
|
+
export async function ensureFeatureSystem(projectRoot, options = {}) {
|
|
326
|
+
const { activeMeta } = await resolveFeatureSystemState(projectRoot, options);
|
|
283
327
|
return activeMeta;
|
|
284
328
|
}
|
|
285
|
-
export async function readFeatureWorktreeRegistry(projectRoot) {
|
|
286
|
-
const { registry } = await
|
|
329
|
+
export async function readFeatureWorktreeRegistry(projectRoot, options = {}) {
|
|
330
|
+
const { registry } = await resolveFeatureSystemState(projectRoot, options);
|
|
287
331
|
return registry;
|
|
288
332
|
}
|
|
289
|
-
export async function readActiveFeature(projectRoot) {
|
|
290
|
-
const meta = await ensureFeatureSystem(projectRoot);
|
|
333
|
+
export async function readActiveFeature(projectRoot, options = {}) {
|
|
334
|
+
const meta = await ensureFeatureSystem(projectRoot, options);
|
|
291
335
|
return normalizedFeatureId(meta.activeFeature);
|
|
292
336
|
}
|
|
293
|
-
export async function listFeatures(projectRoot) {
|
|
294
|
-
const registry = await readFeatureWorktreeRegistry(projectRoot);
|
|
337
|
+
export async function listFeatures(projectRoot, options = {}) {
|
|
338
|
+
const registry = await readFeatureWorktreeRegistry(projectRoot, options);
|
|
295
339
|
return registry.entries.map((entry) => entry.featureId).sort((a, b) => a.localeCompare(b));
|
|
296
340
|
}
|
|
297
341
|
export async function syncActiveFeatureSnapshot(projectRoot) {
|
package/dist/flow-state.d.ts
CHANGED
|
@@ -92,8 +92,10 @@ export declare function isFlowTrack(value: unknown): value is FlowTrack;
|
|
|
92
92
|
export declare function trackStages(track: FlowTrack): FlowStage[];
|
|
93
93
|
export declare function skippedStagesForTrack(track: FlowTrack): FlowStage[];
|
|
94
94
|
export declare function firstStageForTrack(track: FlowTrack): FlowStage;
|
|
95
|
+
export declare function createRunId(date?: Date): string;
|
|
95
96
|
export declare function createInitialFlowState(activeRunIdOrOptions?: string | InitialFlowStateOptions, maybeTrack?: FlowTrack): FlowState;
|
|
96
97
|
export declare function canTransition(from: FlowStage, to: FlowStage): boolean;
|
|
98
|
+
export declare function getAvailableTransitions(from: FlowStage, track?: FlowTrack): TransitionRule[];
|
|
97
99
|
export declare function getTransitionGuards(from: FlowStage, to: FlowStage, track?: FlowTrack): string[];
|
|
98
100
|
export declare function nextStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
|
99
101
|
export declare function previousStage(stage: FlowStage, track?: FlowTrack): FlowStage | null;
|
package/dist/flow-state.js
CHANGED
|
@@ -58,11 +58,14 @@ export function firstStageForTrack(track) {
|
|
|
58
58
|
const stages = TRACK_STAGES[track];
|
|
59
59
|
return stages[0] ?? "brainstorm";
|
|
60
60
|
}
|
|
61
|
-
export function
|
|
61
|
+
export function createRunId(date = new Date()) {
|
|
62
|
+
return `run-${date.getTime().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
63
|
+
}
|
|
64
|
+
export function createInitialFlowState(activeRunIdOrOptions = {}, maybeTrack) {
|
|
62
65
|
const options = typeof activeRunIdOrOptions === "string"
|
|
63
66
|
? { activeRunId: activeRunIdOrOptions, track: maybeTrack }
|
|
64
67
|
: activeRunIdOrOptions;
|
|
65
|
-
const activeRunId = options.activeRunId ??
|
|
68
|
+
const activeRunId = options.activeRunId ?? createRunId();
|
|
66
69
|
const track = options.track ?? "standard";
|
|
67
70
|
const skippedStages = skippedStagesForTrack(track);
|
|
68
71
|
const stageGateCatalog = {};
|
|
@@ -97,6 +100,20 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
|
|
|
97
100
|
export function canTransition(from, to) {
|
|
98
101
|
return TRANSITION_RULES.some((rule) => rule.from === from && rule.to === to);
|
|
99
102
|
}
|
|
103
|
+
export function getAvailableTransitions(from, track = "standard") {
|
|
104
|
+
const natural = nextStage(from, track);
|
|
105
|
+
const fromRules = TRANSITION_RULES.filter((rule) => rule.from === from);
|
|
106
|
+
if (!natural) {
|
|
107
|
+
return fromRules;
|
|
108
|
+
}
|
|
109
|
+
return fromRules.sort((a, b) => {
|
|
110
|
+
if (a.to === natural && b.to !== natural)
|
|
111
|
+
return -1;
|
|
112
|
+
if (b.to === natural && a.to !== natural)
|
|
113
|
+
return 1;
|
|
114
|
+
return a.to.localeCompare(b.to);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
100
117
|
export function getTransitionGuards(from, to, track = "standard") {
|
|
101
118
|
// Natural forward edge on this track: derive guards fresh from the
|
|
102
119
|
// track-specific gate schema. `TRANSITION_RULES` collapses shared edges
|
package/dist/fs-utils.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
27
27
|
const staleAfterMs = options.staleAfterMs ?? 60_000;
|
|
28
28
|
await ensureDir(path.dirname(lockPath));
|
|
29
29
|
let acquired = false;
|
|
30
|
+
let lastError = null;
|
|
30
31
|
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
31
32
|
try {
|
|
32
33
|
await fs.mkdir(lockPath);
|
|
@@ -34,6 +35,7 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
34
35
|
break;
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
38
|
+
lastError = error;
|
|
37
39
|
const code = error?.code;
|
|
38
40
|
if (code !== "EEXIST") {
|
|
39
41
|
throw error;
|
|
@@ -52,13 +54,20 @@ export async function withDirectoryLock(lockPath, fn, options = {}) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
if (!acquired) {
|
|
55
|
-
|
|
57
|
+
const details = lastError instanceof Error ? lastError.message : String(lastError);
|
|
58
|
+
throw new Error(`Failed to acquire lock: ${lockPath} (attempts=${retries}, retryDelayMs=${retryDelayMs}, staleAfterMs=${staleAfterMs}, lastError=${details})`);
|
|
56
59
|
}
|
|
57
60
|
try {
|
|
58
61
|
return await fn();
|
|
59
62
|
}
|
|
60
63
|
finally {
|
|
61
|
-
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => {
|
|
64
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch((cleanupError) => {
|
|
65
|
+
// Lock cleanup failure should not shadow the original operation result,
|
|
66
|
+
// but keep a diagnostic breadcrumb for flaky FS environments.
|
|
67
|
+
const details = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.warn(`cclaw lock cleanup failed for ${lockPath}: ${details}`);
|
|
70
|
+
});
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
export async function writeFileSafe(filePath, content) {
|
package/dist/gate-evidence.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface ReconciliationNotice {
|
|
|
41
41
|
export interface ReconciliationNoticesPayload {
|
|
42
42
|
schemaVersion: number;
|
|
43
43
|
notices: ReconciliationNotice[];
|
|
44
|
+
parseOk: boolean;
|
|
45
|
+
schemaOk: boolean;
|
|
44
46
|
}
|
|
45
47
|
export interface ReconciliationNoticeBuckets {
|
|
46
48
|
activeBlocked: ReconciliationNotice[];
|
package/dist/gate-evidence.js
CHANGED
|
@@ -72,7 +72,9 @@ function reconciliationNoticesPath(projectRoot) {
|
|
|
72
72
|
function defaultReconciliationNoticesPayload() {
|
|
73
73
|
return {
|
|
74
74
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
75
|
-
notices: []
|
|
75
|
+
notices: [],
|
|
76
|
+
parseOk: true,
|
|
77
|
+
schemaOk: true
|
|
76
78
|
};
|
|
77
79
|
}
|
|
78
80
|
function sanitizeReconciliationNotice(raw) {
|
|
@@ -104,6 +106,7 @@ export async function readReconciliationNotices(projectRoot) {
|
|
|
104
106
|
}
|
|
105
107
|
try {
|
|
106
108
|
const raw = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
109
|
+
const schemaOk = raw.schemaVersion === RECONCILIATION_NOTICES_SCHEMA_VERSION;
|
|
107
110
|
const notices = Array.isArray(raw.notices)
|
|
108
111
|
? raw.notices
|
|
109
112
|
.map((value) => sanitizeReconciliationNotice(value))
|
|
@@ -111,11 +114,17 @@ export async function readReconciliationNotices(projectRoot) {
|
|
|
111
114
|
: [];
|
|
112
115
|
return {
|
|
113
116
|
schemaVersion: RECONCILIATION_NOTICES_SCHEMA_VERSION,
|
|
114
|
-
notices
|
|
117
|
+
notices,
|
|
118
|
+
parseOk: true,
|
|
119
|
+
schemaOk
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
catch {
|
|
118
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
...defaultReconciliationNoticesPayload(),
|
|
125
|
+
parseOk: false,
|
|
126
|
+
schemaOk: false
|
|
127
|
+
};
|
|
119
128
|
}
|
|
120
129
|
}
|
|
121
130
|
async function writeReconciliationNotices(projectRoot, payload) {
|
package/dist/install.js
CHANGED
|
@@ -977,7 +977,7 @@ async function writeState(projectRoot, config, forceReset = false) {
|
|
|
977
977
|
if (!forceReset && (await exists(statePath))) {
|
|
978
978
|
return;
|
|
979
979
|
}
|
|
980
|
-
const state = createInitialFlowState(
|
|
980
|
+
const state = createInitialFlowState({ track: config.defaultTrack ?? "standard" });
|
|
981
981
|
await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
982
982
|
}
|
|
983
983
|
async function writeAdapterManifest(projectRoot, harnesses) {
|
|
@@ -1,22 +1,51 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { RUNTIME_ROOT } from "../constants.js";
|
|
3
|
+
import { RUNTIME_ROOT, SHIP_FINALIZATION_MODES } from "../constants.js";
|
|
4
4
|
import { stageSchema } from "../content/stage-schema.js";
|
|
5
5
|
import { appendDelegation, checkMandatoryDelegations } from "../delegation.js";
|
|
6
6
|
import { readActiveFeature } from "../feature-system.js";
|
|
7
7
|
import { verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "../gate-evidence.js";
|
|
8
8
|
import { extractMarkdownSectionBody, parseLearningsSection } from "../artifact-linter.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getAvailableTransitions, getTransitionGuards, isFlowTrack } from "../flow-state.js";
|
|
10
10
|
import { appendKnowledge } from "../knowledge-store.js";
|
|
11
11
|
import { readFlowState, writeFlowState } from "../runs.js";
|
|
12
12
|
import { FLOW_STAGES } from "../types.js";
|
|
13
13
|
function unique(values) {
|
|
14
14
|
return [...new Set(values)];
|
|
15
15
|
}
|
|
16
|
+
function resolveSuccessorTransition(stage, track, transitionTargets, satisfiedGuards, selectedTransitionGuards) {
|
|
17
|
+
const natural = transitionTargets[0] ?? null;
|
|
18
|
+
const specialTargets = transitionTargets.filter((target) => target !== natural);
|
|
19
|
+
for (const target of specialTargets) {
|
|
20
|
+
const guards = getTransitionGuards(stage, target, track);
|
|
21
|
+
if (guards.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
const selectedSpecial = guards.some((guard) => selectedTransitionGuards.has(guard));
|
|
24
|
+
if (!selectedSpecial)
|
|
25
|
+
continue;
|
|
26
|
+
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (natural) {
|
|
31
|
+
const guards = getTransitionGuards(stage, natural, track);
|
|
32
|
+
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
33
|
+
return natural;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const target of specialTargets) {
|
|
37
|
+
const guards = getTransitionGuards(stage, target, track);
|
|
38
|
+
if (guards.every((guard) => satisfiedGuards.has(guard))) {
|
|
39
|
+
return target;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return natural;
|
|
43
|
+
}
|
|
16
44
|
const TEST_COMMAND_HINT_PATTERN = /\b(?:npm test|pnpm test|yarn test|bun test|vitest|jest|pytest|go test|cargo test|mvn test|gradle test|dotnet test)\b/iu;
|
|
17
45
|
const SHA_WITH_LABEL_PATTERN = /\b(?:sha|commit)(?:\s*[:=]|\s+)\s*[0-9a-f]{7,40}\b/iu;
|
|
18
46
|
const PASS_STATUS_PATTERN = /\b(?:pass|passed|green|ok)\b/iu;
|
|
19
|
-
const SHIP_FINALIZATION_MODE_PATTERN =
|
|
47
|
+
const SHIP_FINALIZATION_MODE_PATTERN = new RegExp(`\\b(?:${SHIP_FINALIZATION_MODES.join("|")})\\b`, "u");
|
|
48
|
+
const SHIP_FINALIZATION_MODE_HINT = SHIP_FINALIZATION_MODES.join(", ");
|
|
20
49
|
// Per-gate validators keyed by `${stage}:${gateId}`. Returning a non-null
|
|
21
50
|
// string surfaces the reason as an `advance-stage` failure so evidence is
|
|
22
51
|
// guaranteed to carry the structural breadcrumbs downstream tooling
|
|
@@ -36,7 +65,7 @@ const GATE_EVIDENCE_VALIDATORS = {
|
|
|
36
65
|
},
|
|
37
66
|
"ship:ship_finalization_executed": (evidence) => {
|
|
38
67
|
if (!SHIP_FINALIZATION_MODE_PATTERN.test(evidence)) {
|
|
39
|
-
return
|
|
68
|
+
return `must name the finalization mode that ran (for example ${SHIP_FINALIZATION_MODE_HINT}).`;
|
|
40
69
|
}
|
|
41
70
|
return null;
|
|
42
71
|
}
|
|
@@ -395,10 +424,16 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
395
424
|
const requiredGateIds = schema.requiredGates
|
|
396
425
|
.filter((gate) => gate.tier === "required")
|
|
397
426
|
.map((gate) => gate.id);
|
|
427
|
+
const transitionTargets = getAvailableTransitions(args.stage, flowState.track).map((rule) => rule.to);
|
|
398
428
|
const allowedGateIds = new Set(schema.requiredGates.map((gate) => gate.id));
|
|
429
|
+
const transitionGuardIds = new Set(transitionTargets
|
|
430
|
+
.flatMap((target) => getTransitionGuards(args.stage, target, flowState.track))
|
|
431
|
+
.filter((guardId) => !allowedGateIds.has(guardId)));
|
|
432
|
+
const selectableGateIds = new Set([...allowedGateIds, ...transitionGuardIds]);
|
|
399
433
|
const selectedGateIds = args.passedGateIds.length > 0
|
|
400
|
-
? args.passedGateIds.filter((gateId) =>
|
|
434
|
+
? args.passedGateIds.filter((gateId) => selectableGateIds.has(gateId))
|
|
401
435
|
: requiredGateIds;
|
|
436
|
+
const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
|
|
402
437
|
const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIds.includes(gateId));
|
|
403
438
|
if (missingRequired.length > 0) {
|
|
404
439
|
io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
|
|
@@ -436,7 +471,8 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
436
471
|
...nextPassed.filter((gateId) => conditional.has(gateId)),
|
|
437
472
|
...nextBlocked.filter((gateId) => conditional.has(gateId))
|
|
438
473
|
]);
|
|
439
|
-
const
|
|
474
|
+
const guardEvidenceGateIds = unique([...nextPassed, ...selectedTransitionGuards]);
|
|
475
|
+
const missingGuardEvidence = guardEvidenceGateIds.filter((gateId) => {
|
|
440
476
|
const existing = flowState.guardEvidence[gateId];
|
|
441
477
|
if (typeof existing === "string" && existing.trim().length > 0) {
|
|
442
478
|
return false;
|
|
@@ -464,7 +500,7 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
464
500
|
return 1;
|
|
465
501
|
}
|
|
466
502
|
const nextGuardEvidence = { ...flowState.guardEvidence };
|
|
467
|
-
for (const gateId of
|
|
503
|
+
for (const gateId of guardEvidenceGateIds) {
|
|
468
504
|
const provided = args.evidenceByGate[gateId];
|
|
469
505
|
if (typeof provided === "string" && provided.trim().length > 0) {
|
|
470
506
|
nextGuardEvidence[gateId] = provided.trim();
|
|
@@ -508,7 +544,8 @@ async function runAdvanceStage(projectRoot, args, io) {
|
|
|
508
544
|
io.stderr.write(`cclaw internal advance-stage: learnings harvest failed for "${schema.artifactFile}". ${learningsHarvest.details}\n`);
|
|
509
545
|
return 1;
|
|
510
546
|
}
|
|
511
|
-
const
|
|
547
|
+
const satisfiedGuards = new Set([...nextPassed, ...selectedTransitionGuards]);
|
|
548
|
+
const successor = resolveSuccessorTransition(args.stage, flowState.track, transitionTargets, satisfiedGuards, new Set(selectedTransitionGuards));
|
|
512
549
|
const completedStages = flowState.completedStages.includes(args.stage)
|
|
513
550
|
? [...flowState.completedStages]
|
|
514
551
|
: [...flowState.completedStages, args.stage];
|
package/dist/run-archive.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
7
|
import { evaluateRetroGate } from "./retro-gate.js";
|
|
8
8
|
import { ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
|
|
9
9
|
const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
|
|
@@ -17,6 +17,12 @@ const STATE_SNAPSHOT_EXCLUDE = new Set([
|
|
|
17
17
|
const DELEGATION_LOG_FILE = "delegation-log.json";
|
|
18
18
|
const TDD_CYCLE_LOG_FILE = "tdd-cycle-log.jsonl";
|
|
19
19
|
const RECONCILIATION_NOTICES_FILE = "reconciliation-notices.json";
|
|
20
|
+
const CRITICAL_STATE_SNAPSHOT_FILES = new Set([
|
|
21
|
+
"flow-state.json",
|
|
22
|
+
DELEGATION_LOG_FILE,
|
|
23
|
+
TDD_CYCLE_LOG_FILE,
|
|
24
|
+
RECONCILIATION_NOTICES_FILE
|
|
25
|
+
]);
|
|
20
26
|
function runsRoot(projectRoot) {
|
|
21
27
|
return path.join(projectRoot, RUNS_DIR_REL_PATH);
|
|
22
28
|
}
|
|
@@ -26,6 +32,9 @@ function activeArtifactsPath(projectRoot) {
|
|
|
26
32
|
function stateDirPath(projectRoot) {
|
|
27
33
|
return path.join(projectRoot, STATE_DIR_REL_PATH);
|
|
28
34
|
}
|
|
35
|
+
function archiveLockPath(projectRoot) {
|
|
36
|
+
return path.join(projectRoot, RUNTIME_ROOT, "state", ".archive.lock");
|
|
37
|
+
}
|
|
29
38
|
async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
30
39
|
const sourceDir = stateDirPath(projectRoot);
|
|
31
40
|
if (!(await exists(sourceDir))) {
|
|
@@ -57,8 +66,12 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
|
|
|
57
66
|
copied.push(entry.name);
|
|
58
67
|
}
|
|
59
68
|
}
|
|
60
|
-
catch {
|
|
61
|
-
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (CRITICAL_STATE_SNAPSHOT_FILES.has(entry.name)) {
|
|
71
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new Error(`Archive snapshot failed for critical state file "${entry.name}" (${details}).`);
|
|
73
|
+
}
|
|
74
|
+
// Non-critical snapshot files are best-effort and may be skipped.
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
return copied.sort((a, b) => a.localeCompare(b));
|
|
@@ -147,127 +160,155 @@ export async function listRuns(projectRoot) {
|
|
|
147
160
|
}
|
|
148
161
|
export async function archiveRun(projectRoot, featureName, options = {}) {
|
|
149
162
|
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;
|
|
163
|
+
return withDirectoryLock(archiveLockPath(projectRoot), async () => {
|
|
164
|
+
const activeFeature = await readActiveFeature(projectRoot);
|
|
165
|
+
const artifactsDir = activeArtifactsPath(projectRoot);
|
|
166
|
+
const runsDir = runsRoot(projectRoot);
|
|
167
|
+
await ensureDir(runsDir);
|
|
219
168
|
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
|
-
|
|
169
|
+
const feature = (featureName?.trim() && featureName.trim().length > 0)
|
|
170
|
+
? featureName.trim()
|
|
171
|
+
: await inferFeatureNameFromArtifacts(projectRoot);
|
|
172
|
+
const archiveBaseId = `${toArchiveDate()}-${slugifyFeatureName(feature)}`;
|
|
173
|
+
const archiveId = await uniqueArchiveId(projectRoot, archiveBaseId);
|
|
174
|
+
const archivePath = path.join(runsDir, archiveId);
|
|
175
|
+
const archiveArtifactsPath = path.join(archivePath, "artifacts");
|
|
176
|
+
let sourceState = await readFlowState(projectRoot);
|
|
177
|
+
const retroGate = await evaluateRetroGate(projectRoot, sourceState);
|
|
178
|
+
const shipCompleted = sourceState.completedStages.includes("ship");
|
|
179
|
+
const skipRetro = options.skipRetro === true;
|
|
180
|
+
const skipRetroReason = options.skipRetroReason?.trim();
|
|
181
|
+
if (skipRetro && (!skipRetroReason || skipRetroReason.length === 0)) {
|
|
182
|
+
throw new Error("archive --skip-retro requires --retro-reason=<text>.");
|
|
183
|
+
}
|
|
184
|
+
const retroSkippedInCloseout = sourceState.closeout.retroSkipped === true &&
|
|
185
|
+
typeof sourceState.closeout.retroSkipReason === "string" &&
|
|
186
|
+
sourceState.closeout.retroSkipReason.trim().length > 0;
|
|
187
|
+
const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
|
|
188
|
+
const inShipCloseout = sourceState.currentStage === "ship";
|
|
189
|
+
if (inShipCloseout && skipRetro) {
|
|
190
|
+
throw new Error("Archive blocked: --skip-retro is not allowed while current stage is ship. " +
|
|
191
|
+
"Complete closeout to ready_to_archive via /cc-next.");
|
|
192
|
+
}
|
|
193
|
+
if (inShipCloseout && !readyForArchive) {
|
|
194
|
+
throw new Error("Archive blocked: closeout is not ready_to_archive. " +
|
|
195
|
+
"Resume /cc-next until closeout reaches ready_to_archive.");
|
|
196
|
+
}
|
|
197
|
+
if (shipCompleted && !readyForArchive && !skipRetro) {
|
|
198
|
+
throw new Error("Archive blocked: closeout is not ready_to_archive. " +
|
|
199
|
+
"Resume /cc-next until closeout reaches ready_to_archive, " +
|
|
200
|
+
"or run `cclaw archive --skip-retro --retro-reason=<text>` for CLI-only flows.");
|
|
201
|
+
}
|
|
202
|
+
if (retroGate.required && !retroGate.completed && !skipRetro && !retroSkippedInCloseout) {
|
|
203
|
+
throw new Error("Archive blocked: retro gate is required after ship completion. " +
|
|
204
|
+
"Run /cc-next (auto-runs retro) or, for CLI-only flows, re-run `cclaw archive --skip-retro --retro-reason=<text>`.");
|
|
205
|
+
}
|
|
206
|
+
if (retroGate.completed) {
|
|
207
|
+
const completedAt = sourceState.retro.completedAt ?? new Date().toISOString();
|
|
208
|
+
sourceState = {
|
|
209
|
+
...sourceState,
|
|
210
|
+
retro: {
|
|
211
|
+
required: retroGate.required,
|
|
212
|
+
completedAt,
|
|
213
|
+
compoundEntries: retroGate.compoundEntries
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
await writeFlowState(projectRoot, sourceState, { allowReset: true });
|
|
217
|
+
}
|
|
218
|
+
const retroSummary = {
|
|
219
|
+
required: retroGate.required,
|
|
220
|
+
completed: retroGate.completed,
|
|
221
|
+
skipped: skipRetro || retroSkippedInCloseout,
|
|
222
|
+
skipReason: skipRetro
|
|
223
|
+
? skipRetroReason
|
|
224
|
+
: retroSkippedInCloseout
|
|
225
|
+
? sourceState.closeout.retroSkipReason
|
|
226
|
+
: undefined,
|
|
227
|
+
compoundEntries: retroGate.compoundEntries
|
|
252
228
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
229
|
+
await ensureDir(archivePath);
|
|
230
|
+
// Drop an `.archive-in-progress` sentinel immediately so that a crash
|
|
231
|
+
// between the artifact rename and the final manifest write leaves a
|
|
232
|
+
// recoverable marker (doctor surfaces these; re-running archive on an
|
|
233
|
+
// orphan attempts to complete or roll back). The sentinel is removed
|
|
234
|
+
// only after the manifest lands successfully.
|
|
235
|
+
const sentinelPath = path.join(archivePath, ".archive-in-progress");
|
|
236
|
+
const archivedAt = new Date().toISOString();
|
|
237
|
+
await writeFileSafe(sentinelPath, `${JSON.stringify({ archiveId, startedAt: archivedAt, sourceRunId: sourceState.activeRunId }, null, 2)}\n`);
|
|
238
|
+
const stateBeforeReset = sourceState;
|
|
239
|
+
let artifactsMoved = false;
|
|
240
|
+
let stateReset = false;
|
|
241
|
+
try {
|
|
242
|
+
await fs.rename(artifactsDir, archiveArtifactsPath);
|
|
243
|
+
artifactsMoved = true;
|
|
244
|
+
await ensureDir(artifactsDir);
|
|
245
|
+
const archiveStatePath = path.join(archivePath, "state");
|
|
246
|
+
const snapshottedStateFiles = await snapshotStateDirectory(projectRoot, archiveStatePath);
|
|
247
|
+
const resetState = createInitialFlowState();
|
|
248
|
+
await writeFlowState(projectRoot, resetState, { allowReset: true });
|
|
249
|
+
stateReset = true;
|
|
250
|
+
await resetCarryoverStateFiles(projectRoot, resetState.activeRunId);
|
|
251
|
+
const manifest = {
|
|
252
|
+
version: 1,
|
|
253
|
+
archiveId,
|
|
254
|
+
archivedAt,
|
|
255
|
+
featureName: feature,
|
|
256
|
+
activeFeature,
|
|
257
|
+
sourceRunId: sourceState.activeRunId,
|
|
258
|
+
sourceCurrentStage: sourceState.currentStage,
|
|
259
|
+
sourceCompletedStages: sourceState.completedStages,
|
|
260
|
+
snapshottedStateFiles,
|
|
261
|
+
retro: retroSummary
|
|
262
|
+
};
|
|
263
|
+
await writeFileSafe(path.join(archivePath, "archive-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
264
|
+
// Manifest landed — sentinel is no longer needed.
|
|
265
|
+
await fs.unlink(sentinelPath).catch(() => undefined);
|
|
266
|
+
const knowledgeStats = await readKnowledgeStats(projectRoot);
|
|
267
|
+
await syncActiveFeatureSnapshot(projectRoot);
|
|
268
|
+
return {
|
|
269
|
+
archiveId,
|
|
270
|
+
archivePath,
|
|
271
|
+
archivedAt,
|
|
272
|
+
featureName: feature,
|
|
273
|
+
activeFeature,
|
|
274
|
+
resetState,
|
|
275
|
+
snapshottedStateFiles,
|
|
276
|
+
knowledge: knowledgeStats,
|
|
277
|
+
retro: retroSummary
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
// Best-effort rollback: if artifacts were moved but the subsequent
|
|
282
|
+
// steps failed, put artifacts back so the user is not left without
|
|
283
|
+
// a working run. The sentinel is intentionally left behind for
|
|
284
|
+
// inspection; doctor surfaces it.
|
|
285
|
+
if (artifactsMoved) {
|
|
286
|
+
try {
|
|
287
|
+
await fs.rm(artifactsDir, { recursive: true, force: true });
|
|
288
|
+
await fs.rename(archiveArtifactsPath, artifactsDir);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Rollback failed — sentinel + orphaned archive dir will be
|
|
292
|
+
// surfaced by doctor and can be reconciled manually.
|
|
293
|
+
}
|
|
263
294
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
if (stateReset) {
|
|
296
|
+
try {
|
|
297
|
+
await writeFlowState(projectRoot, stateBeforeReset, { allowReset: true });
|
|
298
|
+
await resetCarryoverStateFiles(projectRoot, stateBeforeReset.activeRunId);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// If rollback of state fails, keep sentinel + archive remnants for
|
|
302
|
+
// manual reconciliation.
|
|
303
|
+
}
|
|
267
304
|
}
|
|
305
|
+
throw err;
|
|
268
306
|
}
|
|
269
|
-
|
|
270
|
-
|
|
307
|
+
}, {
|
|
308
|
+
retries: 400,
|
|
309
|
+
retryDelayMs: 25,
|
|
310
|
+
staleAfterMs: 120_000
|
|
311
|
+
});
|
|
271
312
|
}
|
|
272
313
|
const KNOWLEDGE_SOFT_THRESHOLD = 50;
|
|
273
314
|
async function readKnowledgeStats(projectRoot) {
|
|
@@ -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,8 +377,7 @@ 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 });
|