From 8a08e9d1914ae942bc7423787f9ca43a4cc23fc0 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Tue, 8 Sep 2026 23:18:19 -0700 Subject: [PATCH] docs: prove UI work on a disposable surface, not the user's session cat-mode gains a UI-testing rule under Verify and the live-demo rules gain the matching bullet: disposable surface first, an explicit hands-off window and marker before driving real input, stop when the session goes back to the user, and leave no residue. Contract tests pin the pieces an agent acts on, including that the marker path matches the guard hook's default. The skill's own line cap forced a trim in the same commit, so one bullet's incident narrative is cut back to its rule. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KU2pPKob4MJ1NqjsfTNyYJ Change-Id: Ifa0ea6ce9e6f809dfe162be89ff630d8ec90d363 --- corpus/skills/cat-mode/SKILL.md | 6 +++--- engine/CLAUDE.core.md | 1 + tests/test_cat_mode.py | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/corpus/skills/cat-mode/SKILL.md b/corpus/skills/cat-mode/SKILL.md index d554a7ab..5e09943e 100644 --- a/corpus/skills/cat-mode/SKILL.md +++ b/corpus/skills/cat-mode/SKILL.md @@ -250,9 +250,9 @@ agent switch, or resubmit is a fix, and none comes before the repro. **An interruption or stuck state gets instrument-level proof before a fix, and the fix goes to a subagent.** A poll loop not converging, a process not responding as expected, a restart that doesn't complete — treat this as its own investigation, not something to guess through inline. Gather real evidence first (the target's own logs, `ps -o stat,wchan`, a live query) before naming a cause, then delegate the actual fix to a subagent rather than hand-patching it in the main thread. A DO1 restart once looked hung on a stale PID; the owner's own log showed the real mechanism in two lines — `received SIGTERM, shutting down gracefully` followed 30s later by `process survived SIGTERM for 30000ms after worker stop; restarting worker` — a per-worker watchdog resurrecting mid-shutdown under real task load, not a hang. -**A factual or technical claim gets a real repro script, not a history search.** -Judging an old comment or a "probably confabulated" suspicion needs an actual attempt under the claimed conditions, not a `git log` sweep. No citation means -"never verified," not "false." A live repro proved a dismissed "yauzl hangs" comment was real on the pinned versions. +**UI testing must not disrupt the user's own session.** Prove a UI or surface change somewhere disposable — a test channel or workspace, a throwaway profile, a second display, a VM, a headless run. Driving the user's real keyboard, mouse, or screen is a last resort needing an explicit hands-off window first: state the acceptance test in one line, get the yes, `touch /tmp/.ui-input-window`, and remove it when the window closes; a PreToolUse hook (`engine/hooks/ui-input-guard/`) blocks synthetic input and screen recording while no window is open, the screen is locked, or the user is still typing. Stop at the first sign the session is theirs again (idle time drops, the frontmost app changes, the screen locks), and leave no residue: undo stray messages, pins, or reactions, or say what was left behind. + +**A factual or technical claim gets a real repro script, not a history search.** Judging an old comment or a "probably confabulated" suspicion needs an actual attempt under the claimed conditions, not a `git log` sweep. No citation means "never verified," not "false." **Unhedged root-cause or fix claims about live system behavior need instrument-level proof in the same message, or `UNVERIFIED:`.** The gate is the claim type ("this is why it's slow," "this is the bug"), not a diff --git a/engine/CLAUDE.core.md b/engine/CLAUDE.core.md index 525e1708..488047d4 100644 --- a/engine/CLAUDE.core.md +++ b/engine/CLAUDE.core.md @@ -37,6 +37,7 @@ These override brevity. If proof makes a message longer, the message gets longer - Before inviting me to test: run one full end-to-end machine-verified rehearsal of the exact flow I will perform. Pieces verified separately don't count as ready. Never say "go" on assembly alone. - Before the live test starts, restate the acceptance test in one sentence and get my yes ("the test is: you speak, and X happens"). I should never have to write it myself in caps. - Once I'm testing: freeze the demo surface. No edits, relaunches, or cosmetic changes to the thing I'm looking at unless I asked or the test is failing. Same session: an unrequested layout edit during the test window corrupted the demo page. Mechanically: when the live window opens, write the demo-surface paths (one absolute path, `dir/` prefix, or glob per line) to `/tmp/.demo-freeze`, and delete the file when the window ends — a PreToolUse hook (`engine/hooks/demo-freeze/`) blocks edits to matching paths while it exists (auto-expires after 2h). +- Prove UI work somewhere disposable (test channel, throwaway profile, second display, VM, headless run) instead of my live session. Driving my real keyboard, mouse, or screen needs an explicit hands-off window: state the acceptance test in one line, get my yes, then `touch /tmp/.ui-input-window` and delete it when the window ends — a PreToolUse hook (`engine/hooks/ui-input-guard/`) blocks synthetic input and screen recording while no window is open, the screen is locked, or I am still typing. Undo stray messages or reactions the run created, or say what was left. - Every message during a live window ends with exactly one action for me, or "nothing needed from you for ~N minutes". Never leave me waiting without a named next step. - If the deliverable is a same-day demo, plan the demo path first — the smallest end-to-end visible slice. Product-grade extras (settings UIs, multi-platform parity, test suites) come only after the demo runs. diff --git a/tests/test_cat_mode.py b/tests/test_cat_mode.py index a3526cfc..8f1cf20d 100644 --- a/tests/test_cat_mode.py +++ b/tests/test_cat_mode.py @@ -100,6 +100,33 @@ def test_body_names_the_default_hook_and_flag(self): self.assertEqual(parse_frontmatter(read_skill_text())["disable-model-invocation"], "true") +class TestUiTestingRule(unittest.TestCase): + """The rule that keeps a UI proof run off the user's own session. + + Prose cannot be executed, but the pieces an agent has to act on -- a + disposable surface, a granted window, the marker path the guard hook + reads, and cleanup -- must all still be named, and the marker path must + match the hook that enforces it. + """ + + def test_names_a_disposable_surface_and_a_granted_window(self): + text = read_skill_text().lower() + self.assertIn("disposable", text) + self.assertIn("hands-off window", text) + + def test_names_the_marker_path_the_guard_hook_reads(self): + self.assertIn("/tmp/.ui-input-window", read_skill_text()) + + def test_marker_path_matches_the_hook_default(self): + hook = os.path.join(REPO_ROOT, "engine", "hooks", "ui-input-guard", "detect.py") + with open(hook, encoding="utf-8") as handle: + self.assertIn('"/tmp/.ui-input-window"', handle.read()) + + def test_requires_cleanup_of_what_the_run_left_behind(self): + text = read_skill_text().lower() + self.assertTrue("residue" in text or "undo stray" in text, "cleanup rule missing") + + class TestCatModeReferences(unittest.TestCase): def test_every_referenced_skill_still_exists(self): text = read_skill_text()