portfolio-optimiser/CLAUDE.md
Kjell Tore Guttormsen 6b645ad32a feat(fase6): gate-promote approved verdicts back into the OKF wiki (Steg 8)
Close the last agentic-loop seam (målbilde §3/§6/§7/§11 step 6): an
APPROVED verdict is promoted from the raw output layer into the context
layer (the OKF bundle) as a navigable `type: verdict` concept file, so
human/persona-approved knowledge reaches the next run's hypothesis.

- okf.py (pure stdlib, MAF-free): render_frontmatter / write_concept_file
  / link_in_index — the D7-portable OKF write counterpart of navigate.
- verdicts.py: promote_verdict + PromotionRefused gate (fail-closed; only
  approved decisions enter the wiki, never raw agent output), provenance
  stamp (who/experiment/when; timestamp a required kwarg), neutral index
  label (signal reaches a prompt only via the gated ExpeL fold, never
  bundle_context), _safe_filename_token (id sanitised for path/link).
- R4 = optional+gated: a public opt-in primitive, NOT wired into
  run_project (mirrors write_verdict — the system reads, the gate promotes).
- Load-bearing trio (test_step8_promotion_loadbearing.py): gate refuses a
  non-approved verdict, approved verdict is navigable, promoted signal
  stays out of the read-context — all proven RED-on-detach. Suite 144->148.

