@try-works/dsh-recursive-mode 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,7 +179,7 @@ control plane on disk is the only state it trusts across restarts.
179
179
  | `recursive_audit_team` | Fan a phase out across roles (audit) |
180
180
  | `recursive_review` | **Independent review** of the phase artifact, with a repair path |
181
181
  | `recursive_delegate` | **Delegate the work of a phase** to a durable child; it produces, you judge |
182
- | `recursive_ask` | Ask the workspace a question, with the control plane as context |
182
+ | `recursive_ask` | Ask a **human** gate as a structured decision: `tdd-mode`, `qa-signoff`, `gate-block`, or `run-start` — the phase-0 approval that decides whether the run goal exists |
183
183
  | `recursive_preview` | Preview what a tool would do, without doing it |
184
184
 
185
185
  ### 4.2 The command surface
@@ -405,9 +405,20 @@ prove. Three facts shape the picture:
405
405
  the audit contract: `01-as-is`, `01.5-root-cause`, `02-to-be-plan`, `03-implementation-summary`,
406
406
  `03.5-code-review`, `04-test-summary`, `06-decisions-update`, `07-state-update`, `08-memory-impact`.
407
407
  `00-requirements`, `00-worktree` and `05-manual-qa` are not in it.
408
- 3. **`recursive_ask` is not a subagent tool.** It carries the workflow's three **human** gates —
409
- `ASK_GATE_IDS = ['tdd-mode', 'qa-signoff', 'gate-block']` — as structured decisions rather than prose, so the
410
- answer is validated and citeable.
408
+ 3. **`recursive_ask` is not a subagent tool.** It carries the workflow's **four human gates** —
409
+ `tdd-mode`, `qa-signoff` and `gate-block` (the list in `ASK_GATE_IDS`), plus **`run-start`**, the
410
+ phase-0 approval that decides whether a run's goal exists at all — as structured decisions rather
411
+ than prose, so the answer is validated and citeable.
412
+
413
+ `run-start` is unlike the other three: it is the only gate that asks the harness's **blocking
414
+ human channel**, and the only one that **arms a goal**. Creating a spec therefore creates no goal;
415
+ nothing runs unattended until a person approves it. When the mounted channel cannot deliver the
416
+ question, the refusal **names the channel's own cause** (`RM5503`) instead of asserting one — and a
417
+ composition that cannot render a card at all can still record the decision explicitly by passing
418
+ `answer` with `relay: true`, which is reported as relayed rather than as a person's selection. A
419
+ person's own answer always wins, and a question that was cancelled, aborted or timed out is **never**
420
+ relayable. A channel that *did* reach a person whose answer was not one of the offered labels is
421
+ `RM5504`, which is a different event from `RM5503`.
411
422
 
