S5.2-analog. New notify.py: Notifier protocol + console/file/webhook sinks. The
webhook (the one transport that leaves the machine) fires ONLY behind an explicit
per-run opt-in flag (--allow-webhook-egress), mirroring ingest-spec §8 (the flag
is a run argument, never a config field). Transport is injected — canned in the
suite (NULL socket), real transport behind one seam function default_webhook_transport;
an AST grep-guard proves no network path exists outside that seam. run.py (both
outcomes — a budget stop notifies too) and hitl.py (read-only preserved) share the
same opt-in-gated CLI seam, refusing a webhook-without-opt-in before any spend.
Payload shape is stack-local (no shared notification spec; divergence documented).
Two new load-bearing test files (18 tests): opt-in gate + payload structure + the
grep-guard + run/hitl emit wiring + run-level opt-in threading, each detach-proven
RED. 544 -> 562 green, full gate clean (ruff+format+mypy strict, 26 src files).
README sync (test count x2 + notify.py module note + load-bearing omtale).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
The operator's view of the long feedback loop (S5.1-analog, parity row 22;
buildable after K5): which proposals still AWAIT an expert verdict, and who
should judge each — a pure file-based id-join across the three layers hitl
READS and NEVER writes (role split §3 Step 7: the expert writes the inbox, the
system reads it; notification is K10's job, never this).
- hitl.py:
* pending_proposals — the id-join. An outbox proposal (K5) is pending unless
its persisted verdict_id (read verbatim from {run_id}-outcome.json, minted
the SAME way the inbox mints a verdict id — the K5 assumption) is in the
settled set. settled = §4.2-valid inbox verdicts (THROUGH load_inbox, so a
skipped/unknown decision never settles anything) ∪ promoted verdicts (§6,
optional bundle_dirs, so the core join is exactly outbox↔inbox).
* RoutingContract — nøkkel→ekspert, schema-validated fail-fast (§10): non-empty
table, non-empty keys/expert ids, optional default_expert. route_pending maps
a proposal's measure (a config-string key NOW; K13 formalizes the dimension
catalog) to an expert; an unmatched measure → default, else UNROUTED.
* CLI python -m …hitl pending|route — pending is a pure report (exit 0); route
loads the routing config fail-fast (a malformed/missing config exits non-zero
WITHOUT touching any layer). Neither subcommand writes anything.
- test_hitl_loadbearing.py: 23 tests. TWO seams detach-proven RED — the id-join
seam (drop the `not in settled` filter → a judged proposal is STILL listed →
red) and the read-only seam (any read path that writes a byte → the before/
after outbox+inbox snapshot diverges → red). Covers: undecided → pending,
inbox/promoted verdict settles, exact-id join (no coincidental match), skipped
decision does not settle, deterministic order, malformed routing fail-fast,
measure→expert / default / UNROUTED, and the CLI subcommands.
- 521→544 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
25 src files). README: test-count sync ×2 + hitl module note + load-bearing
mention. IKKE-scope (held): notification (K10), web-UI, writing the inbox.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
A future operator-gated live run (the M2-analog) is fully rigged and rehearsed
OFFLINE — without one model call, without a key (S4.2-analog, parity row 21;
buildable after K5 + K7). `--live-dry-run` builds everything a real run would
(contracts fail-fast §10 → compose §5 → SDK-client construction → preflight)
and captures the run-config + preflight artifacts, then STOPS before the first
model call. The stop IS the boundary: the loop is never entered, so nothing is
spent (strictly offline, no D6 gate).
- run.py --live-dry-run: requires --outbox + --run-id (the drill's artifacts are
run_id-named), rejected fail-fast before any build. Writes a run_id-named PAIR
to the outbox:
* {run_id}-runconfig.json — comparison-protocol §4 pt 3: model-id per role the
loop calls (proposer/checker, THROUGH resolve_model — the run's own path),
profile, and every cap/parameter. Deliberately NO wall-clock date, so the
bytes stay deterministic (the run's date is stamped at report time, §4 pt 3).
* {run_id}-preflight.json — the captured preflight verdict (clear + refusals).
The drill CAPTURES the preflight result rather than gating the build on it:
exit 0 when clear (rig go-live-ready), non-zero when refused — artifacts
captured and ZERO model calls in EITHER case.
- The client is constructed (the verified key-free SDK premise) but never called;
a call-counting stand-in proves 0 calls. Bytes reuse the deterministic house
JSON writer; run_s10.py/runs/ byte-untouched.
- test_dry_run_loadbearing.py: 7 tests. TWO seams detach-proven RED — the
0-calls stop seam (neutralise the branch → falls to execute_run → the counting
client fires → red) and the capture seam (drop the writes → outbox lacks the
pair → red). Env monkeypatched so the preflight verdict is deterministic
regardless of the operator's ambient shell.
- 514→521 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
24 src files). README: test-count sync ×2 + run.py drill note + load-bearing
mention. IKKE-scope (held): the actual live run (M2-analog, operator) and any
change to preflight/outbox.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
Everything that CAN be validated WITHOUT a model call is validated BEFORE the
operator pays for one (S4.1-analog, SDK-native — Foundry-auth is MAF-specific,
not mirrored). The preflight IS the boundary: it never calls query(), never
validates a credential VALUE online, never touches the network. It returns a
list of structured, actionable Refusals; the CLI exits non-zero on any, so a
broken config stops cheaply instead of on the first billed call.
- preflight.py: run_preflight + `python -m …preflight --profile anthropic`.
Four offline checks:
* credential — ANTHROPIC_API_KEY set + not a placeholder form (the value is
NEVER checked online, only presence, §1); the bundled CLI's own
CLAUDE_CODE_OAUTH_TOKEN also satisfies it (run_s10 relies on it — refusing
would be a false alarm).
* model_map — the requested profile exists and every id it resolves to,
THROUGH resolve_model (so the default fall-through is covered), is real,
not a placeholder left in config.
* sdk — claude_agent_sdk imports (a missing install is a structured refusal
naming `uv sync`, never an ImportError out of the preflight), the run-path
symbols exist, and the bundled Claude Code CLI is present on disk — located
OFFLINE via the SDK package's own files, mirroring the SDK's _find_cli
order (bundled first, then a claude on PATH). Verified against installed
0.2.120 (bundled binary present; the STATE 0.2.110 note was stale).
* termination — the §8 stop contract constructs with positive caps and the
per-call USD belt is positive.
- tests/test_preflight.py: credential contract, placeholder detector, model_map
incl. unknown-profile-without-raising, SDK + bundled-CLI offline probe (with a
monkeypatched missing-SDK refusal), termination, run_preflight aggregation,
CLI both paths, and the offline guards (no network import, no query() call —
AST-based so prose stays green). THREE seams detach-proven RED: credential
branch, placeholder model-id guard, no-network grep-guard.
478→514 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
24 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
preflight.py module note + load-bearing mention. IKKE-scope (held): the actual
API call (ALDRI — the preflight IS the boundary) and Foundry/Azure auth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
Before ANY spend the operator sees a deterministic UPPER-BOUND USD estimate
for a (portfolio-)run — a what-if over the models in model_map.json (Claude
models) × effort levels (S3.6-analog, D-I pkt. 3 MUST-krav). No network, no
model call, no key: pure config arithmetic (bound by an import-purity test,
mirroring okf.py).
- contracts.py: ModelPriceContract (usd_per_mtok > 0 + REQUIRED source +
source_date so a stale rate is visible, never silent, §1) + PricingContract
(non-empty; no hardcoded fallback rate) + load_pricing/_bundled_pricing.
- data/pricing.example.json: per-Mtok rate per model id, each with source+date.
Example rates are Anthropic's OUTPUT price (the higher rate) so the whole cap
billed at that single rate can only overstate — the figure is marked ESTIMAT.
Covers the model model_map configures, so the default path runs green.
- costsim.py: estimate_costs (n_projects × cap × effort_factor tokens at the
per-Mtok rate; a model with no price fails fast "missing price for <id>",
never a guess) + render_estimate + `python -m …costsim`. Effort factors are
a coarse modeling weight (not prices) — max effort = full cap = the true
upper bound. No price literal anywhere (grep-guard proves it).
- tests/test_costsim.py: schema fail-fast, missing-price fail-fast, scales with
model × effort + reproducible, grep-guard, import purity, bundled-example +
CLI offline smoke. Three seams detach-proven RED (effort factor, price guard,
price literal).
462→478 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
23 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
costsim.py module note. CLI run-total-cap wiring stays out of scope (planen
lists 4 files); the mechanism is complete and proven load-bearing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
Add a pre-call USD belt on top of the post-charge token/round meter (§8),
so no future live run can loop past its run budget. Belt-and-braces above
the SDK's per-call max_budget_usd cap.
- budget.py: optional run-total `max_cost_usd` on BudgetMeter (fail-fast on
non-positive, §10) + `guard_before_call(spent_usd)` raising the same
structured stop event (BudgetKind widened with "cost_usd"; limit/observed
→ float). Reaching the cap exactly does not stop; crossing it does
(mirrors the token cap).
- loop.py: `_guarded_complete` helper reads the client's accumulated
total_cost_usd (0.0 for scripted clients) and guards BEFORE every
client.complete; all three call sites routed through it — one detach point.
- sdk_client.py: total_cost_usd already exposed/accumulated — untouched.
- tests/test_budget.py: meter-level cap tests + load-bearing loop-wiring
test (counting client; detach the guard → unguarded loop runs to the round
cap → kind "rounds" not "cost_usd" → red).
457→462 green, golden byte-exact, full gate clean (ruff+format+mypy strict,
22 src files), run_s10.py/runs/ byte-untouched. README test-count sync ×2 +
budget.py belt note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
S2.1-analog: each completed run persists a run_id-named proposal/outcome pair
to the outbox — the system's OWN output layer (the role split §3 Step 7 governs
the inbox and wiki, not this). The outcome carries outcome type + figures, the
two §9 falsifiers mirrored verbatim from the RunResult, the provenance stamp,
and verdict_id — minted the SAME way inbox.py mints an expert verdict's id
(mint_verdict_id over the proposal's candidate features), so a later inbox
verdict about the same candidate joins by id (the K9 key assumption, pinned
here in test and reused there).
New outbox.py reuses artifacts' deterministic house JSON writer (sorted keys /
indent 2 / LF) — same input + same run_id => byte-identical files — and never
touches the S10 artifacts.py fasit formats. run.py grows optional
--outbox/--run-id; run_id is REQUIRED when the outbox is set (no wall-clock
default — a timestamp would break determinism) and is fail-fasted at the CLI
BEFORE any client/spend. A budget stop has no proposal, so it writes no pair.
New test_outbox_loadbearing.py (14): unit (pair, verdict_id join key on both
outcome types, percentiles-vs-reason, verbatim falsifiers, provenance,
round-trip, byte-determinism, run_id fail-fast) + wiring (entrance writes the
pair; no-outbox control; --outbox-without-run-id fails fast before spend).
Detach-proved: drop the persist_outbox call in execute_run -> wiring test RED.
443->457 green, golden byte-exact (13/13), run_s10.py/runs/ untouched, full
gate clean (ruff+format+mypy strict). README synced (count + module + seam).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
A single VerdictStore threaded through run_portfolio: a verdict available when
project k composes survives into project k+1's fold (method-spec §5 cross-project
threading). The optional verdict_dir is the portfolio-level expert inbox, read
before each fold (role split §3 Step 7 — the portfolio never writes a run's own
verdict back; §1/§6 — no self-contamination, only expert/seed verdicts cross).
compose_run_context gains an optional passed-in store (None = fresh; every
existing caller composes exactly as before).
Load-bearing (tests/test_portfolio_learning_loadbearing.py), 2 detach proofs +
control + §4.2 idempotency:
- cross-project threading: project 1's bundle seed survives into project 2's
prompt via the shared store; detach (compose ignores the passed-in store,
always fresh) -> red.
- portfolio inbox fold: a verdict_dir marker reaches the project's fold; detach
(drop the run_portfolio merge) -> red; control (no verdict_dir) -> marker absent.
- double-merge idempotency: a verdict merged before every project folds exactly
once (first-write-wins on id).
437->442 green, golden byte-exact, full gate clean (ruff + format + mypy strict).
run_s10.py and runs/ byte-untouched. README synced (test count, portfolio block,
load-bearing list). K2 re-entrancy test stays green — the shared store threads
verdict fold lines only, never bundle context markers.
[skip-docs]: no invariant changed (CLAUDE.md untouched); the run_portfolio and
compose_run_context docstrings + README carry the doc need.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
New portfolio.py: run_portfolio drives N projects sequentially from a
schema-validated reference config, composing each project's §5 context
(merge inbox -> seed -> fold) and running the loop core UNCHANGED per
project, collecting one typed result per project IN CONFIG ORDER. This is
the run path MAF got in its Fase 1 and D7 never had — the prior entrances
(run.py, run_s10.py) drive a single bundle. PortfolioResult holds
per-project results tagged with the config project_id.
Re-entrancy (§3 Step 3): each project composes its OWN context inside the
loop, never a hoisted shared one, so nothing survives one project into the
next except the explicitly shared mutable state — the §8 budget meter, a
portfolio-wide cap. Failure policy is a STACK-LOCAL choice until D-D: the
default RAISES (today everything is thrown); K18 flips it to
collect-and-continue when the D-D wave model lands.
New config contract in contracts.py: ReferenceProjectContract (project_id +
required non-empty bundle_dir + optional inbox_dir) + ReferenceProjectsContract,
loaded fail-fast by load_reference_projects (§10) — a project without a
bundle path is refused before any run. New data/reference_projects.json
example (shape-validated, never executed by the suite). New repo-local
mini-bundle fixture under tests/data/ (a distinct second project, VFD-retrofit
— ALDRI in shared/).
Two detach proofs delivered: drop the bundle_dir Field requirement -> a run
starts on the invalid config and only crashes mid-run -> the fail-fast test
goes red; hoist the per-project composition out of the loop -> project 2 runs
on project 1's context and the VFD marker never reaches its prompt -> the
re-entrancy test goes red. 11 new tests (test_portfolio.py 5 +
test_contracts.py TestReferenceProjects 6). 426 -> 437 tests, golden
byte-exact, full gate clean (ruff + format + mypy strict). README synced
(test count + a Run layer module block).
[skip-docs] — README documents the new module; CLAUDE.md holds invariants
(rules/commands) only, and K2 adds no new invariant, command, or convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiTwaKLesgcwXx2mDviqpt
New ledger.py: typed SavingsLedger; realize is fail-closed on an APPROVED
FeedbackContract + a named expert + an explicit timestamp (the §6 determinism
rule — no wall-clock default). The sum key is DIMENSION-FREE (the dimension
label is annotation only and never participates in the mint), so the same
realized saving surfaced via two dimensions lands in one first-write-wins
slot and is never double-counted. Deterministic JSON persistence
(sort_keys, indent 2, LF, trailing newline), schema-validated on load.
New goals.py: GoalContract (absolute target, hard/soft, fail-fast §10).
A hard goal reached raises GoalReached, a structured stop event carrying
target + observed — never a silent stop; soft flags without stopping.
The percent-goal baseline is D-E-gated: the field is reserved and
construction refuses with an explicit NotImplementedError.
Semantics are marked STACK-LOCAL in the docstrings — mirrored from the MAF
plan's capability description, never from MAF code; format shareability
stays a proposed decision point in the brief.
Two detach proofs delivered (decision gate removed -> red; dimension into
the key mint -> the double-counting test red). 400 -> 426 tests; README
synced (test count + a Value layer module block).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scripted two-run driver over the run.py composition: run A -> persona verdict
(shared skill artifact) -> §6 promotion gate -> run B on a fresh store. The
marker crosses runs via the promoted wiki layer ONLY - run B reads no inbox,
a rejected verdict is refused fail-closed and its marker never crosses.
Two detach proofs delivered (promotion step removed -> red; verdict exclusion
in bundle_context removed -> red via the '## verdict' section anchor).
Known-limitation note (C-F5, deferred to C3.2): a persona verdict over the
bundle seed's own candidate mints the seed's §4.2 id and is silently shadowed
by first-write-wins; the test has run A propose a distinct candidate.
395 -> 400 tests; README synced (test count + the S10 section now reflects
that D7 has its own scripted closed-loop proof).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
Docs-only program-avslutning for ingest-laget (D7-stacken). Bygget fra
commons-spec + eget repo alene — MAF-koden kun eksistens-bekreftet (I6-gate),
aldri lest/reverse-engineert.
- README: ingest-seksjon (file/CSV + sql/sqlite RO) som post-S10-tillegg;
testtall 187 -> 265; HTTP/MCP kun peker (D7 har ingen HTTP-konnektor).
- docs/extending.md (ny): hvordan ingest-laget virker + hvordan legge til en
kildetype; HTTP/MCP som ærlig extension-point-peker (spec §4, MAF I6-demo,
create_sdk_mcp_server som ubygd vehikkel — brukt ingen steder i src/).
- docs/2026-07-04-I7-brief.md + -statusrapport.md: I7-brief + D7-lokal
statusrapport m/ verifiseringslogg. Kryss-stack-kriteriet peker til
MAF-programrapporten.
Ærlighetsregelen (method-spec §1): ingen artefakt påstår HTTP/MCP-støtte i D7
eller live-kilde-integrasjon. Grep-sjekk mot overpåstand ren.
Verifisering: 265 passed uten nøkkel/nettverk · ruff/mypy rene.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7nuUMUyWB9viiBjz8Bo7R
Status, seam-by-seam architecture, load-bearing test map, the executed S10
live run with committed artifacts, and the offline-suite invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS