@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,201 @@
|
|
|
1
|
+
// src/cli/json-mode.ts
|
|
2
|
+
//
|
|
3
|
+
// v6 Phase 5 — strict --json channel discipline.
|
|
4
|
+
//
|
|
5
|
+
// When --json is set, the spec mandates:
|
|
6
|
+
// - stdout: exactly one JSON envelope per command invocation.
|
|
7
|
+
// - stderr: only NDJSON event lines. No human-readable warnings, no color.
|
|
8
|
+
// - All warnings / prompts / human diagnostics route to typed events.
|
|
9
|
+
// - Interactive prompts hard-fail with exit:78.
|
|
10
|
+
//
|
|
11
|
+
// Many existing CLI handlers call `console.log` / `console.error` /
|
|
12
|
+
// `console.warn` / `process.stdout.write` / `process.stderr.write` directly
|
|
13
|
+
// for human output. Migrating each one to thread a json flag and switch
|
|
14
|
+
// behavior would be a multi-thousand-line patch. Instead, we install a
|
|
15
|
+
// channel-discipline shim BEFORE the handler runs that captures every
|
|
16
|
+
// non-NDJSON-shaped write and reroutes it:
|
|
17
|
+
//
|
|
18
|
+
// - stdout writes that aren't valid JSON → captured into messages[]
|
|
19
|
+
// (will be attached to the
|
|
20
|
+
// final envelope by the
|
|
21
|
+
// dispatcher).
|
|
22
|
+
// - stdout writes that ARE valid JSON → captured raw (the dispatcher
|
|
23
|
+
// will pick the LAST one as
|
|
24
|
+
// the envelope, on the
|
|
25
|
+
// assumption that handlers
|
|
26
|
+
// that already emit a JSON
|
|
27
|
+
// envelope want it to win).
|
|
28
|
+
// - stderr writes that are NDJSON → passed through as-is.
|
|
29
|
+
// - stderr writes that aren't NDJSON → wrapped in a synthetic
|
|
30
|
+
// run.warning event and
|
|
31
|
+
// re-emitted as NDJSON.
|
|
32
|
+
//
|
|
33
|
+
// Console wrappers route through the same shim:
|
|
34
|
+
// console.log -> stdout shim
|
|
35
|
+
// console.error / console.warn -> stderr shim (with level metadata)
|
|
36
|
+
//
|
|
37
|
+
// All ANSI color codes are stripped on the way out.
|
|
38
|
+
//
|
|
39
|
+
// The shim is restorable — the dispatcher calls `restore()` after the
|
|
40
|
+
// handler completes so test runs (which spawn many handlers in-process) get
|
|
41
|
+
// a clean slate.
|
|
42
|
+
import { __getChannelTestSink, stripAnsi, syntheticRunWarning } from "./json-envelope.js";
|
|
43
|
+
/** Try to JSON.parse a chunk. Returns the parsed value on success, undefined
|
|
44
|
+
* on failure. We strip a trailing newline before parsing so handlers writing
|
|
45
|
+
* one envelope per line are accepted. */
|
|
46
|
+
function tryParse(chunk) {
|
|
47
|
+
const trimmed = chunk.trim();
|
|
48
|
+
if (trimmed.length === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(trimmed);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Coerce an unknown buffer/string into a string. Mirrors Node's stream
|
|
58
|
+
* semantics where writes accept either. */
|
|
59
|
+
function toText(chunk) {
|
|
60
|
+
if (typeof chunk === 'string')
|
|
61
|
+
return chunk;
|
|
62
|
+
if (chunk instanceof Uint8Array)
|
|
63
|
+
return Buffer.from(chunk).toString('utf8');
|
|
64
|
+
return String(chunk);
|
|
65
|
+
}
|
|
66
|
+
/** Install the channel-discipline shim. Returns a handle the caller uses to
|
|
67
|
+
* collect captured output and to restore the original state.
|
|
68
|
+
*
|
|
69
|
+
* Safe to call when --json is OFF — the function is a no-op in that case
|
|
70
|
+
* (returns a handle whose restore() does nothing and whose buffers stay
|
|
71
|
+
* empty). Callers should still respect the active flag and skip the
|
|
72
|
+
* envelope emission in text mode. */
|
|
73
|
+
export function installJsonModeChannelDiscipline(opts = { active: true }) {
|
|
74
|
+
const capturedMessages = [];
|
|
75
|
+
const capturedJsonStdout = [];
|
|
76
|
+
if (!opts.active) {
|
|
77
|
+
return {
|
|
78
|
+
restore: () => { },
|
|
79
|
+
capturedMessages,
|
|
80
|
+
capturedJsonStdout,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const sink = __getChannelTestSink();
|
|
84
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
85
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
86
|
+
// When a test sink is installed, the shim still wraps process streams so
|
|
87
|
+
// user code goes through capture, but pass-through writes (NDJSON we want
|
|
88
|
+
// to surface as-is) go to the sink so tests can read them deterministically
|
|
89
|
+
// without interleaving with the test runner's own TAP output on the real
|
|
90
|
+
// streams.
|
|
91
|
+
const writeStderr = (line) => {
|
|
92
|
+
if (sink)
|
|
93
|
+
sink.stderr(line);
|
|
94
|
+
else
|
|
95
|
+
origStderrWrite(line);
|
|
96
|
+
};
|
|
97
|
+
const origConsoleLog = console.log;
|
|
98
|
+
const origConsoleError = console.error;
|
|
99
|
+
const origConsoleWarn = console.warn;
|
|
100
|
+
const origConsoleInfo = console.info;
|
|
101
|
+
const origConsoleDebug = console.debug;
|
|
102
|
+
/** Splits a chunk on newlines, tries to JSON.parse each non-empty line.
|
|
103
|
+
* Lines that parse as JSON are recorded as JSON envelopes; the rest are
|
|
104
|
+
* recorded as captured text messages with the given level. */
|
|
105
|
+
function captureStdout(chunk, level) {
|
|
106
|
+
const cleaned = stripAnsi(chunk);
|
|
107
|
+
// Stdout chunks may contain multiple JSON envelopes back-to-back; split
|
|
108
|
+
// on newlines and parse each line. We treat the WHOLE chunk as a single
|
|
109
|
+
// text message if no line parses (preserves multi-line formatted text).
|
|
110
|
+
const lines = cleaned.split('\n');
|
|
111
|
+
let anyParsed = false;
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const parsed = tryParse(line);
|
|
114
|
+
if (parsed !== undefined) {
|
|
115
|
+
capturedJsonStdout.push(parsed);
|
|
116
|
+
anyParsed = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!anyParsed && cleaned.trim().length > 0) {
|
|
120
|
+
capturedMessages.push({ level, text: cleaned.replace(/\n+$/, '') });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Splits a chunk on newlines for stderr. NDJSON lines pass through to the
|
|
124
|
+
* real stderr; non-NDJSON lines are wrapped in synthetic run.warning
|
|
125
|
+
* events and re-emitted. */
|
|
126
|
+
function captureStderr(chunk, level) {
|
|
127
|
+
const cleaned = stripAnsi(chunk);
|
|
128
|
+
const lines = cleaned.split('\n');
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
if (line.length === 0)
|
|
131
|
+
continue;
|
|
132
|
+
const parsed = tryParse(line);
|
|
133
|
+
if (parsed !== undefined) {
|
|
134
|
+
// Pass through as-is (with trailing newline) — already NDJSON.
|
|
135
|
+
writeStderr(line + '\n');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const ev = syntheticRunWarning(line, { level });
|
|
139
|
+
writeStderr(JSON.stringify(ev) + '\n');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Wrap process.stdout.write. Note: returning true keeps the stream
|
|
144
|
+
// signature truthy; we drop the original write entirely under JSON mode
|
|
145
|
+
// because every byte is captured and re-routed via the envelope.
|
|
146
|
+
process.stdout.write = ((chunk, ...rest) => {
|
|
147
|
+
captureStdout(toText(chunk), 'log');
|
|
148
|
+
// Honor the optional callback signature so caller code that passes one
|
|
149
|
+
// (rare in our handlers but possible) doesn't hang.
|
|
150
|
+
const cb = rest.find(r => typeof r === 'function');
|
|
151
|
+
if (cb)
|
|
152
|
+
cb();
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
155
|
+
process.stderr.write = ((chunk, ...rest) => {
|
|
156
|
+
captureStderr(toText(chunk), 'warn');
|
|
157
|
+
const cb = rest.find(r => typeof r === 'function');
|
|
158
|
+
if (cb)
|
|
159
|
+
cb();
|
|
160
|
+
return true;
|
|
161
|
+
});
|
|
162
|
+
console.log = (...args) => {
|
|
163
|
+
captureStdout(args.map(toText).join(' ') + '\n', 'log');
|
|
164
|
+
};
|
|
165
|
+
console.info = (...args) => {
|
|
166
|
+
captureStdout(args.map(toText).join(' ') + '\n', 'info');
|
|
167
|
+
};
|
|
168
|
+
console.debug = (...args) => {
|
|
169
|
+
captureStdout(args.map(toText).join(' ') + '\n', 'debug');
|
|
170
|
+
};
|
|
171
|
+
console.warn = (...args) => {
|
|
172
|
+
captureStderr(args.map(toText).join(' ') + '\n', 'warn');
|
|
173
|
+
};
|
|
174
|
+
console.error = (...args) => {
|
|
175
|
+
captureStderr(args.map(toText).join(' ') + '\n', 'error');
|
|
176
|
+
};
|
|
177
|
+
let restored = false;
|
|
178
|
+
function restore() {
|
|
179
|
+
if (restored)
|
|
180
|
+
return;
|
|
181
|
+
restored = true;
|
|
182
|
+
process.stdout.write = origStdoutWrite;
|
|
183
|
+
process.stderr.write = origStderrWrite;
|
|
184
|
+
console.log = origConsoleLog;
|
|
185
|
+
console.error = origConsoleError;
|
|
186
|
+
console.warn = origConsoleWarn;
|
|
187
|
+
console.info = origConsoleInfo;
|
|
188
|
+
console.debug = origConsoleDebug;
|
|
189
|
+
}
|
|
190
|
+
return { restore, capturedMessages, capturedJsonStdout };
|
|
191
|
+
}
|
|
192
|
+
/** Build a ChannelOptions value from the parsed --json flag plus
|
|
193
|
+
* process.stdin TTY-ness. The caller (dispatcher) usually wants the
|
|
194
|
+
* computed nonInteractive flag for prompt-or-hard-fail decisions. */
|
|
195
|
+
export function computeChannelOptions(json) {
|
|
196
|
+
return {
|
|
197
|
+
json,
|
|
198
|
+
nonInteractive: json || !process.stdin.isTTY,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=json-mode.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { GuardrailConfig } from '../core/config/types.ts';
|
|
2
|
+
import { type RunPhase } from '../core/run-state/phase-runner.ts';
|
|
3
|
+
import type { ResultArtifact } from '../core/migrate/types.ts';
|
|
4
|
+
export interface MigrateCommandOptions {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
configPath?: string;
|
|
7
|
+
/** Target environment from `.autopilot/stack.md`. Defaults to `dev`. */
|
|
8
|
+
env?: string;
|
|
9
|
+
/** When true, the dispatcher passes `dryRun: true` through the envelope so
|
|
10
|
+
* the skill executes a no-side-effect plan rather than applying. */
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
/** `--yes` — required to apply prod migrations in CI per policy. */
|
|
13
|
+
yesFlag?: boolean;
|
|
14
|
+
/** `--non-interactive` / `--json` / not-a-TTY equivalent. */
|
|
15
|
+
nonInteractive?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* v6.0.8 — engine knob inputs. Same precedence as scan / costs / fix /
|
|
18
|
+
* plan / review / validate (CLI > env > config > built-in default off).
|
|
19
|
+
*/
|
|
20
|
+
cliEngine?: boolean;
|
|
21
|
+
envEngine?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Test-only seam — replaces the real dispatcher with a fake so smoke
|
|
24
|
+
* tests can exercise the engine-wrap path without spawning a child
|
|
25
|
+
* process or hitting a real database. Production callers MUST NOT
|
|
26
|
+
* pass this; the CLI dispatcher in `src/cli/index.ts` does not expose
|
|
27
|
+
* a flag that sets it. Underscore-prefixed for grep-ability.
|
|
28
|
+
*/
|
|
29
|
+
__testDispatch?: (input: MigrateInput) => Promise<ResultArtifact>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Phase input — captured as a struct so the engine path's phase body matches
|
|
33
|
+
* the engine-off path's call signature.
|
|
34
|
+
*
|
|
35
|
+
* Exported so the v6.2.1 orchestrator's phase registry can carry the typed
|
|
36
|
+
* I/O shape on its `PhaseRegistration<MigrateInput, MigrateOutput>` slot.
|
|
37
|
+
*/
|
|
38
|
+
export interface MigrateInput {
|
|
39
|
+
cwd: string;
|
|
40
|
+
env: string;
|
|
41
|
+
dryRun: boolean;
|
|
42
|
+
yesFlag: boolean;
|
|
43
|
+
nonInteractive: boolean;
|
|
44
|
+
/** Runtime version string (from package.json) — required by the
|
|
45
|
+
* dispatcher's manifest handshake. Resolved in the outer scope so the
|
|
46
|
+
* phase body stays a pure await on `dispatch()`. */
|
|
47
|
+
runtimeVersion: string;
|
|
48
|
+
/** v6.2.1 — dispatcher seam plumbed into the phase body so the wrap can
|
|
49
|
+
* emit the pre-effect breadcrumb BEFORE invoking the dispatcher. The
|
|
50
|
+
* outer scope assembles either the real `runMigrateDispatch` or the
|
|
51
|
+
* test-only `__testDispatch`; the phase body just calls `dispatchFn`. */
|
|
52
|
+
dispatchFn: (input: MigrateInput) => Promise<ResultArtifact>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Phase output — JSON-serializable summary suitable for persistence as
|
|
56
|
+
* `result` on phases/migrate.json. A future skip-already-applied (Phase 6)
|
|
57
|
+
* could reconstruct the dispatch outcome without re-running by reading the
|
|
58
|
+
* persisted externalRefs + this result.
|
|
59
|
+
*
|
|
60
|
+
* Exported alongside `MigrateInput` for the registry's typed I/O slot.
|
|
61
|
+
*/
|
|
62
|
+
export interface MigrateOutput {
|
|
63
|
+
/** Status from the result artifact (applied | skipped | error | ...). */
|
|
64
|
+
status: ResultArtifact['status'];
|
|
65
|
+
/** Reason code from the result artifact (migration-applied,
|
|
66
|
+
* migration-disabled, env-not-configured, etc.). */
|
|
67
|
+
reasonCode: string;
|
|
68
|
+
/** List of migrations applied this run (empty on `skipped` / `error`). */
|
|
69
|
+
appliedMigrations: string[];
|
|
70
|
+
/** Operator-facing next-action hints surfaced by the skill. */
|
|
71
|
+
nextActions: string[];
|
|
72
|
+
/** Echoed env so the render layer / skip-already-applied has it. */
|
|
73
|
+
env: string;
|
|
74
|
+
}
|
|
75
|
+
/** v6.2.1 — early-exit / build-result discriminants (parity with the v6.2.0
|
|
76
|
+
* builders in scan / spec / plan / implement). `migrate` has no early-exit
|
|
77
|
+
* branches today but the discriminant is included for shape parity. */
|
|
78
|
+
export interface BuildMigratePhaseEarlyExit {
|
|
79
|
+
kind: 'early-exit';
|
|
80
|
+
exitCode: number;
|
|
81
|
+
}
|
|
82
|
+
export interface BuildMigratePhaseResult {
|
|
83
|
+
kind: 'phase';
|
|
84
|
+
phase: RunPhase<MigrateInput, MigrateOutput>;
|
|
85
|
+
input: MigrateInput;
|
|
86
|
+
config: GuardrailConfig;
|
|
87
|
+
renderResult: (output: MigrateOutput) => number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* v6.2.1 — extract the `RunPhase<MigrateInput, MigrateOutput>` construction
|
|
91
|
+
* out of `runMigrate(options)` so the new top-level `autopilot` orchestrator
|
|
92
|
+
* can drive `runPhase` itself with a shared `phaseIdx` against the same run
|
|
93
|
+
* dir. Mirrors the v6.2.0 builder pattern in scan / spec / plan / implement.
|
|
94
|
+
*
|
|
95
|
+
* This builder ALSO closes the v6.2.1 idempotency contract gap on `migrate`:
|
|
96
|
+
* the phase body now emits a `migration-batch` externalRef BEFORE invoking
|
|
97
|
+
* the dispatcher. See the long rationale on `executeMigratePhase` below.
|
|
98
|
+
*
|
|
99
|
+
* The `lastResultArtifact` ref is the seam through which `runMigrate` gets
|
|
100
|
+
* the full `ResultArtifact` for its --json envelope (the JSON-serializable
|
|
101
|
+
* `MigrateOutput` is a compact subset; the full artifact has nonce,
|
|
102
|
+
* contractVersion, sideEffectsPerformed, etc.). Builder callers that don't
|
|
103
|
+
* need the artifact can ignore it.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildMigratePhase(options: MigrateCommandOptions): Promise<BuildMigratePhaseResult | BuildMigratePhaseEarlyExit>;
|
|
106
|
+
export declare function runMigrate(options?: MigrateCommandOptions): Promise<{
|
|
107
|
+
exitCode: number;
|
|
108
|
+
/** Surfaced for the CLI dispatcher's `--json` payload callback. */
|
|
109
|
+
result: ResultArtifact | null;
|
|
110
|
+
}>;
|
|
111
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/cli/migrate.ts
|
|
2
|
+
//
|
|
3
|
+
// v6.0.8 — engine-wrap shell for the `migrate` pipeline phase. Runs the
|
|
4
|
+
// stack-aware migrate dispatcher (`src/core/migrate/dispatcher.ts`) inside
|
|
5
|
+
// a `RunPhase<MigrateInput, MigrateOutput>` so v6 pipeline runs check-
|
|
6
|
+
// point a `migrate` phase entry alongside `plan`, `review`, and
|
|
7
|
+
// `validate`.
|
|
8
|
+
//
|
|
9
|
+
// migrate is the FIRST side-effecting phase to land under
|
|
10
|
+
// `runPhaseWithLifecycle` (followed by `implement` and `pr` in v6.0.7 +
|
|
11
|
+
// v6.0.9 — see the rebase contract in PR #102 / #103). The wrap declares:
|
|
12
|
+
//
|
|
13
|
+
// idempotent: false (per spec table at docs/specs/v6-run-state-engine.md)
|
|
14
|
+
// hasSideEffects: true (applies migrations against a database)
|
|
15
|
+
// externalRefs: migration-version (one per applied migration / per env)
|
|
16
|
+
//
|
|
17
|
+
// Why `idempotent: false` even though the underlying skill is ledger-
|
|
18
|
+
// guarded:
|
|
19
|
+
// The Delegance migrate skill (and all conforming `migrate@1` skills)
|
|
20
|
+
// tracks applied migrations in a ledger table — re-running the verb
|
|
21
|
+
// against a database that already has a given migration applied is a
|
|
22
|
+
// no-op (the dispatcher returns `status: 'skipped'`, `reasonCode:
|
|
23
|
+
// migration-disabled` or skill-specific equivalent). So at the
|
|
24
|
+
// *outcome* layer, replay is safe.
|
|
25
|
+
//
|
|
26
|
+
// At the *engine semantics* layer, however, `idempotent: true` means
|
|
27
|
+
// "re-running the phase against the same input produces equivalent
|
|
28
|
+
// output." A dispatch invocation that previously applied N migrations
|
|
29
|
+
// on attempt 1 and applies 0 on attempt 2 (everything already in the
|
|
30
|
+
// ledger) DOES produce different output (different `appliedMigrations`
|
|
31
|
+
// list, different `status`). The spec table's `idempotent: false` is
|
|
32
|
+
// the right declaration.
|
|
33
|
+
//
|
|
34
|
+
// The practical consequence: when a prior `phase.success` exists for
|
|
35
|
+
// `migrate` and the engine is asked to retry, it consults the
|
|
36
|
+
// persisted `externalRefs` (`migration-version` entries) to decide
|
|
37
|
+
// whether to skip-already-applied or retry. Phase 6 will wire the
|
|
38
|
+
// read-back to live `migration_state` queries; until then, retries on
|
|
39
|
+
// side-effecting phases require `--force-replay`. Documented in
|
|
40
|
+
// docs/v6/migration-guide.md "Idempotency + replay rules".
|
|
41
|
+
//
|
|
42
|
+
// Why `hasSideEffects: true`:
|
|
43
|
+
// Migrations mutate database schema / seed data. The dispatcher writes
|
|
44
|
+
// audit log entries, schema cache refreshes, types regeneration. The
|
|
45
|
+
// engine's "no replay without read-back" gate is exactly what we want.
|
|
46
|
+
//
|
|
47
|
+
// `migration-version` externalRefs:
|
|
48
|
+
// For every migration name in `result.appliedMigrations`, we emit a
|
|
49
|
+
// `phase.externalRef` event with `kind: 'migration-version'` and `id`
|
|
50
|
+
// shaped as `<env>:<migration_name>`. The `<env>:` prefix scopes the
|
|
51
|
+
// ref by target environment (dev / qa / prod) so multi-env pipelines
|
|
52
|
+
// can read back per-env state. Phase 6's read-back rule will compare
|
|
53
|
+
// the persisted set to the live ledger to decide skip-already-applied
|
|
54
|
+
// vs retry vs needs-human.
|
|
55
|
+
//
|
|
56
|
+
// Engine-off path (default through v6.0.x): byte-for-byte identical to
|
|
57
|
+
// the pre-v6.0.8 inline dispatch case in `src/cli/index.ts`. The
|
|
58
|
+
// `runEngineOff` callback supplied to `runPhaseWithLifecycle` invokes
|
|
59
|
+
// the same dispatch + render shape that the legacy code path used. CI /
|
|
60
|
+
// scripts that don't pass `--engine` are unaffected.
|
|
61
|
+
import * as path from 'node:path';
|
|
62
|
+
import * as fs from 'node:fs';
|
|
63
|
+
import * as crypto from 'node:crypto';
|
|
64
|
+
import { loadConfig } from "../core/config/loader.js";
|
|
65
|
+
import { runPhaseWithLifecycle } from "../core/run-state/run-phase-with-lifecycle.js";
|
|
66
|
+
import { dispatch as runMigrateDispatch } from "../core/migrate/dispatcher.js";
|
|
67
|
+
import { findPackageRoot } from "./_pkg-root.js";
|
|
68
|
+
const C = {
|
|
69
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
70
|
+
green: '\x1b[32m', red: '\x1b[31m', cyan: '\x1b[36m',
|
|
71
|
+
};
|
|
72
|
+
const fmt = (c, t) => `${C[c]}${t}${C.reset}`;
|
|
73
|
+
/**
|
|
74
|
+
* v6.2.1 — extract the `RunPhase<MigrateInput, MigrateOutput>` construction
|
|
75
|
+
* out of `runMigrate(options)` so the new top-level `autopilot` orchestrator
|
|
76
|
+
* can drive `runPhase` itself with a shared `phaseIdx` against the same run
|
|
77
|
+
* dir. Mirrors the v6.2.0 builder pattern in scan / spec / plan / implement.
|
|
78
|
+
*
|
|
79
|
+
* This builder ALSO closes the v6.2.1 idempotency contract gap on `migrate`:
|
|
80
|
+
* the phase body now emits a `migration-batch` externalRef BEFORE invoking
|
|
81
|
+
* the dispatcher. See the long rationale on `executeMigratePhase` below.
|
|
82
|
+
*
|
|
83
|
+
* The `lastResultArtifact` ref is the seam through which `runMigrate` gets
|
|
84
|
+
* the full `ResultArtifact` for its --json envelope (the JSON-serializable
|
|
85
|
+
* `MigrateOutput` is a compact subset; the full artifact has nonce,
|
|
86
|
+
* contractVersion, sideEffectsPerformed, etc.). Builder callers that don't
|
|
87
|
+
* need the artifact can ignore it.
|
|
88
|
+
*/
|
|
89
|
+
export async function buildMigratePhase(options) {
|
|
90
|
+
const cwd = options.cwd ?? process.cwd();
|
|
91
|
+
const configPath = options.configPath ?? path.join(cwd, 'guardrail.config.yaml');
|
|
92
|
+
let config = { configVersion: 1 };
|
|
93
|
+
if (fs.existsSync(configPath)) {
|
|
94
|
+
const loaded = await loadConfig(configPath);
|
|
95
|
+
if (loaded)
|
|
96
|
+
config = loaded;
|
|
97
|
+
}
|
|
98
|
+
const envName = options.env ?? 'dev';
|
|
99
|
+
const dryRun = options.dryRun ?? false;
|
|
100
|
+
const yesFlag = options.yesFlag ?? false;
|
|
101
|
+
const nonInteractive = options.nonInteractive ?? !process.stdin.isTTY;
|
|
102
|
+
// Read package version for the runtime handshake. The CLI dispatcher used
|
|
103
|
+
// to do this inline; we keep the same lookup shape so the engine-off path
|
|
104
|
+
// is byte-for-byte identical to v6.0.7.
|
|
105
|
+
const root = findPackageRoot(import.meta.url);
|
|
106
|
+
let runtimeVersion = 'unknown';
|
|
107
|
+
if (root) {
|
|
108
|
+
try {
|
|
109
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
110
|
+
runtimeVersion = pkg.version;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* fall through with 'unknown' — handshake will fail closed */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const dispatchFn = options.__testDispatch
|
|
117
|
+
?? (async (input) => {
|
|
118
|
+
return runMigrateDispatch({
|
|
119
|
+
repoRoot: input.cwd,
|
|
120
|
+
env: input.env,
|
|
121
|
+
yesFlag: input.yesFlag,
|
|
122
|
+
nonInteractive: input.nonInteractive,
|
|
123
|
+
currentRuntimeVersion: input.runtimeVersion,
|
|
124
|
+
dryRun: input.dryRun,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
const migrateInput = {
|
|
128
|
+
cwd,
|
|
129
|
+
env: envName,
|
|
130
|
+
dryRun,
|
|
131
|
+
yesFlag,
|
|
132
|
+
nonInteractive,
|
|
133
|
+
runtimeVersion,
|
|
134
|
+
dispatchFn,
|
|
135
|
+
};
|
|
136
|
+
const phase = {
|
|
137
|
+
name: 'migrate',
|
|
138
|
+
// See top-of-file rationale. The spec table at line 162 of
|
|
139
|
+
// docs/specs/v6-run-state-engine.md declares idempotent: false
|
|
140
|
+
// because dispatch output varies by ledger state — the v6.0.8
|
|
141
|
+
// wrap matches that declaration. The underlying skill IS
|
|
142
|
+
// ledger-guarded against double-apply; that's a property of the
|
|
143
|
+
// skill, not of the phase contract. With `hasSideEffects: true`
|
|
144
|
+
// and persisted `migration-batch` (pre-effect) + `migration-version`
|
|
145
|
+
// (post-effect) externalRefs, Phase 6's resume gate reads back the
|
|
146
|
+
// live migration_state to decide skip-already-applied vs retry vs
|
|
147
|
+
// needs-human.
|
|
148
|
+
idempotent: false,
|
|
149
|
+
hasSideEffects: true,
|
|
150
|
+
run: async (input, ctx) => executeMigratePhase(input, ctx),
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
kind: 'phase',
|
|
154
|
+
phase,
|
|
155
|
+
input: migrateInput,
|
|
156
|
+
config,
|
|
157
|
+
renderResult: (output) => renderMigrateOutput(output),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function runMigrate(options = {}) {
|
|
161
|
+
const built = await buildMigratePhase(options);
|
|
162
|
+
if (built.kind === 'early-exit') {
|
|
163
|
+
return { exitCode: built.exitCode, result: null };
|
|
164
|
+
}
|
|
165
|
+
const { phase, input: migrateInput, config, renderResult } = built;
|
|
166
|
+
// Outer ref so the render path + the wrapper's --json envelope can
|
|
167
|
+
// surface the full ResultArtifact. Updated by `executeMigratePhase` on
|
|
168
|
+
// every dispatch (engine-on and engine-off paths share the same body).
|
|
169
|
+
let resultArtifact = null;
|
|
170
|
+
const captureInput = {
|
|
171
|
+
...migrateInput,
|
|
172
|
+
dispatchFn: async (inp) => {
|
|
173
|
+
const artifact = await migrateInput.dispatchFn(inp);
|
|
174
|
+
resultArtifact = artifact;
|
|
175
|
+
return artifact;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
// v6.0.6+ — lifecycle wiring lives in `runPhaseWithLifecycle`. The
|
|
179
|
+
// helper owns engine resolution, createRun, run.complete, state.json
|
|
180
|
+
// refresh, and lock release. The caller just supplies the phase, the
|
|
181
|
+
// input, the loaded config, and an engine-off escape hatch.
|
|
182
|
+
let output;
|
|
183
|
+
try {
|
|
184
|
+
const lifecycleResult = await runPhaseWithLifecycle({
|
|
185
|
+
cwd: migrateInput.cwd,
|
|
186
|
+
phase,
|
|
187
|
+
input: captureInput,
|
|
188
|
+
config,
|
|
189
|
+
cliEngine: options.cliEngine,
|
|
190
|
+
envEngine: options.envEngine,
|
|
191
|
+
// Engine-off escape hatch — re-uses the same dispatchFn. We do
|
|
192
|
+
// NOT thread a real ctx through here because the engine-off path
|
|
193
|
+
// has no event ledger to write into; externalRefs only matter on
|
|
194
|
+
// the engine path. The artifact still lands on `resultArtifact`
|
|
195
|
+
// for the --json payload callback in the CLI dispatcher.
|
|
196
|
+
runEngineOff: () => executeMigratePhase(captureInput, null),
|
|
197
|
+
});
|
|
198
|
+
output = lifecycleResult.output;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Helper already printed `[migrate] engine: phase failed — <msg>`
|
|
202
|
+
// + the inspect hint, emitted run.complete failed, refreshed
|
|
203
|
+
// state.json, released the lock. Surface the legacy non-zero exit.
|
|
204
|
+
return { exitCode: 1, result: resultArtifact };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
exitCode: renderResult(output),
|
|
208
|
+
result: resultArtifact,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Phase body — emit the pre-effect `migration-batch` breadcrumb, dispatch,
|
|
213
|
+
// then emit one post-effect `migration-version` ref per applied migration.
|
|
214
|
+
//
|
|
215
|
+
// v6.2.1 idempotency contract (per docs/specs/v6.2.1-side-effect-idempotency.md):
|
|
216
|
+
// 1. PRE-effect breadcrumb: a `migration-batch` ref BEFORE `dispatchFn` so
|
|
217
|
+
// a partial crash leaves a resume target. The orchestrator's resume
|
|
218
|
+
// preflight reads this back to distinguish "we started this batch" from
|
|
219
|
+
// "we never started any batch."
|
|
220
|
+
// 2. POST-effect reconciliation: one `migration-version` ref per applied
|
|
221
|
+
// migration AFTER dispatch returns successfully. These are authoritative
|
|
222
|
+
// for the readback's skip-already-applied decision.
|
|
223
|
+
//
|
|
224
|
+
// On the deterministic-id question: the spec prescribes
|
|
225
|
+
// `${env}:${sha256Hex(env + ':' + plannedMigrations.sort().join(','))}`
|
|
226
|
+
// which would let two runs of the same batch share an id (better resume
|
|
227
|
+
// semantics). That requires extracting `planMigrations(input)` from the
|
|
228
|
+
// dispatcher to surface the planned set without applying. The current
|
|
229
|
+
// dispatcher doesn't expose that pre-dispatch — every supported migrate skill
|
|
230
|
+
// (Delegance Supabase, Rails, Alembic, …) discovers its planned set inside
|
|
231
|
+
// the skill subprocess after the manifest handshake + envelope build, so a
|
|
232
|
+
// pre-dispatch list would require an across-the-board skill protocol change
|
|
233
|
+
// (a new "plan" verb that runs without side effects). That's out of scope
|
|
234
|
+
// for v6.2.1 — the spec explicitly approves the fallback breadcrumb form
|
|
235
|
+
// `${env}:pre-dispatch:${Date.now()}` ("less idempotent but still
|
|
236
|
+
// recoverable"). The post-effect `migration-version` refs ARE deterministic
|
|
237
|
+
// (`${env}:${migration}`) and remain authoritative for the readback's
|
|
238
|
+
// skip-already-applied decision; the batch ref's role is purely "did we
|
|
239
|
+
// start this work?" — non-deterministic ids don't break that contract.
|
|
240
|
+
//
|
|
241
|
+
// Cross-run dedup of the batch ref (e.g. recognizing two pre-dispatch
|
|
242
|
+
// breadcrumbs as the same operation) is gated on the deterministic id form
|
|
243
|
+
// and explicitly listed under v6.2.1 "out of scope."
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
async function executeMigratePhase(input, ctx) {
|
|
246
|
+
// PRE-effect breadcrumb. Recorded BEFORE the dispatcher so that even a
|
|
247
|
+
// partial crash leaves a resume target. Engine-off path has no ctx; the
|
|
248
|
+
// breadcrumb is then a no-op (same precedent as pr.ts and every other
|
|
249
|
+
// wrapped verb's engine-off path).
|
|
250
|
+
if (ctx) {
|
|
251
|
+
// Fallback id form per the v6.2.1 spec — see the long rationale comment
|
|
252
|
+
// above. `crypto` is imported even though only the timestamp variant is
|
|
253
|
+
// used today; it's reserved for the deterministic-id upgrade once a
|
|
254
|
+
// `planMigrations()` extraction lands.
|
|
255
|
+
void crypto;
|
|
256
|
+
ctx.emitExternalRef({
|
|
257
|
+
kind: 'migration-batch',
|
|
258
|
+
id: `${input.env}:pre-dispatch:${Date.now()}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const artifact = await input.dispatchFn(input);
|
|
262
|
+
// POST-effect reconciliation refs — one per applied migration. The id is
|
|
263
|
+
// shaped `<env>:<migration_name>` so multi-env pipelines (dev → qa → prod)
|
|
264
|
+
// can disambiguate the same migration across targets. Phase 6's read-back
|
|
265
|
+
// rule compares this set to the live ledger.
|
|
266
|
+
if (ctx) {
|
|
267
|
+
for (const migration of artifact.appliedMigrations) {
|
|
268
|
+
ctx.emitExternalRef({
|
|
269
|
+
kind: 'migration-version',
|
|
270
|
+
id: `${input.env}:${migration}`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
status: artifact.status,
|
|
276
|
+
reasonCode: artifact.reasonCode,
|
|
277
|
+
appliedMigrations: artifact.appliedMigrations,
|
|
278
|
+
nextActions: artifact.nextActions,
|
|
279
|
+
env: input.env,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Render — translate MigrateOutput back to the legacy stdout banner + exit
|
|
284
|
+
// code. Lives outside the wrapped phase because it's pure presentation; doing
|
|
285
|
+
// rendering inside the phase body would couple the engine path's idempotency
|
|
286
|
+
// to console output.
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
function renderMigrateOutput(output) {
|
|
289
|
+
const ok = output.status === 'applied' || output.status === 'skipped';
|
|
290
|
+
const color = ok ? C.green : C.red;
|
|
291
|
+
console.log(`${color}[migrate] status=${output.status} reason=${output.reasonCode}${C.reset}`);
|
|
292
|
+
if (output.appliedMigrations.length > 0) {
|
|
293
|
+
console.log(` applied: ${output.appliedMigrations.join(', ')}`);
|
|
294
|
+
}
|
|
295
|
+
if (output.nextActions.length > 0) {
|
|
296
|
+
console.log(` next: ${output.nextActions.join('; ')}`);
|
|
297
|
+
}
|
|
298
|
+
// Suppress unused-helper TS warning when fmt isn't called above (the dim /
|
|
299
|
+
// bold / cyan helpers are reserved for future render paths — keeping the
|
|
300
|
+
// import + fmt() reference parallel with other wrapped verbs makes
|
|
301
|
+
// bin-mods easier to read).
|
|
302
|
+
void fmt;
|
|
303
|
+
return ok ? 0 : 1;
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=migrate.js.map
|