summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-27 16:42:15 -0300
committerGabriel Schneider <[email protected]>2026-08-27 22:09:08 -0300
commit98b62629795e24de19f535f1072b8a25dcf9f018 (patch)
treefedea7e9b6648236220c054c30e6d2b4f8938288
parent147ebd4a36ec7199074ba05bcfb79d4a656c0b74 (diff)
downloadpardes-98b62629795e24de19f535f1072b8a25dcf9f018.tar.gz
pardes-98b62629795e24de19f535f1072b8a25dcf9f018.zip
build: pardes builds without the ESP32-P4 toolchain checkout beside it
`build.zig.zon` named `../05-zig-p4` as a path dependency, and `build.zig` `@import`ed it inside `if (-Desp32p4-firmware)`. But `@import` in a build script is resolved when the SCRIPT is compiled, not when the branch that needs it is taken -- so naming the package at all meant anyone without that sibling checkout could not build pardes AT ALL. Not the firmware: the terminal shell, the SDL shell, the tests. `zig build` failed with build.zig:1073: error: no module named 'zig_p4' available within module 'root.@build' from a line inside an `if` that was false. Neither escape hatch works for a PATH dependency, and both were tried rather than assumed. `.lazy = true` is about FETCHING; a path dep whose directory is absent is generated as a package with no `build.zig` rather than one marked unavailable, so `b.lazyImport` -- which exists for exactly this and is what the standard library says is to `@import` what `lazyDependency` is to `dependency` -- reaches a `@compileError` instead of returning null. Making it a fetched dependency instead is not available either: the toolchain has no remote. So the duplicate goes. That build tree's firmware block linked an image the toolchain repository already knows how to link -- its own build.zig has `-Dpardes`, `-Dapp=<root>` and `-Dpardes-obj=<path>`, and its comments record having learned this same lesson from the other direction, where nesting pardes's ~30-package graph under it broke every build there. The object is the seam: it crosses by PATH and never by package, and each repository builds what it owns the pieces of. zig build -Dplatform=esp32p4 # here, no toolchain needed zig build -Dpardes # there, the console image zig build -Dpardes -Dapp=<pardes>/src/esp32p4_9p.zig # there, the 9P image For the second and third to work with no module map, `src/board9p.zig` and the 9P firmware root now reach the codec by PATH instead of through a named `ninep` module that only pardes's own build.zig knew to inject -- which is also why the root moved from `src/esp32p4/nine.zig` up to `src/esp32p4_9p.zig`, beside `src/esp32p4.zig`: a path import may not escape its module's own directory. Both files are now self-contained, and `zig test src/board9p.zig` works with no flags. `-Desp32p4-port`, `-Desp32p4-prof` and `-Desp32p4-cpu-mhz` go with the block. An option this build cannot honour is worse than no option, because it accepts the flag and then ignores it; all three are spelled the same way in the toolchain. Verified by moving ../05-zig-p4 out of the way: `zig build`, `zig build -Dplatform=esp32p4` and `zig build unit-test` all pass without it. With it back, the toolchain still links both images -- console 812,720 B, 9P 88,096 B. next-steps.txt gains the six features the 9P chain shipped.
-rw-r--r--build.zig245
-rw-r--r--build.zig.zon18
-rw-r--r--docs/9p.pdf6
-rw-r--r--docs/registry.pdf6
-rw-r--r--next-steps.txt50
5 files changed, 108 insertions, 217 deletions
diff --git a/build.zig b/build.zig
index f8dc3e82..f27e47aa 100644
--- a/build.zig
+++ b/build.zig
@@ -451,19 +451,12 @@ pub fn build(b: *std.Build) void {
// in region l2mem, overflowed by 76036 bytes`, that being the shell's shadow copy of the grid.
const esp32p4_cols = b.option(u16, "esp32p4-cols", "the board's grid width in cells (-Dplatform=esp32p4 only)") orelse 56;
const esp32p4_rows = b.option(u16, "esp32p4-rows", "the board's grid height in cells (-Dplatform=esp32p4 only)") orelse 14;
- // THE OTHER THREE BOARD OPTIONS, registered HERE and not inside the
- // `-Desp32p4-firmware` block that consumes them. Declared in there they
- // existed only once the flag was already on, so `zig build
- // -Dplatform=esp32p4 -Desp32p4-port=/dev/ttyACM0` was rejected as an
- // "invalid option" and told the reader to consult a help menu that did not
- // list it either. Worse for `-Desp32p4-cpu-mhz`: the ESP-IDF read inside
- // `esp32p4.firmware()` happens before the block reaches the `b.option`
- // call, so on a machine with no IDF checkout the build exited before the
- // option was ever declared. An option is part of this build's interface
- // whether or not this invocation reaches the code that uses it.
- const esp32p4_port = b.option([]const u8, "esp32p4-port", "serial port the board is wired to (default /dev/ttyUSB0)") orelse "/dev/ttyUSB0";
- const esp32p4_prof = b.option(bool, "esp32p4-prof", "make the firmware print per-phase cycle counts for every frame") orelse false;
- const esp32p4_cpu_mhz = b.option(u16, "esp32p4-cpu-mhz", "board HP CPU clock: 90 (bootloader default), 180 or 360") orelse 90;
+ // `-Desp32p4-port`, `-Desp32p4-prof` and `-Desp32p4-cpu-mhz` used to be
+ // declared here, for the block that linked and flashed the image. That
+ // block is gone — see "where the FLASHABLE image comes from" below — and so
+ // are they: an option this build cannot honour is worse than no option,
+ // because it accepts the flag and then ignores it. All three belong to the
+ // toolchain repository and are spelled the same way there.
// ANIMATED THEME CHANGES, off on the board because there the animation is not an animation.
//
// A theme change moves the anchored chrome palette - taglines, boxes, line numbers, scroll bars -
@@ -1051,189 +1044,37 @@ pub fn build(b: *std.Build) void {
const obj = b.addObject(.{ .name = "pardes-esp32p4", .root_module = root_mod });
b.getInstallStep().dependOn(&b.addInstallFile(obj.getEmittedBin(), "pardes-esp32p4.o").step);
- // ------------------------------------------------- the whole board, from this build tree
+ // ------------------------------------------------- and where the FLASHABLE image comes from
//
- // `-Dplatform=esp32p4` alone still emits nothing but the object above, which is what the
- // toolchain repo's own `-Dpardes -Dpardes-obj=` path links and what every measurement in
- // its `experiments/` was taken through. `-Desp32p4-firmware` adds the other half here: the
- // firmware executable rooted at src/esp32p4/app.zig, the flashable image, and the steps that
- // write it to a board and talk to it.
+ // Not from here, and this is the one deliberate asymmetry in the board build.
+ // `-Dplatform=esp32p4` emits the object above and needs no toolchain at all. Linking that
+ // object into an image needs the linker script, the app descriptor, the heap, the HAL and
+ // the flash tooling, all of which live in the `05-zig-p4` checkout — so the image is built
+ // THERE, by the repository that owns them:
//
- // OPT-IN, and not out of timidity: `esp32p4.firmware()` reads ESP-IDF's register headers at
- // CONFIGURE time and exits(1) when there is no checkout. Merely DECLARING the dependency
- // costs nothing — that package has no dependencies and its `build()` early-returns when it
- // is not the root — but calling this acquires an ESP-IDF requirement, and a bare
- // `-Dplatform=esp32p4` must not.
+ // zig build -Dpardes # the console firmware
+ // zig build -Dpardes -Dapp=<pardes>/src/esp32p4_9p.zig # the 9P server firmware
//
- // The seam stays the object. `src/esp32p4/app.zig` reaches the editor through the same eight
- // `extern` C functions whichever repository drives the build, so neither path is the
- // better-tested one: the image this produces is byte-identical to the one
- // `05-zig-p4 -Dpardes -Dapp=…/src/esp32p4/app.zig` produces.
- if (b.option(bool, "esp32p4-firmware", "also build the flashable ESP32-P4 firmware image and its board steps (needs an ESP-IDF checkout for the register headers)") orelse false) {
- const esp32p4 = @import("zig_p4");
- // Every entry point in that package takes ITS OWN builder: it resolves source paths and
- // the image step's cache inputs against its own build root, and its `build()` never
- // runs when consumed, so `dep.module`/`dep.artifact` deliberately find nothing.
- const pb = b.dependency("zig_p4", .{}).builder;
- // One spelling of the ISA, asked of the package that owns the linker script and the
- // other half of the image, so the two cannot drift. It is the same set `esp32p4_target`
- // above names, and `f` is the member that decides the float ABI.
- const fw_target = esp32p4.chipTarget(pb);
- const fw = esp32p4.firmware(pb, .{
- .target = fw_target,
- // ReleaseSmall for the PLATFORM half — a heap, a clock, a UART and a trap vector,
- // none of it hot — while `obj` keeps the ReleaseFast that was measured on the die.
- // Two objects, two modes, one link.
- .optimize = .ReleaseSmall,
- // 32768 and not that package's own 8192 default: the core recurses through layout
- // and 8 KiB is not enough for it. It is baked into the generated script's `.stack`.
- .stack_size = 32768,
- .prof = esp32p4_prof,
- .cpu_mhz = esp32p4_cpu_mhz,
- });
- const fw_exe = b.addExecutable(.{
- .name = "pardes-esp32p4-firmware",
- .root_module = b.createModule(.{
- .root_source_file = b.path("src/esp32p4/app.zig"),
- .target = fw_target,
- .optimize = .ReleaseSmall,
- .strip = true,
- .single_threaded = true,
- .unwind_tables = .none,
- .omit_frame_pointer = true,
- .error_tracing = false,
- // The four modules src/esp32p4/app.zig imports, and no more. `uart.zig` and
- // `input_rescue.zig` are sibling FILES of that root, so they need no module.
- .imports = &.{
- .{ .name = "config", .module = fw.config },
- .{ .name = "soc", .module = fw.soc },
- .{ .name = "hal", .module = fw.hal },
- .{ .name = "heap", .module = fw.heap },
- },
- }),
- });
- fw_exe.root_module.addObject(obj);
- // The linker script, `_start` as the entry, the app descriptor the bootloader reads at
- // image offset 0x20, and the register census that gates compiling against `regs` — in
- // one call. It deliberately leaves `--gc-sections` to the caller, because which
- // sections an image keeps belongs to the image.
- fw.attach(fw_exe);
- fw_exe.link_gc_sections = true;
- b.getInstallStep().dependOn(&b.addInstallArtifact(fw_exe, .{}).step);
-
- // ELF -> flashable image, and the four things one does with a board. The image's own
- // defaults come from that package (`0x10000`, 16 MB, chip revision window 1.00-1.99).
- const img = esp32p4.ImageStep.create(pb, fw_exe, .{
- .chip = .esp32p4,
- .min_rev_full = 100,
- .max_rev_full = 199,
- .flash_size = .@"16MB",
- .flash_offset = 0x10000,
- });
- b.getInstallStep().dependOn(&b.addInstallBinFile(img.getOutput(), "pardes-esp32p4.bin").step);
- const flash = esp32p4.FlashStep.create(pb, img, .{
- .port = esp32p4_port,
- // The FLASHER's rate, negotiated by the ROM loader's SYNC auto-detect. Measured
- // reliable on this board at 921600; 2000000 is not.
- .baud = .b921600,
- .verify = true,
- .opts = img.opts,
- });
- b.step("esp32p4-flash", "write pardes into the board's flash and start it (needs an ESP32-P4 on -Desp32p4-port)").dependOn(&flash.step);
-
- // INTERACT. The console is a host binary, not a step: it puts the terminal in raw mode
- // and passes the board's bytes and the user's keystrokes through untouched, which is
- // what makes the editor on the far end see a real terminal answering its capability
- // queries. Inherited stdio for the same reason. It lives in the toolchain package
- // because a serial console is useful to anything on this chip; the RUNNER is here
- // because attaching to pardes is a pardes command.
- //
- // 115200 and not the flasher's rate: this one is whatever the running firmware
- // programmed into UART0, and the bootloader leaves it at 115200.
- const tools = esp32p4.hostTools(pb);
- const console_args: []const []const u8 = &.{ "--port", esp32p4_port, "--baud", "115200" };
- const con = b.addRunArtifact(tools.console);
- con.addArgs(console_args);
- con.stdio = .inherit;
- con.step.dependOn(&tools.console_install.step);
- b.step("esp32p4-attach", "attach this terminal to the pardes already running on the board").dependOn(&con.step);
- // Ordered on purpose: two independent steps let the console reset the board out from
- // under the writer, and in practice the console wins and drives the OLD firmware.
- const interact = b.addRunArtifact(tools.console);
- interact.addArgs(console_args);
- interact.stdio = .inherit;
- interact.step.dependOn(&tools.console_install.step);
- interact.step.dependOn(&flash.step);
- b.step("esp32p4-run", "flash pardes into the board, then attach this terminal to it").dependOn(&interact.step);
-
- b.step("esp32p4-image-size", "print how much flash the firmware image uses, segment by segment (no board needed)")
- .dependOn(&esp32p4.SizeStep.create(pb, img).step);
- b.step("esp32p4-image-check", "print the firmware image's segments and say whether the bootloader would accept it (no board needed)")
- .dependOn(&esp32p4.LayoutStep.create(pb, fw_exe, img.opts).step);
- b.step("esp32p4-reset", "restart the board so the pardes already in its flash runs from the top")
- .dependOn(&esp32p4.ResetStep.create(pb, esp32p4_port).step);
-
- // `zig build esp32p4-test` — the on-die suite, its OWN image and not this firmware. It
- // deliberately links NO `pardes-esp32p4` object: it is not the editor, it is the set of
- // claims about this board that only the board can answer — byte-at-a-time
- // `std.mem.eql`, the lone-ESC decode, the transmit FIFO going full, the heap span the
- // grid is cut from — and linking 1.5 MiB of editor into it would only make those
- // claims slower to flash.
- const st_exe = b.addExecutable(.{
- .name = "pardes-esp32p4-selftest",
- .root_module = b.createModule(.{
- .root_source_file = b.path("src/esp32p4/selftest.zig"),
- .target = fw_target,
- .optimize = .ReleaseSmall,
- .strip = true,
- .single_threaded = true,
- .unwind_tables = .none,
- .omit_frame_pointer = true,
- .error_tracing = false,
- // The four package modules the suite names, and `input_rescue`, which is a
- // sibling FILE here and stays a MODULE anyway: the root says
- // `@import("input_rescue")`, and it must, because `FakePort`'s methods are
- // `pub` precisely so the policy can reach them by duck typing ACROSS a module
- // boundary. The toolchain repo compiles this same root with the same wiring,
- // so one file serves both builds and neither is the tested one.
- .imports = &.{
- .{ .name = "config", .module = fw.config },
- .{ .name = "soc", .module = fw.soc },
- .{ .name = "hal", .module = fw.hal },
- .{ .name = "heap", .module = fw.heap },
- .{ .name = "input_rescue", .module = b.createModule(.{
- .root_source_file = b.path("src/esp32p4/input_rescue.zig"),
- .target = fw_target,
- .optimize = .ReleaseSmall,
- .single_threaded = true,
- }) },
- },
- }),
- });
- // Script, `ENTRY(_start)`, descriptor, register census — and deliberately NO
- // `link_gc_sections`, unlike `fw_exe` above: the toolchain's own selftest image is
- // built without it, and switching it on would change the bytes of the image every
- // on-die measurement was taken against.
- fw.attach(st_exe);
- // Same image rules as the firmware's, by reusing `img.opts` rather than restating
- // them: one flash offset, one revision window, one fitted size.
- const st_img = esp32p4.ImageStep.create(pb, st_exe, img.opts);
- const st_flash = esp32p4.FlashStep.create(pb, st_img, .{
- .port = esp32p4_port,
- .baud = .b921600,
- .verify = true,
- .opts = img.opts,
- });
- // Ordered flash-then-run rather than two independent steps: the suite must be the
- // image that is running when the reader attaches, or the verdict belongs to whatever
- // was on the board before. An absent `MARK SELFTEST DONE` inside the 20-second window
- // is itself a failure, which is what makes a board that never got there fail loudly
- // instead of passing quietly.
- const st_run = esp32p4.SelftestStep.create(pb, esp32p4_port, 20);
- st_run.step.dependOn(&st_flash.step);
- b.step("esp32p4-test", "run pardes's on-die test suite on the board; any failed check fails the build")
- .dependOn(&st_run.step);
- }
+ // both taking `-Dpardes-obj=<pardes>/zig-out/pardes-esp32p4.o`, which is exactly the file
+ // installed above.
+ //
+ // This build tree used to do it too, under `-Desp32p4-firmware`, by declaring `.zig_p4` as
+ // a path dependency on that sibling checkout. That cost far more than the duplication was
+ // worth: `@import` in a build script is resolved when the SCRIPT is compiled and not when
+ // the branch that needs it is taken, so naming the package at all meant that anyone
+ // without a `../05-zig-p4` beside their pardes could not build pardes AT ALL — not the
+ // firmware, not the SDL shell, not the terminal one. `zig build` failed with
+ // `no module named 'zig_p4'` from a line inside an `if` that was false.
+ //
+ // Neither `.lazy = true` nor `b.lazyImport` rescues it: laziness is about FETCHING, and a
+ // path dependency whose directory is absent is generated as a package with no `build.zig`
+ // rather than as one marked unavailable, so `lazyImport` reaches a `@compileError` instead
+ // of returning null. Both were tried. The toolchain has no remote to make it a fetched
+ // dependency instead.
+ //
+ // So the arrangement is the one that repository had already arrived at from its own side,
+ // where its build.zig records the same lesson: the object is the seam, it crosses by PATH
+ // and never by package, and each repository builds what it owns the pieces of.
web_step.dependOn(&b.addFail("web needs -Dplatform=web -Dtarget=wasm32-freestanding -Ddump=<dump.zon>").step);
} else if (platform == .macos) {
// The native macOS shell is a static library plus a Swift app: Zig
@@ -1755,6 +1596,24 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/9p.zig"),
}) });
unit_step.dependOn(&b.addRunArtifact(ninep_test).step);
+ // The board's own 9P tree, and the comptime table that generates it. Its own module for the
+ // reason its neighbours have: nothing `unit-test` compiles reaches src/board9p.zig — the
+ // core does not import it, because the whole point of it is that it does NOT import the
+ // core — so its eight tests would otherwise silently not exist. No `link_libc`: it imports
+ // `std`, `src/board_pins.zig` and, in its tests only, `ninep`, which is what lets the same
+ // source serve a real client here and drive `hal.gpio` on the die.
+ //
+ // `ninep` used to be injected here as a named module over src/9p.zig.
+ // It is a plain path import now (`src/board9p.zig` says why), so this
+ // test needs no module map at all — which is the same property that
+ // lets the toolchain repository root a firmware image at
+ // src/esp32p4_9p.zig without being told what to inject.
+ const board9p_test = b.addTest(.{ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("src/board9p.zig"),
+ }) });
+ unit_step.dependOn(&b.addRunArtifact(board9p_test).step);
// The board's input-rescue policy: drain the receiver while spinning on a full transmitter.
// A measured bug — a 200-byte burst typed into a long frame lost 88 bytes on the die — so
// it gets a test that fails without the fix, and it runs HERE rather than only on hardware.
diff --git a/build.zig.zon b/build.zig.zon
index 380a5d8a..2d745db6 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -179,24 +179,6 @@
.url = "git+https://github.com/allyourcodebase/freetype#884fd2235e6ae1ec4306eda30b3259f20930ed2e",
.hash = "freetype-2.14.3-C3-WdXSKAAB-kHs4qgRoeoFnRkNQn6M_SSlUGZ6ywgBQ",
},
- // The ESP32-P4 toolchain: the SoC/HAL/register layer, the generated
- // linker script, the ELF->image builder, the ROM flasher and the
- // interactive serial console. A PATH dependency, because it is a
- // sibling checkout rather than something to pin a commit of, and
- // because the firmware build reads an ESP-IDF checkout off this
- // machine anyway.
- //
- // Safe to declare unconditionally, and that is measured rather than
- // assumed. The failure this whole seam exists to avoid runs the OTHER
- // way: nesting THIS package's ~30-package graph under that one broke
- // every build there (std/Build.zig:2091's comptime branch quota over
- // the enlarged dependency table, plus seven cached tree_sitter
- // versions whose build.zig no longer compiles on 0.16) — see
- // src/esp32p4.zig:8-15. In this direction there is nothing to enlarge:
- // zig_p4 declares no dependencies at all, so it adds exactly zero
- // packages here, and its `build()` early-returns when it is not the
- // root package. Not lazy, because `build.zig` @imports it.
- .zig_p4 = .{ .path = "../05-zig-p4" },
},
.minimum_zig_version = "0.16.0",
.paths = .{""},
diff --git a/docs/9p.pdf b/docs/9p.pdf
index 9d2dc054..961473bc 100644
--- a/docs/9p.pdf
+++ b/docs/9p.pdf
@@ -2773,12 +2773,12 @@ F��Mk�I���R�B|��&إ�n�ऋ��(�H�r�Tų��9�`�� ըa�&�ʺ���:�l
endstream
endobj
748 0 obj
-<</Creator(Typst 0.15.1)/ModDate(D:20260827161944-03'00)/CreationDate(D:20260827161944-03'00)>>
+<</Creator(Typst 0.15.1)/ModDate(D:20260827220904-03'00)/CreationDate(D:20260827220904-03'00)>>
endobj
749 0 obj
<</Length 997/Type/Metadata/Subtype/XML>>
stream
-<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmp-writer"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/" ><xmp:CreatorTool>Typst 0.15.1</xmp:CreatorTool><dc:language><rdf:Bag><rdf:li>en</rdf:li></rdf:Bag></dc:language><xmp:ModifyDate>2026-08-27T16:19:44-03:00</xmp:ModifyDate><xmp:CreateDate>2026-08-27T16:19:44-03:00</xmp:CreateDate><xmpTPg:NPages>12</xmpTPg:NPages><dc:format>application/pdf</dc:format><xmpMM:InstanceID>HljsDPgWqw9TTnj4ezsOYg==</xmpMM:InstanceID><xmpMM:DocumentID>HljsDPgWqw9TTnj4ezsOYg==</xmpMM:DocumentID><xmpMM:RenditionClass>proof</xmpMM:RenditionClass><pdf:PDFVersion>1.7</pdf:PDFVersion></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="r"?>
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmp-writer"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/" ><xmp:CreatorTool>Typst 0.15.1</xmp:CreatorTool><dc:language><rdf:Bag><rdf:li>en</rdf:li></rdf:Bag></dc:language><xmp:ModifyDate>2026-08-27T22:09:04-03:00</xmp:ModifyDate><xmp:CreateDate>2026-08-27T22:09:04-03:00</xmp:CreateDate><xmpTPg:NPages>12</xmpTPg:NPages><dc:format>application/pdf</dc:format><xmpMM:InstanceID>eoOUAEv34jOjSGsTdiY5Ag==</xmpMM:InstanceID><xmpMM:DocumentID>eoOUAEv34jOjSGsTdiY5Ag==</xmpMM:DocumentID><xmpMM:RenditionClass>proof</xmpMM:RenditionClass><pdf:PDFVersion>1.7</pdf:PDFVersion></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="r"?>
endstream
endobj
750 0 obj
@@ -3538,7 +3538,7 @@ xref
0000178455 00000 n
0000179529 00000 n
trailer
-<</Size 751/Root 750 0 R/Info 748 0 R/ID[(HljsDPgWqw9TTnj4ezsOYg==)(HljsDPgWqw9TTnj4ezsOYg==)]>>
+<</Size 751/Root 750 0 R/Info 748 0 R/ID[(eoOUAEv34jOjSGsTdiY5Ag==)(eoOUAEv34jOjSGsTdiY5Ag==)]>>
startxref
179734
%%EOF \ No newline at end of file
diff --git a/docs/registry.pdf b/docs/registry.pdf
index b714d5ad..ce7d0ef6 100644
--- a/docs/registry.pdf
+++ b/docs/registry.pdf
@@ -6478,12 +6478,12 @@ x�}��Kq�?�U�X 94%MQKS��N�#Ԧ��Q�v��B�ˡ�h�Fk ������!
endstream
endobj
1872 0 obj
-<</Creator(Typst 0.15.1)/ModDate(D:20260827181039-03'00)/CreationDate(D:20260827181039-03'00)>>
+<</Creator(Typst 0.15.1)/ModDate(D:20260827220903-03'00)/CreationDate(D:20260827220903-03'00)>>
endobj
1873 0 obj
<</Length 997/Type/Metadata/Subtype/XML>>
stream
-<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmp-writer"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/" ><xmp:CreatorTool>Typst 0.15.1</xmp:CreatorTool><dc:language><rdf:Bag><rdf:li>en</rdf:li></rdf:Bag></dc:language><xmp:ModifyDate>2026-08-27T18:10:39-03:00</xmp:ModifyDate><xmp:CreateDate>2026-08-27T18:10:39-03:00</xmp:CreateDate><xmpTPg:NPages>20</xmpTPg:NPages><dc:format>application/pdf</dc:format><xmpMM:InstanceID>ZR7RqpW3YzCeQ3Vzke06dw==</xmpMM:InstanceID><xmpMM:DocumentID>ZR7RqpW3YzCeQ3Vzke06dw==</xmpMM:DocumentID><xmpMM:RenditionClass>proof</xmpMM:RenditionClass><pdf:PDFVersion>1.7</pdf:PDFVersion></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="r"?>
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?><x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="xmp-writer"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" xmlns:pdf="http://ns.adobe.com/pdf/1.3/" ><xmp:CreatorTool>Typst 0.15.1</xmp:CreatorTool><dc:language><rdf:Bag><rdf:li>en</rdf:li></rdf:Bag></dc:language><xmp:ModifyDate>2026-08-27T22:09:03-03:00</xmp:ModifyDate><xmp:CreateDate>2026-08-27T22:09:03-03:00</xmp:CreateDate><xmpTPg:NPages>20</xmpTPg:NPages><dc:format>application/pdf</dc:format><xmpMM:InstanceID>ifdbNKHCMpMjAW51d8hH/Q==</xmpMM:InstanceID><xmpMM:DocumentID>ifdbNKHCMpMjAW51d8hH/Q==</xmpMM:DocumentID><xmpMM:RenditionClass>proof</xmpMM:RenditionClass><pdf:PDFVersion>1.7</pdf:PDFVersion></rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="r"?>
endstream
endobj
1874 0 obj
@@ -8367,7 +8367,7 @@ xref
0000402637 00000 n
0000403712 00000 n
trailer
-<</Size 1875/Root 1874 0 R/Info 1872 0 R/ID[(ZR7RqpW3YzCeQ3Vzke06dw==)(ZR7RqpW3YzCeQ3Vzke06dw==)]>>
+<</Size 1875/Root 1874 0 R/Info 1872 0 R/ID[(ifdbNKHCMpMjAW51d8hH/Q==)(ifdbNKHCMpMjAW51d8hH/Q==)]>>
startxref
403916
%%EOF \ No newline at end of file
diff --git a/next-steps.txt b/next-steps.txt
index 3546da42..60605edc 100644
--- a/next-steps.txt
+++ b/next-steps.txt
@@ -2,6 +2,56 @@ WHAT THIS IS: a wishlist, in the author's own words. The items below are left
exactly as written; this header is the only thing kept in sync with the code,
because a list that reports shipped work as pending is worse than no list.
+9P (docs/9p.typ is the note, docs/registry.typ the argument). SIX NEW THINGS,
+each verified against a running program and not assumed:
+
+1. A DETACHED SESSION CAN BE SCRIPTED. `pardes --detach=work --fs` mounts its
+ own /dev/fuse and polls it in the same poll(2) as its frontends and pane
+ shells, so it needs no wake thread — less than the desktop shells pay. The
+ one configuration whose panes outlive every terminal was the one no script
+ could reach. Proof: examples/acmefs/pardesctl panes|new|send|body|del.
+
+2. TERMINALS ARE SCRIPTABLE. Every terminal pane grows `pty/ctl`, `pty/status`
+ and `pty/data`. `winsize 80 24`, `sig INT|TERM|HUP|QUIT|KILL`, `exec`; a
+ write to `pty/data` is input to the process and a read is its RAW output,
+ escape sequences and all. A script could only ever write into a terminal
+ that already existed; now it can start one, resize one and signal one.
+
+3. THE TREE IS SERVED OVER 9P. `--fs9` puts acme's control filesystem on a unix
+ socket as base 9P2000 (src/9p.zig — codec and a sans-io server, no threads,
+ no allocator, builds freestanding). plan9port drives it: `9p -a <sock> ls /`,
+ `read /index`, `write /1/body`, `stat`, a walk through `..`. Errors come back
+ as STRINGS Linux's table knows, not numbers. `--fs` and `--fs9` serve the
+ same tree at once: a write over one reads back through the other.
+
+4. A PATHNAME WITHOUT MOUNTING ANYTHING OURSELVES. `9pfuse <sock> <dir>` and
+ ordinary tools work — ls -l with the right modes, cat, shell `>>`, permission
+ denied on the write-only files. That is the route macOS and the browser take,
+ neither of which has ever had a control filesystem.
+
+5. ONE PARDES READS ANOTHER. The `9p <dial> <path>` word walks another
+ instance's tree and opens the bytes in a pane. Before this, two instances
+ could shout one line at each other (nested.zig, write-only, no reply) or
+ REPLACE one another (`Attach` deinits the local core); they could not ask a
+ question. Proof: B read A's /1/body byte-identically to plan9port.
+
+6. THE BOARD SERVES ITS OWN TREE. A second ESP32-P4 image where UART0 carries
+ 9P and nothing else — no ANSI, no vaxis, no allocator — 88 KB against the
+ console image's 809 KB. Its GPIO tree comes from a comptime table
+ (src/board9p.zig) that is the single description: one entry fans out to a
+ directory per pin. The console firmware is untouched and still what
+ `-Dplatform=esp32p4` feeds. No board was attached, so only what builds and
+ what the host tests cover is claimed.
+
+AND ONE THING THAT WENT AWAY: pardes no longer depends on the ../05-zig-p4
+toolchain checkout to build. It used to be a path dependency named in
+build.zig.zon, and because `@import` in a build script resolves when the SCRIPT
+compiles rather than when the branch needing it is taken, anyone without that
+sibling could not build pardes at all — not the firmware, the terminal shell.
+The object is the seam: `-Dplatform=esp32p4` emits it here with no toolchain,
+and the toolchain repository links the flashable images, which it already knew
+how to do.
+
SHIPPED (verified against the tree, not assumed):
- "version tracking" -> done. `build.zig` reads `.version` from build.zig.zon
via an untyped `@import`, `gitCommit(b)` reads the commit at configure time,