portfolio-optimiser/docs/2026-09-02-misjonsreview-v2.md
Kjell Tore Guttormsen e2d26c50ed docs: misjonsreview v2 mot bruksscenarioet — målt, ikke bygget (S2, ORDRE 20260902T113744Z-1245330375)
M1–M6 med kommando, tall og nevner per punkt. 1 BLOCKER (plan-review viser
`<Message object at 0x…>` på alle tre HITL-flater; fire asserts grønne på
`plan != ""`), 4 MAJOR (vakuøs offline-generalprøve uten tool_calls-rad;
ingen in-run-dør for ekspert-feedback; bundle-kontekst re-sendt 3×/7× =
73–93 % av prompt-tokens; K2 stopper på håndskrevet validator-input.json),
4 MINOR, 3 NICE. Ordreutkast per MAJOR+. Ingen src/tests rørt; golden
byte-uendret; F15-diffen i treet sett og ikke rørt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:17:39 +02:00

489 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Misjonsreview v2 — bruksscenarioet målt, ikke bygget (2026-09-02)
**Ordre:** `20260902T113744Z-1245330375-from-.claude` (PM-plan § 4 / S2). **Økt 72.**
**Reviewer:** Fable 5.1 (high, uten advisor) — derfor kommer hvert tall under fra en kommando som
står i § 8, og ingen tall er sitert fra `STATE.md`. **Mandat: MÅL, IKKE BYGG.** Ingen fil under
`src/` eller `tests/` er endret av denne økten (`git status --short` før og etter: identisk — kun
den ucommittede F15-diffen `tests/test_maf_version_guard.py` 28+/15, som ordren ba meg se og ikke
røre, pluss to fremmede utrackede stier).
**Baseline:** [syretesten 25.08](2026-08-25-syretest-vei-ab.md) ·
[katalogkostnaden 26.08](2026-08-26-katalogkostnaden.md) ·
[misjonsreview 25.08](2026-08-25-fable-misjonsreview.md). Golden `demo-transcript.stdout`
**byte-uendret** (`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`, målt `shasum` ved øktstart).
Suiten: **1085 tester samlet** (`pytest --co`), 120 testfiler.
**Bruksscenarioet (operatørens ord 02.09):** «basert på et helt sett av prosjektdokumenter + OKF
bundles + MCP servere og en konkret oppgave skal portfolio-optimiser gjennom dialog og lengre
analyser komme fram til forslag til besparelser i prosjektet, og med tilbakemeldinger komme med
forbedrede forslag. Det er også viktig at portfolio-optimiser er så effektiv som mulig i forhold
til bruken av tokens.»
**Eksponerings-grense (arvet, holdt):** repoet pusher til `open/`. Rapporten bærer tall, stier,
kommandoer og egne probestrenger. Ingen bundle-innhold er gjengitt; fra produsentens golden-bundle
siteres kun frontmatter-NØKLER.
---
## 0. Sammendrag — én linje per målepunkt
| # | Spørsmål | Svar | Kjernetall |
|---|---|---|---|
| M1 | Åpner navigatøren en base etter `444fea7`? | **Fra CLI-døra: nei, og det KAN den ikke** (konstant tekst per rolle kan ikke emittere et verktøykall). **Fra biblioteket: ja, alle fire verktøy, på alle fire baser.** | CLI: 0 verktøykall, 0 approaches, 1 runde × 4 baser. Bibliotek: `list_bundles`/`read_bundle`/`read_file`/`quick_validate` 1/1/1/1 × 4 baser, 1 rutet approach hver |
| M2 | Blir FORSLAGET bedre av `--plan-review revise`, eller bare planen? | **Bare planen — og planen operatøren viser er ULESELIG:** `<agent_framework._types.Message object at 0x…>` på terminalen, i `{run_id}-exploration.json` og i den parkerte `{run_id}-plan-review.json` | Feedback nådde 5 manager-prompts, 0 hypotesiser-prompts; mandat identisk i begge armer; 3 av 4 CLI-artefakter byte-identiske |
| M3 | Hvor lander feedback på et LEVERT forslag? | **I NESTE kjøring** (innboks → `VerdictStore` → hypotese-prompt), bevist med kontroll. **Ingen vei til «forbedret forslag i samme kjøring» for et menneske** — kun validatorens egen avvisning sløyfes tilbake | Run B: ekspertteksten i proposerens prompt; kontroll uten innboks: 0. In-run: 1 refinement, 4 proposer-kall |
| M4 | Tokenprofil per fase | **Bundle-konteksten re-sendes 3× i debatten og 7× i utforskningen = 7393 % av alle prompt-tokens.** Ledgeren offline er 8 tok/kall (skriptet) — ekte usage finnes kun fra 14.08 (15 306) | Tunnel: debatt 40 605 tok (93 % kontekst), utforskning 103 649 tok (85 %); tre største poster navngitt i § 4 |
| M5 | Klar for K2? | **Nei — og det største hullet er ikke `adjudication`:** ingen kode lager `validator-input.json`/`cost-baseline.json` fra et ingestert korpus, og uten dem nekter pipelinen | Produsentens segmenterte v0.2-golden navigeres (3 filer, 0 hopp), `adjudication` leses som streng, 0 konsumenter i `src/` |
| M6 | Vises MCP-kallet i provenance? Egress-linjen? | **Ja, begge — målt mot en EKTE stdio-subprosess** (første gang på kjørestien; B4-testen bruker en stand-in) | `Contacts: prisregister (lookup_unit_price)` · `external_calls=[{prisregister, lookup_unit_price}]` i resultat OG outbox · serverens svar i neste prompt |
**Rangert:** 1 BLOCKER · 4 MAJOR · 4 MINOR · 3 NICE (§ 7). BLOCKER-en er ny og felles for F4 (økt 63)
og U12 (økt 64): begge dørene stiller mennesket et spørsmål det ikke kan lese, og de fire assertene som pinner planteksten (tre testfiler) er
grønne fordi de bare krever `plan != ""`.
---
## 1. M1 — `--explore` etter `444fea7`
**Oppsett.** Fire baser: `shared/examples/{bygg-energi-mikro, veglys-fv-soer, tunnel-hauglia}` +
`~/repos/vegnormal-okf/build/B-n200-2024-gren-1-1-importert` (grenbasen katalogmålingen 26.08 brukte;
10 filer). Kontrakt: `max_rounds=6, max_tokens=100000, max_stall_count=2, max_reset_count=1,
max_plan_revisions=0, enable_plan_review=false`. Instrumentet (§ 8, vedlegg A) teller hvert
verktøykall ved å wrappe `FunctionTool.func` ETTER konstruksjon — verktøylisten agentene får er
uendret — og hvert modellkall per rolle.
**Arm A — operatørdøra** (`run.main` med `--explore --explore-config --scripted-replies`, én konstant
streng per rolle, som er den ENESTE offline-formen CLI-en tilbyr):
| Base | rc | Modellkall (manager/proposer/checker) | Verktøykall | Artefakt: runder / `quick_validations` / `tokens_spent` | Notis |
|---|---:|---|---:|---|---|
| bygg-energi-mikro | 0 | 8 (4/3/1) | **0** | 1 / 0 / 32 | «concluded after 1 round(s); **0 approach(es)**» |
| veglys-fv-soer | 0 | 8 (4/3/1) | **0** | 1 / 0 / 32 | samme |
| tunnel-hauglia | 0 | 8 (4/3/1) | **0** | 1 / 0 / 32 | samme |
| B-n200-2024-gren-1-1 (PROJECT_ID=PROBE) | 1 | 4 (4/0/0) | **0** | 1 / 0 / 32 | utforskningen FULLFØRER, så `run refused: IR projection not found in bundle: 'validator-input.json'`; `m1a-exploration.json` skrevet fra `finally` ✅ |
To ting er målt her, og de er ulike: (i) `444fea7` lukket krasjen — rc er 0/1 med ren nekt, ingen
traceback (4/4); (ii) sløyfa er **vakuøs ved konstruksjon** på denne døra: en `ScriptedChatClient`
returnerer tekst og kan aldri emittere et `function_call`, så navigatøren KAN ikke åpne en base, og
manageren — som svarer én konstant ledger — sier seg ferdig etter én runde. Dette er nøyaktig det
25.08-rapporten forutså og `444fea7`s commit-melding selv sa («fortsatt delvis vakuøs»). Re-målt: sant.
**Artefaktet kan ikke svare på M1s spørsmål.** `{run_id}-exploration.json` har nøklene `completed,
plan_reviews, quick_validations, rounds, run_id, stop, tokens_spent` — navigatørens tre verktøy har
ingen sink og etterlater ingen rad. Bare `quick_validate` spores (`ExplorationTrace.quick_validations`,
`explore.py:277`). «Åpnet navigatøren en base?» er i dag ubesvarbart fra artefaktet, uansett klient.
**Arm B — bibliotekdøra med en klient som EMITTERER verktøykall** (`RecordingClient`, vedlegg A;
navigatør-manus: `list_bundles``read_bundle(id)``read_file(id, "index.md")` → tekst;
hypotesiser-manus: `quick_validate(id, IR-json)``HYPOTHESIS:`-linje; manager: stadienøklet med
tre ledgere navigator→hypothesiser→satisfied):
| Base | Verktøykall (`list_bundles`/`read_bundle`/`read_file`/`quick_validate`) | Modellkall (mgr/nav/hyp) | Runder | Approaches (label, `bundle_id`) | `quick_validate`-dom | Navigatørens prompt-tokens per kall |
|---|---|---|---:|---|---|---|
| bygg | **1/1/1/1** | 12 (6/4/2) | 3 | 1 (`probe-direction-B`, `bygg-energi-mikro`) | validated, **anchored=false**, P10/P50/P90 68 543 / 95 444 / 121 057 | 3 → 144 → 4 031 → 4 783 |
| veglys | **1/1/1/1** | 12 (6/4/2) | 3 | 1 (…, `veglys-fv-soer`) | validated, anchored=true | 3 → 122 → 10 553 → 11 869 |
| tunnel | **1/1/1/1** | 12 (6/4/2) | 3 | 1 (…, `tunnel-hauglia`) | validated, anchored=true | 3 → 148 → 12 767 → 14 429 |
| B-n200-gren-1-1 | **1/1/1/1** | 12 (6/4/2) | 3 | 1 (…, `B-n200-2024-gren-1-1-importert`) | validated, anchored=false, **P10 = P50 = P90 = 300** | 3 → 137 → 2 476 → 3 050 |
Katalogresultatet (`index_excerpt`) står i navigatørens NESTE prompt i 4/4 baser (målt på
innholdet, ikke på `.text`). Sømmen bærer altså et verktøykall ende til ende — også mot en
regelverksbase uten prosjekt. **Det er første gang dette er målt på utforskningsstien;** 25.08 og
økt 56 kunne bare kalle verktøykroppene direkte.
**MINOR-funn på veien:** `quick_validate` sier `validated` med P10 = P50 = P90 når kandidaten mangler
`assumptions` — alle 512 MC-samples identiske, den stokastiske falsifisereren er inert, og dommen
flagger det ikke. Det er samme defektklasse CLAUDE.md beskriver for `response_format`-skjemaet, nå på
det rådgivende verktøyet (kandidaten var min egen probe uten bånd, men en levende hypotesiser vil
også utelate bånd).
## 2. M2 — `--plan-review revise`: planen eller forslaget?
**Bibliotek-armen med DISKRIMINATOR.** Skriptet manager svarer én plan uten feedback og en ANNEN plan
når feedback-nøkkelen `NIGHT-SETBACK-7731` står i prompten; hypotesiseren svarer `night-setback` når
nøkkelen står i DENS prompt, ellers `led-retrofit`. Da er mandatet forskjellig hvis og bare hvis
feedbacken nådde hypotesiseren.
| Arm | Reviews (index → beslutning) | Manager-kall | Feedback-nøkkel i prompts (rolle: antall) | Hypotesiserens prompt | Mandat |
|---|---|---:|---|---|---|
| approve | 0 → approve | 5 | — | 19 tegn (`shape one direction`) + 375 tegn instruksjoner | `[led-retrofit]` |
| revise, approve | 0 → revise (feedback ordrett), **1 → approve** | 7 | **manager: 5** (replan ×2, ledger ×2, final) · **hypothesiser: 0** | 19 tegn | `[led-retrofit]` |
Manageren replanla (2 ekstra kall, indeks 1 finnes — F4s diskriminator holder). Men **planteksten
kringkastes aldri til deltakerne**: hypotesiseren får KUN managerens `instruction_or_question`
(19 tegn), så den eneste veien fra feedback til forslag går gjennom en LEVENDE managers neste
instruksjon. Offline kan det hoppet ikke måles — det er en ærlighetsgrense, ikke en defekt. Det som
ER målt: med skriptet manager er mandatet identisk, altså forslaget identisk.
**CLI-armen (ekte stdin, ekte artefakter, `--outbox-dir`):** begge rc 0.
| Artefakt | approve vs revise |
|---|---|
| `m2-runconfig.json` | identisk |
| `m2-own-proposal-proposal.json` | identisk |
| `m2-own-proposal-outcome.json` | identisk |
| `m2-exploration.json` | **forskjellig** — kun `plan_reviews` |
**BLOCKER-1 — planen mennesket skal godkjenne er en objekt-repr.** Terminalen skrev:
```
PLAN REVIEW #1
--- the plan the exploration would run ---
<agent_framework._types.Message object at 0x10ff788c0>
--- progress so far ---
None
Answer "approve" to sign it off, or "revise <what to change>":
```
Samme streng står i `m2-exploration.json``plan_reviews[*].plan`, og i den PARKERTE
`park1-plan-review.json``plan` (U12-døra, målt med `--checkpoint-dir`): eksperten som skal svare
dager senere får `'<agent_framework._types.Message object at 0x10725fd40>'`.
**Rotårsak, lest i kilden:** `MagenticPlanReviewRequest.plan` er typet `Message`
(`_magentic.py:846`); `explore.py:1395/1404/1418/1478` gjør `str(review.plan)`, og `Message` har
ingen `__str__` (målt: `type(m).__str__ is object.__str__``True`; `m.text` gir planteksten).
Hjelperen som gjør det riktig finnes alt i samme fil: `_plan_text` (`explore.py:966`), brukt for
span-eventene. **Hvorfor gaten er grønn (fire asserts, tre filer):** `test_plan_review_cli_door_loadbearing.py:161`
asserterer `plan != ""`, `:180` asserterer `reviews[0]["plan"][:40] in out` — repr-strengen står
jo i stdout; `test_async_plan_review_loadbearing.py:408` asserterer truthiness;
`test_explore_loadbearing.py:564` `!= ""`. Repoets vakuøs-gate-klasse, **tolvte gang**, og denne
gangen på begge HITL-dørene samtidig. En planreview ingen kan lese er en review ingen kan svare på;
F4s og U12s målbilde «be om svar, bruke svarene» er dermed ikke nåbart for et menneske i dag.
## 3. M3 — feedback på et LEVERT forslag
Kommandoene er `run_project`-kall med instrumentet (vedlegg A); alle fire skritt på
`bygg-energi-mikro`.
| Skritt | Kommando/handling | Målt |
|---|---|---|
| 1. Kjøring A uten ekspert | `run_project(..., outbox_dir=…, run_id="m3a")`, ingen `verdict_input` | `result.verdict is None` ✅ · `verdict_key = ee44c58e0c0d258a` · `m3a-outcome.json.verdict_id = ee44c58e0c0d258a` · proposer 3 kall, checker 1 |
| 2. Eksperten svarer | `capture_verdict(_features_of(outcome.proposal), "rejected", "EKSPERT-MARKOR-7731: …")``write_verdict(inbox, v)` | `v.id == verdict_key` ✅ → fila `ee44c58e0c0d258a.json` |
| 3. Kjøring B, FERSK store | `run_project(..., verdict_dir=inbox, store=VerdictStore([]))` | markøren står i **proposerens** prompt (3 kall) · checker: nei |
| 3b. Kontroll | samme uten `verdict_dir` | markør i **0** prompts |
| 4. In-run maskin-feedback | proposer overdriver (270 000) til «REJECTED by the deterministic validator» står i prompten | `refinements = ["claimed saving 270000 exceeds P90 feasible 121057"]`, proposer **4** kall, utfall `ValidatedProposal` |
**Hvor den lander i dag:** `run.py:644-647` (`load_verdicts_from_dir``store.add`) FØR
Steg-1-folden → `generate._build_messages(context=…)` i NESTE kjøring. Outboxen bærer nøkkelen
(`RunResult.verdict_key`, F2) så en ukommentert kjøring er dømbar — bevist over.
**Finnes en vei til «forbedret forslag i SAMME kjøring»?** For maskinen: ja — validatorens
`Rejection.reason``_build_messages(prior_rejection=…)` (`generate.py:320-325`), bundet av
`max_attempts=3` + `meter.tick_round`. For et MENNESKE: **nei.** `grep -n "prior_" generate.py` gir
kun `prior_rejection`; checker-kritikken sløyfes ikke tilbake (uttalt grense siden Steg 5); eneste
menneske-søm i sløyfa er `plan_reviewer`, som fyrer FØR hypotesene finnes; hosting
`_response_payload` (`hosting.py:195-203`) legger ut `verdict_id` og stopper. Bruksscenarioets «med
tilbakemeldinger komme med forbedrede forslag» er derfor en to-kjørings-sløyfe med dager mellom,
aldri en dialog om det forslaget som ligger på bordet. Minste søm og vokter: MAJOR-2 (§ 7).
## 4. M4 — tokenprofil per fase
**Instrument, med ærlighetsgrense først.** `BudgetMiddleware` leser `usage_details.total_token_count`
(`budget.py:246`), og den skriptede klienten rapporterer **8 tokens per svar** (`tokens_per_reply=8`)
— ledger-summene under (5696) er derfor KALLTELLINGER × 8, ikke forbruk. Eneste ekte usage repoet
har er den levende kjøringen 14.08: **15 306 tokens** (`docs/2026-08-14-…:221`), tolv runder med
parse-feil, ingen utforskning. Profilen under er derfor **o200k_base-tokens av alt modellen VILLE
fått sendt** (tekst + `function_call` + `function_result`), målt per kall med instrumentet fra § 8 —
samme instrument som reproduserte commons' fasittall 25.08 (3 861 / 12 595 / 10 406 for de tre
eksempelbundlene; `bundle_context` re-målt i dag til nøyaktig de tallene).
**Debatt + generering (CLI-armen, M1 A):**
| Base | Sum prompt-tok | debatt(proposer) ×2 | debatt(checker) ×1 | utforskning(manager) ×4 | generering ×1 | Kontekst re-sendt |
|---|---:|---:|---:|---:|---:|---|
| bygg (ctx 3 861) | 14 283 | 7 868 (55 %) | 3 986 (28 %) | 2 243 (16 %) | 186 (1 %) | 3× = **81 %** |
| veglys (ctx 10 406) | 34 001 | 20 984 (62 %) | 10 556 (31 %) | 2 243 (7 %) | 218 (1 %) | 3× = **92 %** |
| tunnel (ctx 12 595) | 40 605 | 25 376 (62 %) | 12 759 (31 %) | 2 243 (6 %) | 227 (1 %) | 3× = **93 %** |
**Utforskning med verktøykall (bibliotek-armen, M1 B):**
| Base | Sum prompt-tok | manager-ledger ×3 | hypothesiser ×2 | navigator ×4 | manager-final ×1 | plan+facts ×2 | Kontekst re-sendt |
|---|---:|---:|---:|---:|---:|---:|---|
| bygg | 36 919 | 12 064 (33 %) | 9 786 (27 %) | 8 961 (24 %) | 5 292 (14 %) | 816 (2 %) | 7× = **73 %** |
| veglys | 86 013 | 26 262 (31 %) | 23 984 (28 %) | 22 547 (26 %) | 12 404 (14 %) | 816 (1 %) | 7× = **85 %** |
| tunnel | 103 649 | 31 394 (30 %) | 29 116 (28 %) | 27 347 (26 %) | 14 976 (14 %) | 816 (1 %) | 7× = **85 %** |
| B-n200-gren (ctx 2 307) | 24 761 | 8 532 (34 %) | 6 254 (25 %) | 5 666 (23 %) | 3 493 (14 %) | 816 (3 %) | 7× = **65 %** |
«Kontekst re-sendt» = antall prompts som inneholder en 160-tegns skive fra midten av
`okf.bundle_context(base)` × kontekstens tokens, delt på summen. Én `read_bundle` gjør hele
konteksten til et `function_result`, og det resultatet rir med i **hver** senere prompt: navigatørens
egne (2), begge hypotesiserens (2, fordi de deler samtalehistorikk), og alle managerens
ledger-/final-prompts (3). Hele utforskningen koster **8,2 × kontekststørrelsen** (tunnel:
103 649 / 12 595), debatten **3,2 ×**.
**De tre største postene, navngitt:**
1. **Debattens tre turer bærer hele `bundle_context` hver** — 8193 % av en CLI-kjørings
prompt-tokens (proposer ×2 + checker ×1). Kilde: `workflow.py` GroupChat, konteksten i
deltakernes melding, ikke i `instructions` (målt `instructions_chars` 61/307).
2. **Managerens progress-ledger-prompts bærer hele samtalen inkl. verktøyresultater** — 3 × 5 766
(bygg) / 3 × 15 450 (tunnel) tokens, 3034 % av utforskningen. MAF-Magentic-atferd
(`_magentic.py`), ikke vår kode.
3. **Hypotesiseren arver navigatørens verktøyresultater** — 2 × 4 795 / 2 × 14 441 tokens, 2528 %.
**Hva caching og kontekstbudsjett ville kjøpt — regnet fra prompt-prefiksene, ikke antatt:**
| Par | Felles prefiks (tok) | Av | Lesning |
|---|---:|---|---|
| debatt proposer #1 vs #2 (bygg / tunnel) | **3 877 / 12 612** | 3 877 / 12 612 | hele første tur er et re-sendt prefiks (100 %) |
| debatt proposer #1 vs checker | 3 877 / 12 612 | 3 986 / 12 759 | 9799 % delt |
| manager ledger #1 vs #2 | 215 | 751 / 5 547 (bygg) | før første verktøyresultat deles nesten ingenting |
| manager ledger #2 vs #3 (bygg / tunnel) | **5 010 / 14 656** | 5 766 / 15 450 | 8795 % delt |
| hypothesiser #1 vs #2 | 4 795 / 14 441 | 9 786 / 29 116 | 4950 % |
Anslag, per base tunnel: **leverandør-prefiks-caching** (uendret kode, bare byte-stabile prefikser,
som allerede er tilfellet) kunne treffe ≈ 2 av 3 kontekstkopier i debatten (≈ 25 200 av 40 605 =
62 %) og ≈ 2 av 3 ledger-kopier i utforskningen (≈ 29 300 av 103 649 = 28 %); hypotesiserens
andre kall ≈ 14 400 (14 %). Samlet ≈ **40 % av utforskningen og ≈ 60 % av debatten** er
prefiks-cachebart i dag. **Kontekstbudsjett** virker på multiplikatorens grunntall: et tak på
`read_bundle` av samme form som katalogtaket (`_CATALOGUE_EXCERPT_CHARS`) — f.eks. konseptliste +
`read_file` per konsept i stedet for hele `bundle_context` — kutter alle 7 kopiene samtidig; ved
tunnel er hver kopi 12 595 tokens. **Ikke målt her:** hva en levende manager faktisk kaller og hvor
mange runder den bruker; tallene over er én navigatør-tur + én hypotesiser-tur.
## 5. M5 — klar for K2?
**Leseprøve mot produsentens nyeste form** (`~/repos/llm-ingestion-okf/examples/
ingest-golden-segmented-okf-v0-2/expected-bundle`, den eneste committede bundelen med
`adjudication`-nøkkelen; produsent-HEAD `62b6192`, tag `v0.5.0a2`+104). Konsumentens funksjoner,
kalt direkte:
| Funksjon | Resultat | Lesning |
|---|---|---|
| `okf.navigate_bundle` | 3 kontekstfiler, 0 verdicts, **0 hopp**; nestede `krav/index.md` og `krav/1-1/index.md` fulgt | Navigasjonen tåler den segmenterte formen ✅ |
| `okf.parse_frontmatter(konsept)` | nøkler `adjudication, bundle_id, generated, ingested_at, parent, segment_id, source_file, source_offset, source_sha256, title, type`; `adjudication → 'proposed'`, `source_offset → '[94, 176]'` (streng) | Nøkkelen er LESBAR som streng; ingen tolker den |
| `okf.parse_frontmatter(index.md)` | `{okf_version: 0.2, bundle_id: b-golden-segmented-okf-v0-2}` | Rot-`bundle_id` er der; konsumenten bruker basenavnet (`explore.py:666`, `run.py:1512`) |
| `okf.bundle_context` | 132 tokens; `adjudication: proposed` forekommer **1** gang — som index-LABEL-prosa | Tilstanden lekker inn som tekst, ikke som tilstand |
| `explore.list_bundles` | `{"id": "expected-bundle", …, "cost_baseline": false, "documents": 3}` | Katalogen fungerer ✅ |
| `okf.load_ir_projection` | **`FileNotFoundError: IR projection not found in bundle: 'validator-input.json'`** | **Pipelinen nekter** (samme nekt som M1 A på grenbasen) |
| `verdicts.seed_store_from_bundle` | 0 | — |
| `grep -rn adjudication src/ tests/` | **0 treff** (kjent-positiv `bundle_id`: 71) | Ingen konsument |
| `grep -rn concept_id src/ tests/` | **0 treff** | Ingen konsept-identitet i konsumenten |
**Hva som mangler for «hele veien», konkret:**
| # | Gap | Fil/funksjon i dag | Eier / status |
|---|---|---|---|
| 1 | **IR-projeksjon + kostbaseline fra et ingestert korpus.** `validator-input.json` er håndskrevet per prosjekt (README:146 «one file the ingest layer does not write for you»; `docs/kunnskapsbase-for-en-kjoring.md:84`). K2 er anbudsdokumenter (PDF/DOCX/XLSX) — ingen kode leser kostlinjer ut av dem | `run._project_from_bundle` (`run.py:437`) → `okf.load_ir_projection` (`okf.py:433`) fail-fast; `okf.load_optional_cost_baseline` (`:411`) | **Ingen eier, ingen ordre.** Det tyngste enkelthullet — uten det stopper syretesten der M1 A stoppet på grenbasen |
| 2 | **`adjudication`-konsument** (PM B2: fravær = `unknown`, aldri `absent`) | ingen; nærmeste form er B4 Steg 7 `evidence_for(path, key="verified")` med `EvidenceState` | **Bestilt** i S3 amendment B (Steg 7b), K4c-vokter `tests/test_falsification_verdict_loadbearing.py`. Ikke bygget |
| 3 | **(`bundle_id`, `concept_id`)-identitet** | `explore._bundle_index` + `run.py:1512` (basename, to kopier); `mandate.Approach.bundle_id` default `""`; `verdicts._mint_id` (`:86`) korpus-blind; `VerdictStore.add` (`:305`) first-write-wins stille | **Bestilt** S3 amendment D / B4 Steg 910 (`reconcile_bundle_id`, `collisions`). `concept_id` finnes ikke som begrep i konsumenten — S3 B1 gir den, men ingen kode i dag nøkler en dom på et konsept |
| 4 | **Flerkilde-`sources` + K5-terskel** | `parse_frontmatter` holder lister verbatim som streng | B4 Steg 27 / S3 amendments A + C |
| 5 | **Ny profil `SEGMENTED_OKF_V0_2`** | installert `llm_ingestion_okf` **v0.3.2** har moduler `connectors, errors, manifest, materialize, render`**ingen `profiles.py`**; profilen finnes kun i produsent-HEAD | Gjelder KUN Door A (`ingest.materialize`) i dette repoet; LESING trenger ingen pin. STATE (økt 70): «IKKE bump `llm-ingestion-okf`-pinnen» — står ikke i veien for K2-lesing |
| 6 | **Office → tekst** | — | Produsent (S1), utenfor dette repoet |
**Konklusjon M5:** når S1 leverer et K2-bundle, kan portfolio-optimiser **navigere det og
katalogisere det** i dag, men **ikke kjøre pipelinen** — og hindringen er ikke `adjudication`, det er
at ingen lager prosjektets tallfiler. Det er et scenario-hull ingen av de tre planene (PM, B4, S3)
navngir med eier.
## 6. M6 — MCP under kjøring, mot en ekte stdio-server
**Server:** `/tmp/claude-s2/mcp_stub.py``FastMCP("prisregister")`, ett verktøy
`lookup_unit_price(code) -> "unit price for {code}: 1.0 NOK/kWh (STUB-SERVER-7731)"`. Konfig:
`{"servers":[{"name":"prisregister","transport":"stdio","command":<.venv python>,"args":[stub],
"allowed_tools":["lookup_unit_price"],"timeout_seconds":30}]}`.
| Arm | Kommando | Egress-linje (stdout) | `provenance.external_calls` | Serverens svar i neste prompt |
|---|---|---|---|---|
| A — CLI, tekst-agenter | `python -m portfolio_optimiser.run BYGG-KONTOR-NORD … --mcp-config mcp.json --scripted-replies … --outbox-dir … --run-id m6a` (subprosess) | **`Contacts: prisregister (lookup_unit_price)`** ✅, rc 0 | `m6a-proposal.json`: **`[]`** — agentene kalte aldri (kan ikke, tekst) | — |
| B — bibliotek, proposer som KALLER | `run_project(..., mcp_servers=load_mcp_config(cfg), outbox_dir=…, run_id="m6b")` med `RecordingClient`-manus `[call lookup_unit_price(code=ENERGI-TOTAL-EL), IR-json]` | (ikke CLI) | **`[{"server": "prisregister", "tool": "lookup_unit_price"}]`** i `result.provenance` OG i `m6b-proposal.json` ✅ | **ja** (`STUB-SERVER-7731` i proposerens 2. prompt); serverloggen viser `CallToolRequest` |
Dette er første måling av kjørestien (`run_project``build_mcp_tools``MCPStdioTool`
`AsyncExitStack``ToolCallRecorder`) mot en **ekte subprosess**; `test_b4_mcp_call_trace_loadbearing.py`
bruker en in-process stand-in med vilje, og `test_ingest_golden_mcp.py` måler INGEST-transporten, ikke
denne. Attribusjonen (`server="prisregister"`) kom fra vår egen konfig-indeks, som designet.
**To MINOR-funn i samme kjøring:** (a) genereringskallet (`generate.py:460`
`chat_client.get_response(messages, options={"response_format": …})`) går utenom `Agent` og har
**ingen verktøy** — et `function_call`-svar der blir en parse-feil, så prisregisteret kan brukes i
debatten men ikke i steget som skriver forslags-JSON-en (indirekte via debatt-outputen som kontekst);
(b) `m6b-parse-failures.json` fanget det svaret som `"text": ""` — «verbatim»-fangsten mister et
svar som var et funksjonskall, altså nøyaktig beviset den finnes for.
**Instrument-lærdom, uttalt:** første forsøk på arm A krasjet med `io.UnsupportedOperation: fileno`
fordi jeg fanget stderr in-process (`redirect_stderr`) og `stdio_client` gir barnet `sys.stderr`. Det
er min instrumentfeil, ikke repoets — men det betyr at en `capsys`-basert test av denne stien ville
feilet på samme måte; subprosessen er målingen (P4-presedensen, igjen).
---
## 7. Funn, rangert
### BLOCKER
**BLOCKER-1 — Planreviewen viser et menneske `<Message object at 0x…>` i stedet for planen, på
alle tre flater.** `explore.py:1395/1404/1418/1478` `str(review.plan)` på en MAF `Message` uten
`__str__`. Terminal (F4), `{run_id}-exploration.json.plan_reviews[*].plan`, og den parkerte
`{run_id}-plan-review.json.plan` (U12). Fire asserts i tre testfiler grønne på `!= ""`/truthiness/`[:40] in out`.
Konsekvens: begge HITL-dørene er uøvbare for et menneske; en ekspert som svarer `approve` på en
repr signerer blindt. Beviset ligger i § 2 og i `/tmp/claude-s2/out/m2/result.json` +
`m2-park/outbox/park1-plan-review.json` (efemert; kommandoene i § 8 gjenskaper det).
**Ordreutkast B1 (Opus 5 / high, én økt, Iron Law):**
> Oppgave: planteksten som vises og lagres i plan-reviewen skal være `Message.text`, aldri
> `str(Message)`. Bruk den eksisterende `_plan_text` (`explore.py:966`) på alle fire stedene; ingen
> ny hjelper. **Test først, RØD:** i `tests/test_plan_review_cli_door_loadbearing.py` skjerpes T2 til
> å asserte at den skriptede managerens plantekst (en unik streng i `_REPLIES["manager"]`-planen —
> scenariet må gi manageren et stadienøklet svar slik `simulation._exploration_manager_reply` gjør,
> ellers finnes ingen plantekst å finne) står i stdout OG i artefaktets `plan`, og at strengen
> `"Message object at"` IKKE står noe sted; i `tests/test_async_plan_review_loadbearing.py` samme
> assert på `pending_plan_reviews(...)[0].plan` og på `{run_id}-plan-review.json`. Mutasjon: revert
> til `str(review.plan)` → begge røde; kontroll: hele suiten grønn, golden byte-uendret. **Ikke
> utløs:** ingen endring i Magentic-wiring, `checkpoint_storage`, `_ALLOWED_CHECKPOINT_TYPES` eller
> F15-diffen; ingen push.
### MAJOR
**MAJOR-1 — Offline-generalprøven av utforskningen er vakuøs ved konstruksjon, og artefaktet kan
ikke se det.** (M1) `--scripted-replies` tar én konstant streng per rolle → 0 verktøykall, 0
approaches, 1 runde på 4/4 baser; `{run_id}-exploration.json` har ingen rad for navigatørens
verktøy. En operatør som «beviser så mye som mulig gratis før det dyre trinnet» kan i dag ikke bevise
at navigatøren åpner noe, og etter en betalt kjøring kan ingen lese om den gjorde det.
> **Ordreutkast M1 (Opus 5 / high, to halvdeler i én økt):** (a) `ExplorationTrace.tool_calls`
> (navn + `bundle_id`-argument, aldri resultatet) fylt av en `FunctionMiddleware` på
> utforskningsagentene — speil av `mcp_tools.ToolCallRecorder`, én kopi av mønsteret — og skrevet av
> `trace_payload` ved siden av `quick_validations`; (b) `_load_scripted_replies` godtar for
> utforskningsrollene en LISTE av trinn der et trinn kan være `{"call": "<tool>", "args": {…}}`, og
> `scripted_factory` bygger da en klient som emitterer `function_call` (formen målt i denne rapporten,
> vedlegg A). **Test først:** `tests/test_scripted_explore_door_loadbearing.py` — CLI-kjøring med
> manus `list_bundles → read_bundle → tekst` gir `tool_calls == [list_bundles, read_bundle]` i
> artefaktet (RØD uten (a) og uten (b) hver for seg); kontroll: konstant-streng-manus gir `[]`, og
> golden byte-uendret. **Ikke utløs:** ingen endring i verktøykroppene eller katalogtaket; ikke
> `run.py`s debatt-roller; ingen push.
**MAJOR-2 — Ingen dør for menneskelig feedback på det forslaget som ligger på bordet.** (M3)
Feedback lander i neste kjøring (bevist), maskinens egen avvisning sløyfes tilbake (bevist), men
bruksscenarioets «med tilbakemeldinger komme med forbedrede forslag» er en to-kjørings-sløyfe.
> **Ordreutkast M2 (Opus 5 / high, Voyage-brief anbefalt — berører `run.py`, `generate.py`,
> `outbox.py`, CLI):** `run_project(..., proposal_reviewer: ProposalReviewer | None)` — kalles ETTER
> validatorens utfall med (forslag, dom, provenance) og svarer `approve` eller `revise(feedback)`;
> `revise` mater `generate._build_messages(prior_feedback=…)` (parallell til `prior_rejection`, kun
> teksten, aldri forrige JSON) inn i ett nytt bundet forsøk under EKSISTERENDE `max_attempts` +
> `meter.tick_round` — ingen ny løkke; hvert svar registreres verbatim på `RunResult.expert_revisions`
> og i outboxen; CLI-dør `--proposal-review` speiler `--plan-review` (EOF = feil, aldri godkjenning);
> hosting NEKTER (synkron). **Test først:** `tests/test_proposal_review_loop_loadbearing.py` — en
> `revise` gir et ANDRE genereringskall hvis prompt bærer feedbacken ordrett og et annet utfall enn
> en alltid-godkjenn-reviewer (diskriminatoren, F4-formen); EOF raiser; taket holder (revise ×N
> stopper på `max_attempts`); kontroll uten reviewer: byte-identisk kjøring og golden uendret.
> **Ikke utløs:** ingen endring i dom-myntingen (F2), Steg 7-innboksen, `capture_verdict` eller
> checker-gaten.
**MAJOR-3 — Bundle-konteksten re-sendes 3× i debatten og 7× i utforskningen; 7393 % av alle
prompt-tokens.** (M4) Tre poster navngitt i § 4; ≈ 40 % (utforskning) / ≈ 60 % (debatt) er
prefiks-cachebart som koden står, resten krever et kontekstbudsjett på `read_bundle`.
> **Ordreutkast M3 (Fable 5.1 eller Opus 5 / high — MÅL FØRST, så én søm):** (1) mål med instrumentet
> i vedlegg A hva `read_bundle` koster som `function_result` per base og hvor mange prompts det rir
> med i; (2) bygg `read_bundle` om til samme form som katalogen: konseptliste (`name`, `type`,
> `title`, lengde) med `read_file` som neste trinn, tak i TESTEN ikke i koden
> (`test_catalogue_cost_loadbearing.py`-formen); (3) gate: `tests/test_read_bundle_cost_loadbearing.py`
> — `read_bundle` over tunnel-basen < 1 500 o200k-tokens, kontroll at én konseptfil alene sprenger
> taket, og at `read_file` fortsatt gir hele fila. **Ikke utløs:** `okf.bundle_context` selv (den
> bærer debattens kontekst og goldenene), nav-goldenene, `run.py`. Debattens 3× er GroupChat-formen
> og en egen beslutning (kontekst i `instructions` vs melding endrer ikke tokens, bare cache-treff).
**MAJOR-4 — K2 kan ikke kjøres «hele veien» fordi ingen lager prosjektets tallfiler, og ingen plan
eier det.** (M5) `validator-input.json` + `cost-baseline.json` er håndskrevne; `_project_from_bundle`
nekter uten dem (målt to ganger i dag). `adjudication`/identitet ER bestilt (S3), dette er det ikke.
> **Ordreutkast M4 (Fable 5.1 / high — MÅL, IKKE BYGG, til PM):** inventar av K2 (86 filer): hvilke
> bærer kostlinjer (XLSX/tabeller i PDF), hvilken form (kode, mengde, enhetspris), og om
> `ir.SavingsProposal`/`CostBaseline` kan fylles fra dem uten skjønn. Utfall: EN eier-beslutning —
> produsent-profil som emitterer `cost-baseline.json` (llm-ingestion-okf), eller et konsument-steg
> `okf.derive_cost_baseline(bundle)` med vokter `tests/test_cost_baseline_derivation_loadbearing.py`.
> **Ikke utløs:** ingen kode før eieren er valgt; ingen endring i `ir.py`.
### MINOR
**MINOR-1**`quick_validate` sier `validated` med P10 = P50 = P90 når kandidaten mangler
`assumptions` (M1 B, grenbasen: 300/300/300); den inerte MC-en flagges ikke i dommen.
**MINOR-2** — parse-feil-fangsten registrerer et `function_call`-svar som `"text": ""` (M6 B).
**MINOR-3** — MCP-verktøy er nåbare i debatten, ikke i genereringskallet (`generate.py:460`, ingen
`tools`) — by design, men ikke uttalt noe sted.
**MINOR-4** — Planteksten kringkastes aldri til deltakerne (hypotesiserens prompt = 19 tegn
instruksjon); effekten av en planrevisjon på forslaget hviler 100 % på managerens neste
`instruction_or_question`. Ærlighetsgrense som bør stå i `--plan-review`-hjelpeteksten.
### NICE (positivt målt)
**NICE-1** — Verktøysømmen bærer ende til ende, også mot en regelverksbase uten prosjekt (M1 B, 4/4).
**NICE-2** — Innboks-sløyfa virker med kontroll og nøkkel-samsvar (M3), og `finally`-artefaktene
skrives også når pipelinen nekter (M1 A grenbasen, 1 fil).
**NICE-3** — Egress-deklarasjon og B4-recorder holder mot en ekte stdio-subprosess (M6).
---
## 8. Verifiseringslogg
Instrumentet ligger i `/tmp/claude-s2/` (efemert, utenfor repoet — ordren forbød produksjonskode,
og et måleinstrument i `tests/` ville vært en beslutning om å beholde det). Vedlegg A gjengir kjernen
så tallene kan reproduseres.
| # | Påstand | Kommando → resultat |
|---|---|---|
| 1 | Treet urørt; F15-diff sett | `git status --short`` M tests/test_maf_version_guard.py`, `?? docs/presentasjon-…html`, `?? scratchpad/` (før og etter); `git stash list \| wc -l`**0** (STATE sa «stash») |
| 2 | Golden | `PYTHONIOENCODING=utf-8 uv run python -m portfolio_optimiser.simulation \| shasum``ea8c5347…` |
| 3 | Nevner tester | `uv run pytest --co -q \| tail -1` → 1085; `ls tests/*.py \| wc -l` → 120 |
| 4 | M1 A/B × 4 baser | `uv run python /tmp/claude-s2/m1.py <base> <PID\|PROBE> /tmp/claude-s2/out/m1-<n>``result.json` (tall i § 1) |
| 5 | Artefaktnøkler | `python -c 'import json;print(sorted(json.load(open("…/m1a-exploration.json"))))'` → 7 nøkler, ingen `tool_calls` |
| 6 | M2 bibliotek + CLI | `uv run python /tmp/claude-s2/m2.py shared/examples/bygg-energi-mikro BYGG-KONTOR-NORD /tmp/claude-s2/out/m2`; park: `python -m portfolio_optimiser.run … --checkpoint-dir … --run-id park1``park1-plan-review.json.plan` = repr |
| 7 | `Message` uten `__str__` | `uv run python -c "from agent_framework import Message; m=Message(role='assistant',contents=['PLAN: x']); print(str(m)[:40], m.text, type(m).__str__ is object.__str__)"``<agent_framework._types.Message object … PLAN: x True` |
| 8 | Alle `str(review.plan)` | `grep -n "review\.plan\|_plan_text(" src/portfolio_optimiser/explore.py``:966` (hjelper), `:1395/:1404/:1418/:1478` (`str(...)`) |
| 9 | Vakuøse asserts | `sed -n 161p;180p tests/test_plan_review_cli_door_loadbearing.py`; `sed -n 408p tests/test_async_plan_review_loadbearing.py`; `sed -n 564p tests/test_explore_loadbearing.py` |
| 10 | M3 | `uv run python /tmp/claude-s2/m3.py shared/examples/bygg-energi-mikro BYGG-KONTOR-NORD /tmp/claude-s2/out/m3` → § 3 |
| 11 | M4 profil | `uv run --with tiktoken python /tmp/claude-s2/m4.py <calls-*.json>` + re-sendt-skript (§ 4); `bundle_context`-tokens re-målt = commons' fasit |
| 12 | Ekte usage finnes kun 14.08 | `grep -n token_usage docs/2026-08-14-fase1b-forste-levende-kjoring.md``:221 token_usage: 15 306` |
| 13 | M5 leseprøve | `uv run python - <<EOF` mot produsent-golden (§ 5); `grep -rn "adjudication\|concept_id" src/ tests/ --include='*.py'` → 0/0; `grep -rn bundle_id src/ --include='*.py' \| wc -l` → 71 |
| 14 | Pin vs produsent | `uv pip list \| grep llm-ingestion` → 0.3.2; `ls .venv/…/llm_ingestion_okf/` → ingen `profiles.py`; `cd ~/repos/llm-ingestion-okf && git describe --tags``v0.5.0a2-104-g62b6192` |
| 15 | S3 dekker adjudication + identitet | `grep -n "adjudic\|concept_id" ~/.claude/coord/portfolio-optimiser/orders/20260902T113744Z-124778510-*.md` → amendment B (Steg 7b), D/B1 |
| 16 | M6 | `uv run python /tmp/claude-s2/m6.py shared/examples/bygg-energi-mikro BYGG-KONTOR-NORD /tmp/claude-s2/out/m6` → § 6 (serverlogg `CallToolRequest` på stderr) |
| 17 | Genereringen har ingen verktøy | `grep -n "get_response" -A1 src/portfolio_optimiser/generate.py``:460` uten `tools` |
| 18 | Doc-gaten godtar rapporten | `uv run pytest -q tests/test_doc_constant_sync_loadbearing.py tests/test_public_surface_claims_loadbearing.py` → se commit |
**Ikke verifisert i denne økten:** at en LEVENDE manager/navigatør faktisk kaller verktøyene (samme
grense som før — men sømmen som skal bære kallet er nå bevist); at leverandørens caching faktisk
treffer prefiksene (anslaget er regnet av prompt-bytes, ikke målt mot en fakturering); K2-korpusets
faktiske kostlinje-form (MAJOR-4s ordre måler det).
---
## Vedlegg A — instrumentets kjerne (reproduksjon)
```python
# RecordingClient: en ScriptedChatClient som også kan EMITTERE verktøykall (ikke produksjonskode)
from agent_framework import ChatResponse, Content, Message, UsageDetails
from portfolio_optimiser.simulation import ScriptedChatClient
class RecordingClient(ScriptedChatClient):
def __init__(self, role, *, script=(), default_reply="ok"):
super().__init__(role=role, default_reply=default_reply, tokens_per_reply=8)
self._script = list(script) # str | ("call", tool_name, {args})
def _inner_get_response(self, *, messages, options, stream=False, **kw):
item = self._script.pop(0) if self._script else self._default
if isinstance(item, tuple):
_, name, args = item
contents = [Content("function_call", call_id=f"c{self.call_count}", name=name, arguments=args)]
else:
contents = [item]
async def _coro():
return ChatResponse(messages=[Message(role="assistant", contents=contents)],
response_id="synthetic", usage_details=UsageDetails(total_token_count=8))
return _coro()
# Verktøyteller: wrap FunctionTool.func ETTER konstruksjon — agentenes verktøyliste er uendret
import functools
from portfolio_optimiser import explore as ex
TOOLS = []
def _count(tools):
for t in tools:
orig, name = t.func, t.name
@functools.wraps(orig)
def w(*a, __o=orig, __n=name, **kw):
TOOLS.append((__n, {k: str(v)[:80] for k, v in kw.items()})); return __o(*a, **kw)
t.func = w
return tools
_nav, _qv = ex.navigator_tools, ex.quick_validate_tool
ex.navigator_tools = lambda d: _count(list(_nav(d)))
ex.quick_validate_tool = lambda d, **kw: _count([_qv(d, **kw)])[0]
# Prompt-størrelse: tekst + function_call + function_result (IKKE bare .text — første versjon
# målte 16 tegn for en prompt som bar hele bundle-konteksten som function_result)
```
Prompt-tokens: `tiktoken.get_encoding("o200k_base")` over den blob-en, kjørt med
`uv run --with tiktoken` (`tiktoken` er fortsatt ikke en prosjekt-avhengighet).