bareloop 0.36.0 → 0.37.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 +23 -0
- package/bareloop.context.md +34 -15
- 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/panel/authorroutes.js +12 -1
- package/src/panel/authorsession.js +9 -12
- package/src/panel/index.html +189 -42
- package/src/panel/server.js +48 -4
- package/src/panel/status.js +22 -0
- package/src/planrun.js +10 -7
- package/src/source.js +12 -0
- package/types/panel/authorroutes.d.ts +4 -1
- package/types/panel/authorsession.d.ts +2 -7
- package/types/panel/server.d.ts +17 -0
- package/types/panel/status.d.ts +20 -0
- package/types/planrun.d.ts +3 -3
- package/types/source.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.37.0] - 2026-10-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **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).
|
|
16
|
+
- A throwing panel POST handler answers a 500 JSON error instead of killing the panel.
|
|
17
|
+
- 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."
|
|
18
|
+
- A died run's in-flight part on the Run map reads `[?] died`, not passed.
|
|
19
|
+
- 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).
|
|
20
|
+
- The drafting progress line reads "drafting (retry N of M)" on a drafting call after the first.
|
|
21
|
+
- The CLI `run-author` non-repo refusal uses the same message as the panel (`nonRepoSourceMessage`, now in `src/source.js`).
|
|
22
|
+
- A `tree-changed` close detail on a single-file write scope names the file, not the glob path.
|
|
23
|
+
- The Providers table row's closing `</tr>` is no longer missing.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Refactors with unchanged output: `getRunDetailBody` takes its drafting shift from `draftingShift` (one owner); the dead `orphanCalls` accumulator in `readDraftSteps` is dropped.
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
7
30
|
|
|
8
31
|
## [0.36.0] - 2026-10-06
|
|
9
32
|
|
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
|
|
@@ -3508,6 +3510,14 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
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), 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
|
|
@@ -3530,26 +3540,30 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3530
3540
|
spelling), and signals nothing. `404` unknown run; `409` when the run's LATEST leg is not live
|
|
3531
3541
|
(`runIsAlive`) or its spine does not exist yet (still starting). The ENGINE half is library code, so
|
|
3532
3542
|
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
|
|
3543
|
+
panel's spawn pass `<spine>.stop`. The seam is the ROUND BOUNDARY of EVERY worker (scout, plan/replan drafter, step,
|
|
3544
|
+
close-fix) — where the money cap binds
|
|
3534
3545
|
(`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
|
|
3546
|
+
{phase, step?, iteration, round}` (`step` only on a step phase), end the Loop after that round; `ask` then throws category `stopped`
|
|
3547
|
+
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
|
|
3548
|
+
close-fix phases file it through `relay`, which returns `stopped` as itself, never `provider-red`)
|
|
3537
3549
|
(`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
|
|
3550
|
+
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
3551
|
leg ends (`finally`), and at the start of the next leg (a click that raced a leg's end never stops a
|
|
3540
3552
|
resume). `stopped` is in `CHECKPOINT_OUTCOMES`: resumable like a `cap-halt`, the same run continues as a
|
|
3541
3553
|
new leg at the next step. `GET /api/runs/:runid` carries `live` and `stopping` (a stop request is on
|
|
3542
3554
|
disk and the leg is live); Ended row `stopped` = "You stopped it." / "Resume." / `[Resume]`, card line
|
|
3543
3555
|
`stopped — resume`, Audit divider `stopped: you stopped it · resumed <when>`. The page shows `[Stop]`
|
|
3544
3556
|
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
|
-
the leg ends — the money and time caps still bind throughout.
|
|
3557
|
+
`[Resume]` there once the engine would accept one. A Stop clicked while the run is in the
|
|
3558
|
+
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
|
|
3559
|
+
step workers only).
|
|
3549
3560
|
**Reuse workflow (replaces P5 item 3's Start from this, 2026-10-03)** — a button on GREEN runs only (the Run tab's
|
|
3550
3561
|
action row and the Ended block; `ended.actions` carries `{id:'reuse', label:'Reuse workflow'}` on green and
|
|
3551
3562
|
green-with-destination-refused rows, and on no other: a red row's next line is "Change the job: Clear the card and draft a new one.", and
|
|
3552
|
-
stopped/capped/died rows offer `resume` only) and on every imported job
|
|
3563
|
+
stopped/capped/died rows offer `resume` only) and on every imported job; the Chat card's Check type is one radio group of three
|
|
3564
|
+
(Deterministic · Rubric · Reuse workflow), and the third opens a search picker over `GET /api/author/reuse-jobs` (below) that
|
|
3565
|
+
fills the card through this same path (a pick, then `change` returns to the empty search; Deterministic or Rubric leaves reuse).
|
|
3566
|
+
It opens the Chat tab's New job card
|
|
3553
3567
|
filled from the SIGNED job: a NEW run (new runid), never a rerun, nothing drafted ($0). **Only five boxes are
|
|
3554
3568
|
open — Source, Destination, Model, $ cap, Time cap**; every other box (check type, model, job name, goal, success,
|
|
3555
3569
|
guardrails, judge examples) is the signed workflow, greyed, and the SERVER refuses a start that changed one
|
|
@@ -3561,7 +3575,10 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3561
3575
|
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
3576
|
dependency or no longer matches its signed bytes); `POST /api/author/start` accepts `startFrom: <runid>` or
|
|
3563
3577
|
`startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}`. The old
|
|
3564
|
-
`POST /api/author/start-from-check` and the "Changed — new job" mode are gone.
|
|
3578
|
+
`POST /api/author/start-from-check` and the "Changed — new job" mode are gone. `GET /api/author/reuse-jobs[?model=<Name>]` →
|
|
3579
|
+
`{ok, jobs:[{job, runid, checkType:'deterministic'|'rubric', line}]}` is the Chat card's Reuse workflow picker list: one entry per
|
|
3580
|
+
job (same `workflowKey`) with at least one GREEN local run, `runid` = its newest green run (hand it to `start-from`), `line` =
|
|
3581
|
+
start-from's own estimate line, newest green first; imported jobs are not listed. **Identity, two keys:** the
|
|
3565
3582
|
signature hash `jobSpecHash` is unchanged (it covers the caps and the write fence — what the person signs);
|
|
3566
3583
|
`workflowKey(spec)` (`src/job.js`, not exported from the package root) is a sha256 over the signed spec WITHOUT
|
|
3567
3584
|
`source`, `destination`, `writeScope`, `budgetUsd` and `maxWallMs` (`REUSE_OPEN_SPEC_FIELDS`) AND without `provider`,
|
|
@@ -3587,7 +3604,8 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3587
3604
|
is still written beside `resolved-spec.json` when a session reaches `prepared`.
|
|
3588
3605
|
**The progress list (2026-10-04)** — `GET /api/author/:id` state carries `steps: [{id, label, status:
|
|
3589
3606
|
'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
|
|
3607
|
+
`STEP_LABELS`, `src/panel/authorsession.js`); `progressLabel` is the running step's label. A drafting call after the first
|
|
3608
|
+
relabels the ONE drafting line `drafting (retry N of M)` (same line, never a second one). A refusal sets
|
|
3591
3609
|
`state.error` and fails exactly one step with the same text as its `detail`; pipeline text is never a chat message
|
|
3592
3610
|
(`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
3611
|
fence (relative `writeScope` globs, never proven as a directory); for a folder source an absolute directory. Both
|
|
@@ -3595,7 +3613,7 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3595
3613
|
Destination on a repo source → `Destination must be a path inside the repo, like src/digest.js` (before the
|
|
3596
3614
|
copy); a source that is not a git repo → a plain refusal worded by the kind the source door recorded (a single
|
|
3597
3615
|
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)").
|
|
3616
|
+
project (no git repo found)") — `nonRepoSourceMessage` (`src/source.js`), the one spelling the CLI `run-author` uses too.
|
|
3599
3617
|
**Import, read only (item 4)** — `src/panel/importroutes.js`. The Workflows toolbar's `[Import]` opens a folder
|
|
3600
3618
|
browser plus a paste box; an imported job is an exported bundle folder the person can LOOK at: nothing runs,
|
|
3601
3619
|
nothing is signed, the folder is never written. Routes, all behind `checkHumanGuard` (token + own address —
|
|
@@ -3858,7 +3876,8 @@ URL** (blank = the shape's own host), **Test**, **Tokens used** (total, display
|
|
|
3858
3876
|
(DeepSeek fetched, Anthropic a typed note). Saves on change to `config.json`
|
|
3859
3877
|
`keys.<ENV NAME> = { name, shape, baseUrl, priceInPerM?, priceOutPerM? }`. The last two columns are the customer's own price
|
|
3860
3878
|
(**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
|
|
3879
|
+
error and saves nothing, and the page saves NOTHING in a row with exactly one price box filled, showing "Fill both price boxes
|
|
3880
|
+
or clear both." when the person leaves that box, until both boxes are filled or both cleared).
|
|
3862
3881
|
**Open keys folder** (`POST /api/settings/open-keys-folder`) opens `~/.config/bareloop` in the file manager (`xdg-open` / `open`,
|
|
3863
3882
|
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
3883
|
`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.37.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
|
|
|
@@ -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') {
|
|
@@ -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.
|
package/src/panel/index.html
CHANGED
|
@@ -272,10 +272,21 @@
|
|
|
272
272
|
.import-view .imp-row{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
|
|
273
273
|
.import-view .imp-label{color:var(--text-faint);font-size:11px;letter-spacing:0.08em;min-width:84px;}
|
|
274
274
|
.import-view .imp-text{flex:1 1 220px;min-width:0;overflow-wrap:anywhere;}
|
|
275
|
-
.startfrom-line{margin:
|
|
275
|
+
.startfrom-line{margin:4px 0 0;font-size:11px;color:var(--text-dim);overflow-wrap:anywhere;}
|
|
276
276
|
.job-card .locked{opacity:.55;cursor:not-allowed;}
|
|
277
|
-
|
|
278
|
-
.
|
|
277
|
+
/* Reuse workflow picker (hamr 2026-10-06): the third Check type radio; search box + list, then the picked box */
|
|
278
|
+
.radio-row label{white-space:nowrap;}
|
|
279
|
+
.rw-box{position:relative;margin-top:6px;}
|
|
280
|
+
.rw-box input{width:100%;box-sizing:border-box;}
|
|
281
|
+
.rw-list{list-style:none;margin:0;padding:0;border:1px solid var(--border-strong);border-top:none;background:var(--bg);max-height:220px;overflow:auto;}
|
|
282
|
+
.rw-list li{padding:7px 8px;border-bottom:1px dotted var(--border);cursor:pointer;}
|
|
283
|
+
.rw-list li:hover,.rw-list li.hl{background:var(--panel2);}
|
|
284
|
+
.rw-name{font-weight:700;overflow-wrap:anywhere;} .rw-name mark{background:none;color:var(--accent);text-decoration:underline;}
|
|
285
|
+
.rw-meta{font-size:11px;color:var(--text-dim);margin-top:2px;overflow-wrap:anywhere;}
|
|
286
|
+
.rw-empty{padding:7px 8px;font-size:12px;color:var(--text-dim);}
|
|
287
|
+
.rw-picked{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:6px;padding:6px 8px;border:1px solid var(--accent);}
|
|
288
|
+
.rw-picked span{overflow-wrap:anywhere;}
|
|
289
|
+
.rw-picked a{font-size:12px;cursor:pointer;flex:none;}
|
|
279
290
|
.run-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:12px;}
|
|
280
291
|
.run-actions[hidden]{display:none;}
|
|
281
292
|
.run-actions .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
|
|
@@ -519,10 +530,8 @@
|
|
|
519
530
|
<!-- hamr 2026-10-05: the empty card is ALWAYS there when Chat opens (no + New). The top-right button reads
|
|
520
531
|
"Abandon" while a session is live and unsigned, else "Clear"; both leave an empty card. -->
|
|
521
532
|
<div class="job-card compact" id="job-card" data-testid="job-card">
|
|
522
|
-
<div class="startfrom-origin" id="chat-startfrom-origin" data-testid="startfrom-origin" hidden></div>
|
|
523
533
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
524
534
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
525
|
-
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
526
535
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
527
536
|
|
|
528
537
|
<div class="field">
|
|
@@ -530,6 +539,15 @@
|
|
|
530
539
|
<div class="radio-row" role="radiogroup" aria-labelledby="jf-verdict-label">
|
|
531
540
|
<label><input type="radio" name="jf-verdict" value="deterministic" checked> Deterministic</label>
|
|
532
541
|
<label><input type="radio" name="jf-verdict" value="rubric"> Rubric</label>
|
|
542
|
+
<label><input type="radio" name="jf-verdict" value="reuse" data-testid="jf-verdict-reuse"> Reuse workflow</label>
|
|
543
|
+
</div>
|
|
544
|
+
<div id="rw" data-testid="rw" hidden>
|
|
545
|
+
<div class="rw-box" id="rw-search" data-testid="rw-search">
|
|
546
|
+
<input id="rw-q" data-testid="rw-q" type="text" placeholder="type a job name…" autocomplete="off">
|
|
547
|
+
<ul class="rw-list" id="rw-list" data-testid="rw-list"></ul>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="rw-picked" id="rw-picked" data-testid="rw-picked" hidden><span id="rw-picked-txt" data-testid="rw-picked-txt"></span><a id="rw-change" data-testid="rw-change" role="button" tabindex="0">change</a></div>
|
|
550
|
+
<div class="startfrom-line" id="chat-startfrom-line" data-testid="startfrom-line" hidden></div>
|
|
533
551
|
</div>
|
|
534
552
|
</div>
|
|
535
553
|
<div class="field">
|
|
@@ -955,6 +973,7 @@
|
|
|
955
973
|
if(part.outcome === "green" || part.outcome === "satisfied" || part.outcome === "already-green") return "done";
|
|
956
974
|
if(typeof part.outcome === "string") return "stopped";
|
|
957
975
|
// P5-R: its leg's halt cut this part off — the map's existing "stopped" state, never "done"
|
|
976
|
+
if(part.status && part.status.key === "died") return "died";
|
|
958
977
|
if(part.stopReason) return "stopped";
|
|
959
978
|
if(isLastLivePart) return "running";
|
|
960
979
|
return "done"; // scout/plan/replan/judge: no per-part verdict, but the part itself already happened
|
|
@@ -993,6 +1012,8 @@
|
|
|
993
1012
|
partIndex: idx,
|
|
994
1013
|
title: part.label + occSuffix,
|
|
995
1014
|
state: partBoxState(part, isLastLivePart),
|
|
1015
|
+
// the word this box wears: the server's table entry for the part (src/panel/status.js partStatusFor), except the live part, which is running
|
|
1016
|
+
status: isLastLivePart && partBoxState(part, true) === "running" ? {sign:"\u25B6", word:"running"} : (part.status || null),
|
|
996
1017
|
noNumber: part.kind !== "step",
|
|
997
1018
|
// display-only attempts (map inline text + card/audit result glyph
|
|
998
1019
|
// fallback) — [] for a no-verdict part kind even though `part.attempts`
|
|
@@ -1007,6 +1028,14 @@
|
|
|
1007
1028
|
};
|
|
1008
1029
|
});
|
|
1009
1030
|
}
|
|
1031
|
+
// ONE spelling of a part's shown state: `[sign] word` from the server's table; the raw state only when no table entry came
|
|
1032
|
+
function boxStatusWord(box){
|
|
1033
|
+
return box.status && box.status.word ? box.status.word : String(box.state);
|
|
1034
|
+
}
|
|
1035
|
+
// the map box adds the sign; step cards and Audit rows show the word only (an attempt glyph already sits beside them)
|
|
1036
|
+
function boxStatusText(box){
|
|
1037
|
+
return box.status && box.status.word ? "[" + box.status.sign + "] " + boxStatusWord(box) : boxStatusWord(box);
|
|
1038
|
+
}
|
|
1010
1039
|
function naturalBoxWidth(steps){
|
|
1011
1040
|
var numIdx = stepNumberIndices(steps);
|
|
1012
1041
|
var maxChars = 0;
|
|
@@ -1107,7 +1136,7 @@
|
|
|
1107
1136
|
'" text-anchor="middle" font-size="12" fill="var(--text)">' + escapeXml(lines[1]) + '</text>';
|
|
1108
1137
|
}
|
|
1109
1138
|
body += '<text x="' + (x + boxW / 2) + '" y="' + stateY +
|
|
1110
|
-
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(s
|
|
1139
|
+
'" text-anchor="middle" font-size="10" fill="' + colors.textFill + '">' + escapeXml(boxStatusText(s)) + '</text>';
|
|
1111
1140
|
// P2 (2026-09-27 ruling): a running box gets the mockup's pulsing
|
|
1112
1141
|
// amber dot (design/panel-mockup.html ~line 1648) — same fixed color
|
|
1113
1142
|
// and animation, never re-derived from the state's own stroke color.
|
|
@@ -2360,8 +2389,9 @@
|
|
|
2360
2389
|
var stopAsked = {};
|
|
2361
2390
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
2362
2391
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2363
|
-
|
|
2364
|
-
|
|
2392
|
+
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2393
|
+
function reuseWorkflow(runid, model){
|
|
2394
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&model=" + encodeURIComponent(model) : "")).then(function(r){
|
|
2365
2395
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2366
2396
|
document.getElementById("tab-chat").click();
|
|
2367
2397
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
@@ -2650,7 +2680,7 @@
|
|
|
2650
2680
|
// ONE spelling of a step's state: its dot + the bold state word — read by the Run tab's step cards AND the Audit tab's grouped rows
|
|
2651
2681
|
function stepStateHTML(box, testid){
|
|
2652
2682
|
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2653
|
-
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(box
|
|
2683
|
+
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2654
2684
|
}
|
|
2655
2685
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
2656
2686
|
// lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
|
|
@@ -3569,7 +3599,7 @@
|
|
|
3569
3599
|
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
3570
3600
|
+ "<td>" + bal + "</td>"
|
|
3571
3601
|
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-in" data-testid="pv-price-in-' + e + '" placeholder="not set" value="' + (row.priceInPerM === null ? "" : escapeXml(String(row.priceInPerM))) + '"></td>'
|
|
3572
|
-
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td>';
|
|
3602
|
+
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td></tr>';
|
|
3573
3603
|
}).join("") : '<tr><td colspan="9" class="hint" data-testid="pv-empty">no keys with a value yet — add NAME=key to the keys file, then Reload keys</td></tr>';
|
|
3574
3604
|
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
3575
3605
|
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
@@ -3584,15 +3614,15 @@
|
|
|
3584
3614
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3585
3615
|
}).catch(function(){ pvNote.textContent = "could not read providers"; });
|
|
3586
3616
|
}
|
|
3587
|
-
// The row's POST body
|
|
3588
|
-
//
|
|
3589
|
-
//
|
|
3617
|
+
// The row's POST body, or null when exactly ONE price box is filled: a price is both boxes or neither (the server
|
|
3618
|
+
// refuses a half-set one), so nothing in that row saves. This runs only when a save would happen (the row's
|
|
3619
|
+
// change event), so typing the first box never flashes the message; it shows when the person leaves that box.
|
|
3620
|
+
var PV_HALF_PRICE = "Fill both price boxes or clear both.";
|
|
3590
3621
|
function pvRowBody(env, v){
|
|
3591
3622
|
var pin = String(v.priceIn).trim();
|
|
3592
3623
|
var pout = String(v.priceOut).trim();
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
return body;
|
|
3624
|
+
if((pin === "") !== (pout === "")) return null;
|
|
3625
|
+
return { envName: env, name: v.name, shape: v.shape, baseUrl: v.baseUrl, priceInPerM: pin, priceOutPerM: pout };
|
|
3596
3626
|
}
|
|
3597
3627
|
pvRows.addEventListener("change", function(e){
|
|
3598
3628
|
var t = e.target;
|
|
@@ -3606,13 +3636,15 @@
|
|
|
3606
3636
|
}
|
|
3607
3637
|
if(!(t.classList.contains("pv-name") || t.classList.contains("pv-shape") || t.classList.contains("pv-url") || t.classList.contains("pv-price-in") || t.classList.contains("pv-price-out"))) return;
|
|
3608
3638
|
var env = tr.getAttribute("data-env");
|
|
3609
|
-
|
|
3639
|
+
var rowBody = pvRowBody(env, {
|
|
3610
3640
|
name: tr.querySelector(".pv-name").value,
|
|
3611
3641
|
shape: tr.querySelector(".pv-shape").value,
|
|
3612
3642
|
baseUrl: tr.querySelector(".pv-url").value,
|
|
3613
3643
|
priceIn: tr.querySelector(".pv-price-in").value,
|
|
3614
3644
|
priceOut: tr.querySelector(".pv-price-out").value,
|
|
3615
|
-
})
|
|
3645
|
+
});
|
|
3646
|
+
if(!rowBody){ pvNote.textContent = PV_HALF_PRICE; return; }
|
|
3647
|
+
authorPost("/api/settings/providers/row", rowBody).then(function(r){
|
|
3616
3648
|
pvNote.textContent = r.body && r.body.ok ? ("Saved — " + env + ".") : ((r.body && r.body.error) || "could not save");
|
|
3617
3649
|
// no re-render: it would steal focus mid-edit; Chat's Model menu just re-reads
|
|
3618
3650
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
@@ -3692,9 +3724,11 @@
|
|
|
3692
3724
|
// Reuse workflow — set when the card was opened by [Reuse workflow]: the run (or imported job) it came from.
|
|
3693
3725
|
// Only Source, Destination and the two caps are open on that card; the SERVER refuses any other change.
|
|
3694
3726
|
var startFrom = null;
|
|
3727
|
+
// the picked job's OWN check type: with the Check type radio on "Reuse workflow" the card's checkType comes from here, never from the radio
|
|
3728
|
+
var reuseCheckType = "deterministic";
|
|
3729
|
+
var reusePickedName = "";
|
|
3695
3730
|
var reuseSession = false;
|
|
3696
3731
|
var autoSigned = false;
|
|
3697
|
-
var sfOrigin = document.getElementById("chat-startfrom-origin");
|
|
3698
3732
|
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3699
3733
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3700
3734
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
@@ -3709,7 +3743,8 @@
|
|
|
3709
3743
|
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3710
3744
|
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3711
3745
|
document.getElementById("jf-model").disabled = live;
|
|
3712
|
-
|
|
3746
|
+
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); only a live session freezes them
|
|
3747
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live; });
|
|
3713
3748
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3714
3749
|
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3715
3750
|
}
|
|
@@ -3720,11 +3755,105 @@
|
|
|
3720
3755
|
["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"].forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3721
3756
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3722
3757
|
}
|
|
3723
|
-
startFrom = null; reuseSession = false; autoSigned = false;
|
|
3724
|
-
|
|
3758
|
+
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
3759
|
+
sfLine.hidden = true; sfNote.hidden = true;
|
|
3725
3760
|
setReuseLocked(false);
|
|
3761
|
+
renderPicker();
|
|
3726
3762
|
renderMain();
|
|
3727
3763
|
}
|
|
3764
|
+
// ---- Reuse workflow picker (hamr 2026-10-06): Check type is ONE radio group of three; "Reuse workflow" shows a search
|
|
3765
|
+
// box over the jobs that have a green local run (GET /api/author/reuse-jobs), a pick fills the card through the SAME
|
|
3766
|
+
// path the Run tab's button uses (reuseWorkflow -> bareloop-reuse -> applyReuse), then a picked box with `change`.
|
|
3767
|
+
var CARD_BOX_IDS = ["jf-name", "jf-goal", "jf-source", "jf-dest", "jf-success", "jf-guardrails", "jf-judge", "jf-cap-money", "jf-cap-time"];
|
|
3768
|
+
var rwBox = document.getElementById("rw");
|
|
3769
|
+
var rwSearch = document.getElementById("rw-search");
|
|
3770
|
+
var rwQ = document.getElementById("rw-q");
|
|
3771
|
+
var rwList = document.getElementById("rw-list");
|
|
3772
|
+
var rwPicked = document.getElementById("rw-picked");
|
|
3773
|
+
var rwPickedTxt = document.getElementById("rw-picked-txt");
|
|
3774
|
+
var rwJobs = null;
|
|
3775
|
+
var rwFailed = false;
|
|
3776
|
+
var rwShown = [];
|
|
3777
|
+
var rwHl = 0;
|
|
3778
|
+
function verdictValue(){
|
|
3779
|
+
var ck = document.querySelector('input[name="jf-verdict"]:checked');
|
|
3780
|
+
return ck ? ck.value : "deterministic";
|
|
3781
|
+
}
|
|
3782
|
+
function setVerdict(v){
|
|
3783
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === v); });
|
|
3784
|
+
}
|
|
3785
|
+
function clearCardBoxes(){
|
|
3786
|
+
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3787
|
+
document.getElementById("jf-cap-note").textContent = "";
|
|
3788
|
+
}
|
|
3789
|
+
// ONE function shows/hides the picker: visible only while the radio is on Reuse workflow; the search box before a
|
|
3790
|
+
// pick, the picked box (+ the estimate line under it) after. A re-attached reuse session shows as picked too.
|
|
3791
|
+
function renderPicker(){
|
|
3792
|
+
var on = verdictValue() === "reuse";
|
|
3793
|
+
var picked = on && (startFrom !== null || reuseSession);
|
|
3794
|
+
rwBox.hidden = !on;
|
|
3795
|
+
rwSearch.hidden = picked;
|
|
3796
|
+
rwPicked.hidden = !picked;
|
|
3797
|
+
rwPickedTxt.innerHTML = picked ? "<b>" + escapeXml(reusePickedName) + "</b>" : "";
|
|
3798
|
+
if(!picked) sfLine.hidden = true;
|
|
3799
|
+
}
|
|
3800
|
+
function rwMark(name, q){
|
|
3801
|
+
var i = q ? name.toLowerCase().indexOf(q.toLowerCase()) : -1;
|
|
3802
|
+
if(i < 0) return escapeXml(name);
|
|
3803
|
+
return escapeXml(name.slice(0, i)) + "<mark>" + escapeXml(name.slice(i, i + q.length)) + "</mark>" + escapeXml(name.slice(i + q.length));
|
|
3804
|
+
}
|
|
3805
|
+
// the server's estimate line, prefixed with the job's check type (the line itself does not carry it)
|
|
3806
|
+
function rwMeta(j){ return j.checkType + " · " + String(j.line || "").replace(/^Same job — /, ""); }
|
|
3807
|
+
function renderReuseList(){
|
|
3808
|
+
var q = rwQ.value.trim();
|
|
3809
|
+
var all = rwJobs || [];
|
|
3810
|
+
rwShown = all.filter(function(j){ return j.job.toLowerCase().indexOf(q.toLowerCase()) !== -1; });
|
|
3811
|
+
if(rwHl >= rwShown.length) rwHl = 0;
|
|
3812
|
+
if(rwShown.length){
|
|
3813
|
+
rwList.innerHTML = rwShown.map(function(j, i){
|
|
3814
|
+
return '<li data-i="' + i + '" data-testid="rw-item" class="' + (i === rwHl ? "hl" : "") + '"><div class="rw-name">' + rwMark(j.job, q) + '</div><div class="rw-meta">' + escapeXml(rwMeta(j)) + "</div></li>";
|
|
3815
|
+
}).join("");
|
|
3816
|
+
} else {
|
|
3817
|
+
rwList.innerHTML = '<div class="rw-empty" data-testid="rw-empty">' + (rwFailed ? "Could not load the job list."
|
|
3818
|
+
: (all.length === 0 ? "No green jobs yet." : 'No green job matches "' + escapeXml(q) + '".')) + "</div>";
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
function loadReuseJobs(){
|
|
3822
|
+
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?model=" + encodeURIComponent(modelSelect.value) : "")).then(function(r){
|
|
3823
|
+
rwFailed = !(r && r.ok);
|
|
3824
|
+
rwJobs = rwFailed ? null : r.jobs;
|
|
3825
|
+
renderReuseList();
|
|
3826
|
+
}).catch(function(){ rwFailed = true; rwJobs = null; renderReuseList(); });
|
|
3827
|
+
}
|
|
3828
|
+
// the empty search state: card emptied, nothing picked, Sign & run disabled until a job is picked
|
|
3829
|
+
function openPicker(){
|
|
3830
|
+
clearCardBoxes();
|
|
3831
|
+
rwQ.value = ""; rwHl = 0;
|
|
3832
|
+
document.getElementById("jf-judge").disabled = true;
|
|
3833
|
+
renderPicker();
|
|
3834
|
+
renderReuseList();
|
|
3835
|
+
loadReuseJobs();
|
|
3836
|
+
refreshStartEnabled();
|
|
3837
|
+
rwQ.focus();
|
|
3838
|
+
}
|
|
3839
|
+
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, modelSelect.value); }
|
|
3840
|
+
rwQ.addEventListener("input", function(){ rwHl = 0; renderReuseList(); });
|
|
3841
|
+
rwQ.addEventListener("keydown", function(e){
|
|
3842
|
+
if(e.key === "ArrowDown"){ rwHl = Math.min(rwHl + 1, rwShown.length - 1); renderReuseList(); e.preventDefault(); }
|
|
3843
|
+
else if(e.key === "ArrowUp"){ rwHl = Math.max(rwHl - 1, 0); renderReuseList(); e.preventDefault(); }
|
|
3844
|
+
else if(e.key === "Enter" && rwShown[rwHl]){ e.preventDefault(); pickReuseJob(rwShown[rwHl]); }
|
|
3845
|
+
});
|
|
3846
|
+
rwList.addEventListener("click", function(e){
|
|
3847
|
+
var li = e.target.closest ? e.target.closest("li") : null;
|
|
3848
|
+
if(li) pickReuseJob(rwShown[Number(li.getAttribute("data-i"))]);
|
|
3849
|
+
});
|
|
3850
|
+
// `change` = back to the empty search state; the radio stays on Reuse workflow
|
|
3851
|
+
function unpickReuse(){
|
|
3852
|
+
clearStartFrom();
|
|
3853
|
+
setVerdict("reuse");
|
|
3854
|
+
openPicker();
|
|
3855
|
+
}
|
|
3856
|
+
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3728
3857
|
function openNewCard(){
|
|
3729
3858
|
card.hidden = false;
|
|
3730
3859
|
thread.innerHTML = "";
|
|
@@ -3755,12 +3884,12 @@
|
|
|
3755
3884
|
// back to a fresh empty card: every box, check type deterministic, the default (first) Model, origin/estimate/notes
|
|
3756
3885
|
// hidden, progress list and thread cleared, reuse lock lifted
|
|
3757
3886
|
function resetCard(){
|
|
3758
|
-
|
|
3759
|
-
|
|
3887
|
+
clearCardBoxes();
|
|
3888
|
+
setVerdict("deterministic");
|
|
3760
3889
|
document.getElementById("jf-judge").disabled = true;
|
|
3761
3890
|
modelSelect.selectedIndex = 0;
|
|
3762
3891
|
msgInput.value = "";
|
|
3763
|
-
|
|
3892
|
+
rwQ.value = "";
|
|
3764
3893
|
lastPhase = null;
|
|
3765
3894
|
clearStartFrom();
|
|
3766
3895
|
openNewCard();
|
|
@@ -3782,7 +3911,8 @@
|
|
|
3782
3911
|
var moneyCap = Number(document.getElementById("jf-cap-money").value);
|
|
3783
3912
|
var timeCapMin = document.getElementById("jf-cap-time").value.trim();
|
|
3784
3913
|
return {
|
|
3785
|
-
|
|
3914
|
+
// on "Reuse workflow" the type is the picked job's own, never the radio's
|
|
3915
|
+
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3786
3916
|
model: document.getElementById("jf-model").value,
|
|
3787
3917
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3788
3918
|
goal: document.getElementById("jf-goal").value.trim(),
|
|
@@ -3806,7 +3936,8 @@
|
|
|
3806
3936
|
function refreshStartEnabled(){
|
|
3807
3937
|
var v = Number(moneyCapInput.value);
|
|
3808
3938
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
3809
|
-
|
|
3939
|
+
// on Reuse workflow nothing starts before a job is picked
|
|
3940
|
+
startOk = capOk && !sessionLive && keyOk && (verdictValue() !== "reuse" || startFrom !== null);
|
|
3810
3941
|
syncCardLock();
|
|
3811
3942
|
renderMain();
|
|
3812
3943
|
refreshClearBtn();
|
|
@@ -3829,11 +3960,21 @@
|
|
|
3829
3960
|
});
|
|
3830
3961
|
// the limit changed in Settings: re-read the note for whatever cap is typed
|
|
3831
3962
|
document.addEventListener("bareloop-limit-changed", refreshCapNote);
|
|
3963
|
+
// Check type: Deterministic / Rubric / Reuse workflow. Reuse opens the picker on an empty card; Deterministic or
|
|
3964
|
+
// Rubric while the picker is showing (picked or not) LEAVES reuse — the empty card on that type (clearStartFrom).
|
|
3965
|
+
function onVerdictChange(v){
|
|
3966
|
+
if(v === "reuse"){ if(rwBox.hidden) openPicker(); return; }
|
|
3967
|
+
if(!rwBox.hidden){
|
|
3968
|
+
clearStartFrom();
|
|
3969
|
+
clearCardBoxes();
|
|
3970
|
+
setVerdict(v);
|
|
3971
|
+
renderPicker();
|
|
3972
|
+
}
|
|
3973
|
+
document.getElementById("jf-judge").disabled = v !== "rubric";
|
|
3974
|
+
refreshStartEnabled();
|
|
3975
|
+
}
|
|
3832
3976
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
3833
|
-
r.addEventListener("change", function(){
|
|
3834
|
-
var isRubric = document.querySelector('input[name="jf-verdict"]:checked').value === "rubric";
|
|
3835
|
-
document.getElementById("jf-judge").disabled = !isRubric;
|
|
3836
|
-
});
|
|
3977
|
+
r.addEventListener("change", function(){ onVerdictChange(r.value); });
|
|
3837
3978
|
});
|
|
3838
3979
|
|
|
3839
3980
|
// build item 7 / build item 5 (2026-09-28) — key + reachability, $0,
|
|
@@ -3906,10 +4047,11 @@
|
|
|
3906
4047
|
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3907
4048
|
"&model=" + encodeURIComponent(modelSelect.value);
|
|
3908
4049
|
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3909
|
-
if(startFrom && r && r.ok){ sfLine.textContent = r.line
|
|
4050
|
+
if(startFrom && r && r.ok){ sfLine.textContent = r.line ? rwMeta({checkType: reuseCheckType, line: r.line}) : ""; }
|
|
3910
4051
|
}).catch(function(){ /* the line keeps what it had */ });
|
|
3911
4052
|
}
|
|
3912
4053
|
modelSelect.addEventListener("change", refreshReuseLine);
|
|
4054
|
+
modelSelect.addEventListener("change", function(){ if(!rwBox.hidden && startFrom === null && !reuseSession) loadReuseJobs(); });
|
|
3913
4055
|
// P4b — the Model menu IS the Settings rows: every Name, in keys-file order (a blank Name
|
|
3914
4056
|
// is never offered). Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
3915
4057
|
function loadModels(){
|
|
@@ -4028,7 +4170,7 @@
|
|
|
4028
4170
|
var open = askBoxOpenFor(st);
|
|
4029
4171
|
msgInput.disabled = !open;
|
|
4030
4172
|
if(!open) msgInput.value = "";
|
|
4031
|
-
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom);
|
|
4173
|
+
var b = mainButtonFor(st, !msgInput.value.trim(), !!startFrom || verdictValue() === "reuse");
|
|
4032
4174
|
mainBtn.textContent = b.text;
|
|
4033
4175
|
mainBtn.disabled = b.disabled;
|
|
4034
4176
|
mainAction = b.action;
|
|
@@ -4148,7 +4290,9 @@
|
|
|
4148
4290
|
reuseSession = st.reuse === true;
|
|
4149
4291
|
autoSigned = false;
|
|
4150
4292
|
signClickedOnce = reuseSession;
|
|
4151
|
-
|
|
4293
|
+
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4294
|
+
reusePickedName = c.jobName || "job";
|
|
4295
|
+
setVerdict(reuseSession ? "reuse" : reuseCheckType);
|
|
4152
4296
|
if(c.model){
|
|
4153
4297
|
if(!Array.prototype.some.call(modelSelect.options, function(o){ return o.value === c.model; })){
|
|
4154
4298
|
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
@@ -4161,6 +4305,7 @@
|
|
|
4161
4305
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4162
4306
|
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4163
4307
|
if(reuseSession) setReuseLocked(true);
|
|
4308
|
+
renderPicker();
|
|
4164
4309
|
fitCardBoxes();
|
|
4165
4310
|
renderMessages(st);
|
|
4166
4311
|
renderActions(st);
|
|
@@ -4180,15 +4325,17 @@
|
|
|
4180
4325
|
|
|
4181
4326
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4182
4327
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4183
|
-
|
|
4184
|
-
|
|
4328
|
+
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4329
|
+
// `bareloop-reuse` event), so the card ends up in the same picked state whichever door was used.
|
|
4330
|
+
function applyReuse(d){
|
|
4185
4331
|
var pre = d.prefill || {};
|
|
4186
4332
|
var c = pre.card || {};
|
|
4187
4333
|
clearStartFrom();
|
|
4188
4334
|
openNewCard();
|
|
4189
4335
|
startFrom = d.importId ? {importId: d.importId} : {runid: d.runid};
|
|
4190
|
-
|
|
4191
|
-
|
|
4336
|
+
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4337
|
+
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4338
|
+
setVerdict("reuse");
|
|
4192
4339
|
if(c.model){ modelSelect.value = c.model; }
|
|
4193
4340
|
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4194
4341
|
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
@@ -4196,10 +4343,9 @@
|
|
|
4196
4343
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4197
4344
|
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4198
4345
|
setReuseLocked(true);
|
|
4199
|
-
|
|
4200
|
-
sfOrigin.textContent = "Reuse: " + ((pre.origin && pre.origin.job) || "job") + (d.runid ? " (run " + d.runid + ")" : " (imported)");
|
|
4346
|
+
renderPicker();
|
|
4201
4347
|
sfLine.hidden = false;
|
|
4202
|
-
sfLine.textContent = pre.line
|
|
4348
|
+
sfLine.textContent = pre.line ? rwMeta({checkType: reuseCheckType, line: pre.line}) : "";
|
|
4203
4349
|
sfLine.className = "startfrom-line";
|
|
4204
4350
|
sfNote.hidden = !pre.note;
|
|
4205
4351
|
sfNote.textContent = pre.note || "";
|
|
@@ -4207,7 +4353,8 @@
|
|
|
4207
4353
|
refreshReuseLine();
|
|
4208
4354
|
refreshCapNote();
|
|
4209
4355
|
refreshStartEnabled();
|
|
4210
|
-
}
|
|
4356
|
+
}
|
|
4357
|
+
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4211
4358
|
|
|
4212
4359
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
4213
4360
|
// (fed by the every-2s `poll()`) unconditionally overwrites
|
package/src/panel/server.js
CHANGED
|
@@ -53,7 +53,7 @@ import { rowsForHome, findRow, modelChoiceFor } from '../providerrows.js';
|
|
|
53
53
|
import { resolveProvider } from '../providers.js';
|
|
54
54
|
import { REUSE_LOCKED_FIELDS, REUSE_OPEN_FIELDS } from './authorsession.js';
|
|
55
55
|
import { readResume, checkpointAgeGate, CHECKPOINT_OUTCOMES } from '../reuse.js';
|
|
56
|
-
import { statusFor, GOAL_MET_LINE } from './status.js';
|
|
56
|
+
import { statusFor, partStatusFor, GOAL_MET_LINE } from './status.js';
|
|
57
57
|
import { createSettingsRoutes } from './settingsroutes.js';
|
|
58
58
|
|
|
59
59
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -897,7 +897,7 @@ function getRunDetailBody(runid, opts) {
|
|
|
897
897
|
const timelineKind = summary.timelineKind;
|
|
898
898
|
// the drafting part (see draftingFor) heads the parts list; the enrichment below touches the spine's own parts only
|
|
899
899
|
const draftingPartHere = draftingFor(row, summary, opts);
|
|
900
|
-
const draftingShiftHere =
|
|
900
|
+
const draftingShiftHere = draftingShift(row, summary, opts);
|
|
901
901
|
const enrichedParts = enrichPartsWithStageKind(summary.parts, stageKindMetaFromSpec(resolveSpecForRow(row)));
|
|
902
902
|
const death = deriveDeath(row, rawSpineRecords, summary.outcome);
|
|
903
903
|
const { ended, resume, status } = endedForRow(row, rawSpineRecords, summary, death, true);
|
|
@@ -1117,7 +1117,9 @@ function getRunDetailBody(runid, opts) {
|
|
|
1117
1117
|
// {@link enrichPartsWithStageKind}. `kindMeta` is `null` (every stage
|
|
1118
1118
|
// passes through unchanged) whenever no spec resolves at all, so the two
|
|
1119
1119
|
// tabs still never disagree about order/counts/blocked-call figures.
|
|
1120
|
-
|
|
1120
|
+
// a run that DIED (no job-end, runner gone — `death`, decided once above) leaves its last part in flight: that part reads
|
|
1121
|
+
// `died`, never the "happened, so passed" default. Replay alone cannot tell died from still running, so the verdict lives here.
|
|
1122
|
+
parts: (draftingPartHere && Array.isArray(enrichedParts) ? [draftingPartHere, ...enrichedParts] : enrichedParts)?.map((p, i, all) => ({ ...p, status: partStatusFor(death.died && i === all.length - 1 && p.kind !== 'drafting' && p.outcome == null && !p.stopReason ? { ...p, stopReason: 'died' } : p) })),
|
|
1121
1123
|
replans: summary.replans,
|
|
1122
1124
|
close: summary.close,
|
|
1123
1125
|
branch: summary.branch,
|
|
@@ -2459,6 +2461,39 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
2459
2461
|
};
|
|
2460
2462
|
}
|
|
2461
2463
|
|
|
2464
|
+
/**
|
|
2465
|
+
* The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
|
|
2466
|
+
* that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
|
|
2467
|
+
* it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
|
|
2468
|
+
* spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
|
|
2469
|
+
* @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
|
|
2470
|
+
* @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
|
|
2471
|
+
*/
|
|
2472
|
+
export function listReuseJobs(opts = {}) {
|
|
2473
|
+
const { rows } = readRunList(opts);
|
|
2474
|
+
const seen = new Set();
|
|
2475
|
+
/** @type {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]} */
|
|
2476
|
+
const jobs = [];
|
|
2477
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
2478
|
+
const row = rows[i];
|
|
2479
|
+
if (!row || !existsSync(row.spine)) continue;
|
|
2480
|
+
let records;
|
|
2481
|
+
try { records = parseJsonl(row.spine).records; } catch { continue; }
|
|
2482
|
+
const outcome = legsOf(records).at(-1)?.outcome ?? null;
|
|
2483
|
+
// the same endings the Run tab's own Reuse button is offered on (`endedBase`)
|
|
2484
|
+
if (outcome !== 'green' && outcome !== 'already-green' && outcome !== 'satisfied') continue;
|
|
2485
|
+
const signed = signedSpecForRun(row, records);
|
|
2486
|
+
if (!signed) continue;
|
|
2487
|
+
const key = workflowKey(signed.spec);
|
|
2488
|
+
if (seen.has(key)) continue;
|
|
2489
|
+
const pre = getStartFrom(row.runid, opts);
|
|
2490
|
+
if (!pre || !pre.ok) continue;
|
|
2491
|
+
seen.add(key);
|
|
2492
|
+
jobs.push({ job: pre.card.jobName, runid: row.runid, checkType: pre.card.checkType === 'rubric' ? 'rubric' : 'deterministic', line: pre.line });
|
|
2493
|
+
}
|
|
2494
|
+
return jobs;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2462
2497
|
/**
|
|
2463
2498
|
* The reuse card for a signed spec: locked boxes from the spec through the Job tab's own readers, open boxes from
|
|
2464
2499
|
* the spec's own fence and caps (Source and the saved text from the caller — a spec carries neither).
|
|
@@ -2636,7 +2671,15 @@ export function handleRequest(req, res, opts) {
|
|
|
2636
2671
|
/** @type {any} */
|
|
2637
2672
|
let body = null;
|
|
2638
2673
|
if (raw.length > 0) { try { body = JSON.parse(raw); } catch { body = null; } }
|
|
2639
|
-
|
|
2674
|
+
// this callback runs OUTSIDE createServer's try/catch (which covers only the synchronous GET path), so a
|
|
2675
|
+
// synchronous throw in a route handler (a full disk, a permission fault) would be an uncaught exception that
|
|
2676
|
+
// takes the whole panel down: answer 500 JSON instead and keep serving
|
|
2677
|
+
try {
|
|
2678
|
+
routes.handle(req, res, pathname, body);
|
|
2679
|
+
} catch (e) {
|
|
2680
|
+
if (res.headersSent) { res.end(); return; }
|
|
2681
|
+
sendJson(res, 500, { ok: false, error: `internal error: ${/** @type {Error} */ (e)?.message ?? String(e)}` });
|
|
2682
|
+
}
|
|
2640
2683
|
});
|
|
2641
2684
|
return;
|
|
2642
2685
|
}
|
|
@@ -2801,6 +2844,7 @@ export function createPanelServer(opts = {}) {
|
|
|
2801
2844
|
startFrom: {
|
|
2802
2845
|
get: (runid, o = {}) => getStartFrom(runid, { home, ...o }),
|
|
2803
2846
|
getImport: (id, o = {}) => getStartFromImport(id, { home, ...o }),
|
|
2847
|
+
listJobs: (o = {}) => listReuseJobs({ home, ...o }),
|
|
2804
2848
|
},
|
|
2805
2849
|
});
|
|
2806
2850
|
settingsRoutes = createSettingsRoutes({
|
package/src/panel/status.js
CHANGED
|
@@ -49,3 +49,25 @@ export function statusFor(o) {
|
|
|
49
49
|
else if (out === 'escalated' && ((o.category === 'cap-halt' && o.moneyHalt) || o.category === 'wall-halt')) key = 'capped';
|
|
50
50
|
return { key, ...STATUS[key] };
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The word one PART of a run wears on the step map, the step cards and the Audit rows (hamr 2026-10-07: `[✓] passed` /
|
|
55
|
+
* `[✗] failed`, never "done"). Reads the same table; `running` is the page's call (it knows which part is live), so a
|
|
56
|
+
* part with no verdict and no cut-off is `passed` here.
|
|
57
|
+
* drafting, green/satisfied/already-green outcome, or a part that simply happened (scout/plan/replan/judge) -> passed
|
|
58
|
+
* a red outcome -> its run-outcome word (failed; a cap/stop outcome keeps capped/stopped)
|
|
59
|
+
* a part its leg's halt cut off (`stopReason`): 'money cap' / 'time cap' -> capped, 'you stopped it' -> stopped,
|
|
60
|
+
* 'died' -> died, anything else (provider failed, step stalled, a raw outcome) -> failed
|
|
61
|
+
* @param {{kind?: string, outcome?: string|null, stopReason?: string|null}} part
|
|
62
|
+
* @returns {{key: StatusKey, sign: string, word: string}}
|
|
63
|
+
*/
|
|
64
|
+
export function partStatusFor(part) {
|
|
65
|
+
if (part.kind === 'drafting') return statusFor({ outcome: 'green' });
|
|
66
|
+
if (typeof part.outcome === 'string') return statusFor({ outcome: part.outcome });
|
|
67
|
+
const why = part.stopReason;
|
|
68
|
+
if (why) {
|
|
69
|
+
const key = why === 'money cap' || why === 'time cap' ? 'capped' : why === 'you stopped it' ? 'stopped' : why === 'died' ? 'died' : 'failed';
|
|
70
|
+
return { key, ...STATUS[key] };
|
|
71
|
+
}
|
|
72
|
+
return statusFor({ outcome: 'green' });
|
|
73
|
+
}
|
package/src/planrun.js
CHANGED
|
@@ -970,10 +970,10 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
970
970
|
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
971
971
|
* in v1" instead of naming a flag that would fail if typed.
|
|
972
972
|
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
973
|
-
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of
|
|
974
|
-
* (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
973
|
+
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of EVERY worker
|
|
974
|
+
* (scout, plan/replan drafter, step, close-fix: the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
975
975
|
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
976
|
-
* the way a mid-step cap-halt is; resume re-enters that step). Nothing is in flight at that
|
|
976
|
+
* the way a mid-step cap-halt is; resume re-enters that step, or continues the phase). Nothing is in flight at that
|
|
977
977
|
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
978
978
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
979
979
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
@@ -2745,11 +2745,12 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2745
2745
|
// (nothing in flight, the round just metered). It is the ONE owner of the stop request. The file is
|
|
2746
2746
|
// consumed here; `self.stop()` ends the Loop after this round exactly as a bound does; `ask`/`askFrom`
|
|
2747
2747
|
// then throw category `stopped`, which the step loop files as the checkpoint outcome `stopped` the way a
|
|
2748
|
-
// mid-step cap-halt is filed.
|
|
2749
|
-
|
|
2748
|
+
// mid-step cap-halt is filed. EVERY phase that runs rounds reads it here (scout, plan/replan drafter, step,
|
|
2749
|
+
// close-fix) — the one seam the money cap binds at; `relay` files the non-ralph phases' throw as `stopped`.
|
|
2750
|
+
if (stopFile !== null && !stopHit && existsSync(stopFile)) {
|
|
2750
2751
|
try { unlinkSync(stopFile); } catch { /* already consumed */ }
|
|
2751
2752
|
stopHit = true;
|
|
2752
|
-
emit('stop-requested', { phase, step: phase.slice('step:'.length), iteration: roundIteration, round: roundsThisAttempt, meaning: 'the person asked to stop; the leg ends after this round and a resume
|
|
2753
|
+
emit('stop-requested', { phase, ...(phase.startsWith('step:') ? { step: phase.slice('step:'.length) } : {}), iteration: roundIteration, round: roundsThisAttempt, meaning: 'the person asked to stop; the leg ends after this round and a resume continues the run' });
|
|
2753
2754
|
self.stop();
|
|
2754
2755
|
} else if (clock.expired()) {
|
|
2755
2756
|
attemptBounded = { iteration: roundIteration, cause: 'wall', reason: null };
|
|
@@ -2903,6 +2904,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2903
2904
|
// instead and every such fault came back `provider-red` — a broken runner
|
|
2904
2905
|
// reported as the provider's fault, and an upstream ask aimed at a library
|
|
2905
2906
|
// for our own missing wiring (the step-stalled lesson).
|
|
2907
|
+
// P5 item 5 — the PERSON's stop, honoured at a round boundary outside a step (scout, plan drafting, replan): nothing failed
|
|
2908
|
+
'stopped': [`You stopped the run during ${phase}. Nothing is discarded: the work on disk stands, and a resume continues the run.`, ['resume (the same run continues)', 'abandon the task']],
|
|
2906
2909
|
'interpreter-red': [`The ${phase} phase could not be set up — the runner or a primitive it was handed is not correctly bound.`, ['fix the wiring the detail names', 'retry the run', 'abandon the run']],
|
|
2907
2910
|
};
|
|
2908
2911
|
const [decision, options] = (Object.hasOwn(DECIDE, category) ? DECIDE[category] : undefined)
|
|
@@ -2914,7 +2917,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2914
2917
|
// `step-stalled` rides out as ITSELF, not as provider-red. Naming the
|
|
2915
2918
|
// escalation while returning a casualty label would launder a governance stop
|
|
2916
2919
|
// into transport noise at exactly the layer the readout reads.
|
|
2917
|
-
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red'
|
|
2920
|
+
return category === 'cap-halt' || category === 'wall-halt' || category === 'step-stalled' || category === 'interpreter-red' || category === 'stopped'
|
|
2918
2921
|
? category : 'provider-red';
|
|
2919
2922
|
};
|
|
2920
2923
|
|
package/src/source.js
CHANGED
|
@@ -1148,3 +1148,15 @@ export function frontDoorFromManifest(read) {
|
|
|
1148
1148
|
if (typeof destination !== 'string' || !destination) return null;
|
|
1149
1149
|
return { destination };
|
|
1150
1150
|
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
1154
|
+
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
1155
|
+
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
1156
|
+
* @returns {string}
|
|
1157
|
+
*/
|
|
1158
|
+
export function nonRepoSourceMessage(kind) {
|
|
1159
|
+
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.';
|
|
1160
|
+
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.";
|
|
1161
|
+
return "Source is not a code project (no git repo found). bareloop can't check this kind of job yet. Nothing spent.";
|
|
1162
|
+
}
|
|
@@ -41,7 +41,7 @@ export function checkHumanGuard(req: import("node:http").IncomingMessage, { toke
|
|
|
41
41
|
* @param {{ port: number, token: string, env?: Record<string,string|undefined>,
|
|
42
42
|
* sessionsRoot?: string, spawnFn?: typeof realSpawn, bareloopBin?: string,
|
|
43
43
|
* jobsDir?: string, fetchImpl?: typeof fetch, home?: string,
|
|
44
|
-
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any} }} opts
|
|
44
|
+
* startFrom?: {get: (runid: string, o?: {model?: string}) => any, getImport: (id: string, o?: {model?: string}) => any, listJobs?: (o?: {model?: string}) => any[]} }} opts
|
|
45
45
|
*/
|
|
46
46
|
export function createAuthorRoutes(opts: {
|
|
47
47
|
port: number;
|
|
@@ -60,6 +60,9 @@ export function createAuthorRoutes(opts: {
|
|
|
60
60
|
getImport: (id: string, o?: {
|
|
61
61
|
model?: string;
|
|
62
62
|
}) => any;
|
|
63
|
+
listJobs?: (o?: {
|
|
64
|
+
model?: string;
|
|
65
|
+
}) => any[];
|
|
63
66
|
};
|
|
64
67
|
}): {
|
|
65
68
|
handle: (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, pathname: string, body: any) => boolean;
|
|
@@ -67,13 +67,6 @@ export function validateReuseCard(card: any, opts?: {
|
|
|
67
67
|
ok: false;
|
|
68
68
|
error: string;
|
|
69
69
|
};
|
|
70
|
-
/**
|
|
71
|
-
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
72
|
-
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
73
|
-
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
export function nonRepoSourceMessage(kind: string): string;
|
|
77
70
|
/**
|
|
78
71
|
* The ONE Destination rule for a REPO source (fresh card and Reuse card alike): there Destination is the write fence,
|
|
79
72
|
* relative to the repo, so an absolute path is refused — at $0, before any copy or model call. A FOLDER source (or a
|
|
@@ -153,9 +146,11 @@ export const CARD_FIELDS: readonly string[];
|
|
|
153
146
|
export const REUSE_OPEN_FIELDS: readonly string[];
|
|
154
147
|
/** every card field that is NOT open on a reuse card, in card order */
|
|
155
148
|
export const REUSE_LOCKED_FIELDS: readonly string[];
|
|
149
|
+
export { nonRepoSourceMessage };
|
|
156
150
|
export type StepLine = {
|
|
157
151
|
id: string;
|
|
158
152
|
label: string;
|
|
159
153
|
status: "running" | "done" | "failed";
|
|
160
154
|
detail: string;
|
|
161
155
|
};
|
|
156
|
+
import { nonRepoSourceMessage } from '../source.js';
|
package/types/panel/server.d.ts
CHANGED
|
@@ -410,6 +410,23 @@ export function getStartFrom(runid: string, opts?: {
|
|
|
410
410
|
ok: false;
|
|
411
411
|
error: string;
|
|
412
412
|
} | null;
|
|
413
|
+
/**
|
|
414
|
+
* The Reuse workflow picker's list: one entry per JOB (same-job identity = `workflowKey`, the one `getStartFrom` uses)
|
|
415
|
+
* that has at least one GREEN local run, newest green first. `runid` = that job's newest green run, so the page hands
|
|
416
|
+
* it to the existing start-from flow unchanged; `line` is `getStartFrom`'s own estimate line (never a second
|
|
417
|
+
* spelling). Local runs only — an imported job keeps its Reuse button in the Import view. $0, read-only.
|
|
418
|
+
* @param {{ home?: string, model?: string }} [opts] `model` passes through to the estimate line, as start-from's does
|
|
419
|
+
* @returns {{job: string, runid: string, checkType: 'deterministic'|'rubric', line: string}[]}
|
|
420
|
+
*/
|
|
421
|
+
export function listReuseJobs(opts?: {
|
|
422
|
+
home?: string;
|
|
423
|
+
model?: string;
|
|
424
|
+
}): {
|
|
425
|
+
job: string;
|
|
426
|
+
runid: string;
|
|
427
|
+
checkType: "deterministic" | "rubric";
|
|
428
|
+
line: string;
|
|
429
|
+
}[];
|
|
413
430
|
/**
|
|
414
431
|
* Reuse workflow, for an IMPORTED job: the bundle is read AGAIN now and must be clean (`readBundle` — its own hash
|
|
415
432
|
* covers every close script, so a swapped script is `bundle-tampered` here), must resolve its own `bareloop`
|
package/types/panel/status.d.ts
CHANGED
|
@@ -14,6 +14,26 @@ export function statusFor(o: {
|
|
|
14
14
|
sign: string;
|
|
15
15
|
word: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* The word one PART of a run wears on the step map, the step cards and the Audit rows (hamr 2026-10-07: `[✓] passed` /
|
|
19
|
+
* `[✗] failed`, never "done"). Reads the same table; `running` is the page's call (it knows which part is live), so a
|
|
20
|
+
* part with no verdict and no cut-off is `passed` here.
|
|
21
|
+
* drafting, green/satisfied/already-green outcome, or a part that simply happened (scout/plan/replan/judge) -> passed
|
|
22
|
+
* a red outcome -> its run-outcome word (failed; a cap/stop outcome keeps capped/stopped)
|
|
23
|
+
* a part its leg's halt cut off (`stopReason`): 'money cap' / 'time cap' -> capped, 'you stopped it' -> stopped,
|
|
24
|
+
* 'died' -> died, anything else (provider failed, step stalled, a raw outcome) -> failed
|
|
25
|
+
* @param {{kind?: string, outcome?: string|null, stopReason?: string|null}} part
|
|
26
|
+
* @returns {{key: StatusKey, sign: string, word: string}}
|
|
27
|
+
*/
|
|
28
|
+
export function partStatusFor(part: {
|
|
29
|
+
kind?: string;
|
|
30
|
+
outcome?: string | null;
|
|
31
|
+
stopReason?: string | null;
|
|
32
|
+
}): {
|
|
33
|
+
key: StatusKey;
|
|
34
|
+
sign: string;
|
|
35
|
+
word: string;
|
|
36
|
+
};
|
|
17
37
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
18
38
|
export const GOAL_MET_LINE: "goal met";
|
|
19
39
|
/** @typedef {'running'|'waiting'|'passed'|'failed'|'capped'|'stopped'|'died'} StatusKey */
|
package/types/planrun.d.ts
CHANGED
|
@@ -397,10 +397,10 @@ export function planPrompt(job: any, scoutBlob: string, reds: any[] | null, maxS
|
|
|
397
397
|
* the resume-plan-red option, the fix-loop terminal) say "resume is `run-u`-only
|
|
398
398
|
* in v1" instead of naming a flag that would fail if typed.
|
|
399
399
|
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
400
|
-
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of
|
|
401
|
-
* (the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
400
|
+
* (`stopFilePath(spine)`, src/legs.js). Read at ONE seam, the round boundary of EVERY worker
|
|
401
|
+
* (scout, plan/replan drafter, step, close-fix: the `metered` callback, where the money cap binds): present = emit `stop-requested`, consume
|
|
402
402
|
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
403
|
-
* the way a mid-step cap-halt is; resume re-enters that step). Nothing is in flight at that
|
|
403
|
+
* the way a mid-step cap-halt is; resume re-enters that step, or continues the phase). Nothing is in flight at that
|
|
404
404
|
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
405
405
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
406
406
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
package/types/source.d.ts
CHANGED
|
@@ -297,6 +297,13 @@ export function frontDoorFromManifest(read: {
|
|
|
297
297
|
}): {
|
|
298
298
|
destination: string;
|
|
299
299
|
} | null;
|
|
300
|
+
/**
|
|
301
|
+
* The $0 refusal for a non-repo source, worded by the kind the source door recorded (`prep.manifest.kind`) — a single
|
|
302
|
+
* file is not a "folder", and Source must be the repo folder with the file named in Destination (hamr 2026-10-05).
|
|
303
|
+
* @param {string} kind `prep.manifest.kind`: 'file' | 'folder' | 'url' (a 'repo' never reaches this)
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
export function nonRepoSourceMessage(kind: string): string;
|
|
300
307
|
/**
|
|
301
308
|
* a named, non-throwing stop
|
|
302
309
|
*/
|