bareloop 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bareloop.context.md +103 -41
  3. package/package.json +2 -2
  4. package/src/authorflow.js +26 -11
  5. package/src/authoring.js +1 -1
  6. package/src/authorjob.js +33 -15
  7. package/src/authorreadout.js +7 -6
  8. package/src/declaredclose.js +6 -3
  9. package/src/draftspend.js +20 -2
  10. package/src/facts.js +54 -9
  11. package/src/factwords.js +58 -6
  12. package/src/inputlook.js +38 -3
  13. package/src/interviewrun.js +0 -1
  14. package/src/job.js +39 -12
  15. package/src/kinds.js +5 -0
  16. package/src/pagedropwords.js +64 -0
  17. package/src/pagemap.js +264 -0
  18. package/src/pagerules.js +319 -0
  19. package/src/panel/authorroutes.js +138 -60
  20. package/src/panel/authorsession.js +88 -32
  21. package/src/panel/index.html +1251 -522
  22. package/src/panel/jobplan.js +4 -2
  23. package/src/panel/runroutes.js +188 -6
  24. package/src/panel/scheduler.js +156 -0
  25. package/src/panel/schedules.js +667 -0
  26. package/src/panel/schedulestart.js +243 -0
  27. package/src/panel/server.js +1234 -68
  28. package/src/panel/status.js +33 -0
  29. package/src/panelmoney.js +29 -0
  30. package/src/planrun.js +92 -11
  31. package/src/promptregisters.js +4 -0
  32. package/src/providerrows.js +3 -3
  33. package/src/replay.js +118 -7
  34. package/src/reviewdoor.js +37 -0
  35. package/src/rubricstep.js +123 -11
  36. package/src/runlist.js +37 -1
  37. package/src/schedulewords.js +299 -0
  38. package/src/source.js +18 -0
  39. package/src/thinking.js +63 -2
  40. package/src/trigger.js +48 -0
  41. package/src/triggerclock.js +127 -0
  42. package/src/triggerfiles.js +215 -0
  43. package/src/triggerledger.js +79 -0
  44. package/src/userrun.js +34 -21
  45. package/types/authorflow.d.ts +42 -6
  46. package/types/authorjob.d.ts +10 -2
  47. package/types/declaredclose.d.ts +3 -1
  48. package/types/draftspend.d.ts +27 -1
  49. package/types/facts.d.ts +15 -0
  50. package/types/factwords.d.ts +26 -3
  51. package/types/inputlook.d.ts +15 -0
  52. package/types/job.d.ts +8 -1
  53. package/types/kinds.d.ts +5 -0
  54. package/types/pagedropwords.d.ts +26 -0
  55. package/types/pagemap.d.ts +154 -0
  56. package/types/pagerules.d.ts +86 -0
  57. package/types/panel/authorroutes.d.ts +50 -14
  58. package/types/panel/authorsession.d.ts +37 -12
  59. package/types/panel/runroutes.d.ts +32 -1
  60. package/types/panel/scheduler.d.ts +17 -0
  61. package/types/panel/schedules.d.ts +412 -0
  62. package/types/panel/schedulestart.d.ts +106 -0
  63. package/types/panel/server.d.ts +297 -15
  64. package/types/panel/status.d.ts +51 -0
  65. package/types/panelmoney.d.ts +5 -0
  66. package/types/planrun.d.ts +2 -0
  67. package/types/providerrows.d.ts +2 -2
  68. package/types/replay.d.ts +21 -1
  69. package/types/reviewdoor.d.ts +27 -0
  70. package/types/rubricstep.d.ts +57 -1
  71. package/types/runlist.d.ts +24 -1
  72. package/types/schedulewords.d.ts +137 -0
  73. package/types/source.d.ts +11 -0
  74. package/types/thinking.d.ts +39 -0
  75. package/types/trigger.d.ts +37 -0
  76. package/types/triggerclock.d.ts +52 -0
  77. package/types/triggerfiles.d.ts +93 -0
  78. package/types/triggerledger.d.ts +35 -0
  79. package/types/userrun.d.ts +4 -0
package/src/userrun.js CHANGED
@@ -54,7 +54,8 @@ import { resolveJudge } from './judged.js';
54
54
  // path uses (never a second one) and keeps its patient the way it left it.
55
55
  import { readResume, resumeTreeGate, checkpointAgeGate, writeRunGreenRow, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS } from './reuse.js';
56
56
  // PRD item 28's provider factory (src/providers.js): the WORKER provider's
