/** * Runstate protocol (`runstate:v1`) — the milestone comments issue-workflow dossiers * append to a GitHub issue after every phase. * * This module is the executable copy of the "Runstate Milestones" table in * `imboard-ai/git/full-cycle-issue@3.8.0`. Dossiers used to ask agents to reproduce a * markdown heredoc by hand; smaller models skipped it or pasted `$(date …)` literally. * Everything here is pure and dependency-free so it can be unit tested without touching * `gh`, the network, or the filesystem. */ /** Marker that opens every runstate comment. Readers filter on this exact prefix. */ export declare const RUNSTATE_MARKER = ""; /** Workflow phases, in execution order. */ export declare const PHASES: readonly ["gate", "setup", "plan", "implement", "review", "ship", "report"]; export type Phase = (typeof PHASES)[number]; /** Milestone statuses. */ export declare const STATUSES: readonly ["done", "partial", "blocked", "awaiting-merge"]; export type Status = (typeof STATUSES)[number]; /** * Keys whose values are paths and must therefore be absolute — the dossier rule is * "paths are absolute", and a relative worktree path makes a resume unresolvable from * a different working directory. */ export declare const PATH_KEYS: readonly ["worktree", "planning"]; /** * Per-phase specification, transcribed from full-cycle-issue@3.8.0. * * `statuses` is the closed set a phase may report. `required` maps each status to the * keys that must accompany it. `status=blocked` additionally requires `reason` for every * phase (see {@link BLOCKED_REQUIRED}) — the dossier rule is "If a phase aborts, post * `status=blocked` with `reason=` before stopping". */ export interface PhaseSpec { statuses: readonly Status[]; required: Partial>; } /** Keys required whenever a phase reports `status=blocked`. */ export declare const BLOCKED_REQUIRED: readonly ["reason"]; /** * Caps that keep a milestone postable. GitHub rejects an issue comment over 65536 * characters with an opaque 422, and `execFileSync` hits E2BIG well before that on some * platforms — both surface as "gh failed" with no hint that size was the cause. Checking * here turns those into a named key and a number. */ export declare const MAX_VALUE_LENGTH = 4000; export declare const MAX_BODY_LENGTH = 60000; export declare const PHASE_SPECS: Record; /** Consecutive `blocked` milestones on one phase that mean the run is looping. */ export declare const RESUME_LOOP_CAP = 3; /** Every value `next=` may legally carry: a phase to re-enter, or `done`. */ export declare const NEXT_VALUES: readonly string[]; /** GitHub issue numbers are positive integers; anything else is a caller mistake. */ export declare function isIssueNumber(value: string): boolean; /** True when `key` may carry spaces in its value. */ export declare function isAcKey(key: string): boolean; export declare function isPhase(value: string): value is Phase; export declare function isStatus(value: string): value is Status; /** * The phase that follows `phase`, for the milestone's `next=` line. * * - `blocked` ends the run, so `next=done`. * - the two non-terminal statuses keep the run inside the same phase: ship's * `awaiting-merge` is the FIRST of ship's two milestones (CI wait, then teardown), * and a `partial` review still has agents left to run — which is exactly how * gate-issue's resume table reads them back. * - otherwise the linear order gate → setup → plan → implement → review → ship → * report → done. */ export declare function defaultNext(phase: Phase, status: Status): Phase | 'done'; /** The keys `phase` must carry when reporting `status`. */ export declare function requiredKeys(phase: Phase, status: Status): readonly string[]; export interface MilestoneInput { phase: string; status: string; run: string; /** Phase-specific `key=value` pairs, in the order they should be emitted. */ keys?: Array<[string, string]>; /** Override the computed `next=` value. */ next?: string; /** Override the timestamp (tests); defaults to now. */ at?: string; } /** * Validate a milestone against the protocol. * * @returns one actionable line per problem; an empty array means valid. */ export declare function validateMilestone(input: MilestoneInput): string[]; /** Current time as `YYYY-MM-DDTHH:MM:SSZ` (the dossier template's `date -u` format). */ export declare function nowStamp(date?: Date): string; /** * Build the milestone comment body. Does not validate — call * {@link validateMilestone} first. */ export declare function buildMilestone(input: MilestoneInput): string; /** * Split one `key=value` token at its FIRST `=`, so a value may itself contain `=`. * Returns null when nothing precedes the separator. * * The single definition of the pair grammar: {@link parseMilestone} reads it back off an * issue comment and the `--kv` flag parser reads it off argv, and the two must agree. */ export declare function splitPair(token: string): [string, string] | null; export interface ParsedMilestone { phase: string; status: string; run: string; at: string; next: string; /** Every `key=value` line, including the ones from the header line. */ keys: Record; } /** * Parse a runstate comment body. Returns `null` when `body` is not a runstate comment. * * Tolerant by design: a milestone posted by an older dossier (or a hand-written one) * should still be readable by `runstate last` even if it would not pass validation. */ export declare function parseMilestone(body: string): ParsedMilestone | null; /** Extract every runstate milestone from a list of comment bodies, oldest first. */ export declare function parseMilestones(bodies: string[]): ParsedMilestone[]; /** Mint a fresh run id: `r--<4 hex>`. */ export declare function mintRunId(issue: number | string): string; /** * The world-facing checks the resume table needs. Injected so {@link computeResume} * stays pure and testable — the command layer supplies the `git`/`gh`/`fs` versions. */ export interface ResumeProbe { /** `git ls-remote --exit-code origin ` succeeds. */ branchOnRemote(branch: string): boolean; /** The worktree directory exists. */ dirExists(path: string): boolean; /** The planning file exists. */ fileExists(path: string): boolean; /** `git -C rev-parse --short HEAD`, or null if it fails. */ headOf(worktree: string): string | null; /** `gh pr view --json state,mergedAt,mergeable`, or null if it fails. */ prState(pr: string): { state: string; mergedAt: string | null; mergeable: string; } | null; /** The issue's state is CLOSED. */ issueClosed(): boolean; } export interface ResumeResult { /** Phase to resume from: a phase name, `ship-wait`, `ship-teardown`, `done`, or `none`. */ resume_from: string; /** The run id to reuse, or null on a fresh run. */ run_id: string | null; /** Checks that passed, for the gate milestone's `verified=` key. */ verified: string[]; /** * Merged `key=value` state for the run: every milestone's keys, later milestones * winning. Merged rather than last-only because a resume at `plan` still needs * `branch`/`worktree` from the `setup` milestone. */ resume_context: Record; /** The last milestone, or null on a fresh run. */ last: ParsedMilestone | null; /** Set when the run must hard-block instead of resuming (currently `resume-loop`). */ hard_block?: string; /** Human-readable note, e.g. "already complete". */ note?: string; } /** * True when the last {@link RESUME_LOOP_CAP} milestones are all `blocked` on the same * phase — the run is retrying the same wall and needs a human, not another resume. */ export declare function hitLoopCap(milestones: ParsedMilestone[]): boolean; /** * Resolve where a run should resume from, implementing the resume verification table in * `imboard-ai/git/gate-issue`. Never trusts the milestone alone — every claim is checked * against reality through `probe`, and {@link PHASE_RESUMERS} holds the per-phase rules. */ export declare function computeResume(milestones: ParsedMilestone[], probe: ResumeProbe): ResumeResult; //# sourceMappingURL=runstate.d.ts.map