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,174 @@
1
+ /** @param {any} job @returns {boolean} */
2
+ export function isDeclaredClose(job: any): boolean;
3
+ /**
4
+ * The declaration's stages as the RUNNER's stage descriptors: the declared
5
+ * `{name, kind, params}` plus the arbiter's own `gapKeep`.
6
+ *
7
+ * `offer` and `needs` pass through untouched when a declaration carries them
8
+ * (v1's schema does not let one), so `checkMenu`'s derivation is unchanged.
9
+ * @param {any} closeDecl
10
+ * @returns {any[]|null} null when this is not a declaration
11
+ */
12
+ export function declaredStages(closeDecl: any): any[] | null;
13
+ /**
14
+ * Validate a `closeDecl` as a JOB SPEC field: the envelope (genre, language,
15
+ * notes) plus M2's full declaration validator underneath it.
16
+ *
17
+ * `listing` is what splits the two callers, and the split is deliberate:
18
+ * - the JOB VALIDATOR has no repository, so it passes nothing and gets
19
+ * `grounded: false` — every tree-independent rule runs, the path rule and
20
+ * the scoped-job derivation do not;
21
+ * - the RUNNER has the real seed, so it passes the real listing and gets
22
+ * `grounded: true`. It runs this BEFORE any stage and before any token, so
23
+ * the deferred half is proved rather than skipped.
24
+ * A caller that passes neither a listing nor `deferListing` gets M2's
25
+ * `listing-absent` red, unchanged: a validator handed nothing to check against
26
+ * must refuse, never report a declaration it never examined.
27
+ *
28
+ * THREE checks now ride on the listing, not two: the path rule, the scoped-job
29
+ * derivation, and the GROUNDING of a recorded `genreEnv` (a by-value comparison
30
+ * between two copies inside one hand-editable spec proves they agree, never that
31
+ * the arbiter wrote them — see `ungroundedGenreEnv`). All three are deferred at
32
+ * the job-spec gate and all three are re-run GROUNDED by the runner before any
33
+ * stage executes, so the deferral moves the check rather than dropping it. The
34
+ * grounded gates are `prepareSigning`'s gate 1b — where the signature is minted,
35
+ * so a forged env can never be signed in the first place — and `runPlan`'s
36
+ * pre-flight, which catches a spec edited after signing.
37
+ *
38
+ * `verdictType` is the class the USER picked, and it is REQUIRED here because
39
+ * the guard battery keys off it (PRD v1.57 §2): with no class in hand there is
40
+ * no battery, and M2 would be handed nothing to check D5 against. It arrives
41
+ * from the spec's sibling `verdictType` field on every caller — the job
42
+ * validator, the signing gate and the runner all hold the whole spec.
43
+ *
44
+ * @param {any} closeDecl
45
+ * @param {{at?: string, listing?: string[]|null, deferListing?: boolean,
46
+ * catalogue?: Record<string, any>, verdictType?: string|null}} [opts]
47
+ * @returns {{ok: boolean, reds: Red[], closeDecl: any, grounded: boolean,
48
+ * scoped: {scoped: boolean, via: string|null},
49
+ * ceiling: {class: string, kind: string, stage: string|null, locked: boolean}|null}}
50
+ */
51
+ export function validateCloseDecl(closeDecl: any, opts?: {
52
+ at?: string;
53
+ listing?: string[] | null;
54
+ deferListing?: boolean;
55
+ catalogue?: Record<string, any>;
56
+ verdictType?: string | null;
57
+ }): {
58
+ ok: boolean;
59
+ reds: Red[];
60
+ closeDecl: any;
61
+ grounded: boolean;
62
+ scoped: {
63
+ scoped: boolean;
64
+ via: string | null;
65
+ };
66
+ ceiling: {
67
+ class: string;
68
+ kind: string;
69
+ stage: string | null;
70
+ locked: boolean;
71
+ } | null;
72
+ };
73
+ /**
74
+ * The genre's mandatory guard NAMES for a declaration — which stages are D5's
75
+ * guards and which are the job's own work. The seed-verdict read (D9.3) needs
76
+ * exactly this split: `changed-from-seed` is RED at its own seed by
77
+ * construction, so counting it as work would let a close with nothing to do
78
+ * pass the "something must be red at seed" gate.
79
+ * The battery keys off the CLASS the user picked (PRD v1.57 §2), so the class
80
+ * comes in beside the declaration. A class or a language with no battery hands
81
+ * back `[]` — this reads a split, it does not enforce one, and every caller
82
+ * reaches it only after `validateCloseDecl` has already refused both cases.
83
+ * @param {any} closeDecl @param {string} verdictType @returns {string[]}
84
+ */
85
+ export function guardNames(closeDecl: any, verdictType: string): string[];
86
+ /**
87
+ * Run a DECLARED close (or a check's chain out of one) and return `runClose`'s
88
+ * verdict shape, so every consumer downstream — the precheck, ralph's judge, the
89
+ * `CLOSE_FAULTS` routing, the trend reader, Layer R — is unchanged.
90
+ *
91
+ * The extras a declared close can honestly add ride alongside rather than
92
+ * replacing anything: `declared: true` marks the shape, `value`/`baseline` carry
93
+ * the deciding stage's own measurement, and `trendValue` is the ONE number the
94
+ * trend reader may compare (see the header for why a higher-is-better stage
95
+ * donates none).
96
+ *
97
+ * @param {any[]} stages declared stage descriptors, already validated
98
+ * @param {(s: string) => string} [redact] the scrub, applied at THIS boundary
99
+ * @param {{timeoutMs?: number, cwd?: string, seedRef?: string,
100
+ * seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree'}} [opts]
101
+ * @returns {Promise<any>}
102
+ */
103
+ export function runDeclaredStages(stages: any[], redact?: (s: string) => string, opts?: {
104
+ timeoutMs?: number;
105
+ cwd?: string;
106
+ seedRef?: string;
107
+ seedTrees?: any;
108
+ gapCap?: number;
109
+ maxBuffer?: number;
110
+ baselineMode?: "auto" | "worktree";
111
+ }): Promise<any>;
112
+ /**
113
+ * The trend reader's input for ONE close verdict, whichever executor produced
114
+ * it. A command close hands over its gap and `readGrade` scrapes stage and count
115
+ * out of the text; a declared close already KNOWS both, so it says them instead
116
+ * of round-tripping them through prose it would then have to parse back.
117
+ *
118
+ * Byte-identical to the previous call for a command close: `{ gap }`, with
119
+ * `stage`/`value` left UNDEFINED so `trend.record` falls through to `readGrade`
120
+ * exactly as before. Passing `stage: null` instead would silently disable that
121
+ * fallback — undefined and null are different answers here.
122
+ * @param {any} v a `runClose`-shaped verdict
123
+ * @returns {{gap?: string, stage?: string|null, value?: number|null}}
124
+ */
125
+ export function closeGrade(v: any): {
126
+ gap?: string;
127
+ stage?: string | null;
128
+ value?: number | null;
129
+ };
130
+ /** @typedef {{code: string, path: string, detail: string, [k: string]: any}} Red */
131
+ /** The genres a `closeDecl` may name. v1 admits exactly one (D13) — a genre
132
+ * classifier is designed the day there is a second genre to classify. */
133
+ export const DECLARED_GENRES: readonly "TYPES"[];
134
+ /**
135
+ * The exact field set of a `closeDecl` — anything else is a smuggle channel, the
136
+ * same rule `STAGE_FIELDS` enforces on a command stage.
137
+ *
138
+ * `genreEnv` is the ARBITER's own record of what it injected (M3's
139
+ * `applyGenreEnv`), and it is here because the injection has to survive being
140
+ * re-read. The flow applies the genre's variable to every env-capable stage
141
+ * AFTER the model's form passed; from then on every re-validation — signing
142
+ * (`prepareSigning`), the job validator (`src/job.js`), the runner
143
+ * (`src/planrun.js`) — sees that key sitting in `params.env` and cannot, from
144
+ * the stage alone, tell our injection from a model-authored guess. Recording it
145
+ * once, in the SIGNED envelope, is what makes the comparison an EQUALITY rather
146
+ * than a presence flag (the same move `checkGuards` makes for guard params), and
147
+ * it is why the model never gets to author it: `authorCloseForJob` composes this
148
+ * field from the flow's own applied map, never from model output. Its keys are
149
+ * constrained to the names the genre OWNS, so it can never become an environment
150
+ * channel of its own.
151
+ */
152
+ export const CLOSE_DECL_FIELDS: readonly string[];
153
+ /**
154
+ * The prefix M1 puts on EVERY gap line of a declared stage (its `ctx.gapKeep`).
155
+ * A literal, so the anchored pattern below is exact.
156
+ */
157
+ export const DECLARED_GAP_PREFIX: "close: ";
158
+ /** The `gapKeep` PATTERN for a declared stage, DERIVED from the prefix so the
159
+ * two can never drift. Layer R's red-set and `boundGap`'s keep-block both read
160
+ * it; because every declared line carries the prefix, it designates the stage's
161
+ * whole judged output rather than a hand-picked slice of it. */
162
+ export const DECLARED_GAP_KEEP: string;
163
+ /** The verdict class a DECLARED close may claim. Hard, and by CONSTRUCTION
164
+ * rather than by assertion: every live kind is a mechanical command whose
165
+ * measurement is the truth, and the two judgment kinds are locked out at the
166
+ * catalogue (M2) and absent from the executor (M1). Named as data so the job
167
+ * validator reads one hierarchy table rather than hard-coding a class. */
168
+ export const DECLARED_CLOSE_CLASSES: readonly string[];
169
+ export type Red = {
170
+ code: string;
171
+ path: string;
172
+ detail: string;
173
+ [k: string]: any;
174
+ };
package/types/index.d.ts CHANGED
@@ -4,9 +4,17 @@ export { runJob } from "./run.js";
4
4
  export { ralph, runClose, runStages, CLOSE_FAULTS } from "./ralph.js";