57
- // ctor/envKey/tier-table/param-gating come from here now — one seam instead
57
+ // ctor/param-gating come from here now (its key variable and price come from the
58
+ // Settings row, src/providerrows.js) — one seam instead
58
59
  // of an `if (provider === …)` scattered across this script. PRD item 32.2
59
60
  // moved the JUDGE onto this SAME factory: it used to be separately pinned
60
61
  // below to a hardcoded Anthropic constructor at the judged tier because the
@@ -303,6 +304,7 @@ class ExitSignal extends Error {
303
304
  * @property {string} seed
304
305
  * @property {string} spineName
305
306
  * @property {string} [into] see `RunOpts.into`
307
+ * @property {boolean} [refreeze] a FRESH `--spec` run: fetch the web inputs again AFTER the run is listed (never on resume/door/decide)
306
308
  * @property {string|null} model
307
309
  * @property {string|null} providerKey
308
310
  * @property {string|null} readShim
@@ -492,7 +494,8 @@ async function execute(ctx) {
492
494
  const REGISTRY_TAIL = (ctx.registry ?? null) !== null ? ` --registry ${(ctx.registry ?? null)} --workflow ${(ctx.workflow ?? null) ?? spec.job}` : '';
493
495
 
494
496
  const WORKDIR = target.workdir;
495
- const SEED = target.seed;
497
+ // `let`: a fresh run's refetch of its web inputs commits a NEW seed (below, after the run is listed)
498
+ let SEED = target.seed;
496
499
  const specHash = jobSpecHash(spec);
497
500
  // the re-invocation commands this engine prints: a bundle names its own door and hash,
498
501
  // every other caller gets today's byte-identical strings.
@@ -507,10 +510,8 @@ async function execute(ctx) {
507
510
  // and tested apart from this script. The JUDGE's model is resolved separately
508
511
  // (`resolveJudge`, below the key block): it defaults to this same resolved
509
512
  // worker model and a signed `judge` override wins over it.
510
- // The provider's OWN tier table (src/providers.js) supplies the default —
511
- // not the anthropic-only DEFAULT_TIER_MODELS above (that map is the
512
- // `--model` FLAG's vocabulary only, sonnet/haiku, unrelated to which
513
- // provider the spec names). `spec.provider` is not validateJob'd until
513
+ // There is no default model: a spec or flag that names none dies below with
514
+ // REFUSE_NAME_MODEL (the model is one ticked in Settings). `spec.provider` is not validateJob'd until
514
515
  // runJob below, so an unrecognized value dies here with the same operator-
515
516
  // facing message resolveWorkerModel's own errors use, rather than an
516
517
  // unhandled throw.
@@ -1405,8 +1406,8 @@ async function execute(ctx) {
1405
1406
  // token against. That made "provider-agnostic" half-true: you could run on
1406
1407
  // another provider only if you also held an Anthropic account.
1407
1408
  //
1408
- // The WORKER's key comes from the provider's own `envKey` (the factory's, so
1409
- // there is one table saying which provider reads which variable) and is
1409
+ // The WORKER's key comes from its Settings row's own env name (`keyValueOf`, src/providerrows.js,
1410
+ // so there is one place saying which row reads which variable) and is
1410
1411
  // ALWAYS required — the worker always runs.
1411
1412
  //
1412
1413
  // The JUDGE's key is required only when the close actually judges
@@ -1585,6 +1586,24 @@ async function execute(ctx) {
1585
1586
  err(`WARNING: could not add this run to ~/.config/bareloop/runs.jsonl (${e.message}) — the run continues; the panel's list will be missing this row.`);
1586
1587
  }
1587
1588
 
1589
+ // RUN START REFETCH (PART 1c RF22; moved here 2026-10-19, run mvez8s54: the ~18 s barebrowse fetch ran in main() BEFORE this run was
1590
+ // listed, so Sign & run showed nothing for 18 s). The run is listed FIRST (row above, with its pid and cap = the monthly claim), THEN
1591
+ // fetches: the Run tab reads "fetching the page…" while source.json has no refrozenAt at or after the row. A refusal cannot be a
1592
+ // `released` row (that unlists the run and its words); it SETTLES the claim at $0 and keeps the plain reason for the panel.
1593
+ if (ctx.refreeze === true && ctx.into !== undefined) {
1594
+ const fresh = await refreezeUrlInputs({ into: ctx.into, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
1595
+ if (!fresh.ok) {
1596
+ const words = /did not start/.test(fresh.error) ? fresh.error : `${fresh.error}. The run did not start. Nothing spent.`;
1597
+ err(`${SELECTOR}: ${fresh.error}`);
1598
+ if (claimedRow) {
1599
+ try { appendRunEvent({ runid, type: 'settled', by: runid, reason: 'refused before start', words, spentUsd: 0, spendComplete: true, at: new Date().toISOString() }, { home: claimedRow.home }); } catch { /* the exit stands; the dead pid's claim is closed by the next run */ }
1600
+ claimedRow = null; // settled, not released: a released leg-1 row is unlisted, and the person must see why
1601
+ }
1602
+ throw new ExitSignal(2);
1603
+ }
1604
+ if (fresh.refreshed.length > 0 && typeof fresh.seed === 'string' && fresh.seed) SEED = fresh.seed;
1605
+ }
1606
+
1588
1607
  const git = (/** @type {string[]} */ a) => execFileSync('git', ['-C', wd, ...a], { encoding: 'utf8' }).trim();
1589
1608
  if (dead) {
1590
1609
  // A RESUME is the OPPOSITE case from a cold launch: the halted run's edits are work
@@ -2055,7 +2074,7 @@ async function execute(ctx) {
2055
2074
  const legDraftSpentUsd = typeof jobStartRec?.draftSpentUsd === 'number' && jobStartRec.draftSpentUsd > 0 ? jobStartRec.draftSpentUsd : null;
2056
2075
  const legDraftSpendComplete = legDraftSpentUsd === null ? null : jobStartRec.draftSpendComplete !== false;
2057
2076
  const spendCore = je?.spentUsd == null ? 'UNKNOWN' : `${je.spendComplete === false ? '≥' : ''}$${je.spentUsd.toFixed(4)}`;
2058
- out(`spent ${moneyWithDraft(spendCore, legDraftSpentUsd, spec.budgetUsd, legDraftSpendComplete)}`);
2077
+ out(`spent ${moneyWithDraft(spendCore, legDraftSpentUsd, spec.budgetUsd, legDraftSpendComplete, je?.spentUsd ?? null)}`);
2059
2078
  // FOLDED, exactly like the money line above it: on a resume the cap governs both legs
2060
2079
  // together, and a leg-only wall next to a folded spend is two framings on one cap with
2061
2080
  // no label to tell them apart (F83). The leg stays on the line beside it.
@@ -2517,6 +2536,7 @@ export async function main(argv, deps = {}) {
2517
2536
  /** how a "go edit the spec" hint renders — `jobs/<file>` for `--job`, the
2518
2537
  * spec's own path for `--spec` (it has no `jobs/` home to point at). */
2519
2538
  let SPEC_DESC;
2539
+ let refreeze = false;
2520
2540
  let spec;
2521
2541
 
2522
2542
  if (jobArg !== null) {
@@ -2593,24 +2613,16 @@ export async function main(argv, deps = {}) {
2593
2613
  + 'with exactly one source-*/ sibling.');
2594
2614
  }
2595
2615
  const into = candidates[0];
2596
- let manifestRead = await readSourceManifest(into);
2616
+ const manifestRead = await readSourceManifest(into);
2597
2617
  if (manifestRead.stop !== null) {
2598
2618
  die(`--spec ${specPath}: ${manifestRead.code} — ${manifestRead.stop}`);
2599
2619
  }
2600
2620
  if (!manifestRead.present || !manifestRead.manifest) {
2601
2621
  die(`--spec ${specPath}: ${into} carries no source.json — not a prepared copy this runner can trust`);
2602
2622
  }
2603
- // PART 1c (RF22): a FRESH run of a rubric job with web inputs fetches each address again and makes that text the run's frozen
2604
- // input BEFORE the worker starts (the drafting copy was only for the look). A resume / door / decision keeps the copy its
2605
- // first start froze. A refusal is plain words, $0.
2606
- if (argFlag('resume') === null && argFlag('door') === null && argFlag('decide') === null) {
2607
- const fresh = await refreezeUrlInputs({ into, ...(deps.fetchUrl ? { fetchUrl: deps.fetchUrl } : {}) });
2608
- if (!fresh.ok) die(`--spec ${specPath}: ${fresh.error}`);
2609
- if (fresh.refreshed.length > 0) {
2610
- manifestRead = await readSourceManifest(into);
2611
- if (manifestRead.stop !== null || !manifestRead.manifest) die(`--spec ${specPath}: the run copy's record could not be read after the fresh fetch`);
2612
- }
2613
- }
2623
+ // PART 1c (RF22): a FRESH run with web inputs fetches each address again before the worker starts. That happens in execute(),
2624
+ // AFTER the run is listed (`ctx.refreeze`); a resume / door / decision keeps the copy its first start froze.
2625
+ refreeze = argFlag('resume') === null && argFlag('door') === null && argFlag('decide') === null;
2614
2626
  const seed = manifestRead.manifest.seed;
2615
2627
  if (typeof seed !== 'string' || !seed) {
2616
2628
  die(`--spec ${specPath}: ${into}/source.json carries no seed commit — not a prepared copy this runner can trust`);
@@ -2648,6 +2660,7 @@ export async function main(argv, deps = {}) {
2648
2660
  seed: target.seed,
2649
2661
  spineName: target.spine,
2650
2662
  ...(target.into === undefined ? {} : { into: target.into }),
2663
+ ...(refreeze ? { refreeze: true } : {}),
2651
2664
  model: argFlag('model'),
2652
2665
  providerKey: argFlag('provider'),
2653
2666
  readShim: argFlag('read-shim'),
@@ -10,12 +10,16 @@ export function linesNamedByReds(reds: Red[], declaration: any, lineNums: number
10
10
  * The machine's keep-or-drop of the try-3 questions. Kept only when its line is one the previous try's reds named and its
11
11
  * text is non-blank, at most {@link MAX_QUESTIONS}; every drop comes back with its reason, for the draft's log.
12
12
  * @param {any} raw @param {number[]} failedLines
13
- * @returns {{kept: {line: number, question: string}[], dropped: {reason: string, raw: any}[]}}
13
+ * @returns {{kept: {line: number, question: string, rule?: number, source?: string, ask?: 'reading'|'cant-check', reading?: string}[], dropped: {reason: string, raw: any}[]}}
14
14
  */
15
15
  export function normalizeQuestions(raw: any, failedLines: number[]): {
16
16
  kept: {
17
17
  line: number;
18
18
  question: string;
19
+ rule?: number;
20
+ source?: string;
21
+ ask?: "reading" | "cant-check";
22
+ reading?: string;
19
23
  }[];
20
24
  dropped: {
21
25
  reason: string;
@@ -1093,8 +1097,8 @@ export const REVISE_RED_CAP: 20;
1093
1097
  export const AUTHOR_MAX_TOKENS: 32000;
1094
1098
  /** the one slot the model fills on an injected guard */
1095
1099
  export const FILL_MARKER: "<FILL IN>";
1096
- /** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. */
1097
- export const MAX_QUESTIONS: 2;
1100
+ /** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. PRD v1.107 Part 5b: three (hamr's ruling), shared with the code-forced early asks. */
1101
+ export const MAX_QUESTIONS: 3;
1098
1102
  /**
1099
1103
  * The model-visible words of the optional `questions` property — offered ONLY on the last drafting try and only after the
1100
1104
  * two before it both failed validation (a mechanism, never a wish: the schema carries it on that one call and nowhere
@@ -1680,12 +1684,44 @@ export const PARAM_SCHEMAS: Readonly<{
1680
1684
  description: string;
1681
1685
  };
1682
1686
  unit: {
1683
- type: string;
1684
- minLength: number;
1687
+ anyOf: ({
1688
+ type: string;
1689
+ minLength: number;
1690
+ minItems?: undefined;
1691
+ maxItems?: undefined;
1692
+ uniqueItems?: undefined;
1693
+ items?: undefined;
1694
+ } | {
1695
+ type: string;
1696
+ minItems: number;
1697
+ maxItems: number;
1698
+ uniqueItems: boolean;
1699
+ items: {
1700
+ type: string;
1701
+ minLength: number;
1702
+ };
1703
+ minLength?: undefined;
1704
+ })[];
1685
1705
  description: string;
1686
1706
  };
1687
1707
  currency: {
1688
- enum: string[];
1708
+ anyOf: ({
1709
+ enum: string[];
1710
+ type?: undefined;
1711
+ minItems?: undefined;
1712
+ maxItems?: undefined;
1713
+ uniqueItems?: undefined;
1714
+ items?: undefined;
1715
+ } | {
1716
+ type: string;
1717
+ minItems: number;
1718
+ maxItems: number;
1719
+ uniqueItems: boolean;
1720
+ items: {
1721
+ enum: string[];
1722
+ };
1723
+ enum?: undefined;
1724
+ })[];
1689
1725
  description: string;
1690
1726
  };
1691
1727
  phrase: {
@@ -91,8 +91,8 @@ export function refuseLockedKind(kind: string): Refusal;
91
91
  * authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
92
92
  * signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
93
93
  * ask?: ((step: {kind: string, [k: string]: any}) => Promise<string|null>)|null,
94
- * answerQuestions?: ((o: {questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
95
- * Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>}|null>)|null,
94
+ * answerQuestions?: ((o: {questions: {line: number, question: string, rule?: number, source?: string, ask?: string, reading?: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
95
+ * Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>, left?: {line: number, rule: number}[]}|null>)|null,
96
96
  * confirmGenerate?: Function|null, isRepo?: boolean,
97
97
  * langResult?: {kind: string, candidates?: string[], [k: string]: any}|null}} o
98
98
  * @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
@@ -151,6 +151,10 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
151
151
  questions: {
152
152
  line: number;
153
153
  question: string;
154
+ rule?: number;
155
+ source?: string;
156
+ ask?: string;
157
+ reading?: string;
154
158
  }[];
155
159
  dropped: {
156
160
  reason: string;
@@ -164,6 +168,10 @@ export function authorCloseForJob({ answers, repoPath, lang, verdictType, questi
164
168
  rule?: string;
165
169
  }[];
166
170
  answers: Record<string | number, any>;
171
+ left?: {
172
+ line: number;
173
+ rule: number;
174
+ }[];
167
175
  } | null>) | null;
168
176
  confirmGenerate?: Function | null;
169
177
  isRepo?: boolean;
@@ -118,6 +118,7 @@ export function guardNames(closeDecl: any, verdictType: string): string[];
118
118
  * judgeLoop?: ((o: {system: string}) => any)|null,
119
119
  * judgeModel?: string|null,
120
120
  * onJudgeCost?: ((c: any) => void)|null,
121
+ * onRubricEvidence?: ((e: any) => void)|null,
121
122
  * callBounds?: (() => {timeoutMs?: number, deadlineMs?: number})|null}} [opts]
122
123
  * @returns {Promise<any>}
123
124
  */
@@ -138,6 +139,7 @@ export function runDeclaredStages(stages: any[], redact?: (s: string) => string,
138
139
  }) => any) | null;
139
140
  judgeModel?: string | null;
140
141
  onJudgeCost?: ((c: any) => void) | null;
142
+ onRubricEvidence?: ((e: any) => void) | null;
141
143
  callBounds?: (() => {
142
144
  timeoutMs?: number;
143
145
  deadlineMs?: number;
@@ -187,7 +189,7 @@ export const CLOSE_DECL_FIELDS: readonly string[];
187
189
  /** what a stored calibration set carries, ENUMERATED: the frozen cases and the judge
188
190
  * tier that graded them. Both are inside the spec, so `jobSpecHash` covers both — which
189
191
  * is the whole mechanism behind "a judge-model bump forces a full recalibration"
190
- * (src/judged.js, `JUDGE_MODEL`): the field moves, the hash moves, the signature dies,
192
+ * (src/judged.js, `resolveJudge`): the field moves, the hash moves, the signature dies,
191
193
  * and the re-sign re-runs the gate. Anything else in there is a field nobody reads. */
192
194
  export const CALIBRATION_FIELDS: readonly string[];
193
195
  /**
@@ -3,6 +3,8 @@
3
3
  * - `step-start` {no, id, label, at}
4
4
  * - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
5
5
  * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
6
+ * - `human` {no, source: 'confirm'|'try-3'|'look', question, answer, line?, at}: one question the person was asked and what they answered (the answer is secret-masked
7
+ * before it is written; the row is written only when BOTH exist, so a question nobody answered leaves no row)
6
8
  * - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
7
9
  * (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
8
10
  * `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
@@ -58,12 +60,33 @@ export function sessionDirForRow(row: {
58
60
  home?: string;
59
61
  sessionsRoot?: string;
60
62
  }): string | null;
63
+ /**
64
+ * PRD v1.106 Part 5 Q1: one answered question as the two lines the Audit's drafting table shows under its step. ONE place owns the words.
65
+ * @param {{question: string, answer: string, source?: string, line?: number, at?: string}} h
66
+ * @returns {{question: string, answer: string, source: string, line?: number, at?: string, askedText: string, saidText: string}}
67
+ */
68
+ export function humanRowWords(h: {
69
+ question: string;
70
+ answer: string;
71
+ source?: string;
72
+ line?: number;
73
+ at?: string;
74
+ }): {
75
+ question: string;
76
+ answer: string;
77
+ source: string;
78
+ line?: number;
79
+ at?: string;
80
+ askedText: string;
81
+ saidText: string;
82
+ };
61
83
  /**
62
84
  * Fold `<dir>/draft-log.jsonl` into one record per drafting step, each with its calls. A line that cannot be parsed is
63
85
  * skipped (a torn last line must not hide the rest).
64
86
  * @param {string} dir
65
87
  * @returns {{no: number, id: string, label: string, status: 'running'|'done'|'failed', startedAt: string, endedAt: string|null, wallMs: number|null,
66
- * calls: {label: string, model: string|null, costUsd: number|null, unpricedRounds: number, at: string}[]}[]}
88
+ * calls: {label: string, model: string|null, costUsd: number|null, unpricedRounds: number, at: string}[],
89
+ * humans: ReturnType<typeof humanRowWords>[]}[]}
67
90
  */
68
91
  export function readDraftSteps(dir: string): {
69
92
  no: number;
@@ -80,6 +103,7 @@ export function readDraftSteps(dir: string): {
80
103
  unpricedRounds: number;
81
104
  at: string;
82
105
  }[];
106
+ humans: ReturnType<typeof humanRowWords>[];
83
107
  }[];
84
108
  /**
85
109
  * The DRAFTING PART of a run (hamr 2026-10-06, option A): the first part of the run's views, built from the session's
@@ -109,6 +133,8 @@ export const DRAFT_LOG_FILE: "draft-log.jsonl";
109
133
  * - `step-start` {no, id, label, at}
110
134
  * - `step-end` {no, status: 'done'|'failed', label, at} (a re-opened step writes `step-reopen` {no, at} first)
111
135
  * - `step-label` {no, label, at} (the step reads `label` from now on, status and times untouched: the thinking row's "the model thought anyway" / "can't change it" / "not confirmed")
136
+ * - `human` {no, source: 'confirm'|'try-3'|'look', question, answer, line?, at}: one question the person was asked and what they answered (the answer is secret-masked
137
+ * before it is written; the row is written only when BOTH exist, so a question nobody answered leaves no row)
112
138
  * - `call` {no, step, label, model, thinking, thinkingSupport, thinkingStatus?, costUsd (null stays null), unpricedRounds, at}: one METERED model call
113
139
  * (`thinking` = the tick's mode: off | on; `thinkingSupport` = bare-agent's lookup for the model: true | false | 'unverified';
114
140
  * `thinkingStatus` = bare-agent's own verdict for the call's run: applied | unsupported | ignored | unknown, present only when thinkingMode was sent)
package/types/facts.d.ts CHANGED
@@ -23,6 +23,14 @@ export function factClaims(part: any, results: unknown): {
23
23
  quote: string;
24
24
  values?: Record<string, string>;
25
25
  }[];
26
+ /**
27
+ * One amount of a money fact in the fact's OWN signed forms, never a hardcoded sign: `symbolFirst` puts the currency sign before the amount
28
+ * ("$144") when the fact has one, else the unit after it ("144 US dollars"); not `symbolFirst` (a rule's bound) prefers the unit.
29
+ * @param {any} f a money fact @param {string|number} n @param {boolean} symbolFirst @returns {string}
30
+ */
31
+ export function moneyAs(f: any, n: string | number, symbolFirst: boolean): string;
32
+ /** @param {any} f a fact with a rule @returns {string} the rule in words, e.g. "at most 450 US dollars" */
33
+ export function ruleWords(f: any): string;
26
34
  /**
27
35
  * One fact as a plain row, written by code from the signed fields:
28
36
  * `fact · price — money, at most 450 US dollars, after "Price", quoted from "Flights"`.
@@ -69,3 +77,10 @@ export const FACT_OPS: readonly string[];
69
77
  export const MAX_FACTS: 10;
70
78
  /** every field a part of class `fact` may carry (the shared ones are checked by rubricStepShapeReds) */
71
79
  export const FACT_PART_FIELDS: readonly string[];
80
+ /** the limits bareguard 0.24 puts on a money fact's lists (ruling #65), enforced here first so the red is ours and plain */
81
+ export const MAX_CURRENCIES: 3;
82
+ export const MAX_UNITS: 10;
83
+ export const MAX_UNIT_CHARS: 100;
84
+ export function asList(v: unknown): string[];
85
+ export function moneyForms(f: any): string[];
86
+ export function boundOk(shape: string, v: unknown): boolean;
@@ -9,16 +9,35 @@ export function countExact(source: string, words: string): number;
9
9
  * @param {string} source @param {string} words @returns {number}
10
10
  */
11
11
  export function countPhrase(source: string, words: string): number;
12
+ /**
13
+ * What the page-word map (src/pagemap.js) KEPT and DROPPED for one input: the sets the second proof reads. A look from the old search-and-read
14
+ * path has no `dropped` list, so it gives null and the label is proved against the code-made list as before.
15
+ * @param {{facts?: any[], dropped?: (import('./pagerules.js').Dropped & {source?: string})[]}|null|undefined} look @param {string} source
16
+ * @returns {{labels: Set<string>, units: Set<string>, currencies: Set<string>, words: Set<string>, dropped: Map<string, import('./pagerules.js').Dropped>}|null}
17
+ */
18
+ export function keptSetOf(look: {
19
+ facts?: any[];
20
+ dropped?: (import("./pagerules.js").Dropped & {
21
+ source?: string;
22
+ })[];
23
+ } | null | undefined, source: string): {
24
+ labels: Set<string>;
25
+ units: Set<string>;
26
+ currencies: Set<string>;
27
+ words: Set<string>;
28
+ dropped: Map<string, import("./pagerules.js").Dropped>;
29
+ } | null;
12
30
  /**
13
31
  * The reds for ONE fact part against its frozen source text.
14
- * @param {{part: any, text: string, line: number|string, scan?: import('./labelscan.js').LabelScan}} o
32
+ * @param {{part: any, text: string, line: number|string, scan?: import('./labelscan.js').LabelScan, kept?: ReturnType<typeof keptSetOf>}} o
15
33
  * @returns {{code: string, detail: string}[]}
16
34
  */
17
- export function checkFactWords({ part, text, line, scan }: {
35
+ export function checkFactWords({ part, text, line, scan, kept }: {
18
36
  part: any;
19
37
  text: string;
20
38
  line: number | string;
21
39
  scan?: import("./labelscan.js").LabelScan;
40
+ kept?: ReturnType<typeof keptSetOf>;
22
41
  }): {
23
42
  code: string;
24
43
  detail: string;
@@ -27,9 +46,13 @@ export function checkFactWords({ part, text, line, scan }: {
27
46
  * The reds for every fact part of a declaration. A fact part whose input text is not at hand is a red: a check whose words were never
28
47
  * proved against the source is never signed.
29
48
  * @param {any} declaration @param {Record<string, string>|null} texts label -> frozen text
49
+ * @param {{facts?: any[], dropped?: any[]}|null} [look] the page-word map's report: what it kept and dropped (null = no map, the code-made list is the proof)
30
50
  * @returns {{code: string, path: string, detail: string}[]}
31
51
  */
32
- export function checkDeclarationWords(declaration: any, texts: Record<string, string> | null): {
52
+ export function checkDeclarationWords(declaration: any, texts: Record<string, string> | null, look?: {
53
+ facts?: any[];
54
+ dropped?: any[];
55
+ } | null): {
33
56
  code: string;
34
57
  path: string;
35
58
  detail: string;
@@ -148,6 +148,12 @@ export function lookPrompt({ jobLines, labels, lists }: {
148
148
  labels: string[];
149
149
  lists: Record<string, string>;
150
150
  }): string;
151
+ /**
152
+ * A fact the page-word map kept (src/pagerules.js `KeptFact`), as the look report carries it: a phrase fact holds its alternative
153
+ * phrases; any other holds its labels (the first is the primary), and a money fact its units and currencies.
154
+ * @param {import('./pagerules.js').KeptFact} k @param {string} source @returns {LookFact}
155
+ */
156
+ export function lookFactOf(k: import("./pagerules.js").KeptFact, source: string): LookFact;
151
157
  /**
152
158
  * Turn the AI's raw report into a VERIFIED one, by code, at $0. A fact is carried only when: its shape is a real shape, its source is
153
159
  * a real input, and its label is ON THE LIST for that input and shape (an id is resolved to its exact words). Anything else is moved
@@ -207,6 +213,9 @@ export const LOOK_REPORT_TOOL: "report_look";
207
213
  /** how many hits one search returns at most, and how long a snippet or a read line is */
208
214
  export const LOOK_MAX_HITS: 40;
209
215
  export { LABEL_SHAPES };
216
+ /**
217
+ * `words` / `labels` / `unit` / `currency`: what the page-word map kept (idea A core)
218
+ */
210
219
  export type LookFact = {
211
220
  line: number;
212
221
  name: string;
@@ -220,6 +229,11 @@ export type LookFact = {
220
229
  rule?: any;
221
230
  aiProposal?: boolean;
222
231
  forRule?: number;
232
+ reading?: import("./pagerules.js").Reading;
233
+ words?: string[];
234
+ labels?: string[];
235
+ unit?: string[];
236
+ currency?: string[];
223
237
  };
224
238
  export type LookNotFound = {
225
239
  line: number;
@@ -234,5 +248,6 @@ export type LookReport = {
234
248
  text?: string;
235
249
  }[];
236
250
  notFound: LookNotFound[];
251
+ dropped?: import("./pagerules.js").Dropped[];
237
252
  };
238
253
  import { LABEL_SHAPES } from './labelscan.js';
package/types/job.d.ts CHANGED
@@ -99,7 +99,8 @@ export function jobSpecHash(job: object): string;
99
99
  * share a workflow key iff they ran the same goal, checks, guardrails and close — whatever source they ran on, wherever they wrote, whatever caps they had.
100
100
  * It is NOT a signature: {@link jobSpecHash} (which covers the caps and the fence) stays what the person signs, and
101
101
  * a reuse lands on a NEW jobSpecHash with the SAME workflowKey. Same never-throws contract as `jobSpecHash`, and
102
- * the same resolved-`tools` form, so an omitted-`tools` spec and its spelled-out menu share a key.
102
+ * the same resolved-`tools` form, so an omitted-`tools` spec and its spelled-out menu share a key. A soft-green spec keys on its
103
+ * RESOLVED judge identity (an absent `judge` is its worker's), so pinning the default judge never changes the key.
103
104
  * @param {object} job
104
105
  * @returns {string} sha256 hex
105
106
  */
@@ -277,6 +278,12 @@ export const CONDITION_KEYS: readonly string[];
277
278
  * reuse (so what the key ignores is exactly what the person may edit, never two spellings).
278
279
  */
279
280
  export const REUSE_OPEN_SPEC_FIELDS: readonly string[];
281
+ /**
282
+ * The signed spec's schedule fields: {@link workflowKey} ignores them too, so a scheduled spec keeps the workflow
283
+ * key of the green run it was scheduled from (else Reuse would refuse every scheduled spec). They stay inside
284
+ * {@link jobSpecHash}. Kept apart from {@link REUSE_OPEN_SPEC_FIELDS}, which names the Reuse boxes.
285
+ */
286
+ export const SCHEDULE_SPEC_FIELDS: readonly string[];
280
287
  /**
281
288
  * The worker-identity fields {@link workflowKey} ALSO ignores (hamr 2026-10-04: Model is open on a reuse; the Name
282
289
  * chosen in Settings carries its provider and baseUrl with it). They stay inside {@link jobSpecHash}, so what the person
package/types/kinds.d.ts CHANGED
@@ -645,6 +645,11 @@ export type Ctx = {
645
645
  timeoutMs?: number;
646
646
  deadlineMs?: number;
647
647
  };
648
+ onRubricEvidence?: (e: {
649
+ step: number;
650
+ evidence: any[];
651
+ evidenceTotal?: number;
652
+ }) => void;
648
653
  onStage?: (row: {
649
654
  stage: string | null;
650
655
  kind: string | null;
@@ -0,0 +1,26 @@
1
+ /** @param {Dropped} d @returns {string} the plain sentence for one drop */
2
+ export function dropSentence(d: Dropped): string;
3
+ /**
4
+ * One row per fact that lost something: the sentences (one per dropped word, then the fact's own), and whether the whole fact went.
5
+ * @param {Dropped[]|undefined|null} dropped
6
+ * @returns {{line: number, rule?: number, fact: string, full: boolean, sentences: string[]}[]}
7
+ */
8
+ export function dropRows(dropped: Dropped[] | undefined | null): {
9
+ line: number;
10
+ rule?: number;
11
+ fact: string;
12
+ full: boolean;
13
+ sentences: string[];
14
+ }[];
15
+ /**
16
+ * A row as one Not-checked line, in the person's own words: `line N: "<the ~ rule as written, or the fact's name>" — <sentences>`.
17
+ * @param {{line: number, rule?: number, fact: string, sentences: string[]}} row @param {any[]|null|undefined} jobLines @param {(l: any) => string[]} rulesOf
18
+ * @returns {string}
19
+ */
20
+ export function dropLine(row: {
21
+ line: number;
22
+ rule?: number;
23
+ fact: string;
24
+ sentences: string[];
25
+ }, jobLines: any[] | null | undefined, rulesOf: (l: any) => string[]): string;
26
+ export type Dropped = import("./pagerules.js").Dropped;