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,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, 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,343 @@
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 two arbiter books, and the shape of each exclusion is the whole point.
40
+ *
41
+ * `.litectx/` is a PREFIX — everything beneath the store belongs to the
42
+ * arbiter. `gate-audit.jsonl` is an EXACT repo-relative path and never a
43
+ * 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.
48
+ * @param {string} rel a repo-relative POSIX path
49
+ * @returns {string|null} the book it belongs to, or null
50
+ */
51
+ export function isArbiterBook(rel: string): string | null;
52
+ /**
53
+ * Everything this run changed against the seed: the tracked diff PLUS
54
+ * untracked files (an untracked file has no diff — the case a naive
55
+ * `git diff` scan silently misses), minus the arbiter's own books.
56
+ * @param {string} workdir @param {string} seedRef
57
+ * @returns {Promise<{stop: string, fault: string}|{stop: null, paths: string[], tracked: string[], untracked: string[], excluded: {path: string, book: string}[]}>}
58
+ */
59
+ export function changedSet(workdir: string, seedRef: string): Promise<{
60
+ stop: string;
61
+ fault: string;
62
+ } | {
63
+ stop: null;
64
+ paths: string[];
65
+ tracked: string[];
66
+ untracked: string[];
67
+ excluded: {
68
+ path: string;
69
+ book: string;
70
+ }[];
71
+ }>;
72
+ /**
73
+ * The lines this run ADDED to one file. A tracked file's additions come from
74
+ * its own unified diff; an UNTRACKED file has no diff at all, so its ENTIRE
75
+ * body is added — the half of this primitive a diff-only scan misses.
76
+ * @param {string} workdir @param {string} seedRef @param {string} rel
77
+ * @param {{untracked?: boolean}} [o]
78
+ * @returns {Promise<{stop: string, fault: string}|{stop: null, lines: {n: number, text: string}[], note: string|null}>}
79
+ */
80
+ export function addedLines(workdir: string, seedRef: string, rel: string, { untracked }?: {
81
+ untracked?: boolean;
82
+ }): Promise<{
83
+ stop: string;
84
+ fault: string;
85
+ } | {
86
+ stop: null;
87
+ lines: {
88
+ n: number;
89
+ text: string;
90
+ }[];
91
+ note: string | null;
92
+ }>;
93
+ /**
94
+ * Count a pattern's capture groups without running it against real input:
95
+ * `src + '|'` always matches the empty string, so the result array's length
96
+ * names the group count. Also the ONE place a declared regex is compiled at
97
+ * validation time, so a broken pattern reds before any process is spawned.
98
+ * @param {any} src
99
+ * @returns {{count: number, red: string|null}}
100
+ */
101
+ export function regexGroups(src: any): {
102
+ count: number;
103
+ red: string | null;
104
+ };
105
+ /**
106
+ * THE normalisation of a `count-not-worse` parser (gate-1 amendment #5).
107
+ * The declaration validator and this executor both call it, so there is
108
+ * exactly one spelling of what a parser MEANS — two normalisers that
109
+ * disagree are two instruments, and the one that grades is the one nobody
110
+ * validated.
111
+ *
112
+ * value = Σ over terms of sign × (first | sum of that term's captures,
113
+ * inside that term's region)
114
+ *
115
+ * Backwards compatible by construction: the short form
116
+ * `{lineMatch, capture?}` resolves to exactly one term with
117
+ * `sign +1, aggregate "first", region "whole-output"`, which is the
118
+ * pre-amendment semantics unchanged.
119
+ *
120
+ * Never throws — reds come back in the shipped `{ok, reds, ...}` shape.
121
+ * @param {any} parser @param {string} [at] path prefix for reds
122
+ * @returns {{ok: boolean, reds: Red[], terms: any[]|null}}
123
+ */
124
+ export function normalizeParser(parser: any, at?: string): {
125
+ ok: boolean;
126
+ reds: Red[];
127
+ terms: any[] | null;
128
+ };
129
+ /**
130
+ * Read ONE number out of a command's output, as signed arithmetic over
131
+ * normalised terms.
132
+ *
133
+ * The zero-match split is contract (a), not a convenience:
134
+ * - `sum` over zero matches is a COUNTED zero. The region was searched and
135
+ * nothing was there; that is legitimately 0.
136
+ * - `first` over zero matches is UNKNOWN — the number was never reported —
137
+ * and unknown is an instrument stop, never a defaulted 0 (F6).
138
+ * - a region whose anchor matches nothing was never LOCATED: stop.
139
+ * The scope filter runs per OUTPUT LINE, before aggregation, and both its
140
+ * drop count and its unattributable count are ANNOUNCED: a line naming no
141
+ * path is KEPT (no filter can exclude what it cannot locate) and the notes
142
+ * say so, rather than the number quietly meaning something else.
143
+ *
144
+ * `preScopeMatches` is reported alongside the reading because a POST-scope
145
+ * count cannot answer "did the tool run at all". It tallies (term, line) pairs
146
+ * that matched BEFORE the filter judged them — kept, unattributable and dropped
147
+ * alike — and it is the only evidence of liveness a caller has once the scope
148
+ * has removed the whole population. It is never the number being read.
149
+ * @param {string} output @param {any[]} terms normalised terms
150
+ * Every stop here is `crashed` in the arbiter's vocabulary — the command RAN,
151
+ * came back, and the number could not be read out of it. Nothing in this
152
+ * function can observe a spawn failure, a timeout or a signal, so it never
153
+ * claims one.
154
+ * @param {{scope?: any, workdir?: string}} [o]
155
+ * @returns {{stop: string, fault: string, notes?: string[]}|{stop: null, value: number, breakdown: any[], notes: string[], preScopeMatches: number}}
156
+ */
157
+ export function parseValue(output: string, terms: any[], { scope, workdir }?: {
158
+ scope?: any;
159
+ workdir?: string;
160
+ }): {
161
+ stop: string;
162
+ fault: string;
163
+ notes?: string[];
164
+ } | {
165
+ stop: null;
166
+ value: number;
167
+ breakdown: any[];
168
+ notes: string[];
169
+ preScopeMatches: number;
170
+ };
171
+ /**
172
+ * A caller-scoped holder for detached seed worktrees. Scoped rather than
173
+ * module-global on purpose: a global cache in a published library leaks
174
+ * worktrees into whatever process imported it, and nothing would ever remove
175
+ * them. `seedRead`/`runClose` create one, share it across stages (so a
176
+ * multi-stage close pays for one checkout), and remove it in a `finally`.
177
+ * @returns {SeedTrees}
178
+ */
179
+ export function makeSeedTrees(): SeedTrees;
180
+ /**
181
+ * Run ONE declared stage against a workdir at a seed.
182
+ *
183
+ * It never throws. An unexpected throw inside a kind — a malformed parameter
184
+ * the validator did not catch, an unreachable path — is an INSTRUMENT STOP,
185
+ * not a red: that is the casualty-vs-evidence split at its narrowest, and
186
+ * the direction that matters, because a crash rendered as a red is fake
187
+ * evidence about the worker.
188
+ * @param {any} stage `{name, kind, params}`
189
+ * @param {Ctx} ctx
190
+ * @returns {Promise<StageResult>}
191
+ */
192
+ export function runStage(stage: any, ctx: Ctx): Promise<StageResult>;
193
+ /**
194
+ * D12's seed-verdict read: run EVERY declared stage at the seed, regardless
195
+ * of reds, and hand back one row per stage.
196
+ *
197
+ * Not first-red-wins, and the reason is structural rather than a preference:
198
+ * every shipped close opens with `changed-from-seed`, which is RED at its own
199
+ * seed by construction (nothing has changed yet), so a first-red-wins seed
200
+ * read measures exactly one stage and mints no baseline for any other. The
201
+ * baseline every `baseline: "seed"` stage is later graded against comes from
202
+ * here, so a stage that never ran is a stage with no bar.
203
+ * @param {any} declaration `{stages: [...]}`
204
+ * @param {Ctx} ctx
205
+ * @returns {Promise<StageResult[]>}
206
+ */
207
+ export function seedRead(declaration: any, ctx: Ctx): Promise<StageResult[]>;
208
+ /**
209
+ * The GRADING path: stages in declared order, FIRST RED WINS. The deciding
210
+ * stage names the verdict; everything after it is `not-reached` and is
211
+ * reported as such rather than as a green nobody ran.
212
+ *
213
+ * An instrument stop ends the close UNJUDGED — the run has no verdict to
214
+ * carry, which is the whole point of keeping a casualty out of the evidence.
215
+ *
216
+ * NAMED `runDeclaredClose`, not `runClose` (M4's export-surface decision): the
217
+ * arbiter's shell already ships a public `runClose(argv, redact, opts)` that
218
+ * adopters import, and the two take different things and mean different things
219
+ * — one runs a DECLARATION, the other an argv. Renaming the shipped one to make
220
+ * room would break a documented adopter contract for nothing; the new name says
221
+ * exactly what differs. `src/declaredclose.js` is what bridges the two shapes.
222
+ * @param {any} declaration @param {Ctx} ctx
223
+ * @returns {Promise<{verdict: 'green'|'red'|'instrument-stop', exitCode: number, judged: boolean, firstRed: string|null, stages: StageResult[]}>}
224
+ */
225
+ export function runDeclaredClose(declaration: any, ctx: Ctx): Promise<{
226
+ verdict: "green" | "red" | "instrument-stop";
227
+ exitCode: number;
228
+ judged: boolean;
229
+ firstRed: string | null;
230
+ stages: StageResult[];
231
+ }>;
232
+ /** the close's two judgment exits — a stage's verdict, in the shape the
233
+ * hand-written closes already spoke (exit code is truth) */
234
+ export const EXIT_GREEN: 0;
235
+ export const EXIT_RED: 1;
236
+ /** contract (a): the instrument-stop exit. `judged` is withheld with it. */
237
+ export const EXIT_STOP: 97;
238
+ /** The judgment-rendered marker a rendered close prints, for ralph's
239
+ * `runClose` judged floor (src/ralph.js). It rides EVERY real judgment, green or red,
240
+ * and NEVER a stop — this module reports the same fact as the boolean
241
+ * `judged`; the marker exists so a renderer and the floor agree on one
242
+ * spelling. */
243
+ export const JUDGED_MARKER: "judged=1";
244
+ /** contract (b): the per-stage gap ceiling. Trims are announced, never silent. */
245
+ export const GAP_LINE_CAP: 40;
246
+ /**
247
+ * The exact phrase this executor's trim announcement carries. Exported for the
248
+ * same reason ralph exports `GAP_KEEP_TRIM_MARKER`: Layer R's detector must be
249
+ * able to see that a gap window was TRIMMED, because failures beyond the window
250
+ * can move while the visible lines stay identical — and an undetected trim makes
251
+ * the detector claim "reds unchanged" off an instrument that was blind (F43,
252
+ * finding 5). A magic string on the reading side would drift the day this
253
+ * wording changes; the renderer below is its only other use.
254
+ */
255
+ export const GAP_TRIM_MARKER: "gap trimmed:";
256
+ /** used when neither the stage nor the ctx names a bound. A stage that could
257
+ * run forever is not a red — it is a stop, and something must decide when. */
258
+ export const DEFAULT_TIMEOUT_MS: 300000;
259
+ /** PER STREAM, like the arbiter's own close runner. An overrun truncates the
260
+ * output mid-summary, so any number read out of it is unknown, not zero. */
261
+ export const MAX_BUFFER: number;
262
+ /** an untracked file larger than this is not scanned line-by-line; the skip
263
+ * is ANNOUNCED in the gap rather than passing as a clean scan */
264
+ export const MAX_SCAN_BYTES: number;
265
+ /** ceiling on a parser's term list — a runaway parser is a red, not a bill */
266
+ export const MAX_TERMS: 8;
267
+ /**
268
+ * WHICH KIND of instrument stop, in the ARBITER's own four-row vocabulary
269
+ * (`CLOSE_FAULTS`, src/ralph.js). Stamped at the site that OBSERVES the fault —
270
+ * never sniffed out of the stop's prose downstream (the typed-attribution rule:
271
+ * a `lib`/fault field is stamped at the throw site, and prose is a fallback for
272
+ * pre-typed spines only).
273
+ *
274
+ * The rows are kept apart for the reason `CLOSE_FAULTS` keeps them apart: they
275
+ * are DIFFERENT HUMAN DECISIONS. "raise the close timeout", "fix the argv" and
276
+ * "re-run, it was OOM-killed" are three answers, and a declared close that
277
+ * pooled all three into `failed` would hand every one of them the same option
278
+ * list. `failed` is the default because "the close cannot run" is the safe
279
+ * reading of a fault nothing else claimed.
280
+ */
281
+ export const STOP_FAULTS: Readonly<{
282
+ /** the stage cannot RUN at all: spawn failure, a missing seed commit, git refusing */
283
+ FAILED: "failed";
284
+ /** it ran and never finished judging */
285
+ TIMED_OUT: "timed-out";
286
+ /** it died by signal — no opinion was ever rendered */
287
+ KILLED: "killed";
288
+ /** it ran, came back, and judged nothing: unreadable output, a blown buffer,
289
+ * a number that was never reported, a parser that does not normalise */
290
+ CRASHED: "crashed";
291
+ }>;
292
+ export const AGGREGATES: readonly string[];
293
+ export const SIGNS: readonly number[];
294
+ /** every kind this executor implements. `judged-floor` / `human-confirms`
295
+ * are catalogue entries and are deliberately absent here: declaring one is
296
+ * counted demand (the request-red path), and reaching one at runtime is a
297
+ * stop — never a red, and never a silent pass. */
298
+ export const LIVE_KINDS: readonly string[];
299
+ export type Red = {
300
+ code: string;
301
+ path: string;
302
+ detail: string;
303
+ };
304
+ /**
305
+ * The arbiter's half of a stage run. Every field here is operator/runner
306
+ * territory: a declaration parameterises kinds, never the contracts.
307
+ */
308
+ export type Ctx = {
309
+ workdir: string;
310
+ seedRef: string;
311
+ gapKeep: string;
312
+ timeoutMsDefault?: number;
313
+ baselineMode?: "auto" | "worktree";
314
+ gapCap?: number;
315
+ maxBuffer?: number;
316
+ seedTrees?: SeedTrees;
317
+ };
318
+ export type StageResult = {
319
+ verdict: "green" | "red" | "instrument-stop" | "not-reached";
320
+ exitCode: number | null;
321
+ value: number | null;
322
+ baseline: number | null;
323
+ baselineSource: string | null;
324
+ gapLines: string[];
325
+ judged: boolean;
326
+ stage: string | null;
327
+ kind: string | null;
328
+ detail: Record<string, any>;
329
+ };
330
+ export type SeedTrees = {
331
+ ensure: (workdir: string, seedRef: string) => Promise<{
332
+ stop: string;
333
+ fault: string;
334
+ } | {
335
+ stop: null;
336
+ dir: string;
337
+ }>;
338
+ cleanup: () => Promise<{
339
+ ok: boolean;
340
+ leaked: string[];
341
+ detail: string | null;
342
+ }>;
343
+ };
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
@@ -252,12 +252,18 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
252
252
  * default; a caller supplies this to drive time deterministically (the same seam `createClock`
253
253
  * already exposes — a run's terminal cannot otherwise be exercised without waiting out a cap
254
254
  * whose floor is one close timeout).
