/** * The run→delivered-PR index (CREW-UX-8, crew#321). * * `LaunchRunBody.deliver?: 'pr'` is input-only; the deliver phase computes the PR URL * (`core/deliver.ts` — re-derived from the remote, refused without one) and prints it as the * unit's last output line, which is console text, not a wire. This index is the read-side * latency layer that lets the run DTOs echo `session.delivery` on BOTH `GET /runs` and * `GET /runs/:id` without a `workOutput` read per request — the persisted field that makes a * list surface's "4 of 5 siblings delivered" rollup affordable without an N-fetch fan-out * (wicked-studio#27). * * Mirrors `RetryIndex`/`GuidanceIndex`'s posture exactly: the DURABLE record is the audit * trail (the engine run record has no such field) — action `run.delivered`, `detail.url` — * hydrated once at server start (best-effort: a missing or unreadable trail leaves deliveries * blank, the pre-#321 behavior, not an error) and updated at the same post-terminal point * that writes the audit entry. * * KNOWN LIMIT (stated in the contract too): runs that delivered before this landed have no * `run.delivered` entry and carry no field. Do NOT backfill by scanning `work_output` at * hydrate — that is an unbounded boot cost. Historical runs still resolve their URL through * the per-run output endpoint. */ import type { DeliveryAssurance } from 'wicked-crew-api-types'; import type { AuditLog } from './audit.js'; import type { AgentSession, SessionView, WorkUnit, WorkflowDef } from '../core/types.js'; import { type PushedOnlyDelivery } from '../core/deliver.js'; /** What `AgentSession.delivery` + `deliverUrl` spell on the wire (api-types 0.18.0, crew#393). */ export interface DeliveryState { delivery: 'delivered' | 'pushed' | 'stranded' | 'vacuous' | 'none'; /** Present exactly when `delivery === 'delivered'`. */ deliverUrl?: string; /** Present exactly when `delivery === 'pushed'` (N1): the run branch that is on the remote. */ deliverBranch?: string; /** Present exactly when `delivery === 'pushed'`: the remote it was pushed to, userinfo removed. */ deliverRemote?: string; } /** The wire spelling of a recorded push-only delivery (N1). */ export declare function pushedState(p: PushedOnlyDelivery): DeliveryState; /** * Does this workflow DEFINITION do code work — the ONE predicate crew already applies at launch * (`routes.ts` `POST /runs`: `deliver` defaults to `'pr'` only for such a def): some phase declares * `executes_code` and is not the evaluator's. Hoisted here (DES-L8 §5 PR-8A; adjudicated §4.8) so * the launch-time default and the read-time classification below cannot drift — * `tests/delivery-classification.test.ts` pins this function equal to the inline rule for every * shipped def — by DRIVING `POST /runs` with no `deliver` and reading the `run.launched` audit * `detail.deliver` the closure's own rule produced (never a copy of the predicate). The inline call * in the `POST /runs` closure stays this train; its 3-line swap to this import is a follow-up in * that closure's owner lane. */ export declare function isCodeWorkDef(def: WorkflowDef): boolean; /** The phase id a planned unit carries — the `:` suffix (`qe/acceptance.ts`'s rule); * `''` for a free-text unit (`u1`, `u2`, …) or a legacy id with no colon. */ export declare function phaseIdOf(unitId: string): string; /** * Could THIS run ever have delivered (crew#481 / F-BM-006 list half; D-14 — a READ-TIME derivation, * nothing persisted)? TRUE when a `deliver` unit was planned (the composed per-run phase or an * operator overlay's own), when the def is unknown (`null` — a free-text `wf-…` run or a def the * registry no longer holds: today's candidacy, never narrowed on a guess), or when the def does * code work ({@link isCodeWorkDef}). FALSE only for a RESOLVED def with no deliver unit and no * code-work phase — `capture-learnings`, `steering-author`, … — whose * completed runs then read `delivery: 'none'` instead of the stranded/vacuous a live worktree used * to earn them (the board's "nine runs need you" after onboarding). A code-work run launched with * `deliver: 'none'` stays a candidate: its work is on `wicked/`, liftable post hoc. * * A PRESET or USER-PLAN run (seam X2) is judged by what it CONTAINS, never by a def looked up by * name: it could deliver when one of its planned units does code work (`executes_code`, not the * evaluator's — the same rule as {@link isCodeWorkDef}, over the units the engine planned). With * no planned units yet it stays a candidate, and so does a run whose PA is still scoping its plan * (`team_plan.scope`, wicked-core#633 X1): its only unit is the read-only `pa-scope` step, which * says nothing about what the plan will contain. `def` is not read for such a run. */ export declare function runCanDeliver(view: SessionView, def: WorkflowDef | null): boolean; /** * The ONE def-awareness closure the daemon hands its delivery cache and its campaigns rollup — * `runCanDeliver` over the def registered under the run's recorded NAME (`runWorkflowDef`, seam * X2: its preset or workflow, never a phase-sequence guess). Built for a READ path: a run with no * name (a user plan, free text, a fake adapter's minimal session, an older record), no `units`, or a * registry that throws resolves NO def ⇒ `runCanDeliver(view, null)` ⇒ * today's candidacy — never a 500 on `GET /runs` (the auth-required suite's `{ id, status }` * sessions are exactly this shape). Absence of an answer is today's label, not an error. */ export declare function canDeliverResolver(listWorkflows: () => WorkflowDef[], log?: (msg: string) => void): (view: SessionView) => boolean; /** * The tri-state delivery derivation (crew#393) — HONEST by construction, computed at DTO * assembly from three facts and nothing else: * * 1. a recorded PR URL (the `run.delivered` trail via {@link DeliveryIndex}) ⇒ `'delivered'`; * 1b. a recorded PUSH-ONLY delivery (N1: the branch is on a non-GitHub origin, no PR could be * opened — the same trail, `detail.pushed`) ⇒ `'pushed'`. Without it, every successful * delivery to a local/SSH/GitLab origin read `'stranded'` ("sitting uncommitted… No PR is on * record") and offered a post-hoc "open a PR" that cannot succeed; * 2. otherwise a COMPLETED repo-scoped run whose worktree still exists on disk ⇒ `'stranded'` * — reviewable work nobody lifted into a PR. This is a derivation over the run record's * existing fields (`status`, `repo_ref`, `workdir`) plus one stat, so runs recorded BEFORE * this field existed (the run 83052f0b class) read `'stranded'` exactly like new ones; * 3. everything else ⇒ `'none'`: repo-less runs, non-terminal runs, failed/cancelled runs * (their unit rejection already spells the failure), and completed runs whose worktree * was cleaned up (nothing left to lift). * * `worktreeExists` is injected (a `fs.existsSync` in production) so route tests can pin the * derivation without staging real directories. * * `canDeliver` (crew#481, D-14) is the def-awareness — {@link runCanDeliver}'s answer, resolved by * the caller (the cache dep / the rollup dep) so this fn stays a pure derivation: `false` answers * `'none'` for a completed run whose def could never have delivered, AFTER the url check (a recorded * PR always wins, whatever the def) and BEFORE the worktree stat (nothing there is liftable work). * Defaults to `true` = today's behaviour for every caller that has no def in hand. */ export declare function deliveryStateOf(session: Pick, url: string | undefined, worktreeExists: (path: string) => boolean, canDeliver?: boolean, pushed?: PushedOnlyDelivery): DeliveryState; /** The probes behind the `'vacuous'` derivation (crew#311) — injected so route tests can pin the * derivation without staging real repos; production wires the memoized git probes below. * * FAILURE CONTRACT (PR #435 review): a production git probe THROWS * {@link VacuityProbeUnavailable} when git could not answer — spawn failure, timeout, a * worktree or repo racing teardown, a non-git directory. That is the ABSENCE of an answer, not * a verdict, and the caller owns the degrade: the delivery cache retries WITHOUT recording * (never caching a failed probe as an honest label), and the campaigns rollup serves that one * request the stat-only tri-state. Injected test probes return plain booleans. */ export interface VacuityProbes { /** One stat — the `'stranded'` gate (crew#393; `fs.existsSync` in production). */ worktreeExists: (path: string) => boolean; /** True ⇔ the worktree POSITIVELY carries no contribution (both git instruments empty). */ worktreeIsClean: (path: string) => Promise; /** True ⇔ the run's `wicked/` branch in the repo `repoRef` names POSITIVELY carries no * commits of its own (or no longer exists) — the reaped-worktree half of vacuity. */ runBranchIsEmpty: (repoRef: string, runId: string) => Promise; } /** * Thrown by the production probes when git COULD NOT ANSWER — infrastructure absence, not a * verdict. The old behavior folded this into `false` ("not clean" / "not empty"), which was * safe while every consumer re-probed within a TTL, but the delivery cache records derivations: * caching a raced probe's `false` pinned a WRONG 'stranded' on a vacuous run (forever, where no * sweep ticks). Distinguishing absence from verdict at the probe layer is what lets the cache * refuse to record it. */ export declare class VacuityProbeUnavailable extends Error { constructor(what: string, cause: unknown); } /** * The delivery derivation WITH the vacuity split (crew#311) — the async refinement over * {@link deliveryStateOf} the run DTOs actually use. * * The crew#311 class — units all reach `done` while producing NOTHING (three governed builds * folded "done" on 280–509 bytes of read-narration over a worktree clean at main's HEAD) — used * to be indistinguishable on the wire from real outcomes, in BOTH of its shapes: * * - worktree still on disk and clean ⇒ it read `'stranded'`, pointing the operator at a * post-hoc deliver that must refuse (there is nothing to lift); * - worktree already REAPED ⇒ it read `'none'`, silently green. This is the shape a vacuous * completion normally lands in: the engine's terminal reap (FINDING-003, * `reap_worktree_if_clean`) removes exactly the clean trees, and never touches the * `wicked/` branch — so "reaped tree + empty run branch" is PROOF the run left no * work anywhere, while "reaped tree + commits on the branch" is landed work and stays * `'none'` (nothing left in a worktree to lift). * * Both shapes now read `'vacuous'`, and only on POSITIVE reads. A missing repo RECORD still * reads `false` from the branch probe (a permanent state — the label honestly falls back to * `'none'`), but a git failure now PROPAGATES as {@link VacuityProbeUnavailable} instead of * silently keeping the pre-existing label: the caller owns the degrade (see the failure * contract on {@link VacuityProbes}). (One honest edge: an operator who hand-deleted a * completed run's branch has made its work unreachable — it reads vacuous, which is what the * run now amounts to.) */ export declare function deliveryStateWithVacuity(session: Pick, url: string | undefined, probes: VacuityProbes, canDeliver?: boolean, pushed?: PushedOnlyDelivery): Promise; /** How long a per-worktree cleanliness read is trusted before the next request re-probes. A * terminal run's worktree only changes under operator hands, and `GET /runs` is polled — an * unmemoized probe would be two git subprocesses per stranded run per poll. */ export declare const WORKTREE_CLEAN_TTL_MS = 30000; /** How long a FAILED probe ({@link VacuityProbeUnavailable}) is memoized before a real * re-probe. Deliberately short: a failure is usually a raced teardown or a load spike, and * the delivery cache's retry needs a genuine re-probe in ~a second — but a request-path * consumer (the campaigns rollup) polling over a persistently broken repo must not spawn two * 10s-timeout gits per poll tick either. */ export declare const PROBE_FAILURE_TTL_MS = 1000; /** * The production `worktreeIsClean` probe: the SAME two read-only instruments as the engine's * `worktree_is_clean` — uncommitted paths (`git status --porcelain`, untracked included) and * run-branch-only commits (`git log --oneline HEAD --not --exclude=wicked/* --branches`) — * memoized per worktree path for {@link WORKTREE_CLEAN_TTL_MS}. * * A verdict comes only from git ANSWERING: exit 0 with empty/non-empty output. Any failure — * git unspawnable, a timeout kill, a 128 "not a git repository", ENOENT from a worktree racing * the terminal reap — throws {@link VacuityProbeUnavailable} (memoized for * {@link PROBE_FAILURE_TTL_MS}) so no consumer can mistake absence for "not clean" and pin a * wrong 'stranded' (PR #435 review). Callers degrade per the {@link VacuityProbes} contract. */ export declare function gitWorktreeIsClean(ttlMs?: number, now?: () => number): (path: string) => Promise; /** * The production `runBranchIsEmpty` probe — the reaped-worktree half of vacuity (crew#311). * * TRUE only on positive proof that the run left nothing on its branch: either * `refs/heads/wicked/` no longer exists in the run's repo, or it exists and * `git log --not --exclude=wicked/* --branches` is empty (the branch carries no commit * of its own — the exact second instrument of the engine's `worktree_is_clean`, aimed at the * branch instead of a checkout). A repo the registry no longer RESOLVES reads FALSE — a * permanent state, the label honestly falls back to `'none'` — while a git failure or a * registry read failure throws {@link VacuityProbeUnavailable} (absence of an answer, PR #435 * review), memoized for {@link PROBE_FAILURE_TTL_MS}; callers degrade per the * {@link VacuityProbes} contract. * * `resolveRepoRoot` is injected (routes wire it to the adapter's repo registry) and its answer * rides the same TTL memo as the verdict, so a `GET /runs` poll costs at most one registry read * + one git pair per run per window. */ export declare function gitRunBranchIsEmpty(resolveRepoRoot: (repoRef: string) => Promise, ttlMs?: number, now?: () => number): (repoRef: string, runId: string) => Promise; /** * The PR URL in a deliver transcript — crew's own extraction, mirrored * (`core/deliver.ts`: `grep -Eo 'https://[^[:space:]]+/pull/[0-9]+' | tail -1`). Requiring * the digits keeps `…/pull/new/` — the create-PR form git prints on every push — * from ever matching; the LAST match wins, same as `tail -1`. (crew#720) An Azure DevOps pull * request reads `…/_git//pullrequest/` (the script's ADO branch greps exactly that); * Azure Repos' own push hint, `…/pullrequestcreate?…`, carries no `/` and never matches. */ export declare function prUrlFrom(text: string): string | null; /** What an approved deliver unit's transcript puts on the `run.delivered` trail (`detail`). */ export type DeliveryRecord = { url: string; } | { pushed: PushedOnlyDelivery; }; /** * The delivery an APPROVED deliver unit's transcript records, or `null` (nothing to record). * * The script ends each success path with its own verdict on stdout: the PR URL (`echo "$URL"`) or, * on a non-GitHub origin (N1), the `deliver: PUSHED-NO-PR` line. Text before that verdict can be the * remote's — `git push` output is echoed, and a hook may print a `/pull/` URL or even a forged * marker line (Copilot on crew#734, both directions). So whichever of the two appears LAST is the * script's verdict. */ export declare function deliveryRecordFrom(output: string): DeliveryRecord | null; /** * This run's deliver unit, or `null`. The composed id suffix (`:deliver`) is the * primary key; the `deliver` catalog entry and crew's deliver-script refusal marker (crew#720 S3) find an * operator OVERLAY that carried the deliver phase under its own name — do NOT key on `workflow_id`, which is plain for * overlay-carried deliver phases (crew#321). */ export declare function deliverUnitOf(view: SessionView): WorkUnit | null; /** What {@link DeliveryResolver} needs from the daemon — injected so the race is pinnable in a test. */ export interface DeliveryResolverDeps { /** The engine's run views (`adapter.sessionsDetail()`). */ listViews: () => Promise; /** The deliver unit's stored transcript (`adapter.workOutput(coreUnitId(runId, unit))`). */ workOutput: (unitId: string) => Promise; /** Already on record — the durable `run.delivered` trail / {@link DeliveryIndex}. */ isDelivered: (runId: string) => boolean; /** Write the record: the audit entry FIRST, then the index — the caller owns that order. */ record: (runId: string, record: DeliveryRecord) => void; log?: (msg: string) => void; } /** * The ONE post-terminal `workOutput` read that turns a run's deliver transcript into its * `run.delivered` record (crew#321), with the two properties the read path needs (crew#851): * * - **one read per run, shared.** Concurrent callers — the terminal-frame handler and a * `GET /runs(/:id)` that lands in the window — await the SAME promise, so the trail is never * written twice and the engine is never asked twice. A run whose read finished (a record, or * an approved deliver unit whose transcript carries none) is `settled` and never re-read; * a read that threw is not settled, so the next caller retries. * - **the window is detectable from the served view.** {@link DeliveryResolver.pending} is true * exactly while a terminal run with a `done` deliver unit has nothing on record and has not * been read: the record the DTO is built from already holds the delivery (the unit's stored * output), only the index lags. The routes await {@link DeliveryResolver.resolve} for such a * view before deriving `delivery`, so `status: completed` and `delivery` come from one * consistent read and the worktree-stat fallback (`'stranded'`) is unreachable for a run that * pushed and opened its PR (0.8.3 release smoke F-SMOKE-003, the macOS leg). * * Best-effort by construction: `resolve` never throws — a failed read is logged and the field * stays absent until the next caller (or a restart's trail replay) heals it. */ /** The most a run route waits for an in-window delivery read (see {@link DeliveryResolver.settle}). */ export declare const READ_PATH_SETTLE_MS = 2000; export declare class DeliveryResolver { private readonly deps; private readonly inflight; private readonly settled; constructor(deps: DeliveryResolverDeps); /** Is this served view inside the completion window (see the class doc)? */ pending(view: SessionView): boolean; /** Resolve `runId`'s delivery onto the record — memoized while in flight, once when settled. */ resolve(runId: string): Promise; /** * Resolve every served view still inside the window, waiting at most `waitMs` (crew#851, codex * r1 on #859): the request path must stay bounded — the old terminal-frame resolution never * touched it — so a slow or hung engine read for one run costs the list at most `waitMs` and * that run falls through to its stat-derived label for this poll; the read keeps running and * heals the next one. Resolves `true` when every pending read landed within the bound. */ settle(views: SessionView[], waitMs?: number): Promise; private read; } /** * Is this a `failed` run whose ONLY failure was a recoverable deliver LIFT failure (crew#418)? * * The deliver phase is the LAST phase, so a rejected deliver unit whose `denial_reason` carries * the deliver script's {@link DELIVER_LIFT_CONFLICT_MARKER} — with EVERY non-deliver unit still * `done` — means the run's WORK is complete and committed on its `wicked/` branch: only the * lift into origin could not complete (a rebase conflict the changelog union merge could not * clear, or the engine's own lift conflict). A push the REMOTE refused no longer carries the marker * (N4): it parks at the engine's deliver-refusal gate instead. The engine reports the run `failed` because a Tool phase exited * non-zero; crew reinterprets THIS shape on the wire as `completed` + `delivery: 'stranded'` * (recoverable via `POST /runs/:id/deliver`) — the same wire-derivation posture as `delivery` * itself, leaving the engine's durable `failed` record untouched. The deliver unit stays * `rejected` with its marker-bearing `denial_reason`, so WHY it stranded is still on the wire. * * FALSE — the run stays `failed` — for every genuine failure, because each misses a condition: * - a rejected NON-deliver unit (a build/test/work phase failed) — `some(rejected)` guard; * - a deliver rejection WITHOUT the marker: a spawn/infra fault (the crew#400 posture), a * `gh` failure, nothing-to-deliver, or a wrong-worktree-branch refusal — the marker is the * deliver script's own authority on WHY it refused, so crew never guesses from git's output; * - a repo-less run (nothing to strand), or any non-`failed` status. */ export declare function isDeliverConflictStranded(view: SessionView): boolean; /** crew#762: the audit action a post-hoc delivery ATTEMPT records before it runs — the durable "it was asked for". */ export declare const DELIVER_REQUESTED_ACTION = "run.deliver_requested"; export declare class DeliveryIndex { private readonly runToUrl; /** crew#762: runs a post-hoc delivery was attempted for (`run.deliver_requested`), whatever came of it. */ private readonly requested; /** N1: runs whose deliver phase pushed the branch and could open no PR (a non-GitHub origin). */ private readonly runToPushed; /** (wicked-core#850 EX-04) How a post-hoc delivery was assured — `verified: false` and its trees. */ private readonly runToAssurance; /** * Load deliveries from EVERY `run.delivered` entry in the trail — exhaustively, not capped * (BRIEF-UX-002 C5, the same defect class as `RetryIndex.hydrate`: a durable record must * not vanish because 1000+ newer writes landed on top of it). The trail answers newest * first, so the FIRST entry seen per run wins; older entries for the same run are * superseded and skipped. Still best-effort, like its siblings; cost is one full-file scan * at boot. (Third exhaustive trail scan at boot, after RetryIndex and GuidanceIndex — a * FOURTH should trigger consolidating them into one pass, per crew#321.) */ hydrate(audit: AuditLog, log?: (msg: string) => void): Promise; /** Record the run's delivered PR URL (idempotent — the newest write wins) and, for a post-hoc * delivery, how it was assured (EX-04). */ set(runId: string, url: string, assurance?: DeliveryAssurance): void; /** How the run's recorded delivery was assured (EX-04) — set for a post-hoc delivery only. */ assuranceFor(runId: string): DeliveryAssurance | undefined; /** Record a PUSH-ONLY delivery (N1). A recorded PR URL is never downgraded by it. */ setPushed(runId: string, pushed: PushedOnlyDelivery, assurance?: DeliveryAssurance): void; /** The recorded push-only delivery for this run, or `undefined` (N1). */ pushedFor(runId: string): PushedOnlyDelivery | undefined; /** crew#762: record that a post-hoc delivery was attempted for this run (the audit entry is the caller's). */ markRequested(runId: string): void; /** crew#762: was a post-hoc delivery attempted for this run (recorded, whatever its outcome)? */ wasRequested(runId: string): boolean; /** Did this run deliver at all — a PR on record OR a pushed branch on record? */ isDelivered(runId: string): boolean; /** * The recorded PR URL for this run, or `undefined` — the fact {@link deliveryStateOf} turns * into the wire's `delivery: 'delivered'` + `deliverUrl` (api-types 0.18.0; the 0.11.0 object * spelling `{ kind: 'pull_request', url }` is gone from the wire, crew#393). */ urlFor(runId: string): string | undefined; } /** * crew#755: what a launch with neither a workflow nor a plan ran, in the `POST /runs` 201 body. * The engine's free-text planner makes ONE unit, the brief verbatim (wicked-core D-11). */ export declare const FREE_TEXT_NOTICE: string; /** * crew#755: a COMPLETED free-text run with no repo and no write root of its own had nowhere to * change anything — no worktree, no declared root — so whatever it did, it did in TEXT: nothing was * changed, checked or delivered. `'text_only'` says exactly that, true by construction; it does not * claim the text was empty (a free-text answer can be useful), so `delivery` is left as it is. * `undefined` for every other run. Pure. */ export declare function freeTextOutcome(view: SessionView): 'text_only' | undefined; //# sourceMappingURL=delivery-index.d.ts.map