bareloop 0.41.0 → 0.42.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 +63 -1
- package/README.md +13 -21
- package/bareloop.context.md +44 -4
- package/package.json +4 -3
- package/src/authorflow.js +337 -29
- package/src/authoring.js +92 -7
- package/src/authorjob.js +78 -16
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +14 -5
- package/src/authorscout.js +108 -0
- package/src/declaredclose.js +23 -12
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/inputlook.js +301 -0
- package/src/job.js +2 -1
- package/src/jobblock.js +7 -2
- package/src/kinds.js +84 -3
- package/src/labelscan.js +170 -0
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +15 -2
- package/src/panel/authorsession.js +99 -22
- package/src/panel/index.html +33 -7
- package/src/panel/jobplan.js +34 -11
- package/src/panel/server.js +19 -3
- package/src/planrun.js +3 -2
- package/src/rubricstep.js +804 -0
- package/src/source.js +166 -1
- package/src/u-readout.js +1 -1
- package/src/userrun.js +14 -2
- package/types/authorflow.d.ts +267 -13
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +7 -5
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +70 -0
- package/types/declaredclose.d.ts +4 -2
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/inputlook.d.ts +238 -0
- package/types/jobblock.d.ts +8 -1
- package/types/labelscan.d.ts +38 -0
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorsession.d.ts +18 -2
- package/types/panel/jobplan.d.ts +12 -4
- package/types/panel/server.d.ts +4 -2
- package/types/rubricstep.d.ts +286 -0
- package/types/source.d.ts +94 -0
- package/types/userrun.d.ts +11 -0
package/src/panel/index.html
CHANGED
|
@@ -113,8 +113,9 @@
|
|
|
113
113
|
.topbar > div:first-child{display:flex;align-items:baseline;gap:0;white-space:nowrap;overflow:hidden;}
|
|
114
114
|
.topbar .brand{font-weight:700;font-size:17px;letter-spacing:0.06em;flex:none;}
|
|
115
115
|
.topbar .brand::before{content:"■ ";}
|
|
116
|
-
.topbar .tagline{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;}
|
|
117
|
-
.topbar .tagline::before{content:"· ";color:var(--text-faint);}
|
|
116
|
+
.topbar .tagline,.topbar .version{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;font-weight:400;}
|
|
117
|
+
.topbar .tagline::before,.topbar .version::before{content:"· ";color:var(--text-faint);}
|
|
118
|
+
.topbar .version{flex:none;}
|
|
118
119
|
@media (max-width: 600px){ .topbar .tagline{display:none;} }
|
|
119
120
|
.topbar-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
|
120
121
|
|
|
@@ -590,6 +591,9 @@
|
|
|
590
591
|
.pv-typed input{flex:1 1 160px;min-width:0;max-width:260px;min-height:28px;box-sizing:border-box;}
|
|
591
592
|
.pv-typed .hint{flex:1 1 100%;margin:0;}
|
|
592
593
|
.hint.bad{color:var(--red);}
|
|
594
|
+
/* a refused Start (a $0 refusal, e.g. an Inputs line) is read like the red failure line of the progress list, not like a faint hint */
|
|
595
|
+
.card-refusal:not(:empty){font-size:13px;margin-top:8px;line-height:1.4;}
|
|
596
|
+
.card-refusal:empty{display:none;}
|
|
593
597
|
.field label.jf-thinking{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:6px;margin-top:6px;font-size:12px;text-transform:none;letter-spacing:0;}
|
|
594
598
|
.field label.jf-thinking input{width:auto;min-height:0;margin:0;}
|
|
595
599
|
.table-wrap{overflow-x:auto;border:1px solid var(--border-strong);background:var(--panel2);max-width:100%;}
|
|
@@ -638,6 +642,7 @@
|
|
|
638
642
|
<header class="topbar">
|
|
639
643
|
<div>
|
|
640
644
|
<span class="brand">bareloop</span>
|
|
645
|
+
<span class="version" id="panel-version">v__BARELOOP_PANEL_VERSION__</span>
|
|
641
646
|
<span class="tagline">automate a job, verified</span>
|
|
642
647
|
</div>
|
|
643
648
|
<div class="topbar-right">
|
|
@@ -726,7 +731,7 @@
|
|
|
726
731
|
<input id="jf-cap-time" type="text" placeholder="or blank = no wall">
|
|
727
732
|
</div>
|
|
728
733
|
</div>
|
|
729
|
-
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
734
|
+
<div class="hint bad card-refusal" id="chat-card-error" data-testid="chat-card-error" role="alert"></div>
|
|
730
735
|
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
731
736
|
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
732
737
|
<div class="ended-actions"><button class="btn primary" type="button" id="chat-resume-sign" data-testid="btn-sign-resume">Sign & resume</button> <button class="btn" type="button" id="chat-resume-cancel" data-testid="btn-resume-cancel">Cancel</button></div>
|
|
@@ -2488,6 +2493,19 @@
|
|
|
2488
2493
|
}, 1500);
|
|
2489
2494
|
}
|
|
2490
2495
|
|
|
2496
|
+
// M3 (Part 1c full-run fixes): after Sign & run the new run's row is written a few seconds later (the runner starts, then
|
|
2497
|
+
// appends its row), and the Runs list only re-fetches every RUNS_LIST_POLL_MS — so a run just signed showed only after a page
|
|
2498
|
+
// refresh. Like a resume, a sign forces list refreshes until a run that was not listed before the click appears; bounded.
|
|
2499
|
+
function afterSignRefresh(knownIds, tries){
|
|
2500
|
+
refreshRunsList(true).then(function(runs){
|
|
2501
|
+
var fresh = Array.isArray(runs) && runs.some(function(r){ return knownIds.indexOf(r.runid) === -1; });
|
|
2502
|
+
if(!fresh && tries > 0) setTimeout(function(){ afterSignRefresh(knownIds, tries - 1); }, 1500);
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
function currentRunIds(){
|
|
2506
|
+
try { return (JSON.parse(lastRunsSig || "[]") || []).map(function(r){ return r.runid; }); } catch(e){ return []; }
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2491
2509
|
function renderRun(detail){
|
|
2492
2510
|
if(detail.fileMissing){
|
|
2493
2511
|
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
@@ -3235,14 +3253,15 @@
|
|
|
3235
3253
|
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3236
3254
|
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3237
3255
|
function renderJobPlan(plan){
|
|
3238
|
-
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"
|
|
3256
|
+
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">Done when every <b>\u203A</b> check passes</div>' +
|
|
3239
3257
|
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3240
3258
|
var checkLine = function(c){
|
|
3241
3259
|
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>";
|
|
3242
3260
|
};
|
|
3243
3261
|
(plan.lines || []).forEach(function(l){
|
|
3244
3262
|
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3245
|
-
|
|
3263
|
+
// one `~` row per rule (fwdloop's shape); an older plan that carries only the joined `rule` still shows it
|
|
3264
|
+
(l.rules && l.rules.length ? l.rules : (l.rule ? [l.rule] : [])).forEach(function(r){ h += '<div class="sub">~ ' + escapeXml(r) + "</div>"; });
|
|
3246
3265
|
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3247
3266
|
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3248
3267
|
h += "</div>";
|
|
@@ -3256,7 +3275,7 @@
|
|
|
3256
3275
|
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3257
3276
|
};
|
|
3258
3277
|
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3259
|
-
h += fold("Always
|
|
3278
|
+
h += fold("Always checked", plan.alwaysOn, "plan-alwayson");
|
|
3260
3279
|
return h + "</div>";
|
|
3261
3280
|
}
|
|
3262
3281
|
|
|
@@ -3402,7 +3421,7 @@
|
|
|
3402
3421
|
pollFailStreak = 0;
|
|
3403
3422
|
var runs = result.runs || [];
|
|
3404
3423
|
var next = sig(runs);
|
|
3405
|
-
if(next === lastRunsSig) return; // unchanged payload — never rebuild the DOM for nothing
|
|
3424
|
+
if(next === lastRunsSig) return runs; // unchanged payload — never rebuild the DOM for nothing
|
|
3406
3425
|
lastRunsSig = next;
|
|
3407
3426
|
withScrollPreserved(["left-pane-body"], function(){
|
|
3408
3427
|
// setItems re-runs the SAME filter/search state and view mode
|
|
@@ -3410,6 +3429,7 @@
|
|
|
3410
3429
|
// bar's own state) — a poll tick never resets either.
|
|
3411
3430
|
runsFilterBar.setItems(runs);
|
|
3412
3431
|
});
|
|
3432
|
+
return runs;
|
|
3413
3433
|
}).catch(function(){
|
|
3414
3434
|
pollFailStreak++;
|
|
3415
3435
|
// Keep the last good render; a transient failure is invisible. Only a
|
|
@@ -3906,6 +3926,7 @@
|
|
|
3906
3926
|
var mainHint = document.getElementById("chat-main-hint");
|
|
3907
3927
|
var reopenBtn = document.getElementById("chat-reopen-btn");
|
|
3908
3928
|
var mainAction = "none";
|
|
3929
|
+
var knownRunIdsAtSign = null; // the run ids listed when Sign & run was pressed (M3: afterSignRefresh waits for a new one)
|
|
3909
3930
|
var lastState = null;
|
|
3910
3931
|
var startOk = false;
|
|
3911
3932
|
var errEl = document.getElementById("chat-card-error");
|
|
@@ -4596,6 +4617,7 @@
|
|
|
4596
4617
|
renderActions(j.state);
|
|
4597
4618
|
if(reuseSession && !autoSigned && j.state.phase === "prepared" && j.state.specHash){
|
|
4598
4619
|
autoSigned = true;
|
|
4620
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4599
4621
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: j.state.specHash}).then(function(r){
|
|
4600
4622
|
var o = chatPostOutcome(r);
|
|
4601
4623
|
if(o.ok){ chatActionOk(); } else { autoSigned = false; chatActionFailed(o.error); }
|
|
@@ -4604,6 +4626,7 @@
|
|
|
4604
4626
|
}
|
|
4605
4627
|
if(j.state.phase === "signed"){
|
|
4606
4628
|
clearInterval(pollTimer);
|
|
4629
|
+
afterSignRefresh(knownRunIdsAtSign || currentRunIds(), 12);
|
|
4607
4630
|
document.getElementById("tab-runs").click();
|
|
4608
4631
|
// hamr's ruling 2026-10-05: the run has started, so the Chat card goes back to the empty card (the one reset
|
|
4609
4632
|
// Clear uses) — it must not keep showing a signed job that invites a rerun. The run itself is untouched.
|
|
@@ -4626,6 +4649,8 @@
|
|
|
4626
4649
|
authorPost("/api/author/start", body).then(function(r){
|
|
4627
4650
|
if(r.status !== 200 || !r.body.ok){
|
|
4628
4651
|
errEl.textContent = (r.body && r.body.error) || "refused";
|
|
4652
|
+
// the refusal is the answer to the click: bring it into view (it sits under the cap fields, above the progress list)
|
|
4653
|
+
try { errEl.scrollIntoView({block: "nearest"}); } catch(e){}
|
|
4629
4654
|
sessionLive = false;
|
|
4630
4655
|
refreshStartEnabled();
|
|
4631
4656
|
return;
|
|
@@ -4906,6 +4931,7 @@
|
|
|
4906
4931
|
authorGet("/api/author/" + sessionId).then(function(j){
|
|
4907
4932
|
var hash = j.state && j.state.specHash;
|
|
4908
4933
|
if(!hash) return;
|
|
4934
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4909
4935
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: hash}).then(function(r){
|
|
4910
4936
|
var o = chatPostOutcome(r);
|
|
4911
4937
|
if(o.ok){ chatActionOk(); } else { chatActionFailed(o.error); return; }
|
package/src/panel/jobplan.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `renderJobPlan()` draws in two places — the drafting confirm card (the plan bubble) and the read-only Job tab. The
|
|
3
3
|
// server builds it; the page only draws it (so the two places can never read the same plan two ways).
|
|
4
4
|
//
|
|
5
|
-
// { lines: [{n, text, rule, checks: [{name, cls}], refused: [reason...]}],
|
|
5
|
+
// { lines: [{n, text, rule, rules: [string...], checks: [{name, cls}], refused: [reason...]}],
|
|
6
6
|
// loose: [{name, cls}], checks that name no job line (an older caller, or a model slip)
|
|
7
7
|
// notChecked: [text...], "the AI's own reading" — what the drafter says nothing checks
|
|
8
8
|
// alwaysOn: [text...], the mandatory guards / protections, which serve no job line
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// `cls` is 'machine check' | 'judge' | null (null = not known yet: the confirm card's checks are the model's plan, the
|
|
12
12
|
// stage kinds exist only after drafting). Pure; no file IO.
|
|
13
13
|
import { KIND_CATALOGUE } from '../authoring.js';
|
|
14
|
+
import { checksOfClose, proseNotChecked, lineRules } from '../rubricstep.js';
|
|
14
15
|
|
|
15
16
|
/** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
|
|
16
|
-
/** @typedef {{n: number, text: string, rule: string, checks: PlanCheck[], refused: string[]}} PlanLine */
|
|
17
|
+
/** @typedef {{n: number, text: string, rule: string, rules: string[], checks: PlanCheck[], refused: string[]}} PlanLine */
|
|
17
18
|
/** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -29,9 +30,19 @@ export function checkClassOfKind(kind) {
|
|
|
29
30
|
return spec.verdictClass === 'green' ? 'machine check' : 'judge';
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The `~` rules of one job line, each on its own (the page draws one `~` row per rule): the line's own `rules` array when it
|
|
35
|
+
* has one (a rule may itself contain "; "), else the joined `rule` split on "; " (an older line that saved only the join).
|
|
36
|
+
* @param {any} l @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export function rulesOfLine(l) {
|
|
39
|
+
if (Array.isArray(l?.rules)) return l.rules.filter((/** @type {unknown} */ r) => typeof r === 'string' && r);
|
|
40
|
+
return typeof l?.rule === 'string' && l.rule ? l.rule.split('; ').filter(Boolean) : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
/**
|
|
33
44
|
* 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
|
|
45
|
+
* a mandatory guard: "Always checked"), `closeDecl.refused` reasons land under their line, and `closeDecl.notes` are the
|
|
35
46
|
* drafter's own "not checked" words. Without `jobLines` (a spec that predates the job block) the signed goal sentence is
|
|
36
47
|
* line 1 and every stage name is a check under it — `older: true`.
|
|
37
48
|
* @param {any} spec
|
|
@@ -42,24 +53,34 @@ export function planFromSpec(spec) {
|
|
|
42
53
|
let stages = [];
|
|
43
54
|
if (spec && Array.isArray(spec.closeDecl?.stages)) {
|
|
44
55
|
stages = spec.closeDecl.stages.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
|
|
45
|
-
.
|
|
56
|
+
.flatMap((/** @type {any} */ s) => {
|
|
57
|
+
const fromLine = Array.isArray(s.fromLine) && s.fromLine.length ? s.fromLine.filter(Number.isInteger) : null;
|
|
58
|
+
// PART 1b: a rubric-step stage is shown as the checks it RUNS — one code-worded row per fact, per count, per limit — read by
|
|
59
|
+
// the same reader (`checksOfClose`) the confirm card and the sign-time invariant use, so the Job tab cannot differ from them
|
|
60
|
+
const rows = s.kind === 'rubric-step' ? checksOfClose({ stages: [s] }) : [];
|
|
61
|
+
return rows.length > 0 ? rows.map((r) => ({ name: r.text, cls: /** @type {'machine check'} */ ('machine check'), fromLine })) : [{ name: s.name, cls: checkClassOfKind(s.kind), fromLine }];
|
|
62
|
+
});
|
|
46
63
|
} else if (spec && Array.isArray(spec.close)) {
|
|
47
64
|
stages = spec.close.filter((/** @type {any} */ s) => s && typeof s.name === 'string' && s.name)
|
|
48
65
|
.map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
|
|
49
66
|
}
|
|
50
|
-
|
|
67
|
+
// a prose step says what it does not check (PART 1b, RF5): the person judges the text at Accept
|
|
68
|
+
const notes = [
|
|
69
|
+
...(Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : []),
|
|
70
|
+
...proseNotChecked(spec?.closeDecl),
|
|
71
|
+
];
|
|
51
72
|
const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
|
|
52
73
|
if (jl.length === 0) {
|
|
53
74
|
return {
|
|
54
75
|
lines: [{
|
|
55
|
-
n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '',
|
|
76
|
+
n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '', rules: [],
|
|
56
77
|
checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
|
|
57
78
|
}],
|
|
58
79
|
loose: [], notChecked: notes, alwaysOn: [], older: true,
|
|
59
80
|
};
|
|
60
81
|
}
|
|
61
82
|
/** @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: [] }));
|
|
83
|
+
const lines = jl.map((/** @type {any} */ l) => ({ n: l.n, text: l.text, rule: typeof l.rule === 'string' ? l.rule : '', rules: rulesOfLine(l), checks: [], refused: [] }));
|
|
63
84
|
/** @type {PlanCheck[]} */
|
|
64
85
|
const loose = [];
|
|
65
86
|
/** @type {string[]} */
|
|
@@ -74,7 +95,9 @@ export function planFromSpec(spec) {
|
|
|
74
95
|
const refused = Array.isArray(spec?.closeDecl?.refused) ? spec.closeDecl.refused : [];
|
|
75
96
|
for (const r of refused) {
|
|
76
97
|
const line = lines.find((l) => l.n === r?.line);
|
|
77
|
-
|
|
98
|
+
// a refusal of ONE ~ rule (PART 1b, RF16) names the rule in the person's words, so the row reads as that rule's Not checked
|
|
99
|
+
const ruleText = line && Number.isInteger(r.rule) ? lineRules(spec.jobLines.find((/** @type {any} */ l) => l?.n === r.line))[r.rule - 1] : null;
|
|
100
|
+
if (line && typeof r.reason === 'string') line.refused.push(ruleText ? `"${ruleText}" — ${r.reason}` : r.reason);
|
|
78
101
|
}
|
|
79
102
|
// 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
103
|
return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(typeof spec?.goal === 'string' && spec.goal ? { goal: spec.goal } : {}) };
|
|
@@ -82,8 +105,8 @@ export function planFromSpec(spec) {
|
|
|
82
105
|
|
|
83
106
|
/**
|
|
84
107
|
* 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
|
|
86
|
-
* @param {{n: number, text: string, rule: string}[]|null|undefined} jobLines
|
|
108
|
+
* line each names, its own "not checked" words, and the real protections (code-owned) as "Always checked".
|
|
109
|
+
* @param {{n: number, text: string, rule: string, rules?: string[]}[]|null|undefined} jobLines
|
|
87
110
|
* @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
|
|
88
111
|
* @returns {JobPlan}
|
|
89
112
|
*/
|
|
@@ -93,7 +116,7 @@ export function planFromConfirm(jobLines, plan) {
|
|
|
93
116
|
: (Array.isArray(plan?.checks) ? plan.checks.map((c) => ({ text: String(c), fromLine: null })) : []);
|
|
94
117
|
const jl = Array.isArray(jobLines) ? jobLines : [];
|
|
95
118
|
/** @type {PlanLine[]} */
|
|
96
|
-
const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, checks: [], refused: [] }));
|
|
119
|
+
const lines = jl.map((l) => ({ n: l.n, text: l.text, rule: l.rule, rules: rulesOfLine(l), checks: [], refused: [] }));
|
|
97
120
|
/** @type {PlanCheck[]} */
|
|
98
121
|
const loose = [];
|
|
99
122
|
for (const c of items) {
|
package/src/panel/server.js
CHANGED
|
@@ -60,6 +60,16 @@ import { planFromSpec } from './jobplan.js';
|
|
|
60
60
|
|
|
61
61
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
62
62
|
|
|
63
|
+
/** bareloop's own version, from its package.json (read each time the page is served); 'unknown' if it cannot be read, never a guess
|
|
64
|
+
* @param {string} [file] test seam: another package.json
|
|
65
|
+
* @returns {string} */
|
|
66
|
+
function packageVersion(file = join(HERE, '..', '..', 'package.json')) {
|
|
67
|
+
try {
|
|
68
|
+
const v = JSON.parse(readFileSync(file, 'utf8')).version;
|
|
69
|
+
return typeof v === 'string' && v.length > 0 ? v : 'unknown';
|
|
70
|
+
} catch { return 'unknown'; }
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
/** Largest POST body the panel buffers (job cards are short prose); a bigger one is refused with 413. */
|
|
64
74
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
65
75
|
|
|
@@ -1810,6 +1820,7 @@ function resolveSpecForRow(row) {
|
|
|
1810
1820
|
* change any file?"
|
|
1811
1821
|
* - `pattern-absent-in-diff` is always the `no-suppressions` guard: "no
|
|
1812
1822
|
* casts or silencers added?"
|
|
1823
|
+
* - `rubric-step` (Part 1a) is one job line's output file: "line N — does its output pass the line's checks?"
|
|
1813
1824
|
* - `command-exit` is the genre's `suite-green` stage (the only shipped use
|
|
1814
1825
|
* of this kind): "does the test suite pass?"
|
|
1815
1826
|
* - `count-not-worse` splits three ways on its own declared
|
|
@@ -1877,6 +1888,8 @@ function stageQuestionText({
|
|
|
1877
1888
|
if (kind === 'files-changed') return 'did it change any file?';
|
|
1878
1889
|
if (kind === 'pattern-absent-in-diff') return 'no casts or silencers added?';
|
|
1879
1890
|
if (kind === 'command-exit') return 'does the test suite pass?';
|
|
1891
|
+
// Part 1a: one rubric-step stage = one job line's output file
|
|
1892
|
+
if (kind === 'rubric-step') return params && Number.isInteger(params.step) ? `line ${params.step} — does its output pass the line's checks?` : null;
|
|
1880
1893
|
if (kind !== 'count-not-worse' || !params || typeof params !== 'object') return null;
|
|
1881
1894
|
const what = Object.hasOwn(GENRE_WHAT, String(genre)) ? GENRE_WHAT[String(genre)] : null;
|
|
1882
1895
|
if (params.direction === 'higher-is-better' && params.baseline === 'seed') {
|
|
@@ -2710,7 +2723,7 @@ function sendText(res, code, text) {
|
|
|
2710
2723
|
* only `createPanelServer` below always supplies one.
|
|
2711
2724
|
* @param {import('node:http').IncomingMessage} req
|
|
2712
2725
|
* @param {import('node:http').ServerResponse} res
|
|
2713
|
-
* @param {{ home?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
|
|
2726
|
+
* @param {{ home?: string, packageJsonPath?: string, port: number, token?: string, authorRoutes?: ReturnType<typeof createAuthorRoutes>, settingsRoutes?: ReturnType<typeof createSettingsRoutes>, runRoutes?: ReturnType<typeof createRunRoutes>, importRoutes?: ReturnType<typeof createImportRoutes> }} opts
|
|
2714
2727
|
*/
|
|
2715
2728
|
export function handleRequest(req, res, opts) {
|
|
2716
2729
|
/** the ONE writable route family for a path, or null — `/api/author/*` (authoring) or
|
|
@@ -2806,6 +2819,9 @@ export function handleRequest(req, res, opts) {
|
|
|
2806
2819
|
return;
|
|
2807
2820
|
}
|
|
2808
2821
|
html = html.replace(/__BARELOOP_PANEL_PORT__/g, String(opts.port));
|
|
2822
|
+
// the topbar reads `bareloop · v<version> · ...`: the version is bareloop's OWN package.json, read when the page is served
|
|
2823
|
+
// and templated in exactly like the port — never written into the HTML
|
|
2824
|
+
html = html.replace(/__BARELOOP_PANEL_VERSION__/g, packageVersion(opts.packageJsonPath));
|
|
2809
2825
|
// PANEL-BUILD.md P3's human-click guard: a per-server-start token,
|
|
2810
2826
|
// templated into the page exactly like the port — absent on every P1/P2
|
|
2811
2827
|
// caller (no `opts.token`), which is fine: the page's own JS only reads
|
|
@@ -2887,7 +2903,7 @@ export function handleRequest(req, res, opts) {
|
|
|
2887
2903
|
* @param {{ port?: number, home?: string, env?: Record<string,string|undefined>,
|
|
2888
2904
|
* sessionsRoot?: string, spawnFn?: (...a: any[]) => any, bareloopBin?: string,
|
|
2889
2905
|
* fetchImpl?: typeof fetch, settleMs?: number, userHome?: string,
|
|
2890
|
-
* openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string }} [opts]
|
|
2906
|
+
* openFolderSpawn?: (cmd: string, args: string[], o: object) => any, platform?: string, packageJsonPath?: string }} [opts]
|
|
2891
2907
|
* @returns {Promise<{ server: import('node:http').Server, port: number, token: string, close: () => Promise<void> }>}
|
|
2892
2908
|
*/
|
|
2893
2909
|
export function createPanelServer(opts = {}) {
|
|
@@ -2912,7 +2928,7 @@ export function createPanelServer(opts = {}) {
|
|
|
2912
2928
|
const server = createServer((req, res) => {
|
|
2913
2929
|
try {
|
|
2914
2930
|
handleRequest(req, res, {
|
|
2915
|
-
home, port: boundPort, token, authorRoutes, settingsRoutes, runRoutes, importRoutes,
|
|
2931
|
+
home, port: boundPort, token, authorRoutes, settingsRoutes, runRoutes, importRoutes, packageJsonPath: opts.packageJsonPath,
|
|
2916
2932
|
});
|
|
2917
2933
|
} catch (e) {
|
|
2918
2934
|
sendText(res, 500, `internal error: ${/** @type {Error} */ (e).message}`);
|
package/src/planrun.js
CHANGED
|
@@ -45,6 +45,7 @@ import { defaultJudgeLoop } from './judged.js';
|
|
|
45
45
|
export { rateSourceFields };
|
|
46
46
|
import { createClock, isWallTimeout } from './clock.js';
|
|
47
47
|
import { isDeclaredClose, runDeclaredStages, validateCloseDecl, closeGrade, HUMAN_PAUSE, HITL_PAUSE, HITL_DECISION_RED } from './declaredclose.js';
|
|
48
|
+
import { inputLabelsOf } from './rubricstep.js';
|
|
48
49
|
import {
|
|
49
50
|
seedAtHead, seedListing, changedSet, GAP_TRIM_MARKER, GATE_AUDIT_FILE, ARBITER_BOOK_STORES,
|
|
50
51
|
HUMAN_KIND, HUMAN_DECISIONS, normalizeHumanRuling, resolveHumanRuling,
|
|
@@ -1886,7 +1887,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
1886
1887
|
},
|
|
1887
1888
|
decisionReady: true,
|
|
1888
1889
|
decision: `The close minted ${outcome} and it STANDS. What happens to the work is yours: accept it, rerun it with your own words as the gap, or pause and come back.`
|
|
1889
|
-
+ (held ? ' This
|
|
1890
|
+
+ (held ? ' This is a rubric (soft-green) job, so its green earns no reuse until you accept it, whether or not a judge ran.' : ''),
|
|
1890
1891
|
options: [...HUMAN_DECISIONS],
|
|
1891
1892
|
meaning: 'not a verdict — the verdict is already minted and nothing an answer does can change it; this records a disposition',
|
|
1892
1893
|
});
|
|
@@ -2036,7 +2037,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
2036
2037
|
}
|
|
2037
2038
|
const listed = await seedListing(workdir, s.seedRef);
|
|
2038
2039
|
const v = listed.stop === null
|
|
2039
|
-
? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType, writeScope: job.writeScope, jobLines: Array.isArray(job.jobLines) ? job.jobLines : null })
|
|
2040
|
+
? validateCloseDecl(job.closeDecl, { at: 'closeDecl', listing: listed.files, verdictType: job.verdictType, writeScope: job.writeScope, jobLines: Array.isArray(job.jobLines) ? job.jobLines : null , inputLabels: inputLabelsOf(job.inputs) })
|
|
2040
2041
|
: { ok: false, grounded: false, reds: [{ code: 'listing-unreadable', path: 'closeDecl', detail: listed.stop }] };
|
|
2041
2042
|
emit('close-decl', {
|
|
2042
2043
|
genre: job.closeDecl.genre,
|