bareloop 0.10.0 → 0.12.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.
@@ -68,11 +68,14 @@
68
68
  // once instead of once-per-invocation, but `runPlan` has a dozen return paths and
69
69
  // a leaked worktree outlives the process that made it.
70
70
 
71
- import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED } from './kinds.js';
71
+ import { runStage, makeSeedTrees, STOP_FAULTS, EXIT_RED, JUDGED_FLOOR_KIND } from './kinds.js';
72
72
  import {
73
73
  validateDeclaration, classGuards, genreOwnedEnvNames, ungroundedGenreEnv, GENRE_LANGUAGES, TYPES_GENRE,
74
- VERDICT_CLASSES, LIVE_CLASSES,
74
+ VERDICT_CLASSES, LIVE_CLASSES, NEVER_OFFERED_KINDS,
75
75
  } from './authoring.js';
76
+ // the calibration set's own gate lives beside the rulebook it is graded against
77
+ // (src/judged.js) — one opinion about what a judged floor's signed artifacts are
78
+ import { validateCalibrationSet } from './judged.js';
76
79
  import { stageGap } from './ralph.js';
77
80
  import { isObj, isNonEmptyString } from './validate.js';
78
81
 
@@ -99,7 +102,14 @@ export const DECLARED_GENRES = Object.freeze([TYPES_GENRE.name]);
99
102
  * constrained to the names the genre OWNS, so it can never become an environment
100
103
  * channel of its own.
101
104
  */
102
- export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv']);
105
+ export const CLOSE_DECL_FIELDS = Object.freeze(['genre', 'lang', 'stages', 'notes', 'genreEnv', 'calibration']);
106
+
107
+ /** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
108
+ * tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
109
+ * is the whole mechanism behind "a judge-model bump forces a full recalibration"
110
+ * (src/judged.js, `JUDGE_MODEL`): the field moves, the hash moves, the signature dies,
111
+ * and the re-sign re-runs the gate. Anything else in there is a field nobody reads. */
112
+ export const CALIBRATION_FIELDS = Object.freeze(['cases', 'judgeModel']);
103
113
 
104
114
  /**
105
115
  * The prefix M1 puts on EVERY gap line of a declared stage (its `ctx.gapKeep`).
@@ -112,12 +122,75 @@ export const DECLARED_GAP_PREFIX = 'close: ';
112
122
  * whole judged output rather than a hand-picked slice of it. */
113
123
  export const DECLARED_GAP_KEEP = `^${DECLARED_GAP_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`;
114
124
 
