bareloop 0.38.0 → 0.39.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to bareloop are documented here. Format:
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
+
8
+ ## [0.39.0] - 2026-10-10
9
+
10
+ ### Added
11
+
12
+ - **Rubric judge rulebook (F192).** `says-what`: the doc description must say more than the function's own name; name words match by prefix (init/initialize; a shorter word of 4+ letters), and with no doc block `has-doc` alone owns the red. `params` also reds an extra `@param` that names no parameter, and a missing field of a destructured parameter once the doc names any field. `has-doc` checks the artifact itself: the judge only points at the declaration, code finds the `/**` block above it. Quote matching uses bareguard `quoteIn` with `wholeLines`, so a quote that dropped a leading `* ` still matches. The injection facts check shares `has-doc`'s acceptance, and every rule carries a plain-words `means` for the case-proposal prompt.
13
+ - **A calibration case may not expect a red the rulebook cannot raise** (a `has-doc` red on a function that has a JSDoc block); it is refused before any locate call.
14
+ - **Calibration record.** A per-case `diag` (locate facts, full reds, per-attempt cause) is recorded beside the graded rows, and each failing case gets one plain why-line. Grading, hashes and verdicts are untouched.
15
+ - **Judged write fence.** A judged stage may only judge files inside the job's write fence; otherwise the red `judged-outside-write-scope` fires at draft time, before signing, on every door (authoring revise loop, signing, runner pre-flight, hand-written or reused specs).
16
+ - **Worker warning.** When a job's close carries the js no-suppressions check, the worker is told up front that it reads comment lines too.
17
+ - **Panel.**
18
+ - A refused rubric proposal and a refused signing each say why in plain words (panel and CLI share one owner); the full reds go to `draft-log.jsonl`.
19
+ - The calibrating line shows the gate's real score (8 of 10 / 3 of 5), not a stale check mark.
20
+ - The confirm plan reads as headed sections (#PLAN / #CHECKS / #NOT CHECKED, and #QUESTIONS when raised).
21
+ - The step map is fit-to-text chips with arrows, a line-start arrow on wrapped lines, and ↻N plus a dashed box for retries.
22
+
23
+ ### Changed
24
+
25
+ - **Strike governor.** A judged stage's trend number is its distinct red count, compared to best-so-far; the gap headline reads "N red(s) across X of Y artifact(s)". Identical, flip-flopping or growing judged reds now strike (they read as not comparable before).
26
+ - **Judge thinking is off** for the rubric judge on deepseek-flash, per call and allow-listed; workers and the drafter are unchanged and `JUDGE_MAX_TOKENS` stays 4000.
27
+ - **Revise reopens the job's boxes**, filled as drafted; Start drafting then drafts fresh. The typed change request is gone: `POST /api/author/:id/revise`, `session.revise`, the "N changes left" hint and `revisesLeft` are removed. The CLI confirm turn is unchanged.
28
+ - Dependencies: `bareguard` ^0.13.0 to ^0.21.0, `bare-agent` ^0.43.0 to ^0.49.0.
29
+
30
+ ### Fixed
31
+
32
+ - **A passed calibration satisfies the seed-red gate for any close with a judged stage**, mixed or pure. The old test read a function's arity (always true), so signing refused a mixed rubric close after calibration passed 10/10 (live session smv0tasvb0u89).
33
+
34
+ ### Known limits
35
+
36
+ - The judge's false-red classes seen on live run mv1j01sl are parked (FINDINGS F192 f), not fixed.
37
+ - The step-map chips and the new panel text were not checked at phone width by a person.
38
+
7
39
  ## [0.38.0] - 2026-10-08
8
40
 
9
41
  ### Added
@@ -67,7 +67,7 @@ inherited rule carries the green that minted it and the contrast that attributed
67
67
  an unknown provider name THROWS there, it never falls back to a default. Per-model
68
68
  request-key gating lives in that table: `deepseek-flash` sets bare-agent's
69
69
  `legacyMaxTokens`, because DeepSeek silently ignores `max_completion_tokens` and an
70
- output cap that does not bind is a money hazard. `provider: 'gemini-api'` (PRD item 31.3)
70
+ output cap that does not bind is a money hazard. The judge's calls additionally carry a per-model, judge-only `thinking:{type:'disabled'}` (allow-list `OPENAI_JUDGE_CALL_OPTIONS`, read through `judgeCallOptions`, applied by `defaultJudgeLoop`; F192) because deepseek-flash's default thinking ate the 4000-token locate budget; workers and the drafter are untouched. `provider: 'gemini-api'` (PRD item 31.3)
71
71
  takes the same `Loop` path, reads `GEMINI_API_KEY`, and maps two real tiers
72
72
  (`sonnet` → `gemini-2.5-pro`, `haiku` → `gemini-2.5-flash`). It is
73
73
  **ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
@@ -677,6 +677,16 @@ attempt PLUS its close. Unpriced is never free: a null cost is a `pricing-red` s
677
677
  never retried, and the ladder is otherwise ONE retry (`JUDGE_ATTEMPTS`) — after that a broken
678
678
  judge is an instrument stop, never a red about the tree.
679
679
 
680
+ **The rulebook, and what each rule can raise** (`JUDGE_RULES` in `src/judged.js`; every rule carries a plain-words `means` the case-proposal prompt renders, so a proposed practice case never expects a red the rule cannot raise). The judge only POINTS (quotes lines verbatim); code does every check; unsure is red; the judge never says pass/fail. Quotes are matched by bareguard's `quoteIn(..., {wholeLines:true})` (bareguard `^0.21.0`): a contiguous run of whole artifact lines, so a bare fragment cannot pass by hiding in a longer line, while a dropped leading `* ` of a JSDoc tag line still matches. `has-doc` checks only that a JSDoc block sits directly above the function (read off the artifact by `docBlockAbove`; the judge's quote is a pointer that must lie inside that block), so a documented function never breaks it. **`says-what`** (the doc's description must say more than the function's own name) reds when the described prose, minus a fixed list of function words and the name's own words (camel/snake/kebab, light inflections, and a prefix echo such as `init`/`initialize` when the shorter word is at least 4 letters), leaves nothing; no description, or an unlocatable quote, reds. It catches a pure name echo only ("Returns the total." for `getTotal` passes), and with NO doc block at all it stays silent and leaves the red to `has-doc` (the card is not required to carry both). `params` reds a declared parameter with no tag, an `@param` naming no parameter (each destructured slot absorbs one unmatched root tag), and, when the doc names any field of a simple destructured parameter, a missing field; `@param options` alone for `{ x, y }` passes. `returns` reds a returning function with no `@returns`. A practice case that expects a `has-doc` red on a function that has a block is refused as a legality red before any call (`validateCalibrationSet`).
681
+
682
+ **A judged stage may only judge files inside the job's write fence** (`judgedOutsideFence`, `src/authoring.js`, called from `validateDeclaration`, which every door reaches: the authoring revise loop, `validateCloseDecl` at signing and runner pre-flight, and `validateJob` for hand-written or reused specs). A path outside `writeScope` reds `judged-outside-write-scope` at draft time, because a finding on a file the run is fenced out of can never be fixed. With no fence to compare against (the plain-folder deferral) the rule is not run.
683
+
684
+ **One red count.** A judged stage's red COUNT is the number of distinct failing functions it itemizes (`redCount`/`redSet` on the stage detail, `src/kinds.js`; an unsure entry counts as one). The same number is the gap headline ("N red(s) across M of K artifact(s)") and the trend value the close-trend governor compares to best-so-far, direction always down (`trendValueOf`, `src/declaredclose.js`), so a repeated or growing judged failure strikes like any numeric stage instead of running to the wall. Spines written before this change carry the old headline (the card-item count).
685
+
686
+ **A refused draft or signing says WHY in plain words.** `proposalStopText` (a refused rubric proposal: `proposal-invalid` / `rubric-invalid`) and `signingStopText` (refused signing gates) in `src/authorreadout.js` are the one owner of the wording, printed by the panel's failed step and the CLI alike: code-written sentences per red kind naming the case and function, the real spend so far, and what to do; a practice-case/check mismatch is not claimed to be anyone's fault; an unknown red code is quoted, never dropped. The full scrubbed reds go into the session's `draft-log.jsonl` as a `step-reds` record, and the calibrating line shows the gate's real result (`calibrationSummary`: "N of 10 practice cases graded right, M of 5 attack tests resisted").
687
+
688
+ **The worker is warned about the `any` suppression guard.** A job whose actual close stages carry the js `any` suppression pattern gets one extra genre-owned line in the worker system prompt (`ANY_SUPPRESSION_WARNING`, `anySuppressionWarning`, `src/authoring.js`): the guard scans added comment lines too, so prose like `, any` trips it. The guard regex itself is unchanged.
689
+
680
690
  **The two SIGNED artifacts of a judged close** (softgreen module 4, design §4.3/§4.4). The
