bareloop 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/bareloop.context.md +52 -13
  3. package/package.json +2 -2
  4. package/src/authorflow.js +26 -11
  5. package/src/authoring.js +1 -1
  6. package/src/authorjob.js +27 -15
  7. package/src/authorreadout.js +7 -6
  8. package/src/declaredclose.js +4 -1
  9. package/src/draftspend.js +20 -2
  10. package/src/facts.js +54 -9
  11. package/src/factwords.js +58 -6
  12. package/src/inputlook.js +38 -3
  13. package/src/job.js +1 -1
  14. package/src/kinds.js +5 -0
  15. package/src/pagedropwords.js +64 -0
  16. package/src/pagemap.js +264 -0
  17. package/src/pagerules.js +319 -0
  18. package/src/panel/authorroutes.js +34 -30
  19. package/src/panel/authorsession.js +29 -5
  20. package/src/panel/index.html +465 -65
  21. package/src/panel/jobplan.js +4 -2
  22. package/src/panel/runroutes.js +46 -22
  23. package/src/panel/server.js +791 -28
  24. package/src/panel/status.js +33 -0
  25. package/src/panelmoney.js +29 -0
  26. package/src/planrun.js +53 -4
  27. package/src/promptregisters.js +4 -0
  28. package/src/replay.js +118 -7
  29. package/src/rubricstep.js +123 -11
  30. package/src/runlist.js +2 -1
  31. package/src/schedulewords.js +6 -3
  32. package/src/source.js +18 -0
  33. package/src/thinking.js +63 -2
  34. package/src/userrun.js +28 -14
  35. package/types/authorflow.d.ts +42 -6
  36. package/types/authorjob.d.ts +10 -2
  37. package/types/declaredclose.d.ts +2 -0
  38. package/types/draftspend.d.ts +27 -1
  39. package/types/facts.d.ts +15 -0
  40. package/types/factwords.d.ts +26 -3
  41. package/types/inputlook.d.ts +15 -0
  42. package/types/kinds.d.ts +5 -0
  43. package/types/pagedropwords.d.ts +26 -0
  44. package/types/pagemap.d.ts +154 -0
  45. package/types/pagerules.d.ts +86 -0
  46. package/types/panel/authorroutes.d.ts +17 -14
  47. package/types/panel/runroutes.d.ts +2 -2
  48. package/types/panel/server.d.ts +155 -2
  49. package/types/panel/status.d.ts +51 -0
  50. package/types/panelmoney.d.ts +5 -0
  51. package/types/replay.d.ts +21 -1
  52. package/types/rubricstep.d.ts +57 -1
  53. package/types/runlist.d.ts +4 -1
  54. package/types/schedulewords.d.ts +4 -2
  55. package/types/source.d.ts +11 -0
  56. package/types/thinking.d.ts +39 -0
  57. package/types/userrun.d.ts +4 -0
@@ -272,7 +272,7 @@
272
272
  are already both on screen there, nothing to jump back to). */
273
273
  .back-to-list-link{display:none;font-size:12px;color:var(--accent);text-decoration:underline;background:none;border:none;padding:0;cursor:pointer;}
274
274
  @media (max-width: 899px){ .back-to-list-link{display:inline-block;} }
275
- .rp-tabrow{display:flex;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:rtabnum;}
275
+ .rp-tabrow{display:flex;flex-wrap:wrap;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:rtabnum;}
276
276
  .rp-tabrow button{padding:10px 14px;min-height:44px;background:transparent;border:none;border-right:1px dotted var(--border-strong);font-size:13px;color:var(--text-dim);counter-increment:rtabnum;}
277
277
  .rp-tabrow button::before{content:"[" counter(rtabnum) "] ";color:var(--text-faint);font-weight:700;}
278
278
  .tabrow button[aria-selected="true"],.rp-tabrow button[aria-selected="true"]{background:var(--panel);color:var(--accent);font-weight:700;box-shadow:inset 0 -2px 0 var(--accent);}
@@ -394,6 +394,27 @@
394
394
  one) covers every table inside that wrapper, Flat's included. */
395
395
  .audit-table-scroll table th{position:sticky;top:0;z-index:1;}
396
396
  .attempt-check{margin-top:4px;font-size:12px;}
397
+ /* PRD v1.107 Part 6: the Result tab. Boxes are the Job tab's own (.job-card, .field, .ro-value, .field-row); text wraps, nothing scrolls the page. */
398
+ #result-body{min-width:0;overflow-wrap:anywhere;}
399
+ .rs-ok{color:var(--green);font-weight:700;}
400
+ #result-body mark{background:var(--rl-accent-bg-strong);color:var(--text);border-radius:2px;padding:0 1px;}
401
+ #result-door-card{border-color:var(--amber);}
402
+ #result-door-card .st-word{color:var(--amber);}
403
+ #result-door-card textarea{width:100%;min-height:60px;margin:6px 0;background:var(--bg);color:var(--text);border:1px solid var(--text-dim);font:inherit;padding:6px;}
404
+ #result-body details>summary{cursor:pointer;color:var(--text-dim);min-height:28px;}
405
+ .rs-link{background:none;border:none;padding:0;min-height:44px;color:var(--accent);text-decoration:underline;font:inherit;cursor:pointer;}
406
+ .rs-warn{color:var(--amber);overflow-wrap:anywhere;}
407
+ #result-door-card .stopping{color:var(--amber);font-size:13px;overflow-wrap:anywhere;}
408
+ /* PRD v1.106 Part 5 C: the Audit step fold (tries, their numbered checks, the Round table). Every line wraps; nothing forces a page-wide scroll. */
409
+ .actor-mark{color:var(--text-faint);font-size:12px;margin-left:6px;white-space:nowrap;}
410
+ .step-tally{font-size:12px;color:var(--text-dim);overflow-wrap:anywhere;margin:2px 0 6px;}
411
+ .try-line{margin:10px 0 2px;overflow-wrap:anywhere;font-weight:700;}
412
+ .try-check,.try-judged{margin:2px 0 2px 12px;font-size:13px;overflow-wrap:anywhere;}
413
+ .try-judged{font-size:12px;color:var(--text-dim);}
414
+ .try-reasons{margin:2px 0 4px 12px;font-size:12px;color:var(--text-dim);}
415
+ .try-reasons summary{cursor:pointer;}
416
+ .try-reasons div{margin:2px 0 2px 8px;overflow-wrap:anywhere;}
417
+ .step-rounds-note{font-size:12px;color:var(--text-faint);margin:8px 0 2px;overflow-wrap:anywhere;}
397
418
  .rounds-page-info{margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
398
419
 
399
420
  /* build item C: Audit tab grouped-by-part rows */
@@ -670,6 +691,7 @@
670
691
  .topbar{flex-direction:column;align-items:flex-start;}
671
692
  .topbar-right{width:100%;justify-content:space-between;}
672
693
  .btn.small{height:44px;min-height:44px;padding:0 12px;}
694
+ .rs-link{min-height:44px;}
673
695
  .chip{min-height:44px;}
674
696
  .chip.fchip{min-height:40px;min-width:40px;}
675
697
  .rp-header{margin:8px 8px 0;}
@@ -844,8 +866,9 @@
844
866
  </div>
845
867
  <div id="rl-top" data-testid="rl-top"></div>
846
868
  <div class="rl-dash"></div>
847
- <div class="rl-faint" data-testid="rl-filter-note">filters below act on Jobs and quiet jobs only</div>
869
+ <div class="rl-faint" data-testid="rl-filter-note">search also finds Scheduled; &#9719; shows only Scheduled</div>
848
870
  <div id="runs-filterbar-anchor"></div>
871
+ <div id="rl-sched" data-testid="rl-sched"></div>
849
872
  <div id="wf-list" data-testid="wf-list"></div>
850
873
  <div id="import-list" data-testid="import-list"></div>
851
874
  </section>
@@ -867,6 +890,7 @@
867
890
  <button role="tab" id="tab-run" aria-controls="panel-run" aria-selected="true" data-testid="right-tab-run">Run</button>
868
891
  <button role="tab" id="tab-audit" aria-controls="panel-audit" aria-selected="false" data-testid="right-tab-audit">Audit / logs</button>
869
892
  <button role="tab" id="tab-details" aria-controls="panel-details" aria-selected="false" data-testid="right-tab-details">Job</button>
893
+ <button role="tab" id="tab-result" aria-controls="panel-result" aria-selected="false" data-testid="right-tab-result" hidden>Result</button>
870
894
  </div>
871
895
 
872
896
  <div class="pane-body" id="right-pane-body">
@@ -880,6 +904,7 @@
880
904
  <div id="ended-block" class="ended-block" data-testid="ended-block" hidden></div>
881
905
  </div>
882
906
  <div class="run-actions" id="run-actions" data-testid="run-actions" hidden></div>
907
+ <div class="hint" id="result-pointer" data-testid="result-pointer" hidden></div>
883
908
  <div class="counters" id="run-counters" data-testid="live-counters"></div>
884
909
  <div class="summary-box" id="run-summary" data-testid="run-summary"></div>
885
910
  <div class="map-box" data-testid="step-map" id="map-box"><div id="map-mount"></div></div>
@@ -905,6 +930,7 @@
905
930
  <div id="audit-flat-wrap" hidden>
906
931
  <div id="audit-empty" class="hint" data-testid="audit-empty" style="font-size:13px;padding:24px 0;">select a run first</div>
907
932
  <div id="audit-content" data-testid="audit-content" hidden>
933
+ <div class="hint" data-testid="audit-flat-roundnote" style="overflow-wrap:anywhere;margin-bottom:6px;">Round numbers here count the whole run. In the Grouped view each step counts its own rounds from 1.</div>
908
934
  <div class="audit-table-scroll">
909
935
  <table data-testid="audit-table">
910
936
  <thead><tr><th>Round</th><th>Step</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead>
@@ -925,7 +951,10 @@
925
951
  <div class="job-card" id="job-card-readonly" hidden data-testid="job-card-readonly">
926
952
  <h4 id="details-title">Job card — signed</h4>
927
953
  <div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
928
- <div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
954
+ <div class="field-row">
955
+ <div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
956
+ <div class="field" id="details-thinking-field"><label>Thinking</label><div class="ro-value" id="details-thinking" data-testid="details-thinking">—</div></div>
957
+ </div>
929
958
  <div class="field" id="details-started-field" hidden><label>Started</label><div class="ro-value" id="details-started" data-testid="details-started" style="overflow-wrap:anywhere;">—</div></div>
930
959
  <div class="field" id="details-schedule-field" hidden><label>Schedule</label><div class="ro-value" id="details-schedule" data-testid="details-schedule" style="overflow-wrap:anywhere;">—</div></div>
931
960
  <div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
@@ -942,6 +971,11 @@
942
971
  </div>
943
972
  </section>
944
973
 
974
+ <section id="panel-result" class="tabpanel" role="tabpanel" aria-labelledby="tab-result" hidden data-testid="panel-result">
975
+ <div id="result-empty" class="hint" style="font-size:13px;padding:24px 0;">select a run first</div>
976
+ <div id="result-body" data-testid="result-body" hidden></div>
977
+ </section>
978
+
945
979
  </div>
946
980
  </section>
947
981
 
@@ -1067,7 +1101,7 @@
1067
1101
  });