412
423
  ```mermaid
413
424
  flowchart TB
@@ -55,6 +55,16 @@ export interface ClientSlots {
55
55
  /** A registered slot's options. */
56
56
  export interface SlotOptions {
57
57
  name: string;
58
+ /**
59
+ * The dispatch key of a KEYED seat (`tool.call.toolview` is one), required there and ignored elsewhere.
60
+ *
61
+ * ⚠ THIS FIELD WAS MISSING AND ITS ABSENCE WAS NOT COSMETIC: `slots.ts` registers the run-start spec sheet
62
+ * on `tool.call.toolview` under the tool's wire name, and the harness's own options type makes `key`
63
+ * mandatory for a keyed slot (`KindOptions` in `packages/client/ui-slots`, where a keyed registration with
64
+ * no key THROWS `keyed slot "<name>" requires options.key`). Without the field here the face did not match
65
+ * the API it describes, so `tsc --noEmit` rejected the registration and the sheet was unreachable code.
66
+ */
67
+ key?: string;
58
68
  id?: string;
59
69
  order?: number;
60
70
  label?: string;
@@ -6,6 +6,27 @@ export interface DocLine {
6
6
  text: string;
7
7
  /** table: data rows (header separator row dropped); first row is the header. */
8
8
  cells?: string[][];
9
+ /**
10
+ * li: the item WAS a `- [ ]` / `- [x]` task box, and whether it was ticked.
11
+ *
12
+ * ⚠ THIS IS A FIELD ON `li`, NOT A NEW KIND, ON PURPOSE. A task box is a list item — it keeps the bullet
13
+ * layout, the line index and the search behaviour every existing `li` has — and the ONE thing the reader
14
+ * must be able to see that a plain string cannot carry is the BOX ITSELF. The scaffolded Phase 0 template
15
+ * ships seven unticked boxes, so "is this still the template?" is partly a question about these marks.
16
+ *
17
+ * ⚠ AND THE BOX IS NOT LEFT IN `text`. `text` is the item's CONTENT, exactly the shape the base parser
18
+ * already produces for a plain bullet, so `search`/`copy` and every other consumer of a line's text see the
19
+ * words rather than the syntax. The tick survives as this field, and the renderer draws the mark back.
20
+ */
21
+ checked?: boolean;
22
+ /**
23
+ * plain: this line is a `Coverage:` / `Approval:` GATE reading, and how it reads.
24
+ *
25
+ * Same reasoning as `checked`: a gate line is a line of text, so it stays `plain` and gains the one fact
26
+ * the renderer cannot re-derive — whether it currently says PASS or FAIL, which is exactly what a person
27
+ * must be able to see before approving the document that contains it.
28
+ */
29
+ gate?: 'pass' | 'fail';
9
30
  }
10
31
  /** Inline segment parsed from line text (bold / code span / link). */
11
32
  export interface InlineSegment {
@@ -17,6 +38,12 @@ export interface InlineSegment {
17
38
  * Markdown -> line tokens. Base is parsePlan (blank/h1-h4/li/plain, MIT),
18
39
  * extended for fenced code blocks (one code line per block) and pipe tables
19
40
  * (one table line per block, header separator row dropped).
41
+ *
42
+ * AND EXTENDED FOR WHAT THE RUN ARTIFACTS ACTUALLY CONTAIN — the task boxes and gate readings the
43
+ * scaffolded `00-requirements.md` ships (`- [ ] …`, `Coverage: FAIL`, `Approval: FAIL`), because a preview
44
+ * that renders an unticked box and a FAIL gate as generic body text hides the two marks a person who is
45
+ * being asked to approve the document most needs to see. Both are additive FIELDS on the existing `li` and
46
+ * `plain` kinds, so no line is retyped, no character is dropped, and every existing caller keeps working.
20
47
  */
21
48
  export declare function parseDoc(plan: string): DocLine[];
22
49
  /**
@@ -31,6 +58,29 @@ export interface DocViewerProps {
31
58
  theme: BoardTheme;
32
59
  onClose: () => void;
33
60
  }
61
+ /**
62
+ * Render one parsed line as a React element. Headings/bullets get parsePlan
63
+ * sizing; code/table get block layout; inline markup applies to plain-ish text.
64
+ *
65
+ * ⚠ ONE RENDERER, TWO READERS. `DocViewer` and the run-start spec sheet's preview both come through here,
66
+ * so a mark that means "unticked box" or "gate reads FAIL" cannot mean one thing in the phase-doc viewer and
67
+ * another in the document a person is approving. Exported for that reason alone.
68
+ */
69
+ export declare function lineElement(line: DocLine, i: number, isCurrent?: boolean): ReactNode;
70
+ /**
71
+ * The parsed lines as elements — the preview built from `parseDoc` + `lineElement`, with no shell of its own.
72
+ *
73
+ * ⚠ THIS IS WHAT MAKES A SECOND RENDERER UNNECESSARY. Any surface that wants to show a run artifact as a
74
+ * PREVIEW (the phase-doc viewer's body, the run-start spec sheet's document body) renders these nodes inside
75
+ * whatever frame it owns, so the markdown is parsed and drawn exactly once in the plugin. `keyBase` namespaces
76
+ * the React keys when several of these are on screen at once; `current` is the vim cursor line, which the
77
+ * spec sheet never sets.
78
+ */
79
+ export declare function PreviewLines({ lines, keyBase, current }: {
80
+ lines: DocLine[];
81
+ keyBase?: string;
82
+ current?: number;
83
+ }): ReactNode;
34
84
  /**
35
85
  * The per-phase doc viewer. Fetches the route on mount / fileName change.
36
86
  * Vim nav + / search + n/N + Esc; y copies the doc. Esc closes search first,
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Pure display model for the RUN-START SPEC SHEET.
3
+ *
4
+ * WHY THIS MODULE EXISTS. `src/client/spec-sheet.tsx` renders the Phase 0 document beside the run-start
5
+ * question so a person can read what they are approving. Everything that DECIDES what the sheet says is
6
+ * here instead: reading the tool call's own arguments, classifying the document, and choosing the fetch
7
+ * state. It is pure (no React, no fs, no session window), which is what lets the spec drive every branch —
8
+ * a stub, a filled document, a missing file, a failed route, a call with no runId — without a browser.
9
+ *
10
+ * ⚠ THE VERDICT COMES FROM THE TEXT, NEVER FROM THE FETCH. `loading`, `absent`, `error` and `loaded` are
11
+ * four different facts, and a single "not loaded" would collapse them — which is how a client comes to
12
+ * render an empty box for a failure and a failure for a document nobody has written yet.
13
+ *
14
+ * ⚠ AND IT IS NODE-FREE. This file is reached by the BROWSER bundle. Reading the artifact belongs to the
15
+ * server half (`run-start.ts`, `recursive_ask.tool.ts`); this half only ever DISPLAYS what the read-only
16
+ * `/doc` route returned.
17
+ */
18
+ import { type ArtifactMarkerHit, type ArtifactVerdict } from '../run-spec.ts';
19
+ /**
20
+ * The wire name of the tool whose call row the sheet replaces.
21
+ *
22
+ * `tool.call.toolview` is a KEYED seat dispatched by this exact string, so a typo renders nothing at all
23
+ * rather than rendering wrongly — the failure mode the harness documents for a keyed seat.
24
+ */
25
+ export declare const RUN_START_TOOL_NAME = "recursive_ask";
26
+ /**
27
+ * The Phase 0 artifact the run-start decision is recorded in.
28
+ *
29
+ * ⚠ DUPLICATED DELIBERATELY, AND PINNED BY A TEST. `run-start.ts` holds the same string, but importing it
30
+ * here would pull `node:fs` / `node:crypto` into the browser bundle through `status.ts` and take the page
31
+ * down for a constant. `tests/spec-sheet.spec.ts` asserts this value EQUALS `RUN_START_ARTIFACT`, so the copy
32
+ * cannot drift; the client only ever reads through it and never writes.
33
+ */
34
+ export declare const RUN_START_SPEC_FILE = "00-requirements.md";
35
+ /** The gate id whose question this sheet accompanies. */
36
+ export declare const RUN_START_GATE_ID = "run-start";
37
+ /** The tool call's own arguments, as far as the client reads them. */
38
+ export interface RecursiveAskArgs {
39
+ /** The gate id the call asked for; null when the call did not name one. */
40
+ gate: string | null;
41
+ /** The run whose spec the call is about; null when absent or blank. */
42
+ runId: string | null;
43
+ /** The artifact the call named, if any (the run-start gate never lets a caller redirect it). */
44
+ artifact: string | null;
45
+ /** The approval label the call carried, when it carried one. */
46
+ answer: string | null;
47
+ }
48
+ /**
49
+ * Read the arguments out of the raw JSON the model dispatched.
50
+ *
51
+ * A malformed or empty payload yields all-nulls rather than throwing: a truncated mid-stream argument string
52
+ * is a normal sight on this seat (the harness exposes preparing calls with no arguments at all), and a view
53
+ * that threw on one would take the transcript down with it.
54
+ */
55
+ export declare function parseRecursiveAskArgs(argsRaw: string | null): RecursiveAskArgs;
56
+ /**
57
+ * The parts of the owner's call block this sheet reads.
58
+ *
59
+ * Structurally typed rather than imported: the toolview owner passes one of three phase shapes
60
+ * (`preparing` carries no arguments, `start` carries `argsRaw`, `result` carries the paired call — which is
61
+ * NULL when window truncation left the call outside the loaded window).
62
+ */
63
+ export type SpecSheetBlock = {
64
+ readonly phase: 'preparing';
65
+ } | {
66
+ readonly phase: 'start';
67
+ readonly argsRaw: string;
68
+ } | {
69
+ readonly phase: 'result';
70
+ readonly call?: {
71
+ readonly name: string;
72
+ readonly argsRaw: string;
73
+ } | null;
74
+ } | null;
75
+ /** What the sheet could read from the call: its arguments, or WHY it could not. */
76
+ export type CallRead =
77
+ /** The call's arguments were dispatched and parsed. */
78
+ {
79
+ readonly ok: true;
80
+ readonly args: RecursiveAskArgs;
81
+ }
82
+ /** No block was supplied at all. */
83
+ | {
84
+ readonly ok: false;
85
+ readonly reason: 'missing';
86
+ }
87
+ /** The call is still PREPARING: the owner dispatches no arguments until the call starts. */
88
+ | {
89
+ readonly ok: false;
90
+ readonly reason: 'preparing';
91
+ }
92
+ /** The call settled, but the loaded window left its call head outside, so no arguments are available. */
93
+ | {
94
+ readonly ok: false;
95
+ readonly reason: 'truncated';
96
+ };
97
+ /**
98
+ * Read the call's arguments out of whichever phase the owner supplied.
99
+ *
100
+ * ⚠ THE THREE FAILURES ARE KEPT APART. "No block", "the arguments have not been dispatched yet" and "the
101
+ * window truncated the call" are three different facts, and a client that collapsed them into one empty
102
+ * argument set would tell a person the call named no run — a claim about the CALLER, made on evidence about
103
+ * the CLIENT.
104
+ */
105
+ export declare function readCall(block: SpecSheetBlock | undefined): CallRead;
106
+ /** Is this the run-start gate? Everything else keeps the generic tool row. */
107
+ export declare function isRunStartCall(args: RecursiveAskArgs): boolean;
108
+ /** Whether this call is one the sheet claims at all: the run-start gate, with a run named. */
109
+ export declare function isSpecSheetCall(read: CallRead): boolean;
110
+ /** Where the fetch of the document stands. */
111
+ export type SpecFetchState = 'loading' | 'loaded' | 'absent' | 'error';
112
+ /** The fetch outcome, as the component holds it. */
113
+ export interface SpecFetch {
114
+ state: SpecFetchState;
115
+ text?: string | null;
116
+ error?: string | null;
117
+ }
118
+ /** Everything the sheet renders from — pure data, so the spec can assert it without a renderer. */
119
+ export interface SpecSheetModel {
120
+ /** The run the call names, or null. */
121
+ runId: string | null;
122
+ /** The workspace root the live route resolved, or null while it has not answered. */
123
+ root: string | null;
124
+ /** The artifact this sheet shows. */
125
+ file: string;
126
+ /** The fetch's state. */
127
+ state: SpecFetchState;
128
+ /** Why an `error` state happened, in the route's own words. */
129
+ error: string | null;
130
+ /** The document's text VERBATIM, or null when there is nothing to show. */
131
+ text: string | null;
132
+ /** `run-spec.ts`'s verdict over that text, or null when there is no text. */
133
+ verdict: ArtifactVerdict | null;
134
+ /** The marker hits, in line order (empty when filled, and when there is no text). */
135
+ evidence: ArtifactMarkerHit[];
136
+ /** The call's own arguments, so the sheet can show which decision is being asked. */
137
+ args: RecursiveAskArgs;
138
+ }
139
+ /**
140
+ * Build the sheet's model from what the client actually has.
141
+ *
142
+ * ⚠ THE TEXT IS ONLY EVER TAKEN FROM A `loaded` FETCH. A stale body from a previous run is not shown against
143
+ * a new run id, because "this is the document" is the one claim this seat exists to make truthfully.
144
+ */
145
+ export declare function specSheetModel(input: {
146
+ args: RecursiveAskArgs;
147
+ root: string | null;
148
+ fetch: SpecFetch;
149
+ file?: string;
150
+ }): SpecSheetModel;
151
+ /** The document's path, as it should be PRINTED (never as it is fetched). */
152
+ export declare function specPath(runId: string | null, root: string | null, file: string): string;
153
+ /**
154
+ * How the sheet is showing the document.
155
+ *
156
+ * ⚠ TWO MODES, AND THE SECOND ONE IS NOT A CONVENIENCE. The rendered preview is what makes the document
157
+ * REVIEWABLE — reading `##` and `- [ ]` is not reading a spec — but a person is still being asked to approve
158
+ * the document, and a preview is an interpretation. `raw` is the escape hatch that lets them check the
159
+ * interpretation against the bytes, so "the preview is not paraphrasing or hiding anything" is a claim they
160
+ * can verify rather than one they have to take on trust.
161
+ */
162
+ export type SpecViewMode = 'preview' | 'raw';
163
+ /** The mode a freshly mounted sheet opens in: the rendered document. */
164
+ export declare const SPEC_DEFAULT_MODE: SpecViewMode;
165
+ /** The toggle's own label, ALWAYS naming the mode that is on screen — never the one a click would reach. */
166
+ export declare const SPEC_MODE_LABEL: Record<SpecViewMode, string>;
167
+ /** What the toggle does next, in the imperative, so the control reads as an action and its state reads apart. */
168
+ export declare const SPEC_TOGGLE_LABEL: Record<SpecViewMode, string>;
169
+ /** One line describing what is on screen, in both modes, for the announced status. */
170
+ export declare const SPEC_MODE_NOTE: Record<SpecViewMode, string>;
171
+ /** The other mode — what one press of the toggle reaches. */
172
+ export declare function otherMode(mode: SpecViewMode): SpecViewMode;
173
+ /** The toggle as a pure control description: its label, its pressed state and what it will do. */
174
+ export interface SpecViewToggle {
175
+ /** The mode currently on screen. */
176
+ mode: SpecViewMode;
177
+ /** The button's accessible name — the ACTION it performs. */
178
+ action: string;
179
+ /** Whether the control is pressed, i.e. whether the verbatim source is the thing on screen. */
180
+ pressed: boolean;
181
+ /** The line a status region announces, which names the mode CURRENTLY on screen. */
182
+ announce: string;
183
+ }
184
+ /**
185
+ * Describe the toggle for a mode.
186
+ *
187
+ * ⚠ `pressed` IS DERIVED FROM THE MODE, NEVER STORED BESIDE IT. A second boolean that could disagree with the
188
+ * mode is how a control comes to say "pressed" while the rendered document is on screen — and this is the one
189
+ * control whose whole job is to tell the reader which of the two things they are looking at.
190
+ */
191
+ export declare function specViewToggle(mode: SpecViewMode): SpecViewToggle;
192
+ /**
193
+ * What the body should be built from, so the two modes cannot be confused for one another.
194
+ *
195
+ * `preview` carries the TEXT to parse (parsing happens in the component, from the one parser this plugin
196
+ * has); `raw` carries the TEXT to print. Both are the same bytes: neither mode reads a different source.
197
+ */
198
+ export type SpecBodyContent = {
199
+ readonly mode: 'preview';
200
+ readonly text: string;
201
+ } | {
202
+ readonly mode: 'raw';
203
+ readonly text: string;
204
+ };
205
+ /**
206
+ * Decide the body content for a document text and a mode.
207
+ *
208
+ * ⚠ NULL TEXT IS NOT `''`. A `null` document is one that was never loaded, and it must not be printed as an
209
+ * empty document — "there is nothing here" and "the document is empty" are different claims, and this seat
210
+ * exists to make claims a reader can trust.
211
+ */
212
+ export declare function specBodyContent(text: string | null, mode: SpecViewMode): SpecBodyContent | null;
213
+ /** The verdict in the plainest words available — no hedging, and nothing that reads as encouragement. */
214
+ export declare const UNFILLED_NOTICE = "This document is still the UNFILLED TEMPLATE. There is no spec to approve yet.";
215
+ /** The same verdict as one phrase, for a status/data attribute. */
216
+ export declare const UNFILLED_SHORT = "unfilled template";
217
+ /** What the sheet says when the block it was given carries no dispatched call. */
218
+ export declare const NO_CALL_NOTICE = "This panel could not read the tool call it belongs to, so it cannot say which run spec the question is about.";
219
+ /** What the sheet says while the call is still preparing (its arguments are not dispatched yet). */
220
+ export declare const PREPARING_NOTICE = "This tool call has not been dispatched yet, so its arguments \u2014 and the run it names \u2014 are not available.";
221
+ /** What the sheet says when the loaded window truncated the call head away. */
222
+ export declare const TRUNCATED_NOTICE = "The loaded window left this call's own arguments outside it, so the run spec it names cannot be resolved here.";
223
+ /** The labels the run-start gate offers, echoed so the sheet can state what is being decided. */
224
+ export declare const RUN_START_APPROVE_LABEL = "Start run";
225
+ export declare const RUN_START_HOLD_LABEL = "Hold";
226
+ /** The gate's own question, quoted verbatim rather than paraphrased. */
227
+ export declare const RUN_START_QUESTION = "Approve phase 0 and start this run? Approving creates an armed goal the harness will keep driving.";
228
+ /** What each offered label means, quoted from the gate's own option descriptions. */
229
+ export declare const RUN_START_OPTION_MEANING: string;
230
+ /**
231
+ * The recorded decision, when this call carries one.
232
+ *
233
+ * ⚠ A `Hold` IS NOT AN APPROVAL and is not printed as one. The test is the same VALUE test the server makes
234
+ * (`run-start.ts::isRunStartApproval` — the approving label, not the mere presence of a decision line), so
235
+ * the sheet can never describe a hold as an approval.
236
+ */
237
+ export declare function decisionLine(answer: string | null): string | null;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * THE RUN-START SPEC SHEET — the document beside the question it decides.
3
+ *
4
+ * THE DEFECT THIS ANSWERS. The owner asked for a run spec, the plugin scaffolded one and raised the
5
+ * `run-start` gate — "start this run or hold?" — and NOBODY WAS SHOWN THE DOCUMENT:
6
+ * *"the card ui for accepting the spec appeared, but i was never shown the spec before that so how could i
7
+ * approve if i havent seen it"*. The gate was decidable before it was readable.
8
+ *
9
+ * ⚠ WHERE THIS RENDERS, AND WHY NOT IN `conversation.approval.detail`. That seat was the first candidate —
10
+ * its catalog summary reads "Optional detail for the Tool call correlated with an approval request" — and it
11
+ * is the WRONG one, for a reason that is structural rather than stylistic:
12
+ *
13
+ * 1. `conversation.composer` is a CHAIN slot: its entries' selectors run in order and the FIRST non-null
14
+ * match renders. `ui-approval` claims it with `select: pendingInteraction instanceof PendingApproval`
15
+ * and declares `conversation.approval.detail` as its child; `ui-user-questions` claims it with
16
+ * `select: pendingInteraction instanceof PendingQuestion` and declares
17
+ * `conversation.plan-review.actions` instead. They are two mutually exclusive cells of ONE chain.
18
+ * 2. The run-start gate is asked through the USER-QUESTIONS channel (`askRunStartDirectly` →
19
+ * `channel.ask(...)`, correlated by `wait: { callId: exec.callId }`), so while it is pending the
20
+ * pending interaction IS a `PendingQuestion` — the question composer owns the composer, and
21
+ * `conversation.approval.detail` is not mounted at all. A sheet registered there would render on the
22
+ * one occasion it is not needed and never on the one it is.
23
+ * 3. `tool.call.toolview` keyed by tool name is the seat that exists exactly while THIS tool call is on
24
+ * screen, is unclaimed for `recursive_ask` (the harness's own `ask_user_question` IS claimed, which is
25
+ * the parallel that matters), and hands the view its own `callId`, the session `cwd` and the frozen
26
+ * call block — which is how the `runId` is read out of the call's own arguments.
27
+ *
28
+ * WHAT IT SHOWS — AND THE DEFECT THAT CHANGED IT. This seat first showed the ACTUAL bytes of
29
+ * `.recursive/run/<runId>/00-requirements.md` as RAW TEXT, on the reasoning that verbatim is the honest
30
+ * thing to print. The honest answer to "is that a preview?" was NO, and the owner was being asked to APPROVE
31
+ * what they read: *reading `##` headings and pipe-table syntax is not reviewing a spec*. So the default is now
32
+ * a RENDERED PREVIEW — headings, lists, fenced code and pipe tables drawn as such — and the verbatim text is
33
+ * one press away behind "View source".
34
+ *
35
+ * ⚠ AND THE RAW MODE IS NOT DECORATION. A preview is an INTERPRETATION, and this seat asks a person to
36
+ * approve a document on the strength of it. The raw view exists so that "the preview is not paraphrasing or
37
+ * hiding anything" is a claim the reader can CHECK against the bytes rather than one they must take from the
38
+ * renderer. Its state is carried by `aria-pressed` and repeated in a `role="status"` line that names the mode
39
+ * on screen, so the mode is announced and not merely coloured.
40
+ *
41
+ * ⚠ ONE RENDERER, NOT TWO. The markdown is parsed and drawn by `doc-viewer.tsx` — `parseDoc` and its
42
+ * `PreviewLines` — the same code path the phase-doc viewer uses. A second markdown renderer in one plugin
43
+ * would be a second answer to "what does this document say", which is the defect this plugin exists to
44
+ * refuse. What `parseDoc` could not carry for a real `00-requirements.md` (the task boxes and the gate
45
+ * readings the template ships) was added THERE, for both readers, rather than forked here.
46
+ *
47
+ * WHAT IT STILL REFUSES TO DO. It is READ-ONLY (R9): the sheet has no approve/hold control, because a second
48
+ * control that looks like the question card's would be a second way to answer one question — the sheet may
49
+ * change HOW the document is shown, never WHETHER it is approved. And the honesty rule of
50
+ * `settings-view.ts` is unchanged: when the artifact is still the unfilled template the rendered view says so
51
+ * loudly and the unfilled marks stay visible AS unfilled (`☐ …` items and `Coverage: FAIL` gates are drawn as
52
+ * exactly that), so a pretty preview of an empty form can never read as an approvable spec.
53
+ *
54
+ * ACCESSIBILITY. The outer element is a `region` named by its own heading (`aria-labelledby`); the document
55
+ * body is a focusable (`tabIndex=0`) scrolling box with its own `aria-label` and `aria-readonly`, so the
56
+ * keyboard reaches the text and can scroll it without the decision controls leaving the viewport (the cap is
57
+ * CSS `max-height` with `overflow-y: auto`). The box is the SAME element in both modes, so toggling never
58
+ * drops focus. Escape is swallowed deliberately, because on a dialog-ish surface Escape is the key that
59
+ * dismisses and dismissing this sheet must never be read as a decision; and the one transition it has is none
60
+ * under `prefers-reduced-motion` (see the `.rec-spec` rules in `styles.ts`). No modal is used: nothing here
61
+ * needs a focus trap, and a trap would take the keyboard away from the decision controls.
62
+ */
63
+ import { type ReactNode } from 'react';
64
+ import type { SessionListStateLike, SnapshotSelectorHook, WorkspaceListStateLike } from './contract.ts';
65
+ import { type SpecSheetBlock, type SpecSheetModel } from './spec-sheet-view.ts';
66
+ export { RUN_START_TOOL_NAME, RUN_START_SPEC_FILE, parseRecursiveAskArgs, isRunStartCall, specPath, specSheetModel, decisionLine, UNFILLED_NOTICE, UNFILLED_SHORT, NO_CALL_NOTICE, PREPARING_NOTICE, TRUNCATED_NOTICE, SPEC_DEFAULT_MODE, SPEC_MODE_LABEL, SPEC_MODE_NOTE, SPEC_TOGGLE_LABEL, otherMode, specBodyContent, specViewToggle, } from './spec-sheet-view.ts';
67
+ export type { SpecSheetModel, SpecSheetBlock, SpecFetchState, RecursiveAskArgs, SpecViewMode, SpecViewToggle } from './spec-sheet-view.ts';
68
+ /** The session-standard hooks the seat is given, all optional so the spec can render the sheet alone. */
69
+ export interface SpecSheetProps {
70
+ /** The owner's call block; `null` when the owner supplied none (a window-truncated caller). */
71
+ block?: SpecSheetBlock;
72
+ /** The session workspace root the owner supplied (`ToolCallCommonProps.cwd`). */
73
+ cwd?: string;
74
+ useSessions?: SnapshotSelectorHook<SessionListStateLike>;
75
+ useWorkspaces?: SnapshotSelectorHook<WorkspaceListStateLike>;
76
+ }
77
+ /**
78
+ * Resolve the workspace root the `/doc` route will accept.
79
+ *
80
+ * ⚠ THE ROOT CANNOT BE THE SESSION CWD BY GUESSWORK. The route re-validates whatever root it is handed
81
+ * against the host's own workspace registry (`live-route.ts`: `resolveRoot(undefined, root)` must return the
82
+ * same canonical path), so the root used here is the one the live route ITSELF answered with; the workspace
83
+ * path is the hydration hint every other seat passes, and the owner-supplied `cwd` is the last resort.
84
+ */
85
+ export declare function resolveSpecRoot(snapshotRoot: string | null | undefined, workspacePath: string, cwd: string): string | null;
86
+ /**
87
+ * The seat component: renders the spec sheet for ONE `recursive_ask` tool call, and NOTHING for any other.
88
+ *
89
+ * Returning `null` is how an unclaimed key behaves, so a `tdd-mode` / `qa-signoff` / `gate-block` ask, or a
90
+ * preparing call whose arguments have not been dispatched yet, keeps the generic tool row it has today.
91
+ */
92
+ export declare function RunStartSpecSheet({ block, cwd, useSessions, useWorkspaces }: SpecSheetProps): ReactNode;
93
+ /**
94
+ * The body, from a MODEL — split out so the spec can drive every state (stub, filled, absent, error, a
95
+ * recorded Hold, a recorded approval) without a route, a session, or a fetch.
96
+ *
97
+ * The MODE is local state and starts at the rendered preview (`SPEC_DEFAULT_MODE`); everything that decides
98
+ * what the sheet SAYS still comes from the pure model, so a spec can assert the words and the marks without
99
+ * mounting a component.
100
+ */
101
+ export declare function RunStartSpecSheetBody({ model }: {
102
+ model: SpecSheetModel;
103
+ }): ReactNode;
@@ -1,9 +1,25 @@
1
1
  import { type LiveRecursiveState, type LiveScope } from './host-api.ts';
