<feed xmlns='http://www.w3.org/2005/Atom'>
<title>slosh.git/tests/test_skill.py, branch master</title>
<subtitle>an opinionated terminal multiplexer in C with programmable pane chrome</subtitle>
<id>https://git.sl0p.foo/slosh.git/atom/tests/test_skill.py?h=master</id>
<link rel='self' href='https://git.sl0p.foo/slosh.git/atom/tests/test_skill.py?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/'/>
<updated>2026-08-18T19:26:00Z</updated>
<entry>
<title>slosh: rename the project from sl0ppty</title>
<updated>2026-08-18T19:26:00Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-18T19:26:00Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=b9e53cace4eb75263077df8623b011e66f5f2241'/>
<id>urn:sha1:b9e53cace4eb75263077df8623b011e66f5f2241</id>
<content type='text'>
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.

Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-&lt;uid&gt;,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -&gt; src/slosh.h, contrib/sl0ppty-dev -&gt; contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.

The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
</content>
</entry>
<entry>
<title>sl0ppty: ruff format the Python</title>
<updated>2026-08-17T12:36:59Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T12:36:59Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=c0022776d0912042819e4ea330cf7491a8196b45'/>
<id>urn:sha1:c0022776d0912042819e4ea330cf7491a8196b45</id>
<content type='text'>
The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/`
scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no
behaviour changed. Nothing here is worth reading -- the next commit adds it to
`.git-blame-ignore-revs` so `git blame` walks past it.

It is a bigger reformat than the C was, 60% of the lines against 17%, and for a
reason worth writing down rather than tuning away: ruff and black break a call that
does not fit onto one argument per line, where this code aligned the continuation
under the opening paren. The test suite is thousands of `check("some prose", cond,
detail)` calls, so nearly every one of them changed shape and the suite grew 3,308
lines. Line length was the one lever that moved it -- 120 columns would have been a
40% reformat and no growth at all -- and it was left at 88, because picking an
unusual width to dodge a one-time diff is paying forever to avoid paying once.

Import sorting came with it. Two files had `import os, pty, uuid, select, struct,
sys, termios, time, fcntl, signal` on one line and now have ten.

Verified after, not before, because a formatter that alters behaviour would be
worse than none:

- `make retest`, 1801 checks green -- and this reformats the suite that does the
  checking, including `harness.py`, which every one of those files imports.
- `make test-live`, the tty tests the fast suite skips. They matter here more than
  usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports
  were split, and nothing in the fast suite would have noticed if that broke them.
- All 60 files still compile, `make fmt` twice in a row changes nothing the second
  time, and `ruff check --select I` has nothing left to say.
</content>
</entry>
<entry>
<title>sl0ppty: a --script pane is told no session rather than somebody else's</title>
<updated>2026-08-17T11:38:15Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:38:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=ea39e09a52240ce1d5166e9137e39b7caad499fd'/>
<id>urn:sha1:ea39e09a52240ce1d5166e9137e39b7caad499fd</id>
<content type='text'>
`server.c` set `SL0PPTY_SESSION` for its panes and carried a comment saying the
value was "deliberately unset under `--script`". Nothing unset it. A value never
set is a value inherited, so a script driver started from inside a pane handed its
own panes the *outer* session's name -- and the recipe the skill documents,
`-s ${SL0PPTY_SESSION:-main} cmd`, then sent its commands to a session it was not
in. Not a fallback to a guess: a confident wrong answer, because the name it read
was a real session that really answers.

Both variables now go through one `session_env()`, called by the server with its
name and by the headless driver with NULL, because the two modes have to agree
about what a pane is told and a comment in one of them is not agreement.
`SL0PPTY_BIN` is set there too rather than inherited, so it names the binary that
actually made the pane instead of one that made some pane somewhere -- which is
the whole reason that variable exists.

Found while explaining a stale pane title, from a check in `test_skill.py` that
failed under `make test` and passed on its own. It was not flaky: the suite runs
from inside a pane, where these are set, and the tool I ran it from by hand had an
environment where they were not. A test whose answer depends on where you run it
looks exactly like a race.

So it now passes the lie in deliberately: the session under test is started with
`SL0PPTY_SESSION=somebody-elses-session`, which makes the difference between
"never set" and "cleared" visible from any shell. The mirror check is next to it --
a real session tells its panes its *own* name, even when started from inside
another one -- so the obvious wrong fix, unsetting it in both modes, fails. Both
were verified by making both wrong versions and watching each test bite.

The two title checks now wait for the line instead of sampling after 120ms. That
was not the bug (a shell prints in about 12ms here, even under disk churn) and it
is one assumption fewer.
</content>
</entry>
<entry>
<title>sl0ppty: a skill for driving it, and the two variables that made one possible</title>
<updated>2026-08-17T11:08:06Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-08-17T11:08:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/slosh.git/commit/?id=244068e86c03b6c1bf457fac7e576bf5f68a43b7'/>
<id>urn:sha1:244068e86c03b6c1bf457fac7e576bf5f68a43b7</id>
<content type='text'>
`.agents/skills/driving-sl0ppty/SKILL.md`. The convention is
`.agents/skills/&lt;name&gt;/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.
</content>
</entry>
</feed>
