Skip to content

PACT object model & metered universality: recursion fuel, agent shape, abstract bases, inheritance hardening - #1

Merged
dittops merged 51 commits into
masterfrom
pact-oo-universality
Aug 23, 2026
Merged

PACT object model & metered universality: recursion fuel, agent shape, abstract bases, inheritance hardening#1
dittops merged 51 commits into
masterfrom
pact-oo-universality

Conversation

@dittops

@dittops dittops commented Aug 13, 2026

Copy link
Copy Markdown
Member

Implements the object model and metered universality in PACT — designed via an analysis workflow + adversarial red-team, implemented in 9 gated steps, brutal-reviewed, with two RCA cycles, full suite green (Rust 994/0 · adapters 1,958 pass / 8 skip · 11 example workspaces clean under --deny-warnings · clippy -D warnings clean).

What this adds

  • limits: asks-itself-at-most — recursion as fuel. A team: circle (self or ring) is legal iff every member on it writes its own figure. The Python reference port enforces it as a per-request activation counter (not stack depth), failing through the OverBudget path so if-someone-fails: decides. Post-review fix: the spend check reads the same identity the meter increments (AgentSpec.name), pinned by a named-circle regression test.
  • The agent answer shape — appended to &the-answer-shapes (default-first preserved), reaching accepts / answers-with / run-inputs / action.takes / question.answer at once. Higher-order agents as data: a name from this same tree, never a fetch.
  • agent.base: yes — abstract bases: never run, never offered. Excluded from discover, card, team:, ports.*.answers:, stage.may-use. May leave required fields blank; descendants pay the debt through ordinary post-derivation validation.
  • Inheritance hardening — D-1: shallow merge kept (the removal argument stands) + loader/restating-a-block-drops-the-rest names every dropped key. D-5: discover/card project the derived document (no more model: null cards). D-3: two on-disk based-on: trees + acceptance tests (live-run half recorded as residual in C8, honestly).
  • Docs per the same-change rule — C8 §6 price 6 closed on the deferral's own terms, defect rows updated, kinds/field counts bumped, headline counts re-synced (2,960).

Carried along, declared

The working tree held ~176 uncommitted paths from a parallel in-flight workstream (MCP bridge, money-floor, discovery-policy, bundles, CI) with no baseline commit separating them; they are included as-carried in the base commit. This change's own scope is fully enumerated above.

🤖 Generated with Claude Code

https://claude.ai/code/session_013vdfGhbdaADnJDBW2MqWUr


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

jithinAB and others added 30 commits August 13, 2026 06:06
…pe, abstract bases, inheritance hardening

THIS CHANGE (designed via analysis workflow + red-team, implemented in 9 gated
steps, brutal-reviewed, one post-review fix, full suite green: Rust 994/0,
adapters 1958 pass/8 skip, 11 example workspaces clean under --deny-warnings):

- limits: asks-itself-at-most — recursion as fuel. A team: circle (self or
  ring) is legal iff EVERY member on it writes its own figure. teams.rs keeps
  its rule id and pinned messages; the Python reference port enforces the
  figure as a per-request ACTIVATION counter (not stack depth), failing through
  the OverBudget path so if-someone-fails: decides. Post-review fix: the spend
  check now reads the same identity the meter increments (AgentSpec.name), with
  a named-circle regression test — a name: line differing from its key can no
  longer recurse past its figure.
- the `agent` answer shape — appended to &the-answer-shapes (default-first
  preserved); reaches accepts/answers-with/run-inputs/action.takes/
  question.answer at once. Higher-order agents as data: a name from this same
  tree, never a fetch.
- agent.base: yes — abstract bases: never run, never offered. Excluded from
  discover, card, team:, ports.*.answers:, stage.may-use (the last two were
  open doors found by red-team). May leave required fields blank; descendants
  pay the debt through ordinary validation after derivation.
- inheritance hardening: D-1 — shallow merge KEPT (the removal argument
  stands) + loader/restating-a-block-drops-the-rest names every dropped key;
  D-5 — discover/card now project the DERIVED document (no more model:null
  cards); D-3 — two on-disk based-on trees + acceptance tests (the
  cap-bites-in-a-live-run half recorded as residual in C8, honestly).
- docs: C8-profiles.md (§6 price 6 closed on the deferral's own terms, D-rows,
  12→13 fields), gap-register row 7.2, site-docs kinds.md counts, headline
  counts re-synced (2960).

CARRIED ALONG, DECLARED: the working tree held ~176 uncommitted paths from a
parallel in-flight workstream (research notes, MCP bridge, money-floor,
discovery-policy, bundles, CI) with no baseline commit separating them. They
are included as-carried — nothing reverted, nothing separable without a
baseline that never existed. Scope of THIS change is fully enumerated above
and in the per-step gate reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… else moved

Phases P0-P2 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, each written
test-first, each red before green, each with its positive control.

P0 — the net, built before any feature (nothing_that_already_works_changes.rs):
  * a golden `pact show` per shipped tree — eleven of them, found rather than
    listed — so any change to the document every adapter reads is a failure
    that names the tree and the byte;
  * `the_specification_only_ever_grows`: every field the schema had keeps its
    type, its `surface:`, its `tier:` and its requiredness. Additions are free
    and need no blessing; a rename, a retype or a re-tier fails naming the
    field, was: and now:. Proven to bite by softening `agent.policy` from
    S-EXEC to S-GEN — the discovered-spec exploit shape R40 records — and
    watching it name exactly that;
  * `checking_a_tree_that_carries_a_body_runs_nothing`: two hostile scripts in
    a payload folder, four verbs, and the canary file is still not there.
    Written now rather than beside the `program` kind, because a purity test
    that arrives with the feature it guards says nothing about the state
    before it.

P1 — `values:` (one figure, written once). `{use: <name>}` stands where a
scalar belongs and the loader puts the figure there, before `based-on:` and
long before the schema — so a base and everything derived from it read one
number, and a figure landing where it does not belong is the ordinary
`schema/wrong-type` at the line the author wrote `{use:}` on. `values:` is
then REMOVED, exactly as `based-on:` is: `one-figure-in-three-places` and
`one-figure-longhand` produce byte-identical `pact show`, so no adapter, no
second port and no runtime learns the feature exists. Optional `shape:` is
held against the figure at the definition; a circle is refused with the ring
written out; a figure nothing uses is a warning with teeth under
--deny-warnings.

P2 — `expects:`/`with:` (a base that takes arguments). A pattern declares
typed parameters, a caller supplies them, the loader fills the holes. Three
rules, each with its test: holes fill VALUES and never keys or structure (so
the governance surface of what a pattern makes is the governance surface of
the pattern, and the classifier never meets a field it has not seen); every
parameter is declared and shape-checked in both directions; and a pattern is
NOT a document — its body carries holes, so it is removed once used, and
`two-desks-one-pattern` and `two-desks-longhand` are one document. A hole
nothing declares is refused at the pattern rather than surviving into every
caller. Needed no schema field at all: `based-on:` was never declared either,
because derivation strips it before validation.

Both features compose and there is a test that says so: an argument may
itself be `{use: <figure>}`, because values resolve before derivation.

Carried, and each amended in the same change rather than after it:
  * `bundle.brings:` gains `values` — `what_a_bundle_may_bring_is_what_a_
    workspace_can_hold` holds that list equal to the workspace's collections,
    and a kind absent from it is not narrower governance, it is a kind
    `brings:` is blind to;
  * site-docs kind count 44 -> 45 and the `value` row; README 2960 -> 2984
    (1018 Rust + 1966 adapter). Both are guarded by tests that fail when the
    prose drifts from the product, which is how they were found.

