bareloop 0.36.0 → 0.38.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 +44 -0
- package/bareloop.context.md +52 -22
- package/package.json +1 -1
- package/src/authorrun.js +2 -3
- package/src/draftspend.js +1 -3
- package/src/exits.js +3 -0
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +19 -1
- package/src/panel/authorsession.js +14 -16
- package/src/panel/index.html +379 -147
- package/src/panel/server.js +79 -15
- package/src/panel/status.js +27 -4
- package/src/planrun.js +11 -8
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/src/source.js +12 -0
- package/types/legs.d.ts +12 -0
- package/types/panel/authorroutes.d.ts +4 -1
- package/types/panel/authorsession.d.ts +2 -7
- package/types/panel/server.d.ts +22 -4
- package/types/panel/status.d.ts +21 -1
- package/types/planrun.d.ts +3 -3
- package/types/ralph.d.ts +6 -1
- package/types/source.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ All notable changes to bareloop are documented here. Format:
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning:
|
|
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
|
+
## [0.38.0] - 2026-10-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Edit in chat on every run that did not finish green.** The Chat card opens filled from the run's signed job with every box open; Start drafting works as for any new job. The Ended block's NEXT line points at the button, and a refused Resume says so too.
|
|
12
|
+
- **Resume moves to the left Chat card.** Resume mode locks the signed job and opens only the two caps (money and time); Sign & resume posts the same body as before.
|
|
13
|
+
- **Expanded jobs show their latest 7 other runs;** older runs are reached by search.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- The run page's Job tab is read-only on every run; the Job-tab resume mode is gone.
|
|
18
|
+
- A stopped run wears `[■]` (red), not `[✗]`, on every surface.
|
|
19
|
+
- Reuse workflow is offered only from a run that earned its green. The Reuse list shows only jobs with a real green run; an already-green run offers Edit in chat instead; `POST /api/author/start` refuses a reuse of any local run that did not finish green (the server owns the rule, not just the pickers); and a delivery-refused ending offers Reuse only on a run that earned its green.
|
|
20
|
+
- The Reuse picker reads "Loading…" until its list arrives, and switching the Check type radios no longer blanks the New job boxes.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- A run cut by the money or wall cap reads `capped` on every surface: a leg that ended escalated on a money-halt or wall-halt, and a cap-cut open step on a single-leg run, no longer read as failed. One helper (`capKindOf`) owns the rule.
|
|
25
|
+
- A Stop clicked in the fix loop reads "during the fix phase ... a resume continues the run" instead of the step text "a resume re-enters this step", which was wrong where no step exists (found on run muxwwrgs).
|
|
26
|
+
- Small cleanups with unchanged behaviour: the `npm ci` comment matches the code, a dead pid above `pid_max` is handled, and tests follow the new panel behaviour.
|
|
27
|
+
|
|
28
|
+
## [0.37.0] - 2026-10-07
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Reuse workflow picker.** The panel's Check type is one radio group of three, the third being Reuse workflow: a search list of jobs that have a green local run. Picking one goes through the existing reuse door (change and leave-reuse included); the Start button is gated on a pick. The origin line is retired. Backed by `GET /api/author/reuse-jobs`, one entry per job with its newest green run id and the same estimate line "Start from this" shows.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Stop is honoured at every round seam.** The stop request was read only in step rounds; the scout, plan and close-fix rounds ignored it while the page said "stopping". It is now read at each of them (found on run mup3h70u).
|
|
37
|
+
- A throwing panel POST handler answers a 500 JSON error instead of killing the panel.
|
|
38
|
+
- Settings: a price row with exactly one of the two boxes filled saves nothing in that row and says "Fill both price boxes or clear both."
|
|
39
|
+
- A died run's in-flight part on the Run map reads `[?] died`, not passed.
|
|
40
|
+
- Panel status wording comes from the one status table: a step box reads `[sign] word` (passed / failed / capped / stopped / died), never "done"; step cards and Audit rows show the word only (the attempt glyph is their sign).
|
|
41
|
+
- The drafting progress line reads "drafting (retry N of M)" on a drafting call after the first.
|
|
42
|
+
- The CLI `run-author` non-repo refusal uses the same message as the panel (`nonRepoSourceMessage`, now in `src/source.js`).
|
|
43
|
+
- A `tree-changed` close detail on a single-file write scope names the file, not the glob path.
|
|
44
|
+
- The Providers table row's closing `</tr>` is no longer missing.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Refactors with unchanged output: `getRunDetailBody` takes its drafting shift from `draftingShift` (one owner); the dead `orphanCalls` accumulator in `readDraftSteps` is dropped.
|
|
49
|
+
- Tests: page-extraction pins follow the picker; the drafting part is pinned on a running panel-authored run; the resumed already-green worktree path is pinned (it held, no fix needed). Docs: PRD addenda pointer to v1.89; `bareloop.context.md` and docs sweeps.
|
|
50
|
+
|
|
7
51
|
|
|
8
52
|
## [0.36.0] - 2026-10-06
|
|
9
53
|
|
package/bareloop.context.md
CHANGED
|
@@ -1118,7 +1118,8 @@ prefix, F212: a readdir on a file used to read as an empty tree forever). `evalE
|
|
|
1118
1118
|
and never short-circuits — the result names EVERY failing wall (`{ pass, results }`,
|
|
1119
1119
|
each result `{ type, pass, detail?, fault? }`). `tree-changed` reads OUTCOME (bytes vs
|
|
1120
1120
|
the snapshot): an identical re-write is NOT a change (F43) and git status is never
|
|
1121
|
-
consulted (F45).
|
|
1121
|
+
consulted (F45). A single-file scope's pass detail names the file (`src/x.js changed`); a folder scope keeps
|
|
1122
|
+
`N file(s) changed under <scope>`. `artifact-written` rejects zero-byte files. `check-passes` delegates
|
|
1122
1123
|
through the `runCheck` seam (the runner wires runClose); an unwired or crashed seam
|
|
1123
1124
|
fails CLOSED with `fault` carrying a runClose verdict name — an instrument fault
|
|
1124
1125
|
escalates through `CLOSE_FAULTS`, never masquerades as worker feedback. Failing details are
|
|
@@ -3386,7 +3387,8 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3386
3387
|
GET/HEAD answers only when its `Host` header is the panel's own `127.0.0.1:<port>` (`checkHostGuard`,
|
|
3387
3388
|
`src/panel/authorroutes.js`; any other Host is `403`, no token needed to read). Every
|
|
3388
3389
|
P1 endpoint is GET/HEAD only (anything else — including every write verb — is `405`, outside the
|
|
3389
|
-
two human-click-guarded write families `/api/author/*` and `/api/settings/*` below
|
|
3390
|
+
two human-click-guarded write families `/api/author/*` and `/api/settings/*` below; a POST handler that throws answers `500
|
|
3391
|
+
{ok:false,error}` and the panel keeps serving); a URL
|
|
3390
3392
|
never joins a path segment into a filesystem read — a runid is looked up in the run list
|
|
3391
3393
|
first (`RUNID_RE`, `src/panel/server.js` — accepts a `~2`-style backfill-disambiguated
|
|
3392
3394
|
suffix too, and every endpoint echoes back the LISTED runid, never a filename-derived one
|
|
@@ -3501,13 +3503,21 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3501
3503
|
result glyphs are unchanged. The reuse button is described under Reuse workflow, below.
|
|
3502
3504
|
**Status words (click-through 2026-10-04)** → every run row, run detail and imported row carries `status`
|
|
3503
3505
|
`{key, sign, word}` from ONE table (`src/panel/status.js`): `[▶] running · [·] waiting · [✓] passed · [✗] failed ·
|
|
3504
|
-
[✗] capped · [
|
|
3506
|
+
[✗] capped · [■] stopped · [?] died`; `glyph` is that sign (the Runs `✗` filter chip also finds a `[■]` run). Mapping: green/already-green/satisfied → passed; no
|
|
3505
3507
|
job-end + runner alive → running, gone → died; cap-halt, wall-halt, and an escalation that is a money-halt or wall-halt
|
|
3506
|
-
→ capped; `stopped` → stopped; every other outcome (plan-red, check-red, step-red, close-red, provider-red,
|
|
3508
|
+
→ capped (ONE rule, `capKindOf` in `src/legs.js`, shared by the run status, a leg's stop reason and the part cards); `stopped` → stopped; every other outcome (plan-red, check-red, step-red, close-red, provider-red,
|
|
3507
3509
|
step-stalled, other escalations, refusals) → failed. The detail also carries `startedAt` and `endedAt` (the last
|
|
3508
3510
|
`job-end` timestamp, null while live or died). `GET /api/imports` rows and `GET /api/imports/:id` carry `runStatus`
|
|
3509
3511
|
(passed, or null when the bundle shows no green) and `runReason` (`goal met`). The Ended block's lines for died and
|
|
3510
3512
|
stopped runs are `no ending recorded` and `you pressed Stop` (each `— resume` when Resume is offered).
|
|
3513
|
+
Every PART of a run detail (`parts[]`) also carries its own `status` `{key, sign, word}` from the same table
|
|
3514
|
+
(`partStatusFor`, `src/panel/status.js`): drafting, a green outcome and a part with no verdict and no cut-off → passed; a red
|
|
3515
|
+
outcome → its run-outcome word; a part its leg's halt cut off → capped (money cap / time cap — also a leg that ended `escalated` on a money-halt or wall-halt, and the open step of a single-leg run that ended on a cap), stopped (you stopped it),
|
|
3516
|
+
died, else failed. A run that DIED (no `job-end`, runner gone) leaves its last part in flight: when that part has no outcome
|
|
3517
|
+
and no cut-off, the server marks it `died` (`[?] died`), never the "happened, so passed" default — replay alone cannot tell
|
|
3518
|
+
died from still running, so `getRunDetail` decides it from the same `death` verdict as the run's status. A live run's open
|
|
3519
|
+
part stays running; a part that finished before the death keeps its own status. The page's map box reads `[sign] word` (the live part reads `[▶] running`); the step cards and Audit rows
|
|
3520
|
+
show the word only; no surface says "done".
|
|
3511
3521
|
**Resume (item 2)** → `POST /api/runs/:runid/resume` (`src/panel/runroutes.js`), behind
|
|
3512
3522
|
`checkHumanGuard` (token + own address, like `/api/author/*`); body `{budgetUsd?, maxWallMin?}`
|
|
3513
3523
|
(blank = the signed caps). `404` unknown run; `409` when `resumePlanFor` says the engine would
|
|
@@ -3524,32 +3534,45 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3524
3534
|
the log tail is returned as `error` verbatim; otherwise `{ok:true, runid, specHash, capsChanged,
|
|
3525
3535
|
log}`. The run keeps its runid and its one list row: the resumed leg appends to the same spine (P5-R,
|
|
3526
3536
|
`leg-start` in the run list), it never appears as a new run. The page's Resume button opens the
|
|
3527
|
-
|
|
3537
|
+
Chat card in RESUME mode (title `RESUME <job> (run-N)`, filled from `GET /api/author/start-from`, every box locked
|
|
3538
|
+
except the two caps, "spent so far" shown, Clear/Start drafting hidden) and `Sign & resume` on that card is the human
|
|
3539
|
+
click, posting this same route; the run's Job tab is read-only on every run.
|
|
3528
3540
|
**Stop (item 5)** → `POST /api/runs/:runid/stop` (`src/panel/runroutes.js`, `checkHumanGuard`, no body).
|
|
3529
3541
|
It writes the run's STOP REQUEST, a file `<spine>.stop` (`stopFilePath(spine)`, `src/legs.js` — the one
|
|
3530
3542
|
spelling), and signals nothing. `404` unknown run; `409` when the run's LATEST leg is not live
|
|
3531
3543
|
(`runIsAlive`) or its spine does not exist yet (still starting). The ENGINE half is library code, so
|
|
3532
3544
|
CLI runs honour the file too (no new CLI command): `runJob`/`runPlan` take `stopFile`, and `run-u`/the
|
|
3533
|
-
panel's spawn pass `<spine>.stop`. The seam is the ROUND BOUNDARY of
|
|
3545
|
+
panel's spawn pass `<spine>.stop`. The seam is the ROUND BOUNDARY of EVERY worker (scout, plan/replan drafter, step,
|
|
3546
|
+
close-fix) — where the money cap binds
|
|
3534
3547
|
(`src/planrun.js`, the `metered` round callback): file present = delete it, emit `stop-requested
|
|
3535
|
-
{phase, step
|
|
3536
|
-
and the step loop files it as job-end outcome `stopped` the way a mid-step cap-halt is filed
|
|
3548
|
+
{phase, step?, iteration, round}` (`step` only on a step phase), end the Loop after that round; `ask` then throws category `stopped`
|
|
3549
|
+
and the step loop files it as job-end outcome `stopped` the way a mid-step cap-halt is filed (the scout, plan, replan and
|
|
3550
|
+
close-fix phases file it through `relay`, which returns `stopped` as itself, never `provider-red`)
|
|
3537
3551
|
(`spendComplete` exact — nothing is in flight at a round boundary). One owner, one check: a one-step run
|
|
3538
|
-
and a long step both stop within one turn. Resume re-enters that step. The request file is cleared three times: when honoured, when the
|
|
3552
|
+
and a long step both stop within one turn. Resume re-enters that step, or continues the phase. The request file is cleared three times: when honoured, when the
|
|
3539
3553
|
leg ends (`finally`), and at the start of the next leg (a click that raced a leg's end never stops a
|
|
3540
3554
|
resume). `stopped` is in `CHECKPOINT_OUTCOMES`: resumable like a `cap-halt`, the same run continues as a
|
|
3541
3555
|
new leg at the next step. `GET /api/runs/:runid` carries `live` and `stopping` (a stop request is on
|
|
3542
3556
|
disk and the leg is live); Ended row `stopped` = "You stopped it." / "Resume." / `[Resume]`, card line
|
|
3543
3557
|
`stopped — resume`, Audit divider `stopped: you stopped it · resumed <when>`. The page shows `[Stop]`
|
|
3544
3558
|
in the Run tab's action area while live, "stopping after this turn…" after the click, and the same
|
|
3545
|
-
`[Resume]` there once the engine would accept one.
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
**
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3559
|
+
`[Resume]` there once the engine would accept one. A Stop clicked while the run is in the
|
|
3560
|
+
scout, plan or close-fix phase is honoured at that phase's next round boundary, the same as in a step (it used to be read in
|
|
3561
|
+
step workers only). A stop in the close-fix loop reads "You stopped the run during the fix phase … a resume continues the
|
|
3562
|
+
run" (`ralph`'s `phase` option; the step loop keeps "a resume re-enters this step").
|
|
3563
|
+
**Edit in chat (2026-10-08)** — `ended.actions` carries `{id:'edit', label:'Edit in chat'}` on EVERY ending that is not a
|
|
3564
|
+
real green (failed, capped, stopped, died, every other red, and an already-green or delivery-refused already-green),
|
|
3565
|
+
after `resume` when both show. The button reads `GET /api/author/start-from` and opens the Chat card filled from the
|
|
3566
|
+
signed job with EVERY box open and the normal Start drafting path (no reuse state, no borrowed track record), under a
|
|
3567
|
+
code-owned origin line ("Copied from <job> (run-N) — <status word>. …").
|
|
3568
|
+
**Reuse workflow (replaces P5 item 3's Start from this, 2026-10-03)** — a button on runs that EARNED their green only
|
|
3569
|
+
(the Run tab's action row and the Ended block; `ended.actions` carries `{id:'reuse', label:'Reuse workflow'}` on `green`
|
|
3570
|
+
and green-with-destination-refused `green` rows, and on no other: an `already-green` run (the check passed before any
|
|
3571
|
+
work) offers Edit in chat instead, and a red row's next line is "Change the job: press Edit in chat."; stopped/capped/died
|
|
3572
|
+
rows offer `resume` and `edit`) and on every imported job; the Chat card's Check type is one radio group of three
|
|
3573
|
+
(Deterministic · Rubric · Reuse workflow), and the third opens a search picker over `GET /api/author/reuse-jobs` (below) that
|
|
3574
|
+
fills the card through this same path (a pick, then `change` returns to the empty search; Deterministic or Rubric leaves reuse).
|
|
3575
|
+
It opens the Chat tab's New job card
|
|
3553
3576
|
filled from the SIGNED job: a NEW run (new runid), never a rerun, nothing drafted ($0). **Only five boxes are
|
|
3554
3577
|
open — Source, Destination, Model, $ cap, Time cap**; every other box (check type, model, job name, goal, success,
|
|
3555
3578
|
guardrails, judge examples) is the signed workflow, greyed, and the SERVER refuses a start that changed one
|
|
@@ -3560,8 +3583,13 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3560
3583
|
green, notGreen, live, finished, avgSpendUsd, avgWallMs}, line}` (`404` unknown, `409` with an `error` when
|
|
3561
3584
|
there is nothing to copy: no log, the run's signed job is not on disk, or an imported job that changed, lost its
|
|
3562
3585
|
dependency or no longer matches its signed bytes); `POST /api/author/start` accepts `startFrom: <runid>` or
|
|
3563
|
-
`startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}
|
|
3564
|
-
`
|
|
3586
|
+
`startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}`; a local `startFrom: <runid>` whose
|
|
3587
|
+
latest outcome is not `green` (`start-from` carries that `outcome`) is refused `409 "only a run that finished green can
|
|
3588
|
+
be reused — this one did not"` — the server owns the rule, not just the pickers. The old
|
|
3589
|
+
`POST /api/author/start-from-check` and the "Changed — new job" mode are gone. `GET /api/author/reuse-jobs[?model=<Name>]` →
|
|
3590
|
+
`{ok, jobs:[{job, runid, checkType:'deterministic'|'rubric', line}]}` is the Chat card's Reuse workflow picker list: one entry per
|
|
3591
|
+
job (same `workflowKey`) with at least one real `green` local run (`already-green` is not a reuse source), `runid` = its newest green run (hand it to `start-from`), `line` =
|
|
3592
|
+
start-from's own estimate line, newest green first; imported jobs are not listed. **Identity, two keys:** the
|
|
3565
3593
|
signature hash `jobSpecHash` is unchanged (it covers the caps and the write fence — what the person signs);
|
|
3566
3594
|
`workflowKey(spec)` (`src/job.js`, not exported from the package root) is a sha256 over the signed spec WITHOUT
|
|
3567
3595
|
`source`, `destination`, `writeScope`, `budgetUsd` and `maxWallMs` (`REUSE_OPEN_SPEC_FIELDS`) AND without `provider`,
|
|
@@ -3587,7 +3615,8 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3587
3615
|
is still written beside `resolved-spec.json` when a session reaches `prepared`.
|
|
3588
3616
|
**The progress list (2026-10-04)** — `GET /api/author/:id` state carries `steps: [{id, label, status:
|
|
3589
3617
|
'running'|'done'|'failed', detail}]`, one entry per pipeline step in first-seen order, updated in place (ids/labels:
|
|
3590
|
-
`STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A
|
|
3618
|
+
`STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A drafting call after the first
|
|
3619
|
+
relabels the ONE drafting line `drafting (retry N of M)` (same line, never a second one). A refusal sets
|
|
3591
3620
|
`state.error` and fails exactly one step with the same text as its `detail`; pipeline text is never a chat message
|
|
3592
3621
|
(`messages` holds person/model turns only). Two last steps, `hash` ("generating hash", detail `spec hash <full hash>`, done at sign-prepare) and `signed` ("signed hash", pushed by `signRun` when the sign is accepted); the old "SIGNING PREPARED" / "signed — … run starting detached" thread bubbles are gone. The page draws each step's `detail` on its own `> ` line under the step. Destination follows the Source: for a repo source it is the write
|
|
3593
3622
|
fence (relative `writeScope` globs, never proven as a directory); for a folder source an absolute directory. Both
|
|
@@ -3595,7 +3624,7 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3595
3624
|
Destination on a repo source → `Destination must be a path inside the repo, like src/digest.js` (before the
|
|
3596
3625
|
copy); a source that is not a git repo → a plain refusal worded by the kind the source door recorded (a single
|
|
3597
3626
|
file: "Source must be the repo folder — put the file in Destination"; a plain folder or a URL: "not a code
|
|
3598
|
-
project (no git repo found)").
|
|
3627
|
+
project (no git repo found)") — `nonRepoSourceMessage` (`src/source.js`), the one spelling the CLI `run-author` uses too.
|
|
3599
3628
|
**Import, read only (item 4)** — `src/panel/importroutes.js`. The Workflows toolbar's `[Import]` opens a folder
|
|
3600
3629
|
browser plus a paste box; an imported job is an exported bundle folder the person can LOOK at: nothing runs,
|
|
3601
3630
|
nothing is signed, the folder is never written. Routes, all behind `checkHumanGuard` (token + own address —
|
|
@@ -3858,7 +3887,8 @@ URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display
|
|
|
3858
3887
|
(DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
|
|
3859
3888
|
`keys.<ENV NAME> = { name, shape, baseUrl, priceInPerM?, priceOutPerM? }`. The last two columns are the customer's own price
|
|
3860
3889
|
(**In $/1M**, **Out $/1M**, plain boxes; blank = not set, a number >= 0 otherwise; the server refuses a half-set pair with a plain
|
|
3861
|
-
error and saves nothing, and the page
|
|
3890
|
+
error and saves nothing, and the page saves NOTHING in a row with exactly one price box filled, showing "Fill both price boxes
|
|
3891
|
+
or clear both." when the person leaves that box, until both boxes are filled or both cleared).
|
|
3862
3892
|
**Open keys folder** (`POST /api/settings/open-keys-folder`) opens `~/.config/bareloop` in the file manager (`xdg-open` / `open`,
|
|
3863
3893
|
argv array, never the file itself); where it cannot, the page shows the path as text. Defaults for a key with no saved entry:
|
|
3864
3894
|
`ANTHROPIC_API_KEY` claude-sonnet-5 / Anthropic; `DEEPSEEK_API_KEY` deepseek-flash /
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bareloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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/authorrun.js
CHANGED
|
@@ -124,7 +124,7 @@ import { moveFile } from './movefile.js';
|
|
|
124
124
|
import { closeJudges, GATE_AUDIT_FILE } from './kinds.js';
|
|
125
125
|
import { resolveProvider, buildRunnerProviders, apiKeyProblem } from './providers.js';
|
|
126
126
|
import { commandFor } from './invoke.js';
|
|
127
|
-
import { readSourceManifest, missingDependencies } from './source.js';
|
|
127
|
+
import { readSourceManifest, missingDependencies, nonRepoSourceMessage } from './source.js';
|
|
128
128
|
import { tallyCalls } from './text.js';
|
|
129
129
|
import {
|
|
130
130
|
declarationLines, rubricLines, calibrationLines, parseCeiling, ceilingLine, crashRecord, phaseLine,
|
|
@@ -836,8 +836,7 @@ export async function main(argv, deps = {}) {
|
|
|
836
836
|
// confirm turn, no model call — a plain folder's spine is exactly
|
|
837
837
|
// `author-start` → `author-end{outcome:'not-authored', stop:'non-code-source'}`.
|
|
838
838
|
if (!IS_REPO_SOURCE) {
|
|
839
|
-
const message =
|
|
840
|
-
+ 'job yet. Nothing was spent. Your source was not changed.';
|
|
839
|
+
const message = nonRepoSourceMessage(manifestRead.manifest.kind);
|
|
841
840
|
out(`\n${message}`);
|
|
842
841
|
const red = {
|
|
843
842
|
code: 'request-red', path: 'source', verb: 'non-code-source', lib: 'bareloop',
|
package/src/draftspend.js
CHANGED
|
@@ -136,8 +136,6 @@ export function readDraftSteps(dir) {
|
|
|
136
136
|
try { text = readFileSync(join(dir, DRAFT_LOG_FILE), 'utf8'); } catch { return []; }
|
|
137
137
|
/** @type {Map<number, any>} */
|
|
138
138
|
const byNo = new Map();
|
|
139
|
-
/** @type {any[]} */
|
|
140
|
-
const orphanCalls = [];
|
|
141
139
|
for (const line of text.split('\n')) {
|
|
142
140
|
if (!line.trim()) continue;
|
|
143
141
|
let ev;
|
|
@@ -156,7 +154,7 @@ export function readDraftSteps(dir) {
|
|
|
156
154
|
x.endedAt = null;
|
|
157
155
|
} else if (ev.kind === 'call') {
|
|
158
156
|
const c = { label: String(ev.label), model: typeof ev.model === 'string' ? ev.model : null, costUsd: typeof ev.costUsd === 'number' ? ev.costUsd : null, unpricedRounds: Number(ev.unpricedRounds) || 0, at: String(ev.at) };
|
|
159
|
-
|
|
157
|
+
byNo.get(ev.no)?.calls.push(c);
|
|
160
158
|
}
|
|
161
159
|
}
|
|
162
160
|
const steps = [...byNo.values()].sort((a, b) => a.no - b.no);
|
package/src/exits.js
CHANGED
|
@@ -145,6 +145,9 @@ async function evalOne(e, { dir, snapshot, runCheck }) {
|
|
|
145
145
|
if (changed === 0) {
|
|
146
146
|
return { type: e.type, pass: false, detail: `0 files changed under ${e.scope} — the tree is byte-identical to the step start (an identical re-write is not a change)` };
|
|
147
147
|
}
|
|
148
|
+
// a file-valued writeScope (`src/x.js/**`) snapshots that one file keyed by the prefix itself: name the file,
|
|
149
|
+
// never the glob path that does not exist on disk
|
|
150
|
+
if (snapshot.has(prefix) || now.has(prefix)) return { type: e.type, pass: true, detail: `${prefix} changed` };
|
|
148
151
|
return { type: e.type, pass: true, detail: `${changed} file(s) changed under ${e.scope}` };
|
|
149
152
|
}
|
|
150
153
|
|
package/src/legs.js
CHANGED
|
@@ -169,3 +169,18 @@ export function watchdogNotePath(spineFile, leg) {
|
|
|
169
169
|
export function stopFilePath(spineFile) {
|
|
170
170
|
return `${spineFile}.stop`;
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Did a run (or one leg) END on a cap? The ONE spelling of the rule every surface follows (hamr 2026-10-08): the
|
|
175
|
+
* outcome is `cap-halt` / `wall-halt`, or it is `escalated` and the last escalation's category names the cap —
|
|
176
|
+
* `wall-halt`, or `cap-halt` backed by a `money-halt` record (a strike-ladder `cap-halt` is not the money cap).
|
|
177
|
+
* @param {{outcome: string|null|undefined, category?: string|null, moneyHalt?: boolean}} o
|
|
178
|
+
* @returns {'money'|'time'|null}
|
|
179
|
+
*/
|
|
180
|
+
export function capKindOf(o) {
|
|
181
|
+
if (o.outcome === 'cap-halt') return 'money';
|
|
182
|
+
if (o.outcome === 'wall-halt') return 'time';
|
|
183
|
+
if (o.outcome !== 'escalated') return null;
|
|
184
|
+
if (o.category === 'wall-halt') return 'time';
|
|
185
|
+
return o.category === 'cap-halt' && o.moneyHalt ? 'money' : null;
|
|
186
|
+
}
|
|
@@ -91,7 +91,7 @@ const TERMINAL_PHASES = new Set(['refused', 'abandoned', 'error', 'signed', 'sig
|
|
|
91
91
|
* @param {{ port: number, token: string, env?: Record<string,string|undefined>,
|
|
92
92
|
* sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
|
|
93
93
|
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string,
|
|
94
|
-
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any} }} opts
|
|
94
|
+
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any, listJobs?: (o?: {model?: string}) => any[]} }} opts
|
|
95
95
|
*/
|
|
96
96
|
export function createAuthorRoutes(opts) {
|
|
97
97
|
// the RAW env with the keys file re-merged on every use, so an edited file takes effect
|
|
@@ -217,6 +217,17 @@ export function createAuthorRoutes(opts) {
|
|
|
217
217
|
return true;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// The Reuse workflow picker's list: one entry per job with a green local run (newest green's runid + the SAME
|
|
221
|
+
// estimate line start-from returns). $0, read-only; `?model=` passes through exactly as start-from's does.
|
|
222
|
+
if (pathname === '/api/author/reuse-jobs') {
|
|
223
|
+
if (!opts.startFrom || !opts.startFrom.listJobs) { send(404, { ok: false, error: 'not found' }); return true; }
|
|
224
|
+
if (req.method !== 'GET') { send(405, { ok: false, error: 'GET only' }); return true; }
|
|
225
|
+
const sp = new URL(/** @type {string} */ (req.url), 'http://127.0.0.1').searchParams;
|
|
226
|
+
const asModel = sp.has('model') ? { model: sp.get('model') ?? '' } : {};
|
|
227
|
+
send(200, { ok: true, jobs: opts.startFrom.listJobs(asModel) });
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
220
231
|
// A refreshed page re-attaches to the one live (non-terminal) session: its id and state (card, messages, steps),
|
|
221
232
|
// or `null`. The server's session map is the only truth; nothing is stored page-side.
|
|
222
233
|
if (pathname === '/api/author/live') {
|
|
@@ -243,6 +254,13 @@ export function createAuthorRoutes(opts) {
|
|
|
243
254
|
const pre = opts.startFrom ? (fromImport !== null ? opts.startFrom.getImport(fromImport) : opts.startFrom.get(fromRunid)) : null;
|
|
244
255
|
if (pre === null || pre === undefined) { send(404, { ok: false, error: 'no such run to reuse' }); return true; }
|
|
245
256
|
if (!pre.ok) { send(409, pre); return true; }
|
|
257
|
+
// only a run that earned a real green is a reuse source (hamr 2026-10-08) — the pickers already filter, but a
|
|
258
|
+
// hand-made POST must not sign a reuse of an already-green, red or still-running run. Imports are green by ruling;
|
|
259
|
+
// Edit in chat and Resume only READ start-from (GET) and never post a startFrom, so this gate never touches them.
|
|
260
|
+
if (fromImport === null && pre.outcome !== 'green') {
|
|
261
|
+
send(409, { ok: false, error: 'only a run that finished green can be reused — this one did not' });
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
246
264
|
const locked = lockedFieldChanged(card, pre.card);
|
|
247
265
|
if (locked !== null) { send(400, { ok: false, error: `${locked} is locked on a reused workflow — Clear the card to change it` }); return true; }
|
|
248
266
|
const rv = validateReuseCard(card, { rows });
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from 'node:fs';
|
|
28
28
|
import { join, resolve, dirname } from 'node:path';
|
|
29
29
|
import {
|
|
30
|
-
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource, nearestGitAncestor,
|
|
30
|
+
prepareSource, proveDestination, missingDependencies, looksLikeRepoSource, nearestGitAncestor, nonRepoSourceMessage,
|
|
31
31
|
} from '../source.js';
|
|
32
32
|
import {
|
|
33
33
|
worktreePath, uncommittedCount, shortHead, removeWorktree,
|
|
@@ -258,17 +258,7 @@ export function validateReuseCard(card, opts = {}) {
|
|
|
258
258
|
/** A Destination written as an absolute path (POSIX or a Windows drive path). @param {string} destination @returns {boolean} */
|
|
259
259
|
const isAbsoluteDestination = (destination) => /^(\/|[a-zA-Z]:[\\/])/.test(destination);
|
|
260
260
|
|
|
261
|
-
|
|
262
|
-
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
263
|
-
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
264
|
-
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
265
|
-
* @returns {string}
|
|
266
|
-
*/
|
|
267
|
-
export function nonRepoSourceMessage(kind) {
|
|
268
|
-
if (kind === 'file') return 'Source is a single file. Source must be the repo folder — put the file in Destination, like src/digest.js. Nothing spent.';
|
|
269
|
-
if (kind === 'folder') return "Source is a plain folder, not a code project (no git repo found). bareloop can't check this kind of job yet. Nothing spent.";
|
|
270
|
-
return "Source is not a code project (no git repo found). bareloop can't check this kind of job yet. Nothing spent.";
|
|
271
|
-
}
|
|
261
|
+
export { nonRepoSourceMessage };
|
|
272
262
|
|
|
273
263
|
/**
|
|
274
264
|
* The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
|
|
@@ -491,6 +481,13 @@ export function createSession(card, deps = {}) {
|
|
|
491
481
|
// person needs to read; the step-by-step detail lives in one line.
|
|
492
482
|
// ONE owner of the drafting line's detail (model + cap): the drafting line is started here, by the real author phase
|
|
493
483
|
if (PHASE_STEP[name]) stepStart(PHASE_STEP[name], PHASE_STEP[name] === 'draft' ? `${card.model}, $${card.capUsd.toFixed(2)} cap` : undefined);
|
|
484
|
+
// a drafting call after the first (`author-call` i >= 1, of up to `of + 1`) is a RETRY: the line says so in plain words, so a
|
|
485
|
+
// long silence on call 2 of 3 does not read as a hang. The same one line, relabelled — never a second line.
|
|
486
|
+
if (name === 'author-call' && Number.isInteger(data.i) && data.i >= 1 && Number.isInteger(data.of)) {
|
|
487
|
+
const label = `${STEP_LABELS.draft} (retry ${data.i + 1} of ${data.of + 1})`;
|
|
488
|
+
stepLabel('draft', label);
|
|
489
|
+
state.progressLabel = label;
|
|
490
|
+
}
|
|
494
491
|
};
|
|
495
492
|
// ABANDON (hamr's ruling 2026-10-05): once abandoned the phase is frozen at 'abandoned' — whatever the in-flight
|
|
496
493
|
// run() does afterwards (a late refuse, a phase write) never overwrites it, so the one-at-a-time lock stays released.
|
|
@@ -644,10 +641,11 @@ export function createSession(card, deps = {}) {
|
|
|
644
641
|
// a detached checkout of the person's current commit, inside their
|
|
645
642
|
// repo), so a JS/TS repo's tree never carries `node_modules` until
|
|
646
643
|
// someone installs there. Refusing outright here was ITSELF the F182
|
|
647
|
-
// class of bug one layer up.
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
644
|
+
// class of bug one layer up. For an npm lock file bareloop
|
|
645
|
+
// runs `npm ci --ignore-scripts` itself first (P6 item 3, below); any other
|
|
646
|
+
// gap (or a failed install) waits on the person: this re-checks the SAME
|
|
647
|
+
// worktree on demand, via the "Check again" button (`checkDeps()`, below),
|
|
648
|
+
// instead of ending the session.
|
|
651
649
|
let depsGap = missingDependencies(prep.tree, prep.manifest.sourceSubdir ?? '');
|
|
652
650
|
// P6 item 3: a committed npm lock file -> bareloop runs `npm ci --ignore-scripts` itself, $0, before
|
|
653
651
|
// any model call. Any failure (or another lock file) falls through to the wait below, unchanged.
|