bareloop 0.39.0 → 0.40.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 +27 -0
- package/bareloop.context.md +63 -11
- package/package.json +2 -2
- package/src/authorflow.js +333 -58
- package/src/authoring.js +65 -2
- package/src/authorjob.js +57 -9
- package/src/authorreadout.js +50 -2
- package/src/authorrun.js +5 -2
- package/src/authorscout.js +9 -6
- package/src/bridges.js +1 -1
- package/src/calibrate.js +1 -1
- package/src/cardauthor.js +4 -1
- package/src/declaredclose.js +4 -3
- package/src/draftspend.js +6 -1
- package/src/interviewrun.js +56 -27
- package/src/job.js +38 -5
- package/src/jobblock.js +140 -0
- package/src/judged.js +1 -1
- package/src/ledger.js +1 -1
- package/src/panel/authorroutes.js +15 -5
- package/src/panel/authorsession.js +158 -47
- package/src/panel/index.html +179 -70
- package/src/panel/jobplan.js +107 -0
- package/src/panel/server.js +67 -13
- package/src/planrun.js +4 -4
- package/src/promptregisters.js +1 -1
- package/src/providers.js +101 -19
- package/src/reuse.js +1 -1
- package/src/root.js +1 -1
- package/src/run.js +3 -3
- package/src/selection.js +1 -1
- package/src/source.js +64 -1
- package/src/text.js +13 -0
- package/src/transport.js +1 -1
- package/src/userrun.js +3 -3
- package/types/authorflow.d.ts +258 -39
- package/types/authoring.d.ts +7 -1
- package/types/authorjob.d.ts +35 -3
- package/types/authorreadout.d.ts +17 -1
- package/types/authorscout.d.ts +5 -3
- package/types/declaredclose.d.ts +4 -1
- package/types/draftspend.d.ts +8 -2
- package/types/job.d.ts +1 -1
- package/types/jobblock.d.ts +102 -0
- package/types/panel/authorsession.d.ts +50 -14
- package/types/panel/jobplan.d.ts +58 -0
- package/types/panel/server.d.ts +20 -0
- package/types/planrun.d.ts +1 -1
- package/types/providers.d.ts +47 -3
- package/types/run.d.ts +1 -1
- package/types/source.d.ts +26 -0
package/src/panel/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
--grey-dot:#878eac; --grey-bg:#20242f;
|
|
22
22
|
--cyan:#7dcfff; --cyan-bg:#182634;
|
|
23
23
|
--magenta:#bb9af7;
|
|
24
|
-
--accent:#7aa2f7;
|
|
24
|
+
--accent:#7aa2f7; --plan:#7aa2f7;
|
|
25
25
|
--btn-bg:#1f2335; --btn-text:#c0caf5;
|
|
26
26
|
--field-bg:var(--bg); --field-soft-bg:var(--bg); --field-border:var(--text-dim);
|
|
27
27
|
--primary-bg:#7aa2f7; --primary-text:#1a1b26; --primary-border:#7aa2f7;
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
--grey-dot:#626887; --grey-bg:#e2e2e8;
|
|
68
68
|
--cyan:#007197; --cyan-bg:#d6ecf1;
|
|
69
69
|
--magenta:#9854f1;
|
|
70
|
-
--accent:#2668c2;
|
|
70
|
+
--accent:#2668c2; --plan:#2668c2;
|
|
71
71
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
72
72
|
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
73
73
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
--grey-dot:#626887; --grey-bg:#e2e2e8;
|
|
83
83
|
--cyan:#007197; --cyan-bg:#d6ecf1;
|
|
84
84
|
--magenta:#9854f1;
|
|
85
|
-
--accent:#2668c2;
|
|
85
|
+
--accent:#2668c2; --plan:#2668c2;
|
|
86
86
|
--btn-bg:#e9e9ed; --btn-text:#3760bf;
|
|
87
87
|
--field-bg:#ffffff; --field-soft-bg:#f7f7f9;
|
|
88
88
|
--primary-bg:#2668c2; --primary-text:#e1e2e7; --primary-border:#2668c2;
|
|
@@ -121,6 +121,9 @@
|
|
|
121
121
|
[hidden]{display:none !important;}
|
|
122
122
|
.main{display:flex;flex:1;min-height:0;}
|
|
123
123
|
.left-pane{width:420px;flex:none;border-right:1px solid var(--border-strong);display:flex;flex-direction:column;background:var(--panel);}
|
|
124
|
+
/* hamr 2026-10-10: the left pane is 630px on wide screens only. 1200px is the floor: the right pane is then >=570px, never
|
|
125
|
+
narrower than the 480px it already had at 900px with the 420px pane (630 at 900 left it 270 and wrapped its tabs) */
|
|
126
|
+
@media (min-width: 1200px){ .left-pane{width:630px;} }
|
|
124
127
|
.right-pane{flex:1;min-width:0;display:flex;flex-direction:column;}
|
|
125
128
|
|
|
126
129
|
.tabrow{display:flex;border-bottom:1px solid var(--border-strong);background:var(--panel2);counter-reset:tabnum;}
|
|
@@ -466,6 +469,41 @@
|
|
|
466
469
|
/* hamr 2026-10-05: the card's free-text boxes wrap like the ask box (one shared fitBox); a few rows, then they scroll inside */
|
|
467
470
|
/* same look as the card's one-line inputs (white when editable, ordinary 1px border) — a textarea has its own browser border otherwise */
|
|
468
471
|
.job-card textarea.jf-wrap{width:100%;box-sizing:border-box;font:inherit;padding:6px 8px;border:1px solid var(--border-strong);border-radius:0;background:var(--bg);color:var(--text);resize:vertical;overflow-y:auto;max-height:200px;}
|
|
472
|
+
/* P7 — numbered boxes (The job, Inputs): the number gutter sits OUTSIDE the box, left; a hidden mirror measures each
|
|
473
|
+
logical line's wrapped height so a number stays level with its line (docs/product/PANEL-BUILD.md, signed mock) */
|
|
474
|
+
.nbox{display:grid;grid-template-columns:22px minmax(0,1fr);column-gap:6px;align-items:stretch;}
|
|
475
|
+
/* contain:size keeps the gutter's own content out of the row height (the box alone decides it); its padding mirrors the
|
|
476
|
+
textarea's 6px + 1px border top AND bottom so both scroll the same distance and a number never drifts or falls out */
|
|
477
|
+
.nbox .gut{color:var(--text-faint);text-align:right;padding:7px 0;box-sizing:border-box;overflow:hidden;contain:size;font-size:inherit;line-height:1.5;}
|
|
478
|
+
.nbox .gut div{min-height:1.5em;}
|
|
479
|
+
.nbox-in{position:relative;min-width:0;}
|
|
480
|
+
/* the numbered boxes grow with their text up to 8 lines (8 x 1.5em + 6px padding x2 + 1px border x2), then scroll; fitBox reads this cap */
|
|
481
|
+
.job-card .nbox textarea.jf-wrap{display:block;line-height:1.5;max-height:calc(12em + 14px);}
|
|
482
|
+
.nbox-mir{position:absolute;top:0;left:0;visibility:hidden;pointer-events:none;box-sizing:border-box;padding:6px 8px;font:inherit;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
483
|
+
.nbox-mir div{min-height:1.5em;}
|
|
484
|
+
/* P7 — the ONE job plan drawing (chat plan bubble + Job tab): job lines numbered, rule under, checks folded; only the marker is blue */
|
|
485
|
+
.jp{white-space:normal;font-size:13px;}
|
|
486
|
+
.jp-legend{font-size:12px;color:var(--text-faint);margin:0 0 8px;}
|
|
487
|
+
.jp-legend b,.jp .mk{color:var(--plan);font-weight:700;}
|
|
488
|
+
/* a wrapped job line hangs: its text lines up after the number, not under it (the signed mock's gutter shape) */
|
|
489
|
+
.jp .jl{margin-bottom:8px;overflow-wrap:anywhere;padding-left:30px;text-indent:-30px;}
|
|
490
|
+
.jp .jl .sub,.jp .jl .fold{padding-left:0;text-indent:0;}
|
|
491
|
+
.jp .num{display:inline-block;text-indent:0;width:22px;text-align:right;color:var(--text-faint);margin-right:8px;}
|
|
492
|
+
.jp .sub{color:var(--text-dim);padding-left:30px;}
|
|
493
|
+
.jp .fold{padding-left:30px;color:var(--text);}
|
|
494
|
+
.jp details.fold{padding-left:0;margin:6px 0;}
|
|
495
|
+
.jp details>summary{cursor:pointer;list-style:none;}
|
|
496
|
+
.jp details>summary::-webkit-details-marker{display:none;}
|
|
497
|
+
.jp details>summary::before{content:"\203A";color:var(--plan);font-weight:700;margin-right:6px;}
|
|
498
|
+
.jp details[open]>summary::before{content:"\2304";}
|
|
499
|
+
.jp details ul{margin:4px 0 0;padding-left:26px;color:var(--text-dim);}
|
|
500
|
+
.jp .older{font-size:12px;color:var(--text-faint);margin:0 0 8px;}
|
|
501
|
+
.jp .jp-sum{margin:0 0 8px;overflow-wrap:anywhere;}
|
|
502
|
+
.jp .jp-sum .lbl{display:inline-block;color:var(--text-faint);margin-right:8px;}
|
|
503
|
+
.jp-qs{margin:8px 0;overflow-wrap:anywhere;}
|
|
504
|
+
.jp-qs .lbl{display:inline-block;color:var(--text-faint);margin-right:8px;}
|
|
505
|
+
.jp-qs ul{margin:4px 0;padding-left:26px;}
|
|
506
|
+
.jp-goal{font-size:12px;color:var(--text-faint);margin-top:8px;}
|
|
469
507
|
/* hamr 2026-10-05: the focused/selected Chat-card field gets a blue border (the accent), no outline ring */
|
|
470
508
|
#panel-chat :is(input,select,textarea):focus{border-color:var(--accent);outline:none;}
|
|
471
509
|
/* hamr 2026-10-05: the ask box is dimmed until something asks for a reply (placeholder stays readable) */
|
|
@@ -583,29 +621,23 @@
|
|
|
583
621
|
<input id="jf-name" type="text" placeholder="kebab-case, unique">
|
|
584
622
|
</div>
|
|
585
623
|
<div class="field">
|
|
586
|
-
<label for="jf-
|
|
587
|
-
<
|
|
624
|
+
<label for="jf-job">The job</label>
|
|
625
|
+
<div class="nbox" data-testid="nbox-job">
|
|
626
|
+
<div class="gut" id="jf-job-gut" aria-hidden="true"></div>
|
|
627
|
+
<div class="nbox-in"><textarea class="jf-wrap" id="jf-job" rows="3" data-testid="jf-job" spellcheck="false"></textarea><div class="nbox-mir" id="jf-job-mir" aria-hidden="true"></div></div>
|
|
628
|
+
</div>
|
|
588
629
|
</div>
|
|
589
630
|
<div class="field">
|
|
590
|
-
<label for="jf-
|
|
591
|
-
<
|
|
631
|
+
<label for="jf-inputs">Inputs</label>
|
|
632
|
+
<div class="nbox" data-testid="nbox-inputs">
|
|
633
|
+
<div class="gut" id="jf-inputs-gut" aria-hidden="true"></div>
|
|
634
|
+
<div class="nbox-in"><textarea class="jf-wrap" id="jf-inputs" rows="2" data-testid="jf-inputs" spellcheck="false"></textarea><div class="nbox-mir" id="jf-inputs-mir" aria-hidden="true"></div></div>
|
|
635
|
+
</div>
|
|
592
636
|
</div>
|
|
593
637
|
<div class="field">
|
|
594
638
|
<label for="jf-dest">Destination</label>
|
|
595
639
|
<textarea class="jf-wrap" id="jf-dest" rows="1" placeholder="the write fence — where in the repo this run may change files"></textarea>
|
|
596
640
|
</div>
|
|
597
|
-
<div class="field">
|
|
598
|
-
<label for="jf-success">Success</label>
|
|
599
|
-
<textarea class="jf-wrap" id="jf-success" rows="2"></textarea>
|
|
600
|
-
</div>
|
|
601
|
-
<div class="field">
|
|
602
|
-
<label for="jf-guardrails">Guardrails</label>
|
|
603
|
-
<textarea class="jf-wrap" id="jf-guardrails" rows="2"></textarea>
|
|
604
|
-
</div>
|
|
605
|
-
<div class="field">
|
|
606
|
-
<label for="jf-judge">Judge examples <span class="hint" style="display:inline">(rubric only)</span></label>
|
|
607
|
-
<textarea class="jf-wrap" id="jf-judge" rows="2" disabled></textarea>
|
|
608
|
-
</div>
|
|
609
641
|
|
|
610
642
|
<div class="cap-row" data-testid="cap-row">
|
|
611
643
|
<div class="cap-field">
|
|
@@ -750,17 +782,16 @@
|
|
|
750
782
|
<h4 id="details-title">Job card — signed</h4>
|
|
751
783
|
<div class="field"><label>Check type</label><div class="ro-value" id="details-verdict">—</div></div>
|
|
752
784
|
<div class="field"><label>Model</label><div class="ro-value" id="details-model">—</div></div>
|
|
785
|
+
<div class="field" id="details-description-field"><label>Description</label><div class="ro-value" id="details-description">—</div></div>
|
|
786
|
+
<div class="field"><label>The job</label><div class="ro-value" id="details-plan" data-testid="details-plan">—</div></div>
|
|
787
|
+
<div class="field"><label>Inputs</label><div class="ro-value" id="details-inputs" data-testid="details-inputs">—</div></div>
|
|
788
|
+
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
789
|
+
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
753
790
|
<div class="field-row">
|
|
754
791
|
<div class="field"><label>$ cap</label><div class="ro-value" id="details-cap-money">—</div></div>
|
|
755
792
|
<div class="field"><label>Time cap</label><div class="ro-value" id="details-cap-time">—</div></div>
|
|
756
793
|
</div>
|
|
757
|
-
<div class="field"
|
|
758
|
-
<div class="field"><label>Goal</label><div class="ro-value scroll" id="details-goal">—</div></div>
|
|
759
|
-
<div class="field"><label>Success</label><div class="ro-value" id="details-success">—</div></div>
|
|
760
|
-
<div class="field"><label>Guardrails</label><div class="ro-value" id="details-guardrails">—</div></div>
|
|
761
|
-
<div class="field"><label>Source</label><div class="ro-value" id="details-source">—</div></div>
|
|
762
|
-
<div class="field"><label>Destination</label><div class="ro-value" id="details-dest">—</div></div>
|
|
763
|
-
<div class="field"><label>Tools</label><div class="ro-value" id="details-tools">—</div></div>
|
|
794
|
+
<div class="field"><label>Signed</label><div class="ro-value" id="details-signed" data-testid="details-signed">—</div></div>
|
|
764
795
|
<div class="hint" id="job-note"></div>
|
|
765
796
|
</div>
|
|
766
797
|
</section>
|
|
@@ -1991,8 +2022,8 @@
|
|
|
1991
2022
|
var nr = "not recorded";
|
|
1992
2023
|
renderJob({
|
|
1993
2024
|
job: v.job, resolved: false, checkType: v.checkType || nr, model: v.model || nr,
|
|
1994
|
-
goal: v.goal || nr,
|
|
1995
|
-
|
|
2025
|
+
goal: v.goal || nr, plan: v.plan || null, inputs: v.inputs || null, signed: null,
|
|
2026
|
+
destination: nr, tools: nr,
|
|
1996
2027
|
budgetUsd: (typeof v.budgetUsd === "number") ? v.budgetUsd : null,
|
|
1997
2028
|
maxWallMs: (typeof v.maxWallMs === "number") ? v.maxWallMs : null,
|
|
1998
2029
|
note: "imported · view only — the exported job's own spec",
|
|
@@ -3128,6 +3159,35 @@
|
|
|
3128
3159
|
});
|
|
3129
3160
|
});
|
|
3130
3161
|
|
|
3162
|
+
// ---- the ONE job plan drawing (P7): used by the chat's plan bubble AND the Job tab. `plan` is the server's model
|
|
3163
|
+
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3164
|
+
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3165
|
+
function renderJobPlan(plan){
|
|
3166
|
+
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend"><b>\u203A</b> = the approved plan</div>' +
|
|
3167
|
+
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3168
|
+
var checkLine = function(c){
|
|
3169
|
+
return '<div class="fold" data-testid="plan-check"><span class="mk">\u203A</span> check \u00B7 ' + escapeXml(c.name) + (c.cls ? " \u00B7 " + escapeXml(c.cls) : "") + "</div>";
|
|
3170
|
+
};
|
|
3171
|
+
(plan.lines || []).forEach(function(l){
|
|
3172
|
+
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3173
|
+
if(l.rule) h += '<div class="sub">~ ' + escapeXml(l.rule) + "</div>";
|
|
3174
|
+
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3175
|
+
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3176
|
+
h += "</div>";
|
|
3177
|
+
});
|
|
3178
|
+
if((plan.loose || []).length){
|
|
3179
|
+
h += '<div class="jl"><span class="num"></span>other checks' + plan.loose.map(checkLine).join("") + "</div>";
|
|
3180
|
+
}
|
|
3181
|
+
var fold = function(title, items, testid){
|
|
3182
|
+
if(!items || !items.length) return "";
|
|
3183
|
+
return '<details class="fold" data-testid="' + testid + '"><summary>' + escapeXml(title) + " \u00B7 " + items.length + "</summary><ul>" +
|
|
3184
|
+
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3185
|
+
};
|
|
3186
|
+
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3187
|
+
h += fold("Always on", plan.alwaysOn, "plan-alwayson");
|
|
3188
|
+
return h + "</div>";
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3131
3191
|
// ---- Job tab ----
|
|
3132
3192
|
function renderJob(job){
|
|
3133
3193
|
var empty = document.getElementById("job-empty");
|
|
@@ -3143,10 +3203,14 @@
|
|
|
3143
3203
|
var descField = document.getElementById("details-description-field");
|
|
3144
3204
|
if(job.description){ descField.hidden = false; document.getElementById("details-description").textContent = job.description; }
|
|
3145
3205
|
else { descField.hidden = true; }
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
document.getElementById("details-
|
|
3149
|
-
|
|
3206
|
+
// P7: the numbered job lines, their rules and checks — the SAME drawing the chat's plan bubble uses (renderJobPlan)
|
|
3207
|
+
var plan = job.plan || {lines: [{n: 1, text: job.goal || "not recorded", rule: "", checks: [], refused: []}], loose: [], notChecked: [], alwaysOn: [], older: true};
|
|
3208
|
+
document.getElementById("details-plan").innerHTML = renderJobPlan(plan);
|
|
3209
|
+
var ins = Array.isArray(job.inputs) ? job.inputs : [];
|
|
3210
|
+
document.getElementById("details-inputs").innerHTML = ins.length
|
|
3211
|
+
? ins.map(function(i){ return '<div><span class="num-in">' + escapeXml(String(i.n)) + "</span> " + escapeXml(i.label + ": " + i.value) + "</div>"; }).join("")
|
|
3212
|
+
: "not recorded";
|
|
3213
|
+
document.getElementById("details-signed").textContent = job.signed || "not recorded";
|
|
3150
3214
|
document.getElementById("details-dest").textContent = job.destination;
|
|
3151
3215
|
document.getElementById("details-tools").textContent = job.tools;
|
|
3152
3216
|
document.getElementById("details-cap-money").textContent = panelMoney(job.budgetUsd);
|
|
@@ -3218,7 +3282,7 @@
|
|
|
3218
3282
|
// drives both fetches — never two independent timers that could race
|
|
3219
3283
|
// each other. ----
|
|
3220
3284
|
//
|
|
3221
|
-
// F195 (docs/
|
|
3285
|
+
// F195 (docs/wiki/FINDINGS.md): /api/runs re-replays every spine on
|
|
3222
3286
|
// every call (~457ms CPU at 250 rows). hamr's ruling B, 2026-09-27:
|
|
3223
3287
|
// "refresh less often, up to 30s at most, choose the lesser when
|
|
3224
3288
|
// possible" — the Runs list polls every 10s (RUNS_LIST_POLL_MS) instead
|
|
@@ -3594,10 +3658,10 @@
|
|
|
3594
3658
|
var sfNote = document.getElementById("chat-startfrom-note");
|
|
3595
3659
|
var originLine = document.getElementById("chat-origin-line");
|
|
3596
3660
|
// the locked boxes of a reuse card: the signed workflow, greyed and not editable (the server refuses a change anyway)
|
|
3597
|
-
var LOCKED_IDS = ["jf-name", "jf-
|
|
3661
|
+
var LOCKED_IDS = ["jf-name", "jf-job"];
|
|
3598
3662
|
// hamr 2026-10-05: while a drafting session is live NOTHING on the card is editable (the session was started from
|
|
3599
3663
|
// these values). The reuse rule (only Source/Destination/caps open) applies before the start. ONE function owns it.
|
|
3600
|
-
var OPEN_IDS = ["jf-
|
|
3664
|
+
var OPEN_IDS = ["jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time", "jf-model"];
|
|
3601
3665
|
var reuseOn = false;
|
|
3602
3666
|
// Resume mode (hamr 2026-10-07): the card shows the signed job of one run, every box locked except the two caps.
|
|
3603
3667
|
// `resumeRun` = {runid, noTime} while it is on, else null. Never set together with a drafting session or a reuse pick.
|
|
@@ -3613,14 +3677,14 @@
|
|
|
3613
3677
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3614
3678
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3615
3679
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3616
|
-
if(!reuseOn && !live && !resuming){ var ck = document.querySelector('input[name="jf-verdict"]:checked'); document.getElementById("jf-judge").disabled = !ck || ck.value !== "rubric"; }
|
|
3617
3680
|
}
|
|
3618
3681
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3619
3682
|
function clearStartFrom(keepBoxes){
|
|
3620
3683
|
// Clear after a reuse starts a genuinely new job: the reused values do not linger in the boxes — except when a Check type
|
|
3621
3684
|
// radio click leaves reuse (keepBoxes): a radio never blanks what the person has on the card
|
|
3622
3685
|
if(startFrom && !keepBoxes){
|
|
3623
|
-
|
|
3686
|
+
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3687
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3624
3688
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === "deterministic"); });
|
|
3625
3689
|
}
|
|
3626
3690
|
startFrom = null; reuseSession = false; autoSigned = false; reuseCheckType = "deterministic";
|
|
@@ -3632,7 +3696,7 @@
|
|
|
3632
3696
|
// ---- Reuse workflow picker (hamr 2026-10-06): Check type is ONE radio group of three; "Reuse workflow" shows a search
|
|
3633
3697
|
// box over the jobs that have a green local run (GET /api/author/reuse-jobs), a pick fills the card through the SAME
|
|
3634
3698
|
// path the Run tab's button uses (reuseWorkflow -> bareloop-reuse -> applyReuse), then a picked box with `change`.
|
|
3635
|
-
var CARD_BOX_IDS = ["jf-name", "jf-
|
|
3699
|
+
var CARD_BOX_IDS = ["jf-name", "jf-job", "jf-inputs", "jf-dest", "jf-cap-money", "jf-cap-time"];
|
|
3636
3700
|
var rwBox = document.getElementById("rw");
|
|
3637
3701
|
var rwSearch = document.getElementById("rw-search");
|
|
3638
3702
|
var rwQ = document.getElementById("rw-q");
|
|
@@ -3649,9 +3713,11 @@
|
|
|
3649
3713
|
}
|
|
3650
3714
|
function setVerdict(v){
|
|
3651
3715
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.checked = (r.value === v); });
|
|
3716
|
+
setJobPlaceholders(v);
|
|
3652
3717
|
}
|
|
3653
3718
|
function clearCardBoxes(){
|
|
3654
3719
|
CARD_BOX_IDS.forEach(function(id){ document.getElementById(id).value = ""; });
|
|
3720
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
3655
3721
|
document.getElementById("jf-cap-note").textContent = "";
|
|
3656
3722
|
}
|
|
3657
3723
|
// ONE function shows/hides the picker: visible only while the radio is on Reuse workflow; the search box before a
|
|
@@ -3699,7 +3765,6 @@
|
|
|
3699
3765
|
function openPicker(){
|
|
3700
3766
|
rwQ.value = ""; rwHl = 0;
|
|
3701
3767
|
rwJobs = null; rwFailed = false; // a fresh open shows "Loading…", never the previous list
|
|
3702
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3703
3768
|
renderPicker();
|
|
3704
3769
|
renderReuseList();
|
|
3705
3770
|
loadReuseJobs();
|
|
@@ -3740,7 +3805,7 @@
|
|
|
3740
3805
|
card.hidden = false;
|
|
3741
3806
|
originLine.hidden = true;
|
|
3742
3807
|
exitResumeMode();
|
|
3743
|
-
thread.innerHTML = "";
|
|
3808
|
+
thread.innerHTML = ""; lastMsgSig = null;
|
|
3744
3809
|
errEl.textContent = "";
|
|
3745
3810
|
actionErrEl.textContent = "";
|
|
3746
3811
|
sessionId = null;
|
|
@@ -3770,7 +3835,6 @@
|
|
|
3770
3835
|
function resetCard(){
|
|
3771
3836
|
clearCardBoxes();
|
|
3772
3837
|
setVerdict("deterministic");
|
|
3773
|
-
document.getElementById("jf-judge").disabled = true;
|
|
3774
3838
|
modelSelect.selectedIndex = 0;
|
|
3775
3839
|
msgInput.value = "";
|
|
3776
3840
|
rwQ.value = "";
|
|
@@ -3799,12 +3863,9 @@
|
|
|
3799
3863
|
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3800
3864
|
model: document.getElementById("jf-model").value,
|
|
3801
3865
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3802
|
-
|
|
3803
|
-
|
|
3866
|
+
jobText: document.getElementById("jf-job").value.trim(),
|
|
3867
|
+
inputs: document.getElementById("jf-inputs").value.trim(),
|
|
3804
3868
|
destination: document.getElementById("jf-dest").value.trim(),
|
|
3805
|
-
success: document.getElementById("jf-success").value.trim(),
|
|
3806
|
-
guardrails: document.getElementById("jf-guardrails").value.trim(),
|
|
3807
|
-
judgeExamples: document.getElementById("jf-judge").value.trim(),
|
|
3808
3869
|
capUsd: moneyCap,
|
|
3809
3870
|
maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
3810
3871
|
};
|
|
@@ -3853,7 +3914,7 @@
|
|
|
3853
3914
|
setVerdict(v);
|
|
3854
3915
|
renderPicker();
|
|
3855
3916
|
}
|
|
3856
|
-
|
|
3917
|
+
setJobPlaceholders(v);
|
|
3857
3918
|
refreshStartEnabled();
|
|
3858
3919
|
}
|
|
3859
3920
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){
|
|
@@ -3964,11 +4025,23 @@
|
|
|
3964
4025
|
if(role === "system") return "";
|
|
3965
4026
|
return "bareloop";
|
|
3966
4027
|
}
|
|
4028
|
+
// the thread is redrawn only when its messages changed: a 2 s poll tick must not close a fold the person just opened
|
|
4029
|
+
var lastMsgSig = null;
|
|
3967
4030
|
function renderMessages(state){
|
|
4031
|
+
var sig = JSON.stringify(state.messages);
|
|
4032
|
+
if(sig === lastMsgSig) return;
|
|
4033
|
+
lastMsgSig = sig;
|
|
3968
4034
|
thread.innerHTML = state.messages.map(function(m){
|
|
3969
4035
|
var cls = m.role === "you" ? "you" : (m.role === "system" ? "system" : "bot");
|
|
3970
4036
|
var who = chatWhoLabel(m.role);
|
|
3971
4037
|
var whoHtml = who ? ('<div class="who">' + escapeXml(who) + '</div>') : "";
|
|
4038
|
+
if(m.kind === "plan" && m.plan){
|
|
4039
|
+
// P7: the confirm turn's plan is drawn by the one job-plan renderer (the Job tab uses the same)
|
|
4040
|
+
var qs = (m.plan.questions || []).length
|
|
4041
|
+
? '<div class="jp-qs" data-testid="plan-questions"><span class="lbl">AI\'s open questions</span><ul>' +
|
|
4042
|
+
m.plan.questions.map(function(q){ return "<li>" + escapeXml(q) + "</li>"; }).join("") + "</ul><b>Revise if you need to.</b></div>" : "";
|
|
4043
|
+
return '<div class="msg ' + cls + '" data-testid="plan-bubble">' + whoHtml + renderJobPlan(m.plan) + qs + '<div class="jp-goal">' + escapeXml(m.text) + "</div></div>";
|
|
4044
|
+
}
|
|
3972
4045
|
return '<div class="msg ' + cls + '">' + whoHtml + escapeXml(m.text) + '</div>';
|
|
3973
4046
|
}).join("");
|
|
3974
4047
|
thread.scrollTop = thread.scrollHeight;
|
|
@@ -4062,22 +4135,61 @@
|
|
|
4062
4135
|
fitCardBoxes();
|
|
4063
4136
|
}
|
|
4064
4137
|
msgInput.addEventListener("input", renderMain);
|
|
4138
|
+
// P7 — the numbered boxes. A plain job line is numbered 1, 2, 3; a line starting `~` is a rule for the line above and has no
|
|
4139
|
+
// number; Inputs numbers every line; empty lines have none. A hidden mirror (same font/width/padding) measures each logical
|
|
4140
|
+
// line's wrapped height so every number stays level with its own line — the same rules src/jobblock.js parses by.
|
|
4141
|
+
var NUMBERED_IDS = ["jf-job", "jf-inputs"];
|
|
4142
|
+
var FIT_IDS = ["jf-job", "jf-inputs", "jf-dest"];
|
|
4143
|
+
function gutterSync(id){
|
|
4144
|
+
var ta = document.getElementById(id), gut = document.getElementById(id + "-gut"), mir = document.getElementById(id + "-mir");
|
|
4145
|
+
if(!ta || !gut || !mir) return;
|
|
4146
|
+
var lines = ta.value.split("\n");
|
|
4147
|
+
var isJob = id === "jf-job";
|
|
4148
|
+
var n = 0;
|
|
4149
|
+
var nums = lines.map(function(l){
|
|
4150
|
+
var t = l.trim();
|
|
4151
|
+
if(t === "" || (isJob && t.charAt(0) === "~")) return "";
|
|
4152
|
+
n += 1; return String(n);
|
|
4153
|
+
});
|
|
4154
|
+
mir.style.width = ta.clientWidth ? ta.clientWidth + "px" : "";
|
|
4155
|
+
mir.innerHTML = lines.map(function(l){ return "<div>" + (l === "" ? "\u200b" : escapeXml(l)) + "</div>"; }).join("");
|
|
4156
|
+
var rows = mir.children;
|
|
4157
|
+
gut.innerHTML = nums.map(function(t, i){
|
|
4158
|
+
var hgt = rows[i] ? rows[i].offsetHeight : 0;
|
|
4159
|
+
return '<div' + (hgt ? ' style="height:' + hgt + 'px"' : "") + ">" + t + "</div>";
|
|
4160
|
+
}).join("");
|
|
4161
|
+
gut.scrollTop = ta.scrollTop;
|
|
4162
|
+
}
|
|
4163
|
+
// the empty-box examples (grey placeholders that vanish on typing) change with the Check type radio
|
|
4164
|
+
var JOB_PLACEHOLDERS = {
|
|
4165
|
+
deterministic: "Fix the failing date tests in src/date.js\n~ don't edit any test file\nMake npm test pass",
|
|
4166
|
+
rubric: "Write a one-page summary of docs/plan.md\n~ PASS: every number is quoted from the file\n~ FAIL: a number that is not in the file\nKeep it under 300 words",
|
|
4167
|
+
};
|
|
4168
|
+
var INPUTS_PLACEHOLDER = "repo: /home/me/myrepo\nspec: docs/spec.md";
|
|
4169
|
+
function setJobPlaceholders(v){
|
|
4170
|
+
document.getElementById("jf-job").setAttribute("placeholder", JOB_PLACEHOLDERS[v === "rubric" ? "rubric" : "deterministic"]);
|
|
4171
|
+
document.getElementById("jf-inputs").setAttribute("placeholder", INPUTS_PLACEHOLDER);
|
|
4172
|
+
}
|
|
4173
|
+
setJobPlaceholders("deterministic");
|
|
4065
4174
|
// grow a text box with its text (a few rows, then it scrolls inside); ONE function for the ask box and the card boxes.
|
|
4066
4175
|
// A box that is hidden (or not laid out) measures 0 and gets no inline height.
|
|
4067
4176
|
function fitBox(el){
|
|
4068
4177
|
el.style.height = "auto";
|
|
4069
4178
|
if(!el.scrollHeight){ el.style.height = ""; return; }
|
|
4070
4179
|
var border = el.offsetHeight - el.clientHeight;
|
|
4071
|
-
|
|
4180
|
+
var cap = parseFloat(window.getComputedStyle(el).maxHeight);
|
|
4181
|
+
el.style.height = Math.min(el.scrollHeight + border, cap > 0 ? cap : 200) + "px";
|
|
4072
4182
|
}
|
|
4073
4183
|
function fitCardBoxes(){
|
|
4074
|
-
|
|
4184
|
+
FIT_IDS.forEach(function(id){ fitBox(document.getElementById(id)); });
|
|
4185
|
+
NUMBERED_IDS.forEach(gutterSync);
|
|
4075
4186
|
}
|
|
4076
4187
|
// live growth: each card box re-fits on its own input (typing and Enter), through the same fitBox
|
|
4077
|
-
|
|
4188
|
+
FIT_IDS.forEach(function(id){
|
|
4078
4189
|
var box = document.getElementById(id);
|
|
4079
|
-
box.addEventListener("input", function(){ fitBox(box); });
|
|
4190
|
+
box.addEventListener("input", function(){ fitBox(box); if(NUMBERED_IDS.indexOf(id) !== -1) gutterSync(id); });
|
|
4080
4191
|
});
|
|
4192
|
+
NUMBERED_IDS.forEach(function(id){ var box = document.getElementById(id); box.addEventListener("scroll", function(){ document.getElementById(id + "-gut").scrollTop = box.scrollTop; }); });
|
|
4081
4193
|
window.addEventListener("resize", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4082
4194
|
document.getElementById("tab-chat").addEventListener("click", function(){ fitCardBoxes(); fitBox(msgInput); });
|
|
4083
4195
|
// Enter sends through the one main button; Shift+Enter (or IME composition) keeps its newline
|
|
@@ -4182,11 +4294,8 @@
|
|
|
4182
4294
|
}
|
|
4183
4295
|
modelSelect.value = c.model;
|
|
4184
4296
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4188
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4189
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4297
|
+
fillCard(c);
|
|
4298
|
+
|
|
4190
4299
|
if(reuseSession) setReuseLocked(true);
|
|
4191
4300
|
renderPicker();
|
|
4192
4301
|
fitCardBoxes();
|
|
@@ -4208,6 +4317,14 @@
|
|
|
4208
4317
|
|
|
4209
4318
|
// ---- Reuse workflow — the card opens filled from the SIGNED job: four open boxes, the rest greyed ----
|
|
4210
4319
|
function setVal(id, v){ document.getElementById(id).value = (v === undefined || v === null) ? "" : String(v); }
|
|
4320
|
+
// ONE owner of filling the card from a prefill (a re-attached session, Reuse, Edit in chat, Resume): the six boxes P7 left
|
|
4321
|
+
// (the old Goal/Source/Success/Guardrails/Judge boxes are gone), then every box re-fits and its number gutter re-measures.
|
|
4322
|
+
function fillCard(c){
|
|
4323
|
+
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
4324
|
+
setVal("jf-dest", c.destination); setVal("jf-cap-money", c.capUsd);
|
|
4325
|
+
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4326
|
+
fitCardBoxes();
|
|
4327
|
+
}
|
|
4211
4328
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
4212
4329
|
// `bareloop-reuse` event), so the card ends up in the same picked state whichever door was used.
|
|
4213
4330
|
function applyReuse(d){
|
|
@@ -4220,11 +4337,8 @@
|
|
|
4220
4337
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4221
4338
|
setVerdict("reuse");
|
|
4222
4339
|
if(c.model){ modelSelect.value = c.model; }
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4226
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4227
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4340
|
+
fillCard(c);
|
|
4341
|
+
|
|
4228
4342
|
setReuseLocked(true);
|
|
4229
4343
|
renderPicker();
|
|
4230
4344
|
sfLine.hidden = false;
|
|
@@ -4249,11 +4363,8 @@
|
|
|
4249
4363
|
openNewCard();
|
|
4250
4364
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4251
4365
|
if(c.model){ modelSelect.value = c.model; }
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
setVal("jf-judge", c.judgeExamples); setVal("jf-cap-money", c.capUsd);
|
|
4255
|
-
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4256
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4366
|
+
fillCard(c);
|
|
4367
|
+
|
|
4257
4368
|
originLine.textContent = d.origin || "";
|
|
4258
4369
|
originLine.hidden = !d.origin;
|
|
4259
4370
|
renderPicker();
|
|
@@ -4276,13 +4387,10 @@
|
|
|
4276
4387
|
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4277
4388
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4278
4389
|
if(c.model){ modelSelect.value = c.model; }
|
|
4279
|
-
|
|
4280
|
-
setVal("jf-dest", c.destination); setVal("jf-success", c.success); setVal("jf-guardrails", c.guardrails);
|
|
4281
|
-
setVal("jf-judge", c.judgeExamples);
|
|
4390
|
+
fillCard(c);
|
|
4282
4391
|
setVal("jf-cap-money", r.budgetUsd);
|
|
4283
4392
|
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
4284
4393
|
document.getElementById("jf-cap-time").parentNode.hidden = resumeRun.noTime;
|
|
4285
|
-
document.getElementById("jf-judge").disabled = c.checkType !== "rubric";
|
|
4286
4394
|
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.";
|
|
4287
4395
|
originLine.classList.add("res");
|
|
4288
4396
|
originLine.hidden = false;
|
|
@@ -4400,6 +4508,7 @@
|
|
|
4400
4508
|
if(!o.ok){ chatActionFailed(o.error); return; }
|
|
4401
4509
|
openNewCard();
|
|
4402
4510
|
chatActionOk();
|
|
4511
|
+
lastMsgSig = null;
|
|
4403
4512
|
thread.innerHTML = '<div class="msg system">' + escapeXml("The boxes are open as you drafted them. Edit what you want, then Start drafting. The first draft's cost stays counted.") + "</div>";
|
|
4404
4513
|
}).catch(function(){ chatActionFailed("network error — could not reach the panel server, not revised"); });
|
|
4405
4514
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The job plan MODEL (P7, docs/product/PANEL-BUILD.md "The job" block): ONE owner of the structure the page's single
|
|
2
|
+
// `renderJobPlan()` draws in two places — the drafting confirm card (the plan bubble) and the read-only Job tab. The
|
|
3
|
+
// server builds it; the page only draws it (so the two places can never read the same plan two ways).
|
|
4
|
+
//
|
|
5
|
+
// { lines: [{n, text, rule, checks: [{name, cls}], refused: [reason...]}],
|
|
6
|
+
// loose: [{name, cls}], checks that name no job line (an older caller, or a model slip)
|
|
7
|
+
// notChecked: [text...], "the AI's own reading" — what the drafter says nothing checks
|
|
8
|
+
// alwaysOn: [text...], the mandatory guards / protections, which serve no job line
|
|
9
|
+
// older: boolean } an older job: the numbered lines were not saved
|
|
10
|
+
//
|
|
11
|
+
// `cls` is 'machine check' | 'judge' | null (null = not known yet: the confirm card's checks are the model's plan, the
|
|
12
|
+
// stage kinds exist only after drafting). Pure; no file IO.
|
|
13
|
+
import { KIND_CATALOGUE } from '../authoring.js';
|
|
14
|
+
|
|
15
|
+
/** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
|
|
16
|
+
/** @typedef {{n: number, text: string, rule: string, checks: PlanCheck[], refused: string[]}} PlanLine */
|
|
17
|
+
/** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `machine check` for a kind a command decides, `judge` for one that needs judgement; `null` for a stage whose kind is
|
|
21
|
+
* unknown (a hand-written command close has no kind: it is a command, so a machine check).
|
|
22
|
+
* @param {unknown} kind
|
|
23
|
+
* @returns {'machine check'|'judge'|null}
|
|
24
|
+
*/
|
|
25
|
+
export function checkClassOfKind(kind) {
|
|
26
|
+
if (typeof kind !== 'string') return null;
|
|
27
|
+
const spec = Object.hasOwn(KIND_CATALOGUE, kind) ? KIND_CATALOGUE[kind] : null;
|
|
28
|
+
if (!spec) return null;
|
|
29
|
+
return spec.verdictClass === 'green' ? 'machine check' : 'judge';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The plan of a SIGNED spec. With `jobLines`, each stage lands under EACH line its `fromLine` array names (a stage with none is
|
|
34
|
+
* a mandatory guard: "Always on"), `closeDecl.refused` reasons land under their line, and `closeDecl.notes` are the
|
|
35
|
+
* drafter's own "not checked" words. Without `jobLines` (a spec that predates the job block) the signed goal sentence is
|
|
36
|
+
* line 1 and every stage name is a check under it — `older: true`.
|
|
37
|
+
* @param {any} spec
|
|
38
|
+
* @returns {JobPlan}
|
|
39
|
+
*/
|
|
40
|
+
export function planFromSpec(spec) {
|
|
41
|
+
/** @type {{name: string, cls: 'machine check'|'judge'|null, fromLine: number[]|null}[]} */
|
|
42
|
+
let stages = [];
|
|
43
|
+
if (spec && Array.isArray(spec.closeDecl?.stages)) {
|
|
44
|
+
stages = spec.closeDecl.stages.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
|
|
45
|
+
.map((/** @type {any} */ s) => ({ name: s.name, cls: checkClassOfKind(s.kind), fromLine: Array.isArray(s.fromLine) && s.fromLine.length ? s.fromLine.filter(Number.isInteger) : null }));
|
|
46
|
+
} else if (spec && Array.isArray(spec.close)) {
|
|
47
|
+
stages = spec.close.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
|
|
48
|
+
.map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
|
|
49
|
+
}
|
|
50
|
+
const notes = Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : [];
|
|
51
|
+
const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
|
|
52
|
+
if (jl.length === 0) {
|
|
53
|
+
return {
|
|
54
|
+
lines: [{
|
|
55
|
+
n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '',
|
|
56
|
+
checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
|
|
57
|
+
}],
|
|
58
|
+
loose: [], notChecked: notes, alwaysOn: [], older: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** @type {PlanLine[]} */
|
|
62
|
+
const lines = jl.map((/** @type {any} */ l) => ({ n: l.n, text: l.text, rule: typeof l.rule === 'string' ? l.rule : '', checks: [], refused: [] }));
|
|
63
|
+
/** @type {PlanCheck[]} */
|
|
64
|
+
const loose = [];
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const alwaysOn = [];
|
|
67
|
+
for (const s of stages) {
|
|
68
|
+
if (s.fromLine === null) { alwaysOn.push(s.name); continue; }
|
|
69
|
+
// a check that serves several job lines is shown under EACH line it serves (no new element: the same check line, repeated)
|
|
70
|
+
const served = lines.filter((l) => /** @type {number[]} */ (s.fromLine).includes(l.n));
|
|
71
|
+
for (const line of served) line.checks.push({ name: s.name, cls: s.cls });
|
|
72
|
+
if (served.length === 0) loose.push({ name: s.name, cls: s.cls });
|
|
73
|
+
}
|
|
74
|
+
const refused = Array.isArray(spec?.closeDecl?.refused) ? spec.closeDecl.refused : [];
|
|
75
|
+
for (const r of refused) {
|
|
76
|
+
const line = lines.find((l) => l.n === r?.line);
|
|
77
|
+
if (line && typeof r.reason === 'string') line.refused.push(r.reason);
|
|
78
|
+
}
|
|
79
|
+
// the model's signed goal sentence rides along: the page draws it as the "AI's summary" row (an older job shows it as line 1 instead)
|
|
80
|
+
return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(typeof spec?.goal === 'string' && spec.goal ? { goal: spec.goal } : {}) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The plan of the confirm turn, before anything is drafted: the person's own job lines, the model's checks under the
|
|
85
|
+
* line each names, its own "not checked" words, and the real protections (code-owned) as "Always on".
|
|
86
|
+
* @param {{n: number, text: string, rule: string}[]|null|undefined} jobLines
|
|
87
|
+
* @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
|
|
88
|
+
* @returns {JobPlan}
|
|
89
|
+
*/
|
|
90
|
+
export function planFromConfirm(jobLines, plan) {
|
|
91
|
+
const items = Array.isArray(plan?.checkItems) && plan.checkItems.length
|
|
92
|
+
? plan.checkItems
|
|
93
|
+
: (Array.isArray(plan?.checks) ? plan.checks.map((c) => ({ text: String(c), fromLine: null })) : []);
|
|
94
|
+
const jl = Array.isArray(jobLines) ? jobLines : [];
|
|
95
|
+
/** @type {PlanLine[]} */
|
|
96
|
+
const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, checks: [], refused: [] }));
|
|
97
|
+
/** @type {PlanCheck[]} */
|
|
98
|
+
const loose = [];
|
|
99
|
+
for (const c of items) {
|
|
100
|
+
const served = lines.filter((l) => Array.isArray(c.fromLine) && c.fromLine.includes(l.n));
|
|
101
|
+
for (const line of served) line.checks.push({ name: c.text, cls: null });
|
|
102
|
+
if (served.length === 0) loose.push({ name: c.text, cls: null });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
lines, loose, notChecked: [...(plan?.notChecked ?? [])], alwaysOn: [...(plan?.protections ?? [])], older: false,
|
|
106
|
+
};
|
|
107
|
+
}
|