@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 +15 -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/errors.d.ts +47 -1
- package/lib/index.js +483 -48
- package/lib/recursive_ask.tool.d.ts +93 -16
- package/lib/recursive_closeout.tool.d.ts +10 -0
- package/lib/recursive_init.tool.d.ts +8 -0
- package/lib/recursive_phase.tool.d.ts +8 -0
- package/lib/recursive_scratch.tool.d.ts +10 -0
- package/lib/recursive_worktree.tool.d.ts +16 -0
- package/lib/run-id.d.ts +62 -0
- 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/errors.ts +48 -2
- package/src/recursive_ask.tool.ts +217 -41
- package/src/recursive_closeout.tool.ts +53 -35
- package/src/recursive_init.tool.ts +20 -4
- package/src/recursive_phase.tool.ts +22 -2
- package/src/recursive_scratch.tool.ts +17 -1
- package/src/recursive_worktree.tool.ts +27 -2
- package/src/run-id.ts +100 -0
- package/src/run-spec.ts +148 -0
- package/src/run-start.ts +68 -3
package/src/client/slots.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { isRecursivePreset, currentWorkspacePath } from './contract.ts'
|
|
|
22
22
|
import { Board } from './board.tsx'
|
|
23
23
|
import { Inspector } from './inspector.tsx'
|
|
24
24
|
import { RecursiveSettings, RecursiveSettingsLive, type RecursiveSettingsSeatProps } from './settings.tsx'
|
|
25
|
+
import { RunStartSpecSheet, RUN_START_TOOL_NAME } from './spec-sheet.tsx'
|
|
25
26
|
import { useLiveProjection } from './use-live.ts'
|
|
26
27
|
import { boardState, useBoardState } from './open-state.ts'
|
|
27
28
|
import { injectBoardStyles } from './styles.ts'
|
|
@@ -138,6 +139,17 @@ export function registerSlots(ctx: ClientContext): () => void {
|
|
|
138
139
|
return createElement(RecursiveBoardOverlay, { useSessions, useWorkspaces: props?.useWorkspaces })
|
|
139
140
|
})))
|
|
140
141
|
|
|
142
|
+
// RUN-START SPEC SHEET (tool.call.toolview, keyed by tool name). The seat that exists exactly while a
|
|
143
|
+
// `recursive_ask` call is on screen — and the only one that can render BESIDE the question card, because
|
|
144
|
+
// the composer is a chain and the run-start gate's pending interaction is a question, not an approval (see
|
|
145
|
+
// the header of spec-sheet.tsx for the three structural reasons `conversation.approval.detail` cannot be
|
|
146
|
+
// it). The view returns null for every other gate, so `tdd-mode` / `qa-signoff` / `gate-block` calls keep
|
|
147
|
+
// the generic tool row they have today.
|
|
148
|
+
disposers.push(ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
149
|
+
name: 'tool.call.toolview',
|
|
150
|
+
key: RUN_START_TOOL_NAME,
|
|
151
|
+
}, RunStartSpecSheet)))
|
|
152
|
+
|
|
141
153
|
// Settings section (root scope, always present; no gate — configuration is always available).
|
|
142
154
|
// The seat receives the shell's `close` PLUS the root standard kit (useSessions/useWorkspaces,
|
|
143
155
|
// scoped-slots standardProps), so the panel can subscribe to the SAME live route the board
|
|
@@ -0,0 +1,325 @@
|
|
|
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 { classifyArtifact, type ArtifactMarkerHit, type ArtifactVerdict } from '../run-spec.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The wire name of the tool whose call row the sheet replaces.
|
|
22
|
+
*
|
|
23
|
+
* `tool.call.toolview` is a KEYED seat dispatched by this exact string, so a typo renders nothing at all
|
|
24
|
+
* rather than rendering wrongly — the failure mode the harness documents for a keyed seat.
|
|
25
|
+
*/
|
|
26
|
+
export const RUN_START_TOOL_NAME = 'recursive_ask'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The Phase 0 artifact the run-start decision is recorded in.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ DUPLICATED DELIBERATELY, AND PINNED BY A TEST. `run-start.ts` holds the same string, but importing it
|
|
32
|
+
* here would pull `node:fs` / `node:crypto` into the browser bundle through `status.ts` and take the page
|
|
33
|
+
* down for a constant. `tests/spec-sheet.spec.ts` asserts this value EQUALS `RUN_START_ARTIFACT`, so the copy
|
|
34
|
+
* cannot drift; the client only ever reads through it and never writes.
|
|
35
|
+
*/
|
|
36
|
+
export const RUN_START_SPEC_FILE = '00-requirements.md'
|
|
37
|
+
|
|
38
|
+
/** The gate id whose question this sheet accompanies. */
|
|
39
|
+
export const RUN_START_GATE_ID = 'run-start'
|
|
40
|
+
|
|
41
|
+
/** The tool call's own arguments, as far as the client reads them. */
|
|
42
|
+
export interface RecursiveAskArgs {
|
|
43
|
+
/** The gate id the call asked for; null when the call did not name one. */
|
|
44
|
+
gate: string | null
|
|
45
|
+
/** The run whose spec the call is about; null when absent or blank. */
|
|
46
|
+
runId: string | null
|
|
47
|
+
/** The artifact the call named, if any (the run-start gate never lets a caller redirect it). */
|
|
48
|
+
artifact: string | null
|
|
49
|
+
/** The approval label the call carried, when it carried one. */
|
|
50
|
+
answer: string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The all-null argument set: what a malformed, truncated, or absent payload yields. */
|
|
54
|
+
const NO_ARGS: RecursiveAskArgs = { gate: null, runId: null, artifact: null, answer: null }
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the arguments out of the raw JSON the model dispatched.
|
|
58
|
+
*
|
|
59
|
+
* A malformed or empty payload yields all-nulls rather than throwing: a truncated mid-stream argument string
|
|
60
|
+
* is a normal sight on this seat (the harness exposes preparing calls with no arguments at all), and a view
|
|
61
|
+
* that threw on one would take the transcript down with it.
|
|
62
|
+
*/
|
|
63
|
+
export function parseRecursiveAskArgs(argsRaw: string | null): RecursiveAskArgs {
|
|
64
|
+
if (argsRaw === null || argsRaw.trim() === '') return { ...NO_ARGS }
|
|
65
|
+
let parsed: unknown
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(argsRaw)
|
|
68
|
+
} catch {
|
|
69
|
+
return { ...NO_ARGS }
|
|
70
|
+
}
|
|
71
|
+
if (typeof parsed !== 'object' || parsed === null) return { ...NO_ARGS }
|
|
72
|
+
const record = parsed as Record<string, unknown>
|
|
73
|
+
const str = (value: unknown): string | null => (typeof value === 'string' && value.trim() !== '' ? value.trim() : null)
|
|
74
|
+
return { gate: str(record.gate), runId: str(record.runId), artifact: str(record.artifact), answer: str(record.answer) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The parts of the owner's call block this sheet reads.
|
|
79
|
+
*
|
|
80
|
+
* Structurally typed rather than imported: the toolview owner passes one of three phase shapes
|
|
81
|
+
* (`preparing` carries no arguments, `start` carries `argsRaw`, `result` carries the paired call — which is
|
|
82
|
+
* NULL when window truncation left the call outside the loaded window).
|
|
83
|
+
*/
|
|
84
|
+
export type SpecSheetBlock =
|
|
85
|
+
| { readonly phase: 'preparing' }
|
|
86
|
+
| { readonly phase: 'start'; readonly argsRaw: string }
|
|
87
|
+
| { readonly phase: 'result'; readonly call?: { readonly name: string; readonly argsRaw: string } | null }
|
|
88
|
+
| null
|
|
89
|
+
|
|
90
|
+
/** What the sheet could read from the call: its arguments, or WHY it could not. */
|
|
91
|
+
export type CallRead =
|
|
92
|
+
/** The call's arguments were dispatched and parsed. */
|
|
93
|
+
| { readonly ok: true; readonly args: RecursiveAskArgs }
|
|
94
|
+
/** No block was supplied at all. */
|
|
95
|
+
| { readonly ok: false; readonly reason: 'missing' }
|
|
96
|
+
/** The call is still PREPARING: the owner dispatches no arguments until the call starts. */
|
|
97
|
+
| { readonly ok: false; readonly reason: 'preparing' }
|
|
98
|
+
/** The call settled, but the loaded window left its call head outside, so no arguments are available. */
|
|
99
|
+
| { readonly ok: false; readonly reason: 'truncated' }
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read the call's arguments out of whichever phase the owner supplied.
|
|
103
|
+
*
|
|
104
|
+
* ⚠ THE THREE FAILURES ARE KEPT APART. "No block", "the arguments have not been dispatched yet" and "the
|
|
105
|
+
* window truncated the call" are three different facts, and a client that collapsed them into one empty
|
|
106
|
+
* argument set would tell a person the call named no run — a claim about the CALLER, made on evidence about
|
|
107
|
+
* the CLIENT.
|
|
108
|
+
*/
|
|
109
|
+
export function readCall(block: SpecSheetBlock | undefined): CallRead {
|
|
110
|
+
if (block === null || block === undefined) return { ok: false, reason: 'missing' }
|
|
111
|
+
if (block.phase === 'preparing') return { ok: false, reason: 'preparing' }
|
|
112
|
+
if (block.phase === 'start') return { ok: true, args: parseRecursiveAskArgs(block.argsRaw) }
|
|
113
|
+
const carried = block.call ?? null
|
|
114
|
+
return carried === null ? { ok: false, reason: 'truncated' } : { ok: true, args: parseRecursiveAskArgs(carried.argsRaw) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Is this the run-start gate? Everything else keeps the generic tool row. */
|
|
118
|
+
export function isRunStartCall(args: RecursiveAskArgs): boolean {
|
|
119
|
+
return args.gate === RUN_START_GATE_ID
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Whether this call is one the sheet claims at all: the run-start gate, with a run named. */
|
|
123
|
+
export function isSpecSheetCall(read: CallRead): boolean {
|
|
124
|
+
return read.ok && isRunStartCall(read.args) && read.args.runId !== null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Where the fetch of the document stands. */
|
|
128
|
+
export type SpecFetchState = 'loading' | 'loaded' | 'absent' | 'error'
|
|
129
|
+
|
|
130
|
+
/** The fetch outcome, as the component holds it. */
|
|
131
|
+
export interface SpecFetch {
|
|
132
|
+
state: SpecFetchState
|
|
133
|
+
text?: string | null
|
|
134
|
+
error?: string | null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Everything the sheet renders from — pure data, so the spec can assert it without a renderer. */
|
|
138
|
+
export interface SpecSheetModel {
|
|
139
|
+
/** The run the call names, or null. */
|
|
140
|
+
runId: string | null
|
|
141
|
+
/** The workspace root the live route resolved, or null while it has not answered. */
|
|
142
|
+
root: string | null
|
|
143
|
+
/** The artifact this sheet shows. */
|
|
144
|
+
file: string
|
|
145
|
+
/** The fetch's state. */
|
|
146
|
+
state: SpecFetchState
|
|
147
|
+
/** Why an `error` state happened, in the route's own words. */
|
|
148
|
+
error: string | null
|
|
149
|
+
/** The document's text VERBATIM, or null when there is nothing to show. */
|
|
150
|
+
text: string | null
|
|
151
|
+
/** `run-spec.ts`'s verdict over that text, or null when there is no text. */
|
|
152
|
+
verdict: ArtifactVerdict | null
|
|
153
|
+
/** The marker hits, in line order (empty when filled, and when there is no text). */
|
|
154
|
+
evidence: ArtifactMarkerHit[]
|
|
155
|
+
/** The call's own arguments, so the sheet can show which decision is being asked. */
|
|
156
|
+
args: RecursiveAskArgs
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build the sheet's model from what the client actually has.
|
|
161
|
+
*
|
|
162
|
+
* ⚠ THE TEXT IS ONLY EVER TAKEN FROM A `loaded` FETCH. A stale body from a previous run is not shown against
|
|
163
|
+
* a new run id, because "this is the document" is the one claim this seat exists to make truthfully.
|
|
164
|
+
*/
|
|
165
|
+
export function specSheetModel(input: {
|
|
166
|
+
args: RecursiveAskArgs
|
|
167
|
+
root: string | null
|
|
168
|
+
fetch: SpecFetch
|
|
169
|
+
file?: string
|
|
170
|
+
}): SpecSheetModel {
|
|
171
|
+
const file = input.file ?? RUN_START_SPEC_FILE
|
|
172
|
+
const text = input.fetch.state === 'loaded' ? input.fetch.text ?? null : null
|
|
173
|
+
const verdict = text === null ? null : classifyArtifact(text)
|
|
174
|
+
return {
|
|
175
|
+
runId: input.args.runId,
|
|
176
|
+
root: input.root,
|
|
177
|
+
file,
|
|
178
|
+
state: input.fetch.state,
|
|
179
|
+
error: input.fetch.error ?? null,
|
|
180
|
+
text,
|
|
181
|
+
verdict: verdict === null ? null : verdict.verdict,
|
|
182
|
+
evidence: verdict === null ? [] : verdict.hits,
|
|
183
|
+
args: input.args,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The document's path, as it should be PRINTED (never as it is fetched). */
|
|
188
|
+
export function specPath(runId: string | null, root: string | null, file: string): string {
|
|
189
|
+
const at = '.recursive/run/' + (runId ?? '<runId>') + '/' + file
|
|
190
|
+
return root === null || root.trim() === '' ? at : at + ' (in ' + root + ')'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ============================ preview / raw source ============================ */
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* How the sheet is showing the document.
|
|
197
|
+
*
|
|
198
|
+
* ⚠ TWO MODES, AND THE SECOND ONE IS NOT A CONVENIENCE. The rendered preview is what makes the document
|
|
199
|
+
* REVIEWABLE — reading `##` and `- [ ]` is not reading a spec — but a person is still being asked to approve
|
|
200
|
+
* the document, and a preview is an interpretation. `raw` is the escape hatch that lets them check the
|
|
201
|
+
* interpretation against the bytes, so "the preview is not paraphrasing or hiding anything" is a claim they
|
|
202
|
+
* can verify rather than one they have to take on trust.
|
|
203
|
+
*/
|
|
204
|
+
export type SpecViewMode = 'preview' | 'raw'
|
|
205
|
+
|
|
206
|
+
/** The mode a freshly mounted sheet opens in: the rendered document. */
|
|
207
|
+
export const SPEC_DEFAULT_MODE: SpecViewMode = 'preview'
|
|
208
|
+
|
|
209
|
+
/** The toggle's own label, ALWAYS naming the mode that is on screen — never the one a click would reach. */
|
|
210
|
+
export const SPEC_MODE_LABEL: Record<SpecViewMode, string> = {
|
|
211
|
+
preview: 'Showing: rendered preview',
|
|
212
|
+
raw: 'Showing: raw source',
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** What the toggle does next, in the imperative, so the control reads as an action and its state reads apart. */
|
|
216
|
+
export const SPEC_TOGGLE_LABEL: Record<SpecViewMode, string> = {
|
|
217
|
+
preview: 'View source',
|
|
218
|
+
raw: 'Back to preview',
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** One line describing what is on screen, in both modes, for the announced status. */
|
|
222
|
+
export const SPEC_MODE_NOTE: Record<SpecViewMode, string> = {
|
|
223
|
+
preview: 'The document is rendered here as a preview: headings, lists, code and tables are drawn as such.',
|
|
224
|
+
raw: 'The document is shown here VERBATIM — the exact bytes, with nothing rendered and nothing removed.',
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The other mode — what one press of the toggle reaches. */
|
|
228
|
+
export function otherMode(mode: SpecViewMode): SpecViewMode {
|
|
229
|
+
return mode === 'preview' ? 'raw' : 'preview'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The toggle as a pure control description: its label, its pressed state and what it will do. */
|
|
233
|
+
export interface SpecViewToggle {
|
|
234
|
+
/** The mode currently on screen. */
|
|
235
|
+
mode: SpecViewMode
|
|
236
|
+
/** The button's accessible name — the ACTION it performs. */
|
|
237
|
+
action: string
|
|
238
|
+
/** Whether the control is pressed, i.e. whether the verbatim source is the thing on screen. */
|
|
239
|
+
pressed: boolean
|
|
240
|
+
/** The line a status region announces, which names the mode CURRENTLY on screen. */
|
|
241
|
+
announce: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Describe the toggle for a mode.
|
|
246
|
+
*
|
|
247
|
+
* ⚠ `pressed` IS DERIVED FROM THE MODE, NEVER STORED BESIDE IT. A second boolean that could disagree with the
|
|
248
|
+
* mode is how a control comes to say "pressed" while the rendered document is on screen — and this is the one
|
|
249
|
+
* control whose whole job is to tell the reader which of the two things they are looking at.
|
|
250
|
+
*/
|
|
251
|
+
export function specViewToggle(mode: SpecViewMode): SpecViewToggle {
|
|
252
|
+
return {
|
|
253
|
+
mode,
|
|
254
|
+
action: SPEC_TOGGLE_LABEL[mode],
|
|
255
|
+
pressed: mode === 'raw',
|
|
256
|
+
announce: SPEC_MODE_LABEL[mode] + '. ' + SPEC_MODE_NOTE[mode],
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* What the body should be built from, so the two modes cannot be confused for one another.
|
|
262
|
+
*
|
|
263
|
+
* `preview` carries the TEXT to parse (parsing happens in the component, from the one parser this plugin
|
|
264
|
+
* has); `raw` carries the TEXT to print. Both are the same bytes: neither mode reads a different source.
|
|
265
|
+
*/
|
|
266
|
+
export type SpecBodyContent =
|
|
267
|
+
| { readonly mode: 'preview'; readonly text: string }
|
|
268
|
+
| { readonly mode: 'raw'; readonly text: string }
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Decide the body content for a document text and a mode.
|
|
272
|
+
*
|
|
273
|
+
* ⚠ NULL TEXT IS NOT `''`. A `null` document is one that was never loaded, and it must not be printed as an
|
|
274
|
+
* empty document — "there is nothing here" and "the document is empty" are different claims, and this seat
|
|
275
|
+
* exists to make claims a reader can trust.
|
|
276
|
+
*/
|
|
277
|
+
export function specBodyContent(text: string | null, mode: SpecViewMode): SpecBodyContent | null {
|
|
278
|
+
if (text === null) return null
|
|
279
|
+
return mode === 'raw' ? { mode: 'raw', text } : { mode: 'preview', text }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The verdict in the plainest words available — no hedging, and nothing that reads as encouragement. */
|
|
283
|
+
export const UNFILLED_NOTICE = 'This document is still the UNFILLED TEMPLATE. There is no spec to approve yet.'
|
|
284
|
+
|
|
285
|
+
/** The same verdict as one phrase, for a status/data attribute. */
|
|
286
|
+
export const UNFILLED_SHORT = 'unfilled template'
|
|
287
|
+
|
|
288
|
+
/** What the sheet says when the block it was given carries no dispatched call. */
|
|
289
|
+
export const NO_CALL_NOTICE =
|
|
290
|
+
'This panel could not read the tool call it belongs to, so it cannot say which run spec the question is about.'
|
|
291
|
+
|
|
292
|
+
/** What the sheet says while the call is still preparing (its arguments are not dispatched yet). */
|
|
293
|
+
export const PREPARING_NOTICE =
|
|
294
|
+
'This tool call has not been dispatched yet, so its arguments — and the run it names — are not available.'
|
|
295
|
+
|
|
296
|
+
/** What the sheet says when the loaded window truncated the call head away. */
|
|
297
|
+
export const TRUNCATED_NOTICE =
|
|
298
|
+
'The loaded window left this call\'s own arguments outside it, so the run spec it names cannot be resolved here.'
|
|
299
|
+
|
|
300
|
+
/** The labels the run-start gate offers, echoed so the sheet can state what is being decided. */
|
|
301
|
+
export const RUN_START_APPROVE_LABEL = 'Start run'
|
|
302
|
+
export const RUN_START_HOLD_LABEL = 'Hold'
|
|
303
|
+
|
|
304
|
+
/** The gate's own question, quoted verbatim rather than paraphrased. */
|
|
305
|
+
export const RUN_START_QUESTION =
|
|
306
|
+
'Approve phase 0 and start this run? Approving creates an armed goal the harness will keep driving.'
|
|
307
|
+
|
|
308
|
+
/** What each offered label means, quoted from the gate's own option descriptions. */
|
|
309
|
+
export const RUN_START_OPTION_MEANING =
|
|
310
|
+
'“' + RUN_START_APPROVE_LABEL + '” records the approval and arms the run goal; “' + RUN_START_HOLD_LABEL
|
|
311
|
+
+ '” leaves the spec inert: no run goal, no autonomous rounds.'
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The recorded decision, when this call carries one.
|
|
315
|
+
*
|
|
316
|
+
* ⚠ A `Hold` IS NOT AN APPROVAL and is not printed as one. The test is the same VALUE test the server makes
|
|
317
|
+
* (`run-start.ts::isRunStartApproval` — the approving label, not the mere presence of a decision line), so
|
|
318
|
+
* the sheet can never describe a hold as an approval.
|
|
319
|
+
*/
|
|
320
|
+
export function decisionLine(answer: string | null): string | null {
|
|
321
|
+
if (answer === null) return null
|
|
322
|
+
return answer.trim() === RUN_START_APPROVE_LABEL
|
|
323
|
+
? 'This call records: ' + answer + ' — the approving label, which arms the run goal.'
|
|
324
|
+
: 'This call records: ' + answer + ' — not the approving label, so no run goal is armed.'
|
|
325
|
+
}
|