seed_store_from_bundle keyed EVERY `type: verdict` file on bundle_candidate_features — the single candidate the bundle's validator-input.json describes. A bundle carrying verdicts about several candidates collapsed them onto one key, so a verdict about candidate B scored a perfect structural match against candidate A's query and could be folded into A's hypothesis prompt. The ExpeL substrate was single-candidate by construction. A verdict file may now carry its own structural key in frontmatter (affected_codes / measure_type / claimed_saving_nok); absent, keying falls back to the bundle candidate, so every pre-S3.2 seed keeps working unchanged. promote_verdict writes the three fields, so a promoted verdict — frequently about a different candidate than the target bundle's projection — does not impersonate that candidate. Semantics decided HERE, not pulled: commons' seeding rule (method-spec §3 Steg 1 + bundle example) has not arrived; we said we would build locally first. D7 mirroring stays open. - ALL THREE fields or none. A partial declaration raises VerdictFrontmatterError rather than merging with the bundle candidate, which would mint a key belonging to NEITHER candidate. Validation, never repair (mirrors write_concept_file); the tolerant-skip rule belongs to the RAW inbox layer. - claimed_saving_nok parses via json.loads — the SAME literal rule the IR projection went through — and is written back with str() of the raw value. _mint_id hashes that value, so 30000 and 30000.0 are different keys; a normalising writer would split one candidate's signal across two ids. - The structural key is signal-free, so it does not weaken the Step-8 no-leak property (Test C green). Load-bearing MEASURED, five mutations all red: detach per-verdict keying · detach the fields promote_verdict writes · make a partial/unparseable key tolerant · normalise the magnitude on write · remove the fallback (control — breaks the step1 suite at collection, proving the fallback bears load). 589 -> 597 tests. Full gate green (pytest, ruff, mypy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QkjvTTxrg9LTrmghebfiij
14 KiB
portfolio-optimiser
A generic, open framework — built on Microsoft Agent Framework (MAF) — that finds cost savings inside each project of a portfolio of independent projects. A swarm of agents generates candidate measures; a mandatory deterministic validator (solver + Monte Carlo) decides the numbers; domain experts judge the outcomes (human-in-the-loop); and the system learns from their verdicts across runs.
Status: the full 8-step agentic loop is wired and proven with load-bearing tests, and the end-to-end proof is an offline simulation with a scripted stand-in client — no live-model run yet. The ingest layer (real data sources) is implemented — file/CSV and SQL on both stacks with bit-identical golden extractions from the shared spec, plus HTTP as a MAF-only demonstrated extension point against a local mock — but exercised only against committed fixtures: no bundle has yet been materialized from a live source. A sibling implementation of the same method on the Claude Agents SDK is built in parallel from the same shared spec.
Disclaimer — technical framework only. Deploying organizations own their processing purposes and assessments (DPIA, risk/ROS, security review). The framework ships the technical prerequisites — local-only mode, provenance, no silent data egress — but makes no compliance guarantees.
Built on an LLM wiki: Karpathy's idea, Google's format
The knowledge architecture is the heart of the project, and it is deliberately not ours:
- The idea is Andrej Karpathy's "LLM wiki": instead of pointing a model at documents written for people, you curate a small, versioned body of knowledge written for the model to read — concept files, explicit structure, explicit links.
- The format is Google Cloud's Open Knowledge Format (OKF)
(open spec, v0.1), which formalizes that pattern: a knowledge bundle is a directory of
markdown files with YAML frontmatter (one required field,
type), a reservedindex.mdentry point, and intra-bundle cross-links forming an emergent graph. Custom frontmatter fields are allowed and must be preserved — which is exactly where this project's own layers (expert verdicts, ingest provenance) live.
Because OKF is open and vendor-neutral, the same bundles are consumed unchanged by both reference implementations (MAF and the Claude Agents SDK sibling) — the knowledge outlives any particular agent stack.
Not RAG. Agents read a bundle by navigating it — index.md first, then its
cross-links, with progressive disclosure — never by keyword retrieval or stuffing the whole
bundle into a prompt. Query-time retrieval against the bundle is explicitly forbidden by the
method spec: it would leak the verdict layer around the learning gate.
AI-first, humans on top
A traditional wiki is built for people — optimized for humans finding and reading information, with machine access bolted on afterwards. This project inverts that order, and is a concrete example of what that looks like:
- The wiki (the OKF bundle) is written for the model: it is the agent's working memory and the substrate the learning loop reads from and promotes into.
- The human affordances are layers on top: experts judge outcomes by dropping a plain JSON verdict file in an inbox folder; an explicit, fail-closed promotion gate is the only path by which an approved verdict becomes wiki knowledge; reports and reviews are rendered from the machine-readable layers.
Humans stay decisive — nothing enters the wiki without an approval — but the primary reader of every file is the model, not a person browsing.
How it works
One run, one project, eight steps — with the learning loop closing across runs:
- Understand — navigate the project's OKF bundle; fold the candidate's prior expert verdicts into the hypothesis prompt (ExpeL-style, retrieved structurally, never by text).
- Hypothesise — one typed candidate measure (strict IR, fail-fast schema).
- Debate — a maker-checker pair argues the reasoning (round-capped).
- Validate — two falsifiers on the same candidate: the deterministic validator gates the numbers (blocking, never optional) and the checker gates the reasoning.
- Refine — a rejected attempt retries informed by the rejection reason, under hard attempt and token caps. Unbounded loops are forbidden everywhere.
- Propose or discard — a validated proposal with risk percentiles, or a typed rejection.
- Expert feedback — days later, an expert drops a verdict file in an inbox folder; a later run picks it up. Fully resumable; no live session assumed.
- Promote — an approved verdict is lifted into the wiki as a
type: verdictconcept file, navigable by the next run. The gate is fail-closed: raw agent output never self-promotes.
Every proposal carries provenance (citations into the bundle, model, validator decision, token usage). Every seam above is protected by a load-bearing test — a test designed to fail when the seam is detached, so the loop cannot silently degrade into theater.
How it is set up
-
One shared, framework-neutral core (
shared/, a git subtree ofportfolio-optimiser-commons): the business concept, the normative method spec and ingest spec, the expert-reviewer persona as an Agent Skill, and an example bundle with a golden suite as the only ground truth. Both stacks implement from the spec alone. -
Per project: one OKF bundle — the bundled examples are hand-curated; the ingest layer that materializes a bundle from a source (file catalogues/CSV + SQL, HTTP as a MAF-only demonstrated extension point) via a deterministic, schema-validated manifest that runs before the loop is implemented and exercised against committed fixtures — no bundle has yet been materialized from a live source.
-
Run: the
run.pyCLI has three modes — a documented partition, since one invocation cannot exercise every flag:- Single-project —
PROJECT_ID --docs-dir <dir>, plus optional--bundle-dir,--verdict-dir,--outbox-dir(which requires--run-id),--dimension-config,--semantic-retrieval,--decision/--rationale, and--live-dry-run. - Portfolio —
--portfolio, plus optional--goals,--ledger,--dimension-config,--semantic-retrieval; it stops early and prints agoal reached: …line when the accumulated ledger meets a goal. - Value report (S5.4, read-only) —
--report --ledger <file>rolls up the ledger's realized savings to stdout: per-project totals, the portfolio total, flagged cross-dimension overlaps (each counted once), and per-entry provenance. Add--jsonfor deterministic JSON instead of the human table. It makes no model calls and is mode-exclusive — only--ledger/--jsonare permitted alongside--report;--reportrequires--ledger, and a stray--jsonwithout--reportis refused (rc 1, never silently ignored).
# Single-project, offline drill (builds contracts + clients, stops before the first model call): uv run python -m portfolio_optimiser.run FV42-GSV-E1 --docs-dir <docs> --bundle-dir <bundle> --live-dry-run # Portfolio run with a savings goal checked against an accumulated ledger: uv run python -m portfolio_optimiser.run --portfolio --goals goals.json --ledger ledger.json # Read-only value report over an accumulated ledger (human table; add --json for JSON): uv run python -m portfolio_optimiser.run --report --ledger ledger.json--semantic-retrieval(S3.1) is an opt-in ranking change, off by default. Off, prior verdicts are ranked exactly as before: a structural score over the affected cost-code set, measure type and magnitude bucket, with surface text deliberately excluded. On, that score is blended with a cosine term over the same structural triple, which lets a prior verdict on a different cost-code set outrank one that ties structurally.What this ships is the seam, not better retrieval. The bundled
FakeEmbedderis a deterministic sha256 projection carrying no semantics, so over a structural tie the resulting order is deterministic but arbitrary. Retrieval quality depends entirely on injecting a real embedder —--embedder-configselects one from a closed registry (never an import path; a config file can never name arbitrary code to load), anddocs/extending.mddocuments theEmbedderprotocol. The embedding excludesdescription, matching the structural score and the verdict-id hash, so a flag-on run reads no surface text either.The flag is accepted in both run modes, but in single-project mode it requires
--bundle-dirand--verdict-dir: without them it cannot take effect, and the run is refused rather than silently ignoring the flag. Nothing about a flag-off run changes, and no savings claim depends on it.A bundle may hold verdicts about several candidates, while its
validator-input.jsondescribes only one. Atype: verdictfile therefore may declare its own retrieval key in frontmatter —affected_codes,measure_type,claimed_saving_nok— and is keyed on that; omit them and it falls back to the bundle's candidate, exactly as before. The three are all or nothing: a partial declaration is refused rather than merged with the bundle candidate, since the merge would produce a key belonging to neither.promote_verdictwrites all three, so a promoted verdict about one candidate never surfaces for another.The prior-verdict fold — the learning step — happens only on the
--bundle-dirpath; a plain--docs-dir-only run is single-shot (no fold).--decision/--rationaleapply to the single-project path only and are inert in portfolio mode.--outbox-dirmust differ from--verdict-dir: writing the raw outbox into a folder later read as an inbox would re-ingest raw agent output past the promotion gate (self-contamination) — documented here, deliberately not CLI-enforced. Stop criteria and budget caps are required at startup. Try the offline end-to-end proof (no model, no network):uv run python -m portfolio_optimiser.simulation. - Single-project —
-
A global token cap across the whole portfolio, enforced before the call. Per-run caps alone let N projects cost N times that with no ceiling over the pass. Pass a
PortfolioMeter(PortfolioBudget(max_total_tokens=…, max_tokens_per_run=…))torun_portfolioand one ledger bounds the entire pass — and, seeded frombudget.read_spend, a series of passes. It bites in three places: a remainder that cannot fund one run refuses the pass at startup (BudgetRefused); a project that cannot be funded is never started, stopping the pass structurally (budget_stop, completed runs preserved); and a chat call the remainder cannot pay for is refused rather than made (the post-charge check remains, since real usage is only knowable after the response). Spend persists viabudget.write_spend, which takes an explicit stamp and no wall-clock default, so the file is byte-deterministic. Python API only — not yet exposed on the CLI.
What this enables
The reference case is portfolio cost review (the example bundle is a building-energy measure), but the architecture is designed to generalize to any setting with the same shape — candidate measures inside independent projects, numbers a deterministic tool can check, and judgement only an expert has:
- Portfolio reviews — cost savings, energy efficiency, maintenance and procurement measures, proposed per project and validated against the project's own data.
- Compounding organizational memory — approved expert verdicts become navigable knowledge; the next run's hypotheses start from what experts actually decided, including realization gaps no solver can compute.
- Auditable AI — an unbroken provenance chain from expert decision back through proposal, bundle file and text span, and (with ingest) to the source system, query, and timestamp.
- Vendor-neutral knowledge — the same bundles drive two different agent stacks; switching frameworks does not orphan the organization's curated knowledge.
Docs
- Building a knowledge base — the team recipe (technical + domain expert) for curating a bundle, with the honest expectation that a good base takes 1–2 weeks of dedicated work.
- Target picture — the agentic loop + OKF knowledge architecture (north star).
- Prior-art & platform research (incl. implementation register §15).
- Ingest target picture — connectors and the ingest layer (frozen 2026-07-03).
Stack & develop
Python ≥3.10 · MAF via the split GA packages (see pyproject.toml) · uv. Backend profiles:
Azure/Foundry (full) + local (fallback).
uv sync
uv run pytest
uv run ruff check .