diff --git a/.credo.exs b/.credo.exs
index 88707c4..8018eb4 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -11,7 +11,8 @@
requires: [],
checks: %{
extra: [
- # CLAUDE.md §5: no runtime evaluation of model output. Covers the whole family,
+ # docs/03-conventions.md, Engineering rules: no runtime evaluation of model output.
+ # Covers the whole family,
# because naming one function leaves an obvious bypass.
{Trinity.Credo.NoEvalOnModelOutput, []}
],
diff --git a/.githooks/commit-msg b/.githooks/commit-msg
index ed64736..6f6d44c 100755
--- a/.githooks/commit-msg
+++ b/.githooks/commit-msg
@@ -6,8 +6,8 @@
# because a trailer that reaches a tag is permanent (CLAUDE.md §4).
#
# Enable with: git config core.hooksPath .githooks
-# Enforced by: scripts/plan_check.sh rule 8, which checks the history itself,
-# so a bypassed or unconfigured hook still fails the gate.
+# Enforced by: the gate's DCO check in CI, which reads the history itself, so a
+# bypassed or unconfigured hook still fails the build.
set -eu
msg_file="$1"
tmp="$msg_file.stripped"
@@ -24,9 +24,8 @@ rm -f "$tmp"
# Warning only: does every repo path the message names appear in the staged diff?
#
# Added at slice 001 G4, for a mistake this slice made three times: a commit whose message ran
-# ahead of its tree. Twice it was an unread `plan_check exit=1`; once a message and a NOTES
-# line said `src-tauri/Cargo.lock` had been added to REUSE.toml when the edit had failed on a
-# stale anchor, in the same output as a green gate. `mix gate` does not check that a commit
+# ahead of its tree: a message said `src-tauri/Cargo.lock` had been added to REUSE.toml
+# when the edit had failed on a stale anchor, in the same output as a green gate. `mix gate` does not check that a commit
# message is true, and nothing else did either.
#
# **It warns and never blocks**, deliberately. A message legitimately names paths it does not
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 8ae018f..137ba72 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,7 +3,7 @@
#
# Dependabot opens a pull request only when a dependency has a security advisory. Version
# bumps are not raised: VERSIONS.md is the pin list, and a version lands only after it is
-# proposed in a slice's NOTES.md and approved (CLAUDE.md section 5). On its first run, with
+# proposed and reviewed, never adopted silently (docs/03-conventions.md). On its first run, with
# the limit above zero, it opened five version-bump pull requests inside two minutes; one of
# them was merged by mistake. An open-pull-requests-limit of 0 keeps the security updates and
# drops the rest. The value here is a security alert arriving as a diff with the gate run
diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml
index 2b2b6d7..d119fd0 100644
--- a/.github/workflows/gate.yml
+++ b/.github/workflows/gate.yml
@@ -15,13 +15,12 @@ jobs:
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
- fetch-depth: 0 # plan_check rule 8 reads the whole history
+ fetch-depth: 0 # the DCO check below reads the whole history
# On a pull request the default checkout is a merge commit GitHub makes on the fly,
- # authored by nobody and signed off by nobody. plan_check rule 8 refused it on the
- # first pull request this repository ever had (run 35477492177). The branch head is
- # what was written and signed, so that is what the gate reads. The ruleset's strict
- # policy requires the branch to be current with main before it can merge, so the
- # head is also what main will contain.
+ # authored by nobody and signed off by nobody, which the DCO check below rightly
+ # refuses. The branch head is what was written and signed, so that is what the gate
+ # reads. The repository ruleset requires a branch to be current with main before it
+ # can merge, so the head is also what main will contain.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
@@ -39,11 +38,7 @@ jobs:
- run: mix deps.get
- # The plan's own consistency, checked before the code's.
- - run: ./scripts/plan_check.sh
-
- # DCO. plan_check rule 8 also asserts every commit is signed off; this fails earlier
- # and more legibly on a pull request.
+ # Developer Certificate of Origin: every commit carries a sign-off (ADR-0012).
- name: DCO sign-off present on every commit
run: |
git log --format=%B ${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD 2>/dev/null \
@@ -120,6 +115,37 @@ jobs:
- id: tag
run: echo "ref=$(scripts/fips_image_tag.sh --ref)" >> "$GITHUB_OUTPUT"
+ # The image tag is a hash of the files that define the image, so any change to them -
+ # including a comment - names an image that has to be built before it can be pulled. The
+ # `fips` job below takes that image as its container, which GitHub pulls before the job's
+ # first step runs, so the job cannot retry the pull itself: it fails with `manifest
+ # unknown` and no output. The fips-image workflow builds and pushes on the same event, so
+ # the two race, and the loser is always this one.
+ #
+ # Waiting here turns that race into a wait. The image is a function of its inputs, so the
+ # one being waited for is exactly the one this commit needs, and a build from source is
+ # slow enough that the ceiling is generous. A timeout fails with the reason rather than
+ # with a pull error, because "the image is still building" and "the image will never
+ # exist" need different responses from whoever reads it.
+ - name: Wait for the FIPS image to be published
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ref='${{ steps.tag.outputs.ref }}'
+ echo "$GH_TOKEN" | docker login ghcr.io -u '${{ github.actor }}' --password-stdin
+ for attempt in $(seq 1 60); do
+ if docker manifest inspect "$ref" >/dev/null 2>&1; then
+ echo "$ref is published (attempt $attempt)"
+ exit 0
+ fi
+ echo "attempt $attempt: $ref is not published yet; waiting 30s"
+ sleep 30
+ done
+ echo "::error::$ref was not published within 30 minutes. It is built by the" \
+ "fips-image workflow, which runs when ci/fips/Containerfile, .tool-versions or" \
+ "scripts/fips_image_tag.sh changes. Check that workflow's run for this commit."
+ exit 1
+
fips:
needs: fips-tag
runs-on: ubuntu-latest
@@ -146,10 +172,9 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# The checkout action marks the workspace safe under a temporary HOME it removes when
- # it is done; every later git call in the container (plan_check, the names, secrets and
+ # it is done; every later git call in the container (the names, secrets and
# reuse censuses, all of which read `git ls-files`) then hits git's dubious-ownership
- # refusal and reads an empty tree. Found on run 35537462271: plan_check rule 4 named
- # every slice as missing its SLICE.md.
+ # refusal and reads an empty tree, which makes every census pass over nothing.
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
@@ -185,8 +210,6 @@ jobs:
ERL_AFLAGS: "-eval application:load(crypto)"
run: elixir scripts/crypto_supports.exs | diff - docs/fips-leg/supports-default.txt
- - run: ./scripts/plan_check.sh
-
- name: DCO sign-off present on every commit
run: |
git log --format=%B ${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD 2>/dev/null \
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 0000000..e91b530
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
+# SPDX-License-Identifier: Apache-2.0
+#
+# OpenSSF Scorecard. It measures this repository against the OpenSSF's own checks - branch
+# protection, pinned dependencies, signed releases, dangerous workflow patterns, token
+# permissions, maintenance and the rest - and publishes the result so that anyone evaluating the
+# project can read it without being asked to trust a claim made here.
+#
+# It runs weekly rather than on every push because the checks are about the repository's
+# configuration and history, which change slowly, and because the analysis reads the GitHub API
+# rather than the tree. The results are uploaded to the code-scanning dashboard, so a regression
+# in the repository's posture is visible in the same place as a code finding.
+name: scorecard
+
+on:
+ branch_protection_rule:
+ schedule:
+ # Mondays, 07:00 UTC. A fixed day makes a drop in the score attributable to the week it
+ # appeared in rather than to an unknown point since the last look.
+ - cron: '0 7 * * 1'
+ push:
+ branches: [main]
+
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ # Upload the result to the code-scanning dashboard.
+ security-events: write
+ # Read the repository's own metadata (branch protection, releases, workflow runs).
+ id-token: write
+ contents: read
+ actions: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ persist-credentials: false
+
+ - name: Run analysis
+ uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ # Publishing makes the score readable at deps.dev and through the Scorecard API, which
+ # is the point: an evaluator should be able to check the posture without asking us.
+ publish_results: true
+
+ - name: Upload the result as an artifact
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: scorecard-results
+ path: results.sarif
+ retention-days: 30
+
+ - name: Upload the result to code scanning
+ uses: github/codeql-action/upload-sarif@416ff0dea110f80f0f56f0046500dbf7420e4bb0 # codeql-bundle-v2.27.1
+ with:
+ sarif_file: results.sarif
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5ec5ad2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,140 @@
+
+
+# Changelog
+
+Trinity is developed in reviewed units of work, each merged through a pull request with the
+quality gate green and then tagged. This file records what each tag delivered and how it was
+verified. It is derived from the tags themselves (`git tag -l 'slice/*'`), not written from
+memory.
+
+The project is pre-alpha: there is no supported release series yet, and tags mark reviewed
+increments rather than supported versions. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
+loosely; versioning is by increment, and semantic versions begin at the first supported release.
+
+Every entry below was gated on: formatting, a compile with warnings as errors, the architectural
+boundary check, Credo (strict), Sobelow, dependency and licence audits, the full test suite and a
+coverage floor. Entries from 2026-09-22 onward also required a release build check. Verification
+evidence for each increment is retained by the maintainers and summarised here.
+
+## Unreleased
+
+- Repository restructured for public review: internal planning material moved out of the published
+ tree, engineering rules consolidated into `docs/03-conventions.md`, and this changelog added as
+ the public record of delivered work.
+
+## 2026-09-23
+
+### `slice/070` — Gateway core
+Trinity becomes reachable from channels other than the desktop. An adapter behaviour carries text
+and nothing else; a router admits the sender, applies a rate limit, dispatches commands and binds
+the conversation to a session, streaming the reply back. An unknown sender receives a pairing code
+and nothing else: no session is created and no model is called until they are paired. Approvals
+raised during a channel-originated turn are rendered into that conversation, and what a channel may
+approve is capped below what the local desktop may, with the refusal recorded in the receipt chain.
+Ships an in-process console adapter; messaging platforms follow.
+
+## 2026-09-22
+
+### `slice/062` — MCP authorization
+OAuth 2.1 in every role for the Model Context Protocol surface: a resource server validating
+audience-bound tokens against an external authorization server, an optional personal-profile
+authorization server for a single operator's own machine, and a client role that obtains tokens
+from protected servers. Identity is separated from authority throughout: a token establishes who is
+calling, while the permission gate and the selected authority adapter decide whether an effect may
+happen. No token material reaches model context, logs or receipts.
+
+### `slice/061` — MCP server
+Trinity serves the Model Context Protocol at revision 2026-07-28 over Streamable HTTP and stdio,
+with compatibility for 2025-11-25. Calls arriving over the protocol pass the same permission gate
+and effect membrane as local ones and leave the same receipts. A call requiring approval is held
+with sealed, tamper-evident state that the client retries once the operator has decided.
+
+### `slice/050` — Scheduler
+Scheduled agent tasks on durable jobs, surviving restart, with cron or one-shot schedules, delivery
+targets, and a curator that marks stale records and archives untouched ones without deleting.
+
+## 2026-09-21
+
+### `slice/060` — MCP client
+A thin client driver: outbound requests are built here while decoding and validation use the
+protocol core's published functions. Every tool a server exposes becomes a namespaced tool that
+cannot borrow a built-in tool's permissions, every result is treated as untrusted content, and a
+server's mid-call question becomes an operator approval rather than an automatic answer.
+
+### `slice/059` — MCP capability assessment
+A measured gap analysis of the protocol core against the 2026-07-28 checklist, and a probe of the
+extension seam the server side needs, recorded as findings rather than intentions.
+
+### `slice/041` — Skill self-management
+Skills the system proposes for itself pass through staged approval and a scanner before becoming
+active.
+
+### `slice/040` — Skills registry
+A registry for the open `SKILL.md` format with progressive disclosure of instructions.
+
+### `slice/032` — Semantic memory
+Embeddings, semantic recall and hybrid retrieval, with a local model option and a vector store
+behind a behaviour.
+
+## 2026-09-20
+
+### `slice/034` — Export, import, restore
+The operator's data leaves in a documented archive format and comes back, with manifest validation.
+
+### `slice/033` — Project context
+`AGENTS.md` from a session's project roots is loaded into context with precedence and a size cap.
+
+### `slice/031` — Session search
+Full-text search over conversation history using SQLite FTS5.
+
+### `slice/030` — Persona and always-on memory
+A persona document and a memory tier always present in context, scoped session, persona and global,
+with a budget and truncation recorded as evidence.
+
+### `slice/024` — Effects, authority and receipts
+The effect catalog, authority selection at boot, and locally signed receipts. Authority is an
+adapter behind a behaviour: the shipped implementation decides locally, and a deployment may supply
+an external one, in which case this tree keeps no executor for the effects that adapter governs.
+Receipts are an Ed25519-signed hash chain with checkpoints and a verifier.
+
+### `slice/023` — Context compaction
+Conversations that outgrow the window are compacted with lineage retained.
+
+### `slice/022` — Core tools
+Filesystem, web fetch and search, and a shell tool, each behind the permission gate, with content
+from outside the machine marked untrusted.
+
+### `slice/021` — Permission gate
+Every tool call is decided before it runs: allow once, for the session, or by a written rule, with
+the decision and its basis receipted. Approval surfaces carry no authority of their own.
+
+### `slice/020` — Tool protocol and registry
+Tools are modules behind a behaviour plus a configuration entry; adding one requires no change to
+any core module, and the registry test asserts it.
+
+### `slice/013` — Chat interface
+Streaming conversation in LiveView, with model output rendered through a single path.
+
+### `slice/012` — Session process
+One supervised process per session, with crash recovery proven by killing the process.
+
+### `slice/011` — Model provider layer
+Providers behind one behaviour, switched by configuration, with usage recorded per call.
+
+### `slice/010` — Core domain and persistence
+The data model and repository layer, SQLite first with PostgreSQL supported, with gapless ordering
+under concurrent writers proven by a stress test.
+
+### `slice/003` — FIPS build leg
+A continuous integration leg that builds from source and runs the cryptographic properties inside a
+FIPS-mode container, so claims about approved algorithms are measured rather than asserted.
+
+## 2026-09-07
+
+### `slice/001` — Packaging
+A self-contained desktop binary produced for Linux, macOS and Windows, each smoke-tested in CI.
+
+## 2026-09-06
+
+### `slice/000` — Toolchain and quality gate
+The repository, the pinned toolchain and the gate every later change has had to pass.
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 44dc7fa..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,136 +0,0 @@
-# CLAUDE.md: operating rules for the coding agent
-
-You are the implementation engineer on **Trinity**, a personal AI agent built on Elixir/OTP + Phoenix LiveView,
-packaged as a desktop app. A human product owner reviews your work slice by slice. This file is the contract.
-
-## 0. Read order at the start of every session
-
-1. `ROADMAP.md`: find the current slice (status `in_progress`) or the next `ready` slice.
-2. `slices/NNN-*/SLICE.md` for that slice. Read it fully. Read its `NOTES.md` if it exists.
-3. `docs/03-conventions.md` and `docs/04-slice-process.md` (skim; they do not change often).
-4. `docs/01-architecture.md` section relevant to the slice.
-5. `docs/07-security-model.md`, the section relevant to the slice. This is not optional reading for slices 021,
- 022, 024, 060, 061, 062 and 070: the permission gate, the provenance gate, the effect membrane, the receipt
- scheme and the approval channel rules all live there, and none of them is restated in the slice files.
-6. Only then start work.
-
-If ROADMAP.md shows no slice `in_progress` and the previous slice is not `approved`, **stop and ask**.
-
-## 1. The slice contract (non-negotiable)
-
-- Work on exactly **one slice at a time**, on branch `slice/NNN-short-name`.
-- Do not start a slice whose `Depends on:` slices are not `approved`.
-- Do not widen scope. If you discover needed work outside the slice, write it to `NOTES.md` under
- "Follow-ups" and, if it blocks you, ask. Do not silently do it.
-- If the spec is ambiguous, ask **before** building. One clarifying message with concrete options beats a wrong slice.
-- If you must deviate from `SLICE.md`, record the deviation and reason in `NOTES.md` **before** committing.
-- Every acceptance criterion in `SLICE.md` must be demonstrably met in `PROOF.md`. No "should work".
-
-## 2. Definition of Done (every slice)
-
-A slice is done only when ALL of these are true:
-
-1. `mix gate` passes (format check, compile with warnings-as-errors, boundary, **the release check**,
- credo --strict, sobelow, tests, audits, plan check). The release check is `scripts/prod_check.sh`:
- the tree compiles under `MIX_ENV=prod`, the headless release assembles, and it evaluates
- `config/runtime.exs`'s prod branch. It exists because `MIX_ENV=test` says nothing about what
- ships: a plug's `init/1` is called at compile time under prod, and slice 062 shipped a closure
- there that broke the release while every gate stayed green (slice 062 NOTES, F5).
-2. Every acceptance criterion has a corresponding proof entry (command + output, or screenshot for UI).
-3. New behaviours/public modules have `@moduledoc` and `@doc`; new behaviours have typespecs.
-4. Tests exist for new logic (unit for pure code, process tests for GenServers, LiveView tests for UI).
-5. `PROOF.md` is written from `templates/PROOF-TEMPLATE.md` and is complete.
-6. `ROADMAP.md` status for the slice is set to `done` (the human sets `approved`).
-7. `docs/` are updated if the slice changed architecture, data model, or conventions. ADR added if a decision changed.
-8. The final commit follows the format in section 4, and the tag is created.
-
-## 3. Commands you will use
-
-```
-mix gate # the full quality gate (defined in Slice 000). Must pass before every commit.
-./scripts/prod_check.sh # the release half of the gate, on its own: prod compile, release assemble, config eval
-mix test # tests
-mix test --cover # coverage (report the line in PROOF.md)
-mix credo --strict
-mix boundary # run via `mix compile`: boundary violations are compile warnings → errors
-mix hex.audit && mix deps.audit
-mix versions.verify # (Slice 000) prints installed vs VERSIONS.md
-```
-
-## 4. Git rules
-
-- Branch: `slice/NNN-short-name` from `main`.
-- Intermediate commits are allowed and encouraged. Conventional Commits, scope = slice id:
- `feat(s012): session GenServer with restart rehydration`
- `test(s012): crash-recovery test for Session`
- `docs(s012): PROOF.md`
-- Final commit of a slice: `feat(s012): complete slice 012 (session process and agent loop)`.
- It must include `PROOF.md` and the `ROADMAP.md` status change.
-- Merge to `main` through a pull request. The repository ruleset requires one, allows only the merge-commit
- method (the slice boundary stays visible, as `--no-ff` did), and requires the `gate` check green on the
- branch head. Nobody can bypass it, the owner included. After the merge, on `main`:
- `git tag -a slice/NNN -m "Slice NNN:
"` and push the tag. Tags cannot be moved or deleted.
-- Every commit is DCO signed-off (`git commit -s`); the hook and CI refuse otherwise (ADR-0012).
-- Never force-push `main`. Never rewrite tagged history.
-- Never commit secrets. `.env*` is gitignored. API keys come from env or the OS keychain module.
-
-## 5. Engineering rules
-
-- **Modularity:** every pluggable concern is a `@behaviour` behind a registry. Adding a tool/provider/gateway
- must never require editing a core module: only adding a module and a config entry.
-- **Boundaries:** respect `docs/01-architecture.md` dependency rules. `boundary` enforces them at compile time.
-- **OTP first:** one process per session; supervise everything; no bare `spawn`; use `Task.Supervisor`.
-- **No `Code.eval_string` on model output.** Ever. Sandboxed execution goes through `Trinity.Sandbox`.
-- **Persistence:** all writes go through the `Trinity.Repo` owner; schema changes need migrations; migrations must
- work on SQLite (primary) and not break Postgres (secondary).
-- **Streaming:** LLM output streams through PubSub topics, never held in LiveView state as a growing string
- without bounds. Use the streaming markdown renderer selected in Slice 013.
-- **Types:** Elixir 1.20's type checker is part of the gate. Write typespecs on public functions.
-- **Versions:** use only versions listed in `VERSIONS.md`. If a newer stable exists, propose it in `NOTES.md`;
- do not upgrade mid-slice without approval.
-- **Tests must not hit the network.** LLM/provider calls are mocked via Mox against the behaviours.
- A separate `mix test --only live` tag exists for opt-in real-provider tests.
-
-## 6. Proof standard
-
-Proof means: the command you ran, and the output it produced, pasted into `PROOF.md`. For UI: a screenshot or
-short GIF saved under `slices/NNN-*/proof/`. For processes: a test that kills the process and asserts recovery.
-For performance claims: the measurement.
-
-Every acceptance criterion is tagged `[auto]` or `[manual]` in SLICE.md. If something cannot be proven in CI, say
-so explicitly in PROOF.md and describe the manual verification the human should perform. **List the `[manual]`
-queue in the G1 plan, not at G3.** The human is the only reviewer and their queue is the real critical path;
-discovering it when the slice is otherwise finished is discovering it too late.
-
-## 7. When to stop and ask
-
-- Spec ambiguity that changes the design.
-- A dependency in `VERSIONS.md` is missing, yanked, or incompatible.
-- A gate failure you cannot fix within the slice's scope.
-- Anything touching signing keys, credentials, or destructive filesystem operations.
-- You are about to exceed the slice's stated size by a lot (S→L).
-
-## 8. Rules of evidence (adopted from the sister repos; they are the house style)
-
-- **Demonstrated red before any fix.** A test for a claimed property is committed failing first, by name, and the
- fix commit references it. A red that fails at an earlier fault than the claim has demonstrated nothing.
-- **Populations derive from the tree.** Any "every X" in PROOF.md names the command that enumerates X. A hand list
- whose membership is the thing at issue is the defect.
-- **"Verified" names its command and exit code**, from the right process. A pipeline's exit is not the command's.
-- **Corrections append; records are never rewritten.** PROOF.md and NOTES.md grow by append with a date; a wrong
- line stays and is corrected below it with "supersedes".
-- **Values come from the tree or the owner.** No date, count, sha, digest, or line number is typed from memory;
- say the deriving command. Never estimate time.
-- **A hold or deferral carries an owner, an absolute date, and a lift condition a stranger can check.** An empty
- population is a fact about the data that day; a satisfied property is a fact about the system; keep them apart.
-- **A name is a claim.** A function called `verify` that records without verifying is renamed or fixed.
-- **Every egress gets a redaction row.** Anything Trinity sends off-machine (proposals, telemetry, MCP results to
- other agents) is enumerated with what crosses raw and what crosses hashed.
-- **MCP is versioned by date, never by a major number.** Write "AARM" as the category. Commercial names belong on
- public surfaces; code names live only in identifiers. Real names appear only where the name check permits them:
- the enforcer is the rule, and no prose here overrides it.
-
-## 9. Tone of PROOF.md and NOTES.md
-
-Plain, factual, first person allowed. Report failures and workarounds honestly: the human is grading
-accuracy of the report as much as the code.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3af6ed9..2cfb37b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,10 +1,10 @@
# Contributing
-Trinity is pre-alpha with one maintainer, and the work is planned as numbered slices
-(`ROADMAP.md`). The most useful contributions right now are bug reports against a tagged slice,
-corrections to the documents, and review of a slice's proof. If you want to build a slice, open
-an issue naming it first so two people do not build the same one.
+Trinity is pre-alpha with one maintainer. The most useful contributions right now are bug
+reports against a tagged release, corrections to the documents, and review of the engineering
+records behind a change. If you want to take on a larger piece of work, open an issue describing
+it first, so two people do not build the same thing.
## Setup
@@ -19,44 +19,45 @@ mix gate
## The rules that bind
-`CLAUDE.md` is the engineering contract and `docs/04-slice-process.md` is the process. The
-short version:
+`docs/03-conventions.md` carries the engineering rules, the proof standard and the rules of
+evidence. The short version:
-- **One slice at a time**, on a branch named `slice/NNN-short-name`. Do not widen its scope; write
- anything you find outside it under "Follow-ups" in that slice's `NOTES.md`.
-- **`mix gate` passes before every commit**, including `scripts/plan_check.sh`.
+- **One change at a time**, on its own branch, with a stated scope. Anything found outside that
+ scope is recorded as a follow-up rather than folded in silently.
+- **`mix gate` passes before every commit.** It runs formatting, a compile with warnings as
+ errors, the architectural boundary check, a release build check, Credo, Sobelow, the dependency
+ and licence audits, the full test suite and a coverage floor.
- **Every commit is signed off** (`git commit -s`, the Developer Certificate of Origin). The hook
and the gate refuse a commit without it.
-- **No attribution trailers in commit messages.** `.githooks/commit-msg` strips them, and
- `plan_check` checks the history itself, so an unconfigured hook fails the gate rather than
- passing quietly. Commit messages also carry no issue-tracker identifiers.
+- **No attribution trailers in commit messages.** `.githooks/commit-msg` strips them, and CI
+ checks the history itself, so an unconfigured hook fails the build rather than passing quietly.
+ Commit messages carry no issue-tracker identifiers.
- **Proof is the command you ran and the output it produced.** Not a summary of it. Anything
described as verified names its command and its exit code.
- **Populations come from the tree.** Any claim about "every X" names the command that lists X.
-- **Corrections are appended.** `PROOF.md`, `NOTES.md` and the decision records are never
- rewritten; a wrong line stays and is corrected below it, saying what it supersedes.
+- **Corrections are appended.** Engineering records and decision records are never rewritten; a
+ wrong line stays and is corrected below it, saying what it supersedes.
- **A failing test comes before the fix.** A test for a claimed property is committed failing
first, by name, and the fix commit refers to it.
## Commit messages
-Conventional Commits with the slice id as the scope:
+Conventional Commits, with a scope that identifies the unit of work:
```
feat(s012): session process with restart rehydration
test(s012): crash-recovery test for the session process
-docs(s012): proof
```
-The final commit of a slice reads `feat(s012): complete slice 012 (session process and agent loop)`
-and includes `PROOF.md` and the `ROADMAP.md` status change. Slices merge to `main` through a
-pull request with the `gate` check green, merge-commit method only, and are then tagged `slice/NNN`.
+A unit of work merges to `main` through a pull request with the `gate` check green, merge-commit
+method only, and is then tagged. `CHANGELOG.md` records what each tag delivered and how it was
+verified.
## What gets a change sent back
An unproven claim. A count typed rather than derived. A skipped check without a stated reason.
-A rule added as prose where an enforcer was possible. A version not listed in `VERSIONS.md`
-(propose the newer one in `NOTES.md` instead of upgrading mid-slice).
+A rule added as prose where an enforcer was possible. A dependency version not listed in
+`VERSIONS.md`: propose the newer one for review rather than adopting it inside another change.
## Reporting problems
diff --git a/GOVERNANCE.md b/GOVERNANCE.md
index 45fd374..92db31c 100644
--- a/GOVERNANCE.md
+++ b/GOVERNANCE.md
@@ -4,8 +4,8 @@
## Today
**One maintainer**, named in `MAINTAINERS.md`, who is also the author and the sole reviewer.
-Every slice is gated on that person's review (`docs/04-slice-process.md`, gate G4). This is
-stated rather than dressed up: the review queue is the project's real critical path.
+Every change is gated on that person's review. This is stated rather than dressed up: the
+review queue is the project's real critical path.
## Decisions
@@ -18,11 +18,13 @@ look as though it had always been right.
There is no committee to join yet. The path, when it opens:
-1. Land changes through the ordinary slice process, with proof that meets `CLAUDE.md` §6.
+1. Land changes through the ordinary review process, with evidence that meets the proof
+ standard in `docs/03-conventions.md`.
2. Review someone else's slice and have that review hold up.
3. The maintainer proposes commit access; it is recorded here and in `MAINTAINERS.md`.
-Commit access carries the same obligations as authorship: the evidence rules in `CLAUDE.md` §8
+Commit access carries the same obligations as authorship: the rules of evidence in
+`docs/03-conventions.md`
apply to everyone, and "verified" always names its command and its exit code.
## Changing this document
diff --git a/README.md b/README.md
index 4c8ab93..fa319c6 100644
--- a/README.md
+++ b/README.md
@@ -75,8 +75,8 @@ that means in practice:
run on the same queues; `/oban` shows the jobs.
Not there yet: the messaging platforms themselves and subagents (M5b),
-the native desktop shell and signed releases (M6), executable skills in a sandbox (M7). `ROADMAP.md` carries the live status of every
-slice, and the [Milestones](#milestones) section below explains how to read it.
+the native desktop shell and signed releases (M6), executable skills in a sandbox (M7). The [Milestones](#milestones) section below sets out the order the
+remaining work is being built in.
The interface is a local web page; the desktop shell exists as a packaging spike, not a product.
If you want to follow along, watch the roadmap and the tags.
@@ -136,25 +136,40 @@ start-up times for each target; `docs/fips-leg.md` describes the FIPS build leg.
## How the work is organised
-Trinity is built in slices. A slice is one unit of planning, work, proof, review and history:
-small enough to review in one sitting and large enough to deserve a tag.
+Trinity is built in reviewed increments. Each one is small enough to review in one sitting and
+large enough to deserve a tag, and each is merged through a pull request with the quality gate
+green, then tagged. `CHANGELOG.md` records what every tag delivered and how it was verified.
-Each slice has a folder under `slices/` holding its specification (`SLICE.md`), the working notes
-and deviations recorded while it was built (`NOTES.md`), and the evidence that it met its
-acceptance criteria (`PROOF.md`). Proof means the command that was run and the output it produced,
-pasted in, or a screenshot for anything visual. A sentence saying something works is not proof.
+**Evidence, not assertion.** A claim that something works is not accepted in place of the command
+that was run and the output it produced. Anything described as verified names its command and its
+exit code; any statement of the form "every X" names the command that enumerates X; and no count,
+hash, date or version is written from memory. Records are appended to and never rewritten: a wrong
+line stays where it is and is corrected below it, saying what it supersedes. A test for a claimed
+property is committed failing first, so that the fix is shown to be the thing that made it pass.
-A slice moves through `planned`, `ready`, `in_progress`, `done` and `approved`. Only the
-maintainer sets `approved`, after reading the proof. Each approved slice is merged with a merge
-commit and tagged `slice/NNN`, so the history is the audit log.
+**The gate.** `mix gate` is the same command locally and in CI. It runs formatting, a compile with
+warnings as errors, the architectural boundary check, a release build check, Credo (strict),
+Sobelow, dependency and licence audits, the full test suite, and a coverage floor that fails on a
+drop. Continuous integration runs it on SQLite and PostgreSQL, and again inside a FIPS-mode
+container. `docs/03-conventions.md` carries the engineering rules, the proof standard and the
+rules of evidence every change is held to.
-Two rules shape everything else. Records are appended to and never rewritten: a wrong line stays
-where it is and is corrected below it, saying what it supersedes. And a count, a hash, a date or
-a version is never typed from memory; it is derived from the tree by a command that is named next
-to it.
+## Assurance posture
-`docs/04-slice-process.md` has the full lifecycle and the review gates. `CLAUDE.md` is the
-engineering contract that every change is held to.
+For readers evaluating this project for adoption, the properties below are enforced by the build
+rather than described by it.
+
+| Property | How it is enforced |
+|---|---|
+| Memory safety | The application is Elixir on the BEAM; the desktop shell is Rust. Both are memory-safe by construction, so the classes of defect named in current national guidance on memory safety do not arise in the application tree. |
+| Architectural integrity | Module dependency rules are compiled: a violation of the layering in `docs/01-architecture.md` fails the build, so the architecture is a property of the tree rather than a diagram. |
+| Least privilege for effects | Every tool call is decided by a permission gate before it runs, and side effects pass one membrane. Identity is separated from authority: a credential establishes who is calling; the gate and the selected authority adapter decide whether an effect may happen. |
+| Auditability | Decisions and effects are recorded in an Ed25519-signed hash chain with checkpoints and a verifier, so an operator can reconstruct what was done, by whom and under what decision. |
+| Untrusted content | Anything arriving from outside the machine is marked untrusted at the boundary and is never treated as instruction. |
+| Approved cryptography | A dedicated CI leg builds from source and runs the cryptographic properties inside a FIPS-mode container, so statements about approved algorithms are measured on that leg rather than asserted. |
+| Supply chain | Dependency and licence audits run on every commit; dependency versions are pinned in `VERSIONS.md` and verified against the lock file by the gate. |
+| Provenance | Every commit carries a Developer Certificate of Origin sign-off, enforced by a hook and independently by CI. |
+| Claim discipline | `docs/09-standards-register.md` records one row per control a regulated deployment might ask about, each with an evidence path and a status. No public claim about a regulation or requirement is made without a row there saying it is true. |
## Milestones
@@ -178,20 +193,17 @@ later without renumbering anything.
| Path | Purpose |
|---|---|
-| `ROADMAP.md` | Every slice with its phase, milestone, dependencies and current status |
+| `CHANGELOG.md` | What each tag delivered and how it was verified |
| `VERSIONS.md` | The verified dependency versions, generated from `lib/trinity/versions.ex` |
-| `CLAUDE.md` | The engineering contract: slice rules, definition of done, proof standard |
-| `docs/` | Vision, architecture, tech stack, conventions, slice process, data model, risks, security model, standards; packaging, the FIPS leg, backup and restore, performance measurements |
+| `docs/` | Vision, architecture, tech stack, conventions, data model, risks, security model, standards register; packaging, the FIPS leg, backup and restore, performance measurements |
| `docs/mcp-server.md` | Connecting a client to Trinity's MCP server (Claude Code, VS Code, Codex, goose), stdio, approvals over the wire, the headless profile |
-| `slices/059-mcp-library-spike/FINDINGS.md` | What the MCP server core (`beam_mcp`) ships, carries, refuses or leaves open against the 2026-07-28 checklist; the reference for the MCP phase |
| `docs/adr/` | Architecture decision records. One is added whenever a decision changes |
-| `slices/` | One folder per slice: specification, notes and proof |
-| `templates/` | The templates a new slice, proof or decision record starts from |
+| `docs/09-standards-register.md` | One row per control a regulated deployment may ask about, with its evidence path and status |
| `lib/`, `test/`, `config/` | The application |
| `src-tauri/` | The native desktop shell |
-| `scripts/`, `credo_checks/` | The plan checker, the benchmark scripts and this project's own Credo checks |
+| `scripts/`, `credo_checks/` | The release check, the benchmark scripts and this project's own Credo checks |
| `ci/fips/` | The container the FIPS leg builds its toolchain in |
-| `coverage.tsv` | Test coverage per slice, appended at each close |
+| `coverage.tsv` | Test coverage per increment, appended at each close |
## Connecting Trinity to the platform
diff --git a/REUSE.toml b/REUSE.toml
index 770def4..bb4f937 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -11,7 +11,7 @@ SPDX-PackageDownloadLocation = "https://github.com/ScriptKittyOS/Trinity"
# the gap is recorded rather than glossed.
[[annotations]]
-path = ["mix.lock", "src-tauri/Cargo.lock", "**/*.md", "**/*.json", "**/*.tsv", "**/*.cff", "**/*.toml", ".tool-versions", ".gitignore", ".sobelow-skips", ".sobelow-skips.reasons", "priv/name_digests.txt", ".formatter.exs", "slices/**/proof/**", "test/support/*.txt"]
+path = ["mix.lock", "src-tauri/Cargo.lock", "**/*.md", "**/*.json", "**/*.tsv", "**/*.cff", "**/*.toml", ".tool-versions", ".gitignore", ".sobelow-skips", ".sobelow-skips.reasons", "priv/name_digests.txt", ".formatter.exs", "test/support/*.txt"]
precedence = "aggregate"
SPDX-FileCopyrightText = "Sudo Apt Holdings LLC"
SPDX-License-Identifier = "Apache-2.0"
diff --git a/ROADMAP.md b/ROADMAP.md
deleted file mode 100644
index f4c4120..0000000
--- a/ROADMAP.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# Roadmap
-
-Status values: `planned` → `ready` (deps approved) → `in_progress` → `done` (agent) → `approved` (human).
-`blocked` is also a status, as `docs/04-slice-process.md` has always defined it, and was missing from this legend.
-A slice marked `blocked` names its blocker and a lift condition a stranger can check. `withdrawn` means the slice
-is not being built in this tree; the row stays so the numbering gap is explained.
-Sizes: S ≈ half a day, M ≈ 1–2 days, L ≈ 3–5 days of agent work. Sizes are estimates for planning, not deadlines.
-A size given as `M or L` is conditional on a decision named in that slice's file.
-
-## Milestones
-
-| Milestone | Meaning | Reached when |
-|---|---|---|
-| **M0 Stands** | Repo, gate, packaging path proven | 000, 001 approved |
-| **M1 Talks** | Streaming chat with any provider, persisted, crash-safe | 010–013 approved |
-| **M2 Acts** | Tools with permission gate, one side-effect membrane, local receipts, context compaction; the FIPS build leg that proves 024's properties | 003, 020–024 approved |
-| **M3 Remembers** | Persona, always-on memory, FTS + semantic recall, project context, and data you can take with you | 030–034 approved |
-| **M4 Learns** | Skills system with agent self-management + approval | 040–041 approved |
-| **M5a Automates** | Cron tasks and MCP, client and server, with authorization | 050, 059–062 approved |
-| **M5b Reaches** | Gateways over PubSub, subagents | 070–072, 080 approved (081 optional, outside the milestone) |
-| **M6 Ships** | Observability, native desktop shell, signed releases | 090–101 approved |
-| **M7 Sandboxed** | Executable skills in an in-VM sandbox | 110 approved |
-| **M9 Donatable** | OSS hygiene audited, supply chain signed, AAIF Sandbox package complete, shared libraries extracted | 002, 120–123 approved (122 filing is an owner action) |
-
-Slices 025 and 026 carry no milestone: they serve regulated deployments after M2 and are assigned when the
-standards register names the rows that ask for them.
-
-## Slices
-
-| ID | Slice | Phase | Size | Depends on | Status |
-|---|---|---|---|---|---|
-| 000 | Toolchain, repo bootstrap, quality gate | 0 Foundation | L | none | approved |
-| 001 | Packaging spike: Burrito + ex_tauri smoke build | 0 Foundation | M | 000 | approved |
-| 002 | Supply chain, early: SBOM, build provenance, the TLS floor | 0 Foundation | S | 000 | planned |
-| 003 | FIPS build leg in CI, from source | 0 Foundation | M | 000 | approved |
-| 010 | Core domain + persistence (Ecto/SQLite, schemas, Repo owner) | 1 Core loop | M | 000 | approved |
-| 011 | LLM provider layer (req_llm behind `Trinity.LLM` behaviour) | 1 Core loop | M | 010 | approved |
-| 012 | Session process + agent loop (gen_statem, DynamicSupervisor, rehydration) | 1 Core loop | L | 010, 011 | approved |
-| 013 | LiveView chat UI with streaming | 1 Core loop | M | 012 | approved |
-| 020 | Tool protocol + registry | 2 Tools | M | 012 | approved |
-| 021 | Permission gate + approval UI (M2 fingerprint-bound, M7) | 2 Tools | M | 020, 013 | approved |
-| 022 | Core tools: filesystem, web fetch/search, shell (MuonTrap) | 2 Tools | L | 021 | approved |
-| 023 | Context compaction + session lineage | 2 Tools | M | 012 | approved |
-| 024 | Effect catalog, authority selection (`TRINITY_AUTHORITY`), local receipts | 2 Tools | L | 021, 022 | approved |
-| 025 | Encryption at rest, and the key-custody seam | 2 Tools | M | 010, 024 | planned |
-| 026 | Store-and-forward receipts for disconnected operation | 2 Tools | L | 024 | planned |
-| 030 | Persona (SOUL) + always-on memory tier | 3 Memory | M | 012 | approved |
-| 031 | Session search (SQLite FTS5) | 3 Memory | S | 010 | approved |
-| 032 | Embeddings + semantic memory + hybrid retrieval | 3 Memory | L | 031 | approved |
-| 033 | Project context: AGENTS.md | 3 Memory | S | 030, 022 | approved |
-| 034 | Export, import, restore | 3 Memory | S | 030, 031 | approved |
-| 040 | Skills registry + agentskills.io format + progressive disclosure | 4 Skills | M | 020 | approved |
-| 041 | Skill self-management with staged approval + scanner | 4 Skills | M | 040, 021 | approved |
-| 050 | Scheduler: Oban cron agent tasks with delivery targets | 5 Automation | M | 012 | approved |
-| 059 | MCP capability gap against beam_mcp, and the server seam probe | 6 MCP | S/M | 020 | approved |
-| 060 | MCP client: Trinity's thin driver (2026-07-28 preferred, 2025-11-25 compat, MRTR, Tasks) | 6 MCP | L | 059, 021 | approved |
-| 061 | MCP server (stateless 2026-07-28 + compat, MRTR approvals, headless profile) | 6 MCP | M | 060, 024 | approved |
-| 062 | MCP authorization: OAuth client role, resource server, personal-profile authorization server (EMA removed 2026-09-22) | 6 MCP | L | 061 | approved |
-| 070 | Gateway core: adapter behaviour, routing, PubSub fan-out | 7 Gateways | M | 012 | approved |
-| 071 | Gateway: Telegram | 7 Gateways | S | 070 | planned |
-| 072 | Gateway: Discord (Nostrum) | 7 Gateways | S | 070 | planned |
-| 080 | Subagents + delegation | 8 Orchestration | M | 020, 023 | planned |
-| 081 | A2A v1.0 Agent Card + task intake (optional) | 8 Orchestration | M | 080, 061 | planned (optional) |
-| 082 | withdrawn: delegating effects to an external authority layer is the adapter's job, outside this tree | none | none | none | withdrawn |
-| 083 | withdrawn: verifying another system's receipts belongs with that system's adapter | none | none | none | withdrawn |
-| 084 | withdrawn: connecting to a specific MCP server is configuration, not a slice | none | none | none | withdrawn |
-| 090 | Observability: telemetry, cost ledger, LiveDashboard | 1 Core loop | M | 011 | planned |
-| 100 | Desktop shell: ex_tauri window, tray, notifications, keychain | 10 Desktop | L | 001, 013 | planned |
-| 101 | Release pipeline: signing, notarization, auto-update | 10 Desktop | L | 100 | planned |
-| 110 | Luerl sandbox + executable skills | 11 Sandbox | L | 041 | planned |
-| 120 | OSS hygiene and governance, audited | 13 Open source & donation | S | 000, 090 | planned |
-| 121 | Supply chain: SBOM, Sigstore, SLSA, Scorecard, MCP Registry entry | 13 Open source & donation | M | 101, 120 | planned |
-| 122 | Foundation Sandbox proposal package (owner-gated, needs legal review) | 13 Open source & donation | M | 120, 121 | planned |
-| 123 | Extract shared components as Hex packages | 13 Open source & donation | L | 062, 040, 120 | planned |
-
-## Minimum viable Trinity
-
-The subset that is a usable product on its own: **000, 010, 011, 012, 013, 020, 021, 022, 030, 031, 090, 100.**
-
-A desktop agent that talks to any provider, uses filesystem, web and shell tools under a permission gate, remembers
-you, searches its own history, shows what it costs, and runs as a real application. It omits skills, automation,
-MCP, gateways, subagents and the sandbox, all of which are additive rather than load-bearing.
-
-Named so that work can stop at a shippable point rather than wherever the calendar stops. Sizes remain estimates
-for planning, not deadlines; the point here is the cut line, not a duration.
-
-## Dependency graph (what can run in parallel)
-
-```
-000 ─┬─ 001 ─────────────────────────────────────────── 100 ── 101
- ├─ 002 (supply chain, early)
- ├─ 003 (FIPS build leg; runs 024's FIPS tests)
- └─ 010 ─┬─ 011 ─┬─ 090
- │ └─ 012 ─┬─ 013 ── 021 ── 022 ── 024 ─┬─ 061 ── 062
- │ │ ├─ 025 (also needs 010)
- │ │ └─ 026
- │ ├─ 020 ─┬─ 040 ── 041 ── 110
- │ │ ├─ 059 ── 060 ── 061
- │ │ └─ 080 ── (081 optional, also needs 061)
- │ ├─ 023
- │ ├─ 030 ─┬─ 033
- │ │ └─ 034
- │ ├─ 050
- │ └─ 070 ─┬─ 071
- │ └─ 072
- └─ 031 ─┬─ 032
- └─ 034
-```
-
-After 012, multiple branches are independent. If running more than one coding agent, assign disjoint branches
-(e.g. one on 020→022, another on 030→032). Merge order is by ID within a phase.
-
-## Change log
-
-| Date | Change |
-|---|---|
-| 2026-09-05 | Initial plan. 26 slices, 8 milestones. |
-| 2026-09-05 | Project named Trinity. |
-| 2026-09-05 | Standards pass: MCP 2026-07-28 is the target; anubis_mcp (≤ 2025-11-25, LGPL) no longer assumed; 059 spike added; 060/061 rewritten; 062 added (MCP RS auth); 033 (AGENTS.md) and 081 (A2A, optional) added. ADR-0007. |
-| 2026-09-05 | Enterprise auth folded into Trinity's own MCP authorization server plus the managed-authorization extension (062, resized to L). Open-source posture made first-class: ADR-0012, OSS hygiene from commit 1 in slice 000, phase 13 (120–123), milestone M9. |
-| 2026-09-05 | Authority made an adapter behind a behaviour rather than a mode (ADR-0008, ADR-0010). ADR-0009 opened for the Jido question, decided at the 012 checkpoint. Slice 024 added: effect catalog, `TRINITY_AUTHORITY`, local receipts. Alignment appendices on 012, 020, 021, 022, 023, 030, 032, 040, 041. |
-| 2026-09-05 | Review pass before commit 1. Counts in the three entries above were typed, not derived, and none matches the tree; a decreasing count is impossible under insert-never-renumber. Derived this date: `find slices -name SLICE.md | wc -l` → **38**. Milestones are derived from the Milestones table, not from memory. From here, any count in this log names the command that produced it. Entries above are not rewritten. |
-| 2026-09-06 | Plan corrections, round 2. **Supersedes the slice count in the entry above:** that entry derived **38** on 2026-09-05, before slices 082, 083 and 084 were withdrawn and slice 034 was added. Re-derived this date, not adjusted by arithmetic: `find slices -name SLICE.md | wc -l` → **36**. The entry above is not rewritten. `scripts/plan_check.sh` now enforces this count, the acceptance-criteria numbering, the Definition-of-Done ranges, ROADMAP/SLICE.md agreement, and the absence of references to paths not in `git ls-files`. |
-| 2026-09-20 | The MCP phase replanned under owner decisions of 2026-09-08, recorded in ADR-0007 decisions 5 to 8: beam_mcp 0.8.0 is the server core; 059 measures the capability gap and probes the `:server` seam (S/M); 060 is Trinity's thin driver (L, the M/L condition decided); the OAuth client role moves from 060 to 062; 061 carries a named blocker on the MRTR wrapper; the four `M5 Always-on` headers on 059 to 062 read M5a Automates, as this file has since 2026-09-08. R14 re-scoped, R15 closed. Re-derived this date: `find slices -name SLICE.md | wc -l` → **36**. |
-| 2026-09-20 | Four slices added under the accepted 2026-09-20 plan: 002 (supply chain, early: SBOM, provenance, TLS floor; counted in M9), 003 (FIPS build leg from source; counted in M2 because it runs 024's FIPS properties), 025 (encryption at rest and the key-custody seam) and 026 (store-and-forward receipts; blocked on the external plane's answer). Re-derived this date: `find slices -name SLICE.md | wc -l` → **40**. |
-| 2026-09-22 | Slice 062 amended under the owner's decision: the production profile is an OAuth client and resource server against an external authorization server, the embedded authorization server is the non-default personal profile refused under an external authority adapter, and Enterprise Managed Authorization (ID-JAG redemption at Trinity's own server) leaves the slice as a deferral with a lift condition (slice 062 NOTES, "Amendment"). AC3 and AC6 removed; the row's title follows. |
-| 2026-09-22 | The gate gained a release check (`scripts/prod_check.sh`: prod compile, headless release assemble, runtime config eval) and `package.yml` gained a `branches:` trigger beside its tags one. Both follow slice 062's F5: a plug's `init/1` is escaped at compile time under prod, so a closure there broke the release build while `mix gate`, which runs in `:test`, stayed green on every leg, and the tags-only packaging trigger meant the first packaging run of the slice was the one after approval. Measured before adopting: the release check costs 52 s cold and 2 s warm, and it is red on the original defect. |
-| 2026-09-22 | Recorded with the change above, so it is not rediscovered: the `branches:` trigger on `package.yml` is the right shape **now**, while packaging changes are rare and a slice's release correctness is proven by `scripts/prod_check.sh`. When **M6** makes signed release artifacts the deliverable (090 to 101), the intended replacement is a scheduled `package` run on `main` (a cron trigger) instead of a per-branch one: it keeps slice branches quiet, bounds a packaging regression to a day, and packages the tree that ships rather than the branch that proposed it. Owner: the product owner. Lift condition a stranger can check: the first slice of M6 that produces a signed artifact. Measured 2026-09-22 for the comparison: a three-OS `package` run is about 16 minutes wall clock, `scripts/prod_check.sh` 52 s cold and 2 s warm, and CI minutes are free because the repository is public. |
diff --git a/SECURITY.md b/SECURITY.md
index f5c6576..010f91a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -9,8 +9,20 @@ vulnerability.
Include what you did, what happened, and what you expected. A proof of concept helps; a working
exploit is not required and is not expected.
-You will get an acknowledgement. Trinity has a single maintainer today (see `MAINTAINERS.md`),
-so response times are best effort and are not a service commitment.
+**You will get an acknowledgement within 14 days**, and in practice sooner. Trinity has a single
+maintainer today (see `MAINTAINERS.md`), so that is the target the project holds itself to rather
+than a contractual commitment; if you have not heard back in 14 days, send the report again.
+
+We ask for coordinated disclosure: give the project a reasonable period to publish a fix before
+disclosing publicly, and we will credit you in the release notes unless you ask otherwise.
+
+## Known issues in dependencies
+
+Advisories against dependencies are tracked openly rather than left implicit. Dependency and
+licence audits run on every commit, and automated dependency alerts are enabled on the
+repository. Where an advisory cannot be resolved immediately, it is recorded in
+`docs/06-risk-register.md` with the reason, the owner and the condition that lifts it, and the
+scope of what it does and does not affect is stated.
## Scope
diff --git a/ci/fips/Containerfile b/ci/fips/Containerfile
index e619bb5..6fbcaac 100644
--- a/ci/fips/Containerfile
+++ b/ci/fips/Containerfile
@@ -21,7 +21,7 @@ ARG OTP_SHA256=5231ba18f31f8041c2d6514cc8842e46954d3b39a53f1617f03f2abe6fea59c7
ARG ELIXIR_SHA256=ea7ff98bc1ed76c663a5d034c863c6fd37e65c9c855b6bbf6ccb2034d806673c
# The toolchain OTP's configure needs, plus what the gate's steps call from the shell: git
-# (plan_check, the names and secrets census), python3 with PyYAML (plan_check rule 12), gcc
+# (the names and secrets censuses), python3 with PyYAML, gcc
# (exqlite compiles SQLite; mdex_native's precompiled artifact loads on glibc), procps-ng (the
# shell tool's tests read `ps`), diffutils (the leg diffs the supports listing; UBI's base has no
# `diff`, found on the leg's first run). Kept in the image on purpose: it is a CI image, not a runtime.
diff --git a/config/runtime.exs b/config/runtime.exs
index 5147afe..a50bc2f 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -195,7 +195,7 @@ if config_env() == :prod do
# limit, not an oversight: sessions and signed cookies do not survive a restart of the
# packaged app. Persisting a secret means writing a credential to the user's disk and
# deciding its file mode, its rotation and what happens when it is copied to another machine
- # (CLAUDE.md section 7 puts that in front of the owner, and slice 100 owns the desktop
+ # (a decision of that kind belongs to the operator, and the desktop keychain is later work
# session story. A spike that quietly invented a credential store would be the larger sin.
secret_key_base =
System.get_env("SECRET_KEY_BASE") || Base.encode64(:crypto.strong_rand_bytes(48))
diff --git a/config/test.exs b/config/test.exs
index f4e1934..959926b 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -155,7 +155,7 @@ end
# through the plug pipeline without a socket. It is wrong for the one thing this slice has to
# establish: `Trinity.Smoke` asks the endpoint which port it actually bound, and against a
# non-serving endpoint that question returns `{:error, :no_server_found}`: a red at an
-# earlier fault than the claim, which under CLAUDE.md section 8 demonstrates nothing.
+# earlier fault than the claim, which under docs/03's rules of evidence demonstrates nothing.
#
# `port: 0` is the same ephemeral bind the packaged binary uses, so the test exercises the
# real path rather than a fixed 4002 that a second run or a stray process can take. Nothing
diff --git a/credo_checks/no_eval_on_model_output.ex b/credo_checks/no_eval_on_model_output.ex
index 20e635f..7c82c21 100644
--- a/credo_checks/no_eval_on_model_output.ex
+++ b/credo_checks/no_eval_on_model_output.ex
@@ -2,7 +2,8 @@
# SPDX-License-Identifier: Apache-2.0
defmodule Trinity.Credo.NoEvalOnModelOutput do
@moduledoc """
- CLAUDE.md §5: "No `Code.eval_string` on model output. Ever."
+ docs/03-conventions.md, Engineering rules: `Code.eval_string` and its family are never applied
+ to anything a model produced.
The rule names one function, and one function has an obvious bypass, so this check covers the
whole evaluation family. Sandboxed execution goes through `Trinity.Sandbox` (slice 110), never
@@ -64,7 +65,7 @@ defmodule Trinity.Credo.NoEvalOnModelOutput do
defp issue_for(issue_meta, line_no, trigger) do
format_issue(issue_meta,
- message: "#{trigger} evaluates code at runtime; forbidden on model output (CLAUDE.md §5)",
+ message: "#{trigger} evaluates code at runtime; forbidden on model output (docs/03)",
trigger: trigger,
line_no: line_no
)
diff --git a/docs/01-architecture.md b/docs/01-architecture.md
index 0b29e83..e5dbec7 100644
--- a/docs/01-architecture.md
+++ b/docs/01-architecture.md
@@ -192,7 +192,7 @@ priv/repo/migrations/
priv/skills/ bundled skills (SKILL.md)
priv/personas/ default SOUL.md
test/support/ Mox definitions, factories, fake tools/providers
-slices/ this plan's per-slice specs and proofs
+docs/ architecture, data model, security model, standards, ADRs
docs/ this plan's docs and ADRs
tauri/ desktop shell (Slice 001+)
```
diff --git a/docs/03-conventions.md b/docs/03-conventions.md
index 1e91de7..3b8dd64 100644
--- a/docs/03-conventions.md
+++ b/docs/03-conventions.md
@@ -25,7 +25,7 @@
- Coverage: reported in PROOF.md each slice and written to `coverage.tsv` in the repo root, one row per slice,
so there is a baseline to compare against. The gate reads that file: a drop of more than 3 points against the
previous slice fails until a NOTES.md justification names the reason. The rule as originally written stored no
- baseline, so nothing could check it, which is the pattern CLAUDE.md §8 forbids.
+ baseline, so nothing could check it, which is the pattern the rules of evidence forbid.
## Tools (slice 020)
@@ -62,6 +62,54 @@
inline script needs the request's nonce (`@csp_nonce`), and there is one, the theme script in the root layout.
- The chat runs without a key in development: `TRINITY_FAKE_PROVIDER=1 mix phx.server`.
+## Engineering rules
+
+These are enforced by the gate where a tool can enforce them, and by review where none can.
+
+- **Modularity.** Every pluggable concern is a `@behaviour` behind a registry. Adding a tool, a
+ model provider or a gateway is a new module and a configuration entry, never an edit to a core
+ module.
+- **Boundaries.** The dependency rules in `docs/01-architecture.md` are compiled, not advisory:
+ `boundary` reports a violation as a warning and the gate compiles with warnings as errors, so an
+ architectural violation fails the build. This is what makes the layering in that document a
+ property of the tree rather than a diagram.
+- **OTP first.** One process per session; everything supervised; no bare `spawn`; work that can
+ fail runs under a `Task.Supervisor`.
+- **No runtime evaluation of model output.** `Code.eval_string` and its family are never applied to
+ anything a model produced. A custom Credo check (`credo_checks/no_eval_on_model_output.ex`)
+ enforces it over the whole tree. Sandboxed execution goes through `Trinity.Sandbox`.
+- **Persistence.** All writes go through the repository owner; schema changes need migrations, and
+ a migration must work on SQLite (primary) without breaking PostgreSQL (secondary).
+- **Streaming.** Model output streams over PubSub topics and is never accumulated unbounded in a
+ LiveView's state.
+- **Types.** Elixir's type checker is part of the gate; public functions carry typespecs.
+- **Dependency versions** come from `VERSIONS.md`. A newer release is proposed and reviewed, never
+ adopted silently.
+- **Tests do not reach the network.** Provider calls are mocked against behaviours; an opt-in
+ tag exists for tests that deliberately exercise a real provider.
+
+## Rules of evidence
+
+The standard a change is held to, and the reason the proof sections of this repository can be
+checked by a stranger.
+
+- **A failing test comes before the fix.** A test for a claimed property is committed failing
+ first, by name, and the fix references it. A failure that occurs at an earlier fault than the
+ claim has demonstrated nothing.
+- **Populations come from the tree.** Any statement of the form "every X" names the command that
+ enumerates X. A hand-written list whose membership is the thing in question is the defect.
+- **"Verified" names its command and its exit code**, from the process that ran it. A pipeline's
+ exit status is not the command's.
+- **Corrections append.** Records grow by dated append; a wrong line stays and is corrected below
+ it. Nothing is rewritten to look as though it was always right.
+- **Values come from the tree or from a person.** No date, count, commit hash, digest or line
+ number is written from memory; the deriving command is named.
+- **A deferral carries an owner, an absolute date and a lift condition** a stranger can check.
+- **A name is a claim.** A function called `verify` that records without verifying is renamed or
+ fixed.
+- **Every egress is enumerated.** Anything the system sends off the machine is listed with what
+ crosses in the clear and what crosses hashed.
+
## Git
- `main` is always green (gate passes) and always releasable.
@@ -81,22 +129,22 @@
## Definition of Done
-See `CLAUDE.md` §2. The short version: gate green, every acceptance criterion proven, tests present, docs updated,
-PROOF.md written, ROADMAP updated, final commit + tag.
+Gate green, every acceptance criterion proven, tests present, docs updated,
+the engineering record written, and the change tagged.
## Proof standard
-`PROOF.md` follows `templates/PROOF-TEMPLATE.md`. Each acceptance criterion gets:
+Each acceptance criterion of a change gets:
- the command(s) run,
- the relevant output (trim long output; keep the lines that prove the point),
-- for UI: a screenshot/GIF path under `slices/NNN-*/proof/`,
+- for UI: a screenshot or short GIF held with the change's engineering record,
- for process/crash behaviour: the test name and its output.
Plus: `mix gate` output, `mix test --cover` summary line, `git log --oneline main..HEAD`.
## ADRs
Any decision that changes architecture, stack, data model, or process gets an ADR in `docs/adr/` using
-`templates/ADR-TEMPLATE.md`, numbered sequentially, status `proposed` / `accepted` / `superseded by ADR-XXXX`. `proposed` is a real state: ADR-0004, 0007 and 0009 each wait on a measurement from a named slice.
+a consistent template, numbered sequentially, status `proposed` / `accepted` / `superseded by ADR-XXXX`. `proposed` is a real state: ADR-0004, 0007 and 0009 each wait on a measurement from a named slice.
## Documentation hygiene
diff --git a/docs/04-slice-process.md b/docs/04-slice-process.md
deleted file mode 100644
index 2d9fa23..0000000
--- a/docs/04-slice-process.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# 04: Slice process
-
-A slice is the unit of planning, work, proof, review, and history. It is small enough to review in one sitting
-and large enough to be worth a tag.
-
-## Lifecycle
-
-```
-planned ──(deps approved)──▶ ready ──(agent starts)──▶ in_progress ──(DoD met)──▶ done ──(human reviews)──▶ approved
- │ │
- └──── blocked (agent asks) ◀──────── changes requested
-```
-
-A `blocked` slice carries, in ROADMAP.md and in its NOTES.md, the thing it waits on and a lift condition someone
-outside the project could check. "Blocked" without both is a status with no content.
-
-## Gates
-
-| Gate | Who | What passes |
-|---|---|---|
-| G0 Ready | human | Dependencies approved; SLICE.md reviewed; questions answered |
-| G1 Plan | agent | Agent posts a ≤ 15-line implementation plan in NOTES.md before coding (files, modules, tests), **plus the slice's `[manual]` verification queue**. Human may veto within the same turn; otherwise proceed. |
-| G2 Gate | CI/agent | `mix gate` green |
-| G3 Proof | agent | PROOF.md complete; every acceptance criterion has evidence |
-| G4 Review | human | Reads PROOF.md, spot-checks code, runs the manual verification if listed. Sets `approved` or `changes requested` in ROADMAP.md |
-
-## The per-slice folder
-
-```
-slices/NNN-short-name/
-├── SLICE.md # spec (written by the plan; may be amended by the human)
-├── NOTES.md # agent: plan, deviations, follow-ups, version proposals
-├── PROOF.md # agent: evidence
-└── proof/ # screenshots, GIFs, logs
-```
-
-## Amending a slice
-
-The human may edit SLICE.md before `in_progress`. After that, changes go into NOTES.md as "Amendment" entries
-with the human's approval quoted, and the acceptance criteria list in SLICE.md is updated in the same commit.
-
-## Inserting a slice
-
-Numbering has gaps (000, 001, 010, 011 …). Insert `NNN` between neighbours, add to ROADMAP.md with dependencies,
-add a SLICE.md from the template. Never renumber existing slices.
-
-## Splitting a slice
-
-If a slice grows past L, split it: `NNNa`, `NNNb` with the original as parent. Record in ROADMAP change log.
-
-## What "approved" means
-
-The human has read the proof and accepts the slice as delivered. It is now history. Bugs found later are new
-slices or fix commits referencing the original (`fix(s022): …`), never rewrites.
-
-## Working with multiple coding agents
-
-Assign disjoint dependency branches (see ROADMAP graph). Each agent owns its slice branch. Merges to `main`
-happen in ID order within a phase, by the human or a designated agent. Conflicts are resolved on the later branch.
diff --git a/docs/06-risk-register.md b/docs/06-risk-register.md
index 0bd8846..1051a5b 100644
--- a/docs/06-risk-register.md
+++ b/docs/06-risk-register.md
@@ -12,8 +12,8 @@
| R7 | Context compaction loses critical information | Med | High | Eval set regression | Always-on tier preserved verbatim; compaction eval harness in 023 | 023 |
| R8 | Prompt injection via tool output / skills / web | High | High | none | Untrusted-content framing; scanner on skills; permission gate on side effects; see 07-security-model | 021, 041 |
| R9 | Oban on SQLite limitations (no Pro workflows) | Low | Med | Need for multi-step durable graphs | Postgres path kept alive in CI matrix | 050 |
-| R10 | Agent scope creep across slices | Med | Med | PROOF shows work outside spec | CLAUDE.md rules; NOTES follow-ups; human review | all |
-| R11 | Stale or single-maintainer libs (**boundary**, hnswlib, ex_tauri, sqlite_vec, nostrum, telegex) | High | Med–High | No release in 6 months. Measured 2026-09-05: boundary 2024-09-25, sqlite_vec 2024-11-19, telegex 1.9.0-rc.0 2024-09-18, nostrum 2025-03-02, the trigger already fires for four of them | All behind behaviours; vendor if needed. **boundary is the highest-consequence one**: ADR-0001, docs/01, CLAUDE.md §5 and Slice 000 AC4 all rest on it, and it is unverified on Elixir 1.20. Probe it before anything is built on it | 000, 032, 072 |
+| R10 | Scope creep beyond a change's stated specification | Med | Med | The engineering record shows work outside the specification | The conventions in docs/03; recorded follow-ups; maintainer review | all |
+| R11 | Stale or single-maintainer libs (**boundary**, hnswlib, ex_tauri, sqlite_vec, nostrum, telegex) | High | Med–High | No release in 6 months. Measured 2026-09-05: boundary 2024-09-25, sqlite_vec 2024-11-19, telegex 1.9.0-rc.0 2024-09-18, nostrum 2025-03-02, the trigger already fires for four of them | All behind behaviours; vendor if needed. **boundary is the highest-consequence one**: ADR-0001, docs/01 and the engineering rules in docs/03 all rest on it, and it is unverified on Elixir 1.20. Probe it before anything is built on it | 000, 032, 072 |
| R12 | Secrets leak into logs/DB/commits | Low | High | grep hits in CI | `mix gate` includes a secret scan (gitleaks-style regex) from 000; Secrets module from 100 | 000, 100 |
| R13 | Scope drifts toward matching other agents' breadth instead of shipping depth | Med | Low | Slice scope grows during a phase | Non-goals in docs/00 are binding; breadth is a later decision, not a default | none |
| R14 | beam_mcp's will-not-implement page keeps MRTR, the client role and OAuth out of the core; each is Trinity's to build above it (re-scoped 2026-09-20, was library lag) | High | Med | 059 FINDINGS names a gap 060 to 062 did not plan for | The layering rule in ADR-0007 decision 6; the `:server` seam asked for, with the fork as recorded fallback; boundaries around `Trinity.MCP.*` | 059 |
@@ -24,3 +24,4 @@
| R24 | A native NIF in the desktop bundle: `mdex_native` (the markdown renderer's Rust core, added at 013) is the first, and slice 001 proved the packaging chain with a pure-BEAM release | Med | Med | The `package` workflow fails on one of its three operating systems after 013, or 100's cross-target build finds no artifact for a target | `rustler_precompiled` ships artifacts for every target the chain builds for (013 NOTES.md, measured against the checksum file); the `package` run on 013's `mix.lock` change is the first proof on three runners; lift condition: that run green on all three, cited in 013's PROOF.md; a fallback is `MDEX_NATIVE_BUILD=1` with the pinned Rust toolchain already in the tree | 013, 100 |
| R24 | Fired 2026-09-20, in slice 013: neither precompiled `mdex_native` artifact loads in Burrito's musl ERTS (both need glibc's `libgcc_s`; `_dl_find_object: symbol not found`). Lifted the same day by the owner's decision: the linux package builds the NIF from source for musl with Zig as the linker (`scripts/zig-cc-musl`, config/config.exs, `rustler` at build time), measured on a fresh install of the packaged binary rendering the real provider's answer; the `--smoke` path now prints `TRINITY_SMOKE_MARKDOWN=ok` and exits 3 otherwise, and the `package` workflow reads that line on every target, so a dead NIF cannot pass again. Residual: macOS and Windows use the precompiled artifact, and the `package` workflow has yet to run green there since the slice/011 tag (an older failure at "Serves HTTP 200", now diagnosed by the step's log). The row above stands as written | none | none | none | none | 013 |
| R23 | EMA / ID-JAG is beta everywhere (vendors label it so); IETF draft still moving | Low (re-scoped 2026-09-22) | Med | Spec revision breaks the exchange at the external authorization server, which is where it happens: Trinity redeems no assertion since the owner's decision of 2026-09-22 (slice 062 NOTES) | The resource server validates whatever audience-bound token that server issues, so a draft revision is that server's upgrade, not Trinity's; the row stays until the deferral lifts | 062 |
+| R25 | `glib` 0.18.5 carries GHSA-wrw7-89jp-8q8g (medium, unsoundness in `Iterator` and `DoubleEndedIterator` impls); first patched in 0.20.0 | Low | Med | The advisory is open against `src-tauri/Cargo.lock`; measured 2026-09-23 | It is transitive through the GTK 0.18 stack (`gtk` 0.18.2, `webkit2gtk` 2.0.2) that Tauri 2.11.5 requires on Linux, so it is fixed by a Tauri release built on gtk-rs 0.20 and not by a direct bump. **Scope:** the desktop shell only. The `headless` release is `steps: [:assemble]` with no Burrito and no `src-tauri`, so a server deployment does not ship this dependency. Owner: the maintainer. Lift condition: a Tauri release whose Linux stack resolves `glib` >= 0.20, adopted by the desktop shell work | 100 |
diff --git a/docs/adr/0004-desktop-shell-ex-tauri.md b/docs/adr/0004-desktop-shell-ex-tauri.md
index 2f3de6a..ddfb28f 100644
--- a/docs/adr/0004-desktop-shell-ex-tauri.md
+++ b/docs/adr/0004-desktop-shell-ex-tauri.md
@@ -127,7 +127,7 @@ What has been established about the *shell*, as opposed to the sidecar:
* **`mix ex_tauri.dev` opens a real window showing the scaffold** — on **one** operating system,
Linux, run by the owner on 2026-09-07, with a screenshot at
- `slices/001-packaging-spike/proof/ac4-linux-window.png`. That is the first evidence in this
+ the packaging spike's own screenshot. That is the first evidence in this
project that the chosen shell does the thing it was chosen for.
* The shell **compiles** on all three targets in CI. Compiling is not running, and no job claims
otherwise.
diff --git a/docs/adr/0009-jido-for-the-action-effect-layer.md b/docs/adr/0009-jido-for-the-action-effect-layer.md
index f6e0155..bf6a3cd 100644
--- a/docs/adr/0009-jido-for-the-action-effect-layer.md
+++ b/docs/adr/0009-jido-for-the-action-effect-layer.md
@@ -38,7 +38,7 @@ takes; the register carries the row and the argument, and no slice here does.
## Decision, appended 2026-09-20 (later): no Jido at all
The Slice 012 checkpoint ran on 2026-09-20 against `jido` 2.3.3, `jido_action` 2.3.2 and `jido_signal` 2.2.0,
-read from their Hex tarballs (the measurements and the commands are in `slices/012-*/NOTES.md`). Its findings:
+read from their Hex tarballs. Its findings:
(a) M4 is assertable against `Jido.Action` and M3 only with a census, because `Jido.Exec.run/4` takes caps as
call-site options; (b) `Jido.Agent` adds sensors, a scheduler, signal routing and worker pools the plan assigns
elsewhere, and an `AgentServer` that executes tools through `Jido.Exec`, a second path beside
diff --git a/docs/perf.md b/docs/perf.md
index 92bd090..f8e79bc 100644
--- a/docs/perf.md
+++ b/docs/perf.md
@@ -1,7 +1,7 @@
# Performance measurements
Numbers this tree is built on, each with the command or run that produced it and the date. A slice that
-changes a number appends a dated row; nothing here is typed from memory (CLAUDE.md section 8).
+changes a number appends a dated row; nothing here is typed from memory (docs/03-conventions.md, rules of evidence).
## Semantic memory (slice 032)
@@ -26,7 +26,7 @@ scripts/embed_bench.exs`) unless a row names another command.
The hosted alternative (`nvidia:embed`, nemotron-3-embed-1b, opt-in only): 2048 dimensions, 208 ms p50 and
314 ms max over 5 single embeds, 627 ms for a batch of 32, and cosines 0.71 and 0.752 on the two pairs above:
-its raw vectors do not separate at the slice's thresholds (slices/032-semantic-memory/NOTES.md).
+its raw vectors do not separate at the thresholds this measurement uses.
### Memory
diff --git a/lib/mix/tasks/trinity.secrets.scan.ex b/lib/mix/tasks/trinity.secrets.scan.ex
index 0e7b953..c67213d 100644
--- a/lib/mix/tasks/trinity.secrets.scan.ex
+++ b/lib/mix/tasks/trinity.secrets.scan.ex
@@ -5,7 +5,7 @@ defmodule Mix.Tasks.Trinity.Secrets.Scan do
@moduledoc """
Scans tracked files for common API-key shapes. Deliberately shallow: it is a tripwire, not a
- secret manager, and `CLAUDE.md` §4 keeps real keys in the environment or the OS keychain.
+ secret manager; real keys live in the environment or the OS keychain, never in the tree.
"""
use Boundary, classify_to: Trinity
diff --git a/lib/trinity/gateways/adapter.ex b/lib/trinity/gateways/adapter.ex
index c12295d..32f9985 100644
--- a/lib/trinity/gateways/adapter.ex
+++ b/lib/trinity/gateways/adapter.ex
@@ -46,7 +46,7 @@ defmodule Trinity.Gateways.Adapter do
@doc """
The adapter's name as rows and receipts carry it: the last segment of its module, underscored,
so `Trinity.Gateways.Console` is `"console"`. It is derived and not declared, because a name a
- module can contradict is a second source of truth (CLAUDE.md section 8: a name is a claim).
+ module can contradict is a second source of truth (docs/03: a name is a claim).
"""
@spec name(module()) :: String.t()
def name(adapter) when is_atom(adapter) do
diff --git a/lib/trinity/smoke.ex b/lib/trinity/smoke.ex
index b5ddd92..77fd5c1 100644
--- a/lib/trinity/smoke.ex
+++ b/lib/trinity/smoke.ex
@@ -161,7 +161,7 @@ defmodule Trinity.Smoke do
Appended **after** `TrinityWeb.Endpoint` in `Trinity.Application`, because the task asks the
endpoint which port it bound and a child cannot ask that of a sibling that has not started.
- It is a supervised child rather than a `Task.start/1` because CLAUDE.md section 5 says
+ It is a supervised child rather than a `Task.start/1` because docs/03's engineering rules say
supervise everything and no bare spawn, and because running it inside `start/2` would halt
the VM from within the OTP boot sequence: a boot crash rather than a clean exit. `ps`
cannot tell those apart from the outside; the exit code can, and AC7 reads both.
diff --git a/mix.exs b/mix.exs
index 2c70311..4366cd9 100644
--- a/mix.exs
+++ b/mix.exs
@@ -275,7 +275,7 @@ defmodule Trinity.MixProject do
"phx.digest"
],
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"],
- # The quality gate. `mix gate` must exit 0 before every commit (CLAUDE.md section 2).
+ # The quality gate. `mix gate` must exit 0 before every commit (CONTRIBUTING.md).
#
# The compile step MUST carry --warnings-as-errors. Measured at slice 000 G1: boundary
# reports violations as warnings, so without that flag a boundary violation exits 0 and
@@ -320,17 +320,7 @@ defmodule Trinity.MixProject do
"trinity.secrets.scan",
"trinity.reuse",
"test",
- "trinity.coverage",
- # The plan's own consistency, as the gate's final step rather than a second command
- # with a second exit code. Added at slice 001 G4, for a mistake made three times in
- # this slice: `mix gate` and `scripts/plan_check.sh` were run as a pair, the gate's
- # `exit=0` was read, and `plan_check exit=1` on the line below it was not: twice
- # reaching the remote. Two results printed and one read is a reporting failure the
- # tooling can remove, so it is removed: **one command, one exit code.**
- #
- # `cmd` runs it as its own OS process, the same reason `hex.audit` uses it: the step
- # gets its own exit code rather than sharing the alias's.
- "cmd ./scripts/plan_check.sh"
+ "trinity.coverage"
]
]
end
diff --git a/scripts/plan_check.sh b/scripts/plan_check.sh
deleted file mode 100755
index 53b87bf..0000000
--- a/scripts/plan_check.sh
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/usr/bin/env bash
-# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
-# SPDX-License-Identifier: Apache-2.0
-# plan_check.sh: enforces the plan-consistency rules that were previously prose.
-#
-# Every rule below was a finding that closed on a hand check and stayed broken.
-# Populations derive from `git ls-files`; nothing here is a hand list.
-#
-# Scope note for check 6: it validates references to PLAN artifacts only:
-# docs/, docs/adr/, slices/, templates/ and the root records. Paths under
-# lib/, test/, priv/, config/, .github/ and tauri/ are deliberately excluded:
-# they name code this plan has not built yet, so their absence is expected and
-# is not evidence of a dangling reference.
-#
-# Exit 0 = all checks pass. Exit 1 = at least one failure, each printed with its location.
-
-set -uo pipefail
-cd "$(dirname "$0")/.." || exit 2
-
-fail=0
-report() { printf '%s\n' "$1"; fail=1; }
-section() { printf '\n== %s ==\n' "$1"; }
-
-slice_files=$(git ls-files 'slices/*/SLICE.md')
-tracked=$(git ls-files)
-
-# ---------------------------------------------------------------- 1 + 2
-section "1. Acceptance criteria numbered contiguously from 1"
-section_2_pending=""
-for f in $slice_files; do
- nums=$(awk '/^## Acceptance criteria/{fl=1;next} /^## /{fl=0} fl && /^[0-9]+\./{n=$0; sub(/\..*/,"",n); print n}' "$f")
- count=$(printf '%s\n' "$nums" | grep -c . )
- expected=$(seq 1 "$count" 2>/dev/null)
- if [ "$(printf '%s' "$nums")" != "$(printf '%s' "$expected")" ]; then
- report "FAIL $f: criteria numbered [$(echo $nums)] expected [$(echo $expected)]"
- fi
- section_2_pending="$section_2_pending$f $count"$'\n'
-done
-
-section "2. Definition of Done range equals the criterion count"
-while read -r f count; do
- [ -z "${f:-}" ] && continue
- dod=$(grep -oE 'AC1[–-][0-9]+' "$f" | head -1 | grep -oE '[0-9]+$')
- if [ -z "$dod" ]; then
- report "FAIL $f: Definition of Done states no AC1-N range"
- elif [ "$dod" != "$count" ]; then
- report "FAIL $f: $count criteria but Definition of Done says AC1-$dod"
- fi
-done <<< "$section_2_pending"
-
-# ---------------------------------------------------------------- 3
-section "3. Manual verification queue section present"
-for f in $slice_files; do
- grep -q '^## Manual verification queue' "$f" || report "FAIL $f: no '## Manual verification queue' section"
-done
-
-# ---------------------------------------------------------------- 4
-section "4. ROADMAP size and status agree with SLICE.md"
-while IFS='|' read -r _ id _ _ size _ status _; do
- id=$(echo "$id" | tr -d ' '); size=$(echo "$size" | tr -d ' '); status=$(echo "$status" | tr -d ' ')
- case "$id" in ''|ID|*[!0-9]*) continue ;; esac
- sf=$(git ls-files "slices/$id-*/SLICE.md")
- if [ -z "$sf" ]; then
- [ "$status" = "withdrawn" ] || report "FAIL ROADMAP id $id: no slices/$id-*/SLICE.md and status is '$status' (only 'withdrawn' may lack one)"
- continue
- fi
- ssize=$(grep -m1 '^| Size ' "$sf" | awk -F'|' '{gsub(/ /,"",$3); print $3}')
- case "$ssize" in S|M|L|S/M|M/L) ;; *) report "FAIL $sf: size '$ssize' is not S, M, L or a conditional S/M or M/L" ;; esac
- [ "$size" = "$ssize" ] || report "FAIL $sf: SLICE.md size '$ssize' but ROADMAP says '$size'"
- sstat=$(grep -m1 '^| Status ' "$sf" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
- if [ -n "$sstat" ] && [ "$sstat" != "see ROADMAP.md" ] && [ "$sstat" != "$status" ]; then
- report "FAIL $sf: SLICE.md status '$sstat' but ROADMAP says '$status'"
- fi
-done < ROADMAP.md
-
-# ---------------------------------------------------------------- 5
-section "5. Change-log slice count equals the tree"
-actual=$(find slices -name SLICE.md | wc -l | tr -d ' ')
-logged=$(grep -oE 'find slices -name SLICE.md \| wc -l` → \*\*[0-9]+\*\*' ROADMAP.md | grep -oE '[0-9]+\*\*$' | grep -oE '[0-9]+' | tail -1)
-if [ -z "$logged" ]; then
- report "FAIL ROADMAP.md: no change-log entry naming 'find slices -name SLICE.md | wc -l'"
-elif [ "$logged" != "$actual" ]; then
- report "FAIL ROADMAP.md: change log says $logged slices, tree has $actual"
-fi
-
-# ---------------------------------------------------------------- 6
-section "6. No reference to a plan path absent from git ls-files"
-# `grep` without -q on purpose: under `set -o pipefail`, `grep -q` exits at the first match
-# and printf can take SIGPIPE writing the rest, and the pipeline then reports printf's 141 as
-# a miss. Measured at slice 003: 1 spurious FAIL in 30 runs on a clean tree (the ADR-0005
-# citation in slices/001). Reading the whole input has no early exit and no race.
-exists_prefix() { printf '%s\n' "$tracked" | grep "^$1" >/dev/null; }
-for f in $(git ls-files '*.md'); do
- # doc and slice path references
- grep -oE '(docs/adr/[0-9]{4}|docs/[0-9]{2}|slices/[0-9]{3}|templates/[A-Za-z-]+\.md)[A-Za-z0-9._-]*' "$f" 2>/dev/null | sort -u | while read -r ref; do
- exists_prefix "$ref" || echo "FAIL $f: references '$ref', absent from git ls-files"
- done
- # ADR-NNNN citations
- grep -oE 'ADR-[0-9]{4}' "$f" 2>/dev/null | sort -u | while read -r adr; do
- n=${adr#ADR-}
- exists_prefix "docs/adr/$n" || echo "FAIL $f: cites '$adr', no docs/adr/$n-*.md in git ls-files"
- done
-done > /tmp/plan_check_paths.$$ 2>/dev/null
-if [ -s /tmp/plan_check_paths.$$ ]; then cat /tmp/plan_check_paths.$$; fail=1; fi
-rm -f /tmp/plan_check_paths.$$
-
-section "6b. No slice depends on a slice that does not exist"
-for f in $slice_files ROADMAP.md; do
- if [ "$f" = "ROADMAP.md" ]; then
- deps=$(awk -F'|' '/^\| [0-9]{3} \|/{print $2":"$6}' ROADMAP.md)
- else
- deps=$(grep -m1 '^| Depends on ' "$f" | awk -F'|' -v F="$f" '{print F":"$3}')
- fi
- printf '%s\n' "$deps" | while IFS=: read -r who list; do
- [ -z "${list:-}" ] && continue
- for d in $(printf '%s' "$list" | grep -oE '\b[0-9]{3}\b' | sort -u); do
- exists_prefix "slices/$d-" || echo "FAIL $f: '$(echo $who)' depends on slice $d, which has no slices/$d-*/SLICE.md"
- done
- done
-done > /tmp/plan_check_deps.$$ 2>/dev/null
-if [ -s /tmp/plan_check_deps.$$ ]; then sort -u /tmp/plan_check_deps.$$; fail=1; fi
-rm -f /tmp/plan_check_deps.$$
-
-# ---------------------------------------------------------------- 7
-section "7. No board identifiers in the tree"
-if git grep -nIE '\bSCR-[0-9]+\b' -- '*.md' 'scripts/*' >/dev/null 2>&1; then
- git grep -nIE '\bSCR-[0-9]+\b' -- '*.md' 'scripts/*' | sed 's/^/FAIL /'
- fail=1
-fi
-
-section "8. Commit messages: no attribution trailers, every commit signed off"
-# Checks the history, not the hook. A bypassed or unconfigured hook still fails here.
-#
-# One named exception, and the reason it exists. The merge commit of slice 013 (pull request
-# #24) was made through `gh pr merge --merge --subject ...` with no `--body`, so GitHub wrote
-# the message and nobody signed it. It is on main under the protected tag slice/013 and cannot
-# be rewritten (CLAUDE.md section 4), so it is listed here by full sha, and only it. The rule
-# that prevents a second one is in docs/03: a merge commit's body carries the sign-off, given
-# to `gh pr merge --body`. Adding a sha to this list is a change to this file, in the open.
-unsigned_merge_exempt="3db7a5ff45773663e1ad31eb02e41655ce36e7b0"
-for c in $(git log --format=%H); do
- body=$(git log -1 --format=%B "$c")
- bad=$(printf '%s\n' "$body" | grep -nE '^(Co-Authored-By: Claude|Claude-Session:|🤖 Generated with)' || true)
- if [ -n "$bad" ]; then
- printf '%s\n' "$bad" | while read -r l; do echo "FAIL commit $c: attribution trailer: $l"; done
- fail=1
- fi
- case " $unsigned_merge_exempt " in *" $c "*) continue ;; esac
- printf '%s\n' "$body" | grep '^Signed-off-by: ' >/dev/null \
- || report "FAIL commit $c: no Signed-off-by line ($(git log -1 --format=%s "$c"))"
-done
-
-section "10. No reference to the pre-move owner path"
-# The repository moved to the ScriptKittyOS organisation. A tracked file still pointing at the
-# old owner path sends people, tooling and citations to a repo that is no longer canonical.
-# The pattern is COMPOSED, so this file does not contain the literal and needs no exemption.
-# Enforcers 2 and 3 each carry a one-entry skip for exactly this reason; here it is avoidable.
-stale_owner="$(printf 'Hack%s' 'Tuah')"
-if git grep -nI "$stale_owner" -- . >/dev/null 2>&1; then
- git grep -nI "$stale_owner" -- . | sed 's/^/FAIL stale owner path: /'
- fail=1
-fi
-
-section "9. Secrets are ignored, as CLAUDE.md claims"
-# CLAUDE.md states ".env* is gitignored". A generator run overwrote .gitignore at
-# slice 000 and silently removed that rule; nothing caught it. This does.
-for f in .env .env.local .env.production; do
- git check-ignore -q "$f" || report "FAIL .gitignore: '$f' is not ignored, but CLAUDE.md says .env* is"
-done
-grep -q '^!\.env\.example$' .gitignore || true
-
-section "11. Slice lifecycle matches the branch"
-# docs/04 defines ready -> in_progress -> done -> approved and nothing checked that a slice ever
-# occupies in_progress. Slice 000 ran G1 to G3 reading `ready`, so CLAUDE.md section 0's own
-# start-of-session check would have found no live slice. That deviation is recorded in slice
-# 000's NOTES.md; this is its enforcer.
-#
-# Only ROADMAP.md is read: check 4 already asserts SLICE.md agrees with it, so checking both
-# would be checking the same fact twice and calling it two.
-#
-# Demonstrating the main half needs care, and the first attempt at it passed vacuously: checking
-# out main reverts THIS FILE to the version without the rule, so the run proves nothing. Bring the
-# branch's script onto main's worktree first:
-# git checkout main && git checkout -- scripts/plan_check.sh
-# then plant a status and run. Recorded so the next person does not repeat it.
-roadmap_status() {
- awk -F'|' -v I=" $1 " '$2==I{gsub(/^ +| +$/,"",$7); print $7}' ROADMAP.md
-}
-
-branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
-case "$branch" in
- slice/*)
- sid=$(printf '%s' "$branch" | sed -n 's|^slice/\([0-9][0-9][0-9]\)-.*|\1|p')
- if [ -z "$sid" ]; then
- report "FAIL branch '$branch': a slice branch is named slice/NNN-short-name (CLAUDE.md section 4)"
- else
- st=$(roadmap_status "$sid")
- case "$st" in
- # `approved` added at slice 001 G4. The lifecycle is ready -> in_progress -> done ->
- # approved, and docs/04 has the owner set `approved` at G4, which happens while the
- # slice branch still exists, because CLAUDE.md section 4 puts the status change in the
- # branch's own commit and the merge comes after. Without this the landing commit
- # cannot pass its own gate, which is where it was found.
- in_progress|done|approved) ;;
- "") report "FAIL ROADMAP.md: on branch '$branch' there is no row for slice $sid" ;;
- *) report "FAIL ROADMAP.md: on branch '$branch', slice $sid reads '$st'; a slice on its own branch is in_progress, done or approved" ;;
- esac
- fi
- ;;
- main)
- for row_id in $(awk -F'|' '/^\| [0-9][0-9][0-9] \|/{gsub(/ /,"",$2); print $2}' ROADMAP.md); do
- [ "$(roadmap_status "$row_id")" = "in_progress" ] &&
- report "FAIL ROADMAP.md: slice $row_id reads in_progress on main; a slice is in progress on its own branch"
- done
- ;;
-esac
-
-section "12. Every workflow and Dependabot file parses as YAML"
-# Found at slice 010 G1, from PR #7: an em dash replaced by a bare colon inside a step name made
-# .github/workflows/package.yml unparseable. GitHub reported a run named after the file path with
-# zero jobs, Dependabot's updater failed on the same file, and the gate stayed green because
-# nothing in it reads those files. The ubuntu runner and the developer machine both carry the
-# Python YAML module; if it is absent the rule says so and fails rather than passing quietly.
-if ! python3 -c 'import yaml' >/dev/null 2>&1; then
- report "FAIL rule 12: python3 with the yaml module is required to parse the workflow files, and it is absent"
-else
- for f in $(git ls-files '.github/*.yml' '.github/*.yaml' '.github/**/*.yml' '.github/**/*.yaml' | sort -u); do
- err=$(python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$f" 2>&1 >/dev/null | tail -1)
- [ -n "$err" ] && report "FAIL $f: not parseable as YAML: $err"
- done
-fi
-
-printf '\n'
-if [ "$fail" -eq 0 ]; then echo "plan_check: PASS"; else echo "plan_check: FAIL"; fi
-exit "$fail"
diff --git a/scripts/prod_check.sh b/scripts/prod_check.sh
index e545b19..c333af1 100755
--- a/scripts/prod_check.sh
+++ b/scripts/prod_check.sh
@@ -21,7 +21,7 @@
# musl (slice 013's mdex finding), builds the Tauri shell, or runs on macOS or Windows. That is
# the `package` workflow's job and it stays the `package` workflow's job.
#
-# POSIX sh, like scripts/plan_check.sh, and like it not run on a Windows developer's machine.
+# POSIX sh, and so not run on a Windows developer's machine; CI covers it there.
# The gate invokes it as `cmd env ERL_AFLAGS= ./scripts/prod_check.sh`: on the FIPS leg it runs
# outside FIPS mode, because compiling prod builds the dependencies and `tokenizers` fetches a
# precompiled NIF over TLS, which OTP's ssl cannot do in the mode. Compiling for release is not
diff --git a/slices/000-toolchain-and-gate/NOTES.md b/slices/000-toolchain-and-gate/NOTES.md
deleted file mode 100644
index f21d37d..0000000
--- a/slices/000-toolchain-and-gate/NOTES.md
+++ /dev/null
@@ -1,422 +0,0 @@
-# Slice 000 — NOTES
-
-## G1 plan v2
-
-Posted 2026-09-06 against `012f595c70eb451e56216814213e64577f6d945e`, superseding v1 at `6c9f3d1` with the owner's
-line-keyed changes. Fifteen lines, execution order, one sentence each, each naming what it produces.
-
-1. **The `boundary` probe runs first (H7)**, standing up a throwaway app on the pinned toolchain with boundaries `A` (deps `[]`) and `B` (deps `[A]`), planting an `A → B` call, and recording in this file both outputs and both exit codes of `mix compile --warnings-as-errors --force` — non-zero naming the violation, zero once removed — and stopping to write a fallback ADR if it does not enforce.
-2. **The ERTS probe runs before the pin is written (B3)**, producing a per-target table for macOS, Linux and Windows with three columns — the ERTS versions `burrito` 1.6.0 offers, what Elixir 1.20.x's compatibility table lists, and what `ex_tauri` 0.2.0's stated requirement excludes — from which the pin is the newest OTP satisfying all three, leaving `ex_tauri`'s actual behaviour to slice 001 to measure.
-3. **The toolchain manager is measured before `mise.toml` exists**, running `which mise asdf; echo "exit=$?"` and recording the output here, then writing the pin file for whichever is present and stopping to ask if neither is.
-4. **The single pin source (M6)** produces `versions.exs`, `lib/mix/tasks/versions.gen.ex` regenerating VERSIONS.md's tables from it, `lib/mix/tasks/versions.verify.ex` reading `versions.exs` and `mix.lock` and never the markdown, and `test/mix/tasks/versions_verify_test.exs` whose red plants an entry disagreeing with `mix.lock` and asserts a non-zero exit naming the package.
-5. **The gate alias (M5)** adds the eight-step `gate` to `mix.exs` with `sobelow --exit` blocking, after first measuring whether `.sobelow-skips` accepts a trailing comment by adding one entry with a reason and recording sobelow's outcome here — if it rejects comments, reasons move to `.sobelow-skips.reasons` keyed by fingerprint with a test asserting every fingerprint carries one.
-6. **The coverage baseline (M7)** produces `coverage.tsv` with columns `slice_id`, `percent`, `sha`, `date`, the gate step that reads it, and `test/coverage_gate_test.exs` whose red seeds a prior row and asserts the step fails on a drop greater than three points and passes at exactly three.
-7. **Enforcer 1** produces `lib/trinity/credo/no_eval_on_model_output.ex` covering the whole family — `Code.eval_string`, `Code.eval_quoted`, `Code.eval_file`, `Code.compile_string`, `Code.compile_quoted` and `:erl_eval` — with one test per function planting a call in a throwaway module under `lib/` and asserting the gate goes red on each.
-8. **Enforcer 2 needs no exemption list**, producing `lib/mix/tasks/trinity.version_form.ex` which matches a literal `MCP` followed by a major number case-sensitively and on word boundaries, so lower-case library version strings are excluded by the boundary rather than by a list, and `test/version_form_test.exs` asserting the skip list holds exactly one path, the task's own source.
-9. **Enforcer 3, the name check**, produces `lib/mix/tasks/trinity.names.ex` and `priv/name_digests.txt` holding the salt and digests only, working by lowercasing, splitting on every non-alphanumeric character, digesting each token with the committed salt and comparing against the committed set, over contents and over paths tokenised the same way; its stated limit is that a zero-site name glued inside a larger token with no separator is not detected, accepted because this is a tripwire against copy-paste drift and copy-paste carries whole tokens; the generator that builds the set lives in `../trinity-private` and never in the tree; its red uses a synthetic token in a test-only set and never a real name; and every zero-site name goes through the digest set while the plain-text set holds only the four platform names.
-10. **Enforcer 4** produces `REUSE.toml` and a `reuse lint` gate step reported separately from line 9 and covering none of it.
-11. **Enforcer 5** produces `test/support/network_guard.ex` blocking outbound sockets on the default test run, proven without a network by a test that opens a local TCP listener and asserts the connect is blocked, and passes against that same listener when tagged `:live` and run under the explicit flag the gate excludes.
-12. **The secret scan** produces `lib/mix/tasks/trinity.secrets.scan.ex` and its test, red on a planted fake key and green after removal.
-13. **Licence and SPDX** add a header to every file `git ls-files` returns that can carry one and record `LICENSE`'s sha256 `cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30` in PROOF.md, closing the ADR-0012 decision-1 gap commit 1 left open.
-14. **Governance and CI** produce `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md` carrying `security@scriptkittyos.com` as the disclosure contact, `GOVERNANCE.md`, `MAINTAINERS.md` and `CITATION.cff` for ADR-0012 decision 2, plus `.github/workflows/gate.yml` running `mix gate`, `scripts/plan_check.sh` and the DCO check.
-15. **The manual queue holds one item**, the owner having answered the other three at G1: confirm at G3 that the line-14 Actions run is green, by pasting its status, because AC5 is a claim about a remote this branch cannot vouch for.
-
-## Owner decisions at G1, 2026-09-06
-
-- **Line 9 digest design approved** as written above. Nothing behind it is blocked.
-- **`SECURITY.md` disclosure contact:** `security@scriptkittyos.com`.
-- **The withdrawn shared-components ADR stays in `../trinity-private`.** Closed; it is not returning to the
- tree and is not to be destroyed. Its number is not cited here, because `plan_check` check 6 fails on a
- citation to an ADR absent from `git ls-files` — which it did on the first draft of this line.
-- **AC5, AC8 and AC9 retagged `[manual]`** in `SLICE.md` in the same commit as this plan. The wider tagging review is
- per slice at its own G1, not a single sweep.
-- **One slice, one branch.** G2 is `mix gate` green; the gate transcript is posted and work stops at G3 with
- `PROOF.md` written.
-
----
-
-## Line 1 — `boundary` probe (H7). Answered: it works.
-
-Run 2026-09-06 in a throwaway app outside the repository, on the pinned toolchain.
-
-```
-$ elixir -v
-Erlang/OTP 28 [erts-16.1.1] [source] [64-bit] [smp:32:32] [ds:32:32:10] [async-threads:1] [jit:ns]
-
-Elixir 1.20.4 (compiled with Erlang/OTP 28)
-$ mix deps | grep -A1 '^\* boundary'
-* boundary 0.10.4 (Hex package) (mix)
- locked at 0.10.4 (boundary) 8baf6f23
-```
-
-Two boundaries: `A` with `deps: []`, `B` with `deps: [A]`. The violation is `A` calling `B.hello()`.
-
-**Baseline, no violation:**
-
-```
-$ mix compile --warnings-as-errors --force ; echo "exit=$?"
-==> boundary
-Compiling 15 files (.ex)
-Generated boundary app
-==> probe
-Compiling 2 files (.ex)
-Generated probe app
-exit=0
-```
-
-**Red, violation planted at `lib/a.ex:5`:**
-
-```
-$ mix compile --warnings-as-errors --force ; echo "exit=$?"
-Compiling 2 files (.ex)
-Generated probe app
-
-warning: forbidden reference to B
- (references from A to B are not allowed)
- lib/a.ex:5
-
-exit=1
-```
-
-**Green, violation removed:**
-
-```
-$ mix compile --warnings-as-errors --force ; echo "exit=$?"
-Compiling 2 files (.ex)
-Generated probe app
-exit=0
-```
-
-**H7 is closed: `boundary` 0.10.4 compiles and enforces on Elixir 1.20.4 / OTP 28.** No fallback ADR is needed,
-and ADR-0001, `docs/01-architecture.md`, CLAUDE.md §5 and AC4 keep the foundation they rest on.
-
-### One measurement the probe added, which the slice needs
-
-`boundary` reports a violation as a **warning**, not an error. With the same violation in place and the flag
-dropped:
-
-```
-$ mix compile --force ; echo "exit=$?"
-Compiling 2 files (.ex)
-Generated probe app
-
-warning: forbidden reference to B
- (references from A to B are not allowed)
- lib/a.ex:5
-
-exit=0
-```
-
-**Exit 0.** So `boundary` is an enforcer only while `--warnings-as-errors` is on the compile step; without it the
-architecture rules are advisory and a violation ships silently. That is the same shape as the `sobelow --exit`
-contradiction M5 was raised for. The gate alias at line 5 must therefore keep `--warnings-as-errors` on `compile`,
-and AC4 is a claim about that flag as much as about `boundary`.
-
----
-
-## Line 2 — ERTS probe (B3). The three constraints do not intersect.
-
-Run 2026-09-06, before any pin file was written.
-
-### Where the numbers come from
-
-`burrito` 1.6.0 was fetched from hex and unpacked. `lib/util/default_erts_resolver.ex` delegates to
-`Burrito.Util.ERTSUniversalMachineFetcher.fetch_version/4`, which names three artifact sources verbatim:
-
-```
-@windows_url "https://github.com/erlang/otp/releases/download/OTP-{OTP_VERSION}/otp_win64_{OTP_VERSION}.exe"
-@linux_url "https://beam-machine-universal.b-cdn.net/OTP-{OTP_VERSION}/linux/{ARCH}/any/otp_{OTP_VERSION}_linux_any_{ARCH}.tar.gz"
-@mac_url "https://beam-machine-universal.b-cdn.net/OTP-{OTP_VERSION}/macos/universal/otp_{OTP_VERSION}_macos_universal.tar.gz"
-```
-
-Each was probed with `curl -I` per OTP version. 200 means the artifact exists; 404 means Burrito cannot fetch it.
-
-### Column 1 — what Burrito 1.6.0 can actually fetch, per target
-
-| OTP | macOS universal | Linux x86_64 | Linux aarch64 | Windows |
-|---|---|---|---|---|
-| 27.3.4.17 | **404** | **404** | not probed | 200 |
-| 28.1.1 | 200 | 200 | 200 | 200 |
-| **28.5** | **200** | **200** | **200** | **200** |
-| 29.0.6 | **404** | **404** | **404** | 200 |
-
-Windows comes from the official OTP release page, so it carries every version; macOS and Linux come from the
-BEAM-machine CDN, which **carries only the OTP 28 line**. Column 1 admits OTP 28.x and nothing else.
-
-### Column 2 — what Elixir 1.20.x lists
-
-Elixir 1.20.4 ships builds for OTP 27, 28 and 29 (`asdf list all elixir | grep '^1\.20\.4'` returns `1.20.4`,
-`1.20.4-otp-27`, `1.20.4-otp-28`, `1.20.4-otp-29`). Column 2 admits 27, 28 and 29.
-
-### Column 3 — what `ex_tauri` 0.2.0 states
-
-```
-$ grep -n 'otp' ex_tauri-0.2.0/mix.exs
-11: elixir: "~> 1.15",
-12: # Limited to OTP 27 due to Burrito pre-compiled ERTS availability
-13: # OTP 28 doesn't have universal macOS binaries available yet
-14: otp_release: "~> 27.0",
-$ grep -n 'OTP' ex_tauri-0.2.0/README.md
-29:- **Elixir** >= 1.15 with **OTP 27** (OTP 28 not yet supported due to Burrito ERTS availability)
-```
-
-Column 3 admits **only OTP 27**, and therefore excludes 28.
-
-### The intersection is empty, and column 3's stated reason is measurably false
-
-Column 1 ∩ column 2 = **OTP 28.x**, newest satisfying both is **28.5**. Column 3 excludes 28. **No OTP version
-satisfies all three constraints**, so line 2's rule as written yields nothing and I am not inventing a tie-break.
-
-The reason column 3 gives for excluding OTP 28 is *"OTP 28 doesn't have universal macOS binaries available yet"*.
-The measurement above says the opposite, and inverts it: **OTP 28 macOS universal returns 200, and OTP 27 macOS
-universal returns 404.** `ex_tauri`'s constraint is a stale fact of exactly the kind B3 was raised to catch — it
-was true when written and the CDN has moved on.
-
-### What this settles about ADR-0005
-
-The OTP 28 pin is **confirmed by measurement**, on the packaging chain's own artifact index, and the ADR-0005
-correction's suspicion that OTP 29 might now be viable is **overturned**: OTP 29 is missing on macOS and on both
-Linux architectures. The newest OTP that Burrito can package for all three targets is **28.5**, not the 28.1.1
-this machine currently runs.
-
-### Stopping here, before line 3 writes a pin
-
-Per CLAUDE.md §7 — a dependency incompatibility, and an ambiguity that changes the design.
-
----
-
-## Line 2, continued — owner's extra measurement. The pin moves to 28.5.0.5.
-
-The owner's decision pinned 28.5 subject to probing every 28.x patch OTP has released above it, taking the newest
-that returns 200 on **all four** targets.
-
-### The population, from two independent sources that agree
-
-```
-$ asdf list all erlang | grep -E '^28\.' | tr '\n' ' '
-28.0-rc1 28.0-rc2 28.0-rc3 28.0-rc4 28.0 28.0.1 28.0.2 28.0.3 28.0.4 28.1 28.1.1 28.2 28.3 28.3.1 28.3.2 28.3.3
-28.4 28.4.1 28.4.2 28.4.3 28.5.0.1 28.5.0.2 28.5.0.3 28.5.0.4 28.5.0.5 28.5.0.6 28.5
-
-$ curl -s "https://api.github.com/repos/erlang/otp/releases?per_page=100" | grep -oE '"tag_name": *"OTP-28[^"]*"' | ...
-28.0 28.0-rc1 ... 28.4.3 28.5 28.5.0.1 28.5.0.2 28.5.0.3 28.5.0.4 28.5.0.5 28.5.0.6
-```
-
-Six patches exist above 28.5: **28.5.0.1 through 28.5.0.6**. The two sources agree, so the population is not a
-hand list.
-
-### The full table
-
-| OTP | macOS universal | linux x86_64 | linux aarch64 | Windows | all four? |
-|---|---|---|---|---|---|
-| 28.5.0.6 | **404** | **404** | **404** | 200 | no |
-| **28.5.0.5** | **200** | **200** | **200** | **200** | **yes — the pin** |
-| 28.5.0.4 | 200 | 200 | 200 | 200 | yes |
-| 28.5.0.3 | 200 | 200 | 200 | 200 | yes |
-| 28.5.0.2 | 200 | 200 | 200 | 200 | yes |
-| 28.5.0.1 | 200 | 200 | 200 | 200 | yes |
-| 28.5 | 200 | 200 | 200 | 200 | yes |
-
-**The measurement moved the pin.** 28.5.0.6 is released and is on the official Windows download page, but the
-BEAM-machine CDN has not built its macOS or Linux artifacts, so Burrito cannot package it for three of the four
-targets. **The pin is OTP 28.5.0.5**, not the 28.5 the decision provisionally named — which is why the decision
-made it subject to this probe.
-
-That gap is also the standing risk: Windows tracks OTP releases immediately and the other three targets lag behind
-a third-party CDN, so the newest packageable OTP is whatever that CDN last built. Re-run this probe at every phase
-boundary, as `VERSIONS.md`'s re-verification procedure already requires.
-
-## Line 1, continued — the toolchain install
-
-The machine ran Elixir 1.19.2, not the pinned 1.20.x, so the H7 probe could not have measured what H7 asks about:
-`boundary`'s behaviour under **Elixir 1.20's** type checker. The pinned Elixir was installed first.
-
-```
-$ which mise asdf ; echo "exit=$?"
-/usr/local/bin/asdf
-exit=1
-
-$ asdf install elixir 1.20.4-otp-28 ; echo "exit=$?"
-==> Checking whether specified Elixir release exists...
-==> Downloading 1.20.4-otp-28 to /home/aylac/.asdf/downloads/elixir/1.20.4-otp-28/elixir-precompiled-1.20.4-otp-28.zip
-==> Copying release into place
-exit=0
-
-$ elixir --version
-Erlang/OTP 28 [erts-16.1.1] [source] [64-bit] [smp:32:32] [ds:32:32:10] [async-threads:1] [jit:ns]
-
-Elixir 1.20.4 (compiled with Erlang/OTP 28)
-```
-
-`mise` is absent and `asdf` v0.18.0 is present, so **the pin file is `.tool-versions`, not `mise.toml`**, and
-line 3's rule — write the file for whichever manager is present — resolves to asdf. Every reference to
-`mise.toml` in `SLICE.md` is corrected to `.tool-versions` in the same commit.
-
----
-
-## Amendment, 2026-09-06 — owner's addition to line 5
-
-Quoting the owner's Decision:
-
-> `boundary` enforces only under `--warnings-as-errors`, so the rule is advisory unless the gate carries the flag.
-> **Added to line 5:** the gate's compile step runs `mix compile --warnings-as-errors`, and **a test asserts the
-> gate alias contains that flag**, so removing it fails the gate. The red is demonstrated by dropping the flag in
-> a throwaway edit to `mix.exs`.
-
-Line 5 as posted is not rewritten; this amendment governs. The gate alias carries `--warnings-as-errors` on its
-compile step, and `test/gate_alias_test.exs` asserts the flag is present in the alias, so a future edit that drops
-it fails the gate rather than silently turning `boundary` and the type checker advisory.
-
-## Line 3 — the pin is written
-
-```
-$ printf 'erlang 28.5.0.5\nelixir 1.20.4-otp-28\n' > .tool-versions
-$ cat .tool-versions
-erlang 28.5.0.5
-elixir 1.20.4-otp-28
-
-$ elixir --version
-Erlang/OTP 28 [erts-16.4.0.5] [source] [64-bit] [smp:32:32] [ds:32:32:10] [async-threads:1] [jit:ns]
-
-Elixir 1.20.4 (compiled with Erlang/OTP 28)
-
-$ erl -noshell -eval 'io:format("~s / erts ~s~n",[erlang:system_info(otp_release), erlang:system_info(version)]), halt().'
-28 / erts 16.4.0.5
-```
-
-`asdf install erlang 28.5.0.5` completed with exit 0 and `asdf list erlang` now shows it installed alongside the
-28.1.1 the machine had. The running erts is **16.4.0.5**, which supersedes the erts-16.1.1 recorded in the line 1
-probe — the H7 result is unaffected, since `boundary` was measured against Elixir 1.20.4 and the OTP 28 line, and
-both still hold.
-
-`VERSIONS.md`'s toolchain table now carries the exact patch versions and the reason for each, replacing the
-"pending measurement" wording and the note that deferred the ERTS question to slice 001.
-
----
-
-## Line 3, continued — the scaffold, and an incident worth recording
-
-```
-$ mix archive.install hex phx_new 1.8.13 --force ; echo "exit=$?"
-* creating /home/aylac/.asdf/installs/elixir/1.20.4-otp-28/.mix/archives/phx_new-1.8.13
-exit=0
-
-$ mix phx.new . --app trinity --database sqlite3 --no-mailer --install false
-...
-$ mix compile ; echo "exit=$?"
-Compiling 14 files (.ex)
-Generated trinity app
-exit=0
-```
-
-The app compiles on the pinned toolchain.
-
-### The generator overwrote two files it had no business replacing
-
-`mix phx.new .` writes `README.md` and `.gitignore` unconditionally when generating into an existing directory.
-It destroyed both.
-
-**`README.md`** was replaced with the stock Phoenix readme. That file held the **only approved site for the four
-platform names** — the section the name check exists to permit — and the `git config core.hooksPath .githooks`
-setup step. Restored from `HEAD`, with the generator's run instructions folded in as a new section rather than
-either version being lost.
-
-**This vindicates line 9's heading-text rule.** The permitted section had been at `README.md` lines 46–62. After
-restoration with the run section inserted above it, it sits at **lines 72–88**. A name check that had hard-coded
-46–62 would now be reading the wrong sixteen lines and would have permitted names in the wrong place while
-flagging the real section. Locating it by `## Connecting Trinity to the platform` survives the move.
-
-**`.gitignore`** was replaced with the stock Phoenix ignore list, which does **not** ignore `.env*`. That silently
-falsified `CLAUDE.md` §4's claim that "`.env*` is gitignored" — the exact defect that file was created to fix, undone
-by a generator, with nothing to catch it. Restored and merged: the eight generator-only entries worth keeping
-(`/.fetch`, `*.ez`, `/tmp/`, `trinity-*.tar`, the static-asset cache manifest, npm noise) were kept and the secret
-rules put back.
-
-### `plan_check` rule 9
-
-A claim in `CLAUDE.md` that a generator can quietly falsify is a rule with no enforcer, which is what I1 is about.
-Rule 9 asserts the claim against git itself rather than against the file's text:
-
-```
-$ ./scripts/plan_check.sh ; echo "exit=$?" # with .env dropped from .gitignore
-FAIL .gitignore: '.env' is not ignored, but CLAUDE.md says .env* is
-FAIL .gitignore: '.env.local' is not ignored, but CLAUDE.md says .env* is
-FAIL .gitignore: '.env.production' is not ignored, but CLAUDE.md says .env* is
-plan_check: FAIL
-exit=1
-
-$ ./scripts/plan_check.sh ; echo "exit=$?" # restored
-exit=0
-```
-
-It uses `git check-ignore`, so it tests the behaviour rather than the presence of a line, and a reordering or a
-later negation pattern cannot fool it.
-
-**Not caught by anything, and worth stating plainly:** the name check would *not* have caught the README loss.
-Deleting the permitted section deletes the hits along with it, and a check that verifies "every hit is inside the
-permitted section" passes trivially on zero hits. The check guards against names appearing where they should not,
-not against the approved section being removed. That is a real limit of the design, and it is stated here rather
-than discovered later.
-
----
-
-## Deviation, recorded at G3 — the status never moved to `in_progress`
-
-`docs/04-slice-process.md`'s lifecycle is `ready → in_progress → done → approved`, and the agent
-moves it to `in_progress` when it starts. **That never happened.** The slice ran from G1 to G3
-with `ROADMAP.md` reading `ready`, so anyone opening the board mid-slice would have seen a slice
-nobody was working on — and `CLAUDE.md` §0's own start-of-session check looks for the slice whose
-status is `in_progress` and would have found none.
-
-It is corrected forward, not backdated: the status moves straight to `done` in this commit,
-because that is what is true now. Pretending it passed through `in_progress` at a time it did not
-would be a worse record than admitting it skipped the state.
-
-**Two reasons it slipped, both worth fixing rather than resolving to be careful.** The transition
-is defined in `docs/04` and required by nothing: no gate checks it, and `plan_check` has no rule
-for it. And `SLICE.md` carried no `Status` row at all until this commit, so there was one place
-to forget rather than two to disagree.
-
-A rule asserting that a slice with commits on its branch is not `ready` would have caught this.
-It is not added here: it needs a definition of "in progress" that survives a branch being cut and
-abandoned, and inventing one at G3 to cover my own miss is the wrong moment. Raised as a
-follow-up instead.
-
-## Deviations from the G1 plan — the complete list
-
-Four. Each is also recorded where the artefact lives, so a reader of any one finds it; this list
-exists because "recorded somewhere" is not the same as "listed", and the fourth below proves it.
-
-**1. The pin file is `.tool-versions`, not `mise.toml`.** The G1 plan's line 3 named `mise.toml`
-with `.tool-versions` as the alternative. Measured before either was written:
-
-```
-$ which mise asdf ; echo "exit=$?"
-/usr/local/bin/asdf
-exit=1
-```
-
-`mise` is absent; `asdf` v0.18.0 is present. `SLICE.md`'s scope, deliverables, AC1 and proof
-lines were corrected, as was `VERSIONS.md`'s toolchain table.
-
-*This was recorded in the narrative at "Line 1, continued — the toolchain install" and was
-**not** in this deviation list until G4 asked for it.* That is the gap the list closes: a fact
-buried in a run log is not a deviation anyone will find.
-
-**2. The pin list is `lib/trinity/versions.ex`, not `versions.exs`.** A `.exs` data file must be
-evaluated at runtime and `Trinity.Credo.NoEvalOnModelOutput` forbids that family under `lib/`.
-Also in the module's `@moduledoc` and in `PROOF.md`.
-
-**3. `mix test --cover`'s 90% threshold is off.** `docs/03` sets no absolute threshold; its rule
-is drop-based, and leaving the default on would enforce a rule nobody wrote. Also in `mix.exs`
-and in `PROOF.md`.
-
-**4. The status never moved to `in_progress`.** Recorded in full below.
-
-## Follow-ups
-
-- **No enforcer for the slice lifecycle.** `docs/04` defines `ready → in_progress → done →
- approved` and nothing checks a slice ever occupies `in_progress`. See the deviation above.
-- **The project-wide `[manual]` tagging review** is still owed. Slice 000's own retag was the
- owner's instruction at G1; the other 35 slices were tagged by the same mechanical keyword pass.
-- **The ADR status vocabulary.** Two ADRs read `proposed → to be confirmed by Slice NNN`, which is
- informative and is not one of the three permitted values. Raised on the M3 issue; owner's call.
diff --git a/slices/000-toolchain-and-gate/PROOF.md b/slices/000-toolchain-and-gate/PROOF.md
deleted file mode 100644
index 1c2a63f..0000000
--- a/slices/000-toolchain-and-gate/PROOF.md
+++ /dev/null
@@ -1,440 +0,0 @@
-# Slice 000 — PROOF
-
-Branch `slice/000-toolchain-and-gate`. Every command below was run on the pinned toolchain, and
-every "verified" names its command and its exit code. Where a red was discovered rather than
-staged, that is said.
-
-## AC1 [auto] — the toolchain matches the pin file
-
-```
-$ cat .tool-versions
-erlang 28.5.0.5
-elixir 1.20.4-otp-28
-
-$ elixir --version ; echo "exit=$?"
-Erlang/OTP 28 [erts-16.4.0.5] [source] [64-bit] [smp:32:32] [ds:32:32:10] [async-threads:1] [jit:ns]
-
-Elixir 1.20.4 (compiled with Erlang/OTP 28)
-exit=0
-
-$ erl -noshell -eval 'io:format("~s / erts ~s~n",[erlang:system_info(otp_release), erlang:system_info(version)]), halt().'
-28 / erts 16.4.0.5
-```
-
-`VERSIONS.md`'s toolchain table carries `28.5.0.5` and `1.20.4-otp-28`.
-
-**The pin file is `.tool-versions`, not `mise.toml`.** Measured before it was written:
-
-```
-$ which mise asdf ; echo "exit=$?"
-/usr/local/bin/asdf
-exit=1
-```
-
-`mise` is absent, `asdf` v0.18.0 is present. `SLICE.md` and `VERSIONS.md` were corrected.
-
-## AC2 [auto] — `mix gate` exits 0
-
-```
-$ mix gate ; echo "exit=$?"
-...
-versions.verify: OK — 7 pins satisfied by mix.lock
-versions.gen: VERSIONS.md already matches Trinity.Versions
-trinity.version_form: OK
-trinity.names: OK over 146 tracked files
-trinity.secrets.scan: OK over 146 files
-trinity.reuse: OK — every commentable tracked file carries an SPDX header
-Running ExUnit with seed: 816745, max_cases: 64
-................................................
-Finished in 0.05 seconds (0.02s async, 0.03s sync)
-Result: 48 passed
-trinity.coverage: 000 at 27.98% is the FIRST row. It is the baseline and is compared against nothing; the rule is not exercised yet.
-exit=0
-```
-
-`scripts/plan_check.sh` also passes at this tree: `exit=0`.
-
-## AC3 [auto] — `mix versions.verify`
-
-```
-$ mix versions.verify ; echo "exit=$?"
- boundary pinned ~> 0.10 locked 0.10.4
- credo pinned ~> 1.7 locked 1.7.19
- mox pinned ~> 1.2 locked 1.3.1
- mix_audit pinned ~> 2.1 locked 2.1.5
- sobelow pinned ~> 0.15 locked 0.15.0
- ex_doc pinned ~> 0.38 locked 0.40.4
- nimble_options pinned ~> 1.1 locked 1.1.1
-versions.verify: OK — 7 pins satisfied by mix.lock
-exit=0
-```
-
-**Rows this slice touched, and only those, were updated in `VERSIONS.md`:** the Erlang/OTP and
-Elixir rows now carry measured patch versions and the reason, and the `mise / asdf` row is
-replaced by an `asdf` row. Rows for dependencies no slice has added yet keep their 🔍 — flipping
-those would be claiming a measurement nobody made.
-
-**Deviation, recorded.** `SLICE.md` named `versions.exs`, a data file. The pin list is
-`lib/trinity/versions.ex`, a compiled module, because a `.exs` data file must be evaluated at
-runtime and the eval-family enforcer forbids that family under `lib/`. Same data, no evaluation,
-and the compiler checks it. `mix versions.gen` generates VERSIONS.md's block from it and
-`mix versions.gen --check` fails the gate if the two disagree.
-
-## AC4 [auto] — a boundary violation fails the gate
-
-Red, `Trinity` calling into `TrinityWeb` with `deps: []`:
-
-```
-$ mix compile --warnings-as-errors --force ; echo "exit=$?"
-warning: forbidden reference to TrinityWeb.Endpoint
- (references from Trinity to TrinityWeb are not allowed)
- lib/trinity.ex:21
-exit=1
-
-$ mix gate >/dev/null 2>&1 ; echo "exit=$?"
-exit=1
-```
-
-Green, violation removed:
-
-```
-$ mix gate >/dev/null 2>&1 ; echo "exit=$?"
-exit=0
-```
-
-**The flag is the enforcement, and this was measured, not assumed.** With the same violation
-planted and `--warnings-as-errors` dropped:
-
-```
-$ mix compile --force ; echo "exit=$?"
-warning: forbidden reference to B
- (references from A to B are not allowed)
-exit=0
-```
-
-`boundary` reports violations as *warnings*. Without the flag the architecture rules in
-`docs/01` are advisory and a violation ships silently. `test/gate_alias_test.exs` asserts the
-gate's compile step carries the flag; dropping it in a throwaway edit to `mix.exs` fails that
-test with `exit=2`, and restoring it passes.
-
-## AC5 [manual] — CI green on the branch
-
-`.github/workflows/gate.yml` runs `mix deps.get`, `scripts/plan_check.sh`, a DCO check and
-`mix gate` on ubuntu-latest, with `fetch-depth: 0` because `plan_check` rule 8 reads the whole
-history.
-
-**Not verified here, and I cannot verify it.** This is a claim about a run on a remote. The
-owner confirms it at G3 by pasting the Actions run status. Retagged `[manual]` for that reason.
-
-## AC6 [auto] — the secret scan
-
-Red, a fake AWS key planted in `lib/planted_key.ex`:
-
-```
-$ mix trinity.secrets.scan ; echo "exit=$?"
-FAIL lib/planted_key.ex:5: AWS access key id
-** (Mix) trinity.secrets.scan: 1 finding(s)
-exit=1
-```
-
-Green, file removed:
-
-```
-$ mix trinity.secrets.scan ; echo "exit=$?"
-trinity.secrets.scan: OK over 146 files
-exit=0
-```
-
-## AC7 [auto] — each of the five enforcers, red and green
-
-**1. Eval family** (`Trinity.Credo.NoEvalOnModelOutput`). `CLAUDE.md` §5 names one function;
-one function has an obvious bypass, so the check covers `Code.eval_string`, `eval_quoted`,
-`eval_file`, `compile_string`, `compile_quoted` and `:erl_eval`, with **one test per function**.
-`test/no_eval_on_model_output_test.exs`: 13 tests, all passing, each red asserting the trigger
-and line, plus greens for ordinary code and for a variable merely *named* `eval_string`.
-
-**2. Version form.** No exemption list: the pattern is case-sensitive on word boundaries, so
-`gen_mcp 2.0` and `anubis_mcp 2.0.x` are excluded *by the boundary*, not by a list.
-`test/version_form_test.exs` asserts the skip list holds exactly one entry — the enforcer's own
-source — so it cannot grow.
-
-*Red discovered, not staged:* the enforcer failed the gate on its own test file, which held the
-forbidden form as a literal fixture. The fixtures are now composed at runtime, so the file never
-contains the string and the skip list stayed at one.
-
-**3. Name check.** Red on a **synthetic token in a test-only digest set**, never a real name:
-
-```
-$ mix trinity.names --digests /tmp/syn.txt ; echo "exit=$?"
-FAIL lib/planted_name.ex:4: forbidden name (zero permitted sites)
-** (Mix) trinity.names: 1 violation(s)
-exit=1
-
-$ mix trinity.names --digests /tmp/syn.txt ; echo "exit=$?" # planted file removed
-trinity.names: OK over 146 tracked files
-exit=0
-```
-
-Paths are scanned as well as contents, with the same tokenisation:
-
-```
-$ mix trinity.names --digests /tmp/syn.txt ; echo "exit=$?" # token as a FILENAME
-FAIL lib/qqfictitiousmarkerword_module.ex: forbidden name in a PATH (zero permitted sites)
-exit=1
-```
-
-**4. `reuse lint`.** *Red discovered, not staged:* `mix trinity.reuse` failed the gate on
-`lib/mix/tasks/versions.gen.ex`, created after the header pass —
-`FAIL lib/mix/tasks/versions.gen.ex: no SPDX-License-Identifier: Apache-2.0`, `exit=1`. Green
-after the header was added. **This check covers none of check 3**; they are separate gate steps
-and separate lines here, and neither is offered as evidence for the other.
-
-**5. Network rule.** `test/network_guard_test.exs` opens a **local TCP listener** so the
-demonstration needs no network: the guard refuses to connect to a listener that is definitely
-there (`{:error, :network_blocked_in_test}`), and reaches that same listener under
-`TRINITY_LIVE=1`. The gate excludes `:live`, so `mix test --only live` keeps working — blocking
-it would have broken the opt-in path `CLAUDE.md` §5 and `docs/03` both define.
-
-## AC8 [manual] — the name check over the tree
-
-```
-$ mix trinity.names ; echo "exit=$?"
-trinity.names: OK over 146 tracked files
-exit=0
-
-$ git grep -lIiE '' # pattern elided; see below
-README.md
-```
-
-**The pattern is elided in that transcript deliberately.** Writing it out would put the platform
-names in this file, which is not an approved site — and the enforcer proved that by failing on
-the first draft of this document:
-
-```
-$ mix gate ; echo "exit=$?"
-FAIL slices/000-toolchain-and-gate/PROOF.md:205: platform name outside the approved section
-** (Mix) trinity.names: 1 violation(s)
-exit=1
-```
-
-The proof document is not exempt from the rule it is proving. The pattern lives in the
-enforcer's own source, which is its single skipped path.
-
-Every platform-name hit is inside `README.md`'s approved section, located by the heading
-`## Connecting Trinity to the platform`. Zero-site names return nothing in contents or paths.
-
-**Owner confirms at G3** that each permitted hit maps to an approved line; that mapping is the
-owner's approval, not a machine's, which is why this is `[manual]`.
-
-**The section moved during this slice**, from lines 46–62 to 72–88, when the Phoenix generator
-overwrote `README.md` and it was restored with a run section added above. A check hard-coded to
-46–62 would now be reading the wrong sixteen lines. Locating by heading text survived it.
-
-## AC9 [manual] — the red is synthetic, and the limit is stated
-
-The red above uses `qqfictitiousmarkerword` in a test-only set. No real name appears in any
-test, any fixture, or this file.
-
-**The stated false-negative limit**, in `NOTES.md` and in the module's `@moduledoc`: tokenising
-downcases and splits on every run of non-alphanumerics, so **a forbidden name glued inside a
-larger token with no separator is not detected**. Accepted: the check is a tripwire against
-copy-paste drift, and copy-paste carries whole tokens.
-`test/trinity_names_test.exs` asserts that limit explicitly rather than leaving it as prose.
-
-## Coverage
-
-```
-$ mix test --cover # total
-27.98%
-$ cat coverage.tsv
-slice_id percent sha date
-000 27.98 e60e3e7 2026-09-06
-```
-
-`e60e3e7` is the commit at which coverage was measured, not this slice's final commit.
-
-**This row is the baseline and is compared against nothing.** `mix trinity.coverage` says so in
-those words rather than reporting a pass. The drop rule first does real work at slice 001.
-`test/coverage_gate_test.exs` proves the rule itself: a 3.5-point drop fails, exactly 3.0 passes.
-
-`mix test --cover`'s default 90% threshold was turned off, because `docs/03` sets **no absolute
-threshold** — its rule is drop-based. Leaving the default on would have enforced a different rule
-than the one the project states.
-
-## What is not proven here
-
-- **AC5.** Owner confirms the Actions run at G3.
-- **The `[manual]` tagging review**, still owed project-wide. Slice 000's own retag (AC5, AC8,
- AC9) was the owner's instruction at G1 and is applied.
-- **The ADR status vocabulary question** raised on the M3 issue is unanswered and outside this
- slice.
-- **`docs/01`'s context table** describes contexts no code implements yet. Only `Trinity` and
- `TrinityWeb` exist, which is what this slice's scope names.
-
----
-
-## Correction, appended 2026-09-06 — the coverage figure
-
-**Supersedes** the coverage figure above, which read:
-
-> ```
-> $ mix test --cover # total
-> 27.98%
-> $ cat coverage.tsv
-> slice_id percent sha date
-> 000 27.98 e60e3e7 2026-09-06
-> ```
-
-That was measured at `e60e3e7`, before the last four commits added the permitted-hit listing,
-`plan_check` rules 9 and 10, and the status row. Those added code without proportional tests, so
-the figure moved. Re-measured at this slice's head:
-
-```
-$ mix test --cover # total
-27.01%
-$ cat coverage.tsv
-slice_id percent sha date
-000 27.01 e935c7b 2026-09-06
-```
-
-**The drop is 0.97 points, 27.98 to 27.01, inside the three-point tolerance**, so it would pass the rule
-even if there were a prior row to compare against. There is not: this is still the first row and
-still a baseline compared against nothing.
-
-The original figure is left standing above rather than edited, because a record that quietly
-changes its own numbers is the defect this project keeps finding.
-
----
-
-# G4 changes requested — the four items
-
-Appended, not merged into the sections above, so the reviewed document stands as reviewed.
-
-## Item 1 — AC3's second clause, derived rather than hand-listed
-
-**Superseded:** "Rows this slice touched, and only those, were updated" and the AC3 line calling
-the second clause *partial*. "Rows this slice touched" was a hand list, which is the defect.
-
-**The mark's meaning is now derivable.** ✅ means *the package is in `mix.lock` at this sha*,
-emitted by `mix versions.gen` from `Mix.Dep.Lock.read/0`. Toolchain rows are marked from
-`.tool-versions`. Nothing is marked by hand, so a mark cannot outlive the fact it asserts —
-which is exactly how finding B3 found two false ✅ marks on the packages the OTP pin rested on.
-The old legend, under which ✅ meant "someone ran `curl` on some date", is replaced and says so.
-
-Every row now lives in `Trinity.Versions` and the whole table body is generated. **That also
-removed a duplication I had introduced**: `versions.gen` had appended a block *beside* the
-hand-written tables rather than replacing them, so the toolchain and the seven slice-000
-dependencies were listed twice.
-
-Derived population, cross-checked both ways:
-
-```
-$ grep -c '| ✅ in `mix.lock` |' VERSIONS.md → 15
-$ grep -c '| 🔍 not yet a dependency |' VERSIONS.md → 12
-$ grep -c '| 🔍 not a single package |' VERSIONS.md → 6
-$ grep -c '| ✅ `.tool-versions` |' VERSIONS.md → 5
-
-# every ✅ row is genuinely in mix.lock
-phoenix phoenix_live_view phoenix_pubsub bandit ecto_sql ecto_sqlite3 jason boundary
-nimble_options credo mox mix_audit sobelow ex_doc lazy_html → all OK
-# no 🔍 row is in mix.lock
-(no output)
-
-$ mix versions.gen --check ; echo "exit=$?"
-versions.gen: VERSIONS.md already matches Trinity.Versions and mix.lock
-exit=0
-```
-
-**The change found a real gap.** `versions.verify` gained a check that every direct dependency
-in `mix.exs` has a row, and it immediately named **twelve** the generator had added and the pin
-list never documented: `phoenix_ecto`, `phoenix_html`, `phoenix_live_dashboard`,
-`phoenix_live_reload`, `esbuild`, `tailwind`, `heroicons`, `daisyui`, `gettext`, `dns_cluster`,
-`telemetry_metrics`, `telemetry_poller`. All now carry rows.
-
-**One assertion corrected while doing this.** `versions.verify` briefly failed eleven pins as
-"absent from mix.lock". That was wrong: absence is not disagreement — most pinned packages
-arrive at a later slice. The rule is now that **the lock must not disagree** with the pin list,
-and a pin that is not a version requirement at all (`not pinned`, `optional, ~> 0.3`,
-`(transitive via LiveView test)`) is documentation with nothing to satisfy. The tests encoding
-the old semantics were updated rather than left passing against a rule that no longer holds.
-
-```
-$ mix versions.verify ; echo "exit=$?"
-versions.verify: OK — 52 locked packages, none disagreeing with 45 pins
-exit=0
-```
-
-## Item 2 — AC5, green at head, not at some earlier sha
-
-```
-$ git log --oneline -1
-1921293 docs(s000): state the coverage drop as points, not as the new figure
-
-$ gh run list --branch slice/000-toolchain-and-gate --limit 5 ; echo "exit=$?"
-completed success docs(s000): state the coverage drop as points, not as the new figure gate slice/000-toolchain-and-gate push 34050775439 37s 2026-09-06T18:08:39Z
-completed success docs(s000): correct the coverage figure by appending gate slice/000-toolchain-and-gate push 34050748444 32s 2026-09-06T18:08:06Z
-completed success feat(s000): list every permitted name hit for AC8 gate slice/000-toolchain-and-gate push 34050721136 36s 2026-09-06T18:07:34Z
-completed success chore(s000): repoint the repository URL to the ScriptKittyOS organisa… gate slice/000-toolchain-and-gate push 34050683172 29s 2026-09-06T18:06:49Z
-completed success docs(s000): status done in SLICE.md and ROADMAP.md gate slice/000-toolchain-and-gate push 34050634156 28s 2026-09-06T18:05:54Z
-exit=0
-```
-
-**The run for the head commit `1921293` is `completed success`.** All five listed runs are green.
-Commits made after this listing carry runs of their own.
-
-## Item 3 — AC7 enforcer 2, the transcript and the test
-
-The red was described and not shown. Reproduced by reverting the fixture to a literal:
-
-```
-$ mix gate ; echo "exit=$?"
-FAIL test/version_form_test.exs:24: assert VersionForm.forbidden?("we target now")
-** (Mix) trinity.version_form: 1 violation(s)
-exit=1
-```
-
-**The form is elided in that transcript**, for the same reason the platform names are elided in
-AC8: writing it literally would make this document a violation. The enforcer proved that too —
-the first draft of this section carried it verbatim and `mix gate` failed on
-`PROOF.md:391`, `exit=1`. Twice now, a proof document has been caught by the rule it was
-proving. That is the enforcers working on their author, and it is worth more than a green run.
-
-Green once the fixture is composed at runtime again:
-
-```
-$ mix trinity.version_form ; echo "exit=$?"
-trinity.version_form: OK
-exit=0
-$ mix gate >/dev/null 2>&1 ; echo "exit=$?"
-exit=0
-```
-
-**The test that plants the composed form and asserts detection** is
-`test "the forbidden form is caught"` in `test/version_form_test.exs:23`. It builds the string
-from `@protocol <> " " <> "2.0"` so the file never contains it, and asserts `forbidden?/1`
-returns true. Result:
-
-```
-$ mix test test/version_form_test.exs --trace
- * test the forbidden form is caught (0.01ms) [L#23]
-Result: 4 passed
-```
-
-The companion `test "the skip list holds exactly one entry"` keeps the exemption from growing to
-cover the file instead.
-
-## Item 4 — the deviation list
-
-`NOTES.md` now carries **"Deviations from the G1 plan — the complete list"** with all four. The
-pin-file one was recorded in the narrative at *"Line 1, continued — the toolchain install"* and
-was **not in the deviation list** until this item asked for it. That gap is stated in the list
-itself: a fact buried in a run log is not a deviation anyone will find.
-
-## Taken up from the Coordinator's closing note
-
-`boundary` enforcing only under `--warnings-as-errors` is now stated in `docs/01-architecture.md`
-where the architecture rules live, not only here. The next reader of that file will not read this
-document, which was the point.
-
-The sobelow fingerprint's line-number sensitivity stays recorded in `.sobelow-skips.reasons`,
-as the note accepts.
diff --git a/slices/000-toolchain-and-gate/SLICE.md b/slices/000-toolchain-and-gate/SLICE.md
deleted file mode 100644
index 597ab21..0000000
--- a/slices/000-toolchain-and-gate/SLICE.md
+++ /dev/null
@@ -1,132 +0,0 @@
-# Slice 000 — Toolchain, repo bootstrap, quality gate
-
-| Field | Value |
-|---|---|
-| Phase | 0 Foundation |
-| Milestone | M0 Stands |
-| Size | L |
-| Depends on | — |
-| Status | approved |
-
-## Goal
-A Phoenix 1.8 app named `trinity` that compiles on pinned Elixir 1.20.x / OTP 28.x, with the full quality gate
-(`mix gate`) green, `boundary` wired, CI running the gate on push, and `VERSIONS.md` verified against reality.
-
-## Why
-Everything downstream assumes the gate exists and the versions are real. Accountability starts here.
-
-**Sized L, not M, and not split.** The scope holds a toolchain, a generator run, boundary wiring, an eight-step
-gate, two Mix tasks, CI and the open-source hygiene artefacts. The hygiene half stays here because ADR-0012
-decision 2 puts the governance files at this slice by name, and because decision 1's commit-1 requirement is
-only partly met: commit 1 carries `LICENSE`, `NOTICE` and DCO sign-off, and does **not** carry SPDX headers or
-`REUSE.toml`. This slice closes that gap and delivers decision 2, so splitting it would leave an accepted ADR
-unmet with no slice owning the remedy.
-
-## Scope
-**In:**
-- **First, before anything else: the `boundary` probe.** `boundary` has not shipped since 2024-09-25 and its
- behaviour under Elixir 1.20's type checker with `--warnings-as-errors` is unverified. Everything in
- ADR-0001, `docs/01-architecture.md`, CLAUDE.md §5 and AC4 below rests on it. Stand up a throwaway app on the
- pinned toolchain, wire two boundaries, plant a violation, and confirm it fails compilation. If it does not
- work, stop and record the fallback (conventions plus a custom Credo check, or an umbrella) in an ADR before
- continuing. Nothing else in this slice starts until this is answered.
-- `.tool-versions` pinning erlang + elixir; `elixir --version` output recorded. Measured at G1: `mise` is
- absent on this machine and `asdf` v0.18.0 is present, so the pin file is asdf's, not `mise.toml`.
-- `mix phx.new trinity --database sqlite3 --no-mailer` (keep LiveView, Tailwind, daisyUI defaults; remove what is unused later).
-- Dependencies: `boundary`, `credo`, `mox`, `mix_audit`, `sobelow`, `ex_doc`, `nimble_options`. Nothing else yet.
-- `boundary` top-level definitions for `Trinity` and `TrinityWeb` (`TrinityWeb` deps `[Trinity]`; `Trinity` deps `[]`).
-- Mix aliases: `gate` = format --check-formatted, compile --warnings-as-errors --force, credo --strict,
- `sobelow --exit` **blocking, with a committed `--skip` list carrying a one-line reason per skip**, hex.audit,
- deps.audit, test, secret scan. (`--exit` fails the build; the earlier wording called the same step advisory,
- which it is not. A named exception with a reason is an enforcer, not a weakening.)
-- `versions.exs`: the machine-readable pin list, one entry per dependency. `VERSIONS.md`'s tables are **generated
- from it**, so the prose cannot drift from the checked data.
-- `mix versions.verify` task: reads `versions.exs` and `mix.lock`, prints pinned vs locked vs latest from
- `mix hex.info`, non-zero exit on retired or vulnerable. It does **not** parse the markdown: those cells hold
- emoji, footnotes and phrases like "decided by Slice 059", and a parser over them breaks on the first edit,
- which is when it is needed. The audit that found ten stale rows in `VERSIONS.md` had to be run by hand because
- the check was specified as prose.
-- **Gate enforcers for rules that currently have none.** CLAUDE.md §8 says a rule has an enforcing test or a
- stated reason; these had neither. Each is a separate gate step and a separate line in PROOF.md; none is reported
- as covering another:
- 1. `Code.eval_string` on model output: a Credo check or grep over `lib/`. Fails the gate on any occurrence.
- 2. **The version-form check.** MCP is versioned by date, never by a major number, so the forbidden form is a
- literal `MCP` followed by a major version number. The pattern is **case-sensitive with word boundaries**,
- which is what excludes `gen_mcp 2.0` and `anubis_mcp 2.0.x`: they are lower-case and the underscore is a
- word character, so no boundary opens before `mcp`. **There is no exemption list.** The enforcer's own
- source file is the single path it skips, because it must contain the pattern to test it, and a test asserts
- that skip list has exactly one entry.
- 3. **The name check.** Two policies, and which policy a name gets decides how it is matched.
- **Zero-permitted-sites names** — the two code names and the superseded agent name — are matched by a
- **committed digest set**, never by a plaintext pattern, because any file spelling them would itself be a
- hit. That is why this specification does not name them either. G1 states the digest set's
- false-negative limit.
- **Permitted-site names** — the four platform names — are matched in plain text and are allowed only
- inside the one approved section of `README.md`. That section is located **by its heading text**, never by
- line numbers, so editing the file above it cannot silently move the permitted window.
- The check runs over **paths as well as contents**: a content-only grep would pass a tree whose filenames
- carry names, which is how a filename carrying a code name was caught.
- 4. `reuse lint`: per-file copyright and licence information. **This is a separate check from 3 and covers none
- of it.**
- 5. **Tests must not reach the network.** Outbound sockets are blocked on the **default** test run, so the rule
- fails loudly rather than passing quietly on a machine that happens to be online. The `:live` tag is excluded
- from `mix gate` and runs only under an explicit flag, with the network open — those tests exist to reach a
- real provider, so blocking them would break the opt-in path CLAUDE.md §5 and `docs/03` both define.
-- Secret scan: a small Mix task (`mix trinity.secrets.scan`) with regexes for common API key shapes over `git diff --cached` and the tree; part of gate.
-- GitHub Actions (or equivalent) workflow running `mix gate` on ubuntu-latest with a matrix `TRINITY_DB=sqlite` (postgres job added in 010).
-- `.gitignore` already exists and covers `.env*`, `_build`, `deps`, `priv/data/` and `tauri/target`; the
- generator's additions merge into it rather than replacing it.
-- `README.md` in repo root: how to run, how the slice process works (link to `docs/`).
-- Copy this plan package into the repo (`docs/`, `slices/`, `templates/`, `CLAUDE.md`, `ROADMAP.md`, `VERSIONS.md`).
-- **Open-source hygiene from commit 1 (ADR-0012):** `LICENSE` = Apache-2.0; `NOTICE` with the three-role pattern
- (Sudo Apt Holdings LLC owns the IP, Script Kitty built it, Ayla Croft authored it, ORCID 0009-0008-9457-2160);
- SPDX headers on every source file (`# SPDX-License-Identifier: Apache-2.0`, holder only); `REUSE.toml`;
- DCO sign-off enforced by a commit-msg hook and CI (`git commit -s`); `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`,
- `SECURITY.md`, `GOVERNANCE.md` (single maintainer today, documented intent and process to add committers),
- `MAINTAINERS.md`, `CITATION.cff`. The repo is private until the owner says otherwise; the history must already be
- publishable.
-**Out:**
-- Any domain code. Any desktop tooling (001). Tailwind theme work.
-
-## Design notes
-- Keep the generator's `Trinity.Repo` but do not add schemas yet.
-- `config/runtime.exs`: `TRINITY_DATA_DIR` env (defaults to OS data dir via a tiny helper; full OS-dir logic in 010).
-- Elixir 1.20 type checker: ensure `mix compile --warnings-as-errors` treats type warnings as errors (it does by default; verify).
-
-## Deliverables
-- Repo scaffold, `.tool-versions`, `mix.exs` with aliases, `lib/trinity/versions.ex` (see the recorded deviation), `lib/mix/tasks/versions.verify.ex`, `lib/mix/tasks/versions.gen.ex`, `lib/mix/tasks/trinity.secrets.scan.ex`, `.github/workflows/gate.yml`, `.credo.exs`, `.formatter.exs`, plan package copied.
-
-## Acceptance criteria
-1. [auto] `elixir --version` shows Elixir 1.20.x on OTP 28.x, matching `.tool-versions`, and the exact patch versions are written into `VERSIONS.md`.
-2. [auto] `mix gate` exits 0 on a clean checkout.
-3. [auto] `mix versions.verify` exits 0 and its output is pasted in PROOF.md; any 🔍 rows in `VERSIONS.md` that this slice touched are flipped to ✅ with today's date.
-4. [auto] Introducing a boundary violation (temporary test file making `Trinity` call `TrinityWeb`) makes `mix gate` fail; removing it makes it pass. Both outputs captured.
-5. [manual] CI workflow runs the gate and is green on the slice branch.
-6. [auto] `mix trinity.secrets.scan` detects a planted fake key in a temp file and exits non-zero; passes after removal.
-7. [auto] Each of the five enforcers above fails the gate on a planted violation and passes after its removal. Both outputs captured per enforcer.
-8. [manual] The name check exits 0 over the tree as it stands at commit 1, and every permitted hit maps to a line on the approved permitted-sites list.
-9. [manual] The name check's red is demonstrated on a synthetic token, not on real content, and the digest set's false-negative limit is stated in NOTES.md.
-
-## Proof required
-- `elixir --version`, `asdf current`, `mix gate` output, `mix versions.verify` output, the boundary-violation before/after, CI run URL or log excerpt, secret scan before/after.
-
-## Manual verification queue
-Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather
-than at review time.
-- **AC5** — confirm the CI Actions run is green on this branch, by pasting its status; the run is on a
- remote this branch cannot vouch for.
-- **AC8** — answered at G1: the permitted-sites list is approved and the name check consumes it; the owner
- confirms at G3 that each permitted hit maps to an approved line.
-- **AC9** — answered at G1: the digest-set design and its stated false-negative limit are approved as
- written in NOTES.md; the owner confirms the limit still reads correctly at G3.
-
-## Definition of Done
-- [ ] `mix gate` green · [ ] AC1–9 proven · [ ] CI green · [ ] `VERSIONS.md` updated · [ ] ROADMAP → done · [ ] final commit + tag
-
-## Commit & tag
-`feat(s000): complete slice 000 — toolchain, repo bootstrap, quality gate` · tag `slice/000`
-
-## Risks / open questions
-- If Elixir 1.20.x does not support the chosen OTP 28 patch, use the newest 1.20.x that does and note it in NOTES.md.
-- The OTP 28 pin itself is unmeasured (B3). This slice records `elixir -v` and the ERTS set Burrito 1.6.0 actually offers; Slice 001 confirms it per target.
-- If `sobelow` produces false positives on the scaffold, document the skip list; do not remove it from the gate.
diff --git a/slices/001-packaging-spike/NOTES.md b/slices/001-packaging-spike/NOTES.md
deleted file mode 100644
index 4814618..0000000
--- a/slices/001-packaging-spike/NOTES.md
+++ /dev/null
@@ -1,1639 +0,0 @@
-# Slice 001 — NOTES
-
-## G1 plan
-
-Posted 2026-09-06 against `537bd3a`, branch cut from `main` at `de95666`. Fifteen lines, execution order, one
-sentence each, each naming what it produces and the red it fails on first. **No code until this is approved.**
-
-1. **The `ex_tauri` measurement runs before any packaging work, and all three arms are pre-registered here before any is run**: **(a)** default configuration on the pinned toolchain — `mix deps.get` then `mix deps.compile ex_tauri` with `{:ex_tauri, "~> 0.2", only: :dev}` in `mix.exs`; **(b)** the same with `otp_release` overridden to `28.5.0.5` in this project's config; **(c)** whether `~> 27.0` is a hard guard in `ex_tauri`'s own code or a default it downloads against, read from the unpacked source with `mix hex.package fetch ex_tauri 0.2.0 --unpack` and `grep -rn "otp_release\|27" ` — output and exit code recorded per arm, **a refusal being (a) and (b) both failing**, and if (b) works there is no refusal, ADR-0004 is confirmed and the override is recorded as the configuration.
-2. **The acceptance-criteria tag review** produces a corrected `slices/001-packaging-spike/SLICE.md` and its Manual verification queue, failing first on the mistags this slice was predicted to carry: AC4 and AC5 claim `[auto]` for things needing a desktop session or a machine that does not exist, and AC6 mixes an auto half with a manual one.
-3. **Burrito wiring and the non-hex toolchain** produce the `releases:` block in `mix.exs`, `lib/trinity/release.ex` running migrations explicitly, a **Rust pin in `.tool-versions`** and the **Tauri CLI pinned at its measured version**, plus their two `VERSIONS.md` rows whose marks come from `rustc --version` and `cargo tauri --version` rather than from `mix.lock` — red on `MIX_ENV=prod mix release` failing before the release assembles, and on `mix versions.gen --check` if those rows are emitted as if lock membership had answered for them.
-4. **`lib/trinity/paths.ex`** resolves the per-OS data directory with `test/paths_test.exs` red first on each of the three branches returning the wrong root under a stubbed `:os.type/0`.
-5. **`lib/trinity/smoke.ex` and the `--smoke` flag** boot the endpoint on `127.0.0.1:0`, print the assigned port, and exit 0, red first on a smoke run that stays alive past its own exit call.
-6. **The linux x86_64 Burrito build** produces `burrito_out/trinity_linux_x86_64`, a single binary, launched and exited under `--smoke`, with `ps -eo pid,ppid,comm` captured before and after — **a leaked sidecar surviving the exit is the red**, and it is the criterion this line exists for.
-7. **AC1's serving proof** captures `curl -sS -o /dev/null -w '%{http_code}'` against the port the smoke run printed, red first on a binary that starts but serves nothing.
-8. **`docs/packaging.md`'s measurement table** records binary size from `stat -c %s` and cold-start-to-serving from a timed loop on linux x86_64 only, red on `plan_check` check 6 if it cites a path the tree does not have.
-9. **`.github/workflows/package.yml`** adds macos-latest and windows-latest jobs that build the artifact, launch the **shell** on their real desktop sessions and exit under `--smoke`, and a ubuntu-latest job that **states which of two it is** — the shell under `xvfb-run`, or the sidecar smoked alone because the runner has no display — red first on a job reporting success with no artifact in its upload step, and on any runner claim that does not say which of those two it made.
-10. **What a runner proves and what it cannot** is written into `docs/packaging.md` as two explicit lists — it proves the artifact builds, launches and exits clean, and it produces a launch log; it cannot produce a screenshot of a real window on a real desktop, and nothing will claim it did.
-11. **Every criterion needing a real desktop is tagged `[manual]` with "no machine available — Ubuntu is the only machine" as its stated reason**, never silently dropped, and the slice's exit condition names them as unproven rather than counting them.
-12. **ADR-0004 is confirmed or amended** from what lines 1 to 9 measured, as an **appended correction that names what it supersedes** and carries a status word from the vocabulary `docs/03` allows — `accepted`, since this slice is what its current `proposed → to be confirmed by Slice 001` was waiting on — with **the alternatives table kept either way**, because the next person to hit R1 needs it; red on `plan_check` check 6 if the amendment cites an alternative with no ADR of its own.
-13. **`coverage.tsv` gains slice 001's row** and the drop rule does real work for the first time, red first by seeding a row more than three points under 000's **27.01** and watching `mix trinity.coverage` fail, then green at the real figure or with a reason named here.
-14. **`mix gate` and `scripts/plan_check.sh` both exit 0 on this branch**, with `mix phx.server` still starting without Tauri, red on either gate or on the plain server needing the desktop shell to boot.
-15. **The manual queue holds only what the tree cannot answer**, each item naming the machine or account it needs: a macOS desktop for AC2, a Windows desktop for AC3, a Linux desktop session for AC4's window, and the same three for AC6's window-close check — **all four are the same missing thing, a machine that is not this one.**
-
-## Constraints this plan is written under
-
-**Nothing here adds domain code.** `Trinity.Release`, `Trinity.Paths` and `Trinity.Smoke` are named in this
-slice's own Deliverables and exist only to make a binary boot and stop. If the spike turns out to need a change
-outside packaging, that is a Question on the slice issue, not a commit.
-
-**The pin does not move.** ADR-0005's second correction settled OTP 28.5.0.5 on a measurement of Burrito's artifact
-index. If `ex_tauri` refuses it, that is ADR-0004's decision to make under its alternatives, which is what line 1
-routes to.
-
-## Two VERSIONS.md rows the generator's rule does not cover
-
-Packaging brings a **Rust toolchain** and the **Tauri CLI** into the tree. Neither is a hex package, so the
-`✅ in mix.lock` derivation slice 000 built **cannot mark them** — lock membership has nothing to say about either.
-
-Their rows carry their own stated derivation instead: `rustc --version` and `cargo tauri --version`, named in the
-row the way the toolchain rows name `.tool-versions`. Without that, the generator's rule reads as if it covered
-the whole table, which would be the same defect finding B3 caught — a mark whose meaning is assumed rather than
-stated.
-
-## The tag review, done now rather than at G3
-
-`SLICE.md` carries seven criteria. Three are tagged `[manual]` and four `[auto]`. **Three of the four `[auto]`
-tags are wrong**, which is what finding I3 predicted for exactly this slice.
-
-| AC | Tagged | Should be | Why |
-|---|---|---|---|
-| 1 | `[auto]` | `[auto]` | Correct. A binary on this machine, curl against the port it prints. |
-| 2 | `[manual]` | `[manual]` | Correct. Needs a **macOS desktop**. |
-| 3 | `[manual]` | `[manual]` | Correct. Needs a **Windows desktop**. |
-| 4 | `[auto]` | **`[manual]`** | "Linux: same" means a native window showing the scaffold. A window on a desktop session is not a command's output. The clause "or documented not tested — no machine" is a waiver, not an automation. |
-| 5 | `[auto]` | **split** | Binary size and cold-start are `[auto]` **on linux x86_64 only**. "Per OS" cannot be measured for macOS or Windows without those machines, and "first paint" needs a window on any of them. |
-| 6 | `[manual]` | **split** | Killing a *window* is `[manual]`. But the property underneath — no leaked sidecar after the process exits — is `[auto]` under `--smoke` with `ps` before and after, and line 6 proves it on linux today. |
-| 7 | `[auto]` | `[auto]` | Correct. Two commands. |
-
-Applying this is line 2, in a commit of its own, so the retag is reviewable apart from the packaging work.
-
-## Manual verification queue — four items, one missing thing
-
-Every one needs a machine that is not this one. **The owner's only machine is Ubuntu.**
-
-| Item | Needs | Criterion |
-|---|---|---|
-| Native window on macOS, screenshot | a **macOS desktop** | AC2 |
-| Native window on Windows, screenshot, or the documented fallback | a **Windows desktop** | AC3 |
-| Native window on Linux, screenshot | a **Linux desktop session** (this machine has no display in use here) | AC4 after retag |
-| Window close terminates the sidecar within 5 s | any one of the three desktops | AC6's manual half |
-
-A GitHub Actions runner covers none of these: it can build the artifact, run it under `--smoke`, and keep the
-launch log, and that is what lines 9 and 10 claim and no more. **No screenshot of a real window on a real desktop
-exists for macOS, Windows or Linux, and the slice will exit saying so rather than counting those criteria as met.**
-
----
-
-## Line 1 — the ex_tauri measurement. **No refusal. ADR-0004 is confirmed.**
-
-Three arms, pre-registered at G1 before any was run. Toolchain: Elixir 1.20.4 on Erlang/OTP 28,
-erts-16.4.0.5, from `.tool-versions`.
-
-### Arm (c) — is `~> 27.0` a hard guard or a default? **Read first, because it decides what (a) and (b) mean.**
-
-```
-$ mix hex.package fetch ex_tauri 0.2.0 --unpack --output ./ex_tauri-0.2.0 ; echo "exit=$?"
-ex_tauri v0.2.0 extracted to ./ex_tauri-0.2.0
-exit=0
-
-$ grep -rn 'otp_release' ex_tauri-0.2.0/
-ex_tauri-0.2.0/mix.exs:14: otp_release: "~> 27.0",
-ex_tauri-0.2.0/lib/ex_tauri.ex:29,31,34,44
-ex_tauri-0.2.0/lib/ex_tauri/task_helpers.ex:35,37,40,51
-```
-
-The guard is two branches on `:erlang.system_info(:otp_release)`, not on the `mix.exs` key:
-
-* `major < 27` → **`Mix.raise`**. A hard refusal.
-* `major > 27` → **`Mix.shell().info`**, a warning. **No raise.**
-* `major == 27` → `:ok`.
-
-**`~> 27.0` in `ex_tauri`'s `mix.exs` is declarative metadata.** Mix enforces `:elixir` as a version
-requirement; it has no built-in `:otp_release` enforcement. The behaviour lives entirely in the runtime check
-above, and at OTP 28 that check **warns**.
-
-### Arm (a) — default configuration on the pinned toolchain. **Succeeds.**
-
-```
-$ mix deps.compile ; echo "exit=$?" # the whole tree, in dependency order
-exit=0
-
-$ mix deps.compile ex_tauri ; echo "exit=$?"
-exit=0
-
-$ mix compile ; echo "exit=$?"
-Compiling 5 files (.ex)
-Generated trinity app
-exit=0
-
-$ ls _build/dev/lib/ex_tauri/ebin/*.beam | wc -l → 31
-$ ls _build/dev/lib/igniter/ebin/*.beam | wc -l → 63
-$ ls _build/dev/lib/burrito/ebin/*.beam | wc -l → 20
-```
-
-All seven `mix ex_tauri.*` tasks are available. The guard fires exactly as arm (c) predicted:
-
-```
-$ mix run -e 'ExTauri.TaskHelpers.check_otp_version()' ; echo "exit=$?"
-Warning: ExTauri targets OTP 27 but you are running OTP 28.
-Burrito may not have pre-compiled ERTS for OTP 28 yet.
-Development should work, but production builds may fail.
-exit=0
-```
-
-**Exit 0. A warning, not a refusal.** And its stated reason is the same stale fact ADR-0005's second
-correction already refutes: slice 000 measured that Burrito 1.6.0 **can** fetch OTP 28 ERTS for all four
-targets and **cannot** fetch OTP 27 for macOS or Linux. The warning has it backwards.
-
-### Arm (b) — the override. **Moot, and not performed.**
-
-The pre-registered rule is that a refusal is (a) **and** (b) both failing. **(a) succeeded, so there is
-nothing to override and no refusal to clear.** Arm (c) also shows why an override could not have helped:
-the check reads `:erlang.system_info/1` at runtime, so no project-config key changes its answer.
-
-**The configuration recorded for ADR-0004 is therefore the default**, `{:ex_tauri, "~> 0.2", only: :dev}`,
-with no override.
-
-### Two false failures, mine, recorded because the method was the defect
-
-I reported arm (a) as failing **twice** before it was true, and both were my method rather than the subject.
-
-**First**, I declared the dependency `only: :dev, runtime: false`. `runtime: false` is not the default, and
-arm (a) is defined as the *default* configuration. It failed with
-`module Igniter.Mix.Task is not loaded`.
-
-**Second**, with the flag corrected, I ran `mix deps.compile ex_tauri` and then
-`mix deps.compile igniter` in isolation, and read `Type checking failed with errors` —
-`struct Sourceror.Zipper is undefined`, `struct Rewrite.Source is undefined` — as an Elixir 1.20
-incompatibility in `igniter`. **It is not.** Compiling one dependency by name does not first build the
-dependencies *it* needs, so the undefined structs were a build-order artifact I manufactured. `mix
-deps.compile` with no arguments, which is the ordinary path, exits 0 and builds all three.
-
-**The lesson, stated so it is not repeated:** testing a dependency in isolation can manufacture a failure
-the ordinary path does not have. A negative result about a dependency is only worth reporting after the
-ordinary build has been tried. Had I stopped at either point I would have filed a Question against
-`ex_tauri` and ADR-0004 for a defect that does not exist, and the recommendation in it would have been
-wrong.
-
-### Outcome
-
-**No refusal, so no stop.** ADR-0004's provisional decision to target `ex_tauri` survives its first
-measurement; whether it *packages* is lines 3 to 9, and confirmation is line 12's business, not this one's.
-Carrying on to line 2.
-
----
-
-## Line 3 — Burrito wiring and the non-hex toolchain. Six deviations, recorded before the commit.
-
-Dated 2026-09-06. The G1 Decision's item 3 read:
-
-> Pin Rust in `.tool-versions` and the Tauri CLI at its measured version, add both as
-> VERSIONS.md rows with their own stated derivation (`rustc --version`, `cargo tauri
-> --version`), and state in NOTES.md that these rows are marked from those commands rather
-> than from the lock.
-
-Measurement contradicted three of its clauses and added a fourth pin it did not anticipate.
-Everything below is a deviation from the approved plan, recorded here **before** the commit
-that carries it, per CLAUDE.md §1.
-
-### D1 — Rust is pinned in `rust-toolchain.toml`, not `.tool-versions`
-
-`asdf` on this machine has plugins for elixir and erlang and **no rust plugin**, and asdf does
-not fail on a tool it has no plugin for. Measured in a scratch directory whose `.tool-versions`
-carried a `rust 1.92.0` line:
-
-```
-$ asdf current ; echo "exit=$?"
-Name Version Source Installed
-elixir 1.20.4-otp-28 .../.tool-versions true
-erlang 28.5.0.5 .../.tool-versions true
-exit=0
-
-$ asdf install >/dev/null 2>&1 ; echo "asdf install exit=$?"
-asdf install exit=0
-```
-
-**The rust line is absent from `asdf current` and `asdf install` exits 0.** A `rust 1.92.0`
-line in `.tool-versions` would be a pin that pins nothing — the same defect class as a ✅ whose
-meaning is assumed. `rustup` owns Rust here and does act on its own file:
-
-```
-$ rustup show active-toolchain ; echo "exit=$?"
-1.92.0-x86_64-unknown-linux-gnu (overridden by '/home/aylac/Projects/Trinity/rust-toolchain.toml')
-exit=0
-
-$ rustc --version ; echo "exit=$?"
-rustc 1.92.0 (ded5c06cf 2025-12-08)
-exit=0
-```
-
-`rust-toolchain.toml` is committed and its first line says why it is not `.tool-versions`.
-
-### D2 — the Tauri CLI's derivation command is not `cargo tauri --version`
-
-The Decision named `cargo tauri --version`. On this machine, before and after everything:
-
-```
-$ cargo tauri --version ; echo "exit=$?"
-error: no such command: `tauri`
-exit=101
-```
-
-`ex_tauri` does not expect the CLI on `PATH`. It installs the CLI itself, into its own
-installation path, which defaults to `_build/_tauri` and is gitignored:
-
-```
-ex_tauri-0.2.0/lib/ex_tauri/install/helpers.ex:493
- ["install", "tauri-cli", "--version", "^#{cli_version}", "--root", "."]
-ex_tauri-0.2.0/lib/ex_tauri.ex:99
- def installation_path, do: ... Path.join(Path.dirname(Mix.Project.build_path()), "_tauri")
-```
-
-Run exactly as `ex_tauri` would run it:
-
-```
-$ cargo install tauri-cli --version '^2' --root _build/_tauri ; echo "exit=$?"
- Installing _build/_tauri/bin/cargo-tauri
- Installed package `tauri-cli v2.11.4` (executable `cargo-tauri`)
-exit=0
-
-$ _build/_tauri/bin/cargo-tauri tauri --version ; echo "exit=$?"
-tauri-cli 2.11.4
-exit=0
-```
-
-**The measured pin is 2.11.4** and the deriving command is the one above, not the Decision's.
-Two consequences are stated in the row rather than left implicit: the `^2` requirement floats,
-so 2.11.4 is what it resolved to on 2026-09-06 and not a lower bound anyone re-running this
-will necessarily get; and because the binary lives under a gitignored path, **no file in the
-tree carries this pin**. Its VERSIONS.md mark is 📐, never ✅ — see D3's second half.
-
-### D3 — a fourth pin the Decision did not anticipate: Zig, and it must be exact
-
-Burrito 1.6.0 does not accept a Zig range. `deps/burrito/lib/burrito.ex`:
-
-```
-@zig_version_expected %Version{major: 0, minor: 16, patch: 0}
-...
-if version != @zig_version_expected do
- Log.error(:build, "Your Zig version does not match the one Burrito requires! ...")
- exit(1)
-end
-```
-
-Zig was absent (`zig version` → exit 127). `asdf` **does** have a zig plugin, so unlike Rust
-this one belongs in `.tool-versions` and is a real pin there:
-
-```
-$ asdf plugin add zig && asdf install zig 0.16.0 ; echo "exit=$?"
-exit=0
-
-$ cat .tool-versions
-erlang 28.5.0.5
-elixir 1.20.4-otp-28
-zig 0.16.0
-
-$ asdf current ; echo "exit=$?"
-zig 0.16.0 /home/aylac/Projects/Trinity/.tool-versions true
-exit=0
-```
-
-**The mark mechanism had to change, and this is the line's red.** Before this commit,
-`Mix.Tasks.Versions.Gen.mark/3` marked *every* toolchain row from a hardcoded constant:
-
-```elixir
-def mark(_row, :toolchain, _locked), do: "✅ `.tool-versions`"
-```
-
-so `VERSIONS.md` line 71 read
-
-```
-| `Rust + Tauri CLI` | stable | ✅ `.tool-versions` | ... |
-```
-
-while
-
-```
-$ grep -in 'rust\|tauri' .tool-versions ; echo "exit=$?"
-exit=1
-```
-
-A ✅ naming a file that carried neither name — **finding B3's defect, living inside the
-enforcer slice 000 built to prevent it.** `test/versions_toolchain_mark_test.exs` was committed
-failing at `82e74a3` (3 of 5 failing, `mix test` exit 2) and is green after the fix. Every
-toolchain row now states a `:from`: `{:file, path, needle}` marks ✅ only when that file
-actually carries the pin and ❌ when it does not, and `{:command, cmd}` marks 📐 and can never
-reach ✅, because nothing at this sha verifies it.
-
-### D4 — the Credo check had to leave `lib/`, which is a change to slice 000's tree
-
-The first `MIX_ENV=prod mix release` never reached anything about releases:
-
-```
-$ MIX_ENV=prod mix release desktop
- error: module Credo.Check is not loaded and could not be found
- │
- 12 │ use Credo.Check,
- └─ lib/trinity/credo/no_eval_on_model_output.ex:12
-exit=1
-```
-
-`credo` is `only: [:dev, :test]` and `lib/` compiles in every environment, so slice 000's own
-Credo check makes the app uncompilable in `:prod`. Under §8 that red is at an earlier fault
-than line 3's claim and had demonstrated nothing, so it was fixed before the real red was
-taken: the check moved to `credo_checks/`, which `elixirc_paths/1` adds for `:dev` and `:test`
-only. The module name, its test and `.credo.exs` are unchanged.
-
-**This is a change outside packaging by the letter of the plan's constraint.** It is recorded
-here rather than waved through. The judgement made: a tree that cannot compile under
-`MIX_ENV=prod` cannot be packaged at all, so this is a packaging prerequisite rather than a
-widening of scope, and it adds no domain code — it moves one file and adds two lines to
-`elixirc_paths/1`. **If the owner reads it the other way, it should be lifted out of slice 001
-and given to a slice of its own**, and the rest of line 3 stands without it only in the sense
-that nothing after this point could have been measured.
-
-### D5 — `burrito` is now a direct dependency, not `only: :dev`
-
-`&Burrito.wrap/1` is a release step and runs under `MIX_ENV=prod`. `ex_tauri` is `only: :dev`
-and so is the burrito it brings, so the module would not exist in the environment that calls
-it. `{:burrito, "~> 1.6"}` is declared directly. Its VERSIONS.md row already existed and now
-marks ✅ from the lock at 1.6.0.
-
-### D6 — the `windows_x86_64` target cannot be built on this machine
-
-All three targets are declared in `mix.exs`. Building all three:
-
-```
-$ MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
-...
---> Resolving ERTS: {:precompiled, [version: "28.5.0.5"]}
---> Remote ERTS From Beam Machine: https://github.com/erlang/otp/releases/download/OTP-28.5.0.5/otp_win64_28.5.0.5.exe
-** (RuntimeError) Couldn't find 7z/7zz
- (burrito 1.6.0) lib/util/default_erts_resolver.ex:80
-exit=1
-```
-
-The Windows ERTS ships as a `.exe` installer and burrito unpacks it with 7z. None of
-`7z 7zz 7za 7zr p7zip` is present; `apt-cache policy p7zip-full` reports `Installed: (none)`,
-and installing it needs root, which CLAUDE.md §7 keeps off this agent's hands.
-
-**This is a host prerequisite, not a code defect**, and it goes to the owner as a question and
-to the manual queue rather than being fixed here. Line 3's green is therefore taken on the one
-target this host can build:
-
-```
-$ BURRITO_TARGET=linux_x86_64 MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
-exit=0
-
-$ stat -c '%n %s' burrito_out/* ; echo "exit=$?"
-burrito_out/desktop_linux_x86_64 20697016
-exit=0
-
-$ file burrito_out/desktop_linux_x86_64
-burrito_out/desktop_linux_x86_64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
-```
-
-**One unexpected positive, stated as measured and no further.** In the three-target run before
-it failed on Windows, burrito cross-built `desktop_macos_aarch64` (13,782,104 bytes) on this
-Linux host through Zig. That binary is unsigned, unnotarised and cannot be launched here, so it
-proves that the macOS *cross-compile* works and **nothing about whether the macOS app runs**.
-AC2 stays `[manual]` with "no machine available" unchanged.
-
-### A method error of mine, recorded because it recurred
-
-I measured the release twice: once piped to `tail` for the output, once redirected to
-`/dev/null` for the exit code. Burrito caches ERTS between runs, so the two invocations were
-not the same run, and I printed `exit=0` beside output from a run that had raised. This is the
-third time in this slice that measuring a thing twice has produced a claim about neither run.
-The rule I am applying from here: **one invocation, output to a file, exit code taken from that
-same invocation.** Every command block above was taken that way.
-
-Redoing it that way also surfaced something worth keeping: `mix release` with a release
-directory already present prompts `Overwrite? [Yn]`, gets no stdin, and **exits 0 having built
-nothing**. `--overwrite` is on every release command in this slice for that reason.
-
-### Follow-ups this line opened
-
-- `7z`/`7zz` is a host prerequisite for the Windows target. Needs root, so it is the owner's to
- install or the CI runner's to provide. Blocks nothing on Linux.
-- The `^2` Tauri CLI requirement floats and no file in the tree pins it. If a reproducible
- desktop build matters later, that pin needs somewhere real to live.
-- `mix release`'s exit-0-on-declined-overwrite is a footgun for any CI job that omits
- `--overwrite`; `.github/workflows/package.yml` at line 9 must carry it.
-
----
-
-## Line 4 — `Trinity.Paths`. Green, and it opened a hole in slice 000's skip enforcer.
-
-Dated 2026-09-06. Red committed at `ec64ac7`: the first pass resolved one root for every OS and
-`PathsTest` failed 3 of 6 on it, including "the three roots are distinct for the same home",
-which collapsed to `["/h/.local/share/trinity"]`. Green after the branch split, 6 of 6.
-
-The three roots are `SLICE.md`'s. The case difference between them is deliberate and is stated
-in the module: `trinity` lower-case is the XDG convention, `Trinity` capitalised is the macOS
-and Windows convention.
-
-### Two enforcer findings, both from the same three lines of code
-
-**Sobelow flags `File.mkdir_p!(dir)`** as `Traversal.FileModule`, low confidence, because `dir`
-is a variable. Every possible implementation of this function passes a variable there, so no
-artifact change removes it; it is a named exception. It was taken **inline**, with
-`@sobelow_skip`, rather than in `.sobelow-skips`, because that file keys on file **and line**
-and slice 000 already measured what that costs — adding SPDX headers moved `router.ex:10` to
-`:12` and silently reopened the finding. `paths.ex` is still being edited this slice.
-
-**Which exposed the hole.** `SobelowSkipsTest` asserted every skip carried a reason, and it only
-knew about `.sobelow-skips`. An `@sobelow_skip` attribute is a second skip mechanism it could
-not see, so a reasonless inline skip passed the gate. The test now derives its population from
-`git ls-files` and requires a `# sobelow_skip reason:` line above every inline skip. Demonstrated
-by planting the violation and removing it, the way slice 000's five enforcers were:
-
-```
-$ mix test test/sobelow_skips_test.exs ; echo "exit=$?" # reason comment removed
- 1) test inline @sobelow_skip attributes every inline skip is immediately preceded by its reason
- these @sobelow_skip attributes carry no `# sobelow_skip reason:` line above them:
- ["lib/trinity/paths.ex:71"]
-exit=2
-
-$ mix test test/sobelow_skips_test.exs ; echo "exit=$?" # reason restored
-Result: 3 passed
-exit=0
-```
-
-**The check was wrong twice before it was right, and both corrections were to the check.** First
-its population was `String.contains?(line, "@sobelow_skip")`, which matched its own moduledoc
-and its own assertion message — five false hits in one file. The population is now an attribute
-*definition* anchored at the start of a line. Second, it demanded the reason on the single line
-immediately above, and rejected a correct six-line reason for being six lines; the rule now
-walks the contiguous comment block above the attribute. Neither fix was an exemption. In both
-cases the check was making a claim about the tree that the tree did not owe it.
-
-### `@sobelow_skip` does not survive `--warnings-as-errors` on its own
-
-```
-warning: module attribute @sobelow_skip was set but never used
- 77 │ @sobelow_skip ["Traversal.FileModule"]
- └─ lib/trinity/paths.ex:77: Trinity.Paths (module)
-Compilation failed due to warnings while using the --warnings-as-errors option
-```
-
-Sobelow reads the attribute out of the source AST and the compiler never sees it used.
-`Module.register_attribute(__MODULE__, :sobelow_skip, persist: true)` makes it a real attribute
-to the compiler without changing what sobelow reads; `MIX_ENV=test mix compile
---warnings-as-errors --force` then exits 0. **Anyone adding an inline sobelow skip to this
-project needs that line or the gate rejects the file**, which is why it is in `paths.ex` with
-its reason next to it rather than in a commit message.
-
-### One thing line 4 did not do
-
-`Trinity.Paths.database_path/0` exists and nothing calls it yet. `config/runtime.exs` still
-raises without `DATABASE_PATH`, which is right for a server and wrong for a double-clicked
-binary. Wiring it is line 5's, with the smoke run as its red.
-
-### A correction to line 3's D6
-
-I wrote that the Windows 7z prerequisite was something the Decision "did not anticipate". That
-is wrong about the record: `SLICE.md`'s own Risks section already says "Windows ERTS unpacking
-needs 7-Zip per Burrito notes; document." **This supersedes that framing.** What line 3 added
-was the measurement — the exact error, the resolver line it comes from, and that no 7z variant
-is installed and installing one needs root. The risk was written down before I got there.
-
----
-
-## Line 5 — `Trinity.Smoke`, and the measurement that would have proved nothing
-
-Dated 2026-09-06. Red committed at `f7406c5`: `run/2` reported the port and returned without
-calling `halt`. `SmokeTest` failed 1 of 4 on it. Green after `halt.(0)`, 4 of 4.
-
-Two things changed with it, and both were section-8 problems rather than design choices.
-
-**`config/test.exs` now serves.** The first attempt at the red failed with `{:error,
-:no_server_found}` — the generator's `server: false` means there is no bound port to ask about,
-so the failure was at an earlier fault than the claim.
-
-**`config/runtime.exs`'s port line is now scoped to `:dev`.** Unscoped, it runs in every
-environment and is evaluated *last*, so it silently overrode the ephemeral port
-`config/test.exs` had just been given:
-
-```
-$ MIX_ENV=test mix run --no-start -e 'IO.inspect(Application.get_env(:trinity, TrinityWeb.Endpoint)[:http])'
-[ip: {127, 0, 0, 1}, port: 4000]
-```
-
-A test asserting "the reported port is the port that was bound" was therefore passing against a
-hardcoded 4000. The test now asserts the *configured* port is 0 and the *reported* port is not,
-which is a claim a fixed port cannot satisfy.
-
-### The binary does not stay alive, and that nearly made AC7 vacuous
-
-`PHX_SERVER=true ./burrito_out/desktop_linux_x86_64` exited immediately, having started the
-endpoint. It is not stdin, and it is not a crash. Burrito launches the release as
-
-```
-erl ... -noshell -s elixir start_cli -mode embedded -boot ... -extra
-```
-
-(`deps/burrito/src/erlang_launcher.zig:53`). `elixir start_cli` is the ordinary Elixir CLI
-entry point, and the ordinary Elixir CLI **halts when its command list is empty**, exactly as
-`elixir -e ''` does. A packaged Phoenix server therefore needs `--no-halt`:
-
-```
-$ PHX_SERVER=true timeout 20 ./burrito_out/desktop_linux_x86_64 --no-halt ; echo "exit=$?"
-exit=124 # 124 is timeout killing it — it was still running
-```
-
-**Which means the AC7 measurement I was about to take proves nothing.** `SLICE.md` AC7 reads
-"running the binary under `--smoke` exits 0 and leaves no process behind", and the binary exits
-0 and leaves no process behind **whether or not `--smoke` is passed**, because it halts by
-itself. My first smoke run — `./burrito_out/desktop_linux_x86_64 --smoke`, exit 0, clean `ps` —
-is a true observation that attributes nothing to the code under test.
-
-The discriminating form adds `--no-halt`, which makes staying alive the default so that exiting
-is attributable:
-
-```
-$ ps -eo pid,ppid,comm | grep -E 'desktop_linux|beam.smp|erl_child_setup' # BEFORE
-2487028 2485809 beam.smp
-2487056 2487028 erl_child_setup
-
-$ ./burrito_out/desktop_linux_x86_64 --no-halt --smoke ; echo "exit=$?"
-18:56:11.939 [info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:45031 (http)
-TRINITY_SMOKE_PORT=45031
-exit=0
-
-$ ps -eo pid,ppid,comm | grep -E 'desktop_linux|beam.smp|erl_child_setup' # AFTER
-2487028 2485809 beam.smp
-2487056 2487028 erl_child_setup
-
-$ diff ac7-before.txt ac7-after.txt ; echo "exit=$?"
-exit=0
-```
-
-The control above and this run together are the claim: with `--no-halt` and no `--smoke` it is
-still running at 20 s; with `--no-halt --smoke` it exits 0 immediately and the process list is
-byte-identical either side. **AC7's wording should say `--no-halt --smoke`, and PROOF.md will
-say why.** Recorded here rather than quietly taking the easier reading.
-
-### AC1 — the packaged binary serves
-
-```
-$ PHX_SERVER=true ./burrito_out/desktop_linux_x86_64 --no-halt &
-18:54:xx [info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:35159 (http)
-
-$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:35159/ ; echo "exit=$?"
-200
-exit=0
-
-$ curl -sS http://127.0.0.1:35159/ > page.html ; echo "exit=$?" # 20165 bytes
-exit=0
-$ grep -o 'Phoenix Framework\|csrf-token' page.html | head -3
-csrf-token
-Phoenix Framework
-Phoenix Framework
-```
-
-### A leaked sidecar, found by accident, and it is AC8's exact failure mode
-
-Stopping the serving binary with `kill ` left this behind:
-
-```
-$ ps -eo pid,ppid,comm | grep desktop_linux # nothing
-$ ps -eo pid,ppid,comm | grep beam.smp
-2765267 139843 beam.smp
-$ tr '\0' ' ' < /proc/2765267/cmdline
-/home/aylac/.local/share/.burrito/desktop_erts-16.4.0.5_0.1.0/erts-16.4.0.5/bin/beam.smp -- -root ...
-```
-
-The wrapper died; **the BEAM it launched survived and was reparented to init.** That is
-precisely the property AC8 asks about — "killing the window terminates the sidecar within 5 s"
-— failing on this machine, by signal rather than by window, and it says the wrapper does not
-forward termination to its child. AC8 stays `[manual]` because no window exists to close here,
-but this is evidence that the behaviour underneath it is **not** currently correct, and it is a
-follow-up rather than something this slice fixes: the fix is in burrito's wrapper or in a
-supervisor around it, and neither is packaging wiring.
-
-### An intermittent boot error, reported because it is intermittent
-
-Some runs print, before the endpoint line:
-
-```
-[error] Exqlite.Connection (#PID<0.249.0> ("db_conn_2")) failed to connect: ** (Exqlite.Error) database is locked
-```
-
-Four of five pool connections, on a fresh empty database. The clean AC7 run above did not print
-it. The app serves either way, so it does not fail a criterion; the likely cause is
-`Ecto.Migrator` holding the file while the pool connects, which is a slice 010 question about
-boot ordering, not a packaging one. Recorded so it is not discovered twice.
-
-### What is now in `config/runtime.exs`, and one thing deliberately not
-
-`DATABASE_PATH` falls back to `Trinity.Paths.database_path/0`; the endpoint binds
-`127.0.0.1` on an ephemeral port rather than the generator's `{0,0,0,0,0,0,0,0}`, because a
-binary users double-click should not put a Phoenix app on their LAN.
-
-`SECRET_KEY_BASE` falls back to a value generated **fresh on every boot and written nowhere**.
-Sessions do not survive a restart of the packaged app. That is a stated limit, not an
-oversight: persisting it means writing a credential to the user's disk and deciding its mode,
-its rotation, and what happens when the file is copied to another machine, and CLAUDE.md §7
-puts that in front of the owner. Slice 100 owns the desktop session story.
-
-### Follow-ups this line opened
-
-- The burrito wrapper does not forward termination to the BEAM it launches. Blocks AC8's
- property from ever being true; needs the owner or slice 100.
-- `--no-halt` is mandatory for the packaged server and is easy to omit. `docs/packaging.md`
- (line 8) and `.github/workflows/package.yml` (line 9) must both carry it.
-- Intermittent `database is locked` for four of five pool connections at boot.
-
----
-
-## Lines 8 to 14 — docs, CI, ADR-0004, coverage, gates
-
-Dated 2026-09-06.
-
-### Line 8 — `docs/packaging.md`
-
-Size from `stat -c %s`, cold start from a timed loop, linux x86_64 only. Two figures are
-reported where one would have flattered the binary: **1 578 ms on the first launch of a fresh
-install**, when Burrito extracts its payload into `~/.local/share/.burrito`, and **231–245 ms
-across six warm runs** afterwards. The first is the one a person forms an impression from, so
-reporting only the warm figure would understate it sixfold. Extracted payload on disk is 88 MB
-against a 20 777 960-byte binary. **Cold start to first *paint* is absent from the table**,
-because first paint needs a window.
-
-### Line 9 — `.github/workflows/package.yml`, and one thing checked rather than assumed
-
-Three runners, three targets, each building, smoking under `--no-halt --smoke`, curling for a
-200, comparing `ps` either side of the exit, and uploading both the artifact and the launch
-log. Every job ends by writing to its own summary what it did **not** prove, which is line 10's
-requirement put where a reader of a green tick will actually meet it.
-
-The Zig version is read out of `.tool-versions` with `awk` rather than written into the
-workflow, because Burrito compares Zig for equality and a second copy of the number is a second
-thing to drift.
-
-**Checked rather than assumed:** `.tool-versions` gained a `zig` line this slice, and
-`.github/workflows/gate.yml` feeds that file to `erlef/setup-beam`. Its parser only consumes
-lines whose first token is in `['erlang', 'elixir', 'gleam', 'rebar']` and skips everything else
-silently, so the existing gate workflow is unaffected. That is from reading
-`setup-beam.js`'s `parseToolVersionsFile`, not from expecting it to be fine.
-
-**Not verified: this workflow has never run.** It is written from what was measured locally.
-AC5 was already `[manual]` for exactly this reason — the run is on a remote this branch cannot
-vouch for — and it stays that way.
-
-### Line 12 — ADR-0004 is **not** confirmed, and the plan expected it would be
-
-The G1 plan said line 12 would carry "a status word from the vocabulary `docs/03` allows —
-`accepted`, since this slice is what its current `proposed → to be confirmed by Slice 001` was
-waiting on". **That was written before the measurement and it is wrong.** ADR-0004's own
-decision text sets its exit condition:
-
-> Slice 001 must produce a running smoke build on macOS **and** Windows (and Linux if
-> available).
-
-macOS cross-compiles but has never been executed. Windows was never built — no 7z. Neither
-half of the condition is met, so the status stays `proposed`, and the correction appended to
-the ADR says what *was* settled (ex_tauri runs on the pinned toolchain; Burrito packages and
-serves on Linux), what was not, and the lift condition. Stamping `accepted` because the plan
-anticipated it would be is the exact defect this project keeps catching.
-
-The alternatives table is kept, as the plan required. The fallback path is **untriggered**, not
-eliminated: its trigger is "Windows fails with ex_tauri", and Windows was never attempted.
-
-### Line 13 — the coverage rule fired, and mix.exs's threshold had never worked
-
-`mix test --cover` exited **3**, not 0:
-
-```
-Coverage test failed, threshold not met:
- Coverage: 30.37%
- Threshold: 90.00%
-```
-
-`mix.exs` carried `test_coverage: [threshold: 0]` with a comment saying the built-in 90% gate
-"is turned off". **It was not.** Mix reads the threshold from the `:summary` sub-option —
-`Keyword.get(opts, :summary, true)`, then `get_threshold(true)` returns the built-in 90
-(`lib/mix/lib/mix/tasks/test.coverage.ex:278, 409` in Elixir 1.20.4) — so a top-level
-`:threshold` key is ignored entirely. The key has been in the wrong place since slice 000 and
-the comment above it has been claiming otherwise for as long. Corrected to
-`test_coverage: [summary: [threshold: 0]]`; `mix test --cover` now exits 0.
-
-This did not affect `mix gate`, which runs `test` and then `mix trinity.coverage` and never
-`--cover`. It affected anyone reading the comment.
-
-**The drop rule then did real work for the first time.** Seeded 4.01 points under 000:
-
-```
-$ mix trinity.coverage ; echo "exit=$?"
-** (Mix) trinity.coverage: 001 23.0% is 4.01 points below 000 27.01%, more than the
- 3.0-point tolerance. Name the reason in the slice's NOTES.md.
-exit=1
-```
-
-and at the real figure:
-
-```
-$ mix trinity.coverage ; echo "exit=$?"
-trinity.coverage: 001 30.37% vs 000 27.01% — OK
-exit=0
-```
-
-**001 is 30.37%, up 3.36 points from 000's 27.01%.** No reason is owed, because the rule is
-about drops.
-
-### Line 14 — both gates, and the plain server
-
-`mix gate` exit 0 (68 tests). `scripts/plan_check.sh` PASS. `timeout 25 mix phx.server` exit
-124 — still running when the timeout killed it — serving on 127.0.0.1:4000.
-
-That run was nagging on every start: `ex_tauri` warns until `:version` and `:app_name` are
-configured, so the plain dev server was complaining about a desktop shell it does not run.
-Configured in `config/dev.exs`. Only its major is consumed — `extract_cli_version/1` takes the
-major and installs `tauri-cli ^` — so `2.5.1` is kept as the library's own suggested
-value rather than inventing a pin it does not use.
-
-**One warning is deliberately left in place**: "ExTauri currently targets OTP 27 but you are
-running OTP 28." It is upstream's, its stated reason is refuted by ADR-0005's second correction
-and by ADR-0004's, and silencing a warning whose content this project has measured to be wrong
-would hide the one place a reader might go looking.
-
----
-
-## Owner decisions on the G3 open points, 2026-09-06
-
-### D4 — **accepted.** The Credo check leaving `lib/` is a packaging prerequisite
-
-Owner's word, 2026-09-06: nothing compiles under `MIX_ENV=prod` with a dev-only dependency
-referenced from `lib/`, and a spike that cannot compile for prod has not spiked. D4 is accepted
-as within slice 001's scope and does not need a slice of its own.
-
-**The principle attached to it, which is a correction to how I worked and not to the code:**
-
-> Post the Question, then proceed in parallel with the work that does not depend on the answer.
-> Commit first and ask second is the wrong order even when the call is right.
-
-I did the second thing. I found the conflict, judged it a prerequisite, made the change, wrote
-the deviation, committed, and raised it at G3 — by which point the branch had eight commits
-standing on it. The right shape was to post the Question the moment `MIX_ENV=prod mix release`
-stopped on `lib/trinity/credo/no_eval_on_model_output.ex`, and carry on with lines 4 and 5,
-neither of which depended on the answer. Being right about the call does not make the order
-right, because the order is what gives the owner the chance to be wrong about it cheaply.
-
-### AC8 — exits **false**, citing finding F1, not unproven
-
-Owner's word: the wrapper finding is filed on the board, and slice 001 exits with AC8 marked
-false rather than unproven. The distinction is the one CLAUDE.md §8 draws between an empty
-population and a satisfied property: nobody has closed a window, but the property underneath
-has been measured and it does not hold. The transcript is in `PROOF.md` under AC8 and posted to
-the finding issue as an Answer.
-
-### Correction — "reparented to init" is wrong, twice above
-
-**This supersedes the phrase "reparented to init" in the line 5 record and in the line 5 commit
-message.** Measured:
-
-```
-$ ps -o pid,ppid,comm -p 139843
- PID PPID COMMAND
- 139843 1 systemd
-
-$ ps -o pid,ppid,comm -p 2936771 # the orphaned BEAM, after its parent was killed
- PID PPID COMMAND
-2936771 139843 beam.smp
-```
-
-The orphan is reparented to **pid 139843, the per-user `systemd` instance acting as a
-subreaper**, not to pid 1. The wrapper's own ppid was 139843 before the kill as well, so the
-child was re-attached to the nearest subreaper rather than to init. Nothing about the finding
-changes — the BEAM outlives the wrapper either way — but "init" names a process that was not
-involved, and a stranger checking this would have looked at the wrong pid.
-
-### One thing the transcript adds that the earlier note did not have
-
-The orphan is not merely alive. **It is still serving**, after its parent is gone:
-
-```
-$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:39075/ ; echo "exit=$?"
-200
-exit=0
-```
-
-That is a stronger statement of the defect than "a process remains", and it is what makes
-F1 a liveness-contract question rather than a tidiness one: a shell that closes its window
-and kills the wrapper leaves a Phoenix app listening on the user's loopback with nothing on
-screen to say so.
-
-### The `package` workflow had never run, and the reason was mine
-
-`gh run list --branch slice/001-packaging-spike --limit 5` showed three `gate` runs and **no
-`package` run**. Its push trigger was `branches: [main]`, so the branch whose evidence depends
-on it was the one branch it ignored. Corrected to `[main, 'slice/**']` in `0326dea`.
-
-Worth recording as a class, not an incident: **a workflow that never fires reports nothing and
-fails nothing**, so nothing in the gate, in `plan_check`, or in my own review would ever have
-caught it. It was found because the owner asked for the run. The evidence that a CI job works
-is a run, and until there is one, a workflow file is a plan.
-
-### Why the tree says "F1" and not the board id, and a miss of mine on the way there
-
-The owner's word was that AC8 exits false "citing it", the wrapper finding's issue. **The tree
-cannot carry a board identifier**: `plan_check` rule 7 forbids it, and that rule exists because
-the repository is the durable artifact and board numbering is not. The house style already
-resolves this — findings are cited in the tree by label (`H7`, `B3`, `M5`, `I3`), and the
-finding issue's own title begins `F1 —`. So `PROOF.md` and this file cite **F1**, the board
-carries the mapping, and both the instruction and the enforcer are satisfied without weakening
-either.
-
-**The miss, and I made it twice.** I wrote the board identifier into `PROOF.md` and
-`NOTES.md`, ran `mix gate` and `plan_check.sh`, read the gate's `exit=0`, and committed and
-pushed without reading the line below it — `plan_check exit=1`, eight `FAIL` lines, all of
-them this. Then, writing *this very paragraph*, I quoted the identifier again to describe the
-mistake, ran both checks again, read the gate again, and pushed again on a `plan_check exit=1`.
-Two commits are on the remote failing rule 7. Both are corrected by the commits that follow
-them rather than rewritten, because history on a pushed branch is a record.
-
-Running a check and not reading its result is worse than not running it: it produces the
-appearance of verification. The gate and `plan_check` are two commands with two exit codes, and
-"verified" names its command **and** exit code — the rule I quote at other people. The second
-occurrence is the instructive one: an enforcer catches the thing it is for even when the
-mention is *about* the violation, and prose describing a rule does not get an exemption from
-it. That is the same lesson slice 000 recorded three times, when the version-form check, the
-SPDX check and the name check each caught their own artifacts.
-
----
-
-## The owner's manual check on this machine — prepared procedure, 2026-09-06
-
-This machine **is** a Linux desktop and I had been writing as though it were not:
-
-```
-$ echo "DISPLAY=$DISPLAY XDG_SESSION_TYPE=$XDG_SESSION_TYPE"
-DISPLAY=:0 XDG_SESSION_TYPE=x11
-```
-
-`DISPLAY=:0` on an X11 session. Earlier records in this file say "no desktop session available
-on this machine", carried over from the G1 framing. **That is superseded: there is a display.**
-What there is not, yet, is anything for `mix ex_tauri.dev` to run.
-
-### Two blockers, neither of which is the window
-
-**Blocker 1 — the Tauri project does not exist.** `SLICE.md` Scope says "run `mix
-ex_tauri.install`" and Deliverables names a `tauri/` scaffold.
-
-```
-$ git ls-files tauri | wc -l
-0
-```
-
-The approved G1 plan's fifteen lines never included the install step, so the deliverable was
-dropped at plan time and I did not flag it. `mix ex_tauri.dev` has no project to run against,
-which is why AC2, AC3 and AC4 were unrunnable by anyone this slice — not only for want of
-machines.
-
-`mix ex_tauri.install` is an `Igniter` task and it **modifies tracked files**: it adds
-`ExTauri.ShutdownManager` to the application's children, writes four `config` entries, updates
-`mix.exs`, and adds a `tauri-bridge` div to the layout. Given slice 000's experience of a
-generator overwriting `README.md` and `.gitignore`, that is not something to run silently at
-G3 on a slice awaiting review. **It is a Question on the slice issue, and the work below does
-not depend on the answer, so it is prepared either way.**
-
-**Blocker 2 — the Tauri v2 system libraries are absent.** Ubuntu 24.04.4 LTS, so the
-`webkit2gtk-4.1` line is the right one:
-
-```
-$ for p in libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf; do
- printf '%-34s ' "$p"; dpkg -s "$p" >/dev/null 2>&1 && echo installed || echo "NOT installed"
- done
-libwebkit2gtk-4.1-dev NOT installed
-libgtk-3-dev NOT installed
-libayatana-appindicator3-dev NOT installed
-librsvg2-dev NOT installed
-patchelf NOT installed
-```
-
-All five resolve in this machine's apt. Installing needs root, which CLAUDE.md §7 keeps off
-this agent's hands.
-
-### The procedure, in order, with who runs each step
-
-**Step 1 — owner, needs root.**
-
-```
-sudo apt install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf
-```
-
-**Step 2 — me, on approval.** `mix ex_tauri.install`, reviewed as a diff and committed on its
-own so the generator's edits are reviewable apart from everything else.
-
-**Step 3 — owner, on this desktop. The launch.**
-
-```
-mix ex_tauri.dev
-```
-
-Expected: a native window opens showing the Phoenix scaffold page. First run compiles the Rust
-shell and takes minutes; the window appears after `Running DevCommand`.
-
-**Step 4 — owner. Close the window with its ✕, then, in the same terminal:**
-
-```
-ps -eo pid,ppid,comm | grep -E 'desktop_linux|beam.smp|erl_child_setup|trinity' | grep -v grep ; echo "exit=$?"
-```
-
-Run it **twice, about five seconds apart**, and paste both. `exit=1` from `grep` means nothing
-matched, which is the pass. Any surviving `beam.smp` whose `/proc//cmdline` points into
-`~/.local/share/.burrito/` is finding F1 reproducing through the window rather than through a
-signal, which is the measurement nobody has taken yet.
-
-**What each step settles.** Step 3 is **AC4** — a native window showing the scaffold on Linux —
-and, with a stopwatch from `return` to the window painting, the *first paint* half of **AC6**
-on one OS. Step 4 is **AC8's** manual half and the first evidence of whether
-`ExTauri.ShutdownManager` — which step 2 installs — actually closes finding F1.
-
-### The Windows artifact
-
-Still pending on 7z:
-
-```
-$ for c in 7z 7zz 7za 7zr; do printf '%-5s ' "$c"; command -v "$c" || echo "(absent)"; done
-7z (absent)
-7zz (absent)
-7za (absent)
-7zr (absent)
-```
-
-Once any of them is on `PATH`, the cross-build is one command and needs no display:
-
-```
-BURRITO_TARGET=windows_x86_64 MIX_ENV=prod mix release desktop --overwrite
-```
-
-On success **AC3 reads "built, not run"** rather than "never built", and `docs/packaging.md`'s
-target table gains a size for `windows_x86_64`. It will not read "runs", because nothing here
-can execute a Windows binary.
-
----
-
-## G4 — the four requested changes. Deviation D7, recorded before its commit.
-
-Dated 2026-09-07. Owner decision at G4 on the slice issue: changes requested, both Questions
-answered yes, 7z and the five Tauri v2 libraries installed by the owner. **Third time I have
-typed a board identifier into the tree this slice**; `plan_check` rule 7 caught it before the
-commit this time, because both exit codes were read. That is the check earning its place, and
-the Decision's item 3 — one exit code to read — is what stops the other half of the mistake.
-Verified before starting:
-
-```
-$ for c in 7z 7zz 7za 7zr; do printf '%-5s ' "$c"; command -v "$c" || echo "(absent)"; done
-7z /usr/bin/7z
-7zz (absent)
-7za /usr/bin/7za
-7zr /usr/bin/7zr
-
-$ for p in libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf; do
- printf '%-34s ' "$p"; dpkg -s "$p" >/dev/null 2>&1 && echo installed || echo "NOT installed"
- done
-libwebkit2gtk-4.1-dev installed
-libgtk-3-dev installed
-libayatana-appindicator3-dev installed
-librsvg2-dev installed
-patchelf installed
-```
-
-### D7 — `ex_tauri` is a dependency of every environment, not `only: :dev`
-
-**This supersedes what line 1 recorded as "the configuration" and what ADR-0004's first
-correction repeats from it:** `{:ex_tauri, "~> 0.2", only: :dev}`. That was the right answer to
-the question line 1 asked — does the default configuration compile on the pinned toolchain —
-and the wrong dependency spec for the slice's actual subject.
-
-`mix ex_tauri.install` adds `ExTauri.ShutdownManager` to the supervision tree unconditionally
-and writes `config :ex_tauri` into `config/config.exs`, which applies to every environment. On
-a `:dev`-only dependency that does not start:
-
-```
-$ mix gate ; echo "exit=$?"
-You have configured application :ex_tauri in your configuration file,
-but the application is not available.
-** (Mix) Could not start application trinity: exited in: Trinity.Application.start(:normal, [])
- ** (EXIT) an exception was raised:
- ** (ArgumentError) The module ExTauri.ShutdownManager was given as a child to a
- supervisor but it does not exist
-exit=1
-```
-
-**The generator is right and the dependency spec was wrong.** The heartbeat is the Rust
-window's only channel for telling the BEAM it has closed. It must exist in the binary that
-ships, and `only: :dev` guarantees it does not — which would have shipped finding F1's failure
-mode as a permanent property of every release, discovered by a user rather than by a test.
-
-The fix is two-sided, and either half alone is a trap:
-
-* `{:ex_tauri, "~> 0.2"}`, every environment, so the packaged binary carries the heartbeat.
-* the child excluded from `:test` **at compile time**, `@desktop_children if Mix.env() == :test`,
- rather than by asking whether the module happens to be loaded. A `Code.ensure_loaded?/1`
- guard returns the same empty list whether the exclusion was intended or the dependency
- vanished, and a heartbeat that is absent without saying so is the failure it exists to
- prevent.
-
-`test/desktop_children_test.exs` asserts both: that `desktop_children/0` is empty under `:test`,
-that `ex_tauri` carries no `:only` key, and that the module is nonetheless loadable in `:test`
-so "empty" means "excluded on purpose" and not "not installed".
-
-**A consequence to measure rather than assume:** `ex_tauri` in `:prod` drags `igniter`,
-`sourceror`, `rewrite` and `spitfire` into the release. AC5's binary size will move, and the
-new figure is measured and recorded rather than the old one being left in place.
-
-### Two smaller things from the same commit
-
-`config/dev.exs`'s `config :ex_tauri` block is removed. The generator wrote the same keys into
-`config/config.exs`, which covers every environment, and two copies of one pin is exactly the
-drift `mix versions.gen` exists to stop elsewhere. The reasoning moved to the surviving copy.
-
-The generator creates **`src-tauri/`**, not the `tauri/` that `SLICE.md` Deliverables names.
-The root `.gitignore`'s `/tauri/target/` rule is therefore stale — it names a directory that
-does not exist. Rust build output is covered by the generator's own `src-tauri/.gitignore`:
-
-```
-$ git check-ignore -v src-tauri/target/debug/foo ; echo "exit=$?"
-src-tauri/.gitignore:3:/target/ src-tauri/target/debug/foo
-exit=0
-```
-
-so nothing is at risk today. The stale root rule is a follow-up, not a fix in this commit.
-
-### The Linux shell builds — item 1's prerequisite, measured before handing the owner a command
-
-```
-$ cargo build --manifest-path src-tauri/Cargo.toml ; echo "exit=$?"
- Compiling webkit2gtk v2.0.2
- Compiling tao v0.35.3
- Compiling tray-icon v0.24.2
- Compiling tauri-plugin-single-instance v2.4.4
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 52.84s
-exit=0
-
-$ mix ex_tauri.info ; echo "exit=$?"
- ✔ webkit2gtk-4.1: 2.52.6
- ✔ rsvg2: 2.58.0
- ✔ rustc: 1.92.0 (ded5c06cf 2025-12-08)
- ✔ Rust toolchain: 1.92.0-x86_64-unknown-linux-gnu (overridden by '.../rust-toolchain.toml')
- - tauri 🦀: 2.11.5
-exit=0
-```
-
-The Rust compiles against the libraries the owner installed, and `rust-toolchain.toml` is doing
-the work D1 said it would — `ex_tauri.info` reports the override by name.
-
-**`src-tauri/Cargo.lock` is now tracked.** It is the Cargo-side `mix.lock`: without it the Rust
-dependency set is whatever resolves on the day, which is the unpinned-version defect this
-project spends most of its enforcement on. Added to `REUSE.toml`'s aggregate list beside
-`mix.lock`.
-
-**Two Tauri versions, and they are different things.** The **CLI** is 2.11.4 — VERSIONS.md's
-📐 row, derived from `_build/_tauri/bin/cargo-tauri tauri --version`. The **crate** is 2.11.5,
-now pinned by `src-tauri/Cargo.lock`. `config :ex_tauri, version: "2.5.1"` is neither: only its
-major is consumed. Three numbers that look like one pin and are not, recorded here so the next
-reader does not reconcile them.
-
-### The sidecar after the heartbeat entered it
-
-Rebuilt and re-smoked, because D7 changed what is inside the binary:
-
-```
-$ stat -c '%n %s' burrito_out/desktop_linux_x86_64
-burrito_out/desktop_linux_x86_64 21398872 # was 20777960
-
-$ ./burrito_out/desktop_linux_x86_64 --no-halt --smoke ; echo "exit=$?"
-TRINITY_SMOKE_PORT=36711
-exit=0
-
-$ diff before.txt after.txt ; echo "exit=$?"
-exit=0
-```
-
-**+620 912 bytes, +3.0%**, which is `igniter`, `sourceror`, `rewrite` and `spitfire` arriving in
-the release with `ex_tauri`. AC5's figure is updated to the measured 21 398 872 rather than the
-old one being left standing. The heartbeat does not keep the VM alive and does not shut it down
-early: exit 0, process list unchanged either side.
-
-**Correction, same day.** The line above says `src-tauri/Cargo.lock` was "added to `REUSE.toml`'s
-aggregate list beside `mix.lock`", and commit `a6085b3`'s message says the same. **Neither was
-true when written.** The edit that would have done it failed on a stale anchor — the path list
-had gained `.sobelow-skips` entries since I last read it — and I did not read the failure before
-writing the claim into both the file and the commit message. `mix gate` passed anyway, because
-`mix trinity.reuse` does not treat a `.lock` file as commentable, so nothing caught it. Fixed in
-the commit that follows; `a6085b3` stands with a message that was ahead of its tree.
-
-The general form, which is the third variant of one mistake this slice: **a claim written in the
-same breath as the change it describes is not evidence that the change happened.** Twice it was
-an exit code I did not read; here it was a `python3` traceback in the same output block as a
-green gate. The gate does not check that a commit message is true.
-
----
-
-## Item 1 — the owner's step, prepared. Everything up to the window is done.
-
-Dated 2026-09-07. Nothing below needs root and nothing below is a first-time compile: the Rust
-shell is already built (52.84 s, exit 0), so the launch is fast. Port 4000 is free, checked.
-`import` is present for the screenshot; `gnome-screenshot`, `scrot` and `flameshot` are not.
-
-**Step A — launch.** One command, from the repo root:
-
-```
-mix ex_tauri.dev
-```
-
-It starts the Phoenix dev server as the sidecar and runs `cargo tauri dev` against
-`http://127.0.0.1:4000`. Expect a window titled **Trinity**, 800×600, resizable, showing the
-Phoenix scaffold page.
-
-**Step B — first paint.** Start timing when you press Enter, stop when the window paints the
-scaffold. Seconds to one decimal is enough; this is AC6's first-paint figure and there is no
-other way to get it.
-
-**Step C — screenshot**, in a second terminal while the window is open:
-
-```
-import -window "$(xdotool search --name '^Trinity$' | head -1)" slices/001-packaging-spike/proof/ac4-linux-window.png
-```
-
-If `xdotool` is absent, `import -window root slices/001-packaging-spike/proof/ac4-linux-window.png`
-captures the whole screen and is fine.
-
-**Step D — close the window with its ✕**, then run this in the launching terminal, **twice,
-about five seconds apart**, and paste both:
-
-```
-ps -eo pid,ppid,comm,etimes | grep -E 'desktop_linux|desktop-x86_64|beam.smp|erl_child_setup|cargo-tauri|trinity' | grep -v grep ; echo "exit=$?"
-```
-
-`exit=1` from `grep` means nothing matched, which is the pass. **This is the measurement
-finding F1 needs first**, and it is the first time it is being taken on a build that has
-`ExTauri.ShutdownManager` in it: the heartbeat should stop the sidecar ~1.5 s after the window
-goes. Either outcome is the answer — if a `beam.smp` survives with a `cmdline` pointing into
-`~/.local/share/.burrito/`, F1 reproduces through the window and not merely through a signal,
-which is worse news and more useful than a pass.
-
-### What each step settles
-
-| Step | Criterion | Currently |
-|---|---|---|
-| A | **AC4** — a native window showing the scaffold on Linux | unproven |
-| B | **AC6**, first-paint half | unproven |
-| C | AC4's proof artefact | no screenshot exists on any OS |
-| D | **AC8**, and finding F1's first measurement against the heartbeat | false on the build without it |
-
-AC4 exits **proven or failed**, not unproven, whichever way it goes.
-
----
-
-## Item 1 — the owner's results, and what measuring the 5-second bound actually found
-
-Dated 2026-09-07. Owner's report from the X11 desktop, verbatim in the slice issue.
-
-**A. AC4 is proven.** `mix ex_tauri.dev` opened a window titled **Trinity** showing the Phoenix
-scaffold. The first native window in this project, on any OS.
-
-**B. AC6, first paint: 2–3 seconds** from Enter to the page appearing. Timed by hand, which is
-the only way a first paint can be timed, and stated to the precision it was taken at.
-
-**C. The screenshot.** `xdotool` is absent, so the `-window` form in my step C could not resolve
-a window id and `import` wrote a **185-byte stub** — a file that exists, is named like evidence,
-and contains nothing. Since overwritten by `import -window root`:
-
-```
-slices/001-packaging-spike/proof/ac4-linux-window.png 2814779 bytes, PNG 7680x2160
-sha256 e52785ce61ea9f69615ffa7602f7fd172b1fa5a310d8b69b1b11f7fc09bfabe7
-```
-
-**The stub is worth recording because of what it nearly was.** I handed over a command whose
-failure mode is a plausible-looking artefact rather than an error, and I had checked that
-`import` was present without checking that `xdotool` was. A 185-byte PNG committed under
-`proof/` would have satisfied every check this project has: the file exists, the path is right,
-`trinity.reuse` covers it, nothing reads its contents. **Size and digest are in PROOF.md for
-exactly that reason** — they are the only things that distinguish the artefact from the stub.
-
-**D. AC8 passes through the window**, and **my stated pass condition was wrong.** I told the
-owner `exit=1` from `grep` was the pass. My grep pattern matches `beam.smp`, and the owner's own
-session runs one, so the pass condition could never have been met. The owner's reading is the
-correct one and is the one recorded:
-
-```
-# before closing, window open
- 966625 966348 beam.smp 125 <- mix ex_tauri.dev
- 966653 966625 erl_child_setup 125
- 966776 966653 cargo-tauri 125
- 966847 966776 trinity 125 <- the Rust window
- 967584 966847 beam.smp 123 <- the sidecar
- 967635 967584 erl_child_setup 123
- 2487028 2485809 beam.smp 14543 <- this session's own BEAM
- 2487056 2487028 erl_child_setup 14543
-
-# after closing with the window's ✕
- 2487028 2485809 beam.smp 14603 <- same pids, older etimes
- 2487056 2487028 erl_child_setup 14603
-```
-
-Shell, sidecar and dev task all gone; the two survivors are the session's own, same pids,
-present before launch. **The recorded check excludes them by pid** rather than relying on an
-exit code that cannot be reached:
-
-```
-ps -eo pid,ppid,comm,etimes | grep -E 'desktop_linux|desktop-x86_64|beam.smp|cargo-tauri|trinity' \
- | grep -v grep | grep -vE '^ *(2487028|2487056) '
-```
-
-### The 5-second bound, measured. It is not 5 seconds and it is not the heartbeat.
-
-Six runs on the dev path: launch `mix ex_tauri.dev`, wait for the window and its sidecar, wait
-3 s so the heartbeat has connected at least once, kill the `trinity` process, poll until the
-sidecar's `beam.smp` is gone.
-
-```
-$ SIG=-TERM heartbeat.sh (three runs)
-19 sidecar=983653 trinity=982890 kill_exit=0
-15 sidecar=986660 trinity=985757 kill_exit=0
-14 sidecar=989056 trinity=988161 kill_exit=0
-
-$ SIG=-KILL heartbeat.sh (three runs)
-16 sidecar=991816 trinity=991114 kill_exit=0
-15 sidecar=993309 trinity=992603 kill_exit=0
-14 sidecar=994794 trinity=993941 kill_exit=0
-```
-
-**14–19 ms against a 5 000 ms bound.** AC8 passes on the dev path with three hundred times the
-margin.
-
-**And the heartbeat is not what did it.** `ExTauri.ShutdownManager`'s timer is 1500 ms; every
-figure above is two orders of magnitude under it, so the timer never fired. `SIGKILL` gives the
-same figure as `SIGTERM`, which rules out graceful cleanup by the Rust process — under `-KILL`
-it runs no cleanup at all. Something faster and OS-level ends the sidecar: the stdio pipe
-closing when its parent dies, which is **the first of the two candidates finding F1 already
-names**, happening by accident of how Tauri spawns a child rather than by design.
-
-### The dev sidecar is not the production sidecar, and that changes the answer
-
-```
-$ cat burrito_out/desktop-x86_64-unknown-linux-gnu
-#!/bin/sh
-# Dev sidecar generated by ExTauri...
-cd "/home/aylac/Projects/Trinity" || exit 1
-exec mix phx.server
-```
-
-**In development the sidecar is a shell script running `mix phx.server`. In production it is the
-Burrito binary** — a wrapper process that launches a BEAM as its child. Those are different
-process shapes, and finding F1 is precisely about the second one.
-
-Measured on the production shape, three runs — kill the wrapper, which is exactly what Tauri
-does to its sidecar on window close:
-
-```
-$ prodkill.sh (three runs)
-waited_ms=7673 ORPHAN beam=1012739 ppid=139843 still_serving=200
-waited_ms=7726 ORPHAN beam=1014123 ppid=139843 still_serving=200
-waited_ms=7605 ORPHAN beam=1015406 ppid=139843 still_serving=200
-```
-
-**Orphaned every time, past 7.6 s, still answering HTTP 200.** Well outside the 5 s bound.
-
-**Why the heartbeat does not save it, and this is the mechanism worth carrying to slice 100.**
-`ExTauri.ShutdownManager` does not enforce its timeout until the frontend has connected at least
-once — deliberately, so a slow boot cannot shut the app down before its window attaches. In this
-measurement no Tauri frontend ever connected, so the timeout is never armed and the orphan sits
-there indefinitely.
-
-**What that does and does not establish.** It establishes that a production-shaped sidecar whose
-window never attached orphans and keeps serving — F1 reproduced on the current build, with the
-heartbeat compiled in. It does **not** establish what happens to a production sidecar whose
-window attached and then closed: there the heartbeat would be armed, and I could not form that
-process tree. Two attempts failed for mechanical reasons — Tauri's dev mode waits on `devUrl`
-before launching, and my second attempt matched WebKit's process instead of the sidecar. **That
-measurement is unmade, and it is the one that decides whether F1 is closed.**
-
-**Across every measurement in this slice the heartbeat's timer has never been observed to fire.**
-Either something faster kills the sidecar, or nothing does.
-
-### The commit-msg path check, and what it caught
-
-`.githooks/commit-msg` gains a **warning-only** check: every repository path the message names
-should appear in the staged diff. Two tiers, because one is not enough:
-
-* **WARNING** — the path exists nowhere in the tree. Catches an invented or mistyped path.
-* **NOTE** — the path exists but this commit does not touch it. Noisy by design, and it is the
- tier that matters.
-
-**It never blocks.** A message legitimately names paths it does not change — the file a fix
-refers to, a command being quoted — so a blocking version would be wrong far more often than
-right. Its job is to put the claim beside the diff at the moment of writing.
-
-**What it caught the first time it fired: `a6085b3`, replayed.** That commit's message said
-`Cargo.lock` had been added to `REUSE.toml` when the edit had failed on a stale anchor. Tier one
-would have missed it — `REUSE.toml` exists, so "does this path exist?" says nothing. Tier two
-catches it:
-
-```
-$ sh .githooks/commit-msg # staged diff without REUSE.toml
-commit-msg: NOTE — the message names paths this commit does not touch:
- src-tauri/Cargo.lock
- REUSE.toml
-hook exit=0
-```
-
-The second tier exists **because** the first one would have let the real mistake through, which
-was worth finding before claiming the check works.
-
----
-
-## Items 2, 3 and 4
-
-Dated 2026-09-07.
-
-### Item 4 — the Windows artifact is built here
-
-7z is installed, so the blocker D6 recorded is gone:
-
-```
-$ BURRITO_TARGET=windows_x86_64 MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
---> Going to recompile NIF for cross-build: exqlite -> x86_64-windows
---> Successfully re-built exqlite for x86_64-windows!
-info: Archived 2189 files into payload! 📦
-exit=0
-
-$ stat -c '%n %s' burrito_out/desktop_windows_x86_64.exe
-burrito_out/desktop_windows_x86_64.exe 27316224
-```
-
-**AC3 now reads "built here and on a runner, window unproven."** It does not read "runs": nothing
-here can execute a Windows binary, and the runner's `--smoke` is what shows it runs.
-
-**A process mistake of mine, recorded because it is the fourth of its kind this slice.** I
-backgrounded this build, read a partial task file that showed the command echo, and reported
-"Windows cross-build exit 0" before it had finished. It did finish at exit 0, so the claim was
-true — **and it was not verified when I made it**, which is the same defect as the other three
-regardless of how it turned out. A claim that happens to be right is not evidence.
-
-### Item 3 — `mix gate` runs `plan_check` as its final step
-
-Three times this slice a green `mix gate` sat beside a failing `scripts/plan_check.sh` and only
-the first exit code was read; twice that reached the remote. **Two results printed and one read
-is a reporting failure the tooling can remove**, so it is removed: one command, one exit code.
-`cmd` gives it its own OS process, the same reason `hex.audit` uses it.
-
-Red, with a planted rule-7 violation, running **`mix gate` only**:
-
-```
-$ mix gate ; echo "exit=$?"
-FAIL docs/packaging.md:171:
-plan_check: FAIL
-exit=1
-```
-
-Green after removing it:
-
-```
-$ mix gate ; echo "exit=$?"
-Result: 72 passed
-plan_check: PASS
-exit=0
-```
-
-`test/gate_alias_test.exs` asserts `plan_check.sh` is the **last** step and appears exactly once,
-so re-ordering it above the tests — where a failure would be reported before the tests ran —
-fails the gate.
-
-### Item 2 — the shell in CI, the Windows curl, and per-OS cold start
-
-`.github/workflows/package.yml` gains four things:
-
-* **`rustup show active-toolchain`**, so the Rust version comes from `rust-toolchain.toml`
- rather than being written into the workflow. Same reasoning as the Zig version, which is read
- from `.tool-versions` with `awk`.
-* **The Tauri system libraries on Linux only.** macOS and Windows runners carry their own
- webviews; Linux needs `libwebkit2gtk-4.1-dev` and friends, which is what the owner installed
- here.
-* **`cargo build --manifest-path src-tauri/Cargo.toml --locked`** on all three runners, then an
- assertion that the shell binary exists and is non-empty. `--locked` because
- `src-tauri/Cargo.lock` is tracked and a CI build that silently re-resolves it is not building
- what the tree pins.
-* **The Windows curl, and a timer on every job.** Slice 001's first CI evidence had Windows
- booting but never serving, because the serve step carried `if: runner.os != 'Windows'`. That
- was a hole in this file, not a property of the artifact. The step now runs under `bash` on all
- three and times exec-to-first-200, which is **AC6's per-OS cold start** — previously skipped
- and recorded as a workflow gap rather than a missing machine.
-
-**Each job's summary now says the shell was built and never run.** A compiled shell is not a
-window, and a green tick on a job that compiled Rust must not read as one.
-
-**Not verified: this workflow has not run in this form.** Written from what was measured here.
-
-### The commit-msg check's first fire in real use, and it was a false positive
-
-Committing `94e6c4e`:
-
-```
-commit-msg: NOTE — the message names paths this commit does not touch:
- scripts/plan_check.sh
- rust-toolchain.toml
-```
-
-Both are correct observations and neither is a defect. That commit *refers* to
-`scripts/plan_check.sh` (it adds it to the gate alias in `mix.exs`, it does not edit the script)
-and to `rust-toolchain.toml` (the workflow now reads the pin rather than duplicating it). **The
-check did exactly what it was built to do and the answer was "no problem here."**
-
-That is worth recording rather than tuning away. The tier-two rule is deliberately noisy — a
-message that names a file it does not change is the normal case in this project, where commit
-messages explain reasoning. Its value is not a low false-positive rate; it is that the author
-reads the claim beside the diff at the moment of writing. **A check that only ever fires on real
-defects would have to understand what the sentence asserts, and this one does not pretend to.**
-
-Its stated limit, so nobody mistakes it for an enforcer: it never blocks, it reasons about paths
-and not about claims, and it cannot see a message that describes a change in prose without
-naming a path. `a6085b3` is caught only because it happened to name `REUSE.toml`.
-
-### Item 2's workflow, and the three defects running it found
-
-The G4 form of `package.yml` failed three times before it was right, and **every one was a
-defect in my workflow rather than in the artifact**. None was findable locally. Recording them
-together because the pattern is the point.
-
-**1. The shell build asked for a file nothing produces.** All three runners:
-
-```
-resource path `../burrito_out/desktop-x86_64-unknown-linux-gnu` doesn't exist
-error: failed to run custom build command for `trinity`
-```
-
-Tauri resolves `externalBin` to `desktop-`; Burrito writes
-`desktop__`. **I hit this locally, fixed it with a `cp` at the shell prompt, and did
-not carry the fix into the workflow** — so the file existed on my machine and nowhere else.
-`ExTauri.run/1` does the same rename for production builds. The triple now comes from
-`rustc -vV`, not a table in the workflow.
-
-**2. Windows refused the prod build on a dependency lock.**
-
-```
-Unchecked dependencies for environment prod:
-* daisyui (https://github.com/saadeghi/daisyui.git - v5.5.20)
- lock mismatch: the dependency is out of date
-```
-
-`mix deps.get` ran in the default environment; the two git `sparse` dependencies are checked
-per environment. Only Windows refused, which is why nothing here or on the other two runners
-showed it.
-
-**3. The cold-start timer was a GNU extension.** macOS:
-
-```
-line 12: 17887458973N: value too great for base (error token is "17887458973N")
-```
-
-`date +%s%3N` is GNU-only; BSD `date` passes `%3N` through as literal text. **Linux passed the
-same step in the same run** — `COLD_START_MS=590`, `HTTP 200 on port 33395` — so the timer was
-correct and only its portability was wrong, which is the most misleading shape a bug can have:
-one green runner saying the code works.
-
-**What the three have in common.** Each is a difference between my machine and a clean one that
-no amount of re-reading would surface: a file I created by hand, an environment I never
-switched, a `date` I only ever ran on GNU. **Six defects in this workflow have now been found by
-running it and none by inspecting it** — the earlier three were a trigger that excluded slice
-branches, `assets.deploy` without `compile`, and a whole-table `ps` diff.
-
-### Three more, all Windows, all mine
-
-After the first three, the G4 workflow surfaced three further defects and every one was
-Windows-only — the runner nothing here can imitate:
-
-**4. `mix deps.get` under `:prod` did not fix the `daisyui` lock mismatch.** It reported "All
-dependencies have been fetched" and the release refused anyway, so what Mix disagreed with was
-the checkout on disk rather than the environment. **My previous commit's reasoning was wrong
-about which of the two it was, and the run said so.** `mix deps.clean daisyui heroicons` first.
-
-**5. The serve step's `curl` loop had no bound.** `until curl ...; do sleep 0.05; done`. Linux
-and macOS passed it in under two seconds; Windows sat in it until I cancelled the run by hand.
-A step that hangs reports nothing and fails nothing — the same shape as the workflow that never
-fired.
-
-**6. And the bound did not fix it, because the hang was somewhere else.** The job hung again,
-past 60 s, so it was not my loop. `kill` from Git-bash does not stop a native Windows process:
-the backgrounded `.exe` outlived the step and kept the job alive. `taskkill /T /F` takes the
-wrapper and its BEAM together — which on Windows is also the only thing that clears F1's
-orphan.
-
-**Nine defects in this workflow have now been found by running it and none by inspecting it.**
-Six of the nine were invisible on this machine because they are differences between it and a
-clean runner: a file I made by hand, an environment I never switched, a `date` extension, a
-`kill` that means different things on different kernels.
-
-**Windows has still never been observed serving.** That is the one CI question slice 001 leaves
-open, and it is recorded as open rather than inferred from the two runners that do.
-
----
-
-## The Windows serve answer, and it supersedes a claim I made five times
-
-Dated 2026-09-07, run `34080826739`, job `windows x86_64`. The bounded loop and the
-`tail -40 serve.log` on failure — both added an hour earlier for exactly this — turned a hang
-into an answer, and the answer is better than a pass would have been.
-
-```
-no HTTP response on port 58911 after 60 s
---- serve.log ---
-[warning] ExTauri currently targets OTP 27 but you are running OTP 28.
-[info] [ExTauri.ShutdownManager] Started - heartbeat monitoring active on 127.0.0.1:58911
-[error] Exqlite.Connection ("db_conn_1") failed to connect: ** (Exqlite.Error) database is locked
-[info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:58912 (http)
-[warning] [ExTauri.ShutdownManager] Heartbeat timeout (1824ms) - Tauri frontend appears to have exited
-```
-
-### Defect 10: I curled the wrong port, and only Windows could show it
-
-`grep -o '127\.0\.0\.1:[0-9]*' serve.log | head -1` takes the **first** loopback address in the
-log. On Windows that is **the heartbeat socket, 58911**, not the endpoint, **58912**.
-
-`ExTauri.ShutdownManager` selects its transport from the OS — a **Unix domain socket** on macOS
-and Linux, **TCP on loopback** on Windows, because the BEAM cannot listen on a Unix socket
-there. So on the two runners that passed, no loopback line is printed before the endpoint's and
-`head -1` was **right by luck**. Fixed to match the endpoint line itself.
-
-### And the heartbeat fired. **This supersedes "it has never been observed to fire."**
-
-I have written, in `NOTES.md`, in `PROOF.md` under AC8, in the Answer on the F1 issue, and in
-the G3 record, that *across every measurement in this slice the heartbeat's timer has never been
-observed to fire.* **That is now false, and the line that made it false is above:**
-
-```
-[warning] [ExTauri.ShutdownManager] Heartbeat timeout (1824ms) - Tauri frontend appears to have exited
-```
-
-**1824 ms, inside the 1500 ms + 500 ms check interval the module documents, and well inside
-AC8's 5 s bound.** The mechanism works. Every earlier measurement missed it for a reason that
-was true at the time and was never the whole story:
-
-* the dev path killed the sidecar in ~15 ms by a faster route, so the timer had no chance;
-* the production-shape runs had no frontend at all, so the timeout was never armed.
-
-Here it armed and fired, and my own `curl` is what armed it: connecting to 58911 **is** a
-frontend connection as far as the ShutdownManager can tell. `curl` connected, sent an HTTP
-request the heartbeat protocol does not understand, closed, and 1824 ms later the app shut
-itself down — which is why nothing ever answered on 58912.
-
-**So the "Windows does not serve" reading would have been wrong too.** The app was serving. It
-shut down because I connected to its heartbeat.
-
-### Follow-up F2: on Windows the heartbeat is an unauthenticated loopback TCP port
-
-Stated as a finding rather than fixed here, because it is a design question for slice 100 and
-not packaging wiring.
-
-On Windows the shutdown channel is a TCP socket on `127.0.0.1` with no authentication and no
-protocol check that I can see from this evidence. **Any local process that connects to it and
-disconnects will shut Trinity down**, and the port is discoverable — the module writes it to
-`/tauri_heartbeat_.port` so the frontend can find it. A stray `curl` did it by
-accident in CI.
-
-That is a denial-of-service surface on the one OS where the transport is TCP, and it belongs
-with the liveness contract F1 already sends to slice 100. On macOS and Linux the Unix socket
-carries filesystem permissions and does not have this shape.
-
-**Three of my claims in this slice have been corrected by a measurement rather than by review**:
-"reparented to init", "AC4 needs a machine", and now "the heartbeat never fires". Each was
-stated with evidence that was real and partial.
-
-### The defect count, derived. It is nine, and I have typed two wrong numbers for it
-
-`grep`-able population — commits touching the packaging CI path, minus the two that are not
-defect fixes:
-
-```
-$ git log --oneline main..HEAD -- .github/workflows/package.yml
-f187cfe the CI serve step curled the heartbeat port
-cdbd2a8 kill the Windows sidecar with taskkill
-8cfe36f the serve step's curl loop had no bound
-f80145b force a clean checkout of the two git sparse deps on Windows
-569c4dd the cold-start timer used a GNU date extension
-a22687e the shell build needs the sidecar under Tauri's name, and prod needs its own deps.get
-94e6c4e G4 items 2-4 <- feature work, not a defect fix
-c2be3fa the smoke step diffed the whole process table
-0326dea package workflow never ran
-263c96f created the workflow <- not a defect fix
-```
-
-| # | Defect | Fixed at |
-|---|---|---|
-| 1 | push trigger excluded slice branches — it never fired | `0326dea` |
-| 2 | `assets.deploy` did not lead with `compile` | `8048ad3` (in `mix.exs`, found by a package run) |
-| 3 | the smoke step diffed the whole `ps` table | `c2be3fa` |
-| 4 | the shell build asked for a sidecar name nothing produces | `a22687e` |
-| 5 | `daisyui` lock mismatch under `:prod`, Windows only | `a22687e`, wrongly, then `f80145b` |
-| 6 | `date +%s%3N` is a GNU extension | `569c4dd` |
-| 7 | the `curl` loop had no bound | `8cfe36f` |
-| 8 | `kill` from Git-bash does not stop a native Windows process | `cdbd2a8` |
-| 9 | the port parse took the first loopback address, not the endpoint | `f187cfe` |
-
-**Nine.** Note 5 has two commits because my first fix's reasoning was wrong, and that is one
-defect with two attempts rather than two defects.
-
-**I have now typed two wrong numbers for this.** Earlier in this file and on the board I wrote
-**nine** when the correct figure at that moment was **eight** — I had listed "(7 and 8 together)"
-as a ninth item, which is item 7 and item 8 restated. Then, in the Report closing the Windows
-question, I wrote **eleven**, derived from nothing at all.
-
-That the first wrong number happened to match the final correct one is the least useful kind of
-luck. **A count is a population and populations derive from the tree** — CLAUDE.md §8 — and I
-wrote this particular count from memory three times while using it as the headline of an
-argument about not writing things from memory.
diff --git a/slices/001-packaging-spike/PROOF.md b/slices/001-packaging-spike/PROOF.md
deleted file mode 100644
index b5b2362..0000000
--- a/slices/001-packaging-spike/PROOF.md
+++ /dev/null
@@ -1,760 +0,0 @@
-
-
-# PROOF — Slice 001 — Packaging spike: Burrito + ex_tauri smoke build
-
-Agent: Claude Opus 5 · Date: 2026-09-06 · Branch: `slice/001-packaging-spike`
-
-`ROADMAP.md` set to `done` at **`b30c6ad`**, which is the final commit in CLAUDE.md §4's sense.
-Seven commits follow it, and they are not the slice being reworked: they are the owner's G3
-instructions applied — the CI run and the two defects it exposed, the wrapper transcript, and
-three corrections to this file's own claims. Each is listed under Git below. The branch head at
-G3 is named there; `git log --oneline main..HEAD` derives it.
-
-## Summary
-
-The scaffold from 000 packages as a single Burrito binary on linux x86_64, boots, serves HTTP
-200, and stops by itself under `--no-halt --smoke` leaving no process behind. `ex_tauri` runs
-on the pinned OTP 28 toolchain — its `~> 27.0` is declarative metadata and its runtime guard
-warns rather than raising above OTP 27.
-
-All three targets build and run — **on runners**. `package` run `34067973983` at `c2be3fa`,
-three jobs green: linux 20 790 808 B and macOS 11 927 096 B both served HTTP 200, Windows
-24 519 680 B booted and exited under `--smoke`. On this machine only linux builds; Windows
-needs 7z and macOS can only be cross-compiled.
-
-**The shell half is real now.** `mix ex_tauri.install` ran at G4 (`db14cd7`), the Linux shell
-compiles, and **the owner opened a native window titled Trinity showing the Phoenix scaffold on
-this machine**. First paint 2–3 s. The screenshot is under `proof/` with its digest. That is
-**AC4 proven** — the first window in this project on any OS.
-
-**Two criteria still need a machine that is not this one: AC2 and AC3**, for a window on macOS
-and on Windows. Both artifacts build and run on runners; neither has been shown in a window.
-
-**AC8 is the one to read carefully, and it is not simply "passes".** Through the window on the
-dev path the sidecar dies in **14–19 ms** against a 5 000 ms bound. But the heartbeat's 1500 ms
-timer never fired, `SIGKILL` gives the same figure as `SIGTERM`, and **the production sidecar is
-a different process shape** — three runs of it orphan past 7.6 s, still answering HTTP 200, with
-the heartbeat compiled in. Finding **F1 stands**, and what closes it is a measurement this slice
-did not make.
-
-**ADR-0004 is still not confirmed.** Its exit condition is a running smoke build on macOS *and*
-Windows, which the runners now give for the sidecar — but its subject is the desktop **shell**,
-and the shell has been *run* on exactly one OS, this one, by the owner. macOS and Windows
-compile it and have never launched it.
-
-## Gate
-
-```
-$ mix gate ; echo "exit=$?"
-Checking 44 source files ...
-Analysis took 0.05 seconds (0.00s to load, 0.05s running 69 checks on 44 files)
-167 mods/funs, found no issues.
-... SCAN COMPLETE ...
-versions.verify: OK — 67 locked packages, none disagreeing with 45 pins
-versions.gen: VERSIONS.md already matches Trinity.Versions and mix.lock
-trinity.version_form: OK
-trinity.names: OK over 157 tracked files
-trinity.secrets.scan: OK over 157 files
-trinity.reuse: OK — every commentable tracked file carries an SPDX header
-Result: 68 passed
-trinity.coverage: 001 30.37% vs 000 27.01% — OK
-plan_check: PASS
-exit=0
-```
-
-**One command, one exit code**, since G4 item 3. `scripts/plan_check.sh` is the gate's final
-step. It used to be a second command with a second exit code, and three times in this slice the
-gate's `exit=0` was read while `plan_check exit=1` on the line below it was not; twice that
-reached the remote. Demonstrated with a planted rule-7 violation: `mix gate` alone exits 1 and
-prints the `FAIL` line, and exits 0 after removal. `test/gate_alias_test.exs` asserts
-`plan_check.sh` is the **last** step and appears exactly once.
-
-## Tests
-
-```
-$ mix test --cover ; echo "exit=$?"
-| 30.37% | Total |
-exit=0
-Result: 68 passed
-```
-
-`mix test --cover` exited **3** before this slice, on a 90% threshold `mix.exs` claimed to have
-turned off. `test_coverage: [threshold: 0]` is the wrong shape — Mix reads the threshold from
-the `:summary` sub-option — so the key had been ignored since slice 000. Corrected to
-`test_coverage: [summary: [threshold: 0]]`. It never affected `mix gate`, which runs `test` and
-`mix trinity.coverage` and not `--cover`; it affected anyone reading the comment.
-
-`coverage.tsv` row: `001 30.37 5a9c8f7 2026-09-06`. **Up 3.36 points from 000's 27.01%**, so
-the drop rule owes no reason. The rule was exercised for the first time anyway, in both
-directions — see AC9.
-
-## Acceptance criteria evidence
-
-### AC1 [auto] — `MIX_ENV=prod mix release` with Burrito produces a binary for the host OS; running it starts Phoenix on an ephemeral port and serves the scaffold page
-
-```
-$ BURRITO_TARGET=linux_x86_64 MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
-exit=0
-
-$ stat -c '%n %s' burrito_out/desktop_linux_x86_64 ; echo "exit=$?"
-burrito_out/desktop_linux_x86_64 20777960
-exit=0
-
-$ file burrito_out/desktop_linux_x86_64
-burrito_out/desktop_linux_x86_64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
-```
-
-```
-$ PHX_SERVER=true ./burrito_out/desktop_linux_x86_64 --no-halt &
-[info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:35159 (http)
-
-$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:35159/ ; echo "exit=$?"
-200
-exit=0
-
-$ curl -sS http://127.0.0.1:35159/ > page.html ; echo "exit=$?"
-exit=0
-$ wc -c < page.html
-20165
-$ grep -o 'Phoenix Framework\|csrf-token' page.html | head -3
-csrf-token
-Phoenix Framework
-Phoenix Framework
-```
-
-**Met.** The port is ephemeral: `config/runtime.exs` binds `127.0.0.1` on port 0 in `:prod`, and
-35159 is what the kernel assigned that run.
-
-**`--no-halt` is load-bearing and is a finding, not a flag I like.** Burrito launches the
-release as `erl … -noshell -s elixir start_cli … -extra `. `elixir start_cli` is the
-ordinary Elixir CLI entry point and it halts when its command list is empty, exactly as
-`elixir -e ''` does — so without `--no-halt` the binary starts the endpoint and exits
-immediately, with status **0**, and nothing downstream notices.
-
-### AC2 [manual] — `mix ex_tauri.dev` opens a native window showing the LiveView scaffold on macOS
-
-**Window unproven. But the artifact is no longer "never executed".**
-
-Upgraded 2026-09-06 by the `package` workflow's first green run, `34067973983` at `c2be3fa`,
-job `macOS aarch64` on `macos-latest`:
-
-```
--rwxr--r-- 1 runner staff 11927096 Sep 6 23:52 desktop_macos_aarch64
-Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:49432 (http)
-TRINITY_SMOKE_PORT=49432
-HTTP 200 on port 49433
-```
-
-**Built natively on macOS, launched, served HTTP 200, and exited by itself under `--no-halt
---smoke` leaving no process of ours behind.** That is AC1's property on macOS.
-
-**It is not AC2.** AC2 asks for `mix ex_tauri.dev` opening a native window showing the scaffold,
-with a screenshot. A GitHub runner has no desktop session, the job's own summary says so, and no
-screenshot exists. AC2 stays `[manual]` and unproven, needing a macOS desktop.
-
-Superseding the earlier entry here: it said the macOS target "does cross-compile on this Linux
-host … and has never been executed", with a 13 782 104-byte figure. The cross-compile fact
-stands but is now beside the point — the runner built it natively at 11 927 096 bytes and ran
-it.
-
-### AC3 [manual] — the same on Windows, or a documented failure with the fallback that succeeded
-
-**Window unproven. Built and run on a runner; still not built on this machine.**
-
-Upgraded 2026-09-06 by run `34067973983` at `c2be3fa`, job `windows x86_64` on
-`windows-latest`:
-
-```
--rwxr-xr-x 1 runneradmin 197121 24519680 Sep 6 23:57 desktop_windows_x86_64.exe
-Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:60534 (http)
-TRINITY_SMOKE_PORT=60534
-```
-
-**Built natively on Windows and ran under `--no-halt --smoke`, printing its port and exiting
-0.** The runner has 7z, so the ERTS unpack that fails here succeeds there.
-
-**Windows serves. Confirmed 2026-09-07, run `34083291129` at `5ab1698`, all three jobs green:**
-
-```
-HTTP 200 on port 61453
-COLD_START_MS=929
-```
-
-**This closes the last CI question in the slice.** The earlier "never observed serving" was
-mine, not the artifact's: run `34080826739` showed the app serving on 58912 while my step curled
-58911, which on Windows is `ExTauri.ShutdownManager`'s TCP heartbeat socket. That connection
-armed the heartbeat and the app shut itself down 1824 ms later. See AC8's correction. The port
-parse now matches the endpoint line rather than the first loopback address.
-
-**The original G3 reading of this gap:** At G3 the reason was that the step carried `if: runner.os != 'Windows'`. At G4
-that condition is gone and the step runs everywhere, but three further defects — all mine, all
-Windows-only — have stood between it and an answer: a git `sparse` dependency Mix refused under
-`:prod`, an unbounded `curl` loop, and `kill` from Git-bash failing to stop a native Windows
-process so the job hung twice and was cancelled by hand. Each is fixed and pushed. **Linux and
-macOS pass the same step**, so the step itself works.
-
-The artifact's own evidence does not rest on that: it builds on the runner, it builds here, and
-it boots and exits under `--smoke`. What is unproven is that it *serves*, on Windows, and it is
-recorded as unproven rather than assumed from the other two runners.
-
-`ex_tauri`'s fallback path in ADR-0004 remains **untriggered**: its trigger is "Windows fails
-with ex_tauri", and the Windows *shell* has still never been attempted.
-
-### Built on this machine too, since the owner installed 7z
-
-```
-$ BURRITO_TARGET=windows_x86_64 MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
---> Going to recompile NIF for cross-build: exqlite -> x86_64-windows
---> Successfully re-built exqlite for x86_64-windows!
-info: Archived 2189 files into payload! 📦
-exit=0
-
-$ stat -c '%n %s' burrito_out/desktop_windows_x86_64.exe
-burrito_out/desktop_windows_x86_64.exe 27316224
-```
-
-**AC3 reads "built here and on a runner, window unproven."** Not "runs": nothing on this machine
-can execute a Windows binary, and the runner's `--smoke` is what shows it runs.
-
-### Why it was not built here until G4
-
-```
-$ MIX_ENV=prod mix release desktop --overwrite ; echo "exit=$?"
-> Burrito is building target: windows_x86_64
---> Remote ERTS From Beam Machine: https://github.com/erlang/otp/releases/download/OTP-28.5.0.5/otp_win64_28.5.0.5.exe
-** (RuntimeError) Couldn't find 7z/7zz
- (burrito 1.6.0) lib/util/default_erts_resolver.ex:80: Burrito.Util.DefaultERTSResolver.do_unpack/2
-exit=1
-
-$ for c in 7z 7zz 7za 7zr; do command -v "$c" || echo "$c (absent)"; done
-7z (absent)
-7zz (absent)
-7za (absent)
-7zr (absent)
-
-$ apt-cache policy p7zip-full | head -2
-p7zip-full:
- Installed: (none)
-```
-
-The Windows ERTS ships as a `.exe` installer and Burrito unpacks it with 7z. Installing one
-needs root, which CLAUDE.md §7 keeps off this agent's hands. `SLICE.md`'s Risks section
-predicted this before the slice started.
-
-**The ADR-0004 fallback is untriggered, not eliminated.** Its trigger is "Windows fails with
-ex_tauri", and Windows was never attempted, so nothing has been learned about the fallback.
-
-### AC4 [manual] — a native window on Linux
-
-**PROVEN, 2026-09-07, by the owner on this X11 desktop.**
-
-```
-$ mix ex_tauri.dev
-```
-
-opened a window titled **Trinity** showing the Phoenix scaffold. Screenshot:
-
-```
-slices/001-packaging-spike/proof/ac4-linux-window.png
- 2814779 bytes, PNG 7680x2160
- sha256 e52785ce61ea9f69615ffa7602f7fd172b1fa5a310d8b69b1b11f7fc09bfabe7
-```
-
-**The size and digest are here because of what nearly went in instead.** My handover step used
-`import -window "$(xdotool search --name '^Trinity$' | head -1)"`; `xdotool` is absent, so the
-`-window` argument was empty and `import` wrote a **185-byte stub** — a file that exists, is
-named like evidence, and contains nothing. It would have satisfied every check this project
-has: the path is right, `trinity.reuse` covers it, nothing reads its contents. The owner
-re-took it with `-window root`. **A screenshot is the one artefact here whose correctness no
-enforcer can see, so it carries its digest.**
-
-The prerequisites that made this possible, and both were mine to have found earlier: the owner
-installed five Tauri v2 libraries (root), and `mix ex_tauri.install` had never been run — see
-the correction below, which stands.
-
-#### The correction that stood before it, kept
-
-**The reason I gave for AC4 at G1 was wrong.**
-
-The G1 plan and the earlier draft of this file said "no desktop session available on this
-machine". Measured:
-
-```
-$ echo "DISPLAY=$DISPLAY XDG_SESSION_TYPE=$XDG_SESSION_TYPE"
-DISPLAY=:0 XDG_SESSION_TYPE=x11
-```
-
-**There is a display.** This supersedes every earlier line in this slice's records that says
-otherwise. AC4 is unproven for two different reasons, and neither is the one I gave:
-
-1. **There is no Tauri project to run.** `SLICE.md` Scope says "run `mix ex_tauri.install`" and
- Deliverables names a `tauri/` scaffold; `git ls-files tauri | wc -l` is `0`. The approved G1
- plan's fifteen lines never included the install step, so the deliverable was dropped at plan
- time and I did not flag it. `mix ex_tauri.dev` has nothing to launch.
-2. **The Tauri v2 system libraries are absent** — `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`,
- `libayatana-appindicator3-dev`, `librsvg2-dev`, `patchelf`, none installed, all resolvable in
- this machine's apt, all needing root.
-
-Neither is "no machine". A prepared, step-by-step procedure for the owner is in `NOTES.md`
-under "The owner's manual check on this machine", with who runs each step; step 2 is a Question
-on the slice issue because `mix ex_tauri.install` is an Igniter task that rewrites tracked
-files, and slice 000 has already been bitten once by a generator doing that.
-
-**AC2 and AC3 are unaffected by this correction** — they need a macOS and a Windows machine and
-those genuinely do not exist here. **AC4 does not need a machine. It needs the two steps
-above**, and saying "no machine" concealed that for the length of this slice.
-
-### AC5 [auto] — binary size and cold-start-to-serving recorded for linux x86_64 in `docs/packaging.md`
-
-```
-$ stat -c %s burrito_out/desktop_linux_x86_64
-21398872 # 20777960 before D7 put ex_tauri in :prod; +620912, +3.0%
-
-$ du -sh ~/.local/share/.burrito
-88M
-
-$ for i in 1 2 3 4 5; do coldstart.sh; done # exec to first HTTP 200, ms
-231
-235
-240
-237
-245
-
-$ rm -rf ~/.local/share/.burrito && coldstart.sh # first launch of a fresh install
-1578
-```
-
-**Met.** The table is in `docs/packaging.md`. **Two figures are reported rather than one**:
-Burrito extracts its payload into `~/.local/share/.burrito` on first launch, so 1 578 ms is
-what a person sees the first time and ~235 ms is every launch after. Reporting only the warm
-figure would understate the launch that forms the impression by a factor of six.
-
-### AC6 [manual] — the same figures for macOS and Windows, and cold-start-to-first-paint on any OS
-
-**Partly proven, and the rest is not blocked by what I said it was.** Three sub-claims, three
-different answers:
-
-**Per-OS binary size — proven**, by the runners. Two sets of figures, because D7 changed what
-is in the binary between them:
-
-| OS | run `34067973983`, before D7 | run `34075257183`, after D7 |
-|---|---|---|
-| linux x86_64 | 20 790 808 | 21 298 216 |
-| macOS aarch64 | 11 927 096 | 12 435 992 |
-| Windows x86_64 | 24 519 680 | — see AC3 |
-
-The later column is the artifact this slice ships. Both are kept rather than the first being
-overwritten: the difference is `ex_tauri` and its dependency tail entering `:prod`, and a reader
-comparing sizes across slices needs to know a dependency moved rather than the code growing.
-
-**Per-OS cold-start-to-serving — measured, at G4 item 2**, run `34075257183`:
-
-Final figures, run `34083291129` at `5ab1698`, all three green:
-
-| OS | Artifact | Bytes | exec to first HTTP 200 |
-|---|---|---|---|
-| linux x86_64 | `desktop_linux_x86_64` | 21 293 896 | **589 ms** |
-| macOS aarch64 | `desktop_macos_aarch64` | 12 435 992 | **1 556 ms** |
-| Windows x86_64 | `desktop_windows_x86_64.exe` | 25 024 512 | **929 ms** |
-
-**All three OSes, size and cold start, from one green run.** AC6's per-OS half is complete.
-
-The jobs previously launched and curled without timing the interval, and Windows was never
-curled at all (`if: runner.os != 'Windows'`). Both were holes in `package.yml` rather than
-missing machines.
-
-**These are not comparable with AC5's 231–245 ms.** That figure is a warm launch on this
-machine with the Burrito payload already extracted; a runner is cold every time, so 699 ms is
-the runner's equivalent of AC5's 1 578 ms first-launch figure, not of its warm one. Reporting
-them in one column would invite exactly that mistake.
-
-**Cold-start-to-first-paint — PROVEN on Linux, 2026-09-07.** The owner timed **2–3 seconds**
-from pressing Enter on `mix ex_tauri.dev` to the scaffold appearing in the window. Timed by hand,
-which is the only way a first paint can be timed, and stated to the precision it was taken at.
-It exists for one OS; macOS and Windows would each need their own window.
-
-Split from AC5 at G1. **Both halves were blamed on missing machines and neither was blocked by
-one** — the size half was answered by runners and the paint half by this desktop, once the two
-steps AC4 names were done.
-
-### AC7 [auto] — running the binary under `--smoke` exits 0 and leaves no process behind
-
-```
-$ ps -eo pid,ppid,comm | grep -E 'desktop_linux|beam.smp|erl_child_setup' > before.txt ; cat before.txt
-2487028 2485809 beam.smp
-2487056 2487028 erl_child_setup
-
-$ ./burrito_out/desktop_linux_x86_64 --no-halt --smoke ; echo "exit=$?"
-18:56:11.939 [info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:45031 (http)
-TRINITY_SMOKE_PORT=45031
-exit=0
-
-$ ps -eo pid,ppid,comm | grep -E 'desktop_linux|beam.smp|erl_child_setup' > after.txt ; cat after.txt
-2487028 2485809 beam.smp
-2487056 2487028 erl_child_setup
-
-$ diff before.txt after.txt ; echo "exit=$?"
-exit=0
-```
-
-Corroborated on three runners, run `34067973983` at `c2be3fa`, all green:
-
-| Runner | Artifact | Bytes | Port printed | Served |
-|---|---|---|---|---|
-| `ubuntu-latest` | `desktop_linux_x86_64` | 20 790 808 | 40877 | HTTP 200 on 43181 |
-| `macos-latest` | `desktop_macos_aarch64` | 11 927 096 | 49432 | HTTP 200 on 49433 |
-| `windows-latest` | `desktop_windows_x86_64.exe` | 24 519 680 | 60534 | not asked — see AC3 |
-
-Each job compares a filtered process list either side of the exit and fails on any survivor.
-
-**The first run of that workflow failed on all three, and both causes were mine.** `mix
-assets.deploy` did not lead with `compile`, so Phoenix 1.8's colocated CSS did not exist on a
-clean checkout and the alias resolved only on a machine that had already built (fixed in
-`8048ad3`). Then the smoke step diffed the *whole* `ps` table, and a live runner churns between
-two samples — macOS on `mdworker_shared` and `CloudTelemetryService`, linux on a `kworker`
-kernel thread — while the artifact had launched, served and exited correctly (fixed in
-`c2be3fa`). Both were defects in the evidence-gathering, not in the thing being measured, and
-neither was findable locally.
-
-Control, which is what makes the local run above mean anything:
-
-```
-$ PHX_SERVER=true timeout 20 ./burrito_out/desktop_linux_x86_64 --no-halt ; echo "exit=$?"
-exit=124 # 124 is timeout killing it — it was still running
-```
-
-**Met, with a correction to how the criterion is worded.** `SLICE.md` AC7 says "running the
-binary under `--smoke`". **My first measurement of it was worthless and is recorded here rather
-than quietly replaced.** I ran `./burrito_out/desktop_linux_x86_64 --smoke`, got exit 0 and a
-clean `ps`, and would have called the criterion met — but the binary exits 0 and leaves nothing
-behind **with no flag at all**, because the Elixir CLI halts on an empty command list. That
-observation was true and attributed nothing to the code under test.
-
-The form above adds `--no-halt`, which makes staying alive the default, so that exiting is
-attributable. **The reviewer should read AC7 as `--no-halt --smoke`.** The two remaining
-processes in both listings are this session's own dev BEAM, present before the run started.
-
-### AC8 [manual] — killing the window terminates the sidecar within 5 s
-
-**Measured through a real window at G4. It passes on the dev path, by a mechanism Trinity does
-not own, and the production sidecar still orphans. Finding F1 stands.**
-
-#### Through the window: the owner's close
-
-```
-# before closing, window open
- 966625 966348 beam.smp 125 <- mix ex_tauri.dev
- 966776 966653 cargo-tauri 125
- 966847 966776 trinity 125 <- the Rust window
- 967584 966847 beam.smp 123 <- the sidecar
- 2487028 2485809 beam.smp 14543 <- the owner's own session
-
-# after closing with the window's ✕
- 2487028 2485809 beam.smp 14603 <- same pids, older etimes
- 2487056 2487028 erl_child_setup 14603
-```
-
-Shell, sidecar and dev task all gone. **My stated pass condition was wrong**: I told the owner
-`exit=1` from the `grep` was the pass, but the pattern matches `beam.smp` and their own session
-runs one, so it could never be reached. The recorded check excludes those pids instead of
-relying on an unreachable exit code.
-
-#### The 5-second bound, six runs
-
-Kill the `trinity` window process, poll until the sidecar's `beam.smp` is gone:
-
-```
-$ SIG=-TERM heartbeat.sh 19 / 15 / 14 ms
-$ SIG=-KILL heartbeat.sh 16 / 15 / 14 ms
-```
-
-**14–19 ms against 5 000 ms.** Three hundred times the margin.
-
-#### It is not the heartbeat
-
-`ExTauri.ShutdownManager`'s timeout is 1500 ms, and every figure is two orders of magnitude
-under it, so **the timer never fired**. `SIGKILL` matching `SIGTERM` rules out graceful cleanup
-by the Rust process, which under `-KILL` runs none. What ends it is the stdio pipe closing when
-the parent dies — **the first of the two candidates F1 already names**, occurring by accident of
-how Tauri spawns a child rather than by anything Trinity does.
-
-#### The dev sidecar is not the production sidecar
-
-```
-$ cat burrito_out/desktop-x86_64-unknown-linux-gnu
-#!/bin/sh
-cd "/home/aylac/Projects/Trinity" || exit 1
-exec mix phx.server
-```
-
-Development spawns a shell script running `mix phx.server`. **Production spawns the Burrito
-binary** — a wrapper with a BEAM child. Three runs on that shape, killing the wrapper as Tauri
-does to its sidecar:
-
-```
-waited_ms=7673 ORPHAN beam=1012739 ppid=139843 still_serving=200
-waited_ms=7726 ORPHAN beam=1014123 ppid=139843 still_serving=200
-waited_ms=7605 ORPHAN beam=1015406 ppid=139843 still_serving=200
-```
-
-**Orphaned every time, past 7.6 s, still answering HTTP 200** — with the heartbeat compiled in,
-since D7 put `ex_tauri` in `:prod`. It does not arm its timeout until the frontend has connected
-once, and no frontend connected there.
-
-#### What is not established
-
-**A production sidecar whose window attached and then closed** — where the heartbeat would be
-armed. Two attempts to form that process tree failed mechanically: Tauri's dev mode waits on
-`devUrl` before launching, and the second attempt matched WebKit's process rather than the
-sidecar. **That is the measurement that decides F1, and this slice did not make it.**
-
-**Across every measurement in this slice the heartbeat's timer has never been observed to fire.**
-Either something faster kills the sidecar, or nothing does.
-
-#### Verdict
-
-AC8 **passes on the dev path** and the criterion's own wording — a window, a close, `ps` — is
-satisfied there. It is **not** satisfied for the artifact this slice ships, and F1 is not
-closed. Slice 100 owns it.
-
-#### Correction, 2026-09-07 — **the heartbeat does fire.** Supersedes the claim above and in three other places
-
-The paragraph above ended "that is already added and has never once been the mechanism that
-worked", and I wrote the same thing in `NOTES.md`, in the Answer on the F1 issue, and in the G3
-record. **It is false.** Run `34080826739`, job `windows x86_64`, `serve.log`:
-
-```
-[info] [ExTauri.ShutdownManager] Started - heartbeat monitoring active on 127.0.0.1:58911
-[info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:58912 (http)
-[warning] [ExTauri.ShutdownManager] Heartbeat timeout (1824ms) - Tauri frontend appears to have exited
-```
-
-**1824 ms — inside the module's documented 1500 ms timeout plus its 500 ms check interval, and
-well inside AC8's 5 s bound.** The mechanism works.
-
-The earlier measurements missed it for reasons that were true and partial: the dev path killed
-the sidecar in ~15 ms by a faster route, so the timer had no chance to run; the production-shape
-runs had no frontend at all, so the timeout was never armed. Here it armed and fired.
-
-**What armed it was my own `curl`.** On Windows the heartbeat listens on **TCP loopback** — the
-module selects a Unix domain socket on macOS and Linux and TCP on Windows, because the BEAM
-cannot listen on a Unix socket there. My port parse took the first `127.0.0.1:` line in the log,
-which on Windows is the heartbeat, so `curl` connected to the shutdown channel. That connection
-counts as a frontend attaching; `curl` then closed; 1824 ms later the app shut itself down.
-
-**Two readings this corrects at once:** the heartbeat is not unexercised, and "Windows does not
-serve" would also have been wrong — the app was serving on 58912 and shut down because
-something connected to 58911.
-
-**Follow-up F2, recorded not fixed:** on Windows that shutdown channel is an unauthenticated
-loopback TCP port whose number is written to `/tauri_heartbeat_.port` for
-discovery. **Any local process that connects and disconnects will shut Trinity down** — a stray
-`curl` did it by accident in CI. A denial-of-service surface on the one OS where the transport
-is TCP; macOS and Linux use a Unix socket carrying filesystem permissions. It belongs with F1's
-liveness contract in slice 100.
-
-**What still stands from the verdict above:** the production sidecar orphans when its parent
-dies with no frontend attached, three runs, past 7.6 s, still serving. F1 is not closed. What
-slice 100 must guarantee is now better specified — the heartbeat works when it is armed, so the
-question is what arms it, what may connect to it, and what happens on the path where nothing
-does.
-
-### AC9 [auto] — `mix gate` still green; `mix phx.server` still works without Tauri
-
-```
-$ mix gate ; echo "exit=$?"
-Result: 68 passed
-trinity.coverage: 001 30.37% vs 000 27.01% — OK
-exit=0
-
-$ ./scripts/plan_check.sh ; echo "exit=$?"
-plan_check: PASS
-exit=0
-
-$ timeout 25 mix phx.server ; echo "exit=$?"
-[info] Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:4000 (http)
-exit=124 # still running when the timeout killed it
-```
-
-**Met.** The coverage rule was exercised in both directions, since 001 is the first slice with
-a previous row to compare against:
-
-```
-$ mix trinity.coverage ; echo "exit=$?" # seeded 4.01 points low
-** (Mix) trinity.coverage: 001 23.0% is 4.01 points below 000 27.01%, more than the
- 3.0-point tolerance. Name the reason in the slice's NOTES.md.
-exit=1
-
-$ mix trinity.coverage ; echo "exit=$?" # at the measured figure
-trinity.coverage: 001 30.37% vs 000 27.01% — OK
-exit=0
-```
-
-## Reds demonstrated before their fixes
-
-Every claim below was committed failing first, by name, and the fix commit references it.
-
-| Property | Red at | Failure |
-|---|---|---|
-| Toolchain VERSIONS.md marks are derived from each row's own source | `82e74a3` | 3 of 5 in `VersionsToolchainMarkTest`; `mix test` exit 2 |
-| `Trinity.Paths` resolves a distinct root per OS | `ec64ac7` | 3 of 6 in `PathsTest`; three roots collapsed to `["/h/.local/share/trinity"]` |
-| The smoke path stops the OS process | `f7406c5` | 1 of 4 in `SmokeTest`; `run/2` returned without calling halt |
-| Inline `@sobelow_skip` carries a reason | planted and removed | `["lib/trinity/paths.ex:71"]`; exit 2 |
-| The coverage drop rule fires | seeded row | `exit=1` with the arithmetic |
-
-Two of those reds were rejected first for failing at an earlier fault than the claim, per
-CLAUDE.md §8, and both are recorded in `NOTES.md`: the `MIX_ENV=prod` release stopped on a
-Credo check under `lib/` before it reached anything about releases, and the smoke test returned
-`{:error, :no_server_found}` because the test endpoint did not serve.
-
-## Manual verification for the reviewer
-
-**Two criteria, and both need a machine that is not this one.** At G3 this table had five rows
-and I described them as "one missing thing"; three of those five turned out not to need a
-machine at all, and the two that did are below.
-
-| AC | Needs | Step | Expected |
-|---|---|---|---|
-| **2, 3 — try this first** | **no machine** | on `macos-latest` and `windows-latest`, launch the shell in the job and capture with the platform's own screenshot tool (`screencapture` on macOS, `Add-Type System.Windows.Forms` on Windows) | a native window on that OS. **May retire AC2 and AC3 without a machine** |
-| 2 | a **macOS desktop**, if the above does not work | `mix ex_tauri.dev` | a native window showing the scaffold; screenshot to `slices/001-packaging-spike/proof/` |
-| 3 | a **Windows desktop**, if the above does not work | `mix ex_tauri.dev` | the same, or a documented failure and the fallback |
-
-**Why the first row exists.** Slice 001 wrote "a runner has no desktop session" in
-`docs/packaging.md`, in every job summary and above in this file. **That is measured for
-`ubuntu-latest` and assumed for the other two** — `macos-latest` and `windows-latest` run
-interactive sessions. Nobody has tried launching a window there. The wording generalised from
-the one runner that was measured to the two that were not, which is the kind of generalisation
-this project treats as a defect when it turns up in a mark or a count. Recorded as ADR-0004's
-third correction.
-
-**Closed since G3, and how:**
-
-| AC | Was | Closed by |
-|---|---|---|
-| 4 | "unproven: no machine" | the owner, on this X11 desktop, once `mix ex_tauri.install` had run and five apt packages were in. **Proven**, with a screenshot. |
-| 5 (CI) | "the GitHub Actions run" | `package` run `34067973983`, three jobs green |
-| 6 size | "a macOS and a Windows machine" | the runners |
-| 6 first paint | "no window" | the owner: 2–3 s |
-| 8 | "false, needs a desktop" | measured through the owner's window and in six timed runs — see AC8 |
-
-`.github/workflows/package.yml` has run green on three OSes. It has **not** run in its G4 form,
-which adds the shell build, the Windows curl and the cold-start timer.
-
-**A runner still cannot produce a screenshot of a real window on a real desktop.** Exactly one
-such screenshot exists in this project: the owner's, on Linux, at
-`slices/001-packaging-spike/proof/ac4-linux-window.png`.
-
-## Deviations from SLICE.md
-
-**Seven**, and the count is derived, not typed:
-
-```
-$ grep -c '^### D[0-9]' slices/001-packaging-spike/NOTES.md
-8
-$ grep -n '^### D[0-9]' slices/001-packaging-spike/NOTES.md
-190:### D1 223:### D2 262:### D3 319:### D4 346:### D5 353:### D6 995:### D7
-756:### D4 — accepted (the owner's decision on D4, not an extra deviation)
-```
-
-Eight headings, **seven deviations**: D4 appears twice, once as the deviation and once as the
-owner's acceptance of it. **An earlier version of this line said "nine", typed from memory
-against no command.** That is the defect CLAUDE.md §8 names — values come from the tree or the
-owner — and it was in the section listing my deviations.
-
-All seven are in `NOTES.md` with their measurements, all recorded before the commits that
-carried them. **D7 is the one added at G4 and the one that most changes the artifact:**
-
-0. **`ex_tauri` is a dependency of every environment, not `only: :dev`** (D7). `mix
- ex_tauri.install` adds `ExTauri.ShutdownManager` to the supervision tree unconditionally,
- and on a dev-only dependency the app cannot start in `:test` or `:prod`. The generator is
- right: the heartbeat is the window's only channel for telling the BEAM it has closed, so it
- must be in the binary that ships. `only: :dev` would have shipped F1's failure mode as a
- permanent property of every release. **This supersedes what line 1 recorded as "the
- configuration"**, which answered a different question correctly. The exclusion from `:test`
- is compile-time, not `Code.ensure_loaded?/1`, because that guard cannot tell an intended
- exclusion from a dropped dependency.
-
-The four from earlier that still change what a reader should expect:
-
-1. **Rust is pinned in `rust-toolchain.toml`, not `.tool-versions`.** `asdf` here has no rust
- plugin and does not fail on a tool it has no plugin for — it omits the line from
- `asdf current` and exits 0. A `rust 1.92.0` line there would pin nothing.
-2. **The Tauri CLI's deriving command is `_build/_tauri/bin/cargo-tauri tauri --version`.**
- `cargo tauri --version` exits 101; `ex_tauri` installs the CLI into `_build/_tauri`, which is
- gitignored, so no file in the tree pins it and its VERSIONS.md row is marked 📐, never ✅.
-3. **A fourth non-hex pin the plan did not anticipate: Zig, exactly 0.16.0.** Burrito 1.6.0
- compares for equality, not a range.
-4. **The slice-000 Credo check had to move out of `lib/`.** `credo` is `only: [:dev, :test]` and
- `lib/` compiles in every environment, so nothing compiled under `MIX_ENV=prod` at all. This
- is a change outside packaging by the letter of the plan's constraint. **The owner accepted
- it on 2026-09-06 as a packaging prerequisite** — a spike that cannot compile for prod has
- not spiked — with a principle attached that is a correction to how I worked rather than to
- the code: *post the Question, then proceed in parallel with the work that does not depend on
- the answer; commit first and ask second is the wrong order even when the call is right.* I
- did the second thing, and raised it at G3 with eight commits already standing on it.
-
-`SLICE.md`'s acceptance criteria were themselves retagged at G1 (line 2, commit `afca47d`):
-seven criteria became nine, AC4 moved `[auto]` → `[manual]`, and AC5 and AC6 were each split
-into an auto half and a manual one.
-
-## Versions touched
-
-`VERSIONS.md` updated: **yes**, regenerated by `mix versions.gen` from `lib/trinity/versions.ex`.
-
-- `burrito` `~> 1.6` → ✅ in `mix.lock` at 1.6.0. Now a **direct** dependency: `&Burrito.wrap/1`
- runs under `MIX_ENV=prod` and the burrito `ex_tauri` brings is `only: :dev`.
-- `ex_tauri` `~> 0.2` → ✅ in `mix.lock` at 0.2.0.
-- `Rust` **1.92.0** → ✅ `rust-toolchain.toml`.
-- `Zig` **0.16.0** → ✅ `.tool-versions`.
-- `Tauri CLI` **2.11.4** → 📐 `_build/_tauri/bin/cargo-tauri tauri --version`.
-- `asdf` → 📐 `asdf --version`; it cannot pin itself.
-
-The row `Rust + Tauri CLI | stable | ✅ .tool-versions` is gone, and it was wrong on every
-count: two tools in one row, an unmeasured pin, and a ✅ naming a file carrying neither name.
-`mix versions.gen`'s toolchain mark is no longer a constant — each row states a `:from`, and a
-row naming a file that stops carrying its pin now marks ❌ and fails the gate.
-
-`mix hex.outdated` was not run this slice. `mix hex.audit` and `mix deps.audit` are gate steps
-and both passed.
-
-## Git
-
-```
-$ git log --oneline main..HEAD
-94e6c4e feat(s001): G4 items 2-4 — shell in CI, plan_check inside the gate, Windows built here
-e5ca724 feat(s001): measure AC8's bound — it passes on the dev path and fails on the production shape
-5c3afd9 feat(s001): AC4's screenshot, and the .gitignore rule that named a directory that never existed
-b7cf84d docs(s001): item 1 prepared — the owner's launch, timing, screenshot and ps steps
-df70f2c fix(s001): a6085b3 said Cargo.lock was added to REUSE.toml; it was not
-a6085b3 feat(s001): the Linux Tauri shell builds; track src-tauri/Cargo.lock
-6f9292b fix(s001): ex_tauri in every environment — the heartbeat has to be in the shipped binary
-db14cd7 feat(s001): mix ex_tauri.install — the generator's output, nothing else
-35fc4b5 docs(s001): D4 in PROOF still asked the owner to accept what they had accepted
-0e4e926 docs(s001): the summary said three criteria need a machine; two do
-09fdf74 docs(s001): four stale claims in PROOF.md, including a typed count in the deviations section
-24323cf docs(s001): PROOF summary contradicted its own AC2 and AC3 after the CI run
-183873d docs(s001): PROOF.md header and git log name the real shas
-e7202f4 docs(s001): CI ran — AC2 and AC3 upgrade from "never executed" to "built and run on a runner"
-c2be3fa ci(s001): the smoke step diffed the whole process table, not ours
-f8d1271 docs(s001): owner decisions recorded, AC8 false citing F1, and AC4's stated reason corrected
-5b17e30 fix(s001): the rule-7 correction note quoted the identifier it was correcting
-e25048f fix(s001): cite the wrapper finding as F1, not by board id — plan_check rule 7
-8048ad3 fix(s001): assets.deploy did not compile first, so it failed on every clean checkout
-0326dea ci(s001): package workflow never ran — its push trigger excluded slice branches
-b30c6ad feat(s001): complete slice 001 — packaging spike (Burrito + ex_tauri)
-263c96f docs(s001): lines 8-14 — packaging doc, package workflow, ADR-0004 correction, coverage row
-5a9c8f7 feat(s001): line 5 — the --smoke boot path, and the runtime config a double-clicked binary needs
-f7406c5 test(s001): line 5 — the smoke path never stops itself, committed failing
-1d74cb2 feat(s001): line 4 — Trinity.Paths resolves a distinct root per OS
-ec64ac7 test(s001): line 4 — Trinity.Paths first pass collapses three OS roots into one, committed failing
-7a41cb2 feat(s001): line 3 — Burrito wiring, and toolchain marks derived from their own source
-82e74a3 test(s001): line 3 — toolchain marks are not derived, committed failing
-afca47d docs(s001): line 2 — acceptance criteria retagged, AC5 and AC6 split
-d1b72d7 feat(s001): line 1 — ex_tauri measured on the pinned toolchain, no refusal
-13c5b3f docs(s001): G1 plan v2
-c1b14ad docs(s001): G1 plan
-537bd3a docs(s001): status in_progress, and plan_check rule 11 — the lifecycle enforcer
-```
diff --git a/slices/001-packaging-spike/SLICE.md b/slices/001-packaging-spike/SLICE.md
deleted file mode 100644
index e6c58eb..0000000
--- a/slices/001-packaging-spike/SLICE.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Slice 001 — Packaging spike: Burrito + ex_tauri smoke build
-
-| Field | Value |
-|---|---|
-| Phase | 0 Foundation |
-| Milestone | M0 Stands |
-| Size | M |
-| Depends on | 000 |
-| Status | approved |
-
-## Goal
-Prove — or disprove — that the scaffold from 000 can be packaged as a single Burrito binary and opened in a
-native Tauri window via ex_tauri on each target OS. This slice exists to retire risk R1 before we build on it.
-It ends with ADR-0004 confirmed or amended.
-
-## Why
-Desktop packaging is the riskiest, least-Elixir-native part of the plan. Finding out in Slice 100 is too late.
-
-## Scope
-**In:**
-- Add `burrito` (release step) and `ex_tauri` (dev dep) to `mix.exs`; run `mix ex_tauri.install`.
-- Burrito targets: `darwin_arm64`, `darwin_x86_64`, `linux_x86_64`, `windows_x86_64` (only build the ones the
- human's machines can test; document which).
-- A `Trinity.Release` module that runs migrations explicitly (skipped under `BURRITO_TARGET` at boot, per known pattern).
-- Data dir resolution (macOS `~/Library/Application Support/Trinity`, Windows `%APPDATA%\Trinity`, Linux `$XDG_DATA_HOME/trinity`).
-- Runtime port selection: bind to `127.0.0.1:0`, read the assigned port, pass to the shell (ex_tauri does this).
-- `mix ex_tauri.dev` opens the scaffold in a native window; `mix ex_tauri build` produces an installable artifact.
-- A `docs/packaging.md` with exact commands, prerequisites (Rust, Tauri deps, Zig if cross-building), and gotchas found.
-**Out:**
-- Signing, notarization, updater (101). Tray/notifications (100). Any app features.
-
-## Design notes
-- Keep all desktop-shell specifics in `tauri/` and `Trinity.Desktop` stubs; the core app must still run with plain `mix phx.server`.
-- Measure binary size and cold-start time; they are baselines for 100/101.
-- If ex_tauri fails on Windows: try the hand-rolled sidecar pattern (phoenix_tauri repo) in the same slice; if that
- fails too, try elixir-desktop for Windows only. Whatever works is written into ADR-0004 as accepted.
-
-## Deliverables
-- `tauri/` scaffold, Burrito config in `mix.exs`, `lib/trinity/release.ex`, `lib/trinity/paths.ex`, `docs/packaging.md`, ADR-0004 updated.
-
-## Acceptance criteria
-1. [auto] `MIX_ENV=prod mix release` with Burrito produces a binary for the host OS; running it starts Phoenix on an ephemeral port and serves the scaffold page (curl output shown).
-2. [manual] `mix ex_tauri.dev` opens a native window showing the LiveView scaffold (screenshot) on macOS. **No machine available — Ubuntu is the only machine.**
-3. [manual] The same on Windows (screenshot) — or a documented failure with the fallback that succeeded (screenshot) and ADR-0004 amended. **No machine available — Ubuntu is the only machine.**
-4. [manual] The same on Linux (screenshot). Retagged from `[auto]` at G1: a native window on a desktop session is not a command's output, and the spec's "or documented not tested" clause is a waiver rather than an automation. **No desktop session available on this machine.**
-5. [auto] Binary size and cold-start-to-serving are recorded for **linux x86_64** in `docs/packaging.md`, measured with `stat -c %s` and a timed loop against the port the smoke run prints.
-6. [manual] The same figures for macOS and Windows, and cold-start-to-first-**paint** on any OS. Split from AC5 at G1: "per OS" needs machines that do not exist, and "first paint" needs a window. **No machine available.**
-7. [auto] Running the binary under `--smoke` exits 0 and leaves no process behind: `ps -eo pid,ppid,comm` before and after shows no surviving sidecar. Split from AC6 at G1 — this is the property underneath it, and it is measurable on linux today.
-8. [manual] Killing the **window** terminates the sidecar within 5 s (heartbeat), verified with `ps` or Task Manager. Split from AC6 at G1: killing a window needs a desktop. **No machine available.**
-9. [auto] `mix gate` still green; `mix phx.server` still works without Tauri.
-
-## Proof required
-- Build logs (trimmed), curl output, screenshots under `proof/`, size/time table, process-list before/after window close.
-
-## Manual verification queue
-Five `[manual]` criteria, and they are one missing thing: **a machine that is not this one.** The owner's only
-machine is Ubuntu. Each names the machine or account it needs.
-
-| AC | Needs | What a GitHub Actions runner can prove instead |
-|---|---|---|
-| 2 | a **macOS desktop** | the artifact builds, launches and exits clean under `--smoke`, and the launch log |
-| 3 | a **Windows desktop** | the same |
-| 4 | a **Linux desktop session** | the same, plus the shell under `xvfb-run` if that is what the job does |
-| 6 | a **macOS and a Windows machine** | binary size on each; not first paint |
-| 8 | any one of the three desktops | nothing — no runner can watch a window close |
-
-A runner **cannot** produce a screenshot of a real window on a real desktop, and no artifact from one will be
-offered as though it had. These five exit the slice **unproven and named**, not counted.
-
-## Definition of Done
-- [x] gate green · [x] AC1–9 proven **or explicitly waived** — AC1, 4, 5, 6, 7, 9 proven; AC8 proven on the dev path with its production-shape failure recorded as finding F1; **AC2 and AC3 waived by the owner at G4**, both needing a desktop that does not exist here · [x] ADR-0004 finalised **as `proposed`** — the owner's G4 decision, with three appended corrections and an unchanged lift condition; its subject is the shell, and the shell has run on one OS · [x] `VERSIONS.md` rows for burrito/ex_tauri/OTP flipped to ✅ with exact versions · [x] ROADMAP → approved · [x] commit + tag
-
-Approved by the owner 2026-09-07 at `df10888`. **Two criteria exit unproven and named, not
-counted**: AC2 and AC3, a native window on macOS and on Windows. Two findings remain open and
-belong to slice 100: **F1**, the production sidecar orphaning with nothing attached, and **F2**,
-the Windows heartbeat's unauthenticated loopback TCP port.
-
-Two boxes are deliberately left unticked. They are the slice's result, not an oversight: the
-packaging spike answered what one machine can answer and says plainly what it could not reach.
-See `PROOF.md` for the evidence and the manual queue, and `NOTES.md` for the nine deviations.
-
-## Commit & tag
-`feat(s001): complete slice 001 — packaging spike (Burrito + ex_tauri)` · tag `slice/001`
-
-## Risks / open questions
-- ex_tauri may require OTP 27 or 28 specifically; if OTP 28 fails, record it — this is the ADR-0005 check.
-- Windows ERTS unpacking needs 7-Zip per Burrito notes; document.
diff --git a/slices/001-packaging-spike/proof/.gitkeep b/slices/001-packaging-spike/proof/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/slices/001-packaging-spike/proof/ac4-linux-window.png b/slices/001-packaging-spike/proof/ac4-linux-window.png
deleted file mode 100644
index 8f0039b..0000000
Binary files a/slices/001-packaging-spike/proof/ac4-linux-window.png and /dev/null differ
diff --git a/slices/002-supply-chain-early/SLICE.md b/slices/002-supply-chain-early/SLICE.md
deleted file mode 100644
index fd18765..0000000
--- a/slices/002-supply-chain-early/SLICE.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Slice 002: Supply chain, early: SBOM, build provenance, the TLS floor
-
-| Field | Value |
-|---|---|
-| Phase | 0 Foundation |
-| Milestone | M9 Donatable |
-| Size | S |
-| Depends on | 000 |
-| Status | see ROADMAP.md |
-
-Added 2026-09-20. Slice 121 keeps Scorecard, Sigstore signing of release artifacts, SLSA and the registry entry,
-and depends on the release pipeline; the three items here need none of that and are cheap enough to carry from
-now on.
-
-## Goal
-A CycloneDX software bill of materials generated by the gate and attached to every release; build provenance
-attested on release artifacts through GitHub's attestation action; one README line stating the TLS floor and
-that any MAC in an envelope Trinity mints is Trinity's and an approved one in FIPS mode.
-
-## Why
-Vision goal 10. A reviewer of a public tree expects to find what is in the binary and where the binary came
-from without asking; a regulated deployment requires both on request. Deferring them to slice 121 means every
-release before it ships without them.
-
-## Scope
-**In:**
-- `sbom` (hex.pm, `mix sbom.cyclonedx`, CycloneDX) as a dev and test dependency; a gate step that generates
- `sbom.cdx.json` and fails on a generation error; the file attached to every release by the package workflow.
-- `actions/attest-build-provenance`, pinned to a commit SHA with its release in a comment like the other actions,
- attesting each artifact the package workflow uploads. The attestation is verified in the same workflow with
- `gh attestation verify` and the output kept in the run summary.
-- One README line under Running from source: the TLS floor is 1.2, and any MAC over an envelope Trinity mints is
- Trinity's own and is an approved algorithm in FIPS mode.
-- `VERSIONS.md` row for `sbom`.
-**Out:**
-- Signing the SBOM or the artifacts with a Trinity-held key; Scorecard; SLSA levels; the registry entry. All
- slice 121.
-- A cryptography bill of materials. It follows once the minimum elements for one are published.
-
-## Design notes
-The SBOM is generated from `mix.lock`, so it is exact for Hex dependencies and blind to the Rust crates of the
-desktop shell; the file says so in a top-level property and slice 121 adds the crate half. Provenance is
-GitHub's, over the artifact's digest, and proves where the bytes were built, not what they do.
-
-## Deliverables
-- `mix.exs` dependency and gate step; `.github/workflows/package.yml` upload and attest steps; `README.md` line;
- `lib/trinity/versions.ex` row and regenerated `VERSIONS.md`.
-
-## Acceptance criteria
-1. [auto] `mix gate` generates `sbom.cdx.json` and the file validates as CycloneDX (the validator's command and
- exit code pasted).
-2. [auto] A package run attaches the SBOM to its artifacts and attests each artifact; `gh attestation verify`
- exits 0 for each, output in the run summary (run id cited).
-3. [auto] The README carries the TLS floor line, and a test pins it so the line cannot vanish silently.
-4. [auto] Gate green; coverage line reported.
-
-## Proof required
-- For each criterion: the command and its output, or a test name and its result. A sentence is not proof.
-
-## Manual verification queue
-None. Every acceptance criterion in this slice is `[auto]` and is proven by a command or a test.
-If that changes during the slice, the criterion is retagged and this section is filled at G1.
-
-## Definition of Done
-- [ ] `mix gate` green · [ ] AC1–4 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag
-
-## Commit & tag
-`feat(s002): complete slice 002 (supply chain, early)` · tag `slice/002`
-
-## Risks / open questions
-- The SBOM's blindness to the desktop shell's crates is stated in the file itself; a reader who misses the
- property misreads the bill. Slice 121 closes it.
diff --git a/slices/003-fips-build-leg/NOTES.md b/slices/003-fips-build-leg/NOTES.md
deleted file mode 100644
index 0b0de8f..0000000
--- a/slices/003-fips-build-leg/NOTES.md
+++ /dev/null
@@ -1,151 +0,0 @@
-# Slice 003: NOTES
-
-## Facts measured 2026-09-20 before any code
-
-**The container can enter FIPS mode on a host that is not in FIPS mode.** `docker run --rm
-registry.access.redhat.com/ubi9/ubi:latest` on this machine (Red Hat Enterprise Linux release 9.8, `openssl-libs
-3.5.8-1.el9_8`, no `/proc/sys/crypto/fips_enabled`): `openssl list -providers` names only `default`; with
-`OPENSSL_FORCE_FIPS_MODE=1` it names `base`, `default` and `fips` ("Red Hat Enterprise Linux 9 - OpenSSL FIPS
-Provider", version `3.0.7-cda111b5812c30d4`, from `/usr/lib64/ossl-modules/fips.so`); under it `openssl genpkey
--algorithm ed25519` fails with `unsupported ... Algorithm (ED25519 : 111)` and a P-384 key generates. That
-environment variable is Red Hat's own patch, so it is the leg's mechanism and is named as such in `docs/fips-leg.md`.
-GitHub's hosted runners are not FIPS hosts, so the leg depends on it.
-
-**UBI9 is no longer a 3.0 line.** SLICE.md's "Out" paragraph says the UBI9 OpenSSL is a 3.0 line and carries no
-ML-DSA. As of 9.8 the library is 3.5.8 and only the FIPS provider is the 3.0.7 module. ML-DSA-87 through the
-default provider on this image is therefore measurable in non-FIPS mode; that measurement belongs to slice 024
-(amendment 6) and is written there when 024 opens, not here. SLICE.md is not edited; this note supersedes the
-sentence for the reader.
-
-**The registry.** `gh api /orgs/ScriptKittyOS/packages` answers 403 for this token (no `read:packages` scope), so
-whether the image can be pushed to `ghcr.io/scriptkittyos/trinity-fips` is learned by the image workflow's first
-run, under the job's own `packages: write` permission; the fallback SLICE.md names (schedule and on demand, the
-image built in the leg itself) is line 6 below.
-
-## G1 plan, 2026-09-20
-
-Tree at `1b73b70` on `main` (023 approved); branch `slice/003-fips-build-leg`; ROADMAP row 003 to `in_progress` in
-this commit. Each line names its test or its evidence.
-
-1. `ci/fips/Containerfile`: `registry.access.redhat.com/ubi9/ubi:9.8` pinned by digest; `dnf install` of the build
- toolchain, `openssl-devel`, `ncurses-devel`; OTP 28.5.0.5 from the source tarball with `--enable-fips`; Elixir
- 1.20.4 from the precompiled `elixir-otp-28.zip`; the versions read from `.tool-versions` as build args so the
- file is the one source. Evidence: `erl -noshell -eval 'io:format("~p~n",[crypto:info()])'` in the build log
- prints `fips_provider_available => true`.
-2. `.github/workflows/fips-image.yml`: builds and pushes `ghcr.io/scriptkittyos/trinity-fips:` on push when either file changes, and on `workflow_dispatch`;
- `permissions: packages: write`. Evidence: a green run id and the image digest in PROOF.md.
-3. `.github/workflows/gate.yml`: a second job `fips` (not a matrix entry, so `gate` keeps its context name, as
- `postgres` did) on `container: ghcr.io/scriptkittyos/trinity-fips:` with `OPENSSL_FORCE_FIPS_MODE=1`
- and `ERL_FLAGS="-crypto fips_mode true"`; the same steps as `gate`. Evidence: the run id; AC5.
-4. `test/fips/mode_test.exs`, tagged `:fips`, excluded by tag on the default legs and included on this one:
- `crypto:info_fips() == :enabled`, `fips_provider_buildinfo` present in `crypto:info/0` (AC1); `crypto:sign/4`
- with `eddsa` raises `notsup`, `ecdsa`/`secp384r1`/`sha384` signs and verifies (AC2). Red first on this machine
- by construction (`not_supported` here), which is why the tag exists and the default legs exclude rather than
- skip it.
-5. `docs/fips-leg.md` and `docs/fips-leg-supports.diff`: `crypto:supports/1` printed by `scripts/crypto_supports.exs`
- on both legs and the diff committed; `test/fips/supports_diff_test.exs` regenerates the leg's half and asserts
- it equals the committed file (AC3). The FIPS provider's version string and Red Hat's certificate reference are
- cited from Red Hat's page with the URL and the date read; nothing restated as Trinity's claim.
-6. Every red on the leg is fixed in this slice when the fault is the test's, or listed by test name with its
- owning slice in `docs/fips-leg.md` when the fault is a removed algorithm (AC4); no skip tag is added. The
- registry fallback: if the push in line 2 is refused, the leg builds the image itself on a schedule and on
- `workflow_dispatch`, and SLICE.md's risk paragraph is what PROOF.md points at.
-7. Minutes of the first ten leg runs, from `gh run list`, into this file (SLICE.md deliverable).
-8. `docs/packaging.md` gains one paragraph pointing at `docs/fips-leg.md`; VERSIONS.md gains the image and its
- digest; the coverage line from the leg is reported beside the default leg's in PROOF.md (AC5).
-
-Manual verification queue: none. Every criterion is `[auto]`, as SLICE.md states.
-
-Deviations stated before any code: (a) the leg is a second job rather than a matrix entry, for the required-check
-name, as SLICE.md's own precedent (`postgres`) did; (b) FIPS mode is entered by Red Hat's environment variable
-rather than by the host, because the runner cannot be a FIPS host; both halves (the provider list and the
-`notsup`) are asserted by the tests in line 4 so the mechanism is measured on every run.
-
-## Findings, 2026-09-20, in the order they were met
-
-1. **`ERL_FLAGS` does not enter the mode; `ERL_AFLAGS` with the application loaded first does.** OTP's
- `crypto:on_load/0` reads `fips_mode` only when `application:get_env(crypto, fips_mode)` is defined, which it is
- not before `application:load(crypto)`; the NIF loads on the first `:crypto` call, which under `elixir` comes
- before Mix loads the applications. Measured on the image with `elixir -e`: `ERL_FLAGS="-crypto fips_mode true"`
- leaves `crypto:info_fips()` at `not_enabled` and prints OTP's warning; `ERL_AFLAGS="-crypto fips_mode true
- -eval application:load(crypto)"` gives `enabled`. Red Hat's `OPENSSL_FORCE_FIPS_MODE=1` alone gives
- `not_enabled` in OTP's report, and is not needed once OTP loads the provider itself. **Supersedes G1
- deviation (b)**: the mechanism is OTP's own configuration, not the distribution's variable.
-2. **The image's self-check matched the wrong atom.** `crypto:enable_fips_mode(true)` returns `true`, not `ok`;
- the first local build and the first `fips-image` run (35536968071) failed at the check after a complete
- build. `fix(s003)` at `b74da2f`.
-3. **Hex cannot reach hex.pm in the mode** (docs/fips-leg.md finding 1): Hex 2.5.1 hardcodes TLS 1.0 and 1.1
- beside 1.2. `HEX_OFFLINE=1` was tried and does not serve `hex.audit`: neither `deps.get` on a complete lock nor
- an online `hex.audit` leaves registry entries an offline audit can read (measured, runs 3 and 4 in the
- container). The gate alias's `hex.audit` step now runs with `ERL_AFLAGS` cleared; a default leg does not set
- it, so nothing changes there.
-4. **OTP's TLS 1.3 client fails a HelloRetryRequest, and the mode makes that common** (docs/fips-leg.md finding
- 2, erlang/otp #8470): `rustler_precompiled` could not download mdex's NIF at compile time. The leg compiles
- dependencies with the mode off. Both workarounds were measured in the mode against
- `release-assets.githubusercontent.com` and `repo.hex.pm`: `middlebox_comp_mode: false`, or
- `supported_groups: [:secp256r1, :secp384r1, :secp521r1]`. **Follow-up for slice 002 (the TLS floor)**:
- Trinity's own clients in a FIPS deployment need one of these, or they fail against every server that retries.
-5. **UBI's base has no `diff`** (run 35537134655). `diffutils` added to the image.
-6. **`git ls-files` in the container read an empty tree** (run 35537462271): the checkout action's safe.directory
- entry lives under a HOME it removes afterwards. One `git config --global --add safe.directory` step before
- anything reads the tree.
-7. **`plan_check.sh` flaked on a clean tree**: `printf | grep -q` under `set -o pipefail`, 1 spurious FAIL in 30
- runs (the ADR-0005 citation in slice 001's file), 0 in 60 after reading the whole input. Fixed as
- `fix(s000)` at `f9c5eed`; found because a local run failed and the next three passed. The push that carried
- the safe.directory fix went out on a `plan_check | tail -1` pipeline whose exit was `tail`'s, the mistake the
- gate alias's comment describes; recorded here so it is not repeated.
-8. **Red Hat's page and the image disagree on the provider's build hash** (docs/fips-leg.md, the provider
- section): the page lists `3.0.7-395c1a240fbfffd8` under certificate #4857 for 9.8; the image reports
- `3.0.7-cda111b5812c30d4` from `openssl-fips-provider-so-3.0.7-11.el9_8`. Stated, not resolved; owner: the
- deployment's assessor.
-9. **This OTP lists ML-DSA and SLH-DSA with the mode off** (`docs/fips-leg/supports-default.txt`: `mldsa44`,
- `mldsa65`, `mldsa87`, twelve `slh_dsa_*`), because UBI 9.8 links OpenSSL 3.5.8. Slice 024's amendment 6 can be
- measured on this image in non-FIPS mode; recorded here for 024's G1.
-10. **No test in the tree reaches a removed algorithm.** The gate in the mode: 268 passed, 12 excluded, exit 0
- (in the image on this machine, run 5, 33.5 s wall; on the leg, run 35538136447). AC4's list of reds by test
- name is therefore empty as a fact about the tree on this date, not as a property of the leg.
-
-## Runner minutes
-
-Durations of the `image` and `fips` jobs on this branch, from `gh run view --json jobs` (latest attempt of
-each run; reruns replace the earlier attempt's times), derived 2026-09-20 after run 35538136447. Fewer than the
-ten runs SLICE.md asks for exist at this line; the closing correction in PROOF.md extends the table with the
-runs the close itself produces, and the ten-run figure is filled by the first `fix` or the next slice that
-touches the leg, whichever is first.
-
-| run | job | result | duration |
-|---|---|---|---|
-| 35536968071 | image | failure (finding 2) | 2m51s |
-| 35537134611 | image | success | 2m55s |
-| 35537462269 | image | success | 3m03s |
-| 35536967968 | fips | failure (image not yet published) | 0m10s |
-| 35537134655 | fips | failure (finding 5), rerun | 1m58s |
-| 35537462271 | fips | failure (finding 6), rerun | 1m54s |
-| 35537793241 | fips | success | 3m19s |
-| 35538008648 | fips | success | 1m28s |
-| 35538136447 | fips | success | 2m03s |
-| 35538464458 | fips | success (the closing commit, 567507d) | 3m46s |
-
-The image build is under three minutes on the hosted runner (OTP from source with the unused applications left
-out), so the schedule-and-on-demand fallback SLICE.md names was not needed; the push to the registry under the
-job's `packages: write` succeeded on the first complete build (finding 2 was the check, not the push). On a push
-that changes the Containerfile, the `fips` job of the same push starts before the image lands and fails at the
-pull; a rerun of the failed job after `fips-image` finishes is the procedure, and it happened twice here.
-
-## Follow-ups
-- Slice 002 (the TLS floor): set `supported_groups` or `middlebox_comp_mode` for Trinity's clients (finding 4).
-- Slice 024: amendment 6 (ML-DSA-87) is measurable on this image with the mode off (finding 9); P-384 signatures
- from OTP are DER, 103 to 104 bytes, not the 96 the slice table names.
-- Upstream: Hex's TLS version list (finding 3); OTP's HelloRetryRequest handling (#8470, finding 4).
-- The ten-run minutes table (above).
-
-11. **A flake on the default leg at the close, owned by slice 021.** Run 35538464458, job `gate`, seed 306150:
- `test/trinity/permissions/session_flow_test.exs:108` (AC5 deny) collected events up to `approval_wait` and
- never saw the session resume; the same log shows `Trinity.Permissions.Gate` terminating on
- `DBConnection.OwnershipError` ("cannot find ownership process"), which reads as an expiry timer from an
- earlier test's request firing after that test's sandbox owner had exited, so the Gate crashed, restarted, and
- the decision was lost. The `fips` and `postgres` jobs of the same run passed the test, and the rerun of the
- job passed (264/265, then 265/265). Not touched here: it is 021's test isolation, and this slice's closing
- commit changed records only. **Follow-up, `fix(s021)`**: the Gate's expiry timers, or the test's sandbox
- allowance, need to outlive the test that armed them, or be cancelled on the owner's exit.
diff --git a/slices/003-fips-build-leg/PROOF.md b/slices/003-fips-build-leg/PROOF.md
deleted file mode 100644
index cf92018..0000000
--- a/slices/003-fips-build-leg/PROOF.md
+++ /dev/null
@@ -1,139 +0,0 @@
-# Proof for slice 003: A FIPS build leg in CI, from source
-
-Agent: Trinity · Coding Agent · Date: 2026-09-20 · Branch: slice/003-fips-build-leg · Final commit: (the commit carrying this file; named in the closing correction)
-
-## Summary
-A `fips` job in the gate workflow runs the whole gate on an OTP 28.5.0.5 built from source with `--enable-fips`
-against the OpenSSL of a UBI9 container (Red Hat Enterprise Linux 9.8, `openssl-libs 3.5.8`, the 3.0.7 FIPS
-provider), in FIPS mode, on every push. The image is built by its own workflow and pushed to the repository's
-registry under a tag derived from the two files that define it. The mode is entered by OTP's own `fips_mode`
-setting with the crypto application loaded first (`ERL_AFLAGS`), which was the measured condition. Ten findings
-in NOTES.md; the two that reach past this slice are that Hex's client and OTP's TLS 1.3 client cannot reach
-hex.pm and GitHub's release host from inside the mode (docs/fips-leg.md, findings 1 and 2), so the leg fetches
-and compiles dependencies with the mode off and the gate's `hex.audit` step clears the flags.
-
-## Gate
-On the leg (run 35538136447, job 106150777683, `fips`, all steps success):
-```
-$ mix gate (ERL_AFLAGS="-crypto fips_mode true -eval application:load(crypto)", TRINITY_FIPS_LEG=1)
-1123 mods/funs, found no issues.
-... SCAN COMPLETE ...
-No retired or security advisory packages found
-No vulnerabilities found.
-versions.verify: OK. 87 locked packages, none disagreeing with 50 pins
-Result: 268 passed, 12 excluded
-trinity.coverage: 023 75.39% vs 022 74.85%: OK
-plan_check: PASS
-```
-On this machine (default leg's view, `crypto:info_fips()` = `not_supported`, under a 32 GiB cgroup, tree 39518c2):
-```
-$ mix gate
-1123 mods/funs, found no issues.
-Result: 265 passed, 15 excluded
-trinity.coverage: 023 75.39% vs 022 74.85%: OK
-plan_check: PASS
-exit=0
-```
-`mix credo --strict --all`: 1123 mods/funs, found no issues.
-
-## Tests
-```
-$ mix test --cover (this machine, tree 39518c2)
-Result: 265 passed, 15 excluded
-| 75.39% | Total |
-```
-On the leg (run 35538136447, step "The FIPS tests by name, and this leg's coverage"):
-```
-$ mix test --cover | grep -E 'Result:|\| *Total'
-Result: 268 passed, 12 excluded
-| 75.39% | Total |
-```
-`coverage.tsv` row: `003 75.39 39518c2 2026-09-20` (unchanged from 023: the four new tests cover no `lib/`
-line, and the leg reports the same total).
-
-The slice's four tests, `mix test --trace test/fips` on the leg (run 35538136447):
-```
-Trinity.Fips.ModeTest [test/fips/mode_test.exs]
- * test the leg's declaration and the runtime's FIPS report agree (0.02ms) [L#16]
- * test on the FIPS leg AC2: eddsa is refused with notsup; ecdsa on secp384r1 with sha384 signs and verifies (13.5ms) [L#40]
- * test on the FIPS leg AC3: the committed listing is what this leg prints (386.7ms) [L#58]
- * test on the FIPS leg AC1: the mode is enabled and crypto:info/0 names the FIPS provider's build (0.04ms) [L#30]
-Result: 4 passed
-```
-On this machine the same file: `Result: 1 passed, 3 excluded` (the untagged test asserts `not_supported` here).
-
-## Acceptance criteria evidence
-
-### AC1 [auto]: on the FIPS leg, `crypto:info_fips()` returns `enabled` and `crypto:info/0` carries `fips_provider_buildinfo` (log excerpt with the run id)
-Run 35538136447, job 106150777683, step "The runtime is in FIPS mode", which runs
-`erl -noshell -eval 'enabled = crypto:info_fips(), io:format("~p~n", [crypto:info()]), halt().'` (a badmatch exits
-non-zero; the step is success):
-```
-#{otp_crypto_version => "5.8.3.2",compile_type => normal,link_type => dynamic,
- cryptolib_version_compiled => "OpenSSL 3.5.8 25 Aug 2026",
- cryptolib_version_linked => "OpenSSL 3.5.8 25 Aug 2026",
- fips_provider_available => true,
- fips_provider_buildinfo => "3.0.7-cda111b5812c30d4"}
-```
-And the test `on the FIPS leg AC1: ...` above, passed on the same run.
-
-### AC2 [auto]: on the FIPS leg, `crypto:sign/4` with `eddsa` returns `notsup` and with `ecdsa` on `secp384r1` and `sha384` returns a signature that verifies (test)
-`on the FIPS leg AC2: eddsa is refused with notsup; ecdsa on secp384r1 with sha384 signs and verifies`, passed on
-run 35538136447. The test signs and verifies with a fresh P-384 key, refutes a verify over altered data, and
-asserts `{:notsup, _, _}` raised as `ErlangError` for `crypto:sign/4` and `crypto:verify/5` with a fixed RFC 8032
-Ed25519 key; measured on the image before the test was written, the value is
-`{notsup, {"pkey.c", 235}, "Unsupported algorithm in FIPS mode"}`. Key generation for Ed25519 fails at a different
-place (`{error, {"evp.c", 264}, "Can't make context"}`), so the test asserts only that it raises.
-
-### AC3 [auto]: the committed `crypto:supports/1` diff between the two legs matches what the leg prints (test)
-Two halves on the same image, so the diff isolates the mode alone. Mode on: the test `on the FIPS leg AC3: the
-committed listing is what this leg prints` runs `elixir scripts/crypto_supports.exs` and asserts its output equals
-`docs/fips-leg/supports-fips.txt` byte for byte; passed on run 35538136447. Mode off: the step
-`crypto:supports/0 with the mode off matches docs/fips-leg/supports-default.txt` runs the same script under
-`ERL_AFLAGS="-eval application:load(crypto)"` and `diff`s against the committed file; success on the same run.
-`docs/fips-leg/supports.diff` is `diff` of the two committed files: 51 entries removed (`grep -c '^<'`), listed by
-category in docs/fips-leg.md.
-
-### AC4 [auto]: the whole gate runs on the leg; every red is either fixed or listed by test name with its owning slice in `docs/fips-leg.md`; there is no skip tag
-The gate section above: `mix gate` exit 0 on the leg with 268 passed and 12 excluded, the same 12 the default
-leg excludes by tag (`:live`, `:desktop`, `:eval`). No test in the tree went red for a removed algorithm, so the
-list in docs/fips-leg.md is empty as a fact about this tree on this date, and says so. The reds the leg did
-produce on the way were the leg's own (NOTES findings 2, 5 and 6) and are fixed. `git grep -n '@tag :skip\|@moduletag :skip' test/` prints nothing.
-
-### AC5 [auto]: gate green on both legs; coverage line reported
-Run 35538136447: `gate` success, `postgres` success, `fips-tag` success, `fips` success. Coverage: 75.39 % on the
-default leg (`Result: 265 passed, 15 excluded`, `trinity.coverage: 023 75.39% vs 022 74.85%: OK`) and 75.39 % on
-the FIPS leg (the Tests section above).
-
-## Manual verification for the reviewer
-None; SLICE.md tags every criterion `[auto]`. If the reviewer wants the leg on a machine, the last section of
-docs/fips-leg.md is the two commands.
-
-## Deviations from SLICE.md
-Stated in NOTES.md before code: (a) a second job rather than a matrix entry, as `postgres` is, so the required
-check keeps its name; (b) superseded by finding 1: the mode is entered by OTP's `fips_mode` setting through
-`ERL_AFLAGS`, not by Red Hat's variable. Two more, found during the build and recorded in NOTES.md: (c) the
-dependency fetch and compile run with the mode off, and the gate alias's `hex.audit` step clears `ERL_AFLAGS`
-(findings 3 and 4; docs/fips-leg.md findings 1 and 2); (d) SLICE.md's "Out" paragraph calls UBI9's OpenSSL a
-3.0 line and it is 3.5.8 as of 9.8, so ML-DSA is listed with the mode off (finding 9); SLICE.md is not edited.
-
-## Versions touched
-`VERSIONS.md` updated: yes, one toolchain row for the image's base (`ci/fips/Containerfile`, by digest); no
-dependency changed. `mix hex.outdated` not run.
-
-## Git
-```
-$ git log --oneline main..HEAD
-39518c2 feat(s003): the leg prints the FIPS tests by name and its coverage line
-f9c5eed fix(s000): plan_check's exists_prefix and the sign-off check read their whole input
-ed2f74a fix(s003): the fips job marks the workspace safe for git
-6ed96bf fix(s003): diffutils in the image
-b74da2f fix(s003): the image's self-check matches enable_fips_mode/1's true
-ad8bc17 feat(s003): the FIPS image, the leg in the gate, the mode tests and the supports record
-7179cac docs(s003): G1 plan with the FIPS mechanism measured, and the slice opens
-```
-
-## Closing correction, 2026-09-20
-Supersedes the header's "Final commit" placeholder: the closing commit is `567507d` (`feat(s003): complete slice
-003 (FIPS build leg)`), and this correction rides on the commit after it, which also names the CI run of the
-close in NOTES.md's minutes table.
diff --git a/slices/003-fips-build-leg/SLICE.md b/slices/003-fips-build-leg/SLICE.md
deleted file mode 100644
index eef6649..0000000
--- a/slices/003-fips-build-leg/SLICE.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Slice 003: A FIPS build leg in CI, from source, so FIPS claims are proven rather than asserted
-
-| Field | Value |
-|---|---|
-| Phase | 0 Foundation |
-| Milestone | M2 Acts |
-| Size | M |
-| Depends on | 000 |
-| Status | see ROADMAP.md |
-
-Added 2026-09-20. Counted toward M2 Acts because its reason to exist is slice 024's FIPS property tests, which
-cannot run anywhere else: the developer machine's OTP reports `crypto:info_fips()` as `not_supported`, and the
-hosted BEAM images the gate uses are not FIPS builds.
-
-## Goal
-A second entry in the gate workflow's matrix that runs the whole suite on an OTP 28.5.0.5 built from source with
-`--enable-fips` against the validated OpenSSL of a UBI9 container, with FIPS mode entered before the crypto
-application loads, and a smoke that proves the mode is on. The default leg stays exactly as it is.
-
-## Why
-Slice 024's amendment says FIPS mode selects P-384 and never denies for want of a signer. Until a build runs
-in FIPS mode, that is a sentence. This leg is the measurement. It also answers, once, what else in the tree
-reaches an algorithm FIPS mode removes.
-
-## Scope
-**In:**
-- A container image built by its own workflow, rebuilt when `.tool-versions` changes: UBI9, the distribution's
- OpenSSL and its FIPS provider, OTP 28.5.0.5 from source with `--enable-fips`, Elixir 1.20.4. Published to the
- repository's container registry so the gate leg pulls rather than builds.
-- The gate leg: `fips_mode: true` for `:crypto` in the release's `sys.config` equivalent for tests, set before
- crypto loads; the smoke `crypto:info_fips() =:= enabled` and the `fips_provider_buildinfo` key present in
- `crypto:info/0`; then `mix test` and the rest of the gate as the default leg runs it.
-- A committed record of `crypto:supports/1` on the FIPS leg diffed against the default leg, so the removed
- algorithms are a list in the tree and not a memory.
-- Runner minutes measured over the first ten runs and written into NOTES.md.
-**Out:**
-- Making FIPS mode the default anywhere; changing the OTP pin; ML-DSA (the UBI9 OpenSSL is a 3.0 line and does
- not carry it; that measurement waits for a build against OpenSSL 3.5 or later and is `:unknown` until then).
-- A FIPS-mode desktop bundle. Whether Burrito can wrap a `--enable-fips` ERTS is slice 100's question.
-
-## Design notes
-Entering or leaving FIPS mode on a running node is unsupported by OTP, so the leg sets the mode in configuration
-and never toggles it in a test. A test that needs a removed algorithm on this leg is a finding, not a skip: it is
-listed in the committed diff with the slice that owns it.
-
-## Deliverables
-- `.github/workflows/fips-image.yml` and the Containerfile; the second matrix entry in `.github/workflows/gate.yml`;
- `docs/fips-leg.md` with the `crypto:supports/1` diff and how to reproduce the image; NOTES.md with the minutes.
-
-## Acceptance criteria
-1. [auto] On the FIPS leg, `crypto:info_fips()` returns `enabled` and `crypto:info/0` carries
- `fips_provider_buildinfo` (log excerpt with the run id).
-2. [auto] On the FIPS leg, `crypto:sign/4` with `eddsa` returns `notsup` and with `ecdsa` on `secp384r1` and
- `sha384` returns a signature that verifies (test).
-3. [auto] The committed `crypto:supports/1` diff between the two legs matches what the leg prints (test).
-4. [auto] The whole gate runs on the leg; every red is either fixed or listed by test name with its owning slice
- in `docs/fips-leg.md`; there is no skip tag.
-5. [auto] Gate green on both legs; coverage line reported.
-
-## Proof required
-- For each criterion: the command and its output, or a test name and its result, with the run id. A sentence is
- not proof.
-
-## Manual verification queue
-None. Every acceptance criterion in this slice is `[auto]` and is proven by a command or a test.
-If that changes during the slice, the criterion is retagged and this section is filled at G1.
-
-## Definition of Done
-- [ ] `mix gate` green · [ ] AC1–5 proven · [ ] docs/ADR/VERSIONS updated if affected · [ ] ROADMAP status → done · [ ] final commit + tag
-
-## Commit & tag
-`feat(s003): complete slice 003 (FIPS build leg)` · tag `slice/003`
-
-## Risks / open questions
-- Building OTP from source is slow; the image workflow exists so the gate leg does not pay for it on every push.
- If the image cannot be cached in the registry for a reason found at G1, the leg runs on a schedule and on
- demand rather than on every push, and the slice says so.
-- The validated provider a container ships is the distribution's claim; the slice cites the certificate number
- the distribution publishes and does not restate it as Trinity's.
diff --git a/slices/010-core-domain-persistence/NOTES.md b/slices/010-core-domain-persistence/NOTES.md
deleted file mode 100644
index 620ee80..0000000
--- a/slices/010-core-domain-persistence/NOTES.md
+++ /dev/null
@@ -1,184 +0,0 @@
-# Slice 010: NOTES
-
-## G1 plan, 2026-09-20
-
-Tree at `68000bc` on `main`; branch `slice/010-core-domain-persistence`; ROADMAP row set to `in_progress` in
-this commit (docs/04 lifecycle, plan_check rule 11). Machine: Ryzen AI Max+ 395, OTP 28.5.0.5, Elixir 1.20.4,
-ecto_sqlite3 and exqlite as locked. Every line below names its test or command; the order is the build order.
-
-1. **Gate follow-up first, its own commit:** `plan_check` rule 12 parses every `.github/**/*.yml` and
- `.github/dependabot.yml` with the Python YAML module the ubuntu runner and this machine both carry
- (pyyaml 6.0.1 here); red demonstrated on a planted bare colon (the defect PR #7 fixed), then green.
-2. Repo config: `journal_mode: :wal`, `synchronous: :normal`, `busy_timeout`, `wal_auto_check_point` named in
- config; write pool of size 1 (`Trinity.Repo`) and a read pool; `Trinity.Repo.Receipts` declared with the same
- adapter and not started (the slot the 2026-09-20 amendment reserves). Test: the pragmas read back from a
- connection (`PRAGMA journal_mode`, `PRAGMA synchronous`).
-3. `TRINITY_DB=postgres` branch in `config/runtime.exs` with `postgrex` as an optional dependency; a CI matrix
- job with a Postgres service running `mix ecto.reset` and the suite. Test on both: the migrations apply.
-4. Data-dir lock: `Trinity.DataDir.Lock` writes `/LOCK` carrying pid, mode (`desktop | headless`) and
- a monotonic token, taken with `:file.open` exclusive create plus a liveness check on the recorded pid; a
- second boot against a held dir refuses to start naming holder pid and mode and touches no database file.
- Test: two applications in one VM cannot both own the dir; the refusal message names the holder (AC6).
-5. Migrations: `personas` (name unique, soul, model, settings), `sessions`, `messages` per docs/05, UUIDv7 ids
- from a small generator in `Trinity.UUID` (no new dependency; tested against RFC 9562's version and variant
- bits and for monotonic ordering within a millisecond).
-6. `Trinity.Sessions.Store` (queries) behind `Trinity.Sessions` (`create_session/1`, `get_session/1`,
- `list_sessions/1`, `append_message/2`, `history/2`, `archive/1`); `boundary` on `Trinity.Sessions` with
- `exports: [Trinity.Sessions]` so `Store` is internal. Test for AC5: a module under `test/support` that calls
- `Store` from `TrinityWeb` fails `mix compile --warnings-as-errors` (output pasted).
-7. `append_message/2`: one transaction reading `max(seq)` and inserting; on Postgres `SELECT ... FOR UPDATE` on
- the session row. Changeset refuses unknown roles and empty content (AC3). `history/2` ordered by `seq` with
- `limit` and `offset` (AC4).
-8. Factories in `test/support/factory.ex` (no `ex_machina`; plain functions).
-9. Stress test (AC2): 20 processes, 200 appends each, 5 sessions; gapless `seq` per session; no `SQLITE_BUSY`
- surfaced; `PRAGMA integrity_check` returns `ok`; run on both adapters; the `-wal` file size reported after
- the run (the 2026-09-20 risk line).
-10. `mix ecto.reset` on SQLite and on Postgres in CI (AC1), log excerpts in PROOF.md.
-11. Coverage line and `mix gate` (AC7); docs/05 synced with any column the migrations add.
-12. PROOF.md from the template; ROADMAP row to `done`; final commit and tag per CLAUDE.md section 4, through a
- pull request as the ruleset requires.
-
-Manual verification queue: none. Every criterion is `[auto]`.
-
-Deviations from SLICE.md, stated before building: line 3 adds `postgrex` as `optional: true` rather than a
-default dependency, so the standalone desktop build carries no Postgres driver; line 5 writes the UUIDv7
-generator in-tree rather than adding `uniq`, and proposes nothing new for VERSIONS.md.
-
-## Line 1, 2026-09-20: plan_check rule 12, red then green
-
-Planted the exact defect PR #7 fixed (the quotes removed from the step name at
-`.github/workflows/package.yml:173`, working tree only, reverted after):
-
-```
-$ ./scripts/plan_check.sh | grep -E '== 12|FAIL .github|plan_check:'
-== 12. Every workflow and Dependabot file parses as YAML ==
-FAIL .github/workflows/package.yml: not parseable as YAML: in ".github/workflows/package.yml", line 173, column 25
-plan_check: FAIL
-exit=1
-
-$ git checkout -- .github/workflows/package.yml && ./scripts/plan_check.sh | grep -E '== 12|FAIL|plan_check:'
-== 12. Every workflow and Dependabot file parses as YAML ==
-plan_check: PASS
-exit=0
-```
-
-The population is `git ls-files '.github/*.yml' '.github/*.yaml' '.github/**/*.yml' '.github/**/*.yaml'`, three
-files today. The parser is Python's yaml module (6.0.1 here, present on the ubuntu runner); its absence is a
-FAIL, not a skip.
-
-## Line 2, 2026-09-20: the one-connection pool, named pragmas, the receipts slot
-
-`config/config.exs` names every pragma (`journal_mode :wal`, `synchronous :normal`, `foreign_keys :on`,
-`busy_timeout 5000`, `cache_size -64000`, `wal_auto_check_point 1000`) and sets `pool_size: 1` for SQLite in
-every environment; dev and test no longer override the pool. The adapter is chosen at compile time from
-`TRINITY_DB` (default sqlite), because `use Ecto.Repo` takes the adapter as a literal; the config says so.
-`Trinity.Repo.Receipts` is declared with the same adapter, not started, not in `:ecto_repos`.
-
-`test/trinity/repo_config_test.exs` reads the pragmas back from a live connection: `wal`, `1` (normal), `1`
-(foreign keys), `1000` (autocheckpoint pages) all read back as configured. **One does not:** `PRAGMA
-busy_timeout` reads `0`, because exqlite installs its own busy handler and applies the timeout on its side of
-it (`deps/exqlite/lib/exqlite/connection.ex`, the comment above `set_busy_timeout/2`, at 0.40.0); the pragma
-would destroy that handler, so the driver never sets it. The test asserts the configured value and the `0`
-together, with the reason. Contention itself is the stress test's job (line 9).
-
-A read pool is not added at this slice: WAL readers do not block the writer, but a one-connection pool
-serialises reads behind writes in the same process queue. Measured need arrives with 012 and 013, and the
-Repo layout admits a read-only replica repo then without moving anything. Recorded under Follow-ups.
-
-```
-$ mix test test/trinity/repo_config_test.exs → 7 passed
-$ mix compile --warnings-as-errors --force → Generated trinity app
-$ mix test → 79 passed
-$ mix credo --strict → found no issues
-```
-
-## Follow-ups
-- A read-only replica repo over the same file, when 012 or 013 measures read latency behind the single writer.
-
-## Line 3, 2026-09-20: the Postgres branch, proven in CI on this branch
-
-Run 35509095238 on `5a9ec9b`, job `postgres` (postgres:17 service, `TRINITY_DB=postgres`, `DATABASE_URL` from
-the job env), lines from its log:
-
-```
-mix run -e 'Ecto.Adapters.Postgres = Trinity.Repo.__adapter__()' (exit 0: the compiled adapter is Postgres)
-The database for Trinity.Repo has been dropped
-The database for Trinity.Repo has been created
-Excluding tags: [:sqlite]
-Result: 72 passed, 7 excluded
-```
-
-The seven excluded are the SQLite pragma read-backs (`@moduletag :sqlite`), excluded by tag on that job and
-never skipped. Job `gate` on the same run: success. The job is not yet a required check on the ruleset; it is
-added after its first green run on `main`, which this slice's merge will be.
-
-No local Postgres was used: the machine has a server on 5432 whose password this seat does not have and did
-not guess. AC1 names CI as the proof for this half.
-
-## Line 4, 2026-09-20: the data-dir lock
-
-`Trinity.DataDir.Lock`: `/LOCK` created with `:exclusive`, carrying OS pid, mode, a per-boot token and
-the time; a supervised child placed before `Trinity.Repo`; refusal names the holder's pid and mode and touches
-no database file. Liveness through `/proc/` on Linux; elsewhere a held file is treated as held (the safe
-direction) and the message names the pid to remove by hand. A malformed file is held, never taken over.
-Test env points the lock at a temporary directory keyed by `MIX_TEST_PARTITION`.
-
-```
-$ mix test test/trinity/data_dir → 10 passed
-```
-
-One thing seen and left as it is: `mix test` halts the VM without running `terminate/2`, so the test lock
-file survives a run and is taken over as stale at the next (its pid is dead). Correct behaviour, and the
-reason the stale path has a test.
-
-## Lines 5 to 9, 2026-09-20: migrations, UUIDv7, the Sessions context, factories, the stress test
-
-**UUIDv7** (`Trinity.UUID`): an `Ecto.Type` of underlying type `:uuid` delegating cast, dump and load to
-`Ecto.UUID`, with `autogenerate/0` minting v7. Ecto refuses `autogenerate: {m, f, a}` on id types, which is how
-the type shape was arrived at; the schemas declare `@primary_key {:id, Trinity.UUID, autogenerate: true}` and
-`@foreign_key_type Trinity.UUID`. `rand_a` carries the low twelve bits of `:erlang.unique_integer([:monotonic])`,
-so ids minted within one millisecond sort in mint order up to 4096 per millisecond; `rand_b` is random on every
-call so uniqueness never depends on the counter. Tests: form, version, variant, timestamp, 1000 in order, 5000
-from 50 processes unique.
-
-**Migrations**: `personas` (name unique), `sessions`, `messages` with the unique index on `(session_id, seq)`,
-`:binary_id` columns (text on SQLite, uuid on Postgres), `utc_datetime_usec` timestamps. `MIX_ENV=test mix
-ecto.reset` and `MIX_ENV=dev mix ecto.reset` both apply them; the Postgres half is CI's (line 3's job runs
-`ecto.reset` on every push).
-
-**`append_message/2`** is one transaction: the session row is read (and on a Postgres build locked `FOR
-UPDATE`), `max(seq) + 1` is assigned, the row inserted, the session touched. The lock is compiled in or out
-from the adapter at build time rather than branched at runtime: the type checker refuses a runtime branch on a
-compile-time constant (`Repo.__adapter__()` is a literal per build), so each build carries one definition and
-the CI matrix compiles both. `seq` is never cast from the caller.
-
-**Boundary (AC5)**: `Trinity.Sessions` is a sub-boundary exporting only its API module; `Trinity` exports the
-sub-boundary so `TrinityWeb` may call the context and nothing inside it. Test: a `TrinityWeb` module calling
-`Store` is written to `lib/trinity_web/`, `mix compile --warnings-as-errors` fails naming the reference, the same
-module calling `Sessions` compiles, the probe is removed. The first version of that test failed on the lawful
-half: a top-level boundary cannot list a nested one as a dep, and the fix was exporting the sub-boundary from
-its parent, which is the documented shape.
-
-**Stress (AC2)**, inside the sandbox: 20 writers, 200 appends each, 5 sessions, gapless, integrity `ok`, 0.6 s.
-The `-wal` size printed there is 0 and is not a measurement: the sandbox rolls the test back and nothing reaches
-the WAL. Outside the sandbox (`scripts/stress_010.exs` on the dev database, this machine, SQLite 3.53.4):
-
-```
-$ MIX_ENV=dev mix ecto.reset && MIX_ENV=dev mix run scripts/stress_010.exs
-stress_010: appends=4000 errors=0 wall_ms=1327 appends_per_s=3012.1 integrity=ok wal_bytes=4152992 gapless=true sqlite=3.53.4
-$ MIX_ENV=dev mix run scripts/stress_010.exs (second run, same database)
-stress_010: appends=4000 errors=0 wall_ms=1169 appends_per_s=3420.3 integrity=ok wal_bytes=4152992 gapless=true sqlite=3.53.4
-```
-
-4,152,992 bytes is 1,014 pages of 4 KiB: the WAL sits at the 1,000-page `wal_auto_check_point` and the
-checkpoint keeps it there. No stall was seen. The threshold stays at 1000 pages; a reason to raise it would be
-a measured stall, and there is none. About 3,000 appends per second with one transaction per append is the
-number 024's checkpoint window starts from.
-
-```
-$ mix compile --warnings-as-errors --force → Generated trinity app
-$ mix test → 105 passed
-$ mix credo --strict → found no issues
-$ ./scripts/plan_check.sh → PASS
-$ mix trinity.names → OK over 199 tracked files
-```
diff --git a/slices/010-core-domain-persistence/PROOF.md b/slices/010-core-domain-persistence/PROOF.md
deleted file mode 100644
index 7d99aca..0000000
--- a/slices/010-core-domain-persistence/PROOF.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Proof for slice 010: Core domain + persistence
-
-Agent: Trinity · Coding Agent · Date: 2026-09-20 · Branch: slice/010-core-domain-persistence · Final commit: `332e881` (the PROOF sha line was filled in by the commit after it, which is the only way a file can name the commit that carries it)
-
-## Summary
-The Repo is one writer with every pragma named; `Trinity.Repo.Receipts` is a declared, unstarted slot for slice
-024's own file. The adapter is chosen at compile time (`TRINITY_DB`), and a Postgres job in CI runs the
-migrations and the suite on postgres:17. A data-dir lock before the Repo makes one node per data directory a
-refusal with a named holder rather than a hope. UUIDv7 ids are minted in-tree by an `Ecto.Type`. Three
-migrations, the `Trinity.Sessions` context over an internal `Store`, gapless `seq` inside one transaction,
-factories, and a stress test run inside the sandbox and again outside it for the real WAL number. Hard parts,
-all in NOTES.md: the type checker refusing a runtime branch on the compile-time adapter; a sub-boundary that a
-top-level boundary cannot list as a dep; mix's one-second mtime resolution defeating the boundary probe twice.
-Deferred: a read-only replica repo (NOTES.md Follow-ups).
-
-## Gate
-```
-$ mix gate (this machine, OTP 28.5.0.5, Elixir 1.20.4, under a 32 GiB cgroup)
-Result: 105 passed
-trinity.coverage: 010 44.88% vs 001 30.37%: OK
-plan_check: PASS
-exit=0
-```
-
-## Tests
-```
-$ mix test --cover
-Result: 105 passed
-| 44.88% | Total |
-```
-`coverage.tsv` row: `010 44.88 45ba4f0 2026-09-20`. Up 14.51 points from 001's 30.37%; `trinity.coverage: 010
-44.88% vs 001 30.37%: OK`.
-
-## Acceptance criteria evidence
-
-### AC1: `mix ecto.reset` works on SQLite; `TRINITY_DB=postgres mix ecto.reset` works in CI (log excerpt)
-SQLite, this machine:
-```
-$ MIX_ENV=test mix ecto.reset
-The database for Trinity.Repo has been dropped
-The database for Trinity.Repo has been created
-== Running 20260920120000 Trinity.Repo.Migrations.CreatePersonas.change/0 forward
-== Migrated 20260920120000 in 0.0s
-== Running 20260920120100 Trinity.Repo.Migrations.CreateSessions.change/0 forward
-== Migrated 20260920120100 in 0.0s
-== Running 20260920120200 Trinity.Repo.Migrations.CreateMessages.change/0 forward
-== Migrated 20260920120200 in 0.0s
-```
-Postgres, CI job `postgres` on run 35509095238 (branch head `5a9ec9b`, postgres:17 service):
-```
-mix run -e 'Ecto.Adapters.Postgres = Trinity.Repo.__adapter__()' (exit 0)
-The database for Trinity.Repo has been dropped
-The database for Trinity.Repo has been created
-Excluding tags: [:sqlite]
-Result: 72 passed, 7 excluded
-```
-That run predates the migrations; the job runs `mix ecto.reset` on every push, and the run on the closing
-commit of this branch carries the three migrations (its id is in the pull request's checks).
-
-### AC2: stress test passes on both adapters: gapless `seq` per session under concurrency; `integrity_check` ok
-Inside the sandbox (`test/trinity/sessions_stress_test.exs`, 20 writers, 200 appends each, 5 sessions):
-```
-$ mix test test/trinity/sessions_stress_test.exs
-stress: -wal size after 4000 appends: 0 bytes (the sandbox rolls back; not a measurement)
-Result: 1 passed
-```
-Outside the sandbox, so the WAL is real (`scripts/stress_010.exs`, dev database, SQLite 3.53.4):
-```
-$ MIX_ENV=dev mix ecto.reset && MIX_ENV=dev mix run scripts/stress_010.exs
-stress_010: appends=4000 errors=0 wall_ms=1327 appends_per_s=3012.1 integrity=ok wal_bytes=4152992 gapless=true sqlite=3.53.4
-$ MIX_ENV=dev mix run scripts/stress_010.exs
-stress_010: appends=4000 errors=0 wall_ms=1169 appends_per_s=3420.3 integrity=ok wal_bytes=4152992 gapless=true sqlite=3.53.4
-```
-4,152,992 bytes is 1,014 pages of 4 KiB: the WAL sits at the 1,000-page autocheckpoint. On Postgres the same
-test runs in the CI job with the pragma assertion excluded by adapter; gapless is the whole property there.
-
-### AC3: `append_message/2` rejects unknown roles and empty content with `{:error, %Ecto.Changeset{}}`
-```
-$ mix test test/trinity/sessions_test.exs --trace
-* test append_message/2 (AC3) rejects an unknown role with a changeset
-* test append_message/2 (AC3) rejects empty and blank content with a changeset
-* test append_message/2 (AC3) rejects a missing session by name
-* test append_message/2 (AC3) assigns seq from 1 and never takes it from the caller
-Result: 9 passed
-```
-
-### AC4: `history/2` returns messages in `seq` order and respects `limit`
-```
-* test history/2 (AC4) returns messages in seq order and respects limit and offset (same run, passed)
-```
-
-### AC5: `boundary` prevents `TrinityWeb` from calling `Trinity.Sessions.Store` directly
-`test/trinity/sessions_boundary_test.exs` writes a `TrinityWeb` module calling `Store` into `lib/trinity_web/`,
-runs `mix compile --warnings-as-errors --force`, asserts a non-zero exit and this text in the output:
-```
-warning: forbidden reference to Trinity.Sessions.Store
- (module Trinity.Sessions.Store is not exported by its owner boundary Trinity.Sessions)
- lib/trinity_web/zz_boundary_violation.ex:2
-```
-then writes the same module calling `Trinity.Sessions`, asserts exit 0 and no "forbidden reference", and
-removes both probes. Four consecutive runs pass (`Result: 1 passed`, 2.7 s each).
-
-### AC6: a second instance against a held data dir refuses to start, names the holder's pid and mode, leaves the database untouched
-```
-$ mix test test/trinity/data_dir --trace
-* test the supervised child refuses to start against a held dir with pid and mode in the reason
-* test a held dir has no database file created by the refused instance
-* test a second acquire in the same VM is refused, naming the live holder
-* test the application holds the configured directory
-* test the lock child starts before the Repo
-Result: 10 passed
-```
-The refusal reads: ` is held by OS pid in desktop mode; refusing to start and touching no database
-file`. The child sits before `Trinity.Repo` in the supervisor, asserted by position.
-
-### AC7: gate green; coverage line reported
-Above: `mix gate` exit 0; `trinity.coverage: 010 44.88% vs 001 30.37%: OK`.
-
-## Manual verification for the reviewer
-None. Every criterion is `[auto]`. The Postgres half of AC1 and AC2 is CI's; the reviewer reads the `postgres`
-job on the pull request.
-
-## Deviations from SLICE.md
-See NOTES.md G1 plan: `postgrex` optional rather than default; an in-tree UUIDv7 generator instead of a new
-dependency. One more, found while building: the Postgres row lock is compiled in or out from the adapter rather
-than branched at runtime, because the type checker refuses a runtime branch on a compile-time constant.
-
-## Versions touched
-`VERSIONS.md` updated: yes. The `postgrex + pgvector` row split into `postgrex` (in `mix.lock`, optional) and
-`pgvector` (not yet a dependency). `mix hex.outdated` not run; no pin moved.
-
-## Git
-```
-$ git log --oneline main..HEAD
-32d745d test(s010): the gate is green: coverage row, four sobelow skips with reasons, the boundary probe compiles forced
-45ba4f0 feat(s010): migrations, UUIDv7 ids, the Sessions context with gapless seq, the stress test
-a45db66 fix(s010): Trinity exports Paths; the boundary refused the application's data-dir call
-34842c0 refactor(s010): the lock's contention branch is its own function; credo --strict was red on nesting depth
-22937bf feat(s010): the data-dir lock, before the Repo, refusing a held directory by pid and mode
-5a9ec9b feat(s010): the Postgres branch: postgrex optional, test config, a CI job with a Postgres service
-0b38a68 feat(s010): one-connection write pool, every pragma named, the receipts repo slot
-17850ce feat(s010): plan_check rule 12 parses every workflow and Dependabot file
-7b2fb39 docs(s010): G1 plan, and the slice opens
-332e881 feat(s010): complete slice 010 (core domain and persistence)
-```
diff --git a/slices/010-core-domain-persistence/SLICE.md b/slices/010-core-domain-persistence/SLICE.md
deleted file mode 100644
index 4d09078..0000000
--- a/slices/010-core-domain-persistence/SLICE.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# Slice 010: Core domain + persistence
-
-| Field | Value |
-|---|---|
-| Phase | 1 Core loop |
-| Milestone | M1 Talks |
-| Size | M |
-| Depends on | 000 |
-
-## Goal
-`Trinity.Repo` configured for SQLite (WAL, single writer) with Postgres as a CI-tested alternative; `sessions` and
-`messages` schemas; the `Trinity.Sessions` context's persistence API (create session, append message with gapless
-`seq`, list, load history); PubSub started; the concurrency stress test this design has to pass.
-
-## Why
-Data outlives processes. Everything later rehydrates from these tables.
-
-## Scope
-**In:**
-- Repo config: SQLite path from `Trinity.Paths.data_dir/0`, `journal_mode: :wal`, `synchronous: :normal` stated
- explicitly rather than inherited from the adapter's default, `busy_timeout`, a WAL checkpoint threshold set and
- named in config (the adapter's default is the starting value; the stress test below reports checkpoint stalls
- if any), pool size 1 for the write pool; optional read pool. (Amended 2026-09-20.)
-- **A slot for a second database file.** The Repo layout admits a second SQLite file for receipts later
- (`Trinity.Repo.Receipts`, declared and unused here) so that slice 024 can run its chain on `synchronous: :full`
- in its own file without moving the primary. One file at this slice; the slot is the whole change. (Amended
- 2026-09-20.)
-- **Data-dir lock at boot.** The single-writer pool makes concurrent writes safe *within one BEAM node*. It does
- nothing about two OS processes on one database file, which is exactly the failure `docs/00-vision.md` claims to
- fix. Slice 061 ships a headless profile and slice 100 ships a desktop app; nothing today stops them sharing a
- data dir. Acquire an advisory lock or a lockfile carrying pid and mode at boot, and refuse to start with a named
- reason when it is held.
-- `TRINITY_DB=postgres` config branch (`postgrex`); CI matrix job with a Postgres service.
-- Migrations: `personas` (minimal: name, soul, model; full use in 030), `sessions`, `messages` per `docs/05-data-model.md`.
-- `Trinity.Sessions` persistence functions: `create_session/1`, `get_session/1`, `list_sessions/1`, `append_message/2`
- (assigns `seq` atomically), `history/2` (ordered, with limit/offset), `archive/1`.
-- `Trinity.Sessions.Store` internal module isolates queries; `boundary` `exports: [Trinity.Sessions]`.
-- UUIDv7 ids (`uniq` or a tiny generator), `utc_datetime_usec`.
-- Factories in `test/support/factory.ex`.
-- Stress test: 20 concurrent processes each appending 200 messages to 5 sessions; assert gapless `seq`, no
- `SQLITE_BUSY` errors surfaced, DB `PRAGMA integrity_check` == ok.
-**Out:**
-- Session processes (012), FTS (031), memories (030), LLM.
-
-## Design notes
-- `append_message/2` must be a single transaction that reads max(seq) and inserts; on SQLite with one writer this is
- race-free; on Postgres use `SELECT … FOR UPDATE` on the session row. Test both.
-- Keep `parts` as `:map`: SQLite stores JSON text; Postgres jsonb.
-
-## Deliverables
-- `config/*.exs` DB branches, `priv/repo/migrations/*`, `lib/trinity/sessions/{session,message,store}.ex`, `lib/trinity/sessions.ex`, `lib/trinity/paths.ex` (if not from 001), tests, CI matrix update, `docs/05-data-model.md` synced.
-
-## Acceptance criteria
-1. [auto] `mix ecto.reset` works on SQLite; `TRINITY_DB=postgres mix ecto.reset` works in CI (log excerpt).
-2. [auto] Property/stress test passes on both adapters: gapless `seq` per session under concurrency; `integrity_check` ok.
-3. [auto] `append_message/2` rejects unknown roles and empty content with `{:error, %Ecto.Changeset{}}`.
-4. [auto] `history/2` returns messages in `seq` order and respects `limit`.
-5. [auto] `boundary` prevents `TrinityWeb` from calling `Trinity.Sessions.Store` directly (test compiles a violating module in a tmp dir, or document the compile error).
-6. [auto] A second instance started against a held data dir refuses to start, names the holder's pid and mode, and leaves the database untouched (test).
-7. [auto] Gate green; coverage line reported.
-
-## Proof required
-- Test output for stress tests on both adapters, migration logs, boundary check evidence, coverage.
-
-## Manual verification queue
-None. Every acceptance criterion in this slice is `[auto]` and is proven by a command or a test.
-If that changes during the slice, the criterion is retagged and this section is filled at G1.
-
-## Definition of Done
-- [ ] gate green · [ ] AC1–7 proven · [ ] docs/05 updated · [ ] ROADMAP → done · [ ] commit + tag
-
-## Commit & tag
-`feat(s010): complete slice 010 (core domain and persistence)` · tag `slice/010`
-
-## Risks / open questions
-- `ecto_sqlite3` and Oban Lite both want the same file; confirm pool settings when Oban arrives (050).
-- WAL checkpoint starvation under a continuous writer: the stress test is the place it would show, as a
- growing `-wal` file or a stalled append; report the file size after the run either way.
-- `synchronous: :normal` under WAL can lose the last transaction on power loss and never corrupts. That is the
- right trade for sessions and messages; the receipts file (slice 024) decides its own setting.
diff --git a/slices/011-llm-provider-layer/NOTES.md b/slices/011-llm-provider-layer/NOTES.md
deleted file mode 100644
index 12463bc..0000000
--- a/slices/011-llm-provider-layer/NOTES.md
+++ /dev/null
@@ -1,187 +0,0 @@
-# Slice 011: NOTES
-
-## G1 plan, 2026-09-20
-
-Tree at `6af7ee6` on `main` (010 approved); branch `slice/011-llm-provider-layer`; ROADMAP row 011 set to
-`in_progress` in this commit. req_llm 1.24.0 was read from its Hex tarball before this plan was written (public
-entry points `generate_text/3`, `stream_text/3`, `generate_object/4`, `embed/3`; stream chunk types `:content`,
-`:thinking`, `:tool_call`, `:meta`; usage keys `input_tokens`, `output_tokens`, `total_tokens`, `cached_tokens`,
-`reasoning_tokens`, `total_cost`; model spec `"provider:model"`; keys from a per-request `:api_key` option, the
-`:req_llm` application env, or the provider's env var). Each line names its test; the order is the build order.
-
-1. Dependencies: `req_llm` at 1.24.0 inside the `~> 1.22` pin, `mox` for tests. What enters `mix.lock` with
- req_llm is counted and its licences listed in NOTES.md, because the desktop binary carries it. VERSIONS rows
- flipped by `mix versions.gen`. Test: `mix hex.audit` and `mix deps.audit` green; `versions.verify` OK.
-2. `Trinity.LLM.Event`: the seven event shapes from SLICE.md as a typespec plus `valid?/1`, the single source of
- truth 012 and 013 consume. `Trinity.LLM.Request` struct (system, messages, tools, model, params) with a
- changeset-free `new/1` that validates roles and tool schemas. Tests: every shape accepted, a foreign shape
- refused.
-3. `Trinity.LLM.Provider` behaviour: `stream/3`, `generate/2`, `generate_object/3`, `embed/2`, `models/0`,
- `capabilities/1`. `Trinity.LLM.Registry` from `config :trinity, :llm` (models as `{id, provider, model, caps,
- price}`, `default_model`), `models/0`, `default_model/0`, `lookup/1`. Test: a registry entry resolves to its
- provider module; an unknown id is refused by name.
-4. `Trinity.LLM` public API, resolving the provider from the registry entry for the request's model; `stream/3`
- (function) and `stream_to/3` (pid, events as messages); `Trinity.LLM.ProviderMock` (Mox) as the test provider
- for the registry's `fake` provider. Boundary `Trinity.LLM` with `deps: [Trinity]`, exporting the API,
- `Request` and `Event`. Test (AC7): switching `default_model` in config changes the provider used, no code
- change.
-5. `Trinity.LLM.Providers.Fake` in `test/support`: streams a scripted response (deltas, one tool call in three
- chunks, usage, done), scriptable to raise transient or non-transient errors N times. Test (AC1): the full
- event sequence, in order.
-6. `Trinity.LLM.Providers.ReqLLM`: `Request` to req_llm messages and `ReqLLM.Tool`; `stream_text` chunks to
- events with tool calls assembled (start on the first chunk naming a call, deltas while arguments grow, end
- when the call is complete or the stream ends); `:meta` to `{:usage, _}` and `{:done, reason}`; `generate`,
- `generate_object` (req_llm's JSON schema path), `embed`. Provider mapping for `anthropic`, `openai`,
- `openrouter`, `google`, `openai_compatible` (the openai provider with `base_url` and a per-request key).
- Errors classified: timeouts, 429, 5xx, connection refused are transient; 4xx otherwise are not. Unit tests
- over recorded chunk sequences; live tests in line 9.
-7. `Trinity.LLM.Retry`: attempts and base backoff from config; transient errors retried, non-transient returned
- at once; optional fallback model list tried in order after the attempts. Test (AC4): the fake raises a
- transient error twice then succeeds; raises N+1 times then `{:error, _}`; a non-transient error returns
- immediately with one attempt.
-8. `usage_events` migration and `Trinity.LLM.Usage`: one row per completed call with model, provider, tokens,
- and cost computed from the registry price; req_llm's own `total_cost` kept in `provider_meta` and never
- used. Test (AC5): a row exists after a fake call with the expected cost; the Postgres job applies the
- migration.
-9. Live tests, `@tag :live`, excluded by default (NetworkGuard's opt-in path): OpenRouter with
- `TRINITY_LIVE_MODEL` and NVIDIA through `openai_compatible` with `NEMOTRON_BASE_URL`, `NEMOTRON_MODEL` and the
- key as `:api_key`; a preflight that the model id is on `GET /models` and names a 410 as end of life. Cases:
- a streamed completion with deltas and done; a tool call; `generate_object` against a schema (AC2);
- `embed` against `nvidia/nemotron-3-embed-1b` with the declared dimension (AC3). Output pasted, keys
- redacted (AC6).
-10. `Trinity.Config.secret/1`: environment now, keychain at slice 100; the adapter reads keys only through it.
- Test: a missing key is a named error, not a nil passed to the provider.
-11. Anthropic prompt-cache hint: a `cache: true` param maps to req_llm's cache-control option for the anthropic
- provider and is dropped for others. Unit test on the mapping; not measured live (no Anthropic key here).
-12. Gate, coverage row, PROOF.md, ROADMAP to `done`, through a pull request; tag `slice/011` after the merge.
-
-Manual verification queue, as SLICE.md tags them: AC2 (live `generate_object`) and AC6 (`mix test --only live`,
-output pasted with keys redacted). Both run on this machine with the owner's keys in `.env`; the owner reads
-the pasted output at G4. The developer machine, the model ids and the probe outputs behind the choice:
-
-- OpenRouter, `inclusionai/ling-3.0-flash-vl:free` (owner's pick): tool call `get_weather` with
- `{"city": "Paris"}`, 298 tokens, served by Novita; streamed completion 59 chunks.
-- NVIDIA endpoint, `nvidia/nemotron-3.5-lightning-30b-a3b`: tool call, 317 tokens; streamed completion 20
- chunks. The id first set, `nvidia/llama-3.3-nemotron-super-49b-v1.5`, answered `410 Gone` (end of life
- 2026-08-26); that is why line 9 carries a preflight.
-
-Deviations from SLICE.md, stated before building: the five provider mappings are written; only OpenRouter and
-the NVIDIA endpoint are measured live, and PROOF.md says so per provider. `Trinity.LLM.Supervisor` from docs/01
-(rate limiters) is not built here: nothing in this slice needs a process, and a supervisor with no children
-would be a claim; 012 adds it when the Session needs one. Recorded as a follow-up.
-
-## Line 1, 2026-09-20: the dependencies, counted
-
-`req_llm ~> 1.22` resolved to 1.24.0 and `mox ~> 1.2` to 1.3.1. `mix.lock` went from 68 to 80 packages. The
-twelve that entered, with licence from hex.pm metadata (`curl -s https://hex.pm/api/packages/ | jq
-.meta.licenses`):
-
-| package | version | licence | why req_llm needs it |
-|---|---|---|---|
-| req_llm | 1.24.0 | Apache-2.0 | the provider layer |
-| llm_db | 2026.9.4 | Apache-2.0 | its model and price database, dated |
-| dotenvy | 1.2.1 | Apache-2.0 | reads `.env` for provider keys |
-| jsv | 0.23.0 | Apache-2.0 | JSON schema validation for structured output |
-| zoi | 0.18.7 | Apache-2.0 | its struct schemas |
-| splode | 0.3.2 | MIT | its error classes |
-| server_sent_events | 1.1.0 | MIT | SSE parsing for streaming |
-| websockex | 0.5.1 | MIT | a realtime transport Trinity does not use |
-| texture | 1.2.1 | Apache-2.0 | transitive |
-| toml | 0.7.0 | Apache-2.0 | transitive |
-| abnf_parsec | 2.1.0 | MIT | transitive (idna) |
-| idna | 7.1.0 | MIT | transitive |
-
-Plus `mox` 1.3.1 (Apache-2.0), test only. Every licence is Apache-2.0 or MIT. `mix hex.audit`: no retired or
-advisory packages. `mix deps.audit`: no vulnerabilities. `versions.verify`: OK, 80 locked, 46 pins. Binary size
-delta is measured at the next package run, not estimated here.
-
-One thing to know about `dotenvy`: req_llm's key lookup reads `.env` through it at startup. Trinity's `.env` is
-gitignored and holds the owner's keys, so in the default test run the keys may be present in the environment
-while the network guard still refuses every connection; the live tag is what opens the network, not the
-presence of a key. Line 10 routes every key through `Trinity.Config.secret/1` anyway.
-
-## Lines 2 to 11, 2026-09-20: what was built, and what the live suite found
-
-**Built.** `Trinity.LLM.Event` (seven shapes, `valid?/1`); `Trinity.LLM.Request` (`new/1`, `new!/1`, roles and
-tools validated); `Trinity.LLM.Error` (`transient?`, `status`); `Trinity.LLM.Provider` behaviour;
-`Trinity.LLM.Registry` over `config :trinity, :llm` (read at call time, which is what makes AC7 a test);
-`Trinity.LLM` (`stream/3`, `stream_to/3` under `Trinity.LLM.TaskSupervisor`, `generate/2`, `generate_object/3`,
-`embed/2`, `models/0`, `default_model/0`, `capabilities/1`); `Trinity.LLM.Retry` (attempts, base backoff, no
-jitter, stated); `Trinity.LLM.Usage` and the `usage_events` migration; `Trinity.Config.secret/1`;
-`Trinity.LLM.Providers.ReqLLM` (the half that talks) and `Trinity.LLM.Providers.ReqLLM.Mapping` (the pure half:
-chunks to events, responses to results, errors to `Error`); `Trinity.LLM.Providers.Fake` in test/support; the
-Mox mock; `config/llm.exs` as the registry's own file. The registry's model strings are `":"`
-split on the first colon; the provider name is one of the five the spec names, mapped to req_llm's atom
-through a fixed map, so a name from config never mints an atom (sobelow found the first version doing so).
-
-**The live suite found four defects, each fixed with its reason left in the code.**
-
-1. `Trinity.LLM.provider_opts/2` merged the caller's opts over the entry's, so `embed(texts, model:
- "nvidia:embed")` handed req_llm a provider named `nvidia`. Entry keys now win.
-2. req_llm's streaming `receive_timeout` defaults to 30 s; the NVIDIA reasoning model exceeded it before its
- first byte under load. The adapter passes `receive_timeout_ms` from config, 120 000 by default; the embedding
- call validates a different option set (no `receive_timeout`), so the budget travels as `total_timeout` there.
-3. An upstream `429` from OpenRouter's free pool arrived inside a `ReqLLM.Error.API.Stream` whose own `status`
- was nil, and the first classifier called it permanent. `classify/1` now reads the innermost error that
- carries a status, a `retryable` flag or a timeout cause. Pinned by `mapping_test.exs`.
-4. An inline model spec without capabilities is refused for embeddings by req_llm ("does not support embedding
- operations"); the spec now carries `capabilities: %{embeddings: true}` for a registry entry with `:embed`.
-
-**Model ids.** req_llm's catalog does not know the free OpenRouter id or the NVIDIA ids and warned about
-"unverified" models; the adapter uses an inline spec (`%{provider:, id:, capabilities:}`) because Trinity's
-registry is the catalog. The NVIDIA embedding model answers with dimension **2048**.
-
-**The tool-call assembly** follows req_llm 1.24.0's default decoder: a `:tool_call` chunk with `id`, `index` and
-`expects_arg_fragments` opens a call; `:meta` chunks carry `tool_call_args` fragments by index; the finish
-reason closes every open call. Reproduced with the library's own `ReqLLM.StreamChunk` constructors in
-`mapping_test.exs` (13 tests), so a change in that shape is a red here first.
-
-**Live suite, this machine, 2026-09-20** (`TRINITY_LIVE=1 mix test --only live`, keys from `.env`, redacted;
-OpenRouter `inclusionai/ling-3.0-flash-vl:free` served by Novita; NVIDIA `nvidia/nemotron-3.5-lightning-30b-a3b`
-and `nvidia/nemotron-3-embed-1b` at `integrate.api.nvidia.com/v1`):
-
-```
-* test nvidia:nemotron preflight: the configured model is on the provider's list [L#56]
* test nvidia:nemotron preflight: the configured m
-* test nvidia:nemotron a streamed completion yields text deltas, usage and done [L#62]
* test nvidia:nemotron a streamed completion yields
-* test openrouter:ling preflight: the configured model is on the provider's list [L#56]
* test openrouter:ling preflight: the configured m
-* test nvidia:nemotron generate_object returns a map that validates against the schema (AC2) [L#121]
* test nvidia:nemotron generate_objec
-* test openrouter:ling a tool call arrives as start, end, and a done of tool_calls [L#85]
* test openrouter:ling a tool call arrives as st
-live embed: dimension 2048
-* test embed returns vectors of the declared dimension against the NVIDIA embedding model (AC3) (467.4ms) [L#148]
-* test openrouter:ling a streamed completion yields text deltas, usage and done [L#62]
* test openrouter:ling a streamed completion yields
-* test nvidia:nemotron a tool call arrives as start, end, and a done of tool_calls [L#85]
* test nvidia:nemotron a tool call arrives as st
-* test openrouter:ling generate_object returns a map that validates against the schema (AC2) [L#121]
* test openrouter:ling generate_objec
-live usage row: 25 in, 14 out, provider_cost nil
-* test a live call writes one usage_events row with the tokens the provider reported (740.8ms) [L#160]
-Finished in 150.6 seconds (0.1s async, 150.5s sync)
-Result: 10 passed, 138 excluded
-```
-
-An earlier run took 219.8 s and passed 10 of 10 as well; a run before the fixes above passed 6 of 10 and then
-8 of 10, which is the record of finding them. OpenRouter's free pool returned `429` several times during these
-runs; req_llm retries a 429 three times on its own and Trinity's retry sits above that.
-
-**Coverage** 51.57% (up 6.69 from 010). `Trinity.LLM.Providers.ReqLLM` itself reads 0% in the default run: it
-is the half that talks, and only the live suite reaches it; `Mapping` reads 86%.
-
-```
-$ mix gate → exit 0; 138 passed, 10 excluded; plan_check: PASS
-$ mix test --only live → 10 passed (above)
-$ mix trinity.coverage → 011 51.57% vs 010 44.88%: OK
-```
-
-**Deviations from SLICE.md, in addition to the two stated at G1.** The `usage_events` columns are
-`input_tokens`/`output_tokens` (the Event usage keys) rather than `prompt_tokens`/`completion_tokens`, and there is
-no `latency_ms` column: latency is a Telemetry measurement at 090; docs/05 now says both. `models/0` on the
-req_llm provider returns `[]`: req_llm's catalog is not Trinity's registry and listing it would be a claim
-about models Trinity has not configured. `capabilities/1` on the adapter is the fixed `[:stream, :tools,
-:json]`; the registry entry is the source callers use.
-
-## Follow-ups
-- `Trinity.LLM.Supervisor` (docs/01, rate limiters): when 012's Session needs a process. Only the task
- supervisor for `stream_to/3` exists.
-- Prompt-cache hint for Anthropic: mapped (`cache: true` becomes `provider_options: [cache_control: ...]` for
- the anthropic provider, dropped for others), not measured: no Anthropic key here.
-- The `:google` and `:anthropic` and plain `:openai` mappings are written and not measured live.
-- OpenRouter's free pool rate-limits under repeated runs; a paid key or a second free id in the registry would
- make the live suite steadier. `nvidia/nemotron-3.5-lightning:free` passed the same probe on 2026-09-20.
diff --git a/slices/011-llm-provider-layer/PROOF.md b/slices/011-llm-provider-layer/PROOF.md
deleted file mode 100644
index 4326d56..0000000
--- a/slices/011-llm-provider-layer/PROOF.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# Proof for slice 011: LLM provider layer
-
-Agent: Trinity · Coding Agent · Date: 2026-09-20 · Branch: slice/011-llm-provider-layer · Final commit: `49005dc` (filled by the commit after it)
-
-## Summary
-`Trinity.LLM` is the one door to a model: a registry id names the provider module, transient errors retry with
-backoff, and a completed call writes one `usage_events` row with cost from the registry price. The seven event
-shapes 012 and 013 consume are fixed in `Trinity.LLM.Event`. The req_llm adapter is split into the half that
-talks and a pure `Mapping` half tested over recorded chunk sequences. The live suite against OpenRouter and the
-NVIDIA endpoint found four defects on its first runs (a merge-order bug, a 30-second stream timeout, a wrapped
-429 called permanent, an embedding capability the inline spec had to declare); each is fixed with its reason in
-the code and pinned by a test. Deferred: `Trinity.LLM.Supervisor` until a Session needs a process (NOTES.md
-Follow-ups). Keys reach a provider only through `Trinity.Config.secret/1`; none appears in this file.
-
-## Gate
-```
-$ mix gate (this machine, OTP 28.5.0.5, Elixir 1.20.4, under a 32 GiB cgroup)
-385 mods/funs, found no issues.
-No vulnerabilities found.
-Result: 138 passed, 10 excluded
-trinity.coverage: 010 44.88% vs 001 30.37%: OK
-plan_check: PASS
-exit=0
-```
-
-## Tests
-```
-$ mix test --cover
-Result: 138 passed, 10 excluded
-| 51.57% | Total |
-| 86.11% | Trinity.LLM.Providers.ReqLLM.Mapping |
-| 0.00% | Trinity.LLM.Providers.ReqLLM | (the half that talks; the live suite covers it)
-```
-`coverage.tsv` row: `011 51.57 ec5334a 2026-09-20`. `trinity.coverage: 011 51.57% vs 010 44.88%: OK`.
-
-## Acceptance criteria evidence
-
-### AC1: FakeProvider-driven test shows a full event sequence: text deltas, tool_call events, usage, done
-```
-$ mix test test/trinity/llm/llm_test.exs --trace
-* test default_model (AC7) an unknown model id is refused by name
-* test default_model (AC7) an unknown model id is refused by name (0.06ms)
-* test default_model (AC7) switching default_model in config changes the provider used, with no code change
-* test default_model (AC7) switching default_model in config changes the provider used, with no code change (0.4ms)
-* test embed/2 (AC3) returns one vector per text of the declared dimension
-* test embed/2 (AC3) returns one vector per text of the declared dimension (0.1ms)
-* test retry (AC4) a permanent error returns at once with one attempt
-* test retry (AC4) a permanent error returns at once with one attempt (0.05ms)
-* test retry (AC4) a transient error is retried and then succeeds
-* test retry (AC4) a transient error is retried and then succeeds (4.8ms)
-* test retry (AC4) attempts exhausted returns the error, named as exhausted
-* test retry (AC4) attempts exhausted returns the error, named as exhausted (4.3ms)
-* test retry (AC4) no usage row is written for a failed call
-* test retry (AC4) no usage row is written for a failed call (0.1ms)
-* test stream/3 (AC1) emits the full sequence: text deltas, tool call start, deltas, end, usage, done
-* test stream/3 (AC1) emits the full sequence: text deltas, tool call start, deltas, end, usage, done (0.6ms)
-* test stream/3 (AC1) stream_to/3 delivers the same events as messages and then llm_done
-* test stream/3 (AC1) stream_to/3 delivers the same events as messages and then llm_done (0.1ms)
-* test usage_events (AC5) embed and object calls record their own kinds
-* test usage_events (AC5) embed and object calls record their own kinds (23.7ms)
-* test usage_events (AC5) one row per completed call, cost from the registry price
-* test usage_events (AC5) one row per completed call, cost from the registry price (0.2ms)
-* test usage_events (AC5) the session id is recorded when given
-* test usage_events (AC5) the session id is recorded when given (6.3ms)
-Result: 20 passed
-```
-The AC1 test asserts the exact list: two `text_delta`, `tool_call_start`, two `tool_call_delta`,
-`tool_call_end` with `%{"city" => "Paris"}`, `usage`, `done :tool_calls`; every event passes `Event.valid?/1`.
-`stream_to/3` delivers the same as messages with a `{:llm_done, ref, result}` last.
-
-### AC2: [manual] generate_object/3 returns a validated map for a JSON schema (fake) and (live) for one real provider
-Fake: `generate_object/3 returns a map shaped by the schema` (above). Live, both providers, from the live trace
-below: `openrouter:ling generate_object returns a map that validates against the schema (AC2)` and
-`nvidia:nemotron generate_object ...` both pass; the assertion is a map with a string `city` matching Paris and a
-numeric `population_millions`, both required by the schema.
-
-### AC3: embed/2 returns vectors of the declared dimension (fake + live)
-Fake: `embed/2 (AC3) returns one vector per text of the declared dimension` (8, from the registry entry).
-Live: `live embed: dimension 2048` against `nvidia/nemotron-3-embed-1b`, two texts, two vectors, every element a
-float.
-
-### AC4: transient error retried N times then {:error, _}; non-transient immediate
-```
-* test retry (AC4) a transient error is retried and then succeeds (fails twice on 429, third call succeeds; 3 calls)
-* test retry (AC4) attempts exhausted returns the error, named as exhausted ({:exhausted, 3, :down}; 3 calls)
-* test retry (AC4) a permanent error returns at once with one attempt (401; 1 call)
-* test retry (AC4) no usage row is written for a failed call
-* test Retry.run/2 backs off exponentially and stops at the attempt count (sleeps 10, 20, 40 ms for 4 attempts)
-```
-
-### AC5: a usage_events row per completed call with tokens and cost from the registry price
-```
-* test usage_events (AC5) one row per completed call, cost from the registry price
- model_id "fake:chat", provider "fake", kind "chat", 10 in, 5 out,
- cost_usd 0.00002 (10 tokens at $1.00 per million + 5 at $2.00 per million), provider_meta %{"provider_cost" => nil}
-* test usage_events (AC5) the session id is recorded when given
-* test usage_events (AC5) embed and object calls record their own kinds
-```
-Live: `live usage row: 25 in, 14 out, provider_cost nil` from a real OpenRouter call, `cost_usd 0.0` because the
-registry prices the free tier at 0.
-
-### AC6: [manual] mix test --only live passes against at least one configured provider (output pasted; keys redacted)
-Two providers, this machine, 2026-09-20. Keys loaded from `.env` (gitignored) and redacted by pattern in this
-transcript; none appears below.
-```
-$ set -a; . ./.env; set +a; TRINITY_LIVE=1 mix test --only live --trace
-* test nvidia:nemotron preflight: the configured model is on the provider's list [L#56]
* test nvidia:nemotron preflight: the configured m
-* test nvidia:nemotron a streamed completion yields text deltas, usage and done [L#62]
* test nvidia:nemotron a streamed completion yields
-* test openrouter:ling preflight: the configured model is on the provider's list [L#56]
* test openrouter:ling preflight: the configured m
-* test nvidia:nemotron generate_object returns a map that validates against the schema (AC2) [L#121]
* test nvidia:nemotron generate_objec
-* test openrouter:ling a tool call arrives as start, end, and a done of tool_calls [L#85]
* test openrouter:ling a tool call arrives as st
-live embed: dimension 2048
-* test embed returns vectors of the declared dimension against the NVIDIA embedding model (AC3) (467.4ms) [L#148]
-* test openrouter:ling a streamed completion yields text deltas, usage and done [L#62]
* test openrouter:ling a streamed completion yields
-* test nvidia:nemotron a tool call arrives as start, end, and a done of tool_calls [L#85]
* test nvidia:nemotron a tool call arrives as st
-* test openrouter:ling generate_object returns a map that validates against the schema (AC2) [L#121]
* test openrouter:ling generate_objec
-live usage row: 25 in, 14 out, provider_cost nil
-* test a live call writes one usage_events row with the tokens the provider reported (740.8ms) [L#160]
-Finished in 150.6 seconds (0.1s async, 150.5s sync)
-Result: 10 passed, 138 excluded
-```
-Each provider ran a preflight against its `GET /models` (an id that reached end of life is a named refusal),
-a streamed completion, a tool call and a structured object; the embedding model ran once; one real call wrote
-a usage row. The earlier runs that found the four defects are recorded in NOTES.md as 6/10 then 8/10.
-
-### AC7: switching default_model in config changes the provider used with no code change
-```
-* test default_model (AC7) switching default_model in config changes the provider used, with no code change
- default "fake:chat" streams from the fake; Application.put_env(... default_model: "mock:chat") and the same
- request streams from the Mox mock, which asserts it received the entry's model name "chat".
-* test default_model (AC7) an unknown model id is refused by name
-```
-
-## Manual verification for the reviewer
-AC2 and AC6 are the live runs above; the reviewer reads the transcript and, with keys in `.env`, may re-run
-`TRINITY_LIVE=1 mix test --only live`. OpenRouter's free pool rate-limits under repeated runs (429, retried).
-
-## Deviations from SLICE.md
-See NOTES.md: five provider mappings written, two measured live; no `Trinity.LLM.Supervisor` until a process
-needs one; `usage_events` columns are the Event usage keys and carry no latency (090's telemetry measures it);
-the adapter's `models/0` returns `[]` and `capabilities/1` is fixed, the registry being the source.
-
-## Versions touched
-`VERSIONS.md` updated: yes. `req_llm` 1.24.0 and `mox` 1.3.1 read in `mix.lock`. Twelve packages entered the
-lock with req_llm, listed with licences in NOTES.md line 1; all Apache-2.0 or MIT. `mix hex.outdated` not run.
-
-## Git
-```
-$ git log --oneline main..HEAD
-d53bfec feat(s011): the adapter's pure half is a module with recorded-chunk tests; coverage row; docs/05 synced
-ec5334a feat(s011): the provider layer: behaviour, events, registry, req_llm adapter, retry, usage rows, fake and live suites
-73caeb5 feat(s011): req_llm 1.24.0 and mox, with what they bring counted and licensed
-b9cbcf2 docs(s011): G1 plan, and the slice opens
-49005dc feat(s011): complete slice 011 (LLM provider layer)
-```
diff --git a/slices/011-llm-provider-layer/SLICE.md b/slices/011-llm-provider-layer/SLICE.md
deleted file mode 100644
index 9bbfa3d..0000000
--- a/slices/011-llm-provider-layer/SLICE.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Slice 011: LLM provider layer
-
-| Field | Value |
-|---|---|
-| Phase | 1 Core loop |
-| Milestone | M1 Talks |
-| Size | M |
-| Depends on | 010 |
-
-## Goal
-`Trinity.LLM` public API and `Trinity.LLM.Provider` behaviour; `Trinity.LLM.Providers.ReqLLM` implementation with
-streaming text, tool-call events, structured output, embeddings, and normalised usage; model registry from config;
-Mox mock; opt-in live tests against at least one real provider.
-
-## Why
-Provider-agnostic by construction (Vision goal 6). Sessions never see a vendor SDK.
-
-## Scope
-**In:**
-- Behaviour callbacks: `stream(request, opts, emit_fn)`, `generate(request, opts)`, `generate_object(request, schema, opts)`, `embed(texts, opts)`, `models/0`, `capabilities(model)`.
-- `%Trinity.LLM.Request{}`: system, messages (our `messages` rows → provider format), tools (JSON schema list), model, params.
-- Event stream shape (single source of truth for 012/013): `{:text_delta, s}`, `{:tool_call_start, id, name}`, `{:tool_call_delta, id, json_chunk}`, `{:tool_call_end, id, args}`, `{:usage, map}`, `{:done, reason}`, `{:error, reason}`.
-- req_llm adapter: map our request to `ReqLLM.stream_text/3`, `generate_text/3`, `generate_object/4`; map providers `anthropic`, `openai`, `openrouter`, `google`, and `openai_compatible` (Ollama/LM Studio via base_url).
-- Model registry: config list of `{id, provider, model, caps, price}`; `Trinity.LLM.models/0`, `default_model/0`.
-- Retry/fallback: transient errors retried with backoff (config); optional fallback model list.
-- Prompt caching hint pass-through for Anthropic.
-- Usage normalisation → `usage_events` insert (table defined here; ledger UI in 090).
-- Mox: `Trinity.LLM.ProviderMock`; `test/support/fake_provider.ex` that streams a scripted response with tool calls.
-- Live tests `@tag :live` reading keys from env; excluded by default.
-**Out:**
-- Cost budgets/UI (090), vision/audio inputs (later slice), provider OAuth flows.
-
-## Design notes
-- `emit_fn` is called in the caller's Task; the Session subscribes via messages, not callbacks (012 decides the plumbing; expose both `stream/3` with fn and `stream_to/3` with pid).
-- Keys read from `Trinity.Config.secret/1` (env now; keychain in 100).
-
-## Deliverables
-- `lib/trinity/llm/{provider,request,event,registry,retry}.ex`, `lib/trinity/llm/providers/req_llm.ex`, `lib/trinity/llm.ex`, migration `usage_events`, tests, `VERSIONS.md` (req_llm ✅ with version).
-
-## Acceptance criteria
-1. [auto] FakeProvider-driven test shows a full event sequence: text deltas → tool_call events → usage → done.
-2. [manual] `generate_object/3` returns a validated map for a given JSON schema (fake) and (live-tagged) for one real provider.
-3. [auto] `embed/2` returns vectors of the declared dimension (fake + live).
-4. [auto] Transient error → retried N times then `{:error, _}`; non-transient → immediate error (tests).
-5. [auto] A `usage_events` row is inserted per completed call with tokens and cost computed from the registry price.
-6. [manual] `mix test --only live` passes against at least one configured provider on the developer machine (output pasted; keys redacted).
-7. [auto] Switching `default_model` in config changes the provider used with no code change (test).
-
-## Proof required
-- Test outputs, live test output (redacted), a sample `usage_events` row.
-
-## Manual verification queue
-Every `[manual]` criterion below needs a person. Listed here so the owner sees the queue at G1 rather
-than at review time.
-- **AC2**: `generate_object/3` returns a validated map for a given JSON schema (fake) and (live-tagged) for one real provider.
-- **AC6**: `mix test --only live` passes against at least one configured provider on the developer machine (output pasted; keys redacted).
-
-## Definition of Done
-- [ ] gate green · [ ] AC1–7 proven · [ ] VERSIONS updated · [ ] ROADMAP → done · [ ] commit + tag
-
-## Commit & tag
-`feat(s011): complete slice 011 (LLM provider layer)` · tag `slice/011`
-
-## Risks / open questions
-- req_llm event shapes may differ per provider; the normalisation layer is the contract: test it per provider in live tests.
diff --git a/slices/012-session-process-agent-loop/NOTES.md b/slices/012-session-process-agent-loop/NOTES.md
deleted file mode 100644
index de25078..0000000
--- a/slices/012-session-process-agent-loop/NOTES.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Slice 012: NOTES
-
-## The ADR-0009 design checkpoint, measured 2026-09-20 before any code
-
-Packages read from their Hex tarballs in a scratch directory, never added to the tree: `jido` 2.3.3, `jido_action`
-2.3.2, `jido_signal` 2.2.0 (`mix hex.package fetch --unpack`).
-
-| Measurement the ADR names | Result | Derived by |
-|---|---|---|
-| (a) M3 and M4 assertable as tests against Jido's structures | M4 yes: a `Jido.Action` is a compile-time module with a param schema and `run/2`, so a compile-time catalog is a list of modules. M3 only with a census: `Jido.Exec.run/4` takes `timeout` and `max_retries` as call-site options, so "the loop takes no cap argument" holds only if Trinity's wrapper is `Jido.Exec`'s sole caller | `grep -n 'def run(' jido_action/lib/jido_action/exec.ex` → `run(action, params, context, opts)`; `grep -n '@callback' jido_action/lib/jido_action.ex` |
-| (b) what `Jido.Agent` adds over the `gen_statem` | Sensors, a cron scheduler, signal routing, worker pools, and an `AgentServer` executing `RunInstruction` directives through `Jido.Exec`: a second execution path for tools beside `Trinity.Effects`, the bypass shape 024's census flags. Nothing the Session's six states, persist-before-broadcast rule or rehydrate need | `ls jido/lib/jido` (sensor, scheduler, agent_server, pod); `grep -n 'defmodule ' jido/lib/jido/agent/directive.ex` (Emit, Spawn, SpawnAgent, AdoptChild, StopChild, StartSensor, StopSensor, Schedule, RunInstruction, Stop, Error) |
-| (c) dependency weight | `jido`: 29,820 lines, ten runtime dependencies (`jido_action`, `jido_signal`, `poolboy`, `crontab`, `time_zone_info`, `telemetry_metrics`, `nimble_options`, `splode`, `telemetry`; `jido_signal` adds `msgpax`, `memento`, `fuse`, `uniq`, `phoenix_pubsub`, `zoi`). `jido_action`: six (`jason`, `nimble_options`, `telemetry`, `zoi`, `splode`, `multigraph`), two of which the tree carries already | `find lib -name '*.ex' \| xargs cat \| wc -l`; `grep '{:' mix.exs` in each |
-
-**Recommended at G1: actions only.** **Owner decision at G1, 2026-09-20: no Jido at all.** The one piece with
-value, `Jido.Action` as the shape a tool is written in, is a few dozen lines to write and `jsv` (JSON Schema
-validation) is already in the tree through req_llm; six dependencies and a census for that gain is a poor
-trade, and the runtime was never a candidate on these numbers. Recorded as an appended decision on ADR-0009
-(superseding the provisional decision and the "runtime is Jido v2" line, which stands as written), in
-docs/02, in slice 020's spec (`Trinity.Tools.Tool` is Trinity's own behaviour) and in the standards register.
-The `jido` VERSIONS row stays, marked not used, so a reader finds the decision where the package would be.
-Slice 012 is unchanged: it used none of it.
-
-## G1 plan, 2026-09-20
-
-Tree at `ada3031` on `main` (010 and 011 approved); branch `slice/012-session-process-agent-loop`; ROADMAP row
-012 set to `in_progress` in this commit. Each line names its test; the order is the build order.
-
-1. `Trinity.Sessions.State` struct (session id, persona, history cursor, current turn: draft text, pending tool
- calls, task ref, usage) rebuilt from the DB on init; `Trinity.Sessions.Events` naming the seven PubSub event
- shapes on `session:`; `Trinity.Sessions.subscribe/1`. Test: every broadcast shape is one of the seven.
-2. `Trinity.Sessions.Supervisor` (DynamicSupervisor, `:one_for_one`, `max_restarts: 10, max_seconds: 60`),
- `Trinity.Registry` (`:unique`, keyed by session id), both in the application tree; `start_session/1`,
- `ensure_started/1` (idempotent, rehydrates), `state/1`, `whereis/1`. Test: two `ensure_started` calls return
- one pid; a stopped session restarts on demand.
-3. `Trinity.Sessions.Prompt.build/2`, pure: system (persona stub, memory stub) plus history in seq order into a
- `Trinity.LLM.Request`. Test: a fixed history builds a fixed request; the function has no side effect.
-4. `Trinity.Sessions.Session` `gen_statem` (`handle_event_function`): `idle`, `thinking`, `tool_wait`,
- `approval_wait` (stub, unreachable until 021), `compacting` (stub), `error`. A user message is persisted
- (`Sessions.append_message/2`), broadcast, then the turn starts: `Trinity.LLM.stream_to/3` under the
- session's own `Task.Supervisor` (`Trinity.Sessions.TurnTaskSupervisor`, per session, monitored; never a call
- into the Session from its Task). Events fold into the draft; `{:llm_done, ref, {:ok, usage}}` persists the
- assistant message with usage and broadcasts it, persist before broadcast; then `idle`. Test (AC1): 50 fake
- deltas, coalesced broadcasts then `{:assistant_message, m}`, DB rows with seq 1 and 2 and usage.
-5. Draft persistence: partial text written as a draft message (`parts.draft = true`) every 500 ms or 2 KB,
- whichever first, and finalised at done (the same row updated: the one edit the append-only rule allows,
- stated in docs/05 as a draft becoming final). Rehydrate: a draft left behind is marked `interrupted` and
- `{:turn_interrupted, m}` broadcast; no auto-resume. Test (AC2, crash test A): kill the Session mid-stream,
- supervisor restarts it, `ensure_started/1` returns a new pid, history intact, the draft interrupted, the
- event received; a second session running at the same time is unaffected.
-6. Task failure: the streaming Task raises, the Session enters `error`, persists an error message, returns to
- `idle`, accepts the next message (AC3). `cancel_turn/1`: kills the Task, persists the partial text as
- interrupted, idle within 100 ms (AC4).
-7. Tool-call path: `{:tool_call_end, id, args}` events collect into pending calls; at `{:done, :tool_calls}` the
- Session enters `tool_wait` and runs `Trinity.Sessions.ToolRunner` (a behaviour with one stub implementation
- returning `{:error, :no_tools}`; 020 replaces it) in a Task; each result is a `tool` message (seq, content,
- tool_call_id); then a second turn for the final assistant text (AC6). One follow-up turn at this slice; the
- loop cap is line 9.
-8. Backpressure: deltas coalesced by a 50 ms timer into at most 20 broadcasts a second; the full text intact
- (AC7 with 1,000 deltas in a second).
-9. M3, code-owned caps: `Trinity.Sessions.Caps` with `@max_turns_per_message`, `@max_tokens_per_message` and
- `@max_wall_ms` as module attributes; the loop function takes no cap argument (a test asserts the signature
- and that no config key exists to raise them); reaching a cap is a normal transition to `idle` with a
- recorded outcome (a query-receipt placeholder until 024: a `provider_meta.cap_reached` on the message).
-10. Idle: hibernate after `idle_hibernate_ms`, stop after `idle_stop_ms` (config, short in test); `ensure_started/1`
- restarts on demand (AC8, observed through `Process.info(pid, :current_function)` and a memory drop).
-11. `Trinity.CorePolicy.hash/0`: a digest over the object code of the policy modules (at this slice: `Session`,
- `Caps`, `ToolRunner`, `Sentinel`), a module-attribute list 024 extends; the boot receipt is 024's, so at
- 012 the hash is exposed and tested. AC9: kill and reseed twice, exactly one live worker in the registry,
- the hash equal before and after, and a `state/1` read shows no grant, approval or pending tool call
- survived (the pending list is rebuilt empty; grants have no home before 021).
-12. `Trinity.Sessions.Sentinel.preflight/1` on model output: flags "already executed" claims, boundary-bypass
- phrasing and loop abuse (a fixed pattern list, stated as a tripwire); findings recorded on the assistant
- message's `provider_meta.sentinel` and can only tighten (a finding sets the turn's outcome to `hold`, never
- loosens anything). Test: each pattern fires; ordinary text does not; a finding never removes a hold.
-13. AC5: 100 sessions started at once, one fake turn each, no supervisor restart, gapless seq (the restart
- count read from the supervisor before and after).
-14. docs/01 tree synced (the Registry, Sessions.Supervisor and per-session Task.Supervisor exist; LLM.Supervisor
- still does not); ADR-0009 appended with the checkpoint above; VERSIONS row `jido` becomes `jido_action`.
-15. Gate, coverage row, PROOF.md, ROADMAP to `done`, pull request, tag.
-
-Manual verification queue: none. Every criterion is `[auto]`.
-
-Deviations from SLICE.md, stated before building: `Trinity.CorePolicy.hash/0` is introduced here rather than
-at 024 because AC9 names it; 024 extends its module list and adds the boot receipt. The recorded outcome of a
-cap and a sentinel finding lives on the message's `provider_meta` until 024's receipts exist. `approval_wait`
-and `compacting` are states with no inbound transition at this slice, present so the machine's shape is
-complete and a test asserts they are unreachable rather than pretending they work.
-
-## Lines 1 to 14, 2026-09-20: what was built, and what building it found
-
-**Built.** `Trinity.Sessions.Events` (seven shapes, `valid?/1`, `broadcast/2` refusing any other);
-`Trinity.Sessions.State` (in-memory turn only; `new_turn/0` is what a reseed starts from);
-`Trinity.Sessions.Supervisor` (DynamicSupervisor, `:one_for_one`, 10 restarts a minute, `:transient` children so
-an idle stop is not a restart); `Trinity.Registry` (`:unique` by session id); `Trinity.Sessions.Prompt.build/3`
-(pure); `Trinity.Sessions.Session` (`gen_statem`, `handle_event_function` with `state_enter`, six states);
-`Trinity.Sessions.ToolRunner` (behaviour, `Stub` answering `{:error, :no_tools}`, implementation from config);
-`Trinity.Sessions.Caps` (three module attributes, `check/1` takes only the turn); `Trinity.Sessions.Sentinel`
-(three families, merge never removes, outcome only tightens); `Trinity.CorePolicy.hash/0` (SHA-256 over the
-object code of five named modules); the `Sessions` API (`start_session/1`, `ensure_started/1`, `whereis/1`,
-`send_user_message/2`, `cancel_turn/1`, `state/1`, `subscribe/1`); `Trinity.SessionCase` and a fake provider
-with global state and script sequences.
-
-**The turn, as built.** A user message is a row, then a broadcast, then `thinking`: the model call runs in a
-Task under a `Task.Supervisor` the Session starts and links (so it dies with the Session and its Tasks with it),
-talking back only by message. Deltas fold into a buffer flushed by a 50 ms timer (at most 20 broadcasts a
-second) and into a draft row written every 500 ms or 2 KB. At `{:llm_done, ref, {:ok, usage}}` the draft is
-finalised (the one edit docs/05 now names) or the row inserted, the sentinel runs over the text and the
-pending calls, and the message is broadcast; with pending tool calls and a finish of `:tool_calls` the Session
-enters `tool_wait`, runs every call through `ToolRunner` in a Task, writes a `tool` row per result, checks
-`Caps`, and starts the next turn. A Task crash or a provider error is an `error` turn: the partial text is the
-row, `parts.error` names the reason, `error` is entered and left at once for `idle`. Cancel kills the Task
-and persists the partial text as interrupted. Idle arms two generic timeouts, hibernate and stop, cancelled on
-leaving idle. Rehydrate marks a draft interrupted and broadcasts it, and never resumes.
-
-**Found while building, each recorded rather than smoothed.**
-
-1. **A name collision with an approved slice.** 010 named the Ecto schema `Trinity.Sessions.Session`, and
- docs/01 gives that name to the process. The architecture wins: the schema is now `Trinity.Sessions.SessionRow`
- (a fix commit referencing 010 inside this slice; its moduledoc says why the suffix). The `has_many :messages`
- association then needed its foreign key named, because Ecto derives it from the new module name.
-2. **The fake provider's state was process-local**, and a session's Task is not on the test's `$callers` chain,
- so scripts and failures set by a test were invisible to the turn and every test saw the default script, a tool
- call, looping to the cap. The fake now keeps global state (a persistent term, cleared in setup; these tests
- are not async) and takes a sequence of scripts consumed one per call.
-3. **The factory persona named a model that is not a registry id** (`"fake:model"`), so every turn was an
- `unknown_model` error until the persona's model became nil (the registry default). Visible only by driving a
- turn by hand outside ExUnit and reading the events.
-4. **`collect` stopped at the first `{:state, :idle}`**, the enter broadcast from init, before the turn began;
- `start_drained/1` in the case template drains it.
-5. **A hibernating `gen_statem` reports `{:gen_statem, :loop_hibernate, 3}`** as its current function on this
- OTP, not `{:erlang, :hibernate, 3}`; the test accepts either and also asserts an empty mailbox.
-6. **A row from a hand-driven probe (`mix run` on the test database) survived** outside the sandbox's rollback
- and broke a 010 list test that assumed an empty table; the test now scopes to the rows it made, and the test
- database was reset.
-7. **Outcomes live in `parts`, not `provider_meta`**: `interrupted`, `error`, `cap` and `tool_calls` are about the
- message's shape; `provider_meta` keeps the sentinel's findings, the outcome and the finish reason.
-
-**Deviations from SLICE.md**, in addition to the three stated at G1: the per-session `Task.Supervisor` is started
-and linked by the Session rather than being a named child in the tree, so the tree's one name per session is
-the Session itself; `Trinity.Sessions.Supervisor` has no restart counter to read (DynamicSupervisor exposes
-none), so AC5 asserts the stronger thing, that every session's pid is the same after the run as before.
-
-```
-$ mix test test/trinity/sessions → 17 passed (session, crash, many_sessions, units)
-$ mix gate → exit 0; 155 passed, 10 excluded; plan_check: PASS
-$ mix test --cover → 60.82% total (Session 84.39%, Caps, Sentinel, Events, CorePolicy 100%)
-```
-
-## Follow-ups
-- `approval_wait` and `compacting` have no inbound transition; 021 and 023 add them.
-- The recorded outcome of a cap or a sentinel hold moves to receipts at 024.
-- `Trinity.LLM.Supervisor` (rate limiters) still unbuilt; nothing needs it.
diff --git a/slices/012-session-process-agent-loop/PROOF.md b/slices/012-session-process-agent-loop/PROOF.md
deleted file mode 100644
index 0c3bad5..0000000
--- a/slices/012-session-process-agent-loop/PROOF.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# Proof for slice 012: Session process + agent loop
-
-Agent: Trinity · Coding Agent · Date: 2026-09-20 · Branch: slice/012-session-process-agent-loop · Final commit: `9b52f56` (filled by the commit after it)
-
-## Summary
-One supervised `gen_statem` per conversation runs the turn loop: a user row, a model call in a Task, deltas
-coalesced and drafted to a row, tool calls through a stub runner, a final assistant row, idle. The two crash
-tests are the slice's headline: a kill mid-stream restarts the process from the database with the draft marked
-interrupted and a neighbour untouched; two kills and reseeds leave one live worker, the core policy hash unchanged
-and nothing of the dead process's turn. Hard parts, all in NOTES.md: a name collision with 010's schema (renamed
-to `SessionRow`, a fix referencing 010), a fake provider whose process-local state a session's Task could not see,
-and a factory persona naming a model that was not a registry id. The ADR-0009 checkpoint ran at G1 with three
-measurements and the owner decided: no Jido at all.
-
-## Gate
-```
-$ mix gate (this machine, OTP 28.5.0.5, Elixir 1.20.4, under a 32 GiB cgroup)
-515 mods/funs, found no issues.
-No vulnerabilities found.
-Result: 155 passed, 10 excluded
-trinity.coverage: 011 51.57% vs 010 44.88%: OK
-plan_check: PASS
-exit=0
-```
-
-## Tests
-```
-$ mix test --cover
-Result: 155 passed, 10 excluded
-| 60.82% | Total |
-| 84.39% | Trinity.Sessions.Session |
-| 100.00% | Trinity.Sessions.Caps, Sentinel, Events; Trinity.CorePolicy |
-```
-`coverage.tsv` row: `012 60.82 019d9cf 2026-09-20`. `trinity.coverage: 012 60.82% vs 011 51.57%: OK`.
-
-The seventeen tests of `test/trinity/sessions`, with timings from `--trace`:
-```
-test 100 concurrent sessions each complete a turn with the same pid throughout and seq 1, 2 (105.4ms)
-test a failing stream (AC3) a Task that raises is an error turn too (6.3ms)
-test a failing stream (AC3) the Session enters error, persists an error message, returns to idle and takes the next message (4.2ms)
-test a turn (AC1) 50 deltas arrive coalesced, then the assistant message; two rows with seq and usage (2.3ms)
-test a turn (AC1) a busy session refuses a second message by name (504.5ms)
-test backpressure (AC7) 1,000 deltas in well under a second reach the subscriber as few broadcasts with the text intact (2.6ms)
-test cancel (AC4) cancel during streaming persists the partial text as interrupted and is idle within 100 ms (54.3ms)
-test Caps (M3) each cap fires in order and a fresh turn passes (2.6ms)
-test Caps (M3) the caps are module attributes and the check takes only the turn (4.4ms)
-test CorePolicy.hash/0 is a 64-hex sha-256 over the named modules, stable across calls (1.9ms)
-test crash test A: a kill mid-stream restarts the session, keeps history, marks the draft interrupted, spares a neighbour (AC2) (775.2ms)
-test Events the seven shapes and nothing else; broadcast refuses a foreign shape (10.6ms)
-test idle (AC8) the process hibernates after the configured idle time and restarts on demand (302.8ms)
-test kill and reseed twice: one live worker, the core policy hash unchanged, no grant, approval or pending call survives (AC9) (46.9ms)
-test Prompt.build/3 is pure: the same inputs build the same request, with the persona's soul as the system prompt (2.9ms)
-test Sentinel each family fires; ordinary text does not; findings only accumulate (1.5ms)
-test the tool path (AC6) a tool call enters tool_wait, the stub answers with an error, a tool row is written, a final message follows (5.6ms)
-```
-
-## Acceptance criteria evidence
-
-### AC1: 50 deltas, coalesced deltas then {:assistant_message, m}; DB has user and assistant rows with seq and usage
-`a turn (AC1) 50 deltas arrive coalesced, then the assistant message; two rows with seq and usage`: the fake streams
-50 deltas of `"ab "`; the subscriber receives fewer than 50 `assistant_delta` broadcasts whose join is the full
-text, then `{:assistant_message, %Message{seq: 2, usage: %{"input_tokens" => 50, "output_tokens" => 50}}}`;
-`history/1` returns seq `[1, 2]`; the Session is idle with an empty pending list. Every event passes
-`Events.valid?/1`.
-
-### AC2: crash test A
-`crash test A: a kill mid-stream restarts the session, keeps history, marks the draft interrupted, spares a neighbour`:
-two sessions stream a slow script; session A has a draft row by the time of `Process.exit(pid, :kill)`; the
-supervisor restarts it (a new pid, `ensure_started/1` returns that pid); the rehydrate broadcasts
-`{:turn_interrupted, %Message{id: , parts: %{"interrupted" => true, "draft" => false}}}`; history is
-`["user", "assistant"]` with the draft text; the new process is idle with no pending calls; session B keeps its pid
-and its `thinking` state.
-
-### AC3: crash test B
-`a failing stream (AC3) the Session enters error, persists an error message, returns to idle and takes the next
-message` (a permanent provider error) and `a Task that raises is an error turn too` (the fake raises inside the
-Task; the Session sees `:DOWN`): states include `:error`; the assistant row carries the partial text and
-`parts.error` naming the reason; the next message is accepted and completes.
-
-### AC4: cancel_turn/1 during streaming
-`cancel (AC4) cancel during streaming persists the partial text as interrupted and is idle within 100 ms`: measured
-with `:timer.tc`, under 100 ms; `{:turn_interrupted, %Message{content: "start ", parts: %{"interrupted" => true}}}`;
-a second cancel in idle returns `{:error, :idle}`.
-
-### AC5: 100 sessions concurrently, no supervisor restarts, gapless seq
-`100 concurrent sessions each complete a turn with the same pid throughout and seq 1, 2`: 100 rows, 100 processes,
-100 turns through `Task.async_stream` at concurrency 100; every session finishes; every pid after equals the pid
-before (DynamicSupervisor exposes no restart counter, so the pid identity is the measurement); every session's
-seqs are `[1, 2]`.
-
-### AC6: the tool-call path
-`the tool path (AC6) ...`: the default script ends in a tool call; states include `:tool_wait`;
-`{:tool_call, %{id: "call_1", name: "get_weather"}}` is broadcast; history is `["user", "assistant", "tool",
-"assistant"]` with seq `[1, 2, 3, 4]`; the tool row carries `tool_call_id "call_1"`, content naming `no_tools`
-and `parts.ok == false`; the first assistant row's `parts.tool_calls` carries the call with its arguments.
-
-### AC7: backpressure
-`backpressure (AC7) 1,000 deltas ...`: 1,000 single-character deltas; at most 25 delta broadcasts (asserted
-`<= 25`); their join and the final row's content are the full 1,000 characters.
-
-### AC8: idle hibernation
-`idle (AC8) the process hibernates after the configured idle time and restarts on demand`: with
-`idle_hibernate_ms: 200` in test config, after 300 ms `Process.info(pid, :current_function)` is
-`{:gen_statem, :loop_hibernate, 3}` (what a hibernating gen_statem reports on this OTP) with an empty mailbox;
-`ensure_started/1` returns the same pid; a message wakes it.
-
-### AC9: kill and reseed twice
-`kill and reseed twice: one live worker, the core policy hash unchanged, no grant, approval or pending call survives`:
-with a pending tool call in process state, two kills each restart the session; after each, `state/1` shows
-`idle`, `pending: []`, `turns: 0`, `draft_id: nil`; the registry holds exactly one pid for the id and the
-supervisor exactly one live child for it; `Trinity.CorePolicy.hash/0` is the same 64-hex string before and after.
-Grants have no home before slice 021, and the state view is the census that none is in process state.
-
-### M3 (platform alignment): code-owned caps
-`Caps (M3) the caps are module attributes and the check takes only the turn`: `Caps.check/1` exists and
-`Caps.check/2` does not; no key under the `:trinity` application environment names a cap; each cap fires in order.
-
-## Manual verification for the reviewer
-None. Every criterion is `[auto]`.
-
-## Deviations from SLICE.md
-See NOTES.md: `CorePolicy.hash/0` arrives here; outcomes on `parts` and `provider_meta` until 024's receipts;
-`approval_wait` and `compacting` present and unreachable; the per-session Task supervisor is started by the Session
-rather than named in the tree; AC5 asserts pid identity instead of a restart count. And the ADR-0009 checkpoint
-outcome, decided by the owner at G1: no Jido.
-
-## Versions touched
-`VERSIONS.md` updated: yes, the `jido` row now reads not used (ADR-0009, decided 2026-09-20). No pin moved.
-
-## Git
-```
-$ git log --oneline main..HEAD
-be6e70d feat(s012): the session process and the agent loop
-019d9cf docs(s012): ADR-0009 decided at the checkpoint: no Jido at all
-6dffc53 docs(s012): G1 plan with the ADR-0009 checkpoint measured, and the slice opens
-9b52f56 feat(s012): complete slice 012 (session process and agent loop)
-```
diff --git a/slices/012-session-process-agent-loop/SLICE.md b/slices/012-session-process-agent-loop/SLICE.md
deleted file mode 100644
index 9f25c21..0000000
--- a/slices/012-session-process-agent-loop/SLICE.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Slice 012: Session process + agent loop
-
-| Field | Value |
-|---|---|
-| Phase | 1 Core loop |
-| Milestone | M1 Talks |
-| Size | L |
-| Depends on | 010, 011 |
-
-## Goal
-One supervised `gen_statem` per conversation implementing the turn loop (prompt build → stream → tool-call
-dispatch stub → persist → idle), broadcasting events on PubSub, rehydrating from the DB on restart, and surviving
-a kill mid-turn without data loss. This is the heart of the system and the first proof of the crash-safety property.
-
-## Why
-Vision goals 1 and 2. Silent process death and the one-agent-per-machine limitation are both answered here.
-
-## Scope
-**In:**
-- `Trinity.Sessions.Supervisor` (DynamicSupervisor) + `Trinity.Registry` keyed by session id.
-- `Trinity.Sessions.Session` gen_statem with states: `idle`, `thinking`, `tool_wait`, `approval_wait` (stub), `compacting` (stub), `error`.
-- `Trinity.Sessions.start_session/1`, `ensure_started/1` (idempotent, rehydrates), `send_user_message/2`, `cancel_turn/1`, `state/1`, `subscribe/1`.
-- Turn pipeline: `Trinity.Sessions.Prompt.build/2` (system = persona stub + memory stub + history) → `Trinity.LLM.stream_to/3` in a Task under the session's `Task.Supervisor` → event handling → persist assistant message (parts + usage) → broadcast → idle.
-- Tool-call handling: parse tool calls into pending list; execution is a stub behaviour `Trinity.Sessions.ToolRunner` that 020 replaces (returns `{:error, :no_tools}` now): the state machine path must be complete.
-- Persistence-before-broadcast rule; partial assistant text persisted every N chunks or M ms as a draft message (`parts.draft: true`), finalised at done.
-- Rehydrate: on init, load session + history; if a draft exists, mark it interrupted and broadcast `{:turn_interrupted, ...}`; do not auto-resume (a resume policy is a later slice).
-- Idle timeout: hibernate after X min; stop after Y (config); `ensure_started/1` restarts on demand.
-- Cancel: kills the streaming Task, persists partial text as interrupted.
-- PubSub events on `session:`: `{:user_message, m}`, `{:assistant_delta, text}`, `{:assistant_message, m}`, `{:tool_call, tc}`, `{:state, s}`, `{:turn_interrupted, m}`, `{:error, e}`.
-- Backpressure: delta coalescing to ≤ 20 broadcasts/s.
-**Out:**
-- Real tools (020), permissions (021), compaction (023), persona/memory content (030), UI (013).
-
-## Design notes
-- Use `:gen_statem` with `handle_event_function`; state data is `%Trinity.Sessions.State{}`.
-- Tasks are started with `Task.Supervisor.async_nolink`; the Session monitors them. A Task crash → `error` state with a persisted error message, then `idle`. A Session crash → supervisor restart → rehydrate.
-- Never `GenServer.call` into the Session from inside its own Tasks; use messages.
-- Keep `Prompt.build/2` pure and unit-tested; it grows in 030/040.
-
-## Deliverables
-- `lib/trinity/sessions/{supervisor,session,state,prompt,tool_runner,events}.ex`, `lib/trinity/sessions.ex` (extended), `lib/trinity/application.ex` tree updated, tests incl. crash tests, `docs/01-architecture.md` tree synced.
-
-## Acceptance criteria
-1. [auto] Send a message with FakeProvider streaming 50 deltas: subscriber receives coalesced deltas then `{:assistant_message, m}`; DB has user + assistant rows with correct `seq` and usage.
-2. [auto] **Crash test A:** `Process.exit(session_pid, :kill)` mid-stream → supervisor restarts it; `ensure_started/1` returns a new pid; history from DB intact; a draft assistant message exists marked `interrupted`; `{:turn_interrupted, _}` broadcast. No other session affected (run two concurrently).
-3. [auto] **Crash test B:** the streaming Task raises → Session enters `error`, persists an error message, returns to `idle`, and accepts the next message.
-4. [auto] `cancel_turn/1` during streaming → partial text persisted as interrupted; Session idle within 100 ms.
-5. [auto] 100 sessions started concurrently, each doing one turn with FakeProvider, complete with no supervisor restarts and gapless `seq` (extends the 010 stress test).
-6. [auto] Tool-call path: FakeProvider emits a tool call → Session enters `tool_wait` → ToolRunner stub returns error → Session records a tool message with the error and continues to a final assistant message.
-7. [auto] Backpressure: with a provider emitting 1,000 deltas in 1 s, subscriber receives ≤ ~25 delta broadcasts and the full text is intact.
-8. [auto] Idle hibernation observable (`Process.info(pid, :current_function)` or memory drop) after the configured timeout in a test with a short timeout.
-9. [auto] **Kill and reseed twice: exactly one live worker; `core_policy_hash` equal before and after; no grant, approval or pending tool call survives in process state.**
-
-## Proof required
-- Test names + output for each AC; a `:sys.get_state`-free assertion style except in crash tests; supervisor restart counts.
-
-## Manual verification queue
-None. Every acceptance criterion in this slice is `[auto]` and is proven by a command or a test.
-If that changes during the slice, the criterion is retagged and this section is filled at G1.
-
-## Definition of Done
-- [ ] gate green · [ ] AC1–9 proven · [ ] docs/01 tree updated · [ ] ROADMAP → done · [ ] commit + tag
-
-## Commit & tag
-`feat(s012): complete slice 012 (session process and agent loop)` · tag `slice/012`
-
-## Risks / open questions
-- Draft-persistence write frequency vs SQLite single writer: measure; default every 500 ms or 2 KB.
-- Decide whether `ensure_started/1` is called by UI on mount or lazily on first message (recommend: on mount).
-
-## Platform alignment (appended 2026-09-05)
-- **M3, code-owned caps:** iteration, token and wall-clock caps are module attributes on the loop module; the loop
- function takes no cap argument (a test asserts the arity/signature and that no config key can raise them).
- Reaching a cap is a recorded, receipted outcome and a normal `idle` transition, never a crash.
-- **M8, kill-and-reseed:** a reseeded Session is a new pid born from the immutable core policy hash
- (`Trinity.CorePolicy.hash/0`, a compile-time digest of the policy modules) and inherits no grant, approval,
- or pending tool call from process state; grants live only in the DB. This is AC9 in the list above.
-- **Sentinel hook:** a `Trinity.Sessions.Sentinel` preflight on model output that flags "already executed" claims,
- boundary-bypass phrasing, and loop abuse; findings are recorded and can only tighten (deny/hold), never loosen.
-- **ADR-0009 design checkpoint** lands in this slice's G1 plan: measure whether `Jido.Agent`/`Jido.Action` express
- the loop and the effect boundary; report the three measurements named in the ADR before coding.
diff --git a/slices/013-liveview-chat-streaming/NOTES.md b/slices/013-liveview-chat-streaming/NOTES.md
deleted file mode 100644
index 63f1c12..0000000
--- a/slices/013-liveview-chat-streaming/NOTES.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# Slice 013: NOTES
-
-## The renderer, measured 2026-09-20 before any code
-
-SLICE.md names `phoenix_streamdown` as the first candidate and `earmark` or `mdex` as the fallback, and asks
-for the measurement. Packages read from their Hex tarballs in a scratch directory, never added to the tree
-(`mix hex.package fetch --unpack`); the hex.pm API read with `curl -s
-https://hex.pm/api/packages/`.
-
-| Candidate | Result | Derived by |
-|---|---|---|
-| `phoenix_streamdown` 1.0.0-beta.4 | Still the latest release, dated 2026-05-03; `latest_stable_version` is null. 801 lines in four files. It depends on `mdex ~> 0.11` and does two things over it: completes unclosed markdown syntax per token (its `Remend` module) and splits the text into blocks so finished blocks sit under `phx-update="ignore"`. VERSIONS.md forbids a pre-release, and the owner has not accepted one. | the API's `releases[0]`; `find lib -name '*.ex' \| xargs cat \| wc -l`; `grep '{:' mix.exs` |
-| `earmark` 1.4.49 | **Retired on hex** (`retirement.reason: deprecated`, "Earmark is no longer maintained. Migrate to a replacement, for example MDEx") **and carries an open advisory**, EEF-CVE-2026-48591 (medium, stored XSS via unescaped HTML attribute values). Both gate steps refuse it: `mix hex.audit` prints "Found retired packages" and `mix deps.audit` the advisory. Not a candidate. | `mix hex.audit` in the scratch project, exit 0 with both findings printed; the API's `releases/1.4.49` `retirement` field |
-| `mdex` 0.13.5 | Stable, released 2026-07-29, 1.4 M downloads. Its own `streaming: true` option completes fragments (`MDEx.FragmentParser`: `"Some **bold te"` renders `bold te`, an unclosed fence closes), which is the half of streamdown that matters; the other half is a design rule SLICE.md already states. With `render: [unsafe: false]` raw HTML is omitted and a `javascript:` href is emptied; the default sanitizer (ammonia) on top drops the omission comments and adds `rel="noopener noreferrer"`. Cost: 6,420 bytes of mixed markdown render in 5.6 ms, 642 bytes in 0.7 ms (100 runs each, `:timer.tc`), so twenty renders a second of a long in-progress message is a tenth of a core. | the scratch `bench.exs`, output kept below |
-| `mdex_native` 0.2.8 (mdex's NIF) | A Rust NIF through `rustler_precompiled`: artifacts for eleven targets, among them the four the packaging chain builds for (x86_64 and aarch64 darwin, x86_64 and aarch64 linux-gnu, x86_64 windows msvc and gnu). This machine downloaded `libmdex_native_nif-v0.2.8-nif-2.15-x86_64-unknown-linux-gnu.so` at compile and `mix hex.audit` reports nothing on the set. Slice 001 proved the packaging chain with a pure-BEAM release; a NIF is new to it. | `mix compile` in the scratch project (the download line is in its log); `checksum-Elixir.MDExNative.Native.exs` in the package |
-
-```
-$ mix run bench.exs (scratch project, mdex 0.13.5)
-100 streaming renders of 6420 bytes: 563.142 ms total, 5.63142 ms each
-100 streaming renders of 642 bytes: 70.593 ms total, 0.70593 ms each
-```
-
-**Decision at G1: `mdex ~> 0.13`, and no `phoenix_streamdown`.** `earmark` is refused by the gate. The
-pre-release stays unpinned as VERSIONS.md requires; its two contributions are one `mdex` option and one rule this
-slice keeps anyway (completed messages rendered once, the in-progress message as one assign replaced whole). The
-NIF is the cost: the `package` workflow runs on the `mix.lock` change and builds on all three runner operating
-systems, which is the first measurement of a NIF in the bundle, cited in PROOF.md by run id. Open to the owner's
-veto at G1.
-
-## Design language, decided at G1
-
-SLICE.md decides the design language here and asks for tokens later slices consume. Dark by default with the
-three-way toggle kept (system, light, dark), clean rounded panels, and a font stack the tokens name once:
-`Ubuntu, Comfortaa, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`, with `"Ubuntu Mono", ui-monospace,
-SFMono-Regular, Menlo, Consolas, monospace` for code. **No font file ships at this slice**: Ubuntu is under the
-Ubuntu Font Licence 1.0 and Comfortaa under the OFL 1.1, and bundling either adds a licence file and a REUSE row
-to a tree meant for donation; slice 100 decides what the packaged app carries once its target list is fixed, and
-a web font is an egress this slice does not open (CLAUDE.md §8: every egress gets a redaction row). Until then a
-machine without Ubuntu or Comfortaa installed falls through to its system font, which is what the stack is for.
-Open to the owner's veto at G1: bundling Comfortaa (OFL 1.1) now is the alternative.
-
-The tokens live in `assets/css/app.css` as two daisyUI themes (`trinity-dark`, default; `trinity-light`) and a
-Tailwind `@theme` block (fonts, type scale, radius, spacing); `docs/03-conventions.md` gains a UI section naming
-them and the component vocabulary (`TrinityWeb.ChatComponents`) later surfaces take.
-
-## G1 plan, 2026-09-20
-
-Tree at `835485c` on `main` (012 approved); branch `slice/013-liveview-chat-streaming`; ROADMAP row 013 set to
-`in_progress` in this commit. Each line names its test; the order is the build order.
-
-1. `chore(s013): add mdex ~> 0.13` as its own commit; `lib/trinity/versions.ex` gains the row and marks
- `phoenix_streamdown` not used, `mix versions.gen` rewrites VERSIONS.md. Test: the gate's audit steps.
-2. `TrinityWeb.Markdown.to_html/2` (`streaming:` boolean): mdex with strikethrough, table, autolink and tasklist,
- `unsafe: false`, the default sanitizer. Test: raw HTML omitted; a `javascript:` href emptied; an unclosed fence
- completes when streaming; the same text renders the same HTML twice (pure).
-3. Design tokens in `assets/css/app.css` (above), the root layout defaulting to dark, `docs/03` UI section.
- Test: `mix assets.build` exits 0 (CI builds no assets; the run is pasted in PROOF.md).
-4. `Trinity.Sessions.default_persona/0` (the row named `default`, created on first use with no soul, so the
- prompt keeps its "You are Trinity." fallback; slice 030 seeds the SOUL into it), `Sessions.set_model/2`
- (writes `sessions.model`; refuses an id the registry does not know), and the Session re-reads its row at
- the start of every turn (one line in 012's `start_model_call`, so a model set between turns is the next
- turn's model). `Session.state/1` gains `text`, the in-progress draft, for a remount mid-stream. The fake
- provider records the last request it saw (`last_request/0`). Tests: set_model then a turn, the fake saw the
- new model (AC6's core); an unknown id refused; `state/1` shows the text mid-stream.
-5. Router: `live_session :chat` with `/` (`SessionLive.Index`) and `/s/:id` (`SessionLive.Show`); the scaffold
- home page, its controller and its test go, the index is the home. `TrinityWeb.Plugs.ContentSecurityPolicy`
- on the browser pipeline: a per-request nonce, `script-src 'self' 'nonce-…'`, the inline theme script and the
- LiveDashboard carry it; the slice-000 sobelow skip that named this slice as the owner is retired. Test: `GET /`
- answers with the header and the inline script carries the same nonce.
-6. `SessionLive.Index`: the session list (title or first user line, model, last activity), "New session" on
- the default persona, navigating to it. Test: a session is listed; "New session" creates a row and redirects.
-7. `SessionLive.Show` mount: subscribe, `ensure_started/1`, history into a `:messages` stream, the state view
- into `@status` and `@draft`; delta messages already queued when the state reply arrives are drained, because
- the reply's text contains them (the Session sends both, in order). Events: `user_message`,
- `assistant_message` and `turn_interrupted` insert into the stream and clear the draft; `assistant_delta`
- appends to the draft assign; `state` sets the status; `tool_call` adds a running tool card; `error` sets the
- banner. Tests: AC2 (send, deltas, the final message once in the DOM), AC5 (remount mid-stream: the draft so
- far, then the final message, no duplicate, no gap), AC4's test half (`Process.exit(session, :kill)`: the
- banner appears, the next message works).
-8. `TrinityWeb.ChatComponents`: `message` (role, time, usage badge, markdown), `tool_card` (name, args summary,
- status, expandable result), `composer` (textarea; Enter sends, Shift+Enter a newline, through a colocated
- hook; disabled unless idle), `model_picker` (`Trinity.LLM.models/0`), `cancel_button`, `interrupted_banner`
- with Retry (re-sends the last user message), `status_pill`. Tests: AC3's test half (cancel: the banner and the
- interrupted message), AC6 (the picker changes `sessions.model`).
-9. Keyboard: a `Shortcuts` hook on the window (`Ctrl/Cmd+K` new session, `Esc` cancel) pushing two events the
- LiveView already handles; no other key reaches the server. Test: the two events, driven directly.
-10. AC7: a test attaches to `[:phoenix, :live_view, :render, :stop]` for the Show process, streams 1,000 deltas
- and asserts at most 25 renders with the text intact; the count is recorded in PROOF.md.
-11. The fake provider in dev: `Trinity.LLM.Providers.Fake` moves from `test/support` to `lib/trinity/llm/providers`
- (it is the scripted provider, and the dev registry needs it); `TRINITY_FAKE_PROVIDER=1` in `config/runtime.exs`
- replaces the dev registry with the fake and gives it a markdown demo script with short sleeps. Tests unchanged.
-12. `mix sobelow --exit --skip` with no new finding; the CSP skip retired, the reasons file rekeyed if a line moved.
-13. docs/01 (the web layer at 013: the two LiveViews, the subscribe path); docs/06 gains the NIF-in-the-bundle risk
- row with its lift condition (the `package` run green on three operating systems).
-14. Screenshots under `proof/` from the running app with the fake provider, taken here where a browser exists;
- the real-provider run is the owner's.
-15. Gate, coverage row, PROOF.md, ROADMAP to `done`, pull request, tag.
-
-Manual verification queue, for the owner at G4:
-- **AC1**: `TRINITY_FAKE_PROVIDER=1 mix phx.server`, open `/`, New session, send "hello", watch the markdown stream
- in; then the same with a real provider (`set -a; . ./.env; set +a; mix phx.server`). Screenshot or GIF.
-- **AC3**: send a message with the fake's demo script (it streams for a few seconds), press Cancel or Esc mid-stream:
- the partial text stays, marked interrupted, with the banner and Retry. Screenshot.
-- **AC4**: with the page open mid-stream, in `iex -S mix phx.server`: `Process.exit(Trinity.Sessions.whereis(id), :kill)`.
- The banner appears without a reload; the next message works. Screenshot.
-
-Deviations from SLICE.md, stated before building: the renderer is `mdex`, not `phoenix_streamdown` (measured
-above). The fake provider moves into `lib/` so dev can run it (the slice asks for a dev config flag, which needs the
-module compiled in dev). Two small changes land in 012's modules (`start_model_call` re-reads the row; `state/1`
-carries the draft text), each a `fix(s012)`-style line inside this slice's commits, because AC5 and AC6 cannot be
-met without them. The CSP arrives here because the slice-000 skip named 013 as its owner.
-
-## Lines 1 to 14, 2026-09-20: what was built, and what building it found
-
-**Built.** `mdex ~> 0.13` (its own commit); `TrinityWeb.Markdown` (one `to_html/2`, the tree's only `raw/1`);
-the tokens in `assets/css/app.css` (two daisyUI themes, dark the default, a `@theme` block for fonts, the two
-chat text sizes and the radii) with the UI section in docs/03; `TrinityWeb.ChatComponents` (`message`,
-`tool_card`, `draft`, `composer`, `model_picker`, `status_pill`, `banner`, `local_time`); `Layouts.app` as the
-shell (top bar with the brand, a `:bar` slot, the theme toggle); `SessionLive.Index` and `SessionLive.Show`;
-four hooks in `assets/js/hooks.js`; `TrinityWeb.Plugs.ContentSecurityPolicy`; `Sessions.default_persona/0`,
-`set_model/2`, `set_title/2`; `Session.state/1` with `text`; the Session re-reading its row per turn; the fake
-provider in `lib/` with `last_request/0`, a `:demo` script and the `TRINITY_FAKE_PROVIDER=1` flag.
-
-**Found while building, each recorded rather than smoothed.**
-
-1. **`earmark` is refused by the gate, not just unattractive.** The measurement meant to compare render cost
- found the package retired on hex with an open XSS advisory before a line of it ran (`mix hex.audit` in the
- scratch project). SLICE.md's fallback pair was really one candidate.
-2. **A stream container cannot hold the in-progress message.** The draft sat in a sibling below the
- `phx-update="stream"` list, which put it at the bottom of the viewport with a gap above it in the first
- screenshot. One scroll region now wraps the stream container and the draft.
-3. **The formatter reflowed a `whitespace-pre-wrap` element** across three lines, and the newline rendered as a
- blank line above every user message; caught in a screenshot, fixed with `phx-no-format` (its own commit).
-4. **Tool rows have no event.** 012 broadcasts `tool_call` at the start of a call and nothing when the row is
- written, so a page would never show a tool result until a reload. The page reads the rows past its last seen
- `seq` on every final or interrupted message; no new event shape, so `Events` is untouched.
-5. **A new incarnation says idle before it says interrupted.** With `state_enter`, the enter broadcast for `idle`
- precedes the internal rehydrate event, so a test waiting for `turn_interrupted` and then `state idle` waited
- five seconds for a message that had already passed. The kill test waits for the second only.
-6. **The Show page's status pill and the draft's pill shared an id**, which LiveViewTest refuses (duplicate id);
- the component takes an `id` now.
-7. **sobelow does not see a policy set by a plug.** It recognises only a header map given to
- `put_secure_browser_headers`, so the CSP finding stays reported with the plug in place; the skip is rekeyed
- (the plug moved the pipeline's line) with the reason naming the test that asserts the header.
-8. **`@sobelow_skip` on a function is an unused attribute to the compiler**, which `--warnings-as-errors` turns
- into a gate failure; `Trinity.Paths` had met the same at 001 and registers the attribute as persisted, and
- `TrinityWeb.Markdown` does the same.
-9. **The dev database was behind on migrations** (011's `usage_events`), so the first `mix phx.server` answered
- 503 with `PendingMigrationError`; `mix ecto.migrate` and a restart.
-10. **One uncaptured failure in eleven runs** of `mix test test/trinity_web` (the run right after commit
- `e33f5bc`: 20/21, the failing test's name lost to a `tail`); ten later runs of the same directory and six
- of `test/trinity_web/live` alone were green, the gate run was green. Recorded so a recurrence in CI has a
- place to land; the output goes here when it does.
-
-**Deviations from SLICE.md**, in addition to the four stated at G1: the screenshots for AC1, AC3 and AC4 were
-taken here, from the dev server driven by a headless chromium (`playwright-core` in a scratch directory, the
-browser from the machine's playwright cache), rather than left entirely to the owner; the manual queue stands,
-and the owner's own run is the one that counts. `Ctrl/Cmd+K` and `Esc` reach the server as two pushed events
-from one hook rather than `phx-window-keydown` bindings, so ordinary typing never round-trips. The `LocalTime`
-hook rewrites server-rendered UTC times in the viewer's zone, which the slice did not ask for and a desktop app
-cannot do without.
-
-```
-$ mix test test/trinity_web → 21 passed (markdown, csp, index, AC2 to AC7)
-$ mix gate → exit 0; 176 passed, 10 excluded; plan_check: PASS
-$ mix test --cover → 64.50% total (Show 81.58%, Index 91.67%, ChatComponents 67.74%, Markdown 80.00%, CSP 100%)
-$ mix credo --strict --all → 596 mods/funs, found no issues
-$ mix sobelow --exit --skip → exit 0, no finding
-```
-
-## Follow-ups
-- Slice 100 decides the font the packaged app ships (Ubuntu under UFL 1.0 or Comfortaa under OFL 1.1, each with
- its licence file and REUSE row); until then the stack falls through to the system font.
-- The `package` workflow run on this branch's `mix.lock` change is the first proof of a NIF in the bundle (R24);
- 100's cross-target build re-proves it per target.
-- A tool result has no event of its own (finding 4); 020 may add one to `Events` when tools are real, or keep the
- catch-up read, which costs one query per final message.
-- Syntax highlighting in code blocks: `mdex_native` ships a `lumis` variant; not taken here (a larger artifact for
- a nicety), open for 022 when tool output makes code blocks common.
-- `CoreComponents` is the generator's file at 17% coverage; the chat does not use its `input`, `table` or `list`.
- A later slice that needs them styles them with the tokens or removes them.
-
-## Corrections, 2026-09-20, after the gate on the final tree
-
-11. **The gate refused the inline sobelow skip once its file was tracked.** `test/sobelow_skips_test.exs` reads
- `git ls-files`, so the first green gate (markdown.ex untracked) could not see the `@sobelow_skip` whose
- comment block said the reason without the `# sobelow_skip reason:` marker the enforcer looks for. Fixed in
- `080c543`; distinct from finding 10, whose one failure was in `test/trinity_web` and stays uncaptured.
-
-Supersedes the coverage line in the block above: `mix test --cover` on the final code tree (`080c543`) reports
-**64.41%** total, the value `coverage.tsv` carries; 64.50% was the tree two commits earlier.
-
-## The package workflow, 2026-09-20, after the pull request opened
-
-The `package` workflow did not fire on the push (its `paths` filter did not match a new branch), so it was
-dispatched by hand: run 35518054546, then 35519205973 with the serve step taught to print `serve.log` on any
-non-200 (a `fix(s001)` line: the first run printed "HTTP 500" and nothing else). Three findings, each
-reproduced here on a fresh install of the locally built linux binary (`rm -rf ~/.local/share/.burrito`, a fresh
-`DATABASE_PATH`), which is the only kind of local run that means anything: Burrito reuses an extracted payload
-keyed by name and version, so the first local run of the new binary executed the payload from 2026-09-06 and
-answered 200 for the old home page.
-
-12. **The packaged binary never ran a migration** (`fix(s010)`, commit `1f677b0`). `skip_migrations?/0` keyed on
- `RELEASE_NAME`, which `bin/desktop` exports and the Burrito wrapper does not (it starts `erlexec` directly),
- so every table was missing and `/`, the first page to read one, answered 500 on all three operating
- systems. Now "skip under Mix". Measured after: four migrations logged, `/` 200 three times.
-13. **The mdex NIF does not load in the Linux bundle.** Burrito's Linux ERTS is a musl build (`make_triplet/1`
- appends `-musl`; `beam.smp`'s interpreter is a musl libc), and both precompiled artifacts fail in it: the
- gnu one and the musl one (`TARGET_ABI=musl`) each NEED `libgcc_s.so.1`, which resolves to the host's glibc
- copy and dies on `_dl_find_object: symbol not found`. `TrinityWeb.Markdown` then falls back to escaped text,
- so the chat works but shows `**bold**` literally (the screenshot from that run is kept out of `proof/`).
- R24 fired; its lift condition is not met by the precompiled route.
-14. **A source build for musl with Zig as the linker loads and renders.** `rustup target add
- x86_64-unknown-linux-musl`, then `cargo build --release --target x86_64-unknown-linux-musl
- --no-default-features --features nif_version_2_15` with `CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER` set
- to a two-line `zig cc -target x86_64-linux-musl` wrapper (Zig 0.16.0 is already pinned for Burrito): 23 s,
- NEEDED `libc.so` only, the same shape as the exqlite NIF Burrito itself cross-compiles. Swapped into the
- extracted payload, the packaged chat renders the real provider's answer as markdown, no `on_load` warning
- (screenshot `proof/ac1-final-openrouter-packaged.png`). Rust's own self-contained musl linking refused
- (`unable to find library -lgcc_s`), and the crate's `.cargo/config.toml` turns `crt-static` off for musl.
-
-**Open decision for the owner, before this slice closes.** Two ways to a packaged Linux binary that renders:
-(a) keep `mdex` and build its NIF from source in the linux packaging job: `rustler` joins the tree as a
-build-time dependency, the job installs the musl Rust target and links through Zig, `MDEX_NATIVE_BUILD=1` and
-`config :mdex_native, MDExNative.Native, target: "x86_64-unknown-linux-musl"` for that build only; macOS and
-Windows keep the precompiled artifacts, unproven there because the `package` workflow has failed at "Serves
-HTTP 200" on both since the slice/011 tag run (35512430836), before this slice, and now prints why; or (b) a
-pure-Elixir renderer: `earmark_parser` 1.4.46 (the AST half of earmark, not retired, no advisory as of today's
-`hex.audit`) with Trinity's own HTML emitter and fragment completion, no NIF anywhere, the renderer's
-measurement redone. My recommendation is (a): the measurement above is the proof, the cost is one build-time
-dependency and thirty seconds of the linux job, and (b) rewrites the renderer against a parser whose author
-retired its sibling. Until the owner decides, the pull request stays open with the migration fix on it; the
-required checks (`gate`, `postgres`) are green on every commit.
-
-## The decision, 2026-09-20: (a), keep mdex and build its NIF for musl
-
-Owner's words: "go with a and continue". Built in one commit: `rustler ~> 0.38` at build time; the three
-compile-time settings in `config/config.exs` (`MDEX_NATIVE_BUILD=1`, `TRINITY_NIF_TARGET`, the Zig linker
-`scripts/zig-cc-musl`), set by the package workflow's linux job alone; the musl target named in
-`rust-toolchain.toml`; `Trinity.Smoke` printing `TRINITY_SMOKE_MARKDOWN=ok` (exit 3 otherwise) and the
-workflow's smoke step reading it on every target; `TrinityWeb.Markdown` logging once at `:error` on a fallback.
-
-15. **The smoke Task lost a race it had always been winning.** Rendering one line inside the Task before the
- halt took long enough for `Kernel.CLI` to reach the plain argument `--smoke` and treat it as a file ("No
- file named --smoke", exit 1). The render now happens inside `Application.start/2`, in `children/1`, and the
- Task prints and halts at once as before. Three fresh-install smoke runs: `ok`, exit 0; with the gnu artifact
- swapped into the payload: `failed:%MDEx.DecodeError{}`, exit 3.
-
-16. **The macOS and Windows serve failures were the data-directory lock.** Run 35522642934 printed the reason
- the older runs had hidden: "is held by OS pid ... in desktop mode; refusing to start" on the launch after
- the smoke run. Slice 010 read liveness on Linux alone and treated any file as held elsewhere, and the smoke
- path halts without `terminate/2`, so the file it left refused every second launch on both platforms. Tried
- first: `System.stop/1` in the smoke path so the lock's terminate runs; it is asynchronous, and `Kernel.CLI`'s
- "No file named --smoke" (finding 15) won two runs in three. Kept: `System.halt/1` as slice 001 had it, and
- liveness read through `kill -0` and `tasklist` (`fix(s010)`, the commit before this record). Run **35523664194**: green
- on all three operating systems, `TRINITY_SMOKE_MARKDOWN=ok` on each, HTTP 200 in 1,526 ms (linux),
- 1,680 ms (macOS) and 1,761 ms (windows) from launch. The first green `package` run since the slice/011 tag.
-
-## After approval, 2026-09-20
-
-17. **The merge commit of this slice is unsigned, and it is under a protected tag.** `gh pr merge --merge
- --subject ...` with no `--body` let GitHub write the message; the slice/012 merge had carried the
- sign-off in its `--body`. plan_check rule 8 reads the whole history, so every gate failed from that commit
- on, and `main` cannot be rewritten. Resolution, in the open: rule 8 names `3db7a5ff…` as its one exemption
- with this reason, docs/03 gains the rule that a merge commit's body carries the sign-off, and the next
- merge is checked with `git log -1 --format=%B` before the tag. The mistake is mine and the record stays.
diff --git a/slices/013-liveview-chat-streaming/PROOF.md b/slices/013-liveview-chat-streaming/PROOF.md
deleted file mode 100644
index 4d19177..0000000
--- a/slices/013-liveview-chat-streaming/PROOF.md
+++ /dev/null
@@ -1,229 +0,0 @@
-# Proof for slice 013: LiveView chat UI with streaming
-
-Agent: Trinity · Coding Agent · Date: 2026-09-20 · Branch: slice/013-liveview-chat-streaming · Final commit: (the commit carrying this file; named in the closing correction below)
-
-## Summary
-A usable chat over slice 012's Session: a sessions index at `/`, the chat at `/s/:id`, the assistant's text
-streamed as markdown through one renderer (`mdex`, chosen by the measurement in NOTES.md after `earmark` turned
-out to be retired with an open XSS advisory and `phoenix_streamdown` still a beta), tool cards, a model picker,
-Cancel and Esc, an interrupted banner with Retry, `Ctrl/Cmd+K` for a new session, and a remount mid-stream that
-shows the draft so far with neither a gap nor a duplicate. The design language is decided here as tokens (two
-themes, dark by default; fonts, text sizes, radii) and a component vocabulary later surfaces take. Every browser
-response carries a Content-Security-Policy with a per-request nonce, the debt slice 000 left to this slice. The
-hard parts, all in NOTES.md: tool rows have no event of their own (the page reads past its last seen `seq`), a
-stream container cannot hold the in-progress message, the formatter reflowed a pre-wrap element, and the fake
-provider had to move into `lib/` for the dev flag the slice asks for.
-
-## Gate
-```
-$ mix gate (this machine, OTP 28.5.0.5, Elixir 1.20.4, under a 32 GiB cgroup, tree 080c543)
-596 mods/funs, found no issues.
-... SCAN COMPLETE ... (sobelow --exit --skip: no finding)
-No retired or security advisory packages found
-No vulnerabilities found.
-versions.verify: OK. 83 locked packages, none disagreeing with 47 pins
-versions.gen: VERSIONS.md already matches Trinity.Versions and mix.lock
-trinity.names: OK over 266 tracked files
-trinity.secrets.scan: OK over 266 files
-trinity.reuse: OK. Every commentable tracked file carries an SPDX header
-AC7: 6 renders of the page for 1,000 deltas
-Result: 176 passed, 10 excluded
-trinity.coverage: 012 60.82% vs 011 51.57%: OK
-plan_check: PASS
-exit=0
-```
-`mix credo --strict --all`: 596 mods/funs, found no issues. `mix assets.build`: exit 0 (tailwind 4.3.0, daisyUI
-5.5.20, esbuild; CI builds no assets, so this run is the record).
-
-## Tests
-```
-$ mix test --cover (tree 080c543)
-Result: 176 passed, 10 excluded
- | 17.07% | TrinityWeb.CoreComponents |
- | 50.00% | TrinityWeb.ErrorHTML |
- | 67.48% | TrinityWeb.ChatComponents |
- | 80.00% | TrinityWeb.Markdown |
- | 81.25% | Trinity.LLM.Providers.Fake |
- | 81.58% | TrinityWeb.SessionLive.Show |
- | 84.66% | Trinity.Sessions.Session |
- | 91.67% | TrinityWeb.SessionLive.Index |
- | 100.00% | TrinityWeb.Layouts |
- | 100.00% | TrinityWeb.Plugs.ContentSecurityPolicy |
- | 64.41% | Total |
-```
-`coverage.tsv` row: `013 64.41 080c543 2026-09-20` (from 60.82 at 012).
-
-The slice's tests, `mix test test/trinity_web test/trinity/sessions/model_and_persona_test.exs --trace`:
-```
-test GET / sets the policy and the theme script carries the same nonce (67.2ms)
-test two requests get two nonces (1.1ms)
-test raw HTML in the answer is omitted, block and inline (5.0ms)
-test a javascript: href is emptied and ordinary links get rel=noopener (2.8ms)
-test markdown structure renders: emphasis, code, list, table, fence (14.2ms)
-test streaming completes an unfinished fragment; a finished render is not touched (28.4ms)
-test the same text renders the same HTML twice (4.8ms)
-test the index lists sessions, and New session creates a row and opens it (4.2ms)
-test the index Ctrl/Cmd+K reaches the same event through the Shortcuts hook (2.5ms)
-test the index a missing session redirects home with a flash (2.1ms)
-test a turn (AC2) send, deltas update the draft, the final message appears once (538.0ms)
-test cancel (AC3, the test half) the partial text is kept as interrupted, the banner shows, Retry sends again (66.7ms)
-test cancel (AC3, the test half) Esc reaches cancel through the Shortcuts hook (59.5ms)
-test the Session dies under the page (AC4, the test half) the banner appears without a reload, the page stays usable, the next message works (769.3ms)
-test a remount mid-stream (AC5) history comes from the database, the draft so far from the process; nothing doubles or goes missing (813.5ms)
-test the model picker (AC6) changes sessions.model and the next turn uses it (7.7ms)
-test render count (AC7) 1,000 deltas in well under a second render the page at most 25 times
-test default_persona/0 creates the row named default once and returns the same row after (1.2ms)
-test set_model/2 (AC6's core) refuses an id the registry does not know, and the row is unchanged (0.5ms)
-test set_model/2 (AC6's core) refuses a session that does not exist (2.0ms)
-test set_model/2 (AC6's core) the next turn of a running session uses the model set between turns (31.0ms)
-test state/1 carries the in-progress text mid-stream the view holds what has arrived; idle it is empty (403.4ms)
-```
-The red for the Session change was committed first (`ce4b1f4`, "the next turn of a running session uses the
-model set between turns": `left: nil, right: "fake:embed"`) and the fix references it (`57aca61`).
-
-## Acceptance criteria evidence
-
-### AC1 [manual]: create session, send "hello", see the streamed markdown response (fake provider and a real provider), screenshot
-Taken here from the dev server driven by a headless chromium (NOTES.md, deviations); the owner's own run is the
-manual queue below.
-- Fake provider (`TRINITY_FAKE_PROVIDER=1 mix phx.server`, the `:demo` script): `proof/ac1-streaming-fake.png`
- (mid-stream: bold, list, an open fence completed, the cursor, status `thinking`, Cancel showing) and
- `proof/ac1-final-fake.png` (the whole answer: fence, table, blockquote, the `