Suite: 2984 green, 8 skipped, clippy clean in debug and release, TypeScript
type-checked, all eleven shipped trees clean under --deny-warnings, fully
offline. Rust 994 -> 1018 (+3 P0, +10 P1, +11 P2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P3 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, test-first.

A payload directory is carried verbatim — `skills/<n>/scripts/`, `assets/`,
`references/`, `documents/` — and the loader recorded each file's name, media
type and size, and nothing about its CONTENTS. So two workspaces holding the
same filenames at the same sizes and completely different bytes had the same
`workspace-digest`: signing a tree said nothing about the scripts inside it, a
lockfile could not pin one, and a reviewer who had read a body had no way to say
later that it was still the body they read.

EXP-8 (§1.3) specified `{ $file, contentType, sizeBytes, digest }` from the
start and the digest was never built. It is built now: sha256 of the contents,
lower-case hex, on `FileRef`, carried into `to_json` and into the CANONICAL
form — the second half matters, because a fingerprint outside the canonical
string is a fingerprint the workspace digest does not cover, which is the whole
property being bought.

The bytes still never enter the document. `fingerprint()` reads in 64 KiB
chunks and keeps only the hash: a payload is arbitrary bytes and need not be
UTF-8, and a large asset must not be held in memory to be described. A file
that cannot be read carries an EMPTY fingerprint rather than a guess — the walk
already reports unreadable entries, and a made-up digest is worse than none
when the entire value of the field is that it can be compared.

Reading a body to fingerprint it is still not running it, and that is asserted
rather than asserted-in-prose: `fingerprinting_a_body_is_not_executing_it`
plants a hostile script in a payload folder, runs all four verbs, and checks
both that the canary file does not exist AND that the script was fingerprinted
— so the purity claim cannot pass by the file having been skipped.

It lands on its own, before anything executes anything, because it is what
makes a body pinnable at all (P6).

The P0 net caught the one-time cost and named it exactly: two shipped trees
moved, the nine without payloads did not. The diff was verified to be purely an
ADDED `digest` key on the two payload-carrying files before re-blessing —
2 differences, both ADDED, no CHANGED and no REMOVED. `tests/golden/schema-
fields.txt` catches up P1/P2's additive `value` group in the same bless; the
subset test never required it, which is the point of a subset test.

Suite: 2997 green (1026 Rust + 1971 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, eleven shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P4 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, both halves, test-first.

Commit 78460c1 shipped two things and did not join them. The `agent` answer
shape made an agent's NAME a value the format can carry — validated as a plain
key, never a path or an address (`questions.py`), reaching `accepts:`,
`answers-with:`, `run-inputs:`, `action.takes:` and `question.answer` at once.
The recursion fuel made a delegation circle legal exactly when every agent on
it writes `limits.asks-itself-at-most:`. But nothing ever dereferenced an
agent-shaped value: a document could declare `second-look: agent`, a ticketing
system could supply `night-shift`, and the only agents a run could hand work to
were still the static `team:` keys. The shape existed and could not be put to
work.

Joining them needs a rule, because the static one cannot be evaluated here.
`teams.rs` decides a circle by looking at the circle, and an agent named at run
time is on no circle the loader could have looked at — under dynamic dispatch
the potential call graph is "any agent an `agent`-shaped value can name", which
is every agent in the workspace and no smaller set. So the obligation moves off
the circle and onto the agent that can be received, where it can be decided
from that agent alone:

    An agent may be put to work BY VALUE only if it writes its own
    `limits.asks-itself-at-most:` figure.

That is the static rule relocated, not a second rule beside it.

RUN TIME (`harness.py`). An admission pass turns a supplied `run-inputs:` value
of shape `agent` into a delegate the model may hand work to. Which inputs those
are is decided by `Shape.parse` and never by matching a spelling — `an agent`,
`which agent` and `the name of an agent` all mean `agent`, and the closed
vocabulary that says so lives in one place. A name the author already wrote
under `team:` is left alone, because the sentence there is the author's own and
is what the model reads when choosing. The refusal is at the DELEGATION site
and not at admission: that is where the member's own `limits:` is read, and it
is what makes an overspend that member's FAILURE — the same `OverBudget`-shaped
path — so `if-someone-fails:` decides rather than the run crashing over a name
the author never wrote. The activation meter counts a dynamically named member
exactly as it counts a static one, because both arrive through the same asker.

A member named by value also joins the BUDGET POOL. It spends the parent's
money like any other, and sizing the shares as though the team were smaller is
the same overstatement the per-step pot was (R32).

CHECK TIME (`handover.rs`). What the checker can say before anything runs is
the case that can never work: a tree that asks to be handed an agent's name and
holds nobody who could be handed over — every name it is ever given would be
refused, so the line loads and does nothing. It walks the SPECIFICATION's own
types to find the fields carrying the shape, so a sixth field given the answer-
shape vocabulary joins this check by existing rather than by being remembered.
An abstract base does not count as handable: `base: yes` says the agent never
runs and five doors already hold that promise, so a name handed over at run
time would be a sixth. It deliberately does NOT warn when only SOME agents are
handable — which one the surrounding system will name is a fact about the
surrounding system, and refusing an author's correct line because a different
agent has no figure would be a warning about somebody else's document.

Additive inertness, asserted both sides: a document with no agent-shaped input
produces a byte-equal trace and never sees the warning.

`agent_valued_inputs` is a new authored-state field, so it owes the totality
register a row — added with its reason, the same division `knowledge` and
`external_prose` already make: the worked example's one input is
`customer-id: text`, and the mechanism is demonstrated whole in the test that
exists for it.

Suite: 2997 green (1026 Rust + 1971 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, twelve shipped trees clean under
--deny-warnings, fully offline. The second port declares no delegation at all,
so it is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An adversarial audit over the whole specification ran five lenses against
06ac849 and two finished before the run was cut short. Both converged on the
same defects, independently, and every one reproduced against the shipped
binary. They are fixed here, test-first, with the reproducing tree kept as the
test.

The pattern in all of them is the same and worth naming: P1 and P2 were tested
against the paths I anticipated — the happy one and the refusals I had thought
of — and every defect below lives on a path I had not.

**A `{use:}` was read as a reference wherever the word appeared.** The walk was
untyped, so any map carrying the key `use` was a use site — including in the
slots the specification types `anything`, which is the format saying *these keys
are the author's own, read them verbatim*. `case.with:`, `case.expect:`,
`metric.with:`, `knowledge.documents:` and `state.starts-as:` are all typed that
way, and `metric.with:`'s own help is the sharpest statement of the contract
that broke: "written exactly as its own documentation names them. Nothing is
renamed and nothing is filled in for you". Measured: an eval case carrying
`with: {use: the winter catalogue}` — ordinary author data — was REFUSED, and
the quiet direction was worse, because a name that happened to match a figure
was silently replaced. Substitution is now typed from the workspace down and
`anything` is never descended into.

**A figure could not reach the catalogue, and did.** `models:` is the one
collection both ports read straight off disk — `resolve.py` opens
`<workspace>/models/catalog.yaml` with its own reader, because the override
layer is applied row by row (D8, §4.2). A figure written there was substituted
by the loader, so `pact check` passed, and not by the port that binds the model.
That is the "free below the loader" claim failing in the one place it can.
Refused by name now, saying why.

**The `value` group was never validated.** `values:` is removed before the
schema sees it, so every annotation the group carries was decoration: `value:`
was not required, and a misspelt `shape:` turned OFF the only check that line
exists to perform, in silence. R41's shape exactly — an unvalidated group is an
unclassified group. Both checks are made in the pass that owns the collection,
at the definition, and a test holds the Rust vocabulary equal to the schema's
own `choices:` so the two cannot drift.

**Rule 1 was false against the caller.** "Holes fill VALUES, never keys, never
structure" was checked against the pattern and never against what a caller
handed it, and `fill` splices an argument in whole where a value is exactly one
hole — so a Map argument landed where a Map was never written and the CALLER
decided the governance surface of the result, which is the property the rule
exists to deny (LOAD-13). Arguments are scalars now. A hole inside an argument
is refused with one exception, which turned out to be the useful case: a pattern
FORWARDING its own parameter, `with: {domain: <domain>}` on an entry that itself
declares `domain`. That is how a chain of patterns hands a figure down, and it
is checkable because the caller's declarations are right there.

**A pattern built on a pattern shipped with holes in it.** `derive_one` stripped
`expects:` unconditionally — right for the base's declarations, wrong for the
deriving entry's own — so an entry that both derived and declared stopped being
a pattern before the removal pass looked, and survived into the document
carrying literal unfilled `<holes>`. A pattern chain now works end to end and
the middle layer leaves the tree like the root does.

**Four messages carried the indentation of the file they were written in.**
`format!` strings wrapped without a `\` continuation put twenty-two literal
spaces in the middle of the sentence an author reads. O7.3 makes the diagnostic
the product surface, so that is a defect in the product. Fixed, and guarded:
`no_message_carries_the_indentation_of_the_file_it_was_written_in` scans the
loader's own source, because the next one will be written somewhere else.

**One overclaim withdrawn rather than built.** The `bundle.brings: values`
comment said a bundle's figure and a local one were indistinguishable after
mounting. Measured: a bundle's TOOL is not nameable by `uses:` either — the
mounting half is unbuilt for every kind alike, not for this one in particular.
The comment now says that instead of promising it.

Suite: 3007 green (1036 Rust + 1971 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, twelve shipped trees clean under
--deny-warnings, fully offline. No existing test was weakened; the seven new
ones are all refusals with their positive controls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P5 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, test-first — and not the P5 that
plan describes, because the plan was written against a vocabulary the product
does not have.

The plan proposed `learning.applies-up-to: CLASS-2`. There is no CLASS-2 here:
`classify()` answers LOW, HIGH or UNKNOWN, and UNKNOWN is treated as HIGH.
Building a four-class dial to sit in front of a three-value judgement would have
been a second spelling of one decision — R59's mistake, in the file R59 was
about. So the dial is not built, and what IS built is the gap the shipped code
actually has.

`may-improve-on-its-own:` is `tier: core` and offers four words. `SAFE_TO_CHANGE`
maps them onto spec fields: `phrasing` → `instructions`, `examples` →
`description`, `skill-notes` → `content`, `when-skills-are-used` → `use-when`,
`do-not-use-when`, `if-unsure`. `CAN_BE_APPLIED` was the one-element tuple
`("instructions",)`. So THREE OF THE FOUR WORDS granted something no cycle could
ever put into effect: an author who wrote `may-improve-on-its-own: [skill-notes]`
had said "this may change without me", and every proposal came back with a
sentence about a limitation of this process rather than about their document.
A governance surface that loads and does nothing, in the one file whose whole
subject is what may change with nobody watching.

The mechanical excuse was true for one of them and false for the other two.
`_with` rewrites the spec that gets SCORED, so a field applies only if a run
READS it — otherwise both scoring runs grade the same agent and the comparison
has no candidate in it. A skill's body and its four routing lines are not that:
`SkillSpec.in_words()` puts every one of them into the system message. What was
missing was the ability to rewrite one, not the ability to score one.

So `content`, `use-when`, `do-not-use-when` and `if-unsure` join
`CAN_BE_APPLIED`, and a proposal names the procedure it is for by carrying that
procedure's current text: a `before` matching nothing is a proposal about a
document that has already moved, and applying it to whichever procedure happened
to be first would silently rewrite the wrong one. Refused, saying so.

`description` stays unappliable and is now refused for the reason that is TRUE
about it — it reaches the A2A card and no model — through `READ_BY_NO_RUN`, one
entry with one reason. The register is held both ways: every word the author may
grant must name a field that is either applicable or excused with a reason, so a
third state cannot appear by omission, and an excuse without a reason fails.

The message says both halves. An existing test requires the refusal to name what
a cycle CAN apply, and that requirement is right — a reviewer needs it — so the
sentence carries the true reason AND the list, rather than one replacing the
other. No test was weakened to land this.

Nothing about what MAY change moved: `classify()` still decides, `uses:` is
still HIGH and still refused by the classifier rather than by the mechanical
limit, and widening what CAN be applied deliberately does not widen what may be.

Suite: 3018 green (1036 Rust + 1982 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, twelve shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P6 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, test-first. The kind lands; the
executor is P7. Nothing in this commit can run anything.

A PACT agent has had two hands: the MODEL, which understands anything and
fundamentally guesses, and a CONNECTED SERVER, which is exact and is something
somebody has to deploy and keep running. A skill could CARRY a script and PACT
recorded that it was there and nothing else — a person ran it, by hand, out of
process. So the worked example's refund window is six lines of date arithmetic
the model works out in its head, and gets wrong often enough to matter.

A `program` is that script, declared: `engine:`, `determinism:`, `takes:`,
`answers-with:`, `fuel:`, and a `body/` folder carried by name, media type, size
and fingerprint. `tier: expert` on every field of it, which is what keeps D14
true — no core capability may REQUIRE a program, a workspace carrying one does
not earn the `no-code` badge, and deleting `programs/` must leave a working
agent, the standard the worked example's own carried Python helper is held to.

TWO LEDGER ROWS ARE AMENDED, both on their own recorded terms.

R42 refused `runs-as: code` because it "would make `pact check` the thing that
decides whether a script is safe". Its letter is kept: there is no fourth way for
a tool to reach, `connect:` still names a resource the HOST supplies, and the
checker decides nothing about safety — it validates the DECLARATION (the shapes
resolve, the fuel is written, the named room can host the engine) and never opens
a body. `checking_a_program_does_not_run_it` plants a body that would write a
file, runs all four verbs, and the file is not there.

R58 deleted `sandbox` from `resource-kind:` because it "had no field in this kind
that only they would use", and said each deleted choice would return "with the
fields it needs". It returns with two that are its alone: `engines:`, and
`asks-to-run:` — because running somebody's carried body on your machine is at
least as much a decision as opening a connection, and that consent now reaches
`pact waits`, which is the list a runtime is obliged to walk.

The one rule the schema cannot state about itself is a new pass. A program says
which engine it is written for and a room says which it can host; each is held
against its own closed list and NOTHING held them against each other, so a
workspace could carry a program in one language and a locked room that cannot run
it — every file individually correct, the arrangement impossible, and the first
call the only way to find out. `programs.rs` refuses it where the author is,
naming the engine needed and what the room actually runs.

`allow-egress:` gains `programs` as its eighth part. A carried program has no
network unless that line says so, which is the state a reviewer should be able to
assume by reading nothing.

The four fields nothing here reads are DECLARED DELEGATED with their reasons
rather than left to be noticed — `determinism`, `fuel`, `instructions-at-most`,
`memory-at-most` are the locked room's to honour, the §4 split `mcp-server`
already has. `engine` and `engines` are deliberately NOT delegated: the checker
holds them against each other, because that much is knowable from the tree.

Carried in the same change, each found by a standing guard rather than by memory:
`bundle.brings` gains `programs` (the equality test); the reader-coverage and
value-coverage registers gain their rows; site-docs 45 → 47 kinds and 27 → 29
name-resolving fields; README 3018 → 3026.

Suite: 3026 green (1044 Rust + 1982 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, thirteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P7 of docs/41-IMPLEMENTATION-PLAN-PROGRAMS.md, test-first — and it does not ship
a WebAssembly engine, which is a decision rather than a shortfall.

The plan said "the wasm executor". Building one here would mean fetching a
runtime into the core of a project whose D17 promise is that validate, resolve,
build, eval and optimise all run air-gapped — or vendoring one, which makes the
portable artifact carry a runtime it cannot keep current. Neither is a trade this
format makes for a transport, and a program runner is a transport: `Transport`
and `tool_impls` are both in `SUPPLIED_BY_THE_HOST` and nothing in `src/` builds
either, for exactly this reason. `mcp/calling.py` says the same thing in its own
first line about MCP clients — *"Nothing in `src/` calls it, and nothing should"*.

So what ships is the seam, and the honesty:

  * `run_program` joins `SUPPLIED_BY_THE_HOST`, declared with why. A host that
    has a locked room passes one; nothing here constructs it.
  * `AgentSpec.programs` carries what THIS agent's tools can reach — walked the
    way a run reaches one, `uses:` → the tool → the action's `program:` — so a
    program another agent's tool reaches is never reported here. Reporting it
    would be a sentence about somebody else's document.
  * A run with no runner names every one of them on `unenforced` BEFORE the
    first call, with the tool and action that reach it and the engine it needs.

That last line is the whole point. Without it the author meets `error: no tool
named 'refund-window'` on the first call — a true sentence about a different
thing, which reads as a typo in a file that is correct. A capability the runtime
cannot honour is named before the run, or it is silent degradation on the one
feature whose selling point is exactness.

`ProgramSpec` carries names and shapes and never a body. Whether a machine can
start a `wasm` program is the machine's business; what this port does with the
word is report it.

Additive inertness is asserted: a document declaring no programs gains no
sentence and the word appears nowhere in its trace. `programs` joins the
authored-state register with its reason — the worked example carries none
BECAUSE it is the example that demonstrates the no-code ceiling, and a program is
`tier: expert` everywhere.

Suite: 3031 green (1044 Rust + 1987 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, thirteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8, first wave, test-first. `uses:` names a carried program directly, and the
rule that makes the shortcut safe is `determinism: pure`.

P6 made a program reachable through a tool's action, which is exactly right
whenever the call needs governing: `needs-a-person:`, `spends-money:`,
`same-request-key:`, `bind:` and `inspects:` all apply to a program action
unchanged, so the whole approval algebra composes with programs for free and
nothing had to be rebuilt for them.

For a pure calculation that is ceremony. Working out whether a date is inside a
window took a tool file, a `connect:` line, an action and a resource — four files
in front of six lines of arithmetic that cannot touch anything. So the program is
offered to the model as itself: its `takes:` is the argument list, its
`answers-with:` the result.

THE RULE IS THE POINT. A pure program works from what it is given, observes
nothing and changes nothing, so there is no act for an approval rule to be about
and nothing an `inspects:` line could usefully look at — the governance
vocabulary has nothing to say, which is why skipping it costs nothing. A program
that may read the outside world, or answer differently the second time, is
precisely the kind of call that vocabulary exists for, so it keeps its tool.
Letting it through the short door would make the shortcut the way round the gate,
and the refusal says which door to use instead.

Two holes closed in the same change, both found by standing guards rather than by
memory. `stage.may-use:` gains `programs`: a stage only ever NARROWS what the
agent has, so a kind the agent can use and no stage can name is a hole rather
than a narrowing — the argument that put `skills` there, held by a test that
computes the difference between the two lists and names it. `variant.may-use:`
gains it for the same reason, before the same hole is found there.

One existing assertion moved and it is not a weakening: a DX test pins the
"did you mean" sentence for a mistyped `uses:` name, and its own comment says
the property is that the list is GRAMMATICAL (`or_list`, not `.join(" or ")`).
Four kinds instead of three is the same property over a longer list, and the
message still teaches the construct and still names the file to write —
`programs/<name>/program.yaml`, which `file_for` already knew.

Suite: 3036 green (1049 Rust + 1987 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fourteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three lenses that died on a session limit ran against the hardened code.
Ten agents finished; these are the findings that reproduced. Two are defects I
introduced in the commits above, and both are the same kind of mistake: a fix
that solved the case I was looking at and reopened the one I had just closed.

**`with:` hijacked an eval case's own data — the untyped-walk defect arriving
through the fix for it.** 498875c made substitution schema-typed precisely so a
map carrying the word `use` in an `anything` slot would be read verbatim. Then
P2's arguments needed filling, `with:` is not a field of any group, and I keyed
the interception on the WORD. But `with` IS a real field: `case.with:` and
`metric.with:` are `map of anything`, which is the specification saying *these
keys are the author's, read them verbatim*. Measured: `with: {attachment: {use:
spend-cap}}` on an eval case came out as `{"attachment": "0.05 USD"}` — an
author's data silently rewritten into a figure. The interception is gated on the
specification now: a group that declares a field called `with` has one, and its
`with:` is data.

**`base: yes` was bypassed by the handover I shipped one commit earlier.**
bf9a29b's own message says "a base is not an agent a name can be handed to", and
the checker holds that — for the case it can see. A VALUE arrives from a
surrounding system the tree cannot see, so the name it carries was never checked
against anything, and the harness had none of the rule. `base: yes` means it
never runs; five doors hold that promise at check time and this is the sixth, the
only one the harness has to hold itself. It is a SECOND rule and not a special
case of the dynamic-bottom one: a base may perfectly well carry
`asks-itself-at-most:` — it is the pattern its descendants inherit — and satisfy
that rule completely while still being a thing that must never run. Refused at
the delegation site, so it is that member's failure and `if-someone-fails:`
decides.

**A parameter's `shape:` was switched off by a typo, in silence.** The same
defect `values.rs` was fixed for in 498875c, one file over and unfixed: an
unrecognised word made the lookup answer `None` and the check was skipped. Worse
here than there, because a pattern's declaration is inherited by every caller.

**A pattern could not hold an angle bracket.** `holes_in` reads `<name>` as a
hole, and prose is full of them the moment anybody writes an XML-ish prompt tag —
`<thinking>`, `<answer>`. In a pattern that refused the whole document, and the
fix line said to declare it, which would have SUBSTITUTED the tag away. `<<name>>`
is the escape, unescaped last so it can never become a hole.

Every fix has its test and every refusal its positive control; no existing test
was weakened. The remaining findings — a figure inside a sentence, a figure in an
eval case, list-valued arguments, `x-` blocks read by templates, provenance for
what was substituted — are real, are recorded, and are not in this commit.

Suite: 3040 green (1053 Rust + 1987 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fourteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last of the audit's high findings, and the one that needed a decision rather
than a fix.

Two rules this format lives by pull in opposite directions here, and both are
right. `case.expect:`, `case.with:`, `metric.with:`, `knowledge.documents:` and
`state.starts-as:` are typed `anything`, which is the specification saying *these
keys are the author's own, read them verbatim* — and 498875c made that true by
walking the document typed, so an ordinary English word cannot become a
reference. But an author who writes `{use: spend-cap}` in one of those slots and
means the figure gets nothing at all: no substitution, no message, and the
literal map ships into the document and on to whatever reads it. "Loads and does
nothing" is the failure this format refuses everywhere else.

They are separable, and what separates them is whether the name is a figure this
workspace actually HAS. `{use: the winter catalogue}` names nothing and is
plainly data. `{use: spend-cap}` names a figure, in a slot that cannot take one,
and was written by somebody who meant it.

So: a warning, naming the figure, saying that this line is read exactly as
written and that `{use: spend-cap}` is therefore what gets used. Not substituted
— the verbatim contract holds and the data is unchanged — and with teeth under
`--deny-warnings` for whoever wants them. A word that names no figure draws
nothing, which is the half that keeps an author's own key from becoming a warning
because a feature they never used exists.

Both halves have a test.

Still open from the same audit, recorded rather than silently dropped: a figure
INSIDE a sentence (`says: Refunds over {use: cap} …`) is interpolation and a new
capability rather than a defect; list-valued pattern arguments; templates reading
inside `x-` blocks (AD-14); and provenance in the LoadReport for what was
substituted, which is P2's own exit gate.

Suite: 3042 green (1055 Rust + 1987 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fourteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three phases did not land as written and the document said otherwise, which is
the drift its own protocol exists to catch. P5 named a CLASS-1..4 vocabulary the
classifier does not have; P7 said 'the wasm executor' for a project that may not
fetch a runtime into its core; and the plan assumed its analysis was sound when
five defects in P1/P2 were found by audit instead — two of them introduced by
the fixes for the other three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 2, test-first. Memory becomes readable and writable from the format, and
the guard that was already written finally has something to bite on.

`remembers:` has been the closest thing PACT has to a variable since it landed:
declared, lifetime-scoped, and write-guarded by `never-from:`. What it could not
do is take part in a call. `bind:` filled an argument from `run-inputs.<name>` —
what the surrounding system supplied for this run — and from nothing else, so a
fact the conversation had ESTABLISHED could only reach a tool by the model
retyping it. That is exactly the value you least want the model choosing, on the
field whose own help says it is "how 'whose order' stops being something the
model decides".

Two lines, in opposite directions:

  * `bind: { account: remembers.verified-account }` — the argument is filled from
    what the run remembers. Held against what agents actually remember, the same
    way the first namespace is held against what they supply.
  * `remember-as: last-order-seen` — what the action answered is kept under a
    name the author declared.

THE SECOND ONE IS WHY `never-from:` EXISTS. That field names the sources that
may never write to a remembered fact, `tool output` first among them, and its own
help says why: letting a tool result become remembered instruction is how a
single poisoned page becomes permanent. Until now no line in the format was a
write, so the guard could not fire — it was a boundary a reviewer could read and
nothing could cross. `remember-as:` is that line, and it is refused BY DEFAULT
where the state says so, at the line the author wrote. Taking the guard off is
one visible edit in the fact's own file, and then it is allowed: a guard the
author cannot reverse is a wall, and the next person works around a wall.

Together they are a variable a run can read and write, and still not one it can
branch on — F4's refusal stands untouched, because nothing here is a condition.

One diagnostic is written out rather than routed through `offer()`, and the
reason is a real defect it would have had: a desk that remembers things and
supplies no run inputs was told "nothing is declared there yet", which is true of
one namespace and false of the other. The sentence now names both namespaces
always, and the fix names only what this tree really has — because a fix offering
`run-inputs.<something>` to a desk that supplies none is a line the author cannot
type.

Suite: 3049 green (1062 Rust + 1987 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 3, test-first. `program:<name>` is the third metric scheme, and it runs
in the deterministic-first band beside `pact:`.

`evals.metrics:` names a score by URI and two schemes shipped: `pact:`, the small
set this build takes on its own with no model and nothing installed, and
`deepeval:`, which reaches every score that library has. What an author could not
bring is a score of their own that is EXACT.

That is the gap the `program` kind exists to close, one surface over. A refund
amount, a checksum, a date window each have a right answer, and grading one meant
choosing between two bad options: a `judged:` rule put to a model — which costs
money, needs a judge binding, and cannot be decided offline — or a
`must-contain:` string match that grades the WORDING rather than the number.

The band it belongs in is the point. `pact:` scores run first so a fully
decidable suite never invokes a model (AC-4.5), and a carried grader has nothing
to bind: it is arithmetic in the folder. So a suite built from these is decidable
air-gapped, which is what D17 asks of the whole eval pipeline and what a
judge-graded suite can never be.

It inherits the rule every program has: what RUNS it is the host's. The seam is
declared, and a score nothing can run is reported with a line to type rather than
counted as a failure the agent caused — the promise `evals.metrics:`'s own help
already makes ("a score this machine cannot take is named on the report with a
line to type, never quietly skipped"). `program:` with no name after the colon is
refused for the same reason a scheme with no colon is: guessing which grader was
meant is how a suite comes to measure something nobody asked for.

A runner that raises is THAT SCORE's failure and not the suite's — the same
reading `_call_tool` gives a tool that could not run, because one grader that
could not answer must not take the other scores down with it. A grader that
answers something that is not a number is refused the same way, saying what it
said.

The Rust checker needed no change: `every_metric_says_who_provides_it` requires a
colon and leaves which schemes exist to the port that runs them, which is why the
third one costs a constant and a branch rather than a schema edit.

Suite: 3057 green (1062 Rust + 1995 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 4, test-first. `question.checked-by:` names a carried program that looks
at a human's answer before anything acts on it.

`question.answer:` declares the shape of what a person types — `approved: yes or
no`, `amount: money` — and `Question.validate` holds each field to it, saying
exactly what to type when it does not fit. That is a check on the KIND of value,
and it was the only check there was.

Some answers have more to them than a kind. An account number has a checksum. A
refund has a ceiling the policy sets. A date has to be one that happened. None of
those is expressible as a shape, and the only place to put them was AFTER the
fact: the run resumes on the answer, the tool is called with it, and the mistake
is found by whatever is on the other end, having already acted.

What this buys is not validation for its own sake — it is WHEN. The person is
told while they are still standing there, which is the one moment a correction is
free, instead of after the run has moved on.

Three rules, each with its test.

The shape is checked FIRST and a program never sees a value that is not the kind
the author declared. Otherwise every grader would have to re-implement the shape
vocabulary, and two of them would disagree about what `money` is.

The program's own sentence is what the person reads. It is the thing that knows
why the amount is wrong; a generic "invalid" would throw that away at exactly the
moment somebody could act on it.

A check nothing can run REFUSES the answer rather than passing it. An author who
wrote a check, watched it load and never had it run is the failure this whole
feature exists to remove, arriving one level up — and a person told nothing is a
person who believes they were checked. Same for a grader that raises: the answer
is not accepted and the sentence says what happened.

Additive inertness is asserted both ways: a question that names no check behaves
exactly as before, with or without a runner handed in.

Carried: site-docs 29 → 30 name-resolving fields, found by the test that guards
that number rather than by remembering.

Suite: 3062 green (1062 Rust + 2000 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 5, test-first — and it closes a §6 deferral on the terms §6 itself set.

`docs/50-NOT-COPIED.md` §6 recorded Eve's `toModelOutput` as DEFERRED, with a
re-admission condition and a shape: *"a case where `shorten-long-results` in a
context policy is measurably worse than projecting at the source"*, and *"a
projection on `tool.actions.<a>`, and a way to write one that is not code"*.

Both halves are answerable now. The way to write one that is not code is the
`program` kind. And the case is not hypothetical, because tidying answers a
different question: `shorten-long-results` trims whatever is LONGEST once the
conversation stops fitting, which is not "the model needs four of these forty
fields". Three costs follow from deciding late, and the third is not about money:

  * every token of the other thirty-six is paid on every turn until the tidy
    fires;
  * the trim is by length rather than by meaning, so which fields survive is an
    accident;
  * a poisoned record buried in field thirty-seven reaches the model IN FULL,
    and goes on doing so every turn until something trims it by accident.

That last one has its own test, with a field carrying an instruction the model
was never meant to read: the projection decides by meaning, at the source, before
the first read, and the string appears nowhere in the trace.

The projection runs BEFORE the interceptor chain and before `history` — before
the chain deliberately, so a redaction rule sees what the model will actually be
told and does not do work over thirty-six fields nobody will read; before
`history` necessarily, since the whole point is that the payload never becomes
something paid for on every later turn.

`pure` only, and the reason is stronger here than for `uses:`. There, purity buys
the absence of anything to govern. Here it buys DETERMINISM: `projects-with:`
decides what the model is told, and a projection that could read the outside
world or answer differently twice would make the conversation unreproducible —
while the trace is exactly what the portability claim is measured on. Refused at
check time, naming the program and what it says it is.

A projection nothing can run leaves the answer WHOLE and says so on `unenforced`,
naming the action. Silently serving the full payload while reporting success is
the failure the line exists to remove, arriving one level up.

A call carries which action it is, so a tool offering several actions projects
only the one the author wrote it on; a call naming no action matches only when
the tool has exactly one, because guessing is how the wrong projection runs.

Additive inertness asserted by byte-equal traces with and without a runner.

Suite: 3066 green (1062 Rust + 2004 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 6, test-first. The two rewrite powers become authorable, and half of R24
is withdrawn — recorded in `50-NOT-COPIED.md` §8.5 rather than deleted, the way
§8.3 records R29's.

`Power` has five members and an author could write three. R24 took
`change-the-request` and `change-the-answer` off `interceptor.may:` for a reason
that was exactly right: no sentence in the closed vocabulary rewrote — every one
hides, stops, or sends the run elsewhere — so declaring either got the rule
refused by the next check down. A choice a non-coder can type and nothing can
ever exercise reads as a capability, which is worse than an absent one. §6
recorded them host-only with the condition that would bring them back: "a
sentence somebody actually wants".

Two sentences now want them:

    replace the answer with what house-style returns
    replace what the model is told with what redact-clinical-terms returns

§6's sharper worry was that a mid-run rewrite "is not reviewable in a way
`instructions:` and a stage's `says:` are". A carried program answers that rather
than dodging it: it is a file in the folder, fingerprinted since P3, declared
with what it takes and answers with, and refused unless it is `pure`. What the
rewrite does is as readable as the instructions beside it, and the same twice.

A rewriter with nothing to run it leaves the words EXACTLY as they were and
records why on the chain, so the run can report it. Half-applying would be worse
than not running, and silently passing them through would leave the author
believing their program had run — the failure this vocabulary exists to refuse,
one level up. A program that raises does the same and never a half-rewritten
answer.

THREE TESTS HELD THE OLD PREMISE AND EACH MOVED TO THE PROPERTY THAT SURVIVES,
which is not the same as weakening them. All three asserted "these powers are not
offered", and their own comments named the reason: "no sentence in the closed
vocabulary rewrites". That premise went stale rather than being wrong — R29's
shape exactly. What survives is what R24 really states, and it is still enforced:
`may:` and the rules must AGREE. Declaring a rewrite power beside rules that only
mask is still refused, and the refusal now names what those rules actually need,
which is the more useful half and was never available while the choice did not
exist. The closed-list test keeps its real property — every member is a NAMED
EFFECT rather than a way in — and now asserts the whole set plus that no choice
reads as "run this code", which is the cascade it exists to keep out.

Each of the three gained a positive control, because "still refused" is
indistinguishable from "never possible" without one.

One fixture of mine was wrong and the checker was right three times over: bolting
a rewrite onto the card-number rule drew a power that reaches nothing at those
moments, a sentence carryable at neither, and two hiding rules still needing
`hide-values`. The test writes its own interceptor now.

Suite: 3074 green (1070 Rust + 2004 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 7, test-first. `decided-by:` routes on what was SAID rather than only on
what kind of thing happened — the `router` escape §5.5 already lists, realised
with the `program` kind.

The three outcomes are complete, readable by anybody, and deliberately blind:
they know what KIND of thing happened and never what the answer said. So a
checking stage can write "I found a contradiction with the policy" and the loop
still marches to `reply`, because that is where `answered:` points — leaving the
correction to the model noticing its own prose, which is the one thing this
format never relies on anywhere else.

`docs/remediation/F1` refuses a predicate over content and that refusal STANDS
for the authored surface: a condition language would be a second programming
language inside the file meant to remove the first, and D14's reader would have
to learn it. This is not that. §5.5 names `router` among six typed escapes, and
an escape is priced rather than hidden.

Four things make it an escape and not a hole, each with its test.

THE DESTINATIONS ARE DECLARED. A program picks BETWEEN stops the author wrote in
`may-go-to:`; a name outside that list stops the run rather than going somewhere
nobody wrote down. Otherwise the list is decoration and a reader cannot tell
where the loop can go by reading it.

FUEL OUTRANKS ROUTING, and this is the property that matters most. Ceilings are
checked before a stage runs and after every model call, and no program decision
reopens a spent budget. Tested with a router that always says "keep going"
against `steps-at-most: 3` — because "the router said continue" must never mean
"the money cap did not apply", which would make every limit in the format
advisory on any loop that uses one.

IT IS PRICED. `tier: expert` on both lines, and a workspace using them is no
longer one a non-technical author could have written. A support lead's loop stays
the three-outcome table.

A ROUTER WITH NOTHING TO RUN IT STOPS THE RUN rather than guessing. Guessing a
destination is inventing control flow nobody wrote.

The outcome vocabulary is unchanged and stays the closed three: `decided-by:` and
`may-go-to:` are not outcomes and never were — they say WHO decides and WHERE it
may go, so they are exempted by name from the outcome check rather than widening
it.

One bug of my own, caught by an existing test: I threaded the words into both
routing sites with the same variable name, and in the `ask-someone` branch the
words are the PERSON'S answer under a different name — `text` is not bound there
at all. A router reads what the stage produced, and what that stage produces is
the reply somebody typed.

Suite: 3081 green (1070 Rust + 2011 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P8 wave 8, test-first. CodeAct lands, and P8 is complete.

`docs/30-FRD.md` FR-6.1.5 requires six loop patterns and names CodeAct among
them; AC-5.2 repeats it. It has never been shippable for a reason that was
correct rather than an oversight: there was nowhere to run anything. The locked
room is what changed.

IT IS NOT R16, AND THE DIFFERENCE IS AUTHORITY. That row refuses orchestration
code the model writes while it runs — Eve's fan-out tool — because it "cannot be
reviewed before it runs, cannot be diffed, cannot be signed, and is not the same
twice", and D22/D23 require structural change to pass a person first. Its subject
is code that decides TOPOLOGY: which agents exist, who is asked, what shape the
run has. A CodeAct snippet has none of that. It is an ACTION inside one step,
with the same standing as a tool call the model asked for, and:

  * it is offered NOTHING else — no tools, no teammates, `step_tools` is not
    passed — so it cannot call a gated action, spend money, or ask an agent,
    because none of them is in front of it. That is the whole safety argument,
    and it is one assertion in the tests;
  * it runs in the locked room under deny-by-default egress and the stage's own
    fuel, so it reaches nothing the author did not grant;
  * it lands in the transcript verbatim, so what it did is reviewable after the
    fact exactly as the model's own prose already is — the standard that prose is
    held to, not a lower one.

AD-83's line is `meta-depth = 1`: no run-created thing holds topology-authoring
authority. A snippet that computes a refund total is nowhere near it.

A `run-code` stage with no locked room STOPS THE RUN and says so, rather than
quietly behaving like a `think` stage. An author who wrote `does: run-code`,
watched it load and got prose has been told something untrue.

The room is asked under the reserved `pact:run-code` rather than a name from the
tree, because the code was written this second by the model and there is no
program for it to collide with.

Three mistakes of mine, all caught by the suite rather than by me: a stray double
comma from a regex splice broke the wording table; the branch was placed before
`asking_now` and the skill narrowing existed; and it called `_meter_usage` with
the argument order reversed. Each was a compile-or-run failure within seconds,
which is the argument for writing the tests first rather than for being careful.

Suite: 3086 green (1070 Rust + 2016 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P9, test-first. The last phase of docs/41, and the one every phase before it was
safety equipment for.

D22 grants an agent the right to author tools for itself — the strongest form of
self-modification this format has. FR-6.2.5 and M7.4 carried it as planned and
unbuilt, for the reason everything else in this plan was unbuilt: there was
nowhere to run a body and nothing to hold one to.

AD-85's shape is the interesting part. It does not answer "may an agent write a
tool?" once; it SPLITS the grant by what the tool is.

Under the `no-code` badge a self-authored tool must be a COMPOSITE — a
declarative composition of actions already approved and already pinned. There is
no new behaviour in one, only a new arrangement of behaviour somebody already
signed off, so a support lead can read it line by line, which is what D13
requires and what a code body can never give them. "Already approved" is the
whole of what makes it safe: without it the lane is a way to reach any action at
all by composing it, and the composite becomes the door round every gate the
author wrote. Refused, naming the action nobody approved.

A code-bodied tool requires a distinct `engineer` role, and the approval surface
must say, in those words, "this tool contains code that has not been read by a
person". Under D13 the human signing cannot read the body. Saying so is the only
honest thing to put in front of them, and every softer phrasing makes the
signature mean something it does not.

AND THE ACCEPTANCE RULE, which is why any of this is careful. "Does not raise an
exception" is forbidden as an acceptance criterion in AD-85's own words, and the
reason is measured rather than theoretical: SkillWeaver's exception criterion was
gamed by silencing every atomic action's errors — a tool that swallows its own
failures passes it perfectly. A tool offering "it ran without raising" as its
evidence is refused and told what would count, and the positive control keeps
that refusal about the CRITERION rather than about learned tools.

Asked of both lanes, deliberately: a composition kept because nothing went wrong
is the same absence of evidence as a body kept that way.

Removal is the half nobody builds. §8.5 requires it to be as expressible as
addition, so a revoked digest cannot bind — deleting the file and hoping is not
removal, because every lockfile already written still names it — and `supersedes`
carries the edge that makes rollback possible rather than archaeological.

TWO GUARD-RAILS CAUGHT THAT I HAD SHIPPED A MODULE NOTHING CALLS, and they were
right. Both functions are declared HOST_API with the reason, on the precedent
`interceptors.guard` sets one entry down: nothing in `src/` calls them because
nothing in `src/` is a host, and both are things a host puts in front of a
person. The residual is written into `docs/41` §0.1 rather than hidden in a
register — `Learner` has no tool-proposal shape, so nothing in this port yet
ROUTES a self-authored tool to this review.

Suite: 3098 green (1062 Rust + 2036 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…igure

Two audit findings, closed with tests.

**`x-` blocks were being read and rewritten.** AD-14 makes `x-` the one extension
point that survives: preserved in the document, never emitted into any substrate,
and — the half that broke — not INTERPRETED on the way through. Templates scan
every string in a document looking for holes, so an author's reserved space was
scanned like any other: a block carrying `<ticket-id>` was refused as an
undeclared hole, and one carrying a name the pattern declared was substituted
away. The whole point of the reserved space is that PACT does not know what is in
it, so it may not decide that something in it was a hole. Skipped in all four
places that walk a document — including the top-level loop in
`holes_match_declarations`, which hands values to the scanner directly and so did
not inherit the skip one level down.

**A pattern could not take a list.** Arguments were scalars only, so `uses:`,
`may-use:`, `when:` and `may:` could be parameterised one entry at a time and
never as a whole — while a shared FIGURE may already be a list, which is the
inconsistency that gave it away. The rule was never "scalars": it is that an
argument is a VALUE and not a block of settings, so a caller cannot manufacture
fields the classifier has not seen (LOAD-13). A list of scalars manufactures
nothing. A list holding a block is still refused, and that has its own test, so
the widening is exactly as wide as the argument for it.

Suite: 3101 green (1065 Rust + 2036 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P2's own exit gate, and the last audit finding with substance.

`values:` and a pattern are both resolved and REMOVED. That is deliberate and
load-bearing: it is what makes a tree using them the same document as one written
longhand, so the digest of one means something about the other and nothing below
the loader — no adapter, no second port, no runtime — learns the features exist.

The cost is that afterwards nothing says which lines an author typed and which
arrived. A reviewer reading `pact show` cannot tell a spend cap somebody wrote
from one three desks share, and a figure changed in one file moves values in
documents that never mention it.

It cannot be recomputed from the finished document, because by then the reference
is gone — so it is recorded AS IT HAPPENS and carried on the report `pact waits`
already emits. Each entry says what kind it was, which figure or pattern, and
which file it landed in: the use site's file, because that is what a reviewer
would open, and the definition is one lookup away and the same for every use.

On the REPORT and not in a diagnostic, deliberately. This is not a problem, and
printing a line on every `check` for every figure a tree uses would be noise on
the command an author runs most. An empty list rather than an absent key, because
"nothing was substituted" and "this build does not record substitutions" are
different answers and a consumer must be able to tell them apart — there is a
test for the empty case for exactly that reason.

Threading it out of `validate` widened one signature and touched six call sites,
which is the honest cost of the report being built by the caller rather than
inside the pass. Two mistakes on the way, both caught in seconds: a replace hit
`load`'s signature instead of `validate`'s, and the first version used
`std::mem::take` to move the list onto the internal report — which left the
caller with an empty one and nothing saying why. It clones now, and the comment
says which two readers want it.

Suite: 3103 green (1067 Rust + 2036 adapter), 8 skipped, clippy clean in debug
and release, TypeScript type-checked, fifteen shipped trees clean under
--deny-warnings, fully offline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The audit's one real regression, and it was mine.

`body` returned the moment a rewriting sentence had run. Every masking rule in
the same interceptor document sat below that return, so a document that says
"hide anything that looks like a card number" and "say it in house style" did the
second and silently skipped the first. Measured, on the commonest real shape
there is: a desk that must never print a card number printed one.

That is the attack §8.5 says a rewrite cannot mount. The claim was true of the
sentence and false of the machine underneath it, which is the worse way round —
an author who reads two rules and gets one is worse off than an author whose
rewrite was refused outright, because nothing tells them.

So the rewrite falls through instead of returning. Order is rewrite THEN hide,
and it is that way round on purpose: the hiders have to see the words that will
actually be said, including any a rewriter introduced — the second test writes a
program that puts a card number INTO the answer, and it is masked. The reported
power is `hide-values` when both fired, because that is the one a reviewer cares
the chain still had; the rewrite is visible in the words themselves.

The whole line is no longer uppercase in the first test, and that is the correct
result rather than a weakened assertion: `[removed]` is written by the hider,
which runs second, in its own words and not the rewriter's.

README's count moves 3103 -> 3105 for the two new tests.
…claims

P6 watched one door and P8 cut six more.

`programs.rs` asked two questions of a program behind a tool — is there a locked
room, and can that room run this kind of program. Then `uses:`, `projects-with:`,
`checked-by:`, `decided-by:`, a metric's `program:` address and a rewriting
interceptor sentence all reached the same carried body, and only the first two
inherited anything. Two promises were left written and unkept.

PURITY. The schema says of `decided-by:` that the program it names is "refused
unless it is `pure`", and says the same of the two rewriting sentences — where it
is the entire argument for giving `change-the-answer` back to authors after R24
took it away. Neither was enforced. A `nondeterministic` router loaded cleanly,
which is a run whose path is not the same twice for a reason no line records; a
`nondeterministic` rewriter loaded cleanly, and with it §8.5's argument was
untrue of the file that had just been accepted.

`checked-by:` and a metric's grader are deliberately NOT held to it, and that is
a decision rather than an omission: neither line claims it and neither wants it.
An account number held against a ledger is the shape `checked-by:`'s own help
describes. A restriction the document never asked for costs a capability and
buys nobody anything.

THE ENGINE. `engines:` says it "is checked in both directions" and promises the
refusal arrives "when the file is read, rather than on the first call". Measured:
a workspace whose only room runs `wasm`, carrying a `python` program its agent
named directly, printed "loaded cleanly" — this file's own failure, through a
door it did not watch. Now every reach without a tool is asked the same question
against the whole workspace, since a `decided-by:` line has no `connect:` to
follow. A workspace declaring NO room stays silent on purpose: that is P7's seam,
and `a-desk-that-uses-a-program` is the shape that depends on it.

The reaches are found by walking the specification's own `names: programs`
declarations, so the next field that reaches a program joins by existing — the
rule `currency.rs`, `available.rs` and `handover.rs` are written to, and the one
this file had broken.

AND THE NAME INSIDE A SENTENCE. `unnamed.rs` asked "does any line name this?" by
looking at whole string values, so `replace the answer with what house-style
returns` never registered the name in the middle of it: the ONE authoring path
§8.5 gives for `change-the-answer` drew a warning saying its program "never takes
effect", about a reference the resolver follows and refuses when it is absent.
Two checks disagreed about one line, and under this repository's own
`--deny-warnings` gate no shipped tree could have demonstrated §8.5's path.

Not repaired by splitting every string on whitespace — that would let any word of
any `description:` count as a mention and silence the check wherever a name
appears in prose. Repaired by reading the same holes the resolver reads, from one
parse: `Schema::names_in_sentences` is written beside the vocabulary it reads and
both checks call it.

New fixture `tests/trees/a-desk-whose-program-decides/` reaches one workspace's
programs three ways at once — a tool's action, a `decided-by:` line and a
rewriting rule — so the positive control is a tree an author could have written.

README's count moves 3105 -> 3113.
…es not

`allow-egress:` gained `programs` in f1fbdc8, and that commit claimed "a carried
program has no network unless that line says so, which is the state a reviewer
should be able to assume by reading nothing." Nothing established that state. No
check read the word, no run reported it, and `ProgramSpec` — the only thing a
host is ever handed about a program — did not carry it. Measured: the same tree
under `allow-egress: []` and `allow-egress: [programs]` differed in the settings
COUNT and in nothing else.

That is §8.5's own rule landing on a word added after it was written: a choice a
non-coder can type and nothing can exercise reads as a capability, which is worse
than an absent one.

WHAT WAS NOT BUILT, AND WHY. The plan (docs/41 P6) named a refusal — a reaching
program under an empty egress list. Nothing in the tree says a program reaches
outward. `nothing_reaches_outside_the_box` works because `reaches-outside:` is
DATA in the schema, and the three fields carrying it are on tools and models, not
on `program`; the body is never opened (R5); `determinism:` is no proxy, since
`deterministic` covers reading a pinned local table. The check would have to
guess, which R25 and R28 refuse — and refusing on a guess would make `programs`
mandatory boilerplate on every workspace carrying a pure calculation, which is
the over-grant `egress.rs` exists to remove. The plan row is struck through and
the reason recorded, per §8.3: a withdrawal is written down, never deleted.

WHAT WAS BUILT. Honest delegation, with the datum actually delivered.
`ProgramSpec.may_reach_outside` carries the author's own answer to whatever
supplies the runner — on the program, not beside it, because a host starting one
body holds that body's spec and should not have to go and find the workspace. A
run that withheld the grant and CAN start a program says so once on `unenforced`:
PACT never opens a carried body and never watches the room, so it is trusting
somebody else to hold that door, and R30's rule is that such a promise is said
out loud rather than assumed.

AND THE GUARD THAT WOULD HAVE CAUGHT IT. The Python one passes by accident: it
greps the shipping corpus for `"programs"` and every occurrence is a lookup of
the `programs:` COLLECTION, so the word collides with a collection name and can
never be reported orphaned. `every_part_the_boundary_offers_has_something_that_
reads_it` asks the question in the port that owns the boundary, where no such
collision exists: every word of `allow-egress:` is a model role read by
`egress.rs::plays`, or has a row naming what reads it — or, for a word somebody
else honours, saying so and where the answer reaches them. Mutation run: delete
the `programs` row, red, naming the word.

One existing assertion was tightened rather than weakened. `a_run_with_a_runner_
says_nothing_about_it` looked for the program's NAME, which stopped meaning what
it said once a second, unrelated sentence about the same program existed. It now
asserts the wording that must be gone, and carries the positive control that
takes the runner away and watches it come back.

`egress.py` had also never learned about the eighth word: its header still
described the list as six model roles plus `tools`.

README's count moves 3113 -> 3118.
…costs

R5's promise is that `pact check` never runs an author's code, so reading a
workspace a stranger sent you is safe. Safe is two promises: the reading must not
RUN anything, and it must END. The second had two holes.

A `.pactignore` THAT IS NOT A FILE. `Ignore::load` called `read_to_string` on
`<dir>/.pactignore` with no check of what that name is, at every directory from
the root down. `mkfifo .pactignore` in a workspace made `check`, `show`, `waits`,
`discover` and `card` all block for ever, with no output at all — a pipe with no
writer never returns. The loader already knows this hazard: a pipe, socket or
device found by either walk raises `loader/not-a-regular-file`. This file is
opened before either walk can see it.

And `.pactignore -> /etc/passwd` loaded cleanly, with that file's lines becoming
this tree's ignore patterns and its text quoted back in a diagnostic —
`Ignore::inherited` already says nothing above the tree may reach into it, and a
shortcut walked round the rule. Now `symlink_metadata` asks about the link
itself, and only a regular file is opened. A skipped one leaves no patterns,
which is the same state as no file at all, so entries it would have hidden are
reported rather than silently dropped — and it is said out loud, in the same
words the walks already use, rather than read as an empty file.

Pre-existing rather than introduced here; fixed because it is the sharpest hole
in the reader and it matters most for `pact discover`, which is specified to be
run over trees its operator did not write.

A PAYLOAD FILE WITH NO CEILING — and this one is P3's. Before payload digests the
walk only asked the filesystem for each entry's size: the cost was the NUMBER of
files. Fingerprinting reads every byte, and nothing bounded it. `MAX_LOAD_TEXT`
is charged from `load_file`, and neither fingerprint call passes through there;
`read_file`'s opaque arm returns before the size guard below it. A tree states a
size independently of what it occupies, so a 48 KiB directory could cost a
reviewer minutes: measured on `examples/answers-from-documents` with one sparse
file planted in it, release build, 0.006 s before, 1.05 s at 512 MB, 8.65 s at
4 GiB, the tree 48 KiB on disk throughout.

A file over the ceiling is carried by name and size with no fingerprint, and a
warning names it. That is the honest answer the field already documents — "a
made-up digest would be worse than none, because the whole value of the field is
that it can be compared" — and it is a warning rather than a refusal because
nothing is wrong with the tree; it just cannot be pinned by content.

`load_file`'s docstring said an attachment folder "carries file *names*, never
contents", which was true when written and false from the day digests landed. It
now points at the ceiling that replaced the claim.

README's count moves 3118 -> 3123.
P8 wave 2 said "memory becomes readable and writable from the format" and shipped
a checker. Measured on its own fixture: `bind: remembers.verified-account` filled
NOTHING — `_bound_args` stripped only the `run-inputs.` prefix, so it looked up
the literal key `"remembers.verified-account"`, found nothing, and called the tool
without the account. That is the field whose help says "this is how 'whose order'
stops being something the model decides". `remember-as:` was read by no runtime at
all, so `never-from:` guarded a write that never happened. And the sentence
reporting the miss said `run-inputs.remembers.verified-account` — one namespace
glued onto the other, the same defect the checker's own diagnostic goes out of
its way to avoid, on the other side of the same field.

Three things had to be true and none was.

THE STORE HELD ONLY PINNED FACTS. `Facts.from_document` kept entries writing
`survives-shortening: yes` and said anything else "is none of this module's
business" — true while this was only about a summary, false the moment two fields
existed that read and write the agent's memory. Every declared entry is held now,
and the flag decides only what a shortening RE-STATES, which is what it always
meant. `Fact` built directly still defaults to surviving, because every such
construction in this repository is somebody writing out a pinned fact.

NOTHING SEEDED IT. `lasts: one-conversation` outlives a single `run()`, so where
that memory is KEPT is the surrounding system's — the same split as the model and
the tools. It arrives as `remembered=`, registered in `SUPPLIED_BY_THE_HOST`, and
an undeclared name is dropped on the way in exactly as `record` drops one on the
way out.

THE WRITE HAD NOWHERE SAFE TO GO. `remember-as:` is applied after the interceptor
chain, deliberately: a tool result goes into `history` and is read back next
turn, and `context-policy` re-states facts into later prompts — so a fact written
before the chain would be the one copy nothing masked, and the memory would put
back exactly what the redaction took out. There is a test that plants a card
number in a tool's answer and reads the memory.

AND ONE MORE THING THE TESTS FOUND. A bound argument was in the dict the model is
shown, while `bind:`'s own help says the model "cannot see them, name them, or
change them". Offered-and-then-overwritten is the worst of the three possible
behaviours: the model spends a decision on a value that is discarded, and nobody
reading the trace can tell which value the tool got. Left out only when EVERY
action taking that argument binds it, since one action binding it and another
taking it from the model is a real thing to write.

Two existing tests were moved to the property that survived, not weakened. Both
asserted that a `remembers:` entry without `survives-shortening:` is not READ —
a premise that is now false — and both now assert what the flag really decides:
the entry is held, and it is never re-stated. Each gained the positive control
beside it.

The first Python test to drive `tests/trees/a-desk-that-remembers` at all; until
now only the Rust CLI test touched it.

README's count moves 3123 -> 3130.
…roviders ship

Two small honesty repairs, both of them a written claim the code did not keep.

THE PROVENANCE HALF THAT WAS NEVER WRITTEN. `Substitution.kind` has said
"`figure` or `pattern`" since P2 and only `figure` was ever emitted. So a
reviewer reading `pact waits` over a tree where every desk came out of one
pattern was told nothing at all — on the feature whose entire selling point is
that several documents share one shape, and where the answer matters more than it
does for a figure, because changing a pattern changes every document built from
it. It is recorded where it happens, in `derive_one`, for the same reason the
figure half is: a pattern is resolved and REMOVED, so the finished document
cannot be asked afterwards.

THE THIRD PROVIDER. `metric.uri:`'s help says "Two providers ship" and names
`deepeval:` and `pact:`. P8 added a third — `program:`, which names one of this
workspace's own carried programs — and the sentence an author reads never
learned about it. That is the one place somebody looking for "how do I write a
bar only I can define" would look.

README's count moves 3130 -> 3131.
§8.3a's CLASS-3 floor under a normative clause was written and never built. It
exists because of one observation: the surface annotation was attached to the
FIELD, and that one field holds both explanatory prose and the rules the model
treats as authority — "an identical sentence in `policies/approvals.yaml` is
`S-EXEC`/CLASS-4; in a `SKILL.md` body it was CLASS-1."

Two of the three directions were already answered, and by accident. Removing a
clause is HIGH because ESC-SHRINK's list trigger fires on a removed list item.
Editing one is HIGH because the old line is a removed list item, and usually
because the prose stems catch it as well. Nothing watched the third, and it is
the one that matters most. Measured on the flagship's own refund policy, with a
tree `pact check --deny-warnings` accepts:

  a sixth numbered clause under `## Rules` -> risk=low, needs_a_person=False,
  applied, and spliced into the system message with nobody reading it.

While the schema's own `tier: core` help for `may-improve-on-its-own:` promises
the opposite in as many words: "Written rules are not here at all: changing one
always needs a person, which is what `needs-a-person-to-approve:` below is for."

AND THE HEADING IS A GOVERNANCE SURFACE. Rule 4 says the author moves the
boundary by editing a heading — so a cycle that can rename `## Rules` to
`## Working guidance` moves every clause out of the zone in one LOW edit, and
every edit after it is outside the closed set. That rename classified LOW too.

The floor is asked BEFORE the author's own permissions and is never keyed to
anybody having written the word `policy-clauses`. `needs-a-person-to-approve:`
carries no `required:`, so a workspace granting `skill-notes` and omitting it
would otherwise own a body with no floor at all — and rule 2 calls the permission
CLASS-4 by construction, which is a property of the clause, not of memory.

WHAT WAS DELIBERATELY NOT DONE. `policy-clauses` is not mapped onto `content` in
`SAFE_TO_CHANGE`. `Permissions.of` tests `field in self.high` first, and the
flagship lists `policy-clauses` under `needs-a-person-to-approve:` — so that
mapping would make every skill-body edit need a person, `skill-notes` a grant no
cycle could ever act on, and §8.3a rule 3 broken in the course of enforcing rule
2. It names a surface INSIDE a document, which is what §8.3a is about, so only
the clause check consults it. Two control tests hold that line: ordinary prose
under a rules heading, and a list under `## Examples`, both still LOW.

Rule 4's per-skill clause counts on `pact check` stay out of this change.
`docs/20` already records that half as open work in another crate, and bundling
it would hide a one-guard classifier fix inside a CLI feature.

README's count moves 3131 -> 3141.
…ing else

Two things a `does: run-code` stage did that nothing else in this harness does.

IT LEFT BY A DOOR NO RULE WATCHED. The snippet the model wrote went straight into
`history`, and so did what the locked room printed. The tool path already makes
the argument in its own comment — a result read back to the model next turn
"leaves by the same door as anything else" — and `redaction.yaml`'s promise is
"what must never leave this workspace". A card number in a comment in the working
was the same leak by a shorter route, and a card number the room printed was the
one result in this harness no rule ever saw.

The snippet meets the chain at `step.message.after`, which is where a stage's
prose meets it, because that is what it is: words the model produced. The room's
output meets it at `step.tool.completed`, which is where a tool result meets it,
because it is metered as one and appended as one.

What CANNOT happen there is recorded in the test rather than papered over:
`stop and say "..."` is offered at `step.message.after` and `turn.message.after`
and nowhere else, so a rule stops a run on what the model wrote and deliberately
not on what the room printed. That is the schema's boundary, held where an author
reads it.

AND IT LOADED IN A WORKSPACE WITH NOWHERE TO RUN IT. `docs/27` says a `run-code`
stage is "legal only when the agent's workspace declares a sandbox resource", and
the adapter test's own header says it is "refused before anything runs, rather
than discovered on the first step". Nothing in the loader had ever heard of
`run-code`: `grep -rn run-code crates/` found nothing, and the only thing holding
the rule was a halt at run time — a true sentence arriving after the author has
gone and while a customer is waiting.

This is the ONE reach where a missing room is refused rather than passed over,
and the difference is what the line names. Every other reach names a program the
AUTHOR wrote, and P7's seam says its room may come from the host —
`a-desk-that-uses-a-program` depends on exactly that. A `run-code` stage names
nothing: no program document, no engine, no fuel, nothing for a host to match a
room against. With no room in the tree it is a stage that can only ever halt.

README's count moves 3141 -> 3147.
P3's plan named `an_ignore_rule_is_part_of_the_document` and proposed lifting
`.pactignore` into the canonical document, on the theory that a rule could
otherwise take a carried body out of a workspace with nothing moving. Nothing of
the kind was built, and nothing should be.

A rule that takes effect takes a FILE out of the payload, and the payload is what
the digest is over — so ignoring a body moves the workspace digest because the
tree really is different, which is the honest reason for a digest to move. A note
names the file, the rule and the line to delete to bring it back. Measured both
ways: `check_window.py` ignored moves the digest and prints the note; a line
naming a file that is not there moves nothing.

Lifting the file in would have broken the second half. Two trees that behave
identically would digest differently the moment one carried a line matching
nothing — a digest reporting a change nobody made.

Both properties were measured when P3 landed and neither was written down, which
is how a plan row comes to name a test nobody can find. They are pinned now, and
the row is struck through with its reason rather than deleted.

README's count moves 3147 -> 3149.
`workspace.programs`' own comment states the D14 bargain: "no core capability may
REQUIRE a program, a workspace that carries one simply does not earn the
`no-code` badge, and deleting `programs/` must leave a working agent."

The badge is not a computed artifact and does not need to be. What makes the
sentence true is the TIER: every door into a carried program is `tier: expert`,
so the author D13 describes — who builds everything in YAML and Markdown — never
meets one, and every capability a program reaches has a plain-words way to do the
same thing written beside it. `checked-by:`'s help ends "leave it out and the
answer only has to be the right kind of thing"; `decided-by:`'s points back at
the three outcomes; `projects-with:`'s at `shorten-long-results`.

That tier is what nothing was checking. `governance_is_complete` asks that every
field HAS a tier and never which tier these have, so the whole bargain rested on
nobody editing a word. It is held now for the door, for the room a program runs
in, and for every field a program itself is made of — `description:` excepted,
which is core in every group in the specification and is not a way in.

Mutation run: `workspace.programs` set to `tier: core`, red, naming the field;
restored, green.
…as hidden

1948a9f closed a real leak and opened a worse one. Routing the snippet through
the interceptor chain stopped a card number typed into a comment reaching
`history` unmasked — and then handed the room the masked text, so a hiding rule
rewrote the code that ran.

The card pattern deliberately over-matches, which is right for prose and
destructive for code. Measured: `order_id = 9780306406157` became
`order_id = [removed]` and the room raised `NameError`. Under the SHIPPED
`examples/refund-desk/redaction.yaml` it is worse, because the bank-account
pattern contains `\b\d{8}\b` — so any eight-digit literal, an order id or a
date-as-int, was destroyed. And it does not always fail loudly:
`print(len("9780306406157"))` becomes `print(len("[removed]"))`, the step result
is `9`, and the model answers from it. A control quietly doing something other
than what it says is the shape T7 forbids.

This file already draws the line, two hundred lines away, about a tool's
arguments: "Rewriting `call` decides what is WRITTEN DOWN, never what runs." So
the transcript gets the masked words — nothing unmasked is recorded, and the
room's OUTPUT is still masked at `step.tool.completed`, which is the half that is
genuinely a leak channel — and the room gets what the model wrote.

WITH ONE EXCEPTION, and it is the reason `allow-egress: programs` now means
something. Handing the room verbatim text is sound only while the room is inside
the boundary. Where the author granted a carried body the outside world, a
verbatim snippet is a real way out, so the masked form is what runs. A
`run-code` stage names no program and so has no `ProgramSpec` to read that word
off; `AgentSpec.programs_may_reach_outside` carries the same answer as a fact
about the workspace, read once in one place.

Either way the run says so. A transcript that shows something other than what ran
is exactly what has to be said out loud, and it was silent on every mangled run.

The boundary register gains the new field with its reason: `False` here is the
authored answer — a workspace whose `allow-egress:` does not name `programs` has
said the door is shut — not a boundary dropping something.

Found by an adversarial audit of the ten repair commits, reproduced end to end
against a room that really executes what it is handed.

README's count moves 3150 -> 3154.
f225aa4 asked, of every program reached without a tool: does some declared
sandbox host its engine? The trigger was "the workspace declares at least one
room". That reads one declared room as a claim about all of them, and no line in
any tree ever says these are all the rooms there are. The tell is that the same
tree with NO rooms was accepted in silence — a workspace could not be wrong until
it declared something, and then it was wrong about things it had not mentioned.

Measured on the shipped tree this module's own header cites. Take
`a-desk-that-uses-a-program` — a `wasm` program named straight from `uses:`, no
`resources/` at all, clean — and add one self-contained `python` capability
beside it, with its own room, its own program and its own tool. Nothing about the
`wasm` arrangement changes, and it was refused.

Every way out was worse than the problem. Writing `wasm` into the python room's
`engines:` passes and is a false statement about a room the tree does not own —
the T7 shape exactly. Adding a real `wasm` room clears the error and draws
`nothing-points-at-it`, because a `uses:`-reached program has no `connect:` to
write, so `--deny-warnings` still fails. And two commits in this range collided
head-on: a `run-code` stage is refused with a fix saying "add a sandbox", and
obeying that fix refused the `uses:` program beside it. That tree had no clean
state at all.

So the workspace-wide form is withdrawn, and the withdrawal is written where the
code was rather than deleted. What survives is the question a tree can actually
answer, and it is the one P6 always asked: a program reached THROUGH A TOOL is
held against the room that tool `connect:`s to, in both directions, because that
pairing is written down. Where no pairing exists the room comes from the host —
P7's seam — whether the tree declares nought rooms or nine.

AND THE FIX THAT DREW A WARNING IS A FIX AGAIN. A `does: run-code` stage names no
tool, and a program reached by `uses:`, `decided-by:`, `checked-by:` or a
rewriting sentence names none either; all of them need a room and none can write
the `connect:` line `nothing-points-at-it` asks for. A sandbox is excused in a
workspace that really has such a need — narrowly: only `resource-kind: sandbox`,
and only there. Every other connected system, and every sandbox in a tree whose
programs all go through tools, is warned about exactly as before.

`engines:`' own sentence is narrowed to say what is really checked, rather than
staying a promise about every reach that only one reach keeps.

Two tests are replaced rather than weakened: both asserted the rule being
withdrawn, and both are now the property that survived — the tool pairing still
refuses, and a room declared for one purpose does not retract the seam for
another. The collision case is pinned so it cannot come back.

Found by an adversarial audit of the repair commits, reproduced against a binary
built from the pre-repair commit for the before and after.
The §8.3a clause floor read a fenced code block as prose, and that broke it in
both directions at once — on the same sample.

A `#` is a comment in half the languages an author is likely to paste, so
`# Rules` inside a fence read as a HEADING. Everything after the sample fell out
of the normative region, and a real rule written there got no floor at all. That
is the dangerous direction: the floor silently stopped covering the part of the
policy that comes after an example.

And a hyphen starts a list in YAML, so a line inside the sample read as a WRITTEN
RULE. Fixing a typo in an example needed a person — the over-restriction that
costs an author a capability and buys nobody anything.

One repair for both, because they are one mistake: fences are taken out before
anything is read, so a sample is invisible to the heading walk and the clause
walk alike. That is the only way the two stay consistent about where an example
begins and ends. Both spellings markdown offers are handled.

Four tests, two of which were already green and are pinned as controls: a rule
after a sample still needs a person, and a line inside one still does not.

Found by an adversarial audit of the repair commits; three of the four lenses
reported it independently, from opposite ends.

README's count moves 3154 -> 3158.
`metric.uri:` is written as who provides the score, a colon, then what it
measures, and `program:<name>` is the third provider — added in P8 so an author
can define a bar only they can define, and documented in the schema in this same
range.

The reachability register walks whole string values and their `/`-separated
parts, so `program:house-style` registered the whole address and never the name
inside it. Measured: a suite grading every case with `uri: program:house-style`
was told that program "never takes effect — the file loads, and no run ever reads
it", and `--deny-warnings` exited 1. The reference is real, and the offered fix
named a different wiring entirely.

The same shape as the rewriting sentence one door over, and the same repair in
spirit: an address that HAS structure is decomposed, exactly as `zendesk/reply`
already is. Not every string split on every space — that would let any word of
any `description:` silence a check whose whole job is to notice a document
nothing reaches.

Found by an adversarial audit of the repair commits; two lenses reported it.

README's count moves 3158 -> 3159.
…nnot

Every test of the rewriting sentence built a `Chain` by hand and handed it a
runner. That is the right way to test a chain and the wrong way to believe a
claim about a RUN. Through the shipped entry point the whole path was inert:
`AgentSpec.from_document` builds the chain from a loaded document (P-1) and has
no host to ask for a runner, and `run()` passed its runner to loops, projections
and code stages and never to the chain.

Measured: `run(..., run_program=shout)` over a document whose one rule says
"replace the answer with what house-style returns" came back with the words
unchanged and `RunResult.unenforced` empty — while the chain object, which a host
never holds, carried the sentence explaining why. So the one authoring path §8.5
gave back to authors did nothing, and the run was silent about it. Pre-existing
wiring; the claims resting on it are recent, including f225aa4's fixture
described as reaching one workspace's programs "three ways at once", which at run
time reached two.

THE RUNNER NOW ARRIVES WHEN IT EXISTS. A `Runner` box is built with the chain and
read at call time, so `chain.use_runner(...)` at the start of a run reaches every
rule in it. A box rather than a rebuilt chain, because rebuilding needs the
document the spec no longer carries, and because this way one chain has one
runner by construction.

AND WHAT THE CHAIN COULD NOT DO IS PART OF THE RUN'S REPORT. `unenforced` is now
read rather than stored, merging the chain's own live list. The chain records its
sentences WHILE the run is going, at whichever moment a rule fires, and this run
has a dozen ways to end — so any single copying point would catch some and miss
the rest. Deduplicated, because one chain is shared across steps and a rule that
could not fire three times is one thing to fix.

The channel census learned to count a property as well as a field, and says why:
asking only for fields would have called this channel missing at the moment it
started working better.

Found by an adversarial audit of the repair commits.

README's count moves 3159 -> 3162.
…iple

Every test of `checked-by:` called `Question.validate` directly and handed it a
runner. Through the door a run actually uses it was unreachable: `rulings.
answer_to` is the only caller in this port and never passed one, so a question
carrying `checked-by:` rejected EVERY answer with "nothing here can run a carried
program — so this answer could not be checked and has not been accepted."

That sentence is untrue on a run whose host supplied a runner, and it is told to
a person standing there with a correct answer, at the one moment the field's own
help says a correction is free. A check that refuses everything is worse than no
check at all: the author wrote a ceiling and got a wall, and the reason names a
limitation that does not apply.

The runner is threaded to the four places a wait is read — the approval ruling,
the context-too-long ruling, the per-call ruling and the refusal renderer — so
what a person typed is checked wherever it is read. Where there really is nothing
to run it, the old sentence is still what comes back, and a test pins that too.

Found by an adversarial audit of the repair commits. Pre-existing, and it is the
premise f225aa4's own header argued from, so it does not get to stay broken while
the checker beside it grows.

README's count moves 3162 -> 3165.
Two repairs to the same shape, found by an adversarial audit of the repair
commits.

A RUN DID NOT KNOW ABOUT HALF ITS PROGRAMS. `ProgramSpec` is the only thing a
host is ever handed about a carried program, and it was built from two doors: a
tool's action, and `uses:`. A program reached by `decided-by:` or `checked-by:`
produced no spec at all — so a host with no runner was never told it could not
route the loop, and the `allow-egress:` answer P8 made real never reached the
room that would run it. Both doors are walked now, and each says which line
reached it so an author knows where to look.

ONE MISTAKE, ONE SENTENCE. The ignore list is INHERITED, so every directory from
the root down asks for the same `.pactignore` and a skipped one is found again at
each level, by both walks. The comment that shipped with that repair said
`Diagnostics` folds identical entries; it does not. Measured on a six-directory
tree: NINE copies of one sentence about one file — one thing to fix rendered as a
wall, on a diagnostic whose whole job is to be noticed. Deduplicated against the
report being written rather than by threading a seen-set through two unrelated
walks.

README's count moves 3165 -> 3168.
A batch where one call is gated carries out the ungated calls first and puts
their answers aside under `already`; the resumed run replays them from there,
appending the output and moving on. Two things sat below that replay and neither
happened to it.

`remember-as:` never wrote, so a gated batch lost the fact the author declared —
the memory works on an ordinary run and silently does not on the one where a
person was asked, which is the run where losing it matters most.

And the interceptor chain never saw the result, so a redaction rule did not apply
to a value that goes into `history` and is read back to the model exactly like
any other. `redaction.yaml`'s promise is "what must never leave this workspace",
and a card number a tool handed back before the run parked left by a door no rule
watched.

The same shape as the code stage one file over. Both are results reaching the
model on a path the rules were never wired into, and both are one line below a
`continue`.

The test drives a real park and resume — two calls in one batch, one of them
gated — and it was confirmed against the unpatched branch: red without the fix,
green with it.

README's count moves 3168 -> 3169.
…ma reads it

`uses:` names four collections — tools, skills, knowledge, programs — and
resolution takes the first that has the key. The reach walk asked only whether
`programs:` had it, so in a workspace holding both `tools: {tidy}` and
`programs: {tidy}`, `uses: tidy` counted as a program reach when it is a tool.

Harmless in itself and wrong in a way nothing would have shown: the only reader
is the rule that excuses a locked room from `nothing-points-at-it`, so the
symptom is a warning that quietly does not appear. A check whose failure mode is
silence is exactly the kind that has to be right without being watched.

The order comes from the schema's own `names:` list rather than a rule written
here, so a field that adds a fifth collection is read the same way by both.
The repair audit's second half — findings whose skeptics died on a usage limit,
so they were never judged. I checked each by measurement instead of counting
them refuted.

A SHORTCUT INSIDE THE TREE IS FOLLOWED AGAIN. Stopping `.pactignore ->
/etc/passwd` refused EVERY link, so a workspace whose ignore list is a shortcut
to a shared file one folder along silently stopped ignoring anything — and was
told "a shortcut to something that is not a file", which is untrue of a link
pointing straight at a file. The rule `Ignore::inherited` states is that nothing
ABOVE the tree may reach into it, so that is the rule: a link landing on a
regular file inside the tree is read, and one that leaves is refused.

THE SECOND PORT TOLD A LIE ABOUT THE SPECIFICATION. A `does: run-code` stage made
the TypeScript port throw "which is not something a stage can do" — false;
`run-code` is in `phase.does`'s closed list. Refusing to start is right and
matches the reference port, which halts rather than quietly turning the stage
into a `think`. What had to be true is the reason, from the port whose whole
discipline is being able to say exactly what it is smaller by.

A TEST THAT CLAIMED MORE THAN IT CHECKED. It was called "every door into a
carried program is priced as an expert one" and checks the collection, the room
and a program's own fields — not `agent.uses:` or `action.program:`, which are
`tier: core` and MUST be: they name tools first, and pricing the ordinary act of
using a capability as expert work is not what D14 asks for. It now claims what it
checks, and says why the other half is right as it is.

AND THREE DOCUMENTS THAT WENT STALE UNDER THE FEATURE. `interceptor.rules`' help
said "six sentences exist and they are the only ones" — there are eight, and the
two it never learned about are the rewriting pair that is the whole of what §8.5
gave back; they are named in the help now. `docs/70` said "CodeAct is deliberately
absent and that is unchanged" in three places while `does: run-code` shipped, and
`docs/30-FRD` still marked FR-6.1.5 unbuilt. The refusal was ANSWERED rather than
overruled and the correction says how: R42 is about a spec naming a script for
PACT to run, and a `run-code` stage names no script — the model writes it, a
locked room the host supplies runs it, `pact check` decides nothing about safety,
and the snippet holds no structural authority, so `meta-depth = 1` and R16 are
untouched.

README's count moves 3169 -> 3170.
The first repair put rewriting before hiding inside ONE document's body, and did
nothing about the commoner shape: the hiding is `redaction.yaml`, the rewrite is
an interceptor, and they are separate rules in the chain.

The floor runs FIRST and has to — `_hiding_runs_first` refuses any other order,
because a guard that reads a value must never see an unmasked one. So a rewriter
running after it introduced a card number that nothing masked. Measured on two
files an author would ordinarily write, the whole number came out: the exact
attack §8.5 says a rewrite cannot mount, through the door the first repair did
not close.

The floor is applied once more, and only when the words really changed under it,
so a chain that hides and does nothing else is byte-identical to what it was.
`redaction.yaml`'s promise is "what must never leave this workspace", and a floor
the last rule can step over is not one.

Found by an adversarial audit of the repair commits, in the half whose skeptics
died on a usage limit — checked by measurement rather than counted refuted.

README's count moves 3170 -> 3172.
`if the answer mentions "<word>", stop and say "<why>"` is the one sentence in
the vocabulary whose condition reads what the agent SAID. It ran with the other
guards, before any rewriting — so a rewriting rule in the same document could
introduce the very word it forbids and walk straight past it. Measured: a program
returning "this is a diagnosis" beside `if the answer mentions "diagnos", stop and
say "not here"` produced no stop at all.

It now runs after any rewriting and before the hiders: after, because that is
what makes it true of what will be said; before, because a rule that stops on a
word must see the word rather than `[removed]`.

The counting guards stay where they were and must. They are about how many times
a TOOL was called, re-running one would count the same call twice, and a rewrite
does not change a call count. That is the whole reason this is a split rather
than a second pass over everything.

Third of the same family: rewrite-then-hide inside one document, the workspace
floor last across documents, and now the word-reading guard. All three are the
same mistake — a rule deciding on words that a later rule changed.

README's count moves 3172 -> 3173.
The ceiling on fingerprinting was per FILE, and the budget it says it mirrors is
a running total: `loaded_text` accumulates across the whole load and
`MAX_LOAD_TEXT` is compared against the sum. So a thousand files of 64 MB each
was 64 GB of reading with nothing to stop it — the same hole the per-file ceiling
was written to close, one level up from where it was closed.

Payload bytes are charged against a total now. Five 24 MB files, none of them
near the ceiling on its own, stop being read once the load has spent what a
reader will spend; every one is still carried by name and size.

And the sentence says WHICH ceiling was reached. It said "more than the 64 MB
this reader will spend on one file" either way, so a 39-byte file skipped because
the budget was already gone was reported as "0 MB … more than 64 MB", which is
nonsense in front of an author. Two readings, two sentences.

Three stale statements corrected in the same pass, each measured first:
`_compile_rules`' comment said a rewrite is applied AFTER the hiders, which was
true when it was written and has been the opposite since; docs/41 said "four
built, four to go" of P8's eight waves while its own table said all eight; and a
`bind:` reported as unfilled when the fact IS held turned out not to reproduce —
that one is refuted, not fixed.

README's count moves 3173 -> 3174.
The `allow-egress: programs` report spoke only when the grant was WITHHELD. So
the safer arrangement was the noisy one, and granting a carried body the outside
world said nothing at all — the signal inverted, on the one line of this a
reviewer most wants to see. A grant is not the absence of a refusal.

There is a line either way now, and each says what is true of that arrangement:
what PACT cannot check when the door is shut, and what has been allowed when it
is open. Either way it says PACT never opens a carried body and never watches the
room, because that is the part somebody else holds.

The test that asserted silence is moved to the property that survived rather than
weakened; its old argument — "a report that fires either way is a report about
nothing" — is written out with why it does not hold here.

And the export loss ledger's line for `remembers:` still described it as "no
field for what survives a summary", which was the whole of it before P8. It is
the agent's memory now: a flag decides what a summary puts back, and two fields
read and write it.
The payload the conformance driver sends carried a tool as two strings — a name
and a sentence. `ToolSpec` has six fields, so four fell off the wall, and
dropping is worse than refusing: the second port could then neither honour the
line nor report it. `notDoneHere`'s own docstring calls that silence the T7
breach it exists to prevent, and it prevented it at the AGENT level — where
`interceptors:`, `policy:` and `team:` are named — while four fields one level
down went straight past.

WHAT EACH COST. `parameters` was the one that cost a capability rather than a
report: `toolDefs` built every tool with `parameters: {}`, so a model on that
runtime was told a tool exists and never what to put in it. Word for word the
defect the reference port's `_takes` records and fixed on its own side, still
live on the other. `bind:` — whose whole promise is that the model cannot see,
name or change the argument — arrived as nothing, so "whose order" went back to
being something the model decides. `remember-as:` and `connect:` this port
genuinely cannot do; being smaller is allowed, being smaller in silence is not,
and it could not say what it was smaller by without being told what it was given.

ONE PROJECTION, NOT FIVE COPIES. `ports.tool_payload` is the wall, stated once —
the same standing as `SUPPLIED_BY_THE_HOST` one level out. It was five
hand-written copies of one line across five test files, which is a rule in none
of them, and that is how four fields fell between them. `WIRE_NAME` declares
which key carries which field and the projection READS it, so the correspondence
is decided in one place: the wire uses the author's words (`bind`,
`remember-as`) so somebody reading a payload beside the YAML that made it does
not have to translate.

AND A GUARD THE TOOL LEVEL DID NOT HAVE. `test_the_loader_to_adapter_boundary_is_
total` asks this of `AgentSpec`; nothing asked it of `ToolSpec`. Now a field
added to it is either on the wall, or in `NOT_SENT` with the reason — and a field
`WIRE_NAME` has never heard of is red on its own.

The conformance trace also records the tool SHAPES the model was shown, not only
their names. Two ports agreed on the names and disagreed on everything the model
needed to fill a call, and a trace that cannot see that difference cannot be the
evidence for the portability claim.

Three standing guard-rails caught the new module and each was answered rather
than silenced: it is a LIBRARY reached by whoever drives a second port, its table
is read by the projection itself, and its one public function is HOST_API.

README's count moves 3174 -> 3181.
§8.3a's rules 1 and 2 are built — a numbered or bulleted line under a heading in
the closed set is a written rule, and editing one needs a person however the edit
is made. Rule 4 is the half that makes the floor usable rather than merely
enforced, and it was the last piece of §8.3a left unbuilt:

> `pact check` prints, per skill, how many clauses it classified as normative and
> under which heading, so the author can see the boundary and move it by editing
> a heading.

Without it the boundary is invisible until somebody trips over it. A rule written
under `## Notes` applies itself with nobody reading it; a heading reworded from
`## Rules` to `## Working guidance` takes every clause under it out of the zone.
Both are correct behaviour and both are surprises, and the remedy §8.3a names is
not a warning — it is showing the author where the line already falls. On the
shipped example:

  note: 'refund-policy' holds 5 written rules under `## Rules` — lines a person
  has to approve before they change, whoever or whatever proposes it.
  fix: Nothing to do. Move a rule out from under `## Rules`, or reword that
  heading, and it stops being one — which is how you move this boundary.

A NOTE, not a warning: nothing is wrong with a skill that holds five rules or
with one that holds none, and a fact printed as a problem teaches an author to
stop reading the output. `--deny-warnings` stays green, which a test pins.

The parse is the one the other side uses — fences out first, closed heading set,
list items and anchors — and the two are held against each other rather than
trusted: `both_readings_of_the_shipped_policy_agree` asserts the number printed
here is the number of clauses `learning.classify` will not let change without a
person. A second copy of a rule is what this project refuses everywhere else.

THREE TESTS MET THEIR FIRST NOTE and each was narrowed to what it meant rather
than silenced. `one_typo_is_one_message` counts PROBLEMS — its own sentence says
so — and a note is not one. `shares_are_checked_where_the_author_is` asserts
nothing is said about the SHARES, which is what it is about; it had been
asserting that no loader rule fired at all, the same thing only while shares were
the only thing that tree could draw a line about. `watching_a_run`'s `rule_of`
finds the first problem, not the first line of any kind.

README's count moves 3181 -> 3186.
Found by driving the self-modification gate over six real edits to the shipped
policy, which is what "test it in full" turns up that a suite does not.

Renaming `## Rules` to `## Working guidance` takes every clause under it out of
the normative zone, so the clause-set difference fires as well as the heading
check — and the clause-set branch was first. An author who reworded a title was
told that five written rules had been TAKEN OUT, and went looking for a deletion
they did not make.

The heading is asked about first, because it explains everything below it. Both
answers are HIGH either way; what changes is whether the reason names the edit
the person actually did.

The other five edits behave as they should and are now measured end to end:
adding a rule, deleting one, and changing "30 days" to "300 days" are each HIGH;
a line added under `## Notes` is LOW; and rewording a note that contains the word
"refund" is HIGH through the prose stems, which is the conservative direction
that field is documented to take.
… told so

Found by exporting a program-carrying tree, which is the kind of thing only
driving the system turns up.

`uses:` names four collections and a registry record has a field for one of them,
so all four went into `skills:`. Measured on the shipped fixture, the record read

    "skills": ["check-window"]

of a WebAssembly body. A registry's `skills:` means WRITTEN PROCEDURES this agent
consults — a person opens one and reads it. A carried program is the opposite
thing: a compiled body with an engine, a determinism promise and a fuel ceiling,
which nothing opens and no one reads. A consumer indexing that record would
believe this agent holds a procedure by that name, and nothing said otherwise:
the loss ledger, whose whole job is to name what a target format cannot hold, had
no row for `programs:` at all.

It comes out of `skills:` and goes into the ledger, naming the program so a
reader knows exactly what did not travel. The control pins the other half: taking
programs out must not take skills out, and the flagship's `refund-policy` still
exports.

README's count moves 3186 -> 3188.
The README described the format as it stood before programs, the object model
and the erasure invariant, and those are the largest things an author meets
after their first desk. A front page that is a round behind is the same defect
as a stale count, one level up.

WHAT IS NEW ON IT. One section covering both halves an author reaches for once a
workspace stops being one desk — "these five desks are the same shape" and "this
bit must be exactly right" — with the authoring shown rather than described:
`expects:`/`with:`, `based-on:`, `base: yes`, `values:`, and a carried program
reached seven ways with the rules each reach claims.

Every claim on it is reproducible from the repository rather than asserted, and
each was run before it was written:

  pact discover tests/trees/two-desks-one-pattern   sha256:554f1ac71be9a23f…
  pact discover tests/trees/two-desks-longhand      sha256:554f1ac71be9a23f…

A pattern and a longhand tree are not similar — they are the same document, down
to the digest, and the same holds for a figure written once against the same
figure typed three times. That is the invariant every downstream claim rests on,
and it was nowhere on the front page.

Also added: `note:` as a severity, with the shipped example's own output and why
`--deny-warnings` stays green for one; the erasure invariant as a sixth thing
worth knowing; and the written-rule floor beside the learning paragraph, where
an author looking for it would be.

CORRECTED. The layout named two framework transports where there are six plus a
second port in another language, and omitted `adapters/typescript/`,
`spec/loops/`, `tests/trees/` and the eight orchestration patterns. The document
index gained the refusal ledger and the gap register — the two documents that
say what was deliberately NOT built and what is still owed, which are the first
two anybody reviewing this should read.
@dittops
dittops merged commit 0387260 into master Aug 23, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants