framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
225 lines • 13.5 kB
TypeScript
import { type CliRunner } from '../cli-exec.js';
import { type Cached } from './cache.js';
import type { AutoMergeOutcome } from '../events.js';
/**
* The `gh` CLI, in one place: the two JSON reads the dashboard makes and the runner its write
* actions use.
*
* There were four separate `gh` adapters across three modules. Three were reads that each
* hand-rolled `execFile` + `JSON.parse` + a swallowed failure and each spelled the 8s timeout
* again, and two of those differed only in whether a branch positional was passed; the fourth was
* a generic runner for the write actions, which rejects with stderr and waits longer.
*/
/** Runs `gh`, resolving stdout and rejecting with the CLI's own stderr on failure. */
export type GhRunner = CliRunner;
/**
* A {@link GhRunner} for the write actions (push, open a PR). Longer timeout than a read: these
* talk to the network and to git, and the user is waiting on a button they pressed.
*/
export declare function nodeGhRunner(): GhRunner;
/**
* The GitHub token an Actions run authenticates with (#1352).
*
* `GH_TOKEN` / `GITHUB_TOKEN` win, because CI sets them and must beat whatever `gh` happens to be
* logged in as on the runner. With neither set, fall back to the `gh` CLI's own credential — the
* same one every PR this framework opens is already authenticated by. A machine that can open a PR
* could always have run an Actions session too; it just had no way to say so, and the agent failed
* with the credential sitting one `gh auth token` away.
*
* Undefined when there is no token to be had (gh missing, logged out, or refusing to hand it over),
* which the caller turns into the agent's stated reason for not starting. Deliberately quiet about
* *why* gh declined: the caller's message names both ways to fix it, and a keyring prompt's stderr
* is not something to put in front of someone who simply has not set GH_TOKEN.
*/
export declare function githubToken(cwd: string, env?: Record<string, string | undefined>, gh?: GhRunner): Promise<string | undefined>;
/** A forgiving `gh --json` read: resolves `empty` when gh is missing/unauthed, or its output is not JSON. */
export declare function ghJson<T>(args: string[], cwd: string, empty: T, gh?: GhRunner): Promise<T>;
/** The PR opened for a branch, when there is one. */
export interface LinkedPr {
number: number;
url: string;
/** OPEN / MERGED / CLOSED (as gh reports it). */
state: string;
title: string;
/** ISO creation time, when the read included it: what tells one agent's PR from a predecessor's. */
createdAt?: string;
/**
* The head commit the PR covers, when the read included it (#1512): what tells "the branch's
* PR already landed everything" from "the session kept working after its PR merged".
*/
headRefOid?: string;
}
/**
* A best-effort PR lookup, for a named branch or for the checkout's current branch. One type for
* both: the named-branch form is the general one, and "the current branch" is just omitting it.
*/
export type PrLookup = (cwd: string, branch?: string) => Promise<LinkedPr | undefined>;
/**
* The branch-addressed form, for a caller that always names one (#799): a finished session's
* worktree may be gone, so "the current branch" would silently be the project's, not the
* session's. Narrower than {@link PrLookup} on purpose, so that invariant is in the type.
*/
export type BranchPrLookup = (cwd: string, branch: string) => Promise<LinkedPr | undefined>;
/**
* The PR for `branch`, or for whatever branch `cwd` is on when none is named. Resolves undefined
* when gh is missing/unauthed or there is no PR.
*
* The named-branch form is what a finished session needs (#799): its worktree may be gone, so the
* checkout's current branch is the project's, not the session's. The fields are copied out rather
* than passed through, so a future `--json` addition cannot leak into what callers store.
*
* Every optional field {@link LinkedPr} declares must be both asked for and copied out here, or it
* is silently absent for every caller of this path. `createdAt` was neither, and the CI watch reads
* it to decide whether a check-less PR has outlived the window a check suite takes to attach — with
* the field missing, that age is unknowable and such a PR could never be merged at all.
*/
export declare function ghPrView(cwd: string, branch?: string, gh?: GhRunner): Promise<LinkedPr | undefined>;
/**
* The cached form of {@link ghPrView} (#1028), and what the dashboard's panels use.
*
* A PR lookup costs about 600ms where the git reads beside it cost ten, and the answer changes
* about as often as someone opens a PR. Cached per checkout and branch, shared between the
* worktree bar and the handoff summary, and refreshed behind whoever asks. `pending` says the
* answer is not known yet rather than that there is no PR — the difference matters to a caller
* deciding whether to offer "Open PR".
*/
export declare function cachedPrView(cwd: string, branch?: string): Promise<Cached<LinkedPr | undefined>>;
/** Forget a branch's PR, after an action that changes whether it has one. */
export declare function forgetPr(cwd: string, branch?: string): void;
/**
* Every PR a branch name has ever had, newest first (#1251).
*
* `gh pr view <branch>` answers with the newest PR for that head *in any state*, so a session
* whose prompt pins its branch name (`the-framework/triage-quick`) inherits a predecessor's
* merged PR as its own. The list form keeps the whole history so {@link pickAgentPr} can decide
* which entry, if any, belongs to the agent asking. Resolves `[]` when gh is missing/unauthed —
* indistinguishable from "no PRs", which is what every caller would do with a failure anyway.
*/
export declare function ghPrsForBranch(cwd: string, branch: string): Promise<LinkedPr[]>;
/**
* {@link ghPrsForBranch} for a caller about to *open* a PR (#1601): a listing that fails throws
* instead of reading as "no PRs", because "none" and "could not tell" must not look alike there —
* the difference is a second draft PR on a branch that already has one.
*/
export declare function ghPrsForBranchOrThrow(cwd: string, branch: string): Promise<LinkedPr[]>;
/**
* How {@link ghMergePr} answers GitHub refusing to arm auto-merge. `merge-now` (the default, and
* everything before #1418) merges directly: right where a human just said "land it". `watch`
* merges directly only when the PR's checks have already passed, and otherwise answers `watched`
* — the daemon's CI watch merges it on green. That is the auto path's mode, because the direct
* fallback there is precisely the lands-before-CI hazard (#1406): a repo without GitHub
* auto-merge saw every armed PR merged seconds after opening, before its first check ran.
*/
export interface MergePrOptions {
whenUnarmed?: 'merge-now' | 'watch';
}
/**
* Merge a PR the handoff just opened (#1216): GitHub auto-merge first, so the PR lands when its
* checks pass rather than before them; where the repo does not allow auto-merge, merged directly
* or handed to the daemon's CI watch, per {@link MergePrOptions.whenUnarmed} (#1418). Squash in
* all forms — a session's branch is working history, not a story worth preserving.
*
* Never throws: the caller reports the outcome alongside the handoff's, and a merge that could
* not happen must not turn a successful handoff into a failed one.
*/
export declare function ghMergePr(cwd: string, number: number, gh?: GhRunner, opts?: MergePrOptions): Promise<AutoMergeOutcome>;
/** Where a PR's CI stands (#1418), summarised to the one question the merge path asks. */
export interface PrCiStatus {
/**
* `passing`: every check has concluded and none failed — the PR may land. `failing`: at least
* one concluded check failed, whatever the rest are doing — red now, and more green later will
* not unsay it. `pending`: something is still running and nothing has failed yet. `none`: no
* checks reported — either the repo has no CI, or the suite has not attached yet (they take a
* few seconds after a push), which is why callers treat it with a grace period rather than as
* green. Also the answer when `gh` itself could not say: acting on an unreadable status must
* never merge anything.
*/
checks: 'passing' | 'failing' | 'pending' | 'none';
/** The names of the failed checks, for the CI-fix agent's prompt. */
failed: string[];
/** The PR's head commit, so a fix attempt can be recorded against the state it saw. */
headSha?: string;
/** The PR's head branch, where a CI fix must land. Rides this read because it is the same `gh` call. */
branch?: string;
}
/**
* A PR's combined check state (#1418): GitHub Actions check runs and classic commit statuses,
* both of which `statusCheckRollup` carries.
*
* Skipped and neutral conclusions count as passing, matching how GitHub's own merge box treats
* them; everything else that concluded non-successfully counts as failed — a cancelled or
* timed-out check is not evidence the work is good, and the merge this feeds exists to stop
* unverified work landing (#1406).
*/
export declare function ghPrCiStatus(cwd: string, number: number, gh?: GhRunner): Promise<PrCiStatus>;
/** Whether the repo lets PRs use GitHub auto-merge (#1417); `known: false` when `gh` could not say. */
export interface RepoAutoMerge {
known: boolean;
allowed: boolean;
}
/**
* Whether this repo allows GitHub auto-merge (#1417).
*
* An armed merge on a repo that does not (the {@link DIRECT_MERGE_FALLBACK} half of #1216) is
* handed to the daemon's CI watch (#1418): merge on green, but only while the daemon runs — so
* the launcher notes the local fallback and names the server-side Allow auto-merge setup.
* `known: false` (gh missing, unauthenticated, not a GitHub repo) is "could not say", which
* renders nothing: no crying wolf, same stance as the trust (#1318) read.
*
* The probe is the REST endpoint, not `gh repo view --json autoMergeAllowed`: `repo view` has no
* such field (any gh version), so that spelling always errored into "could not say". REST omits
* `allow_auto_merge` for viewers without push access — absent lands in the same "could not say".
*/
export declare function ghRepoAutoMerge(cwd: string, gh?: GhRunner): Promise<RepoAutoMerge>;
/** The cached form of {@link ghRepoAutoMerge} (#1028): the launcher polls, the setting barely changes. */
export declare function cachedRepoAutoMerge(cwd: string): Promise<Cached<RepoAutoMerge>>;
/** The cached form of {@link ghPrsForBranch}, shared through the same read-through cache (#1028). */
export declare function cachedPrsForBranch(cwd: string, branch: string): Promise<Cached<LinkedPr[]>>;
/** Forget a branch's PR history, after an action that changes it (opening one). */
export declare function forgetBranchPrs(cwd: string, branch: string): void;
/**
* The PR that belongs to an agent, out of every PR its branch name has had (#1251/#1255).
*
* An OPEN PR always counts: GitHub allows one open PR per head branch, so whatever is open on the
* run's branch is where its pushed commits land. A closed one counts only when it was created
* after the agent started (`since`, the agent's `startedAt`) — the oldest such entry, which is the one
* this agent's handoff opened. Anything older is a previous agent's PR wearing the same branch name,
* which is exactly what showed a merged two-day-old PR as a fresh session's own. Without `since`
* only an open PR is trusted.
*
* `order` exists for the one caller asking a different question (#1512). `'first'` answers
* identity — which PR did *this agent* open, so a later agent's must not be the answer. `'latest'`
* answers the handoff decision — which PR last saw the branch, so "did the session keep working
* past it" is readable off that PR's `headRefOid`; there the oldest entry would call work that a
* second PR already landed unlanded.
*/
export declare function pickAgentPr(prs: LinkedPr[], since?: string, order?: 'first' | 'latest'): LinkedPr | undefined;
/** An open PR on the interventions queue (#632). */
export interface OpenPr {
number: number;
title: string;
url: string;
/**
* Draft PRs are generally left off the queue: a draft is not asking for review.
*
* The exception is a draft the framework opened for itself (#1102), which {@link headRefName}
* is what tells apart.
*/
isDraft: boolean;
/** The branch the PR is from, so a session's own PR can be recognised as ours (#1102). */
headRefName?: string;
createdAt?: string;
}
/**
* A checkout's open PRs. Unlike the other reads here it *rejects* when gh could not answer — no
* remote, not authenticated, GitHub unreachable — instead of resolving `[]`.
*
* "No PRs are open" and "I could not look" are different answers, and its caller keeps a baseline
* of what it has already announced (#1623): taking the second for the first makes the next
* successful read announce every already-open PR as new. The caller decides what a failure costs;
* it cannot decide what it never hears about.
*/
export declare function ghPrList(cwd: string, gh?: GhRunner): Promise<OpenPr[]>;
/** Lists a checkout's open PRs; rejects when there is no remote / gh is unavailable. */
export type PrLister = (cwd: string) => Promise<OpenPr[]>;
//# sourceMappingURL=gh.d.ts.map