5
5
  export { globToPrefix, scanSecrets } from "./validate.js";
6
6
  export { validateJob, jobSpecHash, checkApproval, checkMenu, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
7
- export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from "./plan.js";
7
+ export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS, closeStagesOf } from "./plan.js";
8
8
  export { snapshotScope, evalExits } from "./exits.js";
9
9
  export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists } from "./bridges.js";
10
10
  export { renderListing, selectionPrompt } from "./selection.js";
11
11
  export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from "./reuse.js";
12
12
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from "./ledger.js";
13
+ export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from "./workbranch.js";
14
+ export { runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees, LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP } from "./kinds.js";
15
+ export { validateDeclaration, normalizeDeclaration, KIND_CATALOGUE, CATALOGUE_KINDS, CATALOGUE_LIVE_KINDS, LOCKED_KINDS, TYPES_GENRE, TYPES_GENRE_TEMPLATE, GENRE_LANGUAGES, genreEnv, genreOwnedEnvNames, genreInstruments, DIRECTIONS, BASELINES, MAX_STAGES, VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, closeCeiling } from "./authoring.js";
16
+ export { runAuthorScout, buildSeedListing, seedFileList, classifySurvey, AUTHOR_SCOUT_VERBS, SCOUT_ATTEMPTS, SURVEY_CAUSES, SCOUT_RETRY_CAUSES } from "./authorscout.js";
17
+ export { scrubRaw, RAW_PERSIST_MAX, RAW_TRIM_MARKER } from "./text.js";
18
+ export { authorClose, authorPrompt, declarationSchema, makeLoopGenerate, MAX_REVISIONS, QUESTION_SETS, GREEN_QUESTIONS, CLASS_STATEMENTS, questionsFor, requiredAnswersFor } from "./authorflow.js";
19
+ export { validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade, DECLARED_GENRES, DECLARED_GAP_PREFIX, DECLARED_GAP_KEEP, DECLARED_CLOSE_CLASSES, CLOSE_DECL_FIELDS } from "./declaredclose.js";
20
+ export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
package/types/job.d.ts CHANGED
@@ -93,6 +93,7 @@ export const CLASS_BY_CLOSE: Readonly<{
93
93
  gold: readonly string[];
94
94
  rubric: readonly string[];
95
95
  hitl: readonly string[];
96
+ declared: readonly string[];
96
97
  }>;
