bareloop 0.27.0 → 0.29.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.
@@ -72,8 +72,10 @@ inherited rule carries the green that minted it and the contrast that attributed
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 —
74
74
  `PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
75
- `probeWarningLines(provider)` renders the loud launch-time marker `scripts/run-u.mjs`
76
- prints. That table is read to WARN, never to REFUSE. Each entry also declares its own
75
+ `probeWarningLines(provider)` renders the loud launch-time marker `src/userrun.js`
76
+ prints (also called from `src/interviewrun.js` at interview time — PANEL-BUILD.md P0
77
+ lifted both call sites out of the `scripts/run-u.mjs`/`scripts/run-interview.mjs` scripts
78
+ that used to hold them). That table is read to WARN, never to REFUSE. Each entry also declares its own
77
79
  `endpointKey` — the CONSTRUCTOR option name it reads an endpoint from — because
78
80
  bare-agent's providers do not agree on one spelling (`baseUrl` for anthropic/openai/
79
81
  gemini, `url` for Ollama) and none of them validate unknown option names, so a
@@ -94,7 +96,8 @@ inherited rule carries the green that minted it and the contrast that attributed
94
96
  - **Reuse — where a workflow comes from:** a plain `runJob` always drafts cold. Passing
95
97
  `bridge` starts from one standalone bridge file (`src/reuse.js`'s envelope, `## The reuse
96
98
  ENVELOPE and runReuse` below). The CLI's `--registry <dir>` / library-level `registryDir`
97
- (`src/reuse.js`, `scripts/run-u.mjs`) instead reads/writes dated rows in an
99
+ (`src/reuse.js`, `src/userrun.js` — lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0)
100
+ instead reads/writes dated rows in an
98
101
  operator-supplied directory of plain JSON files — no database, no default location; a
99
102
  named-but-missing registry is a red, never conjured (`## The reuse registry (Layer 3)`
100
103
  below).
@@ -234,7 +237,7 @@ contract the single close object has always used) plus:
234
237
  | `name` | unique kebab-case slug | the plan references it via `check-passes(name)`; duplicates red `duplicate-id` |
235
238
  | `offer` | optional boolean, default offered | `offer: false` hides the stage from the derived menu — it never reaches the agent. For a stage that cannot stand alone as a ruler: a PRECONDITION (e.g. "the seed commit exists" — passes instantly, teaches nothing) or the **final grading stage** — of the fourteen specs in `jobs/`, five hide a final `verdict` stage and seven hide a `changed-from-seed` precondition instead (the remaining two use an object-form close with no staged `offer` flags) (a per-spec convention, not a schema rule — nothing stops a stage named last from being offered except the spec author remembering to set the flag; preflight's `check-menu` event always names the full menu either way, so a forgotten flag is visible in the run's own record) |
236
239
  | `needs` | optional non-empty array of EARLIER stage names | a stage that reads what an earlier stage built (e.g. "the public API matches what an earlier stage emitted") names its prerequisite chain; picking it via `check-passes` runs the chain first, then the stage itself. Every name must be declared before this stage (`invalid-value` otherwise); `needs` + `offer: false` together is incoherent (`invalid-value`) — a stage with a chain to run must be reachable |
