Every one of the run path's seven `outbox.write_*` steps was reachable only through `run.main`, and every path through that builds a chat client. The steps need no model: they take already-rendered data and put it on disk in a byte-deterministic form. Seven subcommands, seven thin adapters. The outbox directory is always the caller's to name -- never a default, never the repository's own, because a step that wrote into a folder the framework also reads as an inbox would bypass the Step-8 promotion gate. `write-outbox` is the one that is not purely mechanical: `outbox.write_outbox` branches on the outcome TYPE, so a door that took the outcome as an argument would let anyone author an outbox of claims and hand it to Step 8 as results. The door DERIVES it through `validate_proposal` -- the run path's own composition -- and a blocked proposal exits 3 with the artefacts still written, since that is where the rejection is recorded. `verdict_id` stays an argument: `verdict-key` already owns that minting. `--stop-reason` is required rather than defaulted to the empty string, inheriting the core writer's measured reason: "the run finished" and "we never found out" must not be the same value. Eight probes, each a subprocess with the subcommand in argv, each asserting on the FILE the command wrote. The ground truth is composed in the test -- the payload it wrote and counted itself, and the byte form the contract requires -- never `outbox._dump`, which would have measured the module against itself. The refusal arm carries its rc-0 control. Measured, own run of the B gate: row 1 8 of 17 -> 15 of 17, exit 1 unchanged, no other row moved. 0 chat-client names reachable from the toolbox (known-positive control: 24 in run.py). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
6.8 KiB
Markdown
67 lines
6.8 KiB
Markdown
# 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](https://claude.com/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`](docs/invarianter.md).
|
|
>
|
|
> **You need none of it to use the framework** — start with the [README](README.md). 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](docs/research/2026-06-23-prior-art-platform.md) (§15 implementeringsregister), [plan](docs/plan/2026-06-23-incremental-plan.md). 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 (`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: in-process `FunctionTool` er 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 fra `ingest_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** av [`portfolio-optimiser-commons`](https://git.fromaitochitta.com/open/portfolio-optimiser-commons) (source of truth, R1 realisert 2026-07-03; publisert i `open/` 2026-08-04 — `commons`-remoten peker fortsatt på den private namespacen og virker uendret). Synk er **pull-only**: endringer committes i commons og hentes med `git subtree pull --prefix=shared commons main --squash`. ALDRI `git subtree push` fra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Se `shared/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`) og `portfolio-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 seks `main()` —
|
|
`costsim`/`hitl`/`preflight` er 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 gjennom `run` bygger 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 utboks-skriverne
|
|
`write-run-config`, `write-coverage`, `write-outbox`, `write-prepass`, `write-parse-failures`,
|
|
`write-proposal-reviews`, `write-debate-tools`) 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 av `tests/test_console_entry_points.py` mot den INSTALLERTE
|
|
distribusjonens metadata, ikke mot TOML-en: en `[project.scripts]`-linje som aldri er `uv 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`](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.)
|