97
98
  export const GOLD_COMPARE: readonly string[];
98
99
  /** the tool grant menu (2b interview #1): read/grep/write, plus the two litectx
@@ -0,0 +1,395 @@
1
+ /**
2
+ * D8's seed, READ rather than typed: HEAD at run start. A close stores the
3
+ * counting rule and never the number (D12), so which commit the run is measured
4
+ * from is a fact about THIS run — recorded and shown, never carried in a signed
5
+ * spec where it would judge run 5 against run 1's tree.
6
+ *
7
+ * A fault is a named `stop`, never a fallback ref: measuring against the wrong
8
+ * baseline is the failure direction that reads as a clean tree.
9
+ * @param {string} workdir
10
+ * @returns {Promise<{stop: string, seedRef: null}|{stop: null, seedRef: string}>}
11
+ */
12
+ export function seedAtHead(workdir: string): Promise<{
13
+ stop: string;
14
+ seedRef: null;
15
+ } | {
16
+ stop: null;
17
+ seedRef: string;
18
+ }>;
19
+ /**
20
+ * The files that exist AT THE SEED COMMIT. Deliberately not `ls-files`, which
21
+ * reads the index and the working tree: this listing is what a declared path is
22
+ * judged against, and it must describe the tree the close measures from rather
23
+ * than whatever is lying around today.
24
+ *
25
+ * ONE spelling, here beside the other git reads (`src/authorscout.js`'s
26
+ * `seedFileList` delegates to it) — a second listing command is a second
27
+ * instrument, and the one that grades would be the one nobody validated.
28
+ * @param {string} workdir @param {string} seedRef
29
+ * @returns {Promise<{stop: string, files: null}|{stop: null, files: string[]}>}
30
+ */
31
+ export function seedListing(workdir: string, seedRef: string): Promise<{
32
+ stop: string;
33
+ files: null;
34
+ } | {
35
+ stop: null;
36
+ files: string[];
37
+ }>;
38
+ /**
39
+ * The arbiter books, and the shape of each exclusion is the whole point.
40
+ *
41
+ * `.litectx/` and `.smoke/` are PREFIXES — everything beneath either store
42
+ * belongs to the arbiter. `gate-audit.jsonl` is an EXACT repo-relative path and
43
+ * never a basename or a pattern: the gate audit is written to
44
+ * `join(workdir, 'gate-audit.jsonl')` and nowhere else, so a worker-authored
45
+ * `src/gate-audit.jsonl` is NOT an arbiter book and must still count as work.
46
+ * Matching by basename would swallow it — and swallowing a worker's real
47
+ * write is the failure direction that reads as a clean tree. `src/smoke.js` and
48
+ * `tests/smoke.test.js` are that same decoy for the store below.
49
+ *
50
+ * `.smoke/` — F98's parked residual, closed here. The fence has always denied
51
+ * `join(workdir, '.smoke')` on every worker (src/planrun.js, src/authorscout.js)
52
+ * and the persona now states the rule; this reader, which decides what COUNTS AS
53
+ * WORK, was the one place that never knew the name. On patients whose .gitignore
54
+ * default-denies dot-directories the store never reaches `ls-files --others` and
55
+ * the gap is masked — by the PATIENT's config, not by anything here. Incidental
56
+ * masking is not an exclusion: on a patient without that line the arbiter's own
57
+ * store would enter the changed set as the worker's writing.
58
+ * @param {string} rel a repo-relative POSIX path
59
+ * @returns {string|null} the book it belongs to, or null
60
+ */
61
+ export function isArbiterBook(rel: string): string | null;
62
+ /**
63
+ * Everything this run changed against the seed: the tracked diff PLUS
64
+ * untracked files (an untracked file has no diff — the case a naive
65
+ * `git diff` scan silently misses), minus the arbiter's own books.
66
+ * @param {string} workdir @param {string} seedRef
67
+ * @returns {Promise<{stop: string, fault: string}|{stop: null, paths: string[], tracked: string[], untracked: string[], excluded: {path: string, book: string}[]}>}
68
+ */
69
+ export function changedSet(workdir: string, seedRef: string): Promise<{
70
+ stop: string;
71
+ fault: string;
72
+ } | {
73
+ stop: null;
74
+ paths: string[];
75
+ tracked: string[];
76
+ untracked: string[];
77
+ excluded: {
78
+ path: string;
79
+ book: string;
80
+ }[];
81
+ }>;
82
+ /**
83
+ * The lines this run ADDED to one file. A tracked file's additions come from
84
+ * its own unified diff; an UNTRACKED file has no diff at all, so its ENTIRE
85
+ * body is added — the half of this primitive a diff-only scan misses.
86
+ * @param {string} workdir @param {string} seedRef @param {string} rel
87
+ * @param {{untracked?: boolean}} [o]
88
+ * @returns {Promise<{stop: string, fault: string}|{stop: null, lines: {n: number, text: string}[], note: string|null}>}
89
+ */
90
+ export function addedLines(workdir: string, seedRef: string, rel: string, { untracked }?: {
91
+ untracked?: boolean;
92
+ }): Promise<{
93
+ stop: string;
94
+ fault: string;
95
+ } | {
96
+ stop: null;
97
+ lines: {
98
+ n: number;
99
+ text: string;
100
+ }[];
101
+ note: string | null;
102
+ }>;
103
+ /**
104
+ * Count a pattern's capture groups without running it against real input:
105
+ * `src + '|'` always matches the empty string, so the result array's length
106
+ * names the group count. Also the ONE place a declared regex is compiled at
107
+ * validation time, so a broken pattern reds before any process is spawned.
108
+ * @param {any} src
109
+ * @returns {{count: number, red: string|null}}
110
+ */
111
+ export function regexGroups(src: any): {
112
+ count: number;
113
+ red: string | null;
114
+ };
115
+ /**
116
+ * THE normalisation of a `count-not-worse` parser (gate-1 amendment #5).
117
+ * The declaration validator and this executor both call it, so there is
118
+ * exactly one spelling of what a parser MEANS — two normalisers that
119
+ * disagree are two instruments, and the one that grades is the one nobody
120
+ * validated.
121
+ *
122
+ * value = Σ over terms of sign × (first | sum of that term's captures,
123
+ * inside that term's region)
124
+ *
125
+ * Backwards compatible by construction: the short form
126
+ * `{lineMatch, capture?}` resolves to exactly one term with
127
+ * `sign +1, aggregate "first", region "whole-output"`, which is the
128
+ * pre-amendment semantics unchanged.
129
+ *
130
+ * Never throws — reds come back in the shipped `{ok, reds, ...}` shape.
131
+ * @param {any} parser @param {string} [at] path prefix for reds
132
+ * @returns {{ok: boolean, reds: Red[], terms: any[]|null}}
133
+ */
134
+ export function normalizeParser(parser: any, at?: string): {
135
+ ok: boolean;
136
+ reds: Red[];
137
+ terms: any[] | null;
138
+ };
139
+ /**
140
+ * Read ONE number out of a command's output, as signed arithmetic over
141
+ * normalised terms.
142
+ *
143
+ * The zero-match split is contract (a), not a convenience:
144
+ * - `sum` over zero matches is a COUNTED zero. The region was searched and
145
+ * nothing was there; that is legitimately 0.
146
+ * - `first` over zero matches is UNKNOWN — the number was never reported —
147
+ * and unknown is an instrument stop, never a defaulted 0 (F6).
148
+ * - a region whose anchor matches nothing was never LOCATED: stop.
149
+ * The scope filter runs per OUTPUT LINE, before aggregation, and both its
150
+ * drop count and its unattributable count are ANNOUNCED: a line naming no
151
+ * path is KEPT (no filter can exclude what it cannot locate) and the notes
152
+ * say so, rather than the number quietly meaning something else.
153
+ *
154
+ * `preScopeMatches` is reported alongside the reading because a POST-scope
155
+ * count cannot answer "did the tool run at all". It tallies (term, line) pairs
156
+ * that matched BEFORE the filter judged them — kept, unattributable and dropped
157
+ * alike — and it is the only evidence of liveness a caller has once the scope
158
+ * has removed the whole population. It is never the number being read.
159
+ *
160
+ * `matched` is the KEPT lines themselves, PER TERM and index-aligned with
161
+ * `breakdown`: entry i holds, in output order, exactly the lines term i's
162
+ * subtotal was computed FROM. Run u-msn227nq paid for the echo — the executor
163
+ * counted 8 real `error TS…` lines and told the worker "8 match(es)", discarding
164
+ * every address, and a worker with a number and no file to open went looking for
165
+ * the arbiter's own books instead.
166
+ *
167
+ * TWO EXCLUSIONS, both of them "this line is not what the count is made of":
168
+ * - a scope-DROPPED line is never in here. It was never counted, so aiming the
169
+ * worker at it would aim it outside its own population.
170
+ * - under `first`, only the line the aggregate actually READ. The later matches
171
+ * are real matches (`breakdown[i].matches` still counts them) but the value
172
+ * never contained them, and echoing them points the worker at a number that
173
+ * is not in the reading.
174
+ *
175
+ * And the GROUPING is the third: a term's sign lives in `breakdown[i]`, so lines
176
+ * pooled flat across terms lose the one fact that says what they DID to the
177
+ * number. A subtracted term's line rendered beside an added one reads as a wall
178
+ * to go fix, and fixing it moves the count the wrong way. Deduplication is
179
+ * therefore within a term only — one line matched by two terms plays two
180
+ * arithmetic roles and is named under each. The count is computed from `values`,
181
+ * never from this list.
182
+ * @param {string} output @param {any[]} terms normalised terms
183
+ * Every stop here is `crashed` in the arbiter's vocabulary — the command RAN,
184
+ * came back, and the number could not be read out of it. Nothing in this
185
+ * function can observe a spawn failure, a timeout or a signal, so it never
186
+ * claims one.
187
+ * @param {{scope?: any, workdir?: string}} [o]
188
+ * @returns {{stop: string, fault: string, notes?: string[]}|{stop: null, value: number, breakdown: any[], notes: string[], preScopeMatches: number, matched: string[][]}}
189
+ */
190
+ export function parseValue(output: string, terms: any[], { scope, workdir }?: {
191
+ scope?: any;
192
+ workdir?: string;
193
+ }): {
194
+ stop: string;
195
+ fault: string;
196
+ notes?: string[];
197
+ } | {
198
+ stop: null;
199
+ value: number;
200
+ breakdown: any[];
201
+ notes: string[];
202
+ preScopeMatches: number;
203
+ matched: string[][];
204
+ };
205
+ /**
206
+ * A caller-scoped holder for detached seed worktrees. Scoped rather than
207
+ * module-global on purpose: a global cache in a published library leaks
208
+ * worktrees into whatever process imported it, and nothing would ever remove
209
+ * them. `seedRead`/`runClose` create one, share it across stages (so a
210
+ * multi-stage close pays for one checkout), and remove it in a `finally`.
211
+ * @returns {SeedTrees}
212
+ */
213
+ export function makeSeedTrees(): SeedTrees;
214
+ /**
215
+ * Run ONE declared stage against a workdir at a seed.
216
+ *
217
+ * It never throws. An unexpected throw inside a kind — a malformed parameter
218
+ * the validator did not catch, an unreachable path — is an INSTRUMENT STOP,
219
+ * not a red: that is the casualty-vs-evidence split at its narrowest, and
220
+ * the direction that matters, because a crash rendered as a red is fake
221
+ * evidence about the worker.
222
+ * @param {any} stage `{name, kind, params}`
223
+ * @param {Ctx} ctx
224
+ * @returns {Promise<StageResult>}
225
+ */
226
+ export function runStage(stage: any, ctx: Ctx): Promise<StageResult>;
227
+ /**
228
+ * D12's seed-verdict read: run EVERY declared stage at the seed, regardless
229
+ * of reds, and hand back one row per stage.
230
+ *
231
+ * Not first-red-wins, and the reason is structural rather than a preference:
232
+ * every shipped close opens with `changed-from-seed`, which is RED at its own
233
+ * seed by construction (nothing has changed yet), so a first-red-wins seed
234
+ * read measures exactly one stage and mints no baseline for any other. The
235
+ * baseline every `baseline: "seed"` stage is later graded against comes from
236
+ * here, so a stage that never ran is a stage with no bar.
237
+ * @param {any} declaration `{stages: [...]}`
238
+ * @param {Ctx} ctx
239
+ * @returns {Promise<StageResult[]>}
240
+ */
241
+ export function seedRead(declaration: any, ctx: Ctx): Promise<StageResult[]>;
242
+ /**
243
+ * The GRADING path: stages in declared order, FIRST RED WINS. The deciding
244
+ * stage names the verdict; everything after it is `not-reached` and is
245
+ * reported as such rather than as a green nobody ran.
246
+ *
247
+ * An instrument stop ends the close UNJUDGED — the run has no verdict to
248
+ * carry, which is the whole point of keeping a casualty out of the evidence.
249
+ *
250
+ * NAMED `runDeclaredClose`, not `runClose` (M4's export-surface decision): the
251
+ * arbiter's shell already ships a public `runClose(argv, redact, opts)` that
252
+ * adopters import, and the two take different things and mean different things
253
+ * — one runs a DECLARATION, the other an argv. Renaming the shipped one to make
254
+ * room would break a documented adopter contract for nothing; the new name says
255
+ * exactly what differs. `src/declaredclose.js` is what bridges the two shapes.
256
+ * @param {any} declaration @param {Ctx} ctx
257
+ * @returns {Promise<{verdict: 'green'|'red'|'instrument-stop', exitCode: number, judged: boolean, firstRed: string|null, stages: StageResult[]}>}
258
+ */
259
+ export function runDeclaredClose(declaration: any, ctx: Ctx): Promise<{
260
+ verdict: "green" | "red" | "instrument-stop";
261
+ exitCode: number;
262
+ judged: boolean;
263
+ firstRed: string | null;
264
+ stages: StageResult[];
265
+ }>;
266
+ /** the close's two judgment exits — a stage's verdict, in the shape the
267
+ * hand-written closes already spoke (exit code is truth) */
268
+ export const EXIT_GREEN: 0;
269
+ export const EXIT_RED: 1;
270
+ /** contract (a): the instrument-stop exit. `judged` is withheld with it. */
271
+ export const EXIT_STOP: 97;
272
+ /** The judgment-rendered marker a rendered close prints, for ralph's
273
+ * `runClose` judged floor (src/ralph.js). It rides EVERY real judgment, green or red,
274
+ * and NEVER a stop — this module reports the same fact as the boolean
275
+ * `judged`; the marker exists so a renderer and the floor agree on one
276
+ * spelling. */
277
+ export const JUDGED_MARKER: "judged=1";
278
+ /** contract (b): the per-stage gap ceiling. Trims are announced, never silent. */
279
+ export const GAP_LINE_CAP: 40;
280
+ /**
281
+ * The exact phrase this executor's trim announcement carries. Exported for the
282
+ * same reason ralph exports `GAP_KEEP_TRIM_MARKER`: Layer R's detector must be
283
+ * able to see that a gap window was TRIMMED, because failures beyond the window
284
+ * can move while the visible lines stay identical — and an undetected trim makes
285
+ * the detector claim "reds unchanged" off an instrument that was blind (F43,
286
+ * finding 5). A magic string on the reading side would drift the day this
287
+ * wording changes; the renderer below is its only other use.
288
+ */
289
+ export const GAP_TRIM_MARKER: "gap trimmed:";
290
+ /** used when neither the stage nor the ctx names a bound. A stage that could
291
+ * run forever is not a red — it is a stop, and something must decide when. */
292
+ export const DEFAULT_TIMEOUT_MS: 300000;
293
+ /** PER STREAM, like the arbiter's own close runner. An overrun truncates the
294
+ * output mid-summary, so any number read out of it is unknown, not zero. */
295
+ export const MAX_BUFFER: number;
296
+ /** an untracked file larger than this is not scanned line-by-line; the skip
297
+ * is ANNOUNCED in the gap rather than passing as a clean scan */
298
+ export const MAX_SCAN_BYTES: number;
299
+ /** ceiling on a parser's term list — a runaway parser is a red, not a bill */
300
+ export const MAX_TERMS: 8;
301
+ /**
302
+ * WHICH KIND of instrument stop, in the ARBITER's own four-row vocabulary
303
+ * (`CLOSE_FAULTS`, src/ralph.js). Stamped at the site that OBSERVES the fault —
304
+ * never sniffed out of the stop's prose downstream (the typed-attribution rule:
305
+ * a `lib`/fault field is stamped at the throw site, and prose is a fallback for
306
+ * pre-typed spines only).
307
+ *
308
+ * The rows are kept apart for the reason `CLOSE_FAULTS` keeps them apart: they
309
+ * are DIFFERENT HUMAN DECISIONS. "raise the close timeout", "fix the argv" and
310
+ * "re-run, it was OOM-killed" are three answers, and a declared close that
311
+ * pooled all three into `failed` would hand every one of them the same option
312
+ * list. `failed` is the default because "the close cannot run" is the safe
313
+ * reading of a fault nothing else claimed.
314
+ */
315
+ export const STOP_FAULTS: Readonly<{
316
+ /** the stage cannot RUN at all: spawn failure, a missing seed commit, git refusing */
317
+ FAILED: "failed";
318
+ /** it ran and never finished judging */
319
+ TIMED_OUT: "timed-out";
320
+ /** it died by signal — no opinion was ever rendered */
321
+ KILLED: "killed";
322
+ /** it ran, came back, and judged nothing: unreadable output, a blown buffer,
323
+ * a number that was never reported, a parser that does not normalise */
324
+ CRASHED: "crashed";
325
+ }>;
326
+ export const AGGREGATES: readonly string[];
327
+ export const SIGNS: readonly number[];
328
+ /** every kind this executor implements. `judged-floor` / `human-confirms`
329
+ * are catalogue entries and are deliberately absent here: declaring one is
330
+ * counted demand (the request-red path), and reaching one at runtime is a
331
+ * stop — never a red, and never a silent pass. */
332
+ export const LIVE_KINDS: readonly string[];
333
+ /**
334
+ * The arbiter's book NAMES, one home. F98's mechanism was exactly this list
335
+ * living as independent literals: the fence deny arrays (src/planrun.js,
336
+ * src/authorscout.js) knew `.smoke` while `isArbiterBook` — the reader that
337
+ * decides what COUNTS AS WORK — did not, and the drift was masked by the
338
+ * patient's own .gitignore. Every consumer (the fence deny lists, this file's
339
+ * reader, the persona's stated law, run.js's smoke-store root) now spells the
340
+ * names from here, so adding or renaming a book is one edit that cannot leave
341
+ * a site behind.
342
+ *
343
+ * `ARBITER_BOOK_STORES` are directory PREFIXES; `GATE_AUDIT_FILE` is an EXACT
344
+ * repo-relative path (see `isArbiterBook`'s docstring for why never a
345
+ * basename). Order is the fence deny-list order and the persona's prose order.
346
+ */
347
+ export const SMOKE_STORE: ".smoke";
348
+ export const LITECTX_STORE: ".litectx";
349
+ export const GATE_AUDIT_FILE: "gate-audit.jsonl";
350
+ export const ARBITER_BOOK_STORES: readonly string[];
351
+ export type Red = {
352
+ code: string;
353
+ path: string;
354
+ detail: string;
355
+ };
356
+ /**
357
+ * The arbiter's half of a stage run. Every field here is operator/runner
358
+ * territory: a declaration parameterises kinds, never the contracts.
359
+ */
360
+ export type Ctx = {
361
+ workdir: string;
362
+ seedRef: string;
363
+ gapKeep: string;
364
+ timeoutMsDefault?: number;
365
+ baselineMode?: "auto" | "worktree";
366
+ gapCap?: number;
367
+ maxBuffer?: number;
368
+ seedTrees?: SeedTrees;
369
+ };
370
+ export type StageResult = {
371
+ verdict: "green" | "red" | "instrument-stop" | "not-reached";
372
+ exitCode: number | null;
373
+ value: number | null;
374
+ baseline: number | null;
375
+ baselineSource: string | null;
376
+ gapLines: string[];
377
+ judged: boolean;
378
+ stage: string | null;
379
+ kind: string | null;
380
+ detail: Record<string, any>;
381
+ };
382
+ export type SeedTrees = {
383
+ ensure: (workdir: string, seedRef: string) => Promise<{
384
+ stop: string;
385
+ fault: string;
386
+ } | {
387
+ stop: null;
388
+ dir: string;
389
+ }>;
390
+ cleanup: () => Promise<{
391
+ ok: boolean;
392
+ leaked: string[];
393
+ detail: string | null;
394
+ }>;
395
+ };
package/types/plan.d.ts CHANGED
@@ -48,6 +48,24 @@ export function legalScopes(writeScope: string[], dirs?: string[], cap?: number)
48
48
  * @returns {any[]|null} the stage list, or null when the close names no command
