bareloop 0.8.0 → 0.10.0

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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * The spine events a refusal produces — returned rather than emitted, because
3
+ * this module owns no spine (the runner shell does, exactly as with `runJob`).
4
+ *
5
+ * The `job-red` carries the structured `verb`/`lib`, which is the channel
6
+ * `classifyIncidents` already reads; the escalation carries the human's levers
7
+ * and is EXCLUDED from classification so the same demand is not counted twice.
8
+ * @param {Refusal} refusal
9
+ * @returns {{type: string, [k: string]: any}[]}
10
+ */
11
+ export function refusalEvents(refusal: Refusal): {
12
+ type: string;
13
+ [k: string]: any;
14
+ }[];
15
+ /**
16
+ * Read the picked verdict class and that class's own frozen answers. Pure: no
17
+ * model, no repository, no clock.
18
+ *
19
+ * Four outcomes, and the middle two are the ones that matter:
20
+ * - `ok` — a class v1 builds, every answer present, a repository named;
21
+ * - a CLASS REFUSAL — the user picked soft-green or hitl. Counted demand for a
22
+ * VERDICT CLASS, refused at ADMISSION, before that class's questions run
23
+ * (they are named but locked, and asking them would be an interview for a
24
+ * job nothing here can close);
25
+ * - a REPO REFUSAL — a green pick with nothing deterministic to measure
26
+ * against. Counted demand too;
27
+ * - plain `reds` — the interview is not finished, or the radio was not set.
28
+ * NOT demand: an incomplete form is not a user asking for a capability, and
29
+ * an unknown class is a typo, never a request (the same split `src/job.js`
30
+ * makes between a locked verdict and an invalid one).
31
+ *
32
+ * @param {{answers: Record<string|number, any>, verdictType?: string|null,
33
+ * repoPath?: string|null, questions?: Record<string|number, string>|null}} o
34
+ * `verdictType` and `repoPath` are STRUCTURED input from the UI, never parsed
35
+ * out of the prose of an answer: a class is picked off a radio and a path is
36
+ * read off the machine, or neither exists.
37
+ * @returns {{ok: boolean, answers: Record<string|number, string>,
38
+ * verdictType: string|null, repoPath: string|null, refusal: Refusal|null, reds: Red[]}}
39
+ */
40
+ export function runInterview({ answers, verdictType, repoPath, questions }: {
41
+ answers: Record<string | number, any>;
42
+ verdictType?: string | null;
43
+ repoPath?: string | null;
44
+ questions?: Record<string | number, string> | null;
45
+ }): {
46
+ ok: boolean;
47
+ answers: Record<string | number, string>;
48
+ verdictType: string | null;
49
+ repoPath: string | null;
50
+ refusal: Refusal | null;
51
+ reds: Red[];
52
+ };
53
+ /**
54
+ * The refusal for a user who asks for a LOCKED KIND by name — a judged score or
55
+ * a person's sign-off. Exposed because the tool-mode declaration schema makes a
56
+ * locked kind INEXPRESSIBLE (M3's header states the cost): the demand cannot
57
+ * arrive through the model, so it has to arrive through the interview layer, and
58
+ * this is that layer.
59
+ * @param {string} kind @returns {Refusal}
60
+ */
61
+ export function refuseLockedKind(kind: string): Refusal;
62
+ /**
63
+ * Interview answers in, a signable close DECLARATION out.
64
+ *
65
+ * The order is the design's own, and every cheap refusal happens before any
66
+ * token: interview → seed → scout → listing → the grounded authoring loop (M3).
67
+ *
68
+ * `scoutFn`, `listingFn` and `seedFn` are seams so a caller can supply a survey
69
+ * it already paid for (the prereg reuses one across arms), and so the whole
70
+ * pipeline is testable without a provider. All three are REAL by default.
71
+ *
72
+ * THE GENRE REFUSAL LIVES HERE NOW (PRD v1.57 §2). D13's interview slot asked
73
+ * the user to confirm a genre; the interview no longer asks, and the refusal
74
+ * moved to the COMPOSER — a job the catalogue cannot measure refuses at this
75
+ * layer, on the same `request-red` path, still counted as demand. Two mechanical
76
+ * signals reach it and neither is a reading of prose: a language the genre owns
77
+ * no data for (so there is no guard battery to attach), and a LOCKED KIND the
78
+ * model reached for (the catalogue's own counted-demand red, carried up). D13's
79
+ * one-genre-at-a-time law is untouched.
80
+ *
81
+ * @param {{answers: Record<string|number, any>, repoPath?: string|null, lang: string,
82
+ * verdictType?: string|null,
83
+ * questions?: Record<string|number, string>|null, generate?: Function, provider?: any,
84
+ * seedRef?: string|null, scout?: any, listing?: any, ceilingUsd?: number|null,
85
+ * seedFn?: Function, scoutFn?: Function, listingFn?: Function,
86
+ * authorFn?: Function, authorOpts?: object}} o
87
+ * @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
88
+ * closeDecl: any, seedRef: string|null, authoring: any, reds: Red[], stop: string|null,
89
+ * cost: any, interview: any}>}
90
+ */
91
+ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questions, generate, provider, seedRef, scout, listing, ceilingUsd, seedFn, scoutFn, listingFn, authorFn, authorOpts, }: {
92
+ answers: Record<string | number, any>;
93
+ repoPath?: string | null;
94
+ lang: string;
95
+ verdictType?: string | null;
96
+ questions?: Record<string | number, string> | null;
97
+ generate?: Function;
98
+ provider?: any;
99
+ seedRef?: string | null;
100
+ scout?: any;
101
+ listing?: any;
102
+ ceilingUsd?: number | null;
103
+ seedFn?: Function;
104
+ scoutFn?: Function;
105
+ listingFn?: Function;
106
+ authorFn?: Function;
107
+ authorOpts?: object;
108
+ }): Promise<{
109
+ ok: boolean;
110
+ refusal: Refusal | null;
111
+ verdictType: string | null;
112
+ closeDecl: any;
113
+ seedRef: string | null;
114
+ authoring: any;
115
+ reds: Red[];
116
+ stop: string | null;
117
+ cost: any;
118
+ interview: any;
119
+ }>;
120
+ /**
121
+ * Fold an authored close into the OPERATOR's own half of the spec.
122
+ *
123
+ * The two halves never mix: budgets, the fence, cadence, escalation and the
124
+ * provider are operator input and are not authored by anything here (the
125
+ * permanent hard line). What this adds is the close, the derived verdict class,
126
+ * and nothing else.
127
+ *
128
+ * The GOAL is passed through, not generated. F87 requires the goal to state
129
+ * everything the close judges, and §3 of the design record says that becomes
130
+ * structural once the goal RENDERS from the same answers — that rendering is UI
131
+ * work (PRD v1.51 §5, "lands with the close-authoring interview UI") and is
132
+ * deliberately not invented here.
133
+ *
134
+ * A draft that ALREADY carries the authored half is REFUSED, not merged and not
135
+ * overwritten. `src/job.js` states the rule for the close in its own words — two
136
+ * closes are two arbiters, and "picking one silently is how the signed artefact
137
+ * stops meaning what the signer read" — and the same holds one step earlier and
138
+ * for the class: this fold used to drop a draft's `close` on the floor and stamp
139
+ * its own `verdictType` over the operator's, so a person who typed either would
140
+ * read back a signed spec that never contained what they wrote. Refusing by name
141
+ * costs the legitimate path nothing (a draft is the operator's half BY
142
+ * DEFINITION, and nothing in the pipeline builds one carrying these) and it is
143
+ * the same direction every other gate here takes: a mismatch a human cannot see
144
+ * is worse than a stop a human can read.
145
+ * @param {any} specDraft the operator's half (everything except close/closeDecl/verdictType)
146
+ * @param {{closeDecl: any, verdictType: string}} authored
147
+ * @throws {Error} when the draft already carries close, closeDecl or verdictType
148
+ */
149
+ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
150
+ closeDecl: any;
151
+ verdictType: string;
152
+ }): any;
153
+ /**
154
+ * D9, in code: *nothing JUDGES the close; three mechanical gates plus a
155
+ * signature.* No LLM validates another LLM's close, here or anywhere.
156
+ *
157
+ * 1. THE DECLARATION VALIDATOR — the spec's own gate plus M2's, run GROUNDED
158
+ * against the real seed listing (the job validator defers that half).
159
+ * 2. THE CLOSE PRECHECK — every stage runs against the real patient. A stage
160
+ * that cannot run is `broken-close`, and it is a CASUALTY, never a red.
161
+ * 3. THE SEED-VERDICT READ — every stage, offered or not, run at the seed. Which
162
+ * are RED (the work) and which are GREEN (the guards) is shown to the user.
163
+ * A close with NO work stage red at seed has nothing to do and is refused
164
+ * decision-ready — gate-2 round 2's arm B died exactly there, on a mypy that
165
+ * scanned nothing and therefore read clean.
166
+ *
167
+ * Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
168
+ * job creation the tree IS the seed, so "does every stage run against the real
169
+ * patient" and "what does every stage say at the seed" are two readings of the
170
+ * same pass. `seedRead` runs EVERY stage rather than first-red-wins, which is
171
+ * D12's own named build item — a stage that never ran mints no baseline, and
172
+ * every shipped close opens with a guard that is red at its own seed.
173
+ *
174
+ * IT NEVER SIGNS. The approvals array and the human's word are unchanged; what
175
+ * comes back is the resolved spec, its hash, and the evidence to sign against.
176
+ *
177
+ * @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
178
+ * timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function}} o
179
+ * @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
180
+ * gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
181
+ */
182
+ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, }: {
183
+ spec: any;
184
+ workdir: string;
185
+ seedRef?: string | null;
186
+ shellCapUsd?: number;
187
+ timeoutMs?: number;
188
+ seedFn?: Function;
189
+ listingFn?: Function;
190
+ seedReadFn?: Function;
191
+ }): Promise<{
192
+ ok: boolean;
193
+ specHash: string | null;
194
+ seedRef: string | null;
195
+ gates: any;
196
+ work: any[];
197
+ guards: any[];
198
+ stops: any[];
199
+ reds: Red[];
200
+ refusal: Refusal | null;
201
+ }>;
202
+ /** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
203
+ /** @typedef {{kind: 'request-red'|'decision-ready', verb: string|null, path: string,
204
+ * detail: string, options: string[], red: Red|null}} Refusal */
205
+ /** the one genre v1 admits (D13). The interview no longer ASKS about it — the
206
+ * catalogue is one genre wide and the composer is where that refuses — but the
207
+ * signed `closeDecl` still names the genre it was authored under. */
208
+ export const GENRE: "TYPES";
209
+ /** The territory every refusal here lands against. bareloop's OWN catalogue —
210
+ * the genre menu, the verdict menu, the kind menu — never a bare-suite package.
211
+ * Stamped at the emit site (the typed-lib rule). */
212
+ export const REFUSAL_LIB: "bareloop";
213
+ /** the escalation category a refusal escalates under. It is DELIBERATELY in the
214
+ * ledger's executable excluded set: the demand is already counted once as a
215
+ * `request-red`, and counting the escalation too would inflate the very number
216
+ * the admission path exists to measure. */
217
+ export const REFUSAL_CATEGORY: "close-unauthorable";
218
+ /** The fields `assembleSpec` writes, and therefore the fields a DRAFT must not
219
+ * carry. Named as data so the refusal and the fold can never disagree about
220
+ * which half of the spec is which. */
221
+ export const AUTHORED_SPEC_FIELDS: readonly string[];
222
+ export type Red = {
223
+ code: string;
224
+ path: string;
225
+ detail: string;
226
+ [k: string]: any;
227
+ };
228
+ export type Refusal = {
229
+ kind: "request-red" | "decision-ready";
230
+ verb: string | null;
231
+ path: string;
232
+ detail: string;
233
+ options: string[];
234
+ red: Red | null;
235
+ };
236
+ import { QUESTION_SETS } from './authorflow.js';
237
+ import { questionsFor } from './authorflow.js';
238
+ import { requiredAnswersFor } from './authorflow.js';
239
+ import { VERDICT_CLASSES } from './authoring.js';
240
+ import { LOCKED_CLASSES } from './authoring.js';
241
+ import { LIVE_CLASSES } from './authoring.js';
242
+ export { QUESTION_SETS, questionsFor, requiredAnswersFor, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES };
@@ -0,0 +1,320 @@
1
+ /**
2
+ * The re-ask, when the survey came back MALFORMED. It names what failed
3
+ * MECHANICALLY — the parse error and its position — which is the gap genre that
4
+ * converts (F38/F39), and it says explicitly that the FINDINGS are not in
5
+ * question, only their form.
6
+ *
7
+ * There is NO JSON repair anywhere behind this and there never will be: a
8
+ * repairer decides what the model meant to say and writes it down as though the
9
+ * model had said it. That is a dishonest instrument in the one artefact whose
10
+ * entire job is to be honest about what the repository contains.
11
+ * @param {string} reason the classifier's own mechanical reason
12
+ */
13
+ export function scoutReaskTurn(reason: string): string;
14
+ /**
15
+ * The survey brief. Deliberately NEUTRAL about what anyone will do with the
16
+ * facts: this scout is not allowed to have an opinion about the work, because
17
+ * the authoring call is the only thing that composes a close (D11).
18
+ * @param {string} workdir the ABSOLUTE repository root
19
+ */
20
+ export function scoutPrompt(workdir: string): string;
21
+ /**
22
+ * @typedef {{bytes: number, rounds: number, bounded: boolean, recovered: boolean, error: string|null,
23
+ * attempts?: number, attemptsAllowed?: number}} SurveyMeta
24
+ * @typedef {{state: 'PRESENT'|'ABSENT', facts: any, reason: string|null, cause: string|null, meta: SurveyMeta}} Survey
25
+ */
26
+ /**
27
+ * THE F59 CONTRACT, IN CODE. Six routes to ABSENT, one route to PRESENT — and
28
+ * the LAST of them is the whole point of the finding: a parsed `{}` is the
29
+ * scout FAILING, never "no special facts are needed".
30
+ *
31
+ * Every ABSENT route also carries a TYPED `cause` beside its prose reason. The
32
+ * prose is for the operator; the type is what the retry gate reads, so widening
33
+ * that gate requires editing the enum rather than a regular expression over a
34
+ * sentence (the standing rule: hand over the enumerated set, never a rule about
35
+ * prose). `empty` is split OUT of the byte floor for exactly that reason — "the
36
+ * model said nothing" and "the model was cut off mid-sentence" are two findings
37
+ * that used to render as one number.
38
+ * @param {string} blob the scrubbed, capped survey text
39
+ * @param {SurveyMeta} meta
40
+ * @returns {Survey}
41
+ */
42
+ export function classifySurvey(blob: string, meta: SurveyMeta): Survey;
43
+ /**
44
+ * The DEFAULT surveyor: a real bareguard fence plus the granted read-only tools.
45
+ * Read-only TWICE OVER — the menu carries no write verb, and the fence grants no
46
+ * write path — and the arbiter's own books are denied outright, exactly as every
47
+ * worker's are.
48
+ *
49
+ * It is a seam because it is the only part of this module that needs a live
50
+ * process, an index and a gate; the ROUND BOUND and the F59 recovery stay in
51
+ * `runAuthorScout` where they can be proven.
52
+ * @param {{workdir: string, granted: readonly string[], auditPath?: string, ctx?: boolean}} o
53
+ */
54
+ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
55
+ workdir: string;
56
+ granted: readonly string[];
57
+ auditPath?: string;
58
+ ctx?: boolean;
59
+ }): Promise<{
60
+ tools: any[];
61
+ policy: Function;
62
+ onLlmResult: Function;
63
+ cleanup: () => Promise<void>;
64
+ }>;
65
+ /**
66
+ * Run the authoring scout.
67
+ *
68
+ * `createLoop` and `createSurveyor` are the two seams, both REAL by default. The
69
+ * round bound, F59's reserved round, the blob cap, the scrub and the
70
+ * classification all live HERE rather than behind a seam, because those are the
71
+ * behaviours the finding is about.
72
+ *
73
+ * THE ATTEMPT LADDER (hamr, 2026-08-09) sits OUTSIDE all of that, and it fires
74
+ * on the typed malformed class only — see `SCOUT_RETRY_CAUSES` for what is
75
+ * deliberately excluded and why. Attempt 1 is the full survey; a re-ask is
76
+ * TOOLLESS over the conversation the survey already produced, because the model
77
+ * has already READ the repository and only its final emission was unreadable —
78
+ * re-paying the exploration would buy the same facts twice. Every attempt is a
79
+ * paid call, metered under its own label, and every attempt's raw text is
80
+ * persisted so the malformation can be READ later rather than remembered.
81
+ *
82
+ * ONE FALLBACK, stated because the code has it: a retry with NO conversation to
83
+ * re-ask over re-runs the FULL toolable survey instead. The toolless form rests
84
+ * entirely on "the model already read the tree", and an empty transcript is that
85
+ * premise gone — re-asking over nothing would be a bare "say it again" to a model
86
+ * that was never shown the repository. Re-surveying costs the exploration twice;
87
+ * the alternative buys a second sample of noise. It is not reachable through the
88
+ * shipped Loop (whose `msgs` starts from the caller's own messages and is never
89
+ * empty) and it is reachable through the `createLoop` seam, so it is a fail-safe
90
+ * with a test rather than a promise with a comment.
91
+ *
92
+ * @param {{workdir: string, provider?: any,
93
+ * rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
94
+ * attempts?: number, ceilingUsd?: number|null,
95
+ * createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
96
+ * createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
97
+ * @returns {Promise<Survey & {raw: string, raws: ReturnType<typeof scrubRaw>[],
98
+ * calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
99
+ * budgetStop: 'cap-halt'|'pricing-red'|null}>}
100
+ */
101
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, createLoop, createSurveyor, }: {
102
+ workdir: string;
103
+ provider?: any;
104
+ rounds?: number;
105
+ minBytes?: number;
106
+ blobMax?: number;
107
+ maxTokens?: number;
108
+ ctx?: boolean;
109
+ attempts?: number;
110
+ ceilingUsd?: number | null;
111
+ createLoop?: (o: {
112
+ system: string;
113
+ policy: any;
114
+ onLlmResult: any;
115
+ provider: any;
116
+ }) => any;
117
+ createSurveyor?: (o: {
118
+ workdir: string;
119
+ granted: readonly string[];
120
+ ctx: boolean;
121
+ }) => Promise<any>;
122
+ }): Promise<Survey & {
123
+ raw: string;
124
+ raws: ReturnType<typeof scrubRaw>[];
125
+ calls: {
126
+ label: string;
127
+ costUsd: number | null;
128
+ unpricedRounds: number;
129
+ }[];
130
+ budgetStop: "cap-halt" | "pricing-red" | null;
131
+ }>;
132
+ /**
133
+ * `git ls-tree -r --name-only <seed>` — the files as of the SEED COMMIT.
134
+ * Deliberately NOT `ls-files`, which reads the index and the working tree: this
135
+ * listing is what a path is judged against, and it must describe the tree the
136
+ * close will be measured from, not whatever is lying around today.
137
+ *
138
+ * A fault comes back as a named `stop`, never as an empty list — an unreadable
139
+ * listing that rendered as "no files" would fail every path in the declaration
140
+ * for the wrong reason.
141
+ * `files` is spelled `null` on the stop branch rather than left off: a caller
142
+ * that destructures the failure and finds `undefined` reads it as falsy, which
143
+ * is the "absent renders as empty" shape this whole module exists to refuse.
144
+ *
145
+ * The command itself lives in `src/kinds.js` beside the executor's other git
146
+ * reads (`seedListing`) — the declared close's runtime gate judges paths against
147
+ * the SAME listing this scout hands the author, and two spellings of "what
148
+ * exists at the seed" would be two instruments.
149
+ * @param {string} workdir @param {string} seedRef
150
+ * @returns {Promise<{stop: string, files: null}|{stop: null, files: string[]}>}
151
+ */
152
+ export function seedFileList(workdir: string, seedRef: string): Promise<{
153
+ stop: string;
154
+ files: null;
155
+ } | {
156
+ stop: null;
157
+ files: string[];
158
+ }>;
159
+ /**
160
+ * THE SEED LISTING, both halves.
161
+ *
162
+ * `files` is the WHOLE tree and is what the validator judges paths against.
163
+ * `block` is SCOPED to the survey's own sourcePaths/testPaths and is what the
164
+ * prompt carries. Giving the validator the scoped half instead would make a job
165
+ * scoped to `src/` read as whole-tree, and M2's one-population law — which fires
166
+ * only on a SCOPED job — would silently stop firing.
167
+ *
168
+ * @param {{workdir: string, seedRef: string, sourcePaths?: any, testPaths?: any,
169
+ * listCap?: number, byteCap?: number}} o
170
+ * @returns {Promise<{stop: string, files: null, block: null, meta: null}
171
+ * |{stop: null, files: string[], block: string, meta: {seedRef: string, totalSeedFiles: number,
172
+ * entries: {entry: string, raw: string, section: string, matched: number}[], tier: string}}>}
173
+ */
174
+ export function buildSeedListing({ workdir, seedRef, sourcePaths, testPaths, listCap, byteCap }: {
175
+ workdir: string;
176
+ seedRef: string;
177
+ sourcePaths?: any;
178
+ testPaths?: any;
179
+ listCap?: number;
180
+ byteCap?: number;
181
+ }): Promise<{
182
+ stop: string;
183
+ files: null;
184
+ block: null;
185
+ meta: null;
186
+ } | {
187
+ stop: null;
188
+ files: string[];
189
+ block: string;
190
+ meta: {
191
+ seedRef: string;
192
+ totalSeedFiles: number;
193
+ entries: {
194
+ entry: string;
195
+ raw: string;
196
+ section: string;
197
+ matched: number;
198
+ }[];
199
+ tier: string;
200
+ };
201
+ }>;
202
+ /** src/planrun.js:42 — the survey's hard round bound. */
203
+ export const AUTHOR_SCOUT_ROUNDS: 8;
204
+ /** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
205
+ export const AUTHOR_SCOUT_BLOB_MAX: 8000;
206
+ /**
207
+ * src/planrun.js:62 — below this a survey is ABSENT, not empty. Sized off the
208
+ * archive rather than taste: 15 truncated scouts produced 0/74/86 bytes; every
209
+ * real survey was 5991-8056. It is imported as a NUMBER, not as an import, on
210
+ * purpose — planrun does not export it, and a `SCOUT_MIN_BYTES` that drifts
211
+ * between the two scouts is two instruments, so the suite pins them equal.
212
+ */
213
+ export const AUTHOR_SCOUT_MIN_BYTES: 200;
214
+ /** F30 — the 4096 provider default cannot hold a whole survey. */
215
+ export const AUTHOR_SCOUT_MAX_TOKENS: 32000;
216
+ /**
217
+ * TOTAL survey attempts, HARDCODED (hamr, 2026-08-09: *"depending on causes, we
218
+ * can hardcode 3 attempts, but if reasons are beyond malformed then we may verge
219
+ * into self-healing but i don't think we should"*).
220
+ *
221
+ * It is a ceiling, not a target: an operator may pass a LOWER cap and never a
222
+ * higher one, exactly the direction the standing budget rule runs in (*the agent
223
+ * may only TIGHTEN*). A widened value CLAMPS here rather than throwing, because
224
+ * the number is not a signed artefact — nothing downstream depends on a caller
225
+ * having asked for the right one, and refusing a run over a too-generous cap
226
+ * costs a paid scout for no safety.
227
+ *
228
+ * Sited beside `MAX_REVISIONS`/`MAX_STRUCTURE_RETRIES` in spirit: the same
229
+ * bounded-retry family, one axis each. This one is the SURVEY's axis.
230
+ */
231
+ export const SCOUT_ATTEMPTS: 3;
232
+ /**
233
+ * THE TYPED CAUSE, at the detection site.
234
+ *
235
+ * The retry gate reads this enum and nothing else. Typing it here is what stops
236
+ * the gate quietly widening to "any ABSENT" the next time someone edits the
237
+ * classifier: a new route to ABSENT has to be named, and naming it is the moment
238
+ * somebody decides whether it retries.
239
+ */
240
+ export const SURVEY_CAUSES: Readonly<{
241
+ /** the call itself died — provider error, transport, `truncated:max_tokens` */
242
+ CALL_FAILED: "call-failed";
243
+ /** the model returned nothing at all, and nothing went wrong to explain it */
244
+ EMPTY: "empty";
245
+ /** short of the ABSENT floor but not empty — F59's cut-off population */
246
+ SHORT: "short";
247
+ /** nothing extractable in the reply */
248
+ NO_ARTIFACT: "no-artifact";
249
+ /** there IS a body and it is not JSON — run mslhn707's own death */
250
+ UNPARSEABLE: "unparseable";
251
+ /** valid JSON, wrong shape */
252
+ NON_OBJECT: "non-object";
253
+ /** valid JSON, no content — F59's whole point, and a SEMANTIC failure */
254
+ EMPTY_OBJECT: "empty-object";
255
+ /**
256
+ * THE SURVEY WAS NEVER ASKED FOR: the operator's money ceiling was already
257
+ * spent when the first attempt came due, so no call was made. Named as its own
258
+ * cause for the reason this enum exists at all — a new route to ABSENT has to
259
+ * be named, and naming it is the moment somebody decides whether it retries.
260
+ * This one does not, and cannot: a retry is precisely what the cap forbade.
261
+ * It is deliberately ABSENT of the model's own doing, and the prose says so —
262
+ * "the scout did not complete" would blame a model that never spoke.
263
+ */
264
+ NOT_FUNDED: "not-funded";
265
+ }>;
266
+ /**
267
+ * THE MALFORMED CLASS — the only causes a retry fires on.
268
+ *
269
+ * What is deliberately NOT here, and why:
270
+ * - `call-failed` covers `truncated:max_tokens` and every transport death.
271
+ * Standing doctrine routes both as provider-red with NO redraft; a retry
272
+ * here would pay for the same failure again under a different name.
273
+ * - `short` is F59's cut-off population, and it already has an instrument: the
274
+ * reserved TOOLLESS recovery round, which fires inside the attempt.
275
+ * - `non-object` and `empty-object` are VALID JSON with wrong or vacuous
276
+ * content. That is a SEMANTIC failure, and F38/F39 measured what re-asking
277
+ * one buys: the same distribution, sampled twice. Reacting to content is a
278
+ * designed loop; this is a retry. The line between them is the self-healing
279
+ * line, and it is not crossed here.
280
+ */
281
+ /** @type {readonly string[]} */
282
+ export const SCOUT_RETRY_CAUSES: readonly string[];
283
+ /**
284
+ * THE GRANT: the full menu MINUS every write-class and store-class verb.
285
+ * Read-only by MENU CONSTRUCTION — no write-class tool object is ever built, so
286
+ * there is nothing for a fence to have to refuse. Derived from the shipped
287
+ * constants so a verb added to `TOOL_MENU` is covered the day it lands.
288
+ */
289
+ export const AUTHOR_SCOUT_VERBS: readonly string[];
290
+ export const SCOUT_SYSTEM: string;
291
+ /**
292
+ * F59's reserved round, as a FIXED constant. The round is toolless BY
293
+ * CONSTRUCTION (`defs` is `[]`, so text is the only output the model can
294
+ * produce) — never by asking politely in prose (F19/F37).
295
+ */
296
+ export const SCOUT_RECOVERY_PROMPT: string;
297
+ /** per-path full-listing cap; over it, names give way to counted directories */
298
+ export const LIST_CAP: 40;
299
+ /** whole-block ceiling — the block is a prompt ingredient, like the survey */
300
+ export const LISTING_BYTE_CAP: 12000;
301
+ /** how many bare file NAMES survive the deepest tier of a flat directory */
302
+ export const COUNTS_ONLY_NAMES: 12;
303
+ export function cleanEntry(e: any): string;
304
+ export type SurveyMeta = {
305
+ bytes: number;
306
+ rounds: number;
307
+ bounded: boolean;
308
+ recovered: boolean;
309
+ error: string | null;
310
+ attempts?: number;
311
+ attemptsAllowed?: number;
312
+ };
313
+ export type Survey = {
314
+ state: "PRESENT" | "ABSENT";
315
+ facts: any;
316
+ reason: string | null;
317
+ cause: string | null;
318
+ meta: SurveyMeta;
319
+ };
320
+ import { scrubRaw } from './text.js';