export type Step = | "planning" | "plan_review" | "phase_packaging" | "coding" | "code_review" | "committing" | "finishing" | "done" export type Role = "orchestrator" | "planner" | "reviewer" | "coder" export type RoleMode = "oneshot" | "interactive" export type PendingDelivery = "manual" | "interactive" export type Verdict = "APPROVED" | "CHANGES_REQUIRED" export type ReworkTarget = "code" | "phase_package" export type RequiredReworkTarget = "plan" | ReworkTarget /** Fields every pending commit carries regardless of backend. */ export interface PendingCommitCore { /** Transaction id; appears in the commit message as `Apnea-Transaction: `. */ id: string /** Phase whose completion this transaction commits. */ phase_index: number /** Full commit message including the `Apnea-Transaction:` trailer line. */ message: string /** Whether completion advances to finishing instead of the next phase. */ no_remaining_phases: boolean /** Repo-relative `.apnea/` path of the verify log for this phase. */ verify_log: string } /** Git anchor captured before any ref moves: the exact commit to create. */ export interface GitPendingCommit extends PendingCommitCore { backend: "git" /** Full ref (e.g. `refs/heads/apnea/slug`) the commit must land on. */ branch: string /** Expected HEAD when completion starts; also the created commit's parent. */ parent_commit: string /** Prepared tree id (staged with `.apnea` excluded). */ tree_id: string } /** jj anchor captured right after describing `@`. */ export interface JjPendingCommit extends PendingCommitCore { backend: "jj" /** Change id of the described working-copy change. */ change_id: string /** Fingerprint of the change's non-.apnea diff at preparation time. */ content_fingerprint: string } /** * Durable record of an in-flight commit. Written after VCS preparation and * before completion, so a crash anywhere afterwards can recognize and finish * the transaction exactly once. */ export type PendingCommit = GitPendingCommit | JjPendingCommit /** Internal decode marker for ambiguous version-1 planning state. */ export const LEGACY_PLAN_REWORK = Symbol("apnea.legacy-plan-rework") export const LEGACY_CODE_REWORK = Symbol("apnea.legacy-code-rework") export type VcsBackend = "jj" | "git" export interface Profile { cmd_oneshot?: string[] cmd_interactive?: string[] } export interface RoleBinding { profile: string } export interface ApneaConfig { profiles: Record roles: Record review_round_cap: number timeouts_ms: Record } export interface RunState { version: 2 /** Absent only on legacy runs, which retain their original paths. */ run_id?: string /** Append-only ownership, including replaced and completed workers. */ acquired_panes?: { pane_id: string; label: string }[] slug: string step: Step phase_index: number phase_count_hint: number | null /** Keys: plan_review | phase-NN/code_review | phase-NN/coding | finishing */ rounds: Record vcs: VcsBackend allow_dirty: boolean goal: string last_error: string | null /** Relative artifact path expected for the in-flight dispatch, if any */ pending_artifact: string | null /** Role for pending dispatch */ pending_role: Role | null /** Delivery boundary crossed for the pending dispatch, if known. */ pending_delivery: PendingDelivery | null /** Herdr pane id for the in-flight dispatch */ pending_pane_id: string | null /** Label of that pane (apnea:role:unique) */ pending_pane_label: string | null /** * Epoch ms when the in-flight dispatch was launched. Null when idle. * Persisted so a chunked `workflow_wait` measures elapsed time from the * dispatch, not from the start of the current process. */ pending_started_at: number | null /** * Epoch ms after which the in-flight dispatch is considered timed out. * Extensions granted by the recovery ladder move this forward and are * saved, so the budget cannot be silently reset by re-invoking wait. */ pending_deadline_ms: number | null /** * Epoch ms of the last idle-nudge sent to the role pane. Persisted so a * fresh process does not re-nudge a role it already nudged. */ pending_nudged_at: number | null /** * True once the final-nudge rung has granted its 180s grace. Persisted for * the same reason as `pending_extended`: as a per-call local the grace was * re-granted by every new `wait`, so a role behind a pane that cannot be * prompted extended its own deadline forever and never timed out. */ pending_final_grace: boolean /** True once the one-time deadline extension has been consumed. */ pending_extended: boolean /** * Last known live pane per role, keyed by role name. * Reuse requires its pane_id and effective profile fingerprint. * Labels are never scanned because they are ambiguous. */ role_panes: Partial< Record< Role, { pane_id: string; label: string; profile_fingerprint: string | null } > > /** Absolute path to package root (briefs) */ package_root: string /** Tree snapshot fingerprint before reviewer dispatch */ reviewer_tree_fingerprint: string | null /** Last known phase package path for verify/commit */ current_phase_package: string | null /** Last code-review path for commit gate */ current_code_review: string | null /** The exact dispatch that owns the next review round, if any. */ required_rework: RequiredReworkTarget | null /** * In-flight commit transaction, if any. Set after VCS preparation and * cleared only after completion + bookmark succeed; a crash leaves it * durable so the next `workflow_commit_phase` call resumes that * transaction instead of re-running gates and verification. */ pending_commit: PendingCommit | null /** Never serialized. An old planning state needs an explicit assertion. */ [LEGACY_PLAN_REWORK]?: true /** Never serialized. Ambiguous old coding state needs an explicit assertion. */ [LEGACY_CODE_REWORK]?: true } export interface FrontMatter { status?: string verdict?: string nits?: string rework?: string raw: string body: string } /** * All worker roles use interactive TUIs so you can watch them live in Herdr. * Oneshot (`claude -p`, `pi -p`) is intentionally not used for dispatch: * it dumps shell output and is not observable as a harness session. */ export const ROLE_MODE: Record = { orchestrator: "interactive", planner: "interactive", reviewer: "interactive", coder: "interactive", } export const DEFAULT_TIMEOUTS: Record = { planning: 1_500_000, plan_review: 900_000, phase_packaging: 900_000, coding: 2_700_000, code_review: 900_000, verify: 900_000, finishing: 900_000, default: 900_000, }