bareloop 0.37.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 +21 -0
- package/bareloop.context.md +22 -11
- package/package.json +1 -1
- package/src/legs.js +15 -0
- package/src/panel/authorroutes.js +7 -0
- package/src/panel/authorsession.js +5 -4
- package/src/panel/index.html +198 -113
- package/src/panel/server.js +33 -13
- package/src/panel/status.js +5 -4
- package/src/planrun.js +1 -1
- package/src/ralph.js +8 -2
- package/src/replay.js +23 -3
- package/types/legs.d.ts +12 -0
- package/types/panel/server.d.ts +5 -4
- package/types/panel/status.d.ts +1 -1
- package/types/ralph.d.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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
|
+
|
|
7
28
|
## [0.37.0] - 2026-10-07
|
|
8
29
|
|
|
9
30
|
### Added
|
package/bareloop.context.md
CHANGED
|
@@ -3503,16 +3503,16 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3503
3503
|
result glyphs are unchanged. The reuse button is described under Reuse workflow, below.
|
|
3504
3504
|
**Status words (click-through 2026-10-04)** → every run row, run detail and imported row carries `status`
|
|
3505
3505
|
`{key, sign, word}` from ONE table (`src/panel/status.js`): `[▶] running · [·] waiting · [✓] passed · [✗] failed ·
|
|
3506
|
-
[✗] capped · [
|
|
3506
|
+
[✗] capped · [■] stopped · [?] died`; `glyph` is that sign (the Runs `✗` filter chip also finds a `[■]` run). Mapping: green/already-green/satisfied → passed; no
|
|
3507
3507
|
job-end + runner alive → running, gone → died; cap-halt, wall-halt, and an escalation that is a money-halt or wall-halt
|
|
3508
|
-
→ 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,
|
|
3509
3509
|
step-stalled, other escalations, refusals) → failed. The detail also carries `startedAt` and `endedAt` (the last
|
|
3510
3510
|
`job-end` timestamp, null while live or died). `GET /api/imports` rows and `GET /api/imports/:id` carry `runStatus`
|
|
3511
3511
|
(passed, or null when the bundle shows no green) and `runReason` (`goal met`). The Ended block's lines for died and
|
|
3512
3512
|
stopped runs are `no ending recorded` and `you pressed Stop` (each `— resume` when Resume is offered).
|
|
3513
3513
|
Every PART of a run detail (`parts[]`) also carries its own `status` `{key, sign, word}` from the same table
|
|
3514
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),
|
|
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
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
3517
|
and no cut-off, the server marks it `died` (`[?] died`), never the "happened, so passed" default — replay alone cannot tell
|
|
3518
3518
|
died from still running, so `getRunDetail` decides it from the same `death` verdict as the run's status. A live run's open
|
|
@@ -3534,7 +3534,9 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3534
3534
|
the log tail is returned as `error` verbatim; otherwise `{ok:true, runid, specHash, capsChanged,
|
|
3535
3535
|
log}`. The run keeps its runid and its one list row: the resumed leg appends to the same spine (P5-R,
|
|
3536
3536
|
`leg-start` in the run list), it never appears as a new run. The page's Resume button opens the
|
|
3537
|
-
|
|
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.
|
|
3538
3540
|
**Stop (item 5)** → `POST /api/runs/:runid/stop` (`src/panel/runroutes.js`, `checkHumanGuard`, no body).
|
|
3539
3541
|
It writes the run's STOP REQUEST, a file `<spine>.stop` (`stopFilePath(spine)`, `src/legs.js` — the one
|
|
3540
3542
|
spelling), and signals nothing. `404` unknown run; `409` when the run's LATEST leg is not live
|
|
@@ -3556,11 +3558,18 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3556
3558
|
in the Run tab's action area while live, "stopping after this turn…" after the click, and the same
|
|
3557
3559
|
`[Resume]` there once the engine would accept one. A Stop clicked while the run is in the
|
|
3558
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
|
|
3559
|
-
step workers only).
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
green
|
|
3563
|
-
|
|
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
|
|
3564
3573
|
(Deterministic · Rubric · Reuse workflow), and the third opens a search picker over `GET /api/author/reuse-jobs` (below) that
|
|
3565
3574
|
fills the card through this same path (a pick, then `change` returns to the empty search; Deterministic or Rubric leaves reuse).
|
|
3566
3575
|
It opens the Chat tab's New job card
|
|
@@ -3574,10 +3583,12 @@ key you want first (the panel picks the row by the Model menu).
|
|
|
3574
3583
|
green, notGreen, live, finished, avgSpendUsd, avgWallMs}, line}` (`404` unknown, `409` with an `error` when
|
|
3575
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
|
|
3576
3585
|
dependency or no longer matches its signed bytes); `POST /api/author/start` accepts `startFrom: <runid>` or
|
|
3577
|
-
`startFrom: {importId}` beside the card and answers `{ok, sessionId, reuse, state}
|
|
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
|
|
3578
3589
|
`POST /api/author/start-from-check` and the "Changed — new job" mode are gone. `GET /api/author/reuse-jobs[?model=<Name>]` →
|
|
3579
3590
|
`{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
|
|
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` =
|
|
3581
3592
|
start-from's own estimate line, newest green first; imported jobs are not listed. **Identity, two keys:** the
|
|
3582
3593
|
signature hash `jobSpecHash` is unchanged (it covers the caps and the write fence — what the person signs);
|
|
3583
3594
|
`workflowKey(spec)` (`src/job.js`, not exported from the package root) is a sha256 over the signed spec WITHOUT
|
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/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
|
+
}
|
|
@@ -254,6 +254,13 @@ export function createAuthorRoutes(opts) {
|
|
|
254
254
|
const pre = opts.startFrom ? (fromImport !== null ? opts.startFrom.getImport(fromImport) : opts.startFrom.get(fromRunid)) : null;
|
|
255
255
|
if (pre === null || pre === undefined) { send(404, { ok: false, error: 'no such run to reuse' }); return true; }
|
|
256
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
|
+
}
|
|
257
264
|
const locked = lockedFieldChanged(card, pre.card);
|
|
258
265
|
if (locked !== null) { send(400, { ok: false, error: `${locked} is locked on a reused workflow — Clear the card to change it` }); return true; }
|
|
259
266
|
const rv = validateReuseCard(card, { rows });
|
|
@@ -641,10 +641,11 @@ export function createSession(card, deps = {}) {
|
|
|
641
641
|
// a detached checkout of the person's current commit, inside their
|
|
642
642
|
// repo), so a JS/TS repo's tree never carries `node_modules` until
|
|
643
643
|
// someone installs there. Refusing outright here was ITSELF the F182
|
|
644
|
-
// class of bug one layer up.
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
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.
|
|
648
649
|
let depsGap = missingDependencies(prep.tree, prep.manifest.sourceSubdir ?? '');
|
|
649
650
|
// P6 item 3: a committed npm lock file -> bareloop runs `npm ci --ignore-scripts` itself, $0, before
|
|
650
651
|
// any model call. Any failure (or another lock file) falls through to the wait below, unchanged.
|
package/src/panel/index.html
CHANGED
|
@@ -140,6 +140,8 @@
|
|
|
140
140
|
.dot::before{content:"[·]";color:var(--grey-dot);}
|
|
141
141
|
.dot.green::before{content:"[✓]";color:var(--green);}
|
|
142
142
|
.dot.red::before{content:"[✗]";color:var(--red);}
|
|
143
|
+
/* stopped (hamr 2026-10-07): the person pressed Stop — its own sign, same red as [✗] */
|
|
144
|
+
.dot.stopped::before{content:"[■]";color:var(--red);}
|
|
143
145
|
/* Panel build P3 addition (2026-09-27, "yes, go"): the part cards below the
|
|
144
146
|
step map (`partResultGlyph`/`attemptGlyph`) and the chat job card render
|
|
145
147
|
their result glyph as PLAIN inline text inside a larger string (never
|
|
@@ -295,10 +297,6 @@
|
|
|
295
297
|
.wf-resumed{color:var(--amber);}
|
|
296
298
|
.audit-leg-divider{text-align:center;color:var(--text-dim);font-size:12px;margin:10px 0;overflow-wrap:anywhere;}
|
|
297
299
|
.audit-leg-divider-row td{text-align:center;color:var(--text-dim);font-size:12px;background:var(--panel2);}
|
|
298
|
-
/* Resume opens the run's own Job tab: only the two caps turn into inputs, everything else stays read-only */
|
|
299
|
-
.resume-job{border:1px dashed var(--border-strong);margin-top:12px;padding:10px;display:grid;gap:8px;}
|
|
300
|
-
.resume-cap{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
|
|
301
|
-
.resume-cap input{width:7em;max-width:100%;min-height:36px;background:var(--panel);color:var(--text);border:1px solid var(--border-strong);padding:4px 8px;font:inherit;}
|
|
302
300
|
.summary-box.red{border-color:var(--red);}
|
|
303
301
|
.summary-box.amber{border-color:var(--amber);}
|
|
304
302
|
.summary-box.magenta{border-color:var(--magenta);}
|
|
@@ -374,6 +372,9 @@
|
|
|
374
372
|
.job-card{position:relative;border:1px solid var(--border-strong);padding:14px 10px 10px;background:var(--panel2);margin-top:10px;margin-bottom:10px;}
|
|
375
373
|
.job-card h4{margin:0 0 8px;font-size:13px;position:absolute;top:-10px;left:10px;background:var(--panel2);padding:0 6px;}
|
|
376
374
|
.job-card .card-clear{position:absolute;top:-13px;right:10px;padding:1px 8px;font-size:11px;line-height:1.4;background:var(--panel2);}
|
|
375
|
+
.job-card .origin-line{font-size:12px;padding:6px 8px;border-left:3px solid var(--amber);background:var(--panel);color:var(--text-dim);margin-bottom:6px;overflow-wrap:anywhere;}
|
|
376
|
+
.job-card .origin-line.res{border-color:var(--accent, var(--text));}
|
|
377
|
+
.job-card .chat-resume-box{display:grid;gap:8px;margin-top:10px;}
|
|
377
378
|
.job-card h4::before{content:"┤ ";color:var(--text-faint);}
|
|
378
379
|
.job-card h4::after{content:" ├";color:var(--text-faint);}
|
|
379
380
|
.field{margin-bottom:10px;}
|
|
@@ -533,6 +534,7 @@
|
|
|
533
534
|
<h4 id="job-card-title">Job card — draft</h4>
|
|
534
535
|
<button class="btn card-clear" type="button" id="chat-clear-btn" data-testid="chat-clear">Clear</button>
|
|
535
536
|
<div class="hint" id="chat-startfrom-note" data-testid="startfrom-note" hidden></div>
|
|
537
|
+
<div class="origin-line" id="chat-origin-line" data-testid="origin-line" hidden></div>
|
|
536
538
|
|
|
537
539
|
<div class="field">
|
|
538
540
|
<label id="jf-verdict-label">Check type</label>
|
|
@@ -598,6 +600,11 @@
|
|
|
598
600
|
</div>
|
|
599
601
|
</div>
|
|
600
602
|
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
603
|
+
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
604
|
+
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
605
|
+
<div class="ended-actions"><button class="btn primary" type="button" id="chat-resume-sign" data-testid="btn-sign-resume">Sign & resume</button> <button class="btn" type="button" id="chat-resume-cancel" data-testid="btn-resume-cancel">Cancel</button></div>
|
|
606
|
+
<div class="resume-err" id="chat-resume-err" data-testid="resume-err" hidden></div>
|
|
607
|
+
</div>
|
|
601
608
|
</div>
|
|
602
609
|
|
|
603
610
|
<div class="chat-thread" id="chat-thread" data-testid="chat-thread"></div>
|
|
@@ -723,8 +730,8 @@
|
|
|
723
730
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
724
731
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
725
732
|
<div class="field-row">
|
|
726
|
-
<div class="field"><label
|
|
727
|
-
<div class="field"><label
|
|
733
|
+
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
734
|
+
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
728
735
|
</div>
|
|
729
736
|
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
730
737
|
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
@@ -734,7 +741,6 @@
|
|
|
734
741
|
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
735
742
|
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
736
743
|
<div class="hint" id="job-note"></div>
|
|
737
|
-
<div id="resume-job" class="resume-job" data-testid="resume-job" hidden></div>
|
|
738
744
|
</div>
|
|
739
745
|
</section>
|
|
740
746
|
|
|
@@ -1267,6 +1273,7 @@
|
|
|
1267
1273
|
function glyphClass(g){
|
|
1268
1274
|
if(g === "✓") return "green";
|
|
1269
1275
|
if(g === "✗") return "red";
|
|
1276
|
+
if(g === "■") return "stopped";
|
|
1270
1277
|
if(g === "▶") return "amber";
|
|
1271
1278
|
if(g === "?") return "magenta";
|
|
1272
1279
|
return "grey";
|
|
@@ -1499,6 +1506,17 @@
|
|
|
1499
1506
|
// is a normal, expected action.
|
|
1500
1507
|
var wfExpanded = {};
|
|
1501
1508
|
|
|
1509
|
+
// hamr 2026-10-08 (display only, nothing deleted): with NO search/filter active an expanded job shows at most its
|
|
1510
|
+
// latest WF_SUB_CAP other runs (g.runs is latest-first). A selected older run beyond the cap (picked from History) is
|
|
1511
|
+
// appended after them rather than hidden. With search/filters active every matching sub-card shows.
|
|
1512
|
+
var WF_SUB_CAP = 7;
|
|
1513
|
+
function capSubRuns(childRuns, active, selectedRunid){
|
|
1514
|
+
if(active || childRuns.length <= WF_SUB_CAP) return {shown: childRuns, more: false};
|
|
1515
|
+
var shown = childRuns.slice(0, WF_SUB_CAP);
|
|
1516
|
+
var sel = selectedRunid ? childRuns.slice(WF_SUB_CAP).filter(function(r){ return r.runid === selectedRunid; }) : [];
|
|
1517
|
+
return {shown: shown.concat(sel), more: true};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1502
1520
|
function renderWorkflows(jobs){
|
|
1503
1521
|
var el = document.getElementById("wf-list");
|
|
1504
1522
|
el.innerHTML = "";
|
|
@@ -1585,7 +1603,15 @@
|
|
|
1585
1603
|
if(expanded){
|
|
1586
1604
|
var runsWrap = document.createElement("div");
|
|
1587
1605
|
runsWrap.className = "wf-runs";
|
|
1588
|
-
|
|
1606
|
+
var capped = capSubRuns(childRuns, active, olderSelected);
|
|
1607
|
+
capped.shown.forEach(function(r){ runsWrap.appendChild(buildRunRowEl(r, true)); });
|
|
1608
|
+
if(capped.more){
|
|
1609
|
+
var olderHint = document.createElement("div");
|
|
1610
|
+
olderHint.className = "hint";
|
|
1611
|
+
olderHint.setAttribute("data-testid", "wf-older-" + g.job);
|
|
1612
|
+
olderHint.textContent = "older: use search.";
|
|
1613
|
+
runsWrap.appendChild(olderHint);
|
|
1614
|
+
}
|
|
1589
1615
|
wrap.appendChild(runsWrap);
|
|
1590
1616
|
}
|
|
1591
1617
|
el.appendChild(wrap);
|
|
@@ -1654,7 +1680,8 @@
|
|
|
1654
1680
|
var q = (filters && filters.search) || "";
|
|
1655
1681
|
return (runs || []).filter(function(r){
|
|
1656
1682
|
if(ct.length && ct.indexOf(r.checkType) === -1) return false;
|
|
1657
|
-
|
|
1683
|
+
// a stopped run ([■]) was a [✗] before hamr's 2026-10-07 sign ruling — the fail chip still finds it
|
|
1684
|
+
if(rs.length && rs.indexOf(r.glyph) === -1 && !(r.glyph === "\u25A0" && rs.indexOf("\u2717") !== -1)) return false;
|
|
1658
1685
|
if(!matchesSearch(r.job, r.runid, r.model, q, r.runNo)) return false;
|
|
1659
1686
|
if(time !== "all"){
|
|
1660
1687
|
var days = time === "7d" ? 7 : 30;
|
|
@@ -1966,7 +1993,6 @@
|
|
|
1966
1993
|
clearSelection();
|
|
1967
1994
|
currentRunid = null;
|
|
1968
1995
|
runIsLive = false;
|
|
1969
|
-
resumeMode = null;
|
|
1970
1996
|
currentImportId = id;
|
|
1971
1997
|
document.querySelectorAll("#import-list .wf-row").forEach(function(r){
|
|
1972
1998
|
var on = r.getAttribute("data-testid") === "import-row-" + id;
|
|
@@ -2363,11 +2389,9 @@
|
|
|
2363
2389
|
// The sentences are CODE-OWNED on the server (`endedFor`, src/panel/server.js) — this only
|
|
2364
2390
|
// paints them. `detail.ended` is null while a run is live (no block while running).
|
|
2365
2391
|
var lastEndedSig = null;
|
|
2366
|
-
// P5-R
|
|
2367
|
-
//
|
|
2368
|
-
//
|
|
2369
|
-
var resumeMode = null;
|
|
2370
|
-
var lastJobShown = null;
|
|
2392
|
+
// Resume (P5-R, hamr 2026-10-02: opened the run's own Job tab) MOVED to the left Chat card (hamr 2026-10-07): the card
|
|
2393
|
+
// in RESUME mode, only the money cap and the time cap open (applyResume / signResume). The Job tab is read-only on
|
|
2394
|
+
// every run — values only, no inputs, no buttons. A resume is the SAME run: no new card, no new job.
|
|
2371
2395
|
function renderEnded(detail){
|
|
2372
2396
|
var box = document.getElementById("ended-block");
|
|
2373
2397
|
var e = detail.ended;
|
|
@@ -2384,7 +2408,7 @@
|
|
|
2384
2408
|
'<div class="ended-row"><span class="ended-label">NEXT</span><span class="ended-text" data-testid="ended-next">' + escapeXml(e.next) + '</span></div>';
|
|
2385
2409
|
}
|
|
2386
2410
|
// ---- P5 item 5: the run's ACTION AREA (Run tab, above the counters) — Stop while the run is live, Resume once
|
|
2387
|
-
// it ended in a way the engine would accept (
|
|
2411
|
+
// it ended in a way the engine would accept (opens the Chat card's resume mode), Edit in chat on every non-green ending.
|
|
2388
2412
|
// The Ended block (Audit tab) is text only and carries no buttons.
|
|
2389
2413
|
var stopAsked = {};
|
|
2390
2414
|
// Reuse workflow (replaces P5 item 3's Start from this) — ask the server for the prefill (the signed job), then
|
|
@@ -2397,6 +2421,33 @@
|
|
|
2397
2421
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
2398
2422
|
}).catch(function(x){ window.alert("could not reach the panel: " + x.message); });
|
|
2399
2423
|
}
|
|
2424
|
+
// Edit in chat (hamr 2026-10-07, option A) — the same prefill door as Reuse workflow (`start-from`, the ONE reader of a
|
|
2425
|
+
// run's signed job), but the Chat card opens with EVERY box open and the normal [Start drafting] path: it drafts a new job.
|
|
2426
|
+
// The origin line is code-owned fixed text; run name and status word come from the same helpers the run header uses.
|
|
2427
|
+
function editInChat(detail){
|
|
2428
|
+
var origin = "Copied from " + detail.job + " (" + runLabel(detail.runNo, detail.runid) + ") \u2014 " +
|
|
2429
|
+
((detail.status && detail.status.word) || "ended") +
|
|
2430
|
+
". Every box is open. Change anything and it drafts as a new job; change nothing and it is another run of the same job.";
|
|
2431
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2432
|
+
document.getElementById("tab-chat").click();
|
|
2433
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: (r && r.ok) ? {runid: detail.runid, prefill: r, origin: origin} : {error: (r && r.error) || "nothing to copy"}}));
|
|
2434
|
+
}).catch(function(x){
|
|
2435
|
+
document.getElementById("tab-chat").click();
|
|
2436
|
+
document.dispatchEvent(new CustomEvent("bareloop-edit", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
// Resume (hamr 2026-10-07, option B — moved from the run's Job tab): the Chat card opens in RESUME mode, filled from the
|
|
2440
|
+
// SAME prefill (`start-from`), everything locked but the two caps. The card posts /api/runs/<id>/resume; a resume never
|
|
2441
|
+
// drafts and never makes a new card, job or run id (one run = one id).
|
|
2442
|
+
function resumeInChat(detail){
|
|
2443
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(detail.runid)).then(function(r){
|
|
2444
|
+
document.getElementById("tab-chat").click();
|
|
2445
|
+
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: (r && r.ok) ? {runid: detail.runid, prefill: r, resume: detail.resume || {}, title: detail.job + " (" + runLabel(detail.runNo, detail.runid) + ")"} : {error: (r && r.error) || "nothing to resume"}}));
|
|
2446
|
+
}).catch(function(x){
|
|
2447
|
+
document.getElementById("tab-chat").click();
|
|
2448
|
+
document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2400
2451
|
function renderRunActions(detail){
|
|
2401
2452
|
var bar = document.getElementById("run-actions");
|
|
2402
2453
|
var html = "";
|
|
@@ -2412,6 +2463,10 @@
|
|
|
2412
2463
|
var canResume = !!detail.resume && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "resume"; });
|
|
2413
2464
|
if(canResume) html = '<button class="btn primary" type="button" data-testid="btn-resume-run">Resume</button>';
|
|
2414
2465
|
}
|
|
2466
|
+
// Edit in chat: every non-green ending (the server's Ended block says so) — a died run included
|
|
2467
|
+
if(!live && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "edit"; })){
|
|
2468
|
+
html += '<button class="btn" type="button" data-testid="btn-edit-chat">Edit in chat</button>';
|
|
2469
|
+
}
|
|
2415
2470
|
// Reuse workflow: on a GREEN run only (the server's Ended block says so) — a new run of the same signed job
|
|
2416
2471
|
if(!live && !detail.died && !!detail.ended && (detail.ended.actions || []).some(function(a){ return a.id === "reuse"; })){
|
|
2417
2472
|
html += '<button class="btn" type="button" data-testid="btn-reuse">Reuse workflow</button>';
|
|
@@ -2420,6 +2475,8 @@
|
|
|
2420
2475
|
bar.hidden = html === "";
|
|
2421
2476
|
var reuseBtn = bar.querySelector('[data-testid="btn-reuse"]');
|
|
2422
2477
|
if(reuseBtn) reuseBtn.addEventListener("click", function(){ reuseWorkflow(detail.runid); });
|
|
2478
|
+
var editBtn = bar.querySelector('[data-testid="btn-edit-chat"]');
|
|
2479
|
+
if(editBtn) editBtn.addEventListener("click", function(){ editInChat(detail); });
|
|
2423
2480
|
var stopBtn = bar.querySelector('[data-testid="btn-stop"]');
|
|
2424
2481
|
if(stopBtn){
|
|
2425
2482
|
stopBtn.addEventListener("click", function(){
|
|
@@ -2431,82 +2488,7 @@
|
|
|
2431
2488
|
});
|
|
2432
2489
|
}
|
|
2433
2490
|
var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
|
|
2434
|
-
if(resBtn) resBtn.addEventListener("click", function(){
|
|
2435
|
-
}
|
|
2436
|
-
function openResumeOnJobTab(detail){
|
|
2437
|
-
var r = detail.resume || {};
|
|
2438
|
-
resumeMode = {
|
|
2439
|
-
runid: detail.runid, spentUsd: r.spentUsd, spendComplete: r.spendComplete,
|
|
2440
|
-
wallUsedMs: r.wallUsedMs,
|
|
2441
|
-
budget: (r.budgetUsd === null || r.budgetUsd === undefined) ? "" : String(r.budgetUsd),
|
|
2442
|
-
wall: typeof r.maxWallMin === "number" ? String(r.maxWallMin) : null,
|
|
2443
|
-
painted: false,
|
|
2444
|
-
};
|
|
2445
|
-
document.getElementById("tab-details").click();
|
|
2446
|
-
paintResumeMode();
|
|
2447
|
-
}
|
|
2448
|
-
// Turns the Job tab's two cap values into inputs (and shows the Sign box) while a resume is open on this run;
|
|
2449
|
-
// called again after every Job re-render so a re-render never drops the typed numbers.
|
|
2450
|
-
function paintResumeMode(){
|
|
2451
|
-
var box = document.getElementById("resume-job");
|
|
2452
|
-
var moneyLabel = document.getElementById("details-cap-money-label");
|
|
2453
|
-
var timeLabel = document.getElementById("details-cap-time-label");
|
|
2454
|
-
if(!resumeMode || resumeMode.runid !== currentRunid){
|
|
2455
|
-
box.hidden = true; box.innerHTML = "";
|
|
2456
|
-
moneyLabel.textContent = "$ cap"; timeLabel.textContent = "Time cap";
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
var m = resumeMode;
|
|
2460
|
-
// the unit lives in the caption, never as loose text around the input
|
|
2461
|
-
moneyLabel.textContent = "Money cap ($)";
|
|
2462
|
-
timeLabel.textContent = "Time cap (min)";
|
|
2463
|
-
document.getElementById("details-cap-money").innerHTML =
|
|
2464
|
-
'<span class="resume-cap"><input type="number" min="0" step="0.01" id="resume-budget" data-testid="resume-budget" value="' + escapeXml(m.budget) + '"></span>';
|
|
2465
|
-
if(m.wall !== null){
|
|
2466
|
-
document.getElementById("details-cap-time").innerHTML =
|
|
2467
|
-
'<span class="resume-cap"><input type="number" min="1" step="1" id="resume-wall" data-testid="resume-wall" value="' + escapeXml(m.wall) + '"></span>';
|
|
2468
|
-
}
|
|
2469
|
-
var budgetEl = document.getElementById("resume-budget");
|
|
2470
|
-
budgetEl.addEventListener("input", function(){ m.budget = budgetEl.value; });
|
|
2471
|
-
var wallEl = document.getElementById("resume-wall");
|
|
2472
|
-
if(wallEl) wallEl.addEventListener("input", function(){ m.wall = wallEl.value; });
|
|
2473
|
-
box.hidden = false;
|
|
2474
|
-
if(m.painted) return;
|
|
2475
|
-
m.painted = true;
|
|
2476
|
-
var spent = (m.spentUsd === null || m.spentUsd === undefined) ? "unknown" : ((m.spendComplete ? "" : "at least ") + panelMoney(m.spentUsd));
|
|
2477
|
-
box.innerHTML =
|
|
2478
|
-
'<div data-testid="resume-intro">Resume run ' + escapeXml(m.runid) + ' — the same run, not a new one. Only the money cap and the time cap can change; everything else is the signed job.</div>' +
|
|
2479
|
-
'<div class="hint" data-testid="resume-spent">spent so far ' + escapeXml(spent) +
|
|
2480
|
-
(typeof m.wallUsedMs === "number" && isFinite(m.wallUsedMs) ? ' · time used so far ' + Math.ceil(m.wallUsedMs / 60000) + ' min' : '') + '</div>' +
|
|
2481
|
-
'<div class="ended-actions"><button class="btn primary" type="button" data-testid="btn-sign-resume">Sign & resume</button> ' +
|
|
2482
|
-
'<button class="btn" type="button" data-testid="btn-resume-cancel">Cancel</button></div>' +
|
|
2483
|
-
'<div class="resume-err" id="resume-err" data-testid="resume-err" hidden></div>';
|
|
2484
|
-
var resumeSignBtn = box.querySelector('[data-testid="btn-sign-resume"]');
|
|
2485
|
-
box.querySelector('[data-testid="btn-resume-cancel"]').addEventListener("click", function(){
|
|
2486
|
-
resumeMode = null;
|
|
2487
|
-
renderJob(lastJobShown);
|
|
2488
|
-
});
|
|
2489
|
-
resumeSignBtn.addEventListener("click", function(){
|
|
2490
|
-
var err = document.getElementById("resume-err");
|
|
2491
|
-
err.hidden = true;
|
|
2492
|
-
resumeSignBtn.disabled = true;
|
|
2493
|
-
var body = {budgetUsd: document.getElementById("resume-budget").value};
|
|
2494
|
-
var wall = document.getElementById("resume-wall");
|
|
2495
|
-
if(wall){ body.maxWallMin = wall.value; }
|
|
2496
|
-
return authorPost("/api/runs/" + encodeURIComponent(m.runid) + "/resume", body).then(function(res){
|
|
2497
|
-
if(res.body && res.body.ok){
|
|
2498
|
-
// the SAME run goes on: back to its Run tab, which follows it until it is live again
|
|
2499
|
-
resumeMode = null;
|
|
2500
|
-
renderJob(lastJobShown);
|
|
2501
|
-
document.getElementById("tab-run").click();
|
|
2502
|
-
afterResumeRefresh(m.runid, 8);
|
|
2503
|
-
} else {
|
|
2504
|
-
resumeSignBtn.disabled = false;
|
|
2505
|
-
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
2506
|
-
err.hidden = false;
|
|
2507
|
-
}
|
|
2508
|
-
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
2509
|
-
});
|
|
2491
|
+
if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
|
|
2510
2492
|
}
|
|
2511
2493
|
// After a resume starts, the open run's own page follows it: re-select once the run reads live again (the spine's
|
|
2512
2494
|
// new leg appears a moment after the engine starts), a few tries, never forever.
|
|
@@ -2600,7 +2582,7 @@
|
|
|
2600
2582
|
" · " + wallPhrase;
|
|
2601
2583
|
|
|
2602
2584
|
var box = document.getElementById("run-summary");
|
|
2603
|
-
box.className = "summary-box " + (detail.died ? "magenta" : (detail.glyph === "✗" ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2585
|
+
box.className = "summary-box " + (detail.died ? "magenta" : ((detail.glyph === "✗" || detail.glyph === "■") ? "red" : (detail.glyph === "▶" ? "amber" : "")));
|
|
2604
2586
|
// F196: a synthetic placeholder box (e.g. "died during planning") is a
|
|
2605
2587
|
// LABEL, not a real step — excluded from this count so a run that died
|
|
2606
2588
|
// before any step started reads "0 of 0 done" / "none started", never
|
|
@@ -2679,7 +2661,7 @@
|
|
|
2679
2661
|
|
|
2680
2662
|
// 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
|
|
2681
2663
|
function stepStateHTML(box, testid){
|
|
2682
|
-
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? "red" : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2664
|
+
var cls = box.state === "done" ? "green" : (box.state === "stopped" ? (box.status && box.status.sign === "\u25A0" ? "stopped" : "red") : (box.state === "running" ? "amber" : (box.state === "died" ? "magenta" : "grey")));
|
|
2683
2665
|
return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
|
|
2684
2666
|
}
|
|
2685
2667
|
// build item C: Audit tab attempt row -> its rounds (server-built,
|
|
@@ -3269,7 +3251,6 @@
|
|
|
3269
3251
|
function renderJob(job){
|
|
3270
3252
|
var empty = document.getElementById("job-empty");
|
|
3271
3253
|
var card = document.getElementById("job-card-readonly");
|
|
3272
|
-
lastJobShown = job;
|
|
3273
3254
|
if(!job){ empty.hidden = false; card.hidden = true; return; }
|
|
3274
3255
|
empty.hidden = true;
|
|
3275
3256
|
card.hidden = false;
|
|
@@ -3292,7 +3273,6 @@
|
|
|
3292
3273
|
document.getElementById("job-note").textContent = job.note || "";
|
|
3293
3274
|
lastJobToolsList = Array.isArray(job.toolsList) ? job.toolsList : null;
|
|
3294
3275
|
paintOfferedRow();
|
|
3295
|
-
paintResumeMode();
|
|
3296
3276
|
}
|
|
3297
3277
|
|
|
3298
3278
|
// ---- selection + loading ----
|
|
@@ -3312,7 +3292,6 @@
|
|
|
3312
3292
|
document.querySelectorAll(siblingSelector).forEach(function(r){ r.classList.remove("selected"); r.setAttribute("aria-pressed","false"); });
|
|
3313
3293
|
if(rowEl){ rowEl.classList.add("selected"); rowEl.setAttribute("aria-pressed","true"); }
|
|
3314
3294
|
currentRunid = runid;
|
|
3315
|
-
resumeMode = null;
|
|
3316
3295
|
lastLegDividers = [];
|
|
3317
3296
|
lastJobToolsList = null;
|
|
3318
3297
|
lastRunDetailSig = null;
|
|
@@ -3731,32 +3710,39 @@
|
|
|
3731
3710
|
var autoSigned = false;
|
|
3732
3711
|
var sfLine = document.getElementById("chat-startfrom-line");
|
|
3733
3712
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3713
|
+
var originLine = document.getElementById("chat-origin-line");
|
|
3734
3714
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3735
3715
|
var LOCKED_IDS = ["jf-name", "jf-goal", "jf-success", "jf-guardrails", "jf-judge"];
|
|
3736
3716
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3737
3717
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3738
3718
|
var OPEN_IDS = ["jf-source", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3739
3719
|
var reuseOn = false;
|
|
3720
|
+
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3721
|
+
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
3722
|
+
var resumeRun = null;
|
|
3723
|
+
var CAP_IDS = ["jf-cap-money", "jf-cap-time"];
|
|
3740
3724
|
function syncCardLock(){
|
|
3741
3725
|
var live = sessionLive;
|
|
3726
|
+
var resuming = resumeRun !== null;
|
|
3742
3727
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3743
|
-
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live); });
|
|
3744
|
-
OPEN_IDS.forEach(function(id){ setLock(id, live); });
|
|
3745
|
-
document.getElementById("jf-model").disabled = live;
|
|
3746
|
-
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse);
|
|
3747
|
-
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live; });
|
|
3728
|
+
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3729
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1)); });
|
|
3730
|
+
document.getElementById("jf-model").disabled = live || resuming;
|
|
3731
|
+
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3732
|
+
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3748
3733
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3749
|
-
if(!reuseOn && !live){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3734
|
+
if(!reuseOn && !live && !resuming){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3750
3735
|
}
|
|
3751
3736
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3752
|
-
function clearStartFrom(){
|
|
3753
|
-
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes
|
|
3754
|
-
|
|
3737
|
+
function clearStartFrom(keepBoxes){
|
|
3738
|
+
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3739
|
+
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3740
|
+
if(startFrom && !keepBoxes){
|
|
3755
3741
|
["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 = ""; });
|
|
3756
3742
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3757
3743
|
}
|
|
3758
3744
|
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
3759
|
-
sfLine.hidden = true; sfNote.hidden = true;
|
|
3745
|
+
sfLine.hidden = true; sfNote.hidden = true; originLine.hidden = true;
|
|
3760
3746
|
setReuseLocked(false);
|
|
3761
3747
|
renderPicker();
|
|
3762
3748
|
renderMain();
|
|
@@ -3807,6 +3793,7 @@
|
|
|
3807
3793
|
function renderReuseList(){
|
|
3808
3794
|
var q = rwQ.value.trim();
|
|
3809
3795
|
var all = rwJobs || [];
|
|
3796
|
+
var loading = rwJobs === null && !rwFailed;
|
|
3810
3797
|
rwShown = all.filter(function(j){ return j.job.toLowerCase().indexOf(q.toLowerCase()) !== -1; });
|
|
3811
3798
|
if(rwHl >= rwShown.length) rwHl = 0;
|
|
3812
3799
|
if(rwShown.length){
|
|
@@ -3815,7 +3802,7 @@
|
|
|
3815
3802
|
}).join("");
|
|
3816
3803
|
} else {
|
|
3817
3804
|
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>";
|
|
3805
|
+
: loading ? "Loading\u2026" : (all.length === 0 ? "No green jobs yet." : 'No green job matches "' + escapeXml(q) + '".')) + "</div>";
|
|
3819
3806
|
}
|
|
3820
3807
|
}
|
|
3821
3808
|
function loadReuseJobs(){
|
|
@@ -3826,9 +3813,10 @@
|
|
|
3826
3813
|
}).catch(function(){ rwFailed = true; rwJobs = null; renderReuseList(); });
|
|
3827
3814
|
}
|
|
3828
3815
|
// the empty search state: card emptied, nothing picked, Sign & run disabled until a job is picked
|
|
3816
|
+
// (the boxes are NOT cleared here: clicking the Reuse radio never blanks typing; only a pick fills the card)
|
|
3829
3817
|
function openPicker(){
|
|
3830
|
-
clearCardBoxes();
|
|
3831
3818
|
rwQ.value = ""; rwHl = 0;
|
|
3819
|
+
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3832
3820
|
document.getElementById("jf-judge").disabled = true;
|
|
3833
3821
|
renderPicker();
|
|
3834
3822
|
renderReuseList();
|
|
@@ -3854,8 +3842,22 @@
|
|
|
3854
3842
|
openPicker();
|
|
3855
3843
|
}
|
|
3856
3844
|
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3845
|
+
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3846
|
+
function exitResumeMode(){
|
|
3847
|
+
resumeRun = null;
|
|
3848
|
+
originLine.classList.remove("res");
|
|
3849
|
+
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3850
|
+
document.getElementById("chat-resume-box").hidden = true;
|
|
3851
|
+
document.getElementById("chat-resume-err").hidden = true;
|
|
3852
|
+
document.getElementById("jf-cap-time").parentNode.hidden = false;
|
|
3853
|
+
document.getElementById("chat-clear-btn").hidden = false;
|
|
3854
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = false; });
|
|
3855
|
+
document.querySelector(".chat-input-row").hidden = false;
|
|
3856
|
+
}
|
|
3857
3857
|
function openNewCard(){
|
|
3858
3858
|
card.hidden = false;
|
|
3859
|
+
originLine.hidden = true;
|
|
3860
|
+
exitResumeMode();
|
|
3859
3861
|
thread.innerHTML = "";
|
|
3860
3862
|
errEl.textContent = "";
|
|
3861
3863
|
actionErrEl.textContent = "";
|
|
@@ -3961,12 +3963,11 @@
|
|
|
3961
3963
|
// the limit changed in Settings: re-read the note for whatever cap is typed
|
|
3962
3964
|
document.addEventListener("bareloop-limit-changed", refreshCapNote);
|
|
3963
3965
|
// 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 —
|
|
3966
|
+
// Rubric while the picker is showing (picked or not) LEAVES reuse — boxes untouched (clearStartFrom(true)).
|
|
3965
3967
|
function onVerdictChange(v){
|
|
3966
3968
|
if(v === "reuse"){ if(rwBox.hidden) openPicker(); return; }
|
|
3967
3969
|
if(!rwBox.hidden){
|
|
3968
|
-
clearStartFrom();
|
|
3969
|
-
clearCardBoxes();
|
|
3970
|
+
clearStartFrom(true);
|
|
3970
3971
|
setVerdict(v);
|
|
3971
3972
|
renderPicker();
|
|
3972
3973
|
}
|
|
@@ -4314,7 +4315,7 @@
|
|
|
4314
4315
|
poll();
|
|
4315
4316
|
}
|
|
4316
4317
|
function reattachLive(){
|
|
4317
|
-
if(sessionId || sessionLive) return;
|
|
4318
|
+
if(sessionId || sessionLive || resumeRun !== null) return;
|
|
4318
4319
|
authorGet("/api/author/live").then(function(j){
|
|
4319
4320
|
if(sessionId || sessionLive || !j || !j.ok || !j.sessionId || !j.state) return;
|
|
4320
4321
|
attachSession(j);
|
|
@@ -4356,6 +4357,90 @@
|
|
|
4356
4357
|
}
|
|
4357
4358
|
document.addEventListener("bareloop-reuse", function(e){ applyReuse(e.detail || {}); });
|
|
4358
4359
|
|
|
4360
|
+
// Edit in chat — the card opens filled from the SIGNED job with EVERY box open; the button stays the normal
|
|
4361
|
+
// [Start drafting] (startFrom / reuse state are never set: no borrowed track record). A live drafting session is
|
|
4362
|
+
// handled exactly as applyReuse does (clearStartFrom + openNewCard). A refused prefill opens nothing, error only.
|
|
4363
|
+
function applyEdit(d){
|
|
4364
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4365
|
+
var c = (d.prefill || {}).card || {};
|
|
4366
|
+
clearStartFrom();
|
|
4367
|
+
openNewCard();
|
|
4368
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4369
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4370
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4371
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4372
|
+
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4373
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4374
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4375
|
+
originLine.textContent = d.origin || "";
|
|
4376
|
+
originLine.hidden = !d.origin;
|
|
4377
|
+
renderPicker();
|
|
4378
|
+
refreshModelStatus();
|
|
4379
|
+
refreshCapNote();
|
|
4380
|
+
refreshStartEnabled();
|
|
4381
|
+
}
|
|
4382
|
+
document.addEventListener("bareloop-edit", function(e){ applyEdit(e.detail || {}); });
|
|
4383
|
+
|
|
4384
|
+
// Resume — the card in RESUME mode: the signed job filled and locked, only Cap $ and Time cap open (prefilled from the
|
|
4385
|
+
// engine's own resume plan, the values the Job tab used to show). A live drafting session is handled exactly as
|
|
4386
|
+
// applyReuse does. A refused prefill opens nothing, error only.
|
|
4387
|
+
function applyResume(d){
|
|
4388
|
+
if(d.error){ errEl.textContent = d.error; return; }
|
|
4389
|
+
var c = (d.prefill || {}).card || {};
|
|
4390
|
+
var r = d.resume || {};
|
|
4391
|
+
clearStartFrom();
|
|
4392
|
+
openNewCard();
|
|
4393
|
+
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4394
|
+
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4395
|
+
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4396
|
+
if(c.model){ modelSelect.value = c.model; }
|
|
4397
|
+
setVal("jf-name", c.jobName); setVal("jf-goal", c.goal); setVal("jf-source", c.source);
|
|
4398
|
+
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4399
|
+
setVal("jf-judge", c.judgeExamples);
|
|
4400
|
+
setVal("jf-cap-money", r.budgetUsd);
|
|
4401
|
+
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4402
|
+
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4403
|
+
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4404
|
+
originLine.textContent = "The same run goes on, not a new one. Only the money cap and the time cap can change; everything else is the signed job.";
|
|
4405
|
+
originLine.classList.add("res");
|
|
4406
|
+
originLine.hidden = false;
|
|
4407
|
+
var spent = (r.spentUsd === null || r.spentUsd === undefined) ? "unknown" : ((r.spendComplete ? "" : "at least ") + panelMoney(r.spentUsd));
|
|
4408
|
+
document.getElementById("chat-resume-spent").textContent = "spent so far " + spent +
|
|
4409
|
+
(typeof r.wallUsedMs === "number" && isFinite(r.wallUsedMs) ? " \u00B7 time used so far " + Math.ceil(r.wallUsedMs / 60000) + " min" : "");
|
|
4410
|
+
document.getElementById("chat-resume-box").hidden = false;
|
|
4411
|
+
document.getElementById("chat-clear-btn").hidden = true;
|
|
4412
|
+
["chat-main-btn", "chat-main-hint"].forEach(function(id){ document.getElementById(id).hidden = true; });
|
|
4413
|
+
document.querySelector(".chat-input-row").hidden = true;
|
|
4414
|
+
renderPicker();
|
|
4415
|
+
refreshModelStatus();
|
|
4416
|
+
refreshStartEnabled();
|
|
4417
|
+
}
|
|
4418
|
+
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4419
|
+
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4420
|
+
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4421
|
+
function signResume(){
|
|
4422
|
+
var run = resumeRun;
|
|
4423
|
+
if(run === null) return;
|
|
4424
|
+
var err = document.getElementById("chat-resume-err");
|
|
4425
|
+
err.hidden = true;
|
|
4426
|
+
resumeSignBtn.disabled = true;
|
|
4427
|
+
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4428
|
+
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4429
|
+
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4430
|
+
resumeSignBtn.disabled = false;
|
|
4431
|
+
if(res.body && res.body.ok){
|
|
4432
|
+
// the SAME run goes on: the card goes back to empty, the right side to its Run tab, which follows it until it is live again
|
|
4433
|
+
resetCard();
|
|
4434
|
+
document.getElementById("tab-run").click();
|
|
4435
|
+
afterResumeRefresh(run.runid, 8);
|
|
4436
|
+
} else {
|
|
4437
|
+
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4438
|
+
err.hidden = false;
|
|
4439
|
+
}
|
|
4440
|
+
}).catch(function(x){ resumeSignBtn.disabled = false; err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4441
|
+
}
|
|
4442
|
+
resumeSignBtn.addEventListener("click", signResume);
|
|
4443
|
+
|
|
4359
4444
|
// build item 1 — the "lost answer" fix. ROOT CAUSE: `renderActions`
|
|
4360
4445
|
// (fed by the every-2s `poll()`) unconditionally overwrites
|
|
4361
4446
|
// `#chat-card-error` on every tick (`errEl.textContent = state.error ||
|
package/src/panel/server.js
CHANGED
|
@@ -74,7 +74,7 @@ export const RUNID_RE = /^[A-Za-z0-9._~-]+$/;
|
|
|
74
74
|
export const DEFAULT_PORT = 4700;
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* `[✓]`/`[✗]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
77
|
+
* `[✓]`/`[✗]`/`[■]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
78
78
|
* (auto-memory `ui-verdict-words.md`: never the words green/red/soft-green
|
|
79
79
|
* anywhere in the page). `null` (no `job-end` reached — a killed-mid-run or
|
|
80
80
|
* still-running spine) reads `▶` — the same "in progress / unresolved" glyph
|
|
@@ -82,10 +82,10 @@ export const DEFAULT_PORT = 4700;
|
|
|
82
82
|
* running attempt and an archived spine that never reached its own end are
|
|
83
83
|
* the same fact from this read-only side: no verdict has been recorded yet.
|
|
84
84
|
* @param {string|null} outcome
|
|
85
|
-
* @returns {'✓'|'✗'|'▶'}
|
|
85
|
+
* @returns {'✓'|'✗'|'■'|'▶'}
|
|
86
86
|
*/
|
|
87
87
|
export function glyphForOutcome(outcome) {
|
|
88
|
-
return /** @type {'✓'|'✗'|'▶'} */ (statusFor({ outcome }).sign);
|
|
88
|
+
return /** @type {'✓'|'✗'|'■'|'▶'} */ (statusFor({ outcome }).sign);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
@@ -417,7 +417,7 @@ const GOAL_NOT_MET = new Set(['plan-red', 'check-red', 'step-red', 'escalated'])
|
|
|
417
417
|
* @returns {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null}
|
|
418
418
|
*/
|
|
419
419
|
export function endedFor(summary, death, o = {}) {
|
|
420
|
-
const base = endedBase(summary, death, o);
|
|
420
|
+
const base = withEditAction(endedBase(summary, death, o));
|
|
421
421
|
const wt = o.worktree;
|
|
422
422
|
if (!base || !wt) return base;
|
|
423
423
|
// P6 item 1 — the run worked on a worktree in the person's own repo. GREEN: the branch carries the work (the folder is
|
|
@@ -432,21 +432,31 @@ export function endedFor(summary, death, o = {}) {
|
|
|
432
432
|
return wt.exists ? { ...base, next: `${base.next} Your work is in ${wt.folder}.` } : base;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* "Edit in chat" (hamr 2026-10-07): offered on EVERY non-green ending (failed, capped, stopped, died, every other red),
|
|
437
|
+
* after Resume when both show; a green ending keeps Reuse workflow only. ONE place, so no ending branch can forget it.
|
|
438
|
+
* @param {{reason: string, next: string, line: string, actions: {id: string, label: string}[]}|null} ended
|
|
439
|
+
*/
|
|
440
|
+
function withEditAction(ended) {
|
|
441
|
+
if (!ended || ended.actions.some((a) => a.id === 'reuse')) return ended;
|
|
442
|
+
return { ...ended, actions: [...ended.actions, { id: 'edit', label: 'Edit in chat' }] };
|
|
443
|
+
}
|
|
444
|
+
|
|
435
445
|
/** @param {any} summary @param {any} death @param {any} o */
|
|
436
446
|
function endedBase(summary, death, o) {
|
|
437
447
|
const resumeOk = !!(o.resume && o.resume.ok);
|
|
438
448
|
/** @type {{id: string, label: string}[]} */
|
|
439
449
|
const RESUME = [{ id: 'resume', label: 'Resume' }];
|
|
440
450
|
/** Reuse workflow (replaces P5 item 3's Start from this, hamr 2026-10-03): the same signed job on a new source — a NEW
|
|
441
|
-
* run, never a rerun. Offered on GREEN rows only; every red row says "Change the job:
|
|
451
|
+
* run, never a rerun. Offered on GREEN rows only; every red row says "Change the job: press Edit in chat" instead. */
|
|
442
452
|
const REUSE = [{ id: 'reuse', label: 'Reuse workflow' }];
|
|
443
|
-
const CHANGE = 'Change the job:
|
|
453
|
+
const CHANGE = 'Change the job: press Edit in chat.';
|
|
444
454
|
const detailOf = (/** @type {string|null} */ s) => {
|
|
445
455
|
if (typeof s !== 'string' || s.length === 0) return '';
|
|
446
456
|
return s.length > 120 ? `${s.slice(0, 120)}…` : s;
|
|
447
457
|
};
|
|
448
458
|
/** a resumable ending whose Resume the engine would refuse: say so, never offer the button */
|
|
449
|
-
const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}
|
|
459
|
+
const resumeOr = (/** @type {string} */ okNext) => (resumeOk ? okNext : `Resume is not available for this run${o.resume && o.resume.why ? ` (${o.resume.why})` : ''}. ${CHANGE}`);
|
|
450
460
|
|
|
451
461
|
if (death.died) {
|
|
452
462
|
return {
|
|
@@ -497,13 +507,20 @@ function endedBase(summary, death, o) {
|
|
|
497
507
|
|
|
498
508
|
if (outcome === 'green' || outcome === 'already-green' || outcome === 'satisfied') {
|
|
499
509
|
if (o.destinationRefused) {
|
|
510
|
+
// Reuse workflow only on a run that earned its green (hamr 2026-10-08, as below); the rest take Edit in chat
|
|
511
|
+
// (withEditAction adds it) and are told to change the job, not to reuse it
|
|
512
|
+
const earned = outcome === 'green';
|
|
500
513
|
return {
|
|
501
514
|
reason: 'Goal met, but the output could not be delivered.',
|
|
502
|
-
next: 'Fix the destination, then Reuse workflow.',
|
|
515
|
+
next: earned ? 'Fix the destination, then Reuse workflow.' : CHANGE,
|
|
503
516
|
line: `${GOAL_MET_LINE} — not delivered`,
|
|
504
|
-
actions: REUSE,
|
|
517
|
+
actions: earned ? REUSE : [],
|
|
505
518
|
};
|
|
506
519
|
}
|
|
520
|
+
// hamr 2026-10-08: already-green (the check passed before any work) is not a reusable job — only a run that
|
|
521
|
+
// earned its green offers Reuse workflow; the rest take Edit in chat (withEditAction adds it). `satisfied` is a
|
|
522
|
+
// close VERDICT, never a run outcome (ralph and the job-end emitters write `green`), so it rides with already-green.
|
|
523
|
+
if (outcome !== 'green') return { reason: 'Goal met.', next: CHANGE, line: GOAL_MET_LINE, actions: [] };
|
|
507
524
|
return { reason: 'Goal met.', next: 'Nothing to do.', line: GOAL_MET_LINE, actions: REUSE };
|
|
508
525
|
}
|
|
509
526
|
if (outcome === 'cap-halt') {
|
|
@@ -545,7 +562,7 @@ function endedBase(summary, death, o) {
|
|
|
545
562
|
if (outcome === 'step-stalled') {
|
|
546
563
|
return {
|
|
547
564
|
reason: 'A step stopped making progress.',
|
|
548
|
-
next: resumeOr('Resume, or change the job:
|
|
565
|
+
next: resumeOr('Resume, or change the job: press Edit in chat.'),
|
|
549
566
|
line: resumeOk ? 'step stalled — resume' : 'step stalled',
|
|
550
567
|
actions: resumeOk ? RESUME : [],
|
|
551
568
|
};
|
|
@@ -2408,7 +2425,7 @@ export function reuseLine(record) {
|
|
|
2408
2425
|
* to copy: change the job: Clear the card). `null` when the runid is not listed.
|
|
2409
2426
|
* @param {string} runid
|
|
2410
2427
|
* @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
|
|
2411
|
-
* @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
|
|
2428
|
+
* @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
|
|
2412
2429
|
* locked: readonly string[], open: readonly string[], note: string|null,
|
|
2413
2430
|
* specHash: string, workflowKey: string, spec: any, specPath: string,
|
|
2414
2431
|
* trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
|
|
@@ -2447,6 +2464,8 @@ export function getStartFrom(runid, opts = {}) {
|
|
|
2447
2464
|
return {
|
|
2448
2465
|
ok: true,
|
|
2449
2466
|
origin: { runid, job: row.job },
|
|
2467
|
+
// the run's terminal outcome — the Reuse start (POST /api/author/start) refuses anything but a real green
|
|
2468
|
+
outcome: legsOf(records).at(-1)?.outcome ?? null,
|
|
2450
2469
|
card,
|
|
2451
2470
|
from: 'signed job',
|
|
2452
2471
|
locked: REUSE_LOCKED_FIELDS,
|
|
@@ -2480,8 +2499,9 @@ export function listReuseJobs(opts = {}) {
|
|
|
2480
2499
|
let records;
|
|
2481
2500
|
try { records = parseJsonl(row.spine).records; } catch { continue; }
|
|
2482
2501
|
const outcome = legsOf(records).at(-1)?.outcome ?? null;
|
|
2483
|
-
//
|
|
2484
|
-
|
|
2502
|
+
// only a REAL green (hamr 2026-10-08): `already-green` means the check passed before any work, so there is nothing to reuse
|
|
2503
|
+
// (src/reuse.js 'green-predates-run'). Narrower than the Run tab's Reuse button on purpose.
|
|
2504
|
+
if (outcome !== 'green') continue;
|
|
2485
2505
|
const signed = signedSpecForRun(row, records);
|
|
2486
2506
|
if (!signed) continue;
|
|
2487
2507
|
const key = workflowKey(signed.spec);
|
package/src/panel/status.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// hamr's ruling 2026-10-04 (option A): every surface (run cards, expanded run rows, the right-side header, imported
|
|
3
3
|
// rows) reads `status` from the server; the page never maps an outcome to a word itself.
|
|
4
4
|
//
|
|
5
|
-
// [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [
|
|
5
|
+
// [▶] running · [·] waiting · [✓] passed · [✗] failed · [✗] capped · [■] stopped · [?] died
|
|
6
6
|
//
|
|
7
7
|
// Outcome -> word (every outcome the panel knows; anything not listed is `failed`):
|
|
8
8
|
// green, already-green, satisfied -> passed
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// and every other outcome (refusals, instrument stops) -> failed
|
|
16
16
|
// `waiting` is the step-level sign for a step that has not started; no run outcome maps to it.
|
|
17
17
|
|
|
18
|
+
import { capKindOf } from '../legs.js';
|
|
19
|
+
|
|
18
20
|
/** the sentence a passed run carries as its reason (the Ended block's `line`) */
|
|
19
21
|
export const GOAL_MET_LINE = 'goal met';
|
|
20
22
|
|
|
@@ -27,7 +29,7 @@ export const STATUS = Object.freeze({
|
|
|
27
29
|
passed: Object.freeze({ sign: '✓', word: 'passed' }),
|
|
28
30
|
failed: Object.freeze({ sign: '✗', word: 'failed' }),
|
|
29
31
|
capped: Object.freeze({ sign: '✗', word: 'capped' }),
|
|
30
|
-
stopped: Object.freeze({ sign: '
|
|
32
|
+
stopped: Object.freeze({ sign: '■', word: 'stopped' }),
|
|
31
33
|
died: Object.freeze({ sign: '?', word: 'died' }),
|
|
32
34
|
});
|
|
33
35
|
|
|
@@ -44,9 +46,8 @@ export function statusFor(o) {
|
|
|
44
46
|
if (o.died) key = 'died';
|
|
45
47
|
else if (out === null || out === undefined) key = 'running';
|
|
46
48
|
else if (out === 'green' || out === 'already-green' || out === 'satisfied') key = 'passed';
|
|
47
|
-
else if (
|
|
49
|
+
else if (capKindOf(o) !== null) key = 'capped';
|
|
48
50
|
else if (out === 'stopped') key = 'stopped';
|
|
49
|
-
else if (out === 'escalated' && ((o.category === 'cap-halt' && o.moneyHalt) || o.category === 'wall-halt')) key = 'capped';
|
|
50
51
|
return { key, ...STATUS[key] };
|
|
51
52
|
}
|
|
52
53
|
|
package/src/planrun.js
CHANGED
|
@@ -4108,7 +4108,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4108
4108
|
// is not passed — it survives only inside the governor, as the bound for the
|
|
4109
4109
|
// case the trend instrument is blind.
|
|
4110
4110
|
middle, judge: async () => judgeClose(), ladder: fixGovernor, emit: emitL, redact: scrub,
|
|
4111
|
-
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir,
|
|
4111
|
+
closeTimeoutMs, cwd: workdir, workerWrites: w.workerWrites, closeDir, phase: 'fix',
|
|
4112
4112
|
});
|
|
4113
4113
|
} catch (e) {
|
|
4114
4114
|
planExecuted();
|
package/src/ralph.js
CHANGED
|
@@ -562,9 +562,13 @@ export const CLOSE_FAULTS = Object.freeze({
|
|
|
562
562
|
* (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
|
|
563
563
|
* call unchanged; a caller supplying `judge` instead already closed over
|
|
564
564
|
* its own `closeDir` and this param does nothing for it.
|
|
565
|
+
* @param {string} [opts.phase] the phase this loop runs in, when it is not the step loop
|
|
566
|
+
* (the fix loop passes `'fix'`): the person's-stop text then names it ("during the fix
|
|
567
|
+
* phase") instead of saying a resume re-enters "this step", which only the step loop has.
|
|
568
|
+
* Text only — it changes no category, outcome or option.
|
|
565
569
|
* @returns {Promise<'green'|'escalated'>}
|
|
566
570
|
*/
|
|
567
|
-
export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }) {
|
|
571
|
+
export async function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir, phase }) {
|
|
568
572
|
// PARAM GUARD (the BA-4 class — a caller's malformed argument, which is the one
|
|
569
573
|
// thing this shell throws for; a WORKER's bad reach comes back as a refusal
|
|
570
574
|
// RESULT instead, and that rule is about feedback, not about the API contract).
|
|
@@ -654,7 +658,9 @@ export async function ralph({ middle, close, judge, capRuns, ladder, emit, redac
|
|
|
654
658
|
'revise the goal/spec so the work fits the time (same re-approval)',
|
|
655
659
|
'abandon the task']],
|
|
656
660
|
// P5 item 5 — the PERSON asked to stop at a round boundary: nothing failed, nothing is discarded.
|
|
657
|
-
'stopped': [
|
|
661
|
+
'stopped': [phase
|
|
662
|
+
? `You stopped the run during the ${phase} phase. Nothing is discarded: the work on disk stands, and a resume continues the run.`
|
|
663
|
+
: 'You stopped the run. Nothing is discarded: the work on disk stands, and a resume re-enters this step.',
|
|
658
664
|
['resume (the same run continues)', 'abandon the task']],
|
|
659
665
|
// F66 — the STALL WATCHDOG gave up: no round completed for the stall window,
|
|
660
666
|
// three times over, and reissuing the call did not recover it. It borrows both
|
package/src/replay.js
CHANGED
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
import { runBehaviour, formatBehaviour } from './behaviour.js';
|
|
105
105
|
import { shortSha } from './codeversion.js';
|
|
106
106
|
import { SPEND_RECORD_TYPES, chainSpend } from './ledger.js';
|
|
107
|
-
import { legsOf } from './legs.js';
|
|
107
|
+
import { legsOf, capKindOf } from './legs.js';
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* @param {unknown} e
|
|
@@ -234,6 +234,16 @@ const ALL_REASON_MAX = 60;
|
|
|
234
234
|
* record fired) — printed literally, never blank, never a fabricated 0. */
|
|
235
235
|
const NONE_RECORDED = 'none recorded';
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Did this leg end on a cap? Reads the leg's own last escalation and `money-halt` record.
|
|
239
|
+
* @param {import('./legs.js').Leg} leg
|
|
240
|
+
* @returns {'money'|'time'|null}
|
|
241
|
+
*/
|
|
242
|
+
function legCapKind(leg) {
|
|
243
|
+
const esc = leg.outcome === 'escalated' ? leg.records.findLast((r) => r && r.type === 'escalation') : null;
|
|
244
|
+
return capKindOf({ outcome: leg.outcome, category: esc?.category ?? null, moneyHalt: leg.records.some((r) => r && r.type === 'money-halt') });
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
/**
|
|
238
248
|
* `stopReason` for a non-green outcome: `category — decision — detail`, every
|
|
239
249
|
* part present that the escalation actually carries (see the field-site
|
|
@@ -737,11 +747,21 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
737
747
|
// why this leg stopped the step (code-owned words); null for a step that ended itself or is still running
|
|
738
748
|
/** @type {string|null} */
|
|
739
749
|
let stopReason = null;
|
|
750
|
+
// a single-leg run has no leg boundary to cut a step, but a cap still ends it: its open step reads capped too
|
|
751
|
+
// (hamr 2026-10-08), from the same rule (`capKindOf`) the run's own status word uses
|
|
752
|
+
if (!end && myLeg === null) {
|
|
753
|
+
const only = legs[0];
|
|
754
|
+
const capKind = only ? legCapKind(only) : null;
|
|
755
|
+
if (capKind !== null) stopReason = capKind === 'money' ? 'money cap' : 'time cap';
|
|
756
|
+
}
|
|
740
757
|
if (!end && myLeg !== null) {
|
|
741
758
|
const o = myLeg.outcome;
|
|
742
759
|
const isLastLeg = myLegIdx === legs.length - 1;
|
|
743
760
|
if (cutByLeg || (isLastLeg && o !== null && o !== 'green' && o !== 'already-green')) {
|
|
744
|
-
|
|
761
|
+
// hamr 2026-10-08: a leg that ended `escalated` on a money-halt / wall-halt reads as the cap, exactly as the
|
|
762
|
+
// run's own status word does (src/panel/status.js statusFor) — derived HERE so every surface follows
|
|
763
|
+
const capKind = legCapKind(myLeg);
|
|
764
|
+
stopReason = capKind === 'money' ? 'money cap' : capKind === 'time' ? 'time cap' : o === 'provider-red' ? 'provider failed'
|
|
745
765
|
: o === 'step-stalled' ? 'step stalled' : o === 'stopped' ? 'you stopped it' : o === null ? 'died' : o;
|
|
746
766
|
}
|
|
747
767
|
}
|
|
@@ -832,7 +852,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
832
852
|
return {
|
|
833
853
|
id,
|
|
834
854
|
occurrence: idOccurrence.get(id),
|
|
835
|
-
...(myLeg === null ? {} : { stopReason, continued, tryNumber }),
|
|
855
|
+
...(myLeg === null && stopReason === null ? {} : { stopReason, continued, tryNumber }),
|
|
836
856
|
outcome: end ? (end.outcome ?? null) : null,
|
|
837
857
|
...buildOccurrenceMetrics(startSeq, startTs, endSeq, endTs, roundsInWindow),
|
|
838
858
|
checks: { passed, failed },
|
package/types/legs.d.ts
CHANGED
|
@@ -56,6 +56,18 @@ export function watchdogNotePath(spineFile: string, leg: number): string;
|
|
|
56
56
|
* @returns {string}
|
|
57
57
|
*/
|
|
58
58
|
export function stopFilePath(spineFile: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Did a run (or one leg) END on a cap? The ONE spelling of the rule every surface follows (hamr 2026-10-08): the
|
|
61
|
+
* outcome is `cap-halt` / `wall-halt`, or it is `escalated` and the last escalation's category names the cap —
|
|
62
|
+
* `wall-halt`, or `cap-halt` backed by a `money-halt` record (a strike-ladder `cap-halt` is not the money cap).
|
|
63
|
+
* @param {{outcome: string|null|undefined, category?: string|null, moneyHalt?: boolean}} o
|
|
64
|
+
* @returns {'money'|'time'|null}
|
|
65
|
+
*/
|
|
66
|
+
export function capKindOf(o: {
|
|
67
|
+
outcome: string | null | undefined;
|
|
68
|
+
category?: string | null;
|
|
69
|
+
moneyHalt?: boolean;
|
|
70
|
+
}): "money" | "time" | null;
|
|
59
71
|
/** The marker record's `type`. */
|
|
60
72
|
export const LEG_RESUME: "leg-resume";
|
|
61
73
|
export type Leg = {
|
package/types/panel/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `[✓]`/`[✗]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
2
|
+
* `[✓]`/`[✗]`/`[■]`/`[▶]` — the ONLY vocabulary a result is ever rendered in
|
|
3
3
|
* (auto-memory `ui-verdict-words.md`: never the words green/red/soft-green
|
|
4
4
|
* anywhere in the page). `null` (no `job-end` reached — a killed-mid-run or
|
|
5
5
|
* still-running spine) reads `▶` — the same "in progress / unresolved" glyph
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* running attempt and an archived spine that never reached its own end are
|
|
8
8
|
* the same fact from this read-only side: no verdict has been recorded yet.
|
|
9
9
|
* @param {string|null} outcome
|
|
10
|
-
* @returns {'✓'|'✗'|'▶'}
|
|
10
|
+
* @returns {'✓'|'✗'|'■'|'▶'}
|
|
11
11
|
*/
|
|
12
|
-
export function glyphForOutcome(outcome: string | null): "\u2713" | "\u2717" | "\u25B6";
|
|
12
|
+
export function glyphForOutcome(outcome: string | null): "\u2713" | "\u2717" | "\u25A0" | "\u25B6";
|
|
13
13
|
/**
|
|
14
14
|
* `deterministic` (green) / `rubric` (soft-green) / `unknown` (a class this
|
|
15
15
|
* reader doesn't recognize) — the settled UI wording (PANEL-BUILD.md §6),
|
|
@@ -381,7 +381,7 @@ export function reuseLine(record: {
|
|
|
381
381
|
* to copy: change the job: Clear the card). `null` when the runid is not listed.
|
|
382
382
|
* @param {string} runid
|
|
383
383
|
* @param {{ home?: string, model?: string }} [opts] `model` = the Model box's current Name (the estimate counts only runs on that worker); absent = the card's own Model
|
|
384
|
-
* @returns {{ok: true, origin: {runid: string, job: string}, card: Record<string, any>, from: 'signed job',
|
|
384
|
+
* @returns {{ok: true, origin: {runid: string, job: string}, outcome: string|null, card: Record<string, any>, from: 'signed job',
|
|
385
385
|
* locked: readonly string[], open: readonly string[], note: string|null,
|
|
386
386
|
* specHash: string, workflowKey: string, spec: any, specPath: string,
|
|
387
387
|
* trackRecord: ReturnType<typeof trackRecordFor>, line: string}|{ok: false, error: string}|null}
|
|
@@ -395,6 +395,7 @@ export function getStartFrom(runid: string, opts?: {
|
|
|
395
395
|
runid: string;
|
|
396
396
|
job: string;
|
|
397
397
|
};
|
|
398
|
+
outcome: string | null;
|
|
398
399
|
card: Record<string, any>;
|
|
399
400
|
from: "signed job";
|
|
400
401
|
locked: readonly string[];
|
package/types/panel/status.d.ts
CHANGED
package/types/ralph.d.ts
CHANGED
|
@@ -242,9 +242,13 @@ export function boundGap(s: string, keepPattern?: string): string;
|
|
|
242
242
|
* (PRD item 27/M3 Part B), forwarded to the raw `close` path's `runClose`
|
|
243
243
|
* call unchanged; a caller supplying `judge` instead already closed over
|
|
244
244
|
* its own `closeDir` and this param does nothing for it.
|
|
245
|
+
* @param {string} [opts.phase] the phase this loop runs in, when it is not the step loop
|
|
246
|
+
* (the fix loop passes `'fix'`): the person's-stop text then names it ("during the fix
|
|
247
|
+
* phase") instead of saying a resume re-enters "this step", which only the step loop has.
|
|
248
|
+
* Text only — it changes no category, outcome or option.
|
|
245
249
|
* @returns {Promise<'green'|'escalated'>}
|
|
246
250
|
*/
|
|
247
|
-
export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir }: {
|
|
251
|
+
export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, closeTimeoutMs, cwd, expect, judged, gapKeep, workerWrites, closeDir, phase }: {
|
|
248
252
|
middle: (iteration: number, gap?: string) => void | Promise<void>;
|
|
249
253
|
close?: string[] | undefined;
|
|
250
254
|
judge?: (() => Promise<{
|
|
@@ -270,6 +274,7 @@ export function ralph({ middle, close, judge, capRuns, ladder, emit, redact, clo
|
|
|
270
274
|
gapKeep?: string | undefined;
|
|
271
275
|
workerWrites?: (() => string[]) | undefined;
|
|
272
276
|
closeDir?: string | null | undefined;
|
|
277
|
+
phase?: string | undefined;
|
|
273
278
|
}): Promise<"green" | "escalated">;
|
|
274
279
|
/**
|
|
275
280
|
* What the close child must NOT be able to read (operator ruling, 2026-07-31:
|