bareloop 0.30.0 → 0.32.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/bareloop.context.md +205 -10
  3. package/bin/bareloop.mjs +0 -1
  4. package/package.json +1 -1
  5. package/src/authorflow.js +22 -42
  6. package/src/authorjob.js +5 -8
  7. package/src/authorrun.js +42 -16
  8. package/src/cli.js +12 -6
  9. package/src/config.js +135 -0
  10. package/src/interviewrun.js +10 -2
  11. package/src/keysfile.js +156 -0
  12. package/src/ledger.js +26 -0
  13. package/src/monthly.js +352 -0
  14. package/src/panel/authorroutes.js +328 -0
  15. package/src/panel/authorsession.js +555 -0
  16. package/src/panel/index.html +1247 -50
  17. package/src/panel/server.js +449 -51
  18. package/src/panel/settingsroutes.js +230 -0
  19. package/src/planrun.js +61 -5
  20. package/src/promptregisters.js +3 -0
  21. package/src/providerrows.js +217 -0
  22. package/src/providers.js +84 -0
  23. package/src/replay.js +80 -2
  24. package/src/replayio.js +14 -3
  25. package/src/run.js +48 -2
  26. package/src/runlist.js +13 -0
  27. package/src/userrun.js +134 -13
  28. package/types/authorflow.d.ts +4 -21
  29. package/types/authorrun.d.ts +5 -0
  30. package/types/cli.d.ts +2 -1
  31. package/types/config.d.ts +48 -0
  32. package/types/interviewrun.d.ts +5 -0
  33. package/types/keysfile.d.ts +90 -0
  34. package/types/ledger.d.ts +14 -0
  35. package/types/monthly.d.ts +192 -0
  36. package/types/panel/authorroutes.d.ts +67 -0
  37. package/types/panel/authorsession.d.ts +53 -0
  38. package/types/panel/server.d.ts +42 -5
  39. package/types/panel/settingsroutes.d.ts +14 -0
  40. package/types/providerrows.d.ts +164 -0
  41. package/types/providers.d.ts +34 -0
  42. package/types/replay.d.ts +31 -0
  43. package/types/replayio.d.ts +12 -1
  44. package/types/run.d.ts +26 -1
  45. package/types/runlist.d.ts +1 -0
  46. package/types/userrun.d.ts +22 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,160 @@ 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.32.0] — 2026-09-29
