import type { GhClient } from './gh.js'; import type { GitOps } from './git.js'; import type { GateResult } from './gate.js'; import type { JournalEvent, MergeQueueConfig } from './types.js'; export type BootstrapStage = 'intent' | 'merged' | 'armed'; export interface BootstrapAttempt { bootstrapId: string; pr: number; gatedHeadSha: string; mergeCommitSha: string | null; stage: BootstrapStage; /** Timestamp of the id's latest journaled event. */ at: string; } /** Fold the journal into per-id bootstrap attempts (D9). Events for an id * arrive strictly intent → merged → armed; later stages win. An id with only * an intent is either in-flight or aborted — planResume decides which via * GitHub's authoritative PR state, never the journal alone. */ export declare function reduceBootstrapAttempts(events: JournalEvent[]): Map; /** Latest attempt for a PR — ULIDs sort lexicographically in creation order, * so the max bootstrapId is the newest attempt. */ export declare function latestAttemptFor(events: JournalEvent[], pr: number): BootstrapAttempt | null; export type ResumeDecision = { kind: 'fresh'; } | { kind: 'complete'; attempt: BootstrapAttempt; } | { kind: 'arm-and-verify'; attempt: BootstrapAttempt; } | { kind: 'record-merge-then-arm'; attempt: BootstrapAttempt; } | { kind: 'rerun-merge'; attempt: BootstrapAttempt; } | { kind: 'aborted'; attempt: BootstrapAttempt; reason: string; }; /** Reconcile the journaled attempt against GitHub's AUTHORITATIVE PR state * (D9): intent-without-merged while GitHub says MERGED is the crash window * between the merge API call and the journal write — record retroactively, * never re-merge. An aborted attempt is terminal for its id. */ export declare function planResume(attempt: BootstrapAttempt | null, gh: { state: 'OPEN' | 'MERGED' | 'CLOSED'; headSha: string; }): ResumeDecision; export interface BootstrapDeps { gh: GhClient; git: GitOps; runGate: (opts: { cwd: string; command: string; timeoutMs: number; logPath: string; env?: Record; pidFile?: string; }) => GateResult | Promise; /** Config loaded from PRIMARY (defaults on a first install) — used by resume * paths, where the PR has already merged its config to primary. */ config: MergeQueueConfig; mqDir: string; projectRoot: string; /** D9: read the merge-queue config the PR *installs*, from the GATED PR tree * (the checkoutDetachedInGate checkout). Omitted in pure-engine tests — the * engine then falls back to `config`. */ readMergeConfig?: (gatedTree: string) => MergeQueueConfig; /** D9: verify the PR's committed queue assets (config, guard, poller, hook * registration) exist in the GATED PR tree — they must ride the merge to * primary, not be uncommitted post-gate mutations. Omitted in pure-engine * tests — the engine then treats assets as present. */ verifyGatedAssets?: (gatedTree: string, cfg: MergeQueueConfig) => { ok: boolean; missing: string[]; }; /** D8 primitive (idempotent) — arm the Claude Code hooks. Called POST-merge, * immediately after syncPrimaryToMerge, same as armSched: arming pre-merge * would write an uncommitted change to primary's .claude/settings.json and * could make the post-merge ff-only sync abort if the PR itself commits that * file (the armHooks dirty-tree race). */ armHooks: () => { messages: string[]; }; /** D6 primitive — arm the scheduler. Called POST-merge (the poller script * lives at /scripts/ops/, present only once the PR lands), so it * self-decides on the now-merged executor and no-ops for non-local-poller. */ armSched: () => { ok: boolean; messages: string[]; }; /** Post-merge daemon smoke (`mq daemon --once`). */ smokeDaemon: () => { ok: boolean; detail: string; }; /** Closing doctor pass (advisory) — null when the doctor CLI is unavailable. */ runDoctor: (() => { exitCode: number; summary: string; }) | null; /** Does `command` resolve in `root`? Resolved against the GATED PR tree in * preflight (the PR's gate scripts are not yet at primary). */ gateTargetResolves: (root: string, command: string) => boolean; log: (msg: string) => void; now: () => Date; /** Bounded-backoff sleep between merge-SHA lookups. Injected so tests run * instantly; defaults to real setTimeout when omitted. */ sleep?: (ms: number) => Promise; /** ULID seam. */ newId: () => string; } export interface BootstrapOutcome { ok: boolean; bootstrapId: string | null; stage: 'preflight' | 'arm' | 'merge' | 'verify' | 'complete' | 'aborted'; messages: string[]; } /** D9: guided first merge. Order — preflight (verify the PR's committed queue * assets + gate targets + full gate, all against the GATED PR tree, since the * first queue-installing PR's assets are not yet at primary) → journaled * squash-merge with head revalidation → POST-merge sync primary to the merge * commit (ff-only) → arm hooks (deep-merges into the now-synced, * merge-committed .claude/settings.json — never pre-merge, which could dirty * primary and make the ff-only sync fail if the PR itself commits that file) → * arm scheduler (buildPostMergePollerJob(primary) now resolves) + daemon smoke * + doctor → bootstrap_armed. A crash anywhere resumes via planResume with * GitHub authoritative; a syncPrimaryToMerge failure degrades gracefully to a * recoverable --finish rather than throwing. */ export declare function runBootstrap(deps: BootstrapDeps, opts: { pr: number; finish?: boolean; }): Promise; //# sourceMappingURL=bootstrap.d.ts.map