@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,206 @@
|
|
|
1
|
+
// src/core/run-state/lock.ts
|
|
2
|
+
//
|
|
3
|
+
// Per-run advisory lock. Wraps `proper-lockfile` with a sidecar metadata file
|
|
4
|
+
// (`.lock-meta.json`) that records WHICH writer (pid + hostHash) owns the
|
|
5
|
+
// lock, so a second invocation can either fail-fast with a precise error or
|
|
6
|
+
// take over with `forceTakeover()`.
|
|
7
|
+
//
|
|
8
|
+
// proper-lockfile itself only stores `mtime`; it doesn't track owner identity,
|
|
9
|
+
// so we maintain it ourselves alongside the lock.
|
|
10
|
+
//
|
|
11
|
+
// Spec: docs/specs/v6-run-state-engine.md "Persistence protocol — Per-run
|
|
12
|
+
// advisory lock", "Single-writer invariant".
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import lockfile from 'proper-lockfile';
|
|
18
|
+
import { GuardrailError } from "../errors.js";
|
|
19
|
+
/** File proper-lockfile guards. We pin a specific name so relocation /
|
|
20
|
+
* copy of the run dir doesn't accidentally inherit a stale lock from
|
|
21
|
+
* another path. */
|
|
22
|
+
const LOCK_TARGET = '.lock';
|
|
23
|
+
/** Sidecar JSON that records the current owner. Kept separate from the
|
|
24
|
+
* proper-lockfile-managed `.lock` directory so we never race the
|
|
25
|
+
* acquisition primitive. */
|
|
26
|
+
const LOCK_META = '.lock-meta.json';
|
|
27
|
+
/** Default stale timeout. After this many ms with no `update`, the lock is
|
|
28
|
+
* considered stale and another writer may acquire. Matches proper-lockfile
|
|
29
|
+
* default (10s). */
|
|
30
|
+
const DEFAULT_STALE_MS = 10_000;
|
|
31
|
+
/** Hash the hostname so we never persist raw machine identity. */
|
|
32
|
+
export function makeWriterId() {
|
|
33
|
+
return {
|
|
34
|
+
pid: process.pid,
|
|
35
|
+
hostHash: crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function lockTargetPath(runDir) {
|
|
39
|
+
return path.join(runDir, LOCK_TARGET);
|
|
40
|
+
}
|
|
41
|
+
function metaPath(runDir) {
|
|
42
|
+
return path.join(runDir, LOCK_META);
|
|
43
|
+
}
|
|
44
|
+
function writeMeta(runDir, meta) {
|
|
45
|
+
fs.writeFileSync(metaPath(runDir), JSON.stringify(meta, null, 2), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
function readMeta(runDir) {
|
|
48
|
+
const p = metaPath(runDir);
|
|
49
|
+
if (!fs.existsSync(p))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function deleteMeta(runDir) {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(metaPath(runDir));
|
|
61
|
+
}
|
|
62
|
+
catch { /* idempotent */ }
|
|
63
|
+
}
|
|
64
|
+
/** True iff a process with the given PID is alive on THIS host. We refuse
|
|
65
|
+
* to make a determination for off-host PIDs (different hostHash) and treat
|
|
66
|
+
* them as alive — better to bail with `lock_held` than to silently steal a
|
|
67
|
+
* lock owned by another machine sharing a network filesystem. */
|
|
68
|
+
export function isPidAlive(writerId) {
|
|
69
|
+
if (!writerId)
|
|
70
|
+
return false;
|
|
71
|
+
const me = makeWriterId();
|
|
72
|
+
if (writerId.hostHash !== me.hostHash) {
|
|
73
|
+
// Different host. We can't probe — assume alive (safer default).
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (writerId.pid <= 0)
|
|
77
|
+
return false;
|
|
78
|
+
if (writerId.pid === me.pid)
|
|
79
|
+
return true;
|
|
80
|
+
try {
|
|
81
|
+
// POSIX trick: kill(pid, 0) checks existence without delivering a signal.
|
|
82
|
+
process.kill(writerId.pid, 0);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// ESRCH = no such process → not alive. EPERM = exists but we can't
|
|
87
|
+
// signal it → still alive. Anything else, default to alive.
|
|
88
|
+
const code = err.code;
|
|
89
|
+
if (code === 'ESRCH')
|
|
90
|
+
return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Acquire the per-run advisory lock. Throws GuardrailError(lock_held) if
|
|
95
|
+
* another writer owns it. The caller is expected to hold the returned
|
|
96
|
+
* handle for the duration of the run and call `release()` on shutdown. */
|
|
97
|
+
export async function acquireRunLock(runDir, opts = {}) {
|
|
98
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
99
|
+
const target = lockTargetPath(runDir);
|
|
100
|
+
if (!fs.existsSync(target))
|
|
101
|
+
fs.writeFileSync(target, '');
|
|
102
|
+
const writerId = opts.writerId ?? makeWriterId();
|
|
103
|
+
const stale = opts.stale ?? DEFAULT_STALE_MS;
|
|
104
|
+
let release;
|
|
105
|
+
try {
|
|
106
|
+
release = await lockfile.lock(target, {
|
|
107
|
+
stale,
|
|
108
|
+
retries: opts.retries ?? 0,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// Fail-closed with a typed error so callers can build a good message.
|
|
113
|
+
const owner = readMeta(runDir);
|
|
114
|
+
throw new GuardrailError(`run lock held: cannot acquire ${target}: ${err.message}`, {
|
|
115
|
+
code: 'lock_held',
|
|
116
|
+
provider: 'run-state',
|
|
117
|
+
details: {
|
|
118
|
+
runDir,
|
|
119
|
+
owner: owner?.writerId ?? null,
|
|
120
|
+
acquiredAt: owner?.acquiredAt ?? null,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Write our metadata. We do this AFTER acquisition so a partial-create
|
|
125
|
+
// (if we crash here) leaves us as the orphaned owner rather than a phantom.
|
|
126
|
+
writeMeta(runDir, { writerId, acquiredAt: new Date().toISOString() });
|
|
127
|
+
let released = false;
|
|
128
|
+
return {
|
|
129
|
+
writerId,
|
|
130
|
+
release: async () => {
|
|
131
|
+
if (released)
|
|
132
|
+
return;
|
|
133
|
+
released = true;
|
|
134
|
+
// Always try to clear meta even if release throws; the lockfile is
|
|
135
|
+
// the authoritative gate, and a stale meta with no .lock around
|
|
136
|
+
// would be merely cosmetic.
|
|
137
|
+
try {
|
|
138
|
+
await release();
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
deleteMeta(runDir);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Update the lastSeq field in the lock metadata. Best-effort; never throws.
|
|
147
|
+
* The events.ndjson is the source of truth, so a missed update is harmless. */
|
|
148
|
+
export function updateLockSeq(runDir, lastSeq) {
|
|
149
|
+
const meta = readMeta(runDir);
|
|
150
|
+
if (!meta)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
writeMeta(runDir, { ...meta, lastSeq });
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// intentionally swallowed — observability sidecar
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Non-blocking peek at who currently owns the lock. Returns null if no
|
|
160
|
+
* metadata is present (which generally means no live writer either, but
|
|
161
|
+
* callers should not infer aliveness from that alone). */
|
|
162
|
+
export function peekLockOwner(runDir) {
|
|
163
|
+
return readMeta(runDir);
|
|
164
|
+
}
|
|
165
|
+
/** Forcibly take ownership. Returns the `lock.takeover` event the caller
|
|
166
|
+
* should append (the events log is sequenced by the appender, so this
|
|
167
|
+
* function deliberately does NOT write to events.ndjson itself).
|
|
168
|
+
*
|
|
169
|
+
* Throws GuardrailError(lock_held) if the previous writer is still alive
|
|
170
|
+
* per `isPidAlive` — taking over a live writer would corrupt the log.
|
|
171
|
+
*
|
|
172
|
+
* After this call returns, the caller should:
|
|
173
|
+
* 1. Append the returned event via `appendEvent`.
|
|
174
|
+
* 2. Call `acquireRunLock` to obtain the new handle.
|
|
175
|
+
* Both steps run after takeover. We do not auto-acquire here so the
|
|
176
|
+
* caller can decide on its own retry / stale-ms strategy.
|
|
177
|
+
*/
|
|
178
|
+
export function forceTakeover(runDir, reason) {
|
|
179
|
+
const previous = readMeta(runDir);
|
|
180
|
+
const previousWriter = previous?.writerId ?? null;
|
|
181
|
+
if (isPidAlive(previousWriter)) {
|
|
182
|
+
throw new GuardrailError(`run lock takeover refused: previous writer is still alive`, {
|
|
183
|
+
code: 'lock_held',
|
|
184
|
+
provider: 'run-state',
|
|
185
|
+
details: { runDir, previousWriter, reason },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// Wipe the proper-lockfile state too so the next acquire doesn't trip
|
|
189
|
+
// over a stale entry. lockfile.lock creates a directory at `${file}.lock`;
|
|
190
|
+
// we remove it so the subsequent acquire path is clean.
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(lockTargetPath(runDir) + '.lock', { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// ignore — proper-lockfile will recreate on next acquire
|
|
196
|
+
}
|
|
197
|
+
deleteMeta(runDir);
|
|
198
|
+
// Caller appends this with `appendEvent`. Returning the input shape (no
|
|
199
|
+
// seq/ts/runId/schema_version/writerId yet) — the appender fills them in.
|
|
200
|
+
return {
|
|
201
|
+
event: 'lock.takeover',
|
|
202
|
+
previousWriter,
|
|
203
|
+
reason,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ExternalRef, RunEvent, WriterId } from './types.ts';
|
|
2
|
+
/** What every running phase receives. Public — re-exported from
|
|
3
|
+
* phase-runner.ts. */
|
|
4
|
+
export interface PhaseContext {
|
|
5
|
+
runDir: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
phaseIdx: number;
|
|
8
|
+
writerId: WriterId;
|
|
9
|
+
/** Append a `phase.cost` event during the run. Adapters / SDK calls
|
|
10
|
+
* should call this whenever a cost ledger entry would be written. */
|
|
11
|
+
emitCost(entry: PhaseCostInput): void;
|
|
12
|
+
/** Persist an externalRef so resume decisions can read back from the run.
|
|
13
|
+
* Phase 6 will wire `onResume` to consult these; Phase 2 just records. */
|
|
14
|
+
emitExternalRef(ref: Omit<ExternalRef, 'observedAt'>): void;
|
|
15
|
+
/** Inject a child sub-phase. Records as a separate phase.start under the
|
|
16
|
+
* parent. Useful for things like council (which has N inner consults).
|
|
17
|
+
* Optional in Phase 2 — see phase-runner.ts. */
|
|
18
|
+
subPhase?<SI, SO>(child: import('./phase-runner.ts').RunPhase<SI, SO>, input: SI): Promise<SO>;
|
|
19
|
+
}
|
|
20
|
+
export interface PhaseCostInput {
|
|
21
|
+
provider: string;
|
|
22
|
+
inputTokens: number;
|
|
23
|
+
outputTokens: number;
|
|
24
|
+
costUSD: number;
|
|
25
|
+
}
|
|
26
|
+
/** Inputs the runner needs to build a context. `subPhase` is optional —
|
|
27
|
+
* phase-runner.ts wires it when nested sub-phases are supported. */
|
|
28
|
+
export interface BuildPhaseContextInput {
|
|
29
|
+
runDir: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
phaseName: string;
|
|
32
|
+
phaseIdx: number;
|
|
33
|
+
writerId: WriterId;
|
|
34
|
+
/** Optional sub-phase factory; pass-through to the returned context. */
|
|
35
|
+
subPhase?: PhaseContext['subPhase'];
|
|
36
|
+
}
|
|
37
|
+
/** Construct a PhaseContext bound to a specific (runDir, runId, phaseIdx,
|
|
38
|
+
* phaseName, writerId). The returned object is a thin facade over
|
|
39
|
+
* `appendEvent`; it is a pure function in the no-IO sense — actual disk IO
|
|
40
|
+
* happens lazily on each emit call. */
|
|
41
|
+
export declare function buildPhaseContext(input: BuildPhaseContextInput): PhaseContext;
|
|
42
|
+
/** Helper for phase-runner.ts: aggregate every phase.cost event for a given
|
|
43
|
+
* phase index from an in-memory event stream. Returned in USD.
|
|
44
|
+
*
|
|
45
|
+
* v6.2.0 — pass `'*'` (the cross-phase sentinel) to sum cost across the
|
|
46
|
+
* whole run (every `phase.cost` event regardless of phaseIdx). The
|
|
47
|
+
* orchestrator uses this for run-scope budget enforcement; per-phase
|
|
48
|
+
* callers keep passing a numeric phaseIdx for the legacy semantics. */
|
|
49
|
+
export declare function sumPhaseCost(events: RunEvent[], phaseIdx: number | '*'): number;
|
|
50
|
+
/** Helper for phase-runner.ts: collect every external ref recorded for a
|
|
51
|
+
* given phase index from an in-memory event stream. Dedup by kind+id. */
|
|
52
|
+
export declare function collectExternalRefs(events: RunEvent[], phaseIdx: number): ExternalRef[];
|
|
53
|
+
/** Helper: count successful prior attempts of a given phase (matched by
|
|
54
|
+
* phaseIdx). Lets the runner detect "this phase already succeeded —
|
|
55
|
+
* short-circuit on idempotent replay". */
|
|
56
|
+
export declare function countPhaseSuccesses(events: RunEvent[], phaseIdx: number): number;
|
|
57
|
+
/** Helper: count attempts of a given phase (number of phase.start events
|
|
58
|
+
* for that phaseIdx). The next attempt's `attempt` field is `count + 1`. */
|
|
59
|
+
export declare function countPhaseAttempts(events: RunEvent[], phaseIdx: number): number;
|
|
60
|
+
//# sourceMappingURL=phase-context.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/core/run-state/phase-context.ts
|
|
2
|
+
//
|
|
3
|
+
// Internal helpers used by `runPhase` to assemble the `PhaseContext` passed
|
|
4
|
+
// into `RunPhase.run`. Kept separate from the public surface in
|
|
5
|
+
// phase-runner.ts so tests can probe the cost / externalRef plumbing without
|
|
6
|
+
// going through the full lifecycle wrapper.
|
|
7
|
+
//
|
|
8
|
+
// The functions here only KNOW about Phase 1's appendEvent; they don't
|
|
9
|
+
// orchestrate phase.start / phase.success / phase.failed (that's
|
|
10
|
+
// phase-runner.ts). They are essentially the "ctx surface" the running phase
|
|
11
|
+
// uses to write costs and external references during the run.
|
|
12
|
+
//
|
|
13
|
+
// Spec: docs/specs/v6-run-state-engine.md "Phase contract", "Idempotency
|
|
14
|
+
// rules + external operation ledger".
|
|
15
|
+
import { appendEvent } from "./events.js";
|
|
16
|
+
/** Construct a PhaseContext bound to a specific (runDir, runId, phaseIdx,
|
|
17
|
+
* phaseName, writerId). The returned object is a thin facade over
|
|
18
|
+
* `appendEvent`; it is a pure function in the no-IO sense — actual disk IO
|
|
19
|
+
* happens lazily on each emit call. */
|
|
20
|
+
export function buildPhaseContext(input) {
|
|
21
|
+
const { runDir, runId, phaseName, phaseIdx, writerId, subPhase } = input;
|
|
22
|
+
const emitCost = (entry) => {
|
|
23
|
+
appendEvent(runDir, {
|
|
24
|
+
event: 'phase.cost',
|
|
25
|
+
phase: phaseName,
|
|
26
|
+
phaseIdx,
|
|
27
|
+
provider: entry.provider,
|
|
28
|
+
inputTokens: entry.inputTokens,
|
|
29
|
+
outputTokens: entry.outputTokens,
|
|
30
|
+
costUSD: entry.costUSD,
|
|
31
|
+
}, { writerId, runId });
|
|
32
|
+
};
|
|
33
|
+
const emitExternalRef = (ref) => {
|
|
34
|
+
const fullRef = {
|
|
35
|
+
...ref,
|
|
36
|
+
observedAt: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
appendEvent(runDir, {
|
|
39
|
+
event: 'phase.externalRef',
|
|
40
|
+
phase: phaseName,
|
|
41
|
+
phaseIdx,
|
|
42
|
+
ref: fullRef,
|
|
43
|
+
}, { writerId, runId });
|
|
44
|
+
};
|
|
45
|
+
const ctx = {
|
|
46
|
+
runDir,
|
|
47
|
+
runId,
|
|
48
|
+
phaseIdx,
|
|
49
|
+
writerId,
|
|
50
|
+
emitCost,
|
|
51
|
+
emitExternalRef,
|
|
52
|
+
};
|
|
53
|
+
if (subPhase)
|
|
54
|
+
ctx.subPhase = subPhase;
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
/** Helper for phase-runner.ts: aggregate every phase.cost event for a given
|
|
58
|
+
* phase index from an in-memory event stream. Returned in USD.
|
|
59
|
+
*
|
|
60
|
+
* v6.2.0 — pass `'*'` (the cross-phase sentinel) to sum cost across the
|
|
61
|
+
* whole run (every `phase.cost` event regardless of phaseIdx). The
|
|
62
|
+
* orchestrator uses this for run-scope budget enforcement; per-phase
|
|
63
|
+
* callers keep passing a numeric phaseIdx for the legacy semantics. */
|
|
64
|
+
export function sumPhaseCost(events, phaseIdx) {
|
|
65
|
+
let total = 0;
|
|
66
|
+
for (const ev of events) {
|
|
67
|
+
if (ev.event === 'phase.cost') {
|
|
68
|
+
if (phaseIdx === '*' || ev.phaseIdx === phaseIdx)
|
|
69
|
+
total += ev.costUSD;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return total;
|
|
73
|
+
}
|
|
74
|
+
/** Helper for phase-runner.ts: collect every external ref recorded for a
|
|
75
|
+
* given phase index from an in-memory event stream. Dedup by kind+id. */
|
|
76
|
+
export function collectExternalRefs(events, phaseIdx) {
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const ev of events) {
|
|
79
|
+
if (ev.event === 'phase.externalRef' && ev.phaseIdx === phaseIdx) {
|
|
80
|
+
const dup = out.find(r => r.kind === ev.ref.kind && r.id === ev.ref.id);
|
|
81
|
+
if (!dup)
|
|
82
|
+
out.push(ev.ref);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** Helper: count successful prior attempts of a given phase (matched by
|
|
88
|
+
* phaseIdx). Lets the runner detect "this phase already succeeded —
|
|
89
|
+
* short-circuit on idempotent replay". */
|
|
90
|
+
export function countPhaseSuccesses(events, phaseIdx) {
|
|
91
|
+
let n = 0;
|
|
92
|
+
for (const ev of events) {
|
|
93
|
+
if (ev.event === 'phase.success' && ev.phaseIdx === phaseIdx)
|
|
94
|
+
n += 1;
|
|
95
|
+
}
|
|
96
|
+
return n;
|
|
97
|
+
}
|
|
98
|
+
/** Helper: count attempts of a given phase (number of phase.start events
|
|
99
|
+
* for that phaseIdx). The next attempt's `attempt` field is `count + 1`. */
|
|
100
|
+
export function countPhaseAttempts(events, phaseIdx) {
|
|
101
|
+
let n = 0;
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (ev.event === 'phase.start' && ev.phaseIdx === phaseIdx)
|
|
104
|
+
n += 1;
|
|
105
|
+
}
|
|
106
|
+
return n;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=phase-context.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { GuardrailConfig } from '../config/types.ts';
|
|
2
|
+
import type { RunPhase } from './phase-runner.ts';
|
|
3
|
+
import type { ExternalRefKind } from './types.ts';
|
|
4
|
+
import { type ScanInput, type ScanOutput, type ScanCommandOptions } from '../../cli/scan.ts';
|
|
5
|
+
import { type SpecInput, type SpecOutput, type SpecCommandOptions } from '../../cli/spec.ts';
|
|
6
|
+
import { type PlanInput, type PlanOutput, type PlanCommandOptions } from '../../cli/plan.ts';
|
|
7
|
+
import { type ImplementInput, type ImplementOutput, type ImplementCommandOptions } from '../../cli/implement.ts';
|
|
8
|
+
import { type MigrateInput, type MigrateOutput, type MigrateCommandOptions } from '../../cli/migrate.ts';
|
|
9
|
+
import { type PrInput, type PrOutput, type PrCommandOptions } from '../../cli/pr.ts';
|
|
10
|
+
/** v6.2.0 — early-exit sentinel returned by a builder when the verb's
|
|
11
|
+
* pre-flight (no targets, no LLM key, dry-run, …) decided it can exit
|
|
12
|
+
* without running through the engine lifecycle. The orchestrator surfaces
|
|
13
|
+
* this exit code straight through and short-circuits — no further phases
|
|
14
|
+
* run, no `run.complete` event is emitted (we never created a run dir
|
|
15
|
+
* in this branch). */
|
|
16
|
+
export interface PhaseEarlyExit {
|
|
17
|
+
kind: 'early-exit';
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
/** Result of a successful builder call. Carries everything the orchestrator
|
|
21
|
+
* needs to drive a single-phase `runPhase` invocation. */
|
|
22
|
+
export interface PhaseBuilt<I, O> {
|
|
23
|
+
kind: 'phase';
|
|
24
|
+
phase: RunPhase<I, O>;
|
|
25
|
+
input: I;
|
|
26
|
+
/** Loaded `guardrail.config.yaml` (or the default). The orchestrator
|
|
27
|
+
* uses this for `engine.enabled` resolution; per-phase wrappers also
|
|
28
|
+
* forward it to `runPhaseWithLifecycle`. */
|
|
29
|
+
config: GuardrailConfig;
|
|
30
|
+
/** Translate the phase output back into the legacy stdout banner +
|
|
31
|
+
* exit code path. The orchestrator calls this once per phase after
|
|
32
|
+
* `runPhase` returns. */
|
|
33
|
+
renderResult: (output: O) => number;
|
|
34
|
+
}
|
|
35
|
+
/** Each registered phase defines a `build(deps)` that produces either a
|
|
36
|
+
* `PhaseBuilt` (the happy path) or a `PhaseEarlyExit` (pre-flight bailed).
|
|
37
|
+
* The generic `<I, O>` is preserved at the declaration site via
|
|
38
|
+
* `satisfies PhaseRegistration<I, O>` so the registry doesn't collapse
|
|
39
|
+
* to `PhaseRegistration<unknown, unknown>` on lookup.
|
|
40
|
+
*
|
|
41
|
+
* v6.2.1 — `preEffectRefKinds` and `postEffectRefKinds` capture the per-
|
|
42
|
+
* phase idempotency contract. A side-effecting phase MUST declare both:
|
|
43
|
+
* the registry rejects any `hasSideEffects: true` registration that omits
|
|
44
|
+
* them. The orchestrator's resume preflight reads them back to decide
|
|
45
|
+
* skip-already-applied vs retry vs needs-human. Read-only phases (scan /
|
|
46
|
+
* spec / plan / implement-as-of-v6.2.0) omit both — they never enter the
|
|
47
|
+
* preflight branch.
|
|
48
|
+
*
|
|
49
|
+
* The kinds named here MUST be subsets of `ExternalRefKind`. The registry
|
|
50
|
+
* doesn't statically verify the phase body emits them (would require
|
|
51
|
+
* runtime introspection of `ctx.emitExternalRef` calls); it only requires
|
|
52
|
+
* the contract DECLARATION so the orchestrator knows what to read back. */
|
|
53
|
+
export interface PhaseRegistration<I, O, Opts = unknown> {
|
|
54
|
+
build: (deps: Opts) => Promise<PhaseBuilt<I, O> | PhaseEarlyExit>;
|
|
55
|
+
/** Human-readable name shown in CLI banners + `runs show` output. */
|
|
56
|
+
displayName: string;
|
|
57
|
+
/** v6.2.1 — true iff the registered phase declares `hasSideEffects: true`
|
|
58
|
+
* on its `RunPhase` shape. Required so the registry's `registerPhase`
|
|
59
|
+
* helper can enforce the side-effect idempotency contract at registration
|
|
60
|
+
* time without needing to instantiate the phase. Read-only phases
|
|
61
|
+
* (scan / spec / plan / implement) omit this or set it to false. */
|
|
62
|
+
hasSideEffects?: boolean;
|
|
63
|
+
/** v6.2.1 — kinds the phase emits BEFORE invoking its side effect. Used
|
|
64
|
+
* by the orchestrator's resume preflight to detect "we started this work
|
|
65
|
+
* but didn't finish." Required when `hasSideEffects: true`. */
|
|
66
|
+
preEffectRefKinds?: readonly ExternalRefKind[];
|
|
67
|
+
/** v6.2.1 — kinds the phase emits AFTER its side effect completes
|
|
68
|
+
* successfully. Used by the resume preflight's skip-already-applied
|
|
69
|
+
* check (all post-effect refs `merged`/`live` ⇒ skip). Required when
|
|
70
|
+
* `hasSideEffects: true`; may be empty when the pre-effect ref doubles
|
|
71
|
+
* as the reconciliation ref (e.g. `pr`'s `github-pr` is recorded
|
|
72
|
+
* pre-effect with the same id `gh` reports post-create). */
|
|
73
|
+
postEffectRefKinds?: readonly ExternalRefKind[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* v6.2.1 — registry-time guard that enforces the side-effect idempotency
|
|
77
|
+
* contract. Throws `Error` (caught by the registry-rejection test) when a
|
|
78
|
+
* `hasSideEffects: true` registration omits the contract arrays.
|
|
79
|
+
*
|
|
80
|
+
* Why a runtime throw and not a type-level check: the contract arrays are
|
|
81
|
+
* declarative metadata, not type-derivable from the builder signature. A
|
|
82
|
+
* structural type constraint would require duplicating each builder's
|
|
83
|
+
* shape into a wider type — overkill for a one-line registry-time check
|
|
84
|
+
* that runs once at module load.
|
|
85
|
+
*/
|
|
86
|
+
export declare function registerPhase<I, O, Opts = unknown>(reg: PhaseRegistration<I, O, Opts>): PhaseRegistration<I, O, Opts>;
|
|
87
|
+
/** v6.2.0 — phase registry. `as const` preserves the literal name → entry
|
|
88
|
+
* pairs; `satisfies` per-entry validates the builder signature without
|
|
89
|
+
* collapsing the inferred shape.
|
|
90
|
+
*
|
|
91
|
+
* Adding a new phase: extract its `build<Phase>Phase()` builder out of the
|
|
92
|
+
* CLI verb (parity test required — see spec WARNING #4), then register
|
|
93
|
+
* here. The orchestrator picks it up automatically.
|
|
94
|
+
*
|
|
95
|
+
* v6.2.1 — `migrate` and `pr` enter the registry. Both are side-effecting,
|
|
96
|
+
* so each declares its idempotency contract via `preEffectRefKinds` /
|
|
97
|
+
* `postEffectRefKinds`. `registerPhase()` runs at module load and throws
|
|
98
|
+
* if a side-effect entry omits the contract — that's the registry-time
|
|
99
|
+
* enforcement gate the v6.2.1 spec requires. Read-only phases (scan /
|
|
100
|
+
* spec / plan / implement) omit both arrays. */
|
|
101
|
+
export declare const PHASE_REGISTRY: {
|
|
102
|
+
readonly scan: PhaseRegistration<ScanInput, ScanOutput, ScanCommandOptions>;
|
|
103
|
+
readonly spec: PhaseRegistration<SpecInput, SpecOutput, SpecCommandOptions>;
|
|
104
|
+
readonly plan: PhaseRegistration<PlanInput, PlanOutput, PlanCommandOptions>;
|
|
105
|
+
readonly implement: PhaseRegistration<ImplementInput, ImplementOutput, ImplementCommandOptions>;
|
|
106
|
+
readonly migrate: PhaseRegistration<MigrateInput, MigrateOutput, MigrateCommandOptions>;
|
|
107
|
+
readonly pr: PhaseRegistration<PrInput, PrOutput, PrCommandOptions>;
|
|
108
|
+
};
|
|
109
|
+
/** Literal union of registered phase names. Adding a new phase to
|
|
110
|
+
* PHASE_REGISTRY automatically extends this type. */
|
|
111
|
+
export type PhaseName = keyof typeof PHASE_REGISTRY;
|
|
112
|
+
/** The default `--mode=full` ordering. v6.2.0 shipped scan → spec → plan →
|
|
113
|
+
* implement; v6.2.1 extends with migrate → pr (per spec section "Phase
|
|
114
|
+
* ordering"). After v6.2.1 ships, `claude-autopilot autopilot` runs the
|
|
115
|
+
* full 6-phase pipeline under one runId. */
|
|
116
|
+
export declare const DEFAULT_FULL_PHASES: readonly PhaseName[];
|
|
117
|
+
/** Look up a phase entry by name. Returns the registration with its full
|
|
118
|
+
* typed shape preserved (the `as const` + `satisfies` pattern means the
|
|
119
|
+
* caller can still reach `PhaseInput<'scan'>` even though the lookup is
|
|
120
|
+
* dynamic). Throws if the name is not registered — callers that want a
|
|
121
|
+
* graceful fallback should validate against `PHASE_REGISTRY` keys
|
|
122
|
+
* beforehand (see `validatePhaseNames` below). */
|
|
123
|
+
export declare function getPhase<N extends PhaseName>(name: N): typeof PHASE_REGISTRY[N];
|
|
124
|
+
/** All registered phase names in declaration order. Useful for `--help`
|
|
125
|
+
* text and pre-flight `--phases` validation. */
|
|
126
|
+
export declare function listPhaseNames(): readonly PhaseName[];
|
|
127
|
+
/** Validate a user-supplied list of phase names against the registry.
|
|
128
|
+
* Returns the unknown names (empty array on full match) so the caller
|
|
129
|
+
* can produce a clear `invalid_config` error before any run dir is
|
|
130
|
+
* created. */
|
|
131
|
+
export declare function validatePhaseNames(names: readonly string[]): {
|
|
132
|
+
ok: true;
|
|
133
|
+
} | {
|
|
134
|
+
ok: false;
|
|
135
|
+
unknown: string[];
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=phase-registry.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/core/run-state/phase-registry.ts
|
|
2
|
+
//
|
|
3
|
+
// v6.2.0 — typed phase registry for the multi-phase orchestrator.
|
|
4
|
+
//
|
|
5
|
+
// The new top-level `claude-autopilot autopilot` verb (see src/cli/autopilot.ts)
|
|
6
|
+
// drives N phases under one runId. To do that without losing per-phase I/O
|
|
7
|
+
// types it needs a typed registry: `name → builder` where the builder's
|
|
8
|
+
// `RunPhase<I, O>` shape is preserved through dynamic dispatch. The naive
|
|
9
|
+
// `Record<PhaseName, PhaseRegistration<unknown, unknown>>` shape would
|
|
10
|
+
// collapse every entry to `unknown`-on-both-sides, defeating the purpose
|
|
11
|
+
// of the v6 phase contract.
|
|
12
|
+
//
|
|
13
|
+
// The trick (per codex NOTE #5 on the v6.2 spec):
|
|
14
|
+
// - Each entry is annotated with `satisfies PhaseRegistration<I, O>` to
|
|
15
|
+
// force-check that the builder returns the correct shape.
|
|
16
|
+
// - The wrapping `as const` preserves the literal `name → entry` pairs so
|
|
17
|
+
// `keyof typeof PHASE_REGISTRY` is the literal union, not a generic
|
|
18
|
+
// `string`.
|
|
19
|
+
// - Per-entry I/O types stay reachable through TypeScript's structural
|
|
20
|
+
// inference on the satisfies constraint.
|
|
21
|
+
//
|
|
22
|
+
// v6.2.0 ships with FOUR registered phases: `scan`, `spec`, `plan`,
|
|
23
|
+
// `implement`. The remaining six pipeline verbs (`brainstorm`, `costs`,
|
|
24
|
+
// `fix`, `review`, `validate`) are intentionally unregistered for v6.2.0:
|
|
25
|
+
//
|
|
26
|
+
// - `migrate` and `pr` need explicit per-phase idempotency contracts
|
|
27
|
+
// (preflight readback + externalRef recorded BEFORE the side-effect)
|
|
28
|
+
// before they can land in a multi-phase orchestrator. v6.2.1 gates on
|
|
29
|
+
// those contracts.
|
|
30
|
+
// - `brainstorm`, `costs`, `fix`, `review`, `validate` are advisory /
|
|
31
|
+
// read-only verbs that don't fit the pipeline shape (per spec
|
|
32
|
+
// "phase ordering" section). Users who want them in a custom run
|
|
33
|
+
// should compose them via the eventual `--phases=<csv>` option once
|
|
34
|
+
// they are extracted in a follow-up release.
|
|
35
|
+
//
|
|
36
|
+
// Spec: docs/specs/v6.2-multi-phase-orchestrator.md "Phase registry".
|
|
37
|
+
import { buildScanPhase, } from "../../cli/scan.js";
|
|
38
|
+
import { buildSpecPhase, } from "../../cli/spec.js";
|
|
39
|
+
import { buildPlanPhase, } from "../../cli/plan.js";
|
|
40
|
+
import { buildImplementPhase, } from "../../cli/implement.js";
|
|
41
|
+
import { buildMigratePhase, } from "../../cli/migrate.js";
|
|
42
|
+
import { buildPrPhase, } from "../../cli/pr.js";
|
|
43
|
+
/**
|
|
44
|
+
* v6.2.1 — registry-time guard that enforces the side-effect idempotency
|
|
45
|
+
* contract. Throws `Error` (caught by the registry-rejection test) when a
|
|
46
|
+
* `hasSideEffects: true` registration omits the contract arrays.
|
|
47
|
+
*
|
|
48
|
+
* Why a runtime throw and not a type-level check: the contract arrays are
|
|
49
|
+
* declarative metadata, not type-derivable from the builder signature. A
|
|
50
|
+
* structural type constraint would require duplicating each builder's
|
|
51
|
+
* shape into a wider type — overkill for a one-line registry-time check
|
|
52
|
+
* that runs once at module load.
|
|
53
|
+
*/
|
|
54
|
+
export function registerPhase(reg) {
|
|
55
|
+
if (reg.build === undefined) {
|
|
56
|
+
throw new Error(`registry: missing build for ${reg.displayName}`);
|
|
57
|
+
}
|
|
58
|
+
if (reg.hasSideEffects) {
|
|
59
|
+
const pre = reg.preEffectRefKinds;
|
|
60
|
+
const post = reg.postEffectRefKinds;
|
|
61
|
+
if (!pre || pre.length === 0 || !post) {
|
|
62
|
+
throw new Error(`registry: side-effect phase ${reg.displayName} missing idempotency contract — ` +
|
|
63
|
+
`declare preEffectRefKinds + postEffectRefKinds`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return reg;
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// The actual registry
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/** v6.2.0 — phase registry. `as const` preserves the literal name → entry
|
|
72
|
+
* pairs; `satisfies` per-entry validates the builder signature without
|
|
73
|
+
* collapsing the inferred shape.
|
|
74
|
+
*
|
|
75
|
+
* Adding a new phase: extract its `build<Phase>Phase()` builder out of the
|
|
76
|
+
* CLI verb (parity test required — see spec WARNING #4), then register
|
|
77
|
+
* here. The orchestrator picks it up automatically.
|
|
78
|
+
*
|
|
79
|
+
* v6.2.1 — `migrate` and `pr` enter the registry. Both are side-effecting,
|
|
80
|
+
* so each declares its idempotency contract via `preEffectRefKinds` /
|
|
81
|
+
* `postEffectRefKinds`. `registerPhase()` runs at module load and throws
|
|
82
|
+
* if a side-effect entry omits the contract — that's the registry-time
|
|
83
|
+
* enforcement gate the v6.2.1 spec requires. Read-only phases (scan /
|
|
84
|
+
* spec / plan / implement) omit both arrays. */
|
|
85
|
+
export const PHASE_REGISTRY = {
|
|
86
|
+
scan: registerPhase({
|
|
87
|
+
build: buildScanPhase,
|
|
88
|
+
displayName: 'Scan',
|
|
89
|
+
}),
|
|
90
|
+
spec: registerPhase({
|
|
91
|
+
build: buildSpecPhase,
|
|
92
|
+
displayName: 'Spec',
|
|
93
|
+
}),
|
|
94
|
+
plan: registerPhase({
|
|
95
|
+
build: buildPlanPhase,
|
|
96
|
+
displayName: 'Plan',
|
|
97
|
+
}),
|
|
98
|
+
implement: registerPhase({
|
|
99
|
+
build: buildImplementPhase,
|
|
100
|
+
displayName: 'Implement',
|
|
101
|
+
}),
|
|
102
|
+
migrate: registerPhase({
|
|
103
|
+
build: buildMigratePhase,
|
|
104
|
+
displayName: 'Migrate',
|
|
105
|
+
hasSideEffects: true,
|
|
106
|
+
preEffectRefKinds: ['migration-batch'],
|
|
107
|
+
postEffectRefKinds: ['migration-version'],
|
|
108
|
+
}),
|
|
109
|
+
pr: registerPhase({
|
|
110
|
+
build: buildPrPhase,
|
|
111
|
+
displayName: 'PR',
|
|
112
|
+
hasSideEffects: true,
|
|
113
|
+
// The github-pr ref is recorded pre-effect with the same id gh reports
|
|
114
|
+
// post-create — it serves both purposes. postEffectRefKinds is empty
|
|
115
|
+
// by design, not by omission. The contract guard accepts an empty
|
|
116
|
+
// array; only `undefined` triggers the rejection.
|
|
117
|
+
preEffectRefKinds: ['github-pr'],
|
|
118
|
+
postEffectRefKinds: [],
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
/** The default `--mode=full` ordering. v6.2.0 shipped scan → spec → plan →
|
|
122
|
+
* implement; v6.2.1 extends with migrate → pr (per spec section "Phase
|
|
123
|
+
* ordering"). After v6.2.1 ships, `claude-autopilot autopilot` runs the
|
|
124
|
+
* full 6-phase pipeline under one runId. */
|
|
125
|
+
export const DEFAULT_FULL_PHASES = [
|
|
126
|
+
'scan',
|
|
127
|
+
'spec',
|
|
128
|
+
'plan',
|
|
129
|
+
'implement',
|
|
130
|
+
'migrate',
|
|
131
|
+
'pr',
|
|
132
|
+
];
|
|
133
|
+
/** Look up a phase entry by name. Returns the registration with its full
|
|
134
|
+
* typed shape preserved (the `as const` + `satisfies` pattern means the
|
|
135
|
+
* caller can still reach `PhaseInput<'scan'>` even though the lookup is
|
|
136
|
+
* dynamic). Throws if the name is not registered — callers that want a
|
|
137
|
+
* graceful fallback should validate against `PHASE_REGISTRY` keys
|
|
138
|
+
* beforehand (see `validatePhaseNames` below). */
|
|
139
|
+
export function getPhase(name) {
|
|
140
|
+
const entry = PHASE_REGISTRY[name];
|
|
141
|
+
if (!entry) {
|
|
142
|
+
throw new Error(`[phase-registry] unknown phase: "${name}". Registered: ${listPhaseNames().join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
return entry;
|
|
145
|
+
}
|
|
146
|
+
/** All registered phase names in declaration order. Useful for `--help`
|
|
147
|
+
* text and pre-flight `--phases` validation. */
|
|
148
|
+
export function listPhaseNames() {
|
|
149
|
+
return Object.keys(PHASE_REGISTRY);
|
|
150
|
+
}
|
|
151
|
+
/** Validate a user-supplied list of phase names against the registry.
|
|
152
|
+
* Returns the unknown names (empty array on full match) so the caller
|
|
153
|
+
* can produce a clear `invalid_config` error before any run dir is
|
|
154
|
+
* created. */
|
|
155
|
+
export function validatePhaseNames(names) {
|
|
156
|
+
const known = new Set(listPhaseNames());
|
|
157
|
+
const unknown = names.filter(n => !known.has(n));
|
|
158
|
+
if (unknown.length > 0)
|
|
159
|
+
return { ok: false, unknown };
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=phase-registry.js.map
|