import type { Command } from "commander"; import { ghPRMergedByHead } from "../gh.js"; import { branchWorktreeDirty, cleanupMergedLocalBranch, isAncestorOfMergedHead, localBranchExists, localGcCandidates, localRepoMatches } from "../git-local.js"; import type { GhPR } from "../gh.js"; import { type ForeignConflictedPR, type PRClassification, type PRState, type ReporterCorrectionRow } from "../pr-state.js"; export { prAttentionReasons, issueNeedsReply } from "../pr-state.js"; /** Count unresolved review threads for one PR, tolerating a transient GraphQL * blip: on failure return 0 + `degraded` so a single PR's error can't blank the * whole inbox. `fetchThreads` is injected so this stays pure + testable. */ export declare function safeUnresolvedThreadCount(fetchThreads: () => { isResolved: boolean; }[]): { count: number; degraded: boolean; }; /** The REST clearance read `gateOpenedAt` needs (issue #650), and the ONE place * the "don't pay for it unless the gate is up" rule is written down. * * `gateClearedAt` is consumed exclusively inside `classifyPR`'s `intentBlocked` * branch, so reading it for a PR `evalIntentGate` did not park would buy * nothing and cost one `gh api …/comments --paginate` on EVERY open PR, every * tick. The guard therefore lives here, in front of the reader, rather than * inside it. `intentBlocked` is `evalIntentGate().blocked` (body+label+mode+ * everCleared), not the label alone — issue #620. * * `read` is injected so that invariant is testable without a network: a stub * counts its own calls, and the test asserts ZERO for a non-gated PR. Both * production call sites go through this function for the same reason — * a second, hand-guarded call site is how the guard would rot. */ export declare function gateClearanceReadFor(prNumber: number, intentBlocked: boolean, read: (n: number) => string | undefined): string | undefined; /** One PR row in the `inbox --json` envelope — the contract the orchestrator * reconciles against. Typed (it used to be an inferred object literal on one * branch and `Record` on the other) so a new optional field * like `correction` can be read without a cast, and so the pretty printer keeps * compiling when the shape moves. */ export interface InboxPrRow { number: number; title: string; branch: string; base: string; url: string; draft: boolean; reviewDecision: string; unresolvedThreads: number; closesIssues: number[]; state: string; ciState: string; approved: boolean; ageHours: number; needsAttention: boolean; reasons: string[]; /** Present iff `state === "reporter_corrected"` (issue #442) — the OLDEST * unanswered candidate, i.e. `corrections[0]`. */ correction?: ReporterCorrectionRow; /** Every unanswered candidate, oldest first (PR #446 review, finding 2). * `correction` alone was newest-only, which dropped the real decision as soon * as a thank-you followed it — and the gate's own nudge asks reporters to send * notes as a SEPARATE comment, so that is the documented shape. The * orchestrator judges decision-vs-question over this list; the CLI does not. */ corrections?: ReporterCorrectionRow[]; /** Present with `correction`, and on an `escalateOnce` row: is the PR's parent * issue escalated? Resolved from closing references AND `Part of #N`. */ parentNeedsHuman?: boolean; /** The loop owes this PR ONE `issue escalate` and nothing else (PR #446 * review, finding 6): an `ESCALATE_ONCE_REASONS` refusal for which no * escalation has EVER been filed. The row is actionable while that is true * and parks again — permanently — once the escalation carries the * `escalate-once` marker for this (PR, reason). */ escalateOnce?: boolean; /** Hours the intent gate has stood unanswered (#439); present on gated rows. */ gateAgeHours?: number; /** The reason half of the once-key (issue #488) — pass it straight back as * `issue escalate --for-pr --once-reason `, or the escalation * is filed under no key at all and the row re-escalates next tick. */ escalateOnceReason?: string; /** The parent's `escalate-once` markers could NOT be read this tick, so whether * this (PR, reason) was already escalated is UNKNOWN (PR #489 round 3). The row * suppresses — a duplicate escalation is the expensive direction — but it is * suppressed on a gate that did not run, so the orchestrator must treat the * inbox as incomplete and re-read rather than concluding there is no work. * #482's rule: a gate that could not run is never a footnote. */ escalateOnceUnknown?: boolean; /** The review-thread GraphQL read blipped for THIS row — a DIFFERENT input from * an `escalate-once` marker read that could not run (PR #489 round 4). Kept * separate because `degraded` is now the OR of the two, and `degradedInputs` * must be able to name each source without subtracting one cause from a merged * flag. */ threadsDegraded?: boolean; /** True when ANY input this row depends on went dark — the generic flag the * pretty printer and `summary.degraded` read. See `threadsDegraded` / * `escalateOnceUnknown` for WHICH one. */ degraded?: boolean; foreign?: boolean; author?: string; trustedHead?: boolean; distrust?: string; humanOnly?: boolean; } /** * The inbox row for one foreign conflicted PR (PR #394 review). Split out and * pure because it encodes a security boundary, not formatting: an UNTRUSTED * head (fork, or an author who is not OWNER/MEMBER/COLLABORATOR) is listed so a * human can see it, but carries `needsAttention: false` + `humanOnly: true` so * it can never be admitted to the autonomous queue — where a worker would check * it out and run its test suite with the operator's credentials. */ export declare function foreignPrRow(entry: ForeignConflictedPR, cl: PRClassification): InboxPrRow; /** * The inbox row for one of the loop's OWN PRs. Pure, and split out for the same * reason `foreignPrRow` is: on a `reporter_corrected` PR (issue #442) it decides * what the orchestrator can see, and "the row was byte-identical whether or not * the reporter had replied" is the defect this file is fixing. * * Both parent lookups are injected rather than called here so this stays pure — * and so they stay LAZY. `parentIsEscalated` (a label listing) is consulted only * on a row that actually carries a correction; `parentWasEscalatedFor` (a * per-parent comment read) only on a row that already OWES an escalation. An * ordinary tick touches neither, which is what keeps it at zero extra API calls. */ export declare function minePrRow(pr: GhPR, cl: PRClassification, ctx: { unresolvedThreads: number; degraded: boolean; /** Live `needs-human` LABEL membership — the routing fork for a correction * row. Deliberately still a label read: that field asks "is a human being * waited on right now", which is exactly what the label means. */ parentIsEscalated: (issues: number[]) => boolean; /** Was an escalation EVER filed for this (PR, reason)? Opposite polarity to * `parentIsEscalated`, and to #486's "is one outstanding" — read from the * parent's `escalate-once` comment markers, which no reply can erase. * * Returns `"unknown"` when the parent's comments could NOT be read. The row * still suppresses (never duplicate an escalation on a blip), but it must say * so — see `escalateOnceUnknown` on the row. */ parentWasEscalatedFor: (issues: number[], prNumber: number, reason: string) => boolean | "unknown"; }): InboxPrRow; /** * Conflicted PRs the loop may actually ACT on (PR #394 review). `foreignPrRow` * keeps `state: "conflict"` on untrusted heads by design — a human must see * them — so counting state alone reported work the loop is FORBIDDEN to do: * an orchestrator saw `conflicts: 1` beside `prsNeedingAttention: 0` and went * hunting for it. Those rows stay counted, separately, in `humanOnlyConflicts`. */ export declare function actionableConflicts(prs: { state: string; humanOnly?: boolean; }[]): number; /** * The text inbox's one-line note for an `escalateOnce` row — WHICH refusal it * is, since the three have three different things to tell the human. * * "(no rework left)" was hardcoded, and it was already wrong for * `correction_unreadable` (nothing was ever reworked there). With * `reporter_gate_stale` it would be wrong a third way and hide the only number * that matters on that row: how long the reporter has been silent. The JSON row * carries `reasons` + `gateAgeHours` either way; this is the human-readable * projection of the same two fields. */ export declare function escalateOnceNote(row: { reasons: string[]; gateAgeHours?: number; }): string; /** States with no loop handler — the PR is counted somewhere instead of * vanishing from the summary, but nothing is dispatched at it. * `reporter_corrected` is deliberately absent: it HAS a handler, so a landing * correction must move a PR OUT of `parked` and INTO `prsNeedingAttention`. * That count moving is the observable this fix is judged on. */ export declare const PARKED_STATES: readonly string[]; /** * PRs nothing will be dispatched at this tick. * * Two refinements over "state is in `PARKED_STATES`" (PR #446 review): * - a row the loop must ESCALATE once is `awaiting_reporter` but NOT parked * (finding 6) — counting it in both places would have the summary contradict * itself, `prsNeedingAttention` and `parked` naming the same PR; * - a FOREIGN `reporter_corrected` row (finding 7) is parked: the sweep admits * conflicts only, so the row carries no route. It is the one row the sweep * can produce that no other counter covers — every other foreign row is * `conflict` or already a parked state — so without this it vanishes from * the summary entirely, which is the failure `PARKED_STATES` exists to stop. */ export declare function parkedCount(prs: { state: string; needsAttention?: boolean; foreign?: boolean; }[]): number; /** * Corrections the loop can actually ACT on — the `actionableConflicts` rule, for * the state that gained a route in #442 (PR #446 review, finding 7). * * `count("reporter_corrected")` also counted foreign-sweep rows, which * `foreignPrRow` forces to `needsAttention: false` and never attaches a * `correction` to. A gated foreign PR with one trusted comment therefore * reported `reporterCorrected: 1` beside `prsNeedingAttention: 0` and no * correction anywhere on the row — the field added to make a correction * unmissable advertising work that does not exist. */ export declare function actionableCorrections(prs: { state: string; foreign?: boolean; }[]): number; /** * Open PRs that count against the loop's WIP limit (issue #451): the loop's * OWN PRs minus those parked on a human. An `awaiting_reporter` PR is a timer * exactly like `issue wait --on #X` — only the reporter's token (or a label * removal) moves it — and counting such PRs as WIP jammed admission with * nothing the loop could do: three gated PRs idled a whole session in * reconcile-only. Foreign-sweep rows never counted as WIP and still don't. */ export declare function actionableWip(prs: { state: string; foreign?: boolean; }[]): number; /** What one tick's merged-branch GC did, for the inbox envelope/summary. */ export interface MergedBranchGcResult { /** Branches CONFIRMED cleaned — the ref is verified gone afterwards. */ cleaned: { name: string; prNumber: number; }[]; /** Merged PRs whose local branch holds commits unreachable from the merged * head — un-landed work, kept and reported. */ unpushed: { name: string; prNumber: number; }[]; /** Merged with no unique local commits, but the holding worktree has * uncommitted edits `worktree remove --force` would destroy — kept. */ dirty: { name: string; prNumber: number; }[]; /** Cleanup ran but the branch ref survived (busy worktree, git refusal) — * reported so `gcCleaned` never over-claims; retried next tick. */ failed: { name: string; prNumber: number; }[]; /** Candidates not looked up this tick because the GC's time budget ran out * (stalled GitHub calls must not starve the reconciler). */ lookupsSkipped: number; } /** * Merged-branch GC (issue #455) — run once per inbox tick, beside the other * self-heals. `cleanupMergedLocalBranch` only ever fires inside `pr automerge`/ * `pr merge`; a PR merged via the GitHub UI, `gh pr merge`, the dashboard, or a * human releasing the #190 intent gate leaves its local `fix/*` branch — and * any worktree checked out on it — behind forever. This reconciles the local * side: every local `fix/*` branch whose PR merged at EXACTLY the local tip is * handed to the same `cleanupMergedLocalBranch` the merge path uses (dedicated * worktree removed, loop worktree/main checkout detached, branch deleted, * admin entries pruned). A differing tip is unpushed work — reported, kept. * * Deps are injected for tests; every failure degrades to "no cleanup this * tick" because a heal must never take the inbox down with it. */ export declare function gcMergedLocalBranches(repo: string, deps?: { matches?: typeof localRepoMatches; candidates?: typeof localGcCandidates; lookup?: typeof ghPRMergedByHead; cleanup?: typeof cleanupMergedLocalBranch; isDirty?: typeof branchWorktreeDirty; isAncestor?: typeof isAncestorOfMergedHead; stillExists?: typeof localBranchExists; /** Injectable clock so tests can drive the lookup time budget. */ nowMs?: () => number; }): MergedBranchGcResult; /** One glyph per PR state for the pretty (non-JSON) table. Typed * `Record` so adding a state to the ladder without giving it an * icon is a `tsc` error, not a row that silently renders a bare dot. */ export declare const STATE_ICONS: Record; /** One classified inbox PR plus the `GhPR` it came from — so `loop plan` can * attach mergeDecision flags without a second classifyPR ladder. */ export interface InboxPrScan { pr: GhPR; row: InboxPrRow; } /** Same classifyPR pass `inbox --json` uses (issue #611). Extracted so `loop * plan` cannot grow a second decision table. Inbox `--json` still emits rows * only — this return type is additive for the planner. */ export declare function collectInboxPrRows(repo: string, me: string, opts: { sweepEnabled: boolean; staleHours: number; maxReworks: number; }): InboxPrScan[]; export declare function registerInboxCommand(program: Command): void; //# sourceMappingURL=inbox.d.ts.map