@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,74 @@
1
+ // src/core/run-state/resolve-engine.ts
2
+ //
3
+ // v7.0 — engine-off path retired. The function is preserved for source
4
+ // compatibility with callers that pass `cliEngine` / `envValue` /
5
+ // `configEnabled`, but it now returns `enabled: true` unconditionally.
6
+ //
7
+ // What changed in v7.0 vs v6.x:
8
+ // - `ENGINE_DEFAULT_V6_0` and `ENGINE_DEFAULT_V6_1` exports REMOVED.
9
+ // Direct importers must replace with literal `true` (see
10
+ // docs/v7/breaking-changes.md).
11
+ // - The deprecation warning helpers (`emitEngineOffDeprecationWarning`
12
+ // / `shouldWarnEngineOffDeprecation` / `ENGINE_OFF_DEPRECATION_MESSAGE`)
13
+ // are RETAINED as no-op stubs so call sites don't have to change in
14
+ // the same PR — they always return false / never fire.
15
+ // - `parseEngineEnvValue()` is RETAINED for back-compat with any
16
+ // out-of-tree callers; `resolveEngineEnabled()` ignores the env
17
+ // value entirely (the engine-off env path is gone).
18
+ //
19
+ // Why keep the stub function shape: the CLI dispatcher passes
20
+ // `cliEngine` / `envEngine` / config to `runPhaseWithLifecycle`, which
21
+ // in turn calls `resolveEngineEnabled()`. Those parameters become
22
+ // effective no-ops in v7.0 — the values are observed (so a future PR
23
+ // can re-enable the path or surface a deprecation event) but never
24
+ // override the always-on result.
25
+ /** Parse a stringly-typed env value into a tri-state boolean.
26
+ * Retained for back-compat with any out-of-tree callers; the v7
27
+ * resolver does not consult env values. */
28
+ export function parseEngineEnvValue(raw) {
29
+ if (raw === undefined)
30
+ return undefined;
31
+ const normalized = raw.trim().toLowerCase();
32
+ if (normalized === '')
33
+ return undefined;
34
+ switch (normalized) {
35
+ case 'on':
36
+ case 'true':
37
+ case '1':
38
+ case 'yes':
39
+ return true;
40
+ case 'off':
41
+ case 'false':
42
+ case '0':
43
+ case 'no':
44
+ return false;
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
49
+ /** v7.0+ — engine is always on. Pure function; ignores all inputs.
50
+ * Source compatible with v6.x call sites. */
51
+ export function resolveEngineEnabled(_opts = {}) {
52
+ return {
53
+ enabled: true,
54
+ source: 'default',
55
+ reason: 'v7.0+ — engine always on (engine-off path removed)',
56
+ };
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // v6.1 deprecation helpers — retained as no-op stubs for source compat.
60
+ // v7.0 removed the engine-off path entirely; no warning ever fires.
61
+ // ---------------------------------------------------------------------------
62
+ /** v6.1-era stable deprecation banner. v7.0+ never emits this string —
63
+ * the path is gone. Kept exported so out-of-tree consumers that imported
64
+ * it still type-check. */
65
+ export const ENGINE_OFF_DEPRECATION_MESSAGE = '[deprecation] --no-engine / engine.enabled: false were removed in v7.0. Migration: drop the flag/env/config.';
66
+ /** v7.0+ no-op. Always returns false. */
67
+ export function shouldWarnEngineOffDeprecation(_resolved) {
68
+ return false;
69
+ }
70
+ /** v7.0+ no-op. Always returns false. */
71
+ export function emitEngineOffDeprecationWarning(_resolved, _warn = () => { }) {
72
+ return false;
73
+ }
74
+ //# sourceMappingURL=resolve-engine.js.map
@@ -0,0 +1,66 @@
1
+ import type { ExternalRef } from './types.ts';
2
+ import type { ReadbackResult } from './provider-readback.ts';
3
+ /** What the orchestrator should do for this phase on resume. */
4
+ export type ResumeDecision =
5
+ /** No prior success → run the phase normally (no preflight needed). */
6
+ {
7
+ kind: 'proceed-fresh';
8
+ }
9
+ /** All postEffect refs `merged`/`live` → emit phase.success with
10
+ * `replayed: true` and skip the phase body. */
11
+ | {
12
+ kind: 'skip-already-applied';
13
+ readback: ReadbackResult[];
14
+ reason: string;
15
+ }
16
+ /** PreEffect ref `open` + postEffect incomplete → re-run the phase body;
17
+ * its own ledger handles partial-state dedup. */
18
+ | {
19
+ kind: 'retry';
20
+ readback: ReadbackResult[];
21
+ reason: string;
22
+ }
23
+ /** Ambiguous state → emit replay.override + throw needs_human. */
24
+ | {
25
+ kind: 'needs-human';
26
+ readback: ReadbackResult[];
27
+ reason: string;
28
+ refsConsulted: ExternalRef[];
29
+ };
30
+ export interface ResumePreflightInput {
31
+ /** The phase's contract — read out of `PHASE_REGISTRY[name]`. Empty
32
+ * arrays are valid (e.g. `pr` declares post-effect = []). */
33
+ preEffectRefKinds: readonly string[];
34
+ postEffectRefKinds: readonly string[];
35
+ /** Did the prior orchestrator attempt record `phase.success` for this
36
+ * phaseIdx? When false, we return `proceed-fresh` immediately. */
37
+ priorPhaseSuccess: boolean;
38
+ /** Refs persisted by the prior attempt — both pre-effect and post-effect
39
+ * kinds, in event order. The preflight filters them by kind to map
40
+ * to the contract. */
41
+ priorRefs: readonly ExternalRef[];
42
+ /** Test seam — replace `verifyRefs` so unit tests don't need the real
43
+ * github / supabase readbacks. Production callers omit this. */
44
+ verifyRefsImpl?: (refs: readonly ExternalRef[]) => Promise<ReadbackResult[]>;
45
+ }
46
+ /**
47
+ * Make the resume decision for one side-effecting phase.
48
+ *
49
+ * The contract:
50
+ * - When `priorPhaseSuccess === false` → `proceed-fresh` (no preflight).
51
+ * - When `priorRefs` is empty AND we have a prior phase.success →
52
+ * `needs-human` (the phase claimed success but didn't persist any
53
+ * ref — corrupted state we shouldn't auto-retry).
54
+ * - When all refs whose kind is in `postEffectRefKinds` read back as
55
+ * `merged` / `live` AND postEffect is non-empty → `skip-already-applied`.
56
+ * - When at least one preEffect ref reads back as `open` AND not all
57
+ * post-effect refs merged → `retry`.
58
+ * - Otherwise → `needs-human`.
59
+ *
60
+ * `pr`'s contract has `postEffectRefKinds: []`. For that case the
61
+ * pre-effect ref (`github-pr`) doubles as the reconciliation ref — we
62
+ * inspect IT against `merged`/`open` semantics. Any other state →
63
+ * needs-human.
64
+ */
65
+ export declare function resumePreflight(input: ResumePreflightInput): Promise<ResumeDecision>;
66
+ //# sourceMappingURL=resume-preflight.d.ts.map
@@ -0,0 +1,116 @@
1
+ // src/core/run-state/resume-preflight.ts
2
+ //
3
+ // v6.2.1 — orchestrator resume preflight for side-effecting phases.
4
+ //
5
+ // Background — the v6.2.1 side-effect idempotency contract requires every
6
+ // `hasSideEffects: true` phase to declare two ref kind sets:
7
+ //
8
+ // - `preEffectRefKinds` — recorded BEFORE the side effect runs.
9
+ // - `postEffectRefKinds` — recorded AFTER the side effect completes.
10
+ //
11
+ // On resume of a runId the orchestrator must consult the persisted refs +
12
+ // the platform of record (via `verifyRefs`) before re-invoking the phase
13
+ // body. This module owns that decision.
14
+ //
15
+ // The decision matrix (per spec "Resume preflight pseudocode"):
16
+ //
17
+ // - All postEffect refs read back as `merged` / `live`
18
+ // ⇒ `skip-already-applied` — orchestrator emits `phase.success
19
+ // { replayed: true, reason: 'side-effect-already-applied' }` and
20
+ // advances. NO retry.
21
+ // - PreEffect ref read back as `open` AND postEffect refs incomplete
22
+ // ⇒ `retry` — orchestrator runs the phase body normally; the phase's
23
+ // own ledger / preflight handles dedup against the partial state.
24
+ // - Otherwise (no prior success, ambiguous, or readback unknown)
25
+ // ⇒ `needs-human` — orchestrator emits `replay.override` event and
26
+ // throws `GuardrailError('needs_human')`.
27
+ //
28
+ // Fresh runs (no prior phase.success for this phaseIdx) get `proceed-fresh`
29
+ // — the orchestrator just runs the phase normally.
30
+ import { verifyRefs as defaultVerifyRefs } from "./provider-readback.js";
31
+ /**
32
+ * Make the resume decision for one side-effecting phase.
33
+ *
34
+ * The contract:
35
+ * - When `priorPhaseSuccess === false` → `proceed-fresh` (no preflight).
36
+ * - When `priorRefs` is empty AND we have a prior phase.success →
37
+ * `needs-human` (the phase claimed success but didn't persist any
38
+ * ref — corrupted state we shouldn't auto-retry).
39
+ * - When all refs whose kind is in `postEffectRefKinds` read back as
40
+ * `merged` / `live` AND postEffect is non-empty → `skip-already-applied`.
41
+ * - When at least one preEffect ref reads back as `open` AND not all
42
+ * post-effect refs merged → `retry`.
43
+ * - Otherwise → `needs-human`.
44
+ *
45
+ * `pr`'s contract has `postEffectRefKinds: []`. For that case the
46
+ * pre-effect ref (`github-pr`) doubles as the reconciliation ref — we
47
+ * inspect IT against `merged`/`open` semantics. Any other state →
48
+ * needs-human.
49
+ */
50
+ export async function resumePreflight(input) {
51
+ if (!input.priorPhaseSuccess && input.priorRefs.length === 0) {
52
+ return { kind: 'proceed-fresh' };
53
+ }
54
+ const verify = input.verifyRefsImpl ?? defaultVerifyRefs;
55
+ const readback = input.priorRefs.length > 0 ? await verify(input.priorRefs) : [];
56
+ // postEffect path — if the contract declares post-effect kinds, the
57
+ // pre-effect breadcrumb's readback state is authoritative for the
58
+ // batch's "did all the planned work land?" question. Post-effect refs
59
+ // alone aren't authoritative because the resume doesn't know the total
60
+ // planned set just from the persisted ref count (a partial crash
61
+ // persists 3 of 5 migration-version refs and 0 readback for the missing
62
+ // 2 — counting the persisted set against itself always says "complete").
63
+ // The batch readback is the source of truth: it queries the dispatcher's
64
+ // ledger for the planned set vs the applied set.
65
+ if (input.postEffectRefKinds.length > 0) {
66
+ const preRefs = readback.filter(r => input.preEffectRefKinds.includes(r.refKind));
67
+ const postRefs = readback.filter(r => input.postEffectRefKinds.includes(r.refKind));
68
+ // Skip-already-applied: pre-effect breadcrumb confirms `merged` AND
69
+ // every persisted post-effect ref is live/merged. The pre-effect's
70
+ // `merged` state comes from the batch readback comparing planned to
71
+ // applied — so we trust it as the cross-set check.
72
+ const preMerged = preRefs.length > 0 && preRefs.every(r => r.currentState === 'merged' || r.currentState === 'live');
73
+ const postAllLive = postRefs.every(r => r.currentState === 'merged' || r.currentState === 'live');
74
+ if (preMerged && postAllLive) {
75
+ return {
76
+ kind: 'skip-already-applied',
77
+ readback,
78
+ reason: `pre-effect ref reports merged + ${postRefs.length} post-effect ref(s) live`,
79
+ };
80
+ }
81
+ // Retry: pre-effect breadcrumb is `open` (some planned items still
82
+ // pending). The phase body's own ledger guard handles dedup of the
83
+ // already-applied items.
84
+ const preOpen = preRefs.some(r => r.currentState === 'open');
85
+ if (preOpen) {
86
+ return {
87
+ kind: 'retry',
88
+ readback,
89
+ reason: 'pre-effect breadcrumb open + post-effect refs incomplete',
90
+ };
91
+ }
92
+ }
93
+ else {
94
+ // Empty postEffectRefKinds — pre-effect ref doubles as reconciliation.
95
+ // Used by `pr`: the github-pr ref IS the post-effect ref.
96
+ const preRefs = readback.filter(r => input.preEffectRefKinds.includes(r.refKind));
97
+ if (preRefs.length > 0 && preRefs.every(r => r.currentState === 'merged' || r.currentState === 'live' || r.currentState === 'open')) {
98
+ // For pr: open/merged both mean "the PR exists, don't recreate it."
99
+ return {
100
+ kind: 'skip-already-applied',
101
+ readback,
102
+ reason: `pre-effect ref doubles as reconciliation; all ${preRefs.length} report live/merged/open`,
103
+ };
104
+ }
105
+ }
106
+ // Anything else — refs missing, closed without merge, unknown — punt.
107
+ return {
108
+ kind: 'needs-human',
109
+ readback,
110
+ refsConsulted: [...input.priorRefs],
111
+ reason: readback.length === 0
112
+ ? 'prior phase.success exists but no externalRefs persisted'
113
+ : 'readback could not confirm skip-already-applied or retry-safe',
114
+ };
115
+ }
116
+ //# sourceMappingURL=resume-preflight.js.map
@@ -0,0 +1,69 @@
1
+ import { type RunPhase } from './phase-runner.ts';
2
+ import type { GuardrailConfig } from '../config/types.ts';
3
+ /** Caller-supplied inputs to drive a single-phase engine run.
4
+ *
5
+ * The helper is intentionally narrow: every wrapped verb passes the same
6
+ * shape today. Side-effecting verbs (`pr`, `migrate`, `implement`) will
7
+ * use the same shape too — they emit externalRefs from inside `phase.run`
8
+ * via `ctx.emitExternalRef()`, which the underlying `runPhase()` records.
9
+ * No new helper field is needed for them in v6.0.7+. */
10
+ export interface RunPhaseWithLifecycleOpts<I, O> {
11
+ /** Project working directory. Passed straight through to `createRun`
12
+ * so `.guardrail-cache/runs/<ulid>/` lands in the right place. */
13
+ cwd: string;
14
+ /** The pre-built phase definition (name, idempotent, hasSideEffects, run).
15
+ * Same `RunPhase<I, O>` the caller would pass to `runPhase()` directly. */
16
+ phase: RunPhase<I, O>;
17
+ /** Phase input — same shape the caller would pass to `runPhase()`. */
18
+ input: I;
19
+ /** Loaded `guardrail.config.yaml` (or the default `{ configVersion: 1 }`).
20
+ * Used only for the `engine.enabled` precedence layer. The helper does
21
+ * NOT re-load config — that's the caller's responsibility (and lets
22
+ * callers pass synthetic configs in tests). */
23
+ config: GuardrailConfig;
24
+ /** CLI flag override — `true` from `--engine`, `false` from `--no-engine`,
25
+ * `undefined` if neither was passed. */
26
+ cliEngine: boolean | undefined;
27
+ /** Raw value of `process.env.CLAUDE_AUTOPILOT_ENGINE`. `undefined` if
28
+ * unset. The helper passes this through to `resolveEngineEnabled` —
29
+ * invalid values fall through with a `run.warning` recorded automatically. */
30
+ envEngine: string | undefined;
31
+ /** v6.x escape hatch — invoked when the engine was disabled. Retained
32
+ * in the v7.0 type for source compatibility with existing callers,
33
+ * but the helper NEVER calls it in v7.0+ (engine-off path was
34
+ * removed). Optional in v7.0; can be omitted from new call sites. */
35
+ runEngineOff?: () => Promise<O>;
36
+ }
37
+ /** What the helper hands back. `runId` and `runDir` are null on the
38
+ * engine-off path so callers can branch on whether engine artifacts exist
39
+ * (e.g. for a future `--json` envelope that surfaces the runId). */
40
+ export interface RunPhaseWithLifecycleResult<O> {
41
+ output: O;
42
+ /** ULID of the created run, or null when engine-off. */
43
+ runId: string | null;
44
+ /** Absolute path to the run dir, or null when engine-off. */
45
+ runDir: string | null;
46
+ }
47
+ /** Drive a single-phase engine run with full lifecycle instrumentation,
48
+ * OR fall through to the legacy `runEngineOff` callback when the engine
49
+ * is disabled by config / CLI / env precedence.
50
+ *
51
+ * Engine-on lifecycle (in order):
52
+ * createRun → (optional run.warning for invalid env) → runPhase →
53
+ * run.complete (success or failed) → refresh state.json → release lock.
54
+ *
55
+ * On phase failure the helper:
56
+ * 1. Emits `run.complete` with `status: 'failed'`.
57
+ * 2. Refreshes state.json from the replayed events.
58
+ * 3. Prints the legacy `[<phase>] engine: phase failed — <msg>` banner
59
+ * to stderr (byte-for-byte identical to the inline pattern that
60
+ * lived in 8 of 8 wrapped verbs pre-v6.0.6).
61
+ * 4. Releases the lock and re-throws so the caller can return its
62
+ * legacy non-zero exit code.
63
+ *
64
+ * The lock release in `finally` is best-effort. `release()` is idempotent
65
+ * (the runs lock module accepts double-release without throwing), so the
66
+ * catch block does not need to release the lock itself — `finally` covers
67
+ * both the success and failure exit paths. */
68
+ export declare function runPhaseWithLifecycle<I, O>(opts: RunPhaseWithLifecycleOpts<I, O>): Promise<RunPhaseWithLifecycleResult<O>>;
69
+ //# sourceMappingURL=run-phase-with-lifecycle.d.ts.map
@@ -0,0 +1,193 @@
1
+ // src/core/run-state/run-phase-with-lifecycle.ts
2
+ //
3
+ // v6.0.6 — extract the lifecycle wrapper that's been duplicated across
4
+ // every wrapped CLI verb (`scan`, `costs`, `fix`, `brainstorm`, `spec`,
5
+ // `plan`, `review`, `validate`). The pattern is mechanical:
6
+ //
7
+ // 1. If engine-off → run the legacy phase body via `runEngineOff()` and
8
+ // return its result.
9
+ // 2. If engine-on → createRun → optional run.warning for invalid env →
10
+ // runPhase → emit run.complete (success or failed) → refresh state.json
11
+ // → release lock (best effort, in finally).
12
+ //
13
+ // This helper sits ON TOP of `runPhase()` from `phase-runner.ts` — it does
14
+ // not replace it. Callers continue to define their own `RunPhase<I, O>` with
15
+ // per-phase `idempotent` / `hasSideEffects` / `run` and pass it in.
16
+ //
17
+ // Why now: with 8 of 10 phases wrapped (the v6.0.5 milestone), the pattern
18
+ // is fully evidenced. The remaining 3 phases (`implement`, `migrate`, `pr`)
19
+ // are side-effecting and need externalRefs — those will inform a v6.0.7+
20
+ // extension to this helper but won't change its core shape. Doing the
21
+ // extraction now means those 3 wraps build against the helper instead of
22
+ // re-introducing the boilerplate.
23
+ //
24
+ // What this helper does NOT do:
25
+ // - Print success banners — rendering stays in the caller.
26
+ // - Decide engine-off behavior — that's `runEngineOff`, supplied by the
27
+ // caller (typically a thin closure over the phase body).
28
+ // - Plumb externalRefs / readback — the underlying `runPhase()` already
29
+ // handles those. This helper just owns the run-level lifecycle events.
30
+ //
31
+ // Future extension (v6.0.7+): `implement` / `migrate` / `pr` need
32
+ // externalRef ledger entries (`git-remote-push`, `migration-version`,
33
+ // `github-pr`). The helper's `phase.run` already receives `ctx` so
34
+ // `ctx.emitExternalRef()` works without changes here. If a future PR needs
35
+ // to fan-in run-wide externalRefs from multiple phases (multi-phase
36
+ // pipelines, e.g. autopilot orchestrator), the signature can grow a
37
+ // `phases: RunPhase[]` overload — but the single-phase shape stays identical.
38
+ import { createRun } from "./runs.js";
39
+ import { runPhase } from "./phase-runner.js";
40
+ import { appendEvent, replayState } from "./events.js";
41
+ import { writeStateSnapshot } from "./state.js";
42
+ import { resolveEngineEnabled, } from "./resolve-engine.js";
43
+ // Inline ANSI codes — same shape every wrapped verb uses. Kept here so the
44
+ // helper doesn't depend on a verb-local `fmt`. The error message format
45
+ // (`[<phase>] engine: phase failed — <msg>` + dim inspect hint) is
46
+ // byte-for-byte identical to what every wrapped phase printed pre-extract.
47
+ const ANSI_RESET = '\x1b[0m';
48
+ const ANSI_DIM = '\x1b[2m';
49
+ const ANSI_RED = '\x1b[31m';
50
+ /** Drive a single-phase engine run with full lifecycle instrumentation,
51
+ * OR fall through to the legacy `runEngineOff` callback when the engine
52
+ * is disabled by config / CLI / env precedence.
53
+ *
54
+ * Engine-on lifecycle (in order):
55
+ * createRun → (optional run.warning for invalid env) → runPhase →
56
+ * run.complete (success or failed) → refresh state.json → release lock.
57
+ *
58
+ * On phase failure the helper:
59
+ * 1. Emits `run.complete` with `status: 'failed'`.
60
+ * 2. Refreshes state.json from the replayed events.
61
+ * 3. Prints the legacy `[<phase>] engine: phase failed — <msg>` banner
62
+ * to stderr (byte-for-byte identical to the inline pattern that
63
+ * lived in 8 of 8 wrapped verbs pre-v6.0.6).
64
+ * 4. Releases the lock and re-throws so the caller can return its
65
+ * legacy non-zero exit code.
66
+ *
67
+ * The lock release in `finally` is best-effort. `release()` is idempotent
68
+ * (the runs lock module accepts double-release without throwing), so the
69
+ * catch block does not need to release the lock itself — `finally` covers
70
+ * both the success and failure exit paths. */
71
+ export async function runPhaseWithLifecycle(opts) {
72
+ const { cwd, phase, input, config, cliEngine, envEngine } = opts;
73
+ // v7.0 — engine is always on. resolveEngineEnabled() returns
74
+ // { enabled: true, source: 'default' } unconditionally. We still
75
+ // pass the v6-era inputs through so any future re-introduction of
76
+ // observability (a run.warning when a user passes the removed flags
77
+ // via env vars in CI) is a one-line change.
78
+ const engineResolved = resolveEngineEnabled({
79
+ ...(cliEngine !== undefined ? { cliEngine } : {}),
80
+ ...(envEngine !== undefined ? { envValue: envEngine } : {}),
81
+ ...(typeof config.engine?.enabled === 'boolean'
82
+ ? { configEnabled: config.engine.enabled }
83
+ : {}),
84
+ });
85
+ // Engine on — full lifecycle. Mirrors the pre-v6.0.6 inline shape that
86
+ // every wrapped verb duplicated.
87
+ const created = await createRun({
88
+ cwd,
89
+ phases: [phase.name],
90
+ config: {
91
+ engine: { enabled: true, source: engineResolved.source },
92
+ ...(engineResolved.invalidEnvValue !== undefined
93
+ ? { invalidEnvValue: engineResolved.invalidEnvValue }
94
+ : {}),
95
+ },
96
+ });
97
+ if (engineResolved.invalidEnvValue !== undefined) {
98
+ // Surface the invalid env value as a typed warning so observers
99
+ // (`runs show <id> --events`) can attribute the fallthrough.
100
+ appendEvent(created.runDir, {
101
+ event: 'run.warning',
102
+ message: `invalid CLAUDE_AUTOPILOT_ENGINE=${JSON.stringify(engineResolved.invalidEnvValue)} ignored`,
103
+ details: { resolution: engineResolved },
104
+ }, { writerId: created.lock.writerId, runId: created.runId });
105
+ }
106
+ // v7.0 — emit `engine_off_removed` warning when CLAUDE_AUTOPILOT_ENGINE
107
+ // is set to an off-style value. The env value is otherwise ignored
108
+ // (engine remains on). Softer than --no-engine (which exits 1) because
109
+ // env vars in CI are sticky and silently breaking every v6.x → v7
110
+ // upgrade in CI on day one would burn user trust. See spec test #1(c).
111
+ if (envEngine !== undefined) {
112
+ const normalized = envEngine.trim().toLowerCase();
113
+ if (normalized === 'off' || normalized === 'false' || normalized === '0' || normalized === 'no') {
114
+ appendEvent(created.runDir, {
115
+ event: 'run.warning',
116
+ message: 'engine_off_removed',
117
+ details: {
118
+ code: 'engine_off_removed',
119
+ envValue: envEngine,
120
+ note: 'CLAUDE_AUTOPILOT_ENGINE=off has no effect in v7.0+; engine remains on.',
121
+ },
122
+ }, { writerId: created.lock.writerId, runId: created.runId });
123
+ }
124
+ }
125
+ const runStartedAt = Date.now();
126
+ try {
127
+ const output = await runPhase(phase, input, {
128
+ runDir: created.runDir,
129
+ runId: created.runId,
130
+ writerId: created.lock.writerId,
131
+ phaseIdx: 0,
132
+ });
133
+ // Final lifecycle event — run.complete. The runner doesn't emit this
134
+ // on its own; it's the caller's responsibility (multi-phase pipelines
135
+ // emit it after the LAST phase, single-phase wrappers like this emit
136
+ // after the only phase). Total cost falls back to 0 when the phase
137
+ // doesn't expose a `costUSD` field on its output (read-only verbs
138
+ // don't track cost; scan does).
139
+ const totalCostUSD = extractCostUSD(output);
140
+ appendEvent(created.runDir, {
141
+ event: 'run.complete',
142
+ status: 'success',
143
+ totalCostUSD,
144
+ durationMs: Date.now() - runStartedAt,
145
+ }, { writerId: created.lock.writerId, runId: created.runId });
146
+ // Refresh state.json from the replayed events. The events.ndjson is
147
+ // the source of truth; state.json is a derived snapshot that we MUST
148
+ // rewrite after run.complete so `runs show` / `runs list` reflect the
149
+ // terminal status without needing to replay on every read.
150
+ writeStateSnapshot(created.runDir, replayState(created.runDir));
151
+ return { output, runId: created.runId, runDir: created.runDir };
152
+ }
153
+ catch (err) {
154
+ // Engine-on failure — write run.complete with failed status, refresh
155
+ // state.json, print the legacy banner to stderr, then re-throw so the
156
+ // caller can return its legacy non-zero exit code. (Lock release
157
+ // happens in `finally` regardless of success / failure path.)
158
+ appendEvent(created.runDir, {
159
+ event: 'run.complete',
160
+ status: 'failed',
161
+ totalCostUSD: 0,
162
+ durationMs: Date.now() - runStartedAt,
163
+ }, { writerId: created.lock.writerId, runId: created.runId });
164
+ writeStateSnapshot(created.runDir, replayState(created.runDir));
165
+ const message = err instanceof Error ? err.message : String(err);
166
+ process.stderr.write(`${ANSI_RED}[${phase.name}] engine: phase failed — ${message}${ANSI_RESET}\n`);
167
+ process.stderr.write(`${ANSI_DIM} inspect: claude-autopilot runs show ${created.runId} --events${ANSI_RESET}\n`);
168
+ throw err;
169
+ }
170
+ finally {
171
+ // Best-effort lock release. The lock module's `release()` is
172
+ // idempotent; if the catch path already released (it doesn't, but a
173
+ // future change might), this is a no-op. Wrapping the await in
174
+ // `.catch(() => {})` ensures a release error never masks the original
175
+ // throw — the spec calls this out explicitly.
176
+ await created.lock.release().catch(() => { });
177
+ }
178
+ }
179
+ /** Extract `costUSD` from a phase output if present, else 0. JSON-style
180
+ * duck-typing: we accept any output that exposes a numeric `costUSD`
181
+ * field. Today only `scan` exposes one; the other 7 wrapped verbs
182
+ * return outputs without a cost field, which means `extractCostUSD`
183
+ * returns 0 — byte-for-byte matching the inline `totalCostUSD: 0` they
184
+ * used pre-v6.0.6. */
185
+ function extractCostUSD(output) {
186
+ if (output !== null && typeof output === 'object' && 'costUSD' in output) {
187
+ const v = output.costUSD;
188
+ if (typeof v === 'number' && Number.isFinite(v))
189
+ return v;
190
+ }
191
+ return 0;
192
+ }
193
+ //# sourceMappingURL=run-phase-with-lifecycle.js.map
@@ -0,0 +1,57 @@
1
+ import { type RunLockHandle } from './lock.ts';
2
+ import { type RunIndex, type RunIndexEntry, type RunState } from './types.ts';
3
+ export declare function runsRoot(cwd: string): string;
4
+ export declare function indexPath(cwd: string): string;
5
+ export declare function runDirFor(cwd: string, runId: string): string;
6
+ export interface CreateRunOptions {
7
+ cwd: string;
8
+ /** Phase names in the order they will execute. */
9
+ phases: string[];
10
+ /** Snapshot of the relevant guardrail.config.yaml fields. Free-form. */
11
+ config?: Record<string, unknown>;
12
+ }
13
+ export interface CreateRunResult {
14
+ runId: string;
15
+ runDir: string;
16
+ state: RunState;
17
+ /** Lock handle. Caller MUST `release()` on shutdown. */
18
+ lock: RunLockHandle;
19
+ }
20
+ /** Create a fresh run directory, acquire its advisory lock, write the
21
+ * initial state.json, and emit the `run.start` event.
22
+ *
23
+ * Throws GuardrailError(lock_held) if a stale lock exists for the freshly-
24
+ * generated runId — extremely unlikely (ULIDs are unique) but possible if
25
+ * two parallel invocations on the same OS clock collide on a leftover dir
26
+ * on disk. Caller can simply retry. */
27
+ export declare function createRun(opts: CreateRunOptions): Promise<CreateRunResult>;
28
+ /** Rebuild index.json from each run dir's state.json (or replayed state if
29
+ * the snapshot is missing / corrupt). Newest-first ordering by ULID. */
30
+ export declare function rebuildIndex(cwd: string): RunIndex;
31
+ export interface ListRunsOptions {
32
+ /** Force a rebuild from disk even if index.json is fresh. */
33
+ rebuild?: boolean;
34
+ }
35
+ /** List all runs, newest-first. Lazily rebuilds index.json if missing. */
36
+ export declare function listRuns(cwd: string, opts?: ListRunsOptions): RunIndexEntry[];
37
+ export interface GcRunsOptions {
38
+ /** Delete completed runs older than this many days. Required. */
39
+ olderThanDays: number;
40
+ /** Don't actually delete; just return what would be removed. */
41
+ dryRun?: boolean;
42
+ /** Override "now" for tests. Default Date.now(). */
43
+ now?: number;
44
+ }
45
+ export interface GcRunsResult {
46
+ /** runIds that were (or would be) deleted. */
47
+ deleted: string[];
48
+ /** runIds skipped because they're still active or too young. */
49
+ kept: string[];
50
+ /** runIds skipped for safety reasons (symlink, suspicious path). */
51
+ skippedUnsafe: string[];
52
+ }
53
+ /** Delete completed runs older than N days. Honors the spec's symlink
54
+ * safety: uses lstat so we never traverse a symlink out of the runs/
55
+ * tree. */
56
+ export declare function gcRuns(cwd: string, opts: GcRunsOptions): GcRunsResult;
57
+ //# sourceMappingURL=runs.d.ts.map