237
- | `direction` | optional `'up'` \| `'down'`, allow-list validated (anything else is `invalid-value`) | which way this stage's numeric series is READING as progress — `'down'` (fewer is better: typecheck errors, suppressions) or `'up'` (more is better: a fault-detection rate). **Absent means `'down'`** — every close built before this field existed is a count-shaped close, so the silent default is behaviour-identical for all of them (F120). Never inferred from the goal text and never LLM-judged — prose the agent writes must never influence its own halt rule, the same reasoning that keeps every close hand-authored or declared, never judged. Consulted by `isBetter(stage,a,b)` in `src/trend.js`, the one place the close-fix governor's `countImproved` and `best`-tracking read direction; `scripts/run-u.mjs` prints each stage's direction at the close-stage banner so a silent default is never invisible |
240
+ | `direction` | optional `'up'` \| `'down'`, allow-list validated (anything else is `invalid-value`) | which way this stage's numeric series is READING as progress — `'down'` (fewer is better: typecheck errors, suppressions) or `'up'` (more is better: a fault-detection rate). **Absent means `'down'`** — every close built before this field existed is a count-shaped close, so the silent default is behaviour-identical for all of them (F120). Never inferred from the goal text and never LLM-judged — prose the agent writes must never influence its own halt rule, the same reasoning that keeps every close hand-authored or declared, never judged. Consulted by `isBetter(stage,a,b)` in `src/trend.js`, the one place the close-fix governor's `countImproved` and `best`-tracking read direction; `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) prints each stage's direction at the close-stage banner so a silent default is never invisible |
238
241
 
239
242
  The stages run in declared order as the close itself; the first red renders the verdict and
240
243
  later stages never run. `checkMenu(close)` returns only the offerable stages (each with its
@@ -616,9 +619,9 @@ a step, or test it without a provider.
616
619
  | interview | `runInterview({ verdictType, answers, repoPath })` → `{ ok, answers, verdictType, refusal, reds }` | PURE — no model, no repo, no clock. The interview ADMITS only what is on the menu (`MENU_CLASSES`): nothing is locked today (`LOCKED_CLASSES` is empty), and any pick off the menu returns a refusal rather than `ok:true` — ask `MENU_CLASSES` which classes can be authored, never this sentence. 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 several slots since it was frozen (D13's genre confirm; the repo question, dropped 2026-08-15 because `repoPath` is mandatory structured input; and, at PRD item 33 M3 piece 3, the old "which files change/read" question, replaced by Source and Destination — MECHANICAL fields, proven against the machine, never a numbered answer), and it renumbers CONTIGUOUSLY from 1 each time rather than leaving a gap. Today the green set is THREE questions (Goal, Success, Guardrails); the `soft-green` set is those three plus one more — Judge Examples (one example to pass, one to fail, and why) — the judged floor's one required input, compiled into the signed rubric card and the frozen calibration set. `verdictType` and `repoPath` are STRUCTURED input, never parsed out of prose. Answers are scrubbed at INGEST |
617
620
  | 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 |
618
621
  | 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 |
619
- | 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. **F176 (fixed):** "kept the last accepted revision" is not the same claim as "kept the best revision" — a later revise round can VALIDATE fine yet MEASURE worse (every stage instrument-stops rather than reading a real red/green verdict). `declaration`/`seedRead`/`finalFrom`/the returned `genreEnv.dropped` are therefore the NEWEST SOUND measured iteration, not simply the last one, when the last one instrument-stopped and an earlier one did not; `stop` itself never changes. The swap is never silent: the return also carries **`fellBack`** — `null` on every run where it never fired, else `{from, to, brokenStages}` naming which iteration was dropped, which now stands, and which of the dropped one's stages instrument-stopped — reported through `onPhase('author-fallback', …)` as it happens and printed at the terminal readout (`fellBackLines`, `scripts/author-readout.mjs`). **F179/F180 (history):** bare-agent 0.42.0's OpenAIProvider used to THROW mid-call on malformed tool-call JSON (a raw SyntaxError, after the billed round), crashing past this function; a 2026-09-15 stopgap caught it as a `provider-red`/`ask.providerError` casualty (booked `costUsd: null`) so a sound prior declaration was kept via the fallback above rather than discarded. **F179 (bare-agent 0.43.0, BA-27 — the real mechanism):** the throw is fixed upstream. `OpenAIProvider`/`OllamaProvider.generate` now return a malformed round PRICED (real `usage`, never `costUsd: null`) with `toolCalls: []` plus their own `malformedToolCall: {name, error}` marker, which `Loop.run` surfaces unchanged on its return. For the authoring declaration and confirm-turn calls, `askStructured` reads that marker and RETRIES through the existing malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`) instead of stopping the run — nothing is ever repaired, and a malformed FIRST call no longer needs an earlier sound declaration to fall back to. The generic provider-casualty fallback above (idle-timeout, etc.) still applies for every other admitted class. **F184 (fixed, worker path):** the scout and the worker path never joined that retry ladder (still true — retrying is arbiter-adjacent, out of scope), but the worker path's `ask()` (`src/planrun.js`) now at least READS the marker where it already sees it and emits a distinct spine record, `worker-malformed-tool-call` (`{phase, iteration, name, error}`), before returning `r` unexamined exactly as before — no retry, no change to strikes/ladder/attempt counting or verdict routing, purely the same visibility bar this section's authoring-path fix already set |
622
+ | 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. **F176 (fixed):** "kept the last accepted revision" is not the same claim as "kept the best revision" — a later revise round can VALIDATE fine yet MEASURE worse (every stage instrument-stops rather than reading a real red/green verdict). `declaration`/`seedRead`/`finalFrom`/the returned `genreEnv.dropped` are therefore the NEWEST SOUND measured iteration, not simply the last one, when the last one instrument-stopped and an earlier one did not; `stop` itself never changes. The swap is never silent: the return also carries **`fellBack`** — `null` on every run where it never fired, else `{from, to, brokenStages}` naming which iteration was dropped, which now stands, and which of the dropped one's stages instrument-stopped — reported through `onPhase('author-fallback', …)` as it happens and printed at the terminal readout (`fellBackLines`, `src/authorreadout.js` — lifted out of `scripts/author-readout.mjs` by PANEL-BUILD.md P0; called from `src/authorrun.js`). **F179/F180 (history):** bare-agent 0.42.0's OpenAIProvider used to THROW mid-call on malformed tool-call JSON (a raw SyntaxError, after the billed round), crashing past this function; a 2026-09-15 stopgap caught it as a `provider-red`/`ask.providerError` casualty (booked `costUsd: null`) so a sound prior declaration was kept via the fallback above rather than discarded. **F179 (bare-agent 0.43.0, BA-27 — the real mechanism):** the throw is fixed upstream. `OpenAIProvider`/`OllamaProvider.generate` now return a malformed round PRICED (real `usage`, never `costUsd: null`) with `toolCalls: []` plus their own `malformedToolCall: {name, error}` marker, which `Loop.run` surfaces unchanged on its return. For the authoring declaration and confirm-turn calls, `askStructured` reads that marker and RETRIES through the existing malformed-emission retry ladder (`MAX_STRUCTURE_RETRIES`) instead of stopping the run — nothing is ever repaired, and a malformed FIRST call no longer needs an earlier sound declaration to fall back to. The generic provider-casualty fallback above (idle-timeout, etc.) still applies for every other admitted class. **F184 (fixed, worker path):** the scout and the worker path never joined that retry ladder (still true — retrying is arbiter-adjacent, out of scope), but the worker path's `ask()` (`src/planrun.js`) now at least READS the marker where it already sees it and emits a distinct spine record, `worker-malformed-tool-call` (`{phase, iteration, name, error}`), before returning `r` unexamined exactly as before — no retry, no change to strikes/ladder/attempt counting or verdict routing, purely the same visibility bar this section's authoring-path fix already set |
620
623
  | everything above, composed | `authorCloseForJob({ verdictType, answers, repoPath, lang, generate, ceilingUsd?, ... })` → `{ ok, closeDecl, verdictType, refusal, cost, ... }` | refuses at the cheapest gate that can refuse: an interview refusal costs **zero**. THE GENRE REFUSAL LIVES HERE (not in the interview): a language the catalogue owns no data for, and a LOCKED KIND the model reached for, both come back as counted `request-red` demand. **`ceilingUsd` is ONE number handed to BOTH paid seams** (the survey's and the declaration loop's) — the advertised ceiling and the enforced ceiling are the same ceiling. It travels as an EXPLICIT `null` when unset, because unbounded is a stated choice and an absent field read as falsy is that state reached by accident — and anything that is neither a finite number nor `null` throws at the seam rather than becoming that state by typo |
621
- | assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `scripts/run-author.mjs` asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
624
+ | assembly | `assembleSpec(specDraft, { closeDecl, verdictType })` | folds the authored half into the OPERATOR's half. Budgets, the fence, cadence, escalation and the provider are never authored by anything here. The GOAL is passed through, not generated. **A draft that ALREADY carries the authored half is REFUSED — it THROWS, never merges and never overwrites.** `AUTHORED_SPEC_FIELDS` (`close`, `closeDecl`, `verdictType`) names the fields this fold WRITES, as data, so the refusal and the fold cannot disagree about which half of the spec is which; the rule is `job.js`'s one step earlier (two closes are two arbiters, and picking one silently is how a signed artefact stops meaning what the signer read). `src/authorrun.js` (lifted out of `scripts/run-author.mjs` by PANEL-BUILD.md P0) asks the same question at **$0 BEFORE the scout**, so the answer never arrives after the model has been paid |
622
625
  | the three gates | `prepareSigning({ spec, workdir, seedRef? })` → `{ ok, specHash, gates, work, guards, refusal }` | D9, and it NEVER signs |
623
626
 
624
627
  **D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
@@ -859,13 +862,17 @@ goal has to state everything the close will judge, and **nothing derives one fro
859
862
  or checks them against each other** — the only derivation runs close-stages → check menu, one
860
863
  hop, one direction. So the sole defence against an unstated stage is that the person signing
861
864
  reads both halves at once, and any surface you build around `prepareSigning` inherits that
862
- obligation. Both reference runners now do it: `scripts/run-u.mjs --approve` names every close
865
+ obligation. Both reference runners now do it: `src/userrun.js --approve` (lifted out of
866
+ `scripts/run-u.mjs` by PANEL-BUILD.md P0) names every close
863
867
  stage (name + kind) under the goal — for **BOTH spec forms**, through `closeStagesOf`, since
864
868
  gating that block on `closeDecl` reached exactly one of the eleven shipped specs and left the
