B's premise applied to the three steps that DECIDE a proposal: a proposal authored outside po -- by a human, or by an agent that is not po -- now meets the blocking deterministic gate, mints the learning key, and is captured as a Verdict, all without a chat client on the way. Three thin adapters, no second implementation. The reason is the one the first four doors were built on, but it bites harder here: the refusal SENTENCE is fed back verbatim into the next attempt by step 5, so a door that reworded it would break the repair loop while still looking correct. The probes assert the sentence, not a substring two stages share. One measurement decided a design detail. The IR writes whole magnitudes as JSON integers (30000), the run path carries the pydantic float, and verdicts._mint_id hashes the raw value -- so minting from the undeclared JSON would hand out a DIFFERENT verdict id than the debate does for the same proposal. The door therefore reads the proposal through SavingsProposal and feeds model_dump() to the public features_from_ir; the probe pins both forms and asserts they differ, so the shortcut cannot come back silently. A blocked proposal exits 3, carrying the verdict rather than an exception envelope. "You asked right and the answer is no" is the same fact whether a file was missing or a claim was infeasible, and a caller that only reads the exit code must not see a blocked proposal as a cleared one. Fasit outside the door in every arm: the base's own checked-in golden suite (written before the toolbox existed, so it cannot have been fitted to it), a cost baseline authored in the test, a method cap computed by hand from the fixture, and the public minting rule. Each refusal arm has an rc-0 control on an argv that would otherwise be accepted. STATED LIMIT: the input-grounding stage (P7, stage 0b) has no flag here. It falsifies a proposal against the rendered prompt the model received, and an outside caller has no such prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.6 KiB
portfolio-optimiser
Note for visitors — what this file is. This is the working agreement between the repository and the AI coding agent that builds it (the Claude Code convention), and it is written in Norwegian because that is the maintainer's working language. The repository's invariant ledger — each design decision, the measurement that forced it, and the test that turns red when the decision is undone — lives in
docs/invarianter.md.You need none of it to use the framework — start with the README. It is published anyway, because the reasoning behind a decision is worth more than the decision, and because a rule kept out of sight is a rule that drifts without anyone noticing.
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.16.0, -orchestrations 1.1.1 — F15, 02.09). Pakkehåndtering: uv. To backend-profiler: Azure/Foundry (full) + lokal (fallback).
Konvensjoner
- Type hints overalt (
mypyder mulig). Pydantic for validering/IR. rufffor lint+format.pytestfor 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: in-process
FunctionTooler default-sømmen i kjørestien. MCP er wiret som opt-in i kjørestien (mcp_tools.py+--mcp-config, Trekk B 2026-08-05): konkrete eksterne servere blir verktøy agentene kan kalle UNDER debatten. Uten konfig gjøres null nettverkskall og verktøylista er uendret. Tre regler er load-bearing: allowlist er påkrevd (tom liste ville latt motparten bestemme hva agentene får kalle), hver server og hvert tillatte verktøy navngis i kunngjøringen før første kall (også uten--mandate— ingen udeklarert egress), og--live-dry-runåpner ingenting. Egen søm fraingest_mcp.py(kildedokumenter FØR kjøring, null-argument-tools) — samme protokoll, ulik jobb.build_mcp_server(datasource.py) er fortsatt kun demo. Data-source-konfig JSON-Schema-validert, fail-fast. shared/er en git subtree avportfolio-optimiser-commons(source of truth, R1 realisert 2026-07-03; publisert iopen/2026-08-04 —commons-remoten peker fortsatt på den private namespacen og virker uendret). Synk er pull-only: endringer committes i commons og hentes medgit subtree pull --prefix=shared commons main --squash. ALDRIgit subtree pushfra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Seshared/README.md. Wheelen bærer treet som pakkede data siden Fase 4a — se invarianten under.
Kommandoer
- Sync:
uv sync— installerer tre konsoll-kommandoer:portfolio-optimiser(CLI,run:main),portfolio-optimiser-demo(offline-beviset,simulation:main) ogportfolio-optimiser-toolbox(kjørestiens steg som rene kommandoer,toolbox:main— 20.09.2026).python -m-formene virker uendret og er byte-identiske på stdout (målt). Bevisst KUN tre av seksmain()—costsim/hitl/preflighter operatørverktøy, ikke produktets inngang, og hvert navn her er et navn frysen må bære. Verktøykassen er der fordi den er det ENE de to andre ikke kan brukes til: hver vei gjennomrunbygger en chatklient, og stegene den bygger på trenger ingen modell. Underkommandoene er stegenes egne navn (navigate-bundle,cost-baseline,retrieve-chunks,prepass-admit,validate-proposal,verdict-key,capture-verdict) og hver er en TYNN adapter over den funksjonen kjørestien kaller — aldri en andre implementasjon. Exit 0 kjørte, 2 feil kall, 3 NEKTET med grunnen navngitt; et forslag den deterministiske validatoren BLOKKERER er også 3, og bærer dommen (ordrett grunn + stadiet) framfor en unntaks-konvolutt. Pinnet avtests/test_console_entry_points.pymot den INSTALLERTE distribusjonens metadata, ikke mot TOML-en: en[project.scripts]-linje som aldri eruv sync-et er en påstand, ikke en kommando. - 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.
- Stoppkriterier + budsjett-tak påkrevd ved oppstart (fail-fast, aldri ubegrenset loop).
- Group Chat maker-checker som debatt-default (IKKE Magentic, som er eksperimentell).
- 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. - Invariant-hovedboken bor i
docs/invarianter.md(flyttet 18.09.2026, uendret): hver målte beslutning med sin load-bearing test. Nye rader skrives DER, aldri her — denne fila passerte 300 000 tegn mot Claude Codes 150 000-grense, og en regel bak kuttet når ingen. Les den raden som gjelder sømmen du rører (grep -n '^- \*\*<stikkord>' docs/invarianter.md) FØR du endrer den.
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.)