@delegance/claude-autopilot 5.5.2 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +1776 -6
  2. package/README.md +65 -1
  3. package/bin/_launcher.js +38 -23
  4. package/dist/src/adapters/council/openai.js +12 -6
  5. package/dist/src/adapters/deploy/_http.d.ts +43 -0
  6. package/dist/src/adapters/deploy/_http.js +99 -0
  7. package/dist/src/adapters/deploy/fly.d.ts +206 -0
  8. package/dist/src/adapters/deploy/fly.js +696 -0
  9. package/dist/src/adapters/deploy/index.d.ts +2 -0
  10. package/dist/src/adapters/deploy/index.js +33 -0
  11. package/dist/src/adapters/deploy/render.d.ts +181 -0
  12. package/dist/src/adapters/deploy/render.js +550 -0
  13. package/dist/src/adapters/deploy/types.d.ts +67 -3
  14. package/dist/src/adapters/deploy/vercel.d.ts +17 -1
  15. package/dist/src/adapters/deploy/vercel.js +29 -49
  16. package/dist/src/adapters/pricing.d.ts +36 -0
  17. package/dist/src/adapters/pricing.js +40 -0
  18. package/dist/src/adapters/review-engine/codex.js +10 -7
  19. package/dist/src/cli/autopilot.d.ts +75 -0
  20. package/dist/src/cli/autopilot.js +750 -0
  21. package/dist/src/cli/brainstorm.d.ts +23 -0
  22. package/dist/src/cli/brainstorm.js +131 -0
  23. package/dist/src/cli/costs.d.ts +15 -1
  24. package/dist/src/cli/costs.js +99 -10
  25. package/dist/src/cli/dashboard/index.d.ts +5 -0
  26. package/dist/src/cli/dashboard/index.js +49 -0
  27. package/dist/src/cli/dashboard/login.d.ts +22 -0
  28. package/dist/src/cli/dashboard/login.js +260 -0
  29. package/dist/src/cli/dashboard/logout.d.ts +12 -0
  30. package/dist/src/cli/dashboard/logout.js +45 -0
  31. package/dist/src/cli/dashboard/status.d.ts +30 -0
  32. package/dist/src/cli/dashboard/status.js +65 -0
  33. package/dist/src/cli/dashboard/upload.d.ts +16 -0
  34. package/dist/src/cli/dashboard/upload.js +48 -0
  35. package/dist/src/cli/deploy.d.ts +3 -3
  36. package/dist/src/cli/deploy.js +34 -9
  37. package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
  38. package/dist/src/cli/engine-flag-deprecation.js +20 -0
  39. package/dist/src/cli/fix.d.ts +18 -0
  40. package/dist/src/cli/fix.js +105 -11
  41. package/dist/src/cli/help-text.d.ts +52 -0
  42. package/dist/src/cli/help-text.js +416 -0
  43. package/dist/src/cli/implement.d.ts +91 -0
  44. package/dist/src/cli/implement.js +196 -0
  45. package/dist/src/cli/index.d.ts +2 -1
  46. package/dist/src/cli/index.js +774 -245
  47. package/dist/src/cli/json-envelope.d.ts +187 -0
  48. package/dist/src/cli/json-envelope.js +270 -0
  49. package/dist/src/cli/json-mode.d.ts +33 -0
  50. package/dist/src/cli/json-mode.js +201 -0
  51. package/dist/src/cli/migrate.d.ts +111 -0
  52. package/dist/src/cli/migrate.js +305 -0
  53. package/dist/src/cli/plan.d.ts +81 -0
  54. package/dist/src/cli/plan.js +149 -0
  55. package/dist/src/cli/pr.d.ts +106 -0
  56. package/dist/src/cli/pr.js +191 -19
  57. package/dist/src/cli/preflight.js +26 -0
  58. package/dist/src/cli/review.d.ts +27 -0
  59. package/dist/src/cli/review.js +126 -0
  60. package/dist/src/cli/runs-watch-renderer.d.ts +45 -0
  61. package/dist/src/cli/runs-watch-renderer.js +275 -0
  62. package/dist/src/cli/runs-watch.d.ts +41 -0
  63. package/dist/src/cli/runs-watch.js +395 -0
  64. package/dist/src/cli/runs.d.ts +122 -0
  65. package/dist/src/cli/runs.js +902 -0
  66. package/dist/src/cli/scaffold.d.ts +39 -0
  67. package/dist/src/cli/scaffold.js +287 -0
  68. package/dist/src/cli/scan.d.ts +93 -0
  69. package/dist/src/cli/scan.js +166 -40
  70. package/dist/src/cli/setup.d.ts +30 -0
  71. package/dist/src/cli/setup.js +137 -0
  72. package/dist/src/cli/spec.d.ts +66 -0
  73. package/dist/src/cli/spec.js +132 -0
  74. package/dist/src/cli/validate.d.ts +29 -0
  75. package/dist/src/cli/validate.js +131 -0
  76. package/dist/src/core/config/schema.d.ts +9 -0
  77. package/dist/src/core/config/schema.js +7 -0
  78. package/dist/src/core/config/types.d.ts +11 -0
  79. package/dist/src/core/council/runner.d.ts +10 -1
  80. package/dist/src/core/council/runner.js +25 -3
  81. package/dist/src/core/council/types.d.ts +7 -0
  82. package/dist/src/core/errors.d.ts +1 -1
  83. package/dist/src/core/errors.js +11 -0
  84. package/dist/src/core/logging/redaction.d.ts +13 -0
  85. package/dist/src/core/logging/redaction.js +20 -0
  86. package/dist/src/core/migrate/schema-validator.js +15 -1
  87. package/dist/src/core/phases/static-rules.d.ts +5 -1
  88. package/dist/src/core/phases/static-rules.js +2 -5
  89. package/dist/src/core/run-state/budget.d.ts +88 -0
  90. package/dist/src/core/run-state/budget.js +141 -0
  91. package/dist/src/core/run-state/cli-internal.d.ts +21 -0
  92. package/dist/src/core/run-state/cli-internal.js +174 -0
  93. package/dist/src/core/run-state/events.d.ts +59 -0
  94. package/dist/src/core/run-state/events.js +512 -0
  95. package/dist/src/core/run-state/lock.d.ts +61 -0
  96. package/dist/src/core/run-state/lock.js +206 -0
  97. package/dist/src/core/run-state/phase-context.d.ts +60 -0
  98. package/dist/src/core/run-state/phase-context.js +108 -0
  99. package/dist/src/core/run-state/phase-registry.d.ts +137 -0
  100. package/dist/src/core/run-state/phase-registry.js +162 -0
  101. package/dist/src/core/run-state/phase-runner.d.ts +80 -0
  102. package/dist/src/core/run-state/phase-runner.js +447 -0
  103. package/dist/src/core/run-state/provider-readback.d.ts +130 -0
  104. package/dist/src/core/run-state/provider-readback.js +426 -0
  105. package/dist/src/core/run-state/replay-decision.d.ts +69 -0
  106. package/dist/src/core/run-state/replay-decision.js +144 -0
  107. package/dist/src/core/run-state/resolve-engine.d.ts +45 -0
  108. package/dist/src/core/run-state/resolve-engine.js +74 -0
  109. package/dist/src/core/run-state/resume-preflight.d.ts +66 -0
  110. package/dist/src/core/run-state/resume-preflight.js +116 -0
  111. package/dist/src/core/run-state/run-phase-with-lifecycle.d.ts +69 -0
  112. package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -0
  113. package/dist/src/core/run-state/runs.d.ts +57 -0
  114. package/dist/src/core/run-state/runs.js +288 -0
  115. package/dist/src/core/run-state/snapshot.d.ts +14 -0
  116. package/dist/src/core/run-state/snapshot.js +114 -0
  117. package/dist/src/core/run-state/state.d.ts +40 -0
  118. package/dist/src/core/run-state/state.js +164 -0
  119. package/dist/src/core/run-state/types.d.ts +284 -0
  120. package/dist/src/core/run-state/types.js +19 -0
  121. package/dist/src/core/run-state/ulid.d.ts +11 -0
  122. package/dist/src/core/run-state/ulid.js +95 -0
  123. package/dist/src/core/schema-alignment/extractor/index.d.ts +1 -1
  124. package/dist/src/core/schema-alignment/extractor/index.js +2 -2
  125. package/dist/src/core/schema-alignment/extractor/prisma.d.ts +13 -1
  126. package/dist/src/core/schema-alignment/extractor/prisma.js +65 -10
  127. package/dist/src/core/schema-alignment/git-history.d.ts +19 -0
  128. package/dist/src/core/schema-alignment/git-history.js +53 -0
  129. package/dist/src/core/static-rules/rules/brand-tokens.js +2 -2
  130. package/dist/src/core/static-rules/rules/schema-alignment.js +14 -4
  131. package/dist/src/dashboard/auto-upload.d.ts +26 -0
  132. package/dist/src/dashboard/auto-upload.js +107 -0
  133. package/dist/src/dashboard/config.d.ts +22 -0
  134. package/dist/src/dashboard/config.js +109 -0
  135. package/dist/src/dashboard/upload/canonical.d.ts +3 -0
  136. package/dist/src/dashboard/upload/canonical.js +16 -0
  137. package/dist/src/dashboard/upload/chain.d.ts +9 -0
  138. package/dist/src/dashboard/upload/chain.js +27 -0
  139. package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
  140. package/dist/src/dashboard/upload/snapshot.js +66 -0
  141. package/dist/src/dashboard/upload/uploader.d.ts +54 -0
  142. package/dist/src/dashboard/upload/uploader.js +330 -0
  143. package/package.json +19 -3
  144. package/scripts/autoregress.ts +1 -1
  145. package/scripts/test-runner.mjs +4 -0
  146. package/skills/claude-autopilot.md +1 -1
  147. package/skills/make-interfaces-feel-better/SKILL.md +104 -0
  148. package/skills/simplify-ui/SKILL.md +103 -0
  149. package/skills/ui/SKILL.md +117 -0
  150. 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