1068
1102
  }
1069
1103
  wireTabs([document.getElementById("tab-chat"), document.getElementById("tab-runs")]);
1070
- wireTabs([document.getElementById("tab-run"), document.getElementById("tab-audit"), document.getElementById("tab-details")]);
1104
+ wireTabs([document.getElementById("tab-run"), document.getElementById("tab-audit"), document.getElementById("tab-details"), document.getElementById("tab-result")]);
1071
1105
 
1072
1106
  // ---- fetch helpers ----
1073
1107
  function getJSON(path){
@@ -1167,7 +1201,7 @@
1167
1201
  // result when nothing was ever judged there; `step`/`fix` (real attempts)
1168
1202
  // and `run` (its own real, non-null outcome) are the only kinds worth an
1169
1203
  // inline attempt/result glyph.
1170
- function partHasNoVerdict(part){ return part.kind === "scout" || part.kind === "plan" || part.kind === "replan" || part.kind === "judge"; }
1204
+ function partHasNoVerdict(part){ return part.kind === "scout" || part.kind === "plan" || part.kind === "replan" || part.kind === "judge" || part.kind === "door"; }
1171
1205
  // ONE shared result-glyph rule for both the Run tab's cards and the Audit
1172
1206
  // tab's grouped-row headers — `null` (never a fabricated "?") for a
1173
1207
  // no-verdict part kind; a multi-attempt part (a step with retries, or the
@@ -1383,7 +1417,7 @@
1383
1417
  // manual fold memory, keyed by job ("__quiet" is the quiet fold): a per-viewer convenience, reset on reload. A manual click wins over an automatic open.
1384
1418
  var rlOpen = {};
1385
1419
  // the last markup written into each box: an identical repaint (every poll) writes nothing, so focus, scroll and a half-read card survive it
1386
- var rlLast = { top: null, jobs: null };
1420
+ var rlLast = { top: null, sched: null, jobs: null };
1387
1421
  // The ONE writer of `rlData`: the whole /api/runs payload, as the server sent it.
1388
1422
  function setRunsData(r){
1389
1423
  rlData = { runs: r.runs || [], jobs: r.jobs || [], scheduled: r.scheduled || [], limits: r.limits || rlData.limits };
@@ -1404,7 +1438,7 @@
1404
1438
  function rlRunParts(r){
1405
1439
  var rn = runLabel(r.runNo, r.runid);
1406
1440
  var p = { rn: rn, name: r.job + " (" + rn + ")", date: runAtText(r), model: r.model || "", word: "", why: "", spend: "", wall: "" };
1407
- if(r.starting){ p.word = "running"; p.why = "starting…"; return p; }
1441
+ if(r.starting){ p.word = "running"; p.why = r.startingWord || "starting…"; return p; }
1408
1442
  if(r.fileMissing){ p.word = ""; p.why = "file missing"; return p; }
1409
1443
  p.word = r.status && r.status.word ? r.status.word : "";
1410
1444
  p.why = r.endedLine || (p.word === "passed" ? "goal met" : "");
@@ -1473,8 +1507,19 @@
1473
1507
  ));
1474
1508
  }
1475
1509
 
