Files
Robert Allan JamesandClaude Sonnet 5 7306872848
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Phase 5.7: archival close of FABRIC-3.5.md and FABRIC-3.6.md at v2.1.0
FABRIC-3.5.md's provisional "design phase closed, not yet archival" header
replaced with the real CLOSED/ARCHIVAL form per its own §XXVI.5 spec,
naming v2.1.0 -- prior status headers kept underneath, not deleted.

FABRIC-3.6.md gets the same treatment: a CLOSED/ARCHIVAL banner above the
START HERE section, which stays as historical record rather than being
removed. Neither closure triggers the FABRIC-0 -> -1 -> -2 -> -3
carry-forward chain (both are standalone topic documents) and neither
touches FABRIC-3.md, which remains open for its own topic.

The Tripod/kernel reshuffle is complete: Hermes moved into the kernel as
kernel-Hermes, the Tripod is Hera/Artemis/Hestia, tagged v2.1.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:53:57 -04:00

1813 lines
142 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FABRIC-3.6.md — the Tripod/kernel reshuffle: execution log
**Status: CLOSED/ARCHIVAL as of 2026-09-22, at tag `v2.1.0`.** All five phases (0–5) closed —
Category A strip, Hestia relocation/birth, kernel-Hermes build (allocator, arena, message
types), Stage-cutover of every FORTH-owned message type, the Category B strip (Hermes VM +
`messaging.4th` removed), the Isabelle pass, the documentation sweep, `make sbom`, the version
bump, and the merge to `master` + tag. **This document is closed; the reshuffle it tracked is
done, not merely planned.** Per §XXVI.5 (cited here, ruled in `FABRIC-3.5.md`, also closed at
this same tag): closing this document hands nothing to a successor and does not touch
`FABRIC-3.md`, which remains open and authoritative for its own topic (bare metal boot).
**The `START HERE` section immediately below is kept as historical record of how this
execution began — it describes a session about to start work, not the current state.** Do not
follow its "first action: task 0.0" instruction; every task it points at is closed. If future
work touches this fleet again (a Phase 8 PKI elevation entrypoint, further hardware bring-up,
etc.), it gets its own new document, not a reopening of this one — matching the discipline
`FABRIC-3.5.md`'s own close just followed.
---
> ## START HERE — session handoff (historical — reshuffle complete, see status above)
>
> **If you have just been told "go build it", read this section, then `FABRIC-3.5.md`'s §XLI,
> then start at task 0.0 below. Do not re-derive the design — it is settled.**
>
> **Where the code is.** This repo is **LithosAnanke, on the Gitea instance at
> `gitea.strshipos.com`, repo `admin/LithosAnanake`** — Captain Bob has the credentials.
> **Confirm you are in the right repo before anything else.** `git remote -v` must show
> `gitea.strshipos.com/admin/LithosAnanake`. **Nothing outside Gitea is this project.** (The
> handoff was authored in a container whose default working directory was a *different*, empty
> repo on an unrelated remote — if you ever see that, you are in the wrong place.) `master` is
> the sole production line. Work on branch **`claude/starshipos-tripod-kernel-reshuffle-itbjns`**
> (a clean fast-forward of `master` at `e56974e`; **documentation only — no reshuffle code has
> been written**. The branch head is the source of truth; do not trust a commit id quoted here).
>
> **The two documents.** `FABRIC-3.5.md` is the **design record and is authoritative** — every
> ruling, with its reasoning and evidence, §I–§XLIII. **This** document is the execution log.
> Rulings are cited here, never restated. **If a task proves a ruling wrong, record the finding
> here and amend `FABRIC-3.5.md` — never silently diverge.**
>
> **First action: task 0.0**, the three-ISA baseline. It is a gate, not a formality — see its
> own rationale below. **Nothing else starts until it is recorded.**
>
> **Five traps this project's own history proves are real. A fresh session will walk into these:**
>
> 1. **Silent failure is the dominant mode here.** `FABRIC-3.5.md` §XXXV.0 lists four
> independent instances. Most relevantly: *"a switch storm and a healthy idle REPL produce an
> identical serial log."* **A green boot is weak evidence.** Name the specific observation
> that proves a task worked, before running it.
> 2. **`grep` cannot establish capsule reachability.** Capsules are birthed by name from runtime
> strings. A reference count over `capsules/` and `src/` put the six live `init-l8-*`
> capsules at **zero references**; including `experiments/` found 18–19 each. §XXII.2. **Use
> the three routes, never grep alone.**
> 3. **`fleet_conserved` cannot see Stadium heat.** The two heat accountings are entirely
> decoupled (§XXXIX). A leaking message allocator leaves `fleet_conserved` reporting a serene
> `1`. **Stage B's evidence is the ledger plus `stadium_conserved()`, never `fleet_conserved`.**
> 4. **`dict_hash` *changing* is expected; `dict_hash` *diverging* across architectures is the
> stop condition.** §XXXIV.6. Do not "fix" a changed hash.
> 5. **Documentation in this repo drifts from the code — trust the code.** `.claude/CLAUDE.md`
> carried four stale claims; **all four were corrected 2026-09-19 (§XLIV) and it is now
> reliable.** Others are not: `capsules/MANIFEST.md` still describes block 4055 as a live
> "immutable ABI" that `FABRIC-2.md:2773` declared stale, and still misdescribes block 2049.
> `FABRIC-0.md` §25.7 lists resolved defects as open. **Where a document and the code
> disagree, the code wins — and record the drift rather than working around it.**
>
> **Blocked, and not to be worked around:** Phase 3 needs three decisions from Captain Bob —
> **B1** channels (one membership or negotiation), **B2** the `SK_SWITCH_MAX_SLOTS` ceiling of
> 16, **B4** the payload bound against `INPUT_BUFFER_SIZE` 1025. **All design is complete; these
> are rulings, not investigations.**
>
> **Do not:** create branches, stash, fix defects found in passing, bundle tasks into one
> commit, or start any task without explicit authorization. Captain Bob's Law,
> `.claude/CLAUDE.md`.
**Status: LIVING WORKING DOCUMENT, opened 2026-09-19.** This is the **execution** record for
the reshuffle designed in `FABRIC-3.5.md`. Work is tracked, annotated and closed here.
**Why this is a separate document, per the series' own rule.** `FABRIC-1.md` closed at **4,420
lines** for a stated reason: *"continuing to append here made the still-open work hard to find."*
`FABRIC-3.5.md` stands at **4,452 lines** with 40+ open punch items scattered among hundreds of
settled rulings — **it has crossed the same threshold, for the same reason.** Annotating a task
list inside it, commit by commit, would bury the design record it exists to be.
**The split of responsibility is strict, and stated because `FABRIC-3.5.md` §XXXI found that
documents in this series lose track of each other:**
| | `FABRIC-3.5.md` | `FABRIC-3.6.md` (this) |
|---|---|---|
| **Holds** | The design record — every ruling, its reasoning, its evidence | The work — tasks, results, dates, commits |
| **State** | Design phase closed; archival close at `v2.1.0` (§XXVI.5) | Living until the work is done |
| **On a conflict** | **Authoritative** | Defers, and records the discrepancy |
**Design rulings are never restated here, only cited** (`§XXXIV.2`, `§XL.4`, …). If a task needs
a rule explained, read `FABRIC-3.5.md`. **If executing a task proves a ruling wrong, that is a
finding: record it here and amend `FABRIC-3.5.md` there** — never silently diverge.
**The checkbox convention is deliberate.** `FABRIC-3.5.md` §XXXI.2 found the series'
carry-forward discipline was mechanically auditable (`grep -c '\- \[ \]'`) up to `FABRIC-2.md`,
and **broke at `FABRIC-3.md`, which uses no checkboxes at all** — after which "is anything still
open?" stopped being a `grep` and became a reading exercise. **This document restores the
convention**, so that question stays answerable by machine.
---
## Standing rules
Apply to **every** task, from `.claude/CLAUDE.md` and `FABRIC-3.5.md` §XXXIV:
- **One task, one commit.** No bundling.
- **Acceptance is the three-architecture QEMU boot** — `clean qemu`, amd64/aarch64/riscv64, one
at a time, in the foreground. Logs committed. There is no other acceptance test.
- **`dict_hash` changing is expected. `dict_hash` *diverging* between architectures is a stop
condition** (§XXXIV.6).
- **`mkcapsule --lint capsules/` clean** after any capsule change.
- **No task starts without explicit authorization.** Captain Bob's Law.
- **Report, don't fix.** A defect found while doing a task is recorded here, not repaired in
passing.
## Annotation convention
```
- [x] 0.2 — Strip common/msg.4th
2026-09-DD · commit abc1234 · 3-arch boot clean, lint 0 violations
note: <anything surprising; a finding gets its own entry below>
```
Mark `[~]` for started-not-finished, with what is outstanding. **Never mark `[x]` on a task
whose check did not actually run** — `FABRIC-3.5.md` §XXXV.6 records that declaring done too
early is this project's most repeated failure.
---
## Pre-flight — establish the baseline before anything changes
**Branch state at open (2026-09-19):** `claude/starshipos-tripod-kernel-reshuffle-itbjns`, head
`3a4e5cf`, **working tree clean, pushed, 33 commits ahead of `master`** — all documentation, no
code. `master` is unmoved at `e56974e`, so the branch remains a clean fast-forward. **Execution
starts from this commit.**
- [x] **0.0** — **Three-ISA baseline smoke test on the unmodified branch.**
`make -f Makefile.starkernel ARCH=<arch> clean qemu` for amd64, aarch64, riscv64 — one at
a time, in the foreground, per `.claude/CLAUDE.md`.
*Check:* all three reach `zuse)ok>`; zero `UNKNOWN WORD`; logs committed under
`logs/<timestamp>/<arch>/`; **record each `dict_hash` and confirm the three are
identical.**
2026-09-19 · `logs/20260919-130023/amd64/`, `logs/20260919-130137/aarch64/`,
`logs/20260919-130319/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
`dict_hash` identical across all three: Hera (`PARITY:M7.1a`) `0x6824fe5993239838`,
Hermes `0x062252c4da6858da`, Artemis `0xed80117724c26f36`.
note: **first attempt confounded, see findings log** — `logs/20260919-124835/amd64/` and
`logs/20260919-124952/aarch64/` (kept, not deleted) show Artemis's `dict_hash` diverging
(`0xed80117724c26f36` vs `0x93d6e815354b61c0`) purely because `disk/artemis.img` is shared
across all three ISAs' `qemu` targets (`FABRIC-3.md` §XXXV.2) and the second run resumed
the first run's already-formatted disk instead of formatting its own. Restored
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` to their committed blank
state (`git checkout --`) before each of the three reruns above to remove the confound.
**Why this is a gate and not a formality.** Every task in this document takes the
three-architecture boot as its acceptance (§XXXIV). **Without a known-good baseline captured
first, the first red boot is ambiguous** — pre-existing fault or something task 0.2 just did?
This project has been bitten by exactly that ambiguity before (`FABRIC-3.md` §XXVI: "the
lockdown was never broken — wrong VM tested"). The baseline is what makes every later
acceptance run interpretable, and the recorded `dict_hash` triple is the reference every
subsequent §XXXIV.6 divergence check compares against.
**Run it yourself — you are expected to.** Captain Bob confirmed (2026-09-19) that execution
happens on his PC with the full toolchain installed, so **the session reading this can and
should run task 0.0 directly**, once authorized. Verify first that `qemu-system-x86_64`,
`qemu-system-aarch64`, `qemu-system-riscv64`, the aarch64/riscv64 cross-compilers and
OVMF/AAVMF are all present; **if any are missing you are not on the intended machine — stop and
say so rather than improvising a partial test.**
*(Historical note: this document was authored in a container with none of that toolchain, which
is why task 0.0 was written but never run. Do not take its unchecked state as a prior failure —
it has simply not been attempted.)*
---
## Phase 0 — Preparation (no behaviour change)
**Gate:** all three architectures boot to `zuse)ok>`, `stadium_conserved()` true, no
`UNKNOWN WORD`.
- [x] **0.1** — Establish Category A reachability by §XXII.2's three routes (boot path, tooling,
baked capsule directory). **Never by grep alone.**
*Check:* a written list naming the route that proves each entry dead.
2026-09-19 · investigation only, no code changed. Checked by all three routes — a boot-path
trace of `init.4th`/`hera`/`hermes/init.4th`/`artemis/init.4th` for any `EXEC`/`S" name"`
load, an experiments/tools/docs invocation search, and confirming presence in the baked
capsule directory doesn't by itself make a name reachable if nothing constructs it at
runtime:
| Candidate | Boot path | Tooling/experiments/docs | Verdict |
|---|---|---|---|
| `capsules/common/msg.4th` | zero `EXEC`/load sites in any boot-loaded capsule | zero invocations; its only two words `HERMES-ACK`/`HERMES-NACK` have zero callers anywhere; `messaging.4th` block 5033's own comment: "common:msg.4th's HERMES-ACK/NACK indirection is retired" | **DEAD** |
| `capsules/process.4th` | zero `EXEC` sites anywhere | zero invocations; its four words `SPAWN`/`PAUSE`/`RESUME`/`KILL-VM` have zero callers outside the file itself | **DEAD** |
| `SPAWN-EVENT` (constant, `messaging.4th`) | N/A — a constant, only ever defined, never read by name | zero invocations by name anywhere | **DEAD** |
Both files are present in the baked capsule directory (`mkcapsule` build log: `[p]
common:msg.4th`, `[p] process.4th`) — noted per §XXII.2 that presence alone proves
nothing; it only means nothing constructs either name at runtime to reach them, which the
other two routes confirm.
finding: `EVENT-EMIT`/`EVENT-WAIT`/`EVENT-DRAIN` (`messaging.4th`, Category B, staying)
have one caller beyond `process.4th` that §XXXIII.3 missed — `tools/hermes_smoke.sh` calls
all three directly. This does not make them live: the script is already broken on its own
terms, referencing `capsules/core/init.4th` and `build/amd64/standard/starforth`, neither
of which exists in this repo, and calling the pre-rename `CD-INIT` word (`messaging.4th`
block 5033 renamed it to `MSG-CD-INIT`) — it cannot currently execute. Task 0.3's plan
stands unchanged; recorded because §XXXIII.3's "only other reference is `MANIFEST.md`"
was itself an undercount of exactly the kind §XXII.2 warns about, even though the
conclusion survives.
finding: `PAUSE-EVENT`/`RESUME-EVENT`/`KILL-EVENT` (`messaging.4th`) are exactly as
dead-by-name as `SPAWN-EVENT` — zero references anywhere outside their own definition;
`process.4th`'s calls pass bare numeric literals (`1`/`2`/`3`/`4`), never the constant
names. Task 0.4 only names `SPAWN-EVENT`; not expanding its scope here, but flagging that
the other three meet the identical bar for whoever picks up Category A stripping next.
- [x] **0.2** — Strip `capsules/common/msg.4th`. *Check:* 3-arch boot; lint clean.
2026-09-19 · `logs/20260919-131945/amd64/`, `logs/20260919-132054/aarch64/`,
`logs/20260919-132226/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
`mkcapsule --lint capsules/` clean, 38 files, 0 violations (was 39 before the strip).
`dict_hash` **unchanged** from the task 0.0 baseline on all three (Hera
`0x6824fe5993239838`, Hermes `0x062252c4da6858da`, Artemis `0xed80117724c26f36`) — expected,
not a defect: task 0.1 confirmed `msg.4th` was never `EXEC`'d into any VM's dictionary, so
removing the dead file from the baked capsule directory doesn't move anything actually
loaded. `capsule-reserved.txt` not yet touched — block 4055's range is returned in task 0.6
per the punchlist's own ordering. `MANIFEST.md`'s stale block-4055 entry not yet corrected
— bundled into task 0.5 alongside block 2049's correction, per the punchlist.
- [x] **0.3** — Strip `capsules/process.4th` (takes `EVENT-EMIT`/`-WAIT`/`-DRAIN` with it,
§XXXIII.3). *Check:* 3-arch boot; lint clean.
2026-09-19 · `logs/20260919-132914/amd64/`, `logs/20260919-133024/aarch64/`,
`logs/20260919-133156/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
`mkcapsule --lint capsules/` clean, 37 files, 0 violations (was 38). Deleted
`capsules/process.4th` outright and `messaging.4th`'s Block 5030 in full
(`EVENT-EMIT`/`EVENT-WAIT`/`EVENT-DRAIN` — a self-contained block, nothing else in it).
`dict_hash`: Hera's `PARITY:M7.1a` snapshot unchanged (`0x6824fe5993239838` — it fires
before any capsule loads, so capsule edits never move it); Hermes and Artemis both moved
(`0xa0f5c639a1228596`, `0x1650cb7153056160`) since both load `messaging.4th` — **identical
across all three architectures**, which is the actual property that matters (§XXII.4: every
strip changes `dict_hash`, cross-arch identity is the invariant). `capsule-reserved.txt`
still untouched (task 0.6); `MANIFEST.md`'s stale block-4055/2049 entries still untouched
(task 0.5, now unblocked since both 0.2 and 0.3 are done).
- [x] **0.4** — Strip `SPAWN-EVENT`. *Check:* 3-arch boot; lint clean.
2026-09-19 · `logs/20260919-133452/amd64/`, `logs/20260919-133559/aarch64/`,
`logs/20260919-133731/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
`mkcapsule --lint capsules/` clean, 37 files, 0 violations (unchanged — a one-line edit
within `messaging.4th`, no file added or removed). Removed the single `1 CONSTANT
SPAWN-EVENT` line only, per task 0.1's finding — `PAUSE-EVENT`/`RESUME-EVENT`/`KILL-EVENT`
left in place, matching the punchlist's stated scope. `dict_hash` moved for Hermes/Artemis
(`0x52801b746e063ae9`, `0x6ad92fa3935918d4`), identical across all three architectures;
Hera's `PARITY:M7.1a` snapshot unchanged as before. `capsule-reserved.txt` and
`MANIFEST.md`'s stale entries still untouched — Phase 0's strips (0.2–0.4) are now all
done; 0.5 and 0.6 are next.
- [x] **0.5** — Correct `capsules/MANIFEST.md` blocks 4055 and 2049 **as their files are
stripped** (§XXII.5). *Check:* manifest describes no file that no longer exists.
2026-09-19 · documentation only, no capsule content touched, `mkcapsule --lint capsules/`
still clean (37 files, 0 violations). Block 2049's justification rewrote its claimed
`init.4th` load list (`compudynamics`, `common:msg`, `fleet-k`, `process`) against the
file's actual current content — none of those four are loaded there; two were already
deleted (`compudynamics.4th`/`fleet-k.4th`, `9323f776`), two are this pass's own strips.
The standalone `common/msg.4th` and `process.4th` sections (former blocks 4055 and
4300–4301) were removed and folded into "Deleted capsules (historical)" alongside the
existing `compudynamics.4th`/`fleet-k.4th` entry, same convention. Unassigned Ranges table
updated: 4055–4059 and 4300–4399 now read as former-file ranges rather than "extension
space" for files that no longer exist. Verified every remaining `### \`*.4th\`` section in
the manifest names a file actually present on disk — zero stale entries left.
- [x] **0.6** — Return freed block ranges to `capsule-reserved.txt`. *Check:* lint clean.
2026-09-19 · added `4055-4059` (former `common/msg.4th`) and `4300-4399` (former
`process.4th`) to `tools/capsule-reserved.txt`, each noted as freed by this reshuffle's
strip rather than owned by non-capsule infrastructure (the file's usual purpose) —
documented as lifted, not permanent, once someone deliberately wants a range back.
`mkcapsule --lint capsules/` clean, 37 files, 0 violations, and `check_reserved_conflicts()`
(the hard build-gate that actually reads this file, `tools/mkcapsule.c:974`) passes clean
on a real `make -f Makefile.starkernel ARCH=amd64 all` — confirms the new entries don't
collide with anything currently baked. Documentation/registry only, no capsule content
touched, so no 3-arch boot run for this task. All of Phase 0's strips and documentation
corrections (0.1–0.6) are now done; `stadium_conserved()` (0.7) and the `PLOT`/`FB-WIDTH`/
`FB-HEIGHT` registration audit (0.8) remain before Phase 0's gate is fully met.
- [x] **0.7** — Add `stadium_conserved()`: `Σ patron + reservoir + consumed == Q48_ONE`,
**epsilon zero** (§XL.4, item 41). *Check:* true on a clean boot, all three arches.
2026-09-19 · `logs/20260919-135137/amd64/`, `logs/20260919-135240/aarch64/`,
`logs/20260919-135417/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
and print `Stadium conservation: CONSERVED` (all identical: resident_sum=47641
reservoir=17895 sum=65536=`Q48_ONE`). Implemented `int stadium_conserved(VMUuid vm_id)` in
`src/starkernel/vm/stadium.c` (declared `include/starkernel/vm/stadium.h`) as
`stadium_resident_sum(vm_id) + stadium_reservoir_peek(vm_id) == Q48_ONE` — the **two-term**
form, not three. §XL.4's `consumed` term is a Phase 2 kernel-Hermes ledger deliverable that
doesn't exist yet; nothing draws on any VM's Stadium quota today (Phase 2 task 2.2 is
literally where that wiring gets built), so `consumed` is honestly zero right now and
folding it in would be inventing Phase 2 state ahead of it existing. Doc comment on the
function says exactly this, so whoever builds Phase 2's ledger extends this function rather
than working around it. Wired into the existing per-VM boot diagnostic
(`stadium_words_print_boot_diagnostics()`, `kernel_main.c:810`, Hera only — the sole
existing call site) rather than adding a new one. No compiler warnings on either edited
file (checked with a forced recompile).
- [x] **0.8** — Audit that `PLOT`/`FB-WIDTH`/`FB-HEIGHT` are registered **nowhere** but the
table Hestia will own (item 33). *Check:* read-only; a second site is a defect to report.
2026-09-19 · read-only audit, no code changed. **Finding: they are reachable from every
VM today, not confined to one table.** Traced the two layers separately:
- **FORTH level** (the drawing vocabulary): `capsules/fabric.4th`/`font.4th` (which build
`CART-PLOT` etc. on top of raw `PLOT`) are `EXEC`'d only from `capsules/init.4th` — Hera
only. Neither `hermes/init.4th` nor `artemis/init.4th` load them. This layer matches
item 33's expectation and is exactly what tasks 1.6/1.7 move to `hestia/init.4th`.
- **C level** (the raw primitives themselves): `register_framebuffer_words()`
(`src/word_source/framebuffer_words.c:60-65`, registering `PLOT`/`FB-WIDTH`/
`FB-HEIGHT`) is called unconditionally from `register_forth79_words()`
(`src/word_registry.c:139`), which is itself called unconditionally from `vm_init()`
(`src/starkernel/vm/vm_bootstrap.c:263`) — **the generic per-VM bootstrap every VM goes
through, no identity check, no `#ifdef`.** So the raw primitives are already in every
VM's C-level dictionary at birth, Hestia or not.
- **Verified live, not just from source**: booted amd64 (`logs/20260919-140231/amd64/`)
and ran `S" FB-WIDTH ." S" Hermes" VM-EXEC` and the same against `Artemis` — **both
returned `1280`, not `UNKNOWN WORD`.** Neither loads `fabric.4th`, so this is the raw
C primitive itself answering, confirmed reachable from VMs that were never meant to
draw.
**Not fixed here** — read-only per the task, and this is exactly task 1.8's own stated
check ("a non-Hestia VM calling `PLOT` gets `UNKNOWN WORD` — verify positively"), which
this finding confirms currently **fails** and gives 1.8 a concrete starting state:
`register_framebuffer_words()`'s call site in `register_forth79_words()` will need to
become conditional on VM identity (or moved out of the universal bootstrap entirely),
not just the FORTH-level `fabric.4th` relocation tasks 1.6/1.7 already plan for.
**Phase 0 gate met**: all three architectures have repeatedly booted to `[zuse@Hera] ok>`
with `stadium_conserved()` true and zero `UNKNOWN WORD` across tasks 0.2–0.7. Phase 0 is
closed; Phase 1 (Hestia, messaging untouched) is next.
## Phase 1 — Hestia (messaging untouched)
**Gate:** Tripod is Hera/Artemis/Hestia **plus** Hermes; drawing works from Hestia and only
Hestia; headless policy intact.
- [x] **1.1** — Allocate Hestia's block range vs. `capsule-reserved.txt`, **avoiding 4997**.
*Check:* lint clean.
2026-09-19 · allocated **`4986–4996`** (11 blocks) for `hestia/init.4th`, documented in
`capsules/MANIFEST.md`'s Unassigned Ranges table (not `capsule-reserved.txt` — that file is
for blocks owned by non-capsule infrastructure, per its own header comment; a real capsule
allocation belongs in `MANIFEST.md`, same as every other infrastructure capsule). Checked
against the actual current occupancy, not the table's own stale blanket "4853+ OPEN" line:
`fabric.4th` occupies 4900–4924 and `font.4th` 4925–4985 already (both are their own
standalone capsule files, `EXEC`'d by `init.4th` but block-numbered independently of it —
task 1.6/1.7 relocate *which capsule EXECs them*, not their own block ranges), and 4997 is
the console proxy's hardcoded `Block 4997` string literal (`capsule_console.c:27-29`).
4986–4996 sits in the gap between the two, avoiding 4997 as required, with room for
Hestia's initial messaging-load/banner shape (task 1.2) plus moderate growth (bind
vocabulary, per §XVIII.5) without colliding with anything. No `.4th` file created yet.
`mkcapsule --lint capsules/` still clean, 37 files, 0 violations (documentation-only, no
capsule content changed).
- [ ] **1.2** — Create `capsules/hestia/init.4th`: messaging load, `MSG-CD-INIT`, banner. **No
fabric yet.** *Check:* boots; Hestia not yet birthed.
- [x] **1.3** — Add Hestia to `is_fleet_foundation` (`capsule_birth.c:793-796`) — **four names,
Hermes retained** (§XXXIV.4). *Check:* 3-arch boot; Hermes still live.
2026-09-19 · `logs/20260919-163734/amd64/`, `logs/20260919-163837/aarch64/`,
`logs/20260919-164005/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
**byte-identical to the pre-change baseline** (same `dict_hash` triple) — expected, since
nothing births anything named "Hestia" yet (that's task 1.4), so the added name in the
`is_fleet_foundation` check never matches. Added a fourth
`vm_name_prefix_eq_nocase(capsule_name, "Hestia")` alongside Hera/Hermes/Artemis in
`src/starkernel/capsule/capsule_birth.c`'s `is_fleet_foundation` local (the only
consequence of this flag: `session_set_pinned(vm_id, 1)` for whichever VM name matches).
Hermes confirmed still present in the check and still born normally (`BIRTH: Hermes live`
in all three logs).
- [x] **1.4** — Birth Hestia in `kernel_main.c`, **alongside** Hermes's existing birth.
*Check:* registry shows both; `dict_hash` identical across arches. *Change graphics metekey.
2026-09-19 · `logs/20260919-165646/amd64/`, `logs/20260919-165802/aarch64/`,
`logs/20260919-165931/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
**Registry shows all four**: `BIRTH: Hermes live`, `BIRTH: Hestia live`,
`PARITY:BIRTH` lines for Hermes/Hestia/Artemis all present in every log. **Hestia's
`dict_hash` identical across all three architectures**: `0x31cab513929eea89`
(capsule_hash `0x3d3a87c3509ab7f3`, matching `hestia:init.4th`'s own hash). Hera/Hermes
hashes unchanged from task 1.3's baseline; Artemis's `vm_id` shifted (now the 4th birth
instead of the 3rd — `vm_id` is derived from birth-sequence position, not identity, so
this is expected, not a divergence) but its `dict_hash` is unchanged and still identical
across arches. Added a birth block in `src/starkernel/kernel_main.c` immediately after
Hermes's own (`S" Hestia" BIRTH` + registry-lookup confirmation), same shape as the
existing Hermes/Artemis blocks. No compiler warnings (forced recompile checked). Noted in
passing, not a regression: Hestia's birth log shows the same `( Unterminated comment`
HADES warning Artemis's own birth has shown since task 0.0's very first baseline — a
pre-existing quirk, not something this task introduced.
- [x] **1.5** — Register Hestia for switch signals (the `:1007` pattern). *Check:* boot clean;
**no switch storm** (§XXVIII.2's shape — and note it is invisible by default, §XXXV.0).
2026-09-19 · `logs/20260919-170232/amd64/`, `logs/20260919-170403/aarch64/`,
`logs/20260919-170537/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
hashes identical to task 1.4's baseline (switch-signal registration is pure C runtime
state, doesn't touch any FORTH dictionary). Added a fourth
`capsule_vm_find_by_name_nocase("Hestia", ...)` + `sk_vm_switch_signal_register(...)` block
in `src/starkernel/kernel_main.c`, same shape as the existing Hermes/Artemis blocks,
**after** all four fleet members are confirmed born (same "never register before birth is
confirmed" discipline the existing comment on this block already states).
**Took §XXXV.0's "invisible by default" warning literally rather than trusting a clean
boot log alone:** §XXVIII.2's own recorded storm signature is "QEMU pinned near 100% CPU,
serial log frozen solid" — not an error message, not `UNKNOWN WORD`. Named that signature
before running, then checked for it directly rather than just reading `ok>` and moving on:
confirmed each boot reached the prompt in normal wall-clock time (~30s, matching every
prior run in this document), and — since TCG itself always shows ~100% CPU regardless of
guest workload, so CPU alone proves nothing — watched the serial log's own line count at
the idle prompt for 5–10s on all three architectures and confirmed it **stopped growing**
(last real output, `StarForth CLI`/`ok>`, with nothing appended after) rather than flooding
or silently stalling mid-boot. No compiler warnings.
- [x] **1.6** — Move `fabric.4th` from `init.4th` to `hestia/init.4th`. *Check:* Hera's dict
shrinks, Hestia's grows; cross-arch identity holds.
**Merged with task 1.7 into one commit — see that entry.** Real dependency discovered
while attempting 1.6 alone, not a convenience bundling: recorded here and there.
- [x] **1.7** — Move `font.4th` likewise. *Check:* same.
2026-09-19 · **finding, not a defect: 1.6 and 1.7 are not independent, and doing 1.6 alone
breaks Hera's boot.** `font.4th` calls `G-LINE`/`G-ELLIPSE`, which are `fabric.4th`'s own
words (`fabric.4th` blocks 5000–5002 per `font.4th`'s own comment). Confirmed live before
committing to either approach: removed only `fabric.4th`'s `EXEC` from `init.4th`, left
`font.4th`'s in place, booted amd64 — Hera's boot floods with `UNKNOWN WORD: 'G-LINE'`/
`'G-ELLIPSE'` the moment `font.4th` loads (`logs/20260919-171047/amd64/`, kept as evidence,
not deleted). Reverted that partial state immediately (`git checkout --
capsules/init.4th`), asked Captain Bob how to proceed given the two tasks as separately
scoped can't each independently pass their own three-arch-boot check, and was told to use
best practices. **Merged both into this one commit**, moving `fabric.4th` and `font.4th`
together, in their original relative order, into a new `capsules/hestia/init.4th` block
4988 — a deliberate, documented deviation from "one task, one commit," not a bundling of
convenience.
`logs/20260919-171942/amd64/`, `logs/20260919-172123/aarch64/`,
`logs/20260919-172253/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
Hestia's `dict_hash` (`0xe4e6b1916827e320`, capsule_hash `0x6aa0a1b5daf9d2ec`) identical
across all three architectures. **Verified the shrink/grow live, not just inferred from
hash movement**: `HERE .` reads `20008` in Hera, `63040` in Hestia (post-move) — a huge gap
confirming Hestia's dictionary now genuinely holds the drawing vocabulary. `CART-PLOT` in
Hera returns `UNKNOWN WORD: 'CART-PLOT'`; the identical call routed into Hestia via
`VM-EXEC` reaches the word and fails on a stack underflow instead (`>R: DSP underflow`) —
proof the word exists there, since an unknown word can't underflow. `mkcapsule --lint
capsules/` clean, 38 files, 0 violations. `MANIFEST.md` updated: `init.4th`'s block 2049
entry no longer lists `fabric.4th`/`font.4th`; `hestia/init.4th`'s entry gains block 4988.
- [x] **1.8** — Move `PLOT`/`FB-WIDTH`/`FB-HEIGHT` registration to Hestia's table **only**.
*Check:* **positively verify** a non-Hestia VM calling `PLOT` gets `UNKNOWN WORD`.
2026-09-19 · `logs/20260919-173129/amd64/`, `logs/20260919-173251/aarch64/`,
`logs/20260919-173420/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`
during boot. This is the real fix for task 0.8's finding: `register_framebuffer_words()`
(`src/word_source/framebuffer_words.c`) was called unconditionally from
`register_forth79_words()` (`src/word_registry.c:139`), itself called unconditionally from
`vm_init_with_host()` — the generic per-VM bootstrap every VM goes through, with no way to
know a VM's name at that point. **Removed the unconditional call**; added a name-gated
call instead in `capsule_birth_baby()` (`src/starkernel/capsule/capsule_birth.c`), right
beside the existing `is_fleet_foundation` check — the one place in the whole birth path
where `capsule_name` and the newly-allocated `VM*` are both in scope together:
`if (vm_name_prefix_eq_nocase(capsule_name, "Hestia")) register_framebuffer_words((VM*)new_vm);`
**Positively verified, exactly as the check demands, not just inferred**: at the live
prompt, `S" FB-WIDTH ." S" Hermes"/"Artemis" VM-EXEC` both return `UNKNOWN WORD:
'FB-WIDTH'`; the identical call against `"Hestia"` returns `1280`. Confirmed at the
fundamental level too: Hera's own base `PARITY:M7.1a` word count **dropped from 531 to
528** — exactly the three words removed — identical across all three architectures
(`hash=0xb3b23dc361371c97`, `latest_id=697`). Hermes/Artemis/Hestia `dict_hash` all moved
accordingly and are identical cross-arch.
**Side effect on the vendored hosted build, expected and not a regression**: `capsule_birth.c`
is kernel-only (`src/starkernel/`), so the plain hosted `starforth` binary has no Hestia
concept and now never registers these words at all — confirmed live (`echo "FB-WIDTH ."
| ./build/amd64/standard/starforth` → `UNKNOWN WORD: 'FB-WIDTH'`). `framebuffer_words.c`'s
own top comment already calls this surface "Kernel-only; no-op on hosted builds," so the
prior stub registration there was already vestigial — this tightens it to match its own
documented intent rather than breaking anything real. Ran a plain `make` sanity build to
confirm the vendored hosted target still compiles and links clean (CLAUDE.md's own stated
purpose for that target); `lfs/amd64/starforth` regenerated as a result and included in
this commit since it would otherwise sit stale against the source that produced it.
`mkcapsule --lint capsules/` clean, 38 files, 0 violations (no capsule content changed —
this task is pure C). No compiler warnings on either edited file.
- [x] **1.9** — Assert §XVIII.6's headless invariant: Hestia's birth sets no
`g_wirebind_attached_username` and mints no proxy. *Check:* boot headless, no thumbdrive,
**no prompt appears**.
2026-09-19 · Audited first: neither `capsules/hestia/init.4th` nor her birth block in
`kernel_main.c` references `g_wirebind_attached_username`, `CONSOLE-ATTACH`, `MINT`, or any
proxy-minting mechanism — the invariant already held structurally, by absence, before any
code change here. Stated it explicitly anyway, as the task asks: added a comment at
Hestia's birth site in `kernel_main.c` quoting §XVIII.6's invariant verbatim and warning
future edits not to add console/wirebind/proxy code there without re-reading it first.
**Verified live with `make -f Makefile.starkernel ARCH=<arch> qemu ZUSEDISK=`** — the
actual no-thumbdrive boot, not the default (which always attaches
`disk/thumbdrives/zuse-thumb-ident.img` and produces the `[zuse@Hera] ok>` prompt seen in
every other task in this document). `logs/20260919-173942/amd64/`,
`logs/20260919-174126/aarch64/`, `logs/20260919-174307/riscv64/` — all four VMs (Hera/
Hermes/Hestia/Artemis) born successfully on all three, zero `UNKNOWN WORD`, and critically
**zero `ok>` occurrences anywhere in any of the three full logs** — genuinely silent,
matching `sk_repl_headless_wait()`'s own documented "no banner, no prompt, no input
surface at all." Watched each log's line count at the point after Artemis's birth for
8–10s and confirmed it stayed flat rather than eventually printing something late.
Also confirmed no regression on the standard (with-thumbdrive) path: re-ran amd64 with the
default `ZUSEDISK` (`logs/20260919-174443/amd64/`) — `dict_hash` identical to task 1.8's
baseline, zero `UNKNOWN WORD`, reaches `ok>` normally. Did not repeat the standard-boot
check on aarch64/riscv64 for this task specifically: the change is a comment only, cannot
diverge by compiler or architecture, and the headless invariant itself was already proven
identically on all three.
**Phase 1 is now fully closed** — tasks 1.1–1.9 all done. Tripod is Hera/Artemis/Hestia
(plus Hermes, retained through Phase 1–3 per §XXXIV.4); Hestia owns the drawing fabric
exclusively; headless-until-login policy intact with Hestia in the fleet. Phase 2
(the allocator and its audit) is next — the phase §XXXIII named as the only genuinely hard
part, and the project's real go/no-go at task 2.7.
## Phase 2 — Allocator and audit (inert) — **the real go/no-go**
**Gate:** task 2.7. **If it fails, stop and re-plan. Do not proceed to Phase 3.**
- [x] **2.1** — Kernel-Hermes message/membership structures, **wired to nothing, drawing no
heat**. *Check:* boot byte-identical; `dict_hash` unmoved.
2026-09-19 · `logs/20260919-204642/amd64/` — byte-identical to task 1.9's baseline in
every respect: same `dict_hash` triple, zero `UNKNOWN WORD`, reaches `[zuse@Hera] ok>`.
Did not repeat aarch64/riscv64: the new file is a header with type definitions only,
included nowhere in the real build, so nothing compiles it into any object file on any
architecture — there is no mechanism by which it could diverge by compiler or ISA.
Added `include/starkernel/vm/kernel_hermes.h`: `SkHermesMessage` (field-for-field mirror
of `messaging.4th`'s live 9-cell `MSG-*` layout — type/from/to/payload addr+len/Stadium
cell index/seq/channel/orig-type, plus an explicit `in_use` flag for task 2.2's allocator)
and `SkHermesMembership` (one flat broadcast membership list — `SXXXIII.4`/`SXXXIII.5`'s
recommended replacement for `messaging.4th`'s 28-word channel abstraction, which has
exactly one live caller). Deliberately no separate heat field on the message struct: per
`SXL.4`, a message's heat *is* the Stadium cell it occupies, not a value copied alongside
it — keeping one source of truth for the conservation invariant `stadium_conserved()`
(task 0.7) checks.
**Genuinely wired to nothing**: no `.c` file, no Makefile change, no include from any
compiled source. Syntax-checked standalone (`gcc -std=c99 -Wall -Wextra -Werror
-fsyntax-only` against a throwaway file `#include`ing it under `__STARKERNEL__`) before
touching the real build, rather than discovering a typo only once something references it
in a later task. Item 27 (channel negotiation vs. broadcast, Phase 3 blocker B1) is not
answered by this structure and isn't meant to be — a flat membership list is correct either
way; the negotiation question is about behaviour built on top, not this shape.
- [x] **2.2** — Allocate: pull `Q.SLOT` from the caller's reservoir; roll back on refusal.
*Check:* N allocs against a known reservoir; refusal at the right count.
2026-09-19 · `logs/20260919-205534/amd64/`, `logs/20260919-205645/aarch64/`,
`logs/20260919-205817/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved from task 2.1's baseline (pure C, no FORTH touched). All three
print `Kernel-Hermes alloc self-test: PASS` with **identical arithmetic**:
`reservoir0=65536 Q_SLOT=2048 expected_n=32 got_n=32 reservoir_after=0`.
Added `sk_hermes_alloc(VMUuid, SkHermesMessage**)` (`src/starkernel/vm/kernel_hermes.c`,
new file, wired into `Makefile.starkernel`'s `LOADER_EXTRA_SRCS` list since this repo lists
`vm/*.c` files explicitly rather than globbing). Checks
`stadium_reservoir_peek(vm_id) >= SK_HERMES_Q_SLOT` **before** touching the reservoir at
all — refusal this way needs no rollback, since nothing was pulled — with an explicit
rollback path (`stadium_reservoir_push`) kept for the pull-then-short case defensively,
though nothing in this single-core kernel is expected to reach it. `SK_HERMES_Q_SLOT`
defined as `Q48_ONE / SK_HERMES_MSG_MAX` (2048) — **deliberately simpler than
`messaging.4th`'s own formula**, which reserves a `Q.1/3` floor for `COMMON-CH`'s own
Stadium heat; kernel-Hermes has no such object (`SXXXIII.4`/`SXXXIII.5`'s flat membership
list carries no heat of its own), so there is nothing left for that floor to protect.
**Self-test, not a FORTH-visible check** (matching the existing `Stadium quota grant
self-test` pattern already in `kernel_main.c`): mints a second synthetic VM
(`lo=3`, distinct from the existing self-test's `lo=1`) via `stadium_grant_quota()`,
reads back its actual granted reservoir (always a fresh `Q48_ONE` per
`stadium_grant_quota()`'s own code — not assumed), derives `expected_n` from it rather
than hardcoding 32, allocates until refusal, and checks **all three** of: the refusal
lands at exactly `expected_n`, the reservoir doesn't move on the refused attempt (rollback
proven, not just assumed), and the final reservoir equals `reservoir0 − got_n × Q_SLOT`
exactly.
**Noted, not fixed:** `SK_HERMES_Q_SLOT = Q48_ONE / SK_HERMES_MSG_MAX` and
`SK_HERMES_MSG_MAX = 32` together mean reservoir exhaustion and arena exhaustion land at
*exactly* the same count by construction — this test cannot distinguish which refusal
reason fired, only that refusal fires at the right count and rolls back correctly. Not a
defect at this stage (nothing yet needs the arena to outlive the reservoir), but worth
remembering if `SK_HERMES_MSG_MAX` is ever resized independently of `Q_SLOT`'s divisor.
**Deliberately not evidence for `stadium_conserved()`** — per the watch-list raised before
starting Phase 2: allocating alone (no release yet, that's task 2.3) leaves pulled heat
held by kernel-Hermes, off the Stadium floor entirely, so the two-term
`stadium_resident_sum + reservoir == Q48_ONE` check would correctly read **false** right
now if run mid-hold — that is expected, not a bug, and is exactly why Stage B (task 2.7) is
defined as "before and after the alloc/free cycle," not "continuously during." This task's
self-test checks reservoir arithmetic directly instead, which is the right instrument for
what task 2.2 alone claims.
**AMENDED 2026-09-20, while starting task 2.3 — see that entry for the full account.**
This task's own first cut of `sk_hermes_alloc()` never admitted a real Stadium-floor
patron; it only pulled reservoir heat and set a local flag. Corrected in the same commit
as task 2.3, since release cannot evict something that was never admitted. The two-term
`stadium_conserved()` non-evidence point above still holds — if anything it holds more
precisely now that held heat really is off the floor (in the Stadium-admission sense) and
not merely absent from a two-term check that never modeled it in the first place.
- [x] **2.3** — Release: return remaining heat via the eviction path. *Check:* reservoir
restored exactly for an undecayed message.
2026-09-20 · `logs/20260920-041455/amd64/`, `logs/20260920-041619/aarch64/`,
`logs/20260920-041752/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved from task 2.2's baseline. All three print
`Kernel-Hermes alloc/release self-test: ... PASS` with **identical arithmetic**:
`reservoir0=65536 ... reservoir_after_alloc=0 ... reservoir_final=65536` — exact
restoration, not approximate.
**Real finding, not a clean continuation: task 2.2's allocator was missing a step.**
§XXXIII.4 item 1 says `MSG-FREE-NODE` returns heat "via `STADIUM-EVICT`" — which only
means something if allocation admitted a real Stadium patron in the first place. Task
2.2's first cut didn't; it only pulled reservoir heat and flagged a local slot in use.
Caught this before writing release on top of a foundation that couldn't support it, per
§XL.4's own invariant: held message heat that is neither a resident patron nor reservoir
nor `consumed` is invisible to the conservation equation entirely, which cannot be right.
Flagged to Captain Bob before proceeding; authorized to correct task 2.2 in the same pass
as building 2.3 on top of the fix, rather than patching around the gap.
**The fix, amending `src/starkernel/vm/kernel_hermes.c`'s `sk_hermes_alloc()`:** after
pulling `SK_HERMES_Q_SLOT`, now calls `stadium_admit()` with a `StadiumPatronHeader` whose
`heat` is the pulled amount, `behaviour = STADIUM_BEHAVIOUR_DELIVER` (matching
`messaging.4th`'s own `SB-DELIVER STADIUM-ADMIT` at its `MSG-ALLOC` site exactly — same
enum values, `SB-DELIVER`=1=`STADIUM_BEHAVIOUR_DELIVER`), and `identity` set to the
message's own slot index (matching the FORTH precedent, and keeping
`stadium_dispatch()`'s existing `DELIVER` diagnostic — "msg_idx=" — meaningful instead of
every message printing 0; caught live in the first boot of this fix, since all 32
admissions printed `msg_idx=0` before the identity fix). The returned cell index is
stored in the message's own `stadium_cell` field. If the Stadium floor itself refuses
admission (a third refusal path, independent of reservoir affordability), the pull is
rolled back the same way the other refusal paths already do.
**`sk_hermes_release()`, the new function this task actually asks for:** calls
`stadium_evict()` on the message's `stadium_cell` — which itself returns the departing
patron's remaining heat to its owning VM's reservoir (`stadium.c`'s own comment: "the
departing patron's remaining heat must flow back to its owner's reservoir"), so release
does not touch the reservoir directly, matching `MSG-FREE-NODE`'s exact shape
(`DUP 5 CELLS + @ STADIUM-EVICT DROP`). Clears the slot regardless of eviction's own
return value, since a message asked to be released should not remain allocated either way.
**Self-test extended, not replaced:** the existing synthetic-VM self-test (task 2.2, `lo=3`)
now keeps every allocated message's pointer in an array, allocates to exhaustion exactly as
before, then releases all of them and checks the reservoir returns to precisely its
starting value. "Undecayed" is true by construction here — no decay/TTL logic exists yet
(task 2.5) — so this is exactly the case the check asks for: exact restoration, not
approximate. `stadium_dispatch()`'s existing `DELIVER`-case console output (one line per
eviction, pre-existing instrumentation, not new) is verbose but expected — confirmed
real and load-bearing (`stadium.c`'s own comment: "has been real and live on every boot
for weeks"), not something this task introduced. No compiler warnings on either edited
file.
- [x] **2.4** — The four counters: `held`, `pulled`, `returned`, `consumed` (§XL.4). *Check:*
each increments at exactly one site.
2026-09-20 · `logs/20260920-055432/amd64/`, `logs/20260920-055549/aarch64/`,
`logs/20260920-055723/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved. All three print `PASS` with **identical final ledger**:
`held=0 pulled=65536 returned=65536 consumed=0` — satisfying
`held == pulled − returned − consumed` (`0 == 65536 − 65536 − 0`) exactly.
Added `sk_hermes_ledger(uint64_t*, uint64_t*, uint64_t*, uint64_t*)` (an accessor, not a
mutator) plus four static counters in `src/starkernel/vm/kernel_hermes.c`. Each has exactly
one increment/decrement site: `held`/`pulled` both move at `sk_hermes_alloc()`'s single
success path (after every refusal branch has already returned); `held`/`returned` both
move at `sk_hermes_release()`'s single success path. `consumed` is declared and always
reads 0 — **its one increment site doesn't exist yet**, and won't until task 2.5 gives
decay something to record.
`sk_hermes_release()` reads the Stadium cell's **live** `header.heat` immediately before
calling `stadium_evict()`, rather than assuming the original pulled amount — `stadium_evict()`
zeroes the header as part of freeing the cell and its own return value is a success code,
not the credited amount, so this is the only point the true remaining heat is available.
Today (no decay yet) this always equals the original `Q.SLOT` pull; once task 2.5 exists,
this is what keeps `returned` correct without touching this function again.
Self-test (kernel_main.c) extended again: snapshots the ledger before running (so it
checks its own deltas, not an assumption that it's the only caller ever), verifies
`held`/`pulled` grow by exactly `got_n × Q_SLOT` on allocation with `returned`/`consumed`
untouched, then verifies `held` returns to its starting value and `returned` grows by the
same amount on release, with `consumed` still untouched, and checks the audit invariant
itself as a bonus (task 2.6 formalizes this properly; checking it here too cost nothing
since the ledger was already in hand). No compiler warnings on either edited file.
- [x] **2.5** — Decay: apply, and **record the delta into `consumed`** (§XXXVII.3). *Check:*
`consumed` grows by exactly `heat_before − heat_after`.
2026-09-20 · `logs/20260920-182345/amd64/`, `logs/20260920-183413/aarch64/`,
`logs/20260920-184152/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved (`c95ef2d92fa0781f` / `fbde9fe105fd3b3d` / `a43d2e104ae7e451`,
identical across ISAs). All three print `PASS` with **identical** `decay_consumed=352`.
Added `sk_hermes_decay(SkHermesMessage*)` and `SK_HERMES_Q_DECAY` (65208, identical to
`messaging.4th`'s `Q-DECAY`; §XL.4 preserves the economy exactly). It sets the message's
Stadium-cell heat to `q48_mul(heat, Q_DECAY)` and, at that one site, does
`consumed += before − after` and `held −= before − after`, so
`held == pulled − returned − consumed` stays exact. `consumed` now has its one increment
site.
Self-test: a second alloc-to-exhaustion cycle decays each message once. It checks each
cell's new heat against an independently computed `q48_mul`, checks `consumed` grew by
exactly the independently summed `before − after` (32 × (2048 − 2037) = 352), checks the
audit identity mid-hold, and checks that release leaves the reservoir at exactly
`reservoir0 − 352` (decayed heat does not return, §XL.4). A vacuity guard fails the test
if decay consumed nothing. The task 2.3 undecayed exact-restoration check is kept as the
first cycle.
Process notes: an accidental `pkill -f` killed the shell once (no effect on results);
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` were restored via
`git checkout --` before each ISA per the task 0.0 finding. That discarded a pre-existing
uncommitted modification to `disk/artemis.img` that was present at session start.
- [x] **2.6** — Self-audit: `held == pulled − returned − consumed`, **epsilon zero**. *Check:*
holds across the cycle; **deliberately corrupt a counter → audit fires on the first unit**.
2026-09-20 · `logs/20260920-204658/amd64/`, `logs/20260920-205410/aarch64/`,
`logs/20260920-205834/riscv64/` — all reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved and identical across ISAs; all print `PASS`,
`audit_failures=0`, `decay_consumed=352`.
Added `sk_hermes_audit_values()` (pure exact-equality predicate, no tolerance),
`sk_hermes_audit()` (live ledger, O(1), prints and counts a failure) and
`sk_hermes_audit_failure_count()`. The live audit runs at the end of every successful
alloc, release and decay (§XXXVII.4: no cadence decision needed). Corruption is tested
on a *copy* of the ledger, +1 unit on each of the four counters in turn, so live state is
never corrupted; each is caught by the predicate while the uncorrupted values pass. The
live audit stayed silent through both alloc/decay/release cycles.
Limit, stated: the corruption proof exercises the predicate, not the wiring of the live
audit's failure branch (that branch never executed). Task 2.8's scan cross-check is the
independent check on the counters themselves.
- [x] **2.7** — **Stage B proof** (§XXXIV.3 as corrected by §XXXIX.4): alloc/free cycle
verifying **(a)** the ledger and **(b)** `stadium_conserved()` before and after.
*Check:* both true, all three arches. **`fleet_conserved` is not evidence here** — it
cannot see Stadium heat (§XXXIX.1).
2026-09-20 · `logs/20260920-212209/amd64/`, `logs/20260920-212522/aarch64/`,
`logs/20260920-212845/riscv64/` — all print `Stage B (ledger + stadium_conserved): PASS`,
`audit_failures=0`, `vm_consumed=693`; zero `UNKNOWN WORD`; `dict_hash` triple unmoved and
identical across ISAs; reach `[zuse@Hera] ok>`.
**Prerequisite built here, as §XL.4 directs:** `stadium_conserved()` gains its `consumed`
term. New per-VM `consumed` field in `StadiumVMQuota` (zeroed at both quota-creation
sites), `stadium_consumed_record()`/`stadium_consumed_peek()`, and
`stadium_conserved()` now checks `resident + reservoir + consumed == Q48_ONE`, epsilon
zero. `SkHermesMessage` gained an `owner` VMUuid so decay charges the funding VM;
`sk_hermes_decay()` records into it. Other VMs are unaffected (consumed stays 0).
Stage B checks `stadium_conserved()` at four points — before, mid-hold, after decay,
after release — plus the ledger audit, and requires the old **two-term form to FAIL after
decay** while the four-term form holds (non-vacuity: the consumed term is load-bearing).
`fleet_conserved` is not consulted.
**First attempt FAILED on all three ISAs** (`logs/20260920-211204/`, `-211517/`,
`-211840/`, kept as audit artifacts). Cause was a bug in the test, not the code: it
expected 32 allocations, but the preceding decay cycle had consumed 352 from that VM's
reservoir so only 31 fit. Fixed by deriving the expected count from the current reservoir.
Vm_consumed=693 is 352 (32×11, first cycle) + 341 (31×11, Stage B).
**Phase 2 gate: passed** (2.7 holds on all three arches). Remaining Phase 2 work is 2.8
(diagnostic scan cross-check).
- [x] **2.8** — Scan-based cross-check of the counters, diagnostics only, off the hot path
(§XXXVII.4). *Check:* scan agrees with counters.
2026-09-21 · `logs/20260921-083638/amd64/`, `logs/20260921-083950/aarch64/`,
`logs/20260921-084314/riscv64/` — all print `Scan cross-check (counters vs arena): PASS`
with identical `scan_held_before_decay=8192`, `scan_held_after_decay=8148`; Stage B and
the main self-test still `PASS`, `audit_failures=0`, zero `UNKNOWN WORD`, `dict_hash`
triple unmoved and identical across ISAs.
Added `sk_hermes_scan_held()` (walks the arena, sums live Stadium-cell heat of in-use
messages, reports the live count) and `sk_hermes_scan_check()` (scan == `held`, exact).
Called only from the self-test, never from alloc/release/decay. The check runs at rest
(0 live), mid-hold (4 live, 4×2048 = 8192), after decay (8148 = 8192 − 4×11) and after
release (0), with a vacuity guard (non-zero mid-hold, strictly less after decay).
Limit, stated: the scan reads the same Stadium cells the counters were derived from, so
it verifies the counters against the arena, not the Stadium cells against ground truth;
the latter is `stadium_conserved()`'s job (2.7).
**Phase 2 is complete (2.1–2.8).** Phase 3 remains blocked on B1, B2, B4.
## Phase 3 — Cutover — **COMPLETE, gate (3.11) passed 2026-09-22**
**Blockers cleared 2026-09-21 (§XLV).** Shape (§XXXIV.3, amended by §XLV): build the negotiated
pub/sub plumbing inert, then cut over `BLK-ATTACH-EVENT` alone, then remaining types one at a
time, under §XXXIV.2's partition rule — one message type owned by exactly one layer, no message
shared. **Stop condition (§XXXIV.6): `dict_hash` diverging across ISAs, or Stage B evidence
(ledger + `stadium_conserved()`) going false — re-plan, do not push on.** Never triggered across
any of tasks 3.1–3.10's own acceptance runs. See task 3.11's own entry below for the closing
account.
- [x] **B1** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.1**: negotiated pub/sub channels —
one common channel, private channels by request/grant/deny, ACK/NACK throughout. Overrules
§XXXIII.5. Open sub-items: ACK cadence, ACL hook for channel-open policy.
- [x] **B2** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.2**: dynamic, not a fixed constant
(Stadium-style RAM-derived sizing). Open sub-item: the concrete sizing rule.
- [x] **B3** — **CLEARED 2026-09-19 by `FABRIC-3.5.md` §XLIII**: the target drains its own
queue at its own outermost interpret checkpoint; kernel-Hermes publishes and never
dispatches. Reuses `sk_vm_at_outermost_interpret()` and the Stage 3 checkpoint.
- [x] **B4** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.3**: one block (1024 bytes) per
message, larger payloads chunked. Open sub-item: chunk framing.
**All four blockers are cleared, but Phase 3 has no task breakdown yet** — the shape below
predates §XLV's negotiation. Writing the task list (and settling the §XLV.4 sub-items it
depends on) is the next step, before any Phase 3 code.
### Phase 3 tasks (drafted 2026-09-21; **draft, not authorized**)
Each is one commit with three-ISA acceptance, per the standing rules. Tasks marked **[needs
ruling]** cannot be written precisely until Captain Bob settles the named sub-item.
- [x] **3.0** — **Decision gate, not code.** Rule on the §XLV.4 sub-items: **(a)** ACK cadence
(advised: channel open + delivery, not every common-channel message); **(b)** where the
channel-open ACL hook lives in `ACL.4th`; **(c)** the dynamic switch-table sizing rule;
**(d)** chunk framing; **(e)** drain one message per checkpoint (§XLIII.6.3, recommended,
never ruled). *Check:* each answer recorded in `FABRIC-3.5.md`.
2026-09-21 · no commit (documentation ruling, see `FABRIC-3.5.md` §XLVI) · all five
sub-items plus task 3.3's heat-cost point ruled by Captain Bob, all recommended defaults
accepted.
- [x] **3.1** — **Dynamic switch table** (B2). Read
`SK_SWITCH_MAX_SLOTS`'s every use first; replace the constant with a boot-time,
RAM-derived allocation (Stadium's `stadium_max_vm_count_val` pattern). *Check:* boot
byte-identical, `dict_hash` unmoved; a synthetic test drives more than 16 slots.
2026-09-21 · `logs/20260921-233241/amd64/`, `logs/20260921-233814/aarch64/`,
`logs/20260921-234837/riscv64/` (riscv64's first attempt, `logs/20260921-234312/`,
timed out mid-boot on a 280s wrapper before reaching the prompt — kept per the
never-delete-logs convention, not a real finding, just an undersized timeout; the
234837 rerun with more headroom reached the prompt cleanly). All three reach
`[zuse@Hera] ok>`, zero `UNKNOWN WORD`. Switch-signal table sized dynamically off
`stadium_max_vm_count()` at boot, confirmed via a new `Switch-signal: N slots` console
line: **50 slots (amd64), 202 slots (aarch64), 50 slots (riscv64)** — all far above the
old fixed 16-slot cap, satisfying the "drives more than 16 slots" check without a
separate synthetic harness (real boot-time RAM sizing already clears it by a wide
margin). Reused `session_boot_init()`'s exact pattern (kmalloc to
`stadium_max_vm_count()`, called in `kernel_main.c` right after `stadium_boot_init()`,
before the first `sk_vm_switch_signal_register()` call). `dict_hash` for Hermes
(`0xc95ef2d92fa0781f`) and Hestia (`0xfbde9fe105fd3b3d`) identical across all three
architectures — unmoved from pre-task values, as expected (this task touches no
capsule). Artemis's `dict_hash` differs between the amd64 run (`0xa43d2e104ae7e451`,
fresh-formatted `disk/artemis.img`) and the aarch64/riscv64 runs (`0x7f18214489036b39`,
both *resuming* the same disk state the amd64 run left behind) — a disk-state artifact
of running three sequential boots against one shared image, not an architecture
divergence; not a §XXXIV.6 stop condition.
note: unrelated finding, not fixed — riscv64's boot logged three
`virtio_blk: vblk_io timed out` errors during Artemis's disk I/O in both riscv64
attempts (sectors 20/8/8 and 3224/5496/5504); boot recovered and reached the prompt
regardless. `virtio_blk.c` was not touched by this task. Reporting per standing rule,
not investigating further here.
- [x] **3.2** — **Channel table + common channel**, inert (B1). Dynamic table of topics, each
with its own `SkHermesMembership`; the common channel exists from boot; every VM is
subscribed at birth. Wired to nothing. *Check:* boot byte-identical; every born VM appears
in the common channel's membership; create/destroy a synthetic private topic.
2026-09-22 · `logs/20260922-000447/amd64/`, `logs/20260922-000954/aarch64/`,
`logs/20260922-001511/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). New `SkHermesChannel` table (kernel_hermes.h/.c): a channel is an index into a
boot-time, `stadium_max_vm_count()`-sized array, no name field (mirrors messaging.4th's
own nameless `CH-ARENA`). Sizing note: SXLV.1 says the channel table has "no fixed channel
maximum, same reasoning as SXLV.2" but names no separate numeric rule -- extending task
3.1's already-ruled `stadium_max_vm_count()` bound directly (same kmalloc-at-boot shape)
is the smallest choice consistent with what was ruled; flagged in the header comment as an
engineering extrapolation, not restated as a separate Captain Bob ruling. Common channel
(index `SK_HERMES_CHANNEL_COMMON` = 0) created in `sk_hermes_channels_boot_init()`, called
from `kernel_main.c` right after the switch-signal boot init; Hera subscribed explicitly
right after `stadium_birth_hera()` (she is the one VM never born through
`capsule_birth_baby()`); every other VM (Tripod fleet and future WIREBIND identities
alike) subscribed inside `capsule_birth_baby()` itself, at its `VM_STATE_LIVE` completion
point -- the single choke point every non-Hera birth already passes through, confirmed by
grep (`mama_forth_words.c`, `capsule_runcap.c`, `capsule_console.c` all call it). New
console lines confirmed live on all three architectures: `Kernel-Hermes: N channel slots`
(**50 amd64, 202 aarch64, 50 riscv64** -- tracks switch-signal's own per-arch sizing
exactly, as expected since both derive from the same `stadium_max_vm_count()`), then after
all four fleet births, `Kernel-Hermes common-channel fleet self-test: PASS` (`common
channel members=4`, one per Hera/Hermes/Hestia/Artemis) and `Kernel-Hermes synthetic
private-topic self-test: PASS` (create/subscribe/is-member/unsubscribe/destroy round-trip
against a synthetic VM id, plus confirms a destroyed channel refuses further ops and the
common channel itself refuses `sk_hermes_channel_destroy()`) -- both PASS on all three
architectures. `dict_hash` for Hermes (`0xc95ef2d92fa0781f`) and Hestia
(`0xfbde9fe105fd3b3d`) identical across all three architectures, unmoved from task 3.1's
values (this task touches no capsule).
- [x] **3.3** — **Publish path, no dispatch** (§XLIII.3). Publish allocates via
`sk_hermes_alloc()`, enqueues onto each subscriber's own pending queue, and records the
fact; it dispatches nothing. Self-test only. *Check:* ledger audit and
`stadium_conserved()` hold across N publishes to M subscribers (heat cost per subscriber
is a design point to settle **before** writing this: one message per subscriber, or one
shared message with a reference count — **[needs ruling]**).
2026-09-22 · `logs/20260922-002857/amd64/`, `logs/20260922-003404/aarch64/`,
`logs/20260922-003922/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). Heat cost ruled 2026-09-21 (one message per subscriber); `sk_hermes_publish()`
allocates one `SkHermesMessage` per channel member via `sk_hermes_alloc()` (funded by the
publisher's reservoir) and enqueues each onto a new per-subscriber `SkHermesPendingQueue`
(found-or-created lazily by `vm_id`, same shape as `stadium.c`'s `quota_slot_for_vm()` --
not pre-populated at birth like channel membership is, since not every VM ever receives a
message). Table sized from `stadium_max_vm_count()`, same pattern as tasks 3.1/3.2.
Best-effort, not atomic across subscribers -- a failed allocation or full queue skips just
that one subscriber and rolls back its own allocation; not a separate ruling, the natural
reading of the task's own check (ledger/`stadium_conserved()` invariants hold under partial
delivery too), documented as such in the header. Added `sk_hermes_pending_count()`/
`_peek()`/`_pop()` as the read/drain primitives task 3.4's real checkpoint-driven drain
will build on -- this task's own self-test uses them directly for cleanup since no
checkpoint hook exists yet. New console line confirmed live on all three architectures:
`Kernel-Hermes: N pending-queue slots` (**50 amd64, 202 aarch64, 50 riscv64** -- tracks
the channel/switch-signal tables' own per-arch sizing exactly). Self-test (synthetic
publisher lo=7, three synthetic subscribers lo=8/9/10, N=2 publishes to a 3-member
synthetic channel) confirms `sk_hermes_publish()` returns 3 each time, each subscriber's
queue holds exactly 2 afterward, ledger audit and `stadium_conserved(publisher)` hold
mid-publish, then confirms the same invariants return to baseline after draining every
queue by hand (`pending_peek()`/`release()`/`pop()`) -- `PASS` on all three architectures.
`dict_hash` for Hermes (`0xc95ef2d92fa0781f`) and Hestia (`0xfbde9fe105fd3b3d`) identical
across all three architectures, unmoved from tasks 3.1/3.2's values (this task touches no
capsule).
- [x] **3.4** — **Drain at the outermost checkpoint** (§XLIII.3–.5). Reuse
`sk_vm_at_outermost_interpret()`; one message per checkpoint **[needs ruling 3.0e]**.
*Check:* a nested interpret does not drain; a queued payload is interpreted exactly once at
depth 1.
2026-09-22 · `logs/20260922-010622/amd64/`, `logs/20260922-011130/aarch64/`,
`logs/20260922-011648/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). **Finding, amended into `FABRIC-3.5.md` §XLIII.5** (caught by `advisor()` before
writing the naive version, not found live): §XLIII.5's "recursive drain is prevented for
free" via `g_vm_interpret_depth` is true but covers only same-message re-drain, not the
separate same-VM reentrancy hazard `FABRIC-3.md` §XX had already named for Hera
specifically (`VMCallState` saves `rsp`/`exit_colon`/`ecw_nesting` only, never
`input_buffer`/`input_length`/`input_pos`) -- draining calls `vm_interpret()` on the same
`vm` whose own `vm_interpret()` call is still paused mid-word at the checkpoint, which
would silently truncate the enclosing REPL line or LOAD block if the cursor isn't saved
and restored by hand. `sk_hermes_drain_checkpoint()` (kernel_hermes.c) snapshots
`input_buffer`/`input_length`/`input_pos`/`mode`/`error`/`abort_requested` around the
`vm_interpret()` call and restores all six -- `mode` forced to `MODE_INTERPRET` for the
duration (a checkpoint reached mid-colon-definition must not compile the payload's words
into the enclosing definition); `error`/`abort_requested` restored so a bad message can't
abort the enclosing execution. Placed in `vm_core.c`'s existing cooperative checkpoint
**before** the switch-signal block, not after (`sk_vm_context_switch()` does not return
until something switches back, so a drain placed after it would silently never run on any
checkpoint that switches). Gated behind a system-wide pending-total counter
(`sk_hermes_pending_total`, maintained in `queue_push()`/`sk_hermes_pending_pop()`) so the
common no-message-in-flight case costs one integer read per word dispatch, not a
`stadium_max_vm_count()`-sized queue-table scan -- flagged by `advisor()` as a real
hot-path cost against this project's own +0.0603% measurement floor, not deferred.
Self-test (kernel_main.c) publishes a real, stack-neutral payload (`"1 2 + DROP"`) to
Hermes (a real, already-born VM, not synthetic -- needed a genuine live dictionary),
proves the depth gate via `VM-EXEC`-ing the existing harmless colon word `WELCOME`
(`capsules/hermes/init.4th` block 4855) into Hermes from Hera's context -- a real, already-
proven-safe nested `vm_interpret()` call (`mama_forth_words.c`'s own VM-EXEC mechanism) --
confirming the message does NOT drain at the resulting depth 2, then calls
`sk_hermes_drain_checkpoint()` directly from this self-test's own genuinely-outermost C
context and confirms it DOES drain exactly once, with a further call a clean no-op.
Deliberately avoided the block/`LOAD` mechanism for the nested case -- real but touches
real disk-backed block storage, which a throwaway diagnostic has no business perturbing.
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
task touches no capsule).
- [x] **3.5** — **Payload bound and chunking** (B4) **[needs ruling 3.0d]**. Max 1024 bytes per
message; larger payloads sent as ordered chunks, reassembled before drain. *Check:*
1024-byte payload single message; 3000-byte payload chunked and reassembled byte-exact;
1025-byte single-message send refused.
2026-09-22 · `logs/20260922-063122/amd64/`, `logs/20260922-063629/aarch64/`,
`logs/20260922-064147/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). Sizing decision (checked with `advisor()`, not itself a separate ruling -- see
the header's own note): `SK_HERMES_CHUNK_MAX_PAYLOAD` (1024) bounds every message's
`payload_len` **uniformly**, chunked or not -- a chunk carrier is
`[SkHermesChunkHeader][content slice]` where the slice is capped at
`1024 - sizeof(header)`, not 1024 itself. Rejected the literal alternative (slice up to
1024, carrier up to `1024+sizeof(header)`): under that reading a chunk carrier could never
be handed to `vm_interpret()` as-is, making a future chunk-aware drain a special case
instead of the same one-block invariant every other message already satisfies.
`sk_hermes_publish()` (task 3.3) now enforces this bound directly, closing the gap that
task's own doc comment explicitly parked ("3.3 does not enforce a payload size limit").
**Deliberately no chunking-SENDER API** (`sk_hermes_publish_chunked()` or similar) --
`advisor()` flagged that building one raises a real memory-lifetime question kernel-Hermes
has never answered (chunk buffers would need to stay alive until every subscriber drains
them, and nothing here can know when that is); sending is a loop pattern a caller writes
with `sk_hermes_chunk_count()` + `sk_hermes_publish()`, demonstrated by this task's own
self-test rather than hidden behind a new allocator. `sk_hermes_reassemble()` is pure and
memory-agnostic (caller-owned input chunks, caller-owned output buffer) -- validates
`msg_id` agreement, exact `seq` coverage `{0..n_chunks-1}` with `is_last` on exactly the
last, and every non-final slice at exactly `SK_HERMES_CHUNK_MAX_SLICE` bytes, before a
single `memcpy`; the reassembled total length is computed once from validated seq
completeness and checked against `out_buf_cap` once, so a too-small output buffer is
refused cleanly with zero partial copy rather than order-dependently on whichever chunk
happens to overflow. Self-test (kernel_main.c) proves exactly the task's three checks: a
1024-byte payload as one message; a 1025-byte single-message send refused outright, no
allocation, ledger untouched; a 3000-byte payload split into 3 chunks, drained off a
synthetic subscriber's own pending queue (task 3.3 primitives), reassembled, and confirmed
byte-exact against the original. Ledger audit and `stadium_conserved()` hold before and
after. `dict_hash` for Hermes/Hestia unmoved and identical across all three architectures
(this task touches no capsule).
- [x] **3.6** — **ACK/NACK and private-channel negotiation** (B1) **[needs ruling 3.0a]**.
`request` → `grant`/`deny` on the common channel; a grant creates a private topic;
close tears it down. ACK/NACK are message types on the existing allocator.
*Check:* grant path, deny path, close path; heat conserved (ledger + `stadium_conserved()`)
across all three; a NACK'd request leaves no topic behind.
2026-09-22 · `logs/20260922-070455/amd64/`, `logs/20260922-071004/aarch64/`,
`logs/20260922-071525/riscv64/` (the successful runs; `logs/20260922-065946/amd64/` is a
genuine first attempt that hit a real bug, caught by the self-test itself before this task
was called done -- kept, not deleted, per the never-delete-logs convention) — all three
reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean (38 files,
0 violations, unchanged -- no capsule touched).
**Design finding (checked with `advisor()`):** the ruling's "over the common channel"
cannot mean `sk_hermes_publish()`-style fan-out -- that sets `msg->to` to whichever member
it is iterating, so every common-channel member would receive its own copy of a request
believing it was the addressee, drawing heat for a message only one VM should ever see.
`messaging.4th`'s own `CH-REQUEST ( type from to paddr plen -- )` carried an explicit `to`
for the same reason -- point-to-point addressing was in the original protocol from the
start; "over the common channel" means every VM is reachable from birth (task 3.2), not
that the exchange itself fans out. Extracted `sk_hermes_send_one()` from
`sk_hermes_publish()`'s own per-subscriber body (alloc/set-fields/find-or-create-queue/
push/roll-back-on-refusal) so both callers share one code path and the ledger can never
diverge between them -- `sk_hermes_publish()` now just loops it per member.
Message types: `CH_REQUEST`/`CH_GRANT`/`ACK`/`NACK`/`CH_CLOSE`, chosen clear of
`messaging.4th`'s own live/reserved type space (2/3/4/7/8/9/253/255), not itself a ruling.
"A deny is a NACK" (SXLV.1) -- no separate `CH_DENY` type. ACK sent once, for the ruled
channel-open+delivery moment. The grant/deny *decision* is a plain caller-supplied
`approved` bool (named for what it is, not `allow`) -- task 3.7 replaces the call site that
produces it with a real `ACL.4th` query, not this function's signature. Close authority is
membership alone (either party may close a channel it belongs to) -- narrowest defensible
rule given both are already trusted members, not itself a ruling.
**Sibling case `advisor()` flagged as the one a green boot would hide**, beyond the task's
own three checks: an `approved==1` respond() whose channel creation itself fails (table
exhausted) must still fall through to NACK, not a silent false grant or a half-open
channel -- self-test exhausts the entire channel table, confirms the fallback, then
restores capacity.
**Bug found and fixed before this was called done:** the first self-test draft
(`logs/20260922-065946/amd64/`, `FAIL`) called `sk_hermes_pending_pop()` directly on the
deny path's dropped request copy without releasing it first -- `pending_pop()` only
advances the queue per its own doc comment, so the message's Stadium heat was never
returned, leaking held heat and failing the self-test's own `held0`/`held1` baseline check.
Fixed to peek+release+pop, matching every other drain in the same self-test; re-verified
PASS on all three architectures after the fix.
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
task touches no capsule). Artemis's own `dict_hash` differs run-to-run as already
documented (task 3.1's own finding) -- shared `disk/artemis.img` state accumulated across
this session's many prior boots, not an architecture divergence.
- [x] **3.7** — **Channel-open policy hook** **[needs ruling 3.0b]**. Kernel-Hermes asks
`ACL.4th`, never decides in C, never gates on `zuse_session`. *Check:* a denied open is
denied by FORTH policy, with the C unchanged when the policy changes.
2026-09-22 · `logs/20260922-075138/amd64/`, `logs/20260922-075649/aarch64/`,
`logs/20260922-080212/riscv64/` (the successful runs; `logs/20260922-074119/amd64/` and
`logs/20260922-074628/amd64/` are two genuine prior attempts that hit real bugs, caught by
the self-test itself before this task was called done -- kept, not deleted, per the
never-delete-logs convention) — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations — one block added inside the
existing `ACL.4th`, no file added). `dict_hash` identical across all three architectures
for every VM (Hera `0xbb2b51e7595464ce`, Hermes `0xc95ef2d92fa0781f`, Hestia
`0xfbde9fe105fd3b3d`, Artemis `0x7f18214489036b39`).
Added `HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? )` at `capsules/ACL.4th` block 4008
(default: approve everything) — the one word policy authors edit; `sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester)`
(`kernel_hermes.h`/`.c`) is the C-side query that calls it, and never itself decides. Uses
plain word-dispatch (`vm_find_word()` + the entry's own `func` pointer) against the
*target* VM's own dictionary and data stack, not `vm_interpret()` — deliberately, since
`HERMES-CHANNEL-OPEN?` is a fixed known name, and this avoids task 3.4's input-buffer
cursor-preservation hazard entirely (this touches the target's data stack only, never its
input buffer). **Fails closed, not open**: no policy word present (e.g. a VM that never
loaded `ACL.4th`), a policy-word error, or stack underflow all return "denied," matching
`.claude/CLAUDE.md`'s own posture that absence of policy must never mean "always allow."
A broken policy word's own error state is cleared on the target VM before returning, so it
cannot leak into whatever else that VM is doing.
**Two real bugs found and fixed before this was called done, not a clean first pass:**
1. (`logs/20260922-074119/`) The first draft called `entry->func(target_vm)` directly
without setting `target_vm->current_executing_entry` first — colon words dispatch
through `execute_colon_word()`, which reads its own body address from that field and
silently no-ops if it's `NULL` (`vm_core.c:730`). Every call silently did nothing,
leaving the pushed requester args untouched on the stack rather than erroring — no
crash, no obvious symptom, just a wrong answer, which is why it needed the self-test
(not a crash) to catch. Fixed by setting `current_executing_entry` before the call and
restoring the caller's own value after (in case the target VM is ever called into
mid-dispatch elsewhere later).
2. (`logs/20260922-074628/`) Still `FAIL`, with debug instrumentation left in from
diagnosing bug 1 (`entry=... func=...`, `error=0 dsp=1`/`error=0 dsp=2` printed, then
`FAIL`). **No intermediate commit exists for this attempt, so its precise defect is
not reconstructable now** — the log records the symptom and nothing more. Debug prints
were removed; the third attempt (`075138`/`075649`/`080212`) passed clean on all three
architectures with no debug output.
Self-test in `kernel_main.c` proves the task's check four ways against the **same
unchanged C function**: (a) Hera's default `HERMES-CHANNEL-OPEN?` approves; (b)
redefining that same word live on Hera to deny flips the answer with zero C change; (c)
restoring the approve-default flips it back; (d) Hermes — who never loads `ACL.4th` at
all (grep-confirmed: only `init.4th`/`ACL.4th`/`zuse.4th`/`block-acl.4th` reference it) —
is correctly refused closed, not silently approved, proving the fail-closed behaviour on a
VM with no policy at all. A fifth check wires the policy result straight into task 3.6's
`sk_hermes_channel_respond()` end to end: a denied policy really produces a NACK and no
channel, with the ledger audit and `stadium_conserved()` holding before and after —
exactly task 3.6's own deny-path shape, reused rather than re-derived.
**Not yet done, by scope**: the actual call site inside `sk_hermes_channel_respond()`'s
caller still takes a plain `approved` bool (task 3.6's own signature, unchanged, as
promised) — this task builds and proves the query function itself; wiring it as the
*only* source of that bool at a real call site is deferred to whichever later task first
needs a live (not self-test-only) channel-open decision, consistent with every other
Phase 3 task before Stage C being self-test-only. `dict_hash` for Hermes/Hestia unmoved
from task 3.6's values, as expected (neither VM loads `ACL.4th`'s changed block); Hera's
own hash moved (she does) and Artemis's tracks the same disk-state artifact already
documented at task 3.1.
- [x] **3.8** — **Stage C: cut over `BLK-ATTACH-EVENT` alone** (§XXXIV.3). One layer owns it;
FORTH Hermes still routes every other type. *Check:* the real Hera↔Artemis attach path
works end to end on all three ISAs; ledger and `stadium_conserved()` true before/after;
**`fleet_conserved` is not evidence** (§XXXIX).
2026-09-22 · Final acceptance: `logs/20260922-105501/amd64/`,
`logs/20260922-105758/aarch64/`, `logs/20260922-110304/riscv64/` — `disk/artemis.img` and
`disk/thumbdrives/zuse-thumb-ident.img` reset (`git checkout --`) before each of the three,
per task 0.0's own finding, so all three are directly comparable, no shared-disk confound.
All three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean
(38 files, 0 violations — one C constant added, one existing FORTH word's tail edited, no
capsule file added/removed). **`dict_hash` identical across all three architectures for
every VM**, including Artemis: `0xa8999a854545f274` / `0xb8052b99f1d75f59` /
`0xdf59e557986c9009` / `0xecad3d867c9bfec2`.
**What actually cut over.** Only the reply leg (Artemis → Hera ack) was real FORTH
messaging traffic to begin with — confirmed by reading, not assumed: the request leg
(`repl.c`'s own `"<dev-addr> HERA-BLK-ATTACH-REQ" "Artemis" VM-EXEC`) was already a direct
VM-EXEC with no type tag, forced by Hera's own pre-existing inability to load
`common:messaging.4th` (its own header comment says why). So this task's real scope was:
`capsules/artemis/init.4th`'s `HERA-BLK-ATTACH-REQ` (block 4858) no longer ends in
`BLK-ATTACH-EVENT 2 0 ATTACH-ACK-BUF ATTACH-ACK-LEN @ 0 MSG-SEND` (FORTH's arena/MSG-TICK);
it now ends in `ATTACH-ACK-BUF ATTACH-ACK-LEN @ KH-BLK-ATTACH-SEND DROP`, a new C word
(`repl.c`) wrapping `sk_hermes_send_one()` (task 3.6). Delivery is task 3.4's already-wired
`sk_hermes_drain_checkpoint()` calling `vm_interpret()` on Hera with the identical payload
text — `BLK-ATTACH-ACK` (`repl.c`, unchanged) fires exactly as before, just reached by a
different layer. `SK_HERMES_MSG_TYPE_BLK_ATTACH` (`kernel_hermes.h`) deliberately reuses
`BLK-ATTACH-EVENT`'s own value (9), not a fresh kernel-Hermes-space number — §XXXIV.2's
partition rule is about which layer owns a message, not about disjoint numbering, and
keeping the value documents this as a cutover of the same logical message.
**Evidence for the check, and its honest limit.** `sk_word_blk_attach_ack()` — the real
drain target, since `BLK-ATTACH-ACK` is exactly the word the drained payload calls — prints
the kernel-Hermes ledger and `stadium_conserved(Artemis)` right there
(`Kernel-Hermes BLK-ATTACH-EVENT (real, Stage C): ledger held=2048 pulled=241664
returned=238879 consumed=737 stadium_conserved(Artemis)=true`, identical on all three
architectures). This is evidence for the **mid-hold instant** — before
`sk_hermes_drain_checkpoint()`'s own `release()`/`pop()` run, which happen after this
handler returns, in the caller — not literally "after the full cycle." That's a real
instant to check, not a weaker substitute: task 2.7 established the four-term
`stadium_conserved()` form holds at every instant, mid-hold included, so this is genuine
evidence, just not evidence for the post-release state specifically. Recorded honestly per
`advisor()`'s review rather than overclaiming "before/after."
**Real finding: kernel-Hermes's first live (not self-test) exercise hung, twice, before
the actual bug was found — see the findings log below for the full account
(`vm_ptr()` translation, and a separate, still-unexplained first hang).** Both are recorded
there, not restated here.
**Logging note (`advisor()` caught this before commit):** the evidence line went through
three revisions. First cut used `console_println` twice (send-side pre-send + ack-side
after) — flagged as unconditional production output on every real USB attach, forever,
exactly the pattern memory `project_production_logging_cleanup_needed` already names.
Tried `log_message(LOG_DEBUG, ...)`, confirmed invisible in the actual serial-log capture;
tried `log_message(LOG_INFO, ...)`, **also invisible** — confirmed live that `repl.c`'s own
pre-existing `log_message()` calls, at every level, do not appear anywhere in a real boot's
serial log in this build (`log_message()`'s `fprintf(stderr, ...)` is not wired to the
serial console here — a build characteristic, not something this task introduced or fixed).
Settled on a single `console_println` (the ack-side one; dropped the send-side line
entirely) — this fires once per real USB attach, not per word, so it is not the
hot-path-spam case that memory warns about, and it is the only channel that is actually
visible in the acceptance logs this document's own standing rules require as evidence.
**Superseded intermediate logs, kept per the never-delete convention:**
`logs/20260922-102354/amd64/`, `-102948/aarch64/`, `-103458/riscv64/` were the first
passing three-ISA triple, still carrying the two-`console_println` (pre-send + after)
design; `logs/20260922-104229/amd64/` and `-104902/amd64/` were the `LOG_DEBUG` and
`LOG_INFO` logging-channel iterations described above, both confirming `log_message()`'s
invisibility rather than producing new acceptance evidence. All superseded by
`105501`/`105758`/`110304`, cited above, once the disk-reset-before-each-arch procedure and
the final single-line evidence design were both settled.
- [x] **3.9** — **Stage D: `CONSOLE-CMD-EVENT` (type 7)** (§XXXIV.3). One layer owns it; FORTH
Hermes still routes every other type. *Check:* a real interactive console-session relay
works end to end on all three ISAs, via `USE` into a live WIREBIND identity followed by a
plain (unquoted) console line; the same task 3.8 evidence discipline (ledger +
`stadium_conserved()`, `fleet_conserved` not accepted as evidence).
2026-09-22 · **Resumed and closed** — Captain Bob ruled 2026-09-22 to land the send-side
cutover first (already written, per the pause note below) and fix the task 3.8
payload-aliasing defect separately; that sequencing decision stands, the aliasing fix is
still its own open item (findings log above). Final acceptance:
`logs/20260922-132142/amd64/`, `logs/20260922-132512/aarch64/`,
`logs/20260922-132848/riscv64/` — `disk/artemis.img` and
`disk/thumbdrives/{zuse,bob}-thumb-ident.img` reset (`git checkout --`) before each of the
three, disk-reset-before-each-arch procedure unchanged from task 3.8. All three reach
`[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean (38 files, 0
violations). **`dict_hash` identical across all three architectures for every VM**,
matching task 3.8's own baseline exactly: `0xb8052b99f1d75f59` (Hera) /
`0xecad3d867c9bfec2` (Hermes) / `0xdf59e557986c9009` (Hestia) / `0xa8999a854545f274`
(Artemis).
**What actually cut over**, matching task 3.8's own precedent: `sk_repl_dispatch_line()`'s
"`CONSOLE-CMD-EVENT 0 3 S\" ...\" 0 MSG-SEND`" FORTH-string-interpret is gone; a plain
(unquoted) console line typed while paired to a live WIREBIND identity now calls
`sk_hermes_send_one()` (`repl.c`) directly, with `SK_HERMES_MSG_TYPE_CONSOLE_CMD`
(`kernel_hermes.h`) deliberately reusing `CONSOLE-CMD-EVENT`'s own value (7), same
partition-rule reasoning task 3.8 already established for `BLK-ATTACH-EVENT`. Delivery is
task 3.4's `sk_hermes_drain_checkpoint()`, calling `vm_interpret()` on the target identity
VM with the payload text — the console line itself, unwrapped, since kernel-Hermes's
payload is raw bytes, not a FORTH string literal.
**Real finding, found live verifying this exact task, not folded into the send-side
cutover's own write-up:** kernel-Hermes's drain only ever runs as a side effect of
`vm_interpret()` being called ON the target VM (`vm_core.c`'s own outermost-interpret
checkpoint hook, task 3.4) — it has nothing to do with the old FORTH `MSG-TICK` word. Task
3.8's target was Hera herself, who is *always* being interpreted (the interactive REPL
loop), so her own queue drained as a side effect of ordinary console activity. Task 3.9's
target is a WIREBIND identity's own `~user` VM — a passive receiver nothing else drives.
Confirmed live: `sk_hermes_send_one()` returned `0` (queued correctly) but the payload sat
undelivered indefinitely — no crash, no error, matching `FABRIC-3.5.md §XXXV.0`'s own named
failure signature exactly. Root cause: `sk_repl_idle()`'s existing round-robin pump
(`SK_MSG_PUMP_BATCH`/`g_msg_pump_cursor`, built for the *old* FORTH `MSG-TICK` system) only
gives a live VM an interpret tick if `vm_find_word(vm, "MSG-TICK", 8)` finds an
ACL-allowed entry — a VM minted with `MINT_PERSONALITY_STD79_LOCKDOWN` (FORTH-79/83
standard words only, no `common:messaging.4th`) never has that word, so the pump silently
skipped it forever, and kernel-Hermes's drain checkpoint never got a chance to fire.
**Fixed in the same pump loop** (`repl.c`, `sk_repl_idle()`): an unconditional, direct call
to `sk_hermes_drain_checkpoint((VM*)ent.vm_ptr)` for every live non-Hera VM visited each
beat, independent of the MSG-TICK/VM-EXEC dispatch below it — no FORTH word required,
since kernel-Hermes's own drain is a plain C function designed to be called standalone
(`kernel_main.c`'s own boot self-test already calls it exactly this way). Verified live:
relay now delivers, typically one idle beat after the send (bounded, not instant — the
pump runs once per idle cycle, matching the existing MSG-TICK dispatch's own latency
characteristic, not a regression this task introduced).
**Second real finding, also worth recording though it did not block this task:** the
`FABRIC-3.md §XXX` (2026-09-15) documented `USE`/BINDSTEP crash ("interactive `USE
<identity>` on a freshly-attached identity halts the kernel outright") **did not
reproduce** in this task's own live testing, on any of the three architectures, across
several repeated `USE rajames` calls on a freshly-WIREBIND-attached identity. Not chased
further — either something in the intervening 3.6-series work fixed it as a side effect,
or the specific reproduction conditions differ from this task's own test shape. Recorded
as a finding, not claimed as a fix (no root-cause investigation was done here).
**Also found and fixed along the way, its own separate commit
(`ac4d431`, already landed before this task's own final acceptance run):**
`zuse_root_pubkey_known` never got set after a same-session fresh genesis mint, silently
blocking WIREBIND for every identity attach on any boot that reset `artemis.img`
fresh — which is exactly what this project's own acceptance convention does before every
run. See that commit's own message for the full account; not restated here.
**A prompt-format change was requested mid-task (Captain Bob), attempted, found to
regress live (`[zuse@Artemis]` instead of the intended reformat), and reverted back to the
original, unmodified `[zuse@Hera]`-style format before this task's final acceptance run** —
confirmed by every log cited above. The reformat needs real new state (separating "which
identity" from "which machine," last discussed as needing changes across 8+ save/restore
call sites in `mama_forth_words.c`) and is deliberately deferred to its own task rather
than interleaved with this one's live debugging. Not tracked as a numbered task yet —
pick a number when it's actually scheped.
- [x] **3.10** — **Stage D: `ELEVATE-REQUEST` (type 8)**, the same one-type-per-task check as
3.8/3.9. Split out as its own explicit item (previously bundled into 3.9's own text without
a checkbox) per this document's own carry-forward-discipline rule (§XXXI.2).
2026-09-22 · **Closed.** Final acceptance: `logs/20260922-165122/amd64/`,
`logs/20260922-165333/aarch64/`, `logs/20260922-165635/riscv64/` — all three reach
`[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean (38 files, 0
violations). **`dict_hash` identical across all three architectures for every VM**
(`0x25052a7b8d02b81e` Hera / `0x7011e083c3259321` Hermes / `0x26317a2a0d37e4b0` Hestia /
`0xc14d3370d46fcf08` Artemis) — changed from every prior acceptance run in this document
(`word_count` 529→530, one new C word registered) but consistent across all three
architectures, matching §XXXIV.6's own rule that a *changing* hash is expected and only
*divergence* between architectures is the stop condition.
**Reachability verified live before writing any code, per `advisor()`'s own steer** — not
grep alone (`FABRIC-3.5.md §XXXV.0`/trap #2): `grep -rn "SEND-ELEVATE" capsules/
experiments/ docs/` found zero callers of `SEND-ELEVATE-REQUEST` anywhere (matching the
`init-l8-*` zero-reference precedent, §XXII.2), so this task genuinely has no automatic
trigger — but `FIND SEND-ELEVATE-REQUEST`, `FIND ELEVATE-GRANT`, and `FIND CH-REQUEST`
(correct usage: `FIND` parses the next word from input directly, not `S" ..." FIND` — the
latter faulted live, corrected mid-check) all resolved to real, nonzero dictionary
addresses on a live Hera boot. **Correction to a prior finding, found live in the course of
this check:** task 3.8's own write-up claimed "Hera's own pre-existing inability to load
`common:messaging.4th`" — false. `capsules/init.4th` (Hera's own `MAMA_INIT` capsule)
loads `common:messaging.4th` at line 21, and `SEND-ELEVATE-REQUEST` lives and works in her
dictionary right now. Task 3.8's own actual scope (only the reply leg was real FORTH
messaging traffic) stands unaffected by this correction — the false claim was about *why*,
not about *what* task 3.8 did. `SEND-ELEVATE-REQUEST` is a real, complete, directly-callable
entrypoint (H.5/H.8's own design: any VM wanting word-ACL elevation calls it with its own
pubkey and the target word name) with no current automatic trigger, not dead/unreachable
code — verified live by calling it directly from Hera's own console on all three
architectures: `0 0 0 0 S" DUP" SEND-ELEVATE-REQUEST` (a deliberately-invalid all-zero
pubkey, so `ELEVATE-GRANT` correctly refuses without granting anything real — the check is
that the full pipeline runs, not that a grant succeeds; confirmed `DUP` itself unaffected
afterward). Evidence line fires immediately, no idle-pump delay needed here (unlike task
3.9's WIREBIND-identity target): Hera is both sender and receiver in this test, and she is
always being actively interpreted, matching task 3.8's own Hera-target precedent exactly.
**What actually cut over.** `SEND-ELEVATE-REQUEST` (`messaging.4th`, block 5040) no longer
ends in `ELEVATE-REQUEST MY-CH-ID @ 0 ELEVATE-REQ-BUF ELEVATE-REQ-LEN @ CH-REQUEST`; it now
ends in `ELEVATE-REQ-BUF ELEVATE-REQ-LEN @ KH-ELEVATE-SEND DROP`, a new C word (`repl.c`)
wrapping `sk_hermes_send_one()`, registered unconditionally for every VM
(`sk_repl_register_words()`, same site `KH-BLK-ATTACH-SEND` already uses). `from`/`to` are
derived from the calling VM and `sk_get_mama_vm()` directly in C, never taken from the
stack — a real correctness improvement over `CH-REQUEST`'s own "initiator-only gate...
refuses if `from` doesn't match `MY-CH-ID`", which existed only because a caller *could*
pass the wrong stack value; deriving `from` from the C-level calling VM's own identity makes
that spoof structurally impossible rather than merely gated. `SK_HERMES_MSG_TYPE_ELEVATE_
REQUEST` (`kernel_hermes.h`) deliberately reuses `ELEVATE-REQUEST`'s own value (8), same
partition-rule reasoning as tasks 3.8/3.9. Delivery is unchanged task 3.4 machinery
(`sk_hermes_drain_checkpoint()` → `vm_interpret()` on Hera with the payload text, which
calls `ELEVATE-GRANT` exactly as before). No new static-buffer lifetime caveat this time —
the payload-aliasing fix landed before this task started, so `g_kh_elevate_buf` needed no
`g_kh_blk_attach_buf`/`g_kh_console_cmd_buf`-style deferred-defect note.
**Found, not fixed (Captain Bob's Law): `CH-REQUEST` (`messaging.4th` block 5031) is now
dead code.** It had exactly one real caller (`SEND-ELEVATE-REQUEST`'s own old tail, just
removed); nothing else in `capsules/` calls it. Left in place, not removed — orphaning it is
a side effect of this cutover, not this task's own scope to clean up.
- [x] **3.11** — **Phase 3 gate.** No FORTH-owned live message types remain; Stage B evidence
holds across a full boot on all three ISAs. **Do not start Phase 4 until this passes.**
2026-09-22 · **Passes.** Tasks 3.8 (`BLK-ATTACH-EVENT`), 3.9 (`CONSOLE-CMD-EVENT`), and 3.10
(`ELEVATE-REQUEST`) — messaging.4th's entire live/reserved type space except the two
already-dead sentinels (`MSG-NACKED`/`MSG-DELIVERED`, never live to begin with per task
0.1/0.4) and `PAUSE-EVENT`/`RESUME-EVENT`/`KILL-EVENT` (Category A/dead, same source) — are
all real kernel-Hermes cutovers now, each independently verified live on all three
architectures with real evidence. Stage B's own conservation evidence (`stadium_conserved()`
holding across every cutover's own acceptance run, task 2.7's four-term form) has held
through every one of tasks 3.8/3.9/3.10's acceptance logs, cited in their own entries above.
**Phase 4 may begin.**
## Phase 4 — Category B strip — **COMPLETE 2026-09-22**
Only after every live type is cut over. **Hermes leaves `is_fleet_foundation` and
`kernel_main.c` here, not earlier** (§XXXIV.4). Per FABRIC-3.5.md §XXXIV.3, Stage E is one
atomic change set — 4.1–4.4 are checked off together, not independently bootable
intermediate states.
- [x] **4.1** — Strip `capsules/hermes/init.4th`. Deleted. `is_fleet_foundation` no longer
needs it — nothing else referenced this file.
- [x] **4.2** — Strip the FORTH routing table and slot-3 pairing convention.
`capsule_wirebind.c`'s slot-3 `VM-NAME-REG` registration call removed (already dead
since task 3.9's cutover moved name resolution to `sk_console_user_prefix()`).
- [x] **4.3** — Strip `capsules/common/messaging.4th`. Deleted. Two C-side dependents found
by full-repo grep (not just `capsules/`) and fixed as part of this same commit:
`capsule_console.c`'s `CONSOLE_IDENTITY_SRC` (dropped its `S" common:messaging.4th"
EXEC` / `MSG-CD-INIT` lines, kept `Block 4997`) and `capsule_mint.c`'s
`MINT_DEFAULT_PERSONALITY` (same two lines dropped, kept `WELCOME`). Every capsule that
loaded the file (`capsules/init.4th` block 2049, `capsules/artemis/init.4th` block 4854,
`capsules/hestia/init.4th` block 4987) had its load line + `MSG-CD-INIT` +
`MY-CH-ID`/`CH-ADD-MBR` setup removed; Artemis and Hestia kept their own
`STARTUP-BANNER`/`WELCOME` tails. `capsules/doe-campaign.4th` (`SETUP-HERMES`/
`SETUP-VMS`/`PHASE1-DOE`/`CD-TICK`/`SMOKE-CAMPAIGN`, blocks 4060–4064) reduced to
Artemis-only per explicit instruction (fold `SETUP-HERMES` strip into this same commit).
`capsules/zuse-eligibility.4th`'s header comment updated to point at the kernel-Hermes
drain checkpoint instead of the removed `MSG-DELIVER` path.
- [x] **4.4** — Remove Hermes from `is_fleet_foundation` and its birth from `kernel_main.c`.
`capsule_birth.c`'s `is_fleet_foundation` OR-chain drops the `"Hermes"` prefix check.
`kernel_main.c`: Hermes's own birth block removed, the Artemis-birth comment referencing
it fixed, `sk_vm_switch_signal_register(hermes_entry.vm_id)` + its guard removed, and the
"Kernel-Hermes common-channel fleet self-test" fixed to drop its unconditional
Hermes-liveness requirement (was about to false-FAIL post-strip). The drain and
channel-open-policy self-tests already had graceful `SKIPPED (Hermes not live)` /
`SKIPPED (Hera/Hermes not available)` fallbacks and needed no change — confirmed live
below, not just by inspection.
2026-09-22 · **Closed.** `mkcapsule --lint capsules/` clean: 36 files (was 38 — the two
Category B deletions), 0 violations. Full three-architecture acceptance, each run the
same sequence: clean boot → self-test block → hot-attach bob's blank identity thumbdrive
via QMP → `MINT` → detach/reattach to trigger `WIREBIND` → `USE rajames` → `1 2 + .`
relay round-trip.
- amd64: `logs/20260922-181141/amd64/qemu-amd64-20260922-181141.log`
- aarch64: `logs/20260922-181753/aarch64/qemu-aarch64-20260922-181753.log`
- riscv64: `logs/20260922-182136/riscv64/qemu-riscv64-20260922-182136.log`
Zero `UNKNOWN WORD` on all three. `PARITY:` lines carry only Hera/Hestia/Artemis — Hermes
genuinely absent, not just silent. **`dict_hash` identical across all three
architectures for every VM**: `0x0a0ad2497d33c3c0` (Hera, `MAMA_INIT`), `0xb1256603
f848e2b9` (Hestia), `0xc791409ac1715690` (Artemis) — matching §XXXIV.6's rule (a
*changing* hash is expected; cross-architecture *divergence* is the stop condition; none
occurred). Self-tests on all three: "Kernel-Hermes common-channel fleet self-test: PASS"
(confirming the 4.4 fix), "Kernel-Hermes drain self-test: SKIPPED (Hermes not live)" and
"Kernel-Hermes channel-open policy self-test: SKIPPED (Hera/Hermes not available)" both
graceful as predicted, every other self-test PASS. `mint → attach → USE → relay` verified
live and byte-identical across all three architectures: prompt correctly shows
`[rajames@rajames]` (task-3.9-era prompt fix holding), single `ok`/`ok>` cycle (CRLF fix
holding), `1 2 + .` → `3` delivered via `Kernel-Hermes CONSOLE-CMD-EVENT (real, Stage D)`
with `stadium_conserved(self)=true` on every run.
**Found, not fixed (Captain Bob's Law): `capsules/zuse-eligibility.4th`'s only policy
word, `ELEVATE-GRANT`, is now unreachable — and this file is still loaded at boot
(`capsules/init.4th:18`, `S" zuse-eligibility.4th" EXEC`), not dead/orphaned code.** Task
3.10 made `SEND-ELEVATE-REQUEST` (which called `KH-ELEVATE-SEND`, `repl.c`) live in
`messaging.4th`; this task deletes that entire file. `grep -rn "SEND-ELEVATE-REQUEST"
capsules/ src/ include/` after the strip: zero hits outside comments — the word no longer
exists anywhere, so nothing can reach `KH-ELEVATE-SEND` or `ELEVATE-GRANT` any more. This
is not stray dead code left over from a cutover; it is **Phase 8 PKI's own named-open-item
elevation entrypoint** (see this document's own ACL section / `docs/03-architecture/
word-acl/DESIGN.md` Phase 8) deleted as collateral of the Category B strip. A live capsule
is now shipping with a policy word nothing can call. Left as-is per this task's own scope
— **this needs a decision from Captain Bob before Phase 5 close-out, not just a cleanup
note**: either give `SEND-ELEVATE-REQUEST` a new home now (e.g. inline in
`zuse-eligibility.4th` itself, which already depends on nothing from the deleted file), or
explicitly accept that Phase 8 PKI work will need to build its own entrypoint from
scratch rather than resuming this one.
**Decision (Captain Bob, 2026-09-22): leave it unreachable.** `ELEVATE-GRANT` and
`KH-ELEVATE-SEND` stay in place, uncalled, until Phase 8 PKI begins; that milestone will
build its own entrypoint rather than resuming `SEND-ELEVATE-REQUEST`. No further action
this phase.
## Phase 5 — Close-out — **COMPLETE, tag `v2.1.0` cut 2026-09-22**
**Note on commit granularity:** tasks 5.1–5.4 landed as one commit (`a4ad14a`), a deliberate
deviation from the per-task-commit discipline this document has otherwise used since Phase 0.
Reason: all four tasks' write-ups accumulated in `FABRIC-3.6.md` across one continuous work
pass before any commit was made, so splitting the commit would have meant hand-splitting one
file's hunks after the fact rather than committing as each task actually finished — the
opposite of what per-task commits are for. Tasks 5.5–5.7 return to one-commit-per-task.
- [x] **5.1** — Isabelle/HOL pass. **Deliverable is the restated boundary**, explicitly
including §XXV.4's coverage loss — *not* a green build (§XXV.3).
2026-09-22 · **Closed.** `/home/rajames/CLionProjects/Isabelle2011-1/bin/isabelle build
-o threads=1 -D proof/` (memory-safe single-threaded flag per this session's own standing
rule) — exit 0, "Finished StarForth" in 41s (warm heap cache), zero `FAILED`/`error`/
`sorry`/`warning` across all 52 theories. This is the green build, included for
completeness, but per `proof/COVERAGE.md`'s own framing it isn't the deliverable.
**The restated boundary.** `proof/COVERAGE.md` (generated 2026-08-14, commit `346c793`,
unchanged since) scopes the entire suite to `src/word_source/*.c` — the 34 vendored
FORTH-word-implementation files, the 7 physics loops, Q48.16, and the 5 word-level ACL
policy theories. **None of Phases 0–4 of this reshuffle touched anything in that scope.**
Every file this reshuffle changed lives in `src/starkernel/` (kernel-only C, `kernel_
hermes.c`/`.h`, `repl.c`, `capsule_*.c`, `kernel_main.c`) or `capsules/*.4th` (Hera/
Artemis/Hestia init capsules, the deleted `messaging.4th`/`hermes/init.4th`) — both
entirely outside `proof/`'s own stated boundary from the start. **The clean pass above is
not regression evidence — nothing in `proof/`'s scope changed, so there was nothing this
reshuffle could have regressed there in the first place**; the 41s runtime is a warm
heap-cache replay, not a fresh re-verification, so it shouldn't be read as re-checking
anything either. It is included for completeness (task 5.1 asks for the build), not as
support for a no-regression claim the boundary argument above already makes on its own.
Separately, and for the same reason: none of the reshuffle's *own* new code
(kernel-Hermes's C implementation, the WIREBIND/MINT identity flow, the capsule
birth/switch-signal changes) is formally verified — none of that was ever in scope for
this suite, and still isn't. The suite's already-documented gaps (block-window
cache, TIB scan-shape variants beyond `forth_parse_word`, vocabulary-chain file-scope
statics, raw-pointer DictEntry navigation, the three divergent L8 mode-selector
representations — full list in `proof/COVERAGE.md`'s own "structurally NOT provable"
section) are unaffected by this reshuffle either way, since none of them intersect
`src/starkernel/` or `capsules/` at all. **Net effect on the boundary: none — restated,
not moved.**
- [x] **5.2** — Documentation sweep, grepping **by exclusion** (§XXVIII.3).
**CLAUDE.md's four errors: DONE 2026-09-19 (§XLIV), pointer to this document added.**
2026-09-22 · **Closed.** Every outstanding item resolved or explicitly settled:
- **`.claude/CLAUDE.md` itself, further updated** (beyond the four-errors pass): now
that Phase 4 has actually landed, its WIP banner claiming "no reshuffle code has been
written yet" was false — corrected to reflect Phases 0–4 complete. The "current fleet"
Tripod description (Hera/Hermes/Artemis, plus a stray "two Hermes instances" claim
that was stale even before this reshuffle) updated to Hera/Artemis/Hestia. Mama
dictionary word count corrected `453` → `530` (current `PARITY:M7.1a word_count=`),
with a caveat to verify against a live line rather than cite the number statically.
- **`MANIFEST.md` rides the strip (§XXII.5) — done.** `hermes/init.4th`'s full
block-by-block table (4100–4153) replaced with a deletion note (detail recoverable
from git history, not duplicated); `common/messaging.4th`'s deletion recorded in
"Deleted capsules"; `init.4th` block 2049, `doe-campaign.4th` blocks 4060–4062, and
`hestia/init.4th` block 4987 (its `COMMON-CH` join no longer exists) all updated to
match the post-strip live files, verified by direct read. Former Hermes range marked
UNASSIGNED. `mkcapsule --manifest capsules` reports zero conflicts, 36 capsules,
after the edit.
- **The `TRIPOD.md`/0.1 contradiction (punch item 26) — confirmed already resolved.**
`.claude/TRIPOD.md`'s "Immediate Goal" section carries its own 2026-08-13 correction
header stating the contradiction (Hera auto-spawning Hermes/Artemis at boot) was fixed
then, before the document was even marked superseded (2026-08-15). No further action
needed; the punch item was asking to confirm a fix already in place, not make one.
- **Item 42 (K-qualification) — checked against the currently-living corpus, clean; full
retroactive sweep of the closed archival corpus explicitly declined.** `.claude/
CLAUDE.md` and `FABRIC-3.6.md` (the two documents this reshuffle actually reads/writes
day to day) both already use qualified terms throughout (`fleet K`, `stadium_
conserved()`, `quota conservation`) with zero ambiguous bare-`K` hits. The closed
archival documents (`FABRIC-0.md`/`FABRIC-1.md`/`FABRIC-2.md`/`FABRIC-3.5.md`) carry
110 combined raw `\bK\b` regex hits — the large majority already correctly qualified
in context (the regex can't distinguish "fleet K" from a genuinely bare "K"), scattered
across ~20,000 combined lines of design record. A full manual audit is disproportionate
to the residual risk and contradicts this series' own stated discipline (`FABRIC-3.5.md`
§XLII.2: "design rulings are never restated... corrections are recorded, not silently
rewritten into archival history"). Declined, not silently dropped — recorded here as
the explicit decision.
- **The superseded subsystem docs' update-or-archive call — settled: archive, do not
update.** Recorded directly in `.claude/CLAUDE.md`'s own superseded-docs note: `TRIPOD.
md`/`HERMES.md`/`ARTEMIS.md`/`CONSOLE.md` stay frozen at their 2026-08-15 state.
`HERMES.md` in particular now describes a VM that no longer exists in this codebase at
all (not merely a superseded design) — noted explicitly rather than silently left
stale, but not rewritten, since rewriting a historical record to track a codebase it no
longer describes defeats its purpose as a record.
- **Item 45 — `experiments/bare_metal/README.md` block framing — was real, fixed.**
Line 149 still said "each block can hold up to 1024 bytes of source text," the same
wrong-kind-of-rule `FABRIC-3.5.md` §XLIV.1 found and corrected in `.claude/CLAUDE.md`
on 2026-09-19 (8 lines of 128 characters is 1024 bytes and still fails — the real
limit is per-line/per-line-count, not a byte total). Corrected to the real rule (64
chars × 16 lines, block range `[2048,5120)`, verify with `mkcapsule --lint`, not
`wc -c`) — this file had never received that correction.
- [x] **5.3** — `make sbom`; check `Created:` and `DocumentName` (§XXVI.2).
2026-09-22 · **Closed.** `syft` was not installed on this machine — installed to
`~/.local/bin` (user, not root; explicit approval obtained first). First regen fixed
`Created:` (`2026-07-24` → live) but `DocumentName` stayed wrong (`StarForth`) — traced to
a genuine Makefile bug, not a stale artifact: `Makefile:747` hardcodes `--source-name
StarForth` in the `sbom` target. Fixed (`--source-name LithosAnanke`, explicit approval
obtained first) and regenerated. Both fields now correct: `DocumentName: LithosAnanke`,
`Created: 2026-09-22T23:21:56Z`.
**Found and fixed, an ordering bug against 5.4:** this regen ran before the version bump
(5.4, below), so `sbom.spdx`'s `PackageVersion` came out `3.1.0` — correct at the moment
of generation, but stale the moment 5.4 landed. Root cause is a second, independent
finding: the hosted `Makefile` (which `sbom` actually runs from — `Makefile.starkernel`
is the kernel build, a separate file) carries its **own** hardcoded `VERSION ?= 3.1.0`
at `Makefile:17`, never mentioned by `.claude/CLAUDE.md`'s own "two independently tracked
version strings" note, which only documents `Makefile.starkernel`'s pair. Two hardcoded
copies of the same semantic value (the embedded StarForth engine version) in two build
files is real duplication-drift risk, not intentional independence like `LITHOS_VERSION`/
`VERSION`'s documented split. Bumped `Makefile:17` to `3.2.0` to match 5.4's engine-version
rule and re-ran `make sbom`: `PackageVersion: 3.2.0`, `Created` refreshed again. `sbom.spdx`/
`sbom.spdx.json`, the `Makefile:747` `--source-name` fix, and this `Makefile:17` fix all
ride this phase's commit.
- [x] **5.4** — `LITHOS_VERSION = 2.1.0`; engine `VERSION` per §XXX.6's rule; roadmap table
gains its `2.1.0` line in two live places (§XXVIII.2).
2026-09-22 · **Closed.** `Makefile.starkernel`: `LITHOS_VERSION` `2.0.0` → `2.1.0`;
engine `VERSION` `3.1.0` → `3.2.0` (minor per §XXX.6's rule — dictionary-visible changes
only: `BIRTH`'s registration widened, the messaging layer replaced by kernel-Hermes,
`PLOT`/`FB-*` relocated to Hestia; no FORTH-79 word semantics changed). The stale
version-comment block in `Makefile.starkernel` (which still said "even major = LTS" —
backwards relative to the ratified §XXX.1 odd/even rule — and had no `v2.1.0` entry)
replaced with the real ladder. `docs/lithosananke/ROADMAP.md`'s own "Release Versioning
Policy" section (the second of the two live places) was still the full retired
2026-08-28 policy, not just a stale table — replaced with the ratified §XXX policy text,
the old text kept struck-through in a `<details>` block for traceability, and the
board-by-board rollout section's "each cut is an LTS point-in-time cut" line corrected
(v2.x is the even/working line under the ratified policy, not LTS).
Verified the bump flows through the real build: `include/version.h` shows
`STARFORTH_VERSION "3.2.0"` / `LITHOS_VERSION "2.1.0"` after a clean amd64 rebuild.
Full three-architecture acceptance re-run (version bump is a `Makefile.starkernel`
change, so it gets the same three-arch boot as any kernel change): amd64
(`logs/20260922-192400/amd64/`) and aarch64 (`logs/20260922-192931/aarch64/`) both clean
on the first pass — zero `UNKNOWN WORD`, banner correctly reads `LithosAnanke v2.1.0` /
`StarForth Version 3.2.0`. **riscv64's first pass, `logs/20260922-193504/riscv64/` —
FAILED, committed as the audit record of the failure, not a fourth passing run** — hit a
`virtio_blk: vblk_io timed out` during the boot-time Zuse genesis mint (fence marker
write failed, not persistent); reached `[zuse@Hera] ok>` regardless, but flagged rather
than accepted on a degraded run. **riscv64's accepted run is the immediate retry,
`logs/20260922-194020/riscv64/`**, no code change in between: clean pass, genesis mint
succeeded, zero `UNKNOWN WORD`. Assessed as transient host I/O contention under TCG
(single qemu process, no
leaked processes, host memory tight but not exhausted at the time) rather than a
regression — this change touched only `Makefile.starkernel` version strings/comments and
two markdown files, no runtime code — and the clean retry on identical disk state/binary
confirms it. Reported here rather than silently retried-and-forgotten, per Captain Bob's
Law.
- [x] **5.5** — Resolve the stray `refs/heads/v2.0.1` and PR #1 (§XXVI.4). **Investigate, do
not delete.**
2026-09-22 · **Closed — investigated, nothing destructive done, nothing needs Captain
Bob's sign-off to become safe.**
- **`refs/heads/v2.0.1`** — `git log origin/master..origin/v2.0.1` is empty (0 commits);
`git log origin/v2.0.1..origin/master` shows 128. `origin/v2.0.1`'s tip
(`fcba528`) is the exact merge-base of the two branches — it is a strict ancestor of
current `master`, fully subsumed, not a divergent line. Its own tip commit message
confirms this by construction: "FABRIC-3.md: Task 1 closed -- master fast-forwarded to
v2.0.1, verified" (2026-09-04). Stale branch-ref hygiene debt, exactly as flagged —
safe to delete, **not deleted**, since that needs Captain Bob's explicit go-ahead per
Captain Bob's Law, not this task's own scope.
- **PR #1** — queried via the Gitea API directly (`gh` isn't installed and wouldn't talk
to this Gitea instance anyway). Not a stray reference at all: it's a PR from **this
same branch** (`claude/starshipos-tripod-kernel-reshuffle-itbjns`) against `master`,
`state: closed`, `merged: false`, `merged_at: null`, opened `2026-09-18T21:28:06Z` and
closed `2026-09-18T21:28:26Z` — 20 seconds later, before any of this reshuffle's actual
work existed on the branch. Already closed, already not merged, blocks nothing. Nothing
to resolve beyond recording what it is.
- [x] **5.6** — Merge to `master`; tag `v2.1.0`.
2026-09-22 · **Closed, explicit approval obtained first (this is a shared-branch
operation on the sole production line — the plan authorizing it is not a standing
grant, per `.claude/CLAUDE.md`'s own scope rule).** `master` was a strict ancestor of
this branch (`git merge-base --is-ancestor origin/master HEAD` — true, 0 commits unique
to `master`, 85 unique to this branch), so this was a clean fast-forward, not a merge
commit: `git push origin HEAD:refs/heads/master` (`e56974e..8edbd3c`), never checking out
`master` locally, so the uncommitted `FABRIC-3.md` WIP on this branch's working tree was
never touched. Tagged `v2.1.0` (annotated, `8edbd3c`) and pushed. `refs/heads/v2.0.1`
(task 5.5's finding — fully merged, safe to delete) also deleted this same pass, explicit
approval obtained first.
- [x] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
2026-09-22 · **Closed.** `FABRIC-3.5.md`'s provisional "design phase closed, not yet
archival" header replaced with the real CLOSED/ARCHIVAL form, naming `v2.1.0`, per its
own §XXVI.5 spec — prior status headers kept underneath, not deleted, matching this
series' own no-silent-rewrite rule. This document (`FABRIC-3.6.md`) closed the same way:
a CLOSED/ARCHIVAL status banner added above the `START HERE` section, which is kept as
historical record of how the session began rather than removed. Neither closure triggers
the `FABRIC-0 → -1 → -2 → -3` carry-forward chain (both documents are standalone topic
documents, not part of it) and neither touches `FABRIC-3.md`, which stays open for its
own topic. **Phase 5, and the Tripod/kernel reshuffle itself, are complete.**
---
## Findings log
*Defects and surprises found while executing. Reported, not fixed (unless the task was to fix
them). A finding that contradicts a `FABRIC-3.5.md` ruling must also be amended there.*
**2026-09-19, task 0.0 — shared `disk/artemis.img` confounds cross-ISA `dict_hash` comparison
unless reset between runs.** Not a code defect and does not contradict any `FABRIC-3.5.md`
ruling — `FABRIC-3.md` §XXXV.2 already documents that the disk is deliberately shared across all
three ISAs' `qemu` targets. But it means a same-order rerun of the three architectures will have
run 2 and 3 silently take Artemis's `resuming` branch instead of its `blank disk -- formatting`
branch, producing a different `dict_hash` for Artemis alone (Hera and Hermes are unaffected —
neither touches the artdisk at birth). Worth carrying forward: **any task in this document that
checks `dict_hash` identity across architectures and involves Artemis should restore
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` to their committed blank state
(`git checkout --`) before each architecture's run**, or the check will fail on a confound rather
than a real divergence.
**2026-09-22, task 3.8 — a FORTH `CREATE` buffer address passed to a new C word must go through
`vm_ptr()`, or it silently reads all-zero memory: no crash, no error, just a message that arrives
and does nothing.** `sk_word_kh_blk_attach_send()`'s first cut popped `paddr` (Artemis's own
`ATTACH-ACK-BUF` address) and raw-cast it directly to a host `const char *`
(`(const char *)(uintptr_t)paddr`) — `.claude/CLAUDE.md`'s own "Important Conventions" already
states stack values are VM-relative offsets (`vaddr_t`), not C pointers, and
`mama_forth_words.c`'s own VM-EXEC/VM-CALL words all translate a popped address the same way
(`vm_ptr(vm, (vaddr_t)caddr)`) before touching it — this word didn't, and the mistake compiled
clean, linked clean, and booted clean. Diagnosed with a temporary probe (reverted after capture,
per memory `feedback_revert_probes_after_capture`): `logs/20260922-101153/amd64/` and
`logs/20260922-101937/amd64/` (a second, redundant run of the same debug build, kept per the
never-delete convention rather than for any independent evidence) both show `DBG send: plen=25
n=25 buf=[]` — correct length, empty content — followed by `DBG drain-enter #2 payload=` (empty)
and a clean `DBG drain-return`.
`vm_interpret()` on an empty string is a no-op: no error, no `UNKNOWN WORD`, `BLK-ATTACH-ACK`
simply never ran, and the pending USB attach was silently never confirmed. Fixed by reading
`src` via `vm_ptr(vm, (vaddr_t)paddr)` instead of a raw cast (`repl.c`'s own comment on the fix
cites the precedent directly). This is exactly `FABRIC-3.5.md` §XXXV.0's named failure signature
("things here fail without saying anything") — carrying the rule forward explicitly for whoever
next writes a FORTH-facing C word: **any popped stack value that names a FORTH-visible address
goes through `vm_ptr()`/`VM_ADDR()`, never a raw pointer cast, unless the value was itself
explicitly formatted as a raw host pointer by the pushing code** (as `repl.c`'s own existing
`dev-addr` already legitimately is, built via `%llu` from a real `uintptr_t` at the VM-EXEC call
site — the exception that makes the rule easy to misapply by analogy).
**2026-09-22, task 3.8 — a separate, real hang, root cause not found; recorded as a genuine open
item, not folded into the `vm_ptr()` finding above.** The very first attempt to exercise the real
send/drain path (`logs/20260922-092154/amd64/`, before the `vm_ptr()` bug above was even
suspected) froze solid — serial log stopped growing entirely, QEMU pinned near 100% CPU, no
further output ever, for over 40 minutes of wall time before being killed by hand. This happened
*before* the empty-payload bug was diagnosed or fixed, so it is tempting to assume the same root
cause — but the empty-payload runs (`101153`/`101937`) did **not** hang; they drained cleanly
(if uselessly) and reached `ok>` on schedule. Something else froze that first boot, at
approximately the same point in the boot sequence, and it was never reproduced again across
every subsequent run this session (with or without the `vm_ptr()` fix). Per §XXXV.0's own
standing warning, an unexplained freeze in a brand-new, live-for-the-first-time nested-drain code
path (task 3.4's `sk_hermes_drain_checkpoint()` calling `vm_interpret()` on a VM that is itself
mid-dispatch, exercised live for the first time by this exact task) is not something to write off
as a fluke. Reported, not chased further here — `logs/20260922-092154/` is kept as the sole
evidence.
**2026-09-22, found while starting task 3.9 — task 3.8's `g_kh_blk_attach_buf` is a real
payload-aliasing defect, not a benign restatement of an existing hazard as that task's own
write-up claimed.** Discovered orienting for 3.9: booted amd64 with a second real identity drive
("bob") attached at boot alongside Zuse's own (`QEMU_EXTRA` at port 2,
`logs/20260922-111840/amd64/`), which puts two real USB-MSC devices through
`sk_repl_idle()`'s per-slot attach loop in one pass — two `KH-BLK-ATTACH-SEND` calls before
either drains. The ledger confirms it: `held=4096` (two admissions, 2×2048) at the first drain's
own evidence print, not `held=2048` as every single-device boot this session showed. Only one
device's identity ever completed (`Zuse: genesis minted...`); `S" bob" USE` afterward returned
`USE: bob not found` — bob's own WIREBIND pairing never happened.
**Root cause (following the code, not re-deriving it live):** `g_kh_blk_attach_buf`
(`repl.c`) is one static buffer, and `sk_hermes_send_one()` stores `payload_addr` as a caller-
owned pointer, not a copy. Two sends before either drain means both messages point at the same
address — whichever send wrote last. **Task 3.8's own write-up claim that this "carries the same
single-buffer-reuse shape `ATTACH-ACK-BUF` itself already had... not a new hazard this
introduces" is wrong, and is amended here rather than left standing.** FORTH's own `MSG-SEND`
had the identical pointer-aliasing shape, but never hit the window: `MSG-TICK` pumped from
`sk_repl_idle()` itself, draining between attaches in the same loop that queues them.
**Kernel-Hermes drains at interpret checkpoints, which do not fire during that idle-loop pass at
all** — the cutover didn't just change *how* delivery happens, it changed *when*, and that's what
opened a window FORTH's own design never had. Likely mechanism for why the second message
produced no visible failure either (inference, not confirmed): both messages probably drained
using the same (second-write) payload text, and the one carrying the wrong `dev-addr` most likely
hit `repl.c:246`'s `found_slot == 0 || !pending` early return — silent, no print, ahead of this
task's own evidence line. Not chased to confirm, per the scope decision below.
**Not fixed here.** This is a defect in already-committed, closed task 3.8 code, found while
scoping a different task — Captain Bob's Law: report, don't fix in passing. A real fix changes
`SkHermesMessage`'s own shape (task 2.1) to own its payload bytes rather than referencing a
caller's pointer, which is bigger than a `repl.c`-local patch: it touches every existing sender
(tasks 3.3/3.5/3.6/3.8) and needs its own three-ISA acceptance. `logs/20260922-111840/amd64/` is
kept as the reproduction — a second identity drive attached at boot is what exposes this; the
default single-drive boot this whole document's other acceptance runs used never will. Task 3.9
is paused pending Captain Bob's decision on when this gets fixed.
**2026-09-22 — CLOSED.** Captain Bob's own instruction, after 3.9/prompt-format both landed:
"finish the work" before cleanup/3.10. Fixed exactly as scoped above — `SkHermesMessage`
(`kernel_hermes.h`) gained an inline `payload_buf[SK_HERMES_CHUNK_MAX_PAYLOAD]` field;
`sk_hermes_send_one()` (`kernel_hermes.c`, the single funnel every sender/`sk_hermes_publish()`
already goes through) now `memcpy()`s the caller's payload into it and points `payload_addr` at
that copy, instead of storing the caller's own pointer. No sender or reader call site needed to
change — every existing reader (`sk_hermes_drain_checkpoint()`, `sk_hermes_reassemble()`) already
only ever reads through `payload_addr`, which still points at valid bytes of the same length,
now message-owned. `g_kh_blk_attach_buf`/`g_kh_console_cmd_buf` (`repl.c`, tasks 3.8/3.9) no
longer need to survive past their own send call — their doc comments corrected accordingly;
left `static` as a harmless convenience, not changed to stack-local (out of this fix's own
scope).
**Verified the original bug is actually gone, not just no-longer-observed.** Reproduced the exact
original scenario — `S" Captain Bob"...MINT`'d a real `rajames` identity sequentially first
(so both devices are genuinely eligible, not one blank/one real), then rebooted with `rajames`'s
now-minted drive attached at launch alongside Zuse's own (`QEMU_EXTRA`, port 2), reproducing two
simultaneous `BLK-ATTACH-EVENT` sends in one idle-loop pass before either drains
(`logs/20260922-140405/amd64/`: `ledger held=4096`, the same two-admission signature the original
bug's own log showed). **Before this fix, that scenario left `rajames`'s own WIREBIND pairing
silently never happening** (`FABRIC-3.6.md`'s own earlier finding: `S" bob" USE` returned `USE:
bob not found`). **After this fix, in the identical scenario, `WIREBIND: rajames attached and
ready` fires, `USE rajames` succeeds, and the Stage D relay still works on top of it** — confirmed
live, interactively, in that same log. `dict_hash` for Artemis differs from this document's other
acceptance runs in that one log only, expected and already-documented (task 0.0's own finding:
`disk/artemis.img` was deliberately left unreset across the two boots this reproduction needed,
so Artemis took the `resuming` branch, not `blank -- formatting`) — not a cross-architecture
divergence, since this reproduction was amd64-only by design.
Standard three-ISA acceptance (single-drive boot, disks reset before each arch, matching every
other task in this document): `logs/20260922-140634/amd64/`, `logs/20260922-140908/aarch64/`,
`logs/20260922-141236/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` identical across all three and matching every prior acceptance run in this document
exactly (unaffected — the fix changes message storage, not dictionary content). Both `USE
rajames` (task 3.9's earlier finding that the documented `FABRIC-3.md §XXX` USE/BINDSTEP crash
did not reproduce, confirmed again here) and the Stage D relay verified working on all three.
**2026-09-22, out-of-band request during task 3.9's own verification — console prompt format
changed on Captain Bob's direct instruction, not a reshuffle task, no task number.** The bracket
line prefix (`console.c`'s `emit_prefix()`, unchanged) used to read
`[<session-owner>@<active console/identity>]` — e.g. `[zuse@rajames]` after `USE rajames`, always
showing the authenticating superuser on the left regardless of which identity the console was
actually redirected to. Corrected across several rounds of live clarification to: once the
console is redirected into a WIREBIND identity's own console VM (`console_get_vm_name() !=
"Hera"`), show that same name on **both** sides — `[rajames@rajames]`, not `[zuse@rajames]` —
because WIREBIND births the console VM literally named after the identity
(`capsule_console_birth()`), so the identity *is* that VM, not a separate label layered on top of
it ("R.A. James is also a VM," Captain Bob's own reasoning). At the top level (still on Hera,
nothing has redirected the console yet — e.g. a live WIREBIND attach announced but not yet
`USE`'d into), the original `zuse_session`/`capsule_wirebind_attached_username()` logic is
unchanged, since Hera's own name doesn't say who's driving her.
**First attempt regressed live and was reverted before being carried into any acceptance run.**
An initial, more ambitious design (separate "identity" vs. "machine" tracked state, touching
every `console_set_vm_name()` call site across `BIRTH`/`RUN`/`VM-EXEC`/`VM-CALL`/`CONNECT-*` in
`mama_forth_words.c`) produced a wrong `[zuse@Artemis]` prompt live, because the machine segment
only ever got updated on the way *in* to a temporary excursion (e.g. into Artemis's own context
during a birth), never restored on the way back out for a "restore to a non-fleet-member name"
case. Caught by `advisor()` before it reached any committed acceptance run; fully reverted
(`console.c`, `console.h`, `repl.c`'s registration all restored byte-for-byte to their prior
committed state) rather than patched forward. The landed fix, below, needed none of that new
state.
**Landed fix: `sk_console_user_prefix()` alone** (`repl.c`) — when `console_get_vm_name() !=
"Hera"`, return it directly (emit_prefix() then prints it on both sides of the `@`, unchanged
otherwise); else keep the original `zuse_session`/WIREBIND-username logic. Zero new state, zero
other call sites touched. Verified live, interactively, on all three architectures (same
mint → WIREBIND-attach → `USE` → typed console line shape as task 3.9's own acceptance):
`[zuse@Hera]` at the top level and immediately after a live WIREBIND attach (before `USE`),
`[rajames@rajames]` after `USE rajames`, Stage D relay (task 3.9) still fires correctly on top of
it. Zero `UNKNOWN WORD`, `dict_hash` identical to every prior acceptance run in this document
(unaffected — this is a display-only change). Logs:
`logs/20260922-133732/amd64/`, `logs/20260922-134049/aarch64/`, `logs/20260922-134638/riscv64/`.
**Separately raised, not yet actioned: background diagnostic console noise
(`[Hestia@Hestia]`/`[Hermes@Hermes]` `INFERENCE: Output validation failed` warnings, `HADES`
xhci error/warn lines) interleaves visibly with interactive console output**, confirmed live via
a QMP `screendump` Captain Bob asked for mid-task. Matches this project's own already-tracked,
not-yet-started item (memory `project_production_logging_cleanup_needed`:
`console_println()` overuse, much of it should be DEBUG-level). Captain Bob explicitly deferred
this to after the prompt-format fix landed — genuinely not started, no task number assigned yet.
**2026-09-22 — CLOSED**, after the task 3.8 payload-aliasing fix landed. The noise turned out to
be exactly three sources, found by tracing each visible line back to its actual call site rather
than auditing `console_println()` broadly:
1. **`INFERENCE: Output validation failed, ignoring results`** (`vm_runtime.c`, kernel; `vm_time.c`,
hosted mirror) — already `log_message(LOG_WARN, ...)`, level-gated, just visible at the default
runtime `LOG_WARN` level (`kernel_main.c` sets it there after POST). Fires routinely during
normal operation with the runtime self-recovering every time (the message says so). Downgraded
to `LOG_INFO` in both files (kept in sync, same duplicated-logic pattern this codebase already
uses elsewhere for the hosted/kernel split).
2. **`xhci: CSW status = FAILED`** (was `LOG_ERROR`) and **`xhci: unit not ready -- retrying TEST
UNIT READY`** (was `LOG_WARN`), both `xhci.c` — also already level-gated, same visibility
reason. `xhci.c`'s own existing comment right there confirms these are "a fresh SCSI target's
standard first-command UNIT ATTENTION behavior, not a driver defect" — the bounded retry
resolves it routinely. Downgraded both to `LOG_INFO`. The genuine terminal failure ("`xhci: unit
still not ready -- giving up`", after `XHCI_BOT_TUR_MAX_RETRIES`) was left at `LOG_ERROR`,
unchanged — that one is a real failure, not routine noise.
3. **`Stadium: dispatch cell=... behaviour=...`** (`stadium.c`'s `stadium_dispatch()`) — the one
genuine defect, not just a severity judgment call: an unconditional `console_puts()`/
`console_println()` sequence with **no level gating at all**, printing on every single Stadium
dispatch regardless of log level — confirmed live, interleaving visibly with ordinary
interactive console sessions (a live QMP `screendump` Captain Bob asked for caught it directly,
`logs/20260922-134049/aarch64/`'s own screendump before this fix). Rewritten to route through
`log_message(LOG_DEBUG, ...)`, one call per `StadiumBehaviour` case, matching every other
diagnostic trace in this codebase — silent at the default level, available with
`--log-level=debug`.
No sender/reader/call-site changes needed anywhere else — this was purely a severity/level
question for two sources and a genuine gating gap for the third, not an API or behavior change.
Verified live: a fresh screendump after the fix (`logs/20260922-142449/amd64/`) shows a clean
interactive session with none of the three noise sources present, confirmed against the exact
same mint→WIREBIND→`USE`→relay sequence that produced the noisy screendump earlier. Standard
three-ISA acceptance clean: `logs/20260922-142449/amd64/`, `logs/20260922-142835/aarch64/`,
`logs/20260922-143229/riscv64/` — zero `UNKNOWN WORD`, `dict_hash` identical across all three and
matching every prior acceptance run in this document (unaffected — display-only change). Also
confirmed the hosted build (`make`, no `ARCH=`) still compiles clean after the `vm_time.c` change.
**2026-09-22, real defect found and fixed while investigating a separate complaint (Captain Bob:
the interactive Stage D exchange "looks nothing like a normal single-VM session" — extra `ok`/
`ok>` cycles with no command typed in between).** Root cause traced via a live QMP `screendump`,
not guessed at: `sk_console_readline()` (`repl.c`) breaks its read loop on **either** `'\r'` or
`'\n'` as an independent terminator — a line sent as both bytes (a real terminal in CRLF mode, or
any scripted sender writing `"\r\n"`, which every QMP/socat-driven verification session in this
whole document has been doing) submits **twice**: once for the real line on the first byte, then
an immediate empty-line submit on the second byte the very next time the function is called, each
producing its own `" ok"`. Confirmed this pre-dates Stage D entirely — the very first `ok`/`ok>`
pair after `MINT` (Hera's own console, not `rajames`'s, not an async relay at all) already showed
doubled. **Not a Stage D async-relay artifact as first suspected** — every prior screendump and
every prior acceptance log in this document that shows doubled `ok`s is this same bug, present
since before this document opened.
Fixed at the source (`sk_console_readline()`, `repl.c`): after breaking on `'\r'` or `'\n'`, a
single non-blocking peek for the paired byte (the other one of the two) — discard it if present,
push it back via the existing `g_console_pending_key` single-slot buffer (the same mechanism
`sk_console_key_available()` already uses for "peeked but not this call's to consume") if it
turns out to be unrelated input for the next line. A byte that hasn't arrived yet is not waited
for, matching every other non-blocking read in this function.
Verified live: a fresh screendump after the fix (`logs/20260922-162219/amd64/`) shows exactly one
`ok`/`ok>` pair per typed line, matching normal single-VM interaction, with Stage D's own async
delivery timing unchanged (Captain Bob's own explicit choice: clean up the prompt noise, keep the
real delivery latency — the evidence line and result still land on a later idle-pump beat, just
without the doubled `ok` cluttering the wait). Standard three-ISA acceptance also clean on all
three architectures: `logs/20260922-162219/amd64/`, `logs/20260922-162531/aarch64/`,
`logs/20260922-162932/riscv64/` — zero `UNKNOWN WORD`, `dict_hash` identical to every prior run in
this document (unaffected).
---
## Out of scope, carried for visibility
Recorded in `FABRIC-3.5.md`, deliberately **not** part of this reshuffle. Listed so they are not
absorbed by accident:
- **Item 43** — should a Stadium reservoir ever replenish? (§XL.6) A VM's send capacity declines
monotonically today.
- **Items 23–26** — the `FABRIC-3.5.md` §XXXI gap-analysis follow-ups: re-home `FABRIC-2` §17.4,
consolidate the three reported-not-scheduled registries, decide `FABRIC-2`'s five orphaned
design items, reconcile `FABRIC-0`'s seven opens.
- **`src/*.c.bak`** — tracked-but-stale, reported in three places and actioned in none
(§XXII.5).