@delegance/claude-autopilot 5.2.2 → 6.2.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/CHANGELOG.md +1027 -1
- package/README.md +104 -17
- package/dist/src/adapters/council/claude.js +2 -1
- package/dist/src/adapters/council/openai.js +14 -7
- package/dist/src/adapters/deploy/_http.d.ts +43 -0
- package/dist/src/adapters/deploy/_http.js +99 -0
- package/dist/src/adapters/deploy/fly.d.ts +206 -0
- package/dist/src/adapters/deploy/fly.js +696 -0
- package/dist/src/adapters/deploy/generic.d.ts +39 -0
- package/dist/src/adapters/deploy/generic.js +98 -0
- package/dist/src/adapters/deploy/index.d.ts +15 -0
- package/dist/src/adapters/deploy/index.js +78 -0
- package/dist/src/adapters/deploy/render.d.ts +181 -0
- package/dist/src/adapters/deploy/render.js +550 -0
- package/dist/src/adapters/deploy/types.d.ts +221 -0
- package/dist/src/adapters/deploy/types.js +15 -0
- package/dist/src/adapters/deploy/vercel.d.ts +143 -0
- package/dist/src/adapters/deploy/vercel.js +426 -0
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/claude.js +2 -1
- package/dist/src/adapters/review-engine/codex.js +12 -8
- package/dist/src/adapters/review-engine/gemini.js +2 -1
- package/dist/src/adapters/review-engine/openai-compatible.js +2 -1
- package/dist/src/adapters/sdk-loader.d.ts +15 -0
- package/dist/src/adapters/sdk-loader.js +77 -0
- package/dist/src/cli/autopilot.d.ts +71 -0
- package/dist/src/cli/autopilot.js +735 -0
- package/dist/src/cli/brainstorm.d.ts +23 -0
- package/dist/src/cli/brainstorm.js +131 -0
- package/dist/src/cli/costs.d.ts +15 -1
- package/dist/src/cli/costs.js +99 -10
- package/dist/src/cli/deploy.d.ts +71 -0
- package/dist/src/cli/deploy.js +539 -0
- package/dist/src/cli/fix.d.ts +18 -0
- package/dist/src/cli/fix.js +105 -11
- package/dist/src/cli/help-text.d.ts +52 -0
- package/dist/src/cli/help-text.js +400 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.js +784 -222
- package/dist/src/cli/json-envelope.d.ts +187 -0
- package/dist/src/cli/json-envelope.js +270 -0
- package/dist/src/cli/json-mode.d.ts +33 -0
- package/dist/src/cli/json-mode.js +201 -0
- package/dist/src/cli/migrate.d.ts +111 -0
- package/dist/src/cli/migrate.js +305 -0
- package/dist/src/cli/plan.d.ts +81 -0
- package/dist/src/cli/plan.js +149 -0
- package/dist/src/cli/pr.d.ts +106 -0
- package/dist/src/cli/pr.js +191 -19
- package/dist/src/cli/preflight.js +102 -1
- package/dist/src/cli/review.d.ts +27 -0
- package/dist/src/cli/review.js +126 -0
- package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
- package/dist/src/cli/runs-watch-renderer.js +275 -0
- package/dist/src/cli/runs-watch.d.ts +41 -0
- package/dist/src/cli/runs-watch.js +395 -0
- package/dist/src/cli/runs.d.ts +122 -0
- package/dist/src/cli/runs.js +902 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/spec.d.ts +66 -0
- package/dist/src/cli/spec.js +132 -0
- package/dist/src/cli/validate.d.ts +29 -0
- package/dist/src/cli/validate.js +131 -0
- package/dist/src/core/config/schema.d.ts +43 -0
- package/dist/src/core/config/schema.js +25 -0
- package/dist/src/core/config/types.d.ts +17 -0
- package/dist/src/core/council/runner.d.ts +10 -1
- package/dist/src/core/council/runner.js +25 -3
- package/dist/src/core/council/types.d.ts +7 -0
- package/dist/src/core/errors.d.ts +1 -1
- package/dist/src/core/errors.js +12 -0
- package/dist/src/core/logging/redaction.d.ts +13 -0
- package/dist/src/core/logging/redaction.js +20 -0
- package/dist/src/core/migrate/detector-rules.js +6 -0
- package/dist/src/core/migrate/schema-validator.js +22 -1
- package/dist/src/core/phases/static-rules.d.ts +5 -1
- package/dist/src/core/phases/static-rules.js +2 -5
- package/dist/src/core/run-state/budget.d.ts +88 -0
- package/dist/src/core/run-state/budget.js +141 -0
- package/dist/src/core/run-state/cli-internal.d.ts +21 -0
- package/dist/src/core/run-state/cli-internal.js +174 -0
- package/dist/src/core/run-state/events.d.ts +59 -0
- package/dist/src/core/run-state/events.js +504 -0
- package/dist/src/core/run-state/lock.d.ts +61 -0
- package/dist/src/core/run-state/lock.js +206 -0
- package/dist/src/core/run-state/phase-context.d.ts +60 -0
- package/dist/src/core/run-state/phase-context.js +108 -0
- package/dist/src/core/run-state/phase-registry.d.ts +137 -0
- package/dist/src/core/run-state/phase-registry.js +162 -0
- package/dist/src/core/run-state/phase-runner.d.ts +80 -0
- package/dist/src/core/run-state/phase-runner.js +447 -0
- package/dist/src/core/run-state/provider-readback.d.ts +130 -0
- package/dist/src/core/run-state/provider-readback.js +426 -0
- package/dist/src/core/run-state/replay-decision.d.ts +69 -0
- package/dist/src/core/run-state/replay-decision.js +144 -0
- package/dist/src/core/run-state/resolve-engine.d.ts +100 -0
- package/dist/src/core/run-state/resolve-engine.js +190 -0
- package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
- package/dist/src/core/run-state/resume-preflight.js +116 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +73 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +186 -0
- package/dist/src/core/run-state/runs.d.ts +57 -0
- package/dist/src/core/run-state/runs.js +288 -0
- package/dist/src/core/run-state/snapshot.d.ts +14 -0
- package/dist/src/core/run-state/snapshot.js +114 -0
- package/dist/src/core/run-state/state.d.ts +40 -0
- package/dist/src/core/run-state/state.js +164 -0
- package/dist/src/core/run-state/types.d.ts +278 -0
- package/dist/src/core/run-state/types.js +13 -0
- package/dist/src/core/run-state/ulid.d.ts +11 -0
- package/dist/src/core/run-state/ulid.js +95 -0
- package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
- package/dist/src/core/schema-alignment/extractor/index.js +2 -2
- package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
- package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
- package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
- package/dist/src/core/schema-alignment/git-history.js +53 -0
- package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
- package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
- package/package.json +9 -5
- package/scripts/autoregress.ts +3 -2
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- package/skills/migrate/SKILL.md +193 -47
- package/skills/simplify-ui/SKILL.md +103 -0
- package/skills/ui/SKILL.md +117 -0
- package/skills/ui-ux-pro-max/SKILL.md +90 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/core/run-state/run-phase-with-lifecycle.ts
|
|
2
|
+
//
|
|
3
|
+
// v6.0.6 — extract the lifecycle wrapper that's been duplicated across
|
|
4
|
+
// every wrapped CLI verb (`scan`, `costs`, `fix`, `brainstorm`, `spec`,
|
|
5
|
+
// `plan`, `review`, `validate`). The pattern is mechanical:
|
|
6
|
+
//
|
|
7
|
+
// 1. If engine-off → run the legacy phase body via `runEngineOff()` and
|
|
8
|
+
// return its result.
|
|
9
|
+
// 2. If engine-on → createRun → optional run.warning for invalid env →
|
|
10
|
+
// runPhase → emit run.complete (success or failed) → refresh state.json
|
|
11
|
+
// → release lock (best effort, in finally).
|
|
12
|
+
//
|
|
13
|
+
// This helper sits ON TOP of `runPhase()` from `phase-runner.ts` — it does
|
|
14
|
+
// not replace it. Callers continue to define their own `RunPhase<I, O>` with
|
|
15
|
+
// per-phase `idempotent` / `hasSideEffects` / `run` and pass it in.
|
|
16
|
+
//
|
|
17
|
+
// Why now: with 8 of 10 phases wrapped (the v6.0.5 milestone), the pattern
|
|
18
|
+
// is fully evidenced. The remaining 3 phases (`implement`, `migrate`, `pr`)
|
|
19
|
+
// are side-effecting and need externalRefs — those will inform a v6.0.7+
|
|
20
|
+
// extension to this helper but won't change its core shape. Doing the
|
|
21
|
+
// extraction now means those 3 wraps build against the helper instead of
|
|
22
|
+
// re-introducing the boilerplate.
|
|
23
|
+
//
|
|
24
|
+
// What this helper does NOT do:
|
|
25
|
+
// - Print success banners — rendering stays in the caller.
|
|
26
|
+
// - Decide engine-off behavior — that's `runEngineOff`, supplied by the
|
|
27
|
+
// caller (typically a thin closure over the phase body).
|
|
28
|
+
// - Plumb externalRefs / readback — the underlying `runPhase()` already
|
|
29
|
+
// handles those. This helper just owns the run-level lifecycle events.
|
|
30
|
+
//
|
|
31
|
+
// Future extension (v6.0.7+): `implement` / `migrate` / `pr` need
|
|
32
|
+
// externalRef ledger entries (`git-remote-push`, `migration-version`,
|
|
33
|
+
// `github-pr`). The helper's `phase.run` already receives `ctx` so
|
|
34
|
+
// `ctx.emitExternalRef()` works without changes here. If a future PR needs
|
|
35
|
+
// to fan-in run-wide externalRefs from multiple phases (multi-phase
|
|
36
|
+
// pipelines, e.g. autopilot orchestrator), the signature can grow a
|
|
37
|
+
// `phases: RunPhase[]` overload — but the single-phase shape stays identical.
|
|
38
|
+
import { createRun } from "./runs.js";
|
|
39
|
+
import { runPhase } from "./phase-runner.js";
|
|
40
|
+
import { appendEvent, replayState } from "./events.js";
|
|
41
|
+
import { writeStateSnapshot } from "./state.js";
|
|
42
|
+
import { resolveEngineEnabled, emitEngineOffDeprecationWarning, } from "./resolve-engine.js";
|
|
43
|
+
// Inline ANSI codes — same shape every wrapped verb uses. Kept here so the
|
|
44
|
+
// helper doesn't depend on a verb-local `fmt`. The error message format
|
|
45
|
+
// (`[<phase>] engine: phase failed — <msg>` + dim inspect hint) is
|
|
46
|
+
// byte-for-byte identical to what every wrapped phase printed pre-extract.
|
|
47
|
+
const ANSI_RESET = '\x1b[0m';
|
|
48
|
+
const ANSI_DIM = '\x1b[2m';
|
|
49
|
+
const ANSI_RED = '\x1b[31m';
|
|
50
|
+
/** Drive a single-phase engine run with full lifecycle instrumentation,
|
|
51
|
+
* OR fall through to the legacy `runEngineOff` callback when the engine
|
|
52
|
+
* is disabled by config / CLI / env precedence.
|
|
53
|
+
*
|
|
54
|
+
* Engine-on lifecycle (in order):
|
|
55
|
+
* createRun → (optional run.warning for invalid env) → runPhase →
|
|
56
|
+
* run.complete (success or failed) → refresh state.json → release lock.
|
|
57
|
+
*
|
|
58
|
+
* On phase failure the helper:
|
|
59
|
+
* 1. Emits `run.complete` with `status: 'failed'`.
|
|
60
|
+
* 2. Refreshes state.json from the replayed events.
|
|
61
|
+
* 3. Prints the legacy `[<phase>] engine: phase failed — <msg>` banner
|
|
62
|
+
* to stderr (byte-for-byte identical to the inline pattern that
|
|
63
|
+
* lived in 8 of 8 wrapped verbs pre-v6.0.6).
|
|
64
|
+
* 4. Releases the lock and re-throws so the caller can return its
|
|
65
|
+
* legacy non-zero exit code.
|
|
66
|
+
*
|
|
67
|
+
* The lock release in `finally` is best-effort. `release()` is idempotent
|
|
68
|
+
* (the runs lock module accepts double-release without throwing), so the
|
|
69
|
+
* catch block does not need to release the lock itself — `finally` covers
|
|
70
|
+
* both the success and failure exit paths. */
|
|
71
|
+
export async function runPhaseWithLifecycle(opts) {
|
|
72
|
+
const { cwd, phase, input, config, cliEngine, envEngine, runEngineOff } = opts;
|
|
73
|
+
// Resolve engine via the canonical precedence (CLI > env > config >
|
|
74
|
+
// built-in default). The resolver is pure — same inputs always produce
|
|
75
|
+
// the same decision. We DO consult the loaded config's `engine.enabled`
|
|
76
|
+
// here so the helper's caller doesn't have to repeat the conditional
|
|
77
|
+
// spread that every wrapped verb wrote inline.
|
|
78
|
+
const engineResolved = resolveEngineEnabled({
|
|
79
|
+
...(cliEngine !== undefined ? { cliEngine } : {}),
|
|
80
|
+
...(envEngine !== undefined ? { envValue: envEngine } : {}),
|
|
81
|
+
...(typeof config.engine?.enabled === 'boolean'
|
|
82
|
+
? { configEnabled: config.engine.enabled }
|
|
83
|
+
: {}),
|
|
84
|
+
});
|
|
85
|
+
if (!engineResolved.enabled) {
|
|
86
|
+
// Engine off — call the caller's legacy path. No run dir, no events,
|
|
87
|
+
// no lifecycle work. Behavior is byte-for-byte identical to pre-engine
|
|
88
|
+
// versions of the verb. v6.1+ emits a one-line stderr deprecation
|
|
89
|
+
// notice when the user explicitly opted out (CLI / env / config); the
|
|
90
|
+
// v6.1 default is `enabled: true`, so a `'default'` source can't reach
|
|
91
|
+
// this branch and the deprecation helper no-ops on the `enabled: true`
|
|
92
|
+
// path. v7 removes the opt-out entirely.
|
|
93
|
+
emitEngineOffDeprecationWarning(engineResolved);
|
|
94
|
+
const output = await runEngineOff();
|
|
95
|
+
return { output, runId: null, runDir: null };
|
|
96
|
+
}
|
|
97
|
+
// Engine on — full lifecycle. Mirrors the pre-v6.0.6 inline shape that
|
|
98
|
+
// every wrapped verb duplicated.
|
|
99
|
+
const created = await createRun({
|
|
100
|
+
cwd,
|
|
101
|
+
phases: [phase.name],
|
|
102
|
+
config: {
|
|
103
|
+
engine: { enabled: true, source: engineResolved.source },
|
|
104
|
+
...(engineResolved.invalidEnvValue !== undefined
|
|
105
|
+
? { invalidEnvValue: engineResolved.invalidEnvValue }
|
|
106
|
+
: {}),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
if (engineResolved.invalidEnvValue !== undefined) {
|
|
110
|
+
// Surface the invalid env value as a typed warning so observers
|
|
111
|
+
// (`runs show <id> --events`) can attribute the fallthrough.
|
|
112
|
+
appendEvent(created.runDir, {
|
|
113
|
+
event: 'run.warning',
|
|
114
|
+
message: `invalid CLAUDE_AUTOPILOT_ENGINE=${JSON.stringify(engineResolved.invalidEnvValue)} ignored`,
|
|
115
|
+
details: { resolution: engineResolved },
|
|
116
|
+
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
117
|
+
}
|
|
118
|
+
const runStartedAt = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
const output = await runPhase(phase, input, {
|
|
121
|
+
runDir: created.runDir,
|
|
122
|
+
runId: created.runId,
|
|
123
|
+
writerId: created.lock.writerId,
|
|
124
|
+
phaseIdx: 0,
|
|
125
|
+
});
|
|
126
|
+
// Final lifecycle event — run.complete. The runner doesn't emit this
|
|
127
|
+
// on its own; it's the caller's responsibility (multi-phase pipelines
|
|
128
|
+
// emit it after the LAST phase, single-phase wrappers like this emit
|
|
129
|
+
// after the only phase). Total cost falls back to 0 when the phase
|
|
130
|
+
// doesn't expose a `costUSD` field on its output (read-only verbs
|
|
131
|
+
// don't track cost; scan does).
|
|
132
|
+
const totalCostUSD = extractCostUSD(output);
|
|
133
|
+
appendEvent(created.runDir, {
|
|
134
|
+
event: 'run.complete',
|
|
135
|
+
status: 'success',
|
|
136
|
+
totalCostUSD,
|
|
137
|
+
durationMs: Date.now() - runStartedAt,
|
|
138
|
+
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
139
|
+
// Refresh state.json from the replayed events. The events.ndjson is
|
|
140
|
+
// the source of truth; state.json is a derived snapshot that we MUST
|
|
141
|
+
// rewrite after run.complete so `runs show` / `runs list` reflect the
|
|
142
|
+
// terminal status without needing to replay on every read.
|
|
143
|
+
writeStateSnapshot(created.runDir, replayState(created.runDir));
|
|
144
|
+
return { output, runId: created.runId, runDir: created.runDir };
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
// Engine-on failure — write run.complete with failed status, refresh
|
|
148
|
+
// state.json, print the legacy banner to stderr, then re-throw so the
|
|
149
|
+
// caller can return its legacy non-zero exit code. (Lock release
|
|
150
|
+
// happens in `finally` regardless of success / failure path.)
|
|
151
|
+
appendEvent(created.runDir, {
|
|
152
|
+
event: 'run.complete',
|
|
153
|
+
status: 'failed',
|
|
154
|
+
totalCostUSD: 0,
|
|
155
|
+
durationMs: Date.now() - runStartedAt,
|
|
156
|
+
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
157
|
+
writeStateSnapshot(created.runDir, replayState(created.runDir));
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
process.stderr.write(`${ANSI_RED}[${phase.name}] engine: phase failed — ${message}${ANSI_RESET}\n`);
|
|
160
|
+
process.stderr.write(`${ANSI_DIM} inspect: claude-autopilot runs show ${created.runId} --events${ANSI_RESET}\n`);
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
// Best-effort lock release. The lock module's `release()` is
|
|
165
|
+
// idempotent; if the catch path already released (it doesn't, but a
|
|
166
|
+
// future change might), this is a no-op. Wrapping the await in
|
|
167
|
+
// `.catch(() => {})` ensures a release error never masks the original
|
|
168
|
+
// throw — the spec calls this out explicitly.
|
|
169
|
+
await created.lock.release().catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Extract `costUSD` from a phase output if present, else 0. JSON-style
|
|
173
|
+
* duck-typing: we accept any output that exposes a numeric `costUSD`
|
|
174
|
+
* field. Today only `scan` exposes one; the other 7 wrapped verbs
|
|
175
|
+
* return outputs without a cost field, which means `extractCostUSD`
|
|
176
|
+
* returns 0 — byte-for-byte matching the inline `totalCostUSD: 0` they
|
|
177
|
+
* used pre-v6.0.6. */
|
|
178
|
+
function extractCostUSD(output) {
|
|
179
|
+
if (output !== null && typeof output === 'object' && 'costUSD' in output) {
|
|
180
|
+
const v = output.costUSD;
|
|
181
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
182
|
+
return v;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=run-phase-with-lifecycle.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type RunLockHandle } from './lock.ts';
|
|
2
|
+
import { type RunIndex, type RunIndexEntry, type RunState } from './types.ts';
|
|
3
|
+
export declare function runsRoot(cwd: string): string;
|
|
4
|
+
export declare function indexPath(cwd: string): string;
|
|
5
|
+
export declare function runDirFor(cwd: string, runId: string): string;
|
|
6
|
+
export interface CreateRunOptions {
|
|
7
|
+
cwd: string;
|
|
8
|
+
/** Phase names in the order they will execute. */
|
|
9
|
+
phases: string[];
|
|
10
|
+
/** Snapshot of the relevant guardrail.config.yaml fields. Free-form. */
|
|
11
|
+
config?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateRunResult {
|
|
14
|
+
runId: string;
|
|
15
|
+
runDir: string;
|
|
16
|
+
state: RunState;
|
|
17
|
+
/** Lock handle. Caller MUST `release()` on shutdown. */
|
|
18
|
+
lock: RunLockHandle;
|
|
19
|
+
}
|
|
20
|
+
/** Create a fresh run directory, acquire its advisory lock, write the
|
|
21
|
+
* initial state.json, and emit the `run.start` event.
|
|
22
|
+
*
|
|
23
|
+
* Throws GuardrailError(lock_held) if a stale lock exists for the freshly-
|
|
24
|
+
* generated runId — extremely unlikely (ULIDs are unique) but possible if
|
|
25
|
+
* two parallel invocations on the same OS clock collide on a leftover dir
|
|
26
|
+
* on disk. Caller can simply retry. */
|
|
27
|
+
export declare function createRun(opts: CreateRunOptions): Promise<CreateRunResult>;
|
|
28
|
+
/** Rebuild index.json from each run dir's state.json (or replayed state if
|
|
29
|
+
* the snapshot is missing / corrupt). Newest-first ordering by ULID. */
|
|
30
|
+
export declare function rebuildIndex(cwd: string): RunIndex;
|
|
31
|
+
export interface ListRunsOptions {
|
|
32
|
+
/** Force a rebuild from disk even if index.json is fresh. */
|
|
33
|
+
rebuild?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** List all runs, newest-first. Lazily rebuilds index.json if missing. */
|
|
36
|
+
export declare function listRuns(cwd: string, opts?: ListRunsOptions): RunIndexEntry[];
|
|
37
|
+
export interface GcRunsOptions {
|
|
38
|
+
/** Delete completed runs older than this many days. Required. */
|
|
39
|
+
olderThanDays: number;
|
|
40
|
+
/** Don't actually delete; just return what would be removed. */
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
/** Override "now" for tests. Default Date.now(). */
|
|
43
|
+
now?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface GcRunsResult {
|
|
46
|
+
/** runIds that were (or would be) deleted. */
|
|
47
|
+
deleted: string[];
|
|
48
|
+
/** runIds skipped because they're still active or too young. */
|
|
49
|
+
kept: string[];
|
|
50
|
+
/** runIds skipped for safety reasons (symlink, suspicious path). */
|
|
51
|
+
skippedUnsafe: string[];
|
|
52
|
+
}
|
|
53
|
+
/** Delete completed runs older than N days. Honors the spec's symlink
|
|
54
|
+
* safety: uses lstat so we never traverse a symlink out of the runs/
|
|
55
|
+
* tree. */
|
|
56
|
+
export declare function gcRuns(cwd: string, opts: GcRunsOptions): GcRunsResult;
|
|
57
|
+
//# sourceMappingURL=runs.d.ts.map
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/core/run-state/runs.ts
|
|
2
|
+
//
|
|
3
|
+
// Top-level Run State Engine helpers — createRun, listRuns, gcRuns. These
|
|
4
|
+
// are the entry points the (yet-to-be-built) phase wrapper, CLI, and budget
|
|
5
|
+
// enforcer will call. Phase 1 ships only the data layer; later phases build
|
|
6
|
+
// on top.
|
|
7
|
+
//
|
|
8
|
+
// Spec: docs/specs/v6-run-state-engine.md "State on disk", "Run lifecycle",
|
|
9
|
+
// "Resume command".
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { ulid, decodeTime } from "./ulid.js";
|
|
13
|
+
import { acquireRunLock } from "./lock.js";
|
|
14
|
+
import { appendEvent, foldEvents, readEvents, stateToIndexEntry } from "./events.js";
|
|
15
|
+
import { writeStateSnapshot } from "./state.js";
|
|
16
|
+
import { RUN_STATE_SCHEMA_VERSION, } from "./types.js";
|
|
17
|
+
const CACHE_DIR = '.guardrail-cache';
|
|
18
|
+
const RUNS_DIR = 'runs';
|
|
19
|
+
const INDEX_FILE = 'index.json';
|
|
20
|
+
export function runsRoot(cwd) {
|
|
21
|
+
return path.join(cwd, CACHE_DIR, RUNS_DIR);
|
|
22
|
+
}
|
|
23
|
+
export function indexPath(cwd) {
|
|
24
|
+
return path.join(runsRoot(cwd), INDEX_FILE);
|
|
25
|
+
}
|
|
26
|
+
export function runDirFor(cwd, runId) {
|
|
27
|
+
return path.join(runsRoot(cwd), runId);
|
|
28
|
+
}
|
|
29
|
+
/** Create a fresh run directory, acquire its advisory lock, write the
|
|
30
|
+
* initial state.json, and emit the `run.start` event.
|
|
31
|
+
*
|
|
32
|
+
* Throws GuardrailError(lock_held) if a stale lock exists for the freshly-
|
|
33
|
+
* generated runId — extremely unlikely (ULIDs are unique) but possible if
|
|
34
|
+
* two parallel invocations on the same OS clock collide on a leftover dir
|
|
35
|
+
* on disk. Caller can simply retry. */
|
|
36
|
+
export async function createRun(opts) {
|
|
37
|
+
if (!Array.isArray(opts.phases) || opts.phases.length === 0) {
|
|
38
|
+
throw new Error('createRun: phases[] must be a non-empty array');
|
|
39
|
+
}
|
|
40
|
+
const runId = ulid();
|
|
41
|
+
const runDir = runDirFor(opts.cwd, runId);
|
|
42
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
43
|
+
// Acquire BEFORE first event write so writerId is well-defined.
|
|
44
|
+
const lock = await acquireRunLock(runDir);
|
|
45
|
+
// Seed the state snapshot first (with no events yet) so that even a crash
|
|
46
|
+
// before run.start lands leaves a recoverable artifact.
|
|
47
|
+
const startedAt = new Date(decodeTime(runId)).toISOString();
|
|
48
|
+
const initialState = {
|
|
49
|
+
schema_version: RUN_STATE_SCHEMA_VERSION,
|
|
50
|
+
runId,
|
|
51
|
+
startedAt,
|
|
52
|
+
status: 'pending',
|
|
53
|
+
phases: opts.phases.map((name, idx) => ({
|
|
54
|
+
schema_version: RUN_STATE_SCHEMA_VERSION,
|
|
55
|
+
name,
|
|
56
|
+
index: idx,
|
|
57
|
+
status: 'pending',
|
|
58
|
+
idempotent: false,
|
|
59
|
+
hasSideEffects: false,
|
|
60
|
+
costUSD: 0,
|
|
61
|
+
attempts: 0,
|
|
62
|
+
artifacts: [],
|
|
63
|
+
externalRefs: [],
|
|
64
|
+
})),
|
|
65
|
+
currentPhaseIdx: 0,
|
|
66
|
+
totalCostUSD: 0,
|
|
67
|
+
lastEventSeq: 0,
|
|
68
|
+
writerId: lock.writerId,
|
|
69
|
+
cwd: opts.cwd,
|
|
70
|
+
...(opts.config !== undefined ? { config: opts.config } : {}),
|
|
71
|
+
};
|
|
72
|
+
writeStateSnapshot(runDir, initialState);
|
|
73
|
+
// Emit run.start. The appender owns the seq counter.
|
|
74
|
+
const startEvent = appendEvent(runDir, {
|
|
75
|
+
event: 'run.start',
|
|
76
|
+
phases: opts.phases,
|
|
77
|
+
...(opts.config !== undefined ? { config: opts.config } : {}),
|
|
78
|
+
}, { writerId: lock.writerId, runId });
|
|
79
|
+
// Refresh the snapshot to reflect lastEventSeq=1.
|
|
80
|
+
initialState.lastEventSeq = startEvent.seq;
|
|
81
|
+
writeStateSnapshot(runDir, initialState);
|
|
82
|
+
// Refresh the index (best-effort — index is a pure cache).
|
|
83
|
+
try {
|
|
84
|
+
rebuildIndex(opts.cwd);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Index failure shouldn't block the run.
|
|
88
|
+
}
|
|
89
|
+
return { runId, runDir, state: initialState, lock };
|
|
90
|
+
}
|
|
91
|
+
// ----------------------------------------------------------------------------
|
|
92
|
+
// Listing + indexing.
|
|
93
|
+
// ----------------------------------------------------------------------------
|
|
94
|
+
function readIndex(cwd) {
|
|
95
|
+
const p = indexPath(cwd);
|
|
96
|
+
if (!fs.existsSync(p))
|
|
97
|
+
return null;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null; // treat unreadable index as missing — it's a cache
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function writeIndex(cwd, index) {
|
|
106
|
+
fs.mkdirSync(runsRoot(cwd), { recursive: true });
|
|
107
|
+
fs.writeFileSync(indexPath(cwd), JSON.stringify(index, null, 2), 'utf8');
|
|
108
|
+
}
|
|
109
|
+
/** Rebuild index.json from each run dir's state.json (or replayed state if
|
|
110
|
+
* the snapshot is missing / corrupt). Newest-first ordering by ULID. */
|
|
111
|
+
export function rebuildIndex(cwd) {
|
|
112
|
+
const root = runsRoot(cwd);
|
|
113
|
+
const entries = [];
|
|
114
|
+
if (!fs.existsSync(root)) {
|
|
115
|
+
const empty = { schema_version: RUN_STATE_SCHEMA_VERSION, runs: [] };
|
|
116
|
+
writeIndex(cwd, empty);
|
|
117
|
+
return empty;
|
|
118
|
+
}
|
|
119
|
+
const dirents = fs.readdirSync(root, { withFileTypes: true });
|
|
120
|
+
for (const d of dirents) {
|
|
121
|
+
if (!d.isDirectory())
|
|
122
|
+
continue;
|
|
123
|
+
const runId = d.name;
|
|
124
|
+
const runDir = path.join(root, runId);
|
|
125
|
+
let state;
|
|
126
|
+
let recovered = false;
|
|
127
|
+
try {
|
|
128
|
+
// We don't hold the lock during a list — listing is read-only and
|
|
129
|
+
// races with a concurrent writer are tolerated (we may briefly read
|
|
130
|
+
// a stale snapshot, which is fine). For replay-recovery we DO need
|
|
131
|
+
// a writerId, but only if the snapshot is bad; if so the run isn't
|
|
132
|
+
// healthy anyway, and we use a synthetic writerId so we never
|
|
133
|
+
// mutate the run's events.ndjson during a list operation.
|
|
134
|
+
// Instead of recoverState (which writes events) we just replay
|
|
135
|
+
// in-memory.
|
|
136
|
+
const fromEvents = readEvents(runDir);
|
|
137
|
+
// Build a fresh snapshot if state.json is missing or unreadable.
|
|
138
|
+
// Use the project-internal file paths to avoid pulling readState
|
|
139
|
+
// here just to throw.
|
|
140
|
+
const stateFilePath = path.join(runDir, 'state.json');
|
|
141
|
+
if (fs.existsSync(stateFilePath)) {
|
|
142
|
+
try {
|
|
143
|
+
state = JSON.parse(fs.readFileSync(stateFilePath, 'utf8'));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// fall through to replay
|
|
147
|
+
recovered = true;
|
|
148
|
+
// Replay needs the events; if the events are also corrupt we
|
|
149
|
+
// surface the error via skip.
|
|
150
|
+
state = replayInMemory(runDir, fromEvents.events);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
recovered = true;
|
|
155
|
+
state = replayInMemory(runDir, fromEvents.events);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Corrupt run dir — skip from the index entirely. `runs doctor`
|
|
160
|
+
// (Phase 3) will surface these.
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
entries.push(stateToIndexEntry(state, recovered));
|
|
164
|
+
}
|
|
165
|
+
// ULIDs are sortable; we want NEWEST first → reverse-sort by runId.
|
|
166
|
+
entries.sort((a, b) => (a.runId < b.runId ? 1 : a.runId > b.runId ? -1 : 0));
|
|
167
|
+
const index = { schema_version: RUN_STATE_SCHEMA_VERSION, runs: entries };
|
|
168
|
+
writeIndex(cwd, index);
|
|
169
|
+
return index;
|
|
170
|
+
}
|
|
171
|
+
/** In-memory replay used by rebuildIndex / listRuns — does NOT write to disk
|
|
172
|
+
* or emit events. Lets us pass pre-fetched events so we don't double-read
|
|
173
|
+
* the file. */
|
|
174
|
+
function replayInMemory(runDir, events) {
|
|
175
|
+
return foldEvents(runDir, events);
|
|
176
|
+
}
|
|
177
|
+
/** List all runs, newest-first. Lazily rebuilds index.json if missing. */
|
|
178
|
+
export function listRuns(cwd, opts = {}) {
|
|
179
|
+
if (opts.rebuild)
|
|
180
|
+
return rebuildIndex(cwd).runs;
|
|
181
|
+
const idx = readIndex(cwd);
|
|
182
|
+
if (idx)
|
|
183
|
+
return idx.runs;
|
|
184
|
+
return rebuildIndex(cwd).runs;
|
|
185
|
+
}
|
|
186
|
+
/** Delete completed runs older than N days. Honors the spec's symlink
|
|
187
|
+
* safety: uses lstat so we never traverse a symlink out of the runs/
|
|
188
|
+
* tree. */
|
|
189
|
+
export function gcRuns(cwd, opts) {
|
|
190
|
+
const root = runsRoot(cwd);
|
|
191
|
+
const result = { deleted: [], kept: [], skippedUnsafe: [] };
|
|
192
|
+
if (!fs.existsSync(root))
|
|
193
|
+
return result;
|
|
194
|
+
const cutoff = (opts.now ?? Date.now()) - opts.olderThanDays * 86_400_000;
|
|
195
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
196
|
+
for (const d of entries) {
|
|
197
|
+
if (d.name === INDEX_FILE)
|
|
198
|
+
continue;
|
|
199
|
+
const runId = d.name;
|
|
200
|
+
const runDir = path.join(root, runId);
|
|
201
|
+
// Symlinks (whether to dirs or files) are flagged unsafe. Dirent's
|
|
202
|
+
// isDirectory() returns FALSE for a symlink even if the target is a
|
|
203
|
+
// directory, which matches our policy here — we only operate on real
|
|
204
|
+
// dirs that lstat agrees are not links.
|
|
205
|
+
if (d.isSymbolicLink()) {
|
|
206
|
+
result.skippedUnsafe.push(runId);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (!d.isDirectory())
|
|
210
|
+
continue;
|
|
211
|
+
let lst;
|
|
212
|
+
try {
|
|
213
|
+
lst = fs.lstatSync(runDir);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
result.skippedUnsafe.push(runId);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!lst.isDirectory() || lst.isSymbolicLink()) {
|
|
220
|
+
result.skippedUnsafe.push(runId);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Read state to decide. If unreadable, skip — `runs doctor` will deal.
|
|
224
|
+
let state = null;
|
|
225
|
+
try {
|
|
226
|
+
const sp = path.join(runDir, 'state.json');
|
|
227
|
+
if (fs.existsSync(sp)) {
|
|
228
|
+
state = JSON.parse(fs.readFileSync(sp, 'utf8'));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// fall through
|
|
233
|
+
}
|
|
234
|
+
if (!state) {
|
|
235
|
+
// Defensive: try to derive from ULID alone for "old enough" check.
|
|
236
|
+
// If runId isn't a ULID we treat it as suspicious and skip.
|
|
237
|
+
let createdMs;
|
|
238
|
+
try {
|
|
239
|
+
createdMs = decodeTime(runId);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
result.skippedUnsafe.push(runId);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (createdMs >= cutoff) {
|
|
246
|
+
result.kept.push(runId);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
// Fall through — eligible for delete.
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const terminal = state.status === 'success' || state.status === 'failed' || state.status === 'aborted';
|
|
253
|
+
if (!terminal) {
|
|
254
|
+
result.kept.push(runId);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const endMs = state.endedAt ? Date.parse(state.endedAt) : Date.parse(state.startedAt);
|
|
258
|
+
if (Number.isFinite(endMs) && endMs >= cutoff) {
|
|
259
|
+
result.kept.push(runId);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (opts.dryRun) {
|
|
264
|
+
result.deleted.push(runId);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
// Defense in depth: refuse to recurse out via a symlink hidden inside.
|
|
269
|
+
// fs.rmSync with `force: true, recursive: true` handles dirs but
|
|
270
|
+
// also follows nothing — it doesn't traverse symlinks for deletion
|
|
271
|
+
// boundaries (it deletes the link, not the target).
|
|
272
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
273
|
+
result.deleted.push(runId);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
result.skippedUnsafe.push(runId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Refresh the index after a real GC pass.
|
|
280
|
+
if (!opts.dryRun && result.deleted.length > 0) {
|
|
281
|
+
try {
|
|
282
|
+
rebuildIndex(cwd);
|
|
283
|
+
}
|
|
284
|
+
catch { /* index is cache */ }
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=runs.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type PhaseSnapshot } from './types.ts';
|
|
2
|
+
export declare function phasesDir(runDir: string): string;
|
|
3
|
+
export declare function phaseSnapshotPath(runDir: string, phaseName: string): string;
|
|
4
|
+
/** Write a per-phase snapshot atomically. Identical sequence to
|
|
5
|
+
* state.json:
|
|
6
|
+
* open(tmp, 'w') → write → fsync(fd) → close → rename → fsync(dirfd).
|
|
7
|
+
*
|
|
8
|
+
* Any pre-existing snapshot is left untouched until the rename, so a crash
|
|
9
|
+
* mid-write leaves the previous snapshot intact. */
|
|
10
|
+
export declare function writePhaseSnapshot(runDir: string, snapshot: PhaseSnapshot): void;
|
|
11
|
+
/** Read a per-phase snapshot. Returns null if missing. Throws
|
|
12
|
+
* GuardrailError(corrupted_state) if it's present-but-unparseable. */
|
|
13
|
+
export declare function readPhaseSnapshot(runDir: string, phaseName: string): PhaseSnapshot | null;
|
|
14
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/core/run-state/snapshot.ts
|
|
2
|
+
//
|
|
3
|
+
// Atomic per-phase snapshot writer/reader. Each phase, after run, gets a
|
|
4
|
+
// `phases/<name>.json` artifact mirroring the corresponding entry in
|
|
5
|
+
// state.json. Writes use the same tmp+rename+fsync protocol as state.json so
|
|
6
|
+
// a crash mid-write never leaves a half-baked phase snapshot on disk.
|
|
7
|
+
//
|
|
8
|
+
// Phase 1 left this as a TODO; Phase 2 fills it in to back the lifecycle
|
|
9
|
+
// wrapper (`runPhase`).
|
|
10
|
+
//
|
|
11
|
+
// Spec: docs/specs/v6-run-state-engine.md "State on disk" — `phases/<name>.json`.
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { GuardrailError } from "../errors.js";
|
|
15
|
+
const PHASES_DIR = 'phases';
|
|
16
|
+
export function phasesDir(runDir) {
|
|
17
|
+
return path.join(runDir, PHASES_DIR);
|
|
18
|
+
}
|
|
19
|
+
export function phaseSnapshotPath(runDir, phaseName) {
|
|
20
|
+
return path.join(phasesDir(runDir), `${sanitizePhaseFilename(phaseName)}.json`);
|
|
21
|
+
}
|
|
22
|
+
/** Reject filename characters that would escape `phases/`. Phase names are
|
|
23
|
+
* caller-supplied strings; we bound them to a safe charset rather than
|
|
24
|
+
* letting `..` / path separators sneak in.
|
|
25
|
+
*
|
|
26
|
+
* Allowed: ASCII alphanumerics, dash, underscore, dot. Anything else is
|
|
27
|
+
* rejected with a typed error so callers can correct the call-site rather
|
|
28
|
+
* than silently producing a write to `../somewhere`. */
|
|
29
|
+
function sanitizePhaseFilename(phaseName) {
|
|
30
|
+
if (!phaseName || typeof phaseName !== 'string') {
|
|
31
|
+
throw new GuardrailError(`phase snapshot: name must be a non-empty string`, { code: 'invalid_config', provider: 'run-state', details: { phaseName } });
|
|
32
|
+
}
|
|
33
|
+
if (!/^[A-Za-z0-9._-]+$/.test(phaseName)) {
|
|
34
|
+
throw new GuardrailError(`phase snapshot: name "${phaseName}" contains unsupported characters`, { code: 'invalid_config', provider: 'run-state', details: { phaseName } });
|
|
35
|
+
}
|
|
36
|
+
return phaseName;
|
|
37
|
+
}
|
|
38
|
+
/** Write a per-phase snapshot atomically. Identical sequence to
|
|
39
|
+
* state.json:
|
|
40
|
+
* open(tmp, 'w') → write → fsync(fd) → close → rename → fsync(dirfd).
|
|
41
|
+
*
|
|
42
|
+
* Any pre-existing snapshot is left untouched until the rename, so a crash
|
|
43
|
+
* mid-write leaves the previous snapshot intact. */
|
|
44
|
+
export function writePhaseSnapshot(runDir, snapshot) {
|
|
45
|
+
const dir = phasesDir(runDir);
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
const target = phaseSnapshotPath(runDir, snapshot.name);
|
|
48
|
+
const tmp = `${target}.tmp`;
|
|
49
|
+
const data = JSON.stringify(snapshot, null, 2);
|
|
50
|
+
const fd = fs.openSync(tmp, 'w');
|
|
51
|
+
let wroteOk = false;
|
|
52
|
+
try {
|
|
53
|
+
fs.writeSync(fd, data);
|
|
54
|
+
fs.fsyncSync(fd);
|
|
55
|
+
wroteOk = true;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
fs.closeSync(fd);
|
|
59
|
+
if (!wroteOk) {
|
|
60
|
+
try {
|
|
61
|
+
fs.unlinkSync(tmp);
|
|
62
|
+
}
|
|
63
|
+
catch { /* ignore */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
fs.renameSync(tmp, target);
|
|
67
|
+
// Best-effort dir fsync for rename durability. Same EISDIR/EPERM/ENOTSUP
|
|
68
|
+
// tolerance as state.ts (tmpfs / SMB / Windows quirks).
|
|
69
|
+
try {
|
|
70
|
+
const dirFd = fs.openSync(dir, 'r');
|
|
71
|
+
try {
|
|
72
|
+
fs.fsyncSync(dirFd);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
fs.closeSync(dirFd);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const code = err.code;
|
|
80
|
+
if (code !== 'EISDIR' && code !== 'EPERM' && code !== 'ENOTSUP') {
|
|
81
|
+
throw new GuardrailError(`phase snapshot: dir fsync failed: ${err.message}`, {
|
|
82
|
+
code: 'corrupted_state',
|
|
83
|
+
provider: 'run-state',
|
|
84
|
+
details: { runDir, phaseName: snapshot.name, errno: code },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Read a per-phase snapshot. Returns null if missing. Throws
|
|
90
|
+
* GuardrailError(corrupted_state) if it's present-but-unparseable. */
|
|
91
|
+
export function readPhaseSnapshot(runDir, phaseName) {
|
|
92
|
+
const p = phaseSnapshotPath(runDir, phaseName);
|
|
93
|
+
if (!fs.existsSync(p))
|
|
94
|
+
return null;
|
|
95
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
96
|
+
if (!raw) {
|
|
97
|
+
throw new GuardrailError(`phase snapshot: empty file ${p}`, {
|
|
98
|
+
code: 'corrupted_state',
|
|
99
|
+
provider: 'run-state',
|
|
100
|
+
details: { runDir, phaseName },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw new GuardrailError(`phase snapshot: corrupt JSON: ${err.message}`, {
|
|
108
|
+
code: 'corrupted_state',
|
|
109
|
+
provider: 'run-state',
|
|
110
|
+
details: { runDir, phaseName, error: err.message },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=snapshot.js.map
|