bareloop 0.44.0 → 0.46.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 +40 -0
- package/bareloop.context.md +31 -5
- package/package.json +1 -1
- package/src/attemptdiff.js +116 -0
- package/src/authorflow.js +1 -1
- package/src/authorjob.js +25 -1
- package/src/authorreadout.js +13 -2
- package/src/authorrun.js +8 -2
- package/src/authorscout.js +1 -1
- package/src/bundle.js +90 -6
- package/src/close-integrity.js +39 -5
- package/src/config.js +5 -2
- package/src/fitcheck.js +166 -0
- package/src/inputlook.js +1 -47
- package/src/judged.js +3 -3
- package/src/labelscan.js +13 -4
- package/src/ladder.js +2 -2
- package/src/learn.js +943 -0
- package/src/learnstore.js +154 -0
- package/src/modellist.js +1 -1
- package/src/pagedropwords.js +47 -1
- package/src/pagedry.js +124 -0
- package/src/pagefetch.js +2 -1
- package/src/pagemap.js +7 -2
- package/src/pagerules.js +35 -13
- package/src/panel/authorroutes.js +1 -1
- package/src/panel/authorsession.js +37 -6
- package/src/panel/index.html +100 -0
- package/src/panel/jobplan.js +12 -7
- package/src/panel/server.js +8 -125
- package/src/planrun.js +40 -6
- package/src/promptregisters.js +4 -0
- package/src/providerrows.js +3 -3
- package/src/rubricstep.js +36 -6
- package/src/run.js +4 -2
- package/src/schedulewords.js +1 -1
- package/src/source.js +1 -1
- package/src/stagewords.js +144 -0
- package/src/text.js +4 -2
- package/src/userrun.js +28 -0
- package/src/validate.js +10 -5
- package/src/worktree.js +8 -2
- package/types/attemptdiff.d.ts +38 -0
- package/types/authorjob.d.ts +10 -2
- package/types/config.d.ts +1 -1
- package/types/fitcheck.d.ts +113 -0
- package/types/inputlook.d.ts +0 -23
- package/types/labelscan.d.ts +6 -0
- package/types/learn.d.ts +575 -0
- package/types/learnstore.d.ts +107 -0
- package/types/pagedropwords.d.ts +13 -0
- package/types/pagedry.d.ts +37 -0
- package/types/pagerules.d.ts +7 -1
- package/types/panel/authorsession.d.ts +4 -2
- package/types/panel/jobplan.d.ts +5 -2
- package/types/panel/server.d.ts +1 -0
- package/types/planrun.d.ts +13 -3
- package/types/reuse.d.ts +1 -1
- package/types/rubricstep.d.ts +5 -0
- package/types/run.d.ts +8 -2
- package/types/stagewords.d.ts +90 -0
package/src/panel/index.html
CHANGED
|
@@ -926,6 +926,10 @@
|
|
|
926
926
|
<button class="chip" aria-pressed="false" data-view="flat" data-testid="audit-view-flat">Flat</button>
|
|
927
927
|
</div>
|
|
928
928
|
</div>
|
|
929
|
+
<div id="audit-learnings" class="audit-part" data-testid="audit-learnings" hidden>
|
|
930
|
+
<button type="button" id="audit-learnings-summary" class="btn audit-part-toggle" aria-expanded="false" data-testid="audit-learnings-summary">What the planner was told from earlier runs</button>
|
|
931
|
+
<div id="audit-learnings-list" class="audit-part-body" data-testid="audit-learnings-list" hidden></div>
|
|
932
|
+
</div>
|
|
929
933
|
<div id="audit-groups" data-testid="audit-groups"></div>
|
|
930
934
|
<div id="audit-flat-wrap" hidden>
|
|
931
935
|
<div id="audit-empty" class="hint" data-testid="audit-empty" style="font-size:13px;padding:24px 0;">select a run first</div>
|
|
@@ -2828,6 +2832,98 @@
|
|
|
2828
2832
|
// fetch, wired separately in selectRun below).
|
|
2829
2833
|
lastLegDividers = Array.isArray(detail.legDividers) ? detail.legDividers : [];
|
|
2830
2834
|
renderAuditGroups(detail);
|
|
2835
|
+
renderAuditLearnings(detail);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// Cross-run learnings, READ-ONLY: what the planner was handed, one group per injection (the draft, the first replan). ONE fold for the
|
|
2839
|
+
// whole section (the <details> in the Audit tab); opened, every hint shows at once as a title line + labelled rows, no inner toggles.
|
|
2840
|
+
// The fields come from the server (learn.js parseHintBlock, the one reader of the block) - this page never splits the prose.
|
|
2841
|
+
// No edit, no Reject, no button. Absent (the setting was off, or nothing was readable) = the section stays hidden.
|
|
2842
|
+
// one row of a hint's table: the same label cell + value cell the schedule section body uses
|
|
2843
|
+
function learnRow(tbody, testid, label, lines){
|
|
2844
|
+
var tr = document.createElement("tr");
|
|
2845
|
+
tr.setAttribute("data-testid", testid);
|
|
2846
|
+
var lab = document.createElement("td");
|
|
2847
|
+
lab.className = "hint";
|
|
2848
|
+
lab.style.cssText = "white-space:nowrap";
|
|
2849
|
+
lab.textContent = label;
|
|
2850
|
+
var val = document.createElement("td");
|
|
2851
|
+
val.style.cssText = "overflow-wrap:anywhere;white-space:pre-wrap";
|
|
2852
|
+
val.textContent = lines.join("\n");
|
|
2853
|
+
tr.appendChild(lab);
|
|
2854
|
+
tr.appendChild(val);
|
|
2855
|
+
tbody.appendChild(tr);
|
|
2856
|
+
}
|
|
2857
|
+
function learnHintLabel(h){
|
|
2858
|
+
var bits = [h.n, h.level, "seen " + h.count + (h.count === 1 ? " time" : " times")];
|
|
2859
|
+
if(h.check) bits.push(h.check);
|
|
2860
|
+
if(h.kinds && h.kinds.length) bits.push(h.kinds.join(", "));
|
|
2861
|
+
bits.push(h.fixed ? "fixed" : "never fixed");
|
|
2862
|
+
return bits.join(" \u00B7 ");
|
|
2863
|
+
}
|
|
2864
|
+
function renderAuditLearnings(detail){
|
|
2865
|
+
var box = document.getElementById("audit-learnings");
|
|
2866
|
+
var list = document.getElementById("audit-learnings-list");
|
|
2867
|
+
var sum = document.getElementById("audit-learnings-summary");
|
|
2868
|
+
if(!box || !list) return;
|
|
2869
|
+
var items = detail && Array.isArray(detail.learnings) ? detail.learnings : [];
|
|
2870
|
+
list.innerHTML = "";
|
|
2871
|
+
var head = "What the planner was told from earlier runs";
|
|
2872
|
+
items.forEach(function(it, gi){
|
|
2873
|
+
var hints = Array.isArray(it.hints) ? it.hints : [];
|
|
2874
|
+
var when = it.phase === "replan" ? "first replan" : "draft";
|
|
2875
|
+
if(items.length > 1){
|
|
2876
|
+
var g = document.createElement("div");
|
|
2877
|
+
g.className = "hint";
|
|
2878
|
+
g.style.cssText = "margin-top:10px;font-weight:600;";
|
|
2879
|
+
g.setAttribute("data-testid", "audit-learnings-group-" + it.phase);
|
|
2880
|
+
g.textContent = it.phase === "replan" ? "At the first replan" : "At the draft";
|
|
2881
|
+
list.appendChild(g);
|
|
2882
|
+
}
|
|
2883
|
+
var note = document.createElement("div");
|
|
2884
|
+
note.className = "hint";
|
|
2885
|
+
note.style.cssText = "margin:6px 0;overflow-wrap:anywhere;";
|
|
2886
|
+
note.textContent = (gi === 0 ? "Read-only. " : "") + "Given to the planner at the " + when + " (" + Number(it.chars).toLocaleString("en-US") + " characters)." + (gi === 0 ? " No worker sees it." : "");
|
|
2887
|
+
list.appendChild(note);
|
|
2888
|
+
hints.forEach(function(h){
|
|
2889
|
+
var hint = document.createElement("div");
|
|
2890
|
+
hint.className = "audit-table-scroll";
|
|
2891
|
+
hint.setAttribute("data-testid", "audit-learnings-hint-" + it.phase + "-" + h.n);
|
|
2892
|
+
var table = document.createElement("table");
|
|
2893
|
+
table.className = "rounds-table";
|
|
2894
|
+
var tbody = document.createElement("tbody");
|
|
2895
|
+
var trT = document.createElement("tr");
|
|
2896
|
+
var title = document.createElement("td");
|
|
2897
|
+
title.setAttribute("colspan", "2");
|
|
2898
|
+
title.style.cssText = "font-weight:600;overflow-wrap:anywhere";
|
|
2899
|
+
title.setAttribute("data-testid", "audit-learnings-hint-" + it.phase + "-" + h.n + "-title");
|
|
2900
|
+
title.textContent = learnHintLabel(h);
|
|
2901
|
+
trT.appendChild(title);
|
|
2902
|
+
tbody.appendChild(trT);
|
|
2903
|
+
var t = "audit-learnings-hint-" + it.phase + "-" + h.n + "-";
|
|
2904
|
+
if(h.said) learnRow(tbody, t + "said", "It said:", [h.said]);
|
|
2905
|
+
if(h.touched) learnRow(tbody, t + "touched", "Fix touched:", [h.touched]);
|
|
2906
|
+
if(h.changed && h.changed.length) learnRow(tbody, t + "changed", "What changed:", h.changed.concat(h.moreChanged ? ["(+" + h.moreChanged + " more)"] : []));
|
|
2907
|
+
table.appendChild(tbody);
|
|
2908
|
+
hint.appendChild(table);
|
|
2909
|
+
list.appendChild(hint);
|
|
2910
|
+
});
|
|
2911
|
+
});
|
|
2912
|
+
if(sum){
|
|
2913
|
+
var first = items.length ? (Array.isArray(items[0].hints) ? items[0].hints.length : 0) : 0;
|
|
2914
|
+
var label = items.length ? head + " \u00B7 " + first + (first === 1 ? " hint" : " hints") : head;
|
|
2915
|
+
// no arrow (hamr 2026-10-23): the state chip leads; aria-expanded carries open/closed. The same `[✓] word` state markup the schedule/step headers wear (stateDotHTML), then the plain-words title
|
|
2916
|
+
var paint = function(){ sum.innerHTML = stateDotHTML(glyphClass("\u2713"), "learnings", "audit-learnings-state") + " " + escapeXml(label); };
|
|
2917
|
+
paint();
|
|
2918
|
+
// ONE fold, same as the other Audit sections: the header button toggles the body (wired once)
|
|
2919
|
+
sum.onclick = function(){
|
|
2920
|
+
var open = sum.getAttribute("aria-expanded") === "true";
|
|
2921
|
+
sum.setAttribute("aria-expanded", open ? "false" : "true");
|
|
2922
|
+
list.hidden = open;
|
|
2923
|
+
paint();
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
box.hidden = items.length === 0;
|
|
2831
2927
|
}
|
|
2832
2928
|
|
|
2833
2929
|
// 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
|
|
@@ -3599,6 +3695,7 @@
|
|
|
3599
3695
|
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3600
3696
|
};
|
|
3601
3697
|
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3698
|
+
h += fold("How the page was read", plan.readNotes, "plan-readnotes");
|
|
3602
3699
|
h += fold("Always checked", plan.alwaysOn, "plan-alwayson");
|
|
3603
3700
|
return h + "</div>";
|
|
3604
3701
|
}
|
|
@@ -3831,6 +3928,9 @@
|
|
|
3831
3928
|
if((data.notChecked || []).length){
|
|
3832
3929
|
h += '<div class="job-card"><h4>Not checked</h4>' + resultNotChecked("Not checked (the AI's own reading)", data.notChecked, "result-notchecked") + '</div>';
|
|
3833
3930
|
}
|
|
3931
|
+
if((data.readNotes || []).length){
|
|
3932
|
+
h += '<div class="job-card"><h4>How the page was read</h4>' + resultNotChecked("How the page was read \u00b7 " + data.readNotes.length, data.readNotes, "result-readnotes") + '</div>';
|
|
3933
|
+
}
|
|
3834
3934
|
if(detail.accept === true && !detail.live) h += resultDoorHtml(data.door);
|
|
3835
3935
|
if((data.delivered || []).length){
|
|
3836
3936
|
h += '<div class="job-card" data-testid="result-saved"><h4>Saved</h4>' + data.delivered.map(function(d){
|
package/src/panel/jobplan.js
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
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 {
|
|
14
|
+
import { splitReadNotes } from '../pagedropwords.js';
|
|
15
|
+
import { checksOfClose, signedFactNames, proseNotChecked, lineRules, confirmedGoalOf } from '../rubricstep.js';
|
|
15
16
|
|
|
16
17
|
/** @typedef {{name: string, cls: 'machine check'|'judge'|null}} PlanCheck */
|
|
17
18
|
/** @typedef {{n: number, text: string, rule: string, rules: string[], checks: PlanCheck[], refused: string[]}} PlanLine */
|
|
18
|
-
/** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
|
|
19
|
+
/** @typedef {{lines: PlanLine[], loose: PlanCheck[], notChecked: string[], readNotes: string[], alwaysOn: string[], older: boolean, goal?: string}} JobPlan */
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* `machine check` for a kind a command decides, `judge` for one that needs judgement; `null` for a stage whose kind is
|
|
@@ -65,10 +66,12 @@ export function planFromSpec(spec) {
|
|
|
65
66
|
.map((/** @type {any} */ s) => ({ name: s.name, cls: /** @type {'machine check'} */ ('machine check'), fromLine: null }));
|
|
66
67
|
}
|
|
67
68
|
// a prose step says what it does not check (PART 1b, RF5): the person judges the text at Accept
|
|
68
|
-
const
|
|
69
|
+
const allNotes = [
|
|
69
70
|
...(Array.isArray(spec?.closeDecl?.notes) ? spec.closeDecl.notes.filter((/** @type {any} */ n) => typeof n === 'string' && n) : []),
|
|
70
71
|
...proseNotChecked(spec?.closeDecl),
|
|
71
72
|
];
|
|
73
|
+
// code's notes on which written forms of a still-checked fact were not used are "How the page was read", not Not checked
|
|
74
|
+
const { notChecked: notes, readNotes } = splitReadNotes(allNotes, signedFactNames(spec?.closeDecl));
|
|
72
75
|
const jl = Array.isArray(spec?.jobLines) ? spec.jobLines.filter((/** @type {any} */ l) => l && Number.isInteger(l.n) && typeof l.text === 'string') : [];
|
|
73
76
|
if (jl.length === 0) {
|
|
74
77
|
return {
|
|
@@ -76,7 +79,7 @@ export function planFromSpec(spec) {
|
|
|
76
79
|
n: 1, text: typeof spec?.goal === 'string' && spec.goal ? spec.goal : '(no goal recorded)', rule: '', rules: [],
|
|
77
80
|
checks: stages.map((s) => ({ name: s.name, cls: s.cls })), refused: [],
|
|
78
81
|
}],
|
|
79
|
-
loose: [], notChecked: notes, alwaysOn: [], older: true,
|
|
82
|
+
loose: [], notChecked: notes, readNotes, alwaysOn: [], older: true,
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
/** @type {PlanLine[]} */
|
|
@@ -102,14 +105,15 @@ export function planFromSpec(spec) {
|
|
|
102
105
|
// 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)
|
|
103
106
|
// (a rubric job's signed goal is the whole worker brief: only the sentence the person confirmed is the summary)
|
|
104
107
|
const summary = typeof spec?.goal === 'string' && spec.goal ? confirmedGoalOf(spec.goal) : null;
|
|
105
|
-
return { lines, loose, notChecked: notes, alwaysOn, older: false, ...(summary ? { goal: summary } : {}) };
|
|
108
|
+
return { lines, loose, notChecked: notes, readNotes, alwaysOn, older: false, ...(summary ? { goal: summary } : {}) };
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
/**
|
|
109
112
|
* The plan of the confirm turn, before anything is drafted: the person's own job lines, the model's checks under the
|
|
110
113
|
* line each names, its own "not checked" words, and the real protections (code-owned) as "Always checked".
|
|
111
114
|
* @param {{n: number, text: string, rule: string, rules?: string[]}[]|null|undefined} jobLines
|
|
112
|
-
* @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[]}} plan
|
|
115
|
+
* @param {{checkItems?: {text: string, fromLine: number[]|null}[], checks?: string[], notChecked?: string[], protections?: string[], signedFacts?: string[]}} plan
|
|
116
|
+
* `signedFacts` = the names of the facts the look signs (so an S1 copy of a signed fact reads as a note, not Not checked)
|
|
113
117
|
* @returns {JobPlan}
|
|
114
118
|
*/
|
|
115
119
|
export function planFromConfirm(jobLines, plan) {
|
|
@@ -126,7 +130,8 @@ export function planFromConfirm(jobLines, plan) {
|
|
|
126
130
|
for (const line of served) line.checks.push({ name: c.text, cls: null });
|
|
127
131
|
if (served.length === 0) loose.push({ name: c.text, cls: null });
|
|
128
132
|
}
|
|
133
|
+
const { notChecked, readNotes } = splitReadNotes(plan?.notChecked, plan?.signedFacts);
|
|
129
134
|
return {
|
|
130
|
-
lines, loose, notChecked
|
|
135
|
+
lines, loose, notChecked, readNotes, alwaysOn: [...(plan?.protections ?? [])], older: false,
|
|
131
136
|
};
|
|
132
137
|
}
|
package/src/panel/server.js
CHANGED
|
@@ -47,6 +47,7 @@ import { chainSpend } from '../ledger.js';
|
|
|
47
47
|
import { runBehaviour } from '../behaviour.js';
|
|
48
48
|
import { SPEND_RECORD_TYPES, floorsFromRecords } from '../ledger.js';
|
|
49
49
|
import { jobSpecHash, workflowKey } from '../job.js';
|
|
50
|
+
import { parseHintBlock } from '../learn.js';
|
|
50
51
|
import { confirmProtections } from '../authorflow.js';
|
|
51
52
|
import { createScheduleDoor } from './schedules.js';
|
|
52
53
|
import { createStarter } from './schedulestart.js';
|
|
@@ -74,6 +75,7 @@ import { judgedFilesOf, lineRules, stepFileRel, proseNotCheckedItems } from '../
|
|
|
74
75
|
import { factPartOf, factSentence, ruleWords, moneyAs } from '../facts.js';
|
|
75
76
|
import { createSettingsRoutes } from './settingsroutes.js';
|
|
76
77
|
import { planFromSpec } from './jobplan.js';
|
|
78
|
+
import { stageQuestionsByName } from '../stagewords.js';
|
|
77
79
|
|
|
78
80
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
79
81
|
|
|
@@ -1445,6 +1447,7 @@ export function getRunResult(runid, /** @type {{ home?: string, sessionsRoot?: s
|
|
|
1445
1447
|
pages,
|
|
1446
1448
|
pageNote: pages.length ? RESULT_PAGE_MAY_CHANGE : null,
|
|
1447
1449
|
notChecked: bottom,
|
|
1450
|
+
readNotes: plan.readNotes,
|
|
1448
1451
|
// the door's words, from the one table (src/panel/status.js DOOR_WORDS); the page types none of them
|
|
1449
1452
|
door: { head: DOOR_WORDS.waiting.head, ask: RESULT_DOOR_ASK },
|
|
1450
1453
|
delivered: delivered.map((d, i) => ({
|
|
@@ -1848,6 +1851,9 @@ function getRunDetailBody(runid, opts) {
|
|
|
1848
1851
|
// carries no contamination risk); `null` when the spine carries no
|
|
1849
1852
|
// `memory-cache` record at all (never armed on this run).
|
|
1850
1853
|
behaviour: scopedBehaviour(row, rawSpineRecords),
|
|
1854
|
+
// Cross-run learnings, READ-ONLY (ITEM33 rev 2, RULING 8): the exact block the planner was handed, as the spine recorded it at
|
|
1855
|
+
// injection (the draft, and the first replan); `hints` = the same text split into fields by learn.js's parseHintBlock (the one reader). Empty when the setting was off or nothing was readable.
|
|
1856
|
+
learnings: rawSpineRecords.filter((e) => e?.type === 'learnings' && typeof e.text === 'string').map((e) => ({ phase: String(e.phase), text: e.text, chars: e.text.length, ids: Array.isArray(e.ids) ? e.ids : [], hints: parseHintBlock(e.text) })),
|
|
1851
1857
|
// PRD v1.106 Part 5 F: the two Run summary rows, worded here (the page prints them)
|
|
1852
1858
|
callsLine: callsLineOf(rawSpineRecords, scopedBehaviour(row, rawSpineRecords)),
|
|
1853
1859
|
checksLine: checksLineOf(rawSpineRecords, enrichedParts),
|
|
@@ -2592,116 +2598,6 @@ function resolveSpecForRow(row) {
|
|
|
2592
2598
|
return null;
|
|
2593
2599
|
}
|
|
2594
2600
|
|
|
2595
|
-
/**
|
|
2596
|
-
* THE PLAIN-QUESTION OWNER (plain-checks build, 2026-09-28, hamr's ruling:
|
|
2597
|
-
* the "checks N/M" headline is WITHDRAWN — "confusing, reads like 6 failed
|
|
2598
|
-
* when 5 never ran"). ONE pure function mapping a declared stage's own
|
|
2599
|
-
* signed `kind`+`params` (plus the close's own `genre`, also a signed field)
|
|
2600
|
-
* to a short plain-English question a person can read without knowing the
|
|
2601
|
-
* catalogue — never a second spelling on the client, and never anything the
|
|
2602
|
-
* model authored (the catalogue's kinds/params are the whole vocabulary this
|
|
2603
|
-
* reads).
|
|
2604
|
-
*
|
|
2605
|
-
* Every branch is traced straight off `src/kinds.js`'s runner semantics and
|
|
2606
|
-
* the TYPES genre template (`src/authoring.js` `TYPES_GENRE_TEMPLATE`,
|
|
2607
|
-
* `classGuards`'s `MECHANICAL_GUARDS`) — never guessed:
|
|
2608
|
-
* - `files-changed` is always the `changed-from-seed` guard: "did it
|
|
2609
|
-
* change any file?"
|
|
2610
|
-
* - `pattern-absent-in-diff` is always the `no-suppressions` guard: "no
|
|
2611
|
-
* casts or silencers added?"
|
|
2612
|
-
* - `rubric-step` (Part 1a) is one job line's output file: "line N — does its output pass the line's checks?"
|
|
2613
|
-
* - `command-exit` is the genre's `suite-green` stage (the only shipped use
|
|
2614
|
-
* of this kind): "does the test suite pass?"
|
|
2615
|
-
* - `count-not-worse` splits three ways on its own declared
|
|
2616
|
-
* `direction`/`baseline` (never the runtime number — see
|
|
2617
|
-
* {@link stageKindMetaFromSpec}'s own doc for why the baseline KIND must
|
|
2618
|
-
* travel separately from the measured figure):
|
|
2619
|
-
* - `higher-is-better` + `baseline: 'seed'` is the genre's `tests-kept`
|
|
2620
|
-
* floor: "did all the old tests still exist?"
|
|
2621
|
-
* - `lower-is-better` + `baseline: 0` is the genre's in-scope
|
|
2622
|
-
* `typecheck` stage — UNLESS its own `cmd`+`args` is the SAME
|
|
2623
|
-
* command a `command-exit` stage in the same close already runs
|
|
2624
|
-
* (`suiteCmdKeys`, below): the TYPES template's `suite-green` is "the
|
|
2625
|
-
* suite exits clean AND reports zero failing tests — TWO assertions"
|
|
2626
|
-
* over ONE population (the same `npm test`/`pytest` invocation), so a
|
|
2627
|
-
* `count-not-worse` stage sharing that exact command is the failing-
|
|
2628
|
-
* test-count half of that pair, never a typecheck stage: "no failing
|
|
2629
|
-
* tests?" — derived from the same cmd+args correlation (the stage
|
|
2630
|
-
* counts the suite's own failing tests), never from the stage's own
|
|
2631
|
-
* name string. The live check that found this (run mul5fofw's own
|
|
2632
|
-
* `suite-zero-failing-tests`, cmd `npm test`, same as its sibling
|
|
2633
|
-
* `suite-green` command-exit stage) would otherwise have read "test/
|
|
2634
|
-
* has 0 type errors?", which is false: its parser counts FAILING
|
|
2635
|
-
* TESTS (`^# fail (\d+)$`), not type-checker output. hamr's ruling:
|
|
2636
|
-
* name this half honestly rather than reusing "type errors" for a
|
|
2637
|
-
* population it was never about, or falling back to the stage's raw
|
|
2638
|
-
* name. When the command differs from every sibling `command-exit`
|
|
2639
|
-
* stage, the declared `scope.includePrefixes` naming exactly ONE path
|
|
2640
|
-
* is quoted verbatim (a signed param, never invented): "<path> has 0
|
|
2641
|
-
* type errors?" — otherwise the generic "type errors is 0?", never a
|
|
2642
|
-
* guessed filename.
|
|
2643
|
-
* - `lower-is-better` + `baseline: 'seed'` with a non-empty
|
|
2644
|
-
* `scope.excludePrefixes` is the genre's `typecheck-outside` ceiling:
|
|
2645
|
-
* "other files didn't get more type errors?"
|
|
2646
|
-
* The "type errors" wording is genre-owned (`GENRE_WHAT`), because TYPES is
|
|
2647
|
-
* the only genre this catalogue admits today (doc-genre kinds are not yet
|
|
2648
|
-
* built) — an unrecognised genre, or a `count-not-worse` shape none of the
|
|
2649
|
-
* three above matches, returns `null` rather than invent a fourth shape.
|
|
2650
|
-
*
|
|
2651
|
-
* `null` means "no honest plain question" — the caller (buildAttemptsList,
|
|
2652
|
-
* partLine1Text's `attemptChecksLine`) falls back to the stage's OWN name,
|
|
2653
|
-
* exactly as it already does for a stage with no resolvable kind at all.
|
|
2654
|
-
* @param {{kind: string|null, params: any, genre: string|null, suiteCmdKeys: Set<string>}} o
|
|
2655
|
-
* @returns {string|null}
|
|
2656
|
-
*/
|
|
2657
|
-
const GENRE_WHAT = Object.freeze({ TYPES: 'type errors' });
|
|
2658
|
-
|
|
2659
|
-
/** `cmd`+`args`, as a comparable key — the one signal (besides `kind`
|
|
2660
|
-
* itself) that two stages measure the SAME command's output, used to tell a
|
|
2661
|
-
* genuine typecheck stage apart from the TYPES genre's other `lower-is-
|
|
2662
|
-
* better`/`baseline: 0` stage (the failing-test-count half of `suite-green`'s
|
|
2663
|
-
* "two assertions"), which shares that stage's own shape but not its
|
|
2664
|
-
* population. `null` for a stage whose `cmd` isn't a non-empty string —
|
|
2665
|
-
* never matches anything, so it never falsely suppresses a real typecheck
|
|
2666
|
-
* question.
|
|
2667
|
-
* @param {any} params @returns {string|null} */
|
|
2668
|
-
function cmdKey(params) {
|
|
2669
|
-
if (!params || typeof params.cmd !== 'string' || params.cmd.length === 0) return null;
|
|
2670
|
-
const args = Array.isArray(params.args) ? params.args : [];
|
|
2671
|
-
return JSON.stringify([params.cmd, args]);
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
function stageQuestionText({
|
|
2675
|
-
kind, params, genre, suiteCmdKeys,
|
|
2676
|
-
}) {
|
|
2677
|
-
if (kind === 'files-changed') return 'did it change any file?';
|
|
2678
|
-
if (kind === 'pattern-absent-in-diff') return 'no casts or silencers added?';
|
|
2679
|
-
if (kind === 'command-exit') return 'does the test suite pass?';
|
|
2680
|
-
// Part 1a: one rubric-step stage = one job line's output file
|
|
2681
|
-
if (kind === 'rubric-step') return params && Number.isInteger(params.step) ? `line ${params.step} — does its output pass the line's checks?` : null;
|
|
2682
|
-
if (kind !== 'count-not-worse' || !params || typeof params !== 'object') return null;
|
|
2683
|
-
const what = Object.hasOwn(GENRE_WHAT, String(genre)) ? GENRE_WHAT[String(genre)] : null;
|
|
2684
|
-
if (params.direction === 'higher-is-better' && params.baseline === 'seed') {
|
|
2685
|
-
return 'did all the old tests still exist?';
|
|
2686
|
-
}
|
|
2687
|
-
if (!what) return null;
|
|
2688
|
-
const scope = params.scope && typeof params.scope === 'object' ? params.scope : null;
|
|
2689
|
-
if (params.direction === 'lower-is-better' && params.baseline === 0) {
|
|
2690
|
-
const key = cmdKey(params);
|
|
2691
|
-
if (key !== null && suiteCmdKeys instanceof Set && suiteCmdKeys.has(key)) return 'no failing tests?';
|
|
2692
|
-
const include = scope && Array.isArray(scope.includePrefixes) ? scope.includePrefixes : null;
|
|
2693
|
-
if (include && include.length === 1 && typeof include[0] === 'string' && include[0].length > 0) {
|
|
2694
|
-
return `${include[0]} has 0 ${what}?`;
|
|
2695
|
-
}
|
|
2696
|
-
return `${what} is 0?`;
|
|
2697
|
-
}
|
|
2698
|
-
if (params.direction === 'lower-is-better' && params.baseline === 'seed') {
|
|
2699
|
-
const exclude = scope && Array.isArray(scope.excludePrefixes) ? scope.excludePrefixes : null;
|
|
2700
|
-
if (exclude && exclude.length > 0) return `other files didn't get more ${what}?`;
|
|
2701
|
-
return null;
|
|
2702
|
-
}
|
|
2703
|
-
return null;
|
|
2704
|
-
}
|
|
2705
2601
|
|
|
2706
2602
|
/**
|
|
2707
2603
|
* Per-stage `{kind, direction, baselineKind, question}`, keyed by stage NAME,
|
|
@@ -2723,18 +2619,7 @@ function stageQuestionText({
|
|
|
2723
2619
|
*/
|
|
2724
2620
|
function stageKindMetaFromSpec(spec) {
|
|
2725
2621
|
if (!spec || typeof spec !== 'object' || !spec.closeDecl || !Array.isArray(spec.closeDecl.stages)) return null;
|
|
2726
|
-
const
|
|
2727
|
-
// every command-exit stage's own cmd+args, gathered FIRST (a separate pass)
|
|
2728
|
-
// so stageQuestionText can tell the failing-test-count half of suite-green's
|
|
2729
|
-
// "two assertions" apart from a genuine typecheck stage — see its own doc.
|
|
2730
|
-
/** @type {Set<string>} */
|
|
2731
|
-
const suiteCmdKeys = new Set();
|
|
2732
|
-
for (const s of spec.closeDecl.stages) {
|
|
2733
|
-
if (s && s.kind === 'command-exit') {
|
|
2734
|
-
const key = cmdKey(s.params);
|
|
2735
|
-
if (key !== null) suiteCmdKeys.add(key);
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2622
|
+
const questions = stageQuestionsByName(spec);
|
|
2738
2623
|
/** @type {Map<string, {kind: string|null, direction: string|null, baselineKind: 'seed'|0|null, question: string|null, fromLine: number[]}>} */
|
|
2739
2624
|
const map = new Map();
|
|
2740
2625
|
for (const s of spec.closeDecl.stages) {
|
|
@@ -2743,9 +2628,7 @@ function stageKindMetaFromSpec(spec) {
|
|
|
2743
2628
|
const params = s.params && typeof s.params === 'object' ? s.params : null;
|
|
2744
2629
|
const direction = params && typeof params.direction === 'string' ? params.direction : null;
|
|
2745
2630
|
const baselineKind = params && (params.baseline === 'seed' || params.baseline === 0) ? params.baseline : null;
|
|
2746
|
-
const question =
|
|
2747
|
-
kind, params, genre, suiteCmdKeys,
|
|
2748
|
-
});
|
|
2631
|
+
const question = questions.get(s.name) ?? null;
|
|
2749
2632
|
map.set(s.name, {
|
|
2750
2633
|
kind, direction, baselineKind, question, fromLine: Array.isArray(s.fromLine) ? s.fromLine.filter(Number.isInteger) : [],
|
|
2751
2634
|
});
|
package/src/planrun.js
CHANGED
|
@@ -39,6 +39,7 @@ import { TOOL_BY_VERB, CTX_TOOLS, createCtxTools, toolAction, PERSONA_TOOLS, str
|
|
|
39
39
|
import { anySuppressionWarning } from './authoring.js';
|
|
40
40
|
import { createReadShim, readShimArm, readShimStrategy } from './readshim.js';
|
|
41
41
|
import { globToPrefix, redactSecrets, SECRET_PATTERNS } from './validate.js';
|
|
42
|
+
import { startAttemptDiff } from './attemptdiff.js';
|
|
42
43
|
import { validateBridge, loadGate, newestEligibleVersion, reuseEligibility, quarantinesCredit, QUARANTINED_CODE } from './bridges.js';
|
|
43
44
|
import { extractArtifact, rateSourceFields } from './text.js';
|
|
44
45
|
import { defaultJudgeLoop } from './judged.js';
|
|
@@ -338,6 +339,17 @@ function materialsBlock(m) {
|
|
|
338
339
|
return `\nWhat this run has left to spend — plan within it:\n${lines.join('\n')}\nThese are totals for the run, not per-step allowances. Nothing here is a rate: a step's cost depends on what it does.\n`;
|
|
339
340
|
}
|
|
340
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Cross-run learnings (ITEM33-BUILD.md rev 2, RULING 7): the block `renderHints` (src/learn.js) made, rendered verbatim into
|
|
344
|
+
* the PLANNER's prompt and nowhere else — a worker (scout, step, close-fix) is never handed ledger text. Absent or empty
|
|
345
|
+
* renders '' so every prompt without hints is byte-identical to what it was.
|
|
346
|
+
* @param {string|null|undefined} text
|
|
347
|
+
*/
|
|
348
|
+
function hintsBlock(text) {
|
|
349
|
+
if (typeof text !== 'string' || !text.trim()) return '';
|
|
350
|
+
return `\n${text}\n`;
|
|
351
|
+
}
|
|
352
|
+
|
|
341
353
|
/**
|
|
342
354
|
* MODULE 8 — the signer's words, when this leg is a RERUN taken at a previous run's
|
|
343
355
|
* review door (PRD v1.71 §3.4: *"redo/rerun comes with new authoring"*).
|
|
@@ -632,8 +644,10 @@ export function boundedNote(bounded, rounds) {
|
|
|
632
644
|
* is STATED as well as enforced (the mailbox precedent again: a law only the validator
|
|
633
645
|
* knows costs a redraft per draft). The arms without it — `false` and `'diff'` — render
|
|
634
646
|
* byte-identically to the pre-shim prompt.
|
|
647
|
+
* @param {string|null} [hints] the cross-run learnings block (src/learn.js `renderHints`), verbatim; null/empty
|
|
648
|
+
* renders byte-identically to the prompt without it. Only ever passed for the draft and the first replan.
|
|
635
649
|
*/
|
|
636
|
-
export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes, materials, startingDraft = null, checkFacts = {}, doorNote = null, readShim = false) {
|
|
650
|
+
export function planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopes, materials, startingDraft = null, checkFacts = {}, doorNote = null, readShim = false, hints = null) {
|
|
637
651
|
const scopeMenu = Array.isArray(scopes) && scopes.length ? scopes : legalScopes(job.writeScope ?? []);
|
|
638
652
|
const ceiling = Array.isArray(job.tools) ? job.tools : [...TOOL_MENU];
|
|
639
653
|
// The shape-lottery laws, stated where the check menu is offered (the mailbox
|
|
@@ -723,7 +737,7 @@ ${scopeMenu.map((s) => ` ${JSON.stringify(s)}`).join('\n')}
|
|
|
723
737
|
|
|
724
738
|
Goal:
|
|
725
739
|
${job.goal}
|
|
726
|
-
${doorNoteBlock(doorNote)}${materialsBlock(materials)}
|
|
740
|
+
${doorNoteBlock(doorNote)}${materialsBlock(materials)}${hintsBlock(hints)}
|
|
727
741
|
Repository survey (from a read-only scout):
|
|
728
742
|
${scoutBlob || '(no scout notes)'}`;
|
|
729
743
|
let p = doc;
|
|
@@ -982,12 +996,16 @@ ${scoutBlob || '(no scout notes)'}`;
|
|
|
982
996
|
* the file, end the Loop after that round, and end the leg `stopped` (a checkpoint outcome, filed
|
|
983
997
|
* the way a mid-step cap-halt is; resume re-enters that step, or continues the phase). Nothing is in flight at that
|
|
984
998
|
* point, so the spend is exact. `null` = no stop surface (the default).
|
|
999
|
+
* @param {{text: string, ids: string[]}|null} [opts.learnHints=null] cross-run learnings (src/learn.js `renderHints`, made once at
|
|
1000
|
+
* run start from the ledger as it stood). Handed to the PLANNER only: the first draft and the FIRST replan. No worker ever
|
|
1001
|
+
* receives it, and a later replan gets nothing. Each injection is recorded on the spine as a `learnings` record (the exact
|
|
1002
|
+
* text) so the Audit can show it and the byte check can compare it with a fresh render. `null` = no hints (the default).
|
|
985
1003
|
* @returns {Promise<string>} 'green' | 'already-green' | 'escalated' | 'plan-red' |
|
|
986
1004
|
* 'check-red' | 'close-red' | 'close-unsupported' | 'recipe-stale' | 'pricing-red' |
|
|
987
1005
|
* 'branch-red' | 'cap-halt' | 'wall-halt' | 'stopped' | 'provider-red' | 'interpreter-red' |
|
|
988
1006
|
* 'step-stalled' | 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
989
1007
|
*/
|
|
990
|
-
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null }) {
|
|
1008
|
+
export async function runPlan(job, { workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, remainingUsd, isUnpriced = () => false, spendComplete = () => true, capRuns = 3, strikeLimit = STRIKE_LIMIT, closeTimeoutMs, closeDir = null, maxStepRounds = 40, layerRoot = false, readShim = false, scout = true, scoutRounds = SCOUT_ROUNDS, bridge = null, now, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, priorSpentUsd = 0, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null, learnHints = null }) {
|
|
991
1009
|
// MEMORY-CACHE: what the read shim (src/readshim.js) saved THIS run, summed across
|
|
992
1010
|
// every mkWorker's own shim instance (scout, drafter, each step's worker, the fix
|
|
993
1011
|
// worker) — one accumulator closed over by all of them, because the shim's ledger
|
|
@@ -3068,6 +3086,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3068
3086
|
// is actually left, which is the adaptation channel.
|
|
3069
3087
|
const materials = { balanceUsd: remainingUsd(), remainingMs: clock.bounded ? clock.remainingMs() : null, ...(progress ? { progress } : {}) };
|
|
3070
3088
|
emit('materials', { phase, ...materials });
|
|
3089
|
+
// Cross-run learnings: the planner at the draft and at the FIRST replan, nobody else. The spine keeps the exact text it was
|
|
3090
|
+
// handed (a record, never a verdict), so the Audit can show it and the byte check can compare it with a fresh render.
|
|
3091
|
+
const hints = learnHints && learnHints.text && (phase === 'draft' || (phase === 'replan' && replans === 1)) ? learnHints : null;
|
|
3092
|
+
if (hints) emit('learnings', { phase, text: hints.text, ids: hints.ids, chars: hints.text.length });
|
|
3071
3093
|
// D5 — the bridge rides into the FIRST draft phase ONLY. A replan drafts from the
|
|
3072
3094
|
// RUN'S OWN STATE (what failed, what is left): re-handing the recipe there would
|
|
3073
3095
|
// answer the replan's question with the material the run just demonstrated does not
|
|
@@ -3092,7 +3114,7 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3092
3114
|
const draftPlan = async (/** @type {any[]|null} */ reds) => {
|
|
3093
3115
|
drafter.setIteration(reds ? 'redraft' : 'draft');
|
|
3094
3116
|
replanCut = false;
|
|
3095
|
-
const r = await drafter.ask(planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopeMenu, materials, starting, checkFacts, doorWords?.text ?? null, readShim), []);
|
|
3117
|
+
const r = await drafter.ask(planPrompt(job, scoutBlob, reds, maxStepRounds, failure, scopeMenu, materials, starting, checkFacts, doorWords?.text ?? null, readShim, hints?.text ?? null), []);
|
|
3096
3118
|
if (phase === 'replan' && typeof r.error === 'string' && r.error.startsWith('truncated:')) {
|
|
3097
3119
|
replanCut = true;
|
|
3098
3120
|
return '';
|
|
@@ -3425,7 +3447,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3425
3447
|
// read ONCE: the note and the "was it the attempt just before this one" test
|
|
3426
3448
|
// are two questions about the same record, and two reads is how they drift
|
|
3427
3449
|
const bound = w.wasBounded();
|
|
3428
|
-
|
|
3450
|
+
// attempt-diff: what THIS try changed, recorded to the spine only (never a prompt)
|
|
3451
|
+
const tryDiff = await startAttemptDiff({ workdir, fence: step.scope !== undefined ? [resolve(workdir, globToPrefix(step.scope))] : fencePrefixes, redact: scrub });
|
|
3452
|
+
let r;
|
|
3453
|
+
try { r = await w.ask([
|
|
3429
3454
|
step.action,
|
|
3430
3455
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root — a relative path resolves against a different directory and will be denied by the gate.`,
|
|
3431
3456
|
step.target && `Write your deliverable to: ${resolve(workdir, step.target)}`,
|
|
@@ -3437,6 +3462,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
3437
3462
|
bound?.iteration === iteration - 1 && boundedNote(bound, step.rounds),
|
|
3438
3463
|
rootInj && rootInj.note,
|
|
3439
3464
|
].filter(Boolean).join('\n\n'));
|
|
3465
|
+
} finally {
|
|
3466
|
+
const d = await tryDiff.finish();
|
|
3467
|
+
if (d) emit('attempt-diff', { phase: `step:${step.id}`, step: step.id, iteration, ...d });
|
|
3468
|
+
}
|
|
3440
3469
|
lastText = scrub(r.text ?? '').slice(0, ARTIFACT_MAX);
|
|
3441
3470
|
};
|
|
3442
3471
|
let lastGap = '';
|
|
@@ -4172,7 +4201,8 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4172
4201
|
// same iteration numbering as the step loop: `w.setIteration(iteration)` above
|
|
4173
4202
|
// stamps the bound, so "the attempt just before this one" is `iteration - 1`
|
|
4174
4203
|
const bound = w.wasBounded();
|
|
4175
|
-
await
|
|
4204
|
+
const tryDiff = await startAttemptDiff({ workdir, fence: fencePrefixes, redact: scrub });
|
|
4205
|
+
try { await w.ask([
|
|
4176
4206
|
'The job\'s final verification is failing. Fix the repository so it passes.',
|
|
4177
4207
|
`Repository root (absolute): ${workdir}\nEvery path you pass to a tool MUST be absolute and inside this root.`,
|
|
4178
4208
|
artifacts.length > 0 && `Working context (read-only) — the plan's steps produced:\n${artifacts.map((a) => `[${a.id}] ${a.text}`).join('\n\n')}`,
|
|
@@ -4182,6 +4212,10 @@ export async function runPlan(job, { workdir, provider, nativeProvider, provider
|
|
|
4182
4212
|
bound?.iteration === iteration - 1 && boundedNote(bound, maxStepRounds),
|
|
4183
4213
|
rootInj && rootInj.note,
|
|
4184
4214
|
].filter(Boolean).join('\n\n'));
|
|
4215
|
+
} finally {
|
|
4216
|
+
const d = await tryDiff.finish();
|
|
4217
|
+
if (d) emit('attempt-diff', { phase: 'fix', step: null, iteration, ...d });
|
|
4218
|
+
}
|
|
4185
4219
|
};
|
|
4186
4220
|
fixOutcome = await ralph({
|
|
4187
4221
|
// the staged close rides in as the JUDGE seam (one verdict, every stage in
|
package/src/promptregisters.js
CHANGED
|
@@ -65,6 +65,10 @@ export const PROMPT_REGISTERS = Object.freeze([
|
|
|
65
65
|
Object.freeze({ file: 'src/pagemap.js', name: 'MAP_SYSTEM' }),
|
|
66
66
|
Object.freeze({ file: 'src/pagemap.js', name: 'mapPrompt() (inline template, not a const)' }),
|
|
67
67
|
|
|
68
|
+
// src/fitcheck.js - the fit check call (F243): the AI splits each job line into asks and quotes the page for each
|
|
69
|
+
Object.freeze({ file: 'src/fitcheck.js', name: 'FIT_SYSTEM' }),
|
|
70
|
+
Object.freeze({ file: 'src/fitcheck.js', name: 'fitPrompt() (inline template, not a const)' }),
|
|
71
|
+
|
|
68
72
|
// src/readshim.js — the capped/deduplicating read shim's worker-facing strategy
|
|
69
73
|
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_STRATEGY' }),
|
|
70
74
|
Object.freeze({ file: 'src/readshim.js', name: 'READ_SHIM_DIFF_STRATEGY' }),
|
package/src/providerrows.js
CHANGED
|
@@ -170,8 +170,8 @@ export function endpointOf(provider, baseUrl) {
|
|
|
170
170
|
if (raw === '') return '';
|
|
171
171
|
try {
|
|
172
172
|
const u = new URL(raw);
|
|
173
|
-
return `${u.protocol}//${u.host}${u.pathname.replace(
|
|
174
|
-
} catch { return raw.replace(
|
|
173
|
+
return `${u.protocol}//${u.host}${u.pathname.replace(/(?<!\/)\/+$/, '')}`;
|
|
174
|
+
} catch { return raw.replace(/(?<!\/)\/+$/, ''); }
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/**
|
|
@@ -318,7 +318,7 @@ export function keyValueOf(env, envName) {
|
|
|
318
318
|
function perMoney(n) {
|
|
319
319
|
const shown = String(n);
|
|
320
320
|
if (!shown.includes('e') && (shown.split('.')[1] ?? '').length >= 2) return `$${shown}`;
|
|
321
|
-
if (shown.includes('e')) return `$${n.toFixed(12).replace(/0+$/, '').replace(/\.$/, '')}`;
|
|
321
|
+
if (shown.includes('e')) return `$${n.toFixed(12).replace(/(?<!0)0+$/, '').replace(/\.$/, '')}`;
|
|
322
322
|
return `$${n.toFixed(2)}`;
|
|
323
323
|
}
|
|
324
324
|
|
package/src/rubricstep.js
CHANGED
|
@@ -613,18 +613,37 @@ export function lineRules(l) {
|
|
|
613
613
|
return isRec(l) && typeof l.rule === 'string' && l.rule ? l.rule.split('; ').filter(Boolean) : [];
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
-
/** @param {
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
616
|
+
/** @param {string} t @returns {string} a time token as compared: no spaces, lower case, no leading zero on the hour */
|
|
617
|
+
const timeKey = (t) => t.replace(/\s+/g, '').toLowerCase().replace(/^0(?=\d:)/, '');
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* The person's words read by KIND, so a digit inside a time, a date or an amount belongs to that token only (F242).
|
|
621
|
+
* @param {string} text @returns {{bare: Set<number>, amount: Set<number>, time: Set<string>, date: Set<string>}}
|
|
622
|
+
*/
|
|
623
|
+
const tokensSaid = (text) => {
|
|
624
|
+
const out = { bare: new Set(), amount: new Set(), time: new Set(), date: new Set() };
|
|
625
|
+
const re = /(\d{1,2}:\d{2}(?:\s?[AaPp][Mm])?)|(\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[/.]\d{1,2}[/.]\d{2,4})|([$\u20ac\u00a3]\s?\d[\d,]*(?:\.\d+)?|\d[\d,]*(?:\.\d+)?\s?(?:US\s)?(?:dollars?|euros?|pounds?|USD|EUR|GBP)\b)|(\d[\d,]*(?:\.\d+)?)/gi;
|
|
626
|
+
for (const m of text.matchAll(re)) {
|
|
627
|
+
if (m[1]) out.time.add(timeKey(m[1]));
|
|
628
|
+
else if (m[2]) out.date.add(m[2]);
|
|
629
|
+
else if (m[3]) out.amount.add(Number(m[3].match(/\d[\d,]*(?:\.\d+)?/)?.[0].replace(/,/g, '')));
|
|
630
|
+
else out.bare.add(Number(m[4].replace(/,/g, '')));
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
620
633
|
};
|
|
621
634
|
|
|
622
635
|
/**
|
|
623
636
|
* Is this rule bound in the words the person wrote? The ONE test for "a number the AI made" (`rubric-fact-unmarked`, the L3 settle, and
|
|
624
|
-
* the early ask of PRD v1.107 Part 5b all read it; never a second spelling).
|
|
637
|
+
* the early ask of PRD v1.107 Part 5b all read it; never a second spelling). By kind: a number is said only as a bare number or an
|
|
638
|
+
* amount's number; a time only as a time token (hour leading zero ignored); a date only as a date token. Never a piece of a token.
|
|
625
639
|
* @param {unknown} b @param {string} said @returns {boolean}
|
|
626
640
|
*/
|
|
627
|
-
export const boundInWords = (b, said) =>
|
|
641
|
+
export const boundInWords = (b, said) => {
|
|
642
|
+
const t = tokensSaid(said);
|
|
643
|
+
if (typeof b === 'number') return t.bare.has(b) || t.amount.has(b);
|
|
644
|
+
if (typeof b !== 'string') return false;
|
|
645
|
+
return /^\s*\d{1,2}:\d{2}/.test(b) ? t.time.has(timeKey(b)) : t.date.has(b.trim());
|
|
646
|
+
};
|
|
628
647
|
|
|
629
648
|
/** @param {any} jl one job line @returns {string} the words the person wrote on it: the line and its `~` rules */
|
|
630
649
|
export const wordsSaidOn = (jl) => `${jl.text ?? ''} ${lineRules(jl).join(' ')}`;
|
|
@@ -844,6 +863,17 @@ export function checksOfClose(closeDecl) {
|
|
|
844
863
|
: []));
|
|
845
864
|
}
|
|
846
865
|
|
|
866
|
+
/**
|
|
867
|
+
* The names of the facts a signed close checks (every fact part of every rubric-step stage). Pure.
|
|
868
|
+
* @param {any} closeDecl @returns {string[]}
|
|
869
|
+
*/
|
|
870
|
+
export function signedFactNames(closeDecl) {
|
|
871
|
+
const stages = isRec(closeDecl) && Array.isArray(closeDecl.stages) ? closeDecl.stages : [];
|
|
872
|
+
return stages.flatMap((/** @type {any} */ st) => (isRec(st) && st.kind === 'rubric-step' && isRec(st.params) && Array.isArray(st.params.parts)
|
|
873
|
+
? st.params.parts.filter((/** @type {any} */ p) => isRec(p) && p.class === 'fact' && Array.isArray(p.facts)).flatMap((/** @type {any} */ p) => p.facts.filter((/** @type {any} */ f) => isRec(f) && typeof f.name === 'string').map((/** @type {any} */ f) => f.name))
|
|
874
|
+
: []));
|
|
875
|
+
}
|
|
876
|
+
|
|
847
877
|
/**
|
|
848
878
|
* Reconcile the CONFIRMED plan with the close the composer actually wrote. The plan card after drafting shows exactly
|
|
849
879
|
* the close's own checks (`checksOfClose`); every confirmed check that no part carries out (no part lists its number in
|
package/src/run.js
CHANGED
|
@@ -259,6 +259,8 @@ async function primitiveSmoke(workdir) {
|
|
|
259
259
|
* @param {string|null} [opts.stopFile=null] P5 item 5 — the run's STOP REQUEST file
|
|
260
260
|
* (`stopFilePath(spine)`, src/legs.js), forwarded verbatim to the plan flow, which reads it at the
|
|
261
261
|
* round-boundary seam (where the money cap binds) and ends the leg `stopped`. `null` = no stop surface.
|
|
262
|
+
* @param {{text: string, ids: string[]}|null} [opts.learnHints=null] cross-run learnings for the PLANNER only (the draft and the
|
|
263
|
+
* first replan); forwarded verbatim to the plan flow, which records what it handed over as a `learnings` spine record.
|
|
262
264
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
263
265
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
264
266
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
@@ -273,7 +275,7 @@ async function primitiveSmoke(workdir) {
|
|
|
273
275
|
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
274
276
|
* unknown in-flight state is not a known-safe resume point.
|
|
275
277
|
*/
|
|
276
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null }) {
|
|
278
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, judgeModel = null, rates = null, judgeRates = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, draftSpentUsd = 0, draftSpendComplete = true, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true, stopFile = null, learnHints = null }) {
|
|
277
279
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
278
280
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
279
281
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -540,7 +542,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
540
542
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
541
543
|
{
|
|
542
544
|
const outcome = await runPlan(job, {
|
|
543
|
-
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable, stopFile,
|
|
545
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, judgeModel, rates, judgeRates, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable, stopFile, learnHints,
|
|
544
546
|
// hamr's ruling 2026-09-28 — ONE cap covers drafting + run: the run's
|
|
545
547
|
// enforced ceiling is the signed `budgetUsd` (unchanged, still in the
|
|
546
548
|
// spec hash) MINUS whatever drafting already spent on this job before
|