2
2
  /** Snapshot shape the board/strip consume (null = not loaded / no recursive root). */
3
3
  export type LiveProjectionSnapshot = LiveRecursiveState | null;
4
+ /** Options for {@link useLiveProjection}. */
5
+ export interface LiveProjectionOptions {
6
+ /**
7
+ * Skip the route entirely (default true).
8
+ *
9
+ * ⚠ THIS EXISTS FOR THE PER-CALL SEATS, NOT THE PANELS. A `tool.call.toolview` entry mounts once per Tool
10
+ * call in the transcript, so a seat that always subscribed would open one `/state` fetch and one SSE
11
+ * stream per call row. A row that has nothing to show passes `enabled: false` and asks nothing. The hook
12
+ * itself stays UNCONDITIONAL at every call site — a conditional hook is the run 13 crash, and the flag is
13
+ * how a caller keeps the call order fixed while making the request conditional.
14
+ */
15
+ enabled?: boolean;
16
+ }
4
17
  /**
5
18
  * Subscribe to the live route for one scope. Returns the current state. The SSE
6
19
  * feed pushes full frames on every fs change; a dropped stream (sleep/error)
7
20
  * triggers one refetch so the board never serves a stale fold.
21
+ *
22
+ * @param scope - sessionId PRIMARY + cwd fallback hint (host resolves the root).
23
+ * @param options - `enabled: false` clears the snapshot and never fetches.
8
24
  */
9
- export declare function useLiveProjection(scope: LiveScope): LiveProjectionSnapshot;
25
+ export declare function useLiveProjection(scope: LiveScope, options?: LiveProjectionOptions): LiveProjectionSnapshot;