681
691
  softgreen interview's Judge Examples answer (PRD item 33 M3 piece 3 — one example to pass, one
682
692
  to fail, and why; formerly two questions, Q6+Q7) compiles into the RUBRIC CARD and the FROZEN
@@ -745,12 +755,13 @@ signing record keeps WHAT was certified — `cardHash`, `casesHash`, `setHash` (
745
755
  resolved judge identity), the graded rows and the battery — beside `judgeModel` itself.
746
756
  `runCalibration` takes `judgeModel` as a REQUIRED argument and throws without one.
747
757
 
748
- **D9.3 for a judged-ONLY close** (ruling 3, hamr: *"fix it now, we are delivering softgreen"*).
749
- A judged stage skips the seed read (ruling 8), so a close whose only work stage is judged has no
750
- seed red and used to be unsignable — the one job shape softgreen exists for. For that shape only,
751
- a PASSED calibration gate plays the seed-red role (the polarity law makes *this close can fail* a
752
- demonstrated fact), and the record says so: `gates.seedVerdict.satisfiedBy === 'calibration'`. A
753
- close with any mechanical work stage is UNCHANGED and still needs its seed red.
758
+ **D9.3 for a close with a judged stage** (ruling 3, hamr: *"fix it now, we are delivering softgreen"*;
759
+ widened 2026-10-09: *"a rubric job should check if judge can judge (calibration) ... we can't close
760
+ everything upfront"*). A judged stage skips the seed read (ruling 8), so it can never be red at seed. For
761
+ ANY close carrying a judged stage, pure or mixed with mechanical stages, a PASSED calibration gate plays the
762
+ seed-red role (`seedProofIsCalibration`), and the record says so when no mechanical stage is red itself:
763
+ `gates.seedVerdict.satisfiedBy === 'calibration'`. A failed or missing calibration never satisfies it. A close
764
+ with NO judged stage is UNCHANGED and still needs a work stage red at the seed.
754
765
 
755
766
  **THE JUDGE TIER IS INSIDE THE SIGNATURE, and the stage refuses a mismatch.** *"A judge-model
756
767
  bump forces a full recalibration"* used to be an operator-side rule with nothing to fire on,
@@ -1863,7 +1874,7 @@ hand them to the one `Loop` that drives worker, scout and planner rounds; the ru
1863
1874
  them once. No price on the row = no `rates` key anywhere = the guesstimate above, byte-identical to
1864
1875
  before. bareloop keeps no price list of its own, ever.
1865
1876
 
1866
- **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.43.0`)
1877
+ **The provenance is on the record, per round (`rateSource`).** The pinned bare-agent (`^0.49.0`)
1867
1878
  carries it on every metering payload, and bareloop forwards it VERBATIM (`rateSourceFields`,
1868
1879
  `src/planrun.js`): every API `worker-round` (worker, scout, planner, fix loop) carries it beside
1869
1880
  `pricing` today — `'caller'` when a customer price is on the key's row, else `'tier'`/`'default'`
@@ -3453,13 +3464,13 @@ key you want first (the panel picks the row by the Model menu).
3453
3464
  re-attaches to it from the server's session map (nothing is stored page-side). A panel restart mints a new token and
3454
3465
  loses the in-memory sessions: the page then shows one "reload this page" line and stops the Chat poll (no auto-reload;
3455
3466
  drafting money already spent stays booked via `draft-spend.json`, below). `GET /api/author/:id` polls the session's state
3456
- (phase, chat messages, cost, `revisesLeft`, `specHash` once prepared). `POST /api/author/
3467
+ (phase, chat messages, cost, `specHash` once prepared). `POST /api/author/
3457
3468
  :id/send {text}` answers whatever the confirm turn is currently asking (the `language` pick or a plan's
3458
3469
  own follow-up question — the old `worseThanBefore` ask is retired, 2026-09-28) — refused outright when the pending ask is the
3459
- plan MENU itself (`{ok:false}`, no route from chat text to a plan decision, ever). `POST
3460
- /api/author/:id/revise {text}` is the menu's own `fix` pick, with the chat text as the
3461
- correction (D3: max 2 rounds, `revisesLeft` derived from the confirm turn's own round
3462
- number, never a second hardcoded cap). `POST /api/author/:id/sign-prepare` is the menu's
3470
+ plan MENU itself (`{ok:false}`, no route from chat text to a plan decision, ever). There is no
3471
+ typed-revise route (2026-10-09): the panel's **Revise** button, shown only while a plan waits for the OK,
3472
+ abandons the draft (`/abandon`; spend already booked stays booked) and reopens the New job card with the same
3473
+ values in every box, editable, for a fresh Start drafting; no change request is ever sent to the model. `POST /api/author/:id/sign-prepare` is the menu's
3463
3474
  own `confirm` pick — it runs gates 1–4 and reaches `phase:'prepared'` with a `specHash`; it
3464
3475
  NEVER signs. `POST /api/author/:id/sign {specHash}` is the ONLY route that spawns a run —
3465
3476
  it refuses unless `phase==='prepared'` and the posted hash matches the session's own
@@ -3471,7 +3482,7 @@ key you want first (the panel picks the row by the Model menu).
3471
3482
  server's environment with `~/.config/bareloop/.env` merged in per request (a key is never read
3472
3483
  into or sent to the page — a missing one refuses the session at $0, naming only the env var). `src/panel/authorsession.js` takes
