/** * Wire format for the two status documents an external monitor reads, plus * their atomic persistence. Knows nothing about boards, worktrees, or * sessions — collectors build these shapes, `statusJoin` combines them. */ import type { ResolvedConfig } from "./config.ts"; import type { PullRequestSummary } from "./pullRequests.ts"; import type { RunLifecycleState } from "./runState.ts"; import type { CanonicalStatus } from "./taskSource.ts"; import type { WorktreeDirtiness, WorktreeKind } from "./worktrees.ts"; /** * Contract version for both documents. Bump on any breaking shape change; * readers refuse a version they don't know rather than misreading fields. */ export declare const STATUS_SNAPSHOT_SCHEMA_VERSION = 1; /** * Optional fields are written by `JSON.stringify`, which omits a key whose * value is `undefined`. So every `field?:` below is genuinely ABSENT on disk * rather than present-and-null, and a reader must treat absence as "not set". */ type StatusSchemaVersion = typeof STATUS_SNAPSHOT_SCHEMA_VERSION; /** Two-state health verdict shared by the probe and the board attempt. */ type AvailabilityStatus = "ok" | "unavailable"; export type StatusSessionState = "live" | "exited" | "not-live" | "unknown"; export type StatusLifecycle = RunLifecycleState | "idle"; export interface StatusWorktree { repository: string; kind: WorktreeKind; dir: string; branch: string; git: WorktreeDirtiness; } export interface StatusTask { /** Lowercased source task id, matching `WorktreeEntry.task`. */ task: string; title?: string | undefined; url?: string | undefined; agent?: string | undefined; lifecycle: StatusLifecycle; /** Probe-reconciliation flags, e.g. "session dead". Never a duration. */ flags: string[]; /** * When the run was first recorded. Readers derive elapsed time themselves: * a duration stored in a cached document shows a frozen clock, which reads * as "the agent stopped working" when it did not. */ startedAt?: string | undefined; updatedAt?: string | undefined; resumeCount?: number | undefined; reason?: string | undefined; detail?: string | undefined; session: StatusSessionState; attachCommand?: string | undefined; hint?: string | undefined; worktrees: StatusWorktree[]; /** * The same ten most recent matching lines shown by `crew status `. */ recentLogLines: string[]; } export interface StatusLogCursor { /** File identity, so a rotated log never reuses lines from its predecessor. */ device: number; inode: number; /** Exclusive byte offset covered by `tasks[].recentLogLines`. */ offset: number; } interface StatusProbeState { status: AvailabilityStatus; error?: string | undefined; } /** * The local tier. Every field here comes from a local subprocess or a file * read, so a monitor can poll this document every few seconds. * * Placement rule for a new field: if producing it needs the network, it * belongs in `RemoteStatusPayload`. `--local-only` is a promise about this * whole document, and one network-backed field voids it. */ export interface LocalStatusDocument { schemaVersion: StatusSchemaVersion; capturedAt: string; /** Incremental progress through the append-only shared log. */ logCursor?: StatusLogCursor | undefined; /** From config, never the network, so capacity renders before any fetch. */ maximumInProgress: number; workspaceProbe: StatusProbeState; tasks: StatusTask[]; orphanedSessions: string[]; } export interface StatusBoardIssue { id: string; naturalId: string; title: string; url?: string | undefined; repository?: string | undefined; agent?: string | undefined; } export interface StatusSourceIssue extends StatusBoardIssue { status: CanonicalStatus; } /** * A queued issue. Only groundcrew-eligible todos reach a queue, and * eligibility means both fields resolved, so they are required here. */ export interface StatusQueueIssue extends StatusBoardIssue { repository: string; agent: string; } interface StatusBlocker { id: string; naturalId: string; status: CanonicalStatus; nativeStatus?: string | undefined; } export interface StatusBlockedIssue extends StatusQueueIssue { blockedBy: StatusBlocker[]; } /** * The remote tier. Placement rule for a new field: it belongs here when * producing it needs the network, because this tier polls slowly, near * `pollIntervalMilliseconds`. * * Board-derived facts only. Board-side classification is applied; the local * worktree subtraction deliberately is not. Precomputing that join here would * make the document assert something false as soon as a worktree appears, * because the local tier refreshes far more often than this one. */ export interface RemoteStatusPayload { capturedAt: string; /** Lowercased natural id to current source data. Ambiguous ids are omitted. */ sourceByTask: Record; /** Every in-progress issue. Its length is the used slot count. */ inProgress: StatusBoardIssue[]; queueReady: StatusQueueIssue[]; queueBlocked: StatusBlockedIssue[]; } export interface RemoteStatusDocument { schemaVersion: StatusSchemaVersion; /** The most recent attempt, successful or not. */ lastAttemptAt: string; lastAttemptStatus: AvailabilityStatus; lastAttemptError?: string | undefined; /** The last successful BOARD fetch. Undefined when none has ever succeeded. */ payload?: RemoteStatusPayload | undefined; /** * Keyed by absolute worktree directory. A task with two worktrees has two * branches, each with its own pull requests. * * Always from the current attempt, never carried forward: the lookups do not * depend on the board, so a board outage must not freeze or hide them. An * empty array means none were found OR the lookup failed; `gh` failures are * not distinguishable here. */ pullRequestsByWorktree: Record; } type BoardOutcome = { kind: "ok"; payload: RemoteStatusPayload; } | { kind: "error"; message: string; }; /** One remote pass: a board outcome plus pull requests, which succeed or fail apart. */ export interface RemoteFetchResult { board: BoardOutcome; pullRequestsByWorktree: Record; } export interface BuildRemoteDocumentInput { previous: RemoteStatusDocument | undefined; attemptAt: string; result: RemoteFetchResult; } /** * Merges an attempt into the previous document. A failure advances only the * attempt fields, so last-known-good queue data survives an outage while the * document still states plainly that the board is unreachable. Without that * split a reader cannot tell "nobody polled" from "the board is down", and * only the second is actionable. */ export declare function buildRemoteDocument(input: BuildRemoteDocumentInput): RemoteStatusDocument; type LoggingConfig = Pick; export declare function localSnapshotPath(config: LoggingConfig): string; export declare function remoteSnapshotPath(config: LoggingConfig): string; export interface WriteLocalSnapshotInput { config: LoggingConfig; document: LocalStatusDocument; } /** * Same monotonic rule as `writeRemoteSnapshot`, for the same reason: a slow * run must not republish stale session state over a fresher poll. Returns what * is on disk afterwards so callers print the file's true contents. */ export declare function writeLocalSnapshot(input: WriteLocalSnapshotInput): LocalStatusDocument; /** A missing or unreadable snapshot is an ordinary first-run state, not an error. */ export declare function readLocalSnapshot(config: LoggingConfig): LocalStatusDocument | undefined; export interface WriteRemoteSnapshotInput { config: LoggingConfig; attemptAt: string; result: RemoteFetchResult; } /** * Refuses to write a document whose attempt is strictly older than the one on * disk. Equal timestamps are accepted: same-millisecond attempts are equally * current, and rejecting them would drop a legitimate result. * * This narrows the race, it does not close it. The read and the rename are * separate syscalls, so two processes can still interleave such that the older * attempt lands last. Closing it would need a lock file, which is not worth it * for a cache whose next poll corrects any regression. * * Returns whatever is on disk afterwards, which is the caller's document * unless the guard rejected it. Callers print the return value rather than * their own document, so stdout can never disagree with the file. */ export declare function writeRemoteSnapshot(input: WriteRemoteSnapshotInput): RemoteStatusDocument; /** A missing or unreadable snapshot is an ordinary first-run state, not an error. */ export declare function readRemoteSnapshot(config: LoggingConfig): RemoteStatusDocument | undefined; export {}; //# sourceMappingURL=statusSnapshot.d.ts.map