docs(major2): invariantraden - eksperten svarer paa forslaget paa bordet, og svaret brukes

CLAUDE.md: en invariantrad etter verdict-gate-raden, i husets skjelett - nevneren
(grep "prior_" finner EN soem, maskinens egen), D6 og hvorfor alternativet ble
forkastet, den ledger-bevisste attempts_remaining, honoured = "hentet faktisk",
kanalvalget begrunnet av tre maalinger, den kaller-eide sinken, skriv-iff-reviewer,
de fem nektene ved navn og den hostede pre-whitelist-sjekken. Load-bearing-blokka
lister M1-M40 med roedtall, M29 staar som et FUNN (uvitnet baerer), og de to
armene som var groenne av feil grunn i steg 1-8 er skrevet ned som repoets
vakuoes-gate-klasse, sekstende og syttende gang. Aerlighets-grensene til slutt.

README.md: --proposal-review i enkeltprosjekt-flagglista og en prosablokk etter
--plan-review/--checkpoint-dir-paret, i samme form - hva operatoeren ser og
skriver, at en revise KJOEPER ett forsoek til under de eksisterende takene, at
approve ikke er en ekspertdom, og alle fem nektene navngitt (--checkpoint-dir-en
peker paa --resume, doera som VIRKER). Kundevendt vokabular.

Ny arm: test_the_readme_block_names_every_flag_the_cli_refuses_the_door_with,
Fase-3-formen (raa tekst, uttrukket blokk, kontroll paa at uttrekkeren finner noe
som finnes). Intet M-nummer - lista lukket ved M40 - saa den ble drevet ROED TO
ganger: mot README-en foer blokka fantes, og med blokka paa plass men
--checkpoint-dir omskrevet til aa beskrive nekten uten aa navngi flagget.

1364 passed / 5 skipped. Golden demo-transcript.stdout BYTE-UENDRET
(shasum -a 1 av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). Node-ID-ene
er et strengt supersett: 1319 -> 1369, 0 fjernet. mypy og ruff rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 20:40:16 +02:00
commit 4a3655fc07
3 changed files with 211 additions and 1 deletions

119
CLAUDE.md
View file