Design hardened by an adversarial plan-critic (12 findings; the BLOCKER —
index-link leak into bundle_context via index_summary — closed by the
neutral label + a no-leak test). Honesty limits documented: promoted file
is minimal (signal as prose only), and the learning-key id means
same-candidate approvals share a filename (last-write-wins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
2026-06-30 11:06:28 +02:00

8.5 KiB

portfolio-optimiser

Kontekst

Generisk, åpent Python-rammeverk på Microsoft Agent Framework (MAF) som finner kostnadsbesparelser INNI hvert prosjekt i en portefølje av uavhengige prosjekter. Multi-agent samarbeid genererer kandidat-tiltak; en obligatorisk deterministisk validator avgjør verdiene; fageksperter vurderer via HITL, og systemet lærer av dommene. Publiseres på Forgejo for alle som har MAF.

Bakgrunn og beslutninger: research (§15 implementeringsregister), plan. Løpende state: STATE.md (local-only).

Stack

Python ≥3.10. MAF (agent-framework-core 1.9.0). Pakkehåndtering: uv. To backend-profiler: Azure/Foundry (full) + lokal (fallback).

Konvensjoner

  • Type hints overalt (mypy der mulig). Pydantic for validering/IR.
  • ruff for lint+format. pytest for test.
  • Modell-valg som konfig (modell-map rolle→Foundry-deployment), ikke spredt i kode.
  • Metode kodifiseres som Agent Skill (agentskills.io: SKILL.md + scripts/ + references/).
  • Datatilgang via MCP-servere; data-source-konfig JSON-Schema-validert, fail-fast.

Kommandoer

  • Sync: uv sync
  • Test: uv run pytest
  • Lint: uv run ruff check . + uv run ruff format .
  • Type: uv run mypy src

Arbeidsflyt (invarianter)

  • Rent teknisk rammeverk: deployer eier DPIA/ROS/behandlingsformål. Bygg IKKE compliance-funksjoner — kun tekniske forutsetninger (lokal-only, provenance, ingen stille egress) + disclaimer.
  • 90%-prinsipp: bygg den generiske kjernen + tydelige extension points; jakt IKKE de siste 10 %.
  • Deterministisk validator er obligatorisk og blokkerende — aldri valgfri plugin.
  • Framework-nøytral kontekst-søm: OKF-bundle-navigasjon (okf.py) og den delte shared/-kjernen er ren stdlib — null agent_framework/mcp-import, så samme bundles konsumeres uendret av begge stacker (D7-portabel). Håndhevet av tests/test_okf.py::test_okf_is_maf_free; importér aldri MAF inn i kontekst-laget.
  • OKF-navigert bundle-kontekst (ikke stuffing): på bundle-stien bygges agent-lese-konteksten ved å NAVIGERE bundelen (okf.bundle_context: index + frontmatter + cross-links, progressiv disclosure) — aldri keyword-chunk-stuffing (målbilde §2/§4). type: verdict-laget ekskluderes fra denne konteksten: tidligere dommer når hypotese-prompten KUN via den gatede ExpeL-folden. Load-bearing: test_bundle_context_excludes_verdict_layer + empty-store-kontrollen i test_step1_expel_loadbearing.py (realiseringssignalet lekker aldri inn via kontekst).
  • Stoppkriterier + budsjett-tak påkrevd ved oppstart (fail-fast, aldri ubegrenset loop).
  • Group Chat maker-checker som debatt-default (IKKE Magentic, som er eksperimentell).
  • To falsifiserere, samme kandidat (Steg 3/4, målbilde §2/§6): den deterministiske validatoren gater tallene (blokkerende), checkeren gater resonnementet. Checkeren avslutter turen med en VERDICT: APPROVE / VERDICT: REJECT — <grunn>-linje; et eksplisitt avslag blokkerer et ellers validert forslag (run_project overflater begge debatt-deltakere via output_from=agents og overstyrer utfallet til en checker-kilde-Rejection). Gaten er opt-in-reject (fail-open ved manglende markør), og provenance.validator_decision forblir ærlig — den speiler KUN validatoren, aldri checkeren (de to falsifisererne blandes aldri). Load-bearing: tests/test_checker_gate_loadbearing.py blir rød ved BEGGE detach-punkt (revert output_from, eller fjern override). Checkeren «må faktisk gate, ELLER vi slutter å kalle det maker-checker».
  • Informert forbedring, bundet (Steg 5, målbilde §5/§7): generate_via_llms ytre max_attempts-løkke er ikke lenger blind — validatorens forrige Rejection.reason mates inn i neste forsøks prompt (_build_messages(prior_rejection=...)), så proposeren korrigerer i stedet for å gjenta. Kun den mest-nylige falsifiseringen (last, ikke akkumulert), kun grunnen (aldri forrige proposal-JSON), under EKSISTERENDE tak (meter.tick_round + max_attempts — ingen ny løkke; «forbedre til god nok» uten tak er forbudt). Eneste per-forsøk-falsifiserer her er validatoren; å seede generering med checker-kritikken er run-nivå og separat scoped (IKKE bygget her) — så koden påstår ikke mer enn den gjør. Load-bearing: tests/test_step5_refine_loadbearing.py blir rød når reason-injeksjonen detaches (utfallet flipper aldri + reason-verbatim-asserten faller); kontrollen beviser at løkka forblir bundet.
  • Lang/async fil-løkke (Steg 7, målbilde §3/§7): run_project(verdict_dir=...) er den lange tilbakemeldings-tidsskalaen — en ekspert/persona dropper en verdict-fil (vanlig JSON, RAW-laget per §10 R2) i en inbox-mappe ETTER en kjøring, og en separat, senere kjøring load_verdicts_from_dirstore.add merger den inn FØR Steg-1-folden (ingen endring i folden), så dommen når neste hypotese. Rolledeling (§3, ufravikelig): systemet LESER mappa; eksperten/personaen SKRIVER den — run_project persisterer ALDRI sin egen fangede dom tilbake (det er outbox/Steg 8). Merge, aldri erstatt (run_portfolio-tråding intakt); tolerant last (manglende mappe / fremmede / halvskrevne filer hoppes over, ikke raises — RAW-lag, kontrast okf.load_ir_projections fail-fast); id leses verbatim, re-mintes aldri. write_verdict er den offentlige authoring-primitiven (persona/test + framtidig Steg 8), men wires IKKE inn i run_project. Load-bearing: tests/test_step7_async_loop_loadbearing.py — en dom droppet etter Run A MÅ nå Run B's prompt (Run B bruker FERSK store → overføringen er fil-løkka, ikke in-memory-carryover); tom-inbox-kontroll beviser kausalitet. Markør = realiseringsverdi som finnes ingen steder i bundelen (ikke frøets 0.82).
  • Gated wiki-promotering (Steg 8, målbilde §3/§6/§7): når en ekspert/persona GODKJENNER et utfall, løfter verdicts.promote_verdict det fra RAW output-laget inn i kontekst-laget (OKF-bundelen) som en type: verdict-konseptfil, navigerbar av neste kjørings seed_store_from_bundle. Gaten er fail-closed: en ikke-godkjent dom (decision ∉ {approved, approved_with_adjustment}) raiser PromotionRefused og skriver/linker INGENTING — kun menneske/persona-godkjent kunnskap når wikien, aldri rå agent-output (selv-forurensning). Provenance-stemplet (hvem/eksperiment/når; timestamp er påkrevd keyword, ingen wall-clock-default → deterministisk). OKF-skriveren bor i okf.py og er ren stdlib (D7-portabel, MAF-fri — håndhevet av test_okf_is_maf_free); navigasjon følger KUN index-cross-links, så promote_verdict linker filen i index.md via en NØYTRAL label (ellers lekker signalet inn i index_summarybundle_context utenom gaten). R4 = valgfri+gated: promote_verdict er en offentlig opt-in-primitiv, wires IKKE inn i run_project (speiler write_verdict — systemet leser; gaten/personaen promoterer). Ærlighets-grenser: promotert fil er MINIMAL (læringssignal kun som description/body-prosa, reproduserer ikke seedens strukturerte realization_rate o.l.); id = læringsnøkkel, så to godkjenninger om samme kandidat deler filnavn (last-write-wins, som write_verdict) — wikien vokser én kuratert fil per distinkt kandidat, ikke per dom-hendelse. Load-bearing-trio (tests/test_step8_promotion_loadbearing.py): gaten avviser ikke-godkjent dom (RØD uten gate); godkjent dom er navigerbar (RØD når link_in_index detaches); promotert signal holdes ute av bundle_context (RØD når en beskrivende index-label lekker det inn). Index-RMW er ikke-atomisk (enprosess-MVP).
  • Kostnadsdisiplin: utvikle primært på lokal profil (gratis); Foundry/Azure (privat tenant finnes) kun til målrettet, minimal verifisering; billigste modeller + små syntetiske data + harde token-tak. Ingen tunge test-kjøringer.
  • 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.

Communication patterns

When linking to local files in responses, use named markdown links — [Human-friendly name](file:///absolute/path), never bare file:// URLs or autolinks <file://...>, always absolute paths (never ~/ or relative), one bullet per file when there are several. (Bare file:// URLs render only the first as clickable across multiple lines; named links stay independently clickable.)