bareloop 0.15.0 → 0.17.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
@@ -7,6 +7,95 @@ feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.0] — 2026-08-31
11
+
12
+ ### Added
13
+
14
+ - **Publish workflow gates on the types being usable BY AN ADOPTER, not just internally.** `npm run typecheck` (`tsc --noEmit`) checks the *source*; it cannot see the generated `.d.ts` as an adopter resolves it from inside `node_modules`, which is the one thing consumers actually get. The publish workflow now packs the tarball, installs it into a clean consumer project, and compiles a quickstart against it, so a release whose published types are broken cannot reach the registry. The consumer pins `@types/node` to the major this package builds against instead of floating to the newest, so a stricter DefinitelyTyped release cannot turn the publish gate red for reasons unrelated to the commit being published. Verified locally: the quickstart compiles green against a packed tarball, and a deliberately broken dereference fails it. CI only — no runtime or published-artifact change.
15
+
16
+ - **Worker model name in the signed spec (build-list #3)**: `job-v1` gains an optional
17
+ `model` field — a non-empty string naming the exact worker provider model id. Absent,
18
+ behaviour is unchanged (the runner's own default); present, it is part of the signed
19
+ spec hash and `scripts/run-u.mjs` runs the job on that model. A `--model <tier>` flag
20
+ naming a different model is refused (`resolveWorkerModel`, `src/job.js`) rather than
21
+ silently overridden — the spec always wins. Worker only; the judge model stays
22
+ library-pinned pending recalibration.
23
+
24
+ ## [0.16.0] — 2026-08-30
25
+
26
+ ### Added
27
+
28
+ - **`PROMPT_COMMIT_RANGE` covers every commit of a direct push (PRD #2b, #8)**:
29
+ `scripts/prompt-commit-check.mjs` now honours an env var of that name (`--range` mode
30
+ only — a commit-msg hook's `--message-file` path is never redirected by a stray env
31
+ var), which supersedes `--range` and needs no `HEAD~1` fallback of its own.
32
+ `.github/workflows/ci.yml`'s `npm test` step sets it to `github.event.before..sha` on
33
+ push events, so a linear multi-commit direct push to `main` is inspected whole instead
34
+ of the local `HEAD~1..HEAD` fallback re-covering only the last commit. An unresolvable
35
+ env range (a 40-zero `github.event.before` on a brand-new branch push) is reported and
36
+ falls back to the `--range` arg's own path, including its existing main-push fallback.
37
+ Same option-shaped-value guard as `--range` (a value starting with `-` is refused before
38
+ it reaches `git`). Premise measured on `origin/main`'s own reflog (two 3-commit linear
39
+ pushes on 2026-08-24, one carrying a prompt-register change the `HEAD~1` fallback would
40
+ have skipped); 3 new CLI tests, each proven failing before the fix. **CI-proven
41
+ 2026-08-30**: a branch-birth push (40-zero `before`) fell back and checked 5 commits
42
+ (CI run 33314883771); the next push used the real range `7e975b3..f0c3199` and checked
43
+ exactly 1 (run 33314899690).
44
+ - **Green-class runs mint a registry row, born released** (PRD open item #16,
45
+ `src/reuse.js`'s `writeRunGreenRow`): the fourth refusal (`credit-not-held`) is dropped —
46
+ a mechanical `green` run now mints its registry row at run end exactly like `soft-green`
47
+ does, but unheld: `greenParts` already keys the hold on `quarantinesCredit(verdictType)`
48
+ (`src/bridges.js:372`), so no bridge/door code changed. `door accept` on an unheld green
49
+ row records the disposition and releases nothing — it no longer dies `no-row-for-run`;
50
+ that terminal is reserved for a run that truly earned no row. `scripts/run-u.mjs`'s
51
+ registry line now prints `HELD`/`RELEASED` by class instead of `HELD` unconditionally,
52
+ and its door credit readout distinguishes a genuine release from a repeat `accept` on an
53
+ already-released row (`recordDoor` returns `released: false` on a same-decision repeat —
54
+ the wording now branches on `quarantinesCredit(spec.verdictType)` rather than claiming a
55
+ fresh release every time). 4 new/updated tests (2 proven failing before the fix),
56
+ including a soft-green regression guard (still born `HELD`) and the still-refusing
57
+ no-row-for-run case.
58
+
59
+ ### Changed
60
+
61
+ - **`bareloop.context.md` leads with the shipped design** (PRD #14): the header states the
62
+ v0.15.0 contract first — `green` + `soft-green`, the judged floor and calibration gate,
63
+ the review door (`accept`/`rerun`/`pause`) that never changes the loop's own verdict —
64
+ and demotes `hitl` to a LEGACY block: still admitted in code (`VERDICT_TYPES`,
65
+ `human-confirms`, `hitl-pause`/`hitl-decision-red`), retired by design (PRD v1.71),
66
+ removal a future breaking change. Six inner passages that still called `soft-green`
67
+ "locked" or `hitl` "the forward path" now state the shipped fact.
68
+ - **The authorjob no-repo refusal message** (`src/authorjob.js`) no longer says a judged or
69
+ human close is "declared-but-locked" — `LOCKED_CLASSES` has been empty since v0.12.0; it
70
+ now says what is true: this authoring flow drafts code-genre closes against a git seed
71
+ only.
72
+ - **4 `jobs/*.json` description paths repointed, docs reorg** (PRD #13): the two preregs
73
+ living under `docs/logs/` (`TESTGEN-PREREG.md`, `REUSE-LIFT-CONTRAST-PREREG.md`) moved to
74
+ `docs/product/` beside the other eight; `docs/index.md`, the one live `FINDINGS.md` link
75
+ and `scripts/gen-mutants-testgen.mjs`'s prereg string follow. The four job descriptions
76
+ that still cited the pre-reorg `docs/02-experiments/*-PREREG.md` path now cite the real
77
+ files — each job's signed hash flips and re-signs at its next run. `docs/archive/` and
78
+ CHANGELOG history left as written.
79
+
80
+ ### Fixed
81
+
82
+ - **`--range`-only CLI tests no longer leak an ambient `PROMPT_COMMIT_RANGE`**: the six
83
+ tests exercising `--range` in isolation now pass `PROMPT_COMMIT_RANGE: ''` explicitly, so
84
+ a resolvable ambient value (as CI's own `npm test` step now sets on every push) can never
85
+ silently route them onto the env-range path they don't exist to prove.
86
+
87
+ ### Ruled (no code change)
88
+
89
+ - **Accept's re-proof wall governance** (PRD #15, hamr, 2026-08-30): the door-accept
90
+ re-proof runs unclocked, on the condition it stays mechanical
91
+ (`mechanicalStages()` only, `src/reviewdoor.js:213`) — never a judged floor, never a
92
+ person. A check is never bounded (W-2); any future non-mechanical re-proof is a new
93
+ arbiter question.
94
+ - **`hitl` cleanup deferred** (PRD #21, hamr, 2026-08-30): `hitl` stays admitted in code
95
+ (retired by design only) until hamr decides between finishing the retirement (delete the
96
+ class, the stage kind, the two terminals, migrate any job using it — a breaking rung with
97
+ its own release) or keeping it reopenable. Unscheduled either way.
98
+
10
99
  ## [0.15.0] — 2026-08-27
11
100
 
12
101
  ### Added
@@ -1,31 +1,37 @@
1
1
  # bareloop — Integration Guide
2
2
 
3
- > **Current through Layer 2 (ACCEPTED, F47)**: the plan-v1 flow — a four-field job shape,
4
- > agent-authored validated plans, in-run check exits — validated end-to-end on the
5
- > real-model acceptance battery (3/3 conversion, 3/3 over the owned bar, the agent
6
- > composing its own check exits). **The plan shape is now the ONLY shape**: the legacy
3
+ > **Current: v0.16.0.** The verdict classes are **`green` + `soft-green`**. `soft-green` has
4
+ > a real judged floor (`judged-floor`, `src/judged.js`): a pinned haiku-4.5 judge LOCATEs
5
+ > facts with quotes from the real artifact, an arbiter-owned `decide()` renders pass/fail from
6
+ > a signed rubric card, and *unsure* is a red, never a pass — bought by a signed calibration
7
+ > gate (ten graded truths, five resisted lies, floor 10/10, itemized reds). A soft-green pass
8
+ > mints its bridge row QUARANTINED: no learning credit until an `accept` releases it, forward
9
+ > only; a rerun records a disposition and its outcome is a NEW run's own row, born held
10
+ > again. Every run ends at the signer's **review door** — `accept` / `rerun` / `pause` — which
11
+ > records a disposition and **never changes the loop's own verdict** (`runPlan` still returns
12
+ > `green`/`already-green` exactly as before). The door
13
+ > is open **always for `soft-green`**, and **opt-in for `green`** via `reviewDoor: true` /
14
+ > `--review-door` (the flag wins in both directions, never inferred); `accept` re-runs the
15
+ > close's MECHANICAL stages only — never a judged floor, never a person. Shipped in v0.12.0
16
+ > (2026-08-23); see CHANGELOG `## [0.12.0]` onward for the softgreen rung and the fixes/rungs
17
+ > that followed through v0.15.0. **The plan shape is the ONLY shape**: the legacy
7
18
  > operator-authored `steps[]` path, config-v1 and the draft-PR hitl step were deleted
8
19
  > 2026-07-26 (PRD v1.32), so `interpret`, `validateConfig` and `extractRules` are gone from
9
- > the public surface — a breaking change. `hitl` and `soft-green` return as a Layer 3
10
- > decision, rebuilt in the plan shape rather than ported. Runs on two worker surfaces —
11
- > the API `Loop` and, for
12
- > `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
13
- > as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture,
14
- > the refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this
15
- > file ships with the package and is the complete adopter contract; the README is only
16
- > the pitch.
17
-
18
- > **DIRECTION CHANGE — 2026-08-17 (design decided, NOT built; nothing below has moved yet).**
19
- > **`hitl` is RETIRED as a verdict class.** The verdict classes become **`green` +
20
- > `soft-green`**, and the hitl pause machinery is **RE-HOMED, not deleted**: it becomes the
21
- > **review door** at the end of EVERY run (green and soft-green alike), where the person
22
- > `accept`s, `rerun`s (their text is the gap, funded as a FRESH engagement with its own money
23
- > and time) or `pause`s — and **the door never changes the loop's own verdict**. `soft-green`
24
- > gets a `judged-floor` stage behind a signed rubric card and a signed calibration set.
25
- > **Everything documented in this file describes the code as it actually is on this branch**,
26
- > where `hitl` IS admitted, `human-confirms` IS live, and the three `hitl-*` terminals exist —
27
- > integrate against what is written here, and expect the class-level rename when the next rung
28
- > lands. Records: PRD Addendum v1.71 and
20
+ > the public surface — a breaking change. Runs on two worker surfaces — the API `Loop` and,
21
+ > for `clipipe-subscription`, the CLI's native tool channel (module 4d). API sections fill in
22
+ > as build-ladder rungs land (PRD §10). What is settled — the boundary, the architecture, the
23
+ > refusals, the constraints — is settled for good. Per LIBRARY_CONVENTIONS §3 this file ships
24
+ > with the package and is the complete adopter contract; the README is only the pitch.
25
+
26
+ > **LEGACY — `hitl` (retired by design, PRD Addendum v1.71, 2026-08-17).** `hitl` remains
27
+ > ADMITTED in code today — `VERDICT_TYPES` still lists it, the `human-confirms` stage kind is
28
+ > still live (never offered, at-most-once, must be last), and the two `hitl-*` terminals
29
+ > (`hitl-pause`, `hitl-decision-red`) still exist and still work exactly as documented
30
+ > wherever this file describes them below. It is retired as a *design direction*, not deleted
31
+ > as *code*: `LOCKED_CLASSES` is empty, so nothing refuses a `hitl` job today, but new
32
+ > integrations should target `green` / `soft-green` plus the review door instead — that pair
33
+ > is the forward path for anything needing a person's judgement, and `hitl`'s removal is a
34
+ > future breaking change, not yet scheduled. Records: PRD Addendum v1.71,
29
35
  > `docs/product/2026-08-17-softgreen-review-door-design.md` (repo-only, not shipped);
30
36
  > evidence F102–F105.
31
37
 
@@ -46,6 +52,35 @@ inherited rule carries the green that minted it and the contrast that attributed
46
52
  - **Is not:** a general agent, a swarm, or an orchestrator framework. One-off or small
47
53
  jobs are out of scope — that's a CLI session, not a bareloop job.
48
54
 
55
+ ## Which mode / components do I need?
56
+
57
+ - **Verdict class — how "done" gets decided:** `green` (a mechanical close: exit code =
58
+ truth, `src/kinds.js` `LIVE_KINDS`) is the default and the only class with a live-proven
59
+ paid run. `soft-green` (a judged floor behind a signed calibration gate, plus a review
60
+ door) is fully admitted (`VERDICT_TYPES`, `src/job.js:106`) but has never rendered a live
61
+ verdict — see `## Public API` → "THE JUDGED FLOOR" and "THE REVIEW DOOR" below. `hitl` is
62
+ LEGACY: still admitted (removal is a future breaking change) but retired as a design
63
+ (PRD Addendum v1.71); its pause machinery lives on as the review door, not as a class to
64
+ pick for new jobs.
65
+ - **Worker surface — how the agent runs:** `provider: 'anthropic-api'` (the default, the
66
+ only one with an operational guarantee, F48) drives `bare-agent`'s `Loop` and takes the
67
+ shell-owned `provider` binding. `provider: 'clipipe-subscription'` (`src/job.js:135`)
68
+ drives the worker natively over the Claude CLI's own tool channel and requires
69
+ `opts.nativeProvider` in `runJob`/`runPlan` — its `costUsd` axis is notional, never pools
70
+ with `anthropic-api` on cost, and budgets don't transfer between them.
71
+ `job.provider === 'clipipe-subscription'` with no `nativeProvider` supplied is
72
+ `interpreter-red`, never a silent fall-back to the metered API (`src/planrun.js:1073`).
73
+ - **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
74
+ `bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
75
+ ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
76
+ (`src/reuse.js`, `scripts/run-u.mjs`) instead reads/writes dated rows in an
77
+ operator-supplied directory of plain JSON files — no database, no default location; a
78
+ named-but-missing registry is a red, never conjured (`## The reuse registry (Layer 3)`
79
+ below).
80
+ - **Read shim:** `readShim` defaults to `false` on both `runJob` (`src/run.js:205`) and
81
+ `runPlan` (`src/planrun.js:808`) — the OFF arm wraps nothing at all. Opt in with `true`,
82
+ `'cap'`, or `'diff'` (`src/readshim.js`) only once the read-hygiene tradeoff is wanted.
83
+
49
84
  ## Minimal usage
50
85
 
51
86
  ```js
@@ -100,6 +135,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
100
135
  | `cadence` | `{ unit: hour\|day\|week, every: 1..30 }` | validated now, consumed at N5 (Scheduler) |
101
136
  | `budgetUsd` | `0 < n <= shell cap` | ceiling chain: workflow ≤ job ≤ shell — each layer may tighten, never exceed |
102
137
  | `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage number, so a spec with many stages can validate while its overshoot dwarfs its cap: the clock reports that honestly rather than the floor pretending to prevent it (stage-aware floor parked, PRD v1.39). Operator-only, tighten-only; adding or changing it changes the spec hash |
138
+ | `model` | optional non-empty string, exact provider model id (e.g. `"claude-sonnet-5"`) | the WORKER's model, build-list #3. Absent means today's runner default; present it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the judge model stays library-pinned (`JUDGE_MODEL`) pending recalibration |
103
139
  | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
104
140
  | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
105
141
  | `escalation` | `{ mode: "decision-ready" }` | the pain channel is not optional |
@@ -110,8 +146,8 @@ the step plan at run time (gated by `validatePlan`); the human signs only:
110
146
  | field | shape | notes |
111
147
  |---|---|---|
112
148
  | `goal` | non-empty text | what the agent plans against |
113
- | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **`green` and `hitl` are ADMITTED** (N4 slice 1) — *and `hitl` is RETIRED as a class by the 2026-08-17 design (see the direction note at the top): it still works exactly as documented here on this branch, and the forward path for a job needing judgement is `soft-green` plus the end-of-run review door*; `soft-green` is still declared-but-locked and reds `request-red` with the type as a structured `verb` field (the tool-menu pattern) until its judged floor exists. Every `request-red` also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
114
- | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) **only** for the declared-but-locked verdict classes | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close validates (the declared-but-locked verdict classes still parse) but the plan flow refuses it at runtime as `close-unsupported` — it names no command to run |
149
+ | `verdictType` | `green` \| `soft-green` \| `hitl` | declared radio, never inferred (`VERDICT_TYPES`, frozen). **All three are ADMITTED today** — `LOCKED_VERDICTS` is empty, so nothing reds `request-red` on the class itself. `soft-green` has its judged floor (`judged-floor`, signed rubric card + calibration gate) and is the forward path for a job needing judgement, paired with the end-of-run review door; `hitl` is LEGACY — retired as a design direction (see the direction note at the top) but still admitted and still works exactly as documented here. Every `request-red` (e.g. for a locked TOOL verb, not a verdict class) also carries `lib` — the territory the demand lands against, stamped at the emit site (`verdictType` → `bareloop`, a locked tool verb → `bare-agent`): the ledger keys and its `suggestedAsk` seed on it, so a bareloop-catalogue refusal never files as an upstream ask |
150
+ | `close` | **an ORDERED LIST of named stages** `[{ name, cmd, expect, judged?, gapKeep?, offer?, needs? }, ...]` (PRD v1.28), or a close object (table below) | the destination, the only thing hand-authored; the check menu DERIVES from it (below). The plan flow executes a staged close directly and adapts a bare `predicate` object into a one-stage list; a `gold`/`rubric`/`hitl` object close still VALIDATES (`CLOSE_TYPES`) but the plan flow refuses all three at runtime as `close-unsupported` — an object close names no command to run. The modern route to a judged or human-decided close is a staged `closeDecl` (`judged-floor` / `human-confirms` stage kinds), not this object form |
115
151
  | `closeDecl` | **the AUTHORED close** `{ genre: "TYPES", lang: "js"\|"python", stages: [{ name, kind, params }], notes? }` | the ALTERNATIVE to `close`, and the point of the close-authoring rung: the user answers an interview and an LLM composes a DECLARATION over kinds whose implementations we own — never a script, never a shell fragment, never a new kind. **`close` and `closeDecl` are alternatives**: declaring both reds `close-duplicated` (two closes are two arbiters). It is HARD-class by construction (`CLASS_BY_CLOSE.declared`), so a locked verdict on one reds `close-hierarchy` as well as `request-red`. The declaration is validated by `validateCloseDecl`; the TREE-GROUNDED half of that gate (the path rule, and the scoped-job derivation that arms the F84 one-population law) is DEFERRED at spec-validation time — a job spec is validated with no repository in hand — and the runner re-runs it GROUNDED against the real seed before any stage and before any token. It stores the counting RULE and never a number (D12): there is no seed field, and `baseline: "seed"` is measured at each run's own HEAD |
116
152
  | `checks` | **RETIRED** (PRD v1.28/v1.32) | hand-authored checks are gone, not merely discouraged: declaring `checks` reds `checks-derived` by name. The check menu is DERIVED from the close's own stages instead — see **Staged close** below. The hazard this removes is measured, not theoretical: job #5's three hand-written checks were re-implementations of three stages the close already ran, and a hand-carved copy can drift LENIENT (the worker passes the operator's ruler and fails the real inspection) |
117
153
  | `tools` | optional unique subset of `TOOL_MENU` (14 verbs, below) | the CEILING every plan step's grant must fit inside (omitting it means the full menu — and the hash is taken over that RESOLVED form, so a `TOOL_MENU` widening flips an omitted-`tools` spec's hash and forces a re-sign; see `jobSpecHash`, MED-1); `run` is `LOCKED_TOOLS` and reds `request-red` — locked-but-listed, and the red IS the admission evidence the ledger tallies (a typo stays `invalid-value`). A ceiling of write-class and store-class verbs ONLY reds `invalid-value`: the scout surveys read-only, so it would be handed an empty menu and survey blind |
@@ -203,14 +239,15 @@ redo it. Write the goal from the close's own stage list, in the close's own word
203
239
  a few explicit requirements rather than prose. Preflight already names every stage on the
204
240
  `check-menu` spine event — read it back and check your goal against it before you sign.
205
241
 
206
- **Close types and the hierarchy — the OBJECT form, which survives only for the
207
- declared-but-locked verdict classes** (a close is data, never code; verdict-class laundering
208
- is a named red `close-hierarchy`). The go-forward shape is the staged list above; a
209
- `predicate` object is legal shorthand for a one-stage list and the plan flow adapts it, but
210
- `gold`/`rubric`/`hitl` validate and then refuse at runtime (`close-unsupported`) since v1
211
- admits `verdictType: green` and `hitl` only — and N4's hitl is a STAGE
212
- (`human-confirms` inside a `closeDecl`), never a close TYPE, so the `hitl` OBJECT form below
213
- stays refused at runtime exactly as it was:
242
+ **Close types and the hierarchy — the OBJECT form, which the plan flow only partially
243
+ supports** (a close is data, never code; verdict-class laundering is a named red
244
+ `close-hierarchy`). The go-forward shape is the staged list above; a `predicate` object is
245
+ legal shorthand for a one-stage list and the plan flow adapts it, but `gold`/`rubric`/`hitl`
246
+ validate and then refuse at runtime (`close-unsupported`) regardless of verdict class — the
247
+ plan flow has never executed an object close beyond `predicate`. `hitl` (LEGACY) is a STAGE
248
+ (`human-confirms` inside a `closeDecl`) and `soft-green`'s judged floor is likewise a STAGE
249
+ (`judged-floor` inside a `closeDecl`) — never close TYPES — so both OBJECT forms below stay
250
+ refused at runtime exactly as they were:
214
251
 
215
252
  | type | fields (exact — extras red) | legal class |
216
253
  |---|---|---|
@@ -484,17 +521,19 @@ signed `writeScope` alone, never a free-text fallback. Menus exported: `EXIT_TYP
484
521
  The one layer where a human still wrote code. A close used to be a hand-written script
485
522
  per patient; now the user PICKS A VERDICT CLASS, answers that class's questions, and an LLM
486
523
  composes a DECLARATION over kinds whose implementations bareloop owns. **v1 admits exactly
487
- ONE genre** (`TYPES`: a type checker stops complaining without breaking the tests) and
488
- exactly one verdict class (`green`) — everything else refuses honestly, and every refusal
489
- is COUNTED.
524
+ ONE genre** (`TYPES`: a type checker stops complaining without breaking the tests) —
525
+ everything else refuses honestly, and every refusal is COUNTED.
490
526
 
491
527
  **The verdict class is the USER's answer (PRD v1.57 §1), and it DRIVES the authoring.**
492
528
  `verdictType` is a declared radio the preflight validates, never inferred:
493
- `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` =
494
- `soft-green` alone (N4 slice 1 admitted `hitl`; `soft-green` waits for its judged floor —
495
- **which is now the forward path: `hitl` is retired as a class by the 2026-08-17 design and its
496
- pause becomes the end-of-run review door, unbuilt as of this branch**). Picking a locked one returns a counted
497
- `request-red` refusal at ADMISSION — before that class's questions are ever asked. The
529
+ `VERDICT_CLASSES` = `green` | `soft-green` | `hitl`, with `LOCKED_CLASSES` = **empty** —
530
+ `soft-green` shipped its judged floor in v0.12.0 (2026-08-23) and is fully admitted
531
+ alongside `green`; `hitl` is LEGACY (retired as a class by the 2026-08-17 design, PRD
532
+ Addendum v1.71) but still admitted too, since removal is a future breaking change. The
533
+ review door (`accept`/`rerun`/`pause`) that re-homes hitl's pause machinery is shipped and
534
+ live — always on for `soft-green`, opt-in for `green` via `reviewDoor: true` /
535
+ `--review-door`. A future LOCKED_CLASSES entry would still return a counted `request-red`
536
+ refusal at ADMISSION — before that class's questions are ever asked. The
498
537
  pick is also a PROMISE: every catalogue kind carries the `verdictClass` it can honestly
499
538
  render, `closeCeiling(declaration)` reports the highest class a declaration demands, and a
500
539
  declaration ABOVE the pick is a `class-ceiling` red naming the kind that raised it (inert
@@ -505,7 +544,7 @@ a step, or test it without a provider.
505
544
 
506
545
  | step | call | what it is |
507
546
  |---|---|---|
508
- | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), and the interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the HITL set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the soft-green set is named but LOCKED (`questions: null`, absent rather than empty) and its selection refuses before it runs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
547
+ | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. THREE frozen question sets keyed by verdict class (`QUESTION_SETS`; `questionsFor(cls)` / `requiredAnswersFor(cls)`), all three LIVE as of softgreen module 3 — nothing is locked today (`LOCKED_CLASSES` is empty) — and the interview asks NOTHING about a genre and NOTHING about the repository. **Ask the library for the set and its numbers — never hardcode a count or a slot number**: the green set has lost two slots since it was frozen (D13's genre confirm, then the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input and asking for it again invites a second, drifting answer for a fact the machine already holds), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is five questions, answers keyed by number; the LEGACY `hitl` set is those five byte for byte plus one more (what the signer is deciding when they look at the result — `human-confirms`' `ask`), which is the spread's next number and moves with a green-side deletion; the `soft-green` set is likewise those five plus two more — Q6 the signed rubric card (what separates a pass from a fail) and Q7 the frozen calibration set (one example to pass, one to fail) — the judged floor's two required inputs. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
509
548
  | survey | `runAuthorScout({ workdir, provider, attempts?, ceilingUsd? })` → `{ state, facts, reason, meta, raw, raws, calls, budgetStop }` | a bounded READ-ONLY LLM survey. Read-only by MENU CONSTRUCTION (`AUTHOR_SCOUT_VERBS` = the full menu minus write-class and store-class verbs), 8 rounds, F59's reserved toolless final round. **`state: 'ABSENT'` means the scout did not complete — never "no special facts are needed"**, and a parsed `{}` is one of its five ABSENT routes. Up to `SCOUT_ATTEMPTS` (**3**, hardcoded — PRD v1.58) attempts: `attempts` is TIGHTEN-ONLY and CLAMPS rather than throws (floor **1**, because a scout that never ran is an ABSENT nobody can act on), the direction every cap in this system runs in. **A retry fires on the typed MALFORMED class alone** (`SCOUT_RETRY_CAUSES` = `empty` \| `unparseable`); what is excluded is excluded for a reason — `call-failed` covers transport and `truncated:max_tokens`, both provider-red with NO redraft; `short` is F59's cut-off population and already has its own instrument INSIDE the attempt; `non-object` and `empty-object` are valid JSON with wrong or vacuous content, which is a SEMANTIC failure, and F38/F39 measured what re-asking one buys (the same distribution, sampled twice) — that is the self-healing line, and it is not crossed here. The re-ask is TOOLLESS over the conversation the survey already produced (the repository was already read; only the emission was unreadable) and names the mechanical parse error and nothing else. **There is no JSON repair behind it and there never will be** — a repairer decides what the model MEANT and writes it down as though the model had said it, in the one artefact whose whole job is to be honest about what a repository contains. `meta.attempts` / `meta.attemptsAllowed` record what was paid for; every attempt is metered under its own label (`author-scout`, `author-scout#2`, …) and leaves its raw behind. **`ceilingUsd` is the operator's money ceiling** (PRD v1.62) — checked BETWEEN attempts and before F59's reserved round, so no paid call escapes it; `null`/absent is UNBOUNDED, and a non-null non-finite value THROWS (the shared `capStop` seam — see `authorClose` below). When it ends the ladder, `budgetStop` names which stop (`cap-halt` \| `pricing-red`) and a survey that was never asked for carries the typed cause `not-funded` — deliberately OUTSIDE `SCOUT_RETRY_CAUSES`, because a retry is precisely what the cap forbade |
510
549
  | listing | `buildSeedListing({ workdir, seedRef, sourcePaths, testPaths })` | mechanical, `$0`, no model. `files` is the WHOLE tree (what the validator judges paths against); `block` is scoped to the survey's own paths and capped in ANNOUNCED tiers (what the prompt carries). Handing the validator the scoped half would make a job scoped to `src/` read as whole-tree and silently disarm the one-population law |
511
550
  | authoring | `authorClose({ workdir, seedRef, lang, verdictType, answers, scout, listing, generate, ceilingUsd? })` | the grounded loop: author → validate → run EVERY stage at the seed → feed the MEASURED results back → revise, bounded at `MAX_REVISIONS` (2) — a passed `maxRevisions` is TIGHTEN-ONLY, so a caller may LOWER that ceiling and never buy more revise rounds than the constant (floor **0**: one authoring call with no revise round is a legal ask, and unlimited revising would launder thrash as adaptation) — early-stop on an unchanged declaration. The declaration is emitted through a SCHEMA-FORCED TOOL CALL (`declare_close`), never parsed out of prose; the feedback is EXECUTION OUTPUT only — no model ever reviews another model's close. The return carries **`raws`**: what every model call actually SAID, the scout's attempts absorbed together with the declaration's, in the cost book's own order and under the same labels it meters. Each is `{ label, attempt, bytes, trimmed, text, cause, reason }` through the ONE persist helper (`scrubRaw` — redacted over the ONE `SECRET_PATTERNS` inventory, since a raw is the record most likely in this system to carry a live credential; bounded at `RAW_PERSIST_MAX` (8000) with the trim ANNOUNCING its own full size, and the cut walked back off a continuation byte so a multi-byte character is never split). `raws` is present on EVERY path **including the `$0` preflight refusals** — the path that spends nothing more is exactly the one whose evidence used to vanish with the process. `iterations` records what each turn MEANT (declaration, validation, seed read); `raws` records what it SAID, and a malformation is only ever visible in the second. **`ceilingUsd` is the same operator ceiling the survey took** — the scout's spend is ABSORBED into this loop's cost book before its first call is weighed, so spend already incurred folds in and re-entering cannot silently widen it. It is checked immediately before EVERY paid call (the author call, each revise, and each malformed-emission retry). **A MALFORMED ceiling is an ERROR, not a silent unbounded run** (v1.64 §3): the seam every ceiling read goes through (`capStop`, `src/text.js`) THROWS on a non-null non-finite value — `'2.50'`, `NaN`, `Infinity`, `true`, `{}` — before the first paid call, and the throw propagates uncaught rather than being caught into an ABSENT survey, because a caught one would hand you the very silent-unbounded run it exists to prevent. `null`/omitted is the STATED operator choice for unbounded; `0` and negatives are finite, legal, and cap-halt immediately. The advertised ceiling and the enforced ceiling can therefore never be different numbers. A `cap-halt` here can arrive with `ok:true` — a close that was already validated and measured survives the money stop, exactly as a late provider casualty does |
@@ -1640,13 +1679,17 @@ in the ledger whatever the person then does with it.
1640
1679
  write}` — the terminal registry write for a runner that drives `runJob` directly** (rather
1641
1680
  than through `runReuse`, which writes its own rows). It is what gives that runner's review
1642
1681
  door a row to release: without one, `accept` reaches `recordDoor` and reds `no-row-for-run`.
1643
- STORAGE ONLY — nothing here selects, promotes or reuses a bridge. Four refusals, each with a
1644
- named `reason`: no `registryDir` (`no-registry` — the registry is operator-supplied, never
1682
+ STORAGE ONLY — nothing here selects, promotes or reuses a bridge. Three refusals, each with
1683
+ a named `reason`: no `registryDir` (`no-registry` — the registry is operator-supplied, never
1645
1684
  conjured), `outcome !== 'green'` (`green-predates-run` for already-green — accept confirms a
1646
- verdict, it never mints one — else `not-green`), no executed `plan` (`no-plan-executed`, R1),
1647
- and a verdict class whose credit is not held (`credit-not-held` — `green`-class runs mint
1648
- nothing here, `quarantinesCredit` is the one predicate). `name` falls back to `job.job`; the
1649
- row is born HELD because the record carries the spec's own `verdictType`, never a flag. A
1685
+ verdict, it never mints one — else `not-green`), and no executed `plan` (`no-plan-executed`,
1686
+ R1). Every green class mints (hamr's ruling 2026-08-30): a `soft-green` row is born HELD —
1687
+ its learning credit is quarantined until a signer accepts at the door — and a `green`-class
1688
+ row is born RELEASED, because a machine's own close proved it and there was never a credit
1689
+ to quarantine; `quarantinesCredit(verdictType)` in `greenParts` is the one predicate, read
1690
+ off the record's own `verdictType`, never a flag. `door-accept` on a released row records
1691
+ the disposition and releases nothing — not an error; a run that earned no row at all still
1692
+ refuses `no-row-for-run`. `name` falls back to `job.job`. A
1650
1693
  refused underlying write returns `{minted: false, reason: 'write-refused', write}` with the
1651
1694
  reds on `write` — never a throw. Minting goes through the same cold-leg green write `runReuse`
1652
1695
  uses (shape-fork rule and collision refusals included), so the two runners can never spell
@@ -2467,6 +2510,73 @@ JSONL spine (single source for every UI), litectx store per job, per-run ledger.
2467
2510
  the bare suite: bareagent, bareguard, litectx, barebrowse, baremobile — the full surface
2468
2511
  is disclosed to the authoring agent; only admitted verbs are callable per job.
2469
2512
 
2513
+ ## Extension contract
2514
+
2515
+ hamr's law, verbatim: **the agent may author anything whose only verbs are gated
2516
+ primitives; it may never author the arbiter — the close, the budget, the fence, the
2517
+ merge.** What an adopter can plug into `runJob` (`src/run.js:205`):
2518
+
2519
+ - **Providers.** `provider` (shell-owned LLM binding for the `Loop`/API path),
2520
+ `nativeProvider` (a factory required for a `clipipe-subscription` job),
2521
+ `providerFor(tier)` (per-step model-tier provider, forwarded to the plan flow), and
2522
+ `judgeProvider` (SOFTGREEN's own seam — the provider a judged close stage runs its
2523
+ locate-only call through; distinct from `provider`/`providerFor` because the judge tier
2524
+ is fixed, never the worker's). All four are documented at `src/run.js:91-101`.
2525
+ - **Close stage kinds.** The catalogue is fixed and enumerated (`LIVE_KINDS`,
2526
+ `src/kinds.js:142`: `command-exit`, `count-not-worse`, `pattern-absent-in-diff`,
2527
+ `files-changed`, `human-confirms`, `judged-floor`). An adopter's declaration SELECTS
2528
+ from this menu; it never authors a new kind — a stage naming an unimplemented or unknown
2529
+ kind is a hard runtime stop (`src/kinds.js:1885-1888`), not a way to run adopter code
2530
+ inside the close.
2531
+ - **`emit` — the spine sink.** `makeSpine(file)` (`src/spine.js:21`) returns an
2532
+ `emit(type, data)` that appends one JSONL line per event; any function with that
2533
+ signature can stand in as a custom sink, but it is write-only — nothing in bareloop reads
2534
+ the spine back, so a custom sink cannot feed anything into a run's own decisions.
2535
+ - **`rates` is NOT an extension point.** A rates passthrough was designed but never built;
2536
+ pricing is guesstimate-plus-loud-`estimated`-flag by ruling, and rate tables are the
2537
+ customer's own responsibility, not a bareloop seam to wire up.
2538
+
2539
+ Everything else in `runJob`'s options — budgets, caps, the close, the fence, merge/publish
2540
+ — is arbiter territory and is never adopter-suppliable as behavior, only as signed,
2541
+ schema-validated data.
2542
+
2543
+ ## Threat model summary
2544
+
2545
+ **Local-trust model, NOT a sandbox.** The worker runs with the OS user's own permissions.
2546
+ Both the close (`src/ralph.js:222`) and a declared close stage's environment
2547
+ (`src/kinds.js:351`) start from a full copy of `process.env` and spawn child processes
2548
+ under it — this is a passthrough, not a jail.
2549
+
2550
+ **What the arbiter guarantees:**
2551
+ - **Cap-not-estimate money and time.** `budgetUsd` is validated `0 < budget <= shellCapUsd`
2552
+ with the check's own name, `cap-not-estimate` (`src/job.js:227`); spend is never a
2553
+ projection.
2554
+ - **A signed spec hash refuses drift.** `jobSpecHash`/`checkApproval` (`src/job.js:703,
2555
+ 723`) mean an edited spec is unapproved by construction; `runJob` red-lines
2556
+ `'unapproved-spec'` before any provider call if no approval record matches the exact spec
2557
+ version (`src/run.js:289-290`).
2558
+ - **Secrets never enter the tree, the spine, the ledger, or a close's child process.**
2559
+ `redactSecrets` (`src/validate.js:130`) scrubs at every emission boundary in the
2560
+ authoring path; `CLOSE_ENV_DENY` (`src/ralph.js:136-153`) strips provider keys and
2561
+ known credential-shaped env-var names from the copy of `process.env` handed to a close
2562
+ child, by name/prefix/suffix-shape — a strip of a COPY, so the host process (and
2563
+ bare-agent, which reads the real key every round) keeps its own credentials.
2564
+ - **The worker is fenced off the arbiter's own books.** The authoring scout's read scope
2565
+ denies the gate audit path and every `ARBITER_BOOK_STORES` path under the workdir
2566
+ (`src/authorscout.js:295`); a plan step's read scope is the workdir alone
2567
+ (`src/planrun.js:1894`).
2568
+
2569
+ **What it does not guarantee:** no network egress control and no container/VM isolation —
2570
+ this repo has neither built nor documented either. `CLOSE_ENV_DENY`'s deny rule matches
2571
+ only credential-shaped names (`src/ralph.js:136-153`); any env var that doesn't match that
2572
+ shape passes through to the close's child process untouched. `SSH_AUTH_SOCK` is one of them
2573
+ and stays on purpose — stripping it would break git-over-SSH inside a close — but the deny
2574
+ list itself carries no comment saying so; this paragraph is the record.
2575
+
2576
+ **What an adopter must do:** keys load from the environment only (`.env.example` below);
2577
+ the patient is always a COPY of the target repo, never the original — the copy is the
2578
+ blast radius.
2579
+
2470
2580
  ## What's NOT in bareloop, and why
2471
2581
 
2472
2582
  - **No agent-authored arbiters.** Closes, budgets, caps, merge/publish decisions live
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.15.0",
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.",
3
+ "version": "0.17.0",
4
+ "description": "Workflows that earn their own design, with receipts \u2014 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",
7
7
  "types": "./types/index.d.ts",
package/src/authorjob.js CHANGED
@@ -290,8 +290,8 @@ export function runInterview({ answers, verdictType = null, repoPath = null, que
290
290
  verb: 'non-green-verdict',
291
291
  path: 'repoPath',
292
292
  detail: 'This job has no code repository, so there is no seed to measure against and no changed set to read — '
293
- + 'nothing deterministic can decide whether it came back done. That needs a judged or a human close, and '
294
- + 'both are declared-but-locked in v1.',
293
+ + 'nothing deterministic can decide whether it came back done. That needs a judged (soft-green) or a '
294
+ + 'human close; this authoring flow drafts code-genre closes against a git seed only.',
295
295
  }),
296
296
  });
297
297
  }
package/src/job.js CHANGED
@@ -140,7 +140,7 @@ export const CONDITION_KEYS = Object.freeze(['providerPath', 'closeVerbosity', '
140
140
  // `steps` stays in the field list ONLY so a retired spec reds by name
141
141
  // (`shape-retired`) instead of falling through to a generic unknown-field —
142
142
  // the operator gets told what happened, not just that something is wrong.
143
- const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
143
+ const JOB_FIELDS = ['schema', 'job', 'description', 'provider', 'conditions', 'cadence', 'budgetUsd', 'maxWallMs', 'model', 'writeScope', 'steps', 'escalation', 'goal', 'verdictType', 'close', 'closeDecl', 'checks', 'tools'];
144
144
  /** the four-field plan shape's core (decision 5) — presence of any of these
145
145
  * declares the shape; `tools` (the ceiling) rides the shape but alone does not
146
146
  * declare it, so a legacy spec carrying it gets a pointed red, not a conflict */
@@ -240,6 +240,17 @@ export function validateJob(input, { shellCapUsd = 2 } = {}) {
240
240
  `integer milliseconds >= ${MIN_WALL_MS} (one close timeout). Enforcement is a between-round deadline, measured as maxWallMs + closeStages x closeTimeoutMs (design addendum 1: loop.stop() cannot cut an in-flight call; W5: every close stage runs under the full timeout), so a budget under one close cannot fund its own close and the advertised number would be more wrong than right`);
241
241
  }
242
242
 
243
+ // MODEL — the worker's provider model id (build-list #3, hamr's GO
244
+ // 2026-08-30). OPTIONAL, exactly like budgetUsd/maxWallMs before it: absent
245
+ // means today's behaviour (the runner's own default), present it is part
246
+ // of the signed hash — jobSpecHash canonizes the whole resolved spec
247
+ // (resolveSpec), so a spec that names a model is an operator signing off
248
+ // on THAT model, not merely on the goal/close it runs against. The judge
249
+ // model stays library-pinned pending recalibration — out of scope here.
250
+ if (spec.model !== undefined && !isNonEmptyString(spec.model)) {
251
+ red('invalid-value', 'model', 'non-empty string — exact provider model id (e.g. "claude-sonnet-5")');
252
+ }
253
+
243
254
  // 3. the outer write fence — operator law (interview decision #4), same
244
255
  // containment rules as the workflow layer THROUGH THE SAME CODE (F9: two
245
256
  // transforms would let a fence validate green and mean something else).
@@ -732,3 +743,30 @@ export function checkApproval(job, approvals) {
732
743
  const h = createHash('sha256').update(c).digest('hex');
733
744
  return approvals.some((a) => isObj(a) && /** @type {Record<string, unknown>} */ (a).specHash === h);
734
745
  }
746
+
747
+ /**
748
+ * Pure decision: which worker model id a run actually uses (build-list #3,
749
+ * hamr's GO 2026-08-30). The signed spec's `model`, if present, is what an
750
+ * operator SIGNED — it wins outright. A `--model` runner flag naming a
751
+ * DIFFERENT id is refused rather than silently overridden (spec wins, a
752
+ * mismatched flag is a mistake worth stopping on, never a silent swap); the
753
+ * same id from both is not a conflict. Extracted from scripts/run-u.mjs so
754
+ * the decision is testable without running the script or touching a provider.
755
+ * @param {{ specModel?: string, flagModel?: string, defaultModel: string }} args
756
+ * `specModel` — `job.model` if the signed spec carries one, else undefined.
757
+ * `flagModel` — the model id an explicit `--model <tier>` resolved to, else
758
+ * undefined (a flag that was never passed, not merely a default tier name).
759
+ * `defaultModel` — the runner's own default when neither spec nor flag says.
760
+ * @returns {{ model: string, source: 'spec'|'flag'|'default' }}
761
+ * @throws {Error} when the spec names a model and the flag names a different one
762
+ */
763
+ export function resolveWorkerModel({ specModel, flagModel, defaultModel }) {
764
+ if (specModel !== undefined) {
765
+ if (flagModel !== undefined && flagModel !== specModel) {
766
+ throw new Error(`spec names model "${specModel}" but --model resolved to "${flagModel}" — the signed spec wins; a mismatched --model flag is refused, never silently overridden. Drop --model or point it at the same model.`);
767
+ }
768
+ return { model: specModel, source: 'spec' };
769
+ }
770
+ if (flagModel !== undefined) return { model: flagModel, source: 'flag' };
771
+ return { model: defaultModel, source: 'default' };
772
+ }
package/src/reuse.js CHANGED
@@ -46,7 +46,7 @@ import { existsSync } from 'node:fs';
46
46
  import { join } from 'node:path';
47
47
  import { createHash } from 'node:crypto';
48
48
  import { createRequire } from 'node:module';
49
- import { loadRegistry, loadBridge, saveBridge, appendGreen, appendRed, mintBridge, registryExists, reuseEligibility, recordDoor, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
49
+ import { loadRegistry, loadBridge, saveBridge, appendGreen, appendRed, mintBridge, registryExists, reuseEligibility, recordDoor, QUARANTINED_CODE } from './bridges.js';
50
50
  import { renderListing, selectionPrompt } from './selection.js';
51
51
  import { closeStagesOf } from './plan.js';
52
52
  import { MIN_WALL_MS, jobSpecHash } from './job.js';
@@ -2128,7 +2128,7 @@ export function writeGreenRow({ registryDir, bridge = null, meta, record }) {
2128
2128
  * `no-row-for-run`. This is the row, and STORAGE IS ALL IT IS — selection, promotion
2129
2129
  * and reuse execution stay parked (layer-3-reuse). Nothing here reads a row back.
2130
2130
  *
2131
- * FOUR REFUSALS, each one a guard that already existed somewhere and is kept:
2131
+ * THREE REFUSALS, each one a guard that already existed somewhere and is kept:
2132
2132
  * - NO REGISTRY, no row. The registry path is operator-supplied and never
2133
2133
  * conjured; without one the door says, in words, that nothing was released.
2134
2134
  * - ALREADY-GREEN mints nothing. The close was satisfied before this run did
@@ -2136,16 +2136,15 @@ export function writeGreenRow({ registryDir, bridge = null, meta, record }) {
2136
2136
  * the writing side rather than the door's.
2137
2137
  * - A GREEN WITH NO PLAN mints nothing. The artifact that inherits is the one that
2138
2138
  * RAN, and there is none (R1).
2139
- * - A CLASS WHOSE CREDIT IS NOT HELD mints nothing HERE. That is `green` today, and
2140
- * it is main's behaviour kept rather than a new rule: this runner has never
2141
- * written a registry row for a green-class run, and widening what enters the
2142
- * registry is a reuse-rung question, not a side effect of fixing a door. The
2143
- * predicate is `quarantinesCredit` — the ONE spelling of "is this class held" —
2144
- * so the row this runner mints and the row a door releases can never disagree
2145
- * about which classes they are talking about.
2146
- *
2147
- * The row is born HELD for exactly the reason any judged green is: `greenParts` reads
2148
- * `verdictType` off the record. Nothing here sets the flag.
2139
+ *
2140
+ * hamr's ruling 2026-08-30 (PRD open item #16, "green-class registry hole"): a
2141
+ * green-class run (mechanical close) now MINTS its row too, born RELEASED — a
2142
+ * machine proved it, so there is no credit to quarantine. This drops the fourth
2143
+ * refusal this function used to carry (`credit-not-held`, main's behaviour kept
2144
+ * from before the ruling): every outcome that reaches this line and clears the
2145
+ * three refusals above now mints, held or not. Whether the minted row IS held is
2146
+ * decided one level down, in `greenParts`, off `quarantinesCredit(verdictType)` —
2147
+ * the ONE spelling of "is this class held" — never here.
2149
2148
  * @param {{registryDir: string|null|undefined, job: any, name: string|null,
2150
2149
  * outcome: string, plan: any, record: any}} o
2151
2150
  * @returns {{minted: boolean, reason: string|null, write: any|null}}
@@ -2156,7 +2155,6 @@ export function writeRunGreenRow({ registryDir, job, name, outcome, plan, record
2156
2155
  return { minted: false, reason: outcome === 'already-green' ? 'green-predates-run' : 'not-green', write: null };
2157
2156
  }
2158
2157
  if (!isObj(plan)) return { minted: false, reason: 'no-plan-executed', write: null };
2159
- if (!quarantinesCredit(job?.verdictType)) return { minted: false, reason: 'credit-not-held', write: null };
2160
2158
 
2161
2159
  const target = isNonEmptyString(name) ? name : job.job;
2162
2160
  const meta = {
package/types/job.d.ts CHANGED
@@ -82,6 +82,30 @@ export function jobSpecHash(job: object): string;
82
82
  * @returns {boolean}
83
83
  */
84
84
  export function checkApproval(job: object, approvals: unknown): boolean;
85
+ /**
86
+ * Pure decision: which worker model id a run actually uses (build-list #3,
87
+ * hamr's GO 2026-08-30). The signed spec's `model`, if present, is what an
88
+ * operator SIGNED — it wins outright. A `--model` runner flag naming a
89
+ * DIFFERENT id is refused rather than silently overridden (spec wins, a
90
+ * mismatched flag is a mistake worth stopping on, never a silent swap); the
91
+ * same id from both is not a conflict. Extracted from scripts/run-u.mjs so
92
+ * the decision is testable without running the script or touching a provider.
93
+ * @param {{ specModel?: string, flagModel?: string, defaultModel: string }} args
94
+ * `specModel` — `job.model` if the signed spec carries one, else undefined.
95
+ * `flagModel` — the model id an explicit `--model <tier>` resolved to, else
96
+ * undefined (a flag that was never passed, not merely a default tier name).
97
+ * `defaultModel` — the runner's own default when neither spec nor flag says.
98
+ * @returns {{ model: string, source: 'spec'|'flag'|'default' }}
99
+ * @throws {Error} when the spec names a model and the flag names a different one
100
+ */
101
+ export function resolveWorkerModel({ specModel, flagModel, defaultModel }: {
102
+ specModel?: string;
103
+ flagModel?: string;
104
+ defaultModel: string;
105
+ }): {
106
+ model: string;
107
+ source: "spec" | "flag" | "default";
108
+ };
85
109
  export const CLOSE_TYPES: readonly string[];
86
110
  /** the hierarchy: which verdict classes a close type may claim (PRD §7). Read by
87
111
  * the verdictType check — a declared verdict may not demand a class its close
package/types/reuse.d.ts CHANGED
@@ -470,7 +470,7 @@ export function writeGreenRow({ registryDir, bridge, meta, record }: {
470
470
  * `no-row-for-run`. This is the row, and STORAGE IS ALL IT IS — selection, promotion
471
471
  * and reuse execution stay parked (layer-3-reuse). Nothing here reads a row back.
472
472
  *
473
- * FOUR REFUSALS, each one a guard that already existed somewhere and is kept:
473
+ * THREE REFUSALS, each one a guard that already existed somewhere and is kept:
474
474
  * - NO REGISTRY, no row. The registry path is operator-supplied and never
475
475
  * conjured; without one the door says, in words, that nothing was released.
476
476
  * - ALREADY-GREEN mints nothing. The close was satisfied before this run did
@@ -478,16 +478,15 @@ export function writeGreenRow({ registryDir, bridge, meta, record }: {
478
478
  * the writing side rather than the door's.
479
479
  * - A GREEN WITH NO PLAN mints nothing. The artifact that inherits is the one that
480
480
  * RAN, and there is none (R1).
481
- * - A CLASS WHOSE CREDIT IS NOT HELD mints nothing HERE. That is `green` today, and
482
- * it is main's behaviour kept rather than a new rule: this runner has never
483
- * written a registry row for a green-class run, and widening what enters the
484
- * registry is a reuse-rung question, not a side effect of fixing a door. The
485
- * predicate is `quarantinesCredit` — the ONE spelling of "is this class held" —
486
- * so the row this runner mints and the row a door releases can never disagree
487
- * about which classes they are talking about.
488
- *
489
- * The row is born HELD for exactly the reason any judged green is: `greenParts` reads
490
- * `verdictType` off the record. Nothing here sets the flag.
481
+ *
482
+ * hamr's ruling 2026-08-30 (PRD open item #16, "green-class registry hole"): a
483
+ * green-class run (mechanical close) now MINTS its row too, born RELEASED — a
484
+ * machine proved it, so there is no credit to quarantine. This drops the fourth
485
+ * refusal this function used to carry (`credit-not-held`, main's behaviour kept
486
+ * from before the ruling): every outcome that reaches this line and clears the
487
+ * three refusals above now mints, held or not. Whether the minted row IS held is
488
+ * decided one level down, in `greenParts`, off `quarantinesCredit(verdictType)` —
489
+ * the ONE spelling of "is this class held" — never here.
491
490
  * @param {{registryDir: string|null|undefined, job: any, name: string|null,
492
491
  * outcome: string, plan: any, record: any}} o
493
492
  * @returns {{minted: boolean, reason: string|null, write: any|null}}