bareloop 0.46.0 → 0.47.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 +40 -0
- package/README.md +2 -0
- package/bareloop.context.md +38 -17
- package/package.json +1 -1
- package/src/authorreadout.js +47 -12
- package/src/cli.js +29 -45
- package/src/launcher.js +409 -0
- package/src/learn.js +24 -16
- package/src/learnstore.js +9 -17
- package/src/opener.js +30 -0
- package/src/panel/authorroutes.js +4 -3
- package/src/panel/authorsession.js +5 -7
- package/src/panel/index.html +13 -1
- package/src/panel/schedulestart.js +5 -0
- package/src/panel/server.js +57 -11
- package/src/panel/settingsroutes.js +3 -17
- package/src/panel/status.js +11 -0
- package/src/stagewords.js +18 -17
- package/src/tty-colour.js +27 -0
- package/src/userrun.js +2 -2
- package/types/authorreadout.d.ts +7 -0
- package/types/cli.d.ts +2 -1
- package/types/launcher.d.ts +234 -0
- package/types/learn.d.ts +8 -7
- package/types/learnstore.d.ts +4 -5
- package/types/opener.d.ts +12 -0
- package/types/panel/authorroutes.d.ts +1 -0
- package/types/panel/server.d.ts +24 -0
- package/types/panel/status.d.ts +8 -0
- package/types/stagewords.d.ts +16 -15
- package/types/tty-colour.d.ts +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.47.0] - 2026-10-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`bareloop` is a launcher (PANEL-BUILD.md addendum L, hamr 2026-10-24).** Bare `bareloop` shows a menu (`1 start 2 stop 3 status 4 install q quit`) under two header lines (installed or not, panel running or not, `runs: N running · N need you · N in all`); the same four work as `bareloop start | stop | status | install`. No pid file: the panel is found by asking ports 4700-4709 for the new read-only `GET /api/whoami`. `start` runs the panel detached on the first free port (log: `~/.config/bareloop/panel.log`) and opens the page; `stop` stops every panel it finds (one panel at a time; no `--port`, no which-one question), warns about open drafts summed over them and asks (`--yes` skips); `status` prints `Panel not running.` and exits 3 when no panel runs (0 from the menu); `install` creates only what is missing. The old `1 export / 2 run / 3 history` menu is gone (the sub-commands stay). `bareloop panel [--port N]` is unchanged. Coloured on a terminal (off when piped, `NO_COLOR` set or `TERM=dumb`). `status` and the header show the running panel's version, with a stop-and-start note when it differs from the CLI's. The header's `need you` count matches the panel's Needs-you list, schedules that gave up included. `GET /api/whoami` (app, version, pid, port, drafts, schedules) sits behind the Host guard and needs no token. The panel address has one owner (`panelUrl`); no address literal lives in the launcher, and the OS opener is shared with Settings.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **A rubric run's lessons count at run end, like a deterministic run's (hamr 2026-10-24, option A).** `foldRunIntoLedger` no longer holds a `rubric` run's cards in `pending` until the human Accept: they go through `countLedger` now, FIXED and NEVER FIXED alike, with `trust: 'rubric'` kept on new rows; `held` is always 0. A hold already in a ledger file (written by v0.46.x) still releases on the door Accept. Reuse and bridge credit is unchanged and still waits for the Accept. Noise runs stay dropped.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **A resumed leg updates its own run's ledger row.** `countLedger` skipped a row that already held the run id, so a NEVER FIXED lesson from leg 1 stayed NEVER FIXED when leg 2 (same run id) fixed it. The row is now refreshed from the new card (sample, lesson, key, lastSeen, hash, place) while its count, model tally and run ids stay one per run; an older run's cards re-folded later never step a row back. Found by fwdloop.
|
|
21
|
+
|
|
22
|
+
## [0.46.1] - 2026-10-24
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A blank Base URL in the New job readiness line reads "Type a Base URL."** It showed `[✗] no-base-url — may be flaky, not blocking`; it now shows the server's own sentence (owned by `src/modellist.js`).
|
|
27
|
+
|
|
28
|
+
- **The Audit learnings header counts distinct hints across the draft and the first replan.** It counted only the draft block (3 at draft, 5 at replan read "3 hints"). A hint handed at both draft and replan counts once (identity = ledger row id).
|
|
29
|
+
|
|
30
|
+
- **A scheduled fire refused at the run-start fetch shows its status word on the schedule card.** The card read `last 10:30 · run-4` with no word because the run list's events were not passed to `summarizeRow`; the refused row now carries its `failed` status.
|
|
31
|
+
|
|
32
|
+
- **Every drafting stop reads in plain words from one table.** `confirm-abandoned` and `confirm-restart` printed `Stopped: <code>`, and a code outside an 8-entry table fell back to `Drafting stopped (<code>).`. `STOP_HEADS` (with `STOP_TODO` where "draft again" is wrong) in `src/authorreadout.js` now covers about 20 stop codes and `authorsession` routes every stop through `draftStopText`; an unknown code gets a generic sentence, never the raw code. The confirm-abandoned stop now ends "nothing more was spent" (it promised a figure that reads "unknown" when spend is incomplete). Wording signed by hamr 2026-10-23.
|
|
33
|
+
|
|
34
|
+
- **One `isReusableGreen` decides every Reuse-eligibility check.** The picker, `getStartFrom`, the start gate and prefill flag in `authorroutes`, and the Ended block each spelled it their own way, and the Ended block still treated an already-green as not reusable. An already-green now ends "Nothing to do." like a green, and a scheduled already-green run is held for Accept like a green. Scheduling itself still needs a real green (`greenFor` in `src/schedules.js` is unchanged).
|
|
35
|
+
|
|
36
|
+
- **A watched-folder file whose name holds a line break or ends in whitespace is refused.** A newline in the name became extra Inputs lines in the fire's card text, and a trailing space trimmed to a path that does not exist. `perFireSpec` now refuses it through the ordinary refused-fire path ("Rename the file. Nothing started."; one refused row, no session) and never rewrites the name. A leading space is harmless and not refused.
|
|
37
|
+
|
|
38
|
+
- **`scripts/hermetic.mjs` refuses a test path that does not exist.** A mistyped path was skipped and the run exited 0, so a green count could miss a file. A path-looking argument that does not exist now exits 1 with a plain message and runs nothing; `npm test` with no arguments, flags and globs are untouched. The value of a value-taking flag (`--test-name-pattern "a/b"`, `--test-reporter`, `--import`, `-r`, ...) is never checked as a path; a boolean flag followed by a mistyped path is still refused.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **Linear-time trailing trims in the reuse-preprobe `normPath` (`scripts/`, not shipped).** The `[.,;:)\]}'"`]+$` and `\/+$` trims were the quadratic class (about 1.2 s CPU on 30000 trailing dots); same results on 1.5M random strings, 0 mismatches.
|
|
43
|
+
|
|
44
|
+
- **`stageQuestionText`'s JSDoc now sits on the function, not on `GENRE_WHAT`.** Comments and declaration order only.
|
|
45
|
+
|
|
46
|
+
- **Tests strengthened (test-only, no source change).** A seeded differential test pins the linear `stringLiteralsOf` and `staticImportsOf` scanners against the old regexes; `buildCards`' card-level secret mask, the judged-strike trend number (read from the real `runDeclaredStages`), the Revise-at-menu spend figure (waits for the unwind, runs a second draft), and money-as-read in symbol-first and far-apart layouts each have a test that fails on a mutant.
|
|
47
|
+
|
|
8
48
|
## [0.46.0] - 2026-10-23
|
|
9
49
|
|
|
10
50
|
### Added
|
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ building it.
|
|
|
68
68
|
npm install bareloop
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
Quick start: `npm install -g bareloop`, then `bareloop` — a menu to start the panel, stop it, see its status, and set up `~/.config/bareloop/`.
|
|
72
|
+
|
|
71
73
|
## Usage
|
|
72
74
|
|
|
73
75
|
A signed job spec goes in. A verdict comes out.
|
package/bareloop.context.md
CHANGED
|
@@ -627,7 +627,7 @@ a step, or test it without a provider.
|
|
|
627
627
|
`error: 'truncated:max_tokens'`, neutral `stopReason: 'max_tokens'`) is a FAILED TRY on `askStructured`'s retry ladder with the
|
|
628
628
|
reason "the AI's answer was cut off at the output limit" (`axis: 'output-truncated'`) — NOT `provider-red`; all attempts stay
|
|
629
629
|
booked, `AUTHOR_MAX_TOKENS` is never raised, and only a real transport failure stays `provider-red`. `draftStopText`
|
|
630
|
-
(`src/authorreadout.js`) words
|
|
630
|
+
(`src/authorreadout.js`) words EVERY drafting stop from one table (`STOP_HEADS` for the sentence, `STOP_TODO` where "draft again" is the wrong advice; a code the table lacks reads one generic sentence, never the raw code); the panel puts it on the failed step. (2)
|
|
631
631
|
`authorClose({ offerQuestions })` (default `false`; the CLI never sets it) offers the optional `questions` property on the LAST
|
|
632
632
|
try of the full ladder only, and only when the two tries before it both failed — validation OR the seed read (hamr ruled 2026-10-10; this DIFFERS
|
|
633
633
|
from fwdloop's validation-only rule): a seed-read failure is a work stage that instrument-stopped or a close with no work stage
|
|
@@ -3074,7 +3074,9 @@ Enforcement is **local only** — wired into `npm test` (see `package.json`'s `t
|
|
|
3074
3074
|
script), never into `.github/workflows/*` (CI already runs `npm test`, so the rule is
|
|
3075
3075
|
enforced there without an ask-first CI edit). `npm test` itself now runs `node --test`
|
|
3076
3076
|
through `scripts/hermetic.mjs`, which strips this machine's `HOME`/git identity so a
|
|
3077
|
-
test that leans on either fails locally instead of only on CI (F136).
|
|
3077
|
+
test that leans on either fails locally instead of only on CI (F136). It also refuses a
|
|
3078
|
+
test path that does not exist (`node --test` skips one silently, so a mistyped file could hide
|
|
3079
|
+
behind a green count); the separate value of a value-taking flag is never read as a path. The check itself lives outside the
|
|
3078
3080
|
published package, at `scripts/prompt-commit-check.mjs` (pure decision logic in
|
|
3079
3081
|
`scripts/promptcommitlib.mjs`, `scripts/` is not in `package.json`'s `files`, so none of
|
|
3080
3082
|
this ships):
|
|
@@ -3289,16 +3291,35 @@ real key check is skipped entirely — a scripted provider *is* the run, the
|
|
|
3289
3291
|
same seam `tests/planrun-*.test.js` uses. `deps` also accepts `env`, `stdout`, `stderr`, `cwd`,
|
|
3290
3292
|
`now`, `stdin` — omit any of them and the real `process.*` equivalent is used.
|
|
3291
3293
|
|
|
3292
|
-
Bare `bareloop` (no sub-command)
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
`
|
|
3294
|
+
Bare `bareloop` (no sub-command) is the **launcher menu** (PANEL-BUILD.md addendum L, `src/launcher.js`): two header lines, then
|
|
3295
|
+
`1 start 2 stop 3 status 4 install q quit`. The header is printed first by EVERY call (menu and each of
|
|
3296
|
+
`bareloop start | stop | status | install`), from one function:
|
|
3297
|
+
|
|
3298
|
+
```
|
|
3299
|
+
bareloop 0.47.0 · installed ✓ · panel running at http://127.0.0.1:4701
|
|
3300
|
+
runs: 1 running · 2 need you · 375 in all
|
|
3301
|
+
```
|
|
3302
|
+
|
|
3303
|
+
Colour (`src/tty-colour.js`): on only when stdout is a terminal, `NO_COLOR` is empty/unset and `TERM` is not `dumb`; it wraps words and never
|
|
3304
|
+
changes them, and a piped or `NO_COLOR` run prints the plain words above (the thin `─` rules around the header appear only with colour).
|
|
3305
|
+
|
|
3306
|
+
- `installed ✓` = `~/.config/bareloop/` exists, `config.json` is there and reads with no problem, `.env` exists with mode 600, node >= 22;
|
|
3307
|
+
otherwise `not installed — <first missing thing>, pick 4 install`. The run counts use the panel's own status words and Needs-you rule over the
|
|
3308
|
+
run list file ($0, works with the panel stopped).
|
|
3309
|
+
- **No pid file.** The panel is found by asking `127.0.0.1:4700..4709` for `GET /api/whoami` (300 ms each, in parallel); a reply with `app: "bareloop"` is a panel.
|
|
3310
|
+
`GET /api/whoami` → `{ app: "bareloop", version, pid, port, drafts, schedules }` (open drafting sessions, jobs with a schedule), read-only, same Host guard as every
|
|
3311
|
+
route, no token.
|
|
3312
|
+
- **`bareloop start`** → already running: says so, opens the page, starts nothing (if the running panel's version differs from this bareloop's it adds `It is version X; this bareloop is Y. Run bareloop stop, then bareloop start, to use Y.`). Else starts `bareloop panel --port N` DETACHED (`setsid`; log appended to
|
|
3313
|
+
`~/.config/bareloop/panel.log`) on the first free port from 4700 (a taken port is skipped and named), waits up to 10 s for it to answer, opens the page with
|
|
3314
|
+
`xdg-open`/`open` (no opener: prints the URL). `bareloop panel [--port N]` is unchanged (foreground, fixed port).
|
|
3315
|
+
- **`bareloop stop [--yes]`** → SIGTERM to the pid of EVERY panel the probe finds (one panel at a time; no which-one question), waits until each port is quiet. Open drafts (in memory, summed over all found panels) are warned about and a y/N is asked;
|
|
3316
|
+
`--yes` skips it. Not running: `Panel not running.`, exit 0. Several found: all stopped, `N panels stopped.`. Any other option (e.g. `--port`) is refused in plain words. Runs are separate processes and keep going.
|
|
3317
|
+
- **`bareloop status`** → header plus `pid N · version X · N draft(s) open · N schedule(s) on`. Exit 0 running, 3 not running. A panel whose version differs from the CLI's shows `(version X — stop and start to use Y)` after its URL in the header (yellow on a terminal).
|
|
3318
|
+
- **`bareloop install`** → creates only what is missing (folder mode 700, `.env` with the keys-file comment lines, mode 600, `config.json`); never reads a key value;
|
|
3319
|
+
`Already installed, nothing to do.` when there is nothing to add. Needs node >= 22.
|
|
3320
|
+
|
|
3321
|
+
The old `1 export 2 run 3 history` menu is gone; `bareloop export|run|history` and the other sub-commands are unchanged. `run-u`, `interview` and `author` are dispatched
|
|
3322
|
+
by name only: `bareloop run-u <flags…>`, `bareloop interview <flags…>`, `bareloop author <flags…>`.
|
|
3302
3323
|
|
|
3303
3324
|
When two key rows share one provider and endpoint, a CLI run uses the FIRST row in the keys list, so put the
|
|
3304
3325
|
key you want first (the panel picks the row by the Model menu).
|
|
@@ -3888,8 +3909,8 @@ No model is called anywhere in learnings (a test walks the import closure). Pure
|
|
|
3888
3909
|
- **Run end:** `execute` (`src/userrun.js`, shared by the panel, the CLI and the bundle runner) calls `learnAtRunEnd({ home, runid, events, auditFiles, spec, specHash, workdir, trust })` once, right after the claim settles, in its own try/catch: a failure prints one WARNING and never changes the verdict, exit code or spend. It writes nothing when the engine has no config home.
|
|
3889
3910
|
- **Cards:** `buildCards({ runid, events, audit, workdir, spec, model, questions })` → `{ cards, suspect, thin, noise }`. FIXED = a check red then the same check passes later in the run; NEVER FIXED = still red at run end. Excluded and counted: noise runs (provider-red, close-crashed, worker-crash, no close spoke), CHECK-SUSPECT (a no-suppressions hit on a prose comment), TOO-THIN (no error kind and no fix files). Written files come from `attempt-diff` events and the gate audit's allow write/edit rows relative to the tree.
|
|
3890
3911
|
- **Fold:** `cardsMatch` / `foldCards`: same job + check, a shared error kind, Jaccard >= 60% on flagged-text tokens (a fit on known cards, not a proof). Code-less cards (kind `other`) fold on the text rule alone, only when both token sets are non-empty (R1, 2026-10-23: UNTESTED on real data; the 60% was fitted on cards with codes). `kindAgrees` is the one kind test.
|
|
3891
|
-
- **Ledger** `<config home>/learn/ledger.json` (mode 600, atomic, secrets masked): rows `{ id, lesson, count, sample, models, workflow, place, level, trust, runIds, firstSeen, lastSeen, specHash }`, plus `pending` (
|
|
3892
|
-
- **Reading:** `hintsForRun({ home, spec, specHash })` renders once at run start; `runJob(..., { learnHints: { text, ids } })` → `runPlan` hands it to the PLANNER at the draft and the first replan only, and emits a `learnings` spine record with the exact text. `renderHints(ledger, { workflow, place, specHash, now })` is the one render (this job, then this place, then all-jobs, highest count first, 2,000 characters, a hint whole or not at all; all-jobs hints last); a FIXED hint adds `What changed:` (first 3 `+`/`-` lines of the stored diff, 150 chars each, then `(+N more)`; never on NEVER FIXED) and a repeated path in a hint reads as its basename (R3, R5, 2026-10-23); the spine's recorded text is byte-equal to a fresh `renderHints(...).text` from the ledger as it stood. The Audit run detail carries `learnings` (read-only).
|
|
3912
|
+
- **Ledger** `<config home>/learn/ledger.json` (mode 600, atomic, secrets masked): rows `{ id, lesson, count, sample, models, workflow, place, level, trust, runIds, firstSeen, lastSeen, specHash }`, plus `pending` (holds an older version wrote for rubric runs; new runs write none), `suspect`, `health`. There is no check-wide set-aside (R2, 2026-10-23): a lesson seen in several jobs climbs by the climb rules and is read at its level; all-jobs hints come last. `countLedger` (+1 per run per row), `releasePending` (an OLD hold, written by v0.46.x, counts on the human Accept; `releaseRun` is called from the door's accept path; a rubric run's lessons now count at run end like a deterministic run's, trust `rubric` kept, and `foldRunIntoLedger` returns `held: 0`; reuse/bridge credit still waits for the Accept). A row already holding the run id (a resumed leg, one id) is refreshed from the new card (sample, lesson, key, lastSeen, hash, place) but its count, model tally and run ids do not move, `climb` (the level of a lesson, derived at read time by `readableRows` from the rows not decayed at that moment: it can go back down when siblings age past 56 days; the file stores `workflow` only and ignores an older stored level). The ledger keeps EVERY row (nothing is deleted, no `expired` flag is stored; hamr 2026-10-23). Memory is the filter: `readableRows` / `renderHints` skip a row last seen more than 56 days ago (`isDecayed`) and a workflow row of another signed hash (`otherJobVersion`); a lesson seen again counts +1 on its old row. Old files with `status`/`expiredBy` still load; the fields are ignored and dropped at the next write.
|
|
3913
|
+
- **Reading:** `hintsForRun({ home, spec, specHash })` renders once at run start; `runJob(..., { learnHints: { text, ids } })` → `runPlan` hands it to the PLANNER at the draft and the first replan only, and emits a `learnings` spine record with the exact text. `renderHints(ledger, { workflow, place, specHash, now })` is the one render (this job, then this place, then all-jobs, highest count first, 2,000 characters, a hint whole or not at all; all-jobs hints last); a FIXED hint adds `What changed:` (first 3 `+`/`-` lines of the stored diff, 150 chars each, then `(+N more)`; never on NEVER FIXED) and a repeated path in a hint reads as its basename (R3, R5, 2026-10-23); the spine's recorded text is byte-equal to a fresh `renderHints(...).text` from the ledger as it stood. The Audit run detail carries `learnings` (read-only); its header counts DISTINCT hints (the same ledger row handed at the draft and the first replan counts once).
|
|
3893
3914
|
|
|
3894
3915
|
### 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`)
|
|
3895
3916
|
|
|
@@ -3976,7 +3997,7 @@ above 0, or `null`/blank to clear) — an empty or key-less body is a `400`, nev
|
|
|
3976
3997
|
only the shell holds when config already names it (`keys.<ENV>` or `models.<ENV>`; its status reads `found in shell`) — nothing is hardcoded, there is no add-row and no remove-row control. A missing `.env`
|
|
3977
3998
|
is created (mode 600, dir 700) on the first Providers read with two comment lines and no key. A value of `null` counts as SET and means "no real key" (the provider is handed the placeholder
|
|
3978
3999
|
`no-key-needed`, never the word `null`; the row shows `set in file`). A key whose value has a space or a line break shows `The key has a space or a line break in it. Fix it in your keys file, then
|
|
3979
|
-
reload keys.` Each row: the env NAME + status, an **API shape** select (Anthropic / OpenAI-compatible / Gemini), the typed **Base URL** (empty = no run, no list, `type the Base URL`), the **Old max_tokens**
|
|
4000
|
+
reload keys.` Each row: the env NAME + status, an **API shape** select (Anthropic / OpenAI-compatible / Gemini), the typed **Base URL** (empty = no run, no list, `type the Base URL`; the Test line reads the server's own sentence `Type a Base URL.`), the **Old max_tokens**
|
|
3980
4001
|
tick (applies to `openai-api` only), **Tokens** (the key's spend), `[ Test ] [ ⟳ Refresh ]`, and its model lines: a **Use** tick, the model name, **In / Out $/1M** boxes, tokens (`—` when no spend row names the model).
|
|
3981
4002
|
A listed id from Refresh is shown unticked and is not saved until touched; `+ type a model name` adds a line unticked. Saves are on change; success reads `saved — applies from the next model call`;
|
|
3982
4003
|
every refusal is a plain sentence in `say` (`Fill both prices to tick this model.`, `Fill both price boxes or clear both.`, `A price must be a number, 0 or more.`, …). **Open keys folder** (in the KEYS strip,
|
|
@@ -4439,12 +4460,12 @@ Check what the person asked for, not what the worker chose to write. bareloop ne
|
|
|
4439
4460
|
- **Only the panel fires.** `createPanelServer` runs one 500 ms unref'd scheduler (poll only, never `fs.watch`); the CLI never fires. Panel off = nothing fires; slots missed collapse to ONE catch-up (marked when more than 2 minutes late). `createPanelServer` test seams: `scheduleNow`, `schedulerIntervalMs` (0 = no timer), `sessionDeps`, `acceptWaitMs`.
|
|
4440
4461
|
- **Files live under `<config home>/triggers/<job>/`**: `schedule.json` (the current signed schedule) and ONE append-only `ledger.jsonl` (every row carries `sig`, the spec hash it was written under). A run that a schedule started carries `trigger.json` beside its `resolved-spec.json`.
|
|
4441
4462
|
- **Setting one is two clicks, both $0** (`POST /api/author/schedule-prepare`, then `/api/author/schedule` with the hash). It needs a real green run of the job's CURRENT workflow (a rubric job also an honoured Accept). A file trigger is for plain-files jobs only. Cancel is never refused. A reuse never clears a schedule: Reuse workflow (the one door, v1.101) keeps the origin's name while no job box is changed and switches the Job name to the next free `<base>-N` once one is; a changed job is a new workflow that needs its own green before it can be scheduled.
|
|
4442
|
-
- **A fire goes busy, then monthly limit, then `wouldAccept`, then the one starter** (the same session start and `signRun` as a Reuse the person clicked, with `--review-door`). Busy, monthly and refused fires consume nothing and are told once per `key|outcome|why`, so they retry. A run is busy while its pid is alive AND its process started no later than 5 s after its run row was written (a recycled pid is not that run). A run waiting at its review door is not busy.
|
|
4463
|
+
- **A fire goes busy, then monthly limit, then `wouldAccept`, then the one starter** (the same session start and `signRun` as a Reuse the person clicked, with `--review-door`). Busy, monthly and refused fires consume nothing and are told once per `key|outcome|why`, so they retry. A file fire whose name holds a line break or ends in whitespace is refused at `perFireSpec` (the card text is `label: value` lines, so it would read back as another Inputs line); the person is told to rename the file. A run is busy while its pid is alive AND its process started no later than 5 s after its run row was written (a recycled pid is not that run). A run waiting at its review door is not busy.
|
|
4443
4464
|
- **An install-needed refusal backs off, then gives up.** When a fire's copy needs packages, that job's next attempt waits 1, 2, 3, 5, 8, 13, then 21 minutes (in memory: a restart begins at the first wait); a wait that would pass 60 minutes in total is not taken: the schedule writes ONE `gave-up` row and is paused (nothing fires; files still queue as `arrived`), and the job shows in Needs you with the pill `Check` (`listGaveUp`). Signing the schedule again writes `resumed` and clears it. Constants: `INSTALL_RETRY_STEPS_MS`, `INSTALL_GIVE_UP_MS` (`src/panel/schedules.js`).
|
|
4444
4465
|
- **A scheduled green is held until accepted**: `getStartFrom` reads its outcome as `green-held` and the Reuse list skips it, until the spine holds an honoured accept.
|
|
4445
4466
|
- **Accept** (`POST /api/runs/<id>/accept`) runs the engine's own door path (`run-u --spec <signed spec> --door <id> --decide accept --approve <hash>`): the mechanical stages re-run, a red is `door-accept-red`, the run's verdict is untouched. It shows only while `doorWaiting(events)` (`src/reviewdoor.js`) is true; the same answer fills the runs list's Needs-you `Accept`.
|
|
4446
4467
|
- **Every word the page shows** comes from `src/schedulewords.js` (`SCHEDULE_WORDS` is the one button table the server sends).
|
|
4447
|
-
- **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, word, head, rows}`, drawn `[sign] word schedule · head` with the sign and word both from the status table, v1.106 Part 4; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
|
|
4468
|
+
- **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing; `last` wears the run's status word even for a fire refused at the run-start fetch, which has no spine, only a settled event), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, word, head, rows}`, drawn `[sign] word schedule · head` with the sign and word both from the status table, v1.106 Part 4; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
|
|
4448
4469
|
|
|
4449
4470
|
|
|
4450
4471
|
## The Audit's tries, checks and answered questions (PRD v1.106 Part 5)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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/authorreadout.js
CHANGED
|
@@ -656,6 +656,50 @@ export function proposalStopText({ stop, reds, cases = null, spend = null, sourc
|
|
|
656
656
|
/** the reason inside a `scout-absent` red's detail: `the survey is ABSENT (<reason>) — an absent facts object ...` (src/authorflow.js) */
|
|
657
657
|
const SCOUT_REASON_RE = /^the survey is [^(]*\((.*)\) — an absent facts/s;
|
|
658
658
|
|
|
659
|
+
/**
|
|
660
|
+
* ONE owner of the plain sentence for every drafting stop code (hamr 2026-10-09: "a user should get a reason they can
|
|
661
|
+
* understand and do something about"). What happened; `STOP_TODO` below overrides the default "draft again" where that is wrong.
|
|
662
|
+
* Used by `draftStopText` for every stop the panel and the CLI show; a code not listed reads `STOP_UNKNOWN`, never the raw code.
|
|
663
|
+
* @type {Record<string, string>}
|
|
664
|
+
*/
|
|
665
|
+
export const STOP_HEADS = {
|
|
666
|
+
'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
|
|
667
|
+
'artifact-red': 'The AI answered, but its answer could not be used as a plan.',
|
|
668
|
+
'cap-halt': 'Drafting stopped on your cost cap.',
|
|
669
|
+
'pricing-red': 'Drafting stopped: the cost of a call could not be priced, so the cap cannot be enforced.',
|
|
670
|
+
'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
|
|
671
|
+
precheck: 'Drafting stopped before any call: something it needs was missing.',
|
|
672
|
+
unchanged: 'Drafting stopped: the AI sent the same plan twice.',
|
|
673
|
+
'scout-absent': 'Drafting stopped: the first look at your repo or input did not complete.',
|
|
674
|
+
refused: 'Drafting stopped: something in the job or its inputs is not allowed.',
|
|
675
|
+
'interview-incomplete': 'Drafting stopped: the job form was not complete, so there was nothing to draft from.',
|
|
676
|
+
'calibration-missing': 'Drafting stopped: the practice cases that prove the rubric were missing, so the check could not be trusted.',
|
|
677
|
+
'invalid-set': 'Drafting stopped: the rubric and its practice cases do not fit together, so they could not be graded.',
|
|
678
|
+
'broken-close': 'Drafting stopped: the final check the model wrote could not run.',
|
|
679
|
+
'listing-absent': 'Drafting stopped: the list of your files was not available, so the checks could not name any.',
|
|
680
|
+
'listing-conflict': 'Drafting stopped: the list of your files was given twice and the two did not agree.',
|
|
681
|
+
'listing-unreadable': 'Drafting stopped: your repo\'s file list could not be read.',
|
|
682
|
+
'non-code-source': 'Drafting stopped: this source is not a code repo, and this kind of job only works on code.',
|
|
683
|
+
'authoring-failed': 'Drafting stopped for a reason that was not recorded.',
|
|
684
|
+
'confirm-abandoned': 'Drafting stopped: the confirm step ended before you answered, so nothing was signed.',
|
|
685
|
+
'confirm-restart': 'Drafting stopped: you chose to start over, so nothing was signed.',
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/** the "What to do" for the stops where "draft again" is not the right advice */
|
|
689
|
+
const STOP_TODO = /** @type {Record<string, string>} */ ({
|
|
690
|
+
refused: 'change what the message above names, then draft again.',
|
|
691
|
+
'interview-incomplete': 'fill in every line of the job form, then draft again.',
|
|
692
|
+
'calibration-missing': 'draft again; if it stops the same way twice, make your PASS/FAIL examples more concrete.',
|
|
693
|
+
'invalid-set': 'draft again; if it stops the same way twice, make your PASS/FAIL examples more concrete.',
|
|
694
|
+
'non-code-source': 'point the job at a code repo, or use a folder or file job instead.',
|
|
695
|
+
'listing-unreadable': 'check the repo is a readable git repo with at least one commit, then draft again.',
|
|
696
|
+
'confirm-abandoned': 'start a new draft when you are ready; nothing more was spent.',
|
|
697
|
+
'confirm-restart': 'change the answers you want and start the draft again.',
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
/** a stop code this table does not know: still plain words, never the code */
|
|
701
|
+
const STOP_UNKNOWN = 'Drafting stopped for a reason this screen has no wording for.';
|
|
702
|
+
|
|
659
703
|
/**
|
|
660
704
|
* @param {{stop: string, reds?: PlainRed[]|null, refusal?: {detail?: string}|null,
|
|
661
705
|
* spend?: {knownUsd?: number|null, spendComplete?: boolean|null}|null, wholeJob?: boolean}} o `wholeJob` - a rubric job on the person's
|
|
@@ -674,23 +718,13 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null,
|
|
|
674
718
|
const detail = (Array.isArray(reds) ? reds : []).find((r) => r?.code === 'fit-stopped')?.detail;
|
|
675
719
|
if (detail) return `Stopped before drafting: the page does not show everything the job asks for: ${redactSecrets(String(detail))}. Change the job or the page and draft again. Spent so far: ${spent ?? 'unknown'}.`;
|
|
676
720
|
}
|
|
677
|
-
|
|
678
|
-
const heads = {
|
|
679
|
-
'provider-red': 'The AI provider failed before a usable plan came back — a connection or service fault, not a problem with your job.',
|
|
680
|
-
'artifact-red': 'The AI answered, but its answer could not be used as a plan.',
|
|
681
|
-
'cap-halt': 'Drafting stopped on your cost cap.',
|
|
682
|
-
'pricing-red': 'Drafting stopped: the cost of a call could not be priced, so the cap cannot be enforced.',
|
|
683
|
-
'max-revisions': 'The AI used all its tries without a plan that passed the checks.',
|
|
684
|
-
precheck: 'Drafting stopped before any call: something it needs was missing.',
|
|
685
|
-
unchanged: 'Drafting stopped: the AI sent the same plan twice.',
|
|
686
|
-
'scout-absent': 'Drafting stopped: the first look at your repo or input did not complete.',
|
|
687
|
-
};
|
|
721
|
+
const heads = STOP_HEADS;
|
|
688
722
|
const redList = Array.isArray(reds) ? reds : [];
|
|
689
723
|
// F217: the first look at the repo failing (a dropped connection, a timeout) arrives as a `precheck` stop with a
|
|
690
724
|
// `scout-absent` red. That is NOT "something it needs was missing", and its reason sits past the 60-char quote bound
|
|
691
725
|
// behind a fixed lead-in, so the person saw neither. Word it as the scout's stop and quote the reason in full (scrubbed).
|
|
692
726
|
const scoutRed = stop === 'precheck' ? redList.find((r) => r?.code === 'scout-absent') : undefined;
|
|
693
|
-
const head = scoutRed ? heads['scout-absent'] : (heads[stop]
|
|
727
|
+
const head = scoutRed ? heads['scout-absent'] : (Object.hasOwn(heads, stop) ? heads[stop] : STOP_UNKNOWN);
|
|
694
728
|
/** @type {string[]} */
|
|
695
729
|
const said = scoutRed
|
|
696
730
|
? [SCOUT_REASON_RE.exec(String(scoutRed.detail ?? ''))?.[1]
|
|
@@ -705,6 +739,7 @@ export function draftStopText({ stop, reds = null, refusal = null, spend = null,
|
|
|
705
739
|
}
|
|
706
740
|
lines.push(stop === 'cap-halt' || stop === 'pricing-red'
|
|
707
741
|
? 'What to do: raise the cap or fix the price in Settings, then draft again.'
|
|
742
|
+
: Object.hasOwn(STOP_TODO, stop) ? `What to do: ${STOP_TODO[stop]}`
|
|
708
743
|
: (wholeJob ? 'What to do: review the whole job - every line and its ~ rules, and the inputs - and submit it again.' : 'What to do: draft again.'));
|
|
709
744
|
return lines.join('\n');
|
|
710
745
|
}
|
package/src/cli.js
CHANGED
|
@@ -36,6 +36,8 @@ import { bundleMain } from './bundlerun.js';
|
|
|
36
36
|
// rather than re-parsing argv a second time here keeps exactly one code
|
|
37
37
|
// path for the flag grammar; `src/cli.js` only supplies the deps shape
|
|
38
38
|
// (`out`/`err`/`env`) this command's own callers already use.
|
|
39
|
+
import { startCmd, stopCmd, statusCmd, installCmd, menuCmd } from './launcher.js';
|
|
40
|
+
import { makePaint } from './tty-colour.js';
|
|
39
41
|
import { main as runUMain } from './userrun.js';
|
|
40
42
|
// PANEL-BUILD.md P0 task 3/4 — `bareloop interview` and `bareloop author`
|
|
41
43
|
// wrap the close-authoring interview and authoring-pipeline flows' own
|
|
@@ -272,50 +274,30 @@ function doRuns(args, { out, err, cwd }) {
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
/**
|
|
275
|
-
* The bare `bareloop` menu
|
|
276
|
-
* SAME
|
|
277
|
-
* `
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* reach them, rather than implying only three commands do at all.
|
|
282
|
-
* `keysHome` is the home `main` already resolved (`keysForDoor`), handed on so choice 2 reads the
|
|
283
|
-
* SAME config.json (monthly limit, price, key row) as `bareloop run <bundle>` — never resolved twice.
|
|
284
|
-
* @param {any} deps @param {{ out: (s: string) => void, err: (s: string) => void, cwd: string, env: any, now: () => number }} ctx @param {string|undefined} keysHome
|
|
277
|
+
* The launcher doors (L, PANEL-BUILD.md addendum 2026-10-24): the bare `bareloop` menu and `start | stop | status | install`. All logic is
|
|
278
|
+
* `src/launcher.js`; this only supplies the home (the SAME one `main` resolved for the keys file and config.json) and the line reader.
|
|
279
|
+
* `deps.launcher` is the test seam for every OS touch (ports, spawn, kill, opener, clock).
|
|
280
|
+
* @param {string} cmd 'menu' | 'start' | 'stop' | 'status' | 'install' @param {string[]} rest
|
|
281
|
+
* @param {any} deps @param {{ out: (s: string) => void, err: (s: string) => void }} io @param {string|undefined} home
|
|
282
|
+
* @returns {Promise<number>}
|
|
285
283
|
*/
|
|
286
|
-
async function
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
async function runLauncher(cmd, rest, deps, io, home) {
|
|
285
|
+
const rl = createInterface({ input: deps.stdin ?? process.stdin, output: deps.stdout ?? process.stdout });
|
|
286
|
+
let closed = false;
|
|
287
|
+
rl.once('close', () => { closed = true; });
|
|
288
|
+
// a closed stdin answers '' (= the safe default: quit / "no"), never hangs
|
|
289
|
+
const ask = (/** @type {string} */ q) => (closed ? Promise.resolve('') : Promise.race([
|
|
290
|
+
rl.question(q),
|
|
291
|
+
new Promise((resolve) => { rl.once('close', () => resolve('')); }),
|
|
292
|
+
]).then(String));
|
|
293
|
+
const rawEnv = deps.processEnv ?? process.env;
|
|
294
|
+
const d = { home, ask, paint: makePaint(deps.stdout ?? process.stdout, rawEnv), paintErr: makePaint(deps.stderr ?? process.stderr, rawEnv), ...(deps.launcher ?? {}) };
|
|
290
295
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
const specPath = (await rl.question('job spec path: ')).trim();
|
|
297
|
-
const registry = (await rl.question('registry dir: ')).trim();
|
|
298
|
-
const outDir = (await rl.question('out dir: ')).trim();
|
|
299
|
-
return doExport([specPath, '--registry', registry, '--out', outDir], ctx);
|
|
300
|
-
}
|
|
301
|
-
if (choice === '2') {
|
|
302
|
-
const bundleDir = (await rl.question('bundle dir: ')).trim();
|
|
303
|
-
const repo = (await rl.question('repo path: ')).trim();
|
|
304
|
-
const budget = (await rl.question('budget (blank to skip): ')).trim();
|
|
305
|
-
const wall = (await rl.question('wall minutes (blank to skip): ')).trim();
|
|
306
|
-
const approve = (await rl.question('approve hash (blank to skip): ')).trim();
|
|
307
|
-
const args = [bundleDir, '--repo', repo];
|
|
308
|
-
if (budget) args.push('--budget', budget);
|
|
309
|
-
if (wall) args.push('--wall', wall);
|
|
310
|
-
if (approve) args.push('--approve', approve);
|
|
311
|
-
return await bundleMain(args, { ...ctx, deps, keysHome, parseFlags, printReds });
|
|
312
|
-
}
|
|
313
|
-
if (choice === '3') {
|
|
314
|
-
const bundleDir = (await rl.question('bundle dir: ')).trim();
|
|
315
|
-
return doHistory([bundleDir], ctx);
|
|
316
|
-
}
|
|
317
|
-
ctx.err(`unknown choice ${JSON.stringify(choice)} — one of: 1, 2, 3, q`);
|
|
318
|
-
return 1;
|
|
296
|
+
if (cmd === 'start') return await startCmd(io, d);
|
|
297
|
+
if (cmd === 'stop') return await stopCmd(rest, io, d);
|
|
298
|
+
if (cmd === 'status') return await statusCmd(io, d);
|
|
299
|
+
if (cmd === 'install') return await installCmd(io, d);
|
|
300
|
+
return await menuCmd(io, d);
|
|
319
301
|
} finally {
|
|
320
302
|
rl.close();
|
|
321
303
|
}
|
|
@@ -326,7 +308,7 @@ async function runMenu(deps, ctx, keysHome) {
|
|
|
326
308
|
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
327
309
|
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
328
310
|
* @param {string[]} argv
|
|
329
|
-
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string }} deps
|
|
311
|
+
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, judgeModel?: string|null, now?: () => number, stdin?: any, runlistHome?: string, keysHome?: string, processEnv?: Record<string,string|undefined> }} deps
|
|
330
312
|
* @returns {Promise<number>}
|
|
331
313
|
*/
|
|
332
314
|
export async function main(argv, deps = {}) {
|
|
@@ -345,7 +327,9 @@ export async function main(argv, deps = {}) {
|
|
|
345
327
|
|
|
346
328
|
const [cmd, ...rest] = argv;
|
|
347
329
|
if (keys.warning && cmd && ['run', 'run-u', 'interview', 'author', 'panel'].includes(cmd)) err(`WARNING: ${keys.warning}`);
|
|
348
|
-
|
|
330
|
+
const launcherHome = keys.home ?? deps.runlistHome;
|
|
331
|
+
if (!cmd) return runLauncher('menu', [], { ...deps, stdout }, ctx, launcherHome);
|
|
332
|
+
if (cmd === 'start' || cmd === 'stop' || cmd === 'status' || cmd === 'install') return runLauncher(cmd, rest, { ...deps, stdout }, ctx, launcherHome);
|
|
349
333
|
if (cmd === 'export') return doExport(rest, ctx);
|
|
350
334
|
if (cmd === 'run') return bundleMain(rest, { ...ctx, keysHome: keys.home, parseFlags, printReds });
|
|
351
335
|
if (cmd === 'history') return doHistory(rest, ctx);
|
|
@@ -388,6 +372,6 @@ export async function main(argv, deps = {}) {
|
|
|
388
372
|
// and spine/gate-audit reads over `127.0.0.1` only. `deps.runlistHome`
|
|
389
373
|
// rides through the same injectable seam `run`/`run-u` already use.
|
|
390
374
|
if (cmd === 'panel') return panelMain(rest, { out, err, runlistHome: deps.runlistHome, env: deps.env });
|
|
391
|
-
err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history, run-u, interview, author, replay, runs, panel`);
|
|
375
|
+
err(`unknown command ${JSON.stringify(cmd)} — one of: export, run, history, run-u, interview, author, replay, runs, panel, start, stop, status, install`);
|
|
392
376
|
return 1;
|
|
393
377
|
}
|