Claude Agent SDK sibling implementation of the portfolio-optimiser method: an open Python framework that finds cost savings inside each project in a portfolio.
Find a file
Kjell Tore Guttormsen 3587854074 feat(run): C2.0 — shippable step-7 run entrance + K2.9 seam bindings (closes C-N2, R-10, K2.9)
- run.py: compose_run_context (§5: merge inbox -> seed -> fold, read-only on
  the inbox) + execute_run (§8 meter, artifacts persisted on BOTH outcomes,
  structured exit 3 on budget stop) + thin CLI (python -m ..run). The model
  client is injected; only default_client_factory constructs the SDK client
  (wired, never executed by the suite). The navigated docs dir comes from the
  validated startup contract (resolves review OBS-2 on the shippable path;
  run_s10.py stays byte-frozen fasit -> won't-fix there).
- test_run_entrance_loadbearing.py: inbox verdict reaches the composed
  context (detach-proven: merge dropped -> red), empty/missing-inbox
  controls, read-only inbox byte-proof, R-10 budget-stop binding via the NEW
  entrance (detach-proven: stop persistence dropped -> red), happy path
  through the CLI with the inbox signal surviving the chain, SDK-wiring test.
- test_ingest_adoption.py (K2.9): the two library guarantees the consumer
  relies on, bound through the seam — empty CSV -> typed SourceError with NO
  partial bundle on disk; non-SELECT SQL -> SourceError 'returned no columns'
  (behavior verified empirically against pin dae0bd1a before binding).
- README: inbox section now points at the shippable entrance; run.py added
  to the run layer; stale test count 265 -> 395.

386 -> 395 tests, full gate green (pytest, ruff check+format, mypy strict);
goldens unchanged; runs/s10 and run_s10.py untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 03:28:31 +02:00
docs docs(review): full-paritet review + sesjonsplanverk + beslutningsbrief (Fable 5 + 6 Opus-agenter) 2026-07-16 08:36:07 +02:00
examples feat(ingest): I5 — SQL D7-speil, bygget fra commons-spec alene 2026-07-04 07:18:59 +02:00
runs/s10 feat(run): S10 fullført — live-kjøring validert på 1. forsøk, artefakter persistert 2026-07-03 10:57:28 +02:00
shared Merge commit '6c73d1f7c1' 2026-07-03 22:33:47 +02:00
src/portfolio_optimiser_claude feat(run): C2.0 — shippable step-7 run entrance + K2.9 seam bindings (closes C-N2, R-10, K2.9) 2026-07-17 03:28:31 +02:00
tests feat(run): C2.0 — shippable step-7 run entrance + K2.9 seam bindings (closes C-N2, R-10, K2.9) 2026-07-17 03:28:31 +02:00
.gitignore feat: initial commit — repo bootstrap (D7 sibling on Claude Agent SDK) 2026-07-03 06:00:09 +02:00
CHANGELOG.md chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
CLAUDE.md feat(scaffold): S5 — D7 sibling scaffold: SDK dep, fail-fast startup contracts, CLAUDE.md 2026-07-03 06:06:03 +02:00
CODE_OF_CONDUCT.md chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
CONTRIBUTING.md chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
LICENSE chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
pyproject.toml feat(ingest): adopt llm-ingestion-okf as Door A implementation (first consumer) 2026-07-16 20:46:51 +02:00
README.md feat(run): C2.0 — shippable step-7 run entrance + K2.9 seam bindings (closes C-N2, R-10, K2.9) 2026-07-17 03:28:31 +02:00
SECURITY.md chore(release): MIT license + CONTRIBUTING/SECURITY/CODE_OF_CONDUCT + README badges (S12) 2026-07-05 10:32:24 +02:00
uv.lock feat(ingest): adopt llm-ingestion-okf as Door A implementation (first consumer) 2026-07-16 20:46:51 +02:00

portfolio-optimiser-claude

License: MIT Python Built on Claude Agent SDK

Sibling implementation of the portfolio-optimiser method on the Claude Agent SDK (decision D7). An open, generic Python framework that finds cost savings inside each project in a portfolio of independent projects: agents generate candidate measures, a mandatory deterministic validator gates the numbers, domain experts judge via human-in-the-loop, and the system learns from the verdicts.

Status: the D7 build (S5S10) is complete, and the deterministic ingest layer (CSV and SQL source types) has since been added in front of the loop. The deterministic backbone, the agentic loop, the learning loop, and the ingest connectors are wired seam by seam, each proven by load-bearing tests (395 tests, all running offline without an API key). The programme's single budgeted live model run has been executed and validated — its artifacts are committed under runs/s10/ (see below).

Disclaimer — technical framework only. The deployer owns DPIA, risk assessment, and the legal basis for any processing. The framework ships only the technical preconditions: local-only operation, first-class provenance, no silent data egress.

Built from the spec, not the sibling

The method itself is framework-neutral and lives in portfolio-optimiser-commons (consumed here as a git subtree under shared/): the normative method spec (RFC 2119), the OKF bundle-navigation contract, the golden/conformance suite — the only oracle for the validator — and the shared expert-reviewer persona skill. This repo implements that spec on the Claude Agent SDK; it deliberately does not reverse-engineer the MAF sibling (open/portfolio-optimiser). Two independent implementations of one spec, compared afterwards, is the point of D7.

Architecture — the seams

Everything below the run layer is pure config/file logic and runs deterministically, offline. Module by module:

Deterministic backbone (method-spec §3 step 4, §7§10)

  • ir.py — the typed cost-IR of a candidate measure (§7.1).
  • validator.py — the deterministic validator; blocking, and frozen by the shared golden suite (§7.2), which is the only fasit it answers to.
  • provenance.py — the first-class provenance stamp (§9); authoritative data, not after-the-fact logging.
  • contracts.py — fail-fast startup contracts (§10): stop criteria and budget caps are required at startup, and the model map (data/model_map.json, role → Claude model id per backend profile) is validated before anything runs.

Context seam (§3 step 1)

  • okf.py — read-context built by navigating the project's OKF bundle (index.md + frontmatter + cross-links, progressive disclosure) — never keyword chunk-stuffing. The type: verdict layer is excluded from the read-context.
  • experience.py — the ExpeL-style experience seam: store, structural retrieval, and the gated fold. A prior expert verdict reaches the next hypothesis only through the fold, never by leaking through context.

Agentic loop (§3 steps 25, §8)

  • budget.py — the budget meter: no unbounded loop exists anywhere in the framework.
  • loop.py — generate, makerchecker debate, gate, and informed refinement: the validator's previous rejection reason is fed into the next bounded attempt, so the model corrects against the falsification instead of re-answering identically.

Learning loop (§3 steps 78, §4§6)

  • inbox.py — the async verdict-file contract: an expert drops a plain-JSON verdict into an inbox folder after a run; a later run ingests it tolerantly and merges it before the fold. The shippable entrance for that later run is run.py: uv run python -m portfolio_optimiser_claude.run --bundle <dir> --inbox <dir>.
  • promotion.py — the promotion gate, fail-closed: only an approved verdict is lifted into the OKF context layer; anything else raises and writes nothing.
  • persona.py — the expert-reviewer persona sourced from the shared artifact in shared/skills/expert-reviewer/ at call time, so the shared persona is genuinely consumed and cannot rot silently.

Run layer (the only part that touches the network)

  • sdk_client.py — the Claude Agent SDK client, isolated from local configuration (setting_sources=[]) so no user/project config can leak into a run.
  • artifacts.py — §9 citations plus deterministic run-artifact persistence, including on structured stops (a budget stop still leaves artifacts behind).
  • run.py — the generic run entrance: composes merge-inbox → seed → fold (§5) and drives the loop under the budget meter, persisting artifacts on both outcomes — a structured budget stop included. The model client is injected, so the offline suite proves the same orchestration with a scripted client; only the CLI's default constructs the SDK client.
  • run_s10.py — the programme's ONE live run (cost discipline D6); run-path only.

Load-bearing tests (§11)

Every seam is proven by a test that goes red when the seam is detached — green-but-dead is the failure mode the rule exists for. Among them: test_step1_expel_loadbearing.py (the verdict signal reaches the prompt via the fold, and only via the fold), test_checker_gate_loadbearing.py (an explicit checker reject blocks a validated proposal), test_step5_refine_loadbearing.py (the rejection reason verifiably reaches the retry prompt, and the loop still stops at the cap), test_step7_async_loop_loadbearing.py (a verdict dropped after run A reaches run B's prompt through the file loop, with an empty-inbox control), test_step8_promotion_loadbearing.py (the gate refuses non-approved verdicts; the promoted signal stays out of the read-context), and test_sdk_isolation.py (local config cannot capture the checker).

The ingest layer — CSV and SQL, in front of the loop

The method spec forbids query-time retrieval against the bundle (§3 Step 1), so data reaches the model only via OKF bundles. The ingest layer is the deterministic step that satisfies that: a connector reads a real source, and the extract is materialized as an OKF bundle the existing 8-step loop then consumes unchanged. It makes zero model calls, touches no network, and ingest.py imports nothing from the SDK — it is pure standard library. Built from the shared ingest-spec.md alone.

D7 implements the two conformance-required source types:

  • file — a local CSV catalogue; extraction paths are boundary-checked fail-closed against the source root (the OKF path rule).
  • sql — a local SQLite database, opened read-only (mode=ro), one SELECT per extraction; the connection location is resolved at run time from a named environment variable (connection_ref), never stored in the manifest.

Both are frozen by byte-identical golden extractions (examples/ingest-golden-file/, examples/ingest-golden-sql/) and by the load-bearing seam tests (provenance stamping, navigability through the unchanged okf.py, the reserved verdict layer, and re-ingest safety over a promoted verdict).

Honesty rule (§1): the http source type is an optional extension point, not built in D7 — a manifest naming it is rejected fail-fast at validation (test_malformed_manifest_is_rejected), never silently accepted. The HTTP/MCP extension point is demonstrated only in the MAF sibling (against a local mock, behind an opt-in network flag); this repo ships no network connector and no live-source integration. How the layer works and how one would extend it is documented in docs/extending.md.

The live run — S10, executed and validated

Where the MAF sibling proves its loop end-to-end with a scripted offline simulation, this repo's end-to-end proof is the programme's single budgeted real run (D6: exactly one live API run in the whole programme), executed 2026-07-03 against the micro bundle shared/examples/bygg-energi-mikro/:

  • exit 0 · validator validated · checker approve on the first attempt · 2 of 12 rounds · 36 791 of 150 000 budgeted tokens · cost $0.127514 (Haiku 4.5, per the model map), under a first-class max_budget_usd cap.
  • The proposal claimed a deliberately conservative 30 000 NOK saving against the bundle's p10p90 band of 68.5k121k — and validates.
  • All four artifacts are committed as fixed reference output in runs/s10/: proposal.json, provenance.json (with §9 citations), run_result.json, usage.json.

Honesty rule (§1): everything else in the repo is deterministic and offline; nothing here claims more live behaviour than that one documented run.

Stack

Python ≥3.10 · claude-agent-sdk ≥0.2 (bundles the Claude Code CLI; an API key is needed only at actual query() time) · uv · Pydantic for contract validation.

Development

uv sync          # install dependencies
uv run pytest    # 395 tests — run without any API key and without network
uv run ruff check . && uv run ruff format --check .
uv run mypy src  # strict

The offline invariant is deliberate: everything below the run layer is pure config/file logic, so the full suite (including every load-bearing seam proof) runs with no key and no network.