/** * The first-class deliver phase (crew#293) — a run opens its own PR, opt-in. * * Productizes the operator-side `feature-pr` overlay proven during the DES-MERGE-001 campaign: * a Tool phase appended after the workflow's last phase that COMMITS the run's work, pushes the * run's branch and opens a PR via `gh`. What was data on one machine becomes a launch option * (`deliver: "pr"` on `POST /runs`), composed PER RUN — the shared workflow def is never mutated. * * Field-proven hardening, replicated here: * (a) the branch is derived from the run worktree's basename (`wicked/`), falling back * to the current branch when that ref does not exist; * (b) the script REFUSES to push `main`/`master` (or an empty/detached branch name) — the * deliver phase only ever pushes run branches; * (b2) with the engine's `WICKED_DELIVER_VERIFIED_BASE` pin set (wicked-core#431 / #433: the * remote-tip commit the engine lifted the work onto and re-verified against), it REFUSES — * before staging anything — when origin's default branch no longer resolves to that commit * after its own fetch ({@link DELIVER_BASE_MOVED_MARKER}): a base that moved past the verified * one is re-verified by the engine's retry, never rebased past by this script; * (b3) the crew#426 preflight (lockfile re-sync + codegen) runs AFTER the engine's verification; * when it changes the worktree, an engine-driven delivery REFUSES and names the files * ({@link DELIVER_PREFLIGHT_CHANGED_MARKER}) — a post-hoc lift ({@link DELIVER_POSTHOC_ENV}) * keeps the regeneration and says so; * (c) it STAGES AND COMMITS the run's work, then rebases onto origin's default branch before * pushing. A conflict whose conflicted paths are ALL `CHANGELOG.md` is union-merged (both * sides' additive lines kept) and the rebase continues — the crew#418 collision magnet, made * to just deliver; ANY other conflict aborts the rebase (pushing nothing) and exits carrying * {@link DELIVER_LIFT_CONFLICT_MARKER}, which crew reads as a recoverable STRAND, never a * pushed conflicted tree; * (d) `git push -u origin ` — every rejected push carries the recovery marker. The run * work is already committed locally, so an operator can repair auth/transport and retry; * (e) `gh pr create --head --title … --body-file …`, with gh's output and exit status * captured SEPARATELY so a gh failure fails the phase carrying gh's own message. The title * and body are COMPOSED FROM THE RUN (crew#524 / F-3R2-014, `core/deliver-text.ts`): the * script asks the daemon that launched it (`GET /runs/:id/deliver-text`) for the text derived * from the persisted run record — intent, `Fixes #N`, run link, phases + seats + gate * outcomes, repo checks with exit codes, the evaluator verdict — and falls back to the same * composer's launch-time text (embedded in the script) when the daemon cannot answer. The * commit the phase makes for uncommitted work carries that same text (`git commit -F`), so * ITS subject is the PR title; a run that committed incrementally keeps its own commits; * (f) the PR URL is the last line of the phase output. * * One deliberate change from the field version: NO gh account is baked into crew code (the * overlay guarded a personal account). Instead the push identity is CONFIGURED (the deliver * identity setting, the repository's pin, or the `GH_ACCOUNT` env var) and chosen PER COMMAND * (crew#940): on a github.com origin the script reads that account's own token from gh's keyring * and hands it to its gh calls and its git fetch/push alone, so gh's machine-wide active account * never decides who pushes; an exported `GH_TOKEN` wins, and one that authenticates as another * login is refused. It is disclosed on the deliver gate card and never switched at push time. * Unset ⇒ whatever gh holds, said aloud. * * REVISION mode (DES-L9 / crew#550, `POST /runs {revisesPr}`): the run was based on an OPEN pull * request's head branch; the script pushes `wicked/` onto `refs/heads/` (the PR * gains exactly the run's commits — no rebase onto the default branch, no `gh pr create`), proves * the remote tip, comments the run record on the PR and prints the PR's URL last. A head that moved * or vanished since the run based on it is REFUSED before anything is staged (no LIFT-CONFLICT * marker — a re-push cannot succeed; the operator launches a new revision or rebases by hand). * * Merge stays human: the phase opens the PR, never merges it. * * ## crew#317 — "pushed an empty branch and reported success" * * Run `d1bc72c2` (wicked-studio) delivered nothing while reporting `completed`. The persisted * unit is the evidence, and it names the cause precisely. Its `tool_cmd` was NOT this script — * it was the operator's hand-written `feature-pr` OVERLAY def, which begins `set -e` with **no * `pipefail`**, so `gh pr create … | tail -1` reported `tail`'s status (0) and the phase passed * with gh's error text where the PR URL belongs: * * ```text * could not compute title or body defaults: could not find any commits between origin/main and * wicked/d1bc72c2-… * ``` * * So the masking mechanism the issue hypothesised is real, but it belonged to the overlay, not * here: `pipefail` IS in force for this executor (it is line 1 of this script, `bash -lc` runs it * verbatim, and core's `run_tool_cmd` maps any non-zero exit to `StepStatus::Failed`). The three * defects this script genuinely shared with the overlay are fixed below: * * 1. **No commit.** Agents write files and do not commit, so the pushed branch equalled the * default branch. The script now stages and commits the run's work itself, and REFUSES to * push when there is nothing to deliver. * 2. **A masked result.** `| tail -1` discarded everything gh said except one line and made the * phase's verdict depend on a pipe option. gh's output and status are now captured * separately, and success is re-derived from a real PR URL rather than from an exit code. * 3. **Ungoverned.** The phase shipped `verified_evidence: false` / `validator_pin: null`, so * nothing re-derived what it claimed. It now declares `verified_evidence: true` — see * {@link deliverPrPhase}. */ import type { PhaseDef, WorkflowDef } from './types.js'; import { type AdoRepo, type DeliverCredentials } from './deliver-credentials.js'; import { type DeliverTextFacts } from './deliver-text.js'; /** * The content-address of wicked-core's built-in evidence floor (`builtin_floors::EVIDENCE_FLOOR_PIN`, * criterion "the run left a change in its worktree (done is re-derived from the diff, never * asserted)"). Carried explicitly on every phase crew composes or mirrors that declares * `verified_evidence` or writes code: since wicked-core#414 the engine judges a def AS AUTHORED at * registration — it REFUSES an `executes_code` agent phase with no pin and no human gate, and a * `verified_evidence` phase with no pin — so nothing is armed on crew's behalf any more. * * Duplicating a hash is a real cost, paid because the alternative is worse: a `null` here would * be a refused registration for every deliver-composed run. The floor is seeded on core's plan * path (`pre_distribute`), so this pin always resolves without a provision/approve step. The drift * guards in `tests/armed-workflow-served.test.ts` and `tests/deliver-phase.test.ts` fail loudly on * a developer machine the moment core's value moves. */ export declare const EVIDENCE_FLOOR_PIN = "e2e7af1db9e48454"; /** The id of the appended phase — also the collision probe when a def already delivers. */ export declare const DELIVER_PHASE_ID = "deliver"; /** * The sentinel the deliver script prints when the RUN'S WORK has been committed but its LIFT did * not complete (crew#418): a rebase onto the remote default branch hit a conflict outside the * changelog. The work is safe on its `wicked/` branch; an operator resolves the collision and * retries delivery. A push the REMOTE refused is not a lift collision and carries * {@link DELIVER_PUSH_REJECTED_MARKER} instead (N4). * * crew keys the "stranded, recoverable" reinterpretation on this EXACT substring appearing in * the deliver unit's `denial_reason` (which carries the head+TAIL excerpt of the script's * output, and the marker is always the script's last line — see {@link isDeliverConflictStranded} * in `api/delivery-index.ts`). The script's OTHER loud refusals — wrong worktree branch, nothing * to deliver, `gh` failure — and a spawn/infra failure deliberately OMIT the marker, so they stay * terminal run failures exactly as before (the crew#400 refusal-vs-infra posture). */ export declare const DELIVER_LIFT_CONFLICT_MARKER = "deliver: LIFT-CONFLICT"; /** * The sentinel the deliver script prints LAST when the REMOTE REFUSED the push after the run's work * was committed and its lift came out clean (N4, ship re-proof): a pre-receive hook, a protected * ref, an auth 403, a transport failure, or a non-fast-forward on the run branch. It used to carry * {@link DELIVER_LIFT_CONFLICT_MARKER}, which mislabelled a refusal as a lift collision — the lift * read `unchanged` — and, because the engine EXEMPTS that marker from its deterministic * deliver-refusal arm, sent the failure to the LLM triage judge, which re-ran the push once with no * gate. Without the lift marker the failure takes the engine's deterministic arm: the run parks at * an `escalation` gate on the deliver unit, and nothing is pushed again until a human approves. * The untracked recovery sentinel ({@link DELIVER_STRANDED_SENTINEL}) still keeps the worktree. */ export declare const DELIVER_PUSH_REJECTED_MARKER = "deliver: PUSH-REJECTED"; /** * The machine line the deliver script prints LAST on a successful PUSH-ONLY delivery (N1, the half * of F2 that did not hold): the branch is on the remote, and no pull request was opened because gh * could not resolve the origin to a GitHub repository. `deliver: PUSHED-NO-PR ` — * the branch has no spaces (git forbids them in a ref), and the remote is the rest of the line. * The daemon reads it from the APPROVED deliver unit's output ({@link pushedOnlyFrom}) into the * durable `run.delivered` record, so the run reads `delivery: 'pushed'` instead of `'stranded'`. */ export declare const DELIVER_PUSHED_NO_PR_MARKER = "deliver: PUSHED-NO-PR"; /** * (crew#739) The TRUSTED terminal sentinel: every exit of the deliver script prints exactly one * `deliver: OUTCOME ` line, from its EXIT trap. The nonce is minted per * composition and written into the script text as `DELIVER_NONCE=` (so it rides the unit's * `tool_cmd`, which the engine and crew already hold); a remote never sees the script, only the * branch, so a pre-receive hook cannot echo a sentinel with the right nonce. The engine * (wicked-core#807, `deliver_lift.rs`) and crew (`trustedDeliverOutcome`) classify a nonce-bearing * deliver ONLY on that line; a missing sentinel reads `failed` (fail closed), never `stranded`. */ export declare const DELIVER_OUTCOME_MARKER = "deliver: OUTCOME"; /** The script variable carrying the nonce ({@link DELIVER_OUTCOME_MARKER}). */ export declare const DELIVER_NONCE_VAR = "DELIVER_NONCE"; /** The verdicts the sentinel carries. `stranded` is the post-hoc-liftable LIFT-CONFLICT class. */ export type DeliverOutcome = 'pr' | 'pushed' | 'rejected' | 'stranded' | 'failed'; /** A fresh 16-byte hex nonce for one script composition. */ export declare function mintDeliverNonce(): string; /** What a push-only delivery left on the record: the branch, and the remote it is on. */ export interface PushedOnlyDelivery { branch: string; /** The origin's push URL, with any `user[:password]@` userinfo removed. */ remote: string; } /** A remote URL with its userinfo removed — `https://tok@host/r` → `https://host/r`. A scp-like * `git@host:path` keeps its user (it names an SSH login, not a secret). */ export declare function remoteWithoutUserinfo(url: string): string; /** * The push-only delivery a deliver transcript records, or `null`. The LAST * {@link DELIVER_PUSHED_NO_PR_MARKER} line wins. Whether it outranks a PR URL in the same transcript * is decided by position in `deliveryRecordFrom` (`api/delivery-index.ts`): the script's verdict is * the last of the two, and anything earlier may be the remote's echo. */ export declare function pushedOnlyFrom(text: string): PushedOnlyDelivery | null; /** What the script carries for its PR/commit text (crew#524). All optional: the bare script still * composes a title and a body from the intent alone. */ export interface DeliverScriptOptions { /** The run this script delivers; names the run in the fallback text. */ runId?: string; /** Everything known when the script was composed — the embedded fallback is built from it. * Defaults to the intent + run id alone (no workflow, no phases). */ facts?: DeliverTextFacts; /** The launching daemon's own origin (`http://127.0.0.1:7701`). When set, the script first asks * it for the run-derived text; unset (or unreachable) ⇒ the embedded fallback. */ apiOrigin?: string | null; /** DES-L9 / crew#550 — REVISION mode: the open pull request this run revises. The script pushes * `wicked/` onto `refs/heads/` (the PR gains exactly the run's commits), proves the * remote tip, comments the run record on the PR and prints `url` last; no `gh pr create`. */ revisesPr?: RevisedPullRequest | null; /** The daemon's configured push identity (the `deliverIdentityLogin` setting, else * `GH_ACCOUNT`) for the gate card; `null` = unset. */ ghAccount?: string | null; /** Whether `GH_TOKEN` is exported in the daemon environment — presence only, never the value. */ ghTokenPinned?: boolean; /** (F2) The repo's `git remote get-url origin`, read at compose time so the deliver GATE CARD * says what will actually happen. `undefined`/`null` ⇒ it could not be read (the card keeps the * generic sentence); `''` ⇒ read, and the repository has NO `origin` remote. The script asks git * itself where to push at delivery time; the one thing it takes from here is the github.com * `owner/repo` the card names, as `gh pr create --repo` (R1), so the PR opens where the operator * consented to. */ originUrl?: string | null; /** (crew#549) The DELIVER IDENTITY from system settings (`deliverIdentityLogin`) — the login the * push must run as, baked into the script so the refusal holds even when the daemon was started * without `GH_ACCOUNT` exported. A token is NEVER carried here (or anywhere in the settings): * the credential stays in gh's keyring or in `GH_TOKEN`. `null`/absent ⇒ `GH_ACCOUNT` decides. * Refused at compose time when it is not a GitHub login, so nothing unsafe is spliced. */ deliverIdentity?: string | null; /** (crew#737) Where the configured identity came from, for the gate card: the repository's own * pin (`deliverIdentityByRepo`) or the daemon-wide setting. Absent ⇒ the daemon-wide setting. */ deliverIdentitySource?: 'repo' | 'setting'; /** (crew#739) The sentinel nonce (16-64 hex digits). Absent ⇒ a fresh one per composition; a * test passes one to read the sentinel deterministically. Refused at compose time otherwise. */ nonce?: string; /** (crew#720) The Azure DevOps repository this delivery goes to. Absent ⇒ derived from * `originUrl` ({@link adoRepoOf}); a test passes one to point the push and the REST calls at a * local git server and a mocked API. `null` ⇒ not an Azure DevOps delivery. */ adoTarget?: AdoRepo | null; /** (crew#720) The provider credential preflight read at compose time, for the gate card. */ credentials?: DeliverCredentials | null; } /** * (crew#737) The configured push identity for a launch on `repoRef`: the repository's own pin * (`deliverIdentityByRepo[repoRef]`) wins over the daemon-wide `deliverIdentityLogin`; `''` when * neither is set (then `GH_ACCOUNT`, else the CLI's active account). Pure. */ export declare function deliverIdentityFor(settings: { deliverIdentityLogin?: string | undefined; deliverIdentityByRepo?: Record | undefined; }, repoRef: string | null | undefined): { login: string; source: 'repo' | 'setting'; }; /** (crew#940) The push identity a delivery pins: the configured login (the repository's pin or the * setting), else `GH_ACCOUNT`; `null` = none. Pure over `env`. */ export declare function pushIdentityOf(configured: string | null | undefined, env?: NodeJS.ProcessEnv): string | null; /** The logins in `gh auth status` text: "Logged in to account " (gh ≥ 2.40) or * "Logged in to as " (older). Pure, for the tests. */ export declare function parseGhAuthStatusLogins(text: string): string[]; /** A GitHub login as GitHub itself allows it — alphanumerics and single hyphens, ≤ 39 chars. The * ONE validator for the deliver identity: the setting's 400 and the script's splice share it, so * a value that passed the route can always be baked into a single-quoted shell literal. */ export declare function isGitHubLogin(login: string): boolean; /** The pull request a revision run pushes onto (DES-L9). `headRef` is a same-repository branch. */ export interface RevisedPullRequest { number: number; headRef: string; url: string; } export declare function isSafeRefName(name: string): boolean; /** (crew#933) The seam the Azure DevOps resolver runs the deliver helper through (`node ado.mjs …`). */ export type AdoHelperExec = (args: string[], opts: { timeoutMs: number; }) => Promise<{ stdout: string; stderr: string; code: number | null; }>; /** The helper written to a private temp dir and run with the node that runs crew; the credential * stays in the daemon's environment and is read by the helper alone. */ export declare const defaultAdoHelperExec: AdoHelperExec; /** What `gh pr view` answers for a revision target (DES-L9 `resolvePullRequest`). */ export interface ResolvedPullRequest extends RevisedPullRequest { state: string; } export type PullRequestResolution = { ok: true; pr: ResolvedPullRequest; } | { ok: false; error: string; }; /** The exec seam `resolvePullRequest` reads `gh` through — injectable so tests answer without gh. */ export type GhExec = (args: string[], opts: { cwd: string; timeoutMs: number; }) => Promise<{ stdout: string; stderr: string; code: number | null; }>; export declare const defaultGhExec: GhExec; /** * `git remote get-url --push origin` in `rootPath` — the deliver gate's origin preflight (F2). * * `--push` because the sentence is about the PUSH: `git push origin` uses `remote.origin.pushurl` * when one is configured, so a repo whose FETCH url is GitHub while its PUSH url is not (or the * reverse) would otherwise get a card describing a destination the branch never reaches (codex * review of this PR, HIGH). With no `pushurl` configured git answers the fetch url, so the common * case is unchanged. * * - the trimmed URL when git answered one; * - `''` when git said there is NO such remote — the one answer that licenses the card to claim * "this repository has no origin"; * - `null` for everything else (git missing, a timeout, a root that is not a checkout). "Could * not read it" is not a licence to claim anything about it either way, so the card keeps its * generic sentence. * * A run worktree shares its repository's remotes and does not exist yet at compose time, so this * reads the REGISTERED repo root. Bounded to 5 s, once per delivering launch. */ export declare function readDeliverOriginUrl(rootPath: string): Promise; /** * Resolve `revisesPr` (a PR number) to the head branch the run will base on and push to — the * daemon's job, since the engine has no GitHub client (DES-L9 §2). Bounded to 5 s; every failure * is a NAMED refusal (the route answers 409), never a silent second PR: * - not OPEN (merged / closed) — only an open pull request can be revised; * - a fork PR (`isCrossRepository`) — the run's clone cannot push to another repository's branch; * - a head branch name the script cannot splice safely; * - `gh` unavailable, unauthenticated, timed out or answering anything but the JSON asked for. */ export declare function resolvePullRequest(repoRoot: string, number: number, exec?: GhExec, adoExec?: AdoHelperExec): Promise; /** * The env var the engine hands the deliver command with the remote-tip commit it VERIFIED the * run's work against (wicked-core#431 / #433, `deliver_lift.rs` `VERIFIED_BASE_ENV`): set on a * lift outcome of `unchanged` or `lifted`, absent when the lift was skipped (no remote, no default * ref, a branch carrying its own commits) — and never set on a post-hoc `POST /runs/:id/deliver`, * which has no engine verification to pin to (`api/post-hoc-deliver.ts` strips it). */ export declare const DELIVER_VERIFIED_BASE_ENV = "WICKED_DELIVER_VERIFIED_BASE"; /** * The sentinel the deliver script prints when `origin/` no longer resolves to * {@link DELIVER_VERIFIED_BASE_ENV} after the script's own fetch: the remote advanced between the * engine's re-verify and the push window, so the rebase below would carry the base PAST what was * verified. The script refuses BEFORE staging anything. Deliberately NOT a * {@link DELIVER_LIFT_CONFLICT_MARKER}: a moved base is not a recoverable strand — a post-hoc lift * would push a tree nobody verified on the new base; the remedy is the engine's own retry (approve * the deliver gate: it lifts onto the new tip and re-runs the repository's checks first). */ export declare const DELIVER_BASE_MOVED_MARKER = "deliver: BASE MOVED since verification"; /** * The env var the daemon sets on a POST-HOC lift (`POST /runs/:id/deliver`, `api/post-hoc-deliver.ts`) * and the engine never does. A post-hoc lift has no engine verification to protect, so the * crew#426 preflight may regenerate tracked files and deliver them (disclosed); an engine-driven * deliver Tool unit — the variable absent — refuses instead ({@link DELIVER_PREFLIGHT_CHANGED_MARKER}). */ export declare const DELIVER_POSTHOC_ENV = "WICKED_DELIVER_POSTHOC"; /** * (wicked-core#850, codex audit EX-04) The line a POST-HOC lift prints once its tree is final, and * the label it appends to the PR body: no engine lift re-verified what it pushes, so the delivery * is UNVERIFIED, and both trees are named — the run's work as it stood (`tree-before`, after the * commit) and what is delivered (`tree-after`, after the rebase). The daemon records the run's * delivery as unverified from this line ({@link unverifiedTreesFrom}); an engine-driven deliver * unit (no {@link DELIVER_POSTHOC_ENV}) never prints it — the engine's lift re-verified that tree. */ export declare const DELIVER_UNVERIFIED_MARKER = "deliver: UNVERIFIED post-hoc"; /** The trees a post-hoc lift's {@link DELIVER_UNVERIFIED_MARKER} line names; `null` when absent. */ export declare function unverifiedTreesFrom(output: string): { treeBefore: string | null; treeAfter: string | null; } | null; /** * The sentinel the deliver script prints when the crew#426 preflight (`npm install` + the * `manifest:endpoints` / `generate:api-tests` codegen) CHANGED the worktree AFTER the engine had * verified it (wicked-core#433 review addendum): the tree that would ship is then not the tree the * repository's checks certified, and the script refuses before staging anything, leaving the * regenerated files in the worktree. Not a {@link DELIVER_LIFT_CONFLICT_MARKER}: the remedy is to * approve the retry — the engine re-verifies the changed tree first, and the retry's preflight * regenerates nothing more. */ export declare const DELIVER_PREFLIGHT_CHANGED_MARKER = "deliver: PREFLIGHT CHANGED the verified tree"; /** * The BASE heredoc delimiter the script writes its fallback text through. A QUOTED heredoc expands * nothing — `$`, backticks, quotes and backslashes in the intent are inert — so the only way the * caller-supplied text could break out is a line equal to the delimiter. That line is never * removed or altered (it may be the title itself — Copilot on #525): {@link heredocDelimiter} * picks a delimiter no line of the text equals, so the text rides verbatim and the heredoc still * ends exactly where the script says it ends. */ export declare const DELIVER_TEXT_HEREDOC = "WICKED_CREW_DELIVER_TEXT_EOF"; /** * The framed PR/commit text as heredoc body lines. This is a containment boundary, not cosmetics: * the intent is caller-supplied free text off `POST /runs` and it is being spliced into a bash * script. CR and every control character other than tab and newline are removed (a bare CR could * split a line in the CLI's eyes); nothing else is touched. */ export declare function heredocLines(framed: string): string[]; /** * A heredoc delimiter none of `lines` equals: the base, or the base with a numeric suffix * (`…_EOF_1`, `…_EOF_2`, …) when the text happens to carry the base as a whole line. The framing * (line 1 = title) is therefore never disturbed by containment. */ export declare function heredocDelimiter(lines: readonly string[]): string; /** * The hardened deliver script, run as `bash -lc