9
+
10
+ ### Added
11
+
12
+ - **Panel Settings (P4a)** — `~/.config/bareloop/.env` keys-file loader (shell env wins; keys
13
+ never reach the page), `config.json` (monthly $ limit, chosen key variables, Anthropic balance
14
+ note), a monthly $ limit that refuses a run start with `Max $X (monthly limit)` at both
15
+ `run-u` and the panel Sign & run, and a Settings view with a Money & limits tab and a Providers
16
+ tab (read, $0 Test, key-name dropdown). Add/edit/remove providers is P4b. There is no monthly
17
+ time limit. **The monthly limit is checked by `run-u` and the panel only — `bareloop run
18
+ <bundle>` does not check it.** An unreadable `config.json` refuses a run start; a month whose
19
+ spend is incomplete reads "at least".
20
+ - **Money & limits tab** — total and this-month spend and tokens as two header tiles ($ and
21
+ tokens, month first), a monthly $ limit that saves on change, and a per-provider breakdown
22
+ table ($, wall minutes, tokens; this month and to date). Wall time is job-start to job-end
23
+ when the run ended, otherwise an "at least" floor; unknown is shown as unknown, never 0. A
24
+ spine with no recorded provider is labelled "not recorded (model X)", never guessed from the
25
+ model name.
26
+ - **`replay-live --live-audit`** (`scripts/replay-live.mjs`) — opt-in: a $0 replay keeps its
27
+ sidecar in a scratch patient dir while it paces, so the panel's live tool-log fallback can be
28
+ seen on screen. Default replay is unchanged.
29
+ - **Panel Providers tab is the keys file (P4b)** — one Settings row per key in
30
+ `~/.config/bareloop/.env` that has a value (the file is created with five empty preset lines,
31
+ `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` / `LOCAL_API_KEY`,
32
+ when missing; an existing file is never edited). Each row has an editable Name (the model id Chat
33
+ uses), API shape (Anthropic / OpenAI-compatible / Gemini), Base URL, a $0 Test at that shape and
34
+ URL, and a total Tokens used; changes save to `config.json` `keys.<ENV NAME>`. A key value of
35
+ `null` counts as set and means "no real key" (local servers); the provider gets a placeholder,
36
+ and local rounds price as an `estimated` guess, never a real $0. Reload keys re-merges the
37
+ keys file onto the shell environment per request, so edits and removals take effect without a
38
+ restart. Chat's Model menu is the Settings
39
+ rows, and one owner (`src/providerrows.js`) resolves model to key + shape + URL for Chat, the
40
+ author doors and the run doors.
41
+ - **Providers tab names a shell-only key** — a preset key variable exported only in the shell
42
+ (not in the keys file) gets no row and no Chat model; the tab now says so under the table, by
43
+ name only (`GET /api/settings/providers` returns `shellOnly: string[]`, never a value).
44
+
45
+ ### Changed
46
+
47
+ - **Panel: no price is displayed** — the Providers table Price column and the Chat job card's
48
+ Token price field are removed. Display only: cost recording, the `estimated` provenance, spend
49
+ sums, caps and the monthly limit are unchanged.
50
+ - The Chat cap row is two equal full-width columns (Cap $ | Time cap) at every width.
51
+ - `config.json` `providers.<row>.key` (P4a key-name dropdown) is replaced by
52
+ `keys.<ENV NAME> = { name, shape, baseUrl }`; the hardcoded `PROVIDER_ROWS` and the panel's fixed
53
+ `MODEL_OPTIONS` are gone.
54
+ - **Monthly limit counts running jobs at their full cap** — a run still in flight (no `job-end`,
55
+ spine written to within the panel's died window) is counted against the limit at its own leg
56
+ cap, not its spend so far, so two runs cannot both start against a limit only one fits. A died
57
+ run still counts its floor. The Money tab's "$ month" stays real spend.
58
+
59
+ ### Fixed
60
+
61
+ - **Monthly limit no longer double-counts a resume** — `run-u --resume` on a leg killed within
62
+ `DIED_MTIME_MS` reserved the old leg at its full cap AND asked for the remainder, so a resume
63
+ that fit was refused; the resumed leg now counts its real spend only
64
+ (`checkMonthlyRoom({ resumingSpine })`).
65
+ - **`POST /api/settings/money` no longer clears the limit on a bad body** — an empty or
66
+ unparseable body, or JSON without the `monthlyLimitUsd` key, is a 400 and leaves `config.json`
67
+ alone; only an explicit `null` or blank clears the limit.
68
+ - **A bad `monthlyLimitUsd` no longer reads as "no limit"** — a `monthlyLimitUsd` present in
69
+ `config.json` but not a number above 0 (`"50"`, `-5`, `0`, `true`, `{}`) now refuses the run
70
+ start and shows as a config problem on the Money tab, like an unparseable file. Absent or
71
+ `null` still means no limit.
72
+ - **Panel author routes: `jobsDir` and body size** — the name-collision check's `jobsDir` test
73
+ seam is now server-side only (it was readable off the client's job card, so any
74
+ `/api/author/start` body could repoint it), and POST bodies over 1 MiB get 413 and the request
75
+ is destroyed (the reader had no cap and ran before the click guard).
76
+ - **Providers Test** — a connection failure now reads "nothing answering at <URL>"; the Test
77
+ cell keeps a fixed width and wraps its result. **Tokens used** counts older model-only spines
78
+ toward the row whose Name exactly equals the model.
79
+
80
+ ## [0.31.0] — 2026-09-29
81
+
82
+ ### Added
83
+
84
+ - **Panel Chat authoring (P3)** — the Chat tab now drafts a job from the page: a job card
85
+ (source, goal, Model, `Cap $` / Time cap), a chat thread, and Send / Revise / **Sign &
86
+ run** actions, backed by new `POST /api/author/*` routes. Every POST needs the
87
+ per-server-start token plus an Origin/Host check (the human-click guard); the sign route
88
+ additionally requires the exact spec hash from the session's own signing file and is the
89
+ only path that ever spawns `run-u` (argv array, ends in `--approve <hash>`). A single
90
+ progress indicator (amber cycling dots, green `[done]`, red `[stopped]` with the reason)
91
+ replaces per-phase chat bubbles; step lines carry no who-label; a pending ask always wins
92
+ the label ("waiting for your OK" / "install needed" / "waiting for your answer"). The
93
+ page opens on the Chat tab.
94
+ - **Install-needed pause + Check again** — a repo copy with missing dependencies no longer
95
+ refuses outright: the session pauses, names the copy and the exact install command, and a
96
+ **Check again** button re-runs the same check on the same copy (bareloop still never
97
+ installs anything).
98
+ - **Model check under Model** — one `[✓]`/`[✗]` line: key found/missing/bad-shape/rejected
99
+ (401/403 reads "key rejected", not "may be flaky") plus a $0 GET-only reachability probe
100
+ (`GET /api/author/model-check`, `checkProviderReachable` in `src/providers.js`). A missing
101
+ or bad key blocks Start; an unreachable endpoint shows red but never blocks.
102
+ - **One cap covers drafting + run** — the separate Drafting cap field is gone. Drafting runs
103
+ under the same signed Cap `$`, and at sign the run's enforced ceiling is Cap minus what
104
+ drafting already spent. `runJob` takes `draftSpentUsd` and `draftSpendComplete` (carried on
105
+ `job-start` as their own fields, never `priorSpentUsd`, so a first run never reads as a
106
+ resume); `run-u` gains `--draft-spent-usd <n>` and `--draft-spend-incomplete` (validated
107
+ like `--read-shim`/`--scout`, carried in every resume/decide re-invocation tail);
108
+ `bareloop author` prints its drafting spend and the exact `run-u` command. Spend renders
109
+ as "$X ($Y drafting) of $Z", and an incomplete drafting fold reads "at least" on both
110
+ figures.
111
+ - **Plain-question check lines** — the "checks N/M" headline is withdrawn. Run-tab part
112
+ cards and the Audit tab show "all N checks passed" or the first failing check in plain
113
+ words, e.g. "check #2 src/checks.js has 0 type errors? — 4 left, need 0"; an expanded
114
+ attempt row lists every declared check in order, including ones that never ran
115
+ ("not run"). One server-side owner maps each declared stage to its question ("did it
116
+ change any file?", "no casts or silencers added?", "does the test suite pass?", "no
117
+ failing tests?", …); numbers use three closed shapes ("N left, need T" / "N of total" /
118
+ "N, limit M").
119
+ - Coloured `[✓]`/`[✗]` result glyphs on Run-tab part cards.
120
+ - The fix worker's prompt now carries a facts-only close-grade history: "checks passed
121
+ N/M -> N/M -> …" (over the DECLARED stage count) and each stage's own number history
122
+ ("count-stage 12 -> 0"). Nothing generated, no advice.
123
+
124
+ ### Changed
125
+
126
+ - **Panel money renders at 2 decimals everywhere** (`<$0.01` for a real sub-cent amount, a
127
+ floor rounds down). The CLI's 4-decimal `--all` output is unchanged.
128
+ - **The "worse than before" confirm-turn question is retired** from both the panel and the
129
+ CLI — an empty answer always meant "nothing beyond Guardrails"; it now is never asked.
130
+ - Chat tab cap row and labels follow the mockup (`Cap $`, `Token price $`, Time cap);
131
+ Audit tab Time cells show `HH:MM:SS` (full ISO on hover) instead of overflowing at
132
+ 390px.
133
+
134
+ ### Fixed
135
+
136
+ - **Live runs show a running floor, not "unknown"** — "$X so far (Y drafting) of $CAP ·
137
+ running Zm" on the Summary box and on a live run's own card; part-level tool counts on
138
+ a live run resolve through the same live audit-sidecar reader as the Audit tab.
139
+ - **Job tab was permanently blank** — two elements shared `id="job-card"`; the read-only
140
+ card is now `job-card-readonly`, with a regression test banning duplicate ids.
141
+ - **A failed or rejected chat POST no longer loses the typed answer** — the input clears only
142
+ on a proven success and errors persist in their own element instead of being wiped by
143
+ the 2-second poll.
144
+ - **Fix-loop governor reads a declared close's numbers (F198)** — it fed on text-parsed
145
+ grades that read `null` for every declared count-not-worse gap; it now reads the close's
146
+ structured `{stage, value}`, so its progress/strike counting sees real convergence or
147
+ stalling. No limit or direction change.
148
+ - **Escalated fix-loop exhaustion no longer advertises `--resume` (F197)** — `escalated` is
149
+ not resumable, so the option states that plainly instead of an unreachable "rerun with
150
+ --resume".
151
+ - The plan part's wall time no longer reads ~0.0s for a real multi-second call (replay-side
152
+ fix; display only).
153
+ - "Checks passed N/M" uses the declared stage count, not the count that ran under
154
+ first-red-wins — in the panel and in the fix worker's prompt (F203).
155
+
156
+ ### Docs
157
+
158
+ - `bareloop.context.md` documents the `/api/author/*` routes, `draftSpentUsd`,
159
+ `draftSpendComplete`, `--draft-spent-usd`, `--draft-spend-incomplete` and
160
+ `moneyWithDraft`; PANEL-BUILD.md and the PRD carry the dated P3 / one-cap addenda.
161
+
8
162
  ## [0.30.0] — 2026-09-27