865
869
  other ten (command `close[]`) showing the goal with no stages under it, which is the same
866
870
  half-reading in the guard against it (F100); a command stage prints `[command]` and NOT the
867
- catalogue kind name it does not carry — and `scripts/run-author.mjs` prints the goal above the
868
- declaration via `declarationLines(spec)` in `scripts/author-readout.mjs`, rendered from the
871
+ catalogue kind name it does not carry — and `src/authorrun.js` (lifted out of
872
+ `scripts/run-author.mjs`) prints the goal above the
873
+ declaration via `declarationLines(spec)` in `src/authorreadout.js` (lifted out of
874
+ `scripts/author-readout.mjs` — no adapter script remains for that one, it moved whole),
875
+ rendered from the
869
876
  **RESOLVED** spec — the bytes that get hashed, never the draft and never the authored half
870
877
  alone. An absent goal renders as absent rather than as a bare label. This is a READING, not a
871
878
  validator: nothing compares the goal to the declaration, because deriving one from the other
@@ -881,7 +888,7 @@ calls no provider at all**, so a whole interview costs $0 and no model ever sees
881
888
  is REQUIRED with NO default — bareloop is LLM-agnostic, and defaulting it would silently re-lock
882
889
  every interview onto one vendor; missing or empty dies loud, at the door, naming the same menu
883
890
  `src/job.js`'s own validator admits. It is written into the draft's `provider` field, so
884
- `scripts/run-author.mjs` needs no flag of its own — it resolves whatever the interview wrote
891
+ `src/authorrun.js` (lifted out of `scripts/run-author.mjs` by PANEL-BUILD.md P0) needs no flag of its own — it resolves whatever the interview wrote
885
892
  down.
886
893
 
887
894
  `--base-url` is OPTIONAL with NO default (PRD item 33 close-out — L17 named the provider but
@@ -1023,7 +1030,8 @@ compose, each through a frozen plain-English line in `GUARD_DESCRIPTIONS` — be
1023
1030
  `CONFIRM_MENU` — plus the write fence when one is set; a guard with no line there throws
1024
1031
  rather than showing a blank description. The model instead returns `notChecked`: anything
1025
1032
  the person asked for that no listed check can verify, in the person's own words, never
1026
- silently dropped. It is shown under its own heading in the menu (`scripts/run-author.mjs`)
1033
+ silently dropped. It is shown under its own heading in the menu (`src/authorrun.js`, lifted
1034
+ out of `scripts/run-author.mjs` by PANEL-BUILD.md P0)
1027
1035
  and threaded through to `accepted.notChecked` and `confirmedBlock`'s own "THE PERSON ASKED
1028
1036
  FOR THESE, BUT NOTHING CHECKS THEM" section for the composer.
1029
1037
 
@@ -1042,7 +1050,9 @@ ACCEPTED** (F175, fixed 2026-09-14) — never a prior, superseded round's, and n
1042
1050
  a person's typed "fix" text as it used to be. On the round-2 "fix" terminal path (D3: there is
1043
1051
  no 3rd call) the model's own questions come first, then the person's still-pending fix text.
1044
1052
  Shown to the composer by `confirmedBlock` ("state these in your notes, never decide them
1045
- silently") and at the terminal signing readout (`openQuestionLines`, `scripts/author-readout.mjs`).
1053
+ silently") and at the terminal signing readout (`openQuestionLines`, `src/authorreadout.js` —
1054
+ lifted out of `scripts/author-readout.mjs` by PANEL-BUILD.md P0; no adapter script remains
1055
+ for that one, it moved whole).
1046
1056
  **Not fully closed:** the confirm menu still lets a person pick "Confirm" without being forced
1047
1057
  to resolve a raised question first — only the silent-drop-from-the-signed-record half of F175
1048
1058
  is fixed; the menu-bypass half stays open.
@@ -1153,7 +1163,8 @@ races), never to be folded into worker/interpreter reds.
1153
1163
  ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
1154
1164
 
1155
1165
  **`closeTimeoutMs` (PRD item 27/M3) is now OPTIONAL for every real caller.** Omit it (both
1156
- `src/cli.js`'s bundle runner and `scripts/run-u.mjs` do) and the run AUTOSETS the per-stage
1166
+ `src/cli.js`'s bundle runner and `src/userrun.js` — lifted out of `scripts/run-u.mjs` by
1167
+ PANEL-BUILD.md P0 — do) and the run AUTOSETS the per-stage
1157
1168
  close ceiling from a $0 seed timing pass, or reads the job spec's own signed override —
1158
1169
  "guesstimate + loud sign + customer override", the same shape rates-passthrough already
1159
1170
  uses (F113). Passing an explicit value here still wins outright over both (kept for
@@ -1162,7 +1173,8 @@ backward compatibility and test control) — see `resolveCloseTimeoutMs` further
1162
1173
  **`closeDir` (PRD item 27/M3 Part B, optional but effectively required for any
1163
1174
  hand-authored close that reads its own books)** — the close's own directory (pristine
1164
1175
  copies, calibration thresholds, logs), outside the patient tree. `src/cli.js` sets it to
1165
- `<bundleDir>/runs/<runid>/close/`; `scripts/run-u.mjs` sets it to its existing `spineDir`. A
1176
+ `<bundleDir>/runs/<runid>/close/`; `src/userrun.js` (lifted out of `scripts/run-u.mjs` by
1177
+ PANEL-BUILD.md P0) sets it to its existing `spineDir`. A
1166
1178
  close script whose source mentions `BARELOOP_CLOSE_DIR` and gets none here refuses the run
1167
1179
  at $0 (`close-dir-required`, before any provider call) — see `checkCloseDirRequired` below.
1168
1180
 
@@ -1861,7 +1873,8 @@ that moved (`door-accept-red`) and a missing registry all come back as named red
1861
1873
  a person) before it is honoured: hamr's ruling that an accept is not a rubber stamp, and the
1862
1874
  answer to a tree that can move in the 60 days a door keeps. A red REFUSES the accept with the
1863
1875
  stage named and records **nothing**. A pass records the disposition and, over a held judged
1864
- green, RELEASES the credit through module 6's `applyDoorDecision`. `scripts/run-u.mjs` resolves
1876
+ green, RELEASES the credit through module 6's `applyDoorDecision`. `src/userrun.js` (lifted
1877
+ out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) resolves
1865
1878
  the re-run's ceiling itself via `resolveCloseTimeoutMs` before calling this, and refuses outright
1866
1879
  (F137) as a named `close-timing-red` door stop — `answerReviewDoor` never called, nothing
1867
1880
  recorded — if that timing pass itself times out, rather than falling through to `runClose`'s
