diff options
| author | user <user@clank> | 2026-08-17 13:08:06 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-17 13:08:06 +0200 |
| commit | 244068e86c03b6c1bf457fac7e576bf5f68a43b7 (patch) | |
| tree | c294bf9fdd75d6fb487cc3d820ec955d0f799146 /docs | |
| parent | sl0ppty: workspaces, and layouts that write themselves (diff) | |
| download | slosh-244068e86c03b6c1bf457fac7e576bf5f68a43b7.tar.gz slosh-244068e86c03b6c1bf457fac7e576bf5f68a43b7.tar.xz slosh-244068e86c03b6c1bf457fac7e576bf5f68a43b7.zip | |
sl0ppty: a skill for driving it, and the two variables that made one possible
`.agents/skills/driving-sl0ppty/SKILL.md`. The convention is
`.agents/skills/<name>/SKILL.md` with YAML frontmatter -- already in this tree
under `vendor/libghostty-vt`, so it is not an invention -- and it means an agent
working in a checkout picks the skill up without being told, while a directory per
skill leaves room for adjuncts later. `contrib/README.md` and
`docs/scripting.md` say where it is and how to use it elsewhere: copy or symlink
the directory into wherever your agent keeps skills.
**Writing it turned up the reason there was no skill worth writing.** A pane is
told `SL0PPTY=1` and nothing else, so a program inside one knows it is in a
session and cannot find out *which* -- `ls` lists them all and none of them is
labelled "the one you are in". So the whole most valuable case, an agent driving
the session it is running in, was guesswork. The server now sets
`SL0PPTY_SESSION` and `SL0PPTY_BIN` in its own environment, which every pane
inherits through fork: one place, no signatures threaded, and unset under
`--script`, which has no socket to name. `SL0PPTY_BIN` is read from
`/proc/self/exe` rather than `argv[0]`, and exists because the failure it prevents
is the one I hit writing the examples -- a session started from a build tree, and
`sl0ppty` not on the pane's `PATH`, so the skill's own commands did not run.
What the skill actually teaches, none of which `docs/scripting.md` says because a
reference is not allowed to have opinions:
- Work belongs in a pane that was **given** a command, not typed into somebody's
shell. Then `alive` and `exit_code` off `panes` are how you wait, and the output
survives the program (D14) so it is still there to read.
- There is **no wait primitive over the socket**: `settle` is headless-only, and
`deadline` is about repaints and says nothing about program output. Poll.
- `purpose` is the handle. Titles change underneath you; a declared purpose cannot
be relabelled by the program in the pane, which is what makes it safe to address.
- `snapshot` is the whole composited session, and only what is on screen. Cut a
pane out with its `content_*` rect; do not expect scrollback.
- **The echoed command line matches your own marker.** `echo DONE` puts `DONE` on
screen twice. Count, or use a marker you did not send.
- Write `\e`, never `\033`: the unescaper takes `\0` first, so `\033` arrives as
NUL followed by `33` and nothing complains. That bug was live in this repo's own
test suite until last week.
- A program in a pane can draw its own status and real buttons with OSC 5577 and
read the click off its stdin, which is how it asks a question instead of printing
a prompt and hoping.
- Close what you made; do not `quit` a session you did not start.
`tests/test_skill.py` is the part that keeps it true. A skill is documentation an
agent acts on without a human reading it first, so a stale one is worse than a
missing one: a wrong verb is a failed tool call somebody else has to debug. Every
verb the file names is sent to a live session and must not answer `unknown cmd`;
every `panes` field its central recipe polls must exist; the environment it
promises is read out of a real pane; the recipe itself is run end to end and must
report exit status 3 with its output still on screen. It also greps the file for
paths out of a home directory, because "plug and play" is a property somebody has
to keep.
That test found `close-pane` in the draft, which is the *keybinding* action name --
the verb is `close`. Exactly the class of error the test exists for, found before
anybody's agent ran it.
Verified: 1795 checks, all green, 43 of them in `tests/test_skill.py`. Then the
skill's recipe run verbatim against a real session -- tagged pane, polled to
`alive:false`, `exit status: 3`, `compiling`/`built`/`exited: status 3` still
readable -- and from inside a pane, `session=agentdemo bin=/…/sl0ppty` and a
`building 3/7` status drawn in its own frame.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/scripting.md | 18 |
1 files changed, 18 insertions, 0 deletions
diff --git a/docs/scripting.md b/docs/scripting.md index 3dddc9b..89dd835 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -119,6 +119,24 @@ it is sent (`cat`, a shell with echo on, a REPL waiting for a line) would otherwise be answered into a loop, which is exactly what `hello` used to do: 4 MB of hellos in a second and a half. +## Driving it from an agent + +Everything above is what an agent needs, and none of it says which parts matter. +`.agents/skills/driving-sl0ppty/SKILL.md` is the same socket written as +instructions: how a program in a pane finds out which session it is in +(`SL0PPTY_SESSION`, `SL0PPTY_BIN`), why work belongs in a pane that *was given a +command* rather than typed into somebody's shell, that `alive` and `exit_code` are +how you wait rather than reading the screen for a marker your own echo matches, and +that `purpose` is the handle to find things by because titles change underneath +you. + +It follows the `.agents/skills/<name>/SKILL.md` convention, so an agent working in +a checkout picks it up without being told. To use it elsewhere, copy or symlink the +directory into wherever your agent looks for skills. `tests/test_skill.py` checks +every verb, variable and `panes` field it names against the program, because a +stale skill is worse than a missing one: an agent acts on it without a human +reading it first. + ## Headless `sl0ppty --script` is the whole program without a terminal: commands on stdin, |
