summaryrefslogtreecommitdiff
path: root/.agents
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 20:07:43 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9070942b29bd10dddcdecdb0e88ba0fb40608467 (patch)
treefa1fc5984c7847c52bf4e78d277586edce2d8325 /.agents
parent0122e94fb37422085325f2dc78ca015051ce5f91 (diff)
downloadpardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.tar.gz
pardes-9070942b29bd10dddcdecdb0e88ba0fb40608467.zip
Plan 9 idiom for the control filesystem, and the regressions a624a56 left
The 9P tree stops being a command language wearing a filesystem. /new created a pane as a side effect of a *read*; it is now Tcreate in /pane, with Tremove to close, which cloud9's engine has always supported and the editor never declared: tree.zig now says `features = .{ .create = true, .remove = true }`. Eleven pane ctl verbs become files that can be read as well as written -- dot, limit, dirty, mark, scroll, look, exec -- leaving ctl with `get`, the one verb no file would say better. Root /ctl splits into a read-only /status and the /look and /exec files whose write IS the click. stat carries real sizes where it used to answer 0, and qid versions track a pane's revision, so a client can poll for change without re-reading the body. Commit a624a56 moved raw-tty keys to an early-return branch that knew only Ctrl-B and bare Escape, and in the same edit deleted the paste branch below it. That cost Shift-Escape (the unconditional way out of tty mode) and both paste chords: Ctrl-V and Ctrl-Shift-V reached the child as keystrokes, so an agent CLI running in a pane took Ctrl-V for its image-paste binding and answered "No image found in clipboard". Both are restored, with tests. Nested detection was not subtly broken but deleted: 60367d8 removed nested.zig's process-ancestry walk and left "am I inside pardes" derived from PARDES_FORWARD_LOOK, which read "0" both for --nested and for "the listener did not come up". PARDES_PID now answers that question on its own, checked with kill(pid, 0); PARDES_9P and PARDES_PANE answer how to reach it; the flag is gone. The posted-9P registry also self-heals now -- a session that aborts cannot unlink its own socket, so posting sweeps entries whose target refuses a connection, symlinks only and on a definite ECONNREFUSED only. Elsewhere: tty scrolling is sticky-bottom, following new output only from the last row, with typing and entering raw mode snapping back to live; the boot layouts are a Boot enum instead of a chain of ifs, and the bare tty startup (Boot.tty, which main.zig names) opens an empty text pane under the shell while tests keep Boot.tty_shell; builtins announce themselves on the message row under a Verbose setting that is on by default; Config prints each setting the way you would type it back, so WindowOpacity 70 rather than "WindowOpacity: 70%"; LocationsConfig opens its window only when called bare; every tagline puts the word that closes the thing last, and a column now outlives its panes -- closing the last one leaves an empty pane, and only Delcol, newly on the column tagline, takes the column away. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to '.agents')
-rw-r--r--.agents/skills/pardes-9p/SKILL.md259
1 files changed, 159 insertions, 100 deletions
diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md
index 09dee666..2cea4d4a 100644
--- a/.agents/skills/pardes-9p/SKILL.md
+++ b/.agents/skills/pardes-9p/SKILL.md
@@ -1,127 +1,184 @@
---
name: pardes-9p
-description: Inspect and drive a running Pardes editor over 9P, or exercise its panes, builtins, terminal input and rendered output in an isolated session. Use for Pardes interaction, plugin development and end-to-end debugging through its control filesystem.
+description: Inspect and drive a running Pardes editor through its control filesystem, or exercise its panes, builtins, terminal input and rendered output in an isolated session. Use for Pardes interaction, plugin development and end-to-end debugging.
---
# Pardes over 9P
-Use the existing [Python client](../../../test/ninep.py) for ad hoc interaction
-and functional tests. Do not build another wire client. Project tooling stays
-in Zig. Run the examples from the repository root; paths below are relative to
-that root unless linked.
+Pardes is driven by reading and writing files. Prefer ordinary file tools over
+a mount; reach for the Python client only when nothing is mounted, or when the
+work needs a fid held open. Do not build another wire client: project tooling
+stays in Zig. Run the examples from the repository root; paths below are
+relative to that root unless linked.
-## Connect and identify panes
+## Find the session through the mount
-Every native session opens a Unix socket. Inside a pane, `PARDES_9P` names the
-socket and `PARDES_PANE` is that pane's serial. Outside Pardes, find
-`pardes-9p-*.sock` under `$XDG_RUNTIME_DIR`, or `~/.local/state/pardes` when
-that variable is unset. Select the intended session explicitly; do not assume
-the newest socket is the right one. Names come from `--9p=name`, the detached
-session name, or the process ID.
+This machine keeps a FUSE mount of every posted session under `/mnt/9p`:
+`/mnt/9p/pardes/<pid>/` is one session's served root. Inside a pane,
+`$PARDES_9P` is that session's socket (`/run/user/1000/pardes-9p-<pid>.sock`),
+so its pid is the directory to use, and `$PARDES_PANE` is the calling pane's
+serial:
```sh
-PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY'
-import sys
-from ninep import Client
+m=/mnt/9p/pardes/$(basename "$PARDES_9P" .sock | sed 's/^pardes-9p-//')
+cat "$m/index"
+cat "$m/pane/$PARDES_PANE/body"
+```
-with Client(sys.argv[1]) as client:
- print(client.read('/self/index').decode(), end='')
- print(client.read('/self/listeners').decode(), end='')
-PY
+Entries for dead sessions stay listed and answer `Input/output error` on any
+access, so name the session you mean rather than globbing or taking the newest.
+`cat "$m/index"` is the cheapest liveness check, and `cat "$m/status"` reports
+the `pid`, `version` and `panes` of a session new enough to serve it.
+
+`/mnt/9p/pardes` is the registry of posted sessions, mounted once for the
+machine. It is not the per-pane kernel mount the `Tty9p` builtin makes, which
+gives one pane's shell `$PARDES_MOUNT`; see [docs/v9fs.md](../../../docs/v9fs.md)
+for that. Either mountpoint serves the same tree.
+
+A running session serves whatever binary started it. If a listing does not
+match this document, that session predates the change; restart it.
+
+## The tree, and what to do with it
+
+```
+$m/README the served guide, worth reading first
+$m/index one line per pane: serial, kind (text|term|pdf|image), dirty flag, name
+$m/status pid, version, panes
+$m/look write a line = a right click on it at the active pane
+$m/exec write a line = a middle click: an editor command word, or a shell line
+$m/log one record per read: new|del|rename|save <serial> <name>; reads park
+$m/screen the rendered screen as JSON, frozen per open
+$m/listeners this session's dial addresses
+$m/pane/ mkdir opens a pane; rmdir <serial> closes it
+$m/os/ the host filesystem
+```
+
+The first field of an index row is a stable pane serial, not a slot or row
+number. A row is `serial kind dirty name`, so `row.split(maxsplit=3)` keeps a
+name with spaces intact. Re-read the index after anything that might open,
+reuse or close a pane.
+
+```sh
+cat "$m/index" # which panes exist
+mkdir "$m/pane/x"; n=$(awk 'END{print $1}' "$m/index") # open one, take its serial
+printf 'text\n' > "$m/pane/$n/body" # append
+cat "$m/pane/$n/tag" # what its tagline offers
+echo notes.txt > "$m/pane/$n/name" # rename the buffer
+echo Save > "$m/pane/$n/exec" # save it
+echo "/etc/hosts:3" > "$m/look"; cat "$m/look" # open a file, see where it landed
+rmdir "$m/pane/$n" # close it, dirty or not
```
-`Client` takes a raw Unix socket path, or `(numeric_ip, port)` for TCP; it
-does not parse Pardes dial strings or implement QUIC. It negotiates 9P2000,
-uses a five-second socket timeout, and closes on leaving `with`.
+The name `mkdir` asks for is ignored: a pane is named by the serial the editor
+gives it, and because `/index` is ordered by serial its last row is the pane
+just made. Nothing else in the tree can be created or removed, and no read
+creates anything, so `ls`, `stat` and `find` over the whole tree are inert.
+
+`look` and `exec` are the editor's two clicks, one per line of a write, at the
+active pane from the root and at that pane from `$m/pane/<n>/look` and
+`$m/pane/<n>/exec`. Reading any of them answers the serials the last command
+made, or the pane it focused or acted on. A command that fails is reported in
+the editor, not as a write error, so inspect the resulting pane, index, message
+or screen; only a malformed line fails the write itself.
+
+## Edit through addresses, dot and the flag files
+
+For a file or scratch pane, with `pane=$m/pane/<serial>`:
-The first field of each index row is a stable pane serial, not a slot or row
-number. The tag starts after five numeric fields; use `row.split(maxsplit=5)`
-to preserve spaces. Inspect `/self/pane/<serial>/tag`, `body`, or directory
-entries before choosing a target. Re-read the index after actions that might
-open, reuse or close panes.
+| Operation | Shell | Python client |
+|---|---|---|
+| Read text | `cat $pane/body` | `client.read(pane + '/body')` |
+| Append text | `echo text >> $pane/body` | `client.write(pane + '/body', b'text\n')` |
+| Replace all text | `echo text > $pane/body` | `client.write(pane + '/body', b'text\n', truncate=True)` |
+| Address a byte range | `echo '#0,#2' > $pane/addr` | `client.write(pane + '/addr', b'#0,#2')` |
+| Replace that range | `echo 'pub fn' > $pane/data` | `client.write(pane + '/data', b'pub fn')` |
+| Read the selection | `cat $pane/dot` (offsets), `cat $pane/sel` (text) | the same two reads |
+| Select the addressed range | `cp $pane/addr $pane/dot` | `client.write(pane + '/dot', client.read(pane + '/addr'))` |
+| Reload from disk | `echo get > $pane/ctl` | `client.write(pane + '/ctl', b'get\n')` |
+| Close it | `rmdir $pane` | `client.remove(pane)` |
-Wire paths start with `/self` or `/os`. `/n/self`, `/n/os`, `/n/peer` and
-`/virtual` belong to editor Look paths, not the server root. For example,
-Look `/virtual/src/pardes.zig` corresponds to reading `/self/src/pardes.zig`.
+`addr`, `dot` and `limit` each read the pair of offsets they also accept, which
+is why copying one onto another is all that acme's `addr=dot`, `dot=addr` and
+`limit=addr` ever were; a write may also be an address expression (`#0,#5`,
+`/pattern/`, `2+1`). Moving `dot` scrolls the pane to it. `limit` bounds a
+search and reads empty until set; truncate it to lift it.
-## Edit, Look and execute
+`dirty`, `mark` and `scroll` read `0` or `1` and take `0` or `1`: whether the
+buffer differs from its file, whether a write pushes an undo point, and whether
+a write scrolls. Truncating `tag` clears the part of the tag you may edit.
-For a file or scratch pane, with a connected `client` and a confirmed
-`serial`, let `pane = f'/self/pane/{serial}'`. Terminal body writes instead
-send child input; truncation does not erase terminal history.
+Address state belongs to the pane, not to a client: opening `addr` resets it,
+so two clients addressing the same pane will interfere. `$pane/ctl` reads
+acme's window status line — serial, tag length, body length, a reserved zero,
+the dirty flag, the width in cells, the font and the tab width — and takes the
+one verb `get`.
-| Operation | Client call |
-|---|---|
-| Read text | `client.read(pane + '/body')` |
-| Append text | `client.write(pane + '/body', b'text\n')` |
-| Replace all text | `client.write(pane + '/body', b'text\n', truncate=True)` |
-| Select a byte range | `client.write(pane + '/addr', b'#0,#2')` |
-| Replace that range | `client.write(pane + '/data', b'pub fn')` |
-| Show the addressed selection | `client.write(pane + '/ctl', b'dot=addr\n')` |
-| Look from this pane | `client.write(pane + '/ctl', b'look /virtual/src/pardes.zig:10\n')` |
-| Save | `client.write(pane + '/ctl', b'put\n')` |
-| Reload | `client.write(pane + '/ctl', b'get\n')` |
-| Close, refusing dirty text | `client.write(pane + '/ctl', b'del\n')` |
+Terminal panes have no file: writing their `body` sends child input, and
+truncation does not erase terminal history.
-`delete` force-closes, discarding unsaved text. `get` replaces edits with file
-contents. Use those only when that loss is intended. Reading `/self/new/ctl`
-creates a scratch pane and returns its serial as the first field; it is not
-an observational read.
+## The Python client, for what a shell cannot express
-`ctl` is not a builtin interpreter. Execute builtins through a body Exec event
-on an owned scratch pane:
+Use the existing [Python client](../../../test/ninep.py) when there is no
+mount, or when the work needs a fid held open across several operations —
+`log`, `event` and `pty/data` are consuming queues whose reads park, and shell
+redirection cannot hold one open.
-```python
-from fs import new_pane, execute
+```sh
+PYTHONPATH=test python3 -B - "$PARDES_9P" <<'PY'
+import sys
+from ninep import Client
-control = new_pane(client, b'')
-execute(client, control, 'Msg 9p-ready')
+with Client(sys.argv[1]) as client:
+ print(client.read('/index').decode(), end='')
+ print(client.read('/listeners').decode(), end='')
+PY
```
-`execute` overwrites that pane's body with the command, then writes
-`MX0 <UTF-8-byte-length>\n` to its `event` file. Keep the control pane separate
-from user text. The same helper can run `Mini path`, `Mount peer dial`, and
-`Unmount peer`; commands resolve relative to the control pane's directory.
-A successful event write acknowledges dispatch, not completion: inspect the
-resulting pane, message or screen for success.
+`Client` takes a raw Unix socket path, or `(numeric_ip, port)` for TCP; it does
+not parse Pardes dial strings or implement QUIC. It negotiates 9P2000, uses a
+five-second socket timeout, and closes on leaving `with`. Its paths are the
+served root: `/index`, `/pane/2/body`, `/os/...`. `/n/self`, `/n/os`, `/n/peer`
+and `/virtual` are editor Look paths, not server paths — Look
+`/virtual/src/pardes.zig` corresponds to reading `/src/pardes.zig`.
-Opening `addr` resets its range. To inspect the current selection, open
-`addr` first, write `addr=dot\n` to `ctl`, then use `read_fid` on that already
-open handle. Do not replace that last step with `client.read`, which reopens
-and resets it. Address state is shared by the pane, not private to a client.
+For live terminal output or plugin events, use `open` / `read_fid` / `close`,
+not the read-until-EOF helper. `pty/data` captures output while held open; it
+is not a history replay. Both files are shared, consuming queues, not
+per-client broadcasts, so a slow reader loses older data. Holding `event` open
+intercepts that pane's Look and Exec clicks, so it is not a passive logger: for
+Look/Exec records whose offsets identify the intended text, forward the short
+record `<origin><action><q0> <q1>\n`, not the whole report with flags and text.
+Expansion and chord reports need explicit handling, and terminal-body events
+can carry text only in the report, which a short writeback cannot reproduce.
+Read the event implementation before building an interceptor. Close handles in
+`finally`, and disconnect after a socket timeout. The service shares four
+connection slots and 32 screen/terminal-history snapshot handles.
+
+A file's qid version is the pane's revision for `body`, `data` and `xdata`, so
+`stat` sees an edit land without reading the text; it stays zero elsewhere.
+Stat sizes are real, and for `log`, `event` and `pty/data` report the length of
+the record a read would answer — zero when nothing is waiting.
## Terminal input and screen observations
Only terminal panes have `pty/`. Write keystroke bytes to `pty/data`, not
`body`: `client.write(pane + '/pty/data', b'printf hello\r')` submits a shell
-command. For an interactive application, send its actual input bytes;
-`b'\x03'` is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it.
-These inputs go to the child terminal, not Pardes editor key bindings.
+command. For an interactive application, send its actual input bytes; `b'\x03'`
+is Ctrl-C, and Ctrl-U is `b'\x15'` where that application supports it. These go
+to the child terminal, not to Pardes key bindings. `pty/ctl` takes `winsize C R`,
+`sig INT` and `exec`, one per line.
-`client.screen()` returns `cols`, `rows`, `cursor`, `styles`, and row-major
+`client.screen()` returns `cols`, `rows`, `cursor`, `styles` and row-major
`cells` of `[grapheme, style_index]`. Reconstruct rows using `cols`; resolve
-each cell's style through `styles` when checking highlighting. Compare colors
-and attributes, not just style-table indices or flattened text.
-
-Each screen open freezes one frame. A terminal `body` freezes history on its
-first read. `client.read` and `client.screen` reopen each time; use repeated
-calls for fresh observations. Do not poll a stale handle. Poll a specific
-condition with a deadline and a short delay, rather than a fixed long sleep.
-For large histories, measure whole-body reads separately from screen polling;
-9P write-to-observation timing includes RPC, rendering and polling overhead.
+each cell's style through `styles` when checking highlighting, and compare
+colors and attributes rather than style-table indices or flattened text.
-For live terminal output or plugin events, use `open` / `read_fid` / `close`,
-not the read-until-EOF helper. `pty/data` captures output while held open;
-it is not a history replay. Both files are shared, consuming queues, not
-per-client broadcasts; slow readers can lose older data. Holding `event` open
-intercepts Look/Exec clicks, so it is not a passive logger. For Look/Exec
-records whose offsets identify the intended file/tag text, forward the short
-record `<origin><action><q0> <q1>\n`, not the entire report with flags and text.
-Expansion/chord reports need explicit handling; terminal-body events can have
-empty ranges with text carried only in the report, so short writeback cannot
-reproduce them. Read the event implementation before building an interceptor.
-Close handles in `finally`; disconnect after a socket timeout. The service
-shares four connection slots and 32 screen/terminal-history snapshot handles.
+Each screen open freezes one frame, and a terminal `body` freezes its history
+on its first read. `client.read` and `client.screen` reopen each time, so
+repeat the call for a fresh observation rather than polling a stale handle.
+Poll a specific condition with a deadline and a short delay, not a fixed long
+sleep. For large histories, measure whole-body reads separately from screen
+polling; write-to-observation timing includes RPC, rendering and polling.
## Exercise an isolated session
@@ -140,22 +197,23 @@ binary = str(Path(sys.argv[1]).resolve())
with tempfile.TemporaryDirectory(prefix='pardes-9p-skill-') as directory:
with session(binary, Path(directory), 'skill') as (client, address):
serial = new_pane(client, b'fn main() void {}\n')
- pane = f'/self/pane/{serial}'
- client.write(pane + '/ctl', b'name probe.zig\n')
+ pane = f'/pane/{serial}'
+ client.write(pane + '/name', b'probe.zig\n')
client.write(pane + '/addr', b'#0,#2')
client.write(pane + '/data', b'pub fn')
assert client.read(pane + '/body') == b'pub fn main() void {}\n'
- control = new_pane(client, b'')
- execute(client, control, 'Msg 9p-ready')
+ execute(client, serial, 'Msg 9p-ready')
frame = client.screen()
assert '9p-ready' in ''.join(cell[0] for cell in frame['cells'])
print('9P edit, builtin and screen checks passed')
PY
```
-For terminal tests, use `session(..., tty=True)` and read
+`new_pane` is `mkdir` plus a read of the index; `execute` writes one line to a
+pane's `exec`. Both are in `test/fs.py`. For terminal tests, use
+`session(..., tty=True)` and read
[test/agent_session.py](../../../test/agent_session.py) for bounded interactive
-driving. Its readiness text and history threshold are application-specific;
+driving; its readiness text and history threshold are application-specific, and
session cleanup alone does not guarantee arbitrary grandchildren have exited.
Use [test/snapshot.zig](../../../test/snapshot.zig) when editor key/mouse input
or an independent terminal-rendering comparison matters; 9P screen inspection
@@ -163,6 +221,7 @@ alone does not test physical input routing or the host renderer.
Read [docs/fs.md](../../../docs/fs.md) for runtime mounts, TCP/QUIC listeners,
Plan9port and Linux v9fs compatibility. Unix is always available; network
-listeners are opt-in and grant full session/OS-file access. Use isolated
-loopback listeners for tests. For less common control verbs or event details,
-read their implementation and tests in [src/fs.zig](../../../src/fs.zig).
+listeners are opt-in and grant full session and OS-file access, so use isolated
+loopback listeners for tests. For event details or anything this page leaves
+open, read the implementation and its tests in
+[src/ninep/](../../../src/ninep/).