bareloop 0.45.0 → 0.46.1
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 +48 -0
- package/bareloop.context.md +34 -7
- package/package.json +1 -1
- package/src/attemptdiff.js +116 -0
- package/src/authorreadout.js +55 -14
- 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/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/pagedry.js +2 -3
- package/src/pagefetch.js +2 -1
- package/src/pagerules.js +27 -4
- package/src/panel/authorroutes.js +4 -3
- package/src/panel/authorsession.js +5 -7
- package/src/panel/index.html +108 -0
- package/src/panel/schedulestart.js +5 -0
- package/src/panel/server.js +16 -134
- package/src/panel/status.js +11 -0
- package/src/planrun.js +40 -6
- package/src/providerrows.js +3 -3
- package/src/run.js +4 -2
- package/src/schedulewords.js +1 -1
- package/src/source.js +1 -1
- package/src/stagewords.js +145 -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/authorreadout.d.ts +7 -0
- package/types/config.d.ts +1 -1
- 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/pagerules.d.ts +7 -1
- package/types/panel/status.d.ts +8 -0
- package/types/planrun.d.ts +13 -3
- package/types/reuse.d.ts +1 -1
- package/types/run.d.ts +8 -2
- package/types/stagewords.d.ts +91 -0
package/src/userrun.js
CHANGED
|
@@ -82,6 +82,7 @@ import { keysForDoor } from './keysfile.js';
|
|
|
82
82
|
import { parseJsonl } from './replayio.js';
|
|
83
83
|
import { claimRun, monthlyRefusalText, legSpend, chainSpend } from './monthly.js';
|
|
84
84
|
import { ConfigError } from './config.js';
|
|
85
|
+
import { learnAtRunEnd, hintsForRun, releaseRun } from './learnstore.js';
|
|
85
86
|
import { commandFor } from './invoke.js';
|
|
86
87
|
import { judgeRatesFor, keyNameFor, keyValueOf, priceReadout, ratesFor, ratesOfLine, resolveWorker, judgeRowFor, rowsForHome, REFUSE_NAME_MODEL } from './providerrows.js';
|
|
87
88
|
// the banner's wall arithmetic, extracted so it is reachable by a test (F83): the
|
|
@@ -1306,6 +1307,11 @@ async function execute(ctx) {
|
|
|
1306
1307
|
if (RULING.decision !== 'rerun') {
|
|
1307
1308
|
out(`\n${RULING.decision === 'accept' ? 'ACCEPTED' : 'PAUSED'} — the verdict this run minted (${doorRecord.outcome}) is UNTOUCHED; what you answered is what happens to the work.`);
|
|
1308
1309
|
if (RULING.decision === 'accept') {
|
|
1310
|
+
// Cross-run learnings: the human Accept is what releases a held rubric run's lessons so they count (forward only; a rerun or a
|
|
1311
|
+
// pause never does). Its own try/catch: a record, never a verdict.
|
|
1312
|
+
if (cfgOn && DOOR !== null) {
|
|
1313
|
+
try { releaseRun({ home: cfgHome, runid: DOOR.includes('/') || DOOR.endsWith('.jsonl') ? basename(DOOR).replace(/^u-/, '').replace(/\.jsonl$/, '') : DOOR }); } catch (/** @type {any} */ e) { err(`WARNING: could not release this run's lessons in the learnings ledger (${redactSecrets(String(e?.message ?? e)).split('\n')[0]}).`); }
|
|
1314
|
+
}
|
|
1309
1315
|
out(` proved ${ans.mechanical?.ran ?? 0} mechanical stage(s) re-ran on the tree as it stands, and passed — an accept is not a rubber stamp`);
|
|
1310
1316
|
out(` credit ${ans.released ? 'RELEASED — this workflow is now eligible for reuse' : ((ctx.registry ?? null) === null ? 'not released: no --registry was named, so there is no entry to release (the answer is on the spine)' : (quarantinesCredit(spec.verdictType) ? 'nothing left to release — this row was already released by an earlier accept (a repeat answer records nothing new)' : 'nothing was held to release — a green-class row is minted born RELEASED (hamr, 2026-08-30): a machine proved it, so there was never a credit to quarantine'))}`);
|
|
1311
1317
|
if (ans.note) out(` note ${ans.note}`);
|
|
@@ -1895,10 +1901,18 @@ async function execute(ctx) {
|
|
|
1895
1901
|
// honours it, and the `finally` below clears whatever is left when the leg ends.
|
|
1896
1902
|
const stopFile = stopFilePath(spineFile);
|
|
1897
1903
|
try { rmSync(stopFile, { force: true }); } catch { /* best-effort — a stop surface must never kill the run */ }
|
|
1904
|
+
// Cross-run learnings (src/learn.js): the PLANNER's block, made once from the ledger as it stood — empty when the one setting
|
|
1905
|
+
// (`learningHints` in config.json) is off. A record read, never a verdict: a failure here is no hints, never a failed run.
|
|
1906
|
+
/** @type {{text: string, ids: string[]}|null} */
|
|
1907
|
+
let learnHints = null;
|
|
1908
|
+
if (cfgOn) {
|
|
1909
|
+
try { learnHints = hintsForRun({ home: cfgHome, spec, specHash }); } catch { learnHints = null; }
|
|
1910
|
+
}
|
|
1898
1911
|
let outcome;
|
|
1899
1912
|
try {
|
|
1900
1913
|
outcome = await runJob(spec, {
|
|
1901
1914
|
stopFile,
|
|
1915
|
+
...(learnHints === null ? {} : { learnHints }),
|
|
1902
1916
|
// PRD item 27/M3 — the SAME `emit` the pre-run close-timing reading above
|
|
1903
1917
|
// used (never a second `makeSpine(spineFile)` here: two independent
|
|
1904
1918
|
// emitters against one file would both start their seq counter at 0 and
|
|
@@ -2010,6 +2024,20 @@ async function execute(ctx) {
|
|
|
2010
2024
|
appendRunEvent({ runid, type: 'settled', by: runid, spentUsd: fin.usd, spendComplete: fin.complete, at: new Date().toISOString(), ...(LEG > 1 ? { leg: LEG } : {}) }, { home: cfgHome });
|
|
2011
2025
|
} catch (/** @type {any} */ e) { err(`WARNING: could not settle this run's claim in ~/.config/bareloop/runs.jsonl (${e.message}) — the next run settles it once this process is gone.`); }
|
|
2012
2026
|
}
|
|
2027
|
+
// The run-end tally of cross-run learnings ($0, code only, no model): the whole spine (all legs) and the gate audit become cards
|
|
2028
|
+
// folded into the ONE ledger (a rubric job's are held until the human Accept). Its own try/catch, like the settle above: a
|
|
2029
|
+
// record, never a verdict — a failure prints one WARNING line and can never change the outcome, the exit code or the spend.
|
|
2030
|
+
// `execute` is shared by the panel, the CLI and the bundle runner, so this is the ONE call site. It runs with the hints setting
|
|
2031
|
+
// on or off (OFF only means the planner is handed nothing). With no config home (a test with an injected env) it writes nothing.
|
|
2032
|
+
if (je && cfgOn) {
|
|
2033
|
+
try {
|
|
2034
|
+
const filedAudit = ctx.bundle ? join(spineDir, 'gate-audit.jsonl') : join(spineDir, `u-${runid}-gate-audit.jsonl`);
|
|
2035
|
+
learnAtRunEnd({
|
|
2036
|
+
home: cfgHome, runid, events: allEvents, auditFiles: [filedAudit, join(wd, 'gate-audit.jsonl')], spec, specHash, workdir: wd,
|
|
2037
|
+
trust: quarantinesCredit(spec.verdictType) ? 'rubric' : 'deterministic',
|
|
2038
|
+
});
|
|
2039
|
+
} catch (/** @type {any} */ e) { err(`WARNING: could not record this run's lessons in the learnings ledger (${redactSecrets(String(e?.message ?? e)).split('\n')[0]}) — the run itself is unaffected.`); }
|
|
2040
|
+
}
|
|
2013
2041
|
// the ONE spelling of the text-side scan (src/validate.js) — a hand-rolled copy
|
|
2014
2042
|
// here would be the ninth, and one that misses a shape leaks on the very output
|
|
2015
2043
|
// it guards. Only the COUNT is ever read out; the matches themselves stay here.
|
package/src/validate.js
CHANGED
|
@@ -167,14 +167,19 @@ export function closeScriptCandidateToken(cmd) {
|
|
|
167
167
|
// redactor passes is a leak on the very output the validator was guarding.
|
|
168
168
|
export const SECRET_PATTERNS = [
|
|
169
169
|
/(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{16,}/,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
// The prefixes below are distinctive (an ordinary hyphenated word never starts with `ghp_`, `AKIA`, ...), so their left bound
|
|
171
|
+
// drops only the `-`: a key right after a hyphen (`step-ghp_...`) is still a key. `sk-` keeps the full bound above, because
|
|
172
|
+
// `sk-` ends real words (`task-`, `desk-`, `risk-`...). KNOWN LIMIT: an `sk-` key directly after a hyphen or word character
|
|
173
|
+
// (`step-sk-...`) is not found by THIS inventory, so the scan (validator, source door, pagefetch) misses it. (Measured: the
|
|
174
|
+
// redactor still masks it today, but through bareguard's own built-in `sk-` shape, not through this list.)
|
|
175
|
+
/(?<![A-Za-z0-9_])ghp_[A-Za-z0-9]{20,}/,
|
|
176
|
+
/(?<![A-Za-z0-9_])github_pat_[A-Za-z0-9_]{20,}/,
|
|
177
|
+
/(?<![A-Za-z0-9_])AKIA[0-9A-Z]{16}/,
|
|
178
|
+
/(?<![A-Za-z0-9_])xox[bap]-[A-Za-z0-9-]{10,}/,
|
|
174
179
|
// Google/Gemini API key: `AIza` + 35 chars of [0-9A-Za-z_-] (39 total, fixed
|
|
175
180
|
// length by Google's own issuance format). Admitted with the gemini-api
|
|
176
181
|
// provider (PRD item 31.3, F160) — was missing on this branch until then.
|
|
177
|
-
/(?<![A-Za-z0-9_
|
|
182
|
+
/(?<![A-Za-z0-9_])AIza[0-9A-Za-z_-]{35}/,
|
|
178
183
|
];
|
|
179
184
|
const SECRET_RE = new RegExp(SECRET_PATTERNS.map((r) => r.source).join('|'));
|
|
180
185
|
|
package/src/worktree.js
CHANGED
|
@@ -101,7 +101,8 @@ const COMMIT_CONFIG = [
|
|
|
101
101
|
];
|
|
102
102
|
|
|
103
103
|
/** what never goes into the work commit: the arbiter's own books and installed packages */
|
|
104
|
-
const
|
|
104
|
+
const NOT_WORK_PATHS = ['.smoke', '.litectx', 'gate-audit.jsonl'];
|
|
105
|
+
const NOT_WORK_GLOB = ':(exclude,glob)**/node_modules/**';
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
108
|
* The run's final commit, on whatever branch the worktree stands on: everything the run changed except the
|
|
@@ -113,7 +114,12 @@ const NOT_WORK = [':(exclude).smoke', ':(exclude).litectx', ':(exclude)gate-audi
|
|
|
113
114
|
*/
|
|
114
115
|
export function commitWork(dir, message) {
|
|
115
116
|
const run = (/** @type {string[]} */ args) => execFileSync('git', ['-C', dir, ...COMMIT_CONFIG, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
116
|
-
|
|
117
|
+
// `git add` exits 1 when an exclude names a path that exists AND is gitignored (it prints "paths are ignored").
|
|
118
|
+
// An ignored path is never staged without -f, so its exclude is redundant: only exclude what git would otherwise add.
|
|
119
|
+
// check-ignore: 0 = ignored, 1 = not ignored, anything else = a real failure (execFileSync throws on 128).
|
|
120
|
+
const ignored = (/** @type {string} */ p) => { try { run(['check-ignore', '-q', '--', p]); return true; } catch (e) { if (/** @type {any} */ (e)?.status === 1) return false; throw e; } };
|
|
121
|
+
const excludes = NOT_WORK_PATHS.filter((p) => !ignored(p)).map((p) => `:(exclude)${p}`);
|
|
122
|
+
run(['add', '-A', '--', '.', ...excludes, NOT_WORK_GLOB]);
|
|
117
123
|
const staged = run(['diff', '--cached', '--name-only']).trim();
|
|
118
124
|
if (staged !== '') run(['commit', '-q', '-m', message]);
|
|
119
125
|
return { committed: staged !== '', sha: run(['rev-parse', '--short', 'HEAD']).trim() };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cut `text` to at most `max` bytes at a LINE boundary (never mid-line).
|
|
3
|
+
* @param {string} text @param {number} max
|
|
4
|
+
* @returns {{text: string, truncated: boolean, cutLines: number}}
|
|
5
|
+
*/
|
|
6
|
+
export function cutAtLine(text: string, max: number): {
|
|
7
|
+
text: string;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
cutLines: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Begin recording one worker try. Call BEFORE the worker's turn; call `finish()` after
|
|
13
|
+
* (in a `finally`). Never throws.
|
|
14
|
+
* @param {{workdir: string, fence: string[], redact?: (s: string) => string}} o
|
|
15
|
+
* `fence` = the ABSOLUTE write-scope prefixes the gate enforces for this try.
|
|
16
|
+
* @returns {Promise<{finish: () => Promise<null | {files: {path: string, diff: string, truncated: boolean, cutLines: number}[], binary: string[], truncated: boolean, gitError?: string}>}>}
|
|
17
|
+
* `finish()` resolves null when the try changed nothing in scope.
|
|
18
|
+
*/
|
|
19
|
+
export function startAttemptDiff({ workdir, fence, redact }: {
|
|
20
|
+
workdir: string;
|
|
21
|
+
fence: string[];
|
|
22
|
+
redact?: (s: string) => string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
finish: () => Promise<null | {
|
|
25
|
+
files: {
|
|
26
|
+
path: string;
|
|
27
|
+
diff: string;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
cutLines: number;
|
|
30
|
+
}[];
|
|
31
|
+
binary: string[];
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
gitError?: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
/** per-file and per-event byte caps for the recorded diff text */
|
|
37
|
+
export const ATTEMPT_DIFF_FILE_MAX: 4096;
|
|
38
|
+
export const ATTEMPT_DIFF_EVENT_MAX: 16384;
|
package/types/authorreadout.d.ts
CHANGED
|
@@ -303,6 +303,13 @@ export function redsRecord(reds: PlainRed[], { cases, signing }?: {
|
|
|
303
303
|
}): Record<string, any>[];
|
|
304
304
|
/** the stops this module explains in plain words */
|
|
305
305
|
export const PLAIN_PROPOSAL_STOPS: readonly string[];
|
|
306
|
+
/**
|
|
307
|
+
* ONE owner of the plain sentence for every drafting stop code (hamr 2026-10-09: "a user should get a reason they can
|
|
308
|
+
* understand and do something about"). What happened; `STOP_TODO` below overrides the default "draft again" where that is wrong.
|
|
309
|
+
* Used by `draftStopText` for every stop the panel and the CLI show; a code not listed reads `STOP_UNKNOWN`, never the raw code.
|
|
310
|
+
* @type {Record<string, string>}
|
|
311
|
+
*/
|
|
312
|
+
export const STOP_HEADS: Record<string, string>;
|
|
306
313
|
/** codes the declaration gates (validateJob, validateCloseDecl) raise: one plain sentence covers the family */
|
|
307
314
|
export const DECLARATION_CODES: readonly string[];
|
|
308
315
|
export type PlainRed = {
|
package/types/config.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function modelsProblem(list: unknown): string | null;
|
|
|
36
36
|
* `keys` merges per key variable name, then per field; `models.<ENV>` is REPLACED WHOLE (`null`
|
|
37
37
|
* clears it). Validated before anything is written; nothing is written on a refusal.
|
|
38
38
|
*
|
|
39
|
-
* Patch fields understood: `monthlyLimitUsd` (finite > 0), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
|
|
39
|
+
* Patch fields understood: `monthlyLimitUsd` (finite > 0), `learningHints` (true | false), `keys.<ENV>.{shape,baseUrl,legacyMaxTokens}`,
|
|
40
40
|
* `models.<ENV>` (a list judged by {@link modelsProblem}). The first write of a row's `models` list
|
|
41
41
|
* removes that row's older `name` / `priceInPerM` / `priceOutPerM` in the SAME write (never two
|
|
42
42
|
* sources); a retired top-level `anthropicBalanceNote` is dropped on any write. Any other field is
|
package/types/inputlook.d.ts
CHANGED
|
@@ -185,29 +185,6 @@ export function labelListsOf(texts: Record<string, string>): {
|
|
|
185
185
|
scans: Record<string, import("./labelscan.js").LabelScan>;
|
|
186
186
|
lists: Record<string, string>;
|
|
187
187
|
};
|
|
188
|
-
/**
|
|
189
|
-
* L3: a reason the AI gave, cut only at a sentence end (never mid-sentence): whole sentences while they fit in `max` characters,
|
|
190
|
-
* and nothing at all if the first one alone does not fit. A trailing full stop is dropped (the caller closes the clause).
|
|
191
|
-
* @param {string} text @param {number} max @returns {string}
|
|
192
|
-
*/
|
|
193
|
-
export function wholeSentences(text: string, max: number): string;
|
|
194
|
-
/**
|
|
195
|
-
* I1 (hamr, 2026-10-13, "Ruled by hamr" 2): when the look shows the person's intent on one `~` rule was NOT captured - its words or
|
|
196
|
-
* meaning are not in the source - the person is asked about THAT rule at once, named by its line and rule. Only a look entry that names
|
|
197
|
-
* a rule number counts; anything else the look could not find is not asked piecemeal (it is Not checked, or the whole job is reviewed).
|
|
198
|
-
* The wording is the code's own, never the model's. The keep-or-drop and the cap are `normalizeQuestions` (src/authorflow.js).
|
|
199
|
-
* @param {LookNotFound[]} notFound @param {{n: number, text?: string, rule?: string, rules?: string[]}[]} jobLines
|
|
200
|
-
* @returns {{line: number, question: string}[]} every candidate question (the caller applies the machine's caps)
|
|
201
|
-
*/
|
|
202
|
-
export function ruleQuestions(notFound: LookNotFound[], jobLines: {
|
|
203
|
-
n: number;
|
|
204
|
-
text?: string;
|
|
205
|
-
rule?: string;
|
|
206
|
-
rules?: string[];
|
|
207
|
-
}[]): {
|
|
208
|
-
line: number;
|
|
209
|
-
question: string;
|
|
210
|
-
}[];
|
|
211
188
|
/** the name of the answer tool - the one place the AI's report enters */
|
|
212
189
|
export const LOOK_REPORT_TOOL: "report_look";
|
|
213
190
|
/** how many hits one search returns at most, and how long a snippet or a read line is */
|
package/types/labelscan.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export const MIN_COUNT: 1;
|
|
|
29
29
|
export const MAX_PER_SHAPE: 40;
|
|
30
30
|
/** only this much of a source is scanned (a source over it is scanned up to a line end) */
|
|
31
31
|
export const SCAN_MAX_CHARS: 2000000;
|
|
32
|
+
/**
|
|
33
|
+
* The page reader's bracketed tag (`[ref=549]`), shared by the three places that strip it. The lookahead and lookbehind add no match and drop no
|
|
34
|
+
* match: a `[` that sits inside the run of an earlier `[letters=` (no `]` or white space between) is skipped, because that earlier start reads the same
|
|
35
|
+
* run to the same end - it matches (and consumes this one) or fails (and so does this one). Without them "[a=[a=[a=..." with no `]` is quadratic.
|
|
36
|
+
*/
|
|
37
|
+
export const TAG: RegExp;
|
|
32
38
|
export type LabelEntry = {
|
|
33
39
|
id: string;
|
|
34
40
|
label: string;
|