1510
+ // PRD v1.106 Part 2 (fwd M11 am17-am20): the one search box narrows Scheduled cards by JOB NAME only. A run number (run-3) or a date (10/13, 2026-10-13, 10/)
1511
+ // narrows Jobs only, so those words are skipped here; every other word must be a substring of the job name. The result and time chips never touch Scheduled.
1512
+ function rlScheduledMatches(card, query){
1513
+ var name = String(card && card.job || "").toLowerCase();
1514
+ var words = String(query || "").toLowerCase().replace(/[()]/g, "").split(/\s+/).filter(function(w){ return w; });
1515
+ return words.every(function(w){
1516
+ if(/^run-\d+$/.test(w) || (/^[\d\/-]+$/.test(w) && /[\/-]/.test(w))) return true;
1517
+ return name.indexOf(w) !== -1;
1518
+ });
1519
+ }
1476
1520
  function rlModel(data, groups, filters, limits, sel, open){
1477
1521
  var active = filtersActive(filters);
1522
+ var schedOnly = !!(filters && filters.scheduledOnly);
1478
1523
  var place = {};
1479
1524
  (data.jobs || []).forEach(function(j, i){ place[j.job] = { quiet: j.quiet === true, at: i }; });
1480
1525
  var cards = groups.slice().sort(function(a, b){
@@ -1504,10 +1549,11 @@
1504
1549
  return {
1505
1550
  running: data.runs.filter(function(r){ return r.live === true; }),
1506
1551
  needs: data.runs.filter(function(r){ return typeof r.needs === "string" && r.needs !== ""; }),
1507
- scheduled: data.scheduled || [],
1552
+ scheduled: (data.scheduled || []).filter(function(c){ return rlScheduledMatches(c, filters && filters.search); }),
1553
+ schedOnly: schedOnly,
1508
1554
  gaveUp: data.gaveUp || [],
1509
- jobs: cards.filter(function(c){ return !c.quiet; }),
1510
- quiet: quietCards,
1555
+ jobs: schedOnly ? [] : cards.filter(function(c){ return !c.quiet; }),
1556
+ quiet: schedOnly ? [] : quietCards,
1511
1557
  quietOpen: Object.prototype.hasOwnProperty.call(open, "__quiet") ? open.__quiet : quietCards.some(function(c){ return c.selected; }),
1512
1558
  limits: limits,
1513
1559
  noRows: data.runs.length === 0,
@@ -1545,7 +1591,7 @@
1545
1591
  }
1546
1592
  // Scheduled card (T Part 7): `[·] job today 15:00` / `next in 3h · Every day at 15:00`, all from the server. A file trigger has no clock time:
1547
1593
  // `at` / `in` are null and nothing is made up. A click does what Running now does (rlJump to the job's HEAD run, `runId` from the server and
1548
- // scrolls to its job). A job with no run to open stays a plain box. Never filtered or searched.
1594
+ // scrolls to its job). A job with no run to open stays a plain box. Drawn BELOW the search box (rlSchedHtml); the search and the \u25F7 chip narrow it by job name.
1549
1595
  function rlScheduledCardHtml(s){
1550
1596
  var body = '<span class="rl-l1"><span class="dot grey"></span><span class="rl-nm" title="' + escapeXml(s.job) + '">' + escapeXml(s.job) + '</span>' + rlRunTagHtml(s) +
1551
1597
  (s.at ? ' <span class="rl-rest">&middot; ' + escapeXml(s.at) + '</span>' : '') + '</span>' +
@@ -1553,17 +1599,25 @@
1553
1599
  if(typeof s.runId !== "string" || s.runId === "") return '<div class="rl-card" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</div>';
1554
1600
  return '<button class="rl-card" type="button" data-rl-jump="' + escapeXml(s.runId) + '" data-testid="rl-sched-' + escapeXml(s.job) + '">' + body + '</button>';
1555
1601
  }
1556
- // The sections above the filters, from ONE ordered list: the Scheduled section (PRD v1.99) is one more entry here after Needs you, plus one draw function.
1602
+ // The sections above the search box, from ONE ordered list: only never-filtered sections live here (Scheduled is searched, so rlSchedHtml draws it below).
1557
1603
  function rlTopHtml(m){
1558
1604
  var sections = [
1559
1605
  { label: "Running now", items: m.running, draw: function(r){ return rlJumpCardHtml(r, false); } },
1560
1606
  { label: "Needs you", items: m.needs.concat(m.gaveUp || []), draw: function(r){ return r.pill ? rlGaveUpCardHtml(r) : rlJumpCardHtml(r, true); } },
1561
- { label: "Scheduled", items: m.scheduled, draw: rlScheduledCardHtml },
1562
1607
  ];
1563
1608
  return sections.map(function(s){
1564
1609
  return s.items.length ? rlSectHtml(s.label, s.items.length) + s.items.map(s.draw).join("") : "";
1565
1610
  }).join("");
1566
1611
  }
1612
+ // Scheduled sits BELOW the search box (PRD v1.106 Part 2). No match draws nothing, header included; with the \u25F7 chip pressed it says so in words.
1613
+ function rlSchedHtml(m){
1614
+ if(m.scheduled.length) return rlSectHtml("Scheduled", m.scheduled.length) + m.scheduled.map(rlScheduledCardHtml).join("");
1615
+ return m.schedOnly ? '<div class="hint" data-testid="rl-sched-none">no scheduled job matches</div>' : "";
1616
+ }
1617
+ // the count line under the chips: only the \u25F7 view has one; "showing N scheduled job(s)". Empty otherwise.
1618
+ function rlCountText(m){
1619
+ return m.schedOnly ? "showing " + m.scheduled.length + " scheduled job" + (m.scheduled.length === 1 ? "" : "s") : "";
1620
+ }
1567
1621
  // a run card inside a job: `[sign] run-N MM/DD HH:MM · model` / `word — why · $cost · wall`
1568
1622
  function rlRunCardHtml(r, selected, hit){
1569
1623
  var p = rlRunParts(r);
@@ -1622,6 +1676,7 @@
1622
1676
  return out + '</div></div>';
1623
1677
  }
1624
1678
  function rlJobsHtml(m){
1679
+ if(m.schedOnly) return "";
1625
1680
  var h = "";
1626
1681
  if(m.jobs.length) h += rlSectHtml("Jobs", m.jobs.length, "· run in the last " + m.limits.quietDays + " days") + m.jobs.map(rlJobCardHtml).join("");
1627
1682
  if(m.quiet.length){
@@ -1660,9 +1715,14 @@
1660
1715
  var m = rlModel(rlData, groups, filters, rlData.limits, { runid: currentRunid }, rlOpen);
1661
1716
  var topEl = document.getElementById("rl-top");
1662
1717
  var listEl = document.getElementById("wf-list");
1718
+ var schedEl = document.getElementById("rl-sched");
1719
+ var countEl = document.getElementById("runs-filter-count");
1720
+ var sched = rlSchedHtml(m);
1663
1721
  var top = rlTopHtml(m);
1664
1722
  var jobs = rlJobsHtml(m);
1665
1723
  if(top !== rlLast.top){ rlLast.top = top; withFocusPreserved(topEl, function(){ topEl.innerHTML = top; }); }
1724
+ if(sched !== rlLast.sched){ rlLast.sched = sched; withFocusPreserved(schedEl, function(){ schedEl.innerHTML = sched; }); }
1725
+ if(countEl && (rlCountText(m) !== "" || /^showing /.test(countEl.textContent || ""))) countEl.textContent = rlCountText(m);
1666
1726
  if(jobs !== rlLast.jobs){ rlLast.jobs = jobs; withFocusPreserved(listEl, function(){ listEl.innerHTML = jobs; }); }
1667
1727
  tickSince();
1668
1728
  }
@@ -1715,6 +1775,7 @@
1715
1775
  { group: "result", value: "\u2717", title: "fail", id: "res-fail", label: "&#10007;" },
1716
1776
  { group: "result", value: "?", title: "died", id: "res-died", label: "?" },
1717
1777
  { group: "result", value: "\u25B6", title: "running", id: "res-running", label: "&#9654;" },
1778
+ { group: "view", value: "scheduled", title: "scheduled jobs only", id: "view-scheduled", label: "&#9719;", aria: "scheduled jobs only" },
1718
1779
  { group: "time", value: "7d", title: "last 7 days", id: "time-7d", label: "7d" },
1719
1780
  { group: "time", value: "30d", title: "last 30 days", id: "time-30d", label: "30d" },
1720
1781
  { group: "time", value: "all", title: "all time", id: "time-all", label: "all", pressed: true },
@@ -1728,7 +1789,7 @@
1728
1789
  '<div class="chips" role="group" aria-label="Filter">' +
1729
1790
  RL_CHIPS.map(function(c){
1730
1791
  if(c.clear) return '<button class="chip fchip" type="button" id="' + scope + '-filter-clear" data-testid="' + scope + '-filter-clear">clear</button>';
1731
- return '<button class="chip fchip" type="button" aria-pressed="' + (c.pressed ? 'true' : 'false') + '" data-filter-group="' + c.group + '" data-filter-value="' + escapeXml(c.value) + '" title="' + c.title + '" data-testid="chip-' + scope + '-' + c.id + '">' + c.label + '</button>';
1792
+ return '<button class="chip fchip" type="button" aria-pressed="' + (c.pressed ? 'true' : 'false') + '" data-filter-group="' + c.group + '" data-filter-value="' + escapeXml(c.value) + '" title="' + c.title + '"' + (c.aria ? ' aria-label="' + c.aria + '"' : '') + ' data-testid="chip-' + scope + '-' + c.id + '">' + c.label + '</button>';
1732
1793
  }).join("") +
1733
1794
  '</div>' +
1734
1795
  '<div class="hint" id="' + scope + '-filter-count" data-testid="' + scope + '-filter-count"></div>' +
@@ -1769,7 +1830,7 @@
1769
1830
  function createFilterBar(opts){
1770
1831
  var scope = opts.scope;
1771
1832
  var storageKey = opts.storageKey;
1772
- var state = { checkTypes: [], results: [], time: "all", search: "" };
1833
+ var state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
1773
1834
 
1774
1835
  function load(){
1775
1836
  try{
@@ -1782,6 +1843,7 @@
1782
1843
  results: Array.isArray(parsed.results) ? parsed.results : [],
1783
1844
  time: typeof parsed.time === "string" ? parsed.time : "all",
1784
1845
  search: typeof parsed.search === "string" ? parsed.search : "",
1846
+ scheduledOnly: parsed.scheduledOnly === true,
1785
1847
  };
1786
1848
  }
1787
1849
  }
@@ -1796,6 +1858,7 @@
1796
1858
  var value = chip.getAttribute("data-filter-value");
1797
1859
  var active;
1798
1860
  if(group === "time"){ active = state.time === value; }
1861
+ else if(group === "view"){ active = state.scheduledOnly === true; }
1799
1862
  else if(group === "checkType"){ active = state.checkTypes.indexOf(value) !== -1; }
1800
1863
  else { active = state.results.indexOf(value) !== -1; }
1801
1864
  chip.setAttribute("aria-pressed", active ? "true" : "false");
@@ -1812,6 +1875,8 @@
1812
1875
  var value = chip.getAttribute("data-filter-value");
1813
1876
  if(group === "time"){
1814
1877
  state.time = value; // single-select within time
1878
+ } else if(group === "view"){
1879
+ state.scheduledOnly = !state.scheduledOnly; // the \u25F7 chip: Scheduled only, a toggle
1815
1880
  } else if(group === "checkType"){
1816
1881
  var idx = state.checkTypes.indexOf(value);
1817
1882
  if(idx === -1) state.checkTypes.push(value); else state.checkTypes.splice(idx, 1);
@@ -1827,7 +1892,7 @@
1827
1892
  var clearBtn = document.getElementById(scope + "-filter-clear");
1828
1893
  if(clearBtn){
1829
1894
  clearBtn.addEventListener("click", function(){
1830
- state = { checkTypes: [], results: [], time: "all", search: "" };
1895
+ state = { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
1831
1896
  paint();
1832
1897
  save();
1833
1898
  apply();
@@ -1859,7 +1924,7 @@
1859
1924
  onChange: function(){ paintRunsList(); },
1860
1925
  });
1861
1926
  function currentRunsFilters(){
1862
- return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "" };
1927
+ return runsFilterBar ? runsFilterBar.getState() : { checkTypes: [], results: [], time: "all", search: "", scheduledOnly: false };
1863
1928
  }
1864
1929
 
1865
1930
  // item 4: phone-only "scroll the run view into view" on row select, and
@@ -2145,38 +2210,30 @@
2145
2210
  document.getElementById("tab-runs").addEventListener("click", function(){ loadImports(); });
2146
2211
 
2147
2212
  // ---- Run tab ----
2148
- // panelMoney — hamr's ruling 2026-09-28 ("panel money 2-decimals"): every
2149
- // money display in THIS page (Run tab summary/counters, Runs list rows,
2150
- // Audit header/part cards, Job tab, chat cost lines) renders through this
2151
- // ONE function — no per-site rounding. The CLI's own 4-decimal `money()`
2152
- // (src/replay.js) is UNCHANGED; this is the panel-only render, the
2153
- // browser-script sibling of `src/panel/authorroutes.js`'s own
2154
- // `panelMoney2` (same rule, can't share code — one runs in Node, one
2155
- // inlined in the page).
2213
+ // panelMoney — the panel's ONE money render; every money display in THIS page (Run tab summary/
2214
+ // counters, Runs list rows, Result/Audit/Job tabs, chat cost lines) goes through it, no per-site
2215
+ // rounding. Browser-script copy of `src/panelmoney.js` `panelMoney2` (a page cannot import a
2216
+ // module); tests/panel-page.test.js pins the two to identical output. hamr's ruling 2026-10-19
2217
+ // (PRD "Part 6 rulings" (3)), superseding the 2026-09-28 "<$0.01" spelling:
2156
2218
  // - null/undefined/non-finite -> "unknown" (never a fabricated $0).
2157
- // - a REAL positive amount below a cent -> "<$0.01", never "$0.00" (a
2158
- // tiny real cost must never read as free).
2159
- // - `floor` (this is a FLOOR/"at least" figure, never exact) rounds DOWN
2160
- // to 2 decimals — never overstates a floor as exact-higher.
2161
- // - otherwise rounds half-up to 2 decimals (an exact zero prints
2162
- // "$0.00" — nothing spent is a real number, not an unknown).
2163
- // Two-step rounding guards against float-representation noise on BOTH
2164
- // sides: dollar figures here are derived from at most 6-decimal-precision
2165
- // money (the same precision `src/text.js`'s `tallyCalls`/`priceOf` use),
2166
- // so rounding to 6 decimals first erases that noise (e.g. 0.815 really
2167
- // stored as 0.81499999999999995), and ONLY THEN is the value rounded/
2168
- // floored to cents — a value already exactly on a cent boundary (e.g.
2169
- // 0.82) can be stored as 81.99999999999999 cents post-multiply, and a
2170
- // floor there would wrongly drop a whole cent; the tiny `+1e-6` nudge (at
2171
- // dollar-cents scale, far below anything a real half-cent tie could be)
2172
- // corrects that without ever crossing a true half-cent boundary the wrong
2173
- // way.
2219
+ // - a REAL positive amount under a cent -> 4 decimals ("$0.0054"); one too small for 4
2220
+ // decimals reads "<$0.0001", never "$0.00"; one that rounds up to a cent reads "$0.01".
2221
+ // - $0.01 or more -> 2 decimals, half-up; an exact zero prints "$0.00".
2222
+ // - `floor` (a FLOOR/"at least" figure) rounds DOWN, at 2 decimals, or 4 under a cent.
2223
+ // Rounding to 6 decimals first erases float noise (0.815 stored as 0.81499999999999995); the tiny
2224
+ // `+1e-6` nudge corrects a value on an exact boundary stored just under it, without ever
2225
+ // crossing a true half-unit tie the wrong way.
2174
2226
  function panelMoney(n, floor){
2175
2227
  if(n === null || n === undefined) return "unknown";
2176
2228
  var v = Number(n);
2177
2229
  if(!isFinite(v)) return "unknown";
2178
- if(v > 0 && v < 0.01) return "<$0.01";
2179
2230
  var clean = Math.round(v * 1e6) / 1e6;
2231
+ if(clean > 0 && clean < 0.01){
2232
+ var units = floor ? Math.floor(clean * 1e4 + 1e-6) : Math.round(clean * 1e4 + 1e-6);
2233
+ if(units <= 0) return "<$0.0001";
2234
+ if(units >= 100) return "$0.01";
2235
+ return "$" + (units / 1e4).toFixed(4);
2236
+ }
2180
2237
  var cents = floor ? Math.floor(clean * 100 + 1e-6) : Math.round(clean * 100 + 1e-6);
2181
2238
  if(cents < 0) cents = 0;
2182
2239
  return "$" + (cents / 100).toFixed(2);
@@ -2199,8 +2256,16 @@
2199
2256
  var core = (spendCoreVal === null || spendCoreVal === undefined)
2200
2257
  ? "unknown"
2201
2258
  : ((spendCoreFloor || draftFloor) ? "at least " + panelMoney(spendCoreVal) : panelMoney(spendCoreVal));
2202
- var draftPart = hasDraft ? (shortForm ? " (drafting " : " (") + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + (shortForm ? ")" : " drafting)") : "";
2203
2259
  var capPart = (budgetUsd === undefined) ? "" : (" of " + panelMoney(budgetUsd));
2260
+ if(hasDraft && !shortForm){
2261
+ // PRD v1.106 Part 5 Q4, the panel's spelling of src/replay.js moneyWithDraft: "$0.18 run + $0.0035 drafting = $0.18". The run's figure
2262
+ // EXCLUDES drafting; each part keeps the panel's money rule (panelMoney) and the TOTAL is that rule applied to the true sum (so
2263
+ // two shown parts can round to a total that differs by a cent). Without the run's number the sum is left out.
2264
+ var haveRun = typeof spendCoreVal === "number";
2265
+ var totalPart = haveRun ? " = " + ((spendCoreFloor || draftFloor) ? "at least " : "") + panelMoney(spendCoreVal + draftSpentUsd) : "";
2266
+ return core + " run + " + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + " drafting" + totalPart + capPart;
2267
+ }
2268
+ var draftPart = hasDraft ? " (drafting " + (draftFloor ? "at least " : "") + panelMoney(draftSpentUsd) + ")" : "";
2204
2269
  return core + draftPart + capPart;
2205
2270
  }
2206
2271
  // P5 item 6: a LIVE row (no job-end yet, runner alive) reads the same running floors the
@@ -2478,6 +2543,19 @@
2478
2543
  document.dispatchEvent(new CustomEvent("bareloop-resume", {detail: {error: "could not reach the panel: " + x.message}}));
2479
2544
  });
2480
2545
  }
2546
+ // ONE Accept handler for the Run tab's action bar and the Result tab's door (PRD v1.107 Part 6): same route, same words. `host` is where a refusal is written.
2547
+ function acceptDoor(detail, accBtn, host){
2548
+ accBtn.disabled = true;
2549
+ authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/accept", {}).then(function(res){
2550
+ if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
2551
+ if(res.status === 202 && res.body && res.body.pending){
2552
+ host.insertAdjacentHTML("beforeend", '<span class="stopping" data-testid="accept-pending">still re-checking the tree; look again in a moment</span>');
2553
+ return;
2554
+ }
2555
+ accBtn.disabled = false;
2556
+ host.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
2557
+ }).catch(function(x){ accBtn.disabled = false; host.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
2558
+ }
2481
2559
  function renderRunActions(detail){
2482
2560
  var bar = document.getElementById("run-actions");
2483
2561
  var html = "";
@@ -2544,20 +2622,7 @@
2544
2622
  });
2545
2623
  }
2546
2624
  var accBtn = bar.querySelector('[data-testid="btn-accept-run"]');
2547
- if(accBtn){
2548
- accBtn.addEventListener("click", function(){
2549
- accBtn.disabled = true;
2550
- authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/accept", {}).then(function(res){
2551
- if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); return; }
2552
- if(res.status === 202 && res.body && res.body.pending){
2553
- bar.insertAdjacentHTML("beforeend", '<span class="stopping" data-testid="accept-pending">still re-checking the tree; look again in a moment</span>');
2554
- return;
2555
- }
2556
- accBtn.disabled = false;
2557
- bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">' + escapeXml((res.body && res.body.error) || ("refused (" + res.status + ")")) + '</span>');
2558
- }).catch(function(x){ accBtn.disabled = false; bar.insertAdjacentHTML("beforeend", '<span class="resume-err" data-testid="accept-err">could not reach the panel: ' + escapeXml(x.message) + '</span>'); });
2559
- });
2560
- }
2625
+ if(accBtn) accBtn.addEventListener("click", function(){ acceptDoor(detail, accBtn, bar); });
2561
2626
  var resBtn = bar.querySelector('[data-testid="btn-resume-run"]');
