@try-works/dsh-recursive-mode 0.4.6 → 0.4.8
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 +11 -4
- package/lib/client/contract.d.ts +10 -0
- package/lib/client/doc-viewer.d.ts +50 -0
- package/lib/client/spec-sheet-view.d.ts +237 -0
- package/lib/client/spec-sheet.d.ts +103 -0
- package/lib/client/use-live.d.ts +17 -1
- package/lib/client.js +988 -14
- package/lib/delegation.d.ts +35 -1
- package/lib/errors.d.ts +13 -0
- package/lib/index.js +164 -9
- package/lib/run-spec.d.ts +78 -0
- package/lib/run-start.d.ts +27 -0
- package/package.json +1 -1
- package/src/client/contract.ts +10 -0
- package/src/client/doc-viewer.tsx +131 -12
- package/src/client/slots.ts +12 -0
- package/src/client/spec-sheet-view.ts +325 -0
- package/src/client/spec-sheet.tsx +409 -0
- package/src/client/styles.ts +298 -1
- package/src/client/use-live.ts +23 -2
- package/src/delegation.ts +58 -4
- package/src/errors.ts +13 -0
- package/src/recursive_ask.tool.ts +28 -1
- package/src/run-spec.ts +148 -0
- package/src/run-start.ts +59 -0
- package/src/runtime.ts +78 -24
package/README.md
CHANGED
|
@@ -386,10 +386,17 @@ from `session/event`, with a cheap shape test first, because that event fires fo
|
|
|
386
386
|
observer **waits** — bounded, polling, with a timeout — and reports "no settlement yet" only after actually
|
|
387
387
|
waiting. The park remains as the fallback, so an unobserved round is never an approval.
|
|
388
388
|
|
|
389
|
-
**Action records are written for every delegation**, and they carry `Execution Mode` and `Status
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
389
|
+
**Action records are written for every delegation**, and they carry `Execution Mode` and a `Status` with
|
|
390
|
+
**three** values, not two: `accepted`, `failed`, and `parked (still running; no settlement yet)`. The third
|
|
391
|
+
state exists because a round whose child had not settled is **not** a failure — the delegation interface says
|
|
392
|
+
so, and `recursive_review` already reports it as `parked` — and a binary status forced it to read as one: a live
|
|
393
|
+
run's record said `Status: failed` with *"the child never reported, or never ran"*, the main agent concluded its
|
|
394
|
+
reviewer was dead and obtained the review another way, and the child replied eighteen minutes later. A `failed`
|
|
395
|
+
record carries **`Failure:`** with its cause; a parked record carries **`Parked:`** instead — stating only what
|
|
396
|
+
is known ("no settlement had landed when the wait ended … the child may still be working"), naming the
|
|
397
|
+
`childId`, and saying that resuming with that id is the next step. `operations/operations.jsonl` records the
|
|
398
|
+
same distinction (`parked` rather than `unaccepted`), because the operation log had the identical conflation.
|
|
399
|
+
That distinction cost three rounds of investigation before the third state existed.
|
|
393
400
|
|
|
394
401
|
---
|
|
395
402
|
|
package/lib/client/contract.d.ts
CHANGED
|
@@ -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;
|
package/lib/client/use-live.d.ts
CHANGED
|
@@ -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;
|