cclaw-cli 0.46.7 → 0.46.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,380 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { COMMAND_FILE_ORDER, RUNTIME_ROOT } from "./constants.js";
4
+ import { canTransition, createInitialCloseoutState, createInitialFlowState, isFlowTrack, skippedStagesForTrack, SHIP_SUBSTATES } from "./flow-state.js";
5
+ import { ensureFeatureSystem, syncActiveFeatureSnapshot } from "./feature-system.js";
6
+ import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
7
+ export class InvalidStageTransitionError extends Error {
8
+ from;
9
+ to;
10
+ constructor(from, to, message) {
11
+ super(message);
12
+ this.from = from;
13
+ this.to = to;
14
+ this.name = "InvalidStageTransitionError";
15
+ }
16
+ }
17
+ const FLOW_STATE_REL_PATH = `${RUNTIME_ROOT}/state/flow-state.json`;
18
+ const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
19
+ const ACTIVE_ARTIFACTS_REL_PATH = `${RUNTIME_ROOT}/artifacts`;
20
+ const FLOW_STAGE_SET = new Set(COMMAND_FILE_ORDER);
21
+ function validateFlowTransition(prev, next) {
22
+ if (prev.activeRunId !== next.activeRunId) {
23
+ // New run — only reset paths may change the runId, but those set allowReset.
24
+ throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `cannot change activeRunId from "${prev.activeRunId}" to "${next.activeRunId}" without allowReset.`);
25
+ }
26
+ for (const completed of prev.completedStages) {
27
+ if (!next.completedStages.includes(completed)) {
28
+ throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `completedStages must be monotonic: stage "${completed}" was previously completed but is missing from the new state.`);
29
+ }
30
+ }
31
+ if (prev.currentStage === next.currentStage) {
32
+ return;
33
+ }
34
+ if (!canTransition(prev.currentStage, next.currentStage)) {
35
+ throw new InvalidStageTransitionError(prev.currentStage, next.currentStage, `no transition rule allows "${prev.currentStage}" -> "${next.currentStage}". Use /cc-next to advance stages or archive the run to reset.`);
36
+ }
37
+ }
38
+ function flowStatePath(projectRoot) {
39
+ return path.join(projectRoot, FLOW_STATE_REL_PATH);
40
+ }
41
+ function flowStateLockPath(projectRoot) {
42
+ return path.join(projectRoot, RUNTIME_ROOT, "state", ".flow-state.lock");
43
+ }
44
+ function runsRoot(projectRoot) {
45
+ return path.join(projectRoot, RUNS_DIR_REL_PATH);
46
+ }
47
+ function activeArtifactsPath(projectRoot) {
48
+ return path.join(projectRoot, ACTIVE_ARTIFACTS_REL_PATH);
49
+ }
50
+ function isFlowStage(value) {
51
+ return typeof value === "string" && FLOW_STAGE_SET.has(value);
52
+ }
53
+ function sanitizeStringArray(value) {
54
+ if (!Array.isArray(value)) {
55
+ return [];
56
+ }
57
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
58
+ }
59
+ function sanitizeCompletedStages(value) {
60
+ if (!Array.isArray(value)) {
61
+ return [];
62
+ }
63
+ const unique = new Set();
64
+ const stages = [];
65
+ for (const item of value) {
66
+ if (isFlowStage(item) && !unique.has(item)) {
67
+ unique.add(item);
68
+ stages.push(item);
69
+ }
70
+ }
71
+ return stages;
72
+ }
73
+ function sanitizeGuardEvidence(value) {
74
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
75
+ return {};
76
+ }
77
+ const next = {};
78
+ for (const [key, raw] of Object.entries(value)) {
79
+ if (typeof raw === "string") {
80
+ next[key] = raw;
81
+ }
82
+ }
83
+ return next;
84
+ }
85
+ function sanitizeStageGateCatalog(value, fallback) {
86
+ const uniqueStrings = (items) => [...new Set(items)];
87
+ const next = {};
88
+ for (const stage of COMMAND_FILE_ORDER) {
89
+ const base = fallback[stage];
90
+ next[stage] = {
91
+ required: [...base.required],
92
+ recommended: [...base.recommended],
93
+ conditional: [...base.conditional],
94
+ triggered: [...base.triggered],
95
+ passed: [...base.passed],
96
+ blocked: [...base.blocked]
97
+ };
98
+ }
99
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
100
+ return next;
101
+ }
102
+ const rawCatalog = value;
103
+ for (const stage of COMMAND_FILE_ORDER) {
104
+ const rawStage = rawCatalog[stage];
105
+ if (!rawStage || typeof rawStage !== "object" || Array.isArray(rawStage)) {
106
+ continue;
107
+ }
108
+ const typed = rawStage;
109
+ const stageState = next[stage];
110
+ const allowedGateIds = new Set([
111
+ ...stageState.required,
112
+ ...stageState.recommended,
113
+ ...stageState.conditional
114
+ ]);
115
+ const conditionalGateIds = new Set(stageState.conditional);
116
+ const passed = sanitizeStringArray(typed.passed).filter((gate) => allowedGateIds.has(gate));
117
+ const blocked = sanitizeStringArray(typed.blocked).filter((gate) => allowedGateIds.has(gate));
118
+ const triggeredFromState = sanitizeStringArray(typed.triggered).filter((gate) => conditionalGateIds.has(gate));
119
+ const touchedConditionals = [...passed, ...blocked].filter((gate) => conditionalGateIds.has(gate));
120
+ next[stage] = {
121
+ required: [...stageState.required],
122
+ recommended: [...stageState.recommended],
123
+ conditional: [...stageState.conditional],
124
+ triggered: uniqueStrings([...triggeredFromState, ...touchedConditionals]),
125
+ passed,
126
+ blocked
127
+ };
128
+ }
129
+ return next;
130
+ }
131
+ function coerceTrack(value) {
132
+ return isFlowTrack(value) ? value : "standard";
133
+ }
134
+ function sanitizeSkippedStages(value, track) {
135
+ const trackDefault = skippedStagesForTrack(track);
136
+ if (!Array.isArray(value)) {
137
+ return trackDefault;
138
+ }
139
+ const seen = new Set();
140
+ const out = [];
141
+ for (const raw of value) {
142
+ if (isFlowStage(raw) && !seen.has(raw)) {
143
+ seen.add(raw);
144
+ out.push(raw);
145
+ }
146
+ }
147
+ return out.length > 0 ? out : trackDefault;
148
+ }
149
+ function sanitizeStaleStages(value) {
150
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
151
+ return {};
152
+ }
153
+ const out = {};
154
+ for (const [stage, raw] of Object.entries(value)) {
155
+ if (!isFlowStage(stage))
156
+ continue;
157
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
158
+ continue;
159
+ const typed = raw;
160
+ const rewindId = typeof typed.rewindId === "string" ? typed.rewindId : "";
161
+ const reason = typeof typed.reason === "string" ? typed.reason : "";
162
+ const markedAt = typeof typed.markedAt === "string" ? typed.markedAt : "";
163
+ const acknowledgedAt = typeof typed.acknowledgedAt === "string" ? typed.acknowledgedAt : undefined;
164
+ if (!rewindId || !reason || !markedAt) {
165
+ continue;
166
+ }
167
+ out[stage] = {
168
+ rewindId,
169
+ reason,
170
+ markedAt,
171
+ acknowledgedAt
172
+ };
173
+ }
174
+ return out;
175
+ }
176
+ function sanitizeRewinds(value) {
177
+ if (!Array.isArray(value)) {
178
+ return [];
179
+ }
180
+ const out = [];
181
+ for (const raw of value) {
182
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
183
+ continue;
184
+ }
185
+ const typed = raw;
186
+ if (typeof typed.id !== "string" ||
187
+ !isFlowStage(typed.fromStage) ||
188
+ !isFlowStage(typed.toStage) ||
189
+ typeof typed.reason !== "string" ||
190
+ typeof typed.timestamp !== "string") {
191
+ continue;
192
+ }
193
+ const invalidatedStages = Array.isArray(typed.invalidatedStages)
194
+ ? typed.invalidatedStages.filter((stage) => isFlowStage(stage))
195
+ : [];
196
+ out.push({
197
+ id: typed.id,
198
+ fromStage: typed.fromStage,
199
+ toStage: typed.toStage,
200
+ reason: typed.reason,
201
+ timestamp: typed.timestamp,
202
+ invalidatedStages
203
+ });
204
+ }
205
+ return out;
206
+ }
207
+ function sanitizeRetroState(value) {
208
+ const fallback = {
209
+ required: false,
210
+ completedAt: undefined,
211
+ compoundEntries: 0
212
+ };
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
214
+ return fallback;
215
+ }
216
+ const typed = value;
217
+ const required = typeof typed.required === "boolean" ? typed.required : false;
218
+ const completedAt = typeof typed.completedAt === "string" ? typed.completedAt : undefined;
219
+ const compoundEntriesRaw = typed.compoundEntries;
220
+ const compoundEntries = typeof compoundEntriesRaw === "number" && Number.isFinite(compoundEntriesRaw) && compoundEntriesRaw >= 0
221
+ ? Math.floor(compoundEntriesRaw)
222
+ : 0;
223
+ return {
224
+ required,
225
+ completedAt,
226
+ compoundEntries
227
+ };
228
+ }
229
+ function isShipSubstate(value) {
230
+ return typeof value === "string" && SHIP_SUBSTATES.includes(value);
231
+ }
232
+ function sanitizeCloseoutState(value) {
233
+ const fallback = createInitialCloseoutState();
234
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
235
+ return fallback;
236
+ }
237
+ const typed = value;
238
+ const shipSubstate = isShipSubstate(typed.shipSubstate) ? typed.shipSubstate : fallback.shipSubstate;
239
+ const retroDraftedAt = typeof typed.retroDraftedAt === "string" ? typed.retroDraftedAt : undefined;
240
+ const retroAcceptedAt = typeof typed.retroAcceptedAt === "string" ? typed.retroAcceptedAt : undefined;
241
+ const retroSkipped = typeof typed.retroSkipped === "boolean" ? typed.retroSkipped : undefined;
242
+ const retroSkipReason = typeof typed.retroSkipReason === "string" ? typed.retroSkipReason : undefined;
243
+ const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
244
+ const compoundSkipped = typeof typed.compoundSkipped === "boolean" ? typed.compoundSkipped : undefined;
245
+ const promotedRaw = typed.compoundPromoted;
246
+ const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
247
+ ? Math.floor(promotedRaw)
248
+ : 0;
249
+ return {
250
+ shipSubstate,
251
+ retroDraftedAt,
252
+ retroAcceptedAt,
253
+ retroSkipped,
254
+ retroSkipReason,
255
+ compoundCompletedAt,
256
+ compoundSkipped,
257
+ compoundPromoted
258
+ };
259
+ }
260
+ function coerceFlowState(parsed) {
261
+ const track = coerceTrack(parsed.track);
262
+ const next = createInitialFlowState("active", track);
263
+ const activeRunIdRaw = parsed.activeRunId;
264
+ const activeRunId = typeof activeRunIdRaw === "string" && activeRunIdRaw.trim().length > 0
265
+ ? activeRunIdRaw.trim()
266
+ : next.activeRunId;
267
+ return {
268
+ activeRunId,
269
+ currentStage: isFlowStage(parsed.currentStage) ? parsed.currentStage : next.currentStage,
270
+ completedStages: sanitizeCompletedStages(parsed.completedStages),
271
+ guardEvidence: sanitizeGuardEvidence(parsed.guardEvidence),
272
+ stageGateCatalog: sanitizeStageGateCatalog(parsed.stageGateCatalog, next.stageGateCatalog),
273
+ track,
274
+ skippedStages: sanitizeSkippedStages(parsed.skippedStages, track),
275
+ staleStages: sanitizeStaleStages(parsed.staleStages),
276
+ rewinds: sanitizeRewinds(parsed.rewinds),
277
+ retro: sanitizeRetroState(parsed.retro),
278
+ closeout: sanitizeCloseoutState(parsed.closeout)
279
+ };
280
+ }
281
+ export class CorruptFlowStateError extends Error {
282
+ statePath;
283
+ quarantinedPath;
284
+ constructor(statePath, quarantinedPath, cause) {
285
+ super(`Corrupt flow-state.json detected at ${statePath}. ` +
286
+ `Quarantined to ${quarantinedPath}. ` +
287
+ `Inspect the quarantined file, reconcile by hand, then re-run your command ` +
288
+ `or delete ${statePath} to start over. ` +
289
+ `Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`);
290
+ this.name = "CorruptFlowStateError";
291
+ this.statePath = statePath;
292
+ this.quarantinedPath = quarantinedPath;
293
+ if (cause instanceof Error) {
294
+ this.cause = cause;
295
+ }
296
+ }
297
+ }
298
+ function quarantineTimestamp(date = new Date()) {
299
+ return date.toISOString().replace(/[:.]/gu, "-");
300
+ }
301
+ async function quarantineCorruptState(statePath, cause) {
302
+ const quarantinedPath = `${statePath}.corrupt-${quarantineTimestamp()}.json`;
303
+ try {
304
+ await fs.rename(statePath, quarantinedPath);
305
+ }
306
+ catch (renameErr) {
307
+ try {
308
+ const raw = await fs.readFile(statePath, "utf8");
309
+ await fs.writeFile(quarantinedPath, raw, "utf8");
310
+ await fs.unlink(statePath).catch(() => undefined);
311
+ }
312
+ catch {
313
+ throw new CorruptFlowStateError(statePath, quarantinedPath, renameErr);
314
+ }
315
+ }
316
+ throw new CorruptFlowStateError(statePath, quarantinedPath, cause);
317
+ }
318
+ export async function readFlowState(projectRoot) {
319
+ await ensureFeatureSystem(projectRoot);
320
+ const statePath = flowStatePath(projectRoot);
321
+ if (!(await exists(statePath))) {
322
+ return createInitialFlowState();
323
+ }
324
+ let raw;
325
+ try {
326
+ raw = await fs.readFile(statePath, "utf8");
327
+ }
328
+ catch (readErr) {
329
+ throw new CorruptFlowStateError(statePath, statePath, readErr);
330
+ }
331
+ let parsed;
332
+ try {
333
+ parsed = JSON.parse(raw);
334
+ }
335
+ catch (parseErr) {
336
+ await quarantineCorruptState(statePath, parseErr);
337
+ }
338
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
339
+ await quarantineCorruptState(statePath, new Error("flow-state.json did not deserialize to a JSON object"));
340
+ }
341
+ return coerceFlowState(parsed);
342
+ }
343
+ export async function writeFlowState(projectRoot, state, options = {}) {
344
+ await ensureFeatureSystem(projectRoot);
345
+ await withDirectoryLock(flowStateLockPath(projectRoot), async () => {
346
+ const statePath = flowStatePath(projectRoot);
347
+ if (!options.allowReset && (await exists(statePath))) {
348
+ try {
349
+ const raw = await fs.readFile(statePath, "utf8");
350
+ const parsed = JSON.parse(raw);
351
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
352
+ const prev = coerceFlowState(parsed);
353
+ validateFlowTransition(prev, state);
354
+ }
355
+ }
356
+ catch (err) {
357
+ if (err instanceof InvalidStageTransitionError) {
358
+ throw err;
359
+ }
360
+ // A corrupt prior file is surfaced by readFlowState elsewhere; don't
361
+ // block a legitimate write attempt on parse errors here.
362
+ }
363
+ }
364
+ const safe = coerceFlowState({ ...state });
365
+ await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`);
366
+ });
367
+ await syncActiveFeatureSnapshot(projectRoot);
368
+ }
369
+ export async function ensureRunSystem(projectRoot, _options = {}) {
370
+ await ensureFeatureSystem(projectRoot);
371
+ await ensureDir(runsRoot(projectRoot));
372
+ await ensureDir(activeArtifactsPath(projectRoot));
373
+ const statePath = flowStatePath(projectRoot);
374
+ const state = await readFlowState(projectRoot);
375
+ if (!(await exists(statePath))) {
376
+ await writeFlowState(projectRoot, state, { allowReset: true });
377
+ }
378
+ await syncActiveFeatureSnapshot(projectRoot);
379
+ return state;
380
+ }
package/dist/runs.d.ts CHANGED
@@ -1,79 +1,2 @@
1
- import { type FlowState } from "./flow-state.js";
2
- import type { FlowStage } from "./types.js";
3
- export declare class InvalidStageTransitionError extends Error {
4
- readonly from: FlowStage;
5
- readonly to: FlowStage;
6
- constructor(from: FlowStage, to: FlowStage, message: string);
7
- }
8
- export interface WriteFlowStateOptions {
9
- /**
10
- * When true, skip prior-state validation. Used for run archival, initial
11
- * bootstrap, or explicit recovery; never set from normal stage handlers.
12
- */
13
- allowReset?: boolean;
14
- }
15
- export interface CclawRunMeta {
16
- id: string;
17
- title: string;
18
- createdAt: string;
19
- }
20
- export interface ArchiveRunResult {
21
- archiveId: string;
22
- archivePath: string;
23
- archivedAt: string;
24
- featureName: string;
25
- activeFeature: string;
26
- resetState: FlowState;
27
- snapshottedStateFiles: string[];
28
- /** Knowledge curation hint: total active entries + soft threshold (50). */
29
- knowledge: {
30
- activeEntryCount: number;
31
- softThreshold: number;
32
- overThreshold: boolean;
33
- knowledgePath: string;
34
- };
35
- retro: {
36
- required: boolean;
37
- completed: boolean;
38
- skipped: boolean;
39
- skipReason?: string;
40
- compoundEntries: number;
41
- };
42
- }
43
- export interface ArchiveManifest {
44
- version: 1;
45
- archiveId: string;
46
- archivedAt: string;
47
- featureName: string;
48
- activeFeature: string;
49
- sourceRunId: string;
50
- sourceCurrentStage: FlowStage;
51
- sourceCompletedStages: FlowStage[];
52
- snapshottedStateFiles: string[];
53
- retro: ArchiveRunResult["retro"];
54
- }
55
- interface EnsureRunSystemOptions {
56
- createIfMissing?: boolean;
57
- }
58
- export interface ArchiveRunOptions {
59
- skipRetro?: boolean;
60
- skipRetroReason?: string;
61
- }
62
- export declare class CorruptFlowStateError extends Error {
63
- readonly statePath: string;
64
- readonly quarantinedPath: string;
65
- constructor(statePath: string, quarantinedPath: string, cause: unknown);
66
- }
67
- export declare function readFlowState(projectRoot: string): Promise<FlowState>;
68
- export declare function writeFlowState(projectRoot: string, state: FlowState, options?: WriteFlowStateOptions): Promise<void>;
69
- export declare function ensureRunSystem(projectRoot: string, _options?: EnsureRunSystemOptions): Promise<FlowState>;
70
- export declare function listRuns(projectRoot: string): Promise<CclawRunMeta[]>;
71
- export declare function archiveRun(projectRoot: string, featureName?: string, options?: ArchiveRunOptions): Promise<ArchiveRunResult>;
72
- /**
73
- * Counts entries in the canonical JSONL knowledge store. An "active" entry is one
74
- * non-empty line that parses as JSON with the required `type` field belonging to the
75
- * allowed set. Malformed lines are ignored (not counted) but do not throw so that a
76
- * hand-edited file cannot break doctor/archive flows.
77
- */
78
- export declare function countActiveKnowledgeEntries(text: string): number;
79
- export {};
1
+ export { CorruptFlowStateError, InvalidStageTransitionError, type WriteFlowStateOptions, ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
2
+ export { archiveRun, countActiveKnowledgeEntries, listRuns, type ArchiveManifest, type ArchiveRunOptions, type ArchiveRunResult, type CclawRunMeta } from "./run-archive.js";