bareloop 0.43.0 → 0.45.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 (64) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/bareloop.context.md +53 -13
  3. package/package.json +2 -2
  4. package/src/authorflow.js +27 -12
  5. package/src/authoring.js +1 -1
  6. package/src/authorjob.js +52 -16
  7. package/src/authorreadout.js +12 -6
  8. package/src/authorrun.js +8 -2
  9. package/src/declaredclose.js +4 -1
  10. package/src/draftspend.js +20 -2
  11. package/src/facts.js +54 -9
  12. package/src/factwords.js +58 -6
  13. package/src/fitcheck.js +166 -0
  14. package/src/inputlook.js +38 -3
  15. package/src/job.js +1 -1
  16. package/src/kinds.js +5 -0
  17. package/src/pagedropwords.js +110 -0
  18. package/src/pagedry.js +125 -0
  19. package/src/pagemap.js +269 -0
  20. package/src/pagerules.js +318 -0
  21. package/src/panel/authorroutes.js +34 -30
  22. package/src/panel/authorsession.js +66 -11
  23. package/src/panel/index.html +469 -65
  24. package/src/panel/jobplan.js +14 -7
  25. package/src/panel/runroutes.js +46 -22
  26. package/src/panel/server.js +792 -28
  27. package/src/panel/status.js +33 -0
  28. package/src/panelmoney.js +29 -0
  29. package/src/planrun.js +53 -4
  30. package/src/promptregisters.js +8 -0
  31. package/src/replay.js +118 -7
  32. package/src/rubricstep.js +157 -15
  33. package/src/runlist.js +2 -1
  34. package/src/schedulewords.js +6 -3
  35. package/src/source.js +18 -0
  36. package/src/thinking.js +63 -2
  37. package/src/userrun.js +28 -14
  38. package/types/authorflow.d.ts +42 -6
  39. package/types/authorjob.d.ts +20 -4
  40. package/types/declaredclose.d.ts +2 -0
  41. package/types/draftspend.d.ts +27 -1
  42. package/types/facts.d.ts +15 -0
  43. package/types/factwords.d.ts +26 -3
  44. package/types/fitcheck.d.ts +113 -0
  45. package/types/inputlook.d.ts +15 -0
  46. package/types/kinds.d.ts +5 -0
  47. package/types/pagedropwords.d.ts +39 -0
  48. package/types/pagedry.d.ts +37 -0
  49. package/types/pagemap.d.ts +154 -0
  50. package/types/pagerules.d.ts +86 -0
  51. package/types/panel/authorroutes.d.ts +17 -14
  52. package/types/panel/authorsession.d.ts +4 -2
  53. package/types/panel/jobplan.d.ts +5 -2
  54. package/types/panel/runroutes.d.ts +2 -2
  55. package/types/panel/server.d.ts +156 -2
  56. package/types/panel/status.d.ts +51 -0
  57. package/types/panelmoney.d.ts +5 -0
  58. package/types/replay.d.ts +21 -1
  59. package/types/rubricstep.d.ts +62 -1
  60. package/types/runlist.d.ts +4 -1
  61. package/types/schedulewords.d.ts +4 -2
  62. package/types/source.d.ts +11 -0
  63. package/types/thinking.d.ts +39 -0
  64. package/types/userrun.d.ts +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,66 @@ All notable changes to bareloop are documented here. Format:
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html). Pre-1.0: **minor** = a ladder rung or
6
6
  feature lands, **patch** = docs, fixes, scaffolding.
7
7
 
