/** * The plan artifact, projected from durable session events (B2.5). * * A plan is an **artifact**, not a row: it has an identity that outlives the card * drawn for it, a revision history, and a state that comes from the session log * rather than from whatever the renderer last painted. Everything here is a pure * fold over events, so a replay produces exactly the artifacts a live session did — * the same ids, the same revisions, the same states. That is the whole point of the * module: the transcript, the dock and the review Surface read *this*, and none of * them keeps plan state of its own (AD-15). * * What is durable (measured; see `docs/plans/b2-5-plan-artifact-audit.md`): * * `plan/mode { active }` — plan mode entered / left (plan-mode service) * `todo/write { todos }` — a whole-list snapshot (the `todo_write` tool) * `exit_plan_mode` `tool/call` — `arguments.plan` is the plan body; the call is * the review's identity * `exit_plan_mode` `tool/result` — success = approved; the harness's own refusal * sentences = rejected / dismissed * `command/run` + `command/done` — a `/plan` switch that is queued for the next step * * What is **not** durable, and therefore is never claimed here: * * - the plan-review *question*: `dsh-plan-mode` asks through * `ctx.get('userQuestions').ask(...)`, which appends nothing. The tool call is the * review's only durable trace — which is also enough to recognise it on replay, * something B2.4 could not do from the question alone; * - any plan name or id: derived below, deterministically, from event order; * - "completed" and "abandoned": no event says either. They are folded from the last * snapshot (every step completed) and from the mode switch, and they carry * {@link PlanProvenance} `inferred` so a reader is never told them as fact. * * @module dsh-ssh-tui/plan-projection */ import type { PlanTodoItem } from './transcript-types.js'; /** How the artifact's state is known. `durable` = an event says it; `inferred` = a reading. */ export type PlanProvenance = 'durable' | 'inferred'; /** What the artifact is doing. `unknown` is a real answer, not a placeholder. */ export type PlanState = 'draft' | 'reviewing' | 'approved' | 'rejected' | 'executing' | 'completed' | 'abandoned' | 'unknown'; /** One whole-list snapshot, with the durable sequence that produced it. */ export interface PlanRevision { /** `plan@#` — deterministic, and unique within a session. */ id: string; /** `seq` of the `todo/write` that produced this revision. */ seq: number; steps: PlanTodoItem[]; } /** The review a plan was put through, and what came back. */ export interface PlanReview { /** The `exit_plan_mode` call id: the review's identity, durable and replay-stable. */ id: string; seq: number; /** The plan body as it was reviewed (`arguments.plan`). */ body: string; /** * `pending` is a *live* state: a request is out and nobody has answered yet. A * replayed log that ends here says `unknown` instead — the Host that asked is gone, * and B2.4 set that precedent for approvals. */ outcome: 'pending' | 'approved' | 'rejected' | 'dismissed' | 'unknown'; /** The reader's own words, when they refused with feedback. */ feedback?: string; } /** One plan, folded from the events that describe it. */ export interface PlanArtifact { /** `plan@` — session-local, replay-stable. */ id: string; /** What opened it: plan mode, or a standing todo list. */ openedBy: 'plan-mode' | 'todos'; openSeq: number; state: PlanState; provenance: PlanProvenance; /** The plan body, when the session recorded one. */ body?: string; /** The latest revision's steps. */ steps: PlanTodoItem[]; revisions: PlanRevision[]; review?: PlanReview; /** Plan mode is on, as of the end of the fold. */ active: boolean; /** A `/plan` switch is queued for the next step (derived, not read from the Host). */ pending: boolean; /** `seq` of the event that closed the artifact, when one did. */ closedSeq?: number; } /** The slice of a session event this fold reads. */ export interface PlanEvent { type: string; seq?: number; /** * Whether this event arrived from the running Host (as opposed to a log read). * * Only a live `exit_plan_mode` can be `reviewing`: a resumed process never carries * on a review the Host that asked it did not finish, so a replayed unresolved call * is `unknown`. Recorded per event because one log holds both. */ live?: boolean; data?: { active?: unknown; todos?: unknown; name?: unknown; args?: unknown; commandId?: unknown; kind?: unknown; arguments?: unknown; callId?: unknown; message?: { source?: { callId?: unknown; }; content?: unknown; isError?: unknown; }; error?: { code?: unknown; message?: unknown; }; }; } /** * The harness's own refusal sentences for a plan review. * * Matched exactly, because they are the *only* durable evidence that a review was * refused rather than never asked: `keep planning` (with or without feedback) and the * dismissal that happens when the reader speaks instead of answering. */ export declare function reviewRefusal(text: string): { outcome: 'rejected' | 'dismissed'; feedback?: string; } | undefined; /** * Fold the durable events of one session into its plan artifacts. * * Deterministic by construction: ids are derived from event `seq` alone, and every * decision reads only events that precede it. The same log (live or replayed) folds * to the same list, which is what lets one artifact feed the transcript, the dock and * the review Surface without any of them keeping state. * * @param events - the session's events, in order. Unknown types are ignored. * @param options - `live` marks a fold whose last events come from a running Host, so * an unresolved review may be called `reviewing`; a replay leaves it `unknown`. * @returns every artifact the session accounts for, oldest first. */ export declare function foldPlanArtifacts(events: readonly PlanEvent[], options?: { live?: boolean; }): PlanArtifact[]; /** The artifact a workspace should dock: the newest one that is still live. */ export declare function livePlanArtifact(artifacts: readonly PlanArtifact[]): PlanArtifact | undefined; /** * Whether the artifact still describes work in progress. * * The same question `planIsLive` asked of a row, asked of the projection instead: * plan mode on, a switch queued, a step not finished, or a review awaiting its * answer. Everything else is history, and history belongs in the transcript. */ export declare function artifactIsLive(artifact: PlanArtifact): boolean; /** `3/7`-style progress over the latest revision. */ export declare function artifactProgress(artifact: PlanArtifact): { done: number; total: number; current?: PlanTodoItem; };