@@ -2227,7 +2240,8 @@ it came back for (the declared-fold precedent, `priorSpentUsd`'s exactly). Pass
2227
2240
  `resumeBranch`; `runReuse` does it for you. `null` — a leg killed before it ever branched — is
2228
2241
  the cold path, and correct, because a leg that never branched has no work to strand.
2229
2242
 
2230
- `scripts/run-u.mjs --resume <runid|path>` is the operator-side consumer: it skips the patient
2243
+ `bareloop run-u --resume <runid|path>` (`src/userrun.js`, lifted out of `scripts/run-u.mjs`
2244
+ by PANEL-BUILD.md P0 — the script is now a thin adapter over it) is the operator-side consumer: it skips the patient
2231
2245
  reset (`resumeTreeGate` instead — a dirty tree is what a resume expects; only a moved HEAD
2232
2246
  stops it), folds the halted run's money and wall in, returns to the dead leg's own work branch,
2233
2247
  re-enters at the checkpoint, and arms the outside watchdog on the REMAINING wall. The top-up itself stays a spec edit the human signs.
@@ -2484,12 +2498,15 @@ a count rather than throwing.
2484
2498
  file-hygiene layer, never inside `runBehaviour` itself.** The authoring scout's gate audit
2485
2499
  (`src/authorscout.js`'s `defaultSurveyor`) writes directly into the patient tree at its root
2486
2500
  (`<workdir>/gate-audit.jsonl`), and the tree's own `.gitignore` denies `*.jsonl`, so
2487
- `scripts/run-u.mjs`'s cold reset (`git clean -fd`, no `-x`) never removed it — the file
2501
+ `src/userrun.js`'s cold reset (`git clean -fd`, no `-x`, via `coldReset` — lifted with
2502
+ `moveStaleGateAudit` out of `scripts/u-patient.mjs` into `src/u-patient.js` by PANEL-BUILD.md
2503
+ P0) never removed it — the file
2488
2504
  accreted rows from every authoring scout AND every worker run that had ever touched the tree,
2489
- and `run-u.mjs`'s end-of-run rename claimed the WHOLE accreted file as "this run's own audit"
2505
+ and `run-u`'s end-of-run rename claimed the WHOLE accreted file as "this run's own audit"
2490
2506
  with no filtering. The fix is at the source, never a `run_id` filter bolted onto `runBehaviour`
2491
- (a single run legitimately spans several run_ids): `scripts/run-author.mjs` archives its own
2492
- audit out of the tree the moment authoring ends; `scripts/run-u.mjs` moves any stale audit
2507
+ (a single run legitimately spans several run_ids): `src/authorrun.js` (lifted out of
2508
+ `scripts/run-author.mjs`) archives its own
2509
+ audit out of the tree the moment authoring ends; `src/userrun.js` moves any stale audit
2493
2510
  aside right after `coldReset`, before its own Gate ever opens (never on a resume, where the
2494
2511
  tree's audit is that same run's own prior leg). With both halves in place, the file this
2495
2512
  section describes really is scoped to the one run reading it, restoring the claim above.
@@ -2512,12 +2529,21 @@ is `Math.round(bytesWithheld / 4)`, a bytes→tokens ESTIMATE named accordingly
2512
2529
  it as measured. **Emitted only when the shim is armed** (any `readShim` arm but the off one) —
2513
2530
  an unarmed run emits no record at all, never a fabricated zero. It carries no cost fields, so it
2514
2531
  is not a spend record and no spend-slicing instrument needs to account for it.
2515
- `scripts/run-u.mjs` prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
2532
+ `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) prints it as a `MEMORY-CACHE` line right after `BEHAVIOUR` when armed
2516
2533
  (`no record` if the run ended before the summary could fire); `scripts/behaviour-readout.mjs`
2517
2534
  prints the same line when handed the run's spine file as its optional third positional.
2518
2535
 
2519
2536
 
2520
- ### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` — `src/replay.js`
2537
+ ### `replayRun(spineEvents, auditEvents?, { runId? })` / `formatReplay(summary)` / `summarizeForAllLine(summary)` / `formatAllLines(entries)` — `src/replay.js`
2538
+
2539
+ `summarizeForAllLine(summary)` reduces one `replayRun` summary to one `--all`/directory-
2540
+ listing row's raw field strings (`id`/`job`/`shape`/`class`/`model`/`outcome`/`spend`/
2541
+ `wall`/`steps`/`reason`); `formatAllLines(entries)` renders every row `listSpines` (below)
2542
+ built for one directory as a fixed-column aligned table, with `<name> not-a-spine` lines
2543
+ interleaved at their original position for any file that didn't look like a spine. Both
2544
+ are PANEL-BUILD.md P0 additions to the package root: they already existed inside
2545
+ `src/replay.js` (built for `scripts/run-replay.mjs`'s own `--all` mode) but were not
2546
+ re-exported until this rung, alongside `src/replayio.js` below.
2521
2547
 
2522
2548
  PRD build-list item 6: today no failed run can be reconstructed in under five minutes — every
2523
2549
  byte is on disk (the spine JSONL + its gate-audit sidecar) but only hand-slicing JSONL reads it.
@@ -2767,6 +2793,45 @@ const audit = fs.readFileSync(auditFile, 'utf8').trim().split('\n').map(JSON.par
2767
2793
  console.log(formatReplay(replayRun(spine, audit, { runId: 'mszcthk1' })));
2768
2794
  ```
2769
2795
 
2796
+ ### `replayOne(spinePath, opts?)` / `listSpines(dir)` / `readHistoryLog(file)` — `src/replayio.js`
2797
+
2798
+ PANEL-BUILD.md P0, the rung's last task: the IO layer `replayRun`/`formatReplay` above
2799
+ never had — resolving a spine's gate-audit sibling on disk and reading it, and listing
2800
+ which `.jsonl` files in a directory are spines at all. This is the module `bareloop
2801
+ replay` and `bareloop history` are both thin callers of (previously duplicated ad hoc: a
2802
+ `readFileSync` inline in `src/cli.js`'s own `doHistory`, and a second copy of this exact
2803
+ IO logic living only in `scripts/run-replay.mjs` with no library form and no `bareloop`
2804
+ command over it — the gap PANEL-BUILD.md's own measured table named). Every function here
2805
+ is pure IO + content detection; the actual reconstruction is `replayRun`'s alone (above) —
2806
+ nothing here recomputes a field `replayRun`/`runBehaviour` already derive.
2807
+
2808
+ `src/replayio.js` also holds lower-level plumbing (`parseJsonl`, `isSidecarByName`,
2809
+ `looksLikeSpine`, `resolveSiblings`) that the three functions below compose internally —
2810
+ a raw JSONL parser, a filename predicate, a content predicate, and a sibling-path
2811
+ resolver. These are deliberately **not** re-exported from the package root: they are
2812
+ implementation steps, not their own adopter-facing promise, and each is exercised (and
2813
+ importable) directly from `src/replayio.js` by this repo's own tests.
2814
+
2815
+ - **`replayOne(spinePath, { preParsedSpine?, skipAudit? })`** → `ReturnType<replayRun>`.
2816
+ Reads the spine (+ sidecar unless `skipAudit`) and hands both straight to `replayRun`.
2817
+ `skipAudit` (carried over from the script this replaces, PR #23 review item 5): a
2818
+ directory listing never opens a run's sidecar at all, since `summarizeForAllLine` reads
2819
+ none of the audit-sourced fields — reading every run's sidecar in a `--all` listing was
2820
+ pure unused I/O, multiplied by every spine found.
2821
+ - **`listSpines(dir)`** → `Array<{kind:'spine', row: ReturnType<summarizeForAllLine>} |
2822
+ {kind:'not-a-spine', name}>`, one entry per `.jsonl` file found directly in `dir` (not
2823
+ recursive) that is not a sidecar by name. Feeds `formatAllLines` directly.
2824
+ - **`readHistoryLog(file)`** → `{rows, skipped}`. The one reader for a bundle's
2825
+ `history.jsonl` (`src/bundle.js`'s `appendHistory`); replaces `bareloop history`'s former
2826
+ hand-rolled `readFileSync`/split.
2827
+
2828
+ ```js
2829
+ import { listSpines, replayOne } from 'bareloop';
2830
+ import { formatAllLines, formatReplay } from 'bareloop';
2831
+ console.log(formatAllLines(listSpines('/path/to/patient-repo')));
2832
+ console.log(formatReplay(replayOne('/path/to/patient-repo/u-mszcthk1.jsonl')));
2833
+ ```
2834
+
2770
2835
  ### `PROMPT_REGISTERS` / `isPromptFile(path)` — `src/promptregisters.js`, and the prompt-commit rule
2771
2836
 
2772
2837
  PRD build-list item 5 (TODO #8), Q9 answered (hamr, 2026-08-25): "a check". A commit that
@@ -2932,7 +2997,7 @@ defaulted knob. Arbiter constants, hamr 2026-09-07 verbatim: **floor = 120,000 m
2932
2997
  | `TIMING_PREFLIGHT_CEILING_MS` | `600_000` (library default × K) | the GENEROUS provisional ceiling every stage runs under DURING the timing pass itself — never the autoset/override ceiling the run then uses, which is only known after the pass completes |
2933
2998
  | `timeCloseStages(stages, {cwd, redact})` | → `{ perStage: {name,ms,exitCode,timedOut}[], slowestMs, slowestName, anyTimedOut }` | runs EVERY stage once, ignoring verdicts (first-red-wins stays for the real verdict precheck) — a declared/kind stage (no `.cmd`) is skipped, contributing no reading rather than a false zero. A stage that never finishes within `TIMING_PREFLIGHT_CEILING_MS` reads `timedOut: true` |
2934
2999
  | `computeCloseTimeoutCeiling({slowestMs, overrideMs?})` | → `{ ceilingMs, source: 'estimated'\|'override' }` | `max(FLOOR, K × slowestMs)`, unless `overrideMs` is given, in which case it wins outright (may sit above OR below the estimate — not tighten-only) |
2935
- | `resolveCloseTimeoutMs({job, stages, cwd, redact})` | → `{ closeTimeoutMs, source, timing, timedOut }` | the ONE resolver `src/planrun.js` and `scripts/run-u.mjs` BOTH call (never two independent spellings of this precedence): a signed `job.closeTimeoutMs` wins with NO timing pass run at all; absent, the pass runs and the ceiling autosets. `timedOut: true` means a stage never finished the pass — the caller reds `close-timing-red` rather than trusting any number |
3000
+ | `resolveCloseTimeoutMs({job, stages, cwd, redact})` | → `{ closeTimeoutMs, source, timing, timedOut }` | the ONE resolver `src/planrun.js` and `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) BOTH call (never two independent spellings of this precedence): a signed `job.closeTimeoutMs` wins with NO timing pass run at all; absent, the pass runs and the ceiling autosets. `timedOut: true` means a stage never finished the pass — the caller reds `close-timing-red` rather than trusting any number |
2936
3001
  | `closeTimeoutBanner({ceilingMs, source, slowestMs?, slowestName?})` | → the printed line | ONE spelling for `estimated`/`override`/`explicit` (the last is the pre-M3 shell/test runtime-option escape hatch, never the signed field) — printed on every run so a silent default can never hide behind this feature the way the OLD 120s/900s defaults did |
2937
3002
 
2938
3003
  **`scripts/sign-close.mjs`** — `node scripts/sign-close.mjs <jobs/x.json> [--write]` or
@@ -2981,7 +3046,7 @@ hash and needs re-approval, exactly like any other semantic edit.
2981
3046
  | `close-sha-mismatch` | `exportBundle` (PRD item 27/M2) | a `close[].sha256` the spec carries does not match the bytes of the script actually being packed into the bundle — the two signatures (this one, and `bundleHash`'s manifest hash over the rewritten script) cover the same bytes by two different paths and must never drift apart silently (N4's own hazard, one layer up). Only fires when a value is PRESENT and wrong; a spec with no sha256 at all reds `missing-required` upstream, at `validateJob` |
2982
3047
  | `close-tampered` | `runPlan` (PRD item 27/M2) | a close script's bytes no longer match its signed `sha256` — checked at run start (alongside `close-absolute-path`, before the close-first precheck and before any provider call) AND before EVERY close run thereafter (`runCloseStages`'s wrapper in `src/planrun.js`, the one seam the precheck/preflight/check-passes/close-fix-loop all share). Distinct from `close-red` (a judged "no" from a working close) and from `close-crashed`/`close-timeout`/`close-killed` (the close ran and hit an instrument fault): this is decided BEFORE the close is even spawned, and it means the close that ran would not have been the one the operator approved. Registered in `CLOSE_FAULTS` (`src/ralph.js`) so it rides out through the same forbidden-zone machinery as every other close fault — never retried, never fed back as a gap |
