The egress declaration (Trekk B3) says what a run MAY contact. It cannot say what
it DID: after the run, nothing distinguished "the agents queried the price
register" from "the agents ignored it", and a proposal resting on an external
service should be traceable to it.
ToolCallRecorder(FunctionMiddleware) mirrors BudgetMiddleware(ChatMiddleware) one
layer down — that one observes the debate's chat calls, this one its tool calls.
It observes only: call_next is always awaited, so a trace can never alter the run
it traces. The record lands on ProvenanceStamp.external_calls, read AFTER the
debate so it is a record rather than an intention.
MEASURED, not assumed, before any of it was written: FunctionMiddleware fires for
a tool served over a REAL MCP stdio subprocess, and context.function.name carries
the BARE tool name with no server prefix. That measurement decided the design —
MAF cannot tell us which server a tool came from, so attribution comes from our own
config, and a name allowed by two servers is recorded UNATTRIBUTED (server="")
rather than credited to the first match. Naming a service that may never have been
contacted is the one place a guess must not go.
Only CONFIGURED tools are recorded. The middleware fires for every function the
agents invoke, including the in-process retrieve_cost_docs on the road path;
logging those would turn the record into a false egress claim. An empty list is a
positive statement — nothing outside this process was contacted — which is why it
is always serialized rather than omitted.
Honesty limit, written on ExternalCall itself: this is the call and its source. It
is NOT evidence that the service's answer reached the proposal, nor a verified
rendering of that answer.
One finding, and it is the reason for measuring rather than trusting green: the
road-path negative test was VACUOUS. Its scripted tool call named an argument the
tool does not declare (code vs query), MAF rejected the call before invocation, and
the test asserted an empty record against a run where no tool ran at all — green
under the exact mutation it existed to catch. It now spies on the recorder and
asserts the invocation genuinely reached it before asserting it was not recorded.
This is last session's lesson again: a scenario that cannot distinguish two
implementations proves nothing.
The tool-call double is registered in test_scripted_client_consolidation.py's
_DELEGATING_OVERRIDES — it cannot live in the reply_selector seam, which returns a
reply STRING, and a response that is not text is its whole subject.
Load-bearing MEASURED (tests/test_b4_mcp_call_trace_loadbearing.py) against the
whole 755-test suite, four mutations all red: detach the recorder from the debate
middleware · record every function invocation · attribute an ambiguous name to the
first server · stop reading the recorder into provenance. Control: a run with no
configured servers records nothing, so the empty record is a real answer and not
the only one the seam can produce.
Ran it, not just tested it: the real recorder against a real MCP server subprocess
returns ExternalCall(server='prisregister', tool='lookup_unit_price'), and a
scripted CLI run's outbox artefact carries the empty list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtRd8y1PDPGwkrRXFhubqr
A run commissioned to evaluate three approaches wrote ONE proposal artefact, so
only the approach it selected could ever receive a verdict. The other two were
evaluated, reported in the settlement, and then taught the learning loop nothing.
The defect class is a key collapse, and it had two halves — fixing either alone
leaves it intact:
* the WRITER wrote one pair per run, so the non-selected approaches never existed
on disk;
* the READER (hitl._read_outbox_proposals) joins proposal to outcome on the
run_id FIELD read from file CONTENT, never the filename. Three files sharing
one run_id collapse onto one dict key, last write wins — so widening only the
filename would have produced three artefacts and still one pending row. This is
the S3.2 collision class: two rows under one key silently become one.
Artefacts are now keyed {run_id}-{approach_id}-*.json AND carry approach_id in the
payload; the join key is (run_id, approach_id). Two properties make them genuinely
judgeable rather than merely present:
* verdict_id is minted per approach (verdicts.verdict_key, the S3.2 content hash)
— reusing the run's single id would let one delivered verdict clear all three
from the queue;
* provenance.validator_decision follows ITS OWN approach — the run's stamp would
report a rejected candidate as validated, and nothing downstream could correct it.
verdicts.verdict_key is public so a run can stamp the key a verdict WILL arrive
under without capturing a decision nobody has made; it delegates to _mint_id
rather than restating the hash (the (p) rule: one keying rule, one copy).
The per-approach set REPLACES the run-level pair rather than joining it — the
selected approach is already among them, and writing both would count it twice in
hitl pending. The selected one carries the run's final outcome, so the outbox can
never disagree with the RunResult; the others carry the validator's verdict, the
only falsifier that ran on them.
mandate.py is deliberately untouched: hanging a ValidatedProposal off a coverage
row would drag validator — and pulp — into a module kept to pydantic+stdlib for
D7 portability, so _evaluate_mandate returns the evaluated outcomes alongside.
Ran it, not just tested it: a real CLI run wrote six artefacts and hitl pending
listed three rows. It also showed the honest edge — three approaches that produce
an identical candidate share one content-hash key, so one verdict settles all
three. That is correct (they were one candidate), and it is now documented.
Load-bearing MEASURED (tests/test_a5_per_approach_artifacts_loadbearing.py) against
the whole 750-test suite, five mutations all red: detach the per-approach writer ·
drop approach_id from the join key · reuse the run's verdict id · reuse the run's
provenance stamp · widen the filename but not the payload. Control: on a full
detach exactly the 5 new tests fail and 745 pre-existing ones stay green — the
no-mandate path is inert, and writes neither the filename segment nor the field.
Docs: bestille-en-kjoring.md (what the commissioner gets) + ekspert-svar.md (what
the expert's queue looks like, and that "rejected" is the validator's verdict on
the numbers, never a professional judgement of the idea).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtRd8y1PDPGwkrRXFhubqr
Krav 3, and the operator chose the run path explicitly: the external service must
be reachable WHILE the run works, not only when documents are ingested. Until now
the run path had one in-process tool against a local folder — and on the bundle
path the agents had no tools at all.
MAF already ships the client (MCPStdioTool / MCPStreamableHTTPTool, verified in
the pinned 1.9.0 with allowed_tools and request_timeout), so `mcp_tools.py` owns
only what MAF cannot decide for us: which servers a run may contact, which of
their tools it may call, how long it waits, and where the credential comes from.
This is a DIFFERENT seam from ingest_mcp.py on purpose — that one pulls source
documents before a run and speaks to null-argument tools. Same protocol, different
job.
Every refusal is a live hazard, not tidiness. An empty allowlist would let the far
end decide what the agents may call, so naming the tools is mandatory. A
non-positive timeout is an unbounded wait against a third party. An unknown field
is refused rather than ignored, which is also what keeps a literal secret from
being parked in the config — there is no field for one, only the NAME of an env
var. A named-but-unset credential refuses instead of calling anonymously, because
an anonymous call can succeed with the wrong scope.
Egress is declared, always. Every server and permitted tool is named in the run
announcement before the first call — including when no --mandate is given, which
was a real hole: the announcement only printed with a commission, so configuring
servers without one would have contacted third parties with nothing printed at
all. --live-dry-run still opens nothing, because the tools are entered after the
dry-run cut: the promise to stop before the first call now covers egress too.
Threaded through BOTH modes. A flag accepted in one mode and silently dropped in
the other is the defect class this CLI refuses by name.
Load-bearing MEASURED against the whole 744-test suite, four mutations all red:
build the tools but never hand them to the agents (2) · never enter the
AsyncExitStack, so they are constructed and useless (1) · never declare the egress
(2) · drop the allowlist on the built client (1).
Two live docs claimed MCP was unwired in the run path; both corrected rather than
left to rot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
`docs/bestille-en-kjoring.md` is the commissioning half of the expert-facing pair
(`ekspert-svar.md` is the judging half): the mandate file field by field, how to
run it, and — separated deliberately — what a commission does NOT do. It directs
what is evaluated, never what is approved.
Registered in _LIVE_DOCS, so it cannot silently fall behind the code.
The example output in it is COPIED FROM A REAL RUN, not composed, and running
that run is what found the defect fixed here: three approaches against the same
cost line each validated at 30000 NOK, and the settlement printed
"Validated total: 90000 NOK". Commissioned approaches are ALTERNATIVES — they
usually attack the same line — so summing them reports money the project cannot
realise. A domain expert reading that total would reasonably believe the run
found 90k.
The settlement now reports how many approaches held and which one the run
carries: a selection, not an arithmetic claim. That also removes the last money
addition from this module, which is the right place for it not to be.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
The pin had sat at v0.3.1 with STATE calling the hold "deliberate" and
recording no reason. Measured: no coord message ever announced v0.4.0 or
v0.5.0a* to this repo, so the hold was drift wearing a decision's clothes.
v0.3.2 is a pure fix (frontmatter and index labels emit verbatim; only
source_query is whitespace-collapsed, per ingest-spec §5), keeps
`dependencies = []`, and is green here: 668 passed.
WHY NOT FURTHER, both measured rather than assumed:
1. v0.4.0 introduces a REGRESSION that breaks our §6 removal path.
Bisected v0.3.2 OK / v0.4.0 RED with a minimal repro: materialize a
bundle, then re-materialize it with a CHANGED manifest, and the library
no longer recognises its own stamp —
MaterializationError: generated filename 'ingest-costs.md' collides
with an existing file that does not carry the ingest stamp
The stamp carries the manifest's name+hash (`ingest_manifest: m2@…`), so
editing a manifest makes every file it previously wrote look curated.
Re-ingesting the SAME manifest is fine, which is why fixtures miss it.
It is `tests/test_ingest_loadbearing.py::test_reingest_with_active_
removal_preserves_promoted_and_curated` that catches it. Reported
upstream; not ours to fix.
2. Everything past v0.3.1 adds `llm-ingestion-guard>=0.2,<0.3` as a HARD
runtime dependency (v0.3.1/v0.3.2: `dependencies = []`). That flips two
documented invariants here — pyproject's "zero runtime deps" comment and
the STATE marker line the guard repo reads machine-readably ("not a
runtime dependency today"). An operator decision, not a version bump.
3. v0.5.0a2 is an alpha whose own CHANGELOG scopes it to a named pilot set
— portfolio-optimiser-claude, the marketplace catalog, claude-code-llm-wiki
— and says "do not pin this tag outside the pilot set", with the v0.2
surface free to change without a deprecation cycle. This repo is not a
pilot. Joining is llm-ingestion-okf's call, requested via coord.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GyAbxJoyypnLLUDcMvnKh8
Walked Door A from a fresh clone: materializing the file-family golden
manifest writes index.md plus one concept file per extraction, and pointing
the run at the result is refused —
run refused: IR projection not found in bundle: 'validator-input.json'
A clean fail-fast, but nothing adopter-facing said it was coming, while the
README actively invites it ("swap --bundle-dir for your own bundle"). The
run path needs the bundle's IR projection, which ingest does not and cannot
produce: ingest materializes source documents, the projection states the
candidate measure. Both docs now say so, with the shape reference named.
Also corrects a live-doc claim that was wrong in both halves: the MCP
timeout is `anyio.fail_after` nested inside both task groups, not
`asyncio.wait_for`, and `tests/test_ingest_golden_mcp.py` covers it
(verified — 2 passing timeout tests). And no bundled example ships a
`cost-baseline.json`, so the text no longer implies one does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GyAbxJoyypnLLUDcMvnKh8
The operator is not a domain expert, so the domain content is mine to own --
and the one thing the loop asks a human for is exactly the thing no example
existed for. docs/ekspert-svar.md is written for whoever has to deliver the
verdict: the two forms a judgement can take (a --rationale string during the
run, a JSON file in the inbox for later runs), where each field comes from, and
four complete paste-ready answers.
Every command and every verdict in it was RUN from a fresh clone before it was
written. The `hitl pending` line quoted is verbatim output. The rejection
answers close the gap STATE has carried since the demo shipped: the README
shows the VALIDATOR refusing a number, but nothing showed an EXPERT refusing a
proposal whose numbers are fine -- the only judgement in the whole loop that a
machine cannot make. Two rejection shapes are given, because "not feasible
here" and "right measure, wrong cost base" teach the system different things.
Everything is marked AI-authored and not verified professional judgement.
Also corrects the --outbox-dir help text, which claimed sharing a folder with
--verdict-dir "re-ingests raw agent output past the Step-8 promotion gate".
Measured, by pointing both at one folder and running twice: it does not. The
outbox artefacts are named {run_id}-*.json and carry none of the verdict keys,
so the tolerant inbox loader skips them and the run is unaffected. The hazard is
real but latent -- a future verdict-shaped artefact in the outbox -- so the
warning stays and says what is actually true. This also answers STATE's open
question about enforcing the distinction in the CLI: no. There is no reachable
contamination to refuse, and a guard for an unreachable case is the kind of
error handling this repo declines to write.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118noV9rCfrdREH26XqZB5z
Operator ruling 2026-08-05, which settles decision (g): planning documents are
generally never public, and what OUR OWN sessions generate does not go out on
the forge at all. The example itself stays public so others can run the
process.
`.claude/projects/` is the Voyage session workbench -- 25 briefs/plans/reviews
this project's own sessions produced. Untracked and gitignored, exactly as
STATE.md already is, and for the same stated reason: this repo has a public
mirror, so that class of material is local-only rather than tracked.
The line is drawn at who wrote the document, and it is drawn deliberately:
`docs/plan/`, `docs/research/` and `docs/rapport/` stay tracked. Those are
curated, dated documents written for the repo's readers, three of them linked
from the README as the decision record. Move that line if it was meant wider.
Two files were NOT process artifacts and are not deleted. Both
`build_fixture.py` scripts are cited by tracked tests
(`test_ingest_golden_sql.py`, `test_ingest_golden_http.py`) as the documented
rebuild path for byte-exact goldens -- reproduction code that had landed in the
wrong directory. Moved next to the goldens they build; both docstrings updated,
so no tracked file is left pointing into an untracked tree (verified: the only
remaining `.claude/projects` string in a tracked file is the .gitignore rule
itself). One prose reference in the dated Foundry auth recipe was dropped for
the same reason.
652 tests still pass.
Does NOT address the 27 of these already readable on open/ since the S12
release -- untracking stops future publication only. That retraction is a
separate operator decision and is deliberately not taken here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWsexbQjPo9rsV3aUE54ZS
Ran `repo-standard` (v0.1.1, class `standalone`) and fixed everything it
flagged as ERROR, plus the WARN links that were genuinely dead.
README first screen:
- opening line is now byte-identical to the forge description, so
description == catalog == README is machine-checkable (badges moved below).
- `## Install` (required for class `standalone`): clone + `uv sync`, stated as
clone-only because the shared spec, persona skill and example bundles under
`shared/` are read from the working tree at run time. `uv run pytest` named as
the verification, with the fact that no CI runner exists said out loud rather
than implied by a badge.
- `## Non-goals` (required): the five limits already binding in CLAUDE.md —
not a compliance product, not a portfolio-level reallocator, not autonomous
decision-making, not turnkey, not a model benchmark.
Dead relative links (measured, not guessed):
- `docs/plan/2026-07-10-sesjonsplan-fase2-6.md` pointed at
`../2026-07-14-revisjonspakke-DF-DI.md` six times; the file sits in
`docs/plan/`, not `docs/`. (The sibling `../review-2026-07.md` links are
correct and untouched.)
- the Fase-1 spike brief linked repo-root-relative from
`.claude/projects/…/`; re-anchored with `../../../`.
The one remaining README ERROR was a gate false positive: `checkInternalLinks`
resolves targets against `git ls-files`, which lists files only, so a link to a
directory can never resolve. `[shared/](shared/)` now points at
`shared/README.md` — a better target anyway, since that file carries the
pull-only subtree rule. Not fixed here: the classifier lives in another repo.
Remaining WARNs are all inside `shared/`, deliberately untouched: it is a
pull-only commons subtree, and the nav-golden files are byte-level fixtures
that gate `test_nav_golden_*` — four of them are OKF bundle-internal links,
and the `/etc/passwd` ones are the negative escape fixture doing its job.
Suite green: 630 passed, 4 skipped (markdown-only diff; no test touched).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri3aVJPfynCZtHRhesCzUH
`cosine`'s docstring claimed its guard was load-bearing because "a NaN reaching the
ranking sort key would corrupt ordering silently rather than failing loudly" — but the
guard tested `norm == 0.0` only, which a NaN or inf norm passes straight through. The
claim was prose, not behaviour.
Measured, not assumed: `cosine(unit, nan_vector)` AND `cosine(unit, inf_vector)` both
returned `nan`, and a NaN sort key made ranking INPUT-ORDER-DEPENDENT — six permutations
of the same three candidates produced four distinct orderings. That defeats the total
order `HybridRanker` documents ("`id` makes the result independent of input order").
Refuse rather than coerce, and deliberately NOT symmetric with the zero-norm branch: a
zero vector is a legitimate handled state (`FakeEmbedder` returns `np.zeros` by design),
whereas a non-finite component only ever means the INJECTED embedder is broken. Scoring
it `0.0` would launder that into "no semantic similarity" while ranking proceeded on a
forged signal — validation, never repair, mirroring `read_spend`.
Reachable via the documented `Embedder` extension point, not the shipped fake; scoped to
the norms (90% principle — a finite-normed dot-product overflow is not chased).
Also corrects `docs/extending.md`, which stated `SEMANTIC_WEIGHT_DEFAULT = 0.5` while the
code has said `0.25` since the weight was lowered.
625 -> 630 tests. Load-bearing MEASURED against the WHOLE suite, five mutations all red:
detach the guard entirely · coerce to 0.0 instead of raising · check only the first norm ·
drop "non-finite" from the message · (control) detach the zero-norm branch, which fails
ONLY the zero-norm test — the new guard does not mask the existing one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018V9vNBmxAmgJ2JMoHByiHS
`stdio_call_tool` shipped never having been executed end to end — docs said so
explicitly. Running it found a real defect: `stdio_client` and `ClientSession` are
each an anyio task group, and anyio re-packages anything leaving one in a
`BaseExceptionGroup`. Both errors the transport raises from inside the session
(`mcp_tool_error`, `mcp_non_text_content`) therefore reached callers as exception
groups, never as the `IngestError` the whole Door A path catches and switches on by
`code`. No canned-tool test could see this: they never enter a task group.
`_unwrap_ingest_error` recovers the owned error and re-raises it; anything unowned is
re-raised untouched, so this narrows an exception group rather than blanket-catching.
Duck-typed on `.exceptions` because `except*`/`ExceptionGroup` are 3.11+ and this
project supports >=3.10.
Verified against a REAL server subprocess (a local process costs no model tokens, so
the repo's cost discipline is untouched; the contract tests still spawn nothing):
`examples/ingest-golden-mcp/` + `tests/test_ingest_golden_mcp.py` — byte-identical
golden extraction mirroring the http/sql goldens, plus the tool-error and
missing-`server_ref` branches.
Also recorded: a server on the ingest path must expose a NULL-ARGUMENT tool, so
`datasource.build_mcp_server` cannot serve it (`retrieve_cost_docs(query)` has a
required parameter, verified to return an error result). The two are separate seams
by design.
Load-bearing MEASURED, five mutations all RED: detach the unwrap · detach
`initialize()` · make the error code generic · detach the `isError` branch · change
one byte of the served body.
612 -> 615 tests. ruff + format + mypy clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WiY53sm8JFqk7NN75g5wRS
The hole: `read_http`'s default transport is the library's `urllib_get`, which invokes the
stdlib opener with no `timeout=`. urllib's documented fallback is then the process-wide default
socket timeout — `None` out of the box — so an http source that accepts a connection and never
answers hangs a run indefinitely. That contradicts the invariant that nothing runs unbounded.
The spec text for S2.4 ("a timeout parameter on `_urllib_get`") could NOT be followed literally:
that function is UPSTREAM library code (`llm_ingestion_okf.connectors`, pinned v0.3.1, pull-only),
signature `(url, credential) -> str` — measured, not assumed. Same failure class as S2.2's
"implement it in `ingest.py`": spec text that says "change X" has to be checked against whether
X is ours at all.
So the fix goes in FRONT of the library: `timeout_get` scopes `socket.setdefaulttimeout` around
a delegate call to the library's own `urllib_get`, and `materialize` now hands the library that
wrapped transport instead of letting it resolve its own untimed default. This meets S2.4's own
verification criterion — a bound WITHOUT a second socket path — and avoids duplicating the
credential-header logic. An explicitly injected `http_get` is passed through UNWRAPPED: a
caller-owned transport (MCP fronts a subprocess with its own `timeout_seconds`) keeps its own
policy, and a process-global side effect is not ours to impose on it.
Honest limit, carried in the code comment, the test docstring and `docs/extending.md`, not just
in the commit: the default socket timeout is PROCESS-global. Under `concurrency=k` the runner is
asyncio on one thread, so the scoping holds; driving `read_http` from a thread-pool executor
would make it unsafe.
Half of S2.4's scope was already delivered upstream — transport failures are categorised as
`SourceError(code="http_transport")`. Coarser than the plan envisaged, but not ours to rewrite.
Two pre-existing guards went red on the first pass, both on PROSE only: `ingest.py` must not
contain "urlopen" (no forked connector) or "ingest_mcp" (AST-guarded mcp-free). No code violated
either — my docstrings merely named them. The guards were left exactly as strict as they were and
the prose was reworded; weakening a real guard to save a comment is the trade this repo refuses.
578 -> 583 tests. Five mutations MEASURED red (restored from scratchpad + `shasum -c` each time,
never `git checkout`):
1. remove the timeout scoping entirely -> RED
2. apply the bound AFTER the delegate call -> RED
3. set the bound but never restore it (no finally)-> RED (the unconditional control)
4. hand the library a bare None again (pre-S2.4) -> RED (the wiring)
5. make the wrapping unconditional -> RED (the conditional control)
Mutations 1 and 2 take ~10s to fail rather than failing instantly: that is the loopback test's
join deadline expiring. It is the measurement that the bound actually BITES — a black-hole
listener on 127.0.0.1 that completes the handshake and never answers, run on a daemon thread so
a detached seam fails an assertion instead of hanging the suite forever. Every other assertion
here only proves we set a global; that one proves the global does something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TdLGwd33vqhkToh98Ym34P
Commons settled on 2026-08-01 that MCP is an extension of the `http` source
family, not a fourth family (`shared/ingest-spec.md` §4). This implements it
with ZERO schema change and zero spec amendment.
The transport discriminator lives in `base_url`, not in new manifest fields:
the shared library rejects unknown manifest keys fail-fast, and we consume it
pull-only at a pinned v0.3.1, so `server_ref`/`tool` as fields would have meant
a spec amendment plus a library release. It buys nothing — the library already
joins `base_url` + `/` + `query`, so `mcp+stdio://<server_ref>` + `<tool>`
reproduces exactly the two-part structure the (now stale) reference plan wanted.
Staying inside the family INHERITS what a fourth family would have had to write
and could have forgotten: the §8 network grant (measured to fire before any tool
call), the `max_rows` cap, §5 verbatim fenced rendering, and the §7 provenance
stamp. The discriminator gates rather than labels — `mcp_get` refuses a URL it
does not own, so an MCP transport can never quietly serve an `https://` manifest
and leave the bundle's provenance claiming a transport that was never used.
Parsing is string-based, not `urlsplit`-based: `urlsplit().hostname` lowercases
the host, which would silently break the case-sensitive env lookup `server_ref`
depends on.
`ingest.py` is untouched — it is AST-guarded mcp-free, so the transport lives in
its own module and is opt-in at the call site. `ingest_mcp.py` imports the open
`mcp` protocol client but never `agent_framework`, keeping the seam D7-portable.
Load-bearing, six mutations all measured RED: detach the scheme guard · make the
refusal unconditional · swap parsing to `urlsplit().hostname` · skip non-text
content instead of raising · force `allow_network=True` · smuggle in a MAF
import. Both source files restored byte-identical (`shasum -c`) after each.
Honesty: `stdio_call_tool` (the real stdio path) is written but never executed
end to end — every test injects a canned tool call, so the suite spawns no
subprocess and opens no socket. No golden fixture, and MCP stays unwired in the
optimiser run path. Stated in docs/extending.md rather than implied away.
555 -> 578 tests; ruff + mypy green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112FPR5TX6pDLiNicBPzE8i
New English docs/knowledge-base-recipe.md grounded strictly in the D-H decision record
(revisjonspakke-DF-DI.md §3): setup is always a small team (technical + domain expert), the
deliverable is a recipe NOT a wizard (B9 onboarding interview + guided verdict command rejected),
domain expert delivers files in their own formats never schema/JSON, phased process (inventory ->
skeleton -> seed verdicts -> iterate), reading via Obsidian/VS Code. The honest 1-2 week
expectation is stated early and SOURCED verbatim to the record. Factory-dependent parts
(free-format verdict translation, clone-to-demo) are explicitly marked future/blocked-on-toolkit
so the doc never claims above the evidence level. Linked from README's Docs section with the
1-2 week expectation in context. SC5 (ASCII-only greps): file exists, '1-2 weeks' x2,
'knowledge-base-recipe' in README. src/ untouched; 431 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNNiJRk1sSwxgVLS5AobT1
Door A (manifest -> connector -> deterministic materialization -> index) is no
longer implemented here. src/portfolio_optimiser/ingest.py becomes a thin
consumer seam over the shared library, git-pinned to v0.3.1 on the same Forgejo
channel portfolio-optimiser-claude uses. Net -626/+385; ingest.py 599 -> 145 lines.
shared/ingest-spec.md remains the normative spec: the library implements it, it
does not replace it. Spec changes continue to go via commons.
Acceptance criterion met and proven: all three golden bundles (file/sql/http)
are byte-exact before and after, including the idempotence re-run. examples/ and
shared/ carry ZERO modifications -- the fasit was not adjusted to fit.
The rejection set was verified equivalent, not assumed: all 22 malformations the
repo's pydantic models refused are refused by the library, with typed codes
(okf_type_reserved, credential_embedded, extraction_id_duplicate, ...).
Test rebinding (invariants preserved, vehicle changed): the library has zero
runtime dependencies by design, so pydantic is unavailable to it.
ManifestV1.model_validate(dict) -> load_manifest_bytes(bytes); ValidationError ->
ManifestError; model_fields -> dataclasses.fields; PathSecurityError ->
SourceError(path_escape); ValueError -> MaterializationError(ingested_at_invalid).
Tests now also pin the refusal `code`, the library's documented stability
contract -- a sharper assertion than "some validation error was raised".
Two accepted behavioural deltas, recorded rather than silently dropped:
- Title whitespace is stored verbatim instead of collapsed at validation, so the
frontmatter title and the index label are no longer guaranteed identical for
irregular whitespace. Both behaviours are spec-conformant (the spec is SILENT;
the old one was a repo-local pinned decision). Queued as a commons-amendment
candidate so both stacks pin the same answer. Goldens unaffected.
- The section 8 audit log moves to logger llm_ingestion_okf.materialize. Nothing
in the repo consumed the old channel.
Also: the `type` discriminator is no longer a dataclass field, so the spec
cross-check asserts it explicitly -- without that line the swap would have
silently narrowed the test.
New tests/test_ingest_library_seam.py pins the seam itself: the restated section 5
stamp formula against the stamp the library actually writes (the one place the
adapter does not purely delegate, since v0.3.1 exposes no stamp helper), the
local-only allow_network default, the list[Path] unwrapping, and a guard that the
adapter never regrows local Door A machinery. All four verified RED when detached,
as were both golden regressions under a byte-level render mutation.
Door A is UNGATED: it calls no guard before writing to disk. Gating untrusted
content remains the caller's responsibility (guard wiring still planned).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4jNN186eVqfe1x5DnTU6r
Map untrusted-ingest surface (ingest.materialize http/I6, verdict-inbox
load, promote_verdict, future received-bundle) vs first-party paths;
verdict = planned, wire scan/sanitize before M3 as S2.4/S2.5 extension.
Plan only — guard not wired. shared/ hardening owned by commons session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
Operatørkorreksjon 2026-07-04: §Rammer sa «Fable 5 med xhigh — global default»
(scoped bort fra 2026-07-02-planens Opus-direktiv). Reverseres: Opus 4.8 xhigh
for ALLE økter og alle subagenter, ingen Fable 5 — matcher den globale regelen
(~/.claude/CLAUDE.md, «Modellvalg for subagenter»). Kun plandokumentet endret;
I6-arbeidets untracked-filer urørt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MM6BWb1hWmJZuXFZ7rjxT
Program-planleggingssesjon per brief 2026-07-03: alle brief-premisser
verifisert mot ground truth (retrieval-forbudet sitert ordrett), planen
adversarial-reviewet x2 (2 blockere + 8 majors innarbeidet: verdict-lag-
reservasjon, lag-separasjon ved re-ingest, deterministisk timestamp,
guard-dekning, D7/HTTP-ærlighet, gatede spec-endringer). I1 er GATET på
operatør-godkjenning av målbildet. Kun dokumenter — ingen kode-, shared-
eller søskenrepo-endring; suite 157/4 grønn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
Brief (ikke plan) for nytt program etter S11: ingest-steg som materialiserer
kildeuttrekk til OKF-bundles (metode-spec forbyr query-time retrieval i løkka),
delt manifest-kontrakt i commons, referanseimplementasjon per stack.
Inkluderer oppstartsprompt for planleggingssesjonen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
Defines the S11 yardstick BEFORE either stack exists: pinned commons-ref
as identical input, metrics M1-M4, the verbatim liveness-asymmetry
declaration, five binding LLM non-determinism rules for S10/S11, and a
ban on comparing offline numbers with live numbers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
Findings 4-7 from the 2026-07-02 status analysis, per the session plan (S1):
- CHANGELOG rewritten truthfully (was: 'Plan phase - no framework code yet')
- README stack line names the split GA packages, not the agent-framework meta-package
- CLAUDE.md: MCP downgraded to extension point (in-process FunctionTool is the default seam)
- Verdict conflict semantics documented as chosen (store first-write-wins per id,
disk/wiki last-write-wins per file; full B10 taxonomy deliberately deferred)
- docs/extending.md: explicit 90%-principle cut-list (B10, B11, U12, U14, concurrent fan-out)
- .gitignore covers .trekexecute-progress-* (docs/.DS_Store was already untracked/ignored -
the plan's git rm --cached assumption was stale; no-op)
No code behavior changed (docstring only in verdicts.py). Suite 152/4 green, mypy clean,
ruff format --check clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AaQCFnfsh3tfq1VfzdJpoi
Nordstjerne fra design-samtale 2026-06-26. Konsoliderer: 8-stegs sverm-loop,
trelagsmodell (OKF-kontekst/output-inbox/promoteringsgate), to feedback-
tidsskalaer (kort synkron + lang fil-basert/gjenopptakbar), OKF/LLM-Wiki
datagrunnlag (web-verifisert mot Google knowledge-catalog), den samlende
diagnosen (tilbakemelding-inn-i-prompt-dataflyt mangler 3 steder), invarianter,
testbar "ferdig", delt eksempel for begge repo, fase-nedbrytning. 2 Mermaid-
diagrammer. STATE peker hit. R1 besluttet (shared/-dir nå). Domene lener mot
energieffektivisering (lærings-overflate > FinOps' for-deterministiske kjerne).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019any9zfGNNwWJPX5Zq2QRz
Two independent grounded passes (installed-source introspection + official MS
Learn via MCP) produce a per-need adopt/keep decision table for using MAF
features well in Fase 2, instead of reinventing them.
Headline: Microsoft's Workflows "State Isolation" page documents verbatim the
exact footgun Spike B(b) found today — a reused Workflow accumulates agent
threads across runs; the fix is a fresh-instance-per-run factory. Our
fresh_workflow() IS the official pattern.
Key verdicts: ADOPT real UsageDetails token counts + a budget ChatMiddleware +
native builder round caps + GA @tool/MCP + observability; KEEP the hand-rolled
structural VerdictStore and inline validator (MAF memory/eval are the wrong
shape); ROLL a tiny role->deployment map (declarative is preview/not installed).
Corrections recorded: CLAUDE.md "Magentic experimental" stands at doc-level (no
code gate); Spike D extend_instructions is two-arg (source_id, instructions).
Skills answer: method-as-Skill yes (MAF consumes SKILL.md natively, experimental);
MAF-docs-mirror Skill no (rots vs live MCP); the digest lives in this map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fif1r1En5W542HbZV88yMH
/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
Framework-neutral narrative of what portfolio-optimiser aims to achieve and the
two hypothesised approaches to the same method. Claude Agent SDK paragraphs
corrected by the user: the SDK spans both emergent (one agent + subagents) and
explicit orchestration (hand-written or agent-authored workflow script with a
non-LLM validator gate). The real difference vs MAF is ready-made named
constructs vs building blocks — not emergent vs explicit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fif1r1En5W542HbZV88yMH
Verified comparison of Microsoft Agent Framework (ground-truth introspection of
installed agent-framework-core 1.9.0 + Microsoft Learn) and Claude Agent SDK
(Anthropic docs + npm/PyPI). Grounds decision D7: rebuild the same method on
Claude Agent SDK as a separate sibling repo, in sequence, sharing only the
spec + golden/conformance suite — not orchestration code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fif1r1En5W542HbZV88yMH
Privat MS-tenant tilgjengelig men kostnadstak: lokal profil default i
utvikling, Foundry/Azure kun målrettet/minimal, ingen tunge test-kjøringer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9FyyENxebxVThjrn9et8C