2562
2627
  if(resBtn) resBtn.addEventListener("click", function(){ resumeInChat(detail); });
2563
2628
  }
@@ -2590,6 +2655,7 @@
2590
2655
 
2591
2656
  function renderRun(detail){
2592
2657
  if(detail.fileMissing){
2658
+ syncResultTab({});
2593
2659
  setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
2594
2660
  document.getElementById("run-empty").hidden = false;
2595
2661
  document.getElementById("run-empty").textContent = "spine file no longer on disk";
@@ -2600,13 +2666,29 @@
2600
2666
  return;
2601
2667
  }
2602
2668
 
2669
+ if(detail.refused){
2670
+ // refused before it started (e.g. the page could not be fetched): the server's plain words, status word from the table
2671
+ syncResultTab({});
2672
+ setRunHeader(detail.glyph, runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, "") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
2673
+ document.getElementById("run-empty").hidden = false;
2674
+ document.getElementById("run-empty").textContent = detail.endedLine || "";
2675
+ document.getElementById("run-content").hidden = true;
2676
+ document.getElementById("audit-select-empty").hidden = false;
2677
+ document.getElementById("audit-select-empty").textContent = detail.endedLine || "";
2678
+ document.getElementById("audit-body").hidden = true;
2679
+ return;
2680
+ }
2681
+
2603
2682
  if(detail.starting){
2604
- setRunHeader("\u25B6", runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
2683
+ syncResultTab({});
2684
+ // the words come from the server (startingWord / startingNote), never typed here
2685
+ var startWords = (detail.startingWord || "starting…") + " " + (detail.startingNote || "the run has not written its log yet");
2686
+ setRunHeader("\u25B6", runName(detail.job, detail.runNo, detail.runid), statusWordHtml(detail.status, detail.startingWord || "starting\u2026") + " \u00B7 " + escapeXml("started " + fmtLocalDateTime(detail.at)));
2605
2687
  document.getElementById("run-empty").hidden = false;
2606
- document.getElementById("run-empty").textContent = "starting… the run has not written its log yet";
2688
+ document.getElementById("run-empty").textContent = startWords;
2607
2689
  document.getElementById("run-content").hidden = true;
2608
2690
  document.getElementById("audit-select-empty").hidden = false;
2609
- document.getElementById("audit-select-empty").textContent = "starting… the run has not written its log yet";
2691
+ document.getElementById("audit-select-empty").textContent = startWords;
2610
2692
  document.getElementById("audit-body").hidden = true;
2611
2693
  return;
2612
2694
  }
@@ -2684,10 +2766,13 @@
2684
2766
  '<div class="summary-row"><span class="sum-label">why:</span><span class="sum-val">' + escapeXml(detail.stopReason || (detail.glyph === "✓" ? "run ended clean" : "not recorded")) + '</span></div>' +
2685
2767
  '<div class="summary-row"><span class="sum-label">close:</span><span class="sum-val">' + escapeXml(detail.close && detail.close.verdict ? detail.close.verdict : "none — the run ended before any close ran") + '</span></div>' +
2686
2768
  '<div class="summary-row" data-testid="summary-tools"><span class="sum-label">tools:</span><span class="sum-val">' + escapeXml(detail.fromBridge === true ? bridgeToolsLine(detail.toolsUsed) : toolsLine(detail.behaviour)) + '</span></div>' +
2769
+ (detail.fromBridge !== true && typeof detail.callsLine === "string" ? '<div class="summary-row" data-testid="summary-calls"><span class="sum-label">calls:</span><span class="sum-val">' + escapeXml(detail.callsLine) + '</span></div>' : '') +
2687
2770
  '<div class="summary-row" data-testid="summary-offered" id="summary-offered-row" hidden><span class="sum-label">offered:</span><span class="sum-val" id="summary-offered-val"></span></div>' +
2771
+ (detail.fromBridge !== true && typeof detail.checksLine === "string" ? '<div class="summary-row" data-testid="summary-checks"><span class="sum-label">checks:</span><span class="sum-val">' + escapeXml(detail.checksLine) + '</span></div>' : '') +
2688
2772
  '<div class="summary-row" data-testid="summary-cache"><span class="sum-label">cache:</span><span class="sum-val">' + escapeXml(cacheLine(detail.memoryCache)) + '</span></div>';
2689
2773
 
2690
2774
  paintOfferedRow();
2775
+ syncResultTab(detail);
2691
2776
 
2692
2777
  // build item B (2026-09-26 rewrite): the map AND the card list are both
2693
2778
  // built from ONE list — `detail.parts` (src/replay.js's own ordered part
@@ -2722,9 +2807,11 @@
2722
2807
  '<div class="step-head"><h4>' + titleText + '</h4>' +
2723
2808
  stepStateHTML(box, 'part-state-' + idx) +
2724
2809
  (box.blocked > 0 ? '<span class="badge red" data-testid="part-blocked-badge">' + box.blocked + ' blocked</span>' : '') +
2810
+ actorMarkHtml(part) +
2725
2811
  '</div>' +
2726
2812
  '<div class="step-meta" title="a part is one piece of the run (scout, plan, a step, replan, fix, judge); a call is one model round">' + line1Bits.join(" &middot; ") + '</div>' +
2727
- '<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>';
2813
+ '<div class="step-meta">' + escapeXml(partLine2Text(box.part)) + '</div>' +
2814
+ (typeof part.triesLine === "string" ? '<div class="step-meta" data-testid="part-tries-' + idx + '">' + escapeXml(part.triesLine) + '</div>' : '');
2728
2815
  listEl.appendChild(card);
2729
2816
  var jumpToAudit = function(){
2730
2817
  document.getElementById("tab-audit").click();
@@ -2746,7 +2833,11 @@
2746
2833
  // 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
2747
2834
  function stepStateHTML(box, testid){
2748
2835
  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")));
2749
- return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + (cls === "amber" ? " pulse" : "") + '"></span><b class="st-word">' + escapeXml(boxStatusWord(box)) + '</b></span>';
2836
+ return stateDotHTML(cls, boxStatusWord(box), testid);
2837
+ }
2838
+ // ONE spelling of the dot + bold word every state header wears (part headers and the schedule header).
2839
+ function stateDotHTML(cls, word, testid){
2840
+ return '<span class="step-state" data-testid="' + testid + '"><span class="dot ' + cls + (cls === "amber" ? " pulse" : "") + '"></span><b class="st-word">' + escapeXml(word) + '</b></span>';
2750
2841
  }
2751
2842
  // build item C: Audit tab attempt row -> its rounds (server-built,
2752
2843
  // lazy-fetched on expand via /api/runs/:runid/rounds?part=<i>&attempt=<n>).
@@ -2766,6 +2857,7 @@
2766
2857
  row.innerHTML =
2767
2858
  '<button class="btn small attempt-toggle" type="button" aria-expanded="false">' +
2768
2859
  'attempt ' + a.n + ' ' + attemptGlyph(a.outcome, boxIsLive) +
2860
+ (a.thinking ? ' &middot; ' + escapeXml(a.thinking) : '') +
2769
2861
  (checksText ? ' &middot; ' + checksText : '') +
2770
2862
  '</button>' +
2771
2863
  (checksList ? '<div class="checks-list" data-testid="audit-attempt-checks-' + partIndex + '-' + a.n + '" hidden>' + checksList + '</div>' : '') +
@@ -2788,6 +2880,90 @@
2788
2880
  });
2789
2881
  }
2790
2882
 
2883
+ // PRD v1.106 Part 5: who did a part's work, in the server's own words (`part.marker`: [model] or [human · ask]); nothing typed here
2884
+ function actorMarkHtml(part){
2885
+ return part && typeof part.marker === "string" && part.marker !== "" ? ' <span class="actor-mark" data-testid="actor-mark">' + escapeXml(part.marker) + '</span>' : "";
2886
+ }
2887
+ // PRD v1.106 Part 5 C: the review door's fold: one human row for the ask, one for the answer once there is one
2888
+ function doorTableHtml(part){
2889
+ var rows = (part.doorView && part.doorView.rows ? part.doorView.rows : []).map(function(r){
2890
+ return "<tr><td>&mdash;</td><td>&mdash;</td><td>human</td><td>" + (r.path ? escapeXml(r.path) : "&mdash;") + "</td><td>" + escapeXml(r.text) + "</td><td>" +
2891
+ auditTimeCellHtml({ time: r.time }) + "</td></tr>";
2892
+ }).join("");
2893
+ return '<div class="audit-table-scroll"><table class="rounds-table" data-testid="door-table"><thead><tr><th>Round</th><th>Try</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead><tbody>' + rows + "</tbody></table></div>";
2894
+ }
2895
+ // PRD v1.106 Part 5 C: one try, in the server's words: its line (result glyph, thinking, the first reason and how many more), its numbered checks
2896
+ // in the order they ran (each with its result, a count or a folded reasons list, the job line it serves) and the file it judged
2897
+ function tryBlockHtml(part, t, boxIsLive, partIndex){
2898
+ var attempt = (part.attempts || []).filter(function(a){ return a.n === t.n; })[0] || null;
2899
+ var testid = "audit-try-" + partIndex + "-" + t.n;
2900
+ if(t.cutOff){
2901
+ return '<div class="try-line" data-testid="' + testid + '">try ' + t.n + (t.thinking ? " &middot; " + escapeXml(t.thinking) : "") + " &middot; " + escapeXml(t.words || "") + "</div>";
2902
+ }
2903
+ var checks = attempt && Array.isArray(attempt.checks) ? attempt.checks : [];
2904
+ var firstBad = checks.filter(function(c){ return !c.pass && c.gap; })[0] || null;
2905
+ var html = '<div class="try-line" data-testid="' + testid + '">try ' + t.n + " " + glyphSpan(attemptGlyph(t.outcome, boxIsLive)) +
2906
+ (t.thinking ? " &middot; " + escapeXml(t.thinking) : "") +
2907
+ (firstBad ? " &middot; " + escapeXml(firstBad.gap) + (firstBad.moreText ? " " + escapeXml(firstBad.moreText) : "") : "") + "</div>";
2908
+ checks.forEach(function(c){
2909
+ var label = c.question || c.name || "unknown";
2910
+ html += '<div class="try-check" data-testid="' + testid + '-check-' + c.n + '">' + c.n + " " + glyphSpan(c.pass ? "\u2713" : "\u2717") + " " + escapeXml(label) +
2911
+ (c.note ? " (" + escapeXml(c.note) + ")" : "") + (c.from ? " &middot; " + escapeXml(c.from) : "") + "</div>";
2912
+ if(!c.pass && c.reasonsLabel){
2913
+ html += '<details class="try-reasons" data-testid="' + testid + '-reasons-' + c.n + '"><summary>' + escapeXml(c.reasonsLabel) + "</summary>" +
2914
+ c.reasons.map(function(r){ return "<div>" + escapeXml(r) + "</div>"; }).join("") + "</details>";
2915
+ }
2916
+ });
2917
+ (attempt && Array.isArray(attempt.judged) ? attempt.judged : []).forEach(function(j){
2918
+ html += '<div class="try-judged" data-testid="' + testid + '-judged">' + escapeXml(j) + "</div>";
2919
+ });
2920
+ return html;
2921
+ }
2922
+ // PRD v1.106 Part 5 C: a plan step's fold — tools tally, every try with its checks, then ONE Round table for the whole step (20 rows a page)
2923
+ function buildStepFold(body, partIndex, part, boxIsLive){
2924
+ var wrap = document.createElement("div");
2925
+ wrap.className = "step-fold";
2926
+ wrap.setAttribute("data-testid", "audit-step-fold-" + partIndex);
2927
+ var html = part.tally ? '<div class="step-tally" data-testid="audit-tally-' + partIndex + '">' + escapeXml(part.tally) + "</div>" : "";
2928
+ part.tries.forEach(function(t){ html += tryBlockHtml(part, t, boxIsLive, partIndex); });
2929
+ html += '<div class="rounds-list" data-testid="audit-step-rounds-' + partIndex + '"></div>';
2930
+ wrap.innerHTML = html;
2931
+ body.appendChild(wrap);
2932
+ loadStepRows(wrap.querySelector(".rounds-list"), partIndex, 0, false);
2933
+ }
2934
+ function stepRowHtml(r){
2935
+ if(r.kind === "check"){
2936
+ return "<tr><td>&mdash;</td><td>" + r.try + "</td><td>check</td><td>" + (r.path ? escapeXml(r.path) : "&mdash;") + "</td><td>" + glyphSpan(r.sign) + " " + escapeXml(r.text) + "</td><td>" + auditTimeCellHtml(r) + "</td></tr>";
2937
+ }
2938
+ if(r.kind === "model"){
2939
+ var pseudo = { kind: "model-call", round: r.round, time: r.time, costUsd: r.costUsd, tokens: r.tokens, durationMs: r.durationMs };
2940
+ return '<tr class="round-header-row"><td>' + r.round + "</td><td>" + r.try + "</td><td>" + auditActionCellHtml(pseudo) + "</td><td>" + auditPathCellHtml(pseudo) + "</td><td>" + auditDecisionCellHtml(pseudo) + "</td><td>" + auditTimeCellHtml(pseudo) + "</td></tr>";
2941
+ }
2942
+ var cls = auditRowClassName(r);
2943
+ return "<tr" + (cls ? ' class="' + cls + '"' : "") + "><td>" + auditRoundCellHtml(r) + "</td><td>" + r.try + "</td><td>model &middot; " + auditActionCellHtml(r) + "</td><td>" + auditPathCellHtml(r) + "</td><td>" + auditDecisionCellHtml(r) + "</td><td>" + auditTimeCellHtml(r) + "</td></tr>";
2944
+ }
2945
+ function loadStepRows(el, partIndex, offset, appendMode){
2946
+ if(!appendMode) el.innerHTML = '<div class="hint">loading…</div>';
2947
+ getJSON("/api/runs/" + encodeURIComponent(currentRunid) + "/rounds?part=" + encodeURIComponent(partIndex) + "&all=1&offset=" + offset).then(function(data){
2948
+ var rowsHtml = data.rows.map(stepRowHtml).join("");
2949
+ var shownTo = data.offset + data.rows.length;
2950
+ var info = '<div class="hint">showing ' + (data.offset + 1) + "&ndash;" + shownTo + " of " + data.totalRows + "</div>";
2951
+ if(appendMode){
2952
+ var oldInfo = el.querySelector(".rounds-page-info");
2953
+ if(oldInfo) oldInfo.remove();
2954
+ el.querySelector(".rounds-table tbody").insertAdjacentHTML("beforeend", rowsHtml);
2955
+ } else {
2956
+ el.innerHTML = (data.toolLogSaved ? "" : '<div class="hint">tool calls: no log saved</div>') +
2957
+ '<div class="audit-table-scroll"><table class="rounds-table" data-testid="rounds-table"><thead><tr><th>Round</th><th>Try</th><th>Action</th><th>Path</th><th>Decision</th><th>Time</th></tr></thead><tbody>' + rowsHtml + "</tbody></table></div>";
2958
+ }
2959
+ el.insertAdjacentHTML("beforeend", '<div class="rounds-page-info">' + info + (shownTo < data.totalRows ? '<button class="btn small rounds-load-more" type="button">load more</button>' : "") + "</div>");
2960
+ var more = el.querySelector(".rounds-load-more");
2961
+ if(more) more.addEventListener("click", function(){ loadStepRows(el, partIndex, shownTo, true); });
2962
+ }).catch(function(e){
2963
+ el.innerHTML = '<div class="hint">failed to load rounds: ' + escapeXml(e.message) + "</div>";
2964
+ });
2965
+ }
2966
+
2791
2967
  function roundsUrl(partIndex, attempt, offset){
2792
2968
  var params = "part=" + encodeURIComponent(partIndex) + "&attempt=" + encodeURIComponent(attempt.n) + "&offset=" + offset;
2793
2969
  return "/api/runs/" + encodeURIComponent(currentRunid) + "/rounds?" + params;
@@ -2897,6 +3073,7 @@
2897
3073
  }
2898
3074
  // line 2 of a part's card / Audit body: the drafting step names, or a worker part's tool breakdown
2899
3075
  function partLine2Text(part){
3076
+ if(part && part.kind === "door") return part.doorView ? "last answer: " + part.doorView.last : "";
2900
3077
  if(part && part.kind === "drafting") return (part.draftSteps || []).map(function(x){ return x.label; }).join(" · ");
2901
3078
  return toolBreakdownLine(part.byTool);
2902
3079
  }
@@ -2919,6 +3096,10 @@
2919
3096
  rows += "<tr><td>" + escapeXml(st.label) + " (" + escapeXml(c.label) + ")</td><td>–</td><td>1</td><td>" +
2920
3097
  (c.model ? escapeXml(c.model) : "–") + "</td><td>" + draftingCallCost(c) + "</td></tr>";
2921
3098
  });
3099
+ // PRD v1.106 Part 5 Q1: each question the person answered, under its step, in the server's own words (asked, then said); wraps
3100
+ (Array.isArray(st.humans) ? st.humans : []).forEach(function(h){
3101
+ rows += '<tr class="human-row" data-testid="drafting-human"><td colspan="5" style="overflow-wrap:anywhere;white-space:pre-wrap">' + escapeXml(h.askedText) + "<br>" + escapeXml(h.saidText) + "</td></tr>";
3102
+ });
2922
3103
  });
2923
3104
  rows += '<tr class="drafting-total"><td><b>total</b></td><td>' + (part.wallMs === null || part.wallMs === undefined ? "–" : duration(part.wallMs)) +
2924
3105
  "</td><td>" + (typeof part.rounds === "number" ? part.rounds : 0) + "</td><td>–</td><td>" + draftingMoneyText(part) + "</td></tr>";
@@ -2955,6 +3136,7 @@
2955
3136
  */
2956
3137
  function partLine1Text(part, detail){
2957
3138
  if(part && part.kind === "drafting") return draftingLine1Text(part);
3139
+ if(part && part.kind === "door") return part.doorView ? escapeXml(part.doorView.head) : "";
2958
3140
  var isLiveRunPart = part && part.kind === "run" && detail && !detail.died && detail.spentUsd === null;
2959
3141
  var wallText = (isLiveRunPart && part.wallMs === null) ? liveWallPhrase(detail.wallFloorMs) : duration(part.wallMs);
2960
3142
  var spendText = (isLiveRunPart && part.spentUsd === null)
@@ -2970,6 +3152,8 @@
2970
3152
  // MOST RECENT attempt's own declared-close grading (the opening
2971
3153
  // outer-close grade counts as attempt 1, same list `buildAttemptsList`
2972
3154
  // reads), in the withdrawn headline's place.
3155
+ // PRD v1.106 Part 3: a part with one call (plan, scout, replan, a one-attempt step) says the thinking it was sent here; the server worded it
3156
+ if(typeof part.thinking === "string" && part.thinking !== "") bits.push(escapeXml(part.thinking));
2973
3157
  var latestAttempt = Array.isArray(part.attempts) && part.attempts.length ? part.attempts[part.attempts.length - 1] : null;
2974
3158
  var headline = latestAttempt ? attemptChecksLine(latestAttempt) : "";
2975
3159
  if(headline) bits.push(headline);
@@ -3161,7 +3345,7 @@
3161
3345
  gBtn.className = "btn audit-part-toggle";
3162
3346
  gBtn.setAttribute("aria-expanded", "false");
3163
3347
  gBtn.setAttribute("data-testid", "audit-schedule-toggle");
3164
- var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") + escapeXml(sec.title) + " [" + glyphSpan(sec.sign) + "] " + escapeXml(sec.head); };
3348
+ var secHead = function(open){ return (open ? "\u25BE " : "\u25B8 ") + stateDotHTML(glyphClass(sec.sign), sec.word, "audit-schedule-state") + " " + escapeXml(sec.title) + " &middot; " + escapeXml(sec.head); };
3165
3349
  gBtn.innerHTML = secHead(false);
3166
3350
  var gBody = document.createElement("div");
3167
3351
  gBody.className = "audit-part-body";
@@ -3213,7 +3397,8 @@
3213
3397
  headBtn.innerHTML =
3214
3398
  stepStateHTML(box, 'audit-part-state-' + idx) + ' ' + titleText + ' &middot; ' + partLine1Text(part, detail) +
3215
3399
  (groupResultGlyph !== null ? ' &middot; ' + groupResultGlyph : '') +
3216
- (box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '');
3400
+ (box.blocked > 0 ? ' <span class="badge red" data-testid="audit-part-blocked-' + idx + '">' + box.blocked + ' blocked</span>' : '') +
3401
+ actorMarkHtml(part);
3217
3402
  var body = document.createElement("div");
3218
3403
  body.className = "audit-part-body";
3219
3404
  body.hidden = !startExpanded;
@@ -3232,6 +3417,12 @@
3232
3417
  var draftingEl = document.createElement("div");
3233
3418
  draftingEl.innerHTML = draftingTableHtml(part);
3234
3419
  body.appendChild(draftingEl);
3420
+ } else if(part.kind === "door"){
3421
+ var doorEl = document.createElement("div");
3422
+ doorEl.innerHTML = doorTableHtml(part);
3423
+ body.appendChild(doorEl);
3424
+ } else if(part.kind === "step" && Array.isArray(part.tries) && part.tries.length > 0){
3425
+ buildStepFold(body, idx, part, box.state === "running");
3235
3426
  } else if(attempts.length > 1){
3236
3427
  // parts WITH an attempt sub-loop (a multi-attempt step, or the fix
3237
3428
  // loop) expand to attempt rows first — each attempt expands to its
@@ -3424,6 +3615,8 @@
3424
3615
  var verdictEl = document.getElementById("details-verdict");
3425
3616
  if(job.checkTypeTitle){ verdictEl.title = job.checkTypeTitle; } else { verdictEl.removeAttribute("title"); }
3426
3617
  document.getElementById("details-model").textContent = job.model;
3618
+ // PRD v1.106 Part 3: the server words the Thinking row (src/thinking.js); it is always shown
3619
+ document.getElementById("details-thinking").textContent = typeof job.thinking === "string" ? job.thinking : "";
3427
3620
  // RULED 2026-10-15: how the run began and the job's schedule NOW, both worded on the server (absent on an imported job)
3428
3621
  [["started", job.started], ["schedule", job.schedule]].forEach(function(p){
3429
3622
  var f = document.getElementById("details-" + p[0] + "-field");
@@ -3450,6 +3643,211 @@
3450
3643
  paintOfferedRow();
3451
3644
  }
3452
3645
 
3646
+ // ---- Result tab (PRD v1.107 Part 6, rubric jobs only; signed visual design/result-tab-mock.html variant F) ----
3647
+ // The server decides everything: `detail.hasResult` shows the tab, `/result` carries the signed lines, the facts' own sentences
3648
+ // (src/facts.js factSentence), the rows of the delivered file (sha256-checked against the run's own record) and `passed` (read from the
3649
+ // run's own check-run). The page only draws; it never decides a check and never searches a quote for a word.
3650
+ var resultDetail = null;
3651
+ var resultData = null;
3652
+ function resultTabShown(){ return document.getElementById("tab-result").getAttribute("aria-selected") === "true"; }
3653
+ function clearResult(){
3654
+ resultDetail = null; resultData = null;
3655
+ document.getElementById("result-body").hidden = true;
3656
+ document.getElementById("result-body").innerHTML = "";
3657
+ document.getElementById("result-empty").hidden = false;
3658
+ }
3659
+ // called by renderRun for every detail it draws: shows or removes the tab button, and the Run tab's pointer while the door is open
3660
+ function syncResultTab(detail){
3661
+ var btn = document.getElementById("tab-result");
3662
+ var show = detail.hasResult === true;
3663
+ if(!show && resultTabShown()) document.getElementById("tab-run").click();
3664
+ btn.hidden = !show;
3665
+ resultDetail = show ? detail : null;
3666
+ var ptr = document.getElementById("result-pointer");
3667
+ var waiting = show && detail.accept === true && !detail.live;
3668
+ ptr.hidden = !waiting;
3669
+ ptr.innerHTML = waiting ? 'the result is on the <button class="rs-link" type="button" data-testid="result-pointer-link">Result tab</button>' : "";
3670
+ var link = ptr.querySelector("button");
3671
+ if(link) link.addEventListener("click", function(){ btn.click(); });
3672
+ if(show && resultData && resultData.runid === detail.runid) renderResult();
3673
+ if(!show) clearResult();
3674
+ }
3675
+ function loadResult(runid){
3676
+ return getJSON("/api/runs/" + encodeURIComponent(runid) + "/result").then(function(data){
3677
+ if(runid !== currentRunid || !resultDetail || resultDetail.runid !== runid) return;
3678
+ resultData = data;
3679
+ renderResult();
3680
+ }).catch(function(){ /* the tab keeps what it last drew */ });
3681
+ }
3682
+ document.getElementById("tab-result").addEventListener("click", function(){ if(currentRunid) loadResult(currentRunid); });
3683
+
3684
+ // a RED run's own file words (the last file its check judged: "the file is gone" / "the file changed after the check") and the one note that says
3685
+ // what those rows are. A DELIVERED file's gone/changed words are said once, in the Saved card, never here.
3686
+ function resultFileWords(file){
3687
+ var note = file && file.judged && file.note ? '<div class="hint" data-testid="result-judged-note">' + escapeXml(file.note) + '</div>' : "";
3688
+ return note + (file && file.judged && file.words ? '<div class="rs-warn" data-testid="result-file-words">' + escapeXml(file.words) + '</div>' : "");
3689
+ }
3690
+ function resultField(label, inner){
3691
+ return '<div class="field"><label>' + escapeXml(label) + '</label>' + inner + '</div>';
3692
+ }
3693
+ // the numbered job line and its ~ rules, one box (the Job tab's "N. text" spelling, a space after the number)
3694
+ function resultLineBox(line){
3695
+ return resultField("The job line", '<div class="ro-value" data-testid="result-linetext">' + escapeXml(line.n + ". " + line.text) +
3696
+ (line.rules || []).map(function(r){ return '<br>~ ' + escapeXml(r); }).join("") + '</div>');
3697
+ }
3698
+ // no saved result to show: one line, never the file-state words (those are in Saved)
3699
+ var RESULT_NONE = "no saved result to show";
3700
+ // the proof quote, with the words the check read marked. The server sends the check's own text and offsets (`r.proof`: bareguard's
3701
+ // `full.evidence`, spans already chosen and merged); the page only cuts the text at those offsets, escapes every piece and marks.
3702
+ // It never searches a quote for a word. No proof (an old run, a quote the check did not read): the plain quote, unmarked.
3703
+ function resultProofHtml(r){
3704
+ var p = r.proof;
3705
+ if(!p || typeof p.text !== "string" || !Array.isArray(p.marks)) return escapeXml(r.quote);
3706
+ var h = "", at = 0;
3707
+ p.marks.forEach(function(m){
3708
+ h += escapeXml(p.text.slice(at, m[0])) + "<mark>" + escapeXml(p.text.slice(m[0], m[1])) + "</mark>";
3709
+ at = m[1];
3710
+ });
3711
+ return h + escapeXml(p.text.slice(at));
3712
+ }
3713
+ function resultFactsLine(line){
3714
+ var facts = line.facts || [];
3715
+ var n = facts.length;
3716
+ var ok = line.passed === true ? ' <span class="rs-ok" data-testid="result-ok">\u2713</span>' : "";
3717
+ var h = resultFileWords(line.file);
3718
+ if(Array.isArray(line.rows) && line.rows.length){
3719
+ h += resultField("Results (" + line.rows.length + ")", '<div class="ro-value" data-testid="result-results">' + line.rows.map(function(r){
3720
+ return '<div class="ro-value" data-testid="result-row" style="margin-bottom:8px">' +
3721
+ '<div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:6px"><b style="color:var(--text)">Result ' + r.n + '</b>' +
3722
+ (line.passed === true ? '<span class="rs-ok" data-testid="result-count">' + n + ' of ' + n + ' \u2713</span>' : "") + '</div>' +
3723
+ '<div class="field-row" style="margin-top:6px">' + r.cells.map(function(c, i){
3724
+ return '<div class="field"><label>' + escapeXml(facts[i] ? facts[i].name : "") + '</label><div class="ro-value" data-testid="result-cell">' + escapeXml(c) + ok + '</div></div>';
3725
+ }).join("") + '</div>' +
3726
+ '<details data-testid="result-proof"><summary>proof \u2014 the page\'s own words</summary><div class="ro-value" data-testid="result-quote" style="overflow-wrap:anywhere">' + resultProofHtml(r) + '</div></details></div>';
3727
+ }).join("") + '</div>');
3728
+ } else if(!(line.file && line.file.judged && line.file.words)){
3729
+ h += '<div class="hint" data-testid="result-nofile">' + RESULT_NONE + '</div>';
3730
+ }
3731
+ if(facts.length){
3732
+ h += resultField("The checks", '<div class="ro-value" data-testid="result-rules">' +
3733
+ facts.map(function(f){ return escapeXml(f.sentence) + (f.note ? ' ' + escapeXml(f.note) : ""); }).join("<br>") +
3734
+ (line.passed === true && Array.isArray(line.rows) && line.rows.length ? '<br><span class="hint">\u2713 = code checked it against the saved page</span>' : "") + '</div>');
3735
+ }
3736
+ return h;
3737
+ }
3738
+ function resultTextLine(line){
3739
+ var h = resultFileWords(line.file);
3740
+ if(typeof line.answer === "string") h += resultField("Answer", '<div class="ro-value" data-testid="result-answer" style="color:var(--text);white-space:pre-wrap">' + escapeXml(line.answer) + '</div>');
3741
+ else if(!(line.file && line.file.judged && line.file.words)) h += '<div class="hint" data-testid="result-nofile">' + RESULT_NONE + '</div>';
3742
+ (line.cites || []).forEach(function(c){
3743
+ h += resultField("Quotes from " + c.label + " (" + c.items.length + ")", '<details data-testid="result-cites"><summary>show</summary><div class="ro-value" style="white-space:pre-wrap">' +
3744
+ c.items.map(function(it){ return '<b>' + escapeXml(it.claim) + '</b>\n' + escapeXml(it.quote); }).join("\n\n") + '</div></details>');
3745
+ });
3746
+ return h;
3747
+ }
3748
+ // a list, one row per item (the Job tab lists the same items one per row)
3749
+ function resultNotChecked(label, items, testid){
3750
+ if(!items || !items.length) return "";
3751
+ return resultField(label, '<div class="ro-value" data-testid="' + testid + '"><ul style="margin:0;padding-left:18px">' +
3752
+ items.map(function(t){ return '<li>' + escapeXml(t) + '</li>'; }).join("") + '</ul></div>');
3753
+ }
3754
+ // the web page a result was read from: a link (http/https only, new tab) and the server's words that the page may have moved on
3755
+ function resultPageLink(url, note, testid){
3756
+ if(typeof url !== "string" || !/^https?:\/\//i.test(url)) return "";
3757
+ return resultField("Page read", '<div class="ro-value" data-testid="' + testid + '" style="overflow-wrap:anywhere"><a href="' + escapeXml(url) +
3758
+ '" target="_blank" rel="noopener noreferrer">' + escapeXml(url) + '</a><div class="hint" data-testid="result-page-note">' + escapeXml(note || "") + '</div></div>');
3759
+ }
3760
+ // `own` = the Result tab read 2+ pages, so this line's box shows its own page
3761
+ function resultLineCard(line, own, note){
3762
+ return '<div class="job-card" data-testid="result-line"><h4>Line ' + escapeXml(String(line.n)) + '</h4>' + resultLineBox(line) +
3763
+ (own ? resultPageLink(line.page, note, "result-page-line") : "") +
3764
+ (line.kind === "facts" ? resultFactsLine(line) : resultTextLine(line)) +
3765
+ resultNotChecked("Not checked", line.notChecked, "result-line-notchecked") + '</div>';
3766
+ }
3767
+ function resultDoorHtml(door){
3768
+ return '<div class="job-card" id="result-door-card" data-testid="result-door"><h4>Your call</h4><div class="field"><div class="ro-value"><b class="st-word">' + escapeXml(door.head) + '</b> \u2014 ' + escapeXml(door.ask) + '</div></div>' +
3769
+ '<div class="run-actions" style="margin:6px 0 0;"><button class="btn primary" type="button" data-testid="result-accept">Accept</button>' +
3770
+ '<button class="btn" type="button" data-testid="result-rerun-open">Rerun with my words</button>' +
3771
+ '<button class="btn" type="button" data-testid="result-pause">Pause</button></div>' +
3772
+ '<div id="result-rerun-box" data-testid="result-rerun-box" hidden><textarea id="result-rerun-text" data-testid="result-rerun-text" aria-label="your words for the rerun" placeholder="e.g. only morning flights, and show the airline"></textarea>' +
3773
+ '<button class="btn" type="button" data-testid="result-rerun" disabled>Rerun</button></div>' +
3774
+ '<div id="result-door-msg" data-testid="result-door-msg"></div></div>';
3775
+ }
3776
+ function wireResultDoor(body, detail){
3777
+ var msg = body.querySelector("#result-door-msg");
3778
+ var accept = body.querySelector('[data-testid="result-accept"]');
3779
+ if(!accept) return;
3780
+ var say = function(cls, text, testid){ msg.innerHTML = '<span class="' + cls + '" data-testid="' + testid + '">' + escapeXml(text) + '</span>'; };
3781
+ accept.addEventListener("click", function(){ msg.innerHTML = ""; acceptDoor(detail, accept, msg); });
3782
+ var box = body.querySelector("#result-rerun-box");
3783
+ var txt = body.querySelector("#result-rerun-text");
3784
+ var go = body.querySelector('[data-testid="result-rerun"]');
3785
+ body.querySelector('[data-testid="result-rerun-open"]').addEventListener("click", function(){ box.hidden = !box.hidden; if(!box.hidden) txt.focus(); });
3786
+ // required: the button stays off while the words are empty, and the click re-checks (the engine refuses empty words too)
3787
+ txt.addEventListener("input", function(){ go.disabled = txt.value.trim() === ""; });
3788
+ go.addEventListener("click", function(){
3789
+ var words = txt.value.trim();
3790
+ if(words === ""){ say("resume-err", "Write what you want done differently, then press Rerun.", "result-door-err"); return; }
3791
+ go.disabled = true;
3792
+ authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/rerun", {text: words}).then(function(res){
3793
+ if(res.status === 200 && res.body && res.body.ok){ refreshRunsList(true); selectRun(detail.runid); afterResumeRefresh(detail.runid, 6); return; }
3794
+ go.disabled = false;
3795
+ say("resume-err", (res.body && res.body.error) || ("refused (" + res.status + ")"), "result-door-err");
3796
+ }).catch(function(x){ go.disabled = false; say("resume-err", "could not reach the panel: " + x.message, "result-door-err"); });
3797
+ });
3798
+ var pause = body.querySelector('[data-testid="result-pause"]');
3799
+ pause.addEventListener("click", function(){
3800
+ pause.disabled = true;
3801
+ authorPost("/api/runs/" + encodeURIComponent(detail.runid) + "/pause", {}).then(function(res){
3802
+ pause.disabled = false;
3803
+ if(res.status === 200 && res.body && res.body.ok){ say("stopping", "Paused. Nothing was spent; this stays open for you to answer later.", "result-door-paused"); return; }
3804
+ if(res.status === 202 && res.body && res.body.pending){ say("stopping", "still recording the pause; look again in a moment", "result-door-paused"); return; }
3805
+ say("resume-err", (res.body && res.body.error) || ("refused (" + res.status + ")"), "result-door-err");
3806
+ }).catch(function(x){ pause.disabled = false; say("resume-err", "could not reach the panel: " + x.message, "result-door-err"); });
3807
+ });
3808
+ }
3809
+ function renderResult(){
3810
+ var detail = resultDetail, data = resultData;
3811
+ var body = document.getElementById("result-body");
3812
+ if(!detail || !data || data.runid !== detail.runid){ return; }
3813
+ document.getElementById("result-empty").hidden = true;
3814
+ body.hidden = false;
3815
+ // keep what the person is typing across a poll-driven redraw
3816
+ var keepText = body.querySelector("#result-rerun-text");
3817
+ var typed = keepText ? keepText.value : "";
3818
+ var rerunOpen = !!body.querySelector("#result-rerun-box:not([hidden])");
3819
+ var counters = document.getElementById("run-counters").textContent.split(" \u00b7 ");
3820
+ var h = '<div class="job-card" data-testid="result-head"><h4>Result</h4>' +
3821
+ resultField("Status", '<div class="ro-value">' + stateDotHTML(glyphClass(detail.glyph), detail.status && detail.status.word ? detail.status.word : "", "result-status") +
3822
+ ' \u00b7 ' + escapeXml(detail.checkType) + ' \u00b7 ' + escapeXml(runName(detail.job, detail.runNo, detail.runid)) + '</div>') +
3823
+ '<div class="field-row">' + resultField("Money", '<div class="ro-value" data-testid="result-counters">' + escapeXml(counters[0] || "") + '</div>') +
3824
+ resultField("Time", '<div class="ro-value">' + escapeXml((counters[1] || "").replace(/^took /, "")) + '</div>') + '</div>' +
3825
+ ((data.pages || []).length === 1 ? resultPageLink(data.pages[0], data.pageNote, "result-page-top") : "") + '</div>';
3826
+ var ownPages = (data.pages || []).length >= 2;
3827
+ (data.lines || []).forEach(function(line){ h += resultLineCard(line, ownPages, data.pageNote); });
3828
+ if(data.gap){
3829
+ h += '<div class="rs-warn" data-testid="result-gap">The check said: ' + escapeXml(data.gap.line) + (data.gap.moreText ? ' ' + escapeXml(data.gap.moreText) : "") + '</div>';
3830
+ }
3831
+ if((data.notChecked || []).length){
3832
+ h += '<div class="job-card"><h4>Not checked</h4>' + resultNotChecked("Not checked (the AI's own reading)", data.notChecked, "result-notchecked") + '</div>';
3833
+ }
3834
+ if(detail.accept === true && !detail.live) h += resultDoorHtml(data.door);
3835
+ if((data.delivered || []).length){
3836
+ h += '<div class="job-card" data-testid="result-saved"><h4>Saved</h4>' + data.delivered.map(function(d){
3837
+ return resultField(d.name, '<div class="ro-value" data-testid="result-path" style="overflow-wrap:anywhere">' + escapeXml(d.path) + '</div>' +
3838
+ (d.words ? '<div class="rs-warn" data-testid="result-file-gone">' + escapeXml(d.words) + '</div>'
3839
+ : '<div class="run-actions" style="margin:6px 0 0;"><a class="btn" style="text-decoration:none;" data-testid="result-download" download href="/api/runs/' + encodeURIComponent(data.runid) + '/download?i=' + d.i + '">Download ' + escapeXml(d.name) + '</a></div>'));
3840
+ }).join("") + '</div>';
3841
+ }
3842
+ body.innerHTML = h;
3843
+ var ta = body.querySelector("#result-rerun-text");
3844
+ if(ta){
3845
+ ta.value = typed;
3846
+ if(rerunOpen){ body.querySelector("#result-rerun-box").hidden = false; body.querySelector('[data-testid="result-rerun"]').disabled = typed.trim() === ""; }
3847
+ wireResultDoor(body, detail);
3848
+ }
3849
+ }
3850
+
3453
3851
  // ---- selection + loading ----
3454
3852
  var currentRunid = null;
3455
3853
  // P2: guards a stale in-flight response (detail/audit/job, OR the poll
@@ -3476,6 +3874,7 @@
3476
3874
  runIsLive = false;
3477
3875
  openIsScheduled = false;
3478
3876
  lastOpenFetchAt = Date.now();
3877
+ clearResult();
3479
3878
  var myToken = ++selectToken;
3480
3879
  getJSON("/api/runs/" + encodeURIComponent(runid)).then(function(detail){
3481
3880
  if(myToken !== selectToken) return; // a different run was selected before this resolved
@@ -3483,6 +3882,7 @@
3483
3882
  runIsLive = detail.glyph === "▶" && !detail.died;
3484
3883
  openIsScheduled = !!(detail.schedule && detail.schedule.scheduled);
3485
3884
  renderRun(detail);
3885
+ if(detail.hasResult === true) loadResult(runid);
3486
3886
  }).catch(function(e){
3487
3887
  if(myToken !== selectToken) return;
3488
3888
  document.getElementById("run-empty").hidden = false;