export type PullRequestLifecycle = "open" | "merged" | "closed"; export type CiStatus = "none" | "running" | "success" | "failure" | "failure-blocked"; export type ReviewReadiness = "ready" | "pending"; export type PolicyReadiness = "ready" | "pending"; export type LocalWorktree = "clean" | "dirty"; export type LocalHeadRelation = "equal" | "behind" | "ahead" | "diverged"; export type PullRequestConditions = { draft: boolean; baseUpdateRequired: boolean; conflict: boolean; changesRequested: boolean; unresolvedThreads: number; ci: CiStatus; review: ReviewReadiness; policy: PolicyReadiness; }; export type LocalMergeSafety = { worktree: LocalWorktree; head: LocalHeadRelation; }; export type PullRequest = { lifecycle: PullRequestLifecycle; conditions: PullRequestConditions; local: LocalMergeSafety; }; export type PullRequestTarget = { provenance: "configured" | "inferred"; branch: string; remote: string; ref: string; repository: string; host: string; fetchSource: string; remoteOid: string | null; }; export type BranchCreationState = { ahead: number; worktree: "clean" | "dirty" | "operation"; relation: "same-ref" | "distinct-ref"; }; export type DiscoveryIssue = | { kind: "detached-head" } | { kind: "target-invalid" } | { kind: "origin-invalid" } | { kind: "candidate-remotes-ambiguous"; remotes: string[] } | { kind: "candidate-prs-ambiguous"; urls: URL[] } | { kind: "candidate-oid-mismatch"; remote: string; urls: URL[] } | { kind: "published-without-pr"; remote: string } | { kind: "link-configuration"; remote: string }; export type PullRequestDiscovery = | { kind: "current"; pullRequest: T } | { kind: "none"; creationTarget: PullRequestTarget; branch: BranchCreationState } | { kind: "blocked"; issue: DiscoveryIssue } | { kind: "inactive" }; export type NextStep = "create" | "link-branch" | "blocked" | "none" | "update-branch" | "sweep" | "fix-ci" | "merge"; export type RouteIntent = "automatic" | "feedback"; export type FeedbackRouteBlocker = | { kind: "discovery-blocked" } | { kind: "pull-request-unavailable" } | { kind: "pull-request-not-open" } | { kind: "target-not-configured" } | { kind: "worktree-dirty" } | { kind: "head-not-equal"; relation: Exclude }; export type RouteDecision = | { kind: "selected"; nextStep: NextStep } | { kind: "feedback-blocked"; nextStep: "blocked"; blocker: FeedbackRouteBlocker }; function localMutationSafe(local: LocalMergeSafety): boolean { return local.worktree === "clean" && local.head === "equal"; } function localMergeSafe(local: LocalMergeSafety): boolean { return local.worktree === "clean" && (local.head === "equal" || local.head === "behind"); } export function derivePullRequestNextStep(pullRequest: PullRequest): Exclude { const { lifecycle, conditions, local } = pullRequest; if (lifecycle !== "open" || conditions.draft) return "none"; if (conditions.baseUpdateRequired || conditions.conflict) { return localMutationSafe(local) ? "update-branch" : "none"; } if (conditions.ci === "failure") return localMutationSafe(local) ? "fix-ci" : "none"; if (conditions.ci === "failure-blocked") return "none"; if (conditions.changesRequested || conditions.unresolvedThreads > 0) { return localMutationSafe(local) ? "sweep" : "none"; } if ( conditions.review === "pending" || conditions.policy === "pending" || !localMergeSafe(local) || conditions.ci === "running" ) return "none"; return "merge"; } export function isPullRequestCreationEligible(branch: BranchCreationState): boolean { return branch.relation === "distinct-ref" && branch.worktree !== "operation" && (branch.ahead > 0 || branch.worktree === "dirty"); } function deriveAutomaticNextStep(discovery: PullRequestDiscovery): NextStep { if (discovery.kind === "inactive") return "none"; if (discovery.kind === "blocked") return "blocked"; if (discovery.kind === "none") return isPullRequestCreationEligible(discovery.branch) ? "create" : "none"; if (discovery.pullRequest.target.provenance === "inferred") { return discovery.pullRequest.lifecycle === "open" ? "link-branch" : "none"; } return derivePullRequestNextStep(discovery.pullRequest); } export function deriveRouteDecision( discovery: PullRequestDiscovery, intent: RouteIntent = "automatic", ): RouteDecision { if (intent === "automatic") return { kind: "selected", nextStep: deriveAutomaticNextStep(discovery) }; if (discovery.kind === "blocked") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "discovery-blocked" } }; } if (discovery.kind !== "current") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "pull-request-unavailable" } }; } const pullRequest = discovery.pullRequest; if (pullRequest.lifecycle !== "open") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "pull-request-not-open" } }; } if (pullRequest.target.provenance !== "configured") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "target-not-configured" } }; } if (pullRequest.local.worktree !== "clean") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "worktree-dirty" } }; } if (pullRequest.local.head !== "equal") { return { kind: "feedback-blocked", nextStep: "blocked", blocker: { kind: "head-not-equal", relation: pullRequest.local.head }, }; } return { kind: "selected", nextStep: "sweep" }; } export function deriveNextStep(discovery: PullRequestDiscovery): NextStep { return deriveRouteDecision(discovery).nextStep; }