From 4c52c685a087c18340e42c9a85785f86c5a16308 Mon Sep 17 00:00:00 2001 From: jackiectl2 <305351710+jackiectl2@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:03:33 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20release=202=20is=20verified=20?= =?UTF-8?q?=E2=80=94=20a=20CLI=20permission=20prompt=20reaches=20the=20Mac?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was the one capability the chat panel cannot have at all, so it was worth confirming rather than assuming. It works. Records why the first two attempts looked like a failure: Claude Code auto-approves low-risk commands without raising a permission event, so 'uptime' and 'touch /tmp/x' test nothing. The next person to check this would have drawn the same wrong conclusion twice, as I did. Co-Authored-By: Claude --- docs/STATUS.md | 16 +++++++++++++--- docs/roadmap.md | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index 397efcd..f1e9bba 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -117,9 +117,19 @@ Everything here needs hardware or a session this branch was written without. ## Open — older, still true -- **Never exercised, still:** `StopFailure` · `Elicitation` · - permission prompts in the terminal CLI · alert stack compression · click-to-focus - for anything other than VS Code. +- **Never exercised, still:** `StopFailure` · `Elicitation` · alert stack + compression · click-to-focus for anything other than VS Code. +- **Permission prompts in the terminal CLI: verified 2026-08-20.** A CLI session + on `gl-login4` was made to edit a tracked source file, `Notification` fired with + `notification_type=permission_prompt`, the event reached `events.log`, and the + Mac drew the 🔑 alert. That is release 2's one distinct capability — the chat + panel has no event for this — confirmed end to end for the first time. + + **Two earlier attempts failed and both were the test's fault, not the code's.** + `uptime` and `touch /tmp/…` are auto-approved by Claude Code without raising a + permission event at all, so nothing fired and the run looked like a tailbell + bug. Anyone re-testing this must pick an action Claude Code will genuinely + stop on — editing a tracked file works. - The prototype this grew out of still sits in `~/.claude/hooks/` (`cc-notify.sh`, `cc-hammerspoon.lua`, `gl-listen.py`, …). It is dead code on a live machine — easy to edit by mistake. One part of it is **not** dead: `probe.sh` is still diff --git a/docs/roadmap.md b/docs/roadmap.md index c9c85ca..93200a2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -14,7 +14,7 @@ would be wrong. | --- | --- | --- | --- | | 0 | `v0-ssh-vscode-chatbox` | Claude Code over SSH in the VS Code chat panel → notification on macOS. Zero third-party software or service. | ✅ done | | 1 | `v1-rich-presentation` | Centered, clickable overlay: close button, fade in/out, click to focus the originating editor window. Hammerspoon as an **optional** dependency; release 0's zero-dependency path stays the guarantee. | small | -| 2 | `v2-ssh-terminal-cli` | Claude Code CLI in an SSH terminal. Mostly already working — the addition is `Notification`, which fires in the CLI and covers permission prompts that the chat panel cannot report. | small | +| 2 | `v2-ssh-terminal-cli` | Claude Code CLI in an SSH terminal. The addition is `Notification`, which fires in the CLI and covers permission prompts the chat panel cannot report. **Verified end to end 2026-08-20** — a permission prompt on `gl-login4` drew the 🔑 alert on the Mac. | ✅ done | | 3 | `v3-ssh-any-frontend` | Any editor, terminal or MCP frontend over SSH. Largely documentation and a test matrix rather than new code, for the same reason: hooks are process-level. | small | | 4 | `v4-local-and-remote` | Same behaviour whether Claude Code runs on the cluster or on the Mac. The local path already exists; this release is about making one install cover both without branching config. | medium | | 5 | `v5-claude-and-codex` | Codex as well as Claude Code. **First real architectural step:** an adapter layer, because each agent's event model differs. Needs a stable internal event schema first. | large | From b358490a426f3e68d84558dbb37282895a6baf9b Mon Sep 17 00:00:00 2001 From: jackiectl2 <305351710+jackiectl2@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:13:11 -0400 Subject: [PATCH 2/2] fix: the test suite installed a LaunchAgent on the tester's own Mac MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one-liner branches on uname, and on Darwin it runs mac/install.sh, which writes a LaunchAgent and launchctl-loads it. The suite ran that unguarded, with HOME pointed at a throwaway directory. The directory was cleaned up; the loaded job was not. It went on tailing a deleted fixture and drawing 'oneliner · 完成' onto a real screen for days, and looked exactly like tailbell misbehaving. Written on a cluster, every macOS path in the suite is dead code, so nothing was visible there -- the same blindness docs/MIGRATION.md is about. Two fixes, both of them the rule this project already had rather than a new one: - uname is faked to Linux for that one call. The workstation installer has its own coverage and must never be executed by the suite. - osascript, afplay and say now go through tests/fake-desktop for the whole run, the same seam TAILBELL_CURL has always had for the network. A case asserts the three are still pointed at it, because losing that is silent on Linux. Co-Authored-By: Claude --- tests/fake-desktop | 19 +++++++++++++++++++ tests/run-tests.sh | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 tests/fake-desktop diff --git a/tests/fake-desktop b/tests/fake-desktop new file mode 100755 index 0000000..f9d2fe4 --- /dev/null +++ b/tests/fake-desktop @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Stands in for anything that would reach the user's actual screen or speakers: +# osascript, afplay, say. Records the call and returns success. +# +# This exists because the suite drew real alerts on a real Mac. On the cluster +# where most of it was written, the desktop sink is unreachable and every macOS +# path is dead code, so nothing was visible; on a Mac the same cases pop an +# overlay and play a sound at whoever is running the tests. A recorder here is +# the same seam TAILBELL_CURL already provides for the network. +# +# FAKE_DESKTOP_RECORD names the log; without it the call is simply swallowed. +set -u +if [ -n "${FAKE_DESKTOP_RECORD:-}" ]; then + { printf 'CALL|%s|' "$(basename "$0")" + for a in ${@+"$@"}; do printf '%s ' "$a"; done + printf '\n' + } >> "$FAKE_DESKTOP_RECORD" 2>/dev/null +fi +exit 0 diff --git a/tests/run-tests.sh b/tests/run-tests.sh index a06dfec..8c240e1 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -24,6 +24,15 @@ export TAILBELL_LOG="$TMP/events.log" export TAILBELL_DEBUG_LOG="$TMP/debug.log" export TAILBELL_STATE_DIR="$TMP/state" export TAILBELL_MIN_SECONDS=60 +# Nothing in this suite may reach the screen or the speakers. On the cluster that +# was free -- every macOS path is dead code there -- so it went unnoticed until +# the suite was run on a real Mac and popped overlays at the person running it. +# Same seam as TAILBELL_CURL, for the same reason. +export TAILBELL_OSASCRIPT="$REPO/tests/fake-desktop" +export TAILBELL_AFPLAY="$REPO/tests/fake-desktop" +export TAILBELL_SAY="$REPO/tests/fake-desktop" +export TAILBELL_RENDERER="osascript" # never probe for a live Hammerspoon +export FAKE_DESKTOP_RECORD="$TMP/desktop.log" mkdir -p "$TAILBELL_STATE_DIR" : > "$TAILBELL_LOG" @@ -392,6 +401,15 @@ export TAILBELL_CHANNELS="file" ######################################################################## echo echo "── 声音与语音 ──" +# The seams must be pointed at the recorder for the whole run, not just inside +# the cases below. Without this the suite passes identically on the cluster and +# draws real overlays on a Mac -- which is exactly how it shipped. +if [ "$TAILBELL_OSASCRIPT" = "$REPO/tests/fake-desktop" ] \ + && [ "$TAILBELL_AFPLAY" = "$REPO/tests/fake-desktop" ] \ + && [ "$TAILBELL_SAY" = "$REPO/tests/fake-desktop" ] \ + && [ -x "$REPO/tests/fake-desktop" ]; then + ok "屏幕与扬声器的三个出口全程指向录音器" +else no "有出口没被拦住 —— 在 Mac 上跑会弹真弹窗"; fi # # tailbell-show is macOS code, and this suite runs on the cluster. What IS # testable here is every decision it makes before touching a Mac API: which @@ -674,12 +692,30 @@ else no "install.sh 没生成 cluster-id —— 监听器会静默跳过这台 # point with no coverage at all. Cloned from this working tree at this commit, so # it needs no network either. OH="$TMP/onelinerhome"; mkdir -p "$OH" +# ⚠️ The one-liner branches on `uname`, and on Darwin it runs mac/install.sh, +# which writes a LaunchAgent and `launchctl load`s it. Run unguarded on a Mac +# this test installed a listener into the tester's own login session, pointed at +# this throwaway HOME. The temp directory was later cleaned; the loaded job was +# not, and went on drawing test fixtures onto a real screen for days. +# +# So the machine is told it is Linux for this one call. What is under test here +# is the agent side the one-liner installs -- the workstation installer has its +# own coverage and must never be executed by the suite. +mkdir -p "$TMP/unamebin" +printf '#!/bin/sh\necho Linux\n' > "$TMP/unamebin/uname" +chmod +x "$TMP/unamebin/uname" HOME="$OH" TAILBELL_REPO="$REPO" TAILBELL_DIR="$OH/tailbell" \ TAILBELL_BRANCH="$(git -C "$REPO" rev-parse HEAD)" \ + PATH="$TMP/unamebin:$PATH" \ bash "$REPO/packaging/get-tailbell.sh" >/dev/null 2>&1 if [ -s "$OH/.tailbell/cluster-id" ] && [ -x "$OH/tailbell/bin/tailbell-notify" ]; then ok "一行安装脚本能装出一个完整的 agent 侧" else no "一行安装脚本没装成"; fi +# The bug this guards: a LaunchAgent loaded into the tester's session, pointing +# at a temp directory, outliving both the test and the directory. +if [ -z "$(ls -A "$OH/Library/LaunchAgents" 2>/dev/null)" ]; then + ok "并且没有往用户会话里装 LaunchAgent" +else no "测试装了一个 LaunchAgent —— 它会活过这次测试"; fi # And that what it installed actually notifies, which is the claim being made. s=$(sid o001) mkdir -p "$OH/.tailbell/state"; echo $(( $(date +%s) - 400 )) > "$OH/.tailbell/state/$s.start"