49
49
  */
50
50
  export function stageClose(close: any): any[] | null;
51
+ /**
52
+ * THE ONE STAGING, one level up: a job's close stages, from EITHER of the two
53
+ * fields a spec may carry.
54
+ *
55
+ * `stageClose` reads a command close; `declaredStages` reads an authored
56
+ * declaration (M4). A spec declares exactly one of them — the job validator reds
57
+ * `close-duplicated` on both — so this is a selection, never a merge.
58
+ *
59
+ * Every consumer calls THIS rather than `stageClose` directly, for the reason
60
+ * W4 wrote `stageClose` in the first place: the drafting prompt, the plan
61
+ * validator, the runner, the bridge registry and the reuse envelope must all see
62
+ * the same stage list, and a consumer that reads only `close` sees a declared
63
+ * job as having no close at all — an empty check menu offered to a drafter whose
64
+ * plan the validator would then red for referencing a stage nobody offered.
65
+ * @param {any} job the job spec, any shape
66
+ * @returns {any[]|null} the stage list, or null when the job names no close
67
+ */
68
+ export function closeStagesOf(job: any): any[] | null;
51
69
  /** @typedef {{code: string, path: string, detail?: string, verb?: string}} Red */
52
70
  /**
53
71
  * Validate an agent-authored plan (`schema: "plan-v1"`) against the SIGNED