255
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed leg
256
+ * recorded on its own spine (`readResume`'s `restart.branch`, off the `work-branch`
257
+ * event). With it the resume returns to the branch its own work is sitting on; without
258
+ * it the deterministic name would collide with that very branch and the collision walk
259
+ * would mint a `-2` beside the progress the resume exists to keep. Absent is the cold
260
+ * path. A recorded branch that no longer exists is a STOP, never a fresh start.
255
261
  * @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
256
262
  * 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
257
- * 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' | 'step-stalled' |
258
- * `step-red:<id>`
263
+ * 'branch-red' | 'cap-halt' | 'wall-halt' | 'provider-red' | 'interpreter-red' |
264
+ * 'step-stalled' | `step-red:<id>`
259
265
  */
260
- export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
266
+ export function runPlan(job: any, { workdir, provider, nativeProvider, providerFor, emit, remainingUsd, isUnpriced, spendComplete, capRuns, strikeLimit, closeTimeoutMs, maxStepRounds, layerRoot, scoutRounds, bridge, now, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch }: {
261
267
  workdir: string;
262
268
  provider: any;
263
269
  nativeProvider?: ((o: {
@@ -289,6 +295,7 @@ export function runPlan(job: any, { workdir, provider, nativeProvider, providerF
289
295
  } | null | undefined;
290
296
  spendComplete?: (() => boolean) | undefined;
291
297
  now?: (() => number) | undefined;
298
+ resumeBranch?: string | null | undefined;
292
299
  }): Promise<string>;
293
300
  export type CategorizedError = Error & {
294
301
  category?: string;
package/types/ralph.d.ts CHANGED
@@ -272,6 +272,7 @@ export const CLOSE_ENV_DENY: Readonly<{
272
272
  prefixes: readonly string[];
273
273
  shape: RegExp;
274
274
  }>;
275
+ export function stageGap(name: string, gap: string): string;
275
276
  export const GAP_KEEP_TRIM_MARKER: "more elided by the";
276
277
  /**
277
278
  * The forbidden zone, named (PRD v1.11). Each row is an outcome in which the
package/types/run.d.ts CHANGED
@@ -83,6 +83,13 @@
83
83
  * RUN's ("unlimited replanning launders thrash as adaptation"). It also rides onto this
84
84
  * run's `job-start` as `priorReplans`, the declared-fold precedent `priorSpentUsd`
85
85
  * already sets, so a resume OF a resume folds once. Absent is the cold path.
86
+ * @param {string|null} [opts.resumeBranch] RESUME — the WORK BRANCH the killed attempt
87
+ * created and recorded on its own spine (`readResume`'s `restart.branch`). The branch
88
+ * name is DERIVED from the signed spec and is therefore identical on every leg, so
89
+ * without this the restart would collide with its own predecessor's branch and the
90
+ * collision walk would mint a `-2` beside the work the resume exists to continue. The
91
+ * fold's precedent exactly: the chain's state is DECLARED, never re-derived. Forwarded
92
+ * verbatim to the plan flow; absent is the cold path.
86
93
  * @param {boolean} [opts.layerRoot=false] Layer R (within-run ratchet) — shell
87
94
  * territory, threaded to the plan flow. Defaults OFF
88
95
  * (decided 2026-07-21): fixation is extinct on every current job (F41), so
@@ -92,10 +99,10 @@
92
99
  * binding.)
93
100
  * @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
94
101
  * 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
95
- * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' | 'provider-red' |
102
+ * 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
96
103
  * 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' | `step-red:<id>`
97
104
  */
98
- export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans }: {
105
+ export function runJob(rawSpec: object | string, { approvals, workdir, provider, nativeProvider, providerFor, emit, capRuns, strikeLimit, shellCapUsd, closeTimeoutMs, layerRoot, bridge, priorSpentUsd, priorSpendComplete, priorWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch }: {
99
106
  approvals: unknown;
100
107
  workdir: string;
101
108
  provider: any;
@@ -124,6 +131,7 @@ export function runJob(rawSpec: object | string, { approvals, workdir, provider,
124
131
  count?: number;
125
132
  grantUsed?: boolean;
126
133
  } | null | undefined;
134
+ resumeBranch?: string | null | undefined;
127
135
  layerRoot?: boolean | undefined;
128
136
  }): Promise<string>;
129
137
  export type Red = {