bareloop 0.29.0 → 0.31.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
@@ -5,6 +5,138 @@ All notable changes to bareloop are documented here. Format:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
+ ## [0.31.0] — 2026-09-29
9
+
10
+ ### Added
11
+
12
+ - **Panel Chat authoring (P3)** — the Chat tab now drafts a job from the page: a job card
13
+ (source, goal, Model, `Cap $` / Time cap), a chat thread, and Send / Revise / **Sign &
14
+ run** actions, backed by new `POST /api/author/*` routes. Every POST needs the
15
+ per-server-start token plus an Origin/Host check (the human-click guard); the sign route
16
+ additionally requires the exact spec hash from the session's own signing file and is the
17
+ only path that ever spawns `run-u` (argv array, ends in `--approve <hash>`). A single
18
+ progress indicator (amber cycling dots, green `[done]`, red `[stopped]` with the reason)
19
+ replaces per-phase chat bubbles; step lines carry no who-label; a pending ask always wins
20
+ the label ("waiting for your OK" / "install needed" / "waiting for your answer"). The
21
+ page opens on the Chat tab.
22
+ - **Install-needed pause + Check again** — a repo copy with missing dependencies no longer
23
+ refuses outright: the session pauses, names the copy and the exact install command, and a
24
+ **Check again** button re-runs the same check on the same copy (bareloop still never
25
+ installs anything).
26
+ - **Model check under Model** — one `[✓]`/`[✗]` line: key found/missing/bad-shape/rejected
27
+ (401/403 reads "key rejected", not "may be flaky") plus a $0 GET-only reachability probe
28
+ (`GET /api/author/model-check`, `checkProviderReachable` in `src/providers.js`). A missing
29
+ or bad key blocks Start; an unreachable endpoint shows red but never blocks.
30
+ - **One cap covers drafting + run** — the separate Drafting cap field is gone. Drafting runs
31
+ under the same signed Cap `$`, and at sign the run's enforced ceiling is Cap minus what
32
+ drafting already spent. `runJob` takes `draftSpentUsd` and `draftSpendComplete` (carried on
33
+ `job-start` as their own fields, never `priorSpentUsd`, so a first run never reads as a
34
+ resume); `run-u` gains `--draft-spent-usd <n>` and `--draft-spend-incomplete` (validated
35
+ like `--read-shim`/`--scout`, carried in every resume/decide re-invocation tail);
36
+ `bareloop author` prints its drafting spend and the exact `run-u` command. Spend renders
37
+ as "$X ($Y drafting) of $Z", and an incomplete drafting fold reads "at least" on both
38
+ figures.
39
+ - **Plain-question check lines** — the "checks N/M" headline is withdrawn. Run-tab part
40
+ cards and the Audit tab show "all N checks passed" or the first failing check in plain
41
+ words, e.g. "check #2 src/checks.js has 0 type errors? — 4 left, need 0"; an expanded
42
+ attempt row lists every declared check in order, including ones that never ran
43
+ ("not run"). One server-side owner maps each declared stage to its question ("did it
44
+ change any file?", "no casts or silencers added?", "does the test suite pass?", "no
45
+ failing tests?", …); numbers use three closed shapes ("N left, need T" / "N of total" /
46
+ "N, limit M").
47
+ - Coloured `[✓]`/`[✗]` result glyphs on Run-tab part cards.
48
+ - The fix worker's prompt now carries a facts-only close-grade history: "checks passed
49
+ N/M -> N/M -> …" (over the DECLARED stage count) and each stage's own number history
50
+ ("count-stage 12 -> 0"). Nothing generated, no advice.
51
+
52
+ ### Changed
53
+
54
+ - **Panel money renders at 2 decimals everywhere** (`<$0.01` for a real sub-cent amount, a
55
+ floor rounds down). The CLI's 4-decimal `--all` output is unchanged.
56
+ - **The "worse than before" confirm-turn question is retired** from both the panel and the
57
+ CLI — an empty answer always meant "nothing beyond Guardrails"; it now is never asked.
58
+ - Chat tab cap row and labels follow the mockup (`Cap $`, `Token price $`, Time cap);
59
+ Audit tab Time cells show `HH:MM:SS` (full ISO on hover) instead of overflowing at
60
+ 390px.
61
+
62
+ ### Fixed
63
+
64
+ - **Live runs show a running floor, not "unknown"** — "$X so far (Y drafting) of $CAP ·
65
+ running Zm" on the Summary box and on a live run's own card; part-level tool counts on
66
+ a live run resolve through the same live audit-sidecar reader as the Audit tab.
67
+ - **Job tab was permanently blank** — two elements shared `id="job-card"`; the read-only
68
+ card is now `job-card-readonly`, with a regression test banning duplicate ids.
69
+ - **A failed or rejected chat POST no longer loses the typed answer** — the input clears only
70
+ on a proven success and errors persist in their own element instead of being wiped by
71
+ the 2-second poll.
72
+ - **Fix-loop governor reads a declared close's numbers (F198)** — it fed on text-parsed
73
+ grades that read `null` for every declared count-not-worse gap; it now reads the close's
74
+ structured `{stage, value}`, so its progress/strike counting sees real convergence or
75
+ stalling. No limit or direction change.
76
+ - **Escalated fix-loop exhaustion no longer advertises `--resume` (F197)** — `escalated` is
77
+ not resumable, so the option states that plainly instead of an unreachable "rerun with
78
+ --resume".
79
+ - The plan part's wall time no longer reads ~0.0s for a real multi-second call (replay-side
80
+ fix; display only).
81
+ - "Checks passed N/M" uses the declared stage count, not the count that ran under
82
+ first-red-wins — in the panel and in the fix worker's prompt (F203).
83
+
84
+ ### Docs
85
+
86
+ - `bareloop.context.md` documents the `/api/author/*` routes, `draftSpentUsd`,
87
+ `draftSpendComplete`, `--draft-spent-usd`, `--draft-spend-incomplete` and
88
+ `moneyWithDraft`; PANEL-BUILD.md and the PRD carry the dated P3 / one-cap addenda.
89
+
90
+ ## [0.30.0] — 2026-09-27
91
+
92
+ ### Added
93
+
94
+ - **Panel step map draws the retry loop** — a multi-attempt step (and the post-step fix loop)
95
+ now renders its "try N" curve and label on the map, alongside a pulsing amber dot on the
96
+ live-running box (same geometry as `design/panel-mockup.html`).
97
+ - **Runs list and open run poll live** — a single poll owner refreshes the Runs list every
98
+ tick and the open run's own detail while it is still live (`▶`), stopping once it dies
99
+ (`?`) or gets a real `✓`/`✗` verdict; polling pauses on a hidden tab and catches up on
100
+ return. Re-renders are skipped when the fetched payload is unchanged, and scroll position
101
+ is preserved across poll-driven rebuilds.
102
+ - `scripts/replay-live.mjs` (**dev-only, not shipped** — excluded from the published
103
+ package) paces an archived spine into a live-looking run for panel development and
104
+ screenshot proof, without touching the source spine or `~/.config/bareloop`.
105
+
106
+ ### Changed
107
+
108
+ - **Job tab field order**: Check type, Model, `$` cap / Time cap, then the rest — moved per
109
+ hamr's ruling.
110
+ - Job tab no longer shows a separate Job name field; the run's own card title already
111
+ carries it.
112
+
113
+ ### Fixed
114
+
115
+ - The retry loop's "try N" curve and label now stay inside their own step box at every
116
+ width, including narrow/mobile widths (previously could render past the box's left edge
117
+ at widths like 350px).
118
+ - An open (still-running) attempt on a live run now reads `▶` everywhere — map, part cards,
119
+ and Audit tab — never the died `?`, which is reserved for a genuinely died attempt.
120
+ - The `[▶]` glyph now pulses for a live run everywhere it appears (runs list rows,
121
+ Workflows, the run header).
122
+ - A running step's own in-progress attempt no longer leaks into a phantom fix-loop part on
123
+ the map.
124
+ - A live fix-loop attempt whose window is still open now reports its outcome as unknown
125
+ rather than a fabricated red, so the map correctly shows it as running (with its pulsing
126
+ dot) instead of prematurely stopped.
127
+ - A live run's Audit tab and tool-call/cache summary now fall back to the during-run
128
+ tree/worktree gate-audit path, so tool calls are visible while a run is still in progress,
129
+ not only after it finishes.
130
+
131
+ ### Notes
132
+
133
+ - F195 (open): `/api/runs` re-replays every archived spine on every call (~457ms CPU at 250
134
+ rows, no per-row cache). Mitigated in this release by throttling the Runs list poll to
135
+ 10s; the root cause is still open.
136
+ - F196 (parked): a finished bundle run's tool log can be unreadable due to a sidecar naming
137
+ mismatch between `spine.jsonl` and its renamed gate-audit sidecar. Not fixed in this
138
+ release.
139
+
8
140
  ## [0.29.0] — 2026-09-26
