@delegance/claude-autopilot 5.5.2 → 7.2.0
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 +1776 -6
- package/README.md +65 -1
- package/bin/_launcher.js +38 -23
- package/dist/src/adapters/council/openai.js +12 -6
- 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/index.d.ts +2 -0
- package/dist/src/adapters/deploy/index.js +33 -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 +67 -3
- package/dist/src/adapters/deploy/vercel.d.ts +17 -1
- package/dist/src/adapters/deploy/vercel.js +29 -49
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/codex.js +10 -7
- package/dist/src/cli/autopilot.d.ts +75 -0
- package/dist/src/cli/autopilot.js +750 -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/dashboard/index.d.ts +5 -0
- package/dist/src/cli/dashboard/index.js +49 -0
- package/dist/src/cli/dashboard/login.d.ts +22 -0
- package/dist/src/cli/dashboard/login.js +260 -0
- package/dist/src/cli/dashboard/logout.d.ts +12 -0
- package/dist/src/cli/dashboard/logout.js +45 -0
- package/dist/src/cli/dashboard/status.d.ts +30 -0
- package/dist/src/cli/dashboard/status.js +65 -0
- package/dist/src/cli/dashboard/upload.d.ts +16 -0
- package/dist/src/cli/dashboard/upload.js +48 -0
- package/dist/src/cli/deploy.d.ts +3 -3
- package/dist/src/cli/deploy.js +34 -9
- package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
- package/dist/src/cli/engine-flag-deprecation.js +20 -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 +416 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.d.ts +2 -1
- package/dist/src/cli/index.js +774 -245
- 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 +26 -0
- 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/scaffold.d.ts +39 -0
- package/dist/src/cli/scaffold.js +287 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/setup.d.ts +30 -0
- package/dist/src/cli/setup.js +137 -0
- 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 +9 -0
- package/dist/src/core/config/schema.js +7 -0
- package/dist/src/core/config/types.d.ts +11 -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 +11 -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/schema-validator.js +15 -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 +512 -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 +45 -0
- package/dist/src/core/run-state/resolve-engine.js +74 -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 +69 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -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 +284 -0
- package/dist/src/core/run-state/types.js +19 -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/dist/src/dashboard/auto-upload.d.ts +26 -0
- package/dist/src/dashboard/auto-upload.js +107 -0
- package/dist/src/dashboard/config.d.ts +22 -0
- package/dist/src/dashboard/config.js +109 -0
- package/dist/src/dashboard/upload/canonical.d.ts +3 -0
- package/dist/src/dashboard/upload/canonical.js +16 -0
- package/dist/src/dashboard/upload/chain.d.ts +9 -0
- package/dist/src/dashboard/upload/chain.js +27 -0
- package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
- package/dist/src/dashboard/upload/snapshot.js +66 -0
- package/dist/src/dashboard/upload/uploader.d.ts +54 -0
- package/dist/src/dashboard/upload/uploader.js +330 -0
- package/package.json +19 -3
- package/scripts/autoregress.ts +1 -1
- package/scripts/test-runner.mjs +4 -0
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- 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,395 @@
|
|
|
1
|
+
// src/cli/runs-watch.ts
|
|
2
|
+
//
|
|
3
|
+
// `runs watch <runId>` — tails a run's events.ndjson and renders a live
|
|
4
|
+
// cost/token meter that updates as phases execute.
|
|
5
|
+
//
|
|
6
|
+
// Tail strategy: fs.watchFile with a 1s polling interval. fs.watch (inotify
|
|
7
|
+
// on Linux, FSEvents on macOS) is unreliable for append-only logs in our
|
|
8
|
+
// matrix — it sometimes never fires for tiny appends, sometimes fires twice
|
|
9
|
+
// per write. Polling is consistent across darwin / linux / win32 and the
|
|
10
|
+
// 1-second cadence is plenty for a human-facing meter.
|
|
11
|
+
//
|
|
12
|
+
// Modes:
|
|
13
|
+
// default pretty-rendered live tail (header + per-event lines + final summary)
|
|
14
|
+
// --no-follow render snapshot once, exit
|
|
15
|
+
// --json emit raw NDJSON to stdout (for piping to jq / dashboards)
|
|
16
|
+
// --since <seq> replay forward from a specific seq (resume after disconnect)
|
|
17
|
+
//
|
|
18
|
+
// Spec: tasks/v6.1-runs-watch.md.
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import { GuardrailError } from "../core/errors.js";
|
|
21
|
+
import { foldEvents, readEvents, eventsPath } from "../core/run-state/events.js";
|
|
22
|
+
import { readStateSnapshot } from "../core/run-state/state.js";
|
|
23
|
+
import { runDirFor } from "../core/run-state/runs.js";
|
|
24
|
+
import { isValidULID } from "../core/run-state/ulid.js";
|
|
25
|
+
import { renderEventLine, renderFinalSummary, renderHeader, } from "./runs-watch-renderer.js";
|
|
26
|
+
// ----------------------------------------------------------------------------
|
|
27
|
+
// Implementation.
|
|
28
|
+
// ----------------------------------------------------------------------------
|
|
29
|
+
/** ULID validation, mirrors the helper in runs.ts. Inlined to avoid an
|
|
30
|
+
* internal import that isn't exported. */
|
|
31
|
+
function assertValidRunId(runId) {
|
|
32
|
+
if (!runId) {
|
|
33
|
+
throw new GuardrailError('a run id is required', {
|
|
34
|
+
code: 'invalid_config',
|
|
35
|
+
provider: 'runs-cli',
|
|
36
|
+
details: { runId },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (!isValidULID(runId)) {
|
|
40
|
+
throw new GuardrailError(`run id is not a valid ULID: ${runId}`, {
|
|
41
|
+
code: 'invalid_config',
|
|
42
|
+
provider: 'runs-cli',
|
|
43
|
+
details: { runId },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function formatErr(err) {
|
|
48
|
+
if (err instanceof GuardrailError)
|
|
49
|
+
return `${err.code}: ${err.message}`;
|
|
50
|
+
if (err instanceof Error)
|
|
51
|
+
return err.message;
|
|
52
|
+
return String(err);
|
|
53
|
+
}
|
|
54
|
+
/** Pull a `BudgetConfig` out of `RunState.config` if one was recorded at
|
|
55
|
+
* run-creation time. Returns null on any shape mismatch — we never throw,
|
|
56
|
+
* the watcher should degrade gracefully when the budget block is absent. */
|
|
57
|
+
function extractBudget(state) {
|
|
58
|
+
const cfg = state.config;
|
|
59
|
+
if (!cfg || typeof cfg !== 'object')
|
|
60
|
+
return null;
|
|
61
|
+
// Two recognized shapes:
|
|
62
|
+
// 1. `config.budget = { perRunUSD: ..., ... }` (preferred — matches
|
|
63
|
+
// what `BudgetConfig` looks like in budget.ts)
|
|
64
|
+
// 2. `config.budgets = { perRunUSD: ..., ... }` (yaml plural alias —
|
|
65
|
+
// matches what guardrail.config.yaml uses; see migration-guide.md
|
|
66
|
+
// "Budget config" section)
|
|
67
|
+
const candidate = cfg.budget
|
|
68
|
+
?? cfg.budgets;
|
|
69
|
+
if (!candidate || typeof candidate !== 'object')
|
|
70
|
+
return null;
|
|
71
|
+
const c = candidate;
|
|
72
|
+
if (typeof c.perRunUSD !== 'number')
|
|
73
|
+
return null;
|
|
74
|
+
const out = { perRunUSD: c.perRunUSD };
|
|
75
|
+
if (typeof c.perPhaseUSD === 'number')
|
|
76
|
+
out.perPhaseUSD = c.perPhaseUSD;
|
|
77
|
+
if (typeof c.conservativePhaseReserveUSD === 'number') {
|
|
78
|
+
out.conservativePhaseReserveUSD = c.conservativePhaseReserveUSD;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** Fold the events list into a (totalCostUSD, terminalEvent) tuple. The
|
|
83
|
+
* watcher uses this to know when to stop following — a terminal event is
|
|
84
|
+
* any `run.complete`, the run-level `status` field on it, or a marker
|
|
85
|
+
* event the verb treats as terminal. */
|
|
86
|
+
function isTerminalEvent(ev) {
|
|
87
|
+
if (ev.event === 'run.complete')
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/** Public entry point — exported so the dispatcher in runs.ts can call us
|
|
92
|
+
* uniformly with the other verbs. */
|
|
93
|
+
export async function runRunsWatch(opts) {
|
|
94
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
95
|
+
const json = !!opts.json;
|
|
96
|
+
// ---- 1. Validate inputs --------------------------------------------------
|
|
97
|
+
try {
|
|
98
|
+
assertValidRunId(opts.runId);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
exit: 1,
|
|
103
|
+
stdout: [],
|
|
104
|
+
stderr: [`[claude-autopilot] runs watch: ${formatErr(err)}`],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const runDir = runDirFor(cwd, opts.runId);
|
|
108
|
+
if (!fs.existsSync(runDir)) {
|
|
109
|
+
const err = new GuardrailError(`run not found: ${opts.runId}`, {
|
|
110
|
+
code: 'not_found',
|
|
111
|
+
provider: 'runs-cli',
|
|
112
|
+
details: { runId: opts.runId, runDir },
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
exit: 2,
|
|
116
|
+
stdout: [],
|
|
117
|
+
stderr: [`[claude-autopilot] runs watch: ${formatErr(err)}`],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (opts.since !== undefined && (!Number.isFinite(opts.since) || opts.since < 0)) {
|
|
121
|
+
const err = new GuardrailError(`--since must be a non-negative integer (got ${opts.since})`, { code: 'invalid_config', provider: 'runs-cli', details: { since: opts.since } });
|
|
122
|
+
return {
|
|
123
|
+
exit: 1,
|
|
124
|
+
stdout: [],
|
|
125
|
+
stderr: [`[claude-autopilot] runs watch: ${formatErr(err)}`],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ---- 2. Decide ANSI mode -------------------------------------------------
|
|
129
|
+
//
|
|
130
|
+
// ansi=true iff:
|
|
131
|
+
// - not --json mode, AND
|
|
132
|
+
// - --no-color was not passed, AND
|
|
133
|
+
// - NO_COLOR env var not set, AND
|
|
134
|
+
// - stdout is a TTY (via the test seam or the real flag)
|
|
135
|
+
const realIsTTY = opts.__testIsTTY !== undefined ? opts.__testIsTTY : !!process.stdout.isTTY;
|
|
136
|
+
const ansi = !json && !opts.noColor && !process.env.NO_COLOR && realIsTTY;
|
|
137
|
+
const renderOpts = { ansi };
|
|
138
|
+
// ---- 3. Read initial state -----------------------------------------------
|
|
139
|
+
let state;
|
|
140
|
+
try {
|
|
141
|
+
state = readStateSnapshot(runDir);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
state = null;
|
|
145
|
+
}
|
|
146
|
+
// Fall back to events replay if state.json is missing/corrupt — this is
|
|
147
|
+
// the same path runs show takes. Watch should never refuse to start on
|
|
148
|
+
// snapshot drift; events.ndjson is authoritative.
|
|
149
|
+
if (!state) {
|
|
150
|
+
try {
|
|
151
|
+
state = foldEvents(runDir, readEvents(runDir).events);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
exit: 1,
|
|
156
|
+
stdout: [],
|
|
157
|
+
stderr: [`[claude-autopilot] runs watch: ${formatErr(err)}`],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const budget = extractBudget(state);
|
|
162
|
+
// The output sinks. In tests these are buffer-collectors; in production
|
|
163
|
+
// they wrap process.stdout / process.stderr.
|
|
164
|
+
const stdoutLines = [];
|
|
165
|
+
const stderrLines = [];
|
|
166
|
+
const writeStdout = opts.__testWriteStdout
|
|
167
|
+
?? ((s) => { stdoutLines.push(s.endsWith('\n') ? s.slice(0, -1) : s); });
|
|
168
|
+
const writeStderr = opts.__testWriteStderr
|
|
169
|
+
?? ((s) => { stderrLines.push(s.endsWith('\n') ? s.slice(0, -1) : s); });
|
|
170
|
+
// ---- 4. Snapshot mode (--no-follow) --------------------------------------
|
|
171
|
+
if (opts.noFollow) {
|
|
172
|
+
const since = opts.since ?? 0;
|
|
173
|
+
let runningTotal = 0;
|
|
174
|
+
let allEvents;
|
|
175
|
+
try {
|
|
176
|
+
allEvents = readEvents(runDir).events;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
exit: 1,
|
|
181
|
+
stdout: [],
|
|
182
|
+
stderr: [`[claude-autopilot] runs watch: ${formatErr(err)}`],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (json) {
|
|
186
|
+
// JSON mode: dump every event from `since` forward as raw NDJSON.
|
|
187
|
+
// Strict channel discipline — exactly the bytes that landed on disk.
|
|
188
|
+
for (const ev of allEvents) {
|
|
189
|
+
if (ev.seq < since)
|
|
190
|
+
continue;
|
|
191
|
+
writeStdout(JSON.stringify(ev) + '\n');
|
|
192
|
+
}
|
|
193
|
+
return finishOk(stdoutLines, stderrLines);
|
|
194
|
+
}
|
|
195
|
+
// Pretty: header + every event-line.
|
|
196
|
+
for (const line of renderHeader(state, budget, renderOpts))
|
|
197
|
+
writeStdout(line + '\n');
|
|
198
|
+
for (const ev of allEvents) {
|
|
199
|
+
if (ev.seq < since)
|
|
200
|
+
continue;
|
|
201
|
+
if (ev.event === 'phase.cost')
|
|
202
|
+
runningTotal += ev.costUSD;
|
|
203
|
+
writeStdout(renderEventLine(ev, runningTotal, renderOpts) + '\n');
|
|
204
|
+
}
|
|
205
|
+
// Final summary if the run terminated.
|
|
206
|
+
if (state.status === 'success' || state.status === 'failed' || state.status === 'aborted') {
|
|
207
|
+
const summary = {
|
|
208
|
+
runId: state.runId,
|
|
209
|
+
status: state.status,
|
|
210
|
+
totalCostUSD: state.totalCostUSD,
|
|
211
|
+
durationMs: computeDurationMs(state),
|
|
212
|
+
};
|
|
213
|
+
for (const line of renderFinalSummary(summary, renderOpts))
|
|
214
|
+
writeStdout(line + '\n');
|
|
215
|
+
}
|
|
216
|
+
return finishOk(stdoutLines, stderrLines);
|
|
217
|
+
}
|
|
218
|
+
// ---- 5. Live tail mode ---------------------------------------------------
|
|
219
|
+
// We re-read the file from byte 0 each poll cycle (simpler than offset
|
|
220
|
+
// tracking and the file is bounded). We track lastSeq to decide which
|
|
221
|
+
// events are new. The `since` flag is treated as a floor on what we
|
|
222
|
+
// print, not on what we read — we always need the full event stream
|
|
223
|
+
// to compute runningTotal correctly.
|
|
224
|
+
const since = opts.since ?? 0;
|
|
225
|
+
let lastSeq = 0;
|
|
226
|
+
let runningTotal = 0;
|
|
227
|
+
// Print header up front (default mode only — JSON pipes the raw stream).
|
|
228
|
+
if (!json) {
|
|
229
|
+
for (const line of renderHeader(state, budget, renderOpts))
|
|
230
|
+
writeStdout(line + '\n');
|
|
231
|
+
}
|
|
232
|
+
// Drain whatever's already on disk.
|
|
233
|
+
const initialEvents = readEvents(runDir).events;
|
|
234
|
+
for (const ev of initialEvents) {
|
|
235
|
+
if (ev.event === 'phase.cost')
|
|
236
|
+
runningTotal += ev.costUSD;
|
|
237
|
+
if (ev.seq >= since) {
|
|
238
|
+
if (json)
|
|
239
|
+
writeStdout(JSON.stringify(ev) + '\n');
|
|
240
|
+
else
|
|
241
|
+
writeStdout(renderEventLine(ev, runningTotal, renderOpts) + '\n');
|
|
242
|
+
}
|
|
243
|
+
lastSeq = Math.max(lastSeq, ev.seq);
|
|
244
|
+
}
|
|
245
|
+
// Did the run already terminate before we started watching? Short-circuit
|
|
246
|
+
// so the verb doesn't hang waiting for events that will never arrive.
|
|
247
|
+
const terminalAlready = initialEvents.some(isTerminalEvent);
|
|
248
|
+
if (terminalAlready) {
|
|
249
|
+
if (!json) {
|
|
250
|
+
const finalState = foldEvents(runDir, initialEvents);
|
|
251
|
+
const summary = {
|
|
252
|
+
runId: finalState.runId,
|
|
253
|
+
status: finalState.status,
|
|
254
|
+
totalCostUSD: finalState.totalCostUSD,
|
|
255
|
+
durationMs: computeDurationMs(finalState),
|
|
256
|
+
};
|
|
257
|
+
for (const line of renderFinalSummary(summary, renderOpts))
|
|
258
|
+
writeStdout(line + '\n');
|
|
259
|
+
}
|
|
260
|
+
return finishOk(stdoutLines, stderrLines);
|
|
261
|
+
}
|
|
262
|
+
// Set up the polling watcher.
|
|
263
|
+
const eventsFile = eventsPath(runDir);
|
|
264
|
+
const pollInterval = opts.__testPollIntervalMs ?? 1000;
|
|
265
|
+
let lastFileSize = fs.existsSync(eventsFile) ? fs.statSync(eventsFile).size : 0;
|
|
266
|
+
return await new Promise(resolve => {
|
|
267
|
+
let resolved = false;
|
|
268
|
+
let sigintHandler = null;
|
|
269
|
+
const finish = (status, errMsg) => {
|
|
270
|
+
if (resolved)
|
|
271
|
+
return;
|
|
272
|
+
resolved = true;
|
|
273
|
+
try {
|
|
274
|
+
fs.unwatchFile(eventsFile);
|
|
275
|
+
}
|
|
276
|
+
catch { /* ignore */ }
|
|
277
|
+
if (sigintHandler) {
|
|
278
|
+
try {
|
|
279
|
+
process.off('SIGINT', sigintHandler);
|
|
280
|
+
}
|
|
281
|
+
catch { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
// Final summary in pretty mode.
|
|
284
|
+
if (!json) {
|
|
285
|
+
try {
|
|
286
|
+
const finalEvents = readEvents(runDir).events;
|
|
287
|
+
const finalState = foldEvents(runDir, finalEvents);
|
|
288
|
+
const summary = {
|
|
289
|
+
runId: finalState.runId,
|
|
290
|
+
// For Ctrl-C (interrupted) we tag the summary that way even
|
|
291
|
+
// though the run itself may still be running. The renderer
|
|
292
|
+
// colors `interrupted` as bold-yellow.
|
|
293
|
+
status: status === 'interrupted' ? 'interrupted' : finalState.status,
|
|
294
|
+
totalCostUSD: finalState.totalCostUSD,
|
|
295
|
+
durationMs: computeDurationMs(finalState),
|
|
296
|
+
};
|
|
297
|
+
for (const line of renderFinalSummary(summary, renderOpts)) {
|
|
298
|
+
writeStdout(line + '\n');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Ignore — final summary is best-effort.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const exit = status === 'error' ? 1 : 0;
|
|
306
|
+
const result = {
|
|
307
|
+
exit,
|
|
308
|
+
stdout: stdoutLines,
|
|
309
|
+
stderr: errMsg
|
|
310
|
+
? [...stderrLines, `[claude-autopilot] runs watch: ${errMsg}`]
|
|
311
|
+
: stderrLines,
|
|
312
|
+
};
|
|
313
|
+
resolve(result);
|
|
314
|
+
};
|
|
315
|
+
const tick = () => {
|
|
316
|
+
// File-shrink recovery: if the file is smaller than last poll the
|
|
317
|
+
// log was rotated/truncated externally — re-read from start.
|
|
318
|
+
let stat = null;
|
|
319
|
+
try {
|
|
320
|
+
stat = fs.statSync(eventsFile);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// File doesn't exist yet — wait for it.
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (stat.size < lastFileSize) {
|
|
327
|
+
// Truncation. Reset and re-fold from scratch.
|
|
328
|
+
lastSeq = 0;
|
|
329
|
+
runningTotal = 0;
|
|
330
|
+
}
|
|
331
|
+
lastFileSize = stat.size;
|
|
332
|
+
let events;
|
|
333
|
+
try {
|
|
334
|
+
events = readEvents(runDir).events;
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
// Mid-log corruption — surface and exit. The runs doctor verb
|
|
338
|
+
// can be used to diagnose / fix.
|
|
339
|
+
finish('error', formatErr(err));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const ev of events) {
|
|
343
|
+
if (ev.seq <= lastSeq)
|
|
344
|
+
continue;
|
|
345
|
+
if (ev.event === 'phase.cost')
|
|
346
|
+
runningTotal += ev.costUSD;
|
|
347
|
+
if (ev.seq >= since) {
|
|
348
|
+
if (json)
|
|
349
|
+
writeStdout(JSON.stringify(ev) + '\n');
|
|
350
|
+
else
|
|
351
|
+
writeStdout(renderEventLine(ev, runningTotal, renderOpts) + '\n');
|
|
352
|
+
}
|
|
353
|
+
lastSeq = ev.seq;
|
|
354
|
+
if (isTerminalEvent(ev)) {
|
|
355
|
+
// Run terminated — drain remaining events (already done in the
|
|
356
|
+
// loop) and exit.
|
|
357
|
+
finish('completed');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
// fs.watchFile polls at the interval we pass; the listener fires when
|
|
363
|
+
// mtime changes. We do an explicit tick on each fire because the file
|
|
364
|
+
// size delta isn't enough on its own (an event could land between
|
|
365
|
+
// polls).
|
|
366
|
+
fs.watchFile(eventsFile, { interval: pollInterval, persistent: true }, () => {
|
|
367
|
+
tick();
|
|
368
|
+
});
|
|
369
|
+
// Ctrl-C — clean exit with summary. The handler is removed by `finish`.
|
|
370
|
+
if (!opts.__testStopAfterTerminal) {
|
|
371
|
+
sigintHandler = () => finish('interrupted');
|
|
372
|
+
process.on('SIGINT', sigintHandler);
|
|
373
|
+
}
|
|
374
|
+
// First tick — we already drained initial events above, but kicking
|
|
375
|
+
// tick() once here covers the rare race where new events appended
|
|
376
|
+
// between our drain and our watchFile registration.
|
|
377
|
+
tick();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
/** Compute wall-clock duration from a state snapshot. Used in the final
|
|
381
|
+
* summary line. Falls back to 0 if the run hasn't started or the timestamps
|
|
382
|
+
* are malformed. */
|
|
383
|
+
function computeDurationMs(state) {
|
|
384
|
+
const start = Date.parse(state.startedAt);
|
|
385
|
+
if (!Number.isFinite(start))
|
|
386
|
+
return 0;
|
|
387
|
+
const end = state.endedAt ? Date.parse(state.endedAt) : Date.now();
|
|
388
|
+
if (!Number.isFinite(end))
|
|
389
|
+
return 0;
|
|
390
|
+
return Math.max(0, end - start);
|
|
391
|
+
}
|
|
392
|
+
function finishOk(stdout, stderr) {
|
|
393
|
+
return { exit: 0, stdout, stderr };
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=runs-watch.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { statePath } from '../core/run-state/state.ts';
|
|
2
|
+
import type { ExternalRef, RunState, RunStatus } from '../core/run-state/types.ts';
|
|
3
|
+
interface RunsCliResult {
|
|
4
|
+
exit: number;
|
|
5
|
+
/** Lines (will be newline-joined) for stdout under text mode. Under --json
|
|
6
|
+
* mode this is replaced by a single envelope JSON line. */
|
|
7
|
+
stdout: string[];
|
|
8
|
+
/** Lines for stderr under text mode. Phase 5 will move all human warnings
|
|
9
|
+
* into NDJSON events on stderr; Phase 3 keeps text-mode warnings here. */
|
|
10
|
+
stderr: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface RunRunsListOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** `runs list` — newest-first listing. Optional --status filter narrows to
|
|
18
|
+
* one RunStatus. `--json` emits an envelope; text mode prints a tight
|
|
19
|
+
* table. */
|
|
20
|
+
export declare function runRunsList(opts: RunRunsListOptions): Promise<RunsCliResult>;
|
|
21
|
+
export interface RunRunsShowOptions {
|
|
22
|
+
runId: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
/** Tail the events.ndjson log after the state summary. */
|
|
25
|
+
events?: boolean;
|
|
26
|
+
/** How many tail events to show with --events. Default 20. */
|
|
27
|
+
eventsTail?: number;
|
|
28
|
+
json?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** `runs show <id>` — render state.json (or replay if missing) plus, with
|
|
31
|
+
* --events, the tail of events.ndjson. JSON mode bundles state + events into
|
|
32
|
+
* the envelope. */
|
|
33
|
+
export declare function runRunsShow(opts: RunRunsShowOptions): Promise<RunsCliResult>;
|
|
34
|
+
export interface RunRunsGcOptions {
|
|
35
|
+
cwd?: string;
|
|
36
|
+
/** Default 30. */
|
|
37
|
+
olderThanDays?: number;
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
/** Skip the interactive confirmation prompt. */
|
|
41
|
+
yes?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** `runs gc` — wraps gcRuns with confirmation. Default cutoff 30 days. With
|
|
44
|
+
* --dry-run, lists what would be removed without touching disk. */
|
|
45
|
+
export declare function runRunsGc(opts: RunRunsGcOptions): Promise<RunsCliResult>;
|
|
46
|
+
export interface RunRunsDeleteOptions {
|
|
47
|
+
runId: string;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
/** Override the terminal-status guard. */
|
|
50
|
+
force?: boolean;
|
|
51
|
+
json?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** `runs delete <id>` — explicit single-run delete. Refuses non-terminal
|
|
54
|
+
* status without --force; refuses if the run lock is currently held by
|
|
55
|
+
* another writer.
|
|
56
|
+
*
|
|
57
|
+
* We acquire the lock for the duration of the delete so we never race a
|
|
58
|
+
* concurrent writer. Lock acquisition uses a tiny timeout — we want
|
|
59
|
+
* fail-fast over blocking. */
|
|
60
|
+
export declare function runRunsDelete(opts: RunRunsDeleteOptions): Promise<RunsCliResult>;
|
|
61
|
+
export type ResumeDecision = 'retry' | 'skip-idempotent' | 'needs-human' | 'already-complete';
|
|
62
|
+
export interface RunResumeLookup {
|
|
63
|
+
runId: string;
|
|
64
|
+
status: RunStatus;
|
|
65
|
+
currentPhase: string | null;
|
|
66
|
+
nextPhase: string | null;
|
|
67
|
+
decision: ResumeDecision;
|
|
68
|
+
reason: string;
|
|
69
|
+
externalRefs: ExternalRef[];
|
|
70
|
+
}
|
|
71
|
+
export interface RunRunResumeOptions {
|
|
72
|
+
runId: string;
|
|
73
|
+
cwd?: string;
|
|
74
|
+
/** Optional explicit phase to resume from (by name). Surfaces as a
|
|
75
|
+
* validation hint here; actual execution lands in Phase 6+. */
|
|
76
|
+
fromPhase?: string;
|
|
77
|
+
json?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** `run resume <id>` — Phase 3 LOOKUP ONLY.
|
|
80
|
+
*
|
|
81
|
+
* This verb identifies which phase a future resume would pick up from and
|
|
82
|
+
* the decision the engine would make per the spec's idempotency table. It
|
|
83
|
+
* does NOT execute the phase — that wires in Phase 6+ once the budget
|
|
84
|
+
* enforcer (Phase 4) and the JSON event stream (Phase 5) are in place.
|
|
85
|
+
*
|
|
86
|
+
* Decision rules (mirror `runPhase` in src/core/run-state/phase-runner.ts):
|
|
87
|
+
* - already-complete : run.status === 'success' or every phase succeeded
|
|
88
|
+
* - skip-idempotent : nextPhase has a prior phase.success AND idempotent
|
|
89
|
+
* - needs-human : nextPhase has a prior phase.success AND side-effects
|
|
90
|
+
* - retry : default (no prior success — first attempt or retry
|
|
91
|
+
* of a failed attempt) */
|
|
92
|
+
export declare function runRunResume(opts: RunRunResumeOptions): Promise<RunsCliResult>;
|
|
93
|
+
/** Pure projection over a RunState that decides the next phase + replay rule.
|
|
94
|
+
* Exported for tests. */
|
|
95
|
+
export declare function computeResumeLookup(state: RunState, fromPhase?: string): RunResumeLookup;
|
|
96
|
+
export interface RunRunsDoctorOptions {
|
|
97
|
+
cwd?: string;
|
|
98
|
+
/** Limit the check to a single run id. */
|
|
99
|
+
runId?: string;
|
|
100
|
+
/** Rewrite state.json from the events.ndjson replay where drift is found. */
|
|
101
|
+
fix?: boolean;
|
|
102
|
+
json?: boolean;
|
|
103
|
+
}
|
|
104
|
+
export interface RunsDoctorRunReport {
|
|
105
|
+
runId: string;
|
|
106
|
+
drift: 'none' | 'snapshot-vs-replay' | 'snapshot-missing' | 'snapshot-corrupt' | 'events-corrupt';
|
|
107
|
+
details?: string;
|
|
108
|
+
fixed?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/** `runs doctor` — replay events.ndjson per run, compare against state.json,
|
|
111
|
+
* report drift. With --fix, rewrite state.json from the replay where drift
|
|
112
|
+
* exists.
|
|
113
|
+
*
|
|
114
|
+
* Drift categories:
|
|
115
|
+
* snapshot-vs-replay : both readable but disagree on a key field
|
|
116
|
+
* snapshot-missing : state.json absent, replay successful
|
|
117
|
+
* snapshot-corrupt : state.json present but unparseable
|
|
118
|
+
* events-corrupt : events.ndjson can't be folded (bigger problem)
|
|
119
|
+
*/
|
|
120
|
+
export declare function runRunsDoctor(opts: RunRunsDoctorOptions): Promise<RunsCliResult>;
|
|
121
|
+
export { statePath };
|
|
122
|
+
//# sourceMappingURL=runs.d.ts.map
|