3473
3484
  test-only DI seams (`scout`/`generate`/`confirmGenerate`/`authorFn`/`prepareSigningFn`) so
3474
- a test can drive the real ask()-channel/revise/hash wiring without a live provider call;
3485
+ a test can drive the real ask()-channel/hash wiring without a live provider call;
3475
3486
  none of them are reachable from `authorroutes.js`'s real construction path. Sessions live
3476
3487
  under `~/.config/bareloop/panel-sessions/<id>/` (each one's own `resolved-spec.json` and
3477
3488
  `signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
@@ -3516,7 +3527,7 @@ key you want first (the panel picks the row by the Model menu).
3516
3527
  died, else failed. A run that DIED (no `job-end`, runner gone) leaves its last part in flight: when that part has no outcome
3517
3528
  and no cut-off, the server marks it `died` (`[?] died`), never the "happened, so passed" default — replay alone cannot tell
3518
3529
  died from still running, so `getRunDetail` decides it from the same `death` verdict as the run's status. A live run's open
3519
- part stays running; a part that finished before the death keeps its own status. The page's map box reads `[sign] word` (the live part reads `[▶] running`); the step cards and Audit rows
3530
+ part stays running; a part that finished before the death keeps its own status. The page's step map is a row of fit-to-text chips that flow and wrap (no truncation, no horizontal scroll), `[sign title]` each, in plan order, with an arrow between neighbours and a leading arrow on a wrapped line; the status word (from the same table) rides in the chip's tooltip, a retried step has a dashed border and a `↻N` tries mark, a leg boundary a dotted right edge, and a click jumps to that part's Audit row (the live part reads `[▶]`); the step cards and Audit rows
3520
3531
  show the word only; no surface says "done".
3521
3532
  **Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
3522
3533
  `checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -27,8 +27,8 @@
27
27
  "check:lockfile": "node scripts/check-lockfile.mjs"
28
28
  },
