Punch list §25 item 0.7 complete. This is what finally makes items 0.5 and
0.6 provably work end to end.
apic_timer_start(): writes CNTP_TVAL_EL0 (or CNTHP_TVAL_EL2 at EL2 --
aarch64_current_el(), same EL-aware discipline as 0.4-0.6) to
s_timer_period_tsc, then CTL.ENABLE=1/IMASK=0, followed by an ISB. The ISB
is not decorative: confirmed against Linux's own arch_timer_reg_write_cp15()
(arch/arm64/include/asm/arch_timer.h) that only the *control* register write
needs synchronising before the enable/mask state is guaranteed visible to
the interrupt pipeline -- TVAL/CVAL writes do not carry the same
requirement, which is why apic_timer_rearm() omits it.
TVAL is architecturally 32-bit but MSR-to-system-register is always a
64-bit instruction form -- passing a uint32_t operand directly failed to
build (-Wasm-operand-widths). Fixed by truncating to 32 bits ourselves then
zero-extending back to 64 for the operand, which supplies explicit,
provably-correct zeros in the RES0 upper field rather than depending on
unverified hardware behaviour -- Linux's own driver never exercises this
path (it always uses the 64-bit CVAL form instead), so there was no local
source to confirm the alternative against.
apic_timer_rearm() (new): re-writes TVAL only, no ISB needed. TVAL is
relative to "now," not an absolute deadline like riscv64's SBI interface
(item 0.3), so there is no drift-correction bookkeeping -- each write means
"N ticks from this instant." Wired into aarch64_irq_handler() and called
*first*, before heartbeat_tick(), matching riscv64_timer_rearm()'s ordering
discipline exactly: the ARM Generic Timer does not auto-reload, so a return
path that skips this leaves the interrupt condition latched, which the GIC
would redeliver the instant it's EOI'd -- a real storm, the same class of
failure item 0.6's verification investigated (and that time found absent,
because nothing was armed yet).
Verified: builds clean; every generated instruction checked against
disassembly, not just reviewed by eye (both EL branches, correct TVAL/CTL
register names, single shared ISB in apic_timer_start(), no ISB in
apic_timer_rearm()). Boots to ok> with no regression, dict_hash
0x3d4e1daf289da94f unchanged.
Rate measured directly against real wall-clock time via QEMU's own -d int
trap trace (same method as riscv64's item 0.3), two independent windows:
1,090 interrupts over 11.05 s (98.679 Hz) and 4,031 over 40.88 s (98.614 Hz)
-- consistent across both, so this is a real, small, systematic bias
(~1.3-1.4% slow), not measurement noise from polling granularity, which
would have shrunk with the longer window and did not. Attributed to genuine
per-interrupt service latency: TVAL is rewritten mid-ISR, so the trampoline
save/restore, GICC_IAR read, EL branch and GICC_EOIR write all lengthen the
effective period slightly versus the nominal 10 ms, inherent to any
relative-countdown re-arm scheme. Reported as measured, not smoothed over.
The interrupt sustained continuously across both windows with no stall and
no storm, which is the primary evidence re-arm-every-tick is correct;
the small rate bias is overhead, not a defect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.6 complete.
Ruling applied (AskUserQuestion, this session): DTB is confirmed unreachable
on this system's aarch64 firmware too (qemu-efi-aarch64 2025.11-3ubuntu7,
same finding as riscv64's item 0.3), so GICD/GICC base addresses and the
timer PPI are named QEMU-virt constants with a recorded caveat, not
DTB-discovered as the item originally asked.
Nothing here was recalled from memory. Base addresses (GICD 0x08000000,
GICC 0x08010000) and the timer PPI (30, non-secure EL1 physical) were read
out of QEMU 10.2.1's own internal devicetree via
`qemu-system-aarch64 -machine virt,dumpdtb=...`, decoded with this tree's own
fdt.c reader rather than a new tool -- correct for this exact QEMU version,
not assumed stable across others. Bonus finding from the same dump: PPI 26
for the EL2 hypervisor timer, which item 0.7 will need for its EL2 path.
Register offsets within each block (GICD_CTLR, GICC_IAR, etc.) are GICv2
architectural constants, not board-specific, and were cross-checked against
Linux's own arm-gic.h driver header rather than recalled either.
Acceptance amended before implementing (§25.0 "when an item is genuinely
wrong"): the original text required observing a delivered-and-acknowledged
timer interrupt, which cannot happen within this item's own scope --
apic_timer_start() (item 0.7) is still the no-op stub, so nothing arms the
timer. This is the same defect the earlier review's C2 fix already applied to
items 0.2 and 0.5; it was missed here. Acceptance is now: GIC initialises
without fault, the IAR/EOIR path is wired into aarch64_irq_handler() and
ready, boots with no regression -- item 0.7's tick-advance is what proves
delivery, exactly as 0.5 already defers to 0.7.
EL-aware (B3, same discipline as items 0.4/0.5): apic_init() selects PPI 30
or 26 from aarch64_current_el(), decided once and cached, not re-derived per
interrupt.
aarch64_irq_handler() now does real work: reads GICC_IAR (the GICv2
acknowledgement step), dispatches to heartbeat_tick() when the INTID matches
the timer PPI, and always completes with GICC_EOIR (INTID 1023 = spurious
handled per the GICv2 spec, not as a special case of "unrecognised"). This
mirrors exactly how riscv64's item 0.2 built full cause-dispatch logic before
its timer was armed in 0.3.
Investigated and resolved a real scare during verification: QEMU's `-d int`
trace showed 1,728 "Taking exception 5 [IRQ]" events by the time boot reached
the prompt, which looked exactly like an interrupt storm (hypothesis: EDK2
firmware leaves CNTP_CTL_EL0 enabled with a stale comparator, and enabling
the GIC path exposes it before item 0.7 reprograms the timer). A direct
one-shot probe inside aarch64_irq_handler() itself -- ground truth for
whether this code path runs at all -- fired zero times across a clean,
bounded boot. The trace events were almost certainly from EDK2 firmware's
own internal timer usage during its own boot phase, before control passes to
this kernel; the earlier conclusion was drawn from the external trace alone
without checking that distinction, and the probe (not the trace) is what
settled it. Probe code fully reverted; not part of the commit.
Verified: builds clean, boots to ok> with no regression, dict_hash
0x3d4e1daf289da94f unchanged from the item 0.1-0.5 baseline, EL banner and
IDT-installed lines still print in order, GIC init line confirms PPI 30
selected. Only aarch64-scoped files touched; amd64/riscv64 not rebuilt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.5 complete.
irq_spx now branches (one instruction, well inside the 128-byte vector slot)
to irq_spx_trampoline, a 672-byte-frame save/restore sequence that calls a
C handler and returns via ERET. The other fifteen vectors are untouched,
still routing to the existing fatal handler.
EL selection (B3) happens once, in aarch64_install_vectors(), not per
interrupt: aarch64_current_el() (item 0.4) picks VBAR_EL1 or VBAR_EL2, and
the same answer is cached in a byte flag (el2_mode_flag) that the trampoline
reads to choose ELR_EL1/SPSR_EL1 vs ELR_EL2/SPSR_EL2 -- the two forms are
genuinely different MRS/MSR encodings, not runtime-selectable operands, so
this is the cheapest correct design: decide once at install time, branch
twice (save, restore) per interrupt afterward. VBAR_EL1 was previously
written unconditionally; this closes that half of item 0.4's known gap.
EL2 is coded from the architecture reference and cannot be boot-tested in
this environment (QEMU's aarch64 virt/EDK2 combination here yields EL1) --
reported as unverified rather than asserted as tested.
FP/SIMD save is not optional (B2, carried from item 0.4's finding that the
build has no -mgeneral-regs-only): the AAPCS64 caller-saved set -- v0-v7,
v16-v31, full 128 bits each -- plus FPSR/FPCR is saved and restored around
the C handler call. v8-v15 are callee-saved by the ABI and deliberately
excluded: the handler, being ordinary compiled C, preserves those itself.
aarch64_irq_handler() (interrupts.c) is deliberately empty. Distinguishing
which interrupt fired needs the GIC's IAR, which does not exist until item
0.6; nothing unmasks or routes any source to this vector yet, so the
function is not reachable during a normal boot. Per the item's own text,
no attempt was made to manufacture an interrupt to exercise this path early
-- 0.6 (GIC) and 0.7 (timer) are what prove it took and returned one.
Verified: every hand-computed frame offset (0, 16, 32 ... 640, frame size
0x2a0=672) checked against the actual disassembly of the built kernel, not
just visually reviewed -- save and restore sequences mirror exactly, and
aarch64_install_vectors' branch on the detected EL, the flag write, and the
trampoline's read of the same flag address all confirmed consistent. Boots
clean on real QEMU output, no regression: dict_hash 0x3d4e1daf289da94f
unchanged from the item 0.1-0.4 baseline, and the item 0.4 EL banner
("AArch64: running at EL1") still prints correctly ahead of "IDT installed.".
Only aarch64-scoped files touched (isr.S, interrupts.c) -- no shared loader
or header changed, so amd64 and riscv64 are provably unaffected; not
rebuilt for this item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.4 complete.
Adds aarch64_current_el() in arch.c: reads CurrentEL[3:2] on first call,
caches the result (CurrentEL cannot change post-ExitBootServices, so every
consumer gets the same answer without repeating the MRS). Called from
arch_interrupts_init() in interrupts.c -- the earliest point with both a
working console (up since M1) and a genuine first consumer (vector
installation is the first EL-dependent operation) -- and the detected level
is printed to the boot log there.
Declared via extern-in-place in interrupts.c rather than added to the shared
arch.h: "exception level" has no amd64/riscv64 equivalent, matching the
convention already used for riscv64_timer_rearm() in item 0.3.
Verified on real QEMU output: "AArch64: running at EL1", correctly positioned
immediately before "IDT installed." in the serial log. Boots clean, dict_hash
0x3d4e1daf289da94f unchanged from the item 0.1-0.3 baseline.
Scope: this item establishes the detection and exposes it; it does not yet
change VBAR/ELR/SPSR or timer-register selection to use it. arch_interrupts_init()
still writes VBAR_EL1 unconditionally, and now says so explicitly in its own
doc comment -- if aarch64_current_el() ever reports 2 on real hardware,
exceptions taken at EL2 vector through VBAR_EL2, which nothing programs yet.
That gap is items 0.5 (vectors/saved-state) and 0.7 (CNTP vs CNTHP) to close,
per FABRIC.md's GAP-B3 finding. The boot-log EL2 case prints a note pointing
at both.
Only aarch64-scoped files touched (arch.c, interrupts.c) -- no shared loader
or header changed, so amd64 and riscv64 are provably unaffected; not rebuilt
for this item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.3 complete.
The functional work (FDT reader, `time` CSR switch, SBI TIME extension
arm/re-arm, sie.STIE) was committed separately by Captain Bob as accd79f,
honestly labeled "NOT complete." This finishes it: stale documentation
cleanup, then rigorous verification against the item's literal acceptance
text, which the prior commit had not yet done.
Doc cleanup: every remaining reference to `rdcycle` / "assumed 1 GHz" in
riscv64/timer.c and riscv64/apic.c rewritten to describe the actual `time`
CSR / SBI-armed behavior. Caught and corrected my own arithmetic error in
the process: a draft claimed the ns-overflow bound improved to "~58 years"
under the new counter; direct computation shows the bound is actually
~3.26 days (2^48 ns) and is *frequency-independent* -- s_ns_per_tick and
tick rate scale inversely and cancel. Verified with a script before
writing the final comment, not asserted.
Verification, since "boots to prompt" was not sufficient for this item's
acceptance ("heartbeat_ticks() advances ... within measurement noise"):
- No FORTH word exposes heartbeat_ticks() to the REPL, and adding one would
be a new primitive outside this item's scope. GDB-over-QEMU-stub reads of
the static counter failed ("Cannot access memory"), likely a virtual-vs-
runtime address mismatch; abandoned rather than debugged further, since a
better instrument was available.
- QEMU's own `-d int` trap tracing (independent of all guest code) shows
2,797 `cause:5 desc=s_timer` interrupts delivered by the time boot reaches
the prompt, and the interleaved `desc=supervisor_ecall` entries are exactly
the SBI set_timer calls from riscv64_timer_rearm(), confirming the full
interrupt -> handler -> re-arm -> SBI loop.
- Measured rate over a real 10.127 s window: 1,013 further interrupts,
100.028 Hz observed against 100 Hz configured -- 0.03% deviation. The
sustained, non-decaying rate is itself proof the one-shot re-arm succeeds
on every cycle, not just the first.
- Confirmed regression-free on amd64 and aarch64 too: uefi_loader.c, uefi.h,
boot_info_offsets.h and the new fdt.c/fdt.h are shared across all three
builds (amd64 also consumes the offset constants via kernel_entry.S), so
all three were rebuilt and booted. Identical dict_hash
0x3d4e1daf289da94f on all three, matching the item 0.1/0.2 baseline.
Reported, not fixed, per rule 3:
- DOE_INJECT=1's EXEC-DOE now fails as "UNKNOWN WORD" against the pruned
Hera-alone capsule -- doe.4th was never loaded by init.4th even before
item 0.1's prune, so this is a pre-existing gap the prune surfaced, not a
new defect. Discovered while chasing why a 200-rep injected DoE run
produced no new CSV rows after 25 minutes; it had failed in the first
millisecond, not run the whole time.
- repl.c's own comment claims a "Heartbeat: N ticks" diagnostic prints to
the serial log; no such print exists anywhere in the tree.
- riscv64/arch.c's arch_read_timestamp() still uses rdcycle with a stale
1 GHz comment, but it has zero callers on this architecture -- dead code,
left alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Punch list §25 item 0.3 NOT complete.
- Added `starkernel/fdt.h` and `fdt.c` for minimal read-only devicetree parsing: sufficient for boot-time lookups such as `timebase-frequency`.
- Bootloader now captures the devicetree blob (DTB) from `EFI_DTB_TABLE_GUID` into `BootInfo::dtb`.
- RISC-V timer subsystem now uses the `time` CSR as the primary timestamp source, abandoning the hardcoded `cycle` frequency assumption.
- Timer rate is read from `timebase-frequency` in the DTB when accessible; otherwise, a fallback value is used with a RELATIVE trust level.
- Integrated the SBI TIME extension for one-shot timer deadlines, ensuring re-arming occurs after each tick to avoid missing heartbeats.
Verified: riscv64 builds clean, boots to the ok> prompt with no regression; `riscv64/timer.c` reports accurate frequencies on QEMU's default firmware.
Signed-off-by: Robert Allan James <robert.allan.james@gmail.com>
Punch list §25 item 0.2 complete.
Verified: riscv64 builds clean and boots to the ok> prompt with no regression;
dict_hash 0x3d4e1daf289da94f, unchanged from item 0.1's baseline. Disassembly
confirms the 320-byte frame, all 16 integer caller-saved registers, the FS
check, and SRET on exit; riscv64_trap_entry lands at 0x414fa8, 4-byte aligned
as stvec direct mode requires.
Not verified, and the item says so: neither new path was exercised. No timer is
armed until 0.3, so riscv64_interrupt_handler never ran, and no exception
occurred during boot, so the fatal path was not observed -- it is preserved
structurally, same branch to the same unchanged handler. This is why C2
rewrote the acceptance to no-regression rather than to having taken and
returned from a trap.
Register set is the LP64D psABI caller-saved list, not this document's summary:
integer ra/t0-t6/a0-a7 (16), FP ft0-ft11/fa0-fa7 (20) plus fcsr, and sepc +
sstatus. Callee-saved registers are the C handler's responsibility.
The FP half is conditional on sstatus.FS != Off, which the item did not
anticipate. Nothing in boot.S or kernel_entry.S programs FS, so its value is
whatever firmware leaves; touching an f-register with FS == Off raises an
illegal-instruction trap, and doing that inside the trap handler would be
unrecoverable. Omitting the FP save is not an option either -- the built
riscv64 image contains 530 FP instructions (fld, fmul.d, fcvt.lu.d among them),
confirming B2's finding against the binary rather than the build flags alone.
So the save is conditional, and sstatus is restored after the f-registers.
Dispatch: scause bit 63 routes to riscv64_interrupt_handler with scause in a0;
cause 5 (supervisor timer) calls heartbeat_tick(). Other causes are ignored
rather than fatal -- none are enabled to arrive. Everything else still falls
through to riscv64_exception_handler, unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Punch list §25 item 0.1 complete.
Verified: all three architectures build clean and boot to the ok> prompt with
Hera alone. Blocks executed are exactly 2057 -> 2049 (-> lib.4th/4050) -> 2050.
Zero occurrences of "Hermes" or "Tripod" in any of the three serial logs, and
the only parity record is MAMA_INIT -- no baby VM is born. mkcapsule --lint
passes 26 files, 0 failing, 0 violations.
Deleted blocks 2051, 2052, 2053, 2054, 2055, 2056, 2058, 2059 -- readiness
handshake, broadcast test, TRIPOD-TEST, HERMES-E2E and the fleet-DoE
scaffolding. Edited 2057 (banner), 2049 (dropped both births with their
CD-INIT calls and the common:msg.4th / process.4th loads, which are wholly
Hermes-dependent; kept lib.4th; VM-TREE and VM-CHILDREN no longer name absent
children) and 2050 (kept the BOOT-BANNER call, dropped the two calls to the
deleted words). capsules/hermes/ and capsules/artemis/ untouched on disk.
New Hera-alone parity baseline, identical across all three ISAs:
dict_hash=0x3d4e1daf289da94f capsule_hash=0xb4c4b5559146a3bd
This supersedes the pre-prune baselines in logs/ and is what item 0.10's
double-boot reproducibility check compares against.
Commit contents beyond the item's own edit, noted rather than slipped in:
capsules/BLOCK_MAP.md is regenerated by any build; the three serial logs are
this item's acceptance evidence and CLAUDE.md requires committing them; the
DoE CSVs are auto-extracted by the qemu target on every run. Leaving generated
output from this item's acceptance run for a later item to sweep up would be
worse than including it here.
Reported, not fixed: tools/mkcapsule.c emits two -Wstringop-truncation warnings
on the host build (:404, :562), pre-existing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
D1 The three passages still arguing from the K-denominator justification that
§2's correction removed: §17.3's opening now cites density's need for a
volume (§19.2); §17.5's sizing argument re-grounded on mass swamping and
§23.1's abolition of by-reference payloads; §17.6(d)'s bullet no longer
cites §2 for a claim §2 explicitly disavows.
D2 §19.6 #1 and #2 struck through as resolved (#1 by §23.1 with the residue
scheduled as item 1.12; #2 by §24.3), matching §17.4's convention.
D3 §20.3's stale LEANING now points at §21's DECIDED.
§25.7.1's status updated: all fourteen findings closed. A1 ruled and applied,
B1/B2 verified against the tree and fixed into their items, B3/C1-C7/D1-D3
applied. The findings text is preserved as the record of what was found.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One batch commit for the seven mechanical item fixes from §25.7.1, each tagged
in place:
C1 Item 0.1 no longer contradicts its own Refs line. Delete set is 2051-2056 +
2058-2059; blocks 2057, 2049 and 2050 survive edited, with the per-block
edits now spelled out. The old "remove 2050-2059" wording would have
deleted the banner.
C2 Items 0.2 and 0.5 accept on no-regression. Both previously required having
taken and returned an interrupt at a stage where no interrupt source
exists; the real evidence lands with 0.3's and 0.7's tick-advance
acceptance, and both items now forbid pulling later work forward to
manufacture it.
C3 Item 0.3 carries sie.STIE and the every-tick re-arm -- the SBI timer is
one-shot and a missed re-arm stops the heartbeat forever with no error.
C4 §23.4 #4 is now schedulable as item 1.12 (continuation-cell encoding);
3.1's blocker line and the Phase 3 gate reference it.
C5 Item 1.11 is formally blocked on 4.3 instead of informally deferred.
C6 Item 0.10's "sane" sharpened to trust near Q48_ONE and variance small
relative to the new expected_delta; the amd64-control framing is noted as
valid again under the GAP-A1 ruling.
C7 The commit template no longer hardcodes a model name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
B2, verified against Makefile.starkernel: no architecture restricts FP register
use. amd64 has no -mno-sse (:124) -- works with live interrupts today, save-set
adequacy unverified. aarch64 lacks -mgeneral-regs-only (:146). riscv64 builds
-march=rv64gc -mabi=lp64d (:162) -- hard-float ABI, and kernel code genuinely
uses doubles (hotwords_stats_print). Items 0.2 and 0.5 now require saving the
ABI caller-saved FP set plus control/status registers, with the exact lists
verified against the psABI/AAPCS64 rather than this document, and explicitly
forbid "fixing" it via soft-float, which would break existing code.
B3: item 0.4's EL detection now governs everything EL-dependent -- vector base
register (today's isr.S writes VBAR_EL1 unconditionally, never consulted for
exceptions taken at EL2), saved-state pair, eret target, and timer registers.
Item 0.5's hardcoded ELR_EL1/SPSR_EL1 wording replaced accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read BootInfo (uefi.h:624-639): memory map, runtime services, ACPI,
framebuffer, stack, args -- no FDT pointer. The only FDT mentions in the whole
kernel tree are comments in riscv64/timer.c acknowledging one would be needed.
Items 0.3 and 0.6 instructed "read from the device tree" against a kernel that
cannot reach one.
Item 0.3 now carries the prerequisite explicitly: capture the DTB pointer from
the EFI configuration table into a new BootInfo field in the shared loader,
serving both 0.3 and 0.6. Item 0.6 references that field and, if the DTB is
unreachable on aarch64 EDK2, requires stop-and-report rather than falling back
to constants unilaterally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captain Bob's ruling on §25.7.1 GAP-A1, adopting the recommended resolution.
The engine's tick is a virtual tick: a pure, deterministic function of the
execution stream, which is what exists today and why parity holds today. The
hardware heartbeat is the TIME-TRUST instrument and the idle wake source, and
drives nothing that feeds patron state. When the system idles, the REPL poll
loop pumps virtual ticks so TTLs still expire in real time, in a context where
parity was never claimed.
Applied to: §16.4 (correction appended -- the fire-on-tick-count rule was
necessary but not sufficient, since the hash measures the composition of the
instruction and tick streams, not the engine's schedule alone); §17.1 (the one
clock is the virtual tick, and the mechanisms cannot be split across clocks
because TTL expiry has instruction-stream side effects); §18.4 (L0 advances on
the virtual tick); item 0.8 (disambiguated -- the hardware tick drives
instrumentation only, vm_tick call sites unchanged); item 2.1 (transfer
restates onto the virtual tick, which is what makes its identical-sum
acceptance achievable).
Item 0.10 needed no change: with the engine execution-paced, its double-boot
check is a valid regression guard and amd64 is genuinely a control again.
Phase 0's timer bring-up stands: it makes the instrument real on three ISAs
and is the substrate SMP will need. It does not drive the engine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full re-read of the document as it stood after the first review's corrections,
looking for what would break a lower-capability model working the punch list.
Nothing in this commit fixes anything -- it records fourteen findings for triage.
The one that gates coding: GAP-A1. §16.4's inference "same tick ordinal → same
hash" is unsound, because the hash covers execution_heat, which is co-written by
the instruction stream and the tick stream, and a hardware timer makes the
interleaving of those two streams wall-clock-dependent. Firing on tick count
fixes the engine's schedule, not the composition. Blast radius: item 0.8 is
ambiguous between two different kernels, 0.10's double-boot check fails by
construction under one of them, and 2.1's corrected acceptance is still
unachievable. Recommended resolution recorded (virtual tick as a pure function
of the execution stream; hardware heartbeat as instrument and wake source only)
but explicitly not decided.
Also: three unverified prerequisites (DTB reachability for 0.3/0.6, FP-register
save vs compile flags for 0.2/0.5, EL-dependence of the vector path vs 0.4's
scope), seven punch-item defects (0.1 self-contradiction on block ranges,
0.2/0.5 unsatisfiable acceptance, 0.3's missing STIE and re-arm, §23.4 #4 not
being a schedulable item, 1.11's informal deferral, 0.10's stale control
framing, the hardcoded model attribution), and three amendment inconsistencies
(stale K-denominator language in §17.3/§17.5/§17.6d, §19.6 #1-#2 unmarked as
resolved, §20.3's stale LEANING).
Triage order is stated in the section: A1 first, B1/B2 are ten-minute reads,
C and D mechanical after that. C1-C3 minimum before any coding model starts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review item REVIEW-D4.
Item 1.1 (exclusive access, the "sitting in a car" primitive from §8) sits in
Phase 1 alongside questions with no structural effect, and it is not in that
class. A per-patron exclusivity primitive plausibly needs a held flag or holder
index -- a ninth wire in §3's table, in the header item 3.1 builds. Resolving
1.1 after 3.1 means rebuilding the cell header.
§25.4 already blocks Phase 3 on items 1.1-1.7, so the ordering was right; what
was missing was why 1.1 specifically, which is what gets an item quietly
reordered later by someone who does not know what it was holding up.
Item 3.1 now names both of its blockers explicitly: item 1.1, and §23.4 #4, the
continuation-cell encoding surfaced by REVIEW-C3. It also now requires defining
both members of §3's closed two-valued union rather than just the header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D3.
Item 1.3 decides what triggers a capacity transfer and said it should read the
density gradient rather than a schedule -- correct about what, silent about
when. Hera's arbitration mutates patron mass, so §18.5 binds it directly:
anything influencing patron state advances on tick count, and wall-clock time
must never be an input to a decision.
Without that stated in the item, a reasonable implementation could pace
arbitration off a wall-clock interval and reintroduce, in a new place, exactly
the defect item 2.1 exists to remove. Whatever 1.3 decides must be expressible
in ticks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D2, following from REVIEW-A1/B1.
Item 2.2 claimed the unbounded registry "makes fleet K an identity that cannot
fail" and accepted on VM-CONSERVED? becoming able to fail. Both were wrong.
Heat is transferred rather than renormalised (§20.2), so conservation is already
a real invariant and already falsifiable -- via the dropped-remainder path at
capsule_vm_physics.c:240-244 and integer truncation at :304-305. Bounding the
population changes neither, so the item promised something it could not deliver.
The bound is still needed and now rests on the two grounds §2 states: finite
state for §13's induction and model checking, and density requiring a capacity
to be dense within, without which §19.3's admission rule has nothing to compare
against. Acceptance is now the bound existing, birth-at-bound behaving as 1.5
specifies, and the three-architecture boot unaffected.
Fixing the truncation leak is a separate and larger piece of work and stays in
§25.7 rather than being folded in here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-D1.
Item 2.1 restates vm_physics_touch on tick count and accepted on the
dictionary-hash double-boot check from item 0.10. That check cannot detect the
work: §18.5 establishes that vm_physics_touch writes node->physics, not
DictEntry.execution_heat, and so never reaches the parity hash. The dict hash
would be identical whether 2.1 succeeded, failed, or was skipped entirely.
Acceptance is now an identical fleet heat sum across two boots of the same
capsule -- fleet heat being the quantity the item actually changes. The
dict-hash check still runs, as a regression guard rather than as evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C4.
The header claimed §1-15 retained "arena" to stay quotable, but the rule was
not applied consistently -- §15 had been swept, §13 had not. A document with two
names for its central object costs every reader something on every section.
Swept all body text. "arena" now survives in four places only, each deliberate:
the src/starkernel/vm/arena.c file path; the two naming notes, which discuss the
word itself; §12's preserved question list, which the section explicitly retains
as the source several later sections quote; and the block quotes of §12 in §21
and §23.
One deviation from the review's stated rule, which said quoted text keeps the
original word. §17.3 quotes §4 and §17.6 quotes §13 -- both self-quotes within
this document, whose sources were just swept. Left unswept they would cite text
that no longer exists, so a reader following the reference finds different
words. Those two were updated to track their sources. §12's quotes were not,
because §12 itself is deliberately preserved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C3.
The stated fields sum to 28, not the ~32 claimed, and 28 + 32 inline payload is
60, not the 64-byte cell. Now stated as 28 used plus 4 reserved. The reserve is
deliberate: it keeps the header a clean half-cell, leaves room for the
header/continuation discriminator §3 now requires, and gives §8's exclusivity
primitive somewhere to live if item 1.1 resolves to a holder index.
The larger problem was the "17 cells: 1 header + 16 payload" figure for a
1024-byte block. That silently assumed continuation cells are contiguous and
carry nothing but bytes. §22.3 allocates from a per-VM free list with no
adjacency guarantee, so continuation cells would need a next-index -- 4 bytes
off each one's payload, making the same block 18 continuation cells rather than
16. The alternative, guaranteeing contiguous runs, reintroduces the
fragmentation §3 avoids.
Those are different designs with different costs, and the choice sets the mass
of every large patron. Rather than pick one, the row is marked undetermined and
recorded as §23.4 #4, which gates punch item 3.1: the cell structure cannot be
built until it is settled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C2. The heading claimed all six questions closed while the
table below marked Q5 partial and a note explained why. Five closed, one
partial.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-C1. The heading read "Four patrons die four different ways"
above a table listing five. §17's preamble carries the caveat that its counts
predate §20, but a heading is where a reader anchors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-B2, settled by reading include/vm.h:335-351.
The subsection's table and prose disagreed: the table listed the code field as
present, the prose named it as one of two missing wires, and neither accounted
for payload.
Actual mapping against §3's eight wires: identity, heat, TTL and pin are present
in correct form; link is a pointer rather than an index; the code field is a raw
function pointer rather than an enumerated tag; mass and payload are absent.
Four correct, two wrong-form, two absent -- not "six of eight."
The wrong-form pair matters more than the count. §13 names pointer-free index
links as the single biggest difference between a tractable proof effort and a
research project, and §18.3 requires the function pointer to become a closed
tag. Those are exactly the two.
The claim that the dictionary "is" a Stadium entry is therefore overstated and
has been weakened to what the struct supports: the dictionary already has the
concepts, and two of the eight wires need to change form. That still carries §1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-A3.
§23.1 dissolved the payload threshold by making large patrons heavy -- a
1024-byte block is 17 cells, one header and sixteen of payload. Those sixteen
carry no identity, heat, TTL or code field, so they are a second cell shape,
which §3's opening line ("One structure. No variants") forbade without saying
so. An implementer reading §3 literally would have built the wrong structure.
Declared: a cell is either a patron header or a continuation cell owned by
exactly one patron. The union is closed, two-valued, and fixed at build time.
This introduces no new principle -- it is the same discipline §18.3 applies to
behaviours, and a two-valued union is the smallest instance of a closed
enumeration. It is not a type field: the distinction is structural, tells the
engine only whether a cell begins or continues a patron, and is exhausted by
that. Continuation cells are never ranked, reaped or dispatched; they are floor
space accounted for in their owner's mass.
§13's "one datatype" bullet amended -- the datatype is a two-constructor sum
rather than a bare record, costing one case split.
§23.4 #3 (identity elision) expanded: it is downstream of the
header/continuation encoding, since a scheme reusing the identity field as
discriminator would couple the two decisions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-A2.
§22.3 specified that cells are drawn from a shared free list. That undercut the
row that decided §21: its SMP discriminator claims messages are the only
boundary-crossers, so no shared memory and no locks, permanently. A shared free
list is shared mutable state touched by every VM on every admission and reap,
and would need a lock or atomics under SMP. §22.3's defence -- that VMs never
touch each other's cells -- does not cover it, because the free list is nobody's
cell and allocation touches it.
Each VM now holds its own free-list head index into the global array. Hera hands
over cells when she grants quota; a VM allocates and frees only within what it
holds. One index space, one datatype and §13 are all unaffected, and
disjointness becomes total rather than nearly total.
Capacity transfer remains arithmetic plus a list splice, arbitrated at a known
point per §22.5 #2.
The layout choice itself is unchanged -- one global array, one index space
remains correct for §13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review item REVIEW-A1, settled by reading (REVIEW-B1).
§20.2 claimed fleet K was "bookkeeping" that could not fail, on the premise
that heat is renormalised after population changes. That premise is false. Read
end to end in capsule_vm_physics.c:
- vm_physics_transfer (:147-154) subtracts from one and adds to another,
clamped at zero: "Nothing is created or destroyed."
- Birth (:156-185) seeds Hera with Q48_ONE; every other VM starts at zero --
"cold mass added to a closed system." Population growth rescales nothing.
- Death (:225-247) transfers the dying VM's heat to its root before zeroing.
- Touch (:250-311) pulls proportionally, clamped so it "can never manufacture
heat."
There is no renormalisation. VM-CONSERVED? tests a genuine invariant and is
falsifiable today, by two paths: the documented dropped-remainder case at
:240-244, and integer truncation in the proportional fan-out at :304-305, which
loses heat on every multi-VM touch and drifts the sum downward monotonically.
Consequently §2's justification was also wrong -- capacity does not give a
normalised heat share its denominator, and §19.2 says mass never enters K. §2
now claims the bound on the two grounds it can honestly claim: finite state for
§13, and density needing a capacity to be dense within.
This also reframes the Artemis campaign's K-invariance arm. It was not
measuring an identity; it measured a quantity that could drift and did not trip
a 5% epsilon over that run.
The truncation leak is recorded in §25.7 as reported-not-scheduled. It is small
per touch but monotonic, and its rate has never been measured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconciliation. §1-15 were written before any code was read and had drifted
from §16-24 in twenty places. Each superseded claim is now marked in place
rather than rewritten -- §19.4 and §19.5 quote the original §4 wording
directly, and the retracing trail matters more than a tidy read.
Two real gaps surfaced during the reread, neither previously recorded:
- §3's wire table was missing `mass`. Density is heat / mass, so mass has to
live in the entry, and §3 is the table an implementer would work from. It is
now an eighth wire.
- §8 asserts a per-patron exclusivity primitive -- "one sits in a car, the only
exclusive thing in the room, no global lock" -- that nothing in §16-24
defines. §21 covers ISR-vs-mainline concurrency, which is a different
question. Marked OPEN at the source and carried as punch list item 1.1.
Also corrected: §9's admission table is complete for all five patron kinds with
both `?` marks closed; §12's six questions are all resolved; §5's third
category is supplied; §15's five-line summary had two wrong lines; §4, §6, §7,
§17.4 and §17.6 status markers now match reality.
§25 is the punch list, with its operating instructions first: one item at a
time, no jumping ahead, no scope increase, no fabricating anything unverified,
stop when blocked. Every checked item gets its own commit carrying both the
change and this file with that box ticked, so the document is never a claim
about work not in the branch.
Phase 0 is the substrate -- Hera alone plus real timers and IRQ return paths on
all three ISAs. Nothing else can start until it is done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Q1 (payload threshold) does not need answering, it needs removing. §3 wanted
inline-if-small/by-reference-if-large so a cell need not be sized for a block.
§19 gives a better answer: a large patron occupies more cells, chained by
index, which is exactly what mass already means. A block is not by reference,
a block is heavy.
If the payload is in the Stadium, it counts toward mass. If it is not in the
Stadium, the patron is not resident -- it is a handle to the warehouse.
That closes the §19.6 loophole without a rule: a 1 MB block cannot occupy one
cell and read as dense. It also preserves the hysteresis in §19.3, which
depends on blocks being genuinely expensive to keep resident. Nothing in §3 is
violated -- cells stay fixed-size, links stay indices, the Stadium stays an
array. By-reference is reserved for things outside the Stadium, which are not
patrons.
Q2's premise is stale: §17.5 removed the screen grid, so the screen is no
longer the worst case. Underneath it sits a question §17.5 left half-settled --
it decided the dirty event is the patron but not what one event covers. Per
cell, an 80x25 redraw is 2,000 patrons and floods the Stadium; per region it is
about 25. Leaning region-based, to be confirmed with the console work. The
worst case then becomes messages, giving the rule: size the cell so a typical
message is exactly one cell.
Proposes 64-byte cells (one cache line), ~32-byte header, ~4096 cells per VM
at 256 KB. Structure decided, constants leaning -- heat at 8 bytes is fixed by
the existing Q48.16 convention, the rest need validating against a real build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§7, §12 Q4 and §17.6(c) are one question at three scales. Resolved elastic.
Under §19's density definition elasticity stops being a feature and becomes a
negative feedback loop: a busy VM's heat share rises, density rises, capacity
flows toward it, mass rises, density falls back. Capacity flows down the
density gradient -- diffusion, no threshold, no damping constant. §4's "read,
not decided" applied one level up. §7's own argument also holds: birth sizes
the resting volume rather than a cap, which is far easier to guess right and
self-corrects when wrong.
The larger finding is that hard-versus-elastic was the wrong framing. Elastic
is cheap or expensive entirely according to layout, which §21 did not settle.
Chosen: one global array of cells, one global index space, per-VM quota as a
count rather than a contiguous range. Cells come from a shared free list, so
transfer is arithmetic on two integers -- no fragmentation, no adjacency
requirement, index links keep working.
This makes §13 simpler rather than harder: one array, one datatype, one total
function over one finite index set, with nesting as a partition. §21's
reasoning survives -- K per level and messages as the only boundary-crossers
are both preserved, and SMP-safety holds so long as quota changes are
arbitrated by Hera. What is given up is physical fault containment, recorded
rather than glossed.
Adds a required ordering: capacity must move slower than heat, or two conserved
quantities chase each other and the ratio never settles. That is §12 Q5's
separation of timescales arriving as a concrete instance.
Opens: the resting floor (proposed derivation -- floor a quota at the mass of
its pinned patrons, which is derived rather than tuned), what signal triggers a
transfer, and the exact timescale ratio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Q6 leaned toward nested for the right conclusion and the wrong reason.
Its premise -- that a single region "reintroduces locking, the one mechanism
this architecture has otherwise never wanted" -- is false. Every mutex in the
kernel build is a no-op (shim.c:415); the kernel compiles STARFORTH_MINIMAL and
the shim stubs dict_lock and tuning_lock out entirely. The architecture has not
avoided locking, it has locking, inert. The cost Q6 weighs is currently zero, so
Q6 cannot be decided on it.
Decided nested on six other grounds, the strongest being SMP-readiness: nested
keeps messages the only boundary-crossers, so no shared memory and no locks
ever, whereas a single region would need real locks and the present no-op stubs
would silently become a correctness hole. The most practical is that the outer
level already exists and works (§20.1) -- single-region means discarding a
working two-level structure.
Also records a step-one finding that lands before any Stadium work: enabling
timer interrupts introduces genuine ISR-vs-mainline concurrency where none
exists today. Making the mutexes real would deadlock a single hart outright,
since an ISR spinning on a lock the mainline holds can never be released. The
top-half/bottom-half split of §18.4 is the answer, stated as a rule:
Nothing in interrupt context may mutate Stadium structure. Ever.
That constraint should be written at the stub site so the no-op is not later
"fixed" into a spinlock.
Opens: two capacities to size rather than one, and elasticity (§12 Q4 / §7 /
§17.6c) becomes the live fork now that nesting makes capacity transfer real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§17 named four patrons and omitted the only kind already implemented. §9's
admission table has always included VM, and §6 says Hera becomes the first
entry; neither reconciles with a four-patron taxonomy.
This is a finding rather than a proposal. vm_physics_fleet_heat_sum() already
sums execution_heat_q48 across live VMs against Q48_ONE -- that is a Stadium's
K over VM patrons, and §19.1's definition was derived from it.
Two consequences:
- The outer level is unbounded. The VM physics registry is a kmalloc-backed
linked list, self-described as "unbounded, not a fixed array", so heat is
renormalised to 1.0 however many VMs exist. By §2's own test, fleet K is
currently bookkeeping -- VM-CONSERVED? cannot fail. This also explains why
the Artemis campaign's K-invariance arm found nothing: the quantity cannot
vary. Bounding the population is what would make it measurable.
- Nesting is half-built. Outer Stadium holds VM patrons; each VM's inner
Stadium holds words, blocks, ACLs and messages. That is §12 Q6's nested
option with the outer level already present. LEANING nested.
Proposes VM mass = the capacity share Hera allocated, making §7 concrete and
giving Hera a lifecycle signal that distinguishes starved from small. Marked
proposal: VMPhysics has no share field today.
Resolves §20.5 #3: Hera is pinned, and any attempt to evict her is a kernel
panic asserted at the eviction site, not filtered out of the candidate set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Supplies the concrete definition §4 was missing. §4's claim that ranking is
read rather than decided is empty until the thing being read is a number.
Three quantities, not one:
Heat conserved share moved by traffic, sum = 1.0 always (already built)
Mass cells a patron occupies -- its footprint (new)
Density heat / mass -- heat per cell (new, derived)
K is left untouched. vm_physics_conserved() already defines it as a normalised
heat share summing to Q48_ONE, not an occupancy ratio; defining it as
mass/capacity would have contradicted implemented, tested code.
Ranking, admission-when-full, and migration hysteresis all read off density
with no policy and no damping constant.
Two corrections to §4:
- The self-limiting claim keeps its conclusion but loses its mechanism. A hot
entry is easier to reach, not harder; the real governor is conservation,
since heat is zero-sum and capped at 1.0.
- "Density generates heat" reverses the causality. Traffic confers heat;
density is heat per cell, derived downstream.
Open: mass depends on payload threshold and header size (§12 Q1, Q2), which are
now prerequisites rather than sizing details; and vm_physics_touch scales heat
transfer by wall-clock time, which §18.5 forbids and which must be restated on
tick count before L0 can use it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Captures the 2026-08-03 design session: collapsing the four independent
heat/TTL/pin implementations (blocks, messages, console cells, ACLs) into
one bounded Stadium of fixed-size entries, driven by an engine below every VM.
Sections 1-15 are the original design argument. Sections 16-18 add:
- 16 Substrate findings. No IRQ return path exists on aarch64 or riscv64;
riscv64's time base is a hardcoded 1 GHz guess; the dictionary already
carries six of the seven entry wires; the engine must stay deterministic.
- 17 Patrons. TTL, heat decay and pin are three distinct mechanisms on one
tick, not a type field. Reap means leaves the floor, not destroyed. The
framebuffer is a utility, not a patron. Dynamic in capacity, static in
structure.
- 18 The engine (L0). L0 and L8 bookend the gated loops L1-L7, both ungated.
Jacquard stays 7-bit/128 states, accounting for L0 by its absence.
Dispatch enumerates behaviours, never patron kinds.
Determinism traced end to end and confirmed intact: TIME-TRUST is measured
and never fed back, inference inputs are wholly execution-derived, decay is
tick-based, and the parity hash covers only word name and execution_heat.
One pre-existing exception recorded — vm_physics_touch scales fleet heat by
wall-clock elapsed time, outside the parity path.
Draft. Sections marked DECIDED / LEANING / OPEN throughout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Serial logs for the 30-replicate ART-STRESS-CAMPAIGN run, one per
architecture:
logs/20260802-160052/amd64 30 reps x 50 trials, 1500/1500 pass
logs/20260802-173226/aarch64 30 reps x 50 trials, 1500/1500 pass
logs/20260802-181803/riscv64 30 reps x 50 trials, 1500/1500 pass
4500 trials, zero failures. These are the audit trail for the claim that
the block_words.c stale-pointer cache-aliasing fix holds at scale, and for
cross-arch disk read/write/persistence.
Committed as raw blobs, matching the ~6.7GB of existing log objects in
history. Note that .gitattributes already declares logs/**/*.log and the
bare-metal CSVs as LFS-tracked, but git-lfs is not installed in this
environment, so those filters are silent no-ops -- the 744MB log at
logs/20260802-115945/ is likewise a raw blob, not a pointer. Flagged for a
proper fix (install git-lfs, confirm server-side LFS support, decide
whether to migrate existing history) rather than half-applying LFS to only
these three files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 30-replicate Artemis surface-stress campaign ran across all three
architectures: 30 reps x 50 trials x 3 arches = 4500 trials, zero failures.
The block_words.c stale-pointer cache-aliasing fix holds at scale.
Adds ART-STRESS-CAMPAIGN (block 4174) plus ART-STRESS-REP rep-tagging in
the CSV header/summary emitters, so a multi-replicate run is distinguishable
in the serial log. Campaign auto-invoke is left enabled in block 4170 for
now; Makefile.starkernel's QEMU boot deadlines stay at 12h to accommodate
long-running experiments.
Adds docs/working/architecture/ARTEMIS-COMPUDYNAMICS-IMPLEMENTATION-PLAN-20260802.md,
which documents the real gap this campaign exposed: block heat and message
heat do not decay at all. ART-TICK has zero call sites anywhere in the tree,
and HERMES-TICK has zero C call sites -- every caller is Hera poking it by
hand. BLK-HEAT@/MSG-HEAT@ read a number nothing ages, so blocks never reap
by cooling and message TTL never expires on its own.
The plan mirrors word-level physics as the reference model: lazy decay at
each access point against vm->heartbeat.tick_count, plus a bounded
background sweep with a resumable cursor (the existing answer to "sweeping
22,998 blocks per tick is too expensive"). Phase 1 Artemis, Phase 2 Hermes,
Phase 3 K participation deferred behind the Logical BAM.
The plan's preamble also records a wrong turn taken while investigating:
chasing VM-fleet heat instead of block heat, and building synthetic
Hera-driven VM-EXEC calls to force a physics reading -- which TRIPOD.md
prohibits outright. That work was reverted; the record is kept so it isn't
repeated.
Status: plan approved in shape, NOT final and NOT started. Six open
questions need answers and further design discussion is pending.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generic block subsystem (blk_format_or_load_disk) auto-reformatted
any disk lacking its own low-level 'STFR' header at attach time, before
Artemis's Forth-level BLANK/LithosAnanke/Unrecognized classification
ever ran -- so ART-HALT-UNRECOG's "Disk preserved" message was false.
Split detection from commit: an unrecognized/blank disk is now left
PROVISIONAL (geometry computed in memory only, all writes refused)
until explicitly confirmed via the new blk_subsys_confirm_format() /
BLK-CONFIRM-FORMAT primitive. Artemis calls it from ART-FORMAT and
ART-RESUME, never from ART-HALT-UNRECOG.
Verified on amd64/aarch64/riscv64: parity intact (identical dict_hash),
normal recognized-disk resume + persist-read unaffected, and a
regenerated disk/artemis-unrecognized-test.img (the old copy had itself
been silently corrupted by this exact bug) now stays byte-for-byte
identical across a halted boot on amd64 and riscv64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ABORT is documented and tested in this codebase as standard FORTH-79
behavior -- system_words_test.c:63: "Should clear stacks and return to
QUIT" -- meaning it should unwind all the way back to the outermost
interpreter loop, abandoning whatever's left of the current line/block.
The implementation only unwound one level: every place that checked
vm->abort_requested cleared it the instant it saw it, so it never
survived to propagate past the first nested frame.
This surfaced via Artemis's ART-HALT-UNRECOG (capsules/artemis/init.4th):
on an unrecognized disk it correctly printed "ARTEMIS HALT: unrecognized
disk content" and called ABORT, but WELCOME (the next line in the same
block) ran anyway, and Artemis announced ready to Hermes and joined the
fleet normally -- contradicting .claude/ARTEMIS.md's "Refuse to mount...
do not overwrite it" requirement. Root cause is general, not
Artemis-specific, and present identically in both the hosted and kernel
VM cores.
Fixed at every level execution can nest through, verified by exhaustively
grepping every !vm->error-gated continuation loop and adding the parallel
!vm->abort_requested check:
- execute_colon_word (src/vm.c, src/starkernel/vm/vm_core.c): stop
clearing the flag on return -- every colon-word call is a recursive
call to this same function, so leaving it set lets every enclosing
frame's own check also unwind.
- vm_interpret (src/vm.c, src/starkernel/vm/vm_core.c): stop parsing
further words in the current input string once the flag is set.
- exec_block_with_retry (src/starkernel/capsule/capsule_loader.c):
capsule birth's line-by-line block executor -- stop processing further
lines in the current block, but return 0 (not -1), so
capsule_exec_payload still loads later blocks in the same capsule
payload. Returning -1 here would have silently broken word definitions
in blocks that come after the aborting one for reasons unrelated to
why it aborted (concretely, Artemis's ART-PING/LOAD-DOE in blocks
4851/4852, which follow the entry block 4133).
- THRU and --> (src/word_source/block_words.c): stop processing further
blocks/lines in their own loops.
- DODOES (src/word_source/defining_words.c): the CREATE...DOES> runtime
has its own hand-rolled execution loop, separate from
execute_colon_word -- same bug class, same fix. Also guarded the
post-loop "if (vm->rsp < base_rsp) vm->rsp = base_rsp" clamp so it
doesn't fire on an abort exit -- ABORT's own reset_vm_state() already
set rsp; restoring it to base_rsp would have partially undone that.
- Both REPL loops (src/repl.c, src/starkernel/repl.c x2 call sites):
clear the flag after each line, mirroring the existing vm->error
pattern, so a mid-line abort doesn't silently freeze subsequent
interactive input.
Verified directly: ": AB-TEST 1 2 3 ABORT 999 . ; AB-TEST 42 . CR
777 . CR" -- 999 never prints (stops mid-colon-word), 42 never prints
(stops the rest of the same line), 777 prints fine (next line
unaffected). Artemis: WELCOME/"Artemis ready" no longer fires after the
halt message. No regression: all three architectures still show PASS:
persist-read, PASS: E2E msg flow, and matching dict_hash on the normal
(non-aborted) boot path; hosted test suite 965 passed / 0 failed.
Known follow-up, not fixed here (see memory for details): Artemis still
announces ready to Hermes via a separate call path (CD-INIT, block 4141)
that never went through capsule_exec_payload's block chain in the first
place, and the disk file still picks up incidental writes even on a
correctly-halted boot -- likely generic block-subsystem housekeeping,
not traced yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The flat-pool storage scope ("Immediate Goal") has been implemented for
some time -- capsules/artemis/init.4th boots live in the Tripod fleet on
every kernel boot -- but the doc still said "do not begin implementation
without explicit instruction from Captain Bob" in two places, and
CLAUDE.md's pointer still said "build-authorization deferred."
.claude/ARTEMIS.md: replaced both stale lines with a dated Build Status
section giving an honest per-criterion accounting rather than a blanket
"done": 4 of 6 acceptance criteria fully confirmed (boot-state detection,
free-map init, fetch/persist, and -- as of the persist-read fix earlier
today -- cross-boot/cross-arch persistence), K-conservation implemented
but not hard-asserted, and the unrecognized-disk halt implemented but
never actually exercised against real unrecognized content. Future
material (zones, USB hot-plug, ACL records, PKI) remains correctly
marked deferred -- unchanged.
.claude/CLAUDE.md: updated the Artemis pointer line so it's consistent
with the above instead of contradicting it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Chases down the amd64/aarch64/riscv64 "FAIL: persist-read" that
capsules/artemis/init.4th's ART-READ-TEST self-test has been reporting
in every boot log in this repo's history.
Root cause: not a code bug. disk/artemis.img had been stuck in a
corrupted state (valid LithosAnanke magic header, but data not matching
what ART-READ-TEST expects) since before this repo's own git history
begins -- already broken at the initial commit, carried over from the
pre-split monorepo. The FAIL was accurate: it correctly reported bad
data, not bad code.
Verified via a fresh disk/artemis-debug-roundtrip.img: format ->
self-test PASS -> write-test PASS -> reboot -> resume -> PASS:
persist-read, confirmed 3 times in a row. The write/read/persist code,
free map, block allocator, and C-level block subsystem cache/writeback
logic are all correct.
Fix: blanked disk/artemis.img and let a normal boot format + write-test
it fresh, then verified PASS: persist-read on amd64, aarch64, AND
riscv64 against the same reformatted image -- confirming the arch-neutral
on-disk format works cross-arch too (a boot on one architecture writes
data the other two can correctly read back).
disk/artemis-debug-roundtrip.img is kept as a regression fixture, already
in a known-good passing state -- a future break here is a real
regression, not fixture rot like artemis.img turned out to be.
disk/README.md: documented both images' state, and corrected a stale
claim that these images are managed via scripts/rundisk.sh -- that
script actually targets a separate, currently-unused disks/ (plural)
directory for the hosted VM's --disk-img= flag, not this kernel-QEMU
disk/ (singular) one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran the amd64 kernel acceptance leg 5 times back to back as the punch
list's action item asked. dict_hash was byte-identical across every run
for every VM (Artemis, both Hermes instances, Hera/MAMA_INIT), and
matched aarch64/riscv64 exactly each time. Not jitter, and not
stable-but-different either.
The underlying mechanism the hypothesis pointed at is still real and
unchanged (capsule_dict_hash_hook() still folds execution_heat into the
hash; amd64 still runs its timer in RELATIVE mode under this
hypervisor) — but PARITY:MAMA_INIT and the child-VM PARITY:BIRTH lines
all print before the heartbeat starts, and heat only decays on
heartbeat ticks, so there's no window for the timer's non-determinism to
reach execution_heat before any of these hashes get computed. Most
likely the original 2026-07-24 observation was a one-off (loaded host
machine, coincidental timing), not a real gap.
No code change. No amendment to CLAUDE.md's acceptance criteria needed —
"identical dict_hash across all three architectures" holds up under
repeated testing. Closure note added to the punch list; the 5 verification
runs' logs and DoE CSVs are kept as the supporting evidence.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves item #3 of docs/working/archive/session-logs/2026-07-24-punch-list.md
("riscv64 hosted build isn't reachable via plain make"), the last open
item from that list. Decided against options (b) chasing GCC's riscv64
nanosleep-visibility failure at its root (undiagnosed, open-ended) and
(c) leaving it manual — instead wired the already-verified clang recipe
(commit 4485c38 / e287334) into the Makefile, mirroring the existing
rpi4-cross pattern.
Makefile: new riscv64-clang target. CFLAGS deliberately does not reuse
$(BASE_CFLAGS) (hardcodes -std=c99); clang needs -std=c11 -pthread here
instead. Registered in `make help` and .PHONY.
docs/lithosananke/hosted-acceptance-test/README.md: riscv64 section now
points at `make riscv64-clang` instead of the long manual invocation.
Updated Background section and commit list to reflect that all three
punch-list items touching this doc (#1 asm fix, #2 doc command, #3 make
target) are now resolved.
Verified: `make riscv64-clang` produces a binary with identical results
to the manual command it replaces (965 passed / 0 failed, "ALL
IMPLEMENTED TESTS PASSED!", "3 Goodbye!" for the piped acceptance script).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completes the highest-priority item from
docs/working/archive/session-logs/2026-07-24-punch-list.md (item #1),
intentionally deferred out of commit 4485c38 as a separate, more careful
change.
vm_pop_asm/vm_rpop_asm's inline asm referenced the dsp/rsp memory operand
twice (read near the top, write-back near the bottom) while also writing
a plain register output operand (%[val]) in between. Nothing pinned the
address register computed for the memory operand across that gap, so
clang's allocator could reuse it for %[val], corrupting the write-back.
GCC happened to pick different registers and never hit it — this repo's
kernel build uses GCC and USE_ASM_OPT is never defined there, so the bug
was latent, not live, prior to this fix.
Fixed by reordering: write dsp/rsp back before loading the popped value,
so the memory operand's final use has already happened by the time the
output register is live. Same fix as the old pre-split monorepo's master
commit 4db9946a, re-derived here since that commit lives in a different
repository post-split.
Verified empirically, not just theoretically: rebuilding the riscv64
hosted binary with the documented clang -O3 -DUSE_ASM_OPT=1 acceptance
recipe went from 955 passed / 10 failed (all CASE.* control-flow tests —
exactly what stack-pop corruption would hit) to 965 passed / 0 failed,
"ALL IMPLEMENTED TESTS PASSED!", with nothing else changed. All three
Makefile.starkernel kernel builds still compile clean; the change is
inert there since USE_ASM_OPT is never defined for the kernel build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses items #1 (partial) and #2 of
docs/working/archive/session-logs/2026-07-24-punch-list.md.
docs/lithosananke/hosted-acceptance-test/README.md:
- riscv64 leg used riscv64-linux-gnu-gcc, which fails to build this tree
(nanosleep visibility under -std=c99). Replaced with the working
clang-18 --target=riscv64-linux-gnu --sysroot=/usr/riscv64-linux-gnu
invocation, verified end-to-end.
- All three arch sections referenced a -c "<script>" flag that has never
existed in cli.c/main.c. Corrected to the working
`echo "..." | starforth -s` pattern, verified on all three architectures.
- Updated Prerequisites: qemu-user alone is sufficient (guest binaries are
static; qemu-user-static provides static *emulators*, not required here).
Source fixes (ported from the old pre-split monorepo's master, commit
4db9946a, where they were made but never carried over to this line):
- src/math_portable.c: `-100LL << 16` is UB (shifting a negative value)
under clang's -Wshift-negative-value; changed to `-(100LL << 16)`.
- src/physics_pipelining_metrics.c: removed dead q48_mul_q48()
(-Wunused-function under clang; GCC doesn't flag this by default).
- src/word_source/editor_words.c: removed dead set_scr() (same reason).
These three were required just to get the documented clang build to
compile at all. The punch list's higher-severity item — a genuine
SIGSEGV-causing register-reuse hazard in vm_pop_asm/vm_rpop_asm
(include/vm_asm_opt_riscv64.h) — is intentionally NOT included here; it's
a separate, more careful change and isn't required for this build to
succeed (latent only under clang; this repo's kernel build uses GCC).
Verified: all three hosted builds compile and run correctly (amd64
native, aarch64 via qemu-aarch64, riscv64 via qemu-riscv64), each
printing "3 Goodbye!" for the piped `1 2 + . BYE` script. All three
Makefile.starkernel builds (amd64/aarch64/riscv64) still compile cleanly
with these shared vendored-source changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Write-up of the 2026-08-02 riscv64 boot crash investigation and fix
(commit 7366275), in the same style as docs/lithosananke/amd64-isr-fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kernel_main on riscv64 ran directly on EDK2's UEFI boot-time stack, with
no dedicated stack switch — amd64 has always had a kernel_entry.S
trampoline for exactly this reason (its own comment: "the FORTH
interpreter + DOE experiment loop can easily exceed that depth").
aarch64 happens to get away without one because its firmware's default
stack is apparently larger, but that was never a guarantee.
On riscv64 the VM bootstrap's call depth (27 word-registration modules
-> physics/SSM init -> Tripod capsule birth) overflowed that small
stack, corrupting a return address and producing a wild jump / page
fault right after vm_init_with_host() returned — reproduced consistently
across the 2026-08-01 DoE campaign logs.
- src/starkernel/arch/riscv64/kernel_entry.S (new): RISC-V stack-switch
trampoline mirroring amd64's, giving the kernel a dedicated 2 MiB BSS
stack before anything deep runs.
- kernel_main.c: riscv64 now builds kernel_main_impl (invoked via the
trampoline) instead of kernel_main directly, same pattern as amd64.
- Makefile.starkernel: wires the new file into the riscv64 build.
- uefi_loader.c: RAW_LOG() was silently a no-op on every non-amd64 arch;
added a real raw-UART writer for riscv64 (QEMU virt's uart8250 at MMIO
0x10000000) so existing loader diagnostics actually produce output.
Verified: all three architectures boot clean to [Hera] ok> in the
required order (amd64, aarch64, riscv64); logs and DoE CSVs from these
runs included.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The aarch64 loader link step hardcoded the unversioned "lld-link", which
isn't on PATH by default on this Debian/Ubuntu setup (the package only
installs lld-link-18 under /usr/bin; unversioned lld-link lives under
/usr/lib/llvm-18/bin). CI worked around this with an explicit PATH prefix
in the workflow; a local build without that PATH override failed. Now
auto-detects whichever name resolves, falling back to the versioned name.
Verified: aarch64 builds clean with the default PATH.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kernel version only — the embedded StarForth engine version (3.1.0) is left
alone since it tracks a vendored copy that has genuinely diverged from the
standalone StarForth repo, not something to auto-sync. Verified builds on
amd64, aarch64, and riscv64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>