/** * Lifecycle decision table — the executable bird's-eye view of the subagent * state machine. * * The `LifecycleController` (the single transition authority) maps a CLOSED * vocabulary of sensor-reported events onto state transitions via `DECISIONS`: * a `Record` where each rule is a pure * `(ctx) => { target, effects[] }` function. Reading `DECISIONS` top to bottom * IS reading the lifecycle. * * Why keyed pure functions instead of a static map or a `switch`: * - `ParentAbort` branches on a runtime fact (sentinel fired → salvage as * `completed`, else `interrupted`) — a static map cannot express that and * would silently lie about real behavior. * - A bare `switch` is not enumerable (no doc dump, no test matrix) and only * gets exhaustiveness through an easy-to-omit `never` default. The Record * fails to compile if an event lacks a rule. * See AGENTS.md ("Lifecycle architecture: sensors → owner → effects") for the * full decision record. * * Rules DECIDE; they never execute. Effects are a closed, tiny set of * declarative data the controller interprets: the rule names * `RunTeardownSequence`, the controller knows how teardown works. The tracker's * `VALID_TRANSITIONS` DAG still validates every proposed `target` * (defense-in-depth) — a buggy rule can propose an illegal transition but * cannot corrupt state. */ import type { SubagentStatus } from "#src/types"; /** * Every fact a sensor can report about a subagent. Closed by design: each * event is exactly one row of the decision table, so the full set of things * that can happen to an agent is enumerable and auditable. */ export type LifecycleEvent = "SentinelComplete" | "LaunchFailed" | "SessionFileMissing" | "MaxPollTimeExceeded" | "Stale" | "GraceOverflow" | "ParentAbort" | "SteerCrash" | "WindowGone" | "ResumeRequested"; /** * The runtime facts a rule may branch on. Assembled by the sensor reporting * the event; rules stay pure functions of this context. */ export interface LifecycleContext { /** Whether the harness lifecycle-end sentinel has been observed set. */ sentinelFired: boolean; /** Result text extracted from the session file at decision time, if any. */ result?: string; /** Error message recorded on `failed` transitions. */ error?: string; /** Human-readable reason recorded on `interrupted` transitions. */ reason?: string; } /** * Declarative consequences a rule can request. The controller interprets * them; rules never touch the tracker, tmux, git, or the filesystem. * * Interpretation order (fixed by the controller): * - record effects (`SetResult`, `KeepResult`, `KeepPartial`, `SetError`) * run BEFORE the transition so observers fire with the record populated; * - IO effects (`SendStopCombo`, `RunTeardownSequence`, `ReattachMonitor`) * run AFTER the transition so tracker state is durable before any tmux/git * operation that might throw. * * Keep this set closed and small. If the interpreter ever feels heavier than * the code it interprets, collapse the rare effect back inline rather than * growing this into a general effect system. */ export type Effect = /** Record the given result text on the tracker record. */ { kind: "SetResult"; result: string; } /** Salvage: record the extractable result, or the no-result fallback. */ | { kind: "KeepResult"; } /** Salvage: record partial output if any exists; never substitute a fallback. */ | { kind: "KeepPartial"; } /** Record the given error text on the tracker record. */ | { kind: "SetError"; error: string; } /** Send the harness stop key combo to the agent's tmux window. */ | { kind: "SendStopCombo"; } /** Run the ordered worktree merge → teardown sequence (merge BEFORE delete). */ | { kind: "RunTeardownSequence"; } /** Restart the monitoring loop (resume back-edge). */ | { kind: "ReattachMonitor"; }; /** What a rule decides: the proposed target state plus requested effects. */ export interface Decision { target: SubagentStatus; effects: Effect[]; } /** A pure decision function — one per event, unit-testable with a fake ctx. */ export type Rule = (ctx: LifecycleContext) => Decision; /** * Fallback result recorded when the lifecycle-end sentinel fired but no * result text could be extracted from the session file. Truthful to the * parent agent: the subagent finished, but its session file yielded no * extractable text (e.g. empty/unreadable session file). */ export declare const SENTINEL_NO_RESULT_FALLBACK = "[tmux-pilot] Subagent ended (lifecycle-end sentinel fired), but no result text could be extracted from its session file; the session file may be empty or unreadable."; export declare const DECISIONS: Record; //# sourceMappingURL=lifecycle-decisions.d.ts.map