9
163
 
10
164
  ### 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,18 +3294,25 @@ 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
3234
- endpoint is GET/HEAD only (anything else — including every write verb — is `405`); a URL
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
3305
+ P1 endpoint is GET/HEAD only (anything else — including every write verb — is `405`, outside the
3306
+ two human-click-guarded write families `/api/author/*` and `/api/settings/*` below); a URL
3235
3307
  never joins a path segment into a filesystem read — a runid is looked up in the run list
3236
3308
  first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
3237
3309
  suffix too, and every endpoint echoes back the LISTED runid, never a filename-derived one
3238
3310
  that can silently drop that suffix), and only the path THAT ROW stores is ever read. A
3239
3311
  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
3312
+ different one. This read path runs no interview, no author and no run, and reads no key
3313
+ value (the write families below do, server-side, and never return one). One more caller of `src/replayio.js`/`src/runlist.js`, in-process, per the layering
3242
3314
  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.
3315
+ its own. Chat/authoring (P3) and Settings (P4a/P4b) are built — see below.
3245
3316
 
3246
3317
  - **Panel P2 (live run view, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → the page
3247
3318
  polls, it does not push (no SSE/websocket). One `setInterval` (2s) drives two throttled
@@ -3260,10 +3331,56 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3260
3331
  `resolveAuditPathForRow` (`src/panel/server.js`), the one shared owner the Run tab's tools
3261
3332
  summary and the Audit tab both read through — tried only while the run's own spine has no
3262
3333
  `job-end` yet, so a finished run never risks reading a stale leftover file at that path.
3263
- `scripts/replay-live.mjs <sourceSpine> <outDir> [--speed N]` is a dev-only, unshipped ($0,
3334
+ `scripts/replay-live.mjs <sourceSpine> <outDir> [--speed N] [--live-audit]` is a dev-only, unshipped ($0,
3264
3335
  no provider call) instrument that paces a real archived spine's records back out under a
3265
3336
  new runid so the panel can be exercised against a "live" run without a paid run — it is not
3266
3337
  a substitute for watching a real run start-to-end, which stays a separately authorized step.
3338
+ `--live-audit` (opt-in) puts the sidecar at a scratch patient dir during the replay so the
3339
+ live-fallback path above is exercised, then moves it to the sibling name at the end.
3340
+
3341
+ - **Panel P3 (chat/authoring, `docs/product/PANEL-BUILD.md` Addendum 2026-09-27)** → one of the
3342
+ panel's TWO families of write routes (the other is `/api/settings/*`, see "Settings" below), this
3343
+ one under `/api/author/*` (`src/panel/authorroutes.js`); every other route stays GET/HEAD-only
3344
+ per P1's own rule. A POST body over 1 MiB is refused `413` before it is parsed, and the job
3345
+ card's `jobsDir` (where an existing job name is looked up) is a server-side seam only, never read
3346
+ off the client's card. Every route
3347
+ requires the human-click guard: an `x-bareloop-token` header matching a fresh token minted
3348
+ once per server start (templated into `index.html` like the port) AND an Origin/Host
3349
+ naming this exact `127.0.0.1:<port>` — a request failing either gets `403`, before the
3350
+ route body ever runs. `POST /api/author/start` (one job-card body; `409` while a prior
3351
+ session is still non-terminal — one authoring session at a time) creates a session
3352
+ (`src/panel/authorsession.js`) that runs `prepareSource`/`detectLanguage`/`validateJob`/
3353
+ `authorCloseForJob`/`assembleSpec`/`prepareSigning` in-process, exactly the library calls
3354
+ `bareloop interview`/`bareloop author` already make — never a script's own readline loop,
3355
+ never a reimplementation of any gate. `GET /api/author/model-check?model=<id>` is the $0 readiness probe the job card runs
3356
+ before Start (`checkProviderReachable`, `src/providers.js` — a models-list GET only, never a
3357
+ completion; the page sees the key's NAME and a status word, never the value). `POST
3358
+ /api/author/:id/check-deps` is the install-gap's "Check again" (`phase:'install-needed'`:
3359
+ the session waits on the person's own install, then re-runs `missingDependencies` on the
3360
+ same copy — bareloop never installs). `GET /api/author/:id` polls the session's state
3361
+ (phase, chat messages, cost, `revisesLeft`, `specHash` once prepared). `POST /api/author/
3362
+ :id/send {text}` answers whatever the confirm turn is currently asking (the `language` pick or a plan's
3363
+ own follow-up question — the old `worseThanBefore` ask is retired, 2026-09-28) — refused outright when the pending ask is the
3364
+ plan MENU itself (`{ok:false}`, no route from chat text to a plan decision, ever). `POST
3365
+ /api/author/:id/revise {text}` is the menu's own `fix` pick, with the chat text as the
3366
+ correction (D3: max 2 rounds, `revisesLeft` derived from the confirm turn's own round
3367
+ number, never a second hardcoded cap). `POST /api/author/:id/sign-prepare` is the menu's
3368
+ own `confirm` pick — it runs gates 1–4 and reaches `phase:'prepared'` with a `specHash`; it
3369
+ NEVER signs. `POST /api/author/:id/sign {specHash}` is the ONLY route that spawns a run —
3370
+ it refuses unless `phase==='prepared'` and the posted hash matches the session's own
3371
+ `signing.json` `specHash` exactly, then spawns `setsid systemd-inhibit … node bin/
3372
+ bareloop.mjs run-u --spec <resolved-spec.json> --approve <hash>` (plus `--draft-spent-usd <n>`
3373
+ [`--draft-spend-incomplete`] when the session's drafting spend is > 0 — one cap covers
3374
+ drafting + run) detached (array argv,
3375
+ never a shell string), with its own log file inside the session's own dir, and the
3376
+ server's environment with `~/.config/bareloop/.env` merged in per request (a key is never read
3377
+ into or sent to the page — a missing one refuses the session at $0, naming only the env var). `src/panel/authorsession.js` takes
3378
+ test-only DI seams (`scout`/`generate`/`confirmGenerate`/`authorFn`/`prepareSigningFn`) so
3379
+ a test can drive the real ask()-channel/revise/hash wiring without a live provider call;
3380
+ none of them are reachable from `authorroutes.js`'s real construction path. Sessions live
3381
+ under `~/.config/bareloop/panel-sessions/<id>/` (each one's own `resolved-spec.json` and
3382
+ `signing.json`, the two files `bareloop author` itself already writes). Edit/re-sign, the
3383
+ `~/.config/bareloop/.env` keys-file loader, and Settings have landed (P4a/P4b — see "Settings" below).
3267
3384
 
3268
3385
  - **`bareloop run-u <flags…>`** (PANEL-BUILD.md P0 task 2/4) → the person-path run flow
3269
3386
  (the JOBS-table/`--spec` runner, resume, the review door — `docs/logs/FINDINGS.md`'s
@@ -3327,6 +3444,15 @@ interviews of their own. All three are dispatched by name only: `bareloop run-u
3327
3444
  returns a code / throws" rule `src/userrun.js` already keeps. Like `interview`, `src/cli.js`
3328
3445
  hands it raw `stdin`/`stdout`/`stderr` (its one interactive seam is the confirm turn).
3329
3446
 
3447
+ **`--budget` and `--draft-spent-usd` (hamr's ruling 2026-09-28, "one cap covers drafting +
3448
+ run"):** `--budget` still means the AUTHORING ceiling, unchanged — the panel sets it equal to
3449
+ the job card's one `Cap $`. On a SIGNING PREPARED stop, `author` prints its own known
3450
+ drafting spend (a floor, never $0 on an unpriced call — F6) and the exact `run-u` command to
3451
+ run it, now including `--draft-spent-usd <known-floor>` when that floor is > 0 — the SAME
3452
+ signed `budgetUsd` is the run's cap too, and `run-u` enforces the remainder
3453
+ (`Cap $ − drafting spent`) as ITS own ceiling. Nothing here is left for a person to
3454
+ hand-compute or re-type, the same rule F185 set for `--spec`.
3455
+
3330
3456
  **Tighten-only budget/wall.** `--budget`/`--wall` on `bareloop run` may only lower the
3331
3457
  bundle's own signed `budgetUsd`/`maxWallMs` — `checkEnvelope`'s `envelope-widen` red refuses
3332
3458
  anything higher. Raising a ceiling is a re-export (a new signed bundle), never a runner flag.
@@ -3369,6 +3495,75 @@ provider but `anthropic-api`, spine bundling for `formatReplay` on the minting r
3369
3495
  `runid` is noted), and tarball packing — a bundle is a plain directory; use `npm pack` on it
3370
3496
  yourself if you want a tarball.
3371
3497
 
3498
+ ### Settings — the keys file, `config.json`, the monthly $ limit, the Providers tab (PANEL-BUILD.md P4a, `src/keysfile.js`, `src/config.js`, `src/monthly.js`, `src/providerrows.js`, `src/panel/settingsroutes.js`)
3499
+
3500
+ Two files in `~/.config/bareloop/`, outside any repo:
3501
+
3502
+ - **`.env` — key VALUES only.** Plain `NAME=value` lines (optional `export `, `#` comments, one pair
3503
+ of quotes). `loadKeysEnv({ env?, home? })` merges it over the environment; **the shell wins** —
3504
+ the file only fills names the shell leaves unset. Mode other than 600 = a `warning` string
3505
+ (printed on CLI start, shown in Settings), never a refusal. Values go only to provider
3506
+ construction; the page and every log get NAMES and found / not set. `keysForDoor(deps)` is the
3507
+ door helper: an injected `env` with no `keysHome` skips the file entirely (tests never read the
3508
+ real one). The CLI (`run-u`, `interview`, `author`) and the panel both use it.
3509
+ - **`config.json` — every setting, never a key value.** `readConfig({ home })` /
3510
+ `updateConfig(patch, { home })` (atomic tmp + rename, mode 600, unknown fields kept).
3511
+ Fields: `monthlyLimitUsd` (number above 0; absent or `null` = no limit; any other value present
3512
+ is a `ConfigError`, refused like an unreadable file), `anthropicBalanceNote` (a note the
3513
+ person types; no check ever reads it), `keys.<ENV NAME>` = `{ name, shape, baseUrl }` (the Providers rows' settings). A secret-shaped
3514
+ string anywhere in the document is refused on save. An unreadable file is a `problem` string and
3515
+ a refused run start ($0), never "no limit". There is no monthly TIME limit (dropped).
3516
+
3517
+ **Monthly $ limit.** `checkMonthlyRoom({ capUsd, home, now })` → `{ ok, leftUsd, limitUsd,
3518
+ atLeast }`; `monthlyRefusalText(room)` is the ONE spelling: `Max $<left, 2 decimals> (monthly
3519
+ limit)`. It is called at the run-start seam in `src/userrun.js` `execute` (before any token
3520
+ spends; the CLI prints it to stderr, exit 2) and at the panel's Sign & run (`signRun` in
3521
+ `src/panel/authorroutes.js`, refused server-side whatever the page shows). The page only echoes the
3522
+ same text under `#jf-cap-money` via `GET /api/author/monthly-check?cap=`. "This month" is the local
3523
+ calendar month; a died or incomplete-spend run makes the total an "at least" figure. A run still
3524
+ IN FLIGHT (`job-start`, no `job-end`, spine file written within `DIED_MTIME_MS`, `src/runlist.js`) is
3525
+ counted at its full leg cap (`budgetUsd` less `priorSpentUsd` on its `job-start`), not its spend so
3526
+ far (except the run a resume continues, which counts its real spend), so two runs cannot both start against a limit only one fits; a died run counts its floor.
3527
+ Only the refusal reserves the cap (`monthSpend().reservedUsd`); the Money tab's month figure is
3528
+ real spend.
3529
+
3530
+ **Money & limits tab** (`GET/POST /api/settings/money`): two tiles ($ and tokens, this month / to date, a
3531
+ `≥` prefix when a figure is a floor) and a per-provider table ($, minutes, tokens; a run is grouped
3532
+ by the provider on its `job-start`, an older spine by its model). The limit field auto-saves on
3533
+ change (no Save button); blank clears it. `POST` needs an explicit `monthlyLimitUsd` key (a number
3534
+ above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, never "clear the limit".
3535
+
3536
+ **Providers tab (P4b, `src/providerrows.js`).** One row per key in `.env` that HAS A VALUE — nothing is
3537
+ hardcoded; an empty line is no row, a removed line drops its row on Reload keys. A missing `.env` is
3538
+ created (mode 600) on the first Providers read with five empty preset lines (`ANTHROPIC_API_KEY`,
3539
+ `DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `LOCAL_API_KEY`); an existing file is never
3540
+ edited. A value of `null` counts as SET and means "no real key" (the provider is handed the
3541
+ placeholder `no-key-needed`, never the word `null`). Each row: the env NAME (read-only) + found,
3542
+ **Name** (the model id Chat uses), **API shape** (Anthropic / OpenAI-compatible / Gemini), **Base
3543
+ URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display only), and Balance
3544
+ (DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
3545
+ `keys.<ENV NAME> = { name, shape, baseUrl }`. Defaults for a key with no saved entry:
3546
+ `ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
3547
+ OpenAI-compatible / `https://api.deepseek.com/v1`; `OPENAI_API_KEY` OpenAI-compatible;
3548
+ `GEMINI_API_KEY` Gemini; `LOCAL_API_KEY` OpenAI-compatible / `http://127.0.0.1:11434/v1` (Ollama, LM
3549
+ Studio, llama.cpp: change the URL); any other name OpenAI-compatible; Name blank until typed. **No
3550
+ price is shown anywhere** (display only — cost recording, the estimated provenance, spend sums, caps
3551
+ and the monthly limit are unchanged; a local model has no known rate, so its rounds come out as a
3552
+ non-zero loud guess, never a real $0).
3553
+
3554
+ `providerrows.js` is the ONE owner of "model → key + shape + URL": `keyRows`, `findRow` (rows on the
3555
+ same shape + endpoint; the Name breaks a tie), `modelChoiceFor` / `chatModels` (Chat's Model menu IS
3556
+ the rows; a blank Name is not offered), `keyNameFor` / `applyConfiguredKey` (a job on `(provider,
3557
+ baseUrl[, model])` reads the matching row's variable, else the provider's built-in one). All doors
3558
+ (`run-u`, `interview`, `author`, the panel's drafting) use it. Routes (all behind the per-start token
3559
+ and Origin check; the chat can never reach them): `GET /api/settings/providers`, `POST .../row`
3560
+ (`{envName, name, shape, baseUrl}`), `POST .../balance-note`, `POST .../test` (one
3561
+ `checkProviderReachable` models-list GET at the row's shape + URL, $0; a refused connection reads
3562
+ `nothing answering at <URL>`), `GET .../balance?env=`
3563
+ (DeepSeek-hosted row, server-side), and `GET /api/author/models` (Chat's Model menu). The old
3564
+ `providers.<row>.key` config field and the key dropdown are gone. Like the authoring routes, a POST
3565
+ body over 1 MiB is refused `413`.
3566
+
3372
3567
  ### The source front door — a plain folder/file/URL, no `--patient` (PRD item 33/M2, `src/source.js`)
3373
3568
 
3374
3569
  A repo job still gives `runJob` a `--patient` checkout it owns. A NON-code job (a resume, a
package/bin/bareloop.mjs CHANGED
@@ -15,7 +15,6 @@ process.stdout.on('error', (e) => {
15
15
  });
16
16
 
17
17
  const code = await main(process.argv.slice(2), {
18
- env: process.env,
19
18
  stdout: process.stdout,
20
19
  stderr: process.stderr,
21
20
  cwd: process.cwd(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.30.0",
3
+ "version": "0.32.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 });