Run 7's seed authored six distinct people with memories, reads, and worded
promises, and every declare_subject carried an empty controller: the field
had no description and the tool result said only "recorded as a gap". The
controller variant now describes its two shapes, and a decode failure's
reason goes back to the model in the tool result. The SDK lowering renders a
later user turn into its transcript, so a refusal continuation reaches the
sidecar instead of faulting.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plan step 12. PersonaMaterial is the ontology's thirteenth row, built:
values, voice, memories, and reads keyed by subject, set whole by
set_persona_material, refused for padded text or a doubled read, no-op when
identical, outside every scope digest. A commitment carries a statement of
what is promised, required at authoring; an affordance-made promise states
the action it came through. The Projector context carries material by label,
each promise's statement, and who else stands in the same place; the
Persona's identity is label, voice, and values. Schemas move to consumer.v3.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The membrane traced end to end: the Projector's borrowed voice, the
contentless commitment, the unbuilt PersonaMaterial, and the missing
co-presence are recorded as findings; the harness no longer claims to fail
on silence, which is now the honest baseline. The map and handoff point at
the decision the operator owns.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Projector and Persona prompts asked for what this person remembers,
wants, and fears, with no component to ground any of it, so every seeded
person spoke in the model's one borrowed voice. Both prompts now render only
what the context establishes, say plainly what is unknown, and treat the
word budget as a ceiling. A commitment's due reaches the Projector as
minutes from now, since a bare 1000 was read as a sum of money.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A test-only TracingInferencePort wraps any inference port and appends each
request's purpose, model, instructions, input turns, tool catalog, and
output to the file GHOSTLIGHT_SMOKE_TRACE names. The logs redact prose by
design and the store is a temp directory, so this is the one way to read
what the Projector, the Persona, and the Interpreter were each given and
what each returned on a road run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Projector context carried a name, a clock, and permission to speak, so a
seeded person reasoned correctly from nothing and said "I'm here". It now
carries the acting subject's own place, routes, holdings, dependencies,
authority shape, offices, redress, promises with their counterparties, and
pressure on self, resolved to labels. Another subject is named only where
this subject's own components bind them. Ids, keys, digests, revisions, and
every other subject's state stay out; the leak tests pin it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Run 3 proves speech cited by quote arrives whole with no quarantine. The
map, handoff, and runbook now say the SDK route is run; the remaining road
item is the live interrupted cell. One distilled evidence record carries
the day's belief-changing findings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Interpreter's speak and record_gap tools took byte offsets the model
counted itself; on the Claude lane three of eight utterances came through
cut mid-word and one empty span reached invocation as a non-canonical
utterance and quarantined its cell. Both tools now take `source_quote`; the
harness locates the quote in the preserved prose and derives the span. A
quote not in the prose, empty, or padded is a translation gap at capture.
The invariant is unchanged: only words in the prose become speech.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first SDK-backed seeded runs. Run 1 refused both seed sessions: the
sidecar's schema conversion dropped every field description, `due` was an
undescribed integer against a clock the model never saw, and the subject
tool did not say a grant is required. Run 2 seeded six people in one round
and ticked three times.
A refusal used to reopen the session with an empty draft and a prompt that
named sites from a draft the model could no longer see. It now continues
the same conversation: `Refusal { after_round, mismatches }` on both
authoring checkpoints (controller_work.v12), the refusal as the next user
turn, and the draft rebuilt by one DraftFold shared by evaluator and oracle
from the calls after it. The harness takes the connector only when its
address is set, so an all-claude run needs none.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing an agent loads in this repo said Ghostlight Dungeon ships to Linux
on Yggdrasil, so a Windows workstation build looked like reasonable
verification. Record the target, the fact that Idunn builds the release in
pinned Linux containers, and what a local compile is and is not evidence
for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 1 had grown to three hundred lines of per-pass SHAs, line numbers, and
test counts that git and the ledger own. The handoff now carries what is
true of the tree, the constraints, the ordered next moves, the operator's
open decisions, and the deployment gate. Gating lessons move to the
re-entry warnings; the map points there instead of repeating them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44 records to 12: supersession chains collapsed to their survivors,
per-commit receipts folded into the decision they served, standing
pre-rebuild decisions merged into one record map.yaml already owns. Every
removed record is archived verbatim. `compact-evidence` is now the one
durable deletion path; the status tool reads folded scalars, and the
compaction helper imports the map readers instead of carrying copies.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`take_oracle` now runs above the caller-identity and expiry gates, so a
refused `infer` reclaims the lend instead of leaving a dead lane's
tool-result owner in the map under a request id nothing will call again.
The port's doc already claimed this; the test that recorded the
divergence now asserts the reclaim.
`fixture_prepared` was `prepare_invocation`'s body a third time, with
the same caller id and the same expiry as `fixture_prepared_inference`
beside it. Its call sites use that one instead.
`ElaboratorSession`, `ElaborationCheckpoint`, `SeedSession`, and
`SeedCheckpoint` go back to `pub(super)`: nothing outside `world` names
them, and `pub(crate)` was reach they never needed. `private_interfaces`
then reports them the way it already reports the grouped, narrative, and
operational checkpoints, which are private under the same `pub(crate)`
`ControllerWork`.
Runtime binds the SDK model prefix from `DEFAULT_SDK_MODEL_PREFIX`
rather than spelling `claude` twice more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sidecar registered a closed Zod schema, so the SDK judged arguments
before Rust ever saw them: an extra key was stripped, a missing required
property was refused with an MCP error, and the call never reached the
handler. The connector path decodes the same argument in Rust, where
`deny_unknown_fields` and required-field decoding turn a bad argument
into the gap the evaluator records.
`toolInputSchema` now builds a loose object whose every property
catches, so nothing is stripped and nothing is refused. The conversion
survives only to advertise: the in-process MCP server derives the schema
the model sees from this object, and that schema still carries every
property, type, bound, and `required` name the emitter produced. What is
deleted is the strictness that made it a second validator -- `.strict()`,
the closed-object requirement, and the every-property-is-required
requirement.
One case cannot be carried through as written: a value outside a
property's own type empties that property, so it reaches Rust as an
absent field rather than a wrong one. Rust still names the gap; the SDK
still does not answer the call.
The undispatched-tool_use protocol violation stays, because no argument
reaches it any more but a call never offered to the handler still does.
`resultFault` is the result-subtype mapping lifted out of `runQuery`, so
`error_max_turns` becoming an ordinary round and every other subtype
becoming its named fault are both asserted without a credential.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two transports behind one port, routed per lane by model prefix; the
sidecar as a child process; the oracle as the evaluator's own fold; the
stopgap's liabilities named with its exit condition; the operator's
credential steps in the smoke runbook.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tests only. Per lane, an adversarial call sequence — decode failure, duplicate
terminal, call after terminal, unknown tool name, oversized argument — is
answered by the oracle with exactly the strings the evaluator recomputes for the
same transcript, and the transcripts the oracle refuses are the transcripts the
evaluator refuses. Identity is pinned across every request builder and one
checkpoint variant. The wire's closed enums, the fault disposition table, the
lowering gate table, the dispatch-order gate, the receipt's contents, and the
routing refusal each get a test.
Two recorded findings, characterized and not fixed:
- The emitted grammar's top-level object is not strict in Zod, so a property the
connector's `deny_unknown_fields` decoders record as a gap is silently
stripped and the call succeeds; and a missing or mistyped required property is
answered by the SDK itself, leaving a registered `tool_use` block with no
dispatch, which `assembleEvents` raises as `protocol_violation` and Rust maps
to an integrity violation. Both are proven against the real in-process MCP
server with no credential and no network.
- A lend refused by the caller-identity or expiry gate is never reclaimed: those
gates run before `take_oracle`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sidecar/claude-sdk` is the repo's second TypeScript package, wired to the
root only through `npm --prefix`. It reads length-prefixed MessagePack
frames on stdin, runs one SDK query, forwards every tool call to Rust, and
returns Rust's answer to the model verbatim. It computes no tool result,
reads no credential, logs no prompt, argument, or result, and writes nothing
outside its own stdout.
The query is opened with a custom system prompt, `settingSources: []`,
`tools: []`, `strictMcpConfig`, `persistSession: false`, and `alwaysLoad` on
every tool, so no CLAUDE.md, settings file, output style, built-in tool, or
persisted transcript reaches a caller-derived prompt.
`schema.ts` converts exactly the grammar `tool_schema.rs` emits and throws on
anything else, so an emitter that outgrows it fails a test rather than
registering a looser tool. Two Rust tests publish the fixtures the sidecar
tests read — the frame bytes and every real `parameters_json` — so the halves
cannot drift apart silently.
Dependencies are exact-pinned: @anthropic-ai/claude-agent-sdk 0.3.261,
@msgpack/msgpack 3.1.3, zod 4.5.4, and dev typescript 5.9.3 with
@types/node 24.13.3. `engines.node` is the installed major.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`SdkInferencePort` runs one Claude Agent SDK query per round through a Node
sidecar and returns it as one `InferenceOutput`. It prepares through the
same `prepare_invocation` the connector port uses, so there is one identity
scheme and every `integrity_is_valid` variant is unchanged.
The sidecar computes no tool result. Every call is answered by
`ToolResultOracle`, which is the lane's own evaluator fold replayed over the
calls made so far — the same function that will recompute the string at
re-derivation. A lane lends its oracle keyed by the prepared request id
immediately before `infer`; the connector port ignores the lend.
Rust owns the transcript lowering, the receipt digest, and the fault
disposition. The sidecar names a closed set of reasons and cannot assert an
integrity violation, a request identity, or a tool result.
`open_inference` routes each configured model: a `GHOSTLIGHT_SDK_MODEL_PREFIX`
model reaches the SDK port, anything else the connector, and a model no
built backend claims fails at open rather than at its first tick. Ghostlight
reads no credential for this transport.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`prepare_invocation` is now the one place a `PreparedInference` is built:
the connector port's `prepare_request` and `fixture_prepared_inference`
were the same body twice, and a second transport would have made it three.
`ControllerRunner::open` takes its ports. `with_test_ports` existed only
to bypass the two transport-specific parameters `open` hard-coded, so
removing them deletes the `#[cfg(test)]` twin rather than adding a third
constructor. Runtime binds the transport and the controller-work owner
through `open_inference` and `open_controller_work`, and
`GHOSTLIGHT_CONTROLLER_CREDENTIAL` is no longer unconditionally required.
Each tool lane's per-call body moves whole into a named fold —
`interpreter_tool_result`, `operational_tool_result`,
`grouped_tool_result` — which the batch evaluator now calls. No result
string changes. `apply_tool_call` was already this shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fork D was dead code: affordance_grants is insert-only and a checkpoint
that reached ReadyToSubmit/NoProposal already had speech, so a fresh
opportunity on the same scope always carries it too. Delete the guard,
the overtaken(Some(..)) call it fed, and note why in `interrupted`.
Widen InferencePurpose, PreparedInference::purpose, and InferenceEvent
to pub(crate) (plus fixture_inference_events/fixture_recovery_required
test seams) so runtime::tests can script an inference port by purpose.
Add a driver-level end-to-end test: through run_cover_tick, the world's
operational agent speaks once between the narrative cell's Persona turn
and its first submit (a re-lowering) and once more between the
re-lowered submit and its own commit, spending the one re-lowering the
turn is owed. The second change lands on the driver's own Interrupted
arm rather than ControllerRunner::run_narrative directly, and the tick
commits exactly the operational agent's two acts.
WorldMailbox::submit_controller stays pub(super); the test reaches
speech through the already pub(crate) ControllerPort wrapper instead.
Soul's follow-up on step 10 (the witness pass): add a MidTurnCommit::
Witness cause and a controllers.rs test proving a Witness landing
between the actor's bound turn and its submit re-lowers with only the
anonymous "what this person knows changed" line and no Overheard block
- KnowledgeSource::Witnessed writes no spoken_at, so overheard_since
finds nothing.
Rebased onto origin/codex/ghostlight-dungeon-mvp (step 10 integrated,
5cd8853) after step 9's own follow-ups landed; no conflicts at this
branch's controllers.rs/runtime.rs seams.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One speakerless event over a place subtree: recipients derived at apply
time by the one reach owner, already-holders untouched, an empty subtree
refused at both layers, confinement through the operation's place, an
affordance slot so a world can author a beacon or a blast. Schemas at
consumer.v2; the operation count aligned to the catalog's pinned count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Seven tests, no production change. Two lanes, one selection: a
`Reach::Place` channel's `fan_out` and a witness over the same place
agree over every root and every speaker, so the two callers of
`under_place` and `unheld` are pinned by behaviour and not only by grep.
`under_place` covers a place itself, covers below it transitively, stops
at its edge, and never reaches a subject with no position — including
under the widest root in the world.
Two patches built so the resolver's candidate model and the apply arm
could disagree, and do not: a witness whose only recipient is a subject
the same patch declares and places, and a witness whose only recipient
is a subject the same patch forgot the fact. Each is refused at one
layer if the layers part; both land.
What a witness hands the narrative lane, pinned at the kernel because
the lane's fixtures are another pass's: exactly one scope component
moves, `knows`, which is the anonymous line, and the row it writes
carries no `spoken_at`, which is the only gate on an attributable
`Overheard` row.
The no-`Told` pin named its knowledge-touching kinds by example; an
exhaustive match now breaks the build when a `ComponentOpKind` is added
without saying whether it touches knowledge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An event a region saw cost one AcquireKnowledge per witness against a
128-operation patch cap, so a hemisphere did not fit in a patch. `Witness`
lands `Witnessed` knowledge of one fact on everyone standing anywhere under
one place.
The operation stores the place, never the recipients: the subtree is
re-derived at apply from live positions through `under_place`, and holders
are dropped through `unheld`, so a forged effect cannot assert a landing the
world does not have and a repeat cannot reset a knower's credence. It names
no speaker, so `Told` stays unrepresentable outside `Communicate`.
A witnessed event is only its reception, so a witness that reaches nobody is
refused — at resolve over the candidate graph, which now models the fan-out
including this patch's own relocations, and at apply over committed state.
Confinement is by the named place, which is not weaker than confining by the
recipients: a consumer and an uncovered jurisdiction refuse every witness
through the closure that already refuses every place.
Both store schemas move to consumer.v2 and prior stores are refused at open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`audience`'s `Reach::Place` arm and `fan_out`'s already-holds filter were
inlined bodies. They become `under_place` and `unheld`, so the subtree read
and the never-overwrite rule each have one owner before a second caller
exists. No behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The narrative lane re-lowers once on a moved scope with the delta the
subject could perceive; the kernel is the sole detector for that lane;
an author is named only for speech actually heard; a turn is lowered at
most twice by the shape of its binding; SubjectSnapshot carries the
subject's whole components; schemas controller_work.v11 and
persona_turn_receipt.v3. Plan step 9 landed with its undone list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Soul review of plan step 9. Tests only; no production code changed.
Five additions, each aimed at a claim the pass makes and does not
check.
The leak invariant, over the causes the pass leaves untested: a
transfer out of a neighbour's custody, a route closed under the
actor's feet, and the actor's own grant revoked. The existing fixture
world has no route, no resource, and no jurisdiction, so its subjects
can only ever be interrupted through `knows` and `commitments`; a
world carrying that furniture is what makes the other component kinds
reachable at all. Each cause re-lowers once, reports its own anonymous
line, produces no overheard block, and carries no label, id, or value
belonging to anyone.
The membrane's exact boundary. A mid-turn commitment names its
counterparty by subject id, that value really is inside the persisted
`Interruption`, and no prompt byte carries it. That is the difference
between "the renderer takes values rather than a snapshot" and "the
renderer emits no value at all", and only the second is what the
Interpreter sees.
Fork B against the digest rather than against itself. The pinned
preimage fixes the serialization; this reproduces every live
opportunity's scope digest from the snapshot's own `components` field,
which is what "view and digest cannot drift" has to mean.
Fork A at the call sites it deleted. Both detail lanes resume a
persisted in-flight row after the world moved under it. The
operational lane still aborts at its detector and spends nothing; the
narrative lane spends one Interpreter round on a binding already known
to be stale, is refused at submit, re-lowers, and commits. The fork's
accepted cost is one extra round and it is bounded at one.
The receipt bump. A v2 receipt digest, rebuilt from the previous
version's own preimage rather than derived from the live function, does
not rehydrate under v3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing reads it: the driver arm reports the subject, both digests, and
the gap detail, and the Eve receipt carries the prose and the gap. The
capture is still derived where the gap's source span comes from, and is
not stored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight tests, each producing its interruption from a real commit that
lands on the world mid-turn rather than a hand-built digest: a
co-located neighbour's speech, and an owner patch that moves a component
naming no actor anywhere.
They hold the leak boundary at the delta surface, which no existing test
covered: the section names a speaker only for a telling the subject
actually received, carries no fact id, revision, digest or `spoken_at`,
and an out-of-reach actor's label and utterance reach no surface of the
turn. They hold the bound: four inferences for one re-lowering, and a
second refusal ends the turn with the overtaken gap having spent nothing
and submitted nothing. They hold the shape: a forged ancestry, a chained
re-lowering, and a row that disagrees with its own turn are each refused
at the gate that owns them, and the re-lowered row rebuilds its request
byte for byte at the derived round with no snapshot.
`the_scope_digest_is_unchanged_by_this_pass` pins the preimage digest so
a future field, order, or serde-attribute change fails there rather than
silently reissuing every bound proposal in every live world.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scope digest that moved between a subject's prose and its commit is one
event with one detector — the kernel — and one handler, which renews the
turn's binding without re-running the Persona and lowers the same prose a
second time against an anonymous account of what moved.
Cut first. The narrative lane's three `ensure_scope_unchanged` calls are
gone: two detectors reporting different errors for one fact is what made
the pass need an entry point per stage. `exact_opportunity` is now the
sole authority on whether a scope moved, which is where the binding
already lives, and the operational lane keeps its early abort because it
holds nothing to preserve. `SubjectSnapshot` loses `holdings`,
`dependencies`, `incident_routes`, `authority`, and `controls` — each a
projection of a `ScopeComponents` field — and carries the components
whole instead.
The turn is re-recorded, never re-run. `PersonaTurnBinding` gains
`interrupted_from`, and a re-lowered turn's receipt commits to the
receipt it replaced by value; the prose and both inference receipts are
copied. One re-lowering is a property of the persisted shape rather than
a counter: a turn whose binding carries `interrupted_from` has no
successor arm.
What the Interpreter is shown is the typed component diff of the
subject's *own* components, named by meaning with no value, id, count or
actor, plus every statement told to it since its bound revision. The
renderer takes values rather than a snapshot, so a neighbour's state is
a compile error rather than a review note.
`controller_work.v11` and `persona_turn_receipt.v3` refuse prior rows
with no shim. No kernel arm changes: `ScopeComponents` gains
`Deserialize` and no field, so every scope digest is byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seeded smoke passed on the twelfth run: six persons authored into
Low Sere from the Kalsa vault in one round, the deficit to zero, nine
subjects and six boundaries at activation, eight cells per tick, the
seeded people speaking to each other. Each earlier failure became a rule
in the tree: strict function schemas (anyOf, typed tags) with an offline
totality test over both authoring lanes; a content-addressed provider
request identity so repair rounds do not collide with the connector's
replay record; the connector's expiry skew separated from the response
read timeout; the seed lane's own round budget with parallel tool calls
and a budget-end submit; canonical ids and the placement rule in the
brief; the commitment shape rule in its tool description; inference
faults and cell outcomes logged where the operator can read them; and a
harness that fails on an empty seed or a silent world.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Docs describe the seed lane as landed: scale intent at creation, the
phase-free qualification with the Draft refusal in one owner, the owner
port bound to the Draft revision, the Vault reader's containment and
caps, obligations as the source of boundaries, evidence lifetime at
ingress. The session-zero and compiler language leaves the eve-native,
multiplayer, Delvehold, and mvp docs; plan step 8 is landed; the smoke
note's run block targets Kalsa Public at Low Sere. Steward folded the
boundary and cut the stale drift warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vault: collect_notes canonicalizes every directory and note against the
vault's canonical root during the walk and refuses to descend into a
reparse-point directory entry, closing the junction escape. VaultError
no longer carries an absolute filesystem path: Root names no path and
Scope carries only the caller-supplied scope string. The link-fanout
receipt cap is now checked per receipt, not per link.
Mailbox: SeedPort::submit_seed takes its own snapshot at submission
time, refuses anything off Draft with KernelError::WrongPhase, and
binds expected_revision to that fresh snapshot instead of a caller-
supplied one — closing the window between a runner's initial Draft
check and a multi-round session's eventual submit. submit_principal
refuses evidence whose valid_until has passed, read at the mailbox
ingress boundary rather than in a reduce arm.
App session: VerifiedPrincipalEvidence carries valid_until, minted from
the session's own access_expires_at, so a long-lived SeedPort cannot
hold authority past the session that vouched for it.
Elaboration: SEED_INSTRUCTIONS now requires an obligation with a
counterparty from every authored subject, since a goal alone derives no
MissingStructure boundary; the goals-only reproducer stays as a live
test of that structural fact.
Controllers: updated the two require_answer/phase tests and the two
seed-lane spec tests whose expectations changed now that submit_seed
owns its own phase gate ahead of require_answer.
334 world / 379 crate baseline with 5 FALSIFIED reproducers ignored is
now 385 crate tests passing (2 unrelated live-infra ignores remain), no
FALSIFIED ignore left, fmt clean.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>