115
- /** The verdict class a DECLARED close may claim. Hard, and by CONSTRUCTION
116
- * rather than by assertion: every live kind is a mechanical command whose
117
- * measurement is the truth, and the two judgment kinds are locked out at the
118
- * catalogue (M2) and absent from the executor (M1). Named as data so the job
119
- * validator reads one hierarchy table rather than hard-coding a class. */
120
- export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard']);
125
+ /**
126
+ * The close verdict word for a run WAITING on a person (N4 §1.3/§1.4). A
127
+ * literal, exported, because three readers key on it — the runner's pause
128
+ * branch, the ledger's excluded set, and the trend reader's blind rule — and
129
+ * three hand-spelled copies of one terminal is how they come to disagree.
130
+ *
131
+ * It is not in `CLOSE_FAULTS` and must never be added: a fault says the close
132
+ * could not render a verdict, and this says the close is not FINISHED rendering
133
+ * one. The first escalates; the second is a checkpoint the operator returns to.
134
+ */
135
+ export const HUMAN_PAUSE = 'human-pause';
136
+
137
+ /**
138
+ * The RUN-level terminal names the hitl class mints one layer up from
139
+ * `HUMAN_PAUSE` (N4 §1.3–§1.5): the checkpoint, and the honest refusal of the
140
+ * operator's rerun input. Spelled once here, beside the close-verdict word they
141
+ * translate, for the same reason `HUMAN_PAUSE` is — the emitters (`planrun.js`),
142
+ * the ledger's excluded set, the resume reader (`reuse.js`), and the runner
143
+ * script all key on them, and hand-spelled copies of one terminal is how they
144
+ * come to disagree.
145
+ *
146
+ * There were three (2026-08-17): `hitl-cancel` was the signer's own terminal and
147
+ * is DELETED with the door it belonged to. A person who does not want to carry on
148
+ * now PAUSES — the same checkpoint, resumable, retired by the TTL if nobody comes
149
+ * back — so nothing in this tree mints that name any more. The ledger keeps the
150
+ * bare string in its excluded set so a spine written before this change still
151
+ * reads as governance rather than as a capability gap; that is a READER's
152
+ * backward compatibility, and deliberately not a constant a writer could reach.
153
+ */
154
+ export const HITL_PAUSE = 'hitl-pause';
155
+ export const HITL_DECISION_RED = 'hitl-decision-red';
156
+
157
+ /**
158
+ * The terminals above that leave the run WAITING ON A PERSON — as a list, minted
159
+ * beside the names it holds so a reader keys on membership rather than on a
160
+ * spelling, and a rename here travels to every reader for free.
161
+ *
162
+ * It is deliberately NOT `reuse.js`'s `CHECKPOINT_OUTCOMES`, which is the wider
163
+ * RESUME list: every stop that left work on disk and an allowance unspent, money
164
+ * and time included. Those are governance stops the reuse loop is meant to try
165
+ * again under its remaining tries; this is the half no further attempt can
166
+ * change, because the allowance still unspent is a human answer. `CHECKPOINT_OUTCOMES`
167
+ * is composed FROM this list so the two can never disagree about which is which.
168
+ *
169
+ * `HITL_DECISION_RED` is not here: it is a verdict already rendered on the operator's
170
+ * own input, not a question still open.
171
+ */
172
+ export const HUMAN_CHECKPOINTS = Object.freeze([HITL_PAUSE]);
173
+
174
+ /** The verdict classes a DECLARED close may claim. Named as data so the job
175
+ * validator reads one hierarchy table rather than hard-coding a class.
176
+ *
177
+ * `hard` is by CONSTRUCTION: every mechanical kind is a command whose measurement
178
+ * is the truth. `hitl` joined it at N4 slice 1, and `soft` at softgreen module 3
179
+ * — each only because its kind is genuinely LIVE (`human-confirms`, then
180
+ * `judged-floor`): the widening and the kind go in one commit, or this table
181
+ * admits a class the executor cannot render.
182
+ *
183
+ * `hitl` STAYS despite the class's retirement (PRD v1.71: the human stage moved
184
+ * out of the close and became the review door at the end of every run). It is
185
+ * kept because `jobs/litectx-maintainer.json` is a SIGNED spec carrying a hitl
186
+ * declaration, and a signed spec is untouchable — dropping the spelling here
187
+ * would invalidate a signature the operator already gave, which is a re-sign
188
+ * nobody asked for. Retiring it is a separate, deliberate deletion.
189
+ *
190
+ * The direction that matters is unchanged. This says which class a declaration
191
+ * MAY claim; `closeCeiling` says which class its stages actually NEED. A pick
192
+ * below the ceiling is still the silent upgrade `class-ceiling` refuses. */
193
+ export const DECLARED_CLOSE_CLASSES = Object.freeze(['hard', 'soft', 'hitl']);
121
194
 
122
195
  /** @param {any} job @returns {boolean} */
