/** * The deliver phase's PR + commit TEXT (crew#524, acceptance finding F-3R2-014). * * `gh pr create --fill` opened wicked-studio#249 with a title cut MID-WORD out of the intent's first * 72 characters (`…scenario CLN-2) aga`) and an EMPTY body: no `Fixes #214`, no run link, no record * of what ran or what passed — and the commit carried the same truncated headline. A reviewer got * nothing to review against. This module composes the text from the RUN instead. ONE composer, two * moments it can be asked: * * - at DELIVERY, from the persisted run view (`GET /runs/:id/deliver-text`, text/plain): every * phase the run went through with its seat and gate outcome, the repo checks the verify phase * ran with their exit codes, the evaluator's verdict, and a link to the run; * - at LAUNCH, from the workflow definition — the fallback the deliver script carries in case the * daemon cannot answer when the PR opens (an auth-required daemon, no `curl`, a daemon that went * away): the intent, the issue links, the run id and the phase list ARE known then; the runtime * sections say plainly that they were not available rather than pretending. * * The title is the intent's first line, cut at a WORD boundary within {@link DELIVER_TITLE_MAX} * characters (never mid-word), or `wicked-crew run ` when the intent is blank. `Fixes #N` is * emitted when the intent names an issue with a closing verb (`fix issue #214`, `closes #7`); every * other `#N` / `repo#N` / `owner/repo#N` mention rides as `Refs:`. * * FRAMING, shared by both carriers (the daemon's text/plain answer and the script's embedded * fallback) so the script parses exactly one shape: line 1 = title, line 2 = blank, then the body. * The framed text is also, verbatim, the message of the commit the deliver phase MAKES when the * run left uncommitted work (`git commit -F`: git takes the first paragraph as the subject). A run * that committed incrementally keeps its own commits untouched — the PR title is composed from the * run either way; only the deliver phase's own commit is guaranteed to match it. */ import type { GateSpec, PhaseDef, SessionView, WorkUnit } from './types.js'; /** * The PR title cap (crew#860, S17b F15 / S17a F20). GitHub ACCEPTS 256 characters, and under that * cap wicked-studio#586 opened as the intent's first 244 — a whole paragraph, cut mid-sentence, * wider than the PR list renders. A title is a headline: the intent's FIRST SENTENCE, cut at a word * boundary inside 100 characters; the full intent is the body's first section and the run id is * named there. (crew#550 P-1 — a 72-character cap severed a quoted phrase — is still honoured: the * cut is depth-aware, see {@link boundedTitle}.) The COMMIT subject keeps git's conventional width, * {@link COMMIT_SUBJECT_MAX}; a longer title then opens the commit body in full. */ export declare const DELIVER_TITLE_MAX = 100; /** The commit subject cap — git's conventional subject width (crew#550). */ export declare const COMMIT_SUBJECT_MAX = 72; /** One phase of the run, with as much as is known about it at composition time. */ export interface DeliverPhaseFact { id: string; stage: string; role: string; gate: string; /** The seat that did the work — `null` when unknown (launch time) or when the phase is tooling. */ seat: string | null; /** The recorded gate outcome / unit status, or `—` at launch time. */ outcome: string; } /** One repo check a phase's floor ran (`typecheck` / `lint` / `test`), as the engine recorded it. */ export interface DeliverCheckFact { /** The phase whose floor ran it (`verify`, `deliver`, …) — DES-L9. */ phase: string; name: string; command: string; exitCode: number | null; durationMs: number | null; timedOut: boolean; spawnError: string | null; /** The engine's verdict on a non-zero exit (api-types 0.38.0 `RepoCheckRun.classification`) — * `floor_env_mismatch` / `pre_existing_in_sandbox` / … — so a reader never sees "tests fail" * over a run the engine itself excused (review-benchmark-prs D3). `null` when none. */ classification: string | null; } /** An evaluator-role phase's recorded verdict. */ export interface DeliverVerdictFact { phase: string; seat: string | null; verdict: string; reason: string | null; } /** * What a unit's seat REPORTED in its final output (crew#860): the governed-worker output contract's * "What you did / Commands run (with exit codes) / Counts / Findings" block — for a creator, what * changed and the recipe it ran; for an evaluator, its findings by severity. Read from the unit's * captured output by {@link extractUnitReport}; `report` is bounded markdown, never empty. */ export interface DeliverReportFact { phase: string; seat: string | null; role: 'creator' | 'evaluator'; report: string; /** The commands the seat says it ran, with the exit code it pasted beside each * ({@link extractRecipeExits}) — a creator's recipe, read for the done-when evidence table. * Absent/empty = the output named no command with an exit code. */ exits?: DeliverRecipeExit[]; } /** One command a seat reported running, with the exit code it pasted (crew#860). */ export interface DeliverRecipeExit { command: string; exit: number; } /** * (crew#1186) The run's assurance LABELS, read from the run's own contract (`session.assurance`): the * same on every delivery path (the deliver phase and a post-hoc delivery from the stranded card both * compose from the run view). `qe` is `null` when the run does not require QE acceptance. */ export interface DeliverAssuranceFact { /** `reduced` = launched in reduced-assurance mode (the distinct evaluator and the judge waived). */ mode: 'full' | 'reduced' | (string & {}); qe: { status: string; reason: string; /** (crew#1192) The attributed PASS a `required` decision stood on at delivery: the ledger verdict's * id and reviewer (`QeAcceptanceCheck`). */ pass?: { verdictId: string; reviewer: string | null; }; } | null; /** Every failed floor the operator overrode, with the phase it was on and who overrode it. */ floorOverrides: Array<{ phase: string; by: string; label: string; }>; } /** Everything the composer needs. Built by {@link factsFromRun} or {@link factsFromWorkflow}. */ export interface DeliverTextFacts { runId: string; intent: string; workflowId: string | null; repoRef: string | null; /** The studio bookmark for the run (`/runs/`), when the origin is known. */ runUrl: string | null; /** `run`: from the persisted run view; `workflow`: from the definition at launch (no runtime facts). */ source: 'run' | 'workflow'; phases: DeliverPhaseFact[]; /** Recorded repo checks; `null` when the run recorded none. Ignored when `source` is `workflow`. */ checks: DeliverCheckFact[] | null; /** * WHY `checks` is empty, when the engine said (wave 6, wicked-core#449 @ 9e11685: a * `repoChecksEvaluated` with `checks: []` carries `detectError` / `sandboxError`, persisted on * the unit's repo-checks report). `null` when checks ran or the engine gave no reason. Rendered * as "0 checks detected — ", never as "checks ran". */ checksNote: string | null; verdicts: DeliverVerdictFact[]; /** DES-L9: the open pull request this run REVISES (its commits land there; no new PR). */ revisesPr?: { number: number; url: string; } | null; /** crew#550: extra git trailers for the commit (`Co-Authored-By: …`), from * {@link DELIVER_TRAILERS_ENV}; absent/empty = only the `Delivered-By:` trailer. */ trailers?: string[]; /** crew#550 P-7: the follow-ups the evaluator flagged (its `FOLLOW-UPS:` block, read from the * evaluator units' output by {@link extractFollowUps}). `[]` = read, none flagged; absent = not * read (a definition-time composition), and the section is left out. */ followUps?: string[]; /** crew#1186: the run's assurance labels (QE decision, overridden floors, reduced assurance); * absent = not known (a definition-time composition, an older engine) and the section is left out. */ assurance?: DeliverAssuranceFact; /** crew#860: the creator and evaluator seats' final reports, read from the unit outputs * ({@link reportsFromOutputs}). `[]` = read, nothing reportable; absent = the outputs could not * be read (a definition-time composition, or an adapter without transcripts) — said in the body. */ reports?: DeliverReportFact[]; } export interface DeliverText { /** The PR title: ≤ {@link DELIVER_TITLE_MAX} characters, one line, never cut mid-word. */ title: string; /** Markdown; never empty. */ body: string; } export interface IssueRefs { /** `#N` (or `repo#N`) mentions the intent pairs with a closing verb — emitted as `Fixes …`. */ fixes: string[]; /** Every other issue mention — emitted as `Refs: …`. */ refs: string[]; } /** * The conventional-commit prefix for a headline: derived from the workflow (`bug` → `fix`, * `feature` → `feat`, `migration` → `refactor`, anything else → `chore`), applied only when the * headline does not already start with one (DES-L9; review-benchmark-prs D1). */ export declare function conventionalPrefix(workflowId: string | null | undefined): string; /** * The first SENTENCE of a headline line (crew#860): the text up to the first `.`, `!` or `?` that * is followed by whitespace or the end, at quote/bracket depth 0 (a period inside `(…)` or `"…"` * does not end the sentence), and not the dot of an abbreviation. A closing `.` is dropped (a * headline carries no full stop); `!` and `?` stay. A dot glued to the next word (`0.8.4`, * `deliver.ts`, `wicked-studio#586.`) never splits. No terminator ⇒ the whole line. */ export declare function firstSentence(line: string): string; /** * The PR title / commit subject: the FIRST SENTENCE of the intent's first prose line (crew#860), * whole when it fits, otherwise cut at the last word boundary that leaves room for a single `…` — * so the result is ≤ {@link DELIVER_TITLE_MAX} characters and never ends mid-word (the F-3R2-014 * headline `…scenario CLN-2) aga`). Dangling punctuation before the ellipsis is dropped. A blank * intent names the run instead — through the SAME bounded cut, so a long caller-supplied session id * (the CLI passes `--session` through) is never cut mid-id either (Copilot on #525): the body names * the run id in full. */ export declare function deliverTitle(intent: string, runId: string, workflowId?: string | null): string; /** * `line` whole when it fits, else cut at a word boundary with a single `…` — ≤ `max` characters — * choosing the LAST boundary at nesting depth 0: never inside a quoted or bracketed phrase (crew#550 * P-1: `… truncated at '(Failed):' and 'sign a seat…`). A straight apostrophe opens a quote only * after a space or at the start (so `daemon's` is a word, not a quote). When no depth-0 boundary * exists inside the room, any word boundary is taken; a single 72+ character token is cut hard. */ export declare function boundedTitle(line: string, max?: number): string; /** * The commit subject for a PR title (crew#550): the title itself when it fits git's 72-character * subject width, else the same depth-aware word-boundary cut at 72. The deliver script puts the * full title at the top of the commit body whenever the two differ. */ export declare function commitSubject(title: string): string; /** * The issues the intent names, split into the ones it says it fixes and the rest. * * GitHub links and closes on `#N` (this repo) and `owner/repo#N` only. An OWNER-LESS `repo#N` * does neither — so when its repo segment names the delivery repo (`repoRef`, the registered * repo id the PR lands in) it is emitted as plain `#N` (review W3-K2: `fixes wicked-studio#214` * on a wicked-studio delivery must close #214). Any other owner-less `repo#N` rides verbatim as * information; `owner/repo#N` is left exactly as written. */ export declare function issueRefs(rawIntent: string, repoRef?: string | null): IssueRefs; /** * (crew#933) The Azure Boards work items the intent names — `AB#`, the syntax Azure Boards * links on (case-insensitive, not glued to a word) — in order, deduplicated. On a GitHub delivery * the mention rides the body as written (the Azure Boards app links it); on an Azure DevOps * delivery the ids are linked on the pull request itself (`workItemRefs`). The daemon's * linked-issues block is issue text, never the run's references. */ export declare function adoWorkItemIds(rawIntent: string): string[]; /** * How much of the intent the deliver SCRIPT carries in its embedded fallback text. The script is * one `bash -lc` argument, and `LaunchSchema.problem` has no length cap, so an unbounded intent * could exceed a platform's single-argument limit (Linux: 128 KiB) and fail the phase with E2BIG * before it runs (Copilot on #525). The run-derived text the daemon answers is NOT bounded — it * travels over HTTP, never through argv. */ export declare const EMBEDDED_INTENT_CAP = 8000; /** `intent` bounded for embedding in the script: whole when it fits, else cut and said so. */ export declare function boundIntentForEmbedding(intent: string): string; /** * The launch-time FALLBACK text the deliver script embeds (crew#524; wave-3 isolation review): the * intent rides BOUNDED ({@link boundIntentForEmbedding} — the script is one argv entry) but the * issue references are derived from the FULL intent FIRST, so a `fixes #214` that sits past the * {@link EMBEDDED_INTENT_CAP} still reaches the body as `Fixes #214` — the line GitHub acts on — * when no daemon answers `GET /runs/:id/deliver-text`. The title is the intent's first line either * way. The cut copy carries the "longer than the script embeds" note, so the omission is disclosed * and the reference is not. */ export declare function composeEmbeddedDeliverText(f: DeliverTextFacts): DeliverText; /** * The PR title + body (and, through {@link framedDeliverText}, the commit message) for a run. * Never empty: every section either carries the recorded facts or says why it does not. * * `links` defaults to the issue references of `f.intent`; the embedded-fallback path passes the * references of the FULL intent while `f.intent` is the bounded copy ({@link composeEmbeddedDeliverText}). */ export declare function composeDeliverText(raw: DeliverTextFacts, links?: IssueRefs): DeliverText; /** * crew#886: the PR title, body and commit message leave the machine, and they quote seat reports * verbatim — a creator's "Worktree: `/.wicked/repos//wicked-worktrees/`" put the * operator's home directory on GitHub. Every spelling of the home directory (its realpath and the * macOS `/private` twin too) becomes `~`, through the same rewrite crew#618 applies to chat replies * (`rewriteHostPaths` + `withRootAliases`): `/x` → `~/x`, a bare `` → `~`, and a longer * sibling (`-old/…`) is never touched. */ export declare function redactHostPaths(text: string, home?: string | null): string; /** At most this many pasted recipe commands per phase in the done-when table. */ export declare const RECIPE_EXITS_MAX = 12; /** The one shape both carriers speak: title, blank line, body, trailing newline. */ export declare function framedDeliverText(text: DeliverText): string; /** * The inverse of {@link framedDeliverText}, or `null` when the text is not framed that way — a * non-empty title line, a blank line 2, and a body with something in it (the same three conditions * the deliver script's `_framed` check applies before it accepts a daemon answer). */ export declare function parseFramedDeliverText(framed: string): DeliverText | null; /** (operator ruling 2026-10-11) "judged by operator ()" when the operator judged this phase's * gate because no judge could rule (`assurance.operatorJudges`, core-ts ≥ 0.9.7); (operator ruling * 2026-10-21) "floor overridden by operator ()" for a judgment that overrode the phase's failed * repo-checks floor (`operatorJudges[].floorOverride`, api-types 0.135.0) — the engine's label, * as-is; both, `; `-joined, when the gate carries both; else `null`. */ export declare function operatorJudgedLabel(u: { assurance?: unknown; }): string | null; /** (operator ruling 2026-10-14) The engine's disclosure line — `judge: agy (fallback; codex benched: …)` — when * this phase's gate was judged by a fallback seat (`assurance.judgeFallbacks`, api-types 0.121.0); else `null`. */ export declare function judgeFallbackLabel(u: { assurance?: unknown; }): string | null; export declare function gateLabel(gate: GateSpec | string | undefined): string; /** * A composed per-run workflow id (`-deliver-`, `-verified-`; see * `composeDeliverWorkflow` / `composeDeliverableFloor`) reads as its base. The composed id is * capped at 128 characters, so for a long caller-supplied run id the marker's tail is CUT — the * tail is therefore matched as a prefix of the sanitised run id, not whole. */ export declare function baseWorkflowId(workflowId: string, runId: string): string; /** * The facts from a persisted run view — what the daemon answers on `GET /runs/:id/deliver-text`. * The deliver phase itself (running while it asks) is listed as `this PR`. * * `resolved.workflowId` is the run's preset or workflow NAME the route resolved from the engine's * record (`runIdentityOf`, seam X2) — `null` for a user plan or a free-text run, which names no * workflow. Absent (`undefined`), the view's id is used (with the per-run composition suffix cut). */ export declare function factsFromRun(view: SessionView, runUrl: string | null, resolved?: { workflowId?: string | null; revisesPr?: { number: number; url: string; } | null; /** crew#550: the evaluator follow-ups, when the caller read the evaluator units' output. */ followUps?: string[]; /** crew#860: the seats' final reports, when the caller read the unit outputs. */ reports?: DeliverReportFact[]; /** crew#1192: the run's QE acceptance check as delivery reads it (`qeAcceptanceCheckFor`), when * the caller read it: a satisfied `required` check with a verdict reads "QE acceptance passed". */ qeAcceptance?: { status?: string; satisfied: boolean; verdictId: string | null; reviewer: string | null; } | null; }): DeliverTextFacts; /** What is known at LAUNCH, from the workflow definition — the script's embedded fallback. */ export declare function factsFromWorkflow(input: { runId: string; intent: string | undefined; workflowId: string | null; repoRef: string | null; phases: PhaseDef[]; runUrl: string | null; revisesPr?: { number: number; url: string; } | null; }): DeliverTextFacts; /** * The environment variable naming extra commit trailers for delivered work (crew#550), one per * line — e.g. `Co-Authored-By: Name
`. Only well-formed single-line `Token: value` * trailers are kept; anything else is dropped rather than breaking the commit's trailer block. */ export declare const DELIVER_TRAILERS_ENV = "WICKED_CREW_DELIVER_TRAILERS"; /** The configured trailers, validated; `[]` when none. */ export declare function configuredTrailers(env?: NodeJS.ProcessEnv): string[]; /** * The environment variable naming the PUBLIC origin a PR's run link may use (crew#550 P-2) — the * studio address a reviewer can open (`https://studio.example.com`). Unset, a PR body names the run * id and carries no link: the daemon's own loopback address opens only on the daemon's host. */ export declare const PUBLIC_ORIGIN_ENV = "WICKED_CREW_PUBLIC_ORIGIN"; /** The configured public origin, or `null`. */ export declare function configuredPublicOrigin(env?: NodeJS.ProcessEnv): string | null; /** * `s` as ONE URL path segment, RFC 3986 strict: everything but unreserved characters is * percent-encoded — `encodeURIComponent` alone leaves `!'()*` alone, and `'` in particular must not * reach a single-quoted shell literal (the deliver script bakes this in; Copilot on #525). */ export declare function urlPathSegment(s: string): string; /** * The run link for a PR body under the configured PUBLIC origin ({@link configuredPublicOrigin}), * or null (crew#550 P-2). Never a loopback URL: `http://127.0.0.1:/s/run%3A` opens only on * the daemon's own host, so a PR carrying it gave every other reader a dead link. A loopback, * unparseable or non-http(s) origin yields no link; the run id itself is always named in the text. */ export declare function runUrlFor(origin: string | null | undefined, runId: string): string | null; /** At most this many follow-ups ride a PR body; each is cut to {@link FOLLOW_UP_MAX_CHARS}. */ export declare const FOLLOW_UPS_MAX = 10; export declare const FOLLOW_UP_MAX_CHARS = 300; /** * The follow-ups an evaluator's output flags (crew#550 P-7): the list under its `FOLLOW-UPS:` * heading (or the heading line's own text), ending at the first line that is neither an item nor * the continuation of one. `none` / `n/a` yields nothing. Every block in the text is read, in order, * de-duplicated, capped at {@link FOLLOW_UPS_MAX}. */ export declare function extractFollowUps(text: string): string[]; /** How much of one seat's report rides a PR body (characters); the run record has the rest. */ export declare const REPORT_MAX_CHARS = 4000; /** At most this many reports ride a PR body (one per creator / evaluator unit, by ord). */ export declare const REPORTS_MAX = 6; /** * A seat's final REPORT from its captured output (crew#860): the text from the first report heading * ({@link REPORT_HEADING}) to the end — the output contract puts the report LAST, after the work — * or, when no heading is found, the tail of the output (its last 40 lines), said so. `VERDICT:` * lines are removed (the composer states the recorded verdict beside the phase). Headings are * demoted below the body's own `##` / `###` so the report cannot restyle the PR; an unclosed code * fence is closed so it cannot swallow the footer; control characters are dropped; the result is * cut at a line boundary inside {@link REPORT_MAX_CHARS} and the cut is disclosed. `null` when the * output carries nothing reportable (empty, or whitespace only). */ export declare function extractUnitReport(text: string | null | undefined): string | null; /** * The seats' reports for a run (crew#860), one per creator / evaluator unit in ord order, read * through `readOutput` (the adapter's `workOutput`, injected so the composer stays pure). A unit * whose output cannot be read or carries nothing reportable contributes nothing; the result is * `[]` when nothing was reportable, never `undefined` — the caller says "could not read" only when * it has no reader at all. */ export declare function reportsFromOutputs(view: SessionView, readOutput: (unit: WorkUnit) => Promise): Promise; /** * The commands a seat's output says it ran, with the exit code it pasted beside each (crew#860) — * the "Commands run (with exit codes)" half of the governed-worker output contract, in the shapes * seats write it: `` - `npm run lint` → exit 0 ``, `` | `npx vitest run` | 0 | ``, `typecheck: exit 0`. * A line counts only when it names BOTH a command (a code span, else the list item's text before * the exit word) and an exit code; deduplicated by command (the LAST report wins — a re-run * supersedes), at most {@link RECIPE_EXITS_MAX}. Never throws; `[]` when nothing parses. */ export declare function extractRecipeExits(text: string | null | undefined): DeliverRecipeExit[]; //# sourceMappingURL=deliver-text.d.ts.map