import { type Command } from "commander"; import { type IntentGateMode } from "../config.js"; import { type GhPR } from "../gh.js"; import { INTENT_GATE_NOTICE_HEADLINE, type ClearanceEvidence, type IntentBlockedBy, type IntentGate } from "../pr-state.js"; import { signalBestEffort } from "./helpers.js"; /** Evaluate the intent gate (issue #190) for a PR: does its body carry an * unconfirmed interpretation/deviation, and is that still blocking? Shared by * `pr ready` (read-only), `pr automerge` (which also labels), `inbox` (#620 — * classifyPR stays I/O-free and consumes `gate.blocked`), and `pr merge` * (refuses an unconfirmed signal). One function, one verdict. * The clearance probe (`everCleared`) is only paid when it can change the answer — * a clean PR, or one already carrying the label, skips the extra `gh api` calls, so * the common path stays as cheap as before. * * `everCleared` reads the server's AUDIT ARTIFACT, not the bare `unlabeled` * timeline event (issue #411): the event is permanent and says nothing about * why the label went away, so one stray machine strip used to disarm the gate * forever with no way for the loop to re-arm it. * * Probe fail is fail-CLOSED (issue #620): a thrown clearance read is not * evidence of a human clear, so `everCleared` stays false and a live signal * parks — never `approved_ready`. */ /** `intentGate`'s verdict plus WHICH input produced it (issue #444) — carried * separately so `IntentGate` consumers (armIntentGate, its truth table) stay * byte-identical. `blockedBy` feeds the blocker text via `mergeDecision`. */ export interface EvaluatedIntentGate extends IntentGate { blockedBy?: IntentBlockedBy; } /** Optional overrides so tests can pin mode / clearance without a network. * Production call sites omit this — live config + `ghIntentGateClearance`. */ export interface IntentGateEvalReads { mode?: () => IntentGateMode; clearance?: () => ClearanceEvidence; } export declare function evalIntentGate(repo: string, number: number, prView: GhPR, reads?: IntentGateEvalReads): EvaluatedIntentGate; /** One retry, ~2–5s per the issue-#444 intake; no config knob by design. */ export declare const FRESH_PROBE_BACKOFF_MS = 3000; /** The reads the fresh probe re-runs, injectable so tests are deterministic. */ export interface IntentGateFreshReads { /** Re-read whether the PR currently carries `needs-reporter-review`. */ hasLabel: () => boolean; /** Re-run the clearance probe (ghIntentGateClearance). */ clearance: () => ClearanceEvidence; /** The bounded backoff — injected so tests never actually wait. */ sleep: (ms: number) => Promise; } export declare function freshProbeIntentGate(first: EvaluatedIntentGate, reads: IntentGateFreshReads): Promise; /** The real reads, bound to gh. */ export declare const liveIntentGateFreshReads: (repo: string, number: number) => IntentGateFreshReads; export type ChecksVerdict = "pass" | "fail" | "pending"; /** Classify `gh pr checks` TSV lines via `ciStateOf` (issue #721). * Empty / all-no-verdict → `pending` (`ChecksVerdict` has no `none`; * `pr checks --wait` is time-bounded, exit 11). Pure. */ export declare function classifyChecks(lines: string[]): ChecksVerdict; /** The `pr note` body: caller text + the loop marker (+ optional rework-from * horizon echo via the canonical renderer — never hand-rolled, PR #604 * review). Pure and exported so the test invokes the REAL assembly. */ export declare function renderPrNoteBody(body: string, reworkFrom?: string): string; /** Append the loop-review marker to a reviewer-authored comment body unless it * already carries one. Idempotent, so a body rendered by `renderFindingBody` * (which stamps it already) is not double-marked. */ export declare function stampLoopReview(body: string): string; /** The comment posted the moment `needs-reporter-review` is APPLIED. * * This is not decoration — it is what makes the gate fail-CLOSED rather than * fail-STUCK (issue #411). The server clears the label only on an EXACT * confirmation token, so a reporter who does not know the rule can reply * "yeah looks good" and watch the PR park forever. Stating the exact line at * label time is the mitigation, and the tokens come from the shared contract * so this text cannot drift from the matcher that reads it. * * It lists every token rather than a sample: the rule is whole-line equality * (PR #441), so a reporter shown three of nine has no way to discover the * rest, and a guess that misses is silence. * * It also says the token is the WHOLE reply (PR #441, third review pass): the * rule refuses anything following it, so a thank-you under the token reads as * a correction. This notice and the server's nudge are the ONLY places a * reporter can learn that before they hit the silence. * * Ends with the loop marker: without it, this very comment would be read as a * human decision and clear the gate it just applied. */ /** The notice's first line, re-exported so "did the notice land?" and "when was * the gate armed?" both match the SAME literal the renderer emits — a * hand-copied probe string would drift and silently stop detecting the comment. * Defined in `pr-state.ts` because `gateOpenedAt` reads it back as the ARMING * event (PR #487 review) and that module must stay free of command imports. */ export { INTENT_GATE_NOTICE_HEADLINE }; export declare function renderIntentGateNotice(): string; /** The three GitHub writes that arm the gate, injectable so each can be failed * independently in tests. `noticePosted` is the read that drives the retry. */ export interface IntentGateArmWriters { ensureLabel: (repo: string, number: number) => void; addLabel: (repo: string, number: number) => void; postNotice: (repo: string, number: number) => void; noticePosted: (repo: string, number: number) => boolean; } export interface IntentGateArmOutcome { /** Whether any arming write was attempted at all this pass. */ attempted: boolean; /** Every attempted write succeeded. */ armed: boolean; /** Blockers to merge into the automerge payload (empty when armed). */ blockers: string[]; /** Joined failure detail for the `gateArmError` JSON field. */ gateArmError?: string; } /** The blocker text the loop and the operator both key on. */ export declare const GATE_ARM_BLOCKER = "intent gate could not be armed"; export declare function armIntentGate(repo: string, number: number, gate: IntentGate, w: IntentGateArmWriters): IntentGateArmOutcome; /** The real writes, bound to gh. */ export declare const liveIntentGateWriters: IntentGateArmWriters; /** Exit code for "the security scan's input could not be trusted" — an empty * `pr diff` capture, or an approval whose scan attestation does not verify. * One code for one class, modelled on the exit-7 unresolved-thread gate in * `approve`: both are preconditions that refuse rather than warn. */ export declare const SCAN_EXIT = 9; /** Why the attestation gate answered the way it did. `verified` / `not-required` * pass; everything else refuses. Carried verbatim in `--json` as * `scan.verdict`, so a consumer distinguishes "nobody attested" from "the * numbers disagree" from "we could not check". */ export type ScanVerdict = "verified" | "not-required" | "missing" | "zero" | "mismatch" | "no-report" | "no-digest" | "digest-mismatch" | "undetermined"; export interface ScanAttestation { ok: boolean; /** What the caller attested to having scanned; null when `--scan-files` was absent. */ files: number | null; /** GitHub's own changed-file count; null when it could not be determined. */ expected: number | null; verdict: ScanVerdict; /** One line, empty when `ok`. */ reason: string; /** #482's shape and vocabulary — not a second one. */ degraded: string[]; } /** True only when we KNOW every changed path is documentation. An EMPTY list is * not docs-only: an unknown file set must never buy an exemption — that is the * same "absence read as a pass" move #407 is about. Pure. */ export declare function isDocsOnlyChange(paths: string[]): boolean; /** The attestation gate. Pure — every I/O result is an argument — so the refusal * rules are testable without a network, a repo, or a `gh`. * * Order is load-bearing: a non-approving verdict is released FIRST, because a * reviewer that wants to say `request_changes` must never be blocked from * saying it. A gate that also silences the bad news would be strictly worse * than no gate — and #482's rule is precisely that a gate which could not run * is `request_changes`, never a footnote. */ export declare function evaluateScanAttestation(i: { /** Does this call RECORD AN APPROVAL? Only approvals are gated. */ approving: boolean; attested: number | null; expected: number | null; paths: string[]; /** `--scan-report` names a NON-EMPTY REGULAR FILE. `false` = it does not * (missing, empty, or a directory); `null` = the flag was not passed. */ reportExists?: boolean | null; /** `--scan-digest`: the sha256 the caller says its scan input hashed to. */ attestedDigest?: string | null; /** sha256 of the PR's diff as GitHub serves it NOW; null = could not be read. */ actualDigest?: string | null; }): ScanAttestation; /** The one-line scan record that goes in the review body / approval comment, so * the attestation is visible to a human reading the PR — not only to `--json`. * `ran: true` is only meaningful next to the numbers that could falsify it. */ export declare function scanAttestationLine(a: ScanAttestation, reportPath?: string, digest?: string | null): string; /** Does this call's scan record belong in the PR text? An APPROVAL always * carries one — that record is the falsifiable half of `ran: true`, and an * approval whose attestation is invisible on the PR is back where #407 started. * A non-approving verdict carries one only when the reviewer actually attested * something; otherwise `request_changes` reviews would all sprout a "not * required" line that says nothing about the change. Pure. */ export declare function recordsScanLine(a: ScanAttestation, approving: boolean): boolean; /** `--scan-files` as an integer, or null when absent/unparseable. A garbage * value reads as ABSENT (→ refused), never as 0-and-therefore-something. */ export declare function parseScanFiles(raw: string | undefined): number | null; /** sha256 of the exact bytes a scan was given. Pure. */ export declare function diffDigest(diff: string): string; /** Is `--scan-report` a real artifact? EXISTENCE alone was the check, and an * empty file or a directory passed it (PR #484 review, gemini low). A report * with no bytes in it is not findings — it is the same absence the whole gate * refuses to read as a pass. */ export declare function scanReportUsable(path: string | undefined): boolean | null; /** Write a captured diff so only its owner can read it. * * `writeFileSync` with no mode yields `0666 & ~umask` = **0644** — the full * unfiltered diff of a private repo, world-readable at the predictable * `/tmp/pr-.patch` that §0b itself prescribes (PR #484 review — codex P1 + * gemini, found independently). Worse, a predictable path can be pre-created * as a symlink, so an unguarded write follows it somewhere of the attacker's * choosing. * * So: refuse anything that is not already a regular file, unlink what is * there (mode + ownership must be OURS, not inherited from whatever we found), * and create with `wx` + `0600` so the open itself fails rather than follows if * something reappears in the gap. */ export declare function writeCapture(path: string, diff: string): void; /** Number of files in a raw unified diff — one `diff --git` header per file. * Counted from the BYTES that were captured, never from a file list, so it * cannot agree with `--name-only` by construction. Pure. */ export declare function countDiffFiles(diff: string): number; /** Lines in the captured diff. Pure. */ export declare function countDiffLines(diff: string): number; type ReviewSettleSleep = (ms: number) => Promise; /** Test hook: automerge's settle wait. Production never calls this. */ export declare function _setReviewSettleSleep(fn?: ReviewSettleSleep): void; /** After `--all-ready`: park (exit 5) iff the sweep evaluated PRs and merged * none (issue #638). Empty 0/0 and mixed k/N (k>0) stay 0. `automergeOnce` * itself never exits. */ export declare function allReadySweepExit(evaluated: number, merged: number): number; /** Human line for the `--all-ready` sweep. No ✅ when merged < evaluated * (0/N and partial) — a green check on "merged 0/N" is the same lie as * exit 0 (issue #638). */ export declare function allReadySweepLine(evaluated: number, merged: number): string; /** Issues whose ShipFlow claims automerge releases after a successful merge. * `closingIssuesReferences` is empty by design on a `--partial` slice * (`Part of #N`, #367); `linkedIssueNumbers` unions those with any * closing-keyword refs so the parent is not left claimed (#747). * * Claim-release only — GitHub still only auto-closes closing-keyword * issues. We never add a close keyword or call `gh issue close`. * `pr merge` does not call this. */ export declare function releaseClaimsAfterAutomerge(ctx: Parameters[0], repo: string, prNumber: number, prView: GhPR): Promise; export declare function registerPRCommand(program: Command): void; /** How long the flagless `post-review` watch WAITS for a byte before giving up * on waiting (issue #427). Long enough that a real `echo '[…]' |` producer has * already written — short enough that the headless approve, whose stdin is an * inherited pipe that never carries data, is not perceptibly slowed. * * This window bounds the WAIT only; it is not a deadline on being seen. The * watch below stays armed past it, so a slow producer's first byte still * aborts. A window that also closed the eye would merely make #427 rarer — * a silent drop the docs promise cannot happen is harder to diagnose than a * consistent one. And the wait must stay bounded: unbounded is #219's hang. */ export declare const STDIN_PEEK_MS = 250; /** A watch on a non-TTY stdin that stays ARMED for the whole command. * * Two questions, deliberately separate: * - `within(ms)` — "did a byte show up while I was willing to wait?" * - `sawBytes()` — "has a byte shown up AT ALL, up to right now?", asked again * immediately before the review is posted. * * Bytes are never consumed: the first one settles the watch and destroys the * pipe, so a producer slower than the window can never hand us truncated JSON. * Consume-and-proceed is not a state this can reach. */ export interface StdinByteWatch { /** Resolves `true` as soon as a byte arrives, `false` once `ms` elapses with * the pipe still silent. Never settles later than `ms`. */ within(ms: number): Promise; /** Whether any byte has arrived since arming — including after `within` * resolved `false`. Yields the event loop first, so bytes already sitting in * the pipe buffer behind our synchronous `gh` calls are delivered before we * answer. */ sawBytes(): Promise; /** Disarm and release stdin. Idempotent. */ release(): void; } /** Arm the watch. A TTY gets the idle watch and is left untouched: nothing is * piped to an interactive terminal, and destroying it would break the * surrounding session. * * Staying armed must never mean staying alive. Once the wait window is over * the stdin handle is `unref`'d, so the listener still sees bytes while other * work keeps the loop turning but cannot itself hold the process open — that * back door is exactly #219's loop-tick hang. On a stdin with no `unref` * (a file-redirected `fs.ReadStream`) we release instead of risking it; a * regular file has its bytes ready inside the window anyway. */ export declare function watchStdinBytes(): StdinByteWatch; export declare function detectIssueFromBranch(branch: string): number | undefined; /** The loud no-link warning for `pr create` (issue #635). An unlinked PR merges * without closing anything, so every `issue wait --on` timer parked on the * intended issue waits forever — say so at authoring time, when it is still * one flag away from fixed. Returned rather than printed so a test can pin * the text, same pattern as `buildShipFlowHeader`. */ export declare function unlinkedPrWarning(branch: string): string; /** How the header links the issue. `closes` (default) emits the GitHub closing * keyword `Closes #N` so merging resolves a whole-issue PR; `part-of` emits a * plain `Part of #N` reference with NO closing keyword, so merging a partial * slice leaves the parent issue open for the deferred follow-ups (issue #367). */ export type IssueLinkMode = "closes" | "part-of"; /** ShipFlow header for a PR body: reference the issue with a link, not a copy of * its detail — GitHub renders the linked issue. In the default `closes` mode the * keyword auto-closes the issue on merge; in `part-of` mode it emits a plain * `Part of #N` reference (no closing keyword) for a slice PR. Pure + testable. */ export declare function buildShipFlowHeader(project: string, issueNumber?: number, issueUrl?: string, linkMode?: IssueLinkMode): string; /** What `pr sync --keep-conflicts` prints as the resolution recipe. NEVER * `git add -A`: the UNMERGED index state is the ONLY thing that makes git * refuse to commit a file still containing `<<<<<<<`, and `add -A` clears * exactly that. Stage the paths you actually resolved, then re-gate. * * A FUNCTION of the base (issue #412, finding 3): the printed gate command has * to carry `--base` — always `pr conflict-check --base `, because the * moment `git rebase --continue` commits the markers a base-less gate has * nothing left to enumerate and reports clean. This is also what step 5 of * `references/conflict-resolution.md` tells the agent to run, so the two now * say the same thing. * * `root` anchors the printed `git add` (see `anchoredGit`) so it runs from a * subdirectory too — the paths the operator substitutes come from the gate, and * those are repo-root-relative. Injected, not probed, so the recipe stays pure. */ export declare function resolutionRecipe(baseRef: string, root?: string | null): readonly string[]; /** The ONE place the gate's re-check invocation is spelled (PR #434 review). * * `pr sync` printed the gate WITHOUT `--base` on both of its refusal * paths while `resolutionRecipe` — three lines away, in the same * command — threaded the base. That is the vacuous form this PR exists to stop * recommending: it makes the operator re-resolve a base the caller already * knows, and auto-resolution can land on a DIFFERENT ref than the one `pr sync` * actually rebased onto, so the re-check would certify a tree against the wrong * base. Every printed invocation now goes through here, so the base-less form * can't reappear in one message while the others are correct. Where no concrete * ref is in scope, pass the `origin/` placeholder — never nothing. */ export declare function recheckCommand(baseRef: string): string; /** A git command the operator can paste into ANY shell, at ANY depth in the * worktree (PR #434 review, finding 2). * * The gate reports **repo-root-relative** paths — `gitPaths` pins its CWD to * `repoToplevel()` so enumeration and grep share one frame of reference — so a * printed `git add -- sub/app.ts` pasted from `sub/` resolves `sub/sub/app.ts` * and dies with `could not open directory 'sub/sub/'`. `-C ` puts the * printed command in the same frame the printed path is expressed in, which is * the only way one string can be correct from every CWD. (Under the default * `diff.relative=false` the un-anchored form was already wrong from a subdir; * anchoring makes every printed path-bearing command runnable, not just this * one.) * * Root unknown → print the plain form. A `git -C '' …` would be worse than no * anchor, and a gate that cannot locate its own toplevel is already failing * loudly for that reason. */ export declare function anchoredGit(root: string | null, args: string): string; /** The repo root to anchor a PRINTED command to, or `null` when it can't be * located. `repoToplevel()` throws by design (a gate that cannot find its tree * has proved nothing); a *hint string* must never be the thing that crashes the * process, so this degrades to the un-anchored form instead. */ export declare function printedRoot(): string | null; /** One conflict-marker line found in a tracked file. */ export interface ConflictMarkerHit { path: string; line: number; text: string; } /** What a `git grep -z -n` stream parse recovered. * * `hits` are the marker lines of files that carry a REAL conflict. `paths` is * every path the parse read a well-formed record for — INCLUDING files whose * only match was a bare `=======`. The two are deliberately different sets, and * `paths` (never `hits`) is the parser's coverage claim: it is what the * authoritative files-with-matches pass is checked against in * `scanConflictMarkers`. Checking against `hits` instead would hard-block every * push over any file containing a lone 7-equals line — the parser is *designed* * not to trip on those, and `git grep -l` cannot tell one from a real conflict. */ export interface ConflictMarkerParse { hits: ConflictMarkerHit[]; paths: string[]; } /** The `git grep -E` pattern for the four marker families git can leave behind * (`<<<<<<<`, `|||||||` in diff3 mode, `=======`, `>>>>>>>`). */ export declare const CONFLICT_MARKER_PATTERN = "^(<{7}|\\|{7}|={7}|>{7})( |$)"; /** * Parse `git grep -z -n` output: a file counts as conflicted when it has a * `<<<<<<<` start **OR** a `>>>>>>>` end. A bare `=======` (or a lone diff3 * `|||||||`) never trips the gate on its own — that's the only line a changelog * or a rule of prose plausibly starts with. * * This used to require BOTH arms (the "triad rule"), which let a half-resolved * file through: delete one arm, leave `<<<<<<< HEAD` behind, and since `git add` * has already cleared the UNMERGED state, `conflict-check` exits 0 and `pr sync` * force-pushes a live marker. The triad's stated justification — protecting docs * that quote a marker — does not hold: the `^` anchor already excludes the * mid-line quoting this repo's own conflict-resolution docs use, and running * this exact pattern across the whole repo matches ZERO lines. It bought no * false-positive protection and cost real coverage (PR #394 review). * * Input is the `-z` (NUL-delimited) form — a stream of `path\0line\0text\n` * records. The old parse read the plain `path:line:text` form with * `^(.*?):(\d+):(.*)$`, which a path containing `::` defeats: a tracked * `a:12:b.ts` yields `a:12:b.ts:1:<<<<<<< HEAD`, parsed as path `a`, line 12, * text `b.ts:1:<<<<<<< HEAD` — text that matches NEITHER marker regex, so the * whole file's hits were discarded and the gate reported CLEAN on live markers * (PR #394 review). git prints colons in paths unquoted, so no colon-based * parse can be correct; NUL is the only unambiguous delimiter. * * The walk is POSITIONAL, not a `split("\0")` field walk (issue #412, finding 1). * A `split` treats fields as fixed pairs, so ONE extra NUL anywhere in the stream * shifts parity for everything after it. A matched LINE can legitimately contain * a NUL: git's binary heuristic only inspects the first 8000 bytes, so a >8 KB * text file with an embedded NUL further in is scanned as text and printed * verbatim. Measured: a `======= \0Y` line in a 9.9 KB file desynced the walk and * dropped EVERY later file in sorted-path order — a victim with three live * markers reported CLEAN. `--text` does not fix this; the NUL is still inside the * matched line. Reading each field to its own terminator does: a path cannot * contain a NUL (POSIX), so field 1 ends at the 1st NUL and field 2 at the 2nd, * and a grep match is exactly one line, so the text ends at the next LF — * whatever bytes it contains. * * Pure + testable (`pr sync` shells out to gh+git and has no harness). */ export declare function parseConflictMarkerRecords(out: string): ConflictMarkerParse; /** The marker hits alone — see `parseConflictMarkerRecords` for the coverage set. */ export declare function parseConflictMarkerGrep(out: string): ConflictMarkerHit[]; /** The outcome of enumerating paths for the gate. `ok: false` means the git * command FAILED — which is emphatically NOT the same as "nothing changed". * A gate that cannot enumerate has proved nothing and must fail CLOSED. */ export interface PathEnumeration { paths: string[]; /** false = the command errored (unresolvable ref, git failure). */ ok: boolean; /** The command, for the operator-facing failure message. */ cmd: string; } /** Paths from a `git diff --name-only` invocation. NUL-delimited (`-z`) so a * filename containing a newline or a quotable byte can't split into two bogus * pathspecs — which would silently drop that file from the marker scan. * Failure is REPORTED (`ok: false`), never flattened into an empty result: * `scanConflictMarkers([])` is `[]`, so a swallowed error would have read as a * clean tree and let the push through (PR #394 review). * * Runs from `repoToplevel()`, and that is LOAD-BEARING (issue #412, finding 4 — * PR #434 review). Enumeration and grep are a PAIR and must share one frame of * reference: `git grep` always runs from the root (see `grepConflictMarkers`), * so the paths handed to it must be root-relative too. `git diff --name-only` * honours `diff.relative` — set to true (config, or a `--relative` in the * command) it emits CWD-relative paths AND drops everything outside the CWD. * Run the gate from `sub/` and `sub/app.ts` was enumerated as `app.ts`, which * the root-anchored grep matched nothing for: exit 1 ("no match") read as a * CLEAN tree over three live markers. Pinning the CWD here fixes it at the * source, for every call site, and needs no `--no-relative` flag (git ≥2.30): * at the toplevel, relative and absolute are the same thing. * * A root that cannot be located degrades to `ok: false` rather than throwing — * callers rely on this never throwing (`pr sync`'s best-effort listing), and a * gate that cannot find its tree has enumerated nothing, which is exactly what * `ok: false` means. */ export declare function gitPaths(cmd: string): PathEnumeration; /** Files this branch changed relative to `baseRef` — the only place a rebase * can have left markers, so the gate never scans (or trips on) unrelated files. */ export declare function changedPaths(baseRef: string): PathEnumeration; /** The repository root, as the CWD every gate git command runs FROM — BOTH the * `git diff` that enumerates (`gitPaths`) and the `git grep` that scans * (`grepConflictMarkers`). * * Load-bearing (issue #412, finding 4): `git grep` resolves pathspecs relative * to the CWD, so paths enumerated in one frame and grepped in another match * nothing — `git grep` exits 1 ("no match"), which the gate reads as CLEAN over * live markers. Anchoring only one half is worse than anchoring neither: under * `diff.relative=true` the two halves then disagree where before they at least * agreed (PR #434 review). It must be a toplevel CWD and NOT a `:(top)` * pathspec prefix: `--literal-pathspecs`, which the `:leading.ts` regression * requires, disables pathspec magic outright. Failure to locate the root throws * — a gate that cannot find the tree it is certifying has proved nothing. * (`gitPaths` catches that throw and degrades to `ok: false`, its own way of * saying the same thing.) */ export declare function repoToplevel(): string; /** Split paths into `git grep`-sized batches, budgeting the QUOTED byte length * actually spent on the command line. A single path over budget still gets its * own chunk rather than looping forever. Pure, so the budgeting is testable * without building a 100 KB fixture. */ export declare function chunkPathspecs(paths: string[], maxFiles?: number, maxBytes?: number): string[][]; /** * Scan the working tree for leftover conflict markers in `paths`. * * Two passes per chunk, and the split is the point (issue #412, finding 1): * * 1. `-l` — the AUTHORITY on WHICH files match. `-z -l` emits NUL-terminated * paths, so `split("\0")` is exact and no parse can lose a file. * 2. `-n` — the line/text DETAIL, positionally parsed. * * Then fail CLOSED if the `-l` set is not covered by the paths the `-n` parse * recovered: the detail pass lost a file the authority saw, so this scan cannot * certify anything. The comparison is against `parse.paths` — every path a * record was read for — and emphatically NOT against the filtered `hits`. `-l` * cannot distinguish a bare `=======` from a real conflict and the parser * deliberately does not trip on one, so checking `hits` would make any file * containing a lone 7-equals line permanently unpushable. */ export declare function scanConflictMarkers(paths: string[], budget?: { maxFiles?: number; maxBytes?: number; }): ConflictMarkerHit[]; /** Files the `-l` authority matched that the `-n` parse recovered no record for. * Non-empty ⇒ the scan lost coverage and must fail closed. * * ⚠️ `recovered` is `ConflictMarkerParse.paths` — every path a record was read * for — and NEVER the filtered hits. `git grep -l` matches a bare `=======` the * same as a real `<<<<<<<`, while the parser deliberately does not trip on a * lone separator. Comparing against hits would therefore report every file * containing a single 7-equals line as "lost", hard-blocking its push forever: * a repo-wide denial of service on the gate, invisible today only because this * repo happens to contain zero such lines. */ export declare function unrecoveredMatches(matched: string[], recovered: string[]): string[]; /** Which rebase state git has left in this worktree, if any. Reads the * worktree's own git dir, so it is correct inside `git worktree` checkouts. */ export declare function rebaseInProgress(): "rebase-merge" | "rebase-apply" | null; /** The commit an in-flight rebase is replaying ONTO, read from the rebase state * git itself wrote (`.git/rebase-merge/onto` or `.git/rebase-apply/onto`). * `rebaseInProgress()` already names the directory, so this is a file read. */ export declare function rebaseOnto(): string | null; /** The remote's default branch, from `refs/remotes/origin/HEAD`. */ export declare function originHeadRef(): string | null; /** Last-ditch default branch when `origin/HEAD` was never set (a plain `git * clone --depth` or a hand-added remote leaves it unset). */ export declare function defaultBranchRef(): string | null; /** Which ref `conflict-check` diffs against, and where that ref came from. */ export type BaseSource = "rebase-onto" | "origin-head" | "default-branch" | "explicit"; export interface ResolvedBase { base: string; source: BaseSource; } /** * Resolve the base `conflict-check` scans against (issue #412, finding 3). * * Without a base the check is VACUOUS at exactly the moment it matters. Once * `git rebase --continue` has COMMITTED the markers, nothing is unmerged and * nothing differs from HEAD, so both of the local enumeration sources are empty * and the gate prints `{"clean":true,"scanned":0}` and exits 0 — over a tree * whose HEAD blob literally contains `<<<<<<< HEAD`. The same tree with * `--base main` yields 3 hits and exit 8. * * Precedence, most-specific first: * 1. `rebase-onto` — mid-rebase, the commit git is actually replaying onto is * the true base, whatever anyone typed. * 2. `origin-head` / 3. `default-branch` — the repo's own default branch. * 4. `explicit` — the operator's `--base`. * * NOT `@{upstream}`: that resolves to `origin/`, which is the * branch's own remote copy and not the PR base at all — diffing a branch against * itself is a vacuous scan wearing a base's clothes — and it is unset on any * freshly created branch anyway. * * Ranking `explicit` LAST is deliberate and must stay that way: a stale `--base` * flag must not override the commit git is actually replaying onto mid-rebase, * and ranking it up would change which single ref `base` reports as covered. * * `--base` keeps its documented ADDITIVE contract at the call site: an explicit * ref the precedence didn't pick is still scanned, it just isn't what gets * reported as the resolved base — the caller reports the FULL set it diffed * against as `bases`, so nothing scanned goes unnamed (PR #434 review). Pure * (probes are injected) so the precedence is unit-testable without a rebase * fixture. */ export declare function resolveScanBase(i: { onto: string | null; originHead: string | null; defaultBranch: string | null; explicit?: string; }): ResolvedBase | null; export type SyncEntryVerdict = { ok: true; } | { ok: false; message: string; }; /** * Whether `pr sync` may start, given the worktree's state. The loop reuses ONE * worktree, so a worker turn that dies mid-`--keep-conflicts` strands it on a * detached HEAD inside a rebase — after which the old branch check emitted the * misleading `On branch "HEAD" but PR #N is "x"` and every later loop git op * failed. Each refusal names the state AND the exact command that clears it. * Pure (state is injected) so it's unit-testable without a git fixture. * * `base` is the ref the printed re-check names, and it must be CONCRETE (PR #434 * review, finding 1). This branch fires only when a rebase IS in progress, so * the caller always has one — the rebase's own `onto`, else `origin/` — and the `origin/` placeholder printed here before was a command * the operator could not run: `conflict-check --base 'origin/'` fails to * resolve the ref and exits 8 via `enumerationFailed`, i.e. the recovery hint * itself reported "the gate proved nothing". The placeholder survives only as * the last resort for a caller that truly has neither source. * * `root` anchors the printed `git add` for a subdirectory shell (`anchoredGit`). */ export declare function syncEntryGuard(i: { rebase: "rebase-merge" | "rebase-apply" | null; currentBranch: string; head: string; number: number; base?: string | null; root?: string | null; }): SyncEntryVerdict; /** Hygiene: `pr sync` drops `shipflow-approved` only after a successful * force-push of a *new* HEAD (issue #742). `--no-push`, conflict, marker * gate, push fail, and unchanged HEAD keep the label. The approved-head * SHA stamp is historical and is not touched. */ export declare function shouldDropApprovedLabel(i: { pushed: boolean; headMoved: boolean; }): boolean; /** Call `remove` iff {@link shouldDropApprovedLabel}. Both SHAs must be * 40-hex and differ; unreadable fails closed (do not strip a still-valid * approval). Returns whether `remove` ran. */ export declare function dropApprovedLabelIfNeeded(i: { pushed: boolean; beforeSha: string | null; afterSha: string | null; }, remove: () => void): boolean; //# sourceMappingURL=pr.d.ts.map