123
196
  export function isDeclaredClose(job) {
@@ -130,12 +203,38 @@ export function isDeclaredClose(job) {
130
203
  *
131
204
  * `offer` and `needs` pass through untouched when a declaration carries them
132
205
  * (v1's schema does not let one), so `checkMenu`'s derivation is unchanged.
206
+ *
207
+ * THE JUDGE TIER RIDES ALONG, and it rides as a STAMP for the same reason `offer`
208
+ * does. The tier that graded this close's calibration set is a CLOSE-level fact
209
+ * (`calibration.judgeModel`), and the stage that would buy a judge call is a STAGE;
210
+ * `runStage` is handed stages and never the declaration they came out of. Rather than
211
+ * plumb a second close-level channel down through every executor seam, the one place
212
+ * that already turns a declaration into stages carries it across — and it carries it
213
+ * from the SIGNED bytes, so a stage can never name a tier the signature does not.
214
+ * `runJudgedFloor` refuses a mismatch; the absence of a stamp is the SPEC gate's
215
+ * business (`validateCloseDecl` reds a set with no model), never a runtime fall-back.
133
216
  * @param {any} closeDecl
134
217
  * @returns {any[]|null} null when this is not a declaration
135
218
  */
136
219
  export function declaredStages(closeDecl) {
137
220
  if (!isObj(closeDecl) || !Array.isArray(closeDecl.stages)) return null;
138
- return closeDecl.stages.map((s) => (isObj(s) ? { ...s, gapKeep: DECLARED_GAP_KEEP } : s));
221
+ const calibrationJudgeModel = isObj(closeDecl.calibration) && isNonEmptyString(closeDecl.calibration.judgeModel)
222
+ ? closeDecl.calibration.judgeModel
223
+ : null;
224
+ return closeDecl.stages.map((s) => (isObj(s)
225
+ // RULING 5, stamped by the ARBITER rather than trusted from the artefact:
226
+ // a judged or human stage is `offer: false` BY LAW, so `checkMenu` can never
227
+ // hand the agent `check-passes(<a person>)`. The validator refuses a
228
+ // declaration that says anything else (`human-stage-offered`), so this is
229
+ // never overwriting a signer-visible value — it is filling in a law the
230
+ // declaration schema deliberately gives the composer no way to state.
231
+ ? {
232
+ ...s,
233
+ gapKeep: DECLARED_GAP_KEEP,
234
+ ...(NEVER_OFFERED_KINDS.includes(s.kind) ? { offer: false } : {}),
235
+ ...(s.kind === JUDGED_FLOOR_KIND && calibrationJudgeModel !== null ? { calibrationJudgeModel } : {}),
236
+ }
237
+ : s));
139
238
  }
140
239
 
141
240
  // ── the spec-level gate ──────────────────────────────────────────────────────
@@ -310,6 +409,56 @@ export function validateCloseDecl(closeDecl, opts = {}) {
310
409
  });
311
410
  for (const r of v.reds) reds.push({ ...r, path: r.path ? `${at}.${r.path}` : at });
312
411
 
412
+ // ── THE FROZEN CALIBRATION SET (softgreen module 4, design §4.4). It rides
413
+ // the closeDecl rather than a stage's params because it calibrates the RULER,
414
+ // not one stage's parameters — and because it is read BEFORE any stage runs.
415
+ //
416
+ // WHAT IS CHECKED HERE is that a stored set is LEGAL: exactly the signed size,
417
+ // both polarities present, itemized reds naming lines THIS close's card
418
+ // actually carries. WHAT IS NOT is whether the pipe grades it correctly — that
419
+ // is module 5's gate, it costs paid calls, and a validator cannot buy them.
420
+ //
421
+ // A set with NO judged stage to calibrate is its own red: the card lives on
422
+ // the stage, so an orphan set is graded against nothing and would validate
423
+ // clean forever. The reverse (a judged stage with no set) is deliberately NOT
424
+ // redded here — making the set MANDATORY is the signing gate's call, and
425
+ // module 5 owns it.
426
+ if (closeDecl.calibration !== undefined) {
427
+ const judged = (Array.isArray(closeDecl.stages) ? closeDecl.stages : [])
428
+ .filter((/** @type {any} */ s) => isObj(s) && s.kind === JUDGED_FLOOR_KIND);
429
+ if (!isObj(closeDecl.calibration)) {
430
+ red('invalid-value', `${at}.calibration`, 'the frozen calibration set, as an object carrying `cases`');
431
+ } else if (judged.length !== 1) {
432
+ red('calibration-orphan', `${at}.calibration`, `this close carries a calibration set and ${judged.length} `
433
+ + 'judged-floor stage(s). The set calibrates ONE judged ruler against ONE card — with no judged stage it is '
434
+ + 'graded against nothing and would read clean forever, and with two there is no one card it belongs to',
435
+ { judgedStages: judged.length });
436
+ } else {
437
+ for (const key of Object.keys(closeDecl.calibration)) {
438
+ if (!CALIBRATION_FIELDS.includes(key)) red('invalid-value', `${at}.calibration.${key}`, `a calibration set carries ${CALIBRATION_FIELDS.map((f) => `\`${f}\``).join(' and ')} and nothing else`);
439
+ }
440
+ // WHICH TIER GRADED THIS SET, and it is REQUIRED rather than optional. A
441
+ // stored set is only worth the judge that graded it (§4.2's safety argument
442
+ // is BA-20's haiku-4.5 injection evidence and nothing else), so a set with
443
+ // no model beside it is a set nobody can attribute — and the `JUDGE_MODEL`
444
+ // docstring's promise that a bump forces recalibration has nothing to fire
445
+ // on. Required HERE and not at the stage, for the same reason the SET's own
446
+ // presence is the signing gate's call and not this validator's: this gate
447
+ // reads a stored artifact and says whether it is legal. The RUNTIME half —
448
+ // refusing to grade when the stored tier is not the tier this run would buy
449
+ // — is the judged stage's (`runJudgedFloor`), reached through the stamp
450
+ // `declaredStages` puts on the stage.
451
+ if (closeDecl.calibration.judgeModel === undefined) {
452
+ red('missing-required', `${at}.calibration.judgeModel`, 'the judge tier this set was GRADED by — a calibration '
453
+ + 'nobody can attribute to a model is a floor nobody can tell apart from a bumped one');
454
+ } else if (!isNonEmptyString(closeDecl.calibration.judgeModel)) {
455
+ red('invalid-value', `${at}.calibration.judgeModel`, 'the judge tier this set was graded by, as a non-empty string');
456
+ }
457
+ const cv = validateCalibrationSet(closeDecl.calibration.cases, { card: judged[0]?.params?.card ?? null });
458
+ for (const r of cv.reds) reds.push({ ...r, path: r.path ? `${at}.${r.path}` : `${at}.calibration` });
459
+ }
460
+ }
461
+
313
462
  const ok = reds.length === 0;
314
463
  return {
315
464
  ok,
@@ -361,7 +510,7 @@ export function guardNames(closeDecl, verdictType) {
361
510
  * @param {any} r an M1 `StageResult`
362
511
  * @param {(s: string) => string} redact
363
512
  * @returns {{verdict: string, notes?: string, gap?: string, detail?: string,
364
- * exitCode?: number, judgedCount?: number}} `notes` is absent, never empty —
513
+ * exitCode?: number, judgedCount?: number, ask?: string|null}} `notes` is absent, never empty —
365
514
  * a field that is always there says nothing; a RED carries its diagnostics
366
515
  * inside `gap` and so has none of its own.
367
516
  */
@@ -370,6 +519,13 @@ function translate(r, redact) {
370
519
  /** absent rather than empty: a field that is always there says nothing */
371
520
  const notes = gapText ? { notes: gapText } : {};
372
521
  if (r.verdict === 'green') return { verdict: 'satisfied', judgedCount: 1, ...notes };
522
+ // THE PAUSE (N4 §1.3) — its own verdict word, and deliberately NOT one of the
523
+ // three below. `satisfied` would mint a green nobody gave; `needs_revision`
524
+ // would hand the fix worker a gap the person has not written yet; a
525
+ // `CLOSE_FAULTS` word would file a working arbiter as a broken instrument.
526
+ // No `gap` and no `judgedCount`: nothing was judged, and every consumer guards
527
+ // with `if (gap)`.
528
+ if (r.verdict === 'pause') return { verdict: HUMAN_PAUSE, ask: r.detail?.ask ?? null, ...notes };
373
529
  if (r.verdict === 'red') {
374
530
  return {
375
531
  verdict: 'needs_revision',
@@ -408,11 +564,17 @@ function translate(r, redact) {
408
564
  * @param {any[]} stages declared stage descriptors, already validated
409
565
  * @param {(s: string) => string} [redact] the scrub, applied at THIS boundary
410
566
  * @param {{timeoutMs?: number, cwd?: string, seedRef?: string,
411
- * seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree'}} [opts]
567
+ * seedTrees?: any, gapCap?: number, maxBuffer?: number, baselineMode?: 'auto'|'worktree',
568
+ * humanRuling?: {decision: string, text?: string|null}|null,
569
+ * judgeLoop?: ((o: {system: string}) => any)|null,
570
+ * onJudgeCost?: ((c: any) => void)|null}} [opts]
412
571
  * @returns {Promise<any>}
413
572
  */
414
573
  export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
415
- const { timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode } = opts;
574
+ const {
575
+ timeoutMs, cwd, seedRef, seedTrees: shared, gapCap, maxBuffer, baselineMode, humanRuling = null,
576
+ judgeLoop = null, onJudgeCost = null,
577
+ } = opts;
416
578
  if (!isNonEmptyString(cwd) || !isNonEmptyString(seedRef)) {
417
579
  // Refuse rather than guess. A declared close measures against a SEED; a
418
580
  // missing one is the close unable to run, not a red about the worker.
@@ -436,6 +598,19 @@ export async function runDeclaredStages(stages, redact = (s) => s, opts = {}) {
436
598
  ...(gapCap !== undefined ? { gapCap } : {}),
437
599
  ...(maxBuffer !== undefined ? { maxBuffer } : {}),
438
600
  ...(baselineMode !== undefined ? { baselineMode } : {}),
601
+ // the SIGNER's answer, when this close has a human stage and a leg is
602
+ // carrying one. Absent (null) is the ordinary path AND the pause path — the
603
+ // human stage reads the absence as "nobody has answered yet", which is the
604
+ // whole of what a pause means.
605
+ humanRuling,
606
+ // THE PAID SEAM and THE METER, both operator territory and both absent on
607
+ // every mechanical close. A judged stage with no seam STOPS as a wiring gap
608
+ // rather than falling back to some other model (M1 owns no provider and the
609
+ // judge tier is pinned), and every locate call it does buy is reported OUT
610
+ // through the meter so the run's ONE ledger can see it — a close has cost $0
611
+ // until this kind, and a budget must fund the attempt PLUS its close.
612
+ judgeLoop,
613
+ onJudgeCost,
439
614
  seedTrees,
440
615
  };
441
616
  try {
package/src/index.js CHANGED
@@ -31,23 +31,36 @@ export { validateJob, jobSpecHash, checkApproval, checkMenu, CLOSE_TYPES, CLASS_
31
31
  export { validatePlan, stageClose, EXIT_TYPES, MAX_EXITS_PER_STEP, MAX_PLAN_STEPS, WRITE_VERBS } from './plan.js';
32
32
  export { snapshotScope, evalExits } from './exits.js';
33
33
  export { runPlan } from './planrun.js';
34
- export { runJob } from './run.js';
34
+ // `ACCOUNTED_ROUND_TYPES` rides with it: the round records the ONE ledger sums.
35
+ // A runner reconstructing a killed run's spend (the resume fold) must add exactly
36
+ // these and nothing else, and a second hand-kept list is how a paid call comes to
37
+ // be free (`judge-round` — the close's own spend — joined at softgreen module 2).
38
+ export { runJob, ACCOUNTED_ROUND_TYPES } from './run.js';
35
39
  // Layer 3 — the REUSE registry (bridge-v1) and D3's display half. The registry is a
36
40
  // directory of plain files at an OPERATOR-supplied path (never a default location), the
37
41
  // load gate is the code half of D2's split, and `renderListing`/`selectionPrompt` are
38
42
  // pure text: they send nothing, parse nothing and decide nothing. The selection CALL and
39
43
  // the pin/shortlist/force-cold flow are the adopter's, which is why the pieces they need
40
44
  // are exported rather than kept internal to the runner.
45
+ //
46
+ // Softgreen module 6 exports the QUARANTINE surface with them: a judged green is minted
47
+ // HELD and earns nothing until the signer's `accept` at the review door releases it
48
+ // (PRD v1.71 §3). `recordDoor` is the pure half (record the disposition, release
49
+ // forward-only) and `reuseEligibility`/`newestEligibleVersion` are what every consumer
50
+ // asks before starting from a stored plan.
41
51
  export {
42
52
  BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate,
43
53
  mintBridge, appendGreen, appendRed,
44
54
  loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists,
55
+ QUARANTINED_VERDICTS, QUARANTINED_CODE, quarantinesCredit, newestEligibleVersion, reuseEligibility, recordDoor,
45
56
  } from './bridges.js';
46
57
  export { renderListing, selectionPrompt } from './selection.js';
47
58
  // Layer 3 modules 4+5 — the D7 envelope and the reuse runner. `runReuse` composes
48
- // `runJob` under an operator-signed envelope (three explicit numbers, tighten-only) and
49
- // is the only thing in the library that WRITES the registry — minting is a graded
50
- // green's privilege (R1), never a caller's. `validateEnvelope`/`resolveTrySpec` are
59
+ // `runJob` under an operator-signed envelope (three explicit numbers, tighten-only). It
60
+ // is not the only registry WRITER any more (2B: `writeRunGreenRow`), but the rule it
61
+ // enforced is unchanged and now lives in both — minting is a graded green's privilege
62
+ // (R1), never a caller's: both writers go through `writeGreenRow`, and neither can mint
63
+ // a row for anything but a green that this run actually earned. `validateEnvelope`/`resolveTrySpec` are
51
64
  // exported because an operator runner has to show the resolved per-try spec's HASH at
52
65
  // its approval gate: a tightened envelope is a new spec version, and the human signs it.
53
66
  // `resolveReuse`/`reuseSpecHash` are the pair that gate must actually PRINT: the try
@@ -60,7 +73,15 @@ export { renderListing, selectionPrompt } from './selection.js';
60
73
  // `resumeTreeGate` is the ruling that a resumed patient is continued dirty and never
61
74
  // reset. Both are read by a runner BEFORE the approval gate, so neither can live behind
62
75
  // `runReuse`'s own entry.
63
- export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate } from './reuse.js';
76
+ // `applyDoorDecision` is the registry half of the REVIEW DOOR (softgreen module 6): the
77
+ // door opens AFTER a run has ended, so a person's answer cannot ride the run's own return
78
+ // path — the runner calls this with the runid it printed at the door.
79
+ // `writeRunGreenRow` is that door's other half (2B), and it is exported for the same
80
+ // reason: a runner that drives `runJob` directly rather than through `runReuse` still ends
81
+ // at a review door, and a door with no row to act on can only describe a held learning
82
+ // credit it has no way to release. STORAGE ONLY — nothing here selects, promotes or reuses
83
+ // a bridge; those stay parked on `layer-3-reuse`.
84
+ export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow } from './reuse.js';
64
85
  export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES } from './ledger.js';
65
86
  // THE WORK BRANCH (PRD v1.57 §3). `workBranchName` is exported so an operator runner can
66
87
  // SHOW, before the approval gate, which branch the run will work on — the same reason the
@@ -89,7 +110,66 @@ export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE }
89
110
  export {
90
111
  runDeclaredClose, runStage, seedRead, seedAtHead, seedListing, changedSet, makeSeedTrees,
91
112
  LIVE_KINDS, STOP_FAULTS, JUDGED_MARKER, EXIT_GREEN, EXIT_RED, EXIT_STOP,
113
+ // N4 slice 1 — the hitl surface an adopting runner needs: the three doors, the
114
+ // gate that reads a signer's answer (and refuses an empty one), and the seed
115
+ // exemption ruling 8 states.
116
+ HUMAN_DECISIONS, SEED_EXEMPT_KINDS, JUDGED_FLOOR_KIND, normalizeHumanRuling, resolveHumanRuling,
117
+ // SOFTGREEN module 2 — the judged stage's own arbiter-owned bounds. An adopter
118
+ // wiring `judgeProvider` needs both numbers to reason about what a judged close
119
+ // can cost: one paid call per artifact, at most one retry each.
120
+ JUDGE_ATTEMPTS, MAX_JUDGED_PATHS,
121
+ // SOFTGREEN module 8 — the REVIEW DOOR's vocabulary: the record type a run
122
+ // writes when it ends at the door, which terminals open one, which classes open
123
+ // one unasked, and the mechanical-only rule an `accept` re-proves against.
124
+ REVIEW_DOOR, DOOR_OPEN_OUTCOMES, REVIEW_DOOR_CLASSES, doorOpens, mechanicalStages,
92
125
  } from './kinds.js';
126
+ // SOFTGREEN module 8 — the door's ANSWERING half. A run OPENS the door on its own
127
+ // spine and ends; a person answers minutes or days later, from another process, so
128
+ // the answer cannot ride the run's return path. `answerReviewDoor` is that seam:
129
+ // it re-proves the tree for an `accept`, releases a held judged green through
130
+ // module 6's registry half, and refuses an expired door under the same 60-day TTL a
131
+ // hitl checkpoint keeps. It never returns, writes or implies a verdict.
132
+ export { answerReviewDoor, doorRecordOf, doorAgeGate } from './reviewdoor.js';
133
+ // SOFTGREEN module 1 — the judged floor's core. The pieces an adopter (and an
134
+ // integrating UI) genuinely needs: the PIN (`JUDGE_MODEL` — what a wired judge
135
+ // provider must be bound to, and a bump of which forces recalibration), the
136
+ // rulebook a card SELECTS from, the card gate, and the two halves themselves so a
137
+ // calibration harness can grade the whole pipe without running a close.
138
+ // `defaultJudgeLoop` is the one spelling of how this repo drives a judge; the
139
+ // runner reaches it through `runPlan`'s `judgeProvider`, and it is exported so a
140
+ // caller building its own harness does not spell a second one.
141
+ export {
142
+ JUDGE_MODEL, JUDGE_MAX_TOKENS, JUDGE_RULES, JUDGE_RULE_IDS, LOCATE_AXES, LOCATE_LABEL,
143
+ validateCard, validateFacts, locatePrompt, runLocate, decide, defaultJudgeLoop,
144
+ // SOFTGREEN module 4 — what a legal SIGNED calibration set is. The SIZE is
145
+ // hamr's own ruling and a size change is a spec-level threshold change;
146
+ // `expectedOf` is the ONE reduction of a `decide()` result to the shape a case
147
+ // stores, so a calibration harness compares one shape rather than two.
148
+ CALIBRATION_SIZE, CASE_VERDICTS, validateCalibrationSet, validateJudgedArtifacts, expectedOf,
149
+ } from './judged.js';
150
+ // SOFTGREEN module 4 — the COMPILE. Q6 becomes the rubric card and Q7 becomes the
151
+ // frozen calibration set, both on the D5 shown-and-fixed path: the LLM proposes
152
+ // through `proposeJudgedArtifacts`, an integrating UI shows the proposal, the
153
+ // SIGNER's fix goes through `signJudgedArtifacts`, and `foldJudgedArtifacts`
154
+ // enumerates both into the closeDecl — where the spec hash covers them, so a card
155
+ // line or a case is a re-sign like every other spec edit.
156
+ export {
157
+ proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts,
158
+ proposalSchema, proposalTool, cardCasesPrompt,
159
+ PROPOSAL_TOOL_NAME, PROPOSAL_LABEL, MAX_PROPOSAL_RETRIES, COMPILE_SYSTEM,
160
+ } from './cardauthor.js';
161
+ // SOFTGREEN module 5 — THE CALIBRATION GATE. `runCalibration` grades the whole
162
+ // pipe (locate + decide) over the signed ten and the five arbiter-owned injection
163
+ // artifacts; `prepareSigning` runs it and refuses on anything short of all of
164
+ // them. Exported because an integrating UI has to be able to RUN and RENDER the
165
+ // gate — the graded rows are what a signer reads before giving a signature — and
166
+ // because the battery is evidence a reader may want to inspect rather than take
167
+ // on trust. `INJECTION_LOCATE_BATTERY` and `INJECTION_CARD` are ARBITER-OWNED
168
+ // constants: no signer authors them and nothing stores them in a spec.
169
+ export {
170
+ runCalibration, compareExpectation, factsResist, artifactHash,
171
+ INJECTION_LOCATE_BATTERY, INJECTION_CARD, CALIBRATION_LABEL, CASUALTY_AXES,
172
+ } from './calibrate.js';
93
173
  // M2 — what a declaration may SAY, and whether one said it legally. The
94
174
  // catalogue and the genre are DATA an integrating UI renders (the kind menu, the
95
175
  // guard batteries it must show the user under D5 and cannot let them remove).
@@ -102,7 +182,9 @@ export {
102
182
  DIRECTIONS, BASELINES, MAX_STAGES,
103
183
  // the verdict-class surface (PRD v1.57 §1–§2): the radio's own menu, the guard
104
184
  // battery keyed off it, and the ceiling that makes the pick a promise
105
- VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, closeCeiling,
185
+ VERDICT_CLASSES, LOCKED_CLASSES, LIVE_CLASSES, CLASS_BATTERIES, classGuards, classMenu, closeCeiling,
186
+ // ruling 5 as data: the kinds no agent may ever be offered as an in-run ruler
187
+ NEVER_OFFERED_KINDS,
106
188
  } from './authoring.js';
107
189
  // M3 — the looking half and the writing half. `authorClose` is the grounded
108
190
  // loop; `runAuthorScout`/`buildSeedListing` are exported because a caller that
@@ -119,7 +201,7 @@ export {
119
201
  export { scrubRaw, RAW_PERSIST_MAX, RAW_TRIM_MARKER } from './text.js';
120
202
  export {
121
203
  authorClose, authorPrompt, declarationSchema, makeLoopGenerate, MAX_REVISIONS,
122
- QUESTION_SETS, GREEN_QUESTIONS, CLASS_STATEMENTS, questionsFor, requiredAnswersFor,
204
+ QUESTION_SETS, GREEN_QUESTIONS, SOFTGREEN_QUESTIONS, CLASS_STATEMENTS, questionsFor, requiredAnswersFor,
123
205
  } from './authorflow.js';
124
206
  // M4a — the runtime bridge. `validateCloseDecl` is the spec-level gate (the job
125
207
  // validator's own branch calls it); `runDeclaredStages` is the executor seam a
@@ -128,6 +210,7 @@ export {
128
210
  export {
129
211
  validateCloseDecl, runDeclaredStages, declaredStages, isDeclaredClose, guardNames, closeGrade,
130
212
  DECLARED_GENRES, DECLARED_GAP_PREFIX, DECLARED_GAP_KEEP, DECLARED_CLOSE_CLASSES, CLOSE_DECL_FIELDS,
213
+ HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED, HUMAN_CHECKPOINTS,
131
214
  } from './declaredclose.js';
132
215
  export { closeStagesOf } from './plan.js';
133
216
  // M4b — the interview, the refusal, the composition, and D9's three gates.
package/src/job.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Job-spec validator — the OPERATOR-owned half of the config story (N1,
2
- // design record: docs/plans/2026-07-12-n1-job-close-schema-design.md).
2
+ // design record: docs/02-features/2026-07-12-n1-job-close-schema-design.md).
3
3
  // A job spec is the arbiter's rulebook: the close chain, the budget, the
4
4
  // outer write fence, the escalation route. It is pure declarative data —
5
5
  // no freeform code is expressible anywhere in it — and it validates
@@ -98,12 +98,30 @@ export const MIN_WALL_MS = 120_000;
98
98
  * counts admission demand, and a generic invalid-value would bury it as a typo. */
99
99
  export const LOCKED_TOOLS = Object.freeze(['run']);
100
100
  /** Layer 2 (design record 2026-07-21, decision 5): the verdict-type radio of
101
- * the four-field plan shape. v1 ADMITS only `green`; soft-green/hitl are
102
- * declared-but-locked — declaring one is a `request-red` (the tool-menu
103
- * pattern: disclosure ≠ admission), so the ledger counts the demand and the
104
- * non-code rung later fills a declared slot instead of reshaping the schema. */
101
+ * the four-field plan shape. All three are ADMITTED as of softgreen module 3;
102
+ * the declared-but-locked mechanism below (`LOCKED_VERDICTS`) is what carried
103
+ * them here one rung at a time — declaring a locked one is a `request-red` (the
104
+ * tool-menu pattern: disclosure ≠ admission), so the ledger counted the demand
105
+ * instead of the schema being reshaped when the rung finally landed. */
105
106
  export const VERDICT_TYPES = Object.freeze(['green', 'soft-green', 'hitl']);
106
- export const LOCKED_VERDICTS = Object.freeze(['soft-green', 'hitl']);
107
+ /** N4 slice 1 admitted `hitl`; softgreen module 3 admits `soft-green`, and the
108
+ * menu is now EMPTY — every class the radio names is built.
109
+ *
110
+ * `soft-green`'s lock was never a policy, it was a missing floor: a judged
111
+ * verdict with no ruler mints nothing honestly. Modules 1 and 2 built the ruler
112
+ * (a pinned judge that only LOCATES, an arbiter-owned `decide()`, a calibration
113
+ * gate) and wired it as the `judged-floor` kind, so the lock has nothing left to
114
+ * protect (design record 2026-08-17 §4, and the 2026-08-18 thresholds addendum).
115
+ *
116
+ * EMPTY here means "nothing is locked", which is a TRUE statement about a closed
117
+ * menu — not F59's absent-read-as-empty, where a `{}` stood in for a fact nobody
118
+ * gathered. The mechanism below stays exactly as it is: it is read at call time
119
+ * from this constant, so re-locking a class (or admitting a fourth) is one edit
120
+ * and the counted `request-red` path comes back with it. `LOCKED_CLASSES` in
121
+ * src/authoring.js is the twin and moved in the same commit; the suite pins the
122
+ * two identical. */
123
+ /** @type {readonly string[]} */
124
+ export const LOCKED_VERDICTS = Object.freeze([]);
107
125
  /** which close CLASS a declared verdictType demands — the close hierarchy one
108
126
  * level up: the same laundering guard as CLASS_BY_CLOSE, applied to the plan
109
127
  * shape's single close (green on a rubric would be a fake-hard verdict). */
@@ -515,8 +533,9 @@ function validateStagedClose(stages, red) {
515
533
  * stages instead (PRD v1.28/v1.32), and a spec that still declares it gets the
516
534
  * `checks-derived` red below rather than a bare unknown-field.
517
535
  *
518
- * v1 admits only verdictType `green`; soft-green/hitl are declared-but-locked
519
- * and red as `request-red` — admission demand the ledger counts, never a grant.
536
+ * Every verdictType the radio names is admitted (softgreen module 3); a class
537
+ * that were locked reds as `request-red` — admission demand the ledger counts,
538
+ * never a grant.
520
539
  * The close is validated one level up as well: a declared verdict demands a
521
540
  * close CLASS, so green-on-rubric reds as `close-hierarchy` rather than
522
541
  * laundering a soft judgement into a hard one.
@@ -536,7 +555,7 @@ function validatePlanShape(spec, red, reds) {
536
555
  // the type rides as a structured field — the ledger keys admission demand
537
556
  // on it, never on prose. `lib: 'bareloop'`: the refused catalogue is OURS
538
557
  // (VERDICT_TYPES), so this demand is never an upstream ask.
539
- reds.push({ code: 'request-red', path: 'verdictType', verb: spec.verdictType, lib: 'bareloop', detail: `"${spec.verdictType}" is declared-but-locked — not at this rung (v1 admits green only); this red IS the admission evidence, never a grant` });
558
+ reds.push({ code: 'request-red', path: 'verdictType', verb: spec.verdictType, lib: 'bareloop', detail: `"${spec.verdictType}" is declared-but-locked — not at this rung (admitted: ${VERDICT_TYPES.filter((v) => !LOCKED_VERDICTS.includes(v)).join(', ')}); this red IS the admission evidence, never a grant` });
540
559
  demanded = CLASS_BY_VERDICT[spec.verdictType];
541
560
  } else if (!VERDICT_TYPES.includes(spec.verdictType)) {
542
561
  red('invalid-value', 'verdictType', `menu: ${VERDICT_TYPES.join('|')} — an unknown type is a typo, never a request`);