@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.
- package/CHANGELOG.md +1776 -6
- package/README.md +65 -1
- package/bin/_launcher.js +38 -23
- package/dist/src/adapters/council/openai.js +12 -6
- 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/index.d.ts +2 -0
- package/dist/src/adapters/deploy/index.js +33 -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 +67 -3
- package/dist/src/adapters/deploy/vercel.d.ts +17 -1
- package/dist/src/adapters/deploy/vercel.js +29 -49
- package/dist/src/adapters/pricing.d.ts +36 -0
- package/dist/src/adapters/pricing.js +40 -0
- package/dist/src/adapters/review-engine/codex.js +10 -7
- package/dist/src/cli/autopilot.d.ts +75 -0
- package/dist/src/cli/autopilot.js +750 -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/dashboard/index.d.ts +5 -0
- package/dist/src/cli/dashboard/index.js +49 -0
- package/dist/src/cli/dashboard/login.d.ts +22 -0
- package/dist/src/cli/dashboard/login.js +260 -0
- package/dist/src/cli/dashboard/logout.d.ts +12 -0
- package/dist/src/cli/dashboard/logout.js +45 -0
- package/dist/src/cli/dashboard/status.d.ts +30 -0
- package/dist/src/cli/dashboard/status.js +65 -0
- package/dist/src/cli/dashboard/upload.d.ts +16 -0
- package/dist/src/cli/dashboard/upload.js +48 -0
- package/dist/src/cli/deploy.d.ts +3 -3
- package/dist/src/cli/deploy.js +34 -9
- package/dist/src/cli/engine-flag-deprecation.d.ts +14 -0
- package/dist/src/cli/engine-flag-deprecation.js +20 -0
- package/dist/src/cli/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 +416 -0
- package/dist/src/cli/implement.d.ts +91 -0
- package/dist/src/cli/implement.js +196 -0
- package/dist/src/cli/index.d.ts +2 -1
- package/dist/src/cli/index.js +774 -245
- 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 +26 -0
- 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/scaffold.d.ts +39 -0
- package/dist/src/cli/scaffold.js +287 -0
- package/dist/src/cli/scan.d.ts +93 -0
- package/dist/src/cli/scan.js +166 -40
- package/dist/src/cli/setup.d.ts +30 -0
- package/dist/src/cli/setup.js +137 -0
- 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 +9 -0
- package/dist/src/core/config/schema.js +7 -0
- package/dist/src/core/config/types.d.ts +11 -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 +11 -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/schema-validator.js +15 -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 +512 -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 +45 -0
- package/dist/src/core/run-state/resolve-engine.js +74 -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 +69 -0
- package/dist/src/core/run-state/run-phase-with-lifecycle.js +193 -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 +284 -0
- package/dist/src/core/run-state/types.js +19 -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/dist/src/dashboard/auto-upload.d.ts +26 -0
- package/dist/src/dashboard/auto-upload.js +107 -0
- package/dist/src/dashboard/config.d.ts +22 -0
- package/dist/src/dashboard/config.js +109 -0
- package/dist/src/dashboard/upload/canonical.d.ts +3 -0
- package/dist/src/dashboard/upload/canonical.js +16 -0
- package/dist/src/dashboard/upload/chain.d.ts +9 -0
- package/dist/src/dashboard/upload/chain.js +27 -0
- package/dist/src/dashboard/upload/snapshot.d.ts +23 -0
- package/dist/src/dashboard/upload/snapshot.js +66 -0
- package/dist/src/dashboard/upload/uploader.d.ts +54 -0
- package/dist/src/dashboard/upload/uploader.js +330 -0
- package/package.json +19 -3
- package/scripts/autoregress.ts +1 -1
- package/scripts/test-runner.mjs +4 -0
- package/skills/claude-autopilot.md +1 -1
- package/skills/make-interfaces-feel-better/SKILL.md +104 -0
- 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,80 @@
|
|
|
1
|
+
import { type BudgetCheck, type BudgetConfig } from './budget.ts';
|
|
2
|
+
import { type PhaseContext } from './phase-context.ts';
|
|
3
|
+
import { type ReadbackResult } from './provider-readback.ts';
|
|
4
|
+
import { type ExternalRef, type WriterId } from './types.ts';
|
|
5
|
+
/** What `RunPhase.onResume` receives when a previous attempt of the same
|
|
6
|
+
* phaseIdx exists. Phase 6 will fully wire this; in Phase 2 we expose the
|
|
7
|
+
* shape so callers can author against it without a later breaking change. */
|
|
8
|
+
export interface PhaseResumeContext {
|
|
9
|
+
runDir: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
phaseIdx: number;
|
|
12
|
+
/** All externalRefs recorded for this phase across prior attempts. */
|
|
13
|
+
externalRefs: ExternalRef[];
|
|
14
|
+
/** How many `phase.start` events have been observed for this phaseIdx
|
|
15
|
+
* (i.e. the attempt count of the prior run). */
|
|
16
|
+
attempts: number;
|
|
17
|
+
/** Whether the previous attempt was a phase.success (was the phase already
|
|
18
|
+
* done before the current resume began?). */
|
|
19
|
+
succeeded: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The phase contract — the only object an existing pipeline needs to
|
|
22
|
+
* implement to be run by the engine. Existing phases are wrapped, NOT
|
|
23
|
+
* rewritten; in Phase 2 we ship the wrapper but no actual phase consumes
|
|
24
|
+
* it yet. */
|
|
25
|
+
export interface RunPhase<I = unknown, O = unknown> {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly idempotent: boolean;
|
|
28
|
+
readonly hasSideEffects: boolean;
|
|
29
|
+
estimateCost?(input: I): {
|
|
30
|
+
lowUSD: number;
|
|
31
|
+
highUSD: number;
|
|
32
|
+
};
|
|
33
|
+
run(input: I, ctx: PhaseContext): Promise<O>;
|
|
34
|
+
/** Called when resuming after a previous failure / completion. Decides
|
|
35
|
+
* whether to skip, retry, abort, or bubble to a human. Default behavior
|
|
36
|
+
* (when this method is absent) is encoded in `runPhase` itself: idempotent
|
|
37
|
+
* phases retry, side-effecting phases require `--force-replay`. */
|
|
38
|
+
onResume?(prev: PhaseResumeContext): Promise<'skip' | 'retry' | 'abort' | 'needs-human'>;
|
|
39
|
+
}
|
|
40
|
+
/** What the caller passes in. We require runDir/runId/writerId to be already
|
|
41
|
+
* established (the run-creator already did this). */
|
|
42
|
+
export interface ParentRunContext {
|
|
43
|
+
runDir: string;
|
|
44
|
+
runId: string;
|
|
45
|
+
writerId: WriterId;
|
|
46
|
+
/** Index of this phase within the run's `phases[]`. */
|
|
47
|
+
phaseIdx: number;
|
|
48
|
+
/** When true, override the side-effects gate even if a prior success
|
|
49
|
+
* exists. Records a `run.warning` event noting the override. */
|
|
50
|
+
forceReplay?: boolean;
|
|
51
|
+
/** Phase 4 — optional budget enforcement config. When omitted the
|
|
52
|
+
* runner is back-compat: no `budget.check` event, no preflight, no
|
|
53
|
+
* rejection. When present, the runner consults `checkPhaseBudget`
|
|
54
|
+
* BEFORE emitting `phase.start` and may throw `budget_exceeded`. */
|
|
55
|
+
budget?: BudgetConfig;
|
|
56
|
+
/** When true, a `pause` budget decision becomes `hard-fail` instead of
|
|
57
|
+
* prompting the user. Callers in CI / `--json` mode MUST set this.
|
|
58
|
+
* Default: false (interactive). */
|
|
59
|
+
nonInteractive?: boolean;
|
|
60
|
+
/** Override the interactive confirm prompt. Returning `true` proceeds,
|
|
61
|
+
* `false` rejects. Mainly a test seam; the default uses readline. */
|
|
62
|
+
confirmBudgetPause?: (check: BudgetCheck) => Promise<boolean>;
|
|
63
|
+
/** Phase 6 — override the readback layer. Defaults to `verifyRefs` from
|
|
64
|
+
* `provider-readback.ts`, which uses the registered providers. Tests
|
|
65
|
+
* inject a stub to avoid hitting `gh` / network. */
|
|
66
|
+
verifyRefs?: (refs: ReadonlyArray<ExternalRef>) => Promise<ReadbackResult[]>;
|
|
67
|
+
}
|
|
68
|
+
export type { PhaseContext } from './phase-context.ts';
|
|
69
|
+
/** Run a single phase with full lifecycle instrumentation.
|
|
70
|
+
*
|
|
71
|
+
* Emits, in order:
|
|
72
|
+
* phase.start — always (unless idempotent short-circuit fires first)
|
|
73
|
+
* phase.cost — zero or more, emitted by the phase via ctx.emitCost
|
|
74
|
+
* phase.externalRef — zero or more, via ctx.emitExternalRef
|
|
75
|
+
* phase.success | phase.failed — exactly one
|
|
76
|
+
*
|
|
77
|
+
* Writes phases/<name>.json after either terminal event so a crash between
|
|
78
|
+
* the event and the snapshot is recoverable from events.ndjson. */
|
|
79
|
+
export declare function runPhase<I, O>(phase: RunPhase<I, O>, input: I, parentCtx: ParentRunContext): Promise<O>;
|
|
80
|
+
//# sourceMappingURL=phase-runner.d.ts.map
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// src/core/run-state/phase-runner.ts
|
|
2
|
+
//
|
|
3
|
+
// v6 Phase 2 — phase wrapper / lifecycle layer.
|
|
4
|
+
//
|
|
5
|
+
// `runPhase` is the orchestrator that wraps a single `RunPhase` invocation:
|
|
6
|
+
//
|
|
7
|
+
// 1. emit phase.start (with attempt counter + idempotent/hasSideEffects
|
|
8
|
+
// flags)
|
|
9
|
+
// 2. call phase.run(input, ctx) — the user's phase body
|
|
10
|
+
// 3. on success → emit phase.success + write phases/<name>.json snapshot
|
|
11
|
+
// 4. on throw → emit phase.failed + write a failed snapshot + rethrow
|
|
12
|
+
//
|
|
13
|
+
// Idempotency / side-effect gating:
|
|
14
|
+
//
|
|
15
|
+
// - If a prior phase.success exists for this (runDir, phaseIdx) AND
|
|
16
|
+
// `phase.idempotent === true`, the runner short-circuits with a
|
|
17
|
+
// `phase.skipped` event-shaped recording (we use the existing
|
|
18
|
+
// phase.success replay-equivalence — a one-shot phase.success is OK
|
|
19
|
+
// because the snapshot will be rewritten with attempts++ and a
|
|
20
|
+
// "skipped"-flavored note in `meta`, plus we emit a `run.warning`
|
|
21
|
+
// with reason `idempotent-replay` so observers can attribute the
|
|
22
|
+
// short-circuit). See "skipped variant" below for the exact event.
|
|
23
|
+
// - If a prior phase.success exists AND `phase.hasSideEffects === true`,
|
|
24
|
+
// the runner refuses without `--force-replay`: it throws GuardrailError
|
|
25
|
+
// `needs_human` carrying the prior externalRefs in `details` so a CI /
|
|
26
|
+
// human consumer can resolve.
|
|
27
|
+
//
|
|
28
|
+
// What this file deliberately does NOT do (Phase 4+ work):
|
|
29
|
+
//
|
|
30
|
+
// - Budget enforcement. `estimateCost` is part of the interface but the
|
|
31
|
+
// policy check lives in a future budget enforcer.
|
|
32
|
+
// - Provider read-back ("is PR #123 still open?"). Phase 6 wires `onResume`
|
|
33
|
+
// to consult externalRefs + read back; Phase 2 just records refs.
|
|
34
|
+
// - Locking. `runPhase` does NOT acquire the per-run advisory lock — the
|
|
35
|
+
// caller (createRun / future resume verb) holds it for the lifetime of
|
|
36
|
+
// the run. We just need a writerId to stamp events; we accept it from
|
|
37
|
+
// parentCtx.
|
|
38
|
+
//
|
|
39
|
+
// Spec: docs/specs/v6-run-state-engine.md "Phase contract", "Run lifecycle",
|
|
40
|
+
// "Idempotency rules + external operation ledger".
|
|
41
|
+
import * as readline from 'node:readline';
|
|
42
|
+
import { GuardrailError } from "../errors.js";
|
|
43
|
+
import { checkPhaseBudget } from "./budget.js";
|
|
44
|
+
import { appendEvent, readEvents } from "./events.js";
|
|
45
|
+
import { buildPhaseContext, collectExternalRefs, countPhaseAttempts, countPhaseSuccesses, sumPhaseCost, } from "./phase-context.js";
|
|
46
|
+
import { decideReplay, } from "./replay-decision.js";
|
|
47
|
+
import { verifyRefs as defaultVerifyRefs, } from "./provider-readback.js";
|
|
48
|
+
import { readPhaseSnapshot, writePhaseSnapshot } from "./snapshot.js";
|
|
49
|
+
import { RUN_STATE_SCHEMA_VERSION, } from "./types.js";
|
|
50
|
+
// ----------------------------------------------------------------------------
|
|
51
|
+
// runPhase — the orchestrator
|
|
52
|
+
// ----------------------------------------------------------------------------
|
|
53
|
+
/** Run a single phase with full lifecycle instrumentation.
|
|
54
|
+
*
|
|
55
|
+
* Emits, in order:
|
|
56
|
+
* phase.start — always (unless idempotent short-circuit fires first)
|
|
57
|
+
* phase.cost — zero or more, emitted by the phase via ctx.emitCost
|
|
58
|
+
* phase.externalRef — zero or more, via ctx.emitExternalRef
|
|
59
|
+
* phase.success | phase.failed — exactly one
|
|
60
|
+
*
|
|
61
|
+
* Writes phases/<name>.json after either terminal event so a crash between
|
|
62
|
+
* the event and the snapshot is recoverable from events.ndjson. */
|
|
63
|
+
export async function runPhase(phase, input, parentCtx) {
|
|
64
|
+
const { runDir, runId, writerId, phaseIdx, forceReplay, budget, nonInteractive, confirmBudgetPause, verifyRefs, } = parentCtx;
|
|
65
|
+
// -- Idempotency / side-effect gating (Phase 6) ------------------------
|
|
66
|
+
// We replay events.ndjson once up-front to detect prior outcomes for this
|
|
67
|
+
// phaseIdx. Cheap — Phase 1 already reads the whole file for replayState.
|
|
68
|
+
const prior = readEvents(runDir);
|
|
69
|
+
const priorSuccessCount = countPhaseSuccesses(prior.events, phaseIdx);
|
|
70
|
+
const priorAttemptCount = countPhaseAttempts(prior.events, phaseIdx);
|
|
71
|
+
const priorRefs = collectExternalRefs(prior.events, phaseIdx);
|
|
72
|
+
if (priorSuccessCount > 0) {
|
|
73
|
+
// Run readbacks ONLY when we'd actually need them (side-effect phases
|
|
74
|
+
// with refs). Idempotent / no-side-effect / no-refs branches don't
|
|
75
|
+
// need a network call to decide.
|
|
76
|
+
let readbacks = [];
|
|
77
|
+
if (phase.hasSideEffects && !phase.idempotent && priorRefs.length > 0 && !forceReplay) {
|
|
78
|
+
const verifier = verifyRefs ?? defaultVerifyRefs;
|
|
79
|
+
try {
|
|
80
|
+
readbacks = await verifier(priorRefs);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Defense in depth — verifyRefs is supposed to fail-closed per ref,
|
|
84
|
+
// but if the wrapper itself throws we collapse all refs to unknown.
|
|
85
|
+
readbacks = priorRefs.map(r => ({
|
|
86
|
+
refKind: r.kind,
|
|
87
|
+
refId: r.id,
|
|
88
|
+
existsOnPlatform: false,
|
|
89
|
+
currentState: 'unknown',
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const decision = decideReplay({
|
|
94
|
+
phaseName: phase.name,
|
|
95
|
+
hasPriorSuccess: true,
|
|
96
|
+
priorAttempts: priorAttemptCount,
|
|
97
|
+
idempotent: phase.idempotent,
|
|
98
|
+
hasSideEffects: phase.hasSideEffects,
|
|
99
|
+
externalRefs: priorRefs,
|
|
100
|
+
readbacks,
|
|
101
|
+
forceReplay: forceReplay === true,
|
|
102
|
+
});
|
|
103
|
+
if (decision.decision === 'skip-already-applied') {
|
|
104
|
+
return handleSkipAlreadyApplied({
|
|
105
|
+
decision,
|
|
106
|
+
phase,
|
|
107
|
+
phaseIdx,
|
|
108
|
+
priorEvents: prior.events,
|
|
109
|
+
priorAttemptCount,
|
|
110
|
+
priorRefs,
|
|
111
|
+
runDir,
|
|
112
|
+
runId,
|
|
113
|
+
writerId,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (decision.decision === 'needs-human') {
|
|
117
|
+
appendEvent(runDir, {
|
|
118
|
+
event: 'phase.needs-human',
|
|
119
|
+
phase: phase.name,
|
|
120
|
+
phaseIdx,
|
|
121
|
+
reason: decision.reason,
|
|
122
|
+
nextActions: [
|
|
123
|
+
`Inspect prior externalRefs for phase ${phase.name}.`,
|
|
124
|
+
`Re-run with --force-replay if you accept the risk of duplicate side effects.`,
|
|
125
|
+
],
|
|
126
|
+
}, { writerId, runId });
|
|
127
|
+
throw new GuardrailError(`phase ${phase.name} previously succeeded; ${decision.reason}`, {
|
|
128
|
+
code: 'superseded',
|
|
129
|
+
provider: 'run-state',
|
|
130
|
+
details: {
|
|
131
|
+
runDir,
|
|
132
|
+
phaseIdx,
|
|
133
|
+
priorRefs,
|
|
134
|
+
readbacks: decision.readbacksConsulted,
|
|
135
|
+
reason: 'side-effecting-replay-needs-human',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (decision.decision === 'abort') {
|
|
140
|
+
throw new GuardrailError(`phase ${phase.name} aborted by replay decision: ${decision.reason}`, {
|
|
141
|
+
code: 'user_input',
|
|
142
|
+
provider: 'run-state',
|
|
143
|
+
details: { runDir, phaseIdx, priorRefs, reason: 'replay-decision-abort' },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// decision.decision === 'retry' — continue. If forceReplay drove this,
|
|
147
|
+
// record an explicit replay.override event so the durable log shows the
|
|
148
|
+
// override happened (per spec).
|
|
149
|
+
if (forceReplay === true) {
|
|
150
|
+
appendEvent(runDir, {
|
|
151
|
+
event: 'replay.override',
|
|
152
|
+
phase: phase.name,
|
|
153
|
+
phaseIdx,
|
|
154
|
+
reason: decision.reason,
|
|
155
|
+
refsConsulted: priorRefs,
|
|
156
|
+
}, { writerId, runId });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// -- Budget preflight (Phase 4) ----------------------------------------
|
|
160
|
+
// Runs AFTER idempotency gating (we don't gate replays we're already
|
|
161
|
+
// going to skip) and BEFORE phase.start (a rejection means the phase
|
|
162
|
+
// never started — no phase.start, no phase.failed; the runner throws
|
|
163
|
+
// GuardrailError budget_exceeded so the caller sees a typed failure
|
|
164
|
+
// and the run can be marked aborted/paused at the orchestrator level).
|
|
165
|
+
if (budget) {
|
|
166
|
+
const actualSoFarUSD = sumRunCost(prior.events);
|
|
167
|
+
const estimate = phase.estimateCost ? phase.estimateCost(input) : null;
|
|
168
|
+
const check = checkPhaseBudget({
|
|
169
|
+
budget,
|
|
170
|
+
phaseName: phase.name,
|
|
171
|
+
phaseIdx,
|
|
172
|
+
estimatedCost: estimate,
|
|
173
|
+
actualSoFarUSD,
|
|
174
|
+
nonInteractive: nonInteractive === true,
|
|
175
|
+
});
|
|
176
|
+
appendEvent(runDir, {
|
|
177
|
+
event: 'budget.check',
|
|
178
|
+
phase: phase.name,
|
|
179
|
+
phaseIdx,
|
|
180
|
+
decision: check.decision,
|
|
181
|
+
estimatedHigh: check.estimatedHigh,
|
|
182
|
+
actualSoFar: check.actualSoFar,
|
|
183
|
+
reserveApplied: check.reserveApplied,
|
|
184
|
+
capRemaining: check.capRemaining,
|
|
185
|
+
reason: check.reason,
|
|
186
|
+
scope: check.scope,
|
|
187
|
+
}, { writerId, runId });
|
|
188
|
+
if (check.decision === 'hard-fail') {
|
|
189
|
+
throw new GuardrailError(`phase ${phase.name} blocked by budget: ${check.reason}`, {
|
|
190
|
+
code: 'budget_exceeded',
|
|
191
|
+
provider: 'run-state',
|
|
192
|
+
details: {
|
|
193
|
+
runDir,
|
|
194
|
+
phaseIdx,
|
|
195
|
+
check,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (check.decision === 'pause') {
|
|
200
|
+
const confirm = confirmBudgetPause ?? defaultConfirmBudgetPause;
|
|
201
|
+
const proceed = await confirm(check);
|
|
202
|
+
if (!proceed) {
|
|
203
|
+
throw new GuardrailError(`phase ${phase.name} blocked by budget (user denied resume): ${check.reason}`, {
|
|
204
|
+
code: 'budget_exceeded',
|
|
205
|
+
provider: 'run-state',
|
|
206
|
+
details: {
|
|
207
|
+
runDir,
|
|
208
|
+
phaseIdx,
|
|
209
|
+
check,
|
|
210
|
+
userDenied: true,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// -- Phase start --------------------------------------------------------
|
|
217
|
+
const attempt = priorAttemptCount + 1;
|
|
218
|
+
const startedAtMs = Date.now();
|
|
219
|
+
appendEvent(runDir, {
|
|
220
|
+
event: 'phase.start',
|
|
221
|
+
phase: phase.name,
|
|
222
|
+
phaseIdx,
|
|
223
|
+
idempotent: phase.idempotent,
|
|
224
|
+
hasSideEffects: phase.hasSideEffects,
|
|
225
|
+
attempt,
|
|
226
|
+
}, { writerId, runId });
|
|
227
|
+
// Build the per-phase context. `subPhase` is wired below.
|
|
228
|
+
const ctx = buildPhaseContext({
|
|
229
|
+
runDir,
|
|
230
|
+
runId,
|
|
231
|
+
phaseName: phase.name,
|
|
232
|
+
phaseIdx,
|
|
233
|
+
writerId,
|
|
234
|
+
subPhase: makeSubPhaseFactory({ runDir, runId, writerId, parentPhaseIdx: phaseIdx }),
|
|
235
|
+
});
|
|
236
|
+
// -- Execute ------------------------------------------------------------
|
|
237
|
+
let output;
|
|
238
|
+
try {
|
|
239
|
+
output = await phase.run(input, ctx);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const durationMs = Date.now() - startedAtMs;
|
|
243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
244
|
+
const errorCode = err instanceof GuardrailError ? err.code : undefined;
|
|
245
|
+
appendEvent(runDir, {
|
|
246
|
+
event: 'phase.failed',
|
|
247
|
+
phase: phase.name,
|
|
248
|
+
phaseIdx,
|
|
249
|
+
durationMs,
|
|
250
|
+
error: message,
|
|
251
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
252
|
+
}, { writerId, runId });
|
|
253
|
+
// Re-read events to capture costs / refs the phase emitted before throw.
|
|
254
|
+
const after = readEvents(runDir);
|
|
255
|
+
const failedSnapshot = {
|
|
256
|
+
schema_version: RUN_STATE_SCHEMA_VERSION,
|
|
257
|
+
name: phase.name,
|
|
258
|
+
index: phaseIdx,
|
|
259
|
+
status: 'failed',
|
|
260
|
+
idempotent: phase.idempotent,
|
|
261
|
+
hasSideEffects: phase.hasSideEffects,
|
|
262
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
263
|
+
endedAt: new Date().toISOString(),
|
|
264
|
+
durationMs,
|
|
265
|
+
costUSD: sumPhaseCost(after.events, phaseIdx),
|
|
266
|
+
attempts: attempt,
|
|
267
|
+
lastError: message,
|
|
268
|
+
artifacts: [],
|
|
269
|
+
externalRefs: collectExternalRefs(after.events, phaseIdx),
|
|
270
|
+
};
|
|
271
|
+
writePhaseSnapshot(runDir, failedSnapshot);
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
274
|
+
// -- Success ------------------------------------------------------------
|
|
275
|
+
const durationMs = Date.now() - startedAtMs;
|
|
276
|
+
appendEvent(runDir, {
|
|
277
|
+
event: 'phase.success',
|
|
278
|
+
phase: phase.name,
|
|
279
|
+
phaseIdx,
|
|
280
|
+
durationMs,
|
|
281
|
+
artifacts: [],
|
|
282
|
+
}, { writerId, runId });
|
|
283
|
+
// Re-read to capture costs / refs the phase emitted during run().
|
|
284
|
+
const after = readEvents(runDir);
|
|
285
|
+
// Phase 6 — persist the phase output so a future skip-already-applied
|
|
286
|
+
// can return it without re-execution. Only persist values that JSON
|
|
287
|
+
// round-trip cleanly; if the phase returned something non-serializable
|
|
288
|
+
// (a function, a class instance with circular refs, a Buffer, …) we
|
|
289
|
+
// store undefined and rely on the phase being idempotent enough that a
|
|
290
|
+
// future caller doesn't actually need the prior value.
|
|
291
|
+
const persistedResult = jsonRoundTrip(output);
|
|
292
|
+
const successSnapshot = {
|
|
293
|
+
schema_version: RUN_STATE_SCHEMA_VERSION,
|
|
294
|
+
name: phase.name,
|
|
295
|
+
index: phaseIdx,
|
|
296
|
+
status: 'succeeded',
|
|
297
|
+
idempotent: phase.idempotent,
|
|
298
|
+
hasSideEffects: phase.hasSideEffects,
|
|
299
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
300
|
+
endedAt: new Date().toISOString(),
|
|
301
|
+
durationMs,
|
|
302
|
+
costUSD: sumPhaseCost(after.events, phaseIdx),
|
|
303
|
+
attempts: attempt,
|
|
304
|
+
artifacts: [],
|
|
305
|
+
externalRefs: collectExternalRefs(after.events, phaseIdx),
|
|
306
|
+
...(persistedResult !== undefined ? { result: persistedResult } : {}),
|
|
307
|
+
};
|
|
308
|
+
writePhaseSnapshot(runDir, successSnapshot);
|
|
309
|
+
return output;
|
|
310
|
+
}
|
|
311
|
+
/** Phase 6 — handle a `skip-already-applied` decision. Surfaces the prior
|
|
312
|
+
* result from the persisted snapshot if available; otherwise records the
|
|
313
|
+
* skip and rewrites the snapshot with `meta.skipped=true` then throws a
|
|
314
|
+
* typed `superseded` so the caller can react (matches the Phase 2
|
|
315
|
+
* contract for idempotent short-circuits). */
|
|
316
|
+
function handleSkipAlreadyApplied(opts) {
|
|
317
|
+
const { decision, phase, phaseIdx, priorEvents, priorAttemptCount, priorRefs, runDir, runId, writerId, } = opts;
|
|
318
|
+
appendEvent(runDir, {
|
|
319
|
+
event: 'run.warning',
|
|
320
|
+
message: `phase ${phase.name} short-circuited: ${decision.reason}`,
|
|
321
|
+
details: {
|
|
322
|
+
phase: phase.name,
|
|
323
|
+
phaseIdx,
|
|
324
|
+
reason: 'skip-already-applied',
|
|
325
|
+
decision: decision.decision,
|
|
326
|
+
readbacks: decision.readbacksConsulted,
|
|
327
|
+
},
|
|
328
|
+
}, { writerId, runId });
|
|
329
|
+
const priorSnapshot = readPhaseSnapshot(runDir, phase.name);
|
|
330
|
+
const persistedResult = priorSnapshot?.result;
|
|
331
|
+
const refreshed = {
|
|
332
|
+
schema_version: RUN_STATE_SCHEMA_VERSION,
|
|
333
|
+
name: phase.name,
|
|
334
|
+
index: phaseIdx,
|
|
335
|
+
status: 'succeeded',
|
|
336
|
+
idempotent: phase.idempotent,
|
|
337
|
+
hasSideEffects: phase.hasSideEffects,
|
|
338
|
+
costUSD: sumPhaseCost(priorEvents, phaseIdx),
|
|
339
|
+
attempts: priorAttemptCount, // unchanged — we did NOT start
|
|
340
|
+
artifacts: priorSnapshot?.artifacts ?? [],
|
|
341
|
+
externalRefs: priorRefs.length > 0 ? priorRefs : (priorSnapshot?.externalRefs ?? []),
|
|
342
|
+
meta: { skipped: true, reason: 'skip-already-applied', decisionReason: decision.reason },
|
|
343
|
+
...(persistedResult !== undefined ? { result: persistedResult } : {}),
|
|
344
|
+
};
|
|
345
|
+
writePhaseSnapshot(runDir, refreshed);
|
|
346
|
+
// If we have a prior result, return it. Otherwise throw `superseded` so
|
|
347
|
+
// the caller knows to consult the snapshot / onResume hook (matches the
|
|
348
|
+
// Phase 2 contract for idempotent short-circuits without a stored value).
|
|
349
|
+
if (persistedResult !== undefined) {
|
|
350
|
+
return persistedResult;
|
|
351
|
+
}
|
|
352
|
+
throw new GuardrailError(`phase ${phase.name} was already completed (skip-already-applied) but ` +
|
|
353
|
+
`no prior result is persisted — the caller should consult phases/${phase.name}.json or onResume.`, {
|
|
354
|
+
code: 'superseded',
|
|
355
|
+
provider: 'run-state',
|
|
356
|
+
details: {
|
|
357
|
+
runDir,
|
|
358
|
+
phaseIdx,
|
|
359
|
+
priorRefs,
|
|
360
|
+
readbacks: decision.readbacksConsulted,
|
|
361
|
+
decision: 'skip-already-applied',
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/** JSON round-trip a value to detect serializability. Returns the round-
|
|
366
|
+
* tripped value on success, undefined on any failure (circular refs,
|
|
367
|
+
* bigint, function, undefined, etc.). Persisting only round-trippable
|
|
368
|
+
* values keeps the snapshot file deterministic and prevents subtle
|
|
369
|
+
* type-drift between the in-memory value and what gets restored. */
|
|
370
|
+
function jsonRoundTrip(value) {
|
|
371
|
+
if (value === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
try {
|
|
374
|
+
const serialized = JSON.stringify(value);
|
|
375
|
+
if (serialized === undefined)
|
|
376
|
+
return undefined;
|
|
377
|
+
return JSON.parse(serialized);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Build a `subPhase` callable bound to a parent phase. Sub-phases use a
|
|
384
|
+
* synthetic phaseIdx derived from the parent's index plus a monotonic
|
|
385
|
+
* counter so the durable log distinguishes "outer phase 1, child 0" from
|
|
386
|
+
* "outer phase 1, child 1".
|
|
387
|
+
*
|
|
388
|
+
* Encoding: subPhase index = (parentPhaseIdx + 1) * 1000 + childOrdinal.
|
|
389
|
+
* The +1 offset is critical: without it, parent index 0 (the FIRST phase
|
|
390
|
+
* of any pipeline, since createRun is 0-based) would yield child indices
|
|
391
|
+
* 1, 2, 3… which collide with the regular top-level phases at those
|
|
392
|
+
* exact indices — a sub-phase's idempotency / side-effect events would
|
|
393
|
+
* then incorrectly gate the real top-level phase. Caught by Cursor
|
|
394
|
+
* Bugbot on PR #87 (HIGH). With the +1 offset:
|
|
395
|
+
* parent=0 → children 1001, 1002, 1003
|
|
396
|
+
* parent=1 → children 2001, 2002, 2003
|
|
397
|
+
* parent=N (N<999) → children (N+1)*1000+1..N
|
|
398
|
+
* Top-level pipelines have ~10 phases in practice, so the 1000 multiplier
|
|
399
|
+
* + the +1 offset keep collisions impossible at any realistic depth.
|
|
400
|
+
* Phase 6 may revisit this if nested sub-phases ever need a real tree
|
|
401
|
+
* representation. */
|
|
402
|
+
function makeSubPhaseFactory(opts) {
|
|
403
|
+
let childOrdinal = 0;
|
|
404
|
+
return async function subPhase(child, input) {
|
|
405
|
+
const childIdx = (opts.parentPhaseIdx + 1) * 1000 + (childOrdinal += 1);
|
|
406
|
+
return runPhase(child, input, {
|
|
407
|
+
runDir: opts.runDir,
|
|
408
|
+
runId: opts.runId,
|
|
409
|
+
writerId: opts.writerId,
|
|
410
|
+
phaseIdx: childIdx,
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
// ----------------------------------------------------------------------------
|
|
415
|
+
// Phase 4 — budget helpers
|
|
416
|
+
// ----------------------------------------------------------------------------
|
|
417
|
+
/** Sum every `phase.cost` event across the WHOLE run (not just the current
|
|
418
|
+
* phaseIdx). The budget cap is run-wide; sub-phase costs and prior-phase
|
|
419
|
+
* costs both count against `perRunUSD`. */
|
|
420
|
+
function sumRunCost(events) {
|
|
421
|
+
let total = 0;
|
|
422
|
+
for (const ev of events) {
|
|
423
|
+
if (ev.event === 'phase.cost')
|
|
424
|
+
total += ev.costUSD;
|
|
425
|
+
}
|
|
426
|
+
return total;
|
|
427
|
+
}
|
|
428
|
+
/** Default interactive confirm prompt used when no `confirmBudgetPause`
|
|
429
|
+
* override is supplied. Uses node:readline so the runner doesn't pull in
|
|
430
|
+
* a dependency just for prompting. */
|
|
431
|
+
async function defaultConfirmBudgetPause(check) {
|
|
432
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
433
|
+
try {
|
|
434
|
+
const message = `Budget warning: ${check.reason}\n` +
|
|
435
|
+
` phase: ${check.phase} (idx ${check.phaseIdx})\n` +
|
|
436
|
+
` actualSoFar: $${check.actualSoFar.toFixed(2)}\n` +
|
|
437
|
+
` reserveApplied: $${check.reserveApplied.toFixed(2)}\n` +
|
|
438
|
+
` capRemaining: $${check.capRemaining.toFixed(2)}\n` +
|
|
439
|
+
`Continue and accept the overage? [y/N] `;
|
|
440
|
+
const answer = await new Promise(resolve => rl.question(message, resolve));
|
|
441
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
rl.close();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
//# sourceMappingURL=phase-runner.js.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ExternalRef, ExternalRefKind } from './types.ts';
|
|
2
|
+
/** Canonical platform-state vocabulary. Every readback maps its provider's
|
|
3
|
+
* raw state into one of these so the decision matrix stays provider-agnostic.
|
|
4
|
+
* `unknown` is the fail-closed sentinel — any time the readback can't make a
|
|
5
|
+
* confident assertion it returns `unknown` and the caller treats that as
|
|
6
|
+
* needs-human. */
|
|
7
|
+
export type ReadbackState = 'open' | 'closed' | 'merged' | 'live' | 'failed' | 'rolled-back' | 'unknown';
|
|
8
|
+
/** What a readback returns when asked to verify a single external ref. */
|
|
9
|
+
export interface ReadbackResult {
|
|
10
|
+
refKind: ExternalRefKind;
|
|
11
|
+
refId: string;
|
|
12
|
+
/** Whether the platform reports the ref still exists. False on 404,
|
|
13
|
+
* hard error, missing ID, or any throw. */
|
|
14
|
+
existsOnPlatform: boolean;
|
|
15
|
+
currentState: ReadbackState;
|
|
16
|
+
/** Free-form provider-specific metadata. Engine doesn't introspect.
|
|
17
|
+
* Surfaces in the replay decision's `details` for human triage. */
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** A readback verifies one ref kind against its source-of-truth platform.
|
|
21
|
+
* Implementations MUST NOT throw — any failure (network, auth, unknown
|
|
22
|
+
* shape) collapses to `existsOnPlatform: false, currentState: 'unknown'`.
|
|
23
|
+
* This is the fail-closed contract: an unknown-state readback always
|
|
24
|
+
* routes to needs-human, never to a silent skip-already-applied. */
|
|
25
|
+
export interface ProviderReadback {
|
|
26
|
+
/** Stable identifier — useful in logs / decision details. */
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** Which ref kinds this readback handles. The registry filters first by
|
|
29
|
+
* kind; if multiple entries match a kind, `providers` then disambiguates
|
|
30
|
+
* on `ref.provider`. */
|
|
31
|
+
readonly handles: ReadonlyArray<ExternalRefKind>;
|
|
32
|
+
/** Optional provider-name allowlist. When present, the registry only
|
|
33
|
+
* routes a ref to this readback if `ref.provider` is in this list. Lets
|
|
34
|
+
* multiple readbacks share a kind (e.g. vercel/fly/render all handle
|
|
35
|
+
* `deploy`) without shadowing each other. Omit for kind-exclusive
|
|
36
|
+
* readbacks (e.g. github handles `github-pr`). */
|
|
37
|
+
readonly providers?: ReadonlyArray<string>;
|
|
38
|
+
verifyRef(ref: ExternalRef): Promise<ReadbackResult>;
|
|
39
|
+
}
|
|
40
|
+
/** Test seam — replace the gh CLI invocation in tests without monkey-patching
|
|
41
|
+
* child_process. Returns null on any failure (matches runSafe semantics). */
|
|
42
|
+
export interface GhRunner {
|
|
43
|
+
(args: string[]): string | null;
|
|
44
|
+
}
|
|
45
|
+
export declare function makeGithubReadback(opts?: {
|
|
46
|
+
gh?: GhRunner;
|
|
47
|
+
}): ProviderReadback;
|
|
48
|
+
/** Minimal adapter-status surface. Mirrors `DeployAdapter.status()` from
|
|
49
|
+
* `src/adapters/deploy/types.ts` but typed locally so this module doesn't
|
|
50
|
+
* pull the adapter package at init time. */
|
|
51
|
+
export interface DeployStatusFetcher {
|
|
52
|
+
status(input: {
|
|
53
|
+
deployId: string;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
status: 'pass' | 'fail' | 'in-progress' | 'fail_rolled_back' | 'fail_rollback_failed';
|
|
56
|
+
deployId: string;
|
|
57
|
+
deployUrl?: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
export type DeployAdapterResolver = (provider: string) => DeployStatusFetcher | null;
|
|
61
|
+
/** Register a resolver that maps a provider name (e.g. "vercel") to a
|
|
62
|
+
* status-fetcher. The CLI wires this from `src/adapters/deploy/index.ts`
|
|
63
|
+
* during boot; tests inject mocks directly. */
|
|
64
|
+
export declare function registerDeployAdapterResolver(resolver: DeployAdapterResolver | null): void;
|
|
65
|
+
/** Reset the registered resolver. Test-only seam. */
|
|
66
|
+
export declare function __resetDeployAdapterResolver(): void;
|
|
67
|
+
export declare function makeDeployReadback(name: string, providers: ReadonlyArray<string>): ProviderReadback;
|
|
68
|
+
/** Minimal migration-state fetcher. Implementations query the per-env
|
|
69
|
+
* Supabase project's `migration_state` table. We type it abstractly so this
|
|
70
|
+
* module doesn't pull the supabase client at init time. Returning null
|
|
71
|
+
* indicates "fetch failed" — fail-closed treats it as unknown. */
|
|
72
|
+
export interface MigrationStateFetcher {
|
|
73
|
+
/** Look up a migration version. Returns null on any error or not-found. */
|
|
74
|
+
fetch(version: string): Promise<{
|
|
75
|
+
applied: boolean;
|
|
76
|
+
appliedAt?: string;
|
|
77
|
+
} | null>;
|
|
78
|
+
}
|
|
79
|
+
/** Register the migration-state fetcher used by the supabase readback.
|
|
80
|
+
* CLI boot wires this; tests inject directly. */
|
|
81
|
+
export declare function registerMigrationStateFetcher(fetcher: MigrationStateFetcher | null): void;
|
|
82
|
+
export declare function __resetMigrationStateFetcher(): void;
|
|
83
|
+
/** State of a single planned migration as reported by the dispatcher's
|
|
84
|
+
* ledger. The fetcher returns the per-batch plan + the live ledger view so
|
|
85
|
+
* the readback can compute the aggregate state without re-querying. */
|
|
86
|
+
export interface MigrationBatchPlannedItem {
|
|
87
|
+
/** Migration version (matches the post-effect `migration-version` ref id
|
|
88
|
+
* shape — `<env>:<migration>` is the externalRef id, but the planned
|
|
89
|
+
* list carries just the migration name). */
|
|
90
|
+
version: string;
|
|
91
|
+
/** Live ledger state. `applied` ⇒ merged, `pending` ⇒ open, `errored` ⇒
|
|
92
|
+
* failed. */
|
|
93
|
+
state: 'applied' | 'pending' | 'errored';
|
|
94
|
+
}
|
|
95
|
+
/** Minimal `migration-batch` fetcher. Looks up the planned set for a batch
|
|
96
|
+
* ref id (typically `${env}:${hash}` or `${env}:pre-dispatch:${ts}` per the
|
|
97
|
+
* v6.2.1 spec) and returns the live ledger state of each. Returning null
|
|
98
|
+
* indicates "no plan recorded for this batch" — the readback treats that
|
|
99
|
+
* as unknown (fail closed). */
|
|
100
|
+
export interface MigrationBatchFetcher {
|
|
101
|
+
fetch(batchId: string): Promise<{
|
|
102
|
+
planned: MigrationBatchPlannedItem[];
|
|
103
|
+
} | null>;
|
|
104
|
+
}
|
|
105
|
+
/** Register the `migration-batch` fetcher. The CLI boot wires this from the
|
|
106
|
+
* per-skill adapter; tests inject mocks directly. */
|
|
107
|
+
export declare function registerMigrationBatchFetcher(fetcher: MigrationBatchFetcher | null): void;
|
|
108
|
+
export declare function __resetMigrationBatchFetcher(): void;
|
|
109
|
+
export declare function makeSupabaseReadback(): ProviderReadback;
|
|
110
|
+
/** Live registry — exposed as a getter so tests / callers can introspect. */
|
|
111
|
+
export declare function getProviderReadbacks(): ReadonlyArray<ProviderReadback>;
|
|
112
|
+
/** Replace the registry (test seam). Pass null to reset to defaults. */
|
|
113
|
+
export declare function setProviderReadbacks(list: ProviderReadback[] | null): void;
|
|
114
|
+
/** Look up the readback that handles a given ref. Two-pass match: first try
|
|
115
|
+
* a strict (kind + provider) match so multiple readbacks sharing a kind
|
|
116
|
+
* (vercel/fly/render all on `deploy`) don't shadow each other; then fall
|
|
117
|
+
* back to a kind-only match for readbacks that don't declare a provider
|
|
118
|
+
* allowlist (e.g. the github readback handles `github-pr` regardless of
|
|
119
|
+
* ref.provider). Returns null if no registered readback claims this ref —
|
|
120
|
+
* caller treats null as "no readback available, route to needs-human".
|
|
121
|
+
*
|
|
122
|
+
* Bugbot MEDIUM (PR #91): without provider-aware matching, the first deploy
|
|
123
|
+
* readback registered (vercel) won every `deploy`/`rollback-target` lookup
|
|
124
|
+
* and the fly/render readbacks were dead code. */
|
|
125
|
+
export declare function readbackForRef(ref: ExternalRef): ProviderReadback | null;
|
|
126
|
+
/** Verify a list of refs in parallel. Returns one ReadbackResult per ref.
|
|
127
|
+
* Refs without a registered readback get an unknown-state result so the
|
|
128
|
+
* decision matrix can attribute the gap. Order is preserved. */
|
|
129
|
+
export declare function verifyRefs(refs: ReadonlyArray<ExternalRef>): Promise<ReadbackResult[]>;
|
|
130
|
+
//# sourceMappingURL=provider-readback.d.ts.map
|