@delegance/claude-autopilot 6.2.2 → 7.2.1
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 +886 -0
- package/README.md +10 -1
- package/bin/_launcher.js +38 -23
- package/dist/src/cli/autopilot.d.ts +4 -0
- package/dist/src/cli/autopilot.js +15 -0
- 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/engine-flag-deprecation.d.ts +14 -0
- package/dist/src/cli/engine-flag-deprecation.js +20 -0
- package/dist/src/cli/help-text.d.ts +1 -1
- package/dist/src/cli/help-text.js +44 -28
- package/dist/src/cli/index.d.ts +2 -1
- package/dist/src/cli/index.js +72 -17
- package/dist/src/cli/scaffold.d.ts +39 -0
- package/dist/src/cli/scaffold.js +287 -0
- package/dist/src/cli/setup.d.ts +30 -0
- package/dist/src/cli/setup.js +137 -0
- package/dist/src/core/run-state/events.js +10 -2
- package/dist/src/core/run-state/resolve-engine.d.ts +26 -81
- package/dist/src/core/run-state/resolve-engine.js +39 -155
- package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +5 -9
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +26 -19
- package/dist/src/core/run-state/state.d.ts +1 -1
- package/dist/src/core/run-state/types.d.ts +8 -2
- package/dist/src/core/run-state/types.js +8 -2
- 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 +18 -3
- package/scripts/test-runner.mjs +4 -0
|
@@ -1,50 +1,30 @@
|
|
|
1
1
|
// src/core/run-state/resolve-engine.ts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// "How to opt in".
|
|
3
|
+
// v7.0 — engine-off path retired. The function is preserved for source
|
|
4
|
+
// compatibility with callers that pass `cliEngine` / `envValue` /
|
|
5
|
+
// `configEnabled`, but it now returns `enabled: true` unconditionally.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
7
|
+
// What changed in v7.0 vs v6.x:
|
|
8
|
+
// - `ENGINE_DEFAULT_V6_0` and `ENGINE_DEFAULT_V6_1` exports REMOVED.
|
|
9
|
+
// Direct importers must replace with literal `true` (see
|
|
10
|
+
// docs/v7/breaking-changes.md).
|
|
11
|
+
// - The deprecation warning helpers (`emitEngineOffDeprecationWarning`
|
|
12
|
+
// / `shouldWarnEngineOffDeprecation` / `ENGINE_OFF_DEPRECATION_MESSAGE`)
|
|
13
|
+
// are RETAINED as no-op stubs so call sites don't have to change in
|
|
14
|
+
// the same PR — they always return false / never fire.
|
|
15
|
+
// - `parseEngineEnvValue()` is RETAINED for back-compat with any
|
|
16
|
+
// out-of-tree callers; `resolveEngineEnabled()` ignores the env
|
|
17
|
+
// value entirely (the engine-off env path is gone).
|
|
15
18
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// This module is intentionally pure and side-effect-free: it never reads from
|
|
23
|
-
// the environment or the config file directly. Callers (the CLI dispatcher)
|
|
24
|
-
// gather the inputs and pass them in — that keeps the function trivially
|
|
25
|
-
// testable and lets the dispatcher own all I/O.
|
|
26
|
-
//
|
|
27
|
-
// Invalid env values do NOT throw. The contract from the spec / migration
|
|
28
|
-
// guide is "treat as unset and emit a run.warning so observers can attribute
|
|
29
|
-
// the fallthrough." This module returns metadata — the resolver caller
|
|
30
|
-
// (cli/index.ts) is responsible for emitting the warning event.
|
|
31
|
-
/** v6.1+ ships with the engine ON by default — flipped from the v6.0
|
|
32
|
-
* default (`false`) per `docs/specs/v6.1-default-flip.md`. Exported so
|
|
33
|
-
* tests / future releases can pin a known value. */
|
|
34
|
-
export const ENGINE_DEFAULT_V6_1 = true;
|
|
35
|
-
/** Historical v6.0 default. Preserved verbatim — its semantic meaning
|
|
36
|
-
* ("the v6.0 default was off") doesn't change just because the active
|
|
37
|
-
* default flipped. Out-of-tree consumers that pinned this constant get
|
|
38
|
-
* the value the name promises. Use `ENGINE_DEFAULT_V6_1` for the active
|
|
39
|
-
* default. Removed in v7.
|
|
40
|
-
* @deprecated Use `ENGINE_DEFAULT_V6_1` or omit `builtInDefault` to inherit
|
|
41
|
-
* the active default. */
|
|
42
|
-
export const ENGINE_DEFAULT_V6_0 = false;
|
|
19
|
+
// Why keep the stub function shape: the CLI dispatcher passes
|
|
20
|
+
// `cliEngine` / `envEngine` / config to `runPhaseWithLifecycle`, which
|
|
21
|
+
// in turn calls `resolveEngineEnabled()`. Those parameters become
|
|
22
|
+
// effective no-ops in v7.0 — the values are observed (so a future PR
|
|
23
|
+
// can re-enable the path or surface a deprecation event) but never
|
|
24
|
+
// override the always-on result.
|
|
43
25
|
/** Parse a stringly-typed env value into a tri-state boolean.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* strings — that signals the caller to fall through to the next precedence
|
|
47
|
-
* layer. */
|
|
26
|
+
* Retained for back-compat with any out-of-tree callers; the v7
|
|
27
|
+
* resolver does not consult env values. */
|
|
48
28
|
export function parseEngineEnvValue(raw) {
|
|
49
29
|
if (raw === undefined)
|
|
50
30
|
return undefined;
|
|
@@ -66,125 +46,29 @@ export function parseEngineEnvValue(raw) {
|
|
|
66
46
|
return undefined;
|
|
67
47
|
}
|
|
68
48
|
}
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
export function resolveEngineEnabled(
|
|
72
|
-
const { cliEngine, envValue, configEnabled, builtInDefault } = opts;
|
|
73
|
-
const builtIn = builtInDefault ?? ENGINE_DEFAULT_V6_1;
|
|
74
|
-
// Layer 1 — CLI flag wins outright.
|
|
75
|
-
if (cliEngine === true) {
|
|
76
|
-
return { enabled: true, source: 'cli', reason: '--engine flag' };
|
|
77
|
-
}
|
|
78
|
-
if (cliEngine === false) {
|
|
79
|
-
return { enabled: false, source: 'cli', reason: '--no-engine flag' };
|
|
80
|
-
}
|
|
81
|
-
// Layer 2 — env var.
|
|
82
|
-
if (envValue !== undefined && envValue.trim() !== '') {
|
|
83
|
-
const parsed = parseEngineEnvValue(envValue);
|
|
84
|
-
if (parsed !== undefined) {
|
|
85
|
-
return {
|
|
86
|
-
enabled: parsed,
|
|
87
|
-
source: 'env',
|
|
88
|
-
reason: `CLAUDE_AUTOPILOT_ENGINE=${envValue}`,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
// Invalid value — fall through, but record the raw value so the caller
|
|
92
|
-
// can emit a run.warning. Continue to config / default below.
|
|
93
|
-
// We bind it here so it survives the recursion-style fallthrough.
|
|
94
|
-
return resolveWithFallthrough({
|
|
95
|
-
configEnabled,
|
|
96
|
-
builtIn,
|
|
97
|
-
invalidEnvValue: envValue,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
return resolveWithFallthrough({ configEnabled, builtIn });
|
|
101
|
-
}
|
|
102
|
-
/** Layers 3 + 4 — config, then built-in default. Factored out so the env
|
|
103
|
-
* invalid-value path can reach the same logic without recursing into
|
|
104
|
-
* resolveEngineEnabled (which would re-evaluate the env var and loop). */
|
|
105
|
-
function resolveWithFallthrough(opts) {
|
|
106
|
-
const { configEnabled, builtIn, invalidEnvValue } = opts;
|
|
107
|
-
const invalidSuffix = invalidEnvValue !== undefined
|
|
108
|
-
? `; invalid CLAUDE_AUTOPILOT_ENGINE=${JSON.stringify(invalidEnvValue)} ignored`
|
|
109
|
-
: '';
|
|
110
|
-
if (configEnabled === true) {
|
|
111
|
-
return {
|
|
112
|
-
enabled: true,
|
|
113
|
-
source: 'config',
|
|
114
|
-
reason: `engine.enabled: true in guardrail.config.yaml${invalidSuffix}`,
|
|
115
|
-
...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (configEnabled === false) {
|
|
119
|
-
return {
|
|
120
|
-
enabled: false,
|
|
121
|
-
source: 'config',
|
|
122
|
-
reason: `engine.enabled: false in guardrail.config.yaml${invalidSuffix}`,
|
|
123
|
-
...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
49
|
+
/** v7.0+ — engine is always on. Pure function; ignores all inputs.
|
|
50
|
+
* Source compatible with v6.x call sites. */
|
|
51
|
+
export function resolveEngineEnabled(_opts = {}) {
|
|
126
52
|
return {
|
|
127
|
-
enabled:
|
|
53
|
+
enabled: true,
|
|
128
54
|
source: 'default',
|
|
129
|
-
reason:
|
|
130
|
-
...(invalidEnvValue !== undefined ? { invalidEnvValue } : {}),
|
|
55
|
+
reason: 'v7.0+ — engine always on (engine-off path removed)',
|
|
131
56
|
};
|
|
132
57
|
}
|
|
133
58
|
// ---------------------------------------------------------------------------
|
|
134
|
-
// v6.1 deprecation
|
|
59
|
+
// v6.1 deprecation helpers — retained as no-op stubs for source compat.
|
|
60
|
+
// v7.0 removed the engine-off path entirely; no warning ever fires.
|
|
135
61
|
// ---------------------------------------------------------------------------
|
|
136
|
-
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
export const ENGINE_OFF_DEPRECATION_MESSAGE = '[deprecation] --no-engine / engine.enabled: false will be removed in v7. Migrate to engine-on (default).';
|
|
144
|
-
/** Decide whether v6.1's `--no-engine` deprecation warning applies for a
|
|
145
|
-
* given resolver result. Returns `true` ONLY when the user explicitly
|
|
146
|
-
* opted out (via CLI flag, env var, or config) — never on the v6.1 default
|
|
147
|
-
* (which is `enabled: true`, so it can't trigger here anyway) and never
|
|
148
|
-
* when the engine is actually on. Pure: takes the resolver result, returns
|
|
149
|
-
* a boolean.
|
|
150
|
-
*
|
|
151
|
-
* Why this is a separate predicate (not collapsed into the warner): the
|
|
152
|
-
* CLI dispatcher wants to ALSO emit a typed `run.warning` event into a
|
|
153
|
-
* ledger when the engine ends up on but the resolver came from a layer
|
|
154
|
-
* that's about to be removed — except today, on v6.1, the only path that
|
|
155
|
-
* warns IS the "engine off, explicit opt-out" path. So the predicate
|
|
156
|
-
* collapses cleanly to that single condition. v7 removes both. */
|
|
157
|
-
export function shouldWarnEngineOffDeprecation(resolved) {
|
|
158
|
-
if (resolved.enabled)
|
|
159
|
-
return false;
|
|
160
|
-
return (resolved.source === 'cli' ||
|
|
161
|
-
resolved.source === 'env' ||
|
|
162
|
-
resolved.source === 'config');
|
|
62
|
+
/** v6.1-era stable deprecation banner. v7.0+ never emits this string —
|
|
63
|
+
* the path is gone. Kept exported so out-of-tree consumers that imported
|
|
64
|
+
* it still type-check. */
|
|
65
|
+
export const ENGINE_OFF_DEPRECATION_MESSAGE = '[deprecation] --no-engine / engine.enabled: false were removed in v7.0. Migration: drop the flag/env/config.';
|
|
66
|
+
/** v7.0+ no-op. Always returns false. */
|
|
67
|
+
export function shouldWarnEngineOffDeprecation(_resolved) {
|
|
68
|
+
return false;
|
|
163
69
|
}
|
|
164
|
-
/**
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* - the engine is on (no opt-out happened);
|
|
168
|
-
* - the source is `'default'` (v6.1's flipped default = on, so a default
|
|
169
|
-
* result with `enabled: false` is impossible without a custom
|
|
170
|
-
* `builtInDefault` override — and even that path doesn't warn since
|
|
171
|
-
* it's not a user-driven opt-out).
|
|
172
|
-
*
|
|
173
|
-
* Pure-ish: side-effect is captured behind the optional `warn` callback so
|
|
174
|
-
* tests can assert on the message without spawning a subprocess. The
|
|
175
|
-
* default warner writes to `process.stderr` with a trailing newline.
|
|
176
|
-
*
|
|
177
|
-
* Returns `true` when the warning fired, `false` when it was a no-op. The
|
|
178
|
-
* return value is purely informational — callers can use it to decide
|
|
179
|
-
* whether to also append a `run.warning` event into a run ledger (only
|
|
180
|
-
* meaningful on the engine-on path; the v6.1 deprecation only fires on
|
|
181
|
-
* engine-off, where there's no run dir to write into). */
|
|
182
|
-
export function emitEngineOffDeprecationWarning(resolved, warn = (msg) => {
|
|
183
|
-
process.stderr.write(`${msg}\n`);
|
|
184
|
-
}) {
|
|
185
|
-
if (!shouldWarnEngineOffDeprecation(resolved))
|
|
186
|
-
return false;
|
|
187
|
-
warn(ENGINE_OFF_DEPRECATION_MESSAGE);
|
|
188
|
-
return true;
|
|
70
|
+
/** v7.0+ no-op. Always returns false. */
|
|
71
|
+
export function emitEngineOffDeprecationWarning(_resolved, _warn = () => { }) {
|
|
72
|
+
return false;
|
|
189
73
|
}
|
|
190
74
|
//# sourceMappingURL=resolve-engine.js.map
|
|
@@ -28,15 +28,11 @@ export interface RunPhaseWithLifecycleOpts<I, O> {
|
|
|
28
28
|
* unset. The helper passes this through to `resolveEngineEnabled` —
|
|
29
29
|
* invalid values fall through with a `run.warning` recorded automatically. */
|
|
30
30
|
envEngine: string | undefined;
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
* legacy path's behavior — keeps engine-off byte-for-byte identical to
|
|
37
|
-
* pre-v6 behavior even when the phase body's signature would otherwise
|
|
38
|
-
* pin the call shape. */
|
|
39
|
-
runEngineOff: () => Promise<O>;
|
|
31
|
+
/** v6.x escape hatch — invoked when the engine was disabled. Retained
|
|
32
|
+
* in the v7.0 type for source compatibility with existing callers,
|
|
33
|
+
* but the helper NEVER calls it in v7.0+ (engine-off path was
|
|
34
|
+
* removed). Optional in v7.0; can be omitted from new call sites. */
|
|
35
|
+
runEngineOff?: () => Promise<O>;
|
|
40
36
|
}
|
|
41
37
|
/** What the helper hands back. `runId` and `runDir` are null on the
|
|
42
38
|
* engine-off path so callers can branch on whether engine artifacts exist
|
|
@@ -39,7 +39,7 @@ import { createRun } from "./runs.js";
|
|
|
39
39
|
import { runPhase } from "./phase-runner.js";
|
|
40
40
|
import { appendEvent, replayState } from "./events.js";
|
|
41
41
|
import { writeStateSnapshot } from "./state.js";
|
|
42
|
-
import { resolveEngineEnabled,
|
|
42
|
+
import { resolveEngineEnabled, } from "./resolve-engine.js";
|
|
43
43
|
// Inline ANSI codes — same shape every wrapped verb uses. Kept here so the
|
|
44
44
|
// helper doesn't depend on a verb-local `fmt`. The error message format
|
|
45
45
|
// (`[<phase>] engine: phase failed — <msg>` + dim inspect hint) is
|
|
@@ -69,12 +69,12 @@ const ANSI_RED = '\x1b[31m';
|
|
|
69
69
|
* catch block does not need to release the lock itself — `finally` covers
|
|
70
70
|
* both the success and failure exit paths. */
|
|
71
71
|
export async function runPhaseWithLifecycle(opts) {
|
|
72
|
-
const { cwd, phase, input, config, cliEngine, envEngine
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// the
|
|
76
|
-
//
|
|
77
|
-
//
|
|
72
|
+
const { cwd, phase, input, config, cliEngine, envEngine } = opts;
|
|
73
|
+
// v7.0 — engine is always on. resolveEngineEnabled() returns
|
|
74
|
+
// { enabled: true, source: 'default' } unconditionally. We still
|
|
75
|
+
// pass the v6-era inputs through so any future re-introduction of
|
|
76
|
+
// observability (a run.warning when a user passes the removed flags
|
|
77
|
+
// via env vars in CI) is a one-line change.
|
|
78
78
|
const engineResolved = resolveEngineEnabled({
|
|
79
79
|
...(cliEngine !== undefined ? { cliEngine } : {}),
|
|
80
80
|
...(envEngine !== undefined ? { envValue: envEngine } : {}),
|
|
@@ -82,18 +82,6 @@ export async function runPhaseWithLifecycle(opts) {
|
|
|
82
82
|
? { configEnabled: config.engine.enabled }
|
|
83
83
|
: {}),
|
|
84
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
85
|
// Engine on — full lifecycle. Mirrors the pre-v6.0.6 inline shape that
|
|
98
86
|
// every wrapped verb duplicated.
|
|
99
87
|
const created = await createRun({
|
|
@@ -115,6 +103,25 @@ export async function runPhaseWithLifecycle(opts) {
|
|
|
115
103
|
details: { resolution: engineResolved },
|
|
116
104
|
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
117
105
|
}
|
|
106
|
+
// v7.0 — emit `engine_off_removed` warning when CLAUDE_AUTOPILOT_ENGINE
|
|
107
|
+
// is set to an off-style value. The env value is otherwise ignored
|
|
108
|
+
// (engine remains on). Softer than --no-engine (which exits 1) because
|
|
109
|
+
// env vars in CI are sticky and silently breaking every v6.x → v7
|
|
110
|
+
// upgrade in CI on day one would burn user trust. See spec test #1(c).
|
|
111
|
+
if (envEngine !== undefined) {
|
|
112
|
+
const normalized = envEngine.trim().toLowerCase();
|
|
113
|
+
if (normalized === 'off' || normalized === 'false' || normalized === '0' || normalized === 'no') {
|
|
114
|
+
appendEvent(created.runDir, {
|
|
115
|
+
event: 'run.warning',
|
|
116
|
+
message: 'engine_off_removed',
|
|
117
|
+
details: {
|
|
118
|
+
code: 'engine_off_removed',
|
|
119
|
+
envValue: envEngine,
|
|
120
|
+
note: 'CLAUDE_AUTOPILOT_ENGINE=off has no effect in v7.0+; engine remains on.',
|
|
121
|
+
},
|
|
122
|
+
}, { writerId: created.lock.writerId, runId: created.runId });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
118
125
|
const runStartedAt = Date.now();
|
|
119
126
|
try {
|
|
120
127
|
const output = await runPhase(phase, input, {
|
|
@@ -5,7 +5,7 @@ export declare const RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION: 1;
|
|
|
5
5
|
/** Highest `schema_version` value this binary can replay. Always equal to
|
|
6
6
|
* the writer's `RUN_STATE_SCHEMA_VERSION` — the writer never produces a
|
|
7
7
|
* newer shape than the reader on the same binary. */
|
|
8
|
-
export declare const RUN_STATE_MAX_SUPPORTED_SCHEMA_VERSION:
|
|
8
|
+
export declare const RUN_STATE_MAX_SUPPORTED_SCHEMA_VERSION: 2;
|
|
9
9
|
export declare function statePath(runDir: string): string;
|
|
10
10
|
/** Write the snapshot atomically. Sequence:
|
|
11
11
|
* open(tmp, 'w') → write → fsync(fd) → close → rename → fsync(dirfd).
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** Schema version for everything written by this engine. Bump on breaking
|
|
2
|
-
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
3
|
-
|
|
2
|
+
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
3
|
+
*
|
|
4
|
+
* v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
|
|
5
|
+
* state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
|
|
6
|
+
* because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
|
|
7
|
+
* cannot read v7-written runs; the corrupted_state error includes a
|
|
8
|
+
* "downgrade resume is not supported" hint so operators know why. */
|
|
9
|
+
export declare const RUN_STATE_SCHEMA_VERSION: 2;
|
|
4
10
|
export type SchemaVersion = typeof RUN_STATE_SCHEMA_VERSION;
|
|
5
11
|
/** Identifies a single OS-level writer. PID + a hash of the hostname (we
|
|
6
12
|
* don't persist the raw hostname to the lock metadata so co-tenant signal
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
// Spec: docs/specs/v6-run-state-engine.md ("State on disk", "Run lifecycle",
|
|
9
9
|
// "Idempotency rules + external operation ledger", "Persistence protocol").
|
|
10
10
|
/** Schema version for everything written by this engine. Bump on breaking
|
|
11
|
-
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
12
|
-
|
|
11
|
+
* changes to RunState / RunEvent / PhaseSnapshot shape.
|
|
12
|
+
*
|
|
13
|
+
* v7.0 — bumped from 1 to 2 to signal the v7 cycle on every newly-written
|
|
14
|
+
* state.json. v6.x runs (schema_version=1) remain readable by v7 binaries
|
|
15
|
+
* because `RUN_STATE_MIN_SUPPORTED_SCHEMA_VERSION` stays at 1. v6 binaries
|
|
16
|
+
* cannot read v7-written runs; the corrupted_state error includes a
|
|
17
|
+
* "downgrade resume is not supported" hint so operators know why. */
|
|
18
|
+
export const RUN_STATE_SCHEMA_VERSION = 2;
|
|
13
19
|
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface AutoUploadOptions {
|
|
2
|
+
/** Caller's explicit opt-out (e.g. CLI --no-upload). */
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
/** Test seam — substitute fetch impl. */
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
/** Test seam — silence stdout. */
|
|
7
|
+
silent?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface AutoUploadResult {
|
|
10
|
+
attempted: boolean;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
url: string | null;
|
|
13
|
+
skipped: boolean;
|
|
14
|
+
reason?: 'opt-out-flag' | 'env-off' | 'not-logged-in' | 'no-events' | 'aborted' | 'error' | 'limit-reached';
|
|
15
|
+
}
|
|
16
|
+
export declare function shouldAutoUpload(options?: AutoUploadOptions): {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
reason?: AutoUploadResult['reason'];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Run an auto-upload for the given runId. Wraps the foreground uploader
|
|
22
|
+
* in a SIGINT/SIGTERM handler so Ctrl-C is clean. Always returns a result
|
|
23
|
+
* — never throws. Caller preserves the run's original exit code.
|
|
24
|
+
*/
|
|
25
|
+
export declare function autoUploadAtComplete(runId: string, runDir: string, options?: AutoUploadOptions): Promise<AutoUploadResult>;
|
|
26
|
+
//# sourceMappingURL=auto-upload.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Auto-upload at run.complete — non-fatal hosted-product hook.
|
|
2
|
+
//
|
|
3
|
+
// Contract (per spec): never fails the run. Always preserves the original
|
|
4
|
+
// exit code. Failure prints a resume command. Empty events.ndjson skips
|
|
5
|
+
// upload cleanly (Phase 2.2's POST /api/upload-session 422s expectedChunkCount=0).
|
|
6
|
+
//
|
|
7
|
+
// Opt-outs:
|
|
8
|
+
// - explicit `--no-upload` flag → caller passes options.disabled=true
|
|
9
|
+
// - env CLAUDE_AUTOPILOT_UPLOAD=off
|
|
10
|
+
// - not logged in (no config)
|
|
11
|
+
// - events.ndjson missing or 0 bytes
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { readConfig } from "./config.js";
|
|
15
|
+
import { uploadRun, UploadLimitError } from "./upload/uploader.js";
|
|
16
|
+
export function shouldAutoUpload(options = {}) {
|
|
17
|
+
if (options.disabled)
|
|
18
|
+
return { ok: false, reason: 'opt-out-flag' };
|
|
19
|
+
const env = process.env.CLAUDE_AUTOPILOT_UPLOAD;
|
|
20
|
+
if (env && /^(off|false|0|no)$/i.test(env))
|
|
21
|
+
return { ok: false, reason: 'env-off' };
|
|
22
|
+
return { ok: true };
|
|
23
|
+
}
|
|
24
|
+
async function fileExistsNonEmpty(p) {
|
|
25
|
+
try {
|
|
26
|
+
const stat = await fs.stat(p);
|
|
27
|
+
return stat.isFile() && stat.size > 0;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run an auto-upload for the given runId. Wraps the foreground uploader
|
|
35
|
+
* in a SIGINT/SIGTERM handler so Ctrl-C is clean. Always returns a result
|
|
36
|
+
* — never throws. Caller preserves the run's original exit code.
|
|
37
|
+
*/
|
|
38
|
+
export async function autoUploadAtComplete(runId, runDir, options = {}) {
|
|
39
|
+
const gate = shouldAutoUpload(options);
|
|
40
|
+
if (!gate.ok) {
|
|
41
|
+
return { attempted: false, ok: true, url: null, skipped: true, reason: gate.reason ?? 'opt-out-flag' };
|
|
42
|
+
}
|
|
43
|
+
const cfg = await readConfig();
|
|
44
|
+
if (!cfg) {
|
|
45
|
+
return { attempted: false, ok: true, url: null, skipped: true, reason: 'not-logged-in' };
|
|
46
|
+
}
|
|
47
|
+
const eventsPath = path.join(runDir, 'events.ndjson');
|
|
48
|
+
const hasEvents = await fileExistsNonEmpty(eventsPath);
|
|
49
|
+
if (!hasEvents) {
|
|
50
|
+
return { attempted: false, ok: true, url: null, skipped: true, reason: 'no-events' };
|
|
51
|
+
}
|
|
52
|
+
const ac = new AbortController();
|
|
53
|
+
const sigintHandler = () => ac.abort();
|
|
54
|
+
process.on('SIGINT', sigintHandler);
|
|
55
|
+
process.on('SIGTERM', sigintHandler);
|
|
56
|
+
try {
|
|
57
|
+
const result = await uploadRun(runId, runDir, {
|
|
58
|
+
apiKey: cfg.apiKey,
|
|
59
|
+
signal: ac.signal,
|
|
60
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
61
|
+
});
|
|
62
|
+
if (result.ok && result.url) {
|
|
63
|
+
if (!options.silent)
|
|
64
|
+
process.stdout.write(`[autopilot] uploaded to ${result.url}\n`);
|
|
65
|
+
return { attempted: true, ok: true, url: result.url, skipped: false };
|
|
66
|
+
}
|
|
67
|
+
if (result.ok && result.skipped) {
|
|
68
|
+
if (!options.silent)
|
|
69
|
+
process.stdout.write(`[autopilot] skipping upload — events.ndjson is empty\n`);
|
|
70
|
+
return { attempted: true, ok: true, url: null, skipped: true, reason: 'no-events' };
|
|
71
|
+
}
|
|
72
|
+
if (!options.silent) {
|
|
73
|
+
process.stderr.write(`[autopilot] upload failed: ${result.error}\n`);
|
|
74
|
+
process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
|
|
75
|
+
}
|
|
76
|
+
return { attempted: true, ok: false, url: null, skipped: false, reason: 'error' };
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (ac.signal.aborted) {
|
|
80
|
+
if (!options.silent) {
|
|
81
|
+
process.stderr.write(`\n[autopilot] upload interrupted. Run is saved locally.\n`);
|
|
82
|
+
process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
|
|
83
|
+
}
|
|
84
|
+
return { attempted: true, ok: false, url: null, skipped: false, reason: 'aborted' };
|
|
85
|
+
}
|
|
86
|
+
// Phase 3 — runs/storage cap reached. Print a friendly message but
|
|
87
|
+
// do NOT print the resume hint (resume would just hit 402 again until
|
|
88
|
+
// the user upgrades) and do NOT bubble the error so the run's exit
|
|
89
|
+
// code is preserved.
|
|
90
|
+
if (err instanceof UploadLimitError) {
|
|
91
|
+
if (!options.silent) {
|
|
92
|
+
process.stderr.write(`[autopilot] ${err.message}\n`);
|
|
93
|
+
}
|
|
94
|
+
return { attempted: true, ok: false, url: null, skipped: false, reason: 'limit-reached' };
|
|
95
|
+
}
|
|
96
|
+
if (!options.silent) {
|
|
97
|
+
process.stderr.write(`[autopilot] upload error: ${err.message}\n`);
|
|
98
|
+
process.stderr.write(` Resume with: claude-autopilot dashboard upload ${runId}\n`);
|
|
99
|
+
}
|
|
100
|
+
return { attempted: true, ok: false, url: null, skipped: false, reason: 'error' };
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
process.off('SIGINT', sigintHandler);
|
|
104
|
+
process.off('SIGTERM', sigintHandler);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=auto-upload.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface DashboardConfig {
|
|
2
|
+
schemaVersion: 1;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
accountEmail: string;
|
|
6
|
+
loggedInAt: string;
|
|
7
|
+
lastUploadAt: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function getConfigDir(): string;
|
|
10
|
+
export declare function getConfigPath(): string;
|
|
11
|
+
export declare function readConfig(): Promise<DashboardConfig | null>;
|
|
12
|
+
export declare function writeConfig(config: DashboardConfig): Promise<void>;
|
|
13
|
+
export declare function deleteConfig(): Promise<void>;
|
|
14
|
+
export declare function getAutopilotBaseUrl(): string;
|
|
15
|
+
export declare function _resetAutopilotBaseUrlWarning(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Returns a warning string if the config file is group/world-readable on
|
|
18
|
+
* a POSIX filesystem; null otherwise (or on Windows, where mode bits
|
|
19
|
+
* don't apply meaningfully).
|
|
20
|
+
*/
|
|
21
|
+
export declare function warnIfPermissive(): Promise<string | null>;
|
|
22
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// CLI dashboard config — atomic read/write of ~/.claude-autopilot/dashboard.json.
|
|
2
|
+
//
|
|
3
|
+
// Codex plan-pass WARNING: respect CLAUDE_AUTOPILOT_HOME env override so
|
|
4
|
+
// tests + experimentation never touch the developer's real home dir.
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
const KEY_RE = /^clp_[0-9a-f]{64}$/;
|
|
9
|
+
function resolveHome() {
|
|
10
|
+
return process.env.CLAUDE_AUTOPILOT_HOME ?? path.join(os.homedir(), '.claude-autopilot');
|
|
11
|
+
}
|
|
12
|
+
export function getConfigDir() {
|
|
13
|
+
return resolveHome();
|
|
14
|
+
}
|
|
15
|
+
export function getConfigPath() {
|
|
16
|
+
return path.join(resolveHome(), 'dashboard.json');
|
|
17
|
+
}
|
|
18
|
+
export async function readConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(getConfigPath(), 'utf-8');
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (parsed.schemaVersion !== 1)
|
|
23
|
+
return null;
|
|
24
|
+
if (!KEY_RE.test(parsed.apiKey))
|
|
25
|
+
return null;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function writeConfig(config) {
|
|
33
|
+
if (!KEY_RE.test(config.apiKey)) {
|
|
34
|
+
throw new Error('writeConfig: invalid apiKey shape');
|
|
35
|
+
}
|
|
36
|
+
const dir = getConfigDir();
|
|
37
|
+
const file = getConfigPath();
|
|
38
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
39
|
+
// Try to tighten dir mode even if it already existed.
|
|
40
|
+
try {
|
|
41
|
+
await fs.chmod(dir, 0o700);
|
|
42
|
+
}
|
|
43
|
+
catch { /* best effort */ }
|
|
44
|
+
// Atomic write: temp-file + rename.
|
|
45
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
46
|
+
const payload = JSON.stringify(config, null, 2);
|
|
47
|
+
await fs.writeFile(tmp, payload, { mode: 0o600 });
|
|
48
|
+
await fs.chmod(tmp, 0o600);
|
|
49
|
+
await fs.rename(tmp, file);
|
|
50
|
+
}
|
|
51
|
+
export async function deleteConfig() {
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(getConfigPath());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* idempotent */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Phase 4 — resolve the dashboard / public base URL from env, with
|
|
61
|
+
* AUTOPILOT_PUBLIC_BASE_URL preferred and AUTOPILOT_DASHBOARD_BASE_URL
|
|
62
|
+
* accepted as a deprecated alias. Logs a one-time deprecation warning
|
|
63
|
+
* when only the older variable is set.
|
|
64
|
+
*
|
|
65
|
+
* Defaults to https://autopilot.dev when neither is present.
|
|
66
|
+
*/
|
|
67
|
+
let _deprecationWarned = false;
|
|
68
|
+
export function getAutopilotBaseUrl() {
|
|
69
|
+
const canonical = process.env.AUTOPILOT_PUBLIC_BASE_URL;
|
|
70
|
+
const legacy = process.env.AUTOPILOT_DASHBOARD_BASE_URL;
|
|
71
|
+
if (canonical)
|
|
72
|
+
return canonical;
|
|
73
|
+
if (legacy) {
|
|
74
|
+
if (!_deprecationWarned) {
|
|
75
|
+
_deprecationWarned = true;
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.warn('[autopilot] AUTOPILOT_DASHBOARD_BASE_URL is deprecated; ' +
|
|
78
|
+
'use AUTOPILOT_PUBLIC_BASE_URL instead. Both are accepted for now.');
|
|
79
|
+
}
|
|
80
|
+
return legacy;
|
|
81
|
+
}
|
|
82
|
+
return 'https://autopilot.dev';
|
|
83
|
+
}
|
|
84
|
+
// Test seam — reset the one-shot warning flag.
|
|
85
|
+
export function _resetAutopilotBaseUrlWarning() {
|
|
86
|
+
_deprecationWarned = false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns a warning string if the config file is group/world-readable on
|
|
90
|
+
* a POSIX filesystem; null otherwise (or on Windows, where mode bits
|
|
91
|
+
* don't apply meaningfully).
|
|
92
|
+
*/
|
|
93
|
+
export async function warnIfPermissive() {
|
|
94
|
+
if (process.platform === 'win32')
|
|
95
|
+
return null;
|
|
96
|
+
const file = getConfigPath();
|
|
97
|
+
try {
|
|
98
|
+
const stat = await fs.stat(file);
|
|
99
|
+
const mode = stat.mode & 0o777;
|
|
100
|
+
if ((mode & 0o077) !== 0) {
|
|
101
|
+
return `Warning: ${file} mode is ${mode.toString(8)} (group/world readable). Run: chmod 600 ${file}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* file doesn't exist, no warning */
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Parity copy of apps/web/lib/upload/canonical.ts (RFC 8785 / JCS).
|
|
2
|
+
// CLI ↔ web byte-equality asserted in tests/dashboard/parity.test.ts.
|
|
3
|
+
import canonicalize from 'canonicalize';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
export function canonicalJsonBytes(value) {
|
|
6
|
+
// canonicalize implements RFC 8785 (JCS). Returns undefined only for
|
|
7
|
+
// inputs JSON cannot represent at the root; coerce to '' so callers
|
|
8
|
+
// always get a Buffer.
|
|
9
|
+
const str = canonicalize(value) ?? '';
|
|
10
|
+
return Buffer.from(str, 'utf-8');
|
|
11
|
+
}
|
|
12
|
+
export function sha256OfCanonical(value) {
|
|
13
|
+
const bytes = canonicalJsonBytes(value);
|
|
14
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=canonical.js.map
|