2983
3048
  | `close-dir-required` | `runPlan` (PRD item 27/M3 Part B) | a close script's source mentions `BARELOOP_CLOSE_DIR` (it reads its own books directory) but the runner was given no `closeDir` at all — `checkCloseDirRequired` (`src/close-integrity.js`) reuses the same `readCloseScripts` reader the other two integrity checks share. $0, run start, before the close-first precheck and before any provider call |
2984
- | `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely). `scripts/run-u.mjs`'s review door hits the SAME red on its own `accept` timing pass (F137) — it refuses the door outright rather than calling `answerReviewDoor` at all, so nothing is recorded |
3049
+ | `close-timing-red` | `runPlan` (PRD item 27/M3) | the $0 timing preflight (`resolveCloseTimeoutMs`/`timeCloseStages`, `src/closetimeout.js`) ran every close stage once, ignoring verdicts, and at least one never finished within the provisional ceiling (`TIMING_PREFLIGHT_CEILING_MS`, the library default × K = 600s) — the close cannot run on this machine in a boundable time. Only reachable when NEITHER a signed `job.closeTimeoutMs` NOR a caller-passed runtime override is present (both skip the pass entirely). `src/userrun.js`'s (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) review door hits the SAME red on its own `accept` timing pass (F137) — it refuses the door outright rather than calling `answerReviewDoor` at all, so nothing is recorded |
2985
3050
  | `wall-under-close-timeout` | `runPlan` (PRD item 27/M3) | `job.maxWallMs` is set and is under the EFFECTIVE per-stage close timeout just resolved (autoset or signed) — a budget under one close cannot fund its own close. `validateJob`'s own `MIN_WALL_MS` floor only bounds the un-autoset library default; this run-start check is the honest shape once the real ceiling is known, rather than a silent clamp |
2986
3051
  | `bundle-missing` | `readBundle` | `dir` is not a directory at all |
2987
3052
  | `manifest-invalid` / `spec-invalid` | `readBundle` | `manifest.json`/`spec.json` could not be read or parsed |
@@ -3000,7 +3065,7 @@ between a swapped script and a fake green, so `bareloop run` calls it as literal
3000
3065
  before the envelope check, before the key check, before the worktree, before `runJob` is
3001
3066
  ever reached.
3002
3067
 
3003
- #### The CLI — `bareloop export | run | history`, `bin/bareloop.mjs`
3068
+ #### The CLI — `bareloop export | run | history | replay | run-u | interview | author`, `bin/bareloop.mjs`
3004
3069
 
3005
3070
  `bin/bareloop.mjs` is a ~10-line adapter: it supplies the real `deps` (real `env`/`stdout`/
3006
3071
  `stderr`/`cwd`/`stdin`) and turns the returned number into `process.exitCode` — never
@@ -3015,7 +3080,12 @@ same seam `tests/planrun.test.js` uses. `deps` also accepts `env`, `stdout`, `st
3015
3080
 
