/**
* 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