From 520b98f009e97b84024fde7a77d89059491f9d5c Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Mon, 19 Oct 2026 13:07:33 +0000 Subject: [PATCH 1/4] WIP checkpoint before Eagle reboot: the handoff skill's PAUSED writes carry the computed why, and a why with a line break is refused before step 1 (#80 item 7, #85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checkpoint made on the lane 5 coordinator's word ahead of the 13:06Z Eagle reboot. This is not the finished slice. Done: - skills/handoff/SKILL.md: a new paragraph and sh block before step 1 refuse a $why carrying CR or LF (exit 2), with lane-handoff's door words. Step 4's why_text is now sanitized as lane-handoff's sanitize does (CRLF, CR and LF each one "; ", and every " — "), with an empty why written "handoff". Writes (a) and (b) carry $why_text in place of the template's placeholders, and (b) is lane-handoff's own register line. Step 4's prose says so. - tests/test_handoff_skill_80_85.py (new): runs the door block and step 4's block under bash and zsh -f against a stub helper, and compares them with lane-handoff's own sanitize and register-line template. - Checked: every skill shell block parses (bash -n via test_skill_shell_blocks.parse_problems: none), zsh -n on the door block, and py_compile on the test. Not done: - The RED run (the new test against 5e88a2c's skill) and the GREEN run never executed: both were queued on the shared pytest lock and were killed for the reboot. - No PR has been opened, and CI has not run. - Merge main once #260 lands (merge, never rebase). Co-Authored-By: Claude Opus 5.5 --- skills/handoff/SKILL.md | 52 +++- tests/test_handoff_skill_80_85.py | 448 ++++++++++++++++++++++++++++++ 2 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 tests/test_handoff_skill_80_85.py diff --git a/skills/handoff/SKILL.md b/skills/handoff/SKILL.md index 6be46d77..18bcfd5f 100644 --- a/skills/handoff/SKILL.md +++ b/skills/handoff/SKILL.md @@ -92,6 +92,25 @@ sread() { # sread "" } ``` +**THE WHY IS ONE LINE, AND IT IS REFUSED BEFORE STEP 1** (`opensoft/openRepoTools#85`; `lane-handoff` took +the same refusal in #218). `$why` below is the operator's words, verbatim: the `[why]` of `/handoff [why]`. +It reaches step 2's top block, where a line break writes a malformed prompt (a `---` line is where +`lane-start` ends the prompt it hands the next session), and step 4's object-log `PAUSED` line and register +line, two files nothing rewrites. Both of those writers refuse a newline or a carriage return before they +write, but step 4 runs after step 2 has refreshed and committed the handoff, so a refusal there would end the +act with a top block and no record behind it. So the test is made here, before step 1 renames anything and +before anything is written. It is the test `lane-handoff` makes right after reading its arguments, because +the skill and `lane-handoff` are one act (Amendment 17(a)) and refuse the same why. Step 4 still spells a +break `; `, as `lane-handoff`'s `sanitize` does, and that is the second line of defence, not this one. + +```sh +case "$why" in + *$'\n'*|*$'\r'*) + printf '%s\n' "REFUSED: the why is ONE line: it is written into the handoff's top block, the object log's PAUSED line and the register's file-level line, and a newline or carriage return in it would split each of them — the last two in files nothing rewrites. Nothing was renamed or written. Put it on one line, with '; ' where the break was, and run /handoff again." >&2 + exit 2 ;; +esac +``` + ## 1. Usage, then the identity triple — derived, not asked ```bash @@ -423,9 +442,14 @@ a question. Never move on to step 4 with an unanswered writer unless the operato ## 4. Write the swap record -First sanitize the operator's words **once**: replace every ` — ` with `; `. Amendment 7(b)/R26 refuses any -payload or free text containing ` — ` (exit 2), because that separator is what divides a line's fields from -its free text. +First sanitize the operator's words **once**, as `lane-handoff`'s `sanitize` does: replace every ` — ` with +`; `, and spell every line break `; ` as well, whether it is a CRLF, a lone CR or a lone LF, one `; ` each +(#85). Amendment 7(b)/R26 refuses any payload or free text containing ` — ` (exit 2), because that separator +is what divides a line's fields from its free text, and both one-line writers below refuse a line break before +they write. The check before step 1 has already refused a why that carries a break, so spelling it here is +the second line of defence: where one arrives anyway, the record is still written, on one line. An empty why +is written `handoff`, as `lane-handoff` writes it. Both record writes then carry that text, `$why_text` +below, and never a template placeholder (#80 item 7). ```sh # AMENDMENT 11 CLAUSE (c) — THE RECORD'S TWO NEW SUB-FIELDS, AND ITS SECOND REF. @@ -601,8 +625,22 @@ payload="$payload; agent $agent_name; transcript $transcript_id" # TEXT after the why — `clear in-process`, `clear respawn`, `kind unknown`. kind="${kind:-unknown}" # in-process | respawn | unknown payload="$payload; kind $kind" -why_text="$why" -if [[ "$kind" == unknown ]]; then why_text="$why kind unknown"; else why_text="$why $kind"; fi +# THE OPERATOR'S WORDS, SANITIZED ONCE, AS `lane-handoff`'s `sanitize` DOES +# (#85): every line break is one `; `, CRLF first so that it is one and not +# two, and so is every ` — `. A why left empty is `handoff`, `lane-handoff`'s +# own word for it. The kind's word follows, as the free text carries it. +why_text="${why:-handoff}" +why_text="${why_text//$'\r\n'/; }" +why_text="${why_text//$'\r'/; }" +why_text="${why_text//$'\n'/; }" +why_text="${why_text// — /; }" +if [[ "$kind" == unknown ]]; then why_text="$why_text kind unknown"; else why_text="$why_text $kind"; fi +# BOTH RECORD WRITES BELOW CARRY THIS TEXT (#80 item 7). They used to carry the +# template's angle-bracket placeholders instead, for the operator's words and +# for what is open, so a block run as printed wrote the placeholders into two +# files nothing rewrites. `lane-handoff` hands both writes its computed text, +# and (b) is its line word for word. What is open is for the handoff's state +# line to say, as it is in `lane-handoff`'s record. # (a) the Amendment 7 object-log PAUSED line — the record `swapped` reads. # AMENDMENT 11 CLAUSE (e) — THE SKILL SUPPLIES THE UUID, AND `LANES_SESSION` IS @@ -636,13 +674,13 @@ if [[ -z "$ws_missing" ]]; then if [[ -z "$uuid_missing" ]]; then LANES_LANE="$lane" LANES_SESSION="$uuid" "$L" log PAUSED "lane:$lane" \ → "$payload" \ - "on 's word: " + "$why_text" # (b) the LANES.md file-level line, the shape every prior PAUSED/RESUMED used. # IT IS WRITTEN WHETHER OR NOT (a) COULD BE (A8(a) step 4, `R-A11-11`): a swap # is never left unwritten, so a lane whose object line was refused still gets # this line and the row's state cell, with the gap named in the handoff — EXCEPT # where there is no uuid to name, which is the `$uuid_missing` that skips both. -"$L" append-line "PAUSED — lane $lane, session $uuid@$ws, $(date -u +%Y-%m-%dT%H:%M:%SZ), on 's word \"\"; ; handoff refreshed" +"$L" append-line "PAUSED — lane $lane, session $uuid@$ws, $(date -u +%Y-%m-%dT%H:%M:%SZ), $why_text; handoff refreshed" fi fi # (c) the row: ONE WRITE, which sets the whole state cell (Amendment 13(a)). diff --git a/tests/test_handoff_skill_80_85.py b/tests/test_handoff_skill_80_85.py new file mode 100644 index 00000000..9942b56c --- /dev/null +++ b/tests/test_handoff_skill_80_85.py @@ -0,0 +1,448 @@ +# SPDX-License-Identifier: Apache-2.0 +"""`skills/handoff/SKILL.md`'s two record writes and the why they carry, held to +opensoft/openRepoTools#80 item 7 and #85's skill half. + +#80 ITEM 7 — THE RECORD WRITES CARRIED THE TEMPLATE, NOT THE VALUE. Step 4 +computes `why_text` (the operator's why, then the kind's word) and then wrote +neither line with it. The object log's PAUSED free text was the literal +`on 's word: `, and the register's file-level +line carried `` and ``, +so a person who ran the block as printed recorded the placeholders. +`lane-handoff`, the same act from a shell (Amendment 17(a)), passes its +computed text to both writes. The two are now held to write the same lines for +the same why. + +#85, THE SKILL'S HALF — A LINE BREAK IN THE WHY. #218 made `append-line` and +`lane-handoff` refuse a why carrying a newline or a carriage return before +anything is written, and made `lane-handoff`'s `sanitize` spell every break +`; ` as the second line of defence. The skill did neither. Its why reached step +2's top block and step 4's two one-line writes as typed, and step 4 did not +replace ` — ` either, though its own prose says it does. The skill now refuses +such a why before step 1, as `lane-handoff` does right after reading its +arguments, and step 4 sanitizes the why as `lane-handoff`'s `sanitize` does. + +THE BLOCKS ARE RUN, NOT READ. They are found with +`tests/test_skill_shell_blocks.py`'s own fence reader (imported, not copied) +and run under `bash` and under `zsh -f` where one is installed, because a +session's Bash tool runs a skill's block under the user's own shell. +`lane-handoff`'s side of every comparison is ITS OWN code: its `sanitize` +function, its kind-word lines and its register-line template, cut out of the +file. So the two cannot drift apart with this file still green. Every case +marked RED fails against the skill at 5e88a2c and passes with the fix. + +NOTHING HERE READS THE CALLER'S ENVIRONMENT. A lane window exports +`WORKBENCHES_CLAUDE_*`, `TMUX`, `LANES_AGENT` and the other `LANES_*` seams, +which are exactly what these blocks read, so the environment is an explicit +dict and never `os.environ` plus changes. `lanes-edit.sh` is a stub on a +private `PATH` that records every call's arguments, `tmux` answers nothing and +`date` answers one fixed instant, so nothing reaches a register, a tmux server +or the network. +""" + +from __future__ import annotations + +import os +import re +import shutil +import stat +import subprocess +from pathlib import Path +from typing import Dict, List, NamedTuple, Optional + +import pytest + +from conftest import REPO, WINDOWS_SKIP +from test_skill_shell_blocks import SHELL_TAGS, Block, fenced_blocks + +pytestmark = [WINDOWS_SKIP, pytest.mark.skipif( + shutil.which("bash") is None, reason="the skill's blocks are shell")] + +SKILL = REPO / "skills/handoff/SKILL.md" +LANE_HANDOFF = REPO / "lane-handoff" + +SHELLS = [ + pytest.param("bash", id="bash"), + pytest.param("zsh", id="zsh", marks=pytest.mark.skipif( + shutil.which("zsh") is None, reason="no zsh here")), +] + +LANE = "repoHS-1" +WS = "Eagle" +UUID = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" +#: What the stub `date` answers, so the register line can be compared whole. +NOW = "2026-10-19T01:02:03Z" + +#: The three verbs step 4 writes with. Nothing else it calls writes. +WRITES = ("log", "append-line", "set-row-state") + +#: A template placeholder: `<` and `>` around words, as the template spells +#: ``, `` and ``. +PLACEHOLDER = re.compile(r"<[^<>\n]+>") + +#: The markers that find the blocks, each carried by exactly one shell block. +STEP4 = "# (c) the row: ONE WRITE" +DOOR = 'case "$why" in' + + +# --- the blocks, as the files hold them -------------------------------------- + +def _skill_text() -> str: + return SKILL.read_text(encoding="utf-8") + + +def _shell_blocks() -> List[Block]: + return [b for b in fenced_blocks(_skill_text()) if b.tag in SHELL_TAGS] + + +def _one_block(marker: str) -> Block: + found = [b for b in _shell_blocks() if marker in b.body] + assert len(found) == 1, ( + f"expected exactly one shell block of {SKILL.name} carrying " + f"{marker!r}, found {len(found)}") + return found[0] + + +def _lane_handoff_lines() -> List[str]: + return LANE_HANDOFF.read_text(encoding="utf-8").split("\n") + + +def _the_line(lines: List[str], start: str) -> str: + found = [line for line in lines if line.startswith(start)] + assert len(found) == 1, ( + f"lane-handoff has {len(found)} top-level lines starting {start!r}: " + "this test is stale") + return found[0] + + +def lane_handoff_why_text() -> str: + """`lane-handoff`'s own `sanitize` and the three lines that build its + `why_text` from `$why` and `$handoff_kind`, cut out of the file.""" + lines = _lane_handoff_lines() + starts = [i for i, line in enumerate(lines) if line == "sanitize() {"] + assert len(starts) == 1, "lane-handoff's `sanitize() {` is gone: stale" + end = next(j for j in range(starts[0] + 1, len(lines)) if lines[j] == "}") + return "\n".join(lines[starts[0]:end + 1] + [ + _the_line(lines, 'kind_word="$handoff_kind"'), + _the_line(lines, '[ "$handoff_kind" = unknown ] && kind_word='), + _the_line(lines, 'why_text="$(sanitize '), + ]) + "\n" + + +def lane_handoff_register_line() -> str: + """The double-quoted word `lane-handoff` hands `append-line` for the + register's file-level PAUSED line, cut out of the file.""" + found = re.findall(r'append-line ("PAUSED — lane [^"\n]*")', + LANE_HANDOFF.read_text(encoding="utf-8")) + assert len(found) == 1, ( + f"lane-handoff has {len(found)} register PAUSED lines: stale") + return found[0] + + +# --- a sandbox with a stub helper and no inherited environment --------------- + +#: Each call is one file holding the `LANES_SESSION` it was made under, then +#: every argument, NUL-separated, so an argument carrying a line break is +#: recorded as the one argument it is. `lane-dir` answers 8, no answer, and +#: every other verb succeeds. +STUB = """#!/usr/bin/env bash +n=$(ls "$STUB_CALLS" | wc -l) +n=$((n + 1)) +printf '%s\\0' "${LANES_SESSION-}" "$@" > "$STUB_CALLS/$(printf '%04d' "$n")" +case "$1" in + lane-dir) exit 8 ;; +esac +exit 0 +""" + + +class Call(NamedTuple): + session: str + argv: List[str] + + +class Sandbox: + def __init__(self, root: Path) -> None: + self.root = root + self.bin = root / "bin" + self.bin.mkdir() + self.calls_dir = root / "calls" + self.calls_dir.mkdir() + (root / "home").mkdir() + (root / "tmp").mkdir() + self.helper = self.bin / "lanes-edit.sh" + for name, body in (("lanes-edit.sh", STUB), + ("tmux", "#!/bin/sh\nexit 1\n"), + ("date", f"#!/bin/sh\nprintf '%s\\n' '{NOW}'\n")): + tool = self.bin / name + tool.write_text(body, encoding="utf-8") + tool.chmod(tool.stat().st_mode | stat.S_IXUSR) + dirs: List[str] = [str(self.bin)] + for tool in ("bash", "zsh"): + found = shutil.which(tool) + if found and os.path.dirname(found) not in dirs: + dirs.append(os.path.dirname(found)) + for fallback in ("/usr/bin", "/bin"): + if fallback not in dirs: + dirs.append(fallback) + self.env: Dict[str, str] = { + "HOME": str(root / "home"), "PATH": os.pathsep.join(dirs), + "TMPDIR": str(root / "tmp"), "LC_ALL": "C", "TZ": "UTC", + "STUB_CALLS": str(self.calls_dir), + } + + def calls(self) -> List[Call]: + out = [] + for path in sorted(self.calls_dir.iterdir()): + fields = path.read_bytes().decode("utf-8").split("\0")[:-1] + out.append(Call(fields[0], fields[1:])) + return out + + def writes(self, verb: str) -> List[Call]: + return [c for c in self.calls() if c.argv and c.argv[0] == verb] + + def run(self, shell: str, script: str, + extra: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess: + env = dict(self.env) + env.update(extra or {}) + argv = [shutil.which(shell) or shell] + if shell == "zsh": + argv.append("-f") + return subprocess.run([*argv, "-c", script], cwd=str(self.root), + env=env, capture_output=True, text=True, + stdin=subprocess.DEVNULL, check=False, + timeout=60) + + +@pytest.fixture +def box(tmp_path: Path) -> Sandbox: + return Sandbox(tmp_path) + + +def _why_env(why: Optional[str], kind: Optional[str] = None) -> Dict[str, str]: + env: Dict[str, str] = {} + if why is not None: + env["STUB_WHY"] = why + if kind is not None: + env["STUB_KIND"] = kind + return env + + +#: How a session holds the operator's words when it reaches a block: `$why`, +#: set where the operator gave one and unset where they did not. +WHY_PRELUDE = ('if [ "${STUB_WHY+set}" = set ]; then why="$STUB_WHY"; ' + 'else unset why; fi\n') + + +def _step4(box: Sandbox, shell: str, why: Optional[str], + kind: Optional[str] = None) -> subprocess.CompletedProcess: + """Step 4's whole block, with what steps 1 and 2 would have bound.""" + script = (f'L="{box.helper}"\nlane={LANE}\nws={WS}\nuuid={UUID}\n' + 'ws_missing=""\nuuid_missing=""\n' + WHY_PRELUDE + + '[ -z "${STUB_KIND-}" ] || kind="$STUB_KIND"\n' + + _one_block(STEP4).body) + proc = box.run(shell, script, _why_env(why, kind)) + assert proc.returncode == 0, (proc.returncode, proc.stderr) + return proc + + +def _lane_handoff_writes(box: Sandbox, why: str, + kind: Optional[str]) -> List[str]: + """What `lane-handoff`'s own code hands the two writes for this why: the + log's free text and the register line, with the same lane, session, + workstation and instant the skill is run with.""" + script = ('why="$STUB_WHY"; handoff_kind="$STUB_KIND"\n' + + lane_handoff_why_text() + + f'lane={LANE}; uuid={UUID}; ws={WS}; UTC={NOW}\n' + f'printf \'%s\\0%s\\0\' "$why_text" {lane_handoff_register_line()}\n') + proc = box.run("bash", script, _why_env(why, kind or "unknown")) + assert proc.returncode == 0, (proc.returncode, proc.stderr) + return proc.stdout.split("\0")[:-1] + + +def _the_one(box: Sandbox, verb: str) -> Call: + found = box.writes(verb) + assert len(found) == 1, (verb, box.calls()) + return found[0] + + +# --- #80 item 7: the two writes carry the computed why ----------------------- + +@pytest.mark.parametrize("shell", SHELLS) +def test_both_record_writes_carry_the_why_the_block_computed( + box: Sandbox, shell: str) -> None: + """RED at 5e88a2c. The object log's PAUSED line takes the why, then the + kind's word, as its free text, and the register's file-level line is the + shape `lane-handoff` writes: lane, session@workstation, the instant, that + same text, then `handoff refreshed`.""" + _step4(box, shell, "a usage reset") + log = _the_one(box, "log") + assert log.argv[:4] == ["log", "PAUSED", f"lane:{LANE}", "→"], log.argv + assert log.session == UUID, log + assert log.argv[-1] == "a usage reset kind unknown", ( + "the object log's PAUSED free text is not the why the block computed " + f"(#80 item 7): {log.argv[-1]!r}") + line = _the_one(box, "append-line") + assert line.argv[1:] == [ + f"PAUSED — lane {LANE}, session {UUID}@{WS}, {NOW}, " + "a usage reset kind unknown; handoff refreshed"], ( + f"the register's PAUSED line is not the computed one (#80 item 7): " + f"{line.argv[1:]!r}") + + +@pytest.mark.parametrize("shell", SHELLS) +def test_no_write_records_a_template_placeholder(box: Sandbox, + shell: str) -> None: + """RED at 5e88a2c. Run as printed, no argument of any write is a + ``: what the template showed a reader is never what the + block records.""" + _step4(box, shell, "a usage reset") + left = [(c.argv[0], arg) for c in box.calls() if c.argv[0] in WRITES + for arg in c.argv if PLACEHOLDER.search(arg)] + assert not left, f"a write recorded the template's placeholder: {left}" + assert [c.argv[0] for c in box.calls() if c.argv[0] in WRITES] == \ + list(WRITES), box.calls() + + +def test_no_shell_block_carries_the_placeholders_80_item_7_names() -> None: + """RED at 5e88a2c. The three placeholders the finding quotes are gone from + every shell block of the skill, so no later copy brings one back beside + the fixed writes.""" + left = [line.strip() for block in _shell_blocks() + for line in block.body.split("\n") + if "" in line or "" in line + or " None: + """RED at 5e88a2c. For the same why and the same kind, the skill hands + `log PAUSED` and `append-line` exactly what `lane-handoff`'s own code + hands them: `sanitize` (` — ` and every line break spelled `; `), an empty + why written `handoff`, then the kind's word (`kind unknown`, `respawn`, + `in-process`), and its register-line template around that.""" + want = WHY_CASES[case] + free_text, register_line = _lane_handoff_writes(box, want.why, want.kind) + _step4(box, shell, want.why, want.kind) + assert _the_one(box, "log").argv[-1] == free_text, ( + f"{case}: the skill's PAUSED free text differs from lane-handoff's") + assert _the_one(box, "append-line").argv[1:] == [register_line], ( + f"{case}: the skill's register line differs from lane-handoff's") + + +@pytest.mark.parametrize("shell", SHELLS) +def test_a_line_break_reaches_neither_one_line_write(box: Sandbox, + shell: str) -> None: + """RED at 5e88a2c (#85). A break that reaches step 4 anyway, whatever its + spelling, is one `; `, as #218 spelled it for `lane-handoff`: the record + is still written, on one line, and no write is handed a newline or a + carriage return.""" + _step4(box, shell, "a\r\nb\rc\nd — e") + assert _the_one(box, "log").argv[-1] == "a; b; c; d; e kind unknown" + broken = [(c.argv[0], arg) for c in box.calls() if c.argv[0] in WRITES + for arg in c.argv if "\n" in arg or "\r" in arg] + assert not broken, f"a write was handed a line break (#85): {broken}" + + +# --- #85: the door ------------------------------------------------------------ + +def _door(box: Sandbox, shell: str, + why: Optional[str]) -> subprocess.CompletedProcess: + script = WHY_PRELUDE + _one_block(DOOR).body + "printf 'AFTER\\n'\n" + return box.run(shell, script, _why_env(why)) + + +REFUSED_WHYS = { + "newline": "clear\nsecond line", + "carriage-return": "clear\rsecond line", + "crlf": "clear\r\nsecond line", + "trailing-newline": "usage reset\n", +} + + +@pytest.mark.parametrize("shell", SHELLS) +@pytest.mark.parametrize("case", sorted(REFUSED_WHYS)) +def test_a_why_carrying_a_line_break_is_refused_at_the_door( + box: Sandbox, shell: str, case: str) -> None: + """RED at 5e88a2c, where there was no door. A why carrying a newline or a + carriage return stops the skill where it stands (nothing after the block + runs) with exit 2, and says what `lane-handoff` says at its own door: the + why is ONE line, nothing was written, put `; ` where the break was.""" + proc = _door(box, shell, REFUSED_WHYS[case]) + assert "AFTER" not in proc.stdout, ( + f"{case}: the skill went on with a why carrying a line break (#85)") + assert proc.returncode == 2, (proc.returncode, proc.stderr) + for words in ("the why is ONE line", "Nothing was renamed or written", + "with '; ' where the break was"): + assert words in proc.stderr, (words, proc.stderr) + assert "the why is ONE line" in LANE_HANDOFF.read_text(encoding="utf-8"), ( + "lane-handoff's own door no longer says the why is ONE line: the two " + "refusals were written to say the same thing") + assert box.calls() == [] + + +PASSED_WHYS = { + "plain": "a usage reset", + "em-dash": "swap — usage reset", + "empty": "", + "unset": None, +} + + +@pytest.mark.parametrize("shell", SHELLS) +@pytest.mark.parametrize("case", sorted(PASSED_WHYS)) +def test_a_one_line_why_passes_the_door_untouched(box: Sandbox, shell: str, + case: str) -> None: + """What the door leaves alone: a one-line why, a ` — ` (step 4's sanitize + spells it), an empty why and none at all go through with nothing said.""" + proc = _door(box, shell, PASSED_WHYS[case]) + assert (proc.returncode, proc.stdout, proc.stderr) == (0, "AFTER\n", ""), ( + case, proc.returncode, proc.stdout, proc.stderr) + + +def test_the_door_stands_before_anything_is_renamed_or_written() -> None: + """RED at 5e88a2c. The door is asked before step 1, which renames the + window, and before every block that publishes, commits or writes a record, + so its refusal leaves nothing to undo, as `lane-handoff`'s does.""" + door = _one_block(DOOR) + lines = _skill_text().split("\n") + step1 = [n for n, line in enumerate(lines, 1) if line.startswith("## 1. ")] + assert len(step1) == 1, "the skill's `## 1.` heading is gone: stale" + assert door.opens < step1[0], ( + f"the door opens at line {door.opens}, after step 1 (line {step1[0]})") + acts = ('"$L" log', "append-line", "set-row-state", "publish-handoff", + "commit", "rename-window", "send-keys", "lane-handoff") + later = [b.opens for b in _shell_blocks() + if b.opens != door.opens and any(act in b.body for act in acts)] + assert later and all(opens > door.opens for opens in later), ( + door.opens, later) + + +def test_step_4s_prose_says_a_line_break_is_spelled_too() -> None: + """RED at 5e88a2c. The sentence that tells a reader what "sanitize" means + names the line break beside the ` — `, as the code under it now does.""" + text = _skill_text() + start = text.index("First sanitize the operator's words") + paragraph = text[start:text.index("```sh", start)] + assert "line break" in paragraph, paragraph + assert "lane-handoff" in paragraph, paragraph From c02e267062a3a4258504da41566a398429ac3fb2 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Mon, 19 Oct 2026 14:01:35 +0000 Subject: [PATCH 2/4] The handoff skill's two PAUSED writes carry the why it computed, sanitized as lane-handoff's sanitize does, and a why carrying a line break is refused before step 1 (#80 item 7, #85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit and the WIP checkpoint before it (520b98f) are one slice. Main b707bd7 is merged in, so #260's sread 15(d) arm, its canon-lane read after step 1's rungs and its whole-string window checks sit in front of step 4's writes, unchanged. #80 item 7: step 4 of skills/handoff/SKILL.md computed why_text and then wrote neither record with it. The object log's PAUSED free text was the literal "on 's word: ", and the register's file-level line carried that and "", so running the block as printed recorded the placeholders. Both writes now carry $why_text, and the register line is lane-handoff's own line word for word, which is what "the same act writes the same record" (Amendment 17(a)) asks. That drops the "" segment, which lane-handoff's line never carried. #85, the skill's half: #218 made append-line and lane-handoff refuse a why carrying CR or LF before anything is written, and made lane-handoff's sanitize spell every break "; ". The skill did neither. Its why_text was "$why" as typed, so it did not even replace " — ", though its own prose says it does. A why with a break would reach step 2's top block, and then both step 4 writers would refuse it after step 2 had already committed the handoff. The skill now refuses such a why before step 1, with the words lane-handoff uses at its own door. Step 4 then sanitizes the why as lane-handoff's sanitize does (CRLF, CR and LF each one "; ", and every " — "), with an empty why written "handoff". That is the second line of defence. tests/test_handoff_skill_80_85.py runs the door block and step 4's block under bash and zsh -f against a stub helper. It compares the skill's two writes with what lane-handoff's own sanitize, kind-word lines and register-line template produce, all cut out of lane-handoff. This commit makes the door-position case read only the lines that run. After the merge, #260's comments in the preamble's fence cite lane-handoff by name, and a comment that names an act is not that act. Co-Authored-By: Claude Opus 5.5 --- tests/test_handoff_skill_80_85.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_handoff_skill_80_85.py b/tests/test_handoff_skill_80_85.py index 9942b56c..8b36c061 100644 --- a/tests/test_handoff_skill_80_85.py +++ b/tests/test_handoff_skill_80_85.py @@ -432,8 +432,15 @@ def test_the_door_stands_before_anything_is_renamed_or_written() -> None: f"the door opens at line {door.opens}, after step 1 (line {step1[0]})") acts = ('"$L" log', "append-line", "set-row-state", "publish-handoff", "commit", "rename-window", "send-keys", "lane-handoff") - later = [b.opens for b in _shell_blocks() - if b.opens != door.opens and any(act in b.body for act in acts)] + + def code(block: Block) -> List[str]: + """The lines that run: a comment that NAMES an act (the preamble's + fence cites `lane-handoff`'s own) is not that act.""" + return [line for line in block.body.split("\n") + if not line.lstrip().startswith("#")] + + later = [b.opens for b in _shell_blocks() if b.opens != door.opens + and any(act in line for line in code(b) for act in acts)] assert later and all(opens > door.opens for opens in later), ( door.opens, later) From 9b80eed7af9ad683f24c6e356170d4fd7b2c5f07 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Mon, 19 Oct 2026 14:10:31 +0000 Subject: [PATCH 3/4] The #80/#85 skill test runs in test_handoff_skill_72_80.py's sandbox instead of a copy of it SonarCloud's quality gate failed on #264 with 5.9% duplication on new code (3% allowed). Its two duplicated blocks were the sandbox's PATH and environment setup and its run method, both copied from tests/test_handoff_skill_72_80.py's Sandbox. The test now subclasses that Sandbox and imports its SHELLS, the way that file imports test_skill_shell_blocks.py's fence reader. It adds only the recording stub, tmux and date. The reader of the recorded calls is renamed recorded(), because the parent keeps a `calls` attribute that would shadow a method of that name. No case changes. Co-Authored-By: Claude Opus 5.5 --- tests/test_handoff_skill_80_85.py | 74 ++++++++++--------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/tests/test_handoff_skill_80_85.py b/tests/test_handoff_skill_80_85.py index 8b36c061..a5073356 100644 --- a/tests/test_handoff_skill_80_85.py +++ b/tests/test_handoff_skill_80_85.py @@ -22,9 +22,10 @@ arguments, and step 4 sanitizes the why as `lane-handoff`'s `sanitize` does. THE BLOCKS ARE RUN, NOT READ. They are found with -`tests/test_skill_shell_blocks.py`'s own fence reader (imported, not copied) -and run under `bash` and under `zsh -f` where one is installed, because a -session's Bash tool runs a skill's block under the user's own shell. +`tests/test_skill_shell_blocks.py`'s own fence reader and run in +`tests/test_handoff_skill_72_80.py`'s sandbox (both imported, not copied), +under `bash` and under `zsh -f` where one is installed, because a session's +Bash tool runs a skill's block under the user's own shell. `lane-handoff`'s side of every comparison is ITS OWN code: its `sanitize` function, its kind-word lines and its register-line template, cut out of the file. So the two cannot drift apart with this file still green. Every case @@ -41,7 +42,6 @@ from __future__ import annotations -import os import re import shutil import stat @@ -52,6 +52,8 @@ import pytest from conftest import REPO, WINDOWS_SKIP +from test_handoff_skill_72_80 import SHELLS +from test_handoff_skill_72_80 import Sandbox as SkillSandbox from test_skill_shell_blocks import SHELL_TAGS, Block, fenced_blocks pytestmark = [WINDOWS_SKIP, pytest.mark.skipif( @@ -60,12 +62,6 @@ SKILL = REPO / "skills/handoff/SKILL.md" LANE_HANDOFF = REPO / "lane-handoff" -SHELLS = [ - pytest.param("bash", id="bash"), - pytest.param("zsh", id="zsh", marks=pytest.mark.skipif( - shutil.which("zsh") is None, reason="no zsh here")), -] - LANE = "repoHS-1" WS = "Eagle" UUID = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" @@ -160,37 +156,25 @@ class Call(NamedTuple): argv: List[str] -class Sandbox: +class Sandbox(SkillSandbox): + """`tests/test_handoff_skill_72_80.py`'s sandbox: its private `PATH`, its + explicit environment and its `run` under `bash` or `zsh -f`. On top of it + go a helper stub that records every argument, a `tmux` that answers + nothing and a `date` that answers `NOW`.""" + def __init__(self, root: Path) -> None: - self.root = root - self.bin = root / "bin" - self.bin.mkdir() + super().__init__(root) self.calls_dir = root / "calls" self.calls_dir.mkdir() - (root / "home").mkdir() - (root / "tmp").mkdir() - self.helper = self.bin / "lanes-edit.sh" for name, body in (("lanes-edit.sh", STUB), ("tmux", "#!/bin/sh\nexit 1\n"), ("date", f"#!/bin/sh\nprintf '%s\\n' '{NOW}'\n")): tool = self.bin / name tool.write_text(body, encoding="utf-8") tool.chmod(tool.stat().st_mode | stat.S_IXUSR) - dirs: List[str] = [str(self.bin)] - for tool in ("bash", "zsh"): - found = shutil.which(tool) - if found and os.path.dirname(found) not in dirs: - dirs.append(os.path.dirname(found)) - for fallback in ("/usr/bin", "/bin"): - if fallback not in dirs: - dirs.append(fallback) - self.env: Dict[str, str] = { - "HOME": str(root / "home"), "PATH": os.pathsep.join(dirs), - "TMPDIR": str(root / "tmp"), "LC_ALL": "C", "TZ": "UTC", - "STUB_CALLS": str(self.calls_dir), - } - - def calls(self) -> List[Call]: + self.env["STUB_CALLS"] = str(self.calls_dir) + + def recorded(self) -> List[Call]: out = [] for path in sorted(self.calls_dir.iterdir()): fields = path.read_bytes().decode("utf-8").split("\0")[:-1] @@ -198,19 +182,7 @@ def calls(self) -> List[Call]: return out def writes(self, verb: str) -> List[Call]: - return [c for c in self.calls() if c.argv and c.argv[0] == verb] - - def run(self, shell: str, script: str, - extra: Optional[Dict[str, str]] = None) -> subprocess.CompletedProcess: - env = dict(self.env) - env.update(extra or {}) - argv = [shutil.which(shell) or shell] - if shell == "zsh": - argv.append("-f") - return subprocess.run([*argv, "-c", script], cwd=str(self.root), - env=env, capture_output=True, text=True, - stdin=subprocess.DEVNULL, check=False, - timeout=60) + return [c for c in self.recorded() if c.argv and c.argv[0] == verb] @pytest.fixture @@ -261,7 +233,7 @@ def _lane_handoff_writes(box: Sandbox, why: str, def _the_one(box: Sandbox, verb: str) -> Call: found = box.writes(verb) - assert len(found) == 1, (verb, box.calls()) + assert len(found) == 1, (verb, box.recorded()) return found[0] @@ -296,11 +268,11 @@ def test_no_write_records_a_template_placeholder(box: Sandbox, ``: what the template showed a reader is never what the block records.""" _step4(box, shell, "a usage reset") - left = [(c.argv[0], arg) for c in box.calls() if c.argv[0] in WRITES + left = [(c.argv[0], arg) for c in box.recorded() if c.argv[0] in WRITES for arg in c.argv if PLACEHOLDER.search(arg)] assert not left, f"a write recorded the template's placeholder: {left}" - assert [c.argv[0] for c in box.calls() if c.argv[0] in WRITES] == \ - list(WRITES), box.calls() + assert [c.argv[0] for c in box.recorded() if c.argv[0] in WRITES] == \ + list(WRITES), box.recorded() def test_no_shell_block_carries_the_placeholders_80_item_7_names() -> None: @@ -359,7 +331,7 @@ def test_a_line_break_reaches_neither_one_line_write(box: Sandbox, carriage return.""" _step4(box, shell, "a\r\nb\rc\nd — e") assert _the_one(box, "log").argv[-1] == "a; b; c; d; e kind unknown" - broken = [(c.argv[0], arg) for c in box.calls() if c.argv[0] in WRITES + broken = [(c.argv[0], arg) for c in box.recorded() if c.argv[0] in WRITES for arg in c.argv if "\n" in arg or "\r" in arg] assert not broken, f"a write was handed a line break (#85): {broken}" @@ -398,7 +370,7 @@ def test_a_why_carrying_a_line_break_is_refused_at_the_door( assert "the why is ONE line" in LANE_HANDOFF.read_text(encoding="utf-8"), ( "lane-handoff's own door no longer says the why is ONE line: the two " "refusals were written to say the same thing") - assert box.calls() == [] + assert box.recorded() == [] PASSED_WHYS = { From a7c87c579f55ecc687685684f72dbcb2ebab6610 Mon Sep 17 00:00:00 2001 From: Brett Heap Date: Mon, 19 Oct 2026 15:29:32 +0000 Subject: [PATCH 4/4] The handoff skill's why check reads ${why-}, so an unset why passes under set -u, and the #80/#85 test names b707bd7 as its red base A fix round on #264, from its independent review. main 55aae16 (#246) is merged in before this commit. The check before step 1 was `case "$why" in`. In a shell running under `set -u`, with no why given, that stopped the shell there: bash said "why: unbound variable" and zsh said "parameter not set". It now reads "${why-}". An unset why is no why at all: it passes, and step 4 writes it "handoff", as lane-handoff does. The paragraph above the block says so. tests/test_handoff_skill_80_85.py gains one case, under bash and zsh -f. It runs the check and then step 4 in one `set -u` shell with why unset, and expects no error and "handoff kind unknown" in both writes. The block's marker is now its refusal text, so the case finds the old block and fails on the real error. The docstrings now name b707bd7, where the red run was measured, instead of 5e88a2c. Co-Authored-By: Claude Opus 5.5 --- skills/handoff/SKILL.md | 6 +++-- tests/test_handoff_skill_80_85.py | 43 ++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/skills/handoff/SKILL.md b/skills/handoff/SKILL.md index 0a85ddd4..aba1d6d9 100644 --- a/skills/handoff/SKILL.md +++ b/skills/handoff/SKILL.md @@ -126,10 +126,12 @@ write, but step 4 runs after step 2 has refreshed and committed the handoff, so act with a top block and no record behind it. So the test is made here, before step 1 renames anything and before anything is written. It is the test `lane-handoff` makes right after reading its arguments, because the skill and `lane-handoff` are one act (Amendment 17(a)) and refuse the same why. Step 4 still spells a -break `; `, as `lane-handoff`'s `sanitize` does, and that is the second line of defence, not this one. +break `; `, as `lane-handoff`'s `sanitize` does, and that is the second line of defence, not this one. A +why the operator did not give is no why at all: it passes, and step 4 writes `handoff`. That holds in a shell +running under `set -u` too, which is why the check reads `${why-}`. ```sh -case "$why" in +case "${why-}" in *$'\n'*|*$'\r'*) printf '%s\n' "REFUSED: the why is ONE line: it is written into the handoff's top block, the object log's PAUSED line and the register's file-level line, and a newline or carriage return in it would split each of them — the last two in files nothing rewrites. Nothing was renamed or written. Put it on one line, with '; ' where the break was, and run /handoff again." >&2 exit 2 ;; diff --git a/tests/test_handoff_skill_80_85.py b/tests/test_handoff_skill_80_85.py index a5073356..abf66de5 100644 --- a/tests/test_handoff_skill_80_85.py +++ b/tests/test_handoff_skill_80_85.py @@ -29,7 +29,7 @@ `lane-handoff`'s side of every comparison is ITS OWN code: its `sanitize` function, its kind-word lines and its register-line template, cut out of the file. So the two cannot drift apart with this file still green. Every case -marked RED fails against the skill at 5e88a2c and passes with the fix. +marked RED fails against the skill at b707bd7 and passes with the fix. NOTHING HERE READS THE CALLER'S ENVIRONMENT. A lane window exports `WORKBENCHES_CLAUDE_*`, `TMUX`, `LANES_AGENT` and the other `LANES_*` seams, @@ -77,7 +77,7 @@ #: The markers that find the blocks, each carried by exactly one shell block. STEP4 = "# (c) the row: ONE WRITE" -DOOR = 'case "$why" in' +DOOR = "REFUSED: the why is ONE line" # --- the blocks, as the files hold them -------------------------------------- @@ -242,7 +242,7 @@ def _the_one(box: Sandbox, verb: str) -> Call: @pytest.mark.parametrize("shell", SHELLS) def test_both_record_writes_carry_the_why_the_block_computed( box: Sandbox, shell: str) -> None: - """RED at 5e88a2c. The object log's PAUSED line takes the why, then the + """RED at b707bd7. The object log's PAUSED line takes the why, then the kind's word, as its free text, and the register's file-level line is the shape `lane-handoff` writes: lane, session@workstation, the instant, that same text, then `handoff refreshed`.""" @@ -264,7 +264,7 @@ def test_both_record_writes_carry_the_why_the_block_computed( @pytest.mark.parametrize("shell", SHELLS) def test_no_write_records_a_template_placeholder(box: Sandbox, shell: str) -> None: - """RED at 5e88a2c. Run as printed, no argument of any write is a + """RED at b707bd7. Run as printed, no argument of any write is a ``: what the template showed a reader is never what the block records.""" _step4(box, shell, "a usage reset") @@ -276,7 +276,7 @@ def test_no_write_records_a_template_placeholder(box: Sandbox, def test_no_shell_block_carries_the_placeholders_80_item_7_names() -> None: - """RED at 5e88a2c. The three placeholders the finding quotes are gone from + """RED at b707bd7. The three placeholders the finding quotes are gone from every shell block of the skill, so no later copy brings one back beside the fixed writes.""" left = [line.strip() for block in _shell_blocks() @@ -308,7 +308,7 @@ class WhyCase(NamedTuple): @pytest.mark.parametrize("case", sorted(WHY_CASES)) def test_the_skill_writes_what_lane_handoff_writes_for_the_same_why( box: Sandbox, shell: str, case: str) -> None: - """RED at 5e88a2c. For the same why and the same kind, the skill hands + """RED at b707bd7. For the same why and the same kind, the skill hands `log PAUSED` and `append-line` exactly what `lane-handoff`'s own code hands them: `sanitize` (` — ` and every line break spelled `; `), an empty why written `handoff`, then the kind's word (`kind unknown`, `respawn`, @@ -325,7 +325,7 @@ def test_the_skill_writes_what_lane_handoff_writes_for_the_same_why( @pytest.mark.parametrize("shell", SHELLS) def test_a_line_break_reaches_neither_one_line_write(box: Sandbox, shell: str) -> None: - """RED at 5e88a2c (#85). A break that reaches step 4 anyway, whatever its + """RED at b707bd7 (#85). A break that reaches step 4 anyway, whatever its spelling, is one `; `, as #218 spelled it for `lane-handoff`: the record is still written, on one line, and no write is handed a newline or a carriage return.""" @@ -356,7 +356,7 @@ def _door(box: Sandbox, shell: str, @pytest.mark.parametrize("case", sorted(REFUSED_WHYS)) def test_a_why_carrying_a_line_break_is_refused_at_the_door( box: Sandbox, shell: str, case: str) -> None: - """RED at 5e88a2c, where there was no door. A why carrying a newline or a + """RED at b707bd7, where there was no door. A why carrying a newline or a carriage return stops the skill where it stands (nothing after the block runs) with exit 2, and says what `lane-handoff` says at its own door: the why is ONE line, nothing was written, put `; ` where the break was.""" @@ -392,8 +392,31 @@ def test_a_one_line_why_passes_the_door_untouched(box: Sandbox, shell: str, case, proc.returncode, proc.stdout, proc.stderr) +@pytest.mark.parametrize("shell", SHELLS) +def test_an_unset_why_passes_the_door_under_set_u_and_is_written_handoff( + box: Sandbox, shell: str) -> None: + """RED at 9b80eed, where the door read `$why` bare: under `set -u` an + unset why stopped the shell there ("why: unbound variable" in bash, + "parameter not set" in zsh), before step 1. A why the operator did not + give is no why at all. It passes the door with nothing said, and step 4, + run in the same `set -u` shell, writes it `handoff`, as `lane-handoff` + does.""" + script = (f'set -u\nL="{box.helper}"\nlane={LANE}\nws={WS}\nuuid={UUID}\n' + 'ws_missing=""\nuuid_missing=""\nunset why\n' + + _one_block(DOOR).body + "printf 'AFTER\\n'\n" + + _one_block(STEP4).body) + proc = box.run(shell, script) + assert proc.returncode == 0, (proc.returncode, proc.stderr) + assert proc.stdout.startswith("AFTER\n"), (proc.stdout, proc.stderr) + assert "why" not in proc.stderr, proc.stderr + assert _the_one(box, "log").argv[-1] == "handoff kind unknown" + assert _the_one(box, "append-line").argv[1:] == [ + f"PAUSED — lane {LANE}, session {UUID}@{WS}, {NOW}, " + "handoff kind unknown; handoff refreshed"] + + def test_the_door_stands_before_anything_is_renamed_or_written() -> None: - """RED at 5e88a2c. The door is asked before step 1, which renames the + """RED at b707bd7. The door is asked before step 1, which renames the window, and before every block that publishes, commits or writes a record, so its refusal leaves nothing to undo, as `lane-handoff`'s does.""" door = _one_block(DOOR) @@ -418,7 +441,7 @@ def code(block: Block) -> List[str]: def test_step_4s_prose_says_a_line_break_is_spelled_too() -> None: - """RED at 5e88a2c. The sentence that tells a reader what "sanitize" means + """RED at b707bd7. The sentence that tells a reader what "sanitize" means names the line break beside the ` — `, as the code under it now does.""" text = _skill_text() start = text.index("First sanitize the operator's words")