@ -1839,6 +1839,125 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
nekten er nøklet på den ENE erklærte typen ExpeL-folden eier, aldri på et bredere nekten er nøklet på den ENE erklærte typen ExpeL-folden eier, aldri på et bredere
«ikke i `context_files`»-komplement, som M4 viser ville rammet `index.md` — navigasjon, ikke en «ikke i `context_files`»-komplement, som M4 viser ville rammet `index.md` — navigasjon, ikke en
dom. Måling: `docs/2026-09-04-s2c-debatt-k2.md` § 8. dom. Måling: `docs/2026-09-04-s2c-debatt-k2.md` § 8.
- **Eksperten kan svare på forslaget som ligger på BORDET — og svaret BRUKES, det registreres ikke
bare (MAJOR-2, økt 8889):** før dette fantes ÉN menneske-søm inne i løkka, `plan_reviewer`, og
den fyrer FØR noen hypotese finnes. **Nevneren er målt** (`docs/2026-09-02-misjonsreview-v2.md`
§ 3, re-målt mot HEAD): `grep -n "prior_" generate.py` finner nøyaktig ÉN tilbakemeldings-søm,
`prior_rejection` — MASKINENS egen falsifisering matet tilbake i samme kjøring. Et menneskes ord
landet først i NESTE kjøring (Steg-7-innboksen), så «improved proposals given feedback» var, for
en person, en to-kjørings løkke med dager imellom. `--proposal-review` viser kandidaten
validatoren nettopp AKSEPTERTE og leser `approve` eller `revise <hva>`; en `revise` **kjøper ETT
forsøk til under de EKSISTERENDE takene** (ingen ny løkke — `max_attempts` + `meter.tick_round`,
som Steg 5), og ordene går ORDRETT inn i det forsøkets prompt som en tredje komponerbar blokk
ved siden av `prior_rejection`.
**D6: validatorens SISTE dom vinner, aldri revieweren sin.** Alternativet (falle tilbake til det
sist VALIDERTE forslaget når oppfølgingen blir avvist) ble forkastet fordi det ville latt et
menneskes ønske om ett forsøk til FORBEDRE utfallet uten at noen falsifiserer sa ja — mennesket
er en tredje stemme som gater INGENTING utover å be om ett forsøk til. `M27` er dét vitnet.
**Ekspertens ord er KLEBRIGE, maskinens grunn er PER FORSØK:** `feedback` står til mennesket
svarer neste gang, `prior_rejection` overskrives hver runde (M4/M5, hver sin ene røde arm).
**`attempts_remaining` er LEDGER-BEVISST, ikke `max_attempts`-avledet:**
`min(max_attempts - i - 1, meter.budget.max_rounds - meter.rounds)` — den DELTE rundeboka
(`max_rounds*4` på run-nivå) spenner over hver tilnærming i en kommisjon og er ofte dét som
binder, så et tall regnet fra forsøkstaket alene ville vært en påstand terminalen gjør om seg
selv som måleren straks gjendriver (M38 → 2 røde, BEGGE navngir M38 i sin docstring).
**`honoured` betyr «forsøket som ble kjøpt HENTET faktisk et svar», ikke «ble kjøpt»:** posten
føres `False` og forfremmes først når `_fetch_parsed` har RETURNERT, så en revise rundeboka kappet
før den står `false` ved siden av `rounds limit=2 observed=3` (M40 → 2 røde).
**Kanalen er valgt av TRE målinger, ikke av smak:** `ProposalReviewInputError` er en
`RuntimeError` fanget VED NAVN på CLI-ens fullkjørings-dispatch og printet på en egen
`run stopped:`-linje med rc 1. En `ValueError` ville (a) landet på `run refused:`-tuppelen, som
MISLABLER en kjøring der argv var riktig og tokens allerede var brukt, og (b) — verre — blitt
fanget av `_fetch_parsed` ETT frame unna som en PARSE-feil, altså føyd til `parse_failures`
ordrett og modellen re-kalt til rundeboka fyrte. EOF er ALDRI en signatur (M15/M16).
**Sinken er KALLER-EID** (funn-1-formen, `parse_failures`-presedensen): `run_project` eier lista,
nøkler hver post på `(approach_id, attempt)``OWN_PROPOSAL_ID` for systemets eget forslag, så
to tilnærminger aldri kollapser på én rad — og skriver `{run_id}-proposal-reviews.json` fra en
**`finally`**. En returverdi ville vært tapt på nøyaktig den kjøringen som trenger beviset: et
budsjett-stopp inne i genereringen konstruerer aldri et `GenerationResult` (M35 → T9 alene).
**Skrive-regelen er «iff en reviewer ble gitt, OGSÅ når lista er tom»** (D4) — `write_debate_tools`'
regel, av samme grunn: en reviewer ingen rakk å spørre er dét som må kunne LESES, ikke utledes av
en fil som ikke er der (M12/M13). Uten reviewer er utboksen BYTE-IDENTISK (M13 → 3 røde, hvorav
to i tester eldre enn dette arbeidet).
**Fem nekter, alle VED NAVN**, og plasseringen er MÅLT: blokka ligger på FUNKSJONS-nivå rett etter
mode-dispatchen, aldri nøstet under `if args.scripted_replies` eller `if args.explore` — under
hver av dem ville et bart `--live-dry-run --proposal-review` falt rett gjennom til dry-run-
dispatchen og droppet flagget. `--portfolio` (bølgene deler ÉN terminal), `--report`
(returnerer OVER hver dispatch, så en utelatelse er et stille DROPP — F4-gapet),
`--live-dry-run`, `--proposals-from-mandate` og `--checkpoint-dir` («send `--proposal-review` ved
`--resume` i stedet» — en nekt som bare forbyr etterlater operatøren uten døra som VIRKER).
Den hostede flaten nekter ved navn som en **pre-whitelist rå-payload-sjekk**, ikke en fjerde
liste-oppføring: `_CONSUMED_FIELDS` må være disjunkt fra `run_project`s parametre (Fase 4es
negative halvdel) mens `proposal_reviewer` ER en av dem, og den generiske
`unknown field(s)`-nekten fyrer FØRST — et navn overlatt til den ville gitt samme 400 med en
melding som ikke sier hvor døra faktisk er. **F2 står: en `approve` mynter INGEN `Verdict`**
(M14), og `provenance.validator_decision` speiler fortsatt KUN validatoren.
**Load-bearing MÅLT** (`tests/test_proposal_review_loop_loadbearing.py`, 49 armer),
**M1M40 i ÉN liste, 41 kjøringer (M26 er to lengder), 40 RØDE mot HELE suiten** + grønn kontroll
**1364/5** og golden `demo-transcript.stdout` BYTE-UENDRET (`shasum -a 1` av INNHOLDET =
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`, ALDRI git-blob-id-en): M1 detach reviewer-kallet (16) ·
M2 alltid approve (13) · M3 print-and-discard av feedback (6) · M4 ikke-klebrig feedback (1) ·
M5 akkumuler avvisninger (1) · M6 vis også en AVVIST kandidat (4) · M7 revise tikker ikke
rundeboka (1) · M8 revisjoner løkker UTENFOR `max_attempts` (36, 18 eldre testfiler) ·
M9 `honoured` konstant `True` (6) · M10 `approach_id` alltid `None` (3) · M11 skriv etter returen
i stedet for fra `finally` (2) · M12 skriv kun ved ikke-tom liste (2) · M13 skriv også uten
reviewer (3) · M14 la `approve` mynte en `Verdict` (1) · M15 EOF er approve (2) · M16 fang ikke
feilen ved navn (1) · M17 alt som ikke er revise er approve (1) · M18 drop `single_only`-raden (1)
· M19 drop `report_forbidden`-raden (1) · M20 bibliotekssømmen wiret, argparse-flagget borte (9) ·
M21 strømmene bundet ved konstruksjon (1) · M22 slett den hostede nekten (1) · M23 videresend det
hostede feltet (4) · M24 fersk reviewer per base (1) · M25 render kandidaten som repr (2) ·
M26a trunker feedback til 20 tegn (5) · M26b til 40 tegn, sentinelen OVERLEVER (3) · M27 D6-
alternativ b (1) · **M29 reverter exit-kontrakten (0 — se under)** · M30 drop
`--live-dry-run`-nekten (1) · M31 drop `--proposals-from-mandate`-nekten (1) · M32 rendereren
skriver linja uten reviewer (2) · M33 rendereren returnerer `None` på null (3) · M28 drop
`verdict_key` fra posten (2) · M34 feedback løftet ut av per-approach-kallet (5) · M35 returnert
liste i stedet for kaller-eid sink (1) · M36 dispatcheren slutter å tråde revieweren (1) ·
M37 detach CLI-utskriften mens rendereren returnerer linja (2) · M38 `attempts_remaining` fra
`max_attempts` alene (2) · M39 drop `--checkpoint-dir`-nekten (1) · M40 `honoured=True` ved
revise-tid (2).
**ÉN MUTASJON FORBLIR GRØNN, OG DET ER ET FUNN — IKKE EN GATE:** M29 reverterer `last_ruling` til
`assert last is not None`, og HELE suiten står grønn. Planens «Critical risk #1» — at
«validert → revise» på hvert forsøk ville nå halen med `last is None` — er dermed **FALSIFISERT AV
MÅLINGEN**: D1(a) RETURNERER inne i løkka når `remaining == 0`, og på siste forsøk er
`max_attempts - i - 1` alltid 0, så halen er nåbar KUN etter en validator-AVVISNING, som setter
`last` også. Bæreren er derfor **UVITNET** (`budget_stop`-presedensen) — den står fordi den gjør
D6 eksplisitt og fjerner en assert som hviler på en ikke-lokal invariant, ikke fordi en test
holder den. Kommentaren i `generate.py` som PÅSTO at asserten ville fyrt er rettet i samme
slengen (Fase-3-klassen: en påstand flaten gjør om seg selv); commit-teksten for `bbf4d3b` bærer
fortsatt den gamle påstanden, og historikken skrives ikke om.
**TO ARMER VAR GRØNNE AV FEIL GRUNN (repoets vakuøs-gate-klasse, SEKSTENDE og SYTTENDE gang),
begge funnet i steg 18:** (i) `report_forbidden`-armens ledger-fikstur var et JSON-OBJEKT, som
`SavingsLedger.load` uansett nekter — rc 1 kom fra fiksturen, ikke fra raden; den er nå en JSON-
ARRAY med en rc-0-kontroll på en argv report-modus ellers ville AKSEPTERT. (ii) «offered, never
consulted»-armen kjørte et TO-stegs manus mot `max_attempts=3`, så det tredje forsøket falt
gjennom til selectorens default-svar, som aldri parser — rundeboka fyrte og armen var grønn av en
helt annen mekanisme; manuset har nå like mange oppføringer som `max_attempts`.
**README-armen har intet M-nummer** (lista lukket ved M40) og ble drevet RØD TO ganger, fordi
«blokka mangler» og «blokka er ufullstendig» er ulike feil og bare den andre er dét gaten finnes
for: først mot README-en slik den STO (uttrekket fant ingenting), så med blokka på plass men
`--checkpoint-dir`-setningen omskrevet til å beskrive nekten uten å NAVNGI flagget. Kontrollen er
`--plan-review`-blokka, som har eksistert siden F4.
**K2-omkjøringen (kriterium 8), samme base og samme syntetiske prisfikstur som S7b/S2c**,
instrumentet validert mot en KJENT POSITIV FØR bruk (rotnivå-listingen på levert K2: 3 954 tegn /
1 495 o200k-tokens over 629 konsepter, S7a-3s publiserte tall reprodusert eksakt):
utforskningen **12 prompter / 18 355 tokens UENDRET til tokenet**, debatt-promptene **UENDRET**
(proposer 2 → 2 à 314 tokens, checker 1 → 1 à 199), genererings-promptene **2 → 4** (én ekstra per
tilnærming), totalt **17 → 19 prompter og 19 274 → 19 776 tokens (+2,6 %)**. Ekspertens ord står
ORDRETT i nøyaktig de to andre-forsøks-promptene (2 av 6), utfallet flytter seg **200 000 →
150 000 NOK** og dom-nøkkelen `be8535e204cdc4c6``f23ecff85f4188b5`, mens
`{run_id}-proposal-reviews.json` bærer feedbacken ordrett med `honoured: true`.
**Ærlighets-grenser, uttalt:** Steg 5 UNDER-rapporterer på en revidert kjøring (en revise bruker
ett av de SAMME `max_attempts`-forsøkene, så `refinements` kan mangle en falsifisering som aldri
rakk å skje); `coverage`-radene navngir ALDRI en revise som årsak til en `not_evaluated`
(rundeboka er delt — sann, og `coverage` er et non-goal); feedback-teksten er UBEGRENSET som
plan-review-feedbacken, belastet det bundne `max_tokens`; checker-gaten og dimensjons-gaten
overstyrer ETTER at genereringen har returnert, så en `approve` registrert ordrett kan sitte på en
kjøring hvis utfall er en checker-kildet `Rejection` — posten sier hva mennesket SÅ og SVARTE,
aldri hva kjøringen konkluderte, og å blande dem er nøyaktig dét to-falsifiserer-raden forbyr;
F4s traceback-asymmetri er URØRT (`PlanReviewInputError` forlater fortsatt CLI-en som traceback);
`run_portfolio` tar INGEN reviewer, og fraværet er ASSERTERT; K2-prisene er SYNTETISKE og ingen
LEVENDE modell har svart på en review (structured-output-grensens klasse). Måling:
`docs/2026-09-04-major2-proposal-review-k2.md`.
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet. - **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase. - Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.

View file

@ -416,7 +416,8 @@ when the seam is detached, so the loop cannot silently degrade into theater.
cannot exercise every flag: cannot exercise every flag:
- **Single-project**`PROJECT_ID --docs-dir <dir>`, plus optional `--bundle-dir`, - **Single-project**`PROJECT_ID --docs-dir <dir>`, plus optional `--bundle-dir`,
`--verdict-dir`, `--outbox-dir` (which requires `--run-id`), `--dimension-config`, `--verdict-dir`, `--outbox-dir` (which requires `--run-id`), `--dimension-config`,
`--semantic-retrieval`, `--decision`/`--rationale`, `--live-dry-run`, and `--semantic-retrieval`, `--decision`/`--rationale`, `--live-dry-run`, `--proposal-review`
(opt-in: answer the proposal the validator just accepted, at your terminal — see below), and
`--scripted-replies <file>` (the offline whole-loop door — see `--scripted-replies <file>` (the offline whole-loop door — see
[Walk the whole chain offline](#walk-the-whole-chain-offline); mutually exclusive with [Walk the whole chain offline](#walk-the-whole-chain-offline); mutually exclusive with
`--live-dry-run`, which stops before the first model call rather than answering it), and `--live-dry-run`, which stops before the first model call rather than answering it), and
@ -511,6 +512,44 @@ when the seam is detached, so the loop cannot silently degrade into theater.
`--checkpoint-dir` are refused together (two doors onto one review), as are `--resume` and `--checkpoint-dir` are refused together (two doors onto one review), as are `--resume` and
`--explore` (two sources of one exploration). `--explore` (two sources of one exploration).
**Answering the proposal review (`--proposal-review`).** The plan review asks you about the
*plan*, before any candidate exists. This one asks you about the **candidate on the table**:
every time the deterministic validator accepts a proposal, the run stops and shows you the
measure, the cost lines it touches, the claimed saving, the validator's percentiles, the
checker's verdict and how many further attempts are still affordable. You type `approve` to
take it as it stands, or `revise <what to change>`.
A `revise` is not a note filed for later. It **buys one more generation attempt** under the
caps the run already had — no new loop — and your words go into that attempt's prompt verbatim,
where they stay standing until you next answer. The run then carries whatever the validator
ruled on that later attempt: the expert asks for another try, the machine still decides. Every
round trip is written to `{run_id}-proposal-reviews.json` with your words, which candidate you
were shown, and whether the attempt your `revise` bought actually ran. Input that ends without
an answer stops the run rather than counting as approval.
```bash
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD --docs-dir <docs> \
--bundle-dir <bundle> --proposal-review --outbox-dir out --run-id r1
```
`approve` is **not** an expert verdict, and does not become one: it says the candidate may
stand, not that the measure was reviewed and judged. The verdict still arrives the way it
always did — `--decision`/`--rationale` on the run, or a verdict file dropped into
`--verdict-dir` for a later run to read.
The door belongs to single-project mode, and every other combination is refused by name rather
than quietly ignored: `--portfolio` (its waves share one terminal, so several projects'
candidates would arrive at the same prompt with nothing to tell them apart), `--report` (a
read-only roll-up generates nothing to review), `--live-dry-run` (it stops before the first
model call, so no candidate ever reaches you), `--proposals-from-mandate` (that mode settles the
commission deterministically and never generates a candidate), and `--checkpoint-dir` (a parked
exploration returns before any candidate exists — pass `--proposal-review` at `--resume`
instead, which is where the two doors do compose).
Like the plan review it is **synchronous**, so the hosted surface refuses it by name and points
at this CLI. With `--explore --plan-review --proposal-review` you answer two different doors
from the same terminal in sequence: first the plan, then each candidate the validator accepts.
- **Deriving the cost baseline from the knowledge base**`--derive-cost-baseline` (opt-in, - **Deriving the cost baseline from the knowledge base**`--derive-cost-baseline` (opt-in,
requires `--bundle-dir`). The deterministic validator anchors a proposal's `affected_items` to requires `--bundle-dir`). The deterministic validator anchors a proposal's `affected_items` to
the project's actual cost lines. Normally those come from a hand-written `cost-baseline.json` in the project's actual cost lines. Normally those come from a hand-written `cost-baseline.json` in

View file

@ -1501,3 +1501,55 @@ def test_run_portfolio_takes_no_reviewer() -> None:
is the ``--portfolio`` partition's own reason — so a later "symmetry" edit must be a RED test, is the ``--portfolio`` partition's own reason — so a later "symmetry" edit must be a RED test,
not a silent widening.""" not a silent widening."""
assert "proposal_reviewer" not in inspect.signature(run.run_portfolio).parameters assert "proposal_reviewer" not in inspect.signature(run.run_portfolio).parameters
def test_the_readme_block_names_every_flag_the_cli_refuses_the_door_with() -> None:
"""The README-claim arm, in the Fase-3 form (``test_public_surface_claims_loadbearing``):
a claim the PUBLISHED surface makes about itself, read as RAW TEXT, because prose is the one
thing no behavioural test can see.
The door has five partner refusals. A README block that documents the door but omits one of
them sends a stranger into a refusal the page said nothing about the same drift the wheel
filename gate exists for, and the reason the handover gate matches names rather than prose.
The block is EXTRACTED (the ``--proposal-review`` prose block, from its bold heading to the
next bold heading) rather than substring-matched over the whole file, because ``--portfolio``,
``--report`` and ``--checkpoint-dir`` all appear elsewhere in the README for their own reasons
a whole-file check would be green on exactly the prose it is supposed to protect.
This arm carries no M-number: the M-list closed at M40. It was driven RED TWICE, because
"the block is missing" and "the block is incomplete" are different failures and only the
second is what this gate is for. (i) Written and measured against the README as it stood
BEFORE the block was added extraction found nothing, and ``assert block`` failed. (ii) With
the block in place, the ``--checkpoint-dir`` sentence was rewritten to describe the refusal
without NAMING the flag; the partner loop went red on its own. The control that the extractor
is not silently finding nothing is the ``--plan-review`` block at the end, which has existed
since F4.
"""
readme = (_REPO / "README.md").read_text(encoding="utf-8")
block = _readme_block(readme, "Answering the proposal review (`--proposal-review`)")
assert block, "the README must carry a --proposal-review prose block"
for partner in (
"--portfolio",
"--report",
"--live-dry-run",
"--proposals-from-mandate",
"--checkpoint-dir",
):
assert partner in block, f"the README block never names the refusal with {partner}"
assert "--resume" in block, "the --checkpoint-dir refusal must name the door that DOES work"
assert "OKF bundle" not in block, "customer-facing prose says knowledge base, never OKF bundle"
# CONTROL: the extractor finds a block that has existed since F4, so an empty result above is
# a missing block and not a broken extractor.
plan_block = _readme_block(readme, "Answering the plan review (`--plan-review`)")
assert plan_block and "enable_plan_review" in plan_block
def _readme_block(readme: str, heading: str) -> str:
marker = f"**{heading}.**"
start = readme.find(marker)
if start == -1:
return ""
nxt = readme.find("\n **", start + len(marker))
return readme[start : nxt if nxt != -1 else len(readme)]