bareloop 0.8.0 → 0.9.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,241 @@
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,
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, 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
+ seedFn?: Function;
103
+ scoutFn?: Function;
104
+ listingFn?: Function;
105
+ authorFn?: Function;
106
+ authorOpts?: object;
107
+ }): Promise<{
108
+ ok: boolean;
109
+ refusal: Refusal | null;
110
+ verdictType: string | null;
111
+ closeDecl: any;
112
+ seedRef: string | null;
113
+ authoring: any;
114
+ reds: Red[];
115
+ stop: string | null;
116
+ cost: any;
117
+ interview: any;
118
+ }>;
119
+ /**
120
+ * Fold an authored close into the OPERATOR's own half of the spec.
121
+ *
122
+ * The two halves never mix: budgets, the fence, cadence, escalation and the
123
+ * provider are operator input and are not authored by anything here (the
124
+ * permanent hard line). What this adds is the close, the derived verdict class,
125
+ * and nothing else.
126
+ *
127
+ * The GOAL is passed through, not generated. F87 requires the goal to state
128
+ * everything the close judges, and §3 of the design record says that becomes
129
+ * structural once the goal RENDERS from the same answers — that rendering is UI
130
+ * work (PRD v1.51 §5, "lands with the close-authoring interview UI") and is
131
+ * deliberately not invented here.
132
+ *
133
+ * A draft that ALREADY carries the authored half is REFUSED, not merged and not
134
+ * overwritten. `src/job.js` states the rule for the close in its own words — two
135
+ * closes are two arbiters, and "picking one silently is how the signed artefact
136
+ * stops meaning what the signer read" — and the same holds one step earlier and
137
+ * for the class: this fold used to drop a draft's `close` on the floor and stamp
138
+ * its own `verdictType` over the operator's, so a person who typed either would
139
+ * read back a signed spec that never contained what they wrote. Refusing by name
140
+ * costs the legitimate path nothing (a draft is the operator's half BY
141
+ * DEFINITION, and nothing in the pipeline builds one carrying these) and it is
142
+ * the same direction every other gate here takes: a mismatch a human cannot see
143
+ * is worse than a stop a human can read.
144
+ * @param {any} specDraft the operator's half (everything except close/closeDecl/verdictType)
145
+ * @param {{closeDecl: any, verdictType: string}} authored
146
+ * @throws {Error} when the draft already carries close, closeDecl or verdictType
147
+ */
148
+ export function assembleSpec(specDraft: any, { closeDecl, verdictType }: {
149
+ closeDecl: any;
150
+ verdictType: string;
151
+ }): any;
152
+ /**
153
+ * D9, in code: *nothing JUDGES the close; three mechanical gates plus a
154
+ * signature.* No LLM validates another LLM's close, here or anywhere.
155
+ *
156
+ * 1. THE DECLARATION VALIDATOR — the spec's own gate plus M2's, run GROUNDED
157
+ * against the real seed listing (the job validator defers that half).
158
+ * 2. THE CLOSE PRECHECK — every stage runs against the real patient. A stage
159
+ * that cannot run is `broken-close`, and it is a CASUALTY, never a red.
160
+ * 3. THE SEED-VERDICT READ — every stage, offered or not, run at the seed. Which
161
+ * are RED (the work) and which are GREEN (the guards) is shown to the user.
162
+ * A close with NO work stage red at seed has nothing to do and is refused
163
+ * decision-ready — gate-2 round 2's arm B died exactly there, on a mypy that
164
+ * scanned nothing and therefore read clean.
165
+ *
166
+ * Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
167
+ * job creation the tree IS the seed, so "does every stage run against the real
168
+ * patient" and "what does every stage say at the seed" are two readings of the
169
+ * same pass. `seedRead` runs EVERY stage rather than first-red-wins, which is
170
+ * D12's own named build item — a stage that never ran mints no baseline, and
171
+ * every shipped close opens with a guard that is red at its own seed.
172
+ *
173
+ * IT NEVER SIGNS. The approvals array and the human's word are unchanged; what
174
+ * comes back is the resolved spec, its hash, and the evidence to sign against.
175
+ *
176
+ * @param {{spec: any, workdir: string, seedRef?: string|null, shellCapUsd?: number,
177
+ * timeoutMs?: number, seedFn?: Function, listingFn?: Function, seedReadFn?: Function}} o
178
+ * @returns {Promise<{ok: boolean, specHash: string|null, seedRef: string|null,
179
+ * gates: any, work: any[], guards: any[], stops: any[], reds: Red[], refusal: Refusal|null}>}
180
+ */
181
+ export function prepareSigning({ spec, workdir, seedRef, shellCapUsd, timeoutMs, seedFn, listingFn, seedReadFn, }: {
182
+ spec: any;
183
+ workdir: string;
184
+ seedRef?: string | null;
185
+ shellCapUsd?: number;
186
+ timeoutMs?: number;
187
+ seedFn?: Function;
188
+ listingFn?: Function;
189
+ seedReadFn?: Function;
190
+ }): Promise<{
191
+ ok: boolean;
192
+ specHash: string | null;
193
+ seedRef: string | null;
194
+ gates: any;
195
+ work: any[];
196
+ guards: any[];
197
+ stops: any[];
198
+ reds: Red[];
199
+ refusal: Refusal | null;
200
+ }>;
201
+ /** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
202
+ /** @typedef {{kind: 'request-red'|'decision-ready', verb: string|null, path: string,
203
+ * detail: string, options: string[], red: Red|null}} Refusal */
204
+ /** the one genre v1 admits (D13). The interview no longer ASKS about it — the
205
+ * catalogue is one genre wide and the composer is where that refuses — but the
206
+ * signed `closeDecl` still names the genre it was authored under. */
207
+ export const GENRE: "TYPES";
208
+ /** The territory every refusal here lands against. bareloop's OWN catalogue —
209
+ * the genre menu, the verdict menu, the kind menu — never a bare-suite package.
210
+ * Stamped at the emit site (the typed-lib rule). */
211
+ export const REFUSAL_LIB: "bareloop";
212
+ /** the escalation category a refusal escalates under. It is DELIBERATELY in the
213
+ * ledger's executable excluded set: the demand is already counted once as a
214
+ * `request-red`, and counting the escalation too would inflate the very number
215
+ * the admission path exists to measure. */
216
+ export const REFUSAL_CATEGORY: "close-unauthorable";
217
+ /** The fields `assembleSpec` writes, and therefore the fields a DRAFT must not
218
+ * carry. Named as data so the refusal and the fold can never disagree about
219
+ * which half of the spec is which. */
220
+ export const AUTHORED_SPEC_FIELDS: readonly string[];
221
+ export type Red = {
222
+ code: string;
223
+ path: string;
224
+ detail: string;
225
+ [k: string]: any;
226
+ };
227
+ export type Refusal = {
228
+ kind: "request-red" | "decision-ready";
229
+ verb: string | null;
230
+ path: string;
231
+ detail: string;
232
+ options: string[];
233
+ red: Red | null;
234
+ };
235
+ import { QUESTION_SETS } from './authorflow.js';
236
+ import { questionsFor } from './authorflow.js';
237
+ import { requiredAnswersFor } from './authorflow.js';
238
+ import { VERDICT_CLASSES } from './authoring.js';
239
+ import { LOCKED_CLASSES } from './authoring.js';
240
+ import { LIVE_CLASSES } from './authoring.js';
241
+ export { QUESTION_SETS, questionsFor, requiredAnswersFor, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES };
@@ -0,0 +1,307 @@
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,
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
+ */
100
+ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, createLoop, createSurveyor, }: {
101
+ workdir: string;
102
+ provider?: any;
103
+ rounds?: number;
104
+ minBytes?: number;
105
+ blobMax?: number;
106
+ maxTokens?: number;
107
+ ctx?: boolean;
108
+ attempts?: number;
109
+ createLoop?: (o: {
110
+ system: string;
111
+ policy: any;
112
+ onLlmResult: any;
113
+ provider: any;
114
+ }) => any;
115
+ createSurveyor?: (o: {
116
+ workdir: string;
117
+ granted: readonly string[];
118
+ ctx: boolean;
119
+ }) => Promise<any>;
120
+ }): Promise<Survey & {
121
+ raw: string;
122
+ raws: ReturnType<typeof scrubRaw>[];
123
+ calls: {
124
+ label: string;
125
+ costUsd: number | null;
126
+ unpricedRounds: number;
127
+ }[];
128
+ }>;
129
+ /**
130
+ * `git ls-tree -r --name-only <seed>` — the files as of the SEED COMMIT.
131
+ * Deliberately NOT `ls-files`, which reads the index and the working tree: this
132
+ * listing is what a path is judged against, and it must describe the tree the
133
+ * close will be measured from, not whatever is lying around today.
134
+ *
135
+ * A fault comes back as a named `stop`, never as an empty list — an unreadable
136
+ * listing that rendered as "no files" would fail every path in the declaration
137
+ * for the wrong reason.
138
+ * `files` is spelled `null` on the stop branch rather than left off: a caller
139
+ * that destructures the failure and finds `undefined` reads it as falsy, which
140
+ * is the "absent renders as empty" shape this whole module exists to refuse.
141
+ *
142
+ * The command itself lives in `src/kinds.js` beside the executor's other git
143
+ * reads (`seedListing`) — the declared close's runtime gate judges paths against
144
+ * the SAME listing this scout hands the author, and two spellings of "what
145
+ * exists at the seed" would be two instruments.
146
+ * @param {string} workdir @param {string} seedRef
147
+ * @returns {Promise<{stop: string, files: null}|{stop: null, files: string[]}>}
148
+ */
149
+ export function seedFileList(workdir: string, seedRef: string): Promise<{
150
+ stop: string;
151
+ files: null;
152
+ } | {
153
+ stop: null;
154
+ files: string[];
155
+ }>;
156
+ /**
157
+ * THE SEED LISTING, both halves.
158
+ *
159
+ * `files` is the WHOLE tree and is what the validator judges paths against.
160
+ * `block` is SCOPED to the survey's own sourcePaths/testPaths and is what the
161
+ * prompt carries. Giving the validator the scoped half instead would make a job
162
+ * scoped to `src/` read as whole-tree, and M2's one-population law — which fires
163
+ * only on a SCOPED job — would silently stop firing.
164
+ *
165
+ * @param {{workdir: string, seedRef: string, sourcePaths?: any, testPaths?: any,
166
+ * listCap?: number, byteCap?: number}} o
167
+ * @returns {Promise<{stop: string, files: null, block: null, meta: null}
168
+ * |{stop: null, files: string[], block: string, meta: {seedRef: string, totalSeedFiles: number,
169
+ * entries: {entry: string, raw: string, section: string, matched: number}[], tier: string}}>}
170
+ */
171
+ export function buildSeedListing({ workdir, seedRef, sourcePaths, testPaths, listCap, byteCap }: {
172
+ workdir: string;
173
+ seedRef: string;
174
+ sourcePaths?: any;
175
+ testPaths?: any;
176
+ listCap?: number;
177
+ byteCap?: number;
178
+ }): Promise<{
179
+ stop: string;
180
+ files: null;
181
+ block: null;
182
+ meta: null;
183
+ } | {
184
+ stop: null;
185
+ files: string[];
186
+ block: string;
187
+ meta: {
188
+ seedRef: string;
189
+ totalSeedFiles: number;
190
+ entries: {
191
+ entry: string;
192
+ raw: string;
193
+ section: string;
194
+ matched: number;
195
+ }[];
196
+ tier: string;
197
+ };
198
+ }>;
199
+ /** src/planrun.js:42 — the survey's hard round bound. */
200
+ export const AUTHOR_SCOUT_ROUNDS: 8;
201
+ /** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
202
+ export const AUTHOR_SCOUT_BLOB_MAX: 8000;
203
+ /**
204
+ * src/planrun.js:62 — below this a survey is ABSENT, not empty. Sized off the
205
+ * archive rather than taste: 15 truncated scouts produced 0/74/86 bytes; every
206
+ * real survey was 5991-8056. It is imported as a NUMBER, not as an import, on
207
+ * purpose — planrun does not export it, and a `SCOUT_MIN_BYTES` that drifts
208
+ * between the two scouts is two instruments, so the suite pins them equal.
209
+ */
210
+ export const AUTHOR_SCOUT_MIN_BYTES: 200;
211
+ /** F30 — the 4096 provider default cannot hold a whole survey. */
212
+ export const AUTHOR_SCOUT_MAX_TOKENS: 32000;
213
+ /**
214
+ * TOTAL survey attempts, HARDCODED (hamr, 2026-08-09: *"depending on causes, we
215
+ * can hardcode 3 attempts, but if reasons are beyond malformed then we may verge
216
+ * into self-healing but i don't think we should"*).
217
+ *
218
+ * It is a ceiling, not a target: an operator may pass a LOWER cap and never a
219
+ * higher one, exactly the direction the standing budget rule runs in (*the agent
220
+ * may only TIGHTEN*). A widened value CLAMPS here rather than throwing, because
221
+ * the number is not a signed artefact — nothing downstream depends on a caller
222
+ * having asked for the right one, and refusing a run over a too-generous cap
223
+ * costs a paid scout for no safety.
224
+ *
225
+ * Sited beside `MAX_REVISIONS`/`MAX_STRUCTURE_RETRIES` in spirit: the same
226
+ * bounded-retry family, one axis each. This one is the SURVEY's axis.
227
+ */
228
+ export const SCOUT_ATTEMPTS: 3;
229
+ /**
230
+ * THE TYPED CAUSE, at the detection site.
231
+ *
232
+ * The retry gate reads this enum and nothing else. Typing it here is what stops
233
+ * the gate quietly widening to "any ABSENT" the next time someone edits the
234
+ * classifier: a new route to ABSENT has to be named, and naming it is the moment
235
+ * somebody decides whether it retries.
236
+ */
237
+ export const SURVEY_CAUSES: Readonly<{
238
+ /** the call itself died — provider error, transport, `truncated:max_tokens` */
239
+ CALL_FAILED: "call-failed";
240
+ /** the model returned nothing at all, and nothing went wrong to explain it */
241
+ EMPTY: "empty";
242
+ /** short of the ABSENT floor but not empty — F59's cut-off population */
243
+ SHORT: "short";
244
+ /** nothing extractable in the reply */
245
+ NO_ARTIFACT: "no-artifact";
246
+ /** there IS a body and it is not JSON — run mslhn707's own death */
247
+ UNPARSEABLE: "unparseable";
248
+ /** valid JSON, wrong shape */
249
+ NON_OBJECT: "non-object";
250
+ /** valid JSON, no content — F59's whole point, and a SEMANTIC failure */
251
+ EMPTY_OBJECT: "empty-object";
252
+ }>;
253
+ /**
254
+ * THE MALFORMED CLASS — the only causes a retry fires on.
255
+ *
256
+ * What is deliberately NOT here, and why:
257
+ * - `call-failed` covers `truncated:max_tokens` and every transport death.
258
+ * Standing doctrine routes both as provider-red with NO redraft; a retry
259
+ * here would pay for the same failure again under a different name.
260
+ * - `short` is F59's cut-off population, and it already has an instrument: the
261
+ * reserved TOOLLESS recovery round, which fires inside the attempt.
262
+ * - `non-object` and `empty-object` are VALID JSON with wrong or vacuous
263
+ * content. That is a SEMANTIC failure, and F38/F39 measured what re-asking
264
+ * one buys: the same distribution, sampled twice. Reacting to content is a
265
+ * designed loop; this is a retry. The line between them is the self-healing
266
+ * line, and it is not crossed here.
267
+ */
268
+ /** @type {readonly string[]} */
269
+ export const SCOUT_RETRY_CAUSES: readonly string[];
270
+ /**
271
+ * THE GRANT: the full menu MINUS every write-class and store-class verb.
272
+ * Read-only by MENU CONSTRUCTION — no write-class tool object is ever built, so
273
+ * there is nothing for a fence to have to refuse. Derived from the shipped
274
+ * constants so a verb added to `TOOL_MENU` is covered the day it lands.
275
+ */
276
+ export const AUTHOR_SCOUT_VERBS: readonly string[];
277
+ export const SCOUT_SYSTEM: string;
278
+ /**
279
+ * F59's reserved round, as a FIXED constant. The round is toolless BY
280
+ * CONSTRUCTION (`defs` is `[]`, so text is the only output the model can
281
+ * produce) — never by asking politely in prose (F19/F37).
282
+ */
283
+ export const SCOUT_RECOVERY_PROMPT: string;
284
+ /** per-path full-listing cap; over it, names give way to counted directories */
285
+ export const LIST_CAP: 40;
286
+ /** whole-block ceiling — the block is a prompt ingredient, like the survey */
287
+ export const LISTING_BYTE_CAP: 12000;
288
+ /** how many bare file NAMES survive the deepest tier of a flat directory */
289
+ export const COUNTS_ONLY_NAMES: 12;
290
+ export function cleanEntry(e: any): string;
291
+ export type SurveyMeta = {
292
+ bytes: number;
293
+ rounds: number;
294
+ bounded: boolean;
295
+ recovered: boolean;
296
+ error: string | null;
297
+ attempts?: number;
298
+ attemptsAllowed?: number;
299
+ };
300
+ export type Survey = {
301
+ state: "PRESENT" | "ABSENT";
302
+ facts: any;
303
+ reason: string | null;
304
+ cause: string | null;
305
+ meta: SurveyMeta;
306
+ };
307
+ import { scrubRaw } from './text.js';