29
29
  "dependencies": {
30
- "bare-agent": "^0.43.0",
31
- "bareguard": "^0.13.0",
30
+ "bare-agent": "^0.49.0",
31
+ "bareguard": "^0.21.0",
32
32
  "litectx": "^0.32.0"
33
33
  },
34
34
  "devDependencies": {
package/src/authorflow.js CHANGED
@@ -384,7 +384,8 @@ export const CLASS_STATEMENTS = Object.freeze({
384
384
  + 'the result and deciding whether it is any good — and no command can render it. Compose the mechanical part '
385
385
  + 'first: everything about this repository a command can decide is still a mechanical stage, exactly as it would '
386
386
  + 'be for a green job. Then put ONE judged-floor stage AFTER them, carrying the few things the person said they '
387
- + 'actually look for and the real files the work lands in. A judged stage decides only the lines its card names, '
387
+ + 'actually look for and the real files the work lands in (files the job may EDIT, inside the write fence, '
388
+ + 'because a finding on a file the run is fenced out of can never be fixed). A judged stage decides only the lines its card names, '
388
389
  + 'so name each thing they look for as its own line, and leave nothing to be inferred.',
389
390
  // N4 slice 1. The mechanical-first composition law is stated as an ORDER
390
391
  // because first-red-wins makes the order the mechanism: the cheap stages shield
@@ -2389,7 +2390,7 @@ export async function authorClose({
2389
2390
  }
2390
2391
  previous = ask.declaration;
2391
2392
 
2392
- const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType });
2393
+ const v = validateDeclaration(ask.declaration, { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope });
2393
2394
  // SCRUBBED HERE, once, where the reds enter this module's records — the same
2394
2395
  // boundary rule `renderRejectBlock` already states, applied to the OTHER
2395
2396
  // channel they travel down. A validation red quotes what the model declared
package/src/authoring.js CHANGED
@@ -516,6 +516,32 @@ export const TYPES_GENRE_TEMPLATE = `1. The STRICT form of the language's type c
516
516
 
517
517
  export const GENRE_LANGUAGES = Object.freeze(['js', 'python']);
518
518
 
519
+ /**
520
+ * The js `any` suppression pattern's regex, ONE spelling: the TYPES js battery
521
+ * below composes it into the signed guard, and `anySuppressionWarning` keys on
522
+ * it, so the warning and the guard cannot drift. The guard scans every ADDED
523
+ * line, comments included, so plain prose like ", any" trips it.
524
+ */
525
+ const JS_ANY_REGEX = '(?:[{<|(\\[:,]|\\bas)\\s*any\\b|\\bany\\[\\]';
526
+
527
+ /** The worker-facing line that rides with the js `any` guard (run mv13ery3). */
528
+ export const ANY_SUPPRESSION_WARNING = '\nThe no-suppressions check also reads comments: never write the word any right after a comma, colon, bracket, brace, < or | '
529
+ + "(e.g. ', any', ': any', '(any'), even in plain English — write 'every', start the clause differently, or put it in backticks.";
530
+
531
+ /**
532
+ * The warning line for a job whose ACTUAL stages carry the js `any` pattern, else
533
+ * ''. Derived from the stages' patterns (never goal prose), so a job without the
534
+ * guard is told nothing about it.
535
+ * @param {any[]|null|undefined} stages
536
+ * @returns {string}
537
+ */
538
+ export function anySuppressionWarning(stages) {
539
+ const has = Array.isArray(stages) && stages.some((s) => isObj(s) && s.kind === 'pattern-absent-in-diff'
540
+ && isObj(s.params) && Array.isArray(s.params.patterns)
541
+ && s.params.patterns.some((/** @type {any} */ q) => isObj(q) && q.regex === JS_ANY_REGEX));
542
+ return has ? ANY_SUPPRESSION_WARNING : '';
543
+ }
544
+
519
545
  /**
520
546
  * THE GENRE, as data. The suppression batteries are the hand-written closes'
521
547
  * own `SUPPRESSIONS` tables (`scripts/u-pulselog-close.mjs:35`,
@@ -541,7 +567,7 @@ export const TYPES_GENRE = Object.freeze({
541
567
  Object.freeze({ id: 'ts-expect-error', regex: '@ts-expect-error' }),
542
568
  Object.freeze({ id: 'ts-nocheck', regex: '@ts-nocheck' }),
543
569
  Object.freeze({ id: 'eslint-disable', regex: 'eslint-disable' }),
544
- Object.freeze({ id: 'any', regex: '(?:[{<|(\\[:,]|\\bas)\\s*any\\b|\\bany\\[\\]' }),
570
+ Object.freeze({ id: 'any', regex: JS_ANY_REGEX }),
545
571
  Object.freeze({ id: 'any-star', regex: '@\\w+\\s*\\{\\s*[*?]\\s*\\}' }),
546
572
  Object.freeze({ id: 'cast', regex: '@type\\s*\\{.*\\}\\s*\\*\\/\\s*\\(' }),
547
573
  ]),
@@ -1057,6 +1083,25 @@ function scopeOfJob(declaration, idx, guards) {
1057
1083
  return { scoped: false, via: null };
1058
1084
  }
1059
1085
 
1086
+ /**
1087
+ * Which of a judged stage's paths lie OUTSIDE the job's write fence? One owner of the rule "a check must be
1088
+ * free to edit every file it can report on", called from every door that validates a declaration. It uses
1089
+ * the shipped fence spelling (`globToPrefix`, the same containment reading plan.js's `insideFence` has) and
1090
+ * returns `[]` when there is no fence to compare against or the paths are not a string list (shape is the
1091
+ * schema walk's business).
1092
+ * @param {unknown} paths @param {string[]|null|undefined} writeScope
1093
+ * @returns {string[]}
1094
+ */
1095
+ export function judgedOutsideFence(paths, writeScope) {
1096
+ if (!Array.isArray(writeScope) || writeScope.length === 0 || !Array.isArray(paths)) return [];
1097
+ const fence = writeScope.filter(isNonEmptyString).map(globToPrefix);
1098
+ if (fence.length === 0) return [];
1099
+ return paths.filter(isNonEmptyString).filter((raw) => {
1100
+ const n = normPrefix(raw);
1101
+ return !fence.some((f) => f === '.' || n === f || n.startsWith(`${f}/`));
1102
+ });
1103
+ }
1104
+
1060
1105
  /**
1061
1106
  * Kinds a declaration may carry AT MOST ONCE, as data beside the catalogue.
1062
1107
  *
@@ -1154,7 +1199,9 @@ function readPath(p, dotted) {
1154
1199
  * @param {{catalogue?: Record<string, KindSpec>, listing?: string[]|null,
1155
1200
  * guards?: {name: string, kind: string, params: Record<string, any>, fill: string[]}[]|null,
1156
1201
  * envOwned?: string[]|null, envInjected?: Record<string, string>|null, deferListing?: boolean,
1157
- * verdictType?: string|null}} [opts]
1202
+ * verdictType?: string|null, writeScope?: string[]|null}} [opts]
1203
+ * `writeScope` — the job's signed write fence. Every path a judged stage judges must sit inside it
1204
+ * ({@link judgedOutsideFence}); absent, the rule is not run (the plain-folder deferral has no fence).
1158
1205
  * `listing` — repo-relative paths at the seed (`git ls-tree -r --name-only`).
1159
1206
  * `guards` — the picked class's injected guards (`classGuards`).
1160
1207
  * `verdictType` — the class the USER picked (`VERDICT_CLASSES`).
@@ -1171,7 +1218,7 @@ function readPath(p, dotted) {
1171
1218
  export function validateDeclaration(declaration, opts = {}) {
1172
1219
  const {
1173
1220
  catalogue = KIND_CATALOGUE, listing = null, guards = null, envOwned = null, envInjected = null,
1174
- verdictType = null,
1221
+ verdictType = null, writeScope = null,
1175
1222
  } = opts;
1176
1223
  const deferListing = opts.deferListing === true;
1177
1224
  /** @type {Red[]} */
@@ -1314,6 +1361,18 @@ export function validateDeclaration(declaration, opts = {}) {
1314
1361
  } else kindOwners.set(s.kind, label);
1315
1362
  }
1316
1363
 
1364
+ // A judged stage may only judge what the run can fix: a path outside the write fence is a
1365
+ // finding the worker can never act on (run mv13ery3: bin/pulselog.js, fence src/**).
1366
+ if (s.kind === JUDGED_FLOOR_KIND && isObj(s.params)) {
1367
+ const outside = judgedOutsideFence(s.params.paths, writeScope);
1368
+ if (outside.length > 0) {
1369
+ red('judged-outside-write-scope', `${at}.params.paths`, `stage "${label}" judges ${outside.join(', ')}, but the `
1370
+ + `job may only edit ${writeScope?.join(', ')}. A check that reports on a file the run is fenced out of can `
1371
+ + 'never be satisfied — every finding on it is one the worker cannot fix. Name files inside the write fence',
1372
+ { stage: label, outside, fence: (writeScope ?? []).map(globToPrefix) });
1373
+ }
1374
+ }
1375
+
1317
1376
  // ── RULING 5, AS LAW: judged and human stages are `offer: false`, and the
1318
1377
  // law is checked rather than applied. `checkMenu` filters `offer !== false`,
1319
1378
  // so a declaration that says nothing is silent about a rule the runner
package/src/authorjob.js CHANGED
@@ -99,7 +99,7 @@
99
99
  // in the stored declaration (forward-compat point 3), and the suite pins it.
100
100
 
101
101
  import { jobSpecHash, validateJob } from './job.js';
102
- import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, SEED_EXEMPT_KINDS, judgedStages, closeJudges } from './kinds.js';
102
+ import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, judgedStages, closeJudges } from './kinds.js';
103
103
  // SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
104
104
  // and the gate that decides whether the ruler they describe is a ruler.
105
105
  import { proposeJudgedArtifacts, signJudgedArtifacts, foldJudgedArtifacts } from './cardauthor.js';
@@ -950,6 +950,17 @@ const scrubRed = (r) => /** @type {any} */ (Object.fromEntries(
950
950
  Object.entries(r).map(([k, v]) => [k, typeof v === 'string' ? redactSecrets(v) : v]),
951
951
  ));
952
952
 
953
+ /**
954
+ * THE one spelling of "a passed calibration gate stands in for the seed red": the
955
+ * close carries a judged stage AND the gate ran and passed. A failed or missing
956
+ * calibration never satisfies it, and a close with no judged stage never reaches it.
957
+ * @param {any[]} judged `judgedStages(closeDecl)` @param {any} calibration `gates.calibration`
958
+ * @returns {boolean}
959
+ */
960
+ export function seedProofIsCalibration(judged, calibration) {
961
+ return judged.length > 0 && calibration?.ok === true;
962
+ }
963
+
953
964
  /**
954
965
  * GATE 4 — the calibration gate, and the only gate here that spends money.
955
966
  *
@@ -1033,7 +1044,10 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, judgeModel, onJu
1033
1044
 
1034
1045
  /** what the signing evidence KEEPS about this gate. The per-case artifacts are
1035
1046
  * deliberately not in it — they are already in the signed spec, and a second
1036
- * copy in the evidence file is a second thing to keep in step. What IS kept is
1047
+ * copy in the evidence file is a second thing to keep in step. Each graded row's
1048
+ * `diag` (F192) keeps the judge's own facts and the decision's reds — scrubbed
1049
+ * and bounded quotes, never an artifact body — so a refusal reads from its own
1050
+ * record. What IS kept is
1037
1051
  * WHICH BYTES were certified (`cardHash`/`casesHash`/`setHash`) and BY WHICH
1038
1052
  * MODEL (`judgeModel`), which is what makes a judge-model bump detectable at
1039
1053
  * all. */
@@ -1135,16 +1149,15 @@ async function calibrationGate({ spec, judgedStages, judgeLoop, judgeModel, onJu
1135
1149
  * has already had its chance, and nobody pays to discover a broken
1136
1150
  * declaration.
1137
1151
  *
1138
- * D9.3 AND THE JUDGED-ONLY CLOSE (2026-08-18 second addendum, ruling 3; hamr:
1139
- * *"fix it now, we are delivering softgreen, isn't that the whole point?"*). Gate
1140
- * 3 protects against a close that cannot fail — one that scans nothing and reads
1141
- * green at seed against a true red. A judged stage SKIPS the seed read by ruling
1142
- * 8, so a close whose only work stage is judged has no seed red to show and used
1143
- * to be unsignable: the one job shape softgreen exists for. For that shape, and
1144
- * ONLY that shape, a PASSED calibration gate plays the seed-red role — it is that
1145
- * close's own proof of failability, held to the same all-or-nothing bar. A close
1146
- * with a mechanical work stage is UNCHANGED: it still needs a seed red, whatever
1147
- * its calibration says.
1152
+ * D9.3 AND THE JUDGED CLOSE (2026-08-18 second addendum, ruling 3; hamr: *"fix it now,
1153
+ * we are delivering softgreen, isn't that the whole point?"*; widened 2026-10-09 to mixed
1154
+ * closes, hamr: *"a rubric job should check if judge can judge (calibration) and judge
1155
+ * rules can apply properly to pass ... we can't close everything upfront"*). Gate 3
1156
+ * protects against a close that cannot fail. A judged stage SKIPS the seed read by ruling
1157
+ * 8, so it can never show red there. For ANY close carrying a judged stage (pure or
1158
+ * mixed with mechanical stages), a PASSED calibration gate plays the seed-red role
1159
+ * (`seedProofIsCalibration`). A close with NO judged stage is UNCHANGED: some work stage
1160
+ * must be red at the seed. A failed or missing calibration never satisfies it.
1148
1161
  *
1149
1162
  * Gates 2 and 3 share ONE execution, and that is stated rather than hidden: at
1150
1163
  * job creation the tree IS the seed, so "does every stage run against the real
@@ -1240,7 +1253,7 @@ export async function prepareSigning({
1240
1253
  }
1241
1254
  // the class rides from the SPEC (PRD v1.57 §2) — gate 1a already refused a spec
1242
1255
  // whose class is unknown or locked, so this is the class D5's battery hangs off
1243
- const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType });
1256
+ const dv = validateCloseDecl(spec.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: spec.verdictType, writeScope: spec.writeScope });
1244
1257
  // the grounded gate's reds quote BOTH untrusted sources — the declaration, and
1245
1258
  // the seed listing itself (the listing rule names what really sits beside an
1246
1259
  // invented path). The listing half is a channel gate 1a structurally cannot
@@ -1349,17 +1362,17 @@ export async function prepareSigning({
1349
1362
  // the close-authoring layer, and this gate is what refuses it BEFORE a
1350
1363
  // signature rather than after a paid run.
1351
1364
  //
1352
- // THE JUDGED-ONLY ROUTE (ruling 3). A close whose work stages are ALL
1353
- // seed-exempt has no seed verdict to be red — not because it measures nothing,
1354
- // but because ruling 8 exempts a judged stage from being measured at seed. Its
1355
- // proof of failability is the calibration gate, which has just run and passed
1356
- // (a failed one returned above), and which contains signed cases the pipe must
1357
- // RED as well as ones it must pass — the polarity law makes "this close can
1358
- // fail" a thing that was demonstrated rather than assumed. A close with any
1359
- // mechanical work stage takes the original path unchanged.
1360
- const mechanicalWork = work.filter((r) => !SEED_EXEMPT_KINDS.includes(r.kind));
1361
- const judgedOnly = judgedStages.length > 0 && mechanicalWork.length === 0;
1362
- if (judgedOnly && base.gates.calibration?.ok) {
1365
+ // THE JUDGED ROUTE (ruling 3, widened 2026-10-09 by hamr: "a rubric job should
1366
+ // check if judge can judge (calibration) and judge rules can apply properly to
1367
+ // pass ... we can't close everything upfront"). A close carrying ANY judged stage
1368
+ // has a work-bearing stage that ruling 8 exempts from the seed read, so the seed
1369
+ // cannot show it red — not because it measures nothing, but because it is not
1370
+ // measured there. Its proof of failability is the calibration gate, which has
1371
+ // just run and passed (a failed one returned above), and which contains signed
1372
+ // cases the pipe must RED as well as ones it must pass. That holds for a PURE
1373
+ // judged close and a MIXED one alike; a close with NO judged stage takes the
1374
+ // original path unchanged (some work stage must be red at the seed).
1375
+ if (workRed.length === 0 && seedProofIsCalibration(judged, base.gates.calibration)) {
1363
1376
  // said out loud in the record: this close passed gate 3 on a DIFFERENT proof,
1364
1377
  // and a reader must never have to infer which one from an empty workRed list
1365
1378
  base.gates.seedVerdict.satisfiedBy = 'calibration';