commit 89f8b2233d03e72ec1e61b7a4b40b853f4b2ea7c
parent 5073ee046922edc1734cd7c6abda83c25d743fef
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 5 May 2026 00:08:29 -0700
boot{0,1,2}: extract lib-pipeline DSL; add seed-kernel driver + accept
Refactor the inlined-heredoc run.sh blocks in boot0/1/2 onto a shared
DSL (pipeline_init / pipeline_input / stage / pipeline_export /
pipeline_run) with two backends:
podman — accumulates stages into one /work/run.sh, runs once in the
container (preserves the existing one-container-per-bootN
cost; podman is still the default).
seed — runs each stage as one qemu boot of seed-kernel via the
tier1-gate pattern (cpio /init + named inputs, dump tmpfs
over UART, extract). aarch64 only.
Each bootN.sh is now host prep + linear `stage` calls + exports;
no transport-specific code in the bootN scripts. Outputs are
byte-identical between DRIVER=podman and DRIVER=seed for all of
boot0/1/2.
scripts/seed-accept.sh: end-to-end Tier-2 acceptance. Boots the
boot2-built scheme1 on seed-kernel with a .scm driver that writes
log lines, spawns the boot2 catm as child-prog (clone + execve),
waitids it, opens+reads the resulting file, exits 0. Exercises
all eight Tier-1 syscalls plus the three Tier-2 ones.
Diffstat:
| M | docs/OS-TODO.md | | | 51 | ++++++++++++++++++++++++++++++++++++++++++++------- |
| M | scripts/boot0.sh | | | 108 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| M | scripts/boot1.sh | | | 131 | ++++++++++++++++++++++++++++++++++++------------------------------------------- |
| M | scripts/boot2.sh | | | 158 | ++++++++++++++++++++++++++++++++++--------------------------------------------- |
| A | scripts/lib-pipeline.sh | | | 189 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | scripts/seed-accept.sh | | | 162 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
6 files changed, 565 insertions(+), 234 deletions(-)
diff --git a/docs/OS-TODO.md b/docs/OS-TODO.md
@@ -8,7 +8,11 @@ three Tier-2 syscalls, and supports both the host-side verification
gates `scripts/tier1-gate.sh` and `scripts/tier2-gate.sh`. Verified
against `boot0/catm`, `boot1/M1pp`, and `boot3/tcc0`; the canonical
Tier-2 case (scheme1 driver spawns tcc0 to compile a `.c` into a
-relocatable ELF object) round-trips end-to-end.
+relocatable ELF object) round-trips end-to-end. The bootN scripts
+themselves now run on seed-kernel via `DRIVER=seed scripts/bootN.sh
+aarch64` for N∈{0,1,2}, producing byte-identical outputs to the
+podman path; `scripts/seed-accept.sh` exercises the boot2-built
+scheme1 spawning the boot2-built catm via the .scm prelude.
## Tier 1
@@ -105,12 +109,45 @@ relocatable ELF object) round-trips end-to-end.
## Things still worth doing (out of scope of the original list)
-- **Multi-stage Tier 1 driving**: `make tcc-boot2 ARCH=aarch64` could be
- taught to swap each podman invocation for `tier1-gate.sh`. The hooks
- exist; it would just be a `seed-kernel/Makefile.gate` overlay.
-- **Snapshot speed**: `mem_cpy(USER_POOL_SIZE = 768 MB)` is the dominant
- cost of every clone (~30 s under TCG). A copy-on-write or only-
- touched-pages strategy would help, but isn't needed for compliance.
+- **Port boot3/4/5 to the seed driver.** boot0/1/2 already run under
+ `DRIVER=seed` on top of the shared [`scripts/lib-pipeline.sh`](../scripts/lib-pipeline.sh)
+ DSL (one qemu boot per stage; outputs byte-identical to the podman
+ path). boot3/4/5 follow a different shape: host-side prep stays on
+ the host (`stage1-flatten.sh`, `libc-flatten.sh`, the cc.scm bundle
+ catm) and produces both the input files and a `run.scm` driver; in
+ the VM, scheme1 evaluates `run.scm` to orchestrate the per-bootN
+ pipeline via `(spawn …)` / `(run …)` against the chain binaries
+ (tcc0, tcc1, …) staged as child-progs in the cpio. Each bootN is
+ one qemu boot that replaces today's one `podman run` of an inlined
+ `run.sh`. `scripts/seed-accept.sh` proves the scheme1 + child-prog
+ cycle round-trips end-to-end on seed-kernel, so the prerequisites
+ are in place; the work is generating the per-bootN `run.scm` and a
+ shared driver harness analogous to `lib-pipeline.sh`.
+- **Pool-swap on execve instead of snapshot on clone.** With
+ `run.scm` driving boot3/4 inside the VM, every `(run "tccN" …)`
+ triggers `sys_clone`'s 768 MB `mem_cpy` (~30 s under TCG) followed
+ by a second 768 MB `mem_cpy` at child exit. The boot0/1/2 stages
+ move <100 KB of working RAM so this didn't matter for the existing
+ seed driver, but boot3 forks per tcc TU and the cost compounds.
+
+ Cheap structural fix: don't snapshot at clone; allocate a second
+ physical pool of the same size and swap the L2 user entries to
+ point at it on `execve`. The prelude's rigid
+ `clone → execve → exit → waitid` shape means the child only reads
+ the parent's image between clone and execve (running prelude scheme
+ bytecode); `do_execve` already captures path/argv into a kernel
+ pool before any user-VA-reading kernel code runs, so by the time
+ we're about to call `load_elf` the user pool is no longer needed
+ for the parent. Swap L2 → tlbi vmalle1 → load_elf writes the new
+ image into the second pool; on child exit, swap L2 back. Cost per
+ fork drops from 1.5 GB of memcpy to ~3 KB of L2 writes plus a TLB
+ invalidate. `MAX_PROC_DEPTH = 1` means two pools is enough; a stack
+ of pools would generalise if the contract ever grew nested forks.
+
+ Alternatives — copy-on-write or only-touched-pages tracking — work
+ but need per-page protection, a write-fault path, and a page
+ allocator the kernel doesn't currently have. The pool-swap fix
+ reuses the existing single-L2 single-allocation design.
- **NULL-page hardening**: slot 0 is unmapped so a NULL deref faults to
the kernel as a user sync; the kernel currently panics rather than
delivering a SIGSEGV-equivalent. Acceptable per OS.md (default-action
diff --git a/scripts/boot0.sh b/scripts/boot0.sh
@@ -5,28 +5,16 @@
## brings up: hex0 -> hex1 -> hex2 -> catm -> M0. Three of those (hex2,
## catm, M0) are the binaries every later stage depends on.
##
-## ─── Inputs (sources, copied into staging) ────────────────────────────
-## vendor/seed/$ARCH/hex0-seed — hex-byte seed binary (target ELF)
-## vendor/seed/$ARCH/hex0.hex0 — hex0 source for the hex0 assembler
-## vendor/seed/$ARCH/hex1.hex0 — hex0 source for the hex1 assembler
-## vendor/seed/$ARCH/hex2.hex1 — hex1 source for the hex2 assembler
-## vendor/seed/$ARCH/catm.hex2 — hex2 source for catm (binary cat)
-## vendor/seed/$ARCH/M0.hex2 — hex2 source for the M0 macro asm
-## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment (catm input)
-##
-## ─── Inputs (binaries from prior stages) ──────────────────────────────
-## none (this is stage 0).
-##
-## ─── Tools (in container) ─────────────────────────────────────────────
-## busybox sh + cp + mkdir + chmod (scratch + busybox image only).
+## ─── Inputs (sources) ─────────────────────────────────────────────────
+## vendor/seed/$ARCH/{hex0-seed, hex0.hex0, hex1.hex0, hex2.hex1,
+## catm.hex2, M0.hex2, ELF.hex2}
##
## ─── Outputs ──────────────────────────────────────────────────────────
-## build/$ARCH/boot0/hex2 — hex2 assembler
-## build/$ARCH/boot0/M0 — M0 macro assembler
-## build/$ARCH/boot0/catm — binary concatenator (cat with -m)
+## build/$ARCH/boot0/{hex2, catm, M0}
##
## Usage: scripts/boot0.sh <arch>
-## <arch> ∈ {aarch64, amd64, riscv64}
+## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default).
+## DRIVER=seed currently supports aarch64 only (uses seed-kernel).
set -eu
@@ -44,64 +32,52 @@ esac
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-IMAGE=boot2-scratch:$ARCH
+DRIVER=${DRIVER:-podman}
SEED=vendor/seed/$ARCH
OUT=build/$ARCH/boot0
STAGE=build/$ARCH/.boot0-stage
-# ── ensure container image exists ─────────────────────────────────────
-if ! podman image exists "$IMAGE"; then
- echo "[boot0 $ARCH] building $IMAGE"
- podman build --platform "$PLATFORM" -t "$IMAGE" \
- -f scripts/Containerfile.scratch scripts/
-fi
+case "$DRIVER" in
+ podman)
+ IMAGE=boot2-scratch:$ARCH
+ if ! podman image exists "$IMAGE"; then
+ echo "[boot0 $ARCH] building $IMAGE"
+ podman build --platform "$PLATFORM" -t "$IMAGE" \
+ -f scripts/Containerfile.scratch scripts/
+ fi
+ export PLATFORM IMAGE ;;
+ seed)
+ [ "$ARCH" = "aarch64" ] || { echo "[boot0] DRIVER=seed: aarch64 only" >&2; exit 2; }
+ KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot0] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ export KERNEL_IMAGE EXTRACT ;;
+ *) echo "[boot0] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
+esac
-# ── reset staging, copy inputs explicitly ─────────────────────────────
-rm -rf "$STAGE"
-mkdir -p "$STAGE/in" "$STAGE/out" "$OUT"
+. scripts/lib-pipeline.sh
+pipeline_init "$STAGE" "$OUT" "$DRIVER"
+# ─── inputs ───────────────────────────────────────────────────────────
for f in hex0-seed hex0.hex0 hex1.hex0 hex2.hex1 catm.hex2 M0.hex2 ELF.hex2; do
[ -e "$SEED/$f" ] || { echo "[boot0 $ARCH] missing input: $SEED/$f" >&2; exit 1; }
- cp "$SEED/$f" "$STAGE/in/$f"
+ pipeline_input "$f" "$SEED/$f"
done
-# ── materialize the container's run.sh into staging ───────────────────
-# Each boot stage writes the script the container will execute as a
-# concrete file under $STAGE/in/run.sh, then `podman run` just sh's it.
-# Easier to inspect/diff than a heredoc; matches how boot3/boot4 emit
-# their host-generated run.sh.
-cat > "$STAGE/in/run.sh" <<'RUN'
-#!/bin/sh
-set -eu
-# The stage0 tools do one syscall per byte. Build everything in /tmp
-# (RAM tmpfs from --tmpfs) and only cp the final binaries to /work/out.
-chmod +x /work/in/hex0-seed
-/work/in/hex0-seed /work/in/hex0.hex0 /tmp/hex0
-chmod +x /tmp/hex0
-/tmp/hex0 /work/in/hex1.hex0 /tmp/hex1
-chmod +x /tmp/hex1
-/tmp/hex1 /work/in/hex2.hex1 /tmp/hex2
-chmod +x /tmp/hex2
-/tmp/hex2 /work/in/catm.hex2 /tmp/catm
-chmod +x /tmp/catm
-/tmp/catm /tmp/M0.combined.hex2 /work/in/ELF.hex2 /work/in/M0.hex2
-/tmp/hex2 /tmp/M0.combined.hex2 /tmp/M0
-chmod +x /tmp/M0
-cp /tmp/hex2 /tmp/catm /tmp/M0 /work/out/
-RUN
-chmod +x "$STAGE/in/run.sh"
+# ─── pipeline ─────────────────────────────────────────────────────────
+echo "[boot0 $ARCH/$DRIVER] hex0-seed -> hex0 -> hex1 -> hex2 -> catm -> M0"
-# ── run the seed chain in the scratch container ───────────────────────
-echo "[boot0 $ARCH] hex0-seed -> hex0 -> hex1 -> hex2 -> catm -> M0"
-podman run --rm -i --pull=never --platform "$PLATFORM" \
- --tmpfs /tmp:size=512M \
- -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \
- sh -eu /work/in/run.sh
+stage hex0-seed hex0.hex0 hex0 -- hex0.hex0 -- hex0
+stage hex0 hex1.hex0 hex1 -- hex1.hex0 -- hex1
+stage hex1 hex2.hex1 hex2 -- hex2.hex1 -- hex2
+stage hex2 catm.hex2 catm -- catm.hex2 -- catm
+stage catm M0.combined.hex2 ELF.hex2 M0.hex2 -- ELF.hex2 M0.hex2 -- M0.combined.hex2
+stage hex2 M0.combined.hex2 M0 -- M0.combined.hex2 -- M0
-# ── copy outputs to final destination ─────────────────────────────────
-for f in hex2 catm M0; do
- cp "$STAGE/out/$f" "$OUT/$f"
- chmod 0700 "$OUT/$f"
-done
+pipeline_export hex2
+pipeline_export catm
+pipeline_export M0
+
+pipeline_run
-echo "[boot0 $ARCH] OK -> $OUT/{hex2, catm, M0}"
+echo "[boot0 $ARCH/$DRIVER] OK -> $OUT/{hex2, catm, M0}"
diff --git a/scripts/boot1.sh b/scripts/boot1.sh
@@ -5,25 +5,19 @@
## hex2pp pair, built from their .P1 sources via the seed M0 + hex2
## chain. catm is rebuilt from catm.P1pp in boot2.
##
-## ─── Inputs (sources, copied into staging) ────────────────────────────
-## M1pp/M1pp.P1 — M1pp expander, P1 source
-## hex2pp/hex2pp.P1 — hex2pp assembler/linker, P1 source
-## P1/P1-$ARCH.M1 — pre-pruned per-arch P1 backend
-## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment (catm input)
+## ─── Inputs (sources) ─────────────────────────────────────────────────
+## M1pp/M1pp.P1, hex2pp/hex2pp.P1
+## P1/P1-$ARCH.M1, vendor/seed/$ARCH/ELF.hex2
##
## ─── Inputs (binaries from prior stages) ──────────────────────────────
-## build/$ARCH/boot0/{hex2, M0, catm} — built by scripts/boot0.sh
-##
-## ─── Tools (in container) ─────────────────────────────────────────────
-## busybox sh + cp + mkdir + chmod (scratch + busybox image only).
-## Plus the boot0 binaries (M0, catm, hex2), staged in.
+## build/$ARCH/boot0/{hex2, M0, catm}
##
## ─── Outputs ──────────────────────────────────────────────────────────
-## build/$ARCH/boot1/M1pp — M1pp expander ELF
-## build/$ARCH/boot1/hex2pp — hex2pp assembler/linker ELF
+## build/$ARCH/boot1/{M1pp, hex2pp}
##
## Usage: scripts/boot1.sh <arch>
-## <arch> ∈ {aarch64, amd64, riscv64}
+## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default).
+## DRIVER=seed currently supports aarch64 only.
set -eu
@@ -41,19 +35,11 @@ esac
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-IMAGE=boot2-scratch:$ARCH
+DRIVER=${DRIVER:-podman}
BOOT0=build/$ARCH/boot0
OUT=build/$ARCH/boot1
STAGE=build/$ARCH/.boot1-stage
-# ── ensure container image exists ─────────────────────────────────────
-if ! podman image exists "$IMAGE"; then
- echo "[boot1 $ARCH] building $IMAGE"
- podman build --platform "$PLATFORM" -t "$IMAGE" \
- -f scripts/Containerfile.scratch scripts/
-fi
-
-# ── prerequisite: boot0 binaries must exist ───────────────────────────
for bin in hex2 M0 catm; do
[ -x "$BOOT0/$bin" ] || {
echo "[boot1 $ARCH] missing prerequisite: $BOOT0/$bin (run scripts/boot0.sh $ARCH)" >&2
@@ -61,57 +47,60 @@ for bin in hex2 M0 catm; do
}
done
-# ── reset staging, copy inputs explicitly ─────────────────────────────
-rm -rf "$STAGE"
-mkdir -p "$STAGE/in" "$STAGE/out" "$OUT"
-
-cp "$BOOT0/hex2" "$BOOT0/M0" "$BOOT0/catm" "$STAGE/in/"
-cp M1pp/M1pp.P1 "$STAGE/in/M1pp.P1"
-cp hex2pp/hex2pp.P1 "$STAGE/in/hex2pp.P1"
-cp "P1/P1-$ARCH.M1" "$STAGE/in/P1.M1"
-cp "vendor/seed/$ARCH/ELF.hex2" "$STAGE/in/ELF.hex2"
-
-# ── run the build pipeline ────────────────────────────────────────────
-# .P1 -> ELF via M0 + hex2 (seed), run twice — for M1pp and hex2pp:
-# catm combined.M1 P1.M1 src (per-arch backend prepended)
-# M0 combined.M1 -> prog.hex2
-# catm linked.hex2 ELF.hex2 prog.hex2
-# hex2 linked.hex2 -> ELF binary
-#
-# Stages everything through /tmp because the M0/hex2 seed tools do one
-# syscall per byte; virtiofs round-trips would dominate.
-cat > "$STAGE/in/run.sh" <<'RUN'
-#!/bin/sh
-set -eu
-# Inlined (no function) so the container shell sees only sequential
-# exec — kaem-friendly.
+case "$DRIVER" in
+ podman)
+ IMAGE=boot2-scratch:$ARCH
+ if ! podman image exists "$IMAGE"; then
+ echo "[boot1 $ARCH] building $IMAGE"
+ podman build --platform "$PLATFORM" -t "$IMAGE" \
+ -f scripts/Containerfile.scratch scripts/
+ fi
+ export PLATFORM IMAGE ;;
+ seed)
+ [ "$ARCH" = "aarch64" ] || { echo "[boot1] DRIVER=seed: aarch64 only" >&2; exit 2; }
+ KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot1] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ export KERNEL_IMAGE EXTRACT ;;
+ *) echo "[boot1] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
+esac
+
+. scripts/lib-pipeline.sh
+pipeline_init "$STAGE" "$OUT" "$DRIVER"
+
+# ─── inputs ───────────────────────────────────────────────────────────
+pipeline_input hex2 "$BOOT0/hex2"
+pipeline_input M0 "$BOOT0/M0"
+pipeline_input catm "$BOOT0/catm"
+pipeline_input P1.M1 "P1/P1-$ARCH.M1"
+pipeline_input ELF.hex2 "vendor/seed/$ARCH/ELF.hex2"
+pipeline_input M1pp.P1 "M1pp/M1pp.P1"
+pipeline_input hex2pp.P1 "hex2pp/hex2pp.P1"
+
+# ─── pipeline ─────────────────────────────────────────────────────────
+echo "[boot1 $ARCH/$DRIVER] M1pp.P1 + hex2pp.P1 -> M1pp + hex2pp"
+
+# .P1 -> ELF via M0 + hex2:
+# catm P1.M1 + src -> combined.M1
+# M0 combined.M1 -> prog.hex2
+# catm ELF.hex2 + prog.hex2 -> linked.hex2
+# hex2 linked.hex2 -> ELF binary
# M1pp.P1 -> M1pp
-/work/in/catm /tmp/combined.M1 /work/in/P1.M1 /work/in/M1pp.P1
-/work/in/M0 /tmp/combined.M1 /tmp/prog.hex2
-/work/in/catm /tmp/linked.hex2 /work/in/ELF.hex2 /tmp/prog.hex2
-/work/in/hex2 /tmp/linked.hex2 /work/out/M1pp
-chmod +x /work/out/M1pp
+stage catm combined.M1 P1.M1 M1pp.P1 -- P1.M1 M1pp.P1 -- combined.M1
+stage M0 combined.M1 prog.hex2 -- combined.M1 -- prog.hex2
+stage catm linked.hex2 ELF.hex2 prog.hex2 -- ELF.hex2 prog.hex2 -- linked.hex2
+stage hex2 linked.hex2 M1pp -- linked.hex2 -- M1pp
# hex2pp.P1 -> hex2pp
-/work/in/catm /tmp/combined.M1 /work/in/P1.M1 /work/in/hex2pp.P1
-/work/in/M0 /tmp/combined.M1 /tmp/prog.hex2
-/work/in/catm /tmp/linked.hex2 /work/in/ELF.hex2 /tmp/prog.hex2
-/work/in/hex2 /tmp/linked.hex2 /work/out/hex2pp
-chmod +x /work/out/hex2pp
-RUN
-chmod +x "$STAGE/in/run.sh"
-
-echo "[boot1 $ARCH] M1pp.P1 + hex2pp.P1 -> M1pp + hex2pp"
-podman run --rm -i --pull=never --platform "$PLATFORM" \
- --tmpfs /tmp:size=512M \
- -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \
- sh -eu /work/in/run.sh
-
-# ── copy outputs to final destination ─────────────────────────────────
-for f in M1pp hex2pp; do
- cp "$STAGE/out/$f" "$OUT/$f"
- chmod 0700 "$OUT/$f"
-done
+stage catm combined.M1 P1.M1 hex2pp.P1 -- P1.M1 hex2pp.P1 -- combined.M1
+stage M0 combined.M1 prog.hex2 -- combined.M1 -- prog.hex2
+stage catm linked.hex2 ELF.hex2 prog.hex2 -- ELF.hex2 prog.hex2 -- linked.hex2
+stage hex2 linked.hex2 hex2pp -- linked.hex2 -- hex2pp
+
+pipeline_export M1pp
+pipeline_export hex2pp
+
+pipeline_run
-echo "[boot1 $ARCH] OK -> $OUT/{M1pp, hex2pp}"
+echo "[boot1 $ARCH/$DRIVER] OK -> $OUT/{M1pp, hex2pp}"
diff --git a/scripts/boot2.sh b/scripts/boot2.sh
@@ -5,34 +5,22 @@
## via the freshly-built M1pp + hex2pp pipeline (replacing the seed
## boot0 catm so later stages run with zero boot0 dependencies); then
## builds the scheme1 interpreter from scheme1.P1pp using the new catm.
-## End-to-end through M1pp + hex2pp (no seed M0/hex2 anywhere on the
-## .P1pp pipeline).
##
-## ─── Inputs (sources, copied into staging) ────────────────────────────
-## catm/catm.P1pp — catm, P1pp source
-## scheme1/scheme1.P1pp — interpreter source
-## P1/P1-$ARCH.M1pp — per-arch M1pp backend
-## P1/P1.M1pp — arch-agnostic P1pp frontend
-## P1/P1pp.P1pp — libp1pp standard library
-## vendor/seed/$ARCH/ELF.hex2 — ELF header fragment
+## ─── Inputs (sources) ─────────────────────────────────────────────────
+## catm/catm.P1pp, scheme1/scheme1.P1pp
+## P1/P1-$ARCH.M1pp, P1/P1.M1pp, P1/P1pp.P1pp
+## vendor/seed/$ARCH/ELF.hex2
##
## ─── Inputs (binaries from prior stages) ──────────────────────────────
-## build/$ARCH/boot0/catm — built by scripts/boot0.sh
-## (used only to bootstrap the
-## catm.P1pp build; replaced by
-## the new catm afterwards)
-## build/$ARCH/boot1/{M1pp, hex2pp} — built by scripts/boot1.sh
-##
-## ─── Tools (in container) ─────────────────────────────────────────────
-## busybox sh + cp + mkdir + chmod (scratch + busybox image only).
-## Plus the boot0 catm and boot1 M1pp + hex2pp, staged in.
+## build/$ARCH/boot0/catm (only to bootstrap catm.P1pp build)
+## build/$ARCH/boot1/{M1pp, hex2pp}
##
## ─── Outputs ──────────────────────────────────────────────────────────
-## build/$ARCH/boot2/catm — catm ELF (rebuilt via M1pp+hex2pp)
-## build/$ARCH/boot2/scheme1 — scheme1 interpreter ELF
+## build/$ARCH/boot2/{catm, scheme1}
##
## Usage: scripts/boot2.sh <arch>
-## <arch> ∈ {aarch64, amd64, riscv64}
+## <arch> ∈ {aarch64, amd64, riscv64} for DRIVER=podman (default).
+## DRIVER=seed currently supports aarch64 only.
set -eu
@@ -50,20 +38,12 @@ esac
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
-IMAGE=boot2-scratch:$ARCH
+DRIVER=${DRIVER:-podman}
BOOT0=build/$ARCH/boot0
BOOT1=build/$ARCH/boot1
OUT=build/$ARCH/boot2
STAGE=build/$ARCH/.boot2-stage
-# ── ensure container image exists ─────────────────────────────────────
-if ! podman image exists "$IMAGE"; then
- echo "[boot2 $ARCH] building $IMAGE"
- podman build --platform "$PLATFORM" -t "$IMAGE" \
- -f scripts/Containerfile.scratch scripts/
-fi
-
-# ── prerequisite: prior-stage binaries must exist ─────────────────────
[ -x "$BOOT0/catm" ] || {
echo "[boot2 $ARCH] missing prerequisite: $BOOT0/catm (run scripts/boot0.sh $ARCH)" >&2
exit 1
@@ -75,66 +55,64 @@ for bin in M1pp hex2pp; do
}
done
-# ── reset staging, copy inputs explicitly ─────────────────────────────
-rm -rf "$STAGE"
-mkdir -p "$STAGE/in" "$STAGE/out" "$OUT"
-
-cp "$BOOT0/catm" "$STAGE/in/catm"
-cp "$BOOT1/M1pp" "$BOOT1/hex2pp" "$STAGE/in/"
-cp catm/catm.P1pp "$STAGE/in/catm.P1pp"
-cp scheme1/scheme1.P1pp "$STAGE/in/scheme1.P1pp"
-cp "P1/P1-$ARCH.M1pp" "$STAGE/in/backend.M1pp"
-cp P1/P1.M1pp "$STAGE/in/frontend.M1pp"
-cp P1/P1pp.P1pp "$STAGE/in/libp1pp.P1pp"
-cp "vendor/seed/$ARCH/ELF.hex2" "$STAGE/in/ELF.hex2"
-
-# ── run the build pipelines ───────────────────────────────────────────
-# Two .P1pp -> ELF pipelines, run back to back in a single container:
-#
-# catm.P1pp -> catm (boot0 catm bootstraps; produced binary then
-# takes over for the rest of this script + boot3)
-# catm combined.M1pp backend + frontend + libp1pp + catm.P1pp
-# M1pp combined.M1pp -> expanded.hex2pp
-# catm linked.hex2pp ELF.hex2 expanded.hex2pp
-# hex2pp -B 0x600000 linked.hex2pp -> ELF binary
-#
-# scheme1.P1pp -> scheme1 (uses the just-built catm)
-# (same shape, with /work/in/scheme1.P1pp in place of catm.P1pp)
-cat > "$STAGE/in/run.sh" <<'RUN'
-#!/bin/sh
-set -eu
-# Inlined (no function) so the container shell sees only sequential
-# exec — kaem-friendly.
+case "$DRIVER" in
+ podman)
+ IMAGE=boot2-scratch:$ARCH
+ if ! podman image exists "$IMAGE"; then
+ echo "[boot2 $ARCH] building $IMAGE"
+ podman build --platform "$PLATFORM" -t "$IMAGE" \
+ -f scripts/Containerfile.scratch scripts/
+ fi
+ export PLATFORM IMAGE ;;
+ seed)
+ [ "$ARCH" = "aarch64" ] || { echo "[boot2] DRIVER=seed: aarch64 only" >&2; exit 2; }
+ KERNEL_IMAGE=$ROOT/seed-kernel/build/Image
+ EXTRACT=$ROOT/seed-kernel/scripts/extract-dump.sh
+ [ -f "$KERNEL_IMAGE" ] || { echo "[boot2] missing $KERNEL_IMAGE — make in seed-kernel/" >&2; exit 1; }
+ export KERNEL_IMAGE EXTRACT ;;
+ *) echo "[boot2] unknown DRIVER=$DRIVER" >&2; exit 2 ;;
+esac
+
+. scripts/lib-pipeline.sh
+pipeline_init "$STAGE" "$OUT" "$DRIVER"
+
+# ─── inputs ───────────────────────────────────────────────────────────
+pipeline_input catm0 "$BOOT0/catm" # bootstrap; replaced by output 'catm'
+pipeline_input M1pp "$BOOT1/M1pp"
+pipeline_input hex2pp "$BOOT1/hex2pp"
+pipeline_input backend.M1pp "P1/P1-$ARCH.M1pp"
+pipeline_input frontend.M1pp "P1/P1.M1pp"
+pipeline_input libp1pp.P1pp "P1/P1pp.P1pp"
+pipeline_input ELF.hex2 "vendor/seed/$ARCH/ELF.hex2"
+pipeline_input catm.P1pp "catm/catm.P1pp"
+pipeline_input scheme1.P1pp "scheme1/scheme1.P1pp"
+
+# ─── pipeline ─────────────────────────────────────────────────────────
+echo "[boot2 $ARCH/$DRIVER] catm.P1pp -> catm; scheme1.P1pp -> scheme1"
+
+# .P1pp -> ELF:
+# catm backend + frontend + libp1pp + src -> combined.M1pp
+# M1pp combined.M1pp -> expanded.hex2pp
+# catm ELF.hex2 + expanded.hex2pp -> linked.hex2pp
+# hex2pp -B 0x600000 linked.hex2pp -> ELF binary
# catm.P1pp -> catm (bootstrap with boot0 catm)
-/work/in/catm /tmp/combined.M1pp \
- /work/in/backend.M1pp /work/in/frontend.M1pp \
- /work/in/libp1pp.P1pp /work/in/catm.P1pp
-/work/in/M1pp /tmp/combined.M1pp /tmp/expanded.hex2pp
-/work/in/catm /tmp/linked.hex2pp /work/in/ELF.hex2 /tmp/expanded.hex2pp
-/work/in/hex2pp -B 0x600000 /tmp/linked.hex2pp /work/out/catm
-chmod +x /work/out/catm
-
-# scheme1.P1pp -> scheme1 (uses the just-built catm)
-/work/out/catm /tmp/combined.M1pp \
- /work/in/backend.M1pp /work/in/frontend.M1pp \
- /work/in/libp1pp.P1pp /work/in/scheme1.P1pp
-/work/in/M1pp /tmp/combined.M1pp /tmp/expanded.hex2pp
-/work/out/catm /tmp/linked.hex2pp /work/in/ELF.hex2 /tmp/expanded.hex2pp
-/work/in/hex2pp -B 0x600000 /tmp/linked.hex2pp /work/out/scheme1
-RUN
-chmod +x "$STAGE/in/run.sh"
-
-echo "[boot2 $ARCH] catm.P1pp -> catm; scheme1.P1pp -> scheme1"
-podman run --rm -i --pull=never --platform "$PLATFORM" \
- --tmpfs /tmp:size=512M \
- -v "$ROOT/$STAGE:/work" -w /work "$IMAGE" \
- sh -eu /work/in/run.sh
-
-# ── copy outputs to final destination ─────────────────────────────────
-for f in catm scheme1; do
- cp "$STAGE/out/$f" "$OUT/$f"
- chmod 0700 "$OUT/$f"
-done
+stage catm0 combined.M1pp backend.M1pp frontend.M1pp libp1pp.P1pp catm.P1pp \
+ -- backend.M1pp frontend.M1pp libp1pp.P1pp catm.P1pp -- combined.M1pp
+stage M1pp combined.M1pp expanded.hex2pp -- combined.M1pp -- expanded.hex2pp
+stage catm0 linked.hex2pp ELF.hex2 expanded.hex2pp -- ELF.hex2 expanded.hex2pp -- linked.hex2pp
+stage hex2pp -B 0x600000 linked.hex2pp catm -- linked.hex2pp -- catm
+
+# scheme1.P1pp -> scheme1 (uses just-built catm)
+stage catm combined.M1pp backend.M1pp frontend.M1pp libp1pp.P1pp scheme1.P1pp \
+ -- backend.M1pp frontend.M1pp libp1pp.P1pp scheme1.P1pp -- combined.M1pp
+stage M1pp combined.M1pp expanded.hex2pp -- combined.M1pp -- expanded.hex2pp
+stage catm linked.hex2pp ELF.hex2 expanded.hex2pp -- ELF.hex2 expanded.hex2pp -- linked.hex2pp
+stage hex2pp -B 0x600000 linked.hex2pp scheme1 -- linked.hex2pp -- scheme1
+
+pipeline_export catm
+pipeline_export scheme1
+
+pipeline_run
-echo "[boot2 $ARCH] OK -> $OUT/{catm, scheme1}"
+echo "[boot2 $ARCH/$DRIVER] OK -> $OUT/{catm, scheme1}"
diff --git a/scripts/lib-pipeline.sh b/scripts/lib-pipeline.sh
@@ -0,0 +1,189 @@
+# lib-pipeline.sh — driver-agnostic DSL for boot stage pipelines.
+#
+# A bootN.sh's "wiring" is a sequence of file→file program invocations
+# in a flat namespace. This library exposes that as four primitives so
+# the same wiring can run under different transports:
+#
+# podman — accumulate stages into one /work/run.sh, run once in a
+# container against $IMAGE / $PLATFORM (env-set by caller).
+# seed — run each stage as one qemu boot of seed-kernel via
+# tier1-gate.sh's pattern (cpio /init + inputs, dump tmpfs
+# over UART, extract). aarch64 only.
+#
+# DSL (source as `. scripts/lib-pipeline.sh`):
+#
+# pipeline_init <staging-dir> <out-dir> <driver>
+# pipeline_input <name> <host-path> # may be called repeatedly
+# stage <bin> <argv1> <argv2>... -- <input-names...> -- <output-names...>
+# pipeline_export <name> # may be called repeatedly
+# pipeline_run
+#
+# `stage` semantics: invoke `<bin>` with argv=[<bin>, <argv1>, ...]; the
+# stage reads the listed input names and produces the listed output
+# names. <bin> is also a name in the flat namespace — typically a
+# pipeline_input, but may be the output of an earlier stage.
+#
+# Required env for podman driver: PLATFORM, IMAGE.
+# Required env for seed driver: KERNEL_IMAGE, EXTRACT.
+
+P_DRIVER=
+P_STAGE_DIR=
+P_OUT_DIR=
+P_SCRIPT=
+P_IDX=0
+P_EXPORTS=
+
+pipeline_init() {
+ P_STAGE_DIR=$1; P_OUT_DIR=$2; P_DRIVER=$3
+ rm -rf "$P_STAGE_DIR"
+ mkdir -p "$P_STAGE_DIR/in" "$P_OUT_DIR"
+ P_IDX=0
+ P_EXPORTS=
+ case "$P_DRIVER" in
+ podman)
+ mkdir -p "$P_STAGE_DIR/out"
+ P_SCRIPT=$P_STAGE_DIR/run.sh
+ {
+ echo '#!/bin/sh'
+ echo 'set -eu'
+ # Stage everything in /tmp (RAM tmpfs) — the seed-stage tools
+ # do one syscall per byte, virtiofs round-trips would dominate.
+ echo 'cp /work/in/* /tmp/'
+ echo 'cd /tmp'
+ } > "$P_SCRIPT"
+ ;;
+ seed)
+ mkdir -p "$P_STAGE_DIR/work"
+ : "${KERNEL_IMAGE:?lib-pipeline:seed: KERNEL_IMAGE not set}"
+ : "${EXTRACT:?lib-pipeline:seed: EXTRACT not set}"
+ ;;
+ *)
+ echo "lib-pipeline: unknown driver '$P_DRIVER'" >&2; exit 2 ;;
+ esac
+}
+
+pipeline_input() {
+ name=$1; src=$2
+ cp "$src" "$P_STAGE_DIR/in/$name"
+ if [ "$P_DRIVER" = "seed" ]; then
+ cp "$src" "$P_STAGE_DIR/work/$name"
+ fi
+}
+
+stage() {
+ bin=$1; shift
+ P_HEAD=""; P_IN=""; P_OUT=""; _s=head
+ while [ $# -gt 0 ]; do
+ if [ "$1" = "--" ]; then
+ case "$_s" in
+ head) _s=in ;;
+ in) _s=out ;;
+ *) echo "lib-pipeline: too many --" >&2; exit 2 ;;
+ esac
+ shift; continue
+ fi
+ case "$_s" in
+ head) P_HEAD="$P_HEAD $1" ;;
+ in) P_IN="$P_IN $1" ;;
+ out) P_OUT="$P_OUT $1" ;;
+ esac
+ shift
+ done
+ [ "$_s" = "out" ] || { echo "lib-pipeline: stage needs '<bin> argv... -- inputs... -- outputs...'" >&2; exit 2; }
+ P_IDX=$((P_IDX + 1))
+ case "$P_DRIVER" in
+ podman) _stage_podman ;;
+ seed) _stage_seed ;;
+ esac
+}
+
+_stage_podman() {
+ {
+ echo "# stage $P_IDX: $bin$P_HEAD"
+ echo "chmod +x ./$bin"
+ echo "./$bin$P_HEAD"
+ } >> "$P_SCRIPT"
+}
+
+_stage_seed() {
+ cpio_dir=$P_STAGE_DIR/s$(printf '%02d' "$P_IDX")
+ rm -rf "$cpio_dir"; mkdir -p "$cpio_dir/cpio"
+ cp "$P_STAGE_DIR/work/$bin" "$cpio_dir/cpio/init"
+ chmod +x "$cpio_dir/cpio/init"
+ NAMES="init"
+ for inp in $P_IN; do
+ cp "$P_STAGE_DIR/work/$inp" "$cpio_dir/cpio/$inp"
+ NAMES="$NAMES
+$inp"
+ done
+ INITRAMFS=$cpio_dir/initramfs.cpio
+ ( cd "$cpio_dir/cpio" && printf '%s\n' "$NAMES" | cpio -o -H newc 2>/dev/null ) > "$INITRAMFS"
+
+ APPEND="$bin$P_HEAD dumpfs"
+ TRANSCRIPT=$cpio_dir/transcript.txt
+ echo "[lib-pipeline:seed] stage $P_IDX:$P_HEAD (bin=$bin)" >&2
+ qemu-system-aarch64 \
+ -machine virt -cpu cortex-a72 -m 2048M \
+ -nographic -no-reboot \
+ -kernel "$KERNEL_IMAGE" -initrd "$INITRAMFS" \
+ -append "$APPEND" \
+ > "$TRANSCRIPT" 2>&1 &
+ QPID=$!
+ ( sleep 240; kill -9 $QPID 2>/dev/null ) &
+ WATCHER=$!
+ wait $QPID 2>/dev/null || true
+ kill $WATCHER 2>/dev/null || true
+
+ if ! grep -q '=== DUMP-END ===' "$TRANSCRIPT"; then
+ echo "[lib-pipeline:seed] FAIL stage $P_IDX (bin=$bin): no DUMP-END" >&2
+ tail -40 "$TRANSCRIPT" >&2
+ exit 3
+ fi
+
+ mkdir -p "$cpio_dir/dump"
+ "$EXTRACT" "$cpio_dir/dump" "$TRANSCRIPT" >/dev/null 2>&1 || true
+
+ for o in $P_OUT; do
+ if [ ! -f "$cpio_dir/dump/$o" ]; then
+ echo "[lib-pipeline:seed] FAIL stage $P_IDX: missing output '$o'" >&2
+ ls "$cpio_dir/dump" >&2 || true
+ exit 3
+ fi
+ cp "$cpio_dir/dump/$o" "$P_STAGE_DIR/work/$o"
+ done
+}
+
+pipeline_export() {
+ P_EXPORTS="$P_EXPORTS $1"
+}
+
+pipeline_run() {
+ case "$P_DRIVER" in
+ podman) _run_podman ;;
+ seed) : ;;
+ esac
+ for n in $P_EXPORTS; do
+ case "$P_DRIVER" in
+ podman) cp "$P_STAGE_DIR/out/$n" "$P_OUT_DIR/$n" ;;
+ seed) cp "$P_STAGE_DIR/work/$n" "$P_OUT_DIR/$n" ;;
+ esac
+ chmod 0700 "$P_OUT_DIR/$n"
+ done
+}
+
+_run_podman() {
+ : "${PLATFORM:?lib-pipeline:podman: PLATFORM not set}"
+ : "${IMAGE:?lib-pipeline:podman: IMAGE not set}"
+ if [ -n "$P_EXPORTS" ]; then
+ cmd="cp"
+ for n in $P_EXPORTS; do cmd="$cmd $n"; done
+ cmd="$cmd /work/out/"
+ echo "$cmd" >> "$P_SCRIPT"
+ fi
+ chmod +x "$P_SCRIPT"
+ SDIR=$(cd "$P_STAGE_DIR" && pwd)
+ podman run --rm -i --pull=never --platform "$PLATFORM" \
+ --tmpfs /tmp:size=512M \
+ -v "$SDIR:/work" -w /work "$IMAGE" \
+ sh -eu /work/run.sh
+}
diff --git a/scripts/seed-accept.sh b/scripts/seed-accept.sh
@@ -0,0 +1,162 @@
+#!/bin/sh
+## seed-accept.sh — Tier-2 acceptance for boot0/1/2 on seed-kernel.
+##
+## Loads the boot2-built scheme1 as /init in the seed kernel, runs a
+## .scm driver that:
+## 1. Logs "hello" to stdout (sys_write fd=1 → UART).
+## 2. Spawns child-prog (=catm from boot2) via clone+execve to
+## concatenate two files A + B → C in the in-memory tmpfs.
+## 3. waitids the child, reads C back, prints it to stdout.
+## 4. exit_group(0).
+##
+## End-to-end exercise of every Tier-1 syscall (read/write/openat/close/
+## brk/exit_group) plus the three Tier-2 ones (clone/execve/waitid).
+##
+## Verifies the transcript contains the expected log lines and that
+## sys_exit_or_resume_parent saw the child exit cleanly.
+##
+## Usage: scripts/seed-accept.sh
+
+set -eu
+
+ARCH=aarch64
+ROOT=$(cd "$(dirname "$0")/.." && pwd)
+cd "$ROOT"
+
+KERNEL=seed-kernel/build/Image
+EXTRACT=seed-kernel/scripts/extract-dump.sh
+SCHEME1=build/$ARCH/boot2/scheme1
+CATM=build/$ARCH/boot2/catm
+PRELUDE=scheme1/prelude.scm
+
+[ -f "$KERNEL" ] || { echo "missing $KERNEL — run 'make' in seed-kernel/" >&2; exit 1; }
+[ -x "$SCHEME1" ] || { echo "missing $SCHEME1 — run boot2 first" >&2; exit 1; }
+[ -x "$CATM" ] || { echo "missing $CATM — run boot2 first" >&2; exit 1; }
+
+OUTDIR=$ROOT/build/$ARCH/seed-accept
+rm -rf "$OUTDIR"; mkdir -p "$OUTDIR"
+
+STAGE=$(mktemp -d -t seed-accept.XXXXXX)
+trap 'rm -rf "$STAGE"' EXIT
+
+# ─── driver.scm — the in-VM acceptance program ────────────────────────
+cat > "$STAGE/driver.scm" <<'SCM'
+;; driver.scm — Tier-2 acceptance for seed-kernel.
+(write-string stdout "scheme1: hello from acceptance driver\n")
+(write-string stdout "scheme1: spawning child-prog (catm) C <- A + B\n")
+
+(let ((r (run "child-prog" "C" "A" "B")))
+ (if (car r)
+ (begin
+ (write-string stdout "scheme1: child returned\n"))
+ (begin
+ (write-string stdout "scheme1: spawn FAILED\n")
+ (exit 1))))
+
+(let ((rp (open-input "C")))
+ (if (car rp)
+ (let* ((p (cdr rp))
+ (rb (read-all p)))
+ (if (car rb)
+ (begin
+ (write-string stdout "scheme1: read C: [")
+ (write-bytes stdout (cdr rb))
+ (write-string stdout "]\n"))
+ (write-string stdout "scheme1: read C FAILED\n"))
+ (close p))
+ (write-string stdout "scheme1: open C FAILED\n")))
+
+(write-string stdout "scheme1: ALL-OK\n")
+(exit 0)
+SCM
+
+# ─── Combine prelude + driver via host catm — this matches the chain's
+# own boot-run-scheme1.sh wrapper, so the .scm shape is identical
+# to what scheme1 expects everywhere. ──────────────────────────────
+cat "$PRELUDE" "$STAGE/driver.scm" > "$STAGE/combined.scm"
+
+# ─── Two demo input files. catm reads them from the in-VM tmpfs. ──────
+printf 'Hello, ' > "$STAGE/A"
+printf 'seed-kernel!\n' > "$STAGE/B"
+
+# ─── Stage the cpio: /init=scheme1, /child-prog=catm, plus inputs. ────
+mkdir -p "$STAGE/cpio"
+cp "$SCHEME1" "$STAGE/cpio/init"; chmod +x "$STAGE/cpio/init"
+cp "$CATM" "$STAGE/cpio/child-prog"; chmod +x "$STAGE/cpio/child-prog"
+cp "$STAGE/combined.scm" "$STAGE/cpio/combined.scm"
+cp "$STAGE/A" "$STAGE/cpio/A"
+cp "$STAGE/B" "$STAGE/cpio/B"
+
+NAMES='init
+child-prog
+combined.scm
+A
+B'
+
+INITRAMFS=$STAGE/initramfs.cpio
+( cd "$STAGE/cpio" && printf '%s\n' "$NAMES" | cpio -o -H newc 2>/dev/null ) > "$INITRAMFS"
+
+TRANSCRIPT=$OUTDIR/transcript.txt
+echo "[seed-accept] booting scheme1 + driver.scm on seed-kernel"
+qemu-system-aarch64 \
+ -machine virt -cpu cortex-a72 -m 2048M \
+ -nographic -no-reboot \
+ -kernel "$KERNEL" -initrd "$INITRAMFS" \
+ -append "init combined.scm dumpfs" \
+ > "$TRANSCRIPT" 2>&1 &
+QPID=$!
+( sleep 240; kill -9 $QPID 2>/dev/null ) &
+WATCHER=$!
+wait $QPID 2>/dev/null || true
+kill $WATCHER 2>/dev/null || true
+
+if ! grep -q '=== DUMP-END ===' "$TRANSCRIPT"; then
+ echo "[seed-accept] FAIL: no DUMP-END in transcript" >&2
+ tail -60 "$TRANSCRIPT" >&2
+ exit 3
+fi
+
+# Extract files (we want C from the tmpfs).
+"$EXTRACT" "$OUTDIR" "$TRANSCRIPT" >/dev/null 2>&1 || \
+ "$EXTRACT" "$OUTDIR" "$TRANSCRIPT" >&2
+
+# ─── Verify ───────────────────────────────────────────────────────────
+fail=0
+for needle in \
+ 'scheme1: hello from acceptance driver' \
+ 'scheme1: spawning child-prog' \
+ 'scheme1: child returned' \
+ 'scheme1: read C: \[Hello, seed-kernel!' \
+ 'scheme1: ALL-OK' \
+ 'exit_group(0)'
+do
+ if ! grep -q "$needle" "$TRANSCRIPT"; then
+ echo "[seed-accept] MISSING in transcript: $needle" >&2
+ fail=1
+ fi
+done
+
+if [ ! -f "$OUTDIR/C" ]; then
+ echo "[seed-accept] MISSING extracted file: $OUTDIR/C" >&2
+ fail=1
+elif ! diff -q "$OUTDIR/C" - <<EOF >/dev/null
+Hello, seed-kernel!
+EOF
+then
+ echo "[seed-accept] C differs from expected:" >&2
+ od -c "$OUTDIR/C" | head -3 >&2
+ fail=1
+fi
+
+if [ $fail -ne 0 ]; then
+ echo "[seed-accept] FAIL — see $TRANSCRIPT" >&2
+ exit 4
+fi
+
+echo ""
+echo "=== driver log (excerpt from transcript) ==="
+grep '^scheme1:' "$TRANSCRIPT" || true
+echo "==========================================="
+echo ""
+echo "[seed-accept] PASS — scheme1 + .scm + child-prog cycle complete"
+echo "[seed-accept] artifacts in $OUTDIR/"