9
141
 
10
142
  ### Added
@@ -1160,7 +1160,37 @@ Reserved spine vocabulary (V7, machinery-free until job #1 surfaces one):
1160
1160
  `coordination-red` — a failure between units (scope contention, step order, store
1161
1161
  races), never to be folded into worker/interpreter reds.
1162
1162
 
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`
1163
+ ### `runJob(spec, { approvals, workdir, provider, nativeProvider?, providerFor?, emit, capRuns?, strikeLimit?, shellCapUsd?, closeTimeoutMs?, closeDir?, layerRoot?, readShim?, scout?, bridge?, draftSpentUsd?, draftSpendComplete?, priorSpentUsd?, priorSpendComplete?, priorWallMs?, resumeSeed?, resumeGrades?, resumeReplans?, resumeBranch?, humanRuling?, heldRuling?, reviewDoor?, doorRerun?, resumable? })` → outcome — `src/run.js`
1164
+
1165
+ **`draftSpentUsd` (hamr's ruling 2026-09-28, "one cap covers drafting + run") — money the
1166
+ AUTHORING pipeline already spent on this job before it was signed.** Shrinks THIS run's own
1167
+ enforced ceiling (the one place the arithmetic lives: `remainingUsd() => Math.min(shellCapUsd,
1168
+ job.budgetUsd - draftFoldUsd - spentUsd)`), never the signed `budgetUsd` itself. Deliberately
1169
+ NOT `priorSpentUsd` — that key means "a previous attempt of this run died and folded its spend
1170
+ forward," read by `src/replay.js`'s `resumed` flag off the key's bare presence; a drafting fold
1171
+ must never make an ordinary first run read as a resume. Belted like `priorSpentUsd` (a
1172
+ non-finite or negative value reads as 0). Rides onto `job-start` as its own field,
1173
+ `draftSpentUsd`, only when > 0 — never inside `spentUsd`/`engagementSpentUsd`, which stay
1174
+ run-only. `bareloop run-u --draft-spent-usd <n>` is the CLI door (validated: finite, ≥ 0, else
1175
+ refused); `bareloop author` prints its own known drafting spend floor and the exact `run-u`
1176
+ command including the flag; the panel's sign route passes the session's own tracked drafting
1177
+ spend the same way. `src/replay.js`'s `moneyWithDraft(spendCore, draftSpentUsd, budgetUsd?, draftSpendComplete?)` is
1178
+ the one shared display formatter: `"$3.71 ($0.81 drafting) of $5.00"`, unchanged
1179
+ (`spendCore`/`spendCore of $cap`) when a run carries no drafting share. When
1180
+ `draftSpendComplete === false` it reads `"at least $3.71 (at least $0.81 drafting) of $5.00"`
1181
+ (the core is not double-prefixed if it is already a floor).
1182
+
1183
+ **`draftSpendComplete` (default `true`; hamr's ruling 2026-09-28, 2nd addendum) — was the
1184
+ `draftSpentUsd` fold EXACT?** The authoring pipeline's metered-call list can go unpriced (F6)
1185
+ and the unknown does not heal by folding forward, so a drafting figure that is only a floor
1186
+ must say so. A NEW field beside `draftSpentUsd`, never a value inside `spendComplete` or
1187
+ `priorSpendComplete` (those answer "is THIS run's own ledger exact"; merging them would let a
1188
+ floor drafting spend read as an exact run). Read only when `draftSpentUsd` is emitted: rides
1189
+ onto `job-start` as `draftSpendComplete` beside `draftSpentUsd` (only when the fold is > 0);
1190
+ only an explicit `false` counts as incomplete — anything else, garbage included, reads `true`,
1191
+ so an unreadable flag never manufactures a false floor. `src/replay.js` surfaces it as
1192
+ `summary.draftSpendComplete` (`null` when no drafting fold; a job-start lacking the field reads
1193
+ `true`) and feeds it to `moneyWithDraft`.
1164
1194
 
1165
1195
  **`closeTimeoutMs` (PRD item 27/M3) is now OPTIONAL for every real caller.** Omit it (both
1166
1196
  `src/cli.js`'s bundle runner and `src/userrun.js` — lifted out of `scripts/run-u.mjs` by
@@ -1276,6 +1306,26 @@ knob, not a product default — the spec names no scout, so the signed hash is u
1276
1306
  `--read-shim`: an unrecognised value exits 2 at argv, and every re-invocation the runner prints
1277
1307
  carries `--scout off` when set, so a resume never silently drops the arm.
1278
1308
 
1309
+ **`bareloop run-u --draft-spend-incomplete`** is a boolean presence-flag (no value) that sets
1310
+ `runJob`'s `draftSpendComplete: false`, saying the `--draft-spent-usd` figure is a floor. It
1311
+ has no guard against being passed without `--draft-spent-usd` — alone it is inert (nothing is
1312
+ folded, so no `job-start` field is emitted). Every printed re-invocation tail carries it
1313
+ alongside `--draft-spent-usd` (only when drafting spend > 0), so a resumed leg never turns the
1314
+ floor back into an exact figure; `bareloop author` and the panel's sign route pass it when the
1315
+ session's drafting spend was incomplete.
1316
+
1317
+ **`bareloop run-u --draft-spent-usd <n>`** (hamr's ruling 2026-09-28, "one cap covers drafting +
1318
+ run") is the same runner-territory class: a non-finite or negative value exits 2 at argv, before
1319
+ the approval gate — never coerced, since reading garbage as 0 would silently WIDEN the run's own
1320
+ enforced ceiling and reading it as `Infinity` would silently narrow it into "no cap". The spec
1321
+ names no drafting spend, so the signed `budgetUsd` is unaffected; this only shrinks THIS leg's
1322
+ own enforced remainder (`job.budgetUsd - draftSpentUsd - spentUsd`). Every printed
1323
+ resume/decide/door re-invocation carries `--draft-spent-usd` too, mirroring `--read-shim`'s own
1324
+ tail — a resumed leg that dropped it would silently widen its ceiling back up. `bareloop author`
1325
+ prints the exact `run-u` command including this flag, with its own known drafting spend (a floor,
1326
+ never $0 on an unpriced call — F6). The panel's sign route (`src/panel/authorroutes.js`)
1327
+ passes the same flag off the authoring session's own tracked spend.
1328
+
1279
1329
  **The review door's two checkpoint terminals (N4 slice 1, doors re-cut 2026-08-18)** are the
1280
1330
  class's whole surface at this layer, and each is a CLEAN exit (`spendComplete` stays true —
1281
1331
  only the two casualties floor). The pause terminal is a
@@ -1554,7 +1604,10 @@ greens harmed — all three historical fix-loop greens converted in ≤ 2 verdic
1554
1604
  case caught, dead flat at 2 errors for 7 verdicts until the wall). It now stops on the same
1555
1605
  2-strike no-progress rule, read off a DIFFERENT signal: the close's own graded numbers, **per
1556
1606
  stage** (`src/trend.js`). A stage's series is the first number on the first red-marked line of
1557
- its output, compared only against that stage's own BEST so far (never last-only, the same
1607
+ its output (for a DECLARED close the structured `{stage, value}` its verdict already carries —
1608
+ `closeGrade`, F198 — never a re-parse of the gap prose, which reads every declared grade as
1609
+ uncomparable; a command close, and a worker-crash's synthetic gap, still read the text),
1610
+ compared only against that stage's own BEST so far (never last-only, the same
1558
1611
  oscillator reason the ladder keeps a seen-set); reaching a LATER stage than ever before is
1559
1612
  progress too, since a staged close is first-red-wins. Two consecutive comparable readings with
1560
1613
  nothing improving ends the loop, under the unchanged `cap-halt` terminal and the unchanged
@@ -1578,6 +1631,10 @@ vs `the target files are clean but M error(s) exist outside them`) donates both
1578
1631
  series, and a run crossing that seam can read converging on work that only swapped which wall
1579
1632
  it is behind. Neither is sharpened by teaching the reader to tell prose shapes apart (the F49
1580
1633
  precedent); the second's root fix is a stage split in the close, which is the spec's to change.
1634
+ The fix worker's prompt also gains one facts-only line ("The close's own numbers so far, oldest
1635
+ first": checks-satisfied counts and each stage's number history, F198) — no advice, omitted when
1636
+ nothing was graded. This exhaustion terminal is `escalated`, a graded red that `--resume`
1637
+ refuses, so its options say plainly it cannot be resumed (revise the spec and rerun fresh, F197).
1581
1638
  The spine gains a per-iteration `ladder` record here too; every reading names its `governor`
1582
1639
  (`step-ladder` | `close-trend`) so two instruments under one event type can never be averaged
1583
1640
  into one number.
@@ -2553,7 +2610,7 @@ reimplements for the tool-call breakdown. It returns one plain object:
2553
2610
 
2554
2611
  ```
2555
2612
  { runId, job, goal, budgetUsd, specHash, branch, verdictType, model, code,
2556
- outcome, stopReason, spentUsd, spendComplete, wallMs, chainClock,
2613
+ outcome, stopReason, spentUsd, spendComplete, draftSpentUsd, wallMs, chainClock,
2557
2614
  resumed, resumeSeed, thisFileSpend, spendMismatch,
2558
2615
  timelineKind: 'steps'|'iterations', replans, close,
2559
2616
  steps: [{ id, occurrence, outcome, rounds, toolCalls, checks: {passed, failed},
@@ -2583,6 +2640,13 @@ the model string, or `-`) — `model` is kept FULL, never truncated, unlike the
2583
2640
  column: a wrong or uncertain model reading is exactly the thing a directory-wide scan needs in
2584
2641
  full, not cut.
2585
2642
 
2643
+ **`draftSpentUsd`** (hamr's ruling 2026-09-28, "one cap covers drafting + run") reads
2644
+ `job-start.draftSpentUsd` — `null` on every run that never drafted through the panel/`bareloop
2645
+ author` (the common case, and every spine predating the field), the drafting spend FLOOR
2646
+ otherwise. Never `priorSpentUsd` (that key drives `resumed` below — see `runJob`'s own doc) and
2647
+ never folded into `spentUsd`. `summarizeForAllLine`'s `spend` column and every panel money
2648
+ display route this through `moneyWithDraft` (above).
2649
+
2586
2650
  **`code`** (F118, the run→code direction — the commit→run direction is the prompt-commit
2587
2651
  check's `Failure:` run-reference rule; the two are companion halves of one loop, built the
2588
2652
  same day). `codeVersion()` (`src/codeversion.js`) is a pure, $0, no-shell reader: `version`
@@ -3230,7 +3294,14 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3230
3294
  paged, for the Audit tab's Grouped view); `GET /api/runs/:runid/job` (the signed spec's
3231
3295
  own fields, ONLY when a bundle-layout run's `spec.json` is reachable — a run-u run has
3232
3296
  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
3297
+ `'unknown'`, never guessed from the spine's own narrower `job-start` record). The run
3298
+ detail also carries `draftSpentUsd`/`draftSpendComplete` (the drafting share of the one cap,
3299
+ `null` when none), `spendFloorUsd`/`wallFloorMs` for a died OR still-running spine (a
3300
+ floor, never `unknown`), and — when a spec resolves — per part attempt `declaredStagesTotal`
3301
+ and `declaredStages[{name, question}]` in DECLARED order, plus per stage `kind`/`direction`/
3302
+ `baselineKind`/`question`: the plain-English question is derived in `src/panel/server.js`
3303
+ (`stageQuestionText`) from the signed stage's kind/params only, `null` when no honest
3304
+ wording exists (the client then shows the stage's own name). Every
3234
3305
  endpoint is GET/HEAD only (anything else — including every write verb — is `405`); a URL
3235
3306
  never joins a path segment into a filesystem read — a runid is looked up in the run list
3236
3307
  first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
@@ -3243,6 +3314,69 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3243
3314
  its own. Chat/Settings/authoring (P3/P4) are not built yet; the page says so rather than
3244
3315
  hiding the gap.
3245
3316
 
3317
+ - **Panel P2 (live run view, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → the page
3318
+ polls, it does not push (no SSE/websocket). One `setInterval` (2s) drives two throttled
3319
+ fetches: the OPEN run's own detail (`GET /api/runs/:runid`) refetches every 2s while its
3320
+ glyph is live `▶`, and stops the instant it reaches a real `✓`/`✗` or dies `?`; the left
3321
+ Runs list refetches on a separate 10s throttle inside the same tick (`RUNS_LIST_POLL_MS`,
3322
+ lowered from the original 2s per F195 — `listRuns()` re-replays every archived spine on
3323
+ every call, ~457ms CPU at 250 rows, open/escalated), except the tick right after the open
3324
+ run stops being live, which forces one immediate list refresh so that row's glyph doesn't
3325
+ wait up to 10s. Both fetches skip re-rendering when the payload's JSON is byte-identical to
3326
+ the last one, restore each pane's scroll position across a poll-driven rebuild, and pause
3327
+ while the tab is hidden (`document.hidden`), catching up immediately on
3328
+ `visibilitychange`. The Audit tab does **not** join this loop — it refetches only when
3329
+ opened/switched to. A live run's gate-audit sidecar (before `run-u`/`bareloop run`'s
3330
+ end-of-run rename) is read from its DURING-RUN path (`<row.patient>/gate-audit.jsonl`) via
3331
+ `resolveAuditPathForRow` (`src/panel/server.js`), the one shared owner the Run tab's tools
3332
+ summary and the Audit tab both read through — tried only while the run's own spine has no
3333
+ `job-end` yet, so a finished run never risks reading a stale leftover file at that path.
3334
+ `scripts/replay-live.mjs <sourceSpine> <outDir> [--speed N]` is a dev-only, unshipped ($0,
3335
+ no provider call) instrument that paces a real archived spine's records back out under a
3336
+ new runid so the panel can be exercised against a "live" run without a paid run — it is not
3337
+ a substitute for watching a real run start-to-end, which stays a separately authorized step.
3338
+
3339
+ - **Panel P3 (chat/authoring, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → the ONE
3340
+ family of write routes the panel serves, all under `/api/author/*` (`src/panel/
3341
+ authorroutes.js`); every other route stays GET/HEAD-only per P1's own rule. Every route
3342
+ requires the human-click guard: an `x-bareloop-token` header matching a fresh token minted
3343
+ once per server start (templated into `index.html` like the port) AND an Origin/Host
3344
+ naming this exact `127.0.0.1:<port>` — a request failing either gets `403`, before the
3345
+ route body ever runs. `POST /api/author/start` (one job-card body; `409` while a prior
3346
+ session is still non-terminal — one authoring session at a time) creates a session
3347
+ (`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
3348
+ `authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
3349
+ `bareloop interview`/`bareloop author` already make — never a script's own readline loop,
3350
+ never a reimplementation of any gate. `GET /api/author/model-check?model=<id>` is the $0 readiness probe the job card runs
3351
+ before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
3352
+ completion; the page sees the key's NAME and a status word, never the value). `POST
3353
+ /api/author/:id/check-deps` is the install-gap's "Check again" (`phase:'install-needed'`:
3354
+ the session waits on the person's own install, then re-runs `missingDependencies` on the
3355
+ same copy — bareloop never installs). `GET /api/author/:id` polls the session's state
3356
+ (phase, chat messages, cost, `revisesLeft`, `specHash` once prepared). `POST /api/author/
3357
+ :id/send {text}` answers whatever the confirm turn is currently asking (the `language` pick or a plan's
3358
+ own follow-up question — the old `worseThanBefore` ask is retired, 2026-09-28) — refused outright when the pending ask is the
3359
+ plan MENU itself (`{ok:false}`, no route from chat text to a plan decision, ever). `POST
3360
+ /api/author/:id/revise {text}` is the menu's own `fix` pick, with the chat text as the
3361
+ correction (D3: max 2 rounds, `revisesLeft` derived from the confirm turn's own round
3362
+ number, never a second hardcoded cap). `POST /api/author/:id/sign-prepare` is the menu's
3363
+ own `confirm` pick — it runs gates 1–4 and reaches `phase:'prepared'` with a `specHash`; it
3364
+ NEVER signs. `POST /api/author/:id/sign {specHash}` is the ONLY route that spawns a run —
3365
+ it refuses unless `phase==='prepared'` and the posted hash matches the session's own
3366
+ `signing.json` `specHash` exactly, then spawns `setsid systemd-inhibit … node bin/
3367
+ bareloop.mjs run-u --spec <resolved-spec.json> --approve <hash>` (plus `--draft-spent-usd <n>`
3368
+ [`--draft-spend-incomplete`] when the session's drafting spend is > 0 — one cap covers
3369
+ drafting + run) detached (array argv,
3370
+ never a shell string), with its own log file inside the session's own dir, and the
3371
+ server's own environment (a key is never read into or sent to the page — a missing one
3372
+ refuses the session at $0, naming only the env var). `src/panel/authorsession.js` takes
3373
+ test-only DI seams (`scout`/`generate`/`confirmGenerate`/`authorFn`/`prepareSigningFn`) so
3374
+ a test can drive the real ask()-channel/revise/hash wiring without a live provider call;
3375
+ none of them are reachable from `authorroutes.js`'s real construction path. Sessions live
3376
+ under `~/.config/bareloop/panel-sessions/<id>/` (each one's own `resolved-spec.json` and
3377
+ `signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
3378
+ `~/.config/bareloop/.env` keys-file loader, and Settings are P4, not built here.
3379
+
3246
3380
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3247
3381
  (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
3248
3382
  U-mode). `src/cli.js`'s `run-u` dispatch hands `rest` straight to
@@ -3305,6 +3439,15 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3305
3439
  returns a code / throws" rule `src/userrun.js` already keeps. Like `interview`, `src/cli.js`
3306
3440
  hands it raw `stdin`/`stdout`/`stderr` (its one interactive seam is the confirm turn).
3307
3441
 
3442
+ **`--budget` and `--draft-spent-usd` (hamr's ruling 2026-09-28, "one cap covers drafting +
3443
+ run"):** `--budget` still means the AUTHORING ceiling, unchanged — the panel sets it equal to
3444
+ the job card's one `Cap $`. On a SIGNING PREPARED stop, `author` prints its own known
3445
+ drafting spend (a floor, never $0 on an unpriced call — F6) and the exact `run-u` command to
3446
+ run it, now including `--draft-spent-usd <known-floor>` when that floor is > 0 — the SAME
3447
+ signed `budgetUsd` is the run's cap too, and `run-u` enforces the remainder
3448
+ (`Cap $ − drafting spent`) as ITS own ceiling. Nothing here is left for a person to
3449
+ hand-compute or re-type, the same rule F185 set for `--spec`.
3450
+
3308
3451
  **Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
3309
3452
  bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
3310
3453
  anything higher. Raising a ceiling is a re-export (a new signed bundle), never a runner flag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.29.0",
3
+ "version": "0.31.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",
package/src/authorflow.js CHANGED
@@ -425,32 +425,18 @@ export function labelsFor(verdictType) { return questionSet(verdictType).labels;
425
425
 
426
426
  // ── CONFIRM TURN — person-facing wording (PRD item 33 M3 piece 4) ──────────
427
427
  //
428
- // These three are the $0, no-provider half of the confirm turn (D7): asked
429
- // before the scout ever runs, so a missing person at end of input stops at
430
- // $0 rather than after a paid call. FROZEN wording, exactly like SOURCE_FIELD
428
+ // These are the $0, no-provider half of the confirm turn (D7): asked before
429
+ // the scout ever runs, so a missing person at end of input stops at $0
430
+ // rather than after a paid call. FROZEN wording, exactly like SOURCE_FIELD
431
431
  // and DESTINATION_FIELD_* above — a caller prints these verbatim, never
432
432
  // rewords or reorders them.
433
-
434
- /**
435
- * Asked ONLY for a repo source (ruling 2 addendum's D7) — a plain folder or a
436
- * URL has no "before" a repo's own history gives it to compare against. This
437
- * is the OLD Q5 wording, verbatim ("What would make you say this came back
438
- * worse than before?"), which M3 piece 3's reshape folded into Guardrails —
439
- * it survives here as its OWN field because the confirm turn (ruling 5's
440
- * 2026-09-13 addendum) includes it as a constraint only when the person
441
- * actually gave one, and folding it silently into the Guardrails free-text
442
- * slot would lose that "present or absent" distinction. An empty answer is
443
- * "nothing beyond what Guardrails already said" and is not required — see
444
- * the frozen-wording test below: this phrase appears NOWHERE in
445
- * {@link questionsFor}'s own sets, so a caller cannot double-ask it by
446
- * reading the wrong table.
447
- */
448
- export const WORSE_THAN_BEFORE_FIELD = Object.freeze({
449
- id: 'worseThanBefore',
450
- kind: 'mechanical',
451
- label: 'Worse than before',
452
- prompt: 'What would make you say this came back worse than before?',
453
- });
433
+ //
434
+ // RETIRED (hamr's ruling, 2026-09-28): the old Q5 "worse than before" field
435
+ // and ask are gone — it was optional, and an EMPTY answer already meant
436
+ // "nothing beyond Guardrails", so it never earned its own turn. `worseThanBefore`
437
+ // downstream (confirmPrompt, `accepted.worseThanBefore`) is kept and always
438
+ // resolves to `''` — the same value an empty answer always produced — so
439
+ // nothing downstream needed to change shape, only the ask itself is gone.
454
440
 
455
441
  /**
456
442
  * Asked ONLY when `src/detectlang.js`'s `detectLanguage` reports its
@@ -1832,7 +1818,10 @@ async function askConfirmPlan({ convo, generate, mode, book, label }) {
1832
1818
  * generate: Function, book: ReturnType<typeof makeCostBook>,
1833
1819
  * ask: (step: {kind: string, [k: string]: any}) => Promise<string|null>,
1834
1820
  * onPhase?: (phase: string, data?: any) => void, mode?: 'tool'|'text',
1835
- * worseThanBefore?: string}} o
1821
+ * worseThanBefore?: string}} o retired field (hamr's ruling 2026-09-28):
1822
+ * `worseThanBefore` is never asked any more and defaults to `''` — a
1823
+ * caller may still pass a string through (kept for a future, non-ask
1824
+ * source of the same constraint), it just never comes from an ask() step.
1836
1825
  * @returns {Promise<{ok: boolean,
1837
1826
  * stop: null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart',
1838
1827
  * rounds: number,
@@ -1843,13 +1832,12 @@ async function askConfirmPlan({ convo, generate, mode, book, label }) {
1843
1832
  export async function runConfirmTurn({
1844
1833
  verdictType, answers, questions, labels = {}, facts = null, listing = null, writeScope = null,
1845
1834
  isRepo, lang, generate, book, ask, onPhase = () => {}, mode = 'tool',
1846
- // `undefined` (the default) means "ask me" — every S2 caller (nothing
1847
- // pre-resolves this). A caller that already asked worseThanBefore itself
1848
- // (`authorCloseForJob`'s own $0 phase, run BEFORE the scout per D7) passes
1849
- // the resolved string straight through, and this turn asks nothing a
1850
- // second time. `''` is a legal resolved answer ("nothing beyond
1851
- // Guardrails") and is NOT the same as "ask me" — only `undefined` is.
1852
- worseThanBefore: presetWorseThanBefore = undefined,
1835
+ // RETIRED ASK (hamr's ruling 2026-09-28): this turn never asks
1836
+ // "worseThanBefore" any more — an absent value always resolves to `''`,
1837
+ // byte-identical to what an empty answer always produced. A caller may
1838
+ // still pass a resolved string through (kept as a plain pass-through, not
1839
+ // an ask trigger).
1840
+ worseThanBefore: presetWorseThanBefore = '',
1853
1841
  }) {
1854
1842
  /** @typedef {null|'cap-halt'|'pricing-red'|'provider-red'|'artifact-red'|'confirm-abandoned'|'confirm-restart'} ConfirmStop */
1855
1843
  /** @returns {{ok: boolean, stop: ConfirmStop, rounds: number, accepted: null, reds: Red[], cost: any}} */
@@ -1858,16 +1846,8 @@ export async function runConfirmTurn({
1858
1846
  const abandon = (rounds) => ({ ...base(), stop: /** @type {ConfirmStop} */ ('confirm-abandoned'), rounds, cost: book.report() });
1859
1847
 
1860
1848
  // ── the $0 half, entirely before any token spends (D7) ────────────────────
1861
- /** @type {string} */
1862
- let worseThanBefore = '';
1863
- if (presetWorseThanBefore !== undefined) {
1864
- worseThanBefore = presetWorseThanBefore;
1865
- } else if (isRepo) {
1866
- onPhase('confirm-worse-than-before');
1867
- const wtb = await ask({ kind: 'worseThanBefore', field: WORSE_THAN_BEFORE_FIELD });
1868
- if (wtb === null) return abandon(0);
1869
- worseThanBefore = redactSecrets(String(wtb).trim());
1870
- }
1849
+ // no ask here any more — see the retirement note above.
1850
+ const worseThanBefore = typeof presetWorseThanBefore === 'string' ? presetWorseThanBefore : '';
1871
1851
 
1872
1852
  /** @type {string} */
1873
1853
  let resolvedLang = typeof lang === 'string' ? lang : '';
package/src/authorjob.js CHANGED
@@ -111,7 +111,7 @@ import {
111
111
  } from './authoring.js';
112
112
  import {
113
113
  QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
114
- runConfirmTurn, WORSE_THAN_BEFORE_FIELD, LANGUAGE_PICK_FIELD,
114
+ runConfirmTurn, LANGUAGE_PICK_FIELD,
115
115
  } from './authorflow.js';
116
116
  import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
117
117
  import {
@@ -498,14 +498,11 @@ export async function authorCloseForJob({
498
498
  // paid survey. `ask` ABSENT is the whole confirm turn's off switch — every
499
499
  // caller that predates this piece (or a caller that never wires an
500
500
  // interactive seam) reaches the genre check below exactly as it always did.
501
- let worseThanBefore = '';
501
+ // RETIRED (hamr's ruling 2026-09-28): the "worse than before" ask is gone —
502
+ // it was optional and an empty answer already meant "nothing beyond
503
+ // Guardrails", so `worseThanBefore` always resolves to `''` now, never asked.
504
+ const worseThanBefore = '';
502
505
  if (ask) {
503
- if (isRepo) {
504
- onPhase('confirm-worse-than-before', {});
505
- const wtb = await ask({ kind: 'worseThanBefore', field: WORSE_THAN_BEFORE_FIELD });
506
- if (wtb === null) return { ...base, interview, reds: [], stop: 'confirm-abandoned' };
507
- worseThanBefore = redactSecrets(String(wtb).trim());
508
- }
509
506
  if (langResult?.kind === 'ambiguous') {
510
507
  onPhase('confirm-language-pick', { candidates: langResult.candidates });
511
508
  const pick = await ask({ kind: 'language', field: LANGUAGE_PICK_FIELD, candidates: langResult.candidates });
package/src/authorrun.js CHANGED
@@ -348,8 +348,9 @@ export async function main(argv, deps = {}) {
348
348
  // Only meaningful for a REPO source (PRD item 33 M3 piece 4, step S6): a
349
349
  // plain folder has no genre to detect and no code-shaped manifest to walk —
350
350
  // `LANG`/`langResult` stay at their "nothing to see" values and the confirm
351
- // turn further down runs with `isRepo: false` (D5), which is what already
352
- // skips the repo-only "worse than before" ask and the language pick alike.
351
+ // turn further down runs with `isRepo: false` (D5), which is what skips
352
+ // the language pick (the old repo-only "worse than before" ask is retired
353
+ // entirely as of 2026-09-28 — see src/authorflow.js's own retirement note).
353
354
  let langResult = /** @type {ReturnType<typeof detectLanguage>|null} */ (null);
354
355
  let LANG = 'none-detected';
355
356
  if (IS_REPO_SOURCE) {
@@ -702,9 +703,10 @@ export async function main(argv, deps = {}) {
702
703
  return String(value);
703
704
  };
704
705
  /** one free-text answer, possibly several lines; a blank line ends it.
705
- * `allowBlank` lets the FIRST line be blank — `worseThanBefore`'s "nothing
706
- * beyond Guardrails" is a legal, non-required answer, unlike every other
707
- * free-text step below.
706
+ * `allowBlank` lets the FIRST line be blank — kept as a general option for
707
+ * a future optional free-text step; every current step below passes
708
+ * `false` (the old "worse than before" ask, retired 2026-09-28, was the
709
+ * only caller that ever passed `true`).
708
710
  * @param {boolean} allowBlank @returns {Promise<string|null>} */
709
711
  const readFreeText = async (allowBlank) => {
710
712
  /** @type {string[]} */
@@ -729,11 +731,6 @@ export async function main(argv, deps = {}) {
729
731
  * @param {{kind: string, [k: string]: any}} step @returns {Promise<string|null>} */
730
732
  const ask = async (step) => {
731
733
  out('');
732
- if (step.kind === 'worseThanBefore') {
733
- out(step.field.prompt);
734
- out(' (press Enter on a blank line for "nothing beyond Guardrails")');
735
- return readFreeText(true);
736
- }
737
734
  if (step.kind === 'language') {
738
735
  out(step.field.prompt);
739
736
  for (const c of step.candidates ?? []) out(` · ${c}`);
@@ -1201,7 +1198,15 @@ export async function main(argv, deps = {}) {
1201
1198
  // buys a real judge call per case, so a line saying "the gates spend no
1202
1199
  // tokens" would have been false the moment a judged close reached here.
1203
1200
  out(`authoring ${costLine(authored.cost)}`);
1204
- out(`total cost ${costLine(costSoFar())}${judges ? ' (includes the calibration gate\'s judge calls)' : ' (gates 1-3 spend no tokens — they run commands)'}`);
1201
+ const totalCost = costSoFar();
1202
+ out(`total cost ${costLine(totalCost)}${judges ? ' (includes the calibration gate\'s judge calls)' : ' (gates 1-3 spend no tokens — they run commands)'}`);
1203
+ // hamr's ruling 2026-09-28 ("one cap covers drafting + run") — this
1204
+ // run's own known FLOOR (never $0 on an unpriced call, F6), handed to
1205
+ // the run-u command below as `--draft-spent-usd` so the SIGNED run's
1206
+ // own enforced ceiling becomes Cap $ minus what THIS pipeline already
1207
+ // spent, computed in the one place (`src/run.js`'s `remainingUsd`)
1208
+ // that arithmetic lives — never re-derived here.
1209
+ const draftSpentUsd = typeof totalCost.costUsd === 'number' ? totalCost.costUsd : totalCost.knownUsd;
1205
1210
 
1206
1211
  if (!signing.ok) {
1207
1212
  out('\nSIGNING NOT PREPARED — the close did not clear D9\'s gates. Nothing was signed and nothing was run.');
@@ -1222,8 +1227,19 @@ export async function main(argv, deps = {}) {
1222
1227
  // person needs to run their own job, nothing left to hand off.
1223
1228
  // `providerEntry.envKey` (never a hardcoded ANTHROPIC_API_KEY) is the
1224
1229
  // same F187 rule scripts/run-u.mjs's own hint already follows.
1230
+ // hamr's ruling 2026-09-28 (2nd addendum) — `totalCost.costUsd === null`
1231
+ // means this pipeline's own metered list carried at least one unpriced
1232
+ // call (`tallyCalls`'s `spendComplete: false`), so the drafting fold is
1233
+ // a FLOOR, and the run-u command below must say so via its own
1234
+ // presence-flag rather than let the next leg's job-start read it as
1235
+ // exact.
1236
+ const draftIncomplete = totalCost.costUsd === null;
1237
+ if (draftSpentUsd > 0) {
1238
+ out(`\n drafting spent ${draftIncomplete ? `at least $${draftSpentUsd.toFixed(6)}` : `$${draftSpentUsd.toFixed(6)}`} — the run's own cap is the SAME signed $${spec.budgetUsd}, `
1239
+ + 'and run-u will enforce the remainder (Cap $ minus this) as ITS ceiling — pass --draft-spent-usd exactly as shown below, never a rounded or re-typed figure.');
1240
+ }
1225
1241
  out('\nTo run it (the same signature and gates as any other job — nothing here bypasses them):');
1226
- out(` ${providerEntry.envKey}=... node scripts/run-u.mjs --spec ${specFile} --approve ${hash}`);
1242
+ out(` ${providerEntry.envKey}=... node scripts/run-u.mjs --spec ${specFile} --approve ${hash}${draftSpentUsd > 0 ? ` --draft-spent-usd ${draftSpentUsd}${draftIncomplete ? ' --draft-spend-incomplete' : ''}` : ''}`);
1227
1243
  emit('author-end', { outcome: 'prepared', specHash: hash });
1228
1244
  }
1229
1245
  }