3016
3081
  Bare `bareloop` (no sub-command) prints a numbered menu — `1 export 2 run 3 history q
3017
3082
  quit` — and asks each sub-command's arguments one line at a time over the SAME code paths
3018
- below.
3083
+ below. `run-u`, `interview` and `author` (below) are not on this menu (PANEL-BUILD.md P0
3084
+ tasks 2/4-4/4): each one's own flag grammar is too wide for a line-at-a-time wizard, and the
3085
+ menu's job is to ask the SAME questions the sub-command below already answers, never to
3086
+ invent a new interview — this is doubly true for `interview`/`author`, which already ARE
3087
+ interviews of their own. All three are dispatched by name only: `bareloop run-u <flags…>`,
3088
+ `bareloop interview <flags…>`, `bareloop author <flags…>`.
3019
3089
 
3020
3090
  - **`bareloop export <jobs/x.json> --registry <dir> --out <dir>`** → resolves the spec's
3021
3091
  close-script paths against the spec file's own directory (never the process cwd), calls
@@ -3057,7 +3127,7 @@ below.
3057
3127
  readShim: 'cap', scout: true })`, spine written to `<bundleDir>/runs/<runid>/spine.jsonl`.
3058
3128
  A thrown `runJob` crash is caught and reported, exit `1`.
3059
3129
  8. The worktree's `gate-audit.jsonl` is moved to `<bundleDir>/runs/<runid>/gate-audit.jsonl`
3060
- (same relocation `scripts/run-u.mjs` does for in-repo runs). `spentUsd`/`spendComplete`
3130
+ (same relocation `src/userrun.js`, lifted out of `scripts/run-u.mjs`, does for in-repo runs). `spentUsd`/`spendComplete`
3061
3131
  are read off this run's own `job-end` spine event — never fabricated as `0` when unknown.
3062
3132
  9. `appendHistory` — one `history.jsonl` line: `{ runid, at, outcome, spentUsd,
3063
3133
  spendComplete, budgetUsd, maxWallMs, worktree, branch, bundleHash, approveHash }` (the
@@ -3091,9 +3161,149 @@ below.
3091
3161
  when none matches — which, cross-machine, is the common case, since the historic mint's
3092
3162
  real close-script path and the importer's own bundle path essentially never coincide.
3093
3163
 
3094
- - **`bareloop history <bundleDir>`** → prints every `history.jsonl` line verbatim, then
3095
- every bridge's `listingRow(b)` from `<bundleDir>/bridges`. Always exit `0`; "(no runs
3096
- yet)" when `history.jsonl` doesn't exist.
3164
+ - **`bareloop history <bundleDir>`** → prints every `history.jsonl` row (via
3165
+ `readHistoryLog` — `src/replayio.js`, PANEL-BUILD.md P0's last task; a malformed row is
3166
+ counted and skipped, never reprinted corrupt), re-serialized with `JSON.stringify` (byte-
3167
+ identical to the original line for every well-formed row, since `appendHistory` itself
3168
+ writes `JSON.stringify(row)` — `src/bundle.js`), then every bridge's `listingRow(b)` from
3169
+ `<bundleDir>/bridges`. Always exit `0`; "(no runs yet)" when `history.jsonl` doesn't exist.
3170
+
3171
+ - **`bareloop replay <spine.jsonl>`** / **`bareloop replay --all <dir>`** (PANEL-BUILD.md
3172
+ P0, last of the rung's four tasks) → the read side for the panel's History/Run/Audit/Job
3173
+ tabs: point it at one run's spine (+ its `-gate-audit.jsonl` sidecar, resolved
3174
+ automatically) and get `formatReplay`'s one-page report; point `--all` at a directory and
3175
+ get every spine found directly in it as one aligned table row each
3176
+ (`formatAllLines`/`summarizeForAllLine`). Read-only, $0, mints no verdict, writes nothing.
3177
+ Lifted verbatim out of `scripts/run-replay.mjs` into `src/replayio.js` (see the Public API
3178
+ entry below); that script is now this command's own thin adapter (`bareloop replay` is a
3179
+ fresh sub-command name — `bareloop`'s own numbered menu is unchanged, same reasoning as
3180
+ `run-u`/`interview`/`author` above). A `.jsonl` that carries no `job-start`/`run-start`
3181
+ record — content-detected, never by filename pattern (the real archive uses `u-<id>`,
3182
+ `battery-A1-<id>`, `reuse-<id>`, `job2-<id>`, and more, with no shared convention) — errs
3183
+ `does not look like a spine`, exit `1`, rather than silently printing an empty report; a
3184
+ directory of only such files under `--all` prints each as `<name> not-a-spine` instead of
3185
+ a blank table. Exit `0` on a normal report/listing (regardless of the RUN's own outcome —
3186
+ this command reports, it never grades), `1` on a missing file/directory or a non-spine
3187
+ file, `1` with a usage line on no args.
3188
+
3189
+ - **`bareloop runs`** / **`bareloop runs backfill <dir>`** (PANEL-BUILD.md P1, 2026-09-24
3190
+ rulings — "one home for runs", hamr's option B) → the one run list, at `~/.config/bareloop/
3191
+ runs.jsonl` (the same directory the keys file, PRD §7d, lives in — outside any repo tree,
3192
+ never moved into one). This is a LIST, not a relocation: patient copies stay exactly where
3193
+ they already are (`bareloop-patients/…` for `run-u`, `<bundleDir>/runs/<runid>/` for
3194
+ `bareloop run`); jobs stay in `jobs/` (moving them into the home too is deferred to P3,
3195
+ hamr picked option "A"). `bareloop runs` with no subcommand prints every listed row, one
3196
+ line each — `job (runid) · date · spine path`, with ` — file missing` appended when the
3197
+ row's `spine` no longer exists on disk. `bareloop runs backfill <dir>` scans `dir` and its
3198
+ immediate subdirectories for archived spines already on disk (both the free-standing
3199
+ layout, e.g. `<dir>/<x>/u-<id>.jsonl`, and the bundle layout, `<dir>/<x>/runs/<runid>/
3200
+ spine.jsonl`), reusing `src/replayio.js`'s `parseJsonl`/`isSidecarByName`/`looksLikeSpine`
3201
+ (never a second parser), and adds one row per spine whose runid is not already listed —
3202
+ idempotent, running it twice adds nothing the second time. Prints `added N, already listed
3203
+ M, skipped K (not a spine or unreadable)` — never silent about what it found. Read-only,
3204
+ $0: no interview, no author, no run trigger, no key ever read. A row is `{ at, runid, job,
3205
+ spine, patient, via }` — `spine`/`patient` are always absolute paths (`patient: null` when
3206
+ not known, e.g. every `via:"backfill"` row: no spine record carries a run's workdir);
3207
+ `via` is `'run-u'`, `'bundle'`, or `'backfill'`. `bareloop run-u` and `bareloop run` (the
3208
+ bundle path) each append their own row at run START, before the first paid call — a
3209
+ list-append failure is caught at both call sites and printed loudly to stderr
3210
+ (`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (…)`); the run itself
3211
+ always continues (a panel list must never block real, already-signed work). Interview/
3212
+ author sessions are NOT added (deferred to P3 — nothing to list at that stage yet). These
3213
+ functions (`appendRun`/`readRunList`/`backfillRuns`/`formatRunRow`, `src/runlist.js`) are
3214
+ NOT exported from the package root — the panel's HTTP server (P1's next task) will call
3215
+ them the same in-process way `src/cli.js` does, per the layering law
3216
+ (`docs/product/PANEL-BUILD.md` §2), not through `bareloop`'s public API.
3217
+
3218
+ - **`bareloop panel [--port N]`** (PANEL-BUILD.md P1) → a READ-ONLY `node:http` server
3219
+ (no new dependency), bound to `127.0.0.1` ONLY, default port `4700`. Serves the panel's
3220
+ look (Tokyo Night box-drawing TUI style) over the run list's real data: `GET /` the page;
3221
+ `GET /api/runs` (newest first, each row enriched with a glyph/check-type/spend/wall
3222
+ summary, `fileMissing:true` when the listed spine is gone — the client groups this SAME
3223
+ list by job for the left pane's "Workflows" view and renders it flat for "History", both
3224
+ behind one `Runs` tab toggle; there is no separate `/api/workflows` endpoint, deleted in
3225
+ the P1 Runs-tab merge); `GET /api/runs/:runid` (the full replay — parts/steps, counters,
3226
+ summary, model/provider/judge); `GET /api/runs/:runid/audit` (the gate-audit sidecar rows,
3227
+ ts-windowed to this run alone even when the sidecar file is shared with other runs, plus
3228
+ the matching raw-log text windowed the same way — when one resolves); `GET
3229
+ /api/runs/:runid/rounds?part=&attempt=&offset=&limit=` (one attempt's rounds, lazily
3230
+ paged, for the Audit tab's Grouped view); `GET /api/runs/:runid/job` (the signed spec's
3231
+ own fields, ONLY when a bundle-layout run's `spec.json` is reachable — a run-u run has
3232
+ none on disk, and this reads `resolved:false` with every spec-only field honestly
3233
+ `'unknown'`, never guessed from the spine's own narrower `job-start` record). Every
3234
+ endpoint is GET/HEAD only (anything else — including every write verb — is `405`); a URL
3235
+ never joins a path segment into a filesystem read — a runid is looked up in the run list
3236
+ first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
3237
+ suffix too, and every endpoint echoes back the LISTED runid, never a filename-derived one
3238
+ that can silently drop that suffix), and only the path THAT ROW stores is ever read. A
3239
+ taken port fails loudly (names the port, exit `1`) — this command never silently tries a
3240
+ different one. No interview, no author, no run trigger, no key/`.env` ever read on this
3241
+ path. One more caller of `src/replayio.js`/`src/runlist.js`, in-process, per the layering
3242
+ law (`docs/product/PANEL-BUILD.md` §2) — the panel's HTTP handler holds no flow logic of
3243
+ its own. Chat/Settings/authoring (P3/P4) are not built yet; the page says so rather than
3244
+ hiding the gap.
3245
+
3246
+ - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3247
+ (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
3248
+ U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
3249
+ **`src/userrun.js`'s own `main(argv, deps)`**, unparsed — that function is the flag
3250
+ grammar's ONE owner, and `scripts/run-u.mjs` calls the exact same function directly, so
3251
+ there is exactly one place this grammar is parsed, never two that can drift). Flags:
3252
+ `--job <key>` (one of the JOBS table's rows) **or** `--spec <path>` (an authored
3253
+ `resolved-spec.json`, F185) — exactly one, never both, never neither; `--resume
3254
+ <runid|path>`; `--door <runid|path>` with `--decide <rerun|accept|pause>` and (`rerun`
3255
+ only) `--text <…>`; `--review-door`; `--approve <specHash>`; `--registry <dir>` +
3256
+ `--workflow <name>`; `--model <sonnet|haiku>`; `--read-shim <off|cap|diff|all>`; `--scout
3257
+ <on|off>`. No key/no injected `deps.provider`: prints the preview (invocation hints, the
3258
+ right key name for the job's own provider) and exits `0`, spending nothing — the same
3259
+ preview `scripts/run-u.mjs --job <x>` prints with no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
3260
+ set. `scripts/run-u.mjs` is unchanged in behaviour: it is the pre-existing thin adapter
3261
+ over this same `src/userrun.js:main`, not rewired to go through `src/cli.js`.
3262
+
3263
+ - **`bareloop interview <flags…>`** (PANEL-BUILD.md P0 task 3/4) → the close-authoring
3264
+ interview, at the terminal, one question at a time (D10; `docs/logs/FINDINGS.md`'s
3265
+ interview surface). `src/cli.js`'s `interview` dispatch hands `rest` straight to
3266
+ **`src/interviewrun.js`'s own `main(argv, deps)`**, unparsed — the same one-owner shape
3267
+ `run-u` uses, and `scripts/run-interview.mjs` calls the exact same function directly.
3268
+ Flags: `--verdict <green|soft-green>`, `--provider <anthropic-api|openai-api|gemini-api>`,
3269
+ `--out <outdir>` (all three required, no defaults — PRD item 34 L17: a defaulted provider
3270
+ would silently re-lock every interview onto one vendor) plus optional `--budget <usd>` (the
3271
+ AUTHORING ceiling, unbounded if omitted) and `--base-url <url>`. It never talks to a
3272
+ provider itself: Source and Destination are its own first two questions (proven
3273
+ mechanically, $0, via `prepareSource`/`proveDestination`), then the picked class's frozen
3274
+ question set (`questionsFor`/`requiredAnswersFor`, `src/authorjob.js`), then the operator's
3275
+ own job name/budget/wall. On success it writes `answers.json`/`specdraft.json` under `--out`
3276
+ and either offers to spawn `scripts/run-author.mjs` (only when a clean key is present and no
3277
+ install gap remains — default answer is no) or prints the exact command to run it later.
3278
+ Because this module reads a real TTY/piped stdin, `src/cli.js` hands it `stdin`/`stdout`/
3279
+ `stderr` (the raw streams, not the wrapped `out`/`err` line-functions every other command
3280
+ here uses) rather than re-deriving them.
3281
+
3282
+ - **`bareloop author <flags…>`** (PANEL-BUILD.md P0 task 4/4) → the authoring pipeline: a
3283
+ real scout over a real repository, a real model filling the close declaration, D9's three
3284
+ mechanical gates, stopping at `prepareSigning` (it never signs and never runs the job —
3285
+ signing and running are `run-u`'s territory). `src/cli.js`'s `author` dispatch hands `rest`
3286
+ straight to **`src/authorrun.js`'s own `main(argv, deps)`**, unparsed, the same one-owner
3287
+ shape; `scripts/run-author.mjs` calls the exact same function directly. Flags: `--source
3288
+ <tree>` (a PREPARED tree a source door already froze — an unprepared path dies loud, naming
3289
+ the exact command to prepare one), `--answers <answers.json>`, `--draft <specdraft.json>`,
3290
+ `--verdict <green|soft-green>`, `--out <outdir>` (all required) plus optional `--timeout
3291
+ <ms>` (default 300000, per close stage) and `--budget <usd>` (the AUTHORING ceiling —
3292
+ unbounded, and announced as such, if omitted). A REFUSAL IS A RESULT: every stop (an
3293
+ interview refusal, an unauthorable close, a stage that cannot run, nothing red at the seed)
3294
+ is written to `--out` and counted as admission demand on the module's own spine
3295
+ (`author-<runid>.jsonl`), never swallowed. Exit vocabulary: `0` prepared/signable, `1` a
3296
+ refusal or a failed gate, `2` operator/config (including a malformed or missing provider
3297
+ key), `3` a secret-shaped leak in what this run wrote, `4` an unhandled crash (`author-crash`
3298
+ + `author-end{outcome:'crashed'}` on the spine — a spine that stops mid-sentence is
3299
+ indistinguishable from a run still in flight, F191). A real OS signal (SIGINT/SIGTERM/
3300
+ SIGHUP) during the paid span still writes `author-killed` + `author-end{outcome:'killed'}`
3301
+ and then RE-RAISES the signal itself (128+signo), never invents an exit code; SIGKILL is
3302
+ named but, being uncatchable, cannot be covered. The library function itself never calls
3303
+ `process.exit()` anywhere, on any path — every stop is a returned exit code (an internal
3304
+ `throw new ExitSignal(n)`, caught at the bottom of `main`), the same "a library function
3305
+ returns a code / throws" rule `src/userrun.js` already keeps. Like `interview`, `src/cli.js`
3306
+ hands it raw `stdin`/`stdout`/`stderr` (its one interactive seam is the confirm turn).
3097
3307
 
3098
3308
  **Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
3099
3309
  bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
@@ -3316,7 +3526,8 @@ on a green then that would be the place agent allowed to do changes") — it nam
3316
3526
  FENCE inside the copied repo, the signed `writeScope` field's job (`src/job.js:363`), never a
3317
3527
  filesystem drop-off point. It is recorded as declared, never proven by `proveDestination` and
3318
3528
  never handed to `copyOut` (`frontDoorFromManifest` returns `null` for a repo manifest).
3319
- Wiring it into `writeScope` landed at M3, ruling 2 — `scripts/run-interview.mjs` parses its own
3529
+ Wiring it into `writeScope` landed at M3, ruling 2 — `src/interviewrun.js` (lifted out of
3530
+ `scripts/run-interview.mjs` by PANEL-BUILD.md P0) parses its own
3320
3531
  Destination answer straight into `draft.writeScope` for a repo Source; this door itself still
3321
3532
  only records what it was told, unvalidated, exactly as before.
3322
3533
 
@@ -3339,7 +3550,7 @@ run — the same way `bareloop run <bundle> --repo <path>` varies the repo insta
3339
3550
  touching the bundle's own signature. So `job.js`/`jobSpecHash` carry nothing about it: the
3340
3551
  run's own instance lives entirely in the manifest `prepareSource` writes.
3341
3552
 
3342
- `scripts/run-u.mjs` reads that manifest via `readSourceManifest(dirname(workdir))` (`workdir`
3553
+ `src/userrun.js` (lifted out of `scripts/run-u.mjs` by PANEL-BUILD.md P0) reads that manifest via `readSourceManifest(dirname(workdir))` (`workdir`
3343
3554
  IS `<into>/tree`, so its parent is `<into>`) and reduces it with `frontDoorFromManifest`. No
3344
3555
  manifest → no front-door behaviour at all — every existing repo-patient job is untouched. A
3345
3556
  manifest that exists but cannot be read or parsed is a named stop, never a silent skip. When
@@ -3367,7 +3578,8 @@ manifest `readSourceManifest` already serves elsewhere — never typed, guessed,
3367
3578
  No sibling, more than one, a missing seed, or a gone tree each refuse by name. Everything
3368
3579
  past that resolution — the `--approve <jobSpecHash>` signature, budget/wall ceilings,
3369
3580
  coldReset, the close-first precheck — is untouched: `--spec` only NAMES the job, exactly as
3370
- a `--job` row does. `scripts/run-author.mjs`'s "SIGNING PREPARED — NOT SIGNED" screen now
3581
+ a `--job` row does. `src/authorrun.js`'s (lifted out of `scripts/run-author.mjs` by
3582
+ PANEL-BUILD.md P0) "SIGNING PREPARED — NOT SIGNED" screen now
3371
3583
  prints the ready-to-paste `--spec` command (real path, real `--approve <hash>`, the
3372
3584
  provider's real env-key per F187) — a person finishing the interview alone can reach a
3373
3585
  running job with no `jobs/` edit and no JOBS-table row.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.27.0",
3
+ "version": "0.29.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",