One call-time resolver (env PORTFOLIO_SHARED_ROOT, default the in-repo
shared/) consumed by both MAF-side readers of the shared core:
persona._example_path() (the _EXAMPLE_PATH monkeypatch seam is kept) and
simulation._default_bundle_dir() (replaces the _BUNDLE_DIR module global).
De-risks the S4 extraction: re-pointing the commons becomes an env var,
not a code change. Override test proves the marker follows a tmp copy of
the whole shared tree; both detach points proven RED. Suite 155->157.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
Author shared/method-spec.md: the 8-step loop (normative, RFC-2119), the
verdict JSON contract incl. the id-minting algorithm and the chosen conflict
semantics, the inbox/outbox folder contract, the fail-closed promotion-gate
semantics, the IR projection + golden suite as the only ground truth (incl.
the reproducible Monte Carlo procedure), and the budget/stop, provenance and
startup-contract requirements — every normative claim cross-checked against
the load-bearing tests/code. The sibling implementation builds from this spec
alone.
Load-bearing trio (tests/test_method_spec_loadbearing.py, persona-trio
style): required structure, a name-shaped framework-neutrality guard over the
spec + the persona skill tree, and a cross-check-completeness test driven
from the REAL artifacts and the REAL verdict serializer (red on code drift).
All three detach points proven RED (missing file / framework name / dropped
field). shared/README.md: the "(planned)" line replaced with the real entry.
Suite 152 -> 155 passed / 4 skipped; ruff check+format clean; mypy src clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
The expert reviewer was only a hardcoded verdict_input dict inside the offline
simulation. Build it as the real, shared artifact target picture §8 calls for:
shared/skills/expert-reviewer/ — a SKILL.md persona prompt (energy-advisor / M&V
role + the realization-gap methodology the validator cannot compute) plus a
canonical references/example-verdict.json. shared/ stays pure data; the MAF side
reads it via portfolio_optimiser.persona.load_persona_example (call-time,
fail-fast) and the Claude-SDK sibling reads the same JSON with its own loader.
This de-stubs the simulation: its persona judgement (decision + rationale + traced
marker) is now sourced from the artifact at call time, not an inline literal — so
the shared persona is genuinely consumed and cannot rot silently. decision is
binary (approved/rejected, the FeedbackContract the run path accepts);
approved_with_adjustment is rejected there and lives only in the bundle seed
frontmatter + the promotion gate, so the realization correction is carried in the
rationale prose.
Load-bearing trio (tests/test_persona_skill_loadbearing.py), each proven RED on its
own detach: structure + framework-neutrality, the example is valid pipeline input
(incl. FeedbackContract, on a throwaway copy), and the simulation's marker follows
the artifact file. Suite 149->152.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
The primary method proof, offline — a deliberate, cost-driven substitution
for målbilde §11.8's real-model run (the operator runs MAF against no real
model; API for both repos is too costly privately).
`portfolio_optimiser.simulation` drives `run_project` with a scripted
synthetic chat client across two runs separated by a promotion, and shows
the learning loop close end to end:
- ScriptedChatClient subclasses the LAYERED OpenAIChatCompletionClient (not
bare BaseChatClient — else the always-attached BudgetMiddleware no-ops),
constructs offline (loopback url + dummy key), role-keys proposer/checker
replies, and records every prompt into a shared sink.
- simulate_learning_loop: Run A (fresh wiki) -> validated, persona-approved
verdict carrying a realization marker absent from the bundle -> promote_verdict
into the OKF wiki -> seed_store_from_bundle re-reads it -> Run B's hypothesis
prompt carries the marker. An empty-wiki control on Run A proves causality.
- `python -m portfolio_optimiser.simulation` prints an honest trace.
Honesty (§1): this proves the plumbing, the deterministic spine, and that the
learning dataflow closes — NOT that a live LLM would produce the proposal or
verdict (scripted stand-ins). The genuine model-behaviour comparison lives on
the Claude-SDK side (a minimal API run); the scripted client is MAF-side
scaffolding, not part of the framework-neutral shared/ core.
Load-bearing: tests/test_simulation_loadbearing.py goes red when promotion is
detached (the marker never crosses into Run B). Suite 148->149.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
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
The short loop captured the expert verdict inline into an in-memory store, so a
verdict arriving days/weeks later in a separate run could not influence any future
hypothesis (målbilde §5 row 7). Steg 7 adds the long timescale: run_project gains an
opt-in verdict_dir async inbox that load_verdicts_from_dir -> store.add MERGES into the
store BEFORE the Step-1 ExpeL fold, so a verdict dropped after an earlier run reaches a
separate, later run's hypothesis — fully resumable across runs separated in time.
- verdicts.py: verdict_to_dict / verdict_from_dict (id read verbatim, never re-minted),
write_verdict (public authoring primitive, NOT wired into run_project — system reads
the folder, expert/persona writes it, §3 role split), tolerant load_verdicts_from_dir
(missing/foreign/half-written files skipped, not raised — RAW layer per §10 R2),
VerdictStore.from_dir.
- run.py: verdict_dir kwarg; ingest-merge block after load_contracts (merge not replace
keeps run_portfolio's cross-project threading; store.add idempotent on content-hash id;
no change to the fold). CLI --bundle-dir/--verdict-dir thread the long loop to the
console entry. No auto-persist of the run's own captured verdict (outbox/Steg 8).
- Load-bearing PAIR (test_step7_async_loop_loadbearing.py): a verdict dropped after run A
must reach run B's prompt (run B uses a FRESH store -> the transfer is the file loop,
not in-memory carryover); empty-inbox control proves causality. Marker = a realization
value absent from the bundle (not the seed's 0.82). Proven RED on ingest detach.
Suite 138 -> 140 passed, 4 skipped; mypy + ruff check clean. Målbilde treated as frozen
(no §3/§5/§7 edit). Step 8 (gated wiki promotion) remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
Closes gap #5 (maalbilde §5/§7): generate_via_llm's outer max_attempts loop
built the prompt ONCE and re-sent it identically — a BLIND retry. The validator's
per-attempt Rejection.reason was captured in `last` but never reached the next
prompt, so the proposer re-answered the same question with no knowledge of WHY
the prior candidate failed. Step 5 routes that reason into the next attempt.
- generate.py: _build_messages() gains prior_rejection; when set it appends a
revision block carrying ONLY the falsification reason verbatim (never the
rejected proposal JSON). None -> the byte-identical base prompt, so attempt 1
is unchanged. generate_via_llm() rebuilds messages inside the outer loop with
prior_rejection=`last` (None on attempt 1); _fetch_parsed() takes messages as
an explicit parameter. `last` is overwritten each round -> only the most-recent
falsification ("forrige"), never an accumulated history. Bound unchanged:
max_attempts + meter.tick_round (no new loop; §6 — "improve until good enough"
without a ceiling stays impossible).
- Scope honesty: the only per-attempt falsifier here is the validator. The
checker is a run-level, one-shot signal (run.py, before generation); seeding
generation with the checker critique is separately scoped and NOT done here.
The boundary is written into the generate_via_llm docstring + README + CLAUDE.
Load-bearing (maalbilde §7): tests/test_step5_refine_loadbearing.py is a PAIR —
the positive test keys the proposer's flip on the validator REASON PAYLOAD (the
rejected claim value, derived from validate_proposal(bad).reason so test and SUT
share one source of truth), and asserts the reason reached attempt 2's prompt
VERBATIM (the green-but-dead guard). It goes RED on detach (build messages once):
the flip token never arrives, so the outcome never flips AND the verbatim
assertion fails — proven double-red. The bounded control proves a never-fixed
proposer exhausts exactly max_attempts and returns a Rejection.
Adversarial Plan agent hardened the design pre-implementation (flip on payload
not wrapper/call-count; derive flip-key from the validator reason; drive through
generate_via_llm directly; docstring honesty). Suite 136->138 passed, 4 skipped;
mypy + ruff check clean. New test ruff-formatted; pre-existing ruff-format drift
(budget/verdicts/test_contracts) left untouched for a surgical diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
Closes gap #3 (maalbilde §5): the GroupChat checker critiqued into the void —
output_from=[proposer] surfaced only the proposer, so an explicit checker
rejection was ignored and the deterministic validator was the sole gate. Two
falsifiers now act on the same candidate: the validator gates the NUMBERS
(blocking, unchanged), the checker gates the REASONING (maalbilde §2/§6).
- workflow.py: output_from=agents surfaces both participants; the checker
instruction ends with a VERDICT: APPROVE / VERDICT: REJECT - <reason> line.
- run.py: _authored_texts() reads author_name through out.messages (MAF 1.9.0
puts it there, not on the AgentResponse); _debate_text() now selects the
PROPOSER-authored output (fixes a latent texts[-1] regression that would feed
the checker's verdict to generation at even round counts); _checker_verdict()
parses the gate decision. An explicit REJECT overrides an otherwise-validated
outcome to a checker-sourced Rejection. Opt-in-reject (fail-open on a missing
marker). RunResult gains checker_verdict; provenance.validator_decision is
stamped from the validator outcome BEFORE the override, so it never conflates
the two falsifiers (provenance honesty).
Load-bearing (maalbilde §7): tests/test_checker_gate_loadbearing.py is a PAIR —
an explicit checker REJECT on a VALIDATOR-VALID proposal yields a Rejection whose
reason carries the checker's reason while validator_decision stays "validated";
the causality control (checker APPROVE, same proposer) validates normally. Proven
RED on BOTH detach points (revert output_from, or drop the override).
Suite 134->136 passed, 4 skipped; mypy + ruff check clean. Pre-existing
ruff-format drift (backends/budget/verdicts/test_contracts) left untouched for a
surgical diff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
Closes the honest Fase 2a limitation: docs_dir==bundle_dir let keyword
chunk-stuffing leak the verdict's realization rate ("0.82") into the debate /
generation prompt regardless of the ExpeL fold (it surfaced from both
verdict-led-fro.md AND golden.json). The realization signal now reaches the
hypothesis prompt ONLY via the gated ExpeL fold.
- okf.py: bundle_context() + Bundle.context_files render the navigated bundle
(index + frontmatter + cross-links) as the agent read-context, EXCLUDING
type: verdict (maalbilde §2/§4). Pure stdlib, still MAF-free.
- datasource.py: bundle_citations() derives first-class citations from the
navigated non-verdict files.
- run_project: on the bundle path context + citations + debate tools come from
navigation (tools=[]; navigation replaces query-time RAG); the road path keeps
chunk-stuffing unchanged.
Load-bearing (maalbilde §7): the marker is upgraded from the minted verdict id
to the realization signal itself. The empty-store control now asserts "0.82"
reaches NO prompt — RED against the pre-2b chunk-stuffing path, green after
navigation (TDD red->green). New okf-level test_bundle_context_excludes_verdict_layer
guards the seam directly.
Suite 133->134 passed, 4 skipped; mypy + ruff check clean. Reverted unrelated
ruff-format drift (backends/budget/verdicts/test_contracts) to keep the diff
surgical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
Closes maalbilde §5 gap #1 (the one missing "feedback-into-prompt" dataflow)
for the OKF-bundle path. Before, ExpeL was computed AFTER generation into a
discarded SessionContext, so a prior verdict could not influence any hypothesis
(context_providers=0).
- New okf.py: framework-neutral OKF bundle navigation (index + frontmatter +
cross-links), pure stdlib, no agent_framework/mcp (D7-portable), enforced by
test_okf_is_maf_free.
- verdicts.py: seed_store_from_bundle + bundle_candidate_features build the
ExpeL substrate + the pre-hypothesis query key from a bundle.
- run_project(bundle_dir=...): folds the candidate's prior verdicts into the
generation context BEFORE generate_via_llm; the road path is unchanged.
Load-bearing (maalbilde §7): test_step1_expel_loadbearing proves a prior verdict
reaches the hypothesis prompt and goes RED when the fold is detached (shown via
TDD red->green). The marker is the minted verdict id (content hash) because
docs_dir==bundle_dir lets keyword chunk-stuffing leak the realization rate;
clean layer separation is Fase 2b.
Suite 121->133 passed; mypy + ruff check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
Persistent dev-fixture for energieffektivisering (energiledelse/M&V), valgt for
sin lærings-overflate: gapet mellom modellert besparelse (validatoren regner) og
faktisk realisert besparelse i drift (eksperten kjenner) — det ExpeL skal lære.
Ett kontorbygg, ett LED-retrofit-tiltak. OKF-bundle (index/project/hypothesis/
methodology/reference/verdict) bærer kontekst-laget; verdict-led-fro.md koder
realiseringsgraden (RR ≈ 0,82, forankret i National Grid SBS 2010) som ExpeL-frø.
Energi mappet inn i den EKSISTERENDE kost-IR-en uendret (affected = byggets totale
energikostnad, claimed = modellert besparelse ~10 % < 30 %-cap), så validatoren
kjører som-den-er — src/ urørt. golden.json fryser de seeded percentilene; testen
beviser at fixturen er konsumerbar (validerer, ikke Rejection), ikke bare til stede.
Domenetall verifisert mot primærkilder (EVO/IPMVP, DOE/NREL UMP, CPUC, fire
evalueringsstudier); norsk energipris mot SSB Q1 2026. README + shared/README
oppdatert (eksempel finnes, ikke lenger "planned"). Suite 121/4, ruff+mypy rene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
/trekreview flagged the Spike B(b) fan-out experiment as BROKEN_SUCCESS_CRITERION
(BLOCKER): it asserted a per-client call_count reached 3 on a reused instance vs
1 on a fresh one — a tautology true for any un-reset mutable counter, independent
of MAF, that never exercised the real G2/B7 shared-Workflow state-corruption
footgun. It was a false-confirm of a de-risk assumption.
Rebuilt to observe genuine MAF thread state via the messages each participant
RECEIVES (new FakeChatClient.received_texts seam):
- shared_instance_conversation_bleed: a reused built ConcurrentBuilder Workflow
accumulates the conversation across .run() calls — run N's participants receive
runs 0..N-1's prompts/replies (measured [[p0],[p0,p1],[p0,p1,p2]], strictly
monotonic) => genuine cross-run contamination.
- fresh_instance_conversation_isolation: a fresh instance per run gives each a
clean thread => each participant sees only its own project ([[p0],[p1],[p2]]).
Assumption now CONFIRMED with a meaningful observable. findings-b.md gains a
Method note recording why it was rebuilt; README rows updated.
Also fixes the MINOR: a_groupchat.run_live now mkdirs the findings dir before
write_text so a post-disposal run does not lose the measured result.
Gate green: ruff check + format, mypy src, pytest 48 passed / 1 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fif1r1En5W542HbZV88yMH