8
+ ## [0.45.0] - 2026-10-22
9
+
10
+ ### Added
11
+
12
+ - **Phrase facts get the dry read.** A kept phrase fact (for example "destination city" = "Rome") is read against the page's result lines (the lines the job's kept price facts read), by the same checker the run uses. A phrase found only outside the results (such as a "Track prices from Madrid to Rome" switch) is dropped whole as `phrase-no-result` and shows under Not checked: `"Rome" is on the page but in none of its results, so it is not checked.` A job with no kept price fact has no known result lines, so its phrase facts are left as they were.
13
+
14
+ - **An S1 copy of a signed fact is a read note.** When the AI proposes a second fact that would read the same value as a fact that is still signed, the note now shows under "How the page was read" instead of "Not checked" (the guardrail is checked by the other fact). With the other fact not signed it stays Not checked. The signed fact names reach the split from the signed spec (older specs too) and from the look at the confirm turn.
15
+
16
+ - **A fit check before the look (F243, `src/fitcheck.js`).** For a rubric job in the panel, one AI call splits each job line into the short parts it asks for and quotes the saved page for each; code (not the AI) decides a part is found only when its quote is really on the page. If every part is found nothing changes; if some are not, the person gets one question ("I found X of Y of your asks on this page. Not found: ... Type go on to keep drafting, or anything else to stop...") before the look spends. "go on" continues; anything else stops with "Stopped before drafting: the page does not show everything the job asks for: ...". The answer changes nothing in the signed job. If the check cannot finish it fails open and drafting goes on. Its spend is booked as `fit-check` in the drafting part and its result is one `fit-check` row in the drafting log.
17
+ A bare number in an ask (a count like the 3 in "list 3 flights") is ignored: not counted, not asked about, kept in the drafting log row flagged `ignored: number`. The stop line reads "...the job asks for: wifi (line 1), meal (line 1). Change the job...".
18
+
19
+ ### Fixed
20
+
21
+ - **Code's notes about written forms it did not use no longer sit under Not checked.** A second label, unit or currency dropped from a fact that is still checked (listed twice, "does not show the same values", not on the page) now shows in its own fold, "How the page was read · N", right after Not checked and not counted in it; a whole fact left out stays under Not checked. Same wording per note, on the plan card, the Job tab, the Result tab and the CLI confirm menu (run mvij7kuk).
22
+
23
+ - **A currency word in the wrong box no longer kills the price check (F241).** Drafting now sorts the AI's currency forms by code: a sign ($, €, £) is the currency, any other form ("US dollars") is a unit, whichever box the AI put it in; `currency-not-symbol` is gone. Then a $0 dry read grades an honest claim on each result line with the run's own checker, and a price fact that reads no result line is left under Not checked (`reads-no-result`) instead of being signed to fail at the run.
24
+ - **A number the AI made up no longer passes as the person's own.** The test for "is this limit in the person's words" matched digits inside other words, so 5, 15 and 50 counted as said for "under $150 ... between 05:00 and 23:59". It now reads times, dates, amounts and bare numbers as whole items, and a limit counts as written only if it equals one of them (`boundInWords`, F242).
25
+
26
+ ## [0.44.0] - 2026-10-20
27
+
28
+ ### Added
29
+
30
+ - **bareguard ^0.25.1.** Gap words now JSON-escape the extra-keys keys and end with " and N more" when more items exist than are shown; evidence and `cited` results are unchanged, and bareloop's own behaviour is unchanged.
31
+ - **Result tab shows a money fact as the page wrote it.** The price box reads the value with its currency or unit exactly as the check located them on the page ("144 US dollars"), replacing the interim "$144"; with no evidence (an old run) the fact's own spelling stays.
32
+ - **Result tab marks the words each check read.** Each result's proof fold marks, in the page's own quote, every word the check located (the anchors, the values, the unit and the phrases), using the offsets bareguard returned and the run's `rubric-evidence` record, never a search in the panel. A membership token that is not the result's own is not marked. Escaped throughout; an old run or a quote the check did not read shows the plain quote as before.
33
+ - **`rubric-evidence` spine record (report-only).** A rubric step's `cited` checks now save what they read (bareguard 0.25 `full.evidence`: the quote and the spans of the words each check located) as `{step, iteration?, evidence}`, masked like every spine write; nothing in the verdict, governor or money paths reads it. The Result tab's highlights and money-as-written build on it.
34
+ - **bareguard `^0.25.0` (ruling #66 evidence spans; dependency bump only, `full.evidence` not used yet).** Checked on the saved real Google Flights page: the dump is byte-identical to the 0.24-era evidence and the page check reads 27 blocks, 0 bad.
35
+
36
+ - **Result tab links to the page each result read.** One web page read shows one link at the top of the Result tab; two or more pages show each result box's own page link, with "The page may have changed since this run." beside it. The address is the run's own `source.json` input url (http and https only, opens in a new tab with `rel="noopener noreferrer"`); file and repo inputs get no link. `/result` now carries `pages`, `pageNote` and each line's `page`.
37
+ - **The page-word map replaces the search-and-read look (idea A core, items 1-6; `src/pagemap.js`, `src/pagerules.js`, ITEM33-BUILD.md "Addendum 2026-10-18").** For a rubric job on a page, ONE AI call per page input names the page's own words for the job; plain code keeps only words found on the page (found-on-page), drops a label written 2+ times inside one page line (N2), keeps a second label only if it reads the same values as the first (R1), turns a marker the page writes after the number ("EUR 490 total") into a unit and drops a label that reads no number (U1), compares unit and currency forms by the numbers they read (R1L), and refuses a fact named for a position or two facts with the same shape and label (S1). The AI never grades. A malformed or truncated answer is retried once, then a plain stop; the scrubbed answer is kept in `draft-log.jsonl`.
38
+ - **Currency and unit lists on a money fact (idea A core, item 7; bareguard ruling #65).** A money fact may carry both `unit` and `currency`, each one value or a list (at most 3 currencies and 10 units). Old single-value specs keep their bytes and hash.
39
+ - **No silent drop (idea A core, item 8; F238).** Every word, fact or `~` rule the page rules drop appears as Not checked with a plain reason from one table (`src/pagedropwords.js`), on the confirm card and on the signed plan; a dropped word cannot be signed by the drafter (`rubric-fact-word-dropped`).
40
+
41
+ - **Part 5 of PRD v1.106: each try, its checks and the files judged (Audit), two Run summary rows, a line on each step card.** A plan step's Audit now lists every try with its checks in the order they ran (result, a count or the folded reasons, the job line each serves), the first reason and `(+N more)` on the try line, one Round table per step with a Try column and round numbers from 1 (20 rows a page; the flat view keeps run-wide numbers and says so), a `[model]` marker and a tools tally; a try the wall cut off before its checks ran is listed as such. A run that reached its review door gets a door part (`[human · ask]`). The Run summary gains `calls:` and `checks:` (counts of what ran, never a fraction), and each step card a `N tries · last try: ...` line. New spine record `output-judged {step, iteration, path, bytes, sha256}` (never content) written before each `exit-eval`. Fixes the false "not run" on every check of a multi-try step (D1).
42
+ - **Answered questions are recorded and shown (Part 5 Q1).** Every question the person answers (the confirm card's, the early look ask, the last-try ask) is a `human` row in the session's `draft-log.jsonl` (answer secret-masked), drawn under its step in the Audit's drafting table as `you were asked` / `you said`.
43
+ - **Code forces the early ask for a `~` rule the page map could not tie (Part 5 Q3, T2).** `askableFromLook` (src/pagemap.js) asks, before any paid drafting, one question per such rule (cap 3 since Part 5b); the answer becomes a `~` rule, or "drop it" puts the rule on the look's Not checked list.
44
+ - **The look reads, code asks (PRD v1.107 Part 5b).** The page map may carry a `reading` (a limit such as `between 06:00 and 21:00`) on a time or money fact; when its bound is not in the person's own words, code asks `I read it as: ... Agree?` before any paid drafting (`agree` writes the reading as a `~` rule in plain words, so the fact signs plain; a typed answer is the rule instead), and a rule the page cannot check asks `Drop it?`. The cap is 3 asks (`MAX_QUESTIONS`); more stops the draft with a code-written list of every ask and its reading, at no extra cost. Replaces the unbuilt Part 5 T1.
45
+ - **"fetching the page…" after Sign & run (Part 5 Q2).** The progress list shows the step while the run's own refetch of a web input is running and settles it when it lands.
46
+
47
+ - **The Result tab (PRD v1.107 Part 6, rubric jobs only).** A fourth right tab after Job, shown only when the run's check type is rubric and its signed close has a `rubric-step` stage. Per job line: the line and its `~` rules, then for a facts line one box per result in the delivered step file, with one small box per signed fact name (each value as the quote writes it, `5 of 5 ✓` when the run's own `check-run` for that line was satisfied and the run ended green; never a ✓ otherwise), a `proof - the page's own words` fold with the quote (the words each check read are marked, see the highlight line above), the rules in `factSentence` words, the Not checked list (the Job tab's own, `planFromSpec(spec).notChecked`), the door (`Accept` / `Rerun with my words` / `Pause`, only while waiting), `saved to:` and `Download <file>`. The Run tab says `the result is on the Result tab` while the door is open. New: `GET /api/runs/:runid/result`, `GET /api/runs/:runid/download?i=<n>` (read only paths in the run's own `destination-written` records and re-check their sha256; a changed or missing file says `the saved file changed after the run` / `the saved file is gone`), `POST /api/runs/:runid/rerun {text}` and `POST /api/runs/:runid/pause` (the engine's own `run-u --door --decide`, like Accept; empty rerun words refused in the page and the route, the engine refuses them too). The run detail gains `hasResult` and `delivered`.
48
+ - **Result tab fixes (PRD v1.107 Part 6).** A red run now shows the rows of the last file its check judged (the run's own `output-judged` record, resolved in the run's own tree and sha256-checked: `the file changed after the check` / `the file is gone`; no ✓ ever, the check's gap words sit under the table; an old run with no record still says no result was saved). `(you confirmed)` marks a fact whose limit came from a Part 5b `agree` answer (read from the drafting log's `human` rows; shown only when the written `~` rule and the signed fact match; nothing when the log is gone). Money cells use the fact's own signed currency or unit (`$144` / `144 US dollars`, `moneyAs` in `src/facts.js`, shared with the rule words). Not checked items tied to a job line by the signed plan show under that line; the rest stay at the bottom (in total the Job tab's items). The tab now has the signed look (variation B, `design/result-tab-mock-v2.html`): Result / Line N / Your call / Saved cards in the panel's own `.job-card` boxes (no table), one box per result with one small box per fact, the line numbered `1. text`, Not checked as lists, and a gone or changed delivered file said once per file in Saved (the line falls back to the rows of the file the check read, else `no saved result to show`).
49
+
50
+ ### Fixed
51
+
52
+ - **A currency sign listed in both unit and currency is used once, as the currency.** When the AI listed `$` under both `unit` and `currency` (fwd F71 drafts), the unit copy was checked as a phrase and the person read `"$" is not written on the page.` (false). The unit copy is now dropped with "it is used once, as the currency". A sign listed only under unit is unchanged.
53
+ - **A word the AI listed as both unit and currency no longer reads as "does not show the same amounts as" itself.** On a Google Flights page the page-word map listed `US dollars` under both `unit` and `currency` of the price; R1L compared the currency form with the same word and Not checked showed a nonsense sentence. The word is now never compared with itself: the unit (listed first) stays and the line says it was listed twice and is used once. The kept check ("144 US dollars") is unchanged.
54
+ - **Reuse of a rubric job on a URL or file is no longer refused at $0 (run mvez8s54).** Reuse workflow -> Sign & run (and a scheduled fire) proved Inputs line 1 as a repo and refused a URL with "the repo must be an absolute path". `startSession` now decides rubric-on-files from the reused signed spec's verdict type, in one place for the click and the schedule; a repo job still proves line 1.
55
+
56
+ ### Changed
57
+
58
+ - **A fresh run lists itself before it fetches the page (run mvez8s54).** The run-start refetch of a web input (about 18 s on a flights page) used to finish before the run appeared in the Runs list, so Sign & run looked dead and the run then showed up already 10 s old. The run now lists its row first (pid and cap, the monthly claim), then fetches; the Run tab says "fetching the page… the run has not started yet" meanwhile (words from the server). A refused page settles the row at $0 (`failed`, plain words, monthly room freed) instead of leaving no trace. A resume, door or decision does not refetch, as before.
59
+ - **Panel money keeps its decimals under a cent (PRD Part 6 rulings 2026-10-19, (3)).** An amount under $0.01 now reads with 4 decimals ("$0.0054 run + $0.0035 drafting = $0.0089 of $1.00") instead of "<$0.01"; $0.01 and up stay 2 decimals, zero stays "$0.00", "at least" floors keep "at least". One owner in Node, `src/panelmoney.js` (`panelMoney2`, re-exported by `src/panel/authorroutes.js`), replaces the second Node spelling in `src/authorreadout.js` and a raw `toFixed(2)` in the resume-cap refusal; the page keeps one browser copy (`panelMoney`) pinned to the Node one by a parity test. The CLI's 4-decimal money and the caps' display are unchanged.
60
+
61
+ - **Money reads `run + drafting = total` (Part 5 Q4).** `$0.0054 run + $0.0035 drafting = $0.0089 of $1.00` in the run.log and the panel (from a cent up 2 decimals, under a cent 4 decimals, as the line above); a floor keeps `at least` on all three parts.
62
+ - **Thinking on retries, for every job (PRD v1.106 Part 1; F230-F233).** The worker's per-call thinking mode is now decided in code from the phase and attempt (`workerThinkingMode`, `src/thinking.js`): planner (draft, redraft, replan) on, run scout off, step attempt 1 off, step attempt 2 and later on, close-fix on. The job's "Thinking on" tick makes every worker call on (as it already did for drafting and the rubric judge). It is sent as bare-agent's per-call `thinkingMode` through `thinkingCallOptions` (nothing is sent to a model that cannot switch). New spine records: `thinking-decision {phase, iteration, mode}` before every worker call and `thinking-status {phase, iteration, mode, thinkingStatus}` after it. A step or fix iteration that is not a whole number throws. Measured on DeepSeek deepseek-flash only; on the short changelog job it was slower (1.76 vs 1.46 min), by hamr's one-rule ruling. Budgets, caps and the close are untouched.
63
+ - **The Runs list searches Scheduled, plus a `◷` chip (PRD v1.106 Part 2; fwdloop M11 am17-am20).** Scheduled now sits BELOW the search box (only never-filtered Running now and Needs you stay above it). The one search box narrows Scheduled cards by job name, along with Jobs; a `run-N` or date word narrows Jobs only, and the result and time chips never touch Scheduled. The new `◷` chip (`scheduled jobs only`) shows only Scheduled: Jobs and the quiet fold hide, the count line reads `showing N scheduled job(s)`, and with no match the list says `no scheduled job matches`. The faint line reads `search also finds Scheduled; ◷ shows only Scheduled`. Page only; `GET /api/runs` is unchanged.
64
+ - **Thinking shown in the Audit tab and the Job tab (PRD v1.106 Part 3).** Each Audit attempt line says the thinking its call was sent (`attempt 2 [✗] · thinking on`), and a part with one call (plan, scout, replan, a one-attempt step) says it on its header line, read from the run's `thinking-decision` / `thinking-status` records in drafting's own words (`thinkingRowText`). The Job tab gets a `Thinking` row (`on retries`, or `always on`), always shown, on one row with Model like the caps; a run from before those records reads `—` there (never a guess from the job's tick). A run from before those records shows no thinking word in the Audit tab. `GET /api/runs/:runid` parts and attempts carry `thinking`; `GET /api/runs/:runid/job` carries `thinking`.
65
+ - **The Job tab's AI's summary shows only the sentence the person confirmed (PRD v1.106).** A rubric job's signed goal is the whole worker brief; the Job tab used to show all of it. It now shows the one confirmed sentence (`confirmedGoalOf`, `src/rubricstep.js`), or no summary row when the brief carries none; a code job is unchanged.
66
+ - **The Audit schedule header matches the other part headers (PRD v1.106 Part 4).** `▾ schedule [✗] when a new file arrives ...` is now `▾ [✗] failed schedule · when a new file arrives ...`: sign, word, name, then the `·` parts, with the sign and word both from the status table (`scheduleSection` gains `word`).
67
+
8
68
  ## [0.43.0] - 2026-10-16
9
69
 
10
70
  ### Added
@@ -64,7 +64,7 @@ inherited rule carries the green that minted it and the contrast that attributed
64
64
  back to a default. The request-key spelling is the ROW's tick, never a model-id table: a server that ignores `max_completion_tokens` (F149) does not bind
65
65
  the output cap until the "old max_tokens" box is ticked, and an output cap that does not bind is a money hazard, so tick it on that row before a paid run
66
66
  (`makeProvider(shape, { legacyMaxTokens })`, forwarded for `openai-api` only). The judge's calls carry the per-call `thinkingMode` the job's signed "Thinking on" tick asks for
67
- (`src/thinking.js`, applied by `defaultJudgeLoop`; F192: unticked = off, because a model's default thinking can eat the 4000-token locate budget); workers are untouched. A plan step's
67
+ (`src/thinking.js`, applied by `defaultJudgeLoop`; F192: unticked = off, because a model's default thinking can eat the 4000-token locate budget); the WORKER's per-call mode is decided in code from the phase and attempt (see "Thinking on" below). A plan step's
68
68
  `model: "sonnet"` is a documented schema token meaning "this job's worker model" (`STEP_MODELS`), not a model id. `gemini-api` (PRD item 31.3) is
69
69
  **ADMITTED-PENDING-PROBE**: it has ZERO runs, and the probe rule is not waived for it —
70
70
  `PROBE_STATUS` (`src/providers.js`) is the machine-readable form of that debt, and
@@ -158,7 +158,7 @@ minting claim, or the shell-owned retry cap — all unknown-field reds.
158
158
  | `maxWallMs` | optional integer ms `>= MIN_WALL_MS` (one close timeout) | the run's wall clock. **NO DEFAULT, by ruling** — absent means time-unbounded *by explicit operator choice*, never by fallback (F45: a defaulted cap is a silent second ceiling). Enforcement is a BETWEEN-ROUND deadline, so the honest worst case is `maxWallMs + closeStages × closeTimeoutMs` — every stage of a staged close gets the FULL timeout — and all three numbers are reported (`loop.stop()` cannot cut an in-flight call — F61 measured 500ms→4,018ms). The `MIN_WALL_MS` floor is a ONE-stage, un-autoset-default number, so a spec with many stages OR a raised/autoset `closeTimeoutMs` can validate while its overshoot dwarfs its cap: `runPlan` adds a RUN-START check (PRD item 27/M3) that refuses `wall-under-close-timeout` at $0 once the EFFECTIVE per-stage ceiling is known (autoset or signed), rather than a silent clamp. Operator-only, tighten-only; adding or changing it changes the spec hash |
159
159
  | `closeTimeoutMs` | optional integer ms `>= CLOSE_TIMEOUT_FLOOR_MS` (120,000 — hamr's arbiter floor, 2026-09-07) | PRD item 27/M3's SIGNED override for the close's own per-stage wall-clock cap. Absent (the normal case): the run autosets it from a $0 seed timing pass — `max(FLOOR, K × slowest measured stage)`, `K = 5` (hamr, 2026-09-07: "2 and 5 are fine"). Present: the operator's own number wins outright and may legally sit ABOVE *or below* the autoset estimate (the rates-passthrough shape, F113 — never tighten-only). Arbiter territory exactly like `budgetUsd`/`maxWallMs`/`close[].sha256`: the authoring pipeline's `assembleSpec` refuses a draft carrying it |
160
160
  | `model` | optional non-empty string, the exact model id of a line ticked in Settings > Providers | the WORKER's model. A job carries its own model (P8: bareloop has no default); a spec signed before that (no `model`, perhaps no `baseUrl`) runs on the line `--model <id>` names (plus `--provider <KEY>` when two keys tick it) — the signed hash is untouched. Present, it wins over a `--model` flag outright, and a flag naming a different id is refused (`resolveWorkerModel`, `src/job.js`) rather than silently overridden. Part of the signed hash like every other field. Worker only — the JUDGE's model comes from the `judge` row below, which DEFAULTS to this same worker model |
161
- | `thinking` | optional boolean | the job's "Thinking on" tick (P8): absent = off, and an old spec keeps the hash it always had; present it is signed like `model`. Covers DRAFTING and the rubric JUDGE only — the worker is never handed a thinking option (see "Thinking on" below) |
161
+ | `thinking` | optional boolean | the job's "Thinking on" tick (P8): absent = off, and an old spec keeps the hash it always had; present it is signed like `model`. Covers DRAFTING and the rubric JUDGE, and makes every WORKER call think (see "Thinking on" below) |
162
162
  | `judge` | optional `{provider, model}`, both non-empty; `provider` from the `PROVIDERS` menu | the signed JUDGE override (PRD item 32). Absent, the judge resolves to this job's own `provider`/`model` (`resolveJudge`) — which is what lets a non-Anthropic job judge without a second account. Present, it pins a judge deliberately different from the worker (an independent reader). Both halves are required together: a judge with a provider and no model is an identity nobody signed. Part of the signed hash, so changing it kills the signature and forces recalibration. Unknown nested keys red — no smuggling level exists in a signed spec. The drafting agent cannot express it: naming your own examiner is arbiter territory |
163
163
  | `writeScope` | array of contained globs | the operator's outer fence; the plan's own scopes must fit inside it, same containment code |
164
164
  | `steps` | RETIRED | operator-authored `steps[]` was deleted (PRD v1.32); a spec carrying it reds `shape-retired:steps` by name rather than half-running |
@@ -636,7 +636,7 @@ whose EVERY structure attempt was cut off at the output cap also counts as faile
636
636
  every job line counts), while a try cut off once but finished by a later attempt is judged by that finished attempt (the ladder carries
637
637
  on to try 3 after an all-cut-off try 2 only when that gate can open, i.e. try 1 failed too);
638
638
  the machine keeps at most
639
- `MAX_QUESTIONS` (2) — each on a job line the previous try's reds named — and logs the rest in `iterations[].droppedQuestions`;
639
+ `MAX_QUESTIONS` (3) — each on a job line the previous try's reds named — and logs the rest in `iterations[].droppedQuestions`;
640
640
  the drafter is never told a number. It then stops `questions-open` (`questions: [{line, question}]`, nothing validated).
641
641
  `authorCloseForJob({ answerQuestions })` is the seam that asks the person (required answer, no skip), takes back the job lines
642
642
  with each answer added word for word as a `~` rule, and runs the draft once more over the same survey, listing and confirmed
@@ -646,7 +646,7 @@ are asked after two failed drafts, about a job line.
646
646
 
647
647
  **Thinking on (per job; hamr 2026-10-11).** The New job card has a "Thinking on" tick under Model, unticked by default, saved in the SIGNED job as the optional
648
648
  boolean `thinking` (absent = off; an old spec and any unticked job keep the hash they would have had; a ticked job's `jobSpecHash` and `workflowKey` differ). It
649
- covers DRAFTING (scout, confirm, author, revise, structure retries) AND the rubric JUDGE; the WORKER is never handed a thinking option. A Reuse card does not
649
+ covers DRAFTING (scout, confirm, author, revise, structure retries) AND the rubric JUDGE, and as the think-everywhere override it makes every WORKER call think too (below). A Reuse card does not
650
650
  offer the tick: it never drafts and the judge follows the SIGNED value, so the 10/10 calibration gate runs with the same tick as the later grading
651
651
  (`defaultJudgeLoop({ ..., thinking })` takes it in all three constructors: the run, `run-author`, the panel's gate). bareloop keeps NO per-model thinking list and no
652
652
  vendor regex: bare-agent (>= 0.52) owns the request shapes, and `thinkingSupport(kind, model, { baseUrl })` (a pure lookup, no network) answers `true` (measured
@@ -668,6 +668,23 @@ with a code-owned `thinking` row (`thinkingRowText`, before "checking setup"): `
668
668
  `thinking on (this model can't change it)`; and, once a call reports a status, `thinking on (the model thought anyway)` (`'ignored'`), `thinking on (this model can't change it)`
669
669
  (`'unsupported'`) or `thinking off (not confirmed)` (`'unknown'`) — a `step-label` record in the draft log carries the change. A Reuse workflow session drafts nothing and has no row.
670
670
 
671
+ **Thinking on retries (the worker; PRD v1.106 Part 1, hamr 2026-10-17, every job).** Without the tick, each worker model call's mode is decided in code, never by the model,
672
+ by `workerThinkingMode({ phase, iteration, tick })` in `src/thinking.js` (the one owner) at the `mkWorker` ask/askFrom call sites in `src/planrun.js`:
673
+ planner (draft, redraft, replan; phase `plan`) = on; run scout = off; step attempt 1 = off; step attempt 2 and later = on; close-fix (`fix`) = on. The job's tick makes
674
+ every one of those on. The mode goes out as bare-agent's per-call `thinkingMode` through `thinkingCallOptions` (the same support-aware mechanism drafting uses: a
675
+ model bare-agent measured as not switchable, a test double or a clipipe/native session is sent nothing). A step or fix iteration that is not a whole number >= 1
676
+ throws (operator error, never a guess). Each call writes the spine record `thinking-decision {phase, iteration, mode}` BEFORE it is made (one per call, a transport
677
+ or rate-limit retry inside it carries the same options), and, once the call returns, `thinking-status {phase, iteration, mode, thinkingStatus}` with bare-agent's own
678
+ verdict (`applied` / `unsupported` / `ignored` / `unknown`; absent when nothing was sent). Measured only on DeepSeek deepseek-flash (n=4 per arm); other shapes report
679
+ through `thinkingStatus`, and are not claimed. The close, budgets and caps are untouched.
680
+
681
+ **Thinking shown in the panel (PRD v1.106 Part 3).** The Audit tab says the thinking each worker call was SENT, read by `replayRun` from the run's own `thinking-decision` / `thinking-status`
682
+ records (`thinkingWordsFrom`, `src/thinking.js`), never guessed: each attempt line reads `attempt 2 [glyph] · thinking on` (the decision whose seq lies in that attempt's window), and a part with one
683
+ call (plan, scout, replan, a one-attempt step) carries it on its header line (`part.thinking`). The words are drafting's own (`thinkingRowText`): `thinking on` / `thinking off`, and
684
+ `thinking on (this model can't change it)` / `thinking on (the model thought anyway)` / `thinking off (not confirmed)` when bare-agent was not sure. An attempt whose own call recorded nothing (the fix loop's
685
+ opening precheck) and every part of a run from before the records show no thinking word. `GET /api/runs/:runid/job` adds `thinking` (`thinkingJobText(tick, recorded)`: `on retries`, or `always on` when
686
+ the signed spec carries the tick (an unreadable tick counts as not ticked), but `—` for a run whose spine holds no `thinking-decision`, never a guess from the tick) for EVERY run; the Job tab draws it as the `Thinking` row, beside Model on one row like the caps. The page types none of these words. The Job tab's "AI's summary" (`GET /api/runs/:id/job` plan `goal`) is only the sentence the person confirmed: for a rubric job (`RUBRIC_GOAL_HEAD`) `confirmedGoalOf` reads it out of the worker brief, none when the brief has no such line; a code job's goal is unchanged.
687
+
671
688
 
672
689
  **D5 — the mandatory guards are SHOWN and FIXED, and the battery keys off the VERDICT
673
690
  CLASS** (PRD v1.57 §2 — what a battery is FOR is the class of dishonesty that class of
@@ -721,7 +738,7 @@ attempt PLUS its close. Unpriced is never free: a null cost is a `pricing-red` s
721
738
  never retried, and the ladder is otherwise ONE retry (`JUDGE_ATTEMPTS`) — after that a broken
722
739
  judge is an instrument stop, never a red about the tree.
723
740
 
724
- **The rulebook, and what each rule can raise** (`JUDGE_RULES` in `src/judged.js`; every rule carries a plain-words `means` the case-proposal prompt renders, so a proposed practice case never expects a red the rule cannot raise). The judge only POINTS (quotes lines verbatim); code does every check; unsure is red; the judge never says pass/fail. Quotes are matched by bareguard's `quoteIn(..., {wholeLines:true})` (`wholeLines` arrived in bareguard 0.21; bareloop now needs `^0.24.0`): a contiguous run of whole artifact lines, so a bare fragment cannot pass by hiding in a longer line, while a dropped leading `* ` of a JSDoc tag line still matches. `has-doc` checks only that a JSDoc block sits directly above the function (read off the artifact by `docBlockAbove`; the judge's quote is a pointer that must lie inside that block), so a documented function never breaks it. **`says-what`** (the doc's description must say more than the function's own name) reds when the described prose, minus a fixed list of function words and the name's own words (camel/snake/kebab, light inflections, and a prefix echo such as `init`/`initialize` when the shorter word is at least 4 letters), leaves nothing; no description, or an unlocatable quote, reds. It catches a pure name echo only ("Returns the total." for `getTotal` passes), and with NO doc block at all it stays silent and leaves the red to `has-doc` (the card is not required to carry both). `params` reds a declared parameter with no tag, an `@param` naming no parameter (each destructured slot absorbs one unmatched root tag), and, when the doc names any field of a simple destructured parameter, a missing field; `@param options` alone for `{ x, y }` passes. `returns` reds a returning function with no `@returns`. A practice case that expects a `has-doc` red on a function that has a block is refused as a legality red before any call (`validateCalibrationSet`).
741
+ **The rulebook, and what each rule can raise** (`JUDGE_RULES` in `src/judged.js`; every rule carries a plain-words `means` the case-proposal prompt renders, so a proposed practice case never expects a red the rule cannot raise). The judge only POINTS (quotes lines verbatim); code does every check; unsure is red; the judge never says pass/fail. Quotes are matched by bareguard's `quoteIn(..., {wholeLines:true})` (`wholeLines` arrived in bareguard 0.21; bareloop now needs `^0.25.1`): a contiguous run of whole artifact lines, so a bare fragment cannot pass by hiding in a longer line, while a dropped leading `* ` of a JSDoc tag line still matches. `has-doc` checks only that a JSDoc block sits directly above the function (read off the artifact by `docBlockAbove`; the judge's quote is a pointer that must lie inside that block), so a documented function never breaks it. **`says-what`** (the doc's description must say more than the function's own name) reds when the described prose, minus a fixed list of function words and the name's own words (camel/snake/kebab, light inflections, and a prefix echo such as `init`/`initialize` when the shorter word is at least 4 letters), leaves nothing; no description, or an unlocatable quote, reds. It catches a pure name echo only ("Returns the total." for `getTotal` passes), and with NO doc block at all it stays silent and leaves the red to `has-doc` (the card is not required to carry both). `params` reds a declared parameter with no tag, an `@param` naming no parameter (each destructured slot absorbs one unmatched root tag), and, when the doc names any field of a simple destructured parameter, a missing field; `@param options` alone for `{ x, y }` passes. `returns` reds a returning function with no `@returns`. A practice case that expects a `has-doc` red on a function that has a block is refused as a legality red before any call (`validateCalibrationSet`).
725
742
 
726
743
  **A judged stage may only judge files inside the job's write fence** (`judgedOutsideFence`, `src/authoring.js`, called from `validateDeclaration`, which every door reaches: the authoring revise loop, `validateCloseDecl` at signing and runner pre-flight, and `validateJob` for hand-written or reused specs). A path outside `writeScope` reds `judged-outside-write-scope` at draft time, because a finding on a file the run is fenced out of can never be fixed. With no fence to compare against (the plain-folder deferral) the rule is not run.
727
744
 
@@ -3419,7 +3436,7 @@ key you want first (the panel picks the row by the Model menu).
3419
3436
  summary, `fileMissing:true` when the listed spine is gone, and the states the left list draws, all decided by the server:
3420
3437
  `live`, `liveWhy` (`step N of M`), `liveSince`/`priorWallMs`, `needs` (`'Resume'` while the engine would accept a resume and
3421
3438
  the run stopped under 7 days ago and was not removed; `'Accept'` while the run waits at its review door, `doorWaiting`), `stoppedAt`; `jobs` = `[{job, headRunid, quiet, activityMs}]`
3422
- newest activity first; `scheduled` = the Scheduled section's cards and `gaveUp` = the Needs-you cards of schedules that gave up (both built by the schedule door, `listScheduled` / `listGaveUp`); `limits` = `{quietDays:30, olderRuns:7, needsYouDays:7}`. The page groups `runs` by `jobs` into Running now,
3439
+ newest activity first; `scheduled` = the Scheduled section's cards (v1.106 Part 2: drawn BELOW the search box, narrowed by the one search box on JOB NAME only: a `run-N` or a date word narrows Jobs only, the result and time chips never touch it; the `◷` chip, `scheduled jobs only`, shows only Scheduled with the count line `showing N scheduled job(s)`, and `no scheduled job matches` when none does; Running now and Needs you are never filtered) and `gaveUp` = the Needs-you cards of schedules that gave up (both built by the schedule door, `listScheduled` / `listGaveUp`); `limits` = `{quietDays:30, olderRuns:7, needsYouDays:7}`. The page groups `runs` by `jobs` into Running now,
3423
3440
  Needs you, Jobs and a quiet fold; there is no Workflows / History switch and no separate `/api/workflows` endpoint); `GET /api/runs/:runid` (the full replay — parts/steps, counters,
3424
3441
  summary, model/provider/judge). Every `GET /api/runs` row and the detail carry `runNo` (P6 item 7): runs of one job are numbered
3425
3442
  oldest `at` first by ONE server function (`runNumbers`), and the page names a run `job (run-N)`; the run id stays in the Audit
@@ -3430,7 +3447,10 @@ key you want first (the panel picks the row by the Model menu).
3430
3447
  ts-windowed to this run alone even when the sidecar file is shared with other runs, plus
3431
3448
  the matching raw-log text windowed the same way — when one resolves); `GET
3432
3449
  /api/runs/:runid/rounds?part=&attempt=&offset=&limit=` (one attempt's rounds, lazily
3433
- paged, for the Audit tab's Grouped view); `GET /api/runs/:runid/job` (the signed spec's
3450
+ paged, for the Audit tab's Grouped view); `GET /api/runs/:runid/result` and `GET /api/runs/:runid/download?i=<n>` (PRD v1.107 Part 6, the Result tab of a rubric run: `result`
3451
+ is the signed lines and fact sentences plus the rows of each delivered step file (a row carries `proof: {text, marks: [[start, end]]}` when the run's `rubric-evidence` record holds what the check read for that result: the words to mark, never found by searching; absent for an old run or a quote the check did not read; a money cell then reads as the page wrote it, "144 US dollars", else the fact's own spelling "$144"); both read ONLY the paths of the run's own `destination-written` records, re-check their
3452
+ sha256, and answer in words when the file changed or is gone; the detail carries `hasResult` and `delivered`); `POST /api/runs/:runid/rerun {text}` and `/pause` (human-click
3453
+ guard, the engine's own `run-u --door --decide`, only while the run waits at its review door; empty rerun words are refused); `GET /api/runs/:runid/job` (the signed spec's
3434
3454
  own fields, ONLY when a bundle-layout run's `spec.json` is reachable — a run-u run has
3435
3455
  none on disk, and this reads `resolved:false` with every spec-only field honestly
3436
3456
  `'unknown'`, never guessed from the spine's own narrower `job-start` record). The run
@@ -4368,21 +4388,24 @@ A **Rubric** job whose first Input is the full path of a FILE (not a repo folder
4368
4388
 
4369
4389
  ### Required facts — a line that finds, lists or looks something up (Part 1b, `src/facts.js`, `docs/product/ITEM33-BUILD.md` "Addendum 2026-10-13" and "Part 1c", PRD item 33 RF1–RF22)
4370
4390
 
4371
- Check what the person asked for, not what the worker chose to write. bareloop needs bareguard `^0.24.0` (`cited` + `decides`, bind-only and `text` decides, word-edge anchors; 0.24 lets money currency/unit be lists, ruling #65, but bareloop facts still carry one form).
4391
+ Check what the person asked for, not what the worker chose to write. bareloop needs bareguard `^0.25.1` (`cited` + `decides`, bind-only and `text` decides, word-edge anchors; 0.24 lets money currency/unit be lists, ruling #65; bareloop facts now carry them, see "Currency and unit lists" below; 0.25 returns `full.evidence`, the spans the Result tab marks, see `rubric-evidence`).
4372
4392
 
4373
- - **The part.** A `rubric-step` stage's `params.parts` may hold ONE part of `class: "fact"` (a line that finds, lists or looks up): `{ text, class: "fact", source: <input label>, facts: [{ name, shape, unit? | currency?, phrase?, anchor?, until?, rule?, aiProposal? }], count?: { min?, max? }, rules?: [n], covers?: [n] }`. `shape` is `money | number | date | time | phrase | text`; `rule` is `{ op: lt | lte | gt | gte, value }` or `{ op: "between", lo, hi }` (not on `text` or `phrase`); `money` names exactly one of `unit` (the input's own words, e.g. `"US dollars"`) or `currency` (`$ € £`); `phrase` carries the fixed `phrase` the quote must contain; `anchor` / `until` are labels copied VERBATIM from the input. A fact name is letters, spaces, `_` or `-` only (it is written into the claim text bareguard scans for numbers). A line with a fact part has no word, section or claim-count parts (a drafting red). Code fills `tested` (the early-test evidence) and `sample` (what the drafter saw of a big input) into the signed part; a model-written one is replaced.
4393
+ - **The part.** A `rubric-step` stage's `params.parts` may hold ONE part of `class: "fact"` (a line that finds, lists or looks up): `{ text, class: "fact", source: <input label>, facts: [{ name, shape, unit? | currency?, phrase?, anchor?, until?, rule?, aiProposal? }], count?: { min?, max? }, rules?: [n], covers?: [n] }`. `shape` is `money | number | date | time | phrase | text`; `rule` is `{ op: lt | lte | gt | gte, value }` or `{ op: "between", lo, hi }` (not on `text` or `phrase`); `money` names at least one of `unit` (the input's own words, e.g. `"US dollars"`) or `currency` (`$ € £`), and may name both, each one value or a list (below); `phrase` carries the fixed `phrase` the quote must contain; `anchor` / `until` are labels copied VERBATIM from the input. A fact name is letters, spaces, `_` or `-` only (it is written into the claim text bareguard scans for numbers). A line with a fact part has no word, section or claim-count parts (a drafting red). Code fills `tested` (the early-test evidence) and `sample` (what the drafter saw of a big input) into the signed part; a model-written one is replaced.
4374
4394
  - **The step file.** `output/steps/<n>.json` of a facts line is `{ "results": [ { "quote": "...", "values": { "<fact name>": "<value as the quote writes it>" } } ] }` — no free text, no `cites`. ONE quote per result holds all its facts, so a worker cannot mix a price from one result with a time from another. `buildStepSpec` makes one bareguard `cited` check with `decides` (the fact's index is its `d<j>` key; a phrase fact is a presence decide and takes no value). The claim text is written by CODE from the values (`name: value; ...`); a `count` becomes `min` / `max` on the number of results. Every value is compared by shape and exactly (`8:15 AM` equals `08:15`, `389` equals `389.00`); a red names the result and the fact in plain words (`plainFactItem`). A JSON number in `values` is read as the string it prints as.
4375
4395
  - **No record cutting (Part 1c).** A fact part carries no `record`, `tested` or `sample` (a `record` field is an unknown-field red). The worker's brief says "one quote per result, copied from that result's own text" (bareguard's quote cap of 4000 characters stays its own). Drafting does not grade: it only proves each fact CAN be checked (below).
4376
4396
  - **Repeat rule (Part 1c M1, M4).** On a fact line asking for N results (`count.min`), `checkFactWords` (`src/factwords.js`, the one owner) refuses at $0 an anchor label the label list counts fewer than N times (`rubric-fact-label-rare`) and a `phrase` fact whose words `countPhrase` finds in the input fewer than N times (`rubric-fact-phrase-rare`); count 1 or none keeps a once-only one legal. `RUBRIC_RULES` states both.
4377
- - **The look, then the draft (Part 1c).** For a rubric job on files, `authorCloseForJob` runs `runInputLook` (`src/authorscout.js`; pure parts in `src/inputlook.js`) on the frozen input text BEFORE the confirm turn and the draft, when a `provider` is present (`lookFn: null` = no look; a panel test that replaces `generate` gets none unless it passes `lookFn`). Read-only by construction: tools `search_input` / `read_input` over in-memory text, `LOOK_ROUNDS` 8, a reserved final round whose only tool is the strict answer form `report_look`, spend metered into the drafting book (`calls` absorbed like the repo scout's). Its report (facts with a label picked from the list, prose lines, not-found entries) is verified by code (`verifyLookReport`: a label off the list, an unknown shape/line/input, or a phrase not in the source is moved to not-found, named) and rendered into the drafter's prompt (`renderLookReport`); a failed or reportless look is an ABSENT survey and stops drafting at $0 (`scout-absent`, "the first look at your repo or input did not complete"). Every place, date or name in the job line is a fact too, bound to the label before it (a direction = phrase facts `Leaves <place>` / `arrives at <place>`); prose lines and their `~` limits are not looked up.
4397
+ - **The look, then the draft (Part 1c; the default look is now the page-word map, next bullet).** For a rubric job on files, `authorCloseForJob` runs the look (`lookFn`, default `runPageMap`; the older search-and-read `runInputLook` in `src/authorscout.js`, pure parts in `src/inputlook.js`, is unwired from the default path) on the frozen input text BEFORE the confirm turn and the draft, when a `provider` is present (`lookFn: null` = no look; a panel test that replaces `generate` gets none unless it passes `lookFn`). Read-only by construction: tools `search_input` / `read_input` over in-memory text, `LOOK_ROUNDS` 8, a reserved final round whose only tool is the strict answer form `report_look`, spend metered into the drafting book (`calls` absorbed like the repo scout's). Its report (facts with a label picked from the list, prose lines, not-found entries) is verified by code (`verifyLookReport`: a label off the list, an unknown shape/line/input, or a phrase not in the source is moved to not-found, named) and rendered into the drafter's prompt (`renderLookReport`); a failed or reportless look is an ABSENT survey and stops drafting at $0 (`scout-absent`, "the first look at your repo or input did not complete"). Every place, date or name in the job line is a fact too, bound to the label before it (a direction = phrase facts `Leaves <place>` / `arrives at <place>`); prose lines and their `~` limits are not looked up.
4398
+ - **The page-word map (idea A core, `src/pagemap.js`, `src/pagerules.js`, `docs/product/ITEM33-BUILD.md` "Addendum 2026-10-18 — Idea A core").** The default `lookFn` is `runPageMap`: ONE AI call per page input (the job's drafter identity, thinking off, booked per call into `draft-spend.json`, cap checked before each call) maps the job to the page's OWN words as `{"facts":[{name, from, shape: place|money|time|phrase, words, currency?, unit?}]}`; it takes the old look's arguments and returns the same envelope. Plain code then keeps or drops each word (`applyPageRules(text, mapping, jobLines)` -> `{ facts, dropped }`, pure, ported from the frozen POC): found-on-page (count >= 1), N2 (a label written 2+ times inside one page line is unusable), R1 (a second label stays only if it reads the same values as the first), U1 (a label the page writes AFTER the number, "EUR 490 total", becomes a unit; a label with no number either side is dropped), R1L (a unit/currency form stays only if it reads the same numbers as the primary form; units first; before R1L the forms are sorted by code - a sign is a currency, any other form a unit, whichever box the AI used), S1 (a fact name that counts results is refused; two facts may not share shape AND label). After the rules, `dryReadMoney` (`src/pagedry.js`, called by `runPageMap`) grades an honest claim on every result line with the run's own checker; a money fact that reads none is left under Not checked (`reads-no-result`); then `dryReadPhrases` reads each kept phrase fact against those same result lines (the lines the job's kept money facts read) and drops a phrase found in none of them whole as `phrase-no-result` (Not checked: `"Rome" is on the page but in none of its results, so it is not checked.`); with no kept money fact the result lines are unknown and phrase facts pass untouched. The AI never grades; the answer is read strictly (no JSON repair): a malformed or truncated answer is retried ONCE, twice is the plain stop "The AI's answer about the page was not readable twice. Nothing was signed." (an ABSENT survey; a draft is not a run, so no Resume); one retry on a transport error (booked unpriced, so `spendComplete=false`) or a 429 (the vendor's stated wait). The scrubbed raw answer rides each call into `draft-log.jsonl` (`call.raw`), unsigned, so a run can be replayed at $0. A drafted fact may use only words the map KEPT: a signed anchor, closing label, unit or currency that was dropped is the red `rubric-fact-word-dropped` with the drop's reason, and an anchor must be a kept label (`rubric-fact-label-off-list`, now against the kept set).
4399
+ - **Currency and unit lists (idea A core, `src/facts.js`).** A money fact's `unit` and `currency` may each be one string (every spec signed before lists) or a non-empty list: at most 3 currencies (`$ € £` only; a code like `EUR` is a unit) and 10 units, none repeated, a unit at most 100 characters, enforced by our own plain reds before bareguard is asked. A string reads exactly as a one-element list and is never rewritten, so an old signed spec keeps its bytes and hash. The first listed form is the primary one in the plan row and the worker brief; others read "or". `checkFactWords` requires EVERY listed unit and sign to be on the page.
4400
+ - **No silent drop (idea A core, `src/pagedropwords.js`, `withDropNotes` in `src/rubricstep.js`).** Every word, fact or `~` rule the page rules drop is shown as Not checked with a plain sentence from ONE table (no other file writes a drop reason). Before validation `withDropNotes` appends to the declaration: `closeDecl.refused` `{ line, rule, reason }` for a dropped fact whose `~` rule no part carries (never a line-level refusal; a rule the drafter already listed is not listed twice), and one line in `closeDecl.notes` for any other drop, including a word cut from a fact that is still checked. Those word-level lines are not "Not checked": `splitReadNotes` / `isReadNote` (`src/pagedropwords.js`) sort a row whose every sentence only says which written form of a still-checked fact was not used (not on the page, listed twice, not the same values, list limits, and an `s1` copy of a fact that is still signed) into `readNotes`, shown in their own fold "How the page was read · N" right after Not checked (plan card, Job tab, Result tab `readNotes`, CLI confirm menu) and not counted in it; a whole fact left out (`no-label`, `reads-no-result`, `phrase-no-result`, an `s1` copy of an unsigned fact) stays Not checked. The signed fact names come from `signedFactNames(closeDecl)` (older specs too) or the look at the confirm turn (`plan.signedFacts`). The confirm card gets the same lines (`plan.notChecked`) before drafting is paid for, and the drafter's look block says `NOT CHECKED (rule n)` / `LEFT OUT`. Nothing about this changes a signed spec's verdict rules; it is the Not checked list, in the person's words.
4378
4401
  - **Labels are picked, never typed (`src/labelscan.js`).** `scanLabels(text)` lists, per shape (money, number, date, time, text), the words right before each value with counts (top 40 per shape; one generic scan, no per-site table; a page reader's `[ref=N]` tags are ignored). `labelOnList(scan, shape, idOrText)` is the one test (id or exact words, white-space blind). The look and the drafter pick labels only from the list shown in the prompt (`renderLabelList`). Fact-line reds are plain sentences; the confirm step and the drafter share one rule text (`RUBRIC_RULES`), and the drafter's per-shape field guide is generated from bareguard's `rubricVocabulary`.
4379
4402
  - **Found-once check (`src/factwords.js`, `checkDeclarationWords`, run after validation in `authorClose`).** Every anchor must be on the input's label list for its shape, and every `until`, phrase and unit must be in the source once (white-space blind: U+202F, U+00A0; phrases whole-word, case-blind). A miss is a red naming the word and the input: `rubric-fact-label-off-list`, `rubric-fact-word-missing`, `rubric-fact-unread` (an input that cannot be read is a red, never a skipped check). Nothing is graded; finding and grading are the run's and the close's job.
4380
4403
  - **No silently dropped rule (RF16).** When a close holds a fact part or any part with `rules`, every `~` rule of every job line must be carried out by a part (its number in the part's `rules`, 1 = the first `~` rule) or listed in `refused` as `{ line, rule, reason }`, or its whole line refused; a rule that is none is a $0 red (`rubric-rule-dropped`). `refused` entries may now carry `rule`. A number the person did not write (a bound not found in the line's own words) must be marked `aiProposal: true` (the card says "AI's proposal — check it"), and an AI-proposed time limit must be `between`, never one-sided (RF17).
4381
4404
  - **The prose step.** A line that writes keeps its word / section / line counts. When any fact line comes BEFORE it, code puts one `numbersFromFacts` part on it (`withFactBindings`; the model never writes it, and a missing or wrong one is a drafting red): every number in `text` must appear in a quote of those fact lines' step files (bareguard's `numbersInQuote`: token match, not canonical, so `v0.41.0` is one token). The card says, under Not checked, `line N: what the text says — you judge it at Accept`. At the close, a tied earlier line whose step file does not exist yet (the seed read, before any worker wrote) is a normal red ("line N's answer is not there yet"), never an instrument stop; only an unreadable file is a stop.
4382
4405
  - **The plan card.** One code-worded row per fact under its line, read by the one reader (`checksOfClose` / `factRows`) the confirm card, the Job tab (`planFromSpec`) and the sign-time `planMatchesClose` share, e.g. `fact · price — money in US dollars, at most 450 US dollars, after "Price", quoted from "Flights"`, then the count row. A `~` rule the drafter listed as not checked reads `line N: "<rule as written>" — <reason>`.
4383
4406
  - **The worker brief.** `rubricGoal({ ..., closeDecl })` adds a code-written brief per line (`lineBriefs`): a facts line gets its facts and its count; a text line that follows gets "every number must be copied exactly as written in a quote you wrote for job line N". `stepFileContract()` names both shapes.
4384
- - **Web addresses as inputs (Part 1c, `src/pagefetch.js`, `src/source.js`).** An Inputs line may be `label: https://...` (also a rubric card's FIRST line; `looksLikeRubricInput`). `fetchPageText(url)` calls barebrowse `connect({ incognito: true, blockPrivateNetwork: true })` (`CONNECT_OPTIONS`) → `goto` → `waitForNetworkIdle({ idle: 1500, timeout: 20000 })` (bounded; a page that never settles is read as it stands) → `snapshot({ mode: 'act' })`, always closing the browser, with our own 90 s deadline (L1: a load-time `browse()` snapshot gave 77,951, 8,801 or 719 characters of the same results page; L2: a page whose text outside the banner / navigation / contentinfo landmarks is under `MIN_CONTENT_CHARS` = 100 (`pageContentChars`) is a stop too, "only the site's own header and menus", and an input with no content never reaches the look or an early ask: the survey is ABSENT, "holds nothing to read", nothing spent); an empty or under-200-character page (`MIN_PAGE_CHARS`), a bot challenge, an unreachable address, a credentialed or secret-shaped address, or a page carrying a secret shape is a plain-words $0 stop on its line. `proveRubricInputs({ ..., fetchUrl })` freezes the page text under `input/<label>.txt` and records `url` on the manifest input row. A FRESH `run-u --spec` start (not `--resume` / `--door` / `--decide`) calls `refreezeUrlInputs`: it fetches every recorded address again, writes that text into the prepared tree, commits it as the new seed and rewrites `source.json` (new `sha256` and `seed`, `draftSha256` keeps what the look saw) BEFORE the worker starts, so quotes are checked against the run's copy and the worker reads it, never the live page. A failed or empty fetch stops the run, "The run did not start. Nothing spent." Plan card (L3): `settlePersonsWords` (called in `authorClose` before validation) unmarks AI's proposal on a limit made of the person's own numbers, swaps in the one explicit positive time range the person wrote, and drops a `refused` entry for a `~` rule a part of its line carries; the look's fact may carry `forRule` (a rule it numbered from its own knowledge, marked AI's proposal), which removes that rule from the look's not-found list so it is not asked. Test seams: `fetchPageText(url, { connect })`, `deps.fetchUrl` (`run-u`), `deps.fetchUrl` / `deps.lookFn` (`createSession`).
4385
- - **Ask at once, only about intent (I1).** A look not-found entry that names a `~` rule becomes a question worded by code (`ruleQuestions`: line and rule named), asked before the confirm turn through the existing question gate (`answerQuestions`; `normalizeQuestions` caps it at `MAX_QUESTIONS`); the answer is a new `~` rule on that line. A rubric job on files never offers the piecemeal try-3 question; its failed-draft stop (`draftStopText({ wholeJob: true })`) says to review the whole job and submit it again.
4407
+ - **Web addresses as inputs (Part 1c, `src/pagefetch.js`, `src/source.js`).** An Inputs line may be `label: https://...` (also a rubric card's FIRST line; `looksLikeRubricInput`). `fetchPageText(url)` calls barebrowse `connect({ incognito: true, blockPrivateNetwork: true })` (`CONNECT_OPTIONS`) → `goto` → `waitForNetworkIdle({ idle: 1500, timeout: 20000 })` (bounded; a page that never settles is read as it stands) → `snapshot({ mode: 'act' })`, always closing the browser, with our own 90 s deadline (L1: a load-time `browse()` snapshot gave 77,951, 8,801 or 719 characters of the same results page; L2: a page whose text outside the banner / navigation / contentinfo landmarks is under `MIN_CONTENT_CHARS` = 100 (`pageContentChars`) is a stop too, "only the site's own header and menus", and an input with no content never reaches the look or an early ask: the survey is ABSENT, "holds nothing to read", nothing spent); an empty or under-200-character page (`MIN_PAGE_CHARS`), a bot challenge, an unreachable address, a credentialed or secret-shaped address, or a page carrying a secret shape is a plain-words $0 stop on its line. `proveRubricInputs({ ..., fetchUrl })` freezes the page text under `input/<label>.txt` and records `url` on the manifest input row. A FRESH `run-u --spec` start (not `--resume` / `--door` / `--decide`) calls `refreezeUrlInputs`: it fetches every recorded address again, writes that text into the prepared tree, commits it as the new seed and rewrites `source.json` (new `sha256` and `seed`, `draftSha256` keeps what the look saw) BEFORE the worker starts (inside `execute`, AFTER the run's row is listed with its pid and cap, so the Runs list shows it while the page loads; the Run tab reads "fetching the page…" until `source.json` carries `refrozenAt`), so quotes are checked against the run's copy and the worker reads it, never the live page. A failed or empty fetch stops the run (the row is settled at $0 as `failed` with the plain reason, `refused before start`, and the monthly claim is freed), "The run did not start. Nothing spent." A rubric job on a URL or file is recognised by the signed spec's verdict type on a Reuse and on a scheduled fire (`startSession`, `src/panel/authorroutes.js`), so line 1 is not proved as a repo path; a fresh job reads it from the card. Plan card (L3): `settlePersonsWords` (called in `authorClose` before validation) unmarks AI's proposal on a limit made of the person's own numbers, swaps in the one explicit positive time range the person wrote, and drops a `refused` entry for a `~` rule a part of its line carries; the look's fact may carry `forRule` (a rule it numbered from its own knowledge, marked AI's proposal), which removes that rule from the look's not-found list so it is not asked. Test seams: `fetchPageText(url, { connect })`, `deps.fetchUrl` (`run-u`), `deps.fetchUrl` / `deps.lookFn` (`createSession`).
4408
+ - **Ask at once, only about intent (I1; the asks are now code-forced from the page map, see "The look reads, code asks" below).** A `~` rule the look cannot tie to a kept page word becomes a question worded by code (`askableFromLook`, `src/pagemap.js`: line and rule named; the older `ruleQuestions` in `src/inputlook.js` is no longer called by the draft path), asked before the confirm turn through the existing question gate (`answerQuestions`; `normalizeQuestions` caps it at `MAX_QUESTIONS`); the answer is a new `~` rule on that line. A rubric job on files never offers the piecemeal try-3 question; its failed-draft stop (`draftStopText({ wholeJob: true })`) says to review the whole job and submit it again.
4386
4409
  - **Known limits (documented, not built around).** Airport codes like SFO have no stable label on a flight page: direction is checkable only through the full airport names the page uses. An unanchored `text` fact cannot tell which field a value sits in. `am` / `pm` in lower case and `9:27AM` are malformed times; `v0.41.0` is one token. A fact step's quote proves the value is in a real record, not that it is the RIGHT record: "the cheapest" and "the last 3" are Not checked. Verified on real files only; the live proof (DeepSeek, both jobs through the panel) has not been run.
4387
4410
 
4388
4411
  ## Triggers — a job that starts by itself (panel-only; `src/trigger*.js`, `src/panel/schedules.js`, `schedulestart.js`, `scheduler.js`, `src/schedulewords.js`)
@@ -4396,4 +4419,21 @@ Check what the person asked for, not what the worker chose to write. bareloop ne
4396
4419
  - **A scheduled green is held until accepted**: `getStartFrom` reads its outcome as `green-held` and the Reuse list skips it, until the spine holds an honoured accept.
4397
4420
  - **Accept** (`POST /api/runs/<id>/accept`) runs the engine's own door path (`run-u --spec <signed spec> --door <id> --decide accept --approve <hash>`): the mechanical stages re-run, a red is `door-accept-red`, the run's verdict is untouched. It shows only while `doorWaiting(events)` (`src/reviewdoor.js`) is true; the same answer fills the runs list's Needs-you `Accept`.
4398
4421
  - **Every word the page shows** comes from `src/schedulewords.js` (`SCHEDULE_WORDS` is the one button table the server sends).
4399
- - **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, head, rows}`; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
4422
+ - **The schedule is shown on three tabs** (v1.102): the Run tab's one summary block (ENDED dated, NEXT, SCHEDULE with `last`/`next`; row and frame red when the last fire started nothing), the Job tab's Started and Schedule rows (`getRunJob`), and the Audit's own folded `schedule` section on every run of a scheduled job (v1.104: `detail.scheduleSection` `{title, sign, word, head, rows}`, drawn `[sign] word schedule · head` with the sign and word both from the status table, v1.106 Part 4; a run a cancelled schedule started keeps `detail.startedAudit`). `/api/runs` `scheduled[]` and `gaveUp[]` carry `runNo` so the small lists read `job · run-N`. `trigger.json` carries `triggerKind`, `sessionId`, `originRunid`, `sig`, `lateMs`; each signing appends a `signed` ledger row naming its origin run.
4423
+
4424
+
4425
+ ## The Audit's tries, checks and answered questions (PRD v1.106 Part 5)
4426
+
4427
+ For an integrating agent reading a run's record:
4428
+
4429
+ - **`output-judged` spine record** `{step, iteration, path, bytes, sha256}`: written by `runPlan`'s step `judge()` right before the `exit-eval`, once per step file a `rubric-step` check reads (`outputJudgedRecords`, `src/rubricstep.js`, from the signed `closeDecl`). The file is overwritten by the next try, so this is the only trace; content is never written. Old runs have none, and show no judged-file line.
4430
+ - **`rubric-evidence` spine record** `{step, iteration?, evidence, evidenceTotal?}`: report-only. Written by `runPlan` (`onRubricEvidence`, fed by the `rubric-step` kind) each time a step's `cited` checks read a step file; `evidence` is bareguard 0.25's `full.evidence` (`{check, claim, text?, spans, spansTotal?}` per claim; span offsets are UTF-16 into that claim's normalized `text`). `iteration` is present for a step try's own check and absent for the outer close. The `text` is masked like every spine write; a claim whose text changed by masking keeps `spans: []`. No code in the verdict, governor or money paths reads it; the Result tab reads it to mark the words each check read. Nothing is written when the check read no claim. Old runs have none.
4431
+ - **Per-try checks** come from `exit-eval {step, iteration, results}`: a `check-passes` result is named by the `check-run` record just before it, a `tree-changed` result is the declared `files-changed` stage. A try that began (`iteration-start`) in a step that has ended and has no `exit-eval` was cut off (`part.cutOffTry`). `replayRun` step attempts carry `checks` and `outputJudged`; the panel server adds `tries[]`, `triesLine`, `tally` and `marker`.
4432
+ - **Run summary** words `callsLine` / `checksLine` (`callsLineOf`, `checksLineOf`, `src/panel/server.js`): counts from the raw spine, never a fraction.
4433
+ - **Review door part** (`kind: 'door'`) when the spine has `review-door`; words in `DOOR_WORDS` (`src/panel/status.js`).
4434
+ - **`GET /api/runs/:id/rounds?part=N&all=1`** returns a step's whole Round table as rows (`kind: model | tool | check`), 20 per page (`offset`).
4435
+ - **`draft-log.jsonl` `human` rows** `{no, source: confirm | look | try-3, question, answer, line?, at}`; the answer is secret-masked before it is written and the row exists only when the question was answered.
4436
+ - **Money line** `moneyWithDraft(spendCore, draftUsd, budgetUsd, draftComplete, runUsd)`: `$run run + $draft drafting = $total of $cap`; without `runUsd` the sum is omitted. The panel's own spelling is `panelMoney2` (`src/panelmoney.js`, one Node owner; `src/panel/index.html` carries a pinned browser copy): 2 decimals from a cent up, 4 decimals for a real amount under a cent (`$0.0054`), `<$0.0001` below that, exact zero `$0.00`, null `unknown`; the CLI's `money()` keeps its own 4 decimals.
4437
+ - Not built from the signed spec (see the Part 5 commits): the early ask about a number the AI made (T1), because the data it needs does not exist at that point; T1 is replaced by Part 5b below. The "fetching the page…" state (Q2) was built after the run-start refetch moved to after the run is listed: the sign progress list shows the step (`settleFetchingStep`, `src/panel/authorroutes.js`) and a listed run with no log yet reads "fetching the page… the run has not started yet" (`startingStub`, `webFetchState`), both read from `source.json`'s `refrozenAt`.
4438
+ - **The look reads, code asks (PRD v1.107 Part 5b; `src/pagemap.js`).** A map fact on a `time` or `money` shape may carry an optional `reading: {op, value | lo, hi}` (the signed-rule `op` set; a time bound is a string, a money bound a number); any other `reading` makes the whole map answer malformed (no repair). `askableFromLook(look, jobLines)` sorts every `~` rule: a kept page word carries it (no ask); a `reading` whose bound is NOT in the person's own words (`boundInWords`, the same test as `rubric-fact-unmarked`; it reads the words by kind, F242: a number bound is said only as a bare number or an amount's number, a time bound only as a time token with the hour's leading zero ignored, a date bound only as a date token, never a piece of a token) asks `Your line N says "<rule>". I read it as: <fact> <ruleWords>. Agree?`; a rule no kept fact carries asks `... I can't check this on the page. Drop it?`. Readings come first, then can't-check asks. `agree` (or `yes`) writes the reading as a `~` rule in plain words, so the bound is the person's own and the fact signs plain; a typed answer is the `~` rule instead; `drop it` on a can't-check ask adds no rule and leaves it Not checked. `MAX_QUESTIONS` is 3: more asks than that stops the draft before the confirm turn (stop `look-asks-too-many`, no extra spend) with `tooManyAsksText`, a code-written list of every ask and its reading. Asks carry `ask: 'reading' | 'cant-check'` (and `reading` for the agree text) through `normalizeQuestions`.
4439
+ - **The fit check (F243, `src/fitcheck.js`; `docs/product/ITEM33-BUILD.md` "Addendum 2026-10-21 — fit check build spec").** Rubric jobs only, once, after the inputs are frozen and the content gate passes and BEFORE the look; it runs only when the caller passes the answer seam `fitAsk` to `authorCloseForJob` (the panel; the CLI door and any test without it never run it; `fitFn: null` = no check). `runFitCheck` makes ONE AI call (the drafter identity, the drafting thinking rule, label `fit-check`, cap checked before the call) that splits each job line (no `~` rules) into asks copied from that line and quotes the page for each; plain code (`decideFit`) decides: an ask is found only if `found` is true AND its quote is on an input (whitespace and case ignored), a found-with-off-page-quote is `madeUp` and counts as not found, a reply with an ask not copied from its line or a line with no ask is malformed (one retry). All found = `fits` (nothing shown); any not found = `ask`: one question through the panel `ask` seam (`I found X of Y of your asks on this page. Not found: <ask> (line N), .... Type go on to keep drafting, or anything else to stop and change the job or the page.`); `go on` continues to the look, anything else stops `fit-stopped` (plain words in `draftStopText`), no answer is `confirm-abandoned`. The answer is not a `~` rule and changes nothing in the job. Failure is OPEN: a cut, two unreadable replies, a failed call or a cap stop is `absent`, chat says "The fit check did not finish; drafting goes on." and drafting goes on. The result is one `fit-check` row in `draft-log.jsonl`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bareloop",
3
- "version": "0.43.0",
3
+ "version": "0.45.0",
4
4
  "description": "Workflows that earn their own design, with receipts — agent-authored workflows for repeated, long, verifiable jobs that improve across runs under an un-gameable gate.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "bare-agent": "^0.52.0",
31
31
  "barebrowse": "^0.21.0",
32
- "bareguard": "^0.24.0",
32
+ "bareguard": "^0.25.1",
33
33
  "litectx": "^0.32.0"
34
34
  },
35
35
  "devDependencies": {
package/src/authorflow.js CHANGED
@@ -87,7 +87,8 @@ import {
87
87
  import { buildSeedListing, cleanEntry, SURVEY_CAUSES, AUTHOR_CALL_TIMEOUT_MS } from './authorscout.js';
88
88
  import { extractArtifact, priceOf, scrubRaw, tallyCalls, capStop, callCasualty } from './text.js';
89
89
  import { redactSecrets } from './validate.js';
90
- import { stepFileContract, inputLabelsOf, stepFileRel, readFrozenInputs, withFactBindings, settlePersonsWords } from './rubricstep.js';
90
+ import { stepFileContract, inputLabelsOf, stepFileRel, readFrozenInputs, withFactBindings, settlePersonsWords, withDropNotes, lineRules } from './rubricstep.js';
91
+ import { dropRows, dropLine } from './pagedropwords.js';
91
92
  import { factShapeGuide } from './facts.js';
92
93
  import { renderLookReport, labelListsOf } from './inputlook.js';
93
94
  import { checkDeclarationWords } from './factwords.js';
@@ -116,8 +117,8 @@ export const AUTHOR_MAX_TOKENS = 32000;
116
117
  /** the one slot the model fills on an injected guard */
117
118
  export const FILL_MARKER = '<FILL IN>';
118
119
 
119
- /** P7 item 5 (fwdloop am24/25): the most questions the MACHINE keeps from the try-3 drafter. The drafter is never told this number. */
120
- export const MAX_QUESTIONS = 2;
120
+ /** 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. */
121
+ export const MAX_QUESTIONS = 3;
121
122
 
122
123
  /**
123
124
  * The model-visible words of the optional `questions` property — offered ONLY on the last drafting try and only after the
@@ -158,10 +159,10 @@ export function linesNamedByReds(reds, declaration, lineNums) {
158
159
  * 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
159
160
  * text is non-blank, at most {@link MAX_QUESTIONS}; every drop comes back with its reason, for the draft's log.
160
161
  * @param {any} raw @param {number[]} failedLines
161
- * @returns {{kept: {line: number, question: string}[], dropped: {reason: string, raw: any}[]}}
162
+ * @returns {{kept: {line: number, question: string, rule?: number, source?: string, ask?: 'reading'|'cant-check', reading?: string}[], dropped: {reason: string, raw: any}[]}}
162
163
  */
163
164
  export function normalizeQuestions(raw, failedLines) {
164
- /** @type {{line: number, question: string}[]} */
165
+ /** @type {{line: number, question: string, rule?: number, source?: string, ask?: 'reading'|'cant-check', reading?: string}[]} */
165
166
  const kept = [];
166
167
  /** @type {{reason: string, raw: any}[]} */
167
168
  const dropped = [];
@@ -172,7 +173,13 @@ export function normalizeQuestions(raw, failedLines) {
172
173
  if (!failedLines.includes(q.line)) { dropped.push({ reason: `line ${q.line} is not a line the previous try's checks failed on`, raw: q }); continue; }
173
174
  if (typeof q.question !== 'string' || q.question.trim() === '') { dropped.push({ reason: 'blank question', raw: q }); continue; }
174
175
  if (kept.length >= MAX_QUESTIONS) { dropped.push({ reason: 'beyond the machine\'s limit', raw: q }); continue; }
175
- kept.push({ line: q.line, question: redactSecrets(q.question.trim()) });
176
+ kept.push({
177
+ line: q.line, question: redactSecrets(q.question.trim()),
178
+ // PRD v1.106 Part 5 Q3: a code-forced question (the early look ask) names the `~` rule it is about and says so; a model's question has neither
179
+ ...(Number.isInteger(q.rule) ? { rule: q.rule } : {}), ...(typeof q.source === 'string' ? { source: q.source } : {}),
180
+ // PRD v1.107 Part 5b: the kind of a code-forced ask, and (reading asks) the plain words `agree` writes as the `~` rule
181
+ ...(q.ask === 'reading' || q.ask === 'cant-check' ? { ask: q.ask } : {}), ...(q.ask === 'reading' && typeof q.reading === 'string' ? { reading: redactSecrets(q.reading) } : {}),
182
+ });
176
183
  }
177
184
  return { kept, dropped };
178
185
  }
@@ -838,8 +845,8 @@ export const PARAM_SCHEMAS = Object.freeze({
838
845
  properties: {
839
846
  name: { type: 'string', minLength: 1, description: 'what the fact is called: letters and spaces only, no digits' },
840
847
  shape: { enum: ['money', 'number', 'date', 'time', 'phrase', 'text'], description: 'money / number / date / time may carry a rule; phrase = fixed words the quote must contain; text = words the quote holds, as it writes them' },
841
- unit: { type: 'string', minLength: 1, description: 'money: the words the input writes after the amount, copied verbatim (or give currency instead)' },
842
- currency: { enum: ['$', '€', '£'], description: 'money: the symbol the input writes before the amount (or give unit instead)' },
848
+ unit: { anyOf: [{ type: 'string', minLength: 1 }, { type: 'array', minItems: 1, maxItems: 10, uniqueItems: true, items: { type: 'string', minLength: 1 } }], description: 'money: the words the input writes after the amount, copied verbatim - one, or a list of up to 10 (give unit, currency, or both)' },
849
+ currency: { anyOf: [{ enum: ['$', '€', '£'] }, { type: 'array', minItems: 1, maxItems: 3, uniqueItems: true, items: { enum: ['$', '€', '£'] } }], description: 'money: the symbol the input writes before the amount - one, or a list of up to 3 (give unit, currency, or both)' },
843
850
  phrase: { type: 'string', minLength: 1, description: 'phrase only: the fixed words, copied verbatim from the input' },
844
851
  anchor: { type: 'string', minLength: 1, description: 'a label copied VERBATIM from the input that sits right before the value (picks the right one when a result holds several)' },
845
852
  until: { type: 'string', minLength: 1, description: 'text with an anchor: the label copied VERBATIM that ends the value' },
@@ -1433,6 +1440,11 @@ export const REQUIRED_FACTS_BRIEF = 'REQUIRED FACTS — what a line that finds,
1433
1440
  + 'as the look gives it (the look\'s phrase facts become phrase facts with the same words), and say in "notes" when you used the input\'s '
1434
1441
  + 'words instead of the person\'s. A label (anchor) is PICKED from the labels the look found in the input, never typed; one that is not '
1435
1442
  + 'among them is refused at $0. What the look could not find is not checked: list it under "refused" in the person\'s words.\n'
1443
+ + ' - A money fact names at least one of "unit" (the words the input writes after the amount) or "currency" (a sign: $, € or £), and may name both. Each is one value or a list: '
1444
+ + 'at most 3 currencies and 10 units, none repeated, a unit at most 100 characters. A code like EUR is a unit (it is read after the number), never a currency. '
1445
+ + 'Copy the forms the look gives for the fact, in its order (the first is the main one); a form the look does not give is refused at $0.\n'
1446
+ + ' - A word the look lists under NOT CHECKED or LEFT OUT is never used in a fact - not as a label, a closing label, a unit or a currency - and is refused at $0 with the reason if you do. '
1447
+ + 'A fact or ~ rule listed under NOT CHECKED is not turned into a check: code lists it under Not checked for the person, so you do not write it into "refused" yourself.\n'
1436
1448
  + ' - When one result holds several values of one kind (leaves / arrives, subtotal / total) give the fact an anchor, the label that sits right before the value you mean.\n'
1437
1449
  + ' - Set aiProposal true on a fact whose number you made out of their words.\n'
1438
1450
  + RUBRIC_RULES + '\n'
@@ -2378,7 +2390,7 @@ export async function runConfirmTurn({
2378
2390
  for (let i = 0; i < qs.length; i += 1) {
2379
2391
  const question = qs[i];
2380
2392
  for (;;) {
2381
- const raw = await ask({ kind: 'answer', field: ANSWER_QUESTION_FIELD, question, index: i + 1, total: qs.length });
2393
+ const raw = await ask({ kind: 'answer', source: 'confirm', field: ANSWER_QUESTION_FIELD, question, index: i + 1, total: qs.length });
2382
2394
  if (raw === null) return null;
2383
2395
  const trimmed = redactSecrets(String(raw).trim());
2384
2396
  if (trimmed === '') continue; // blank is not an answer — re-ask the same question
@@ -2426,10 +2438,13 @@ export async function runConfirmTurn({
2426
2438
  // the list the model was prompted with is byte-identical to the list
2427
2439
  // shown at confirm-done and recorded on acceptance. `notChecked` is the
2428
2440
  // model's own honest gap list, carried through unchanged.
2441
+ // NO SILENT DROP (idea A core, spec 4.5): what the page rules removed is said HERE too, in the same words as the signed plan, before
2442
+ // drafting is paid for. The words come from the one table (src/pagedropwords.js).
2429
2443
  const notChecked = [...(r.plan.notChecked ?? [])];
2444
+ for (const row of dropRows(facts?.look?.dropped)) { const text = dropLine(row, jobLines, lineRules); if (!notChecked.includes(text)) notChecked.push(text); }
2430
2445
  const checkItems = normalizeChecks(r.plan.checks);
2431
2446
  r.plan = { ...r.plan, checks: checkItems.map((c) => c.text), checkItems };
2432
- const plan = { ...r.plan, protections, notChecked };
2447
+ const plan = { ...r.plan, protections, notChecked, signedFacts: (Array.isArray(facts?.look?.facts) ? facts.look.facts : []).map((/** @type {any} */ f) => String(f?.name)) };
2433
2448
  onPhase('confirm-done', { round, plan });
2434
2449
  const picked = await ask({ kind: 'menu', field: CONFIRM_MENU, plan });
2435
2450
  if (picked === null) return abandon(round);
@@ -2941,12 +2956,12 @@ export async function authorClose({
2941
2956
  previous = ask.declaration;
2942
2957
 
2943
2958
  // PART 1b: code puts the number tie on every prose step that follows a fact step, before anything is validated or signed
2944
- let v = validateDeclaration(withFactBindings(settlePersonsWords(ask.declaration, jobLines)), { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines, inputLabels: inputLabelsOf(inputs) });
2959
+ let v = validateDeclaration(withFactBindings(withDropNotes(settlePersonsWords(ask.declaration, jobLines), facts?.look?.dropped, jobLines)), { catalogue, listing: seedFiles, guards, envOwned: ownedEnvNames, verdictType, writeScope, jobLines, inputLabels: inputLabelsOf(inputs) });
2945
2960
  // PART 1c: code proves, at $0, that every label is PICKED from the input's own list and every word a fact depends on is really
2946
2961
  // in the source (white-space blind, once is enough). A word that is not found is a red naming the word and the input; the
2947
2962
  // drafter revises like it does for any validation red.
2948
2963
  if (v.ok && lang === RUBRIC_LANG) {
2949
- const words = checkDeclarationWords(v.declaration, inputTexts);
2964
+ const words = checkDeclarationWords(v.declaration, inputTexts, facts?.look ?? null);
2950
2965
  if (words.length > 0) v = { ...v, ok: false, reds: [...v.reds, ...words], declaration: null };
2951
2966
  }
2952
2967
  // SCRUBBED HERE, once, where the reds enter this module's records — the same
package/src/authoring.js CHANGED
@@ -355,7 +355,7 @@ export const KIND_CATALOGUE = Object.freeze({
355
355
  shape: 'step: integer (the job line number), parts: [{text: string, class: "countable" | "fact" | "judged" | "guard" | "none", '
356
356
  + 'check?: "maxWords" | "minWords" | "maxLines" | "sections" | "sectionWords" | "cited" | "minClaims" | "maxClaims", value?: integer, names?: string[], '
357
357
  + 'wordsPerSection?: integer, source?: string (an input label), domains?: string[], covers?: integer[], rules?: integer[], '
358
- + 'fact parts: facts?: [{name, shape: "money"|"number"|"date"|"time"|"phrase"|"text", unit?|currency?, phrase?, anchor?, until?, '
358
+ + 'fact parts: facts?: [{name, shape: "money"|"number"|"date"|"time"|"phrase"|"text", unit?|currency? (each one value or a list; money names at least one), phrase?, anchor?, until?, '
359
359
  + 'rule?: {op, value | lo+hi}, aiProposal?: boolean}], count?: {min?, max?}}]',
360
360
  asserts: 'the file output/<step>.json under output/steps/ exists, is a JSON object, and its "text" is present and within the '
361
361
  + 'limits the person asked for. Write ONE rubric-step stage per numbered job line (step = that line number, fromLine = [that line]). '
package/src/authorjob.js CHANGED
@@ -100,7 +100,7 @@
100
100
 
101
101
  import { jobSpecHash, validateJob } from './job.js';
102
102
  import { seedAtHead, seedListing, seedRead as runSeedRead, makeSeedTrees, judgedStages, closeJudges } from './kinds.js';
103
- import { inputLabelsOf, readFrozenInputs } from './rubricstep.js';
103
+ import { inputLabelsOf, readFrozenInputs, lineRules } from './rubricstep.js';
104
104
  import { proverOf } from './factwords.js';
105
105
  // SOFTGREEN modules 4 and 5 — the compile the interview's Q6/Q7 answers land in,
106
106
  // and the gate that decides whether the ruler they describe is a ruler.
@@ -113,12 +113,13 @@ import {
113
113
  } from './authoring.js';
114
114
  import {
115
115
  QUESTION_SETS, questionsFor, requiredAnswersFor, authorClose, makeCostBook,
116
- runConfirmTurn, LANGUAGE_PICK_FIELD, normalizeQuestions,
116
+ runConfirmTurn, LANGUAGE_PICK_FIELD, normalizeQuestions, MAX_QUESTIONS,
117
117
  } from './authorflow.js';
118
- import { ruleQuestions } from './inputlook.js';
119
118
  import { thinkingCallOptions, assertThinkingMode } from './thinking.js';
119
+ import { runFitCheck, fitQuestionText, notFoundList, isGoOn } from './fitcheck.js';
120
120
  import { pageContentChars } from './pagefetch.js';
121
- import { runAuthorScout, runInputLook, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
121
+ import { runAuthorScout, buildSeedListing, SCOUT_ATTEMPTS } from './authorscout.js';
122
+ import { runPageMap, askableFromLook, tooManyAsksText } from './pagemap.js';
122
123
  import {
123
124
  DECLARED_GAP_PREFIX, DECLARED_GENRES, guardNames, isDeclaredClose, validateCloseDecl,
124
125
  } from './declaredclose.js';
@@ -429,12 +430,13 @@ function composerRefusal(reds) {
429
430
  * rates?: {in: number, out: number}|null, draftThinking?: 'on'|'off',
430
431
  * onPhase?: (phase: string, data?: any) => void,
431
432
  * onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number, thinkingStatus?: string}) => void,
432
- * seedFn?: Function, scoutFn?: Function, listingFn?: Function, lookFn?: Function|null,
433
+ * seedFn?: Function, scoutFn?: Function, listingFn?: Function, lookFn?: Function|null, fitFn?: Function|null,
434
+ * fitAsk?: ((question: string) => Promise<string|null>)|null, onFit?: (fit: {state: string, asks: any[], notFound: any[]}) => void,
433
435
  * authorFn?: Function, authorOpts?: object, writeScope?: string[]|null,
434
436
  * signerFix?: Function|any, proposeFn?: Function, compileOpts?: object,
435
437
  * ask?: ((step: {kind: string, [k: string]: any}) => Promise<string|null>)|null,
436
- * answerQuestions?: ((o: {questions: {line: number, question: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
437
- * Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>}|null>)|null,
438
+ * answerQuestions?: ((o: {questions: {line: number, question: string, rule?: number, source?: string, ask?: string, reading?: string}[], dropped: {reason: string, raw: any}[], confirmed: any}) =>
439
+ * Promise<{jobLines: {n: number, text: string, rule?: string}[], answers: Record<string|number, any>, left?: {line: number, rule: number}[]}|null>)|null,
438
440
  * confirmGenerate?: Function|null, isRepo?: boolean,
439
441
  * langResult?: {kind: string, candidates?: string[], [k: string]: any}|null}} o
440
442
  * @returns {Promise<{ok: boolean, refusal: Refusal|null, verdictType: string|null,
@@ -470,9 +472,12 @@ export async function authorCloseForJob({
470
472
  // callback back, and no decision in this file or below it depends on one.
471
473
  onPhase = () => {}, onCall = () => {},
472
474
  seedFn = seedAtHead, scoutFn = runAuthorScout, listingFn = buildSeedListing,
473
- // PART 1c - the look at a rubric job's inputs (src/authorscout.js `runInputLook`). `null` is an explicit "no look" (a test seam, like
474
- // a caller-supplied `scout`); the shipped default is the real, paid, read-only look.
475
- lookFn = runInputLook,
475
+ // PART 1c / IDEA A CORE - the look at a rubric job's inputs: the default is the page-word map (src/pagemap.js `runPageMap`, one paid AI
476
+ // call per page input, its words kept by code). `null` is an explicit "no look" (a test seam, like a caller-supplied `scout`).
477
+ lookFn = runPageMap,
478
+ // THE FIT CHECK (F243): one paid call BEFORE the look, rubric jobs only. It runs only when the caller supplies the answer seam `fitAsk` (the panel);
479
+ // `fitFn` null is an explicit "no check" (a test seam). With no `fitAsk` (the CLI door) it never runs.
480
+ fitFn = runFitCheck, fitAsk = null, onFit = () => {},
476
481
  authorFn = authorClose, authorOpts = {},
477
482
  // SOFTGREEN modules 4+5. `signerFix` is the D5 seam: a function (or a value)
478
483
  // carrying the SIGNER's corrected artifacts back. Absent means the proposal is
@@ -613,6 +618,25 @@ export async function authorCloseForJob({
613
618
  // nothing to check a rule against. Plain words, before any model call.
614
619
  survey = { state: 'ABSENT', reason: 'your input holds nothing to read (it is empty, or only the site\'s own header and menus) - nothing was asked and nothing was spent', cause: 'call-failed', budgetStop: null, calls: [], raws: [], facts: null };
615
620
  } else if (frozen !== null && lookFn) {
621
+ if (fitAsk && fitFn) {
622
+ // F243: a page that does not show what the job asks for stops here, before the look spends. Failure is OPEN: an `absent` check
623
+ // (cut, unreadable twice, call failed, cap) asks nothing and drafting goes on; the answer is NOT a `~` rule and changes nothing in the job.
624
+ onPhase('fit-check', {});
625
+ const fitCallOptions = thinkingCallOptions(provider, draftThinking);
626
+ const fit = await fitFn({
627
+ jobLines, texts: frozen.texts, provider, ceilingUsd, onCall, ...(rates ? { rates } : {}),
628
+ ...(Object.keys(fitCallOptions).length ? { callOptions: fitCallOptions } : {}),
629
+ });
630
+ onPhase('fit-check-done', { state: fit.state });
631
+ onFit({ state: fit.state, asks: fit.asks, notFound: fit.notFound });
632
+ if (fit.state === 'ask') {
633
+ const answer = await fitAsk(fitQuestionText({ found: fit.asks.filter((/** @type {any} */ a) => !a.ignored && a.found).length, total: fit.asks.filter((/** @type {any} */ a) => !a.ignored).length, notFound: fit.notFound }));
634
+ if (answer === null) return { ...base, interview, verdictType: picked, seedRef: seed, reds: [], stop: 'confirm-abandoned', cost: null };
635
+ if (!isGoOn(answer)) {
636
+ return { ...base, interview, verdictType: picked, seedRef: seed, reds: [{ code: 'fit-stopped', path: 'jobLines', detail: notFoundList(fit.notFound) }], stop: 'fit-stopped', cost: null };
637
+ }
638
+ }
639
+ }
616
640
  onPhase('look', {});
617
641
  const lookCallOptions = thinkingCallOptions(provider, draftThinking);
618
642
  survey = await lookFn({
@@ -645,12 +669,18 @@ export async function authorCloseForJob({
645
669
  onPhase('scout-done', { state: survey?.state ?? null, facts: Object.keys(survey?.facts ?? {}).length });
646
670
  }
647
671
 
648
- // PART 1c, I1 (hamr 2026-10-13): the look found that the person's intent on a particular `~` rule was NOT captured (its words or
649
- // meaning are not in the source) - ask about THAT rule at once, before any plan or draft is paid for. The question gate is the
650
- // existing one (`answerQuestions`: answer required, no skip, the answer becomes a `~` rule on its line; `normalizeQuestions` keeps
651
- // at most MAX_QUESTIONS, each on a line the look named). Everything else the look could not find asks nothing piecemeal.
652
- if (answerQuestions && lang === RUBRIC_LANG && survey?.state === 'PRESENT' && Array.isArray(survey.facts?.look?.notFound) && Array.isArray(jobLines)) {
653
- const candidates = ruleQuestions(survey.facts.look.notFound, jobLines);
672
+ // PRD v1.106 Part 5 Q3 (T2) + v1.107 Part 5b: CODE forces the early asks, from the look's own result, before any plan or draft is paid for
673
+ // (src/pagemap.js `askableFromLook`; the model never decides whether to ask). Two kinds: a READING the code made of a `~` rule whose limit the
674
+ // person did not write (agree / type their own), and a rule the page cannot check (drop it / type how). More than MAX_QUESTIONS asks is a job to
675
+ // rewrite, not a questionnaire: nothing is asked, nothing more is spent, and the stop lists every ask with its reading. The question gate is the
676
+ // existing one (`answerQuestions`: answer required, no skip, the answer becomes a `~` rule on its line; `normalizeQuestions` keeps at most
677
+ // MAX_QUESTIONS). The answer "drop it" adds no rule: the rule is put on the look's own dropped list, so it reads Not checked on the
678
+ // confirm card, in the signed plan and in the drafter's block, and can never be written back as a check.
679
+ if (answerQuestions && lang === RUBRIC_LANG && survey?.state === 'PRESENT' && survey.facts?.look && typeof survey.facts.look === 'object' && Array.isArray(jobLines)) {
680
+ const candidates = askableFromLook(survey.facts.look, jobLines);
681
+ if (candidates.length > MAX_QUESTIONS) {
682
+ return { ...base, interview, verdictType: picked, seedRef: seed, reds: [{ code: 'look-asks-too-many', path: 'jobLines', detail: tooManyAsksText(candidates) }], stop: 'look-asks-too-many', cost: null };
683
+ }
654
684
  const kept = normalizeQuestions(candidates, [...new Set(candidates.map((q) => q.line))]);
655
685
  if (kept.kept.length > 0) {
656
686
  onPhase('look-ask', { questions: kept.kept.length });
@@ -658,6 +688,12 @@ export async function authorCloseForJob({
658
688
  if (updated === null) return { ...base, interview, verdictType: picked, seedRef: seed, reds: [], stop: 'confirm-abandoned', cost: null };
659
689
  jobLines = updated.jobLines;
660
690
  interview.answers = updated.answers;
691
+ for (const lf of Array.isArray(updated.left) ? updated.left : []) {
692
+ const ruleText = lineRules(jobLines.find((l) => l.n === lf.line) ?? {})[lf.rule - 1];
693
+ if (ruleText === undefined) continue;
694
+ const look = survey.facts.look;
695
+ look.dropped = [...(Array.isArray(look.dropped) ? look.dropped : []), { line: lf.line, rule: lf.rule, fact: ruleText, reason: 'person-left', source: 'person' }];
696
+ }
661
697
  }
662
698
  }
663
699