31 KiB
Headless Panel Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Сервер панели «Claude» работает как фоновая служба launchd без iTerm2, iTerm2 только регистрирует web view, а фон панели настраивается на клиенте шестерёнкой ⚙.
Architecture: files/claude_agents.py получает два режима одного файла: --headless (HTTP-сервер, дерево сессий по PID/tmux, лимиты, баланс) и регистратор Toolbelt (запуск iTerm2 из AutoLaunch). Вся связь с живым iTerm2 (тема профиля, вкладки, split) удаляется. LaunchAgent описывается генератором files/launchagent.py и ставится install.sh. Фон выбирается в браузере, хранится в localStorage, производные цвета считает клиентский JS.
Tech Stack: Python 3.8+ (stdlib; тесты — unittest), POSIX shell / bash, launchd (launchctl), HTML/CSS/JS внутри render_page, node — только для теста клиентского JS.
Spec: openspec/changes/headless-panel/ — proposal.md, design.md, specs/panel-service/spec.md, specs/panel-appearance/spec.md.
Issue / branch: #4, feat/4-headless-panel, worktree /Users/andrey.epifancev/projects/cc-agents-iterm-plugin/.claude/worktrees/headless-panel.
Global Constraints
- Рантайм — только стандартная библиотека Python 3.8+; модуль
iterm2импортируется опционально (уже так) и нужен только регистратору. - Тесты —
unittestиз stdlib, без pytest и pip:python3 -m unittest discover -s tests -v. Ни один тест не ходит в сеть, не трогает launchd, Keychain, реальный~/.claudeи реальный~/Library. - Сервер слушает только
127.0.0.1; порт по умолчанию8766(ключportвconfig.json). - Метка службы
com.andrey.claude-agents(та же строка —identifierweb view); лог службы~/Library/Logs/claude-agents/panel.log. - Тема по умолчанию:
bg #002b36,fg #aacace,dim #668a91,green #859900,yellow #b58900,red #dc322f, шрифт'MesloLGLNF', 'MesloLGLNF-Regular', ui-monospace, monospace, размер13. - Образцы фона:
#002b36,#1e1e1e,#000000,#2b2b3a,#fdf6e3; плюс «свой цвет» (<input type="color">) и «сброс». install.shиuninstall.shотказываются работать, если заданSSH_CONNECTIONилиSSH_TTY, ничего не меняя.- Хук
files/agents_panel_hook.shиfiles/settings_hooks.pyне меняются. - Тексты для пользователя — на русском, в стиле существующих (
ok/warn/dieв установщике, короткие подписи в панели).
Review Focus
- Служба стартовала, а токена нет / Keychain заблокирован → страница отвечает 200, в блоке лимитов текст ошибки, процесс не падает (тест в Task 2:
test_serve_survives_usage_failure). - Порт уже занят чужим процессом при старте службы →
serve()падает сOSError, launchd перезапускает позже; второй сервер не появляется (тест в Task 2:test_serve_busy_port_raises). - Мусор в
localStorageили хранилище недоступно (приватный режим) → тема по умолчанию, без ошибки JS (тест в Task 5:panelColors('nope') === null+try/catchвокруг чтения). - Блок лимитов перерисовывается раз в 15 с → раскрытая строка образцов и ⚙ не исчезают (тесты в Task 5:
gear-slotв.foot, статичный#bgctlвне#limits). - Путь к скрипту с пробелами (
Application Support) в LaunchAgent → передаётся одним аргументом (тест в Task 3:test_cli_writes_plist_with_spaces).
Task 1: Тестовая основа и удаление связи с живым iTerm2
Files:
- Create:
tests/test_core.py - Modify:
files/claude_agents.py(CONFIG_KEYS~73–91,DEFAULT_THEME~115–117,state/_app~119–123,live_sessions~1253–1270,open_worktree~1288–1324,activate~1342–1362,refresher~1365–1384,_hex/_mix/theme_poller~1396–1438) - Modify:
files/config.example.json(убратьuse_profile_theme,theme_variant) - Modify:
.gitignore(добавить__pycache__/)
Interfaces:
-
Produces:
async def refresher() -> None(без аргумента; дерево строится какbuild_tree(sessions, None, procs, tmux));activate(uuid)иopen_worktree(uuid, path)— сигнатуры прежние, для целей неtmux:ничего не делают. Удалены:live_sessions,theme_poller,_hex,_mix,_app,USE_PROFILE_THEME,THEME_VARIANT, ключиuse_profile_theme/theme_variantизCONFIG_KEYS, веткиtmux_twinsвactivate. -
build_treeне меняется. -
Step 1: Write the failing tests в
tests/test_core.py(в начале —sys.path.insert(0, <repo>/files);import claude_agents as ca):test_default_theme_matches_chat_profile:ca.DEFAULT_THEME == {"bg": "#002b36", "fg": "#aacace", "dim": "#668a91", "green": "#859900", "yellow": "#b58900", "red": "#dc322f", "font": "'MesloLGLNF', 'MesloLGLNF-Regular', ui-monospace, monospace", "size": 13}.test_iterm_coupling_removed: дляname in ("theme_poller", "live_sessions", "USE_PROFILE_THEME", "THEME_VARIANT", "_app", "_mix", "_hex")—assertFalse(hasattr(ca, name));"use_profile_theme"и"theme_variant"не вca.CONFIG_KEYS.test_old_theme_keys_ignored: временныйconfig.jsonс{"use_profile_theme": false, "theme_variant": "dark", "poll_sec": 2}; подменитьca.CONFIG_PATH,ca._config_mtime = None;ca.load_config()→ca.state.get("config_error") is None,ca.POLL_SEC == 2.0. ВосстановитьPOLL_SECиCONFIG_PATHвtearDown.test_activate_tmux_target:ca.state["tree"] = [{"uuid": "tmux:/tmp/s|%3", "agents": []}],ca._tmuxподменён записью вызовов;asyncio.run(ca.activate("tmux:/tmp/s|%3"))→ вызовы[("/tmp/s", "switch-client", "-t", "%3"), ("/tmp/s", "select-window", "-t", "%3"), ("/tmp/s", "select-pane", "-t", "%3")].test_activate_non_tmux_is_noop: дерево сuuid "w0t0p0:ABC";asyncio.run(ca.activate("w0t0p0:ABC"))— без исключения, вызовов_tmuxнет.test_open_worktree_non_tmux_is_noop:ca.known_rootsподменён наlambda: {tmpdir};asyncio.run(ca.open_worktree("w0t0p0:ABC", tmpdir))— без исключения, вызовов_tmuxнет.test_open_worktree_tmux_splits: дерево сuuid "tmux:/tmp/s|%3",known_roots → {tmpdir},ca.FILE_MANAGER = "lf";_last_open.clear(); вызов →[("/tmp/s", "split-window", "-h", "-t", "%3", "-c", tmpdir, "exec lf")].
-
Step 2: Run tests to verify they fail Run:
python3 -m unittest tests.test_core -vExpected: FAIL наtest_default_theme_matches_chat_profile,test_iterm_coupling_removed,test_old_theme_keys_ignored(ключ всё ещё известен),test_open_worktree_non_tmux_is_noopможет упасть на_app; tmux-тесты могут пройти уже сейчас — это нормально. -
Step 3: Implement в
files/claude_agents.py: обновитьDEFAULT_THEMEзначениями из Global Constraints; удалить перечисленное в Interfaces;refresher()без параметра (в прежнемmain— вызовrefresher()и без запускаtheme_poller, чтобы модуль оставался рабочим до Task 2); вactivateиopen_worktreeоставить только веткуtmux:и раннийreturnдля остального; убрать изstateключ"live"и из/rawполеtheme_error. Обновить модульный docstring: установка черезinstall.sh, фоновая служба, без упоминанияsettings-hooks.json(закрывает O-1 из issue #3). Изconfig.example.jsonубрать два ключа. В.gitignoreдобавить строку__pycache__/. -
Step 4: Run tests to verify they pass Run:
python3 -m unittest tests.test_core -v && python3 -m py_compile files/claude_agents.pyExpected: всеOK;grep -n "iterm2\." files/claude_agents.pyпоказывает толькоasync_register_web_view_toolвmain/регистраторе иrun_forever. -
Step 5: Commit
git add .gitignore tests/test_core.py files/claude_agents.py files/config.example.json git commit -m "refactor: drop live iTerm2 coupling from panel server (#4)"
Task 2: Фоновый режим, регистратор Toolbelt и точка входа
Files:
- Create:
tests/test_service.py - Modify:
files/claude_agents.py(main~1487–1506 →serve,register_toolbelt,port_alive,kickstart_service,run)
Interfaces:
-
Consumes:
refresher(),usage_poller(),balance_poller(),handle(Task 1 / существующие). -
Produces:
SERVICE_LABEL = "com.andrey.claude-agents"(модульная константа).async def serve() -> None—load_config(),asyncio.start_server(handle, "127.0.0.1", PORT)(ошибка bind →OSErrorнаружу), задачиrefresher(),usage_poller(),balance_poller(), затем работает до отмены; при отмене закрывает сервер.def port_alive(port: int, host: str = "127.0.0.1", timeout: float = 0.5) -> bool— удаётся ли TCP-подключение.def kickstart_service() -> None—subprocess.run(["launchctl", "kickstart", f"gui/{os.getuid()}/{SERVICE_LABEL}"], capture_output=True, timeout=10), исключения глотаются.async def register_toolbelt(connection) -> None—load_config(); еслиnot port_alive(PORT)→kickstart_service(); затемiterm2.tool.async_register_web_view_tool(connection, display_name="Claude", identifier=SERVICE_LABEL, reveal_if_already_registered=False, url=f"http://127.0.0.1:{PORT}/"). Сервер не поднимает.def run(argv: list) -> None—"--headless" in argv→asyncio.run(serve()); иначе еслиiterm2 is not None→iterm2.run_forever(register_toolbelt); иначеraise SystemExit("нужен iTerm2 или флаг --headless"). Блокif __name__ == "__main__": run(sys.argv[1:]).- Функция
mainудаляется.
-
Step 1: Write the failing tests в
tests/test_service.py(подмены черезunittest.mock.patch.object; свободный порт —socket.bind(("127.0.0.1", 0))):test_port_alive: слушающий сокет на свободном порту →True; после закрытия →False.test_serve_answers: подменитьPORT(свободный),CONFIG_PATH(несуществующий),load_sessions → [],fetch_usage → {},fetch_balance → ({}, "test"); внутриasyncio.runзапуститьserve()задачей, дождатьсяport_alive(≤ 3 с); GET/(черезloop.run_in_executor+urllib.request) → статус 200, в теле'<div id="root">'; GET/raw→ JSON с ключом"tree"; отменить задачу, порт освобождён.test_serve_survives_usage_failure: как выше, ноfetch_usageбросаетFileNotFoundError; GET/limits(опрос ≤ 3 с) содержит"Не найден токен Claude Code"; GET/→ 200.test_serve_busy_port_raises: занять порт слушающим сокетом,PORT= он;with self.assertRaises(OSError): asyncio.run(ca.serve()).test_register_toolbelt_kickstarts_when_port_dead: фальшивыйiterm2(types.SimpleNamespace(tool=SimpleNamespace(async_register_web_view_tool=<async-запись kwargs>))),port_alive → False,kickstart_service— запись;asyncio.run(ca.register_toolbelt(object()))→ kickstart вызван 1 раз; регистрация сdisplay_name="Claude",identifier="com.andrey.claude-agents",reveal_if_already_registered=False,url="http://127.0.0.1:8766/"(приPORT = 8766).test_register_toolbelt_skips_kickstart_when_alive:port_alive → True→ kickstart не вызван, регистрация вызвана.test_kickstart_command:subprocess.runподменён;ca.kickstart_service()→ argv["launchctl", "kickstart", f"gui/{os.getuid()}/com.andrey.claude-agents"]; приsubprocess.run, бросающемOSError, исключения наружу нет.test_run_headless_calls_serve:serveподменён async-функцией с флагом;ca.run(["--headless"])→ флаг выставлен.test_run_without_iterm_exits:iterm2 = None;with assertRaises(SystemExit): ca.run([]).
-
Step 2: Run tests to verify they fail Run:
python3 -m unittest tests.test_service -vExpected: FAIL / ERROR —AttributeError: ... has no attribute 'serve'и т.п. -
Step 3: Implement перечисленное в Interfaces в
files/claude_agents.py, рядом с бывшимmain; добавитьimport sys. -
Step 4: Run tests to verify they pass Run:
python3 -m unittest discover -s tests -vExpected: всеOK(включая Task 1). -
Step 5: Commit
git add tests/test_service.py files/claude_agents.py git commit -m "feat: headless panel server and Toolbelt registrar entry points (#4)"
Task 3: Генератор LaunchAgent
Files:
- Create:
files/launchagent.py - Create:
tests/test_launchagent.py
Interfaces:
-
Produces:
LABEL = "com.andrey.claude-agents"def plist(python: str, script: str, log: str) -> dict→{"Label": LABEL, "ProgramArguments": [python, script, "--headless"], "RunAtLoad": True, "KeepAlive": True, "StandardOutPath": log, "StandardErrorPath": log}- CLI:
python3 files/launchagent.py write <plist-path> <python> <script> <log>— создаёт родительский каталог plist и каталог лога, пишетplistlib.dump(..., fmt=FMT_XML)атомарно (.tmp+os.replace, какsettings_hooks.py), печатает путь. Неизвестная команда / не те аргументы → usage в stderr, код 2.
-
Step 1: Write the failing tests в
tests/test_launchagent.py:test_label_matches_server:launchagent.LABEL == "com.andrey.claude-agents" == claude_agents.SERVICE_LABEL.test_plist_shape:plist("/opt/homebrew/bin/python3", "/x/claude_agents.py", "/l/panel.log")равен словарю из Interfaces.test_cli_writes_plist_with_spaces: во временном каталогеsubprocess.run([sys.executable, "files/launchagent.py", "write", f"{tmp}/LaunchAgents/a.plist", "/usr/bin/python3", f"{tmp}/Application Support/claude_agents.py", f"{tmp}/Logs/claude-agents/panel.log"])→ код 0;plistlib.loadфайла равенplist(...)с теми же аргументами (путь с пробелами — один элементProgramArguments); каталог{tmp}/Logs/claude-agentsсуществует.test_cli_bad_args:[..., "launchagent.py", "write"]→ код 2.
-
Step 2: Run tests to verify they fail Run:
python3 -m unittest tests.test_launchagent -vExpected: ERRORModuleNotFoundError: No module named 'launchagent'. -
Step 3: Implement
files/launchagent.py— docstring и стиль как уfiles/settings_hooks.py. -
Step 4: Run tests to verify they pass Run:
python3 -m unittest discover -s tests -vExpected: всеOK. -
Step 5: Commit
git add files/launchagent.py tests/test_launchagent.py git commit -m "feat: LaunchAgent plist generator for panel service (#4)"
Task 4: Установка и удаление службы, документация
Files:
- Create:
tests/test_install.py - Modify:
install.sh,uninstall.sh - Modify:
README.md,CLAUDE.md(разделы «Архитектура», «Команды», «Тесты»),ARCHITECTURE.md(схема, таблица модулей, границы)
Interfaces:
-
Consumes: CLI
files/launchagent.py write(Task 3);claude_agents.py --headless(Task 2). -
Produces (пути, на которые опирается Task 6):
- копия службы:
$HOME/.local/share/claude-agents/claude_agents.py - plist:
$HOME/Library/LaunchAgents/com.andrey.claude-agents.plist - лог:
$HOME/Library/Logs/claude-agents/panel.log - копия регистратора:
$AUTOLAUNCH/claude_agents.py(как раньше)
- копия службы:
-
Step 1: Write the failing tests в
tests/test_install.py(каждый — во временном пустомHOME,env = {**os.environ, "HOME": tmp, "SSH_CONNECTION": "10.0.0.2 50000 10.0.0.1 22"}; второй вариант —SSH_TTY="/dev/ttys009"безSSH_CONNECTION):test_install_refuses_over_ssh(оба варианта):subprocess.run(["bash", "install.sh"], env=env, capture_output=True, text=True)→returncode != 0, вstdout + stderrесть"за самим Mac",os.listdir(tmp) == [].test_uninstall_refuses_over_ssh(оба варианта): то же дляuninstall.shиuninstall.sh --purge.
-
Step 2: НЕ запускать тест до охранной проверки. Без неё
install.shдаже во временномHOMEвыполнитlsof … killпо порту 8766 (остановит настоящую панель) иdefaults write com.googlecode.iterm2, аuninstall.sh— тоже kill по порту. Провал теста здесь не проверяется; вместо этого в Step 4 тест должен пройти, а охрана — стоять первой исполняемой проверкой (ревьюер сверяет по диффу). Реальныеinstall.sh/uninstall.shв этой задаче не запускать — это Task 6. -
Step 3: Implement
- Оба скрипта: первой исполняемой проверкой после
set -euo pipefailи объявления функций вывода — если[ -n "${SSH_CONNECTION:-}${SSH_TTY:-}" ], вывестизапустите за самим Mac, не по SSH — из SSH-сессии launchd и iTerm2 не настроитьиexit 1(вinstall.shчерезdie; вuninstall.shдобавить такую жеdie). install.sh, новые переменные:LABEL=com.andrey.claude-agents,SERVICE_DIR="$HOME/.local/share/claude-agents",PLIST="$HOME/Library/LaunchAgents/$LABEL.plist",SERVICE_LOG="$HOME/Library/Logs/claude-agents/panel.log",PY="$(python3 -c 'import sys; print(sys.executable)')",GUI="gui/$(id -u)".- Новая секция «Фоновая служба» после секции «Скрипт iTerm2»:
install -m 644 "$FILES/claude_agents.py" "$SERVICE_DIR/claude_agents.py";python3 "$FILES/launchagent.py" write "$PLIST" "$PY" "$SERVICE_DIR/claude_agents.py" "$SERVICE_LOG";launchctl bootout "$GUI/$LABEL"(ошибку игнорировать); затем существующий блок «старая копия панели держит порт — останавливаем» (перенести сюда, чтобы он шёл послеbootout);launchctl bootstrap "$GUI" "$PLIST"(ошибка →dieс путём к логу); ждать до 10 с, покаcurl -s -o /dev/null --max-time 1 http://127.0.0.1:$PORT/не ответит; ответил →ok "служба запущена: http://127.0.0.1:$PORT/", нет →warn "служба не ответила — смотрите $SERVICE_LOG". - Секция iTerm2: логика
EnableAPIServerи запуск скрипта черезosascriptостаются; сообщения — «регистрация в Toolbelt», а не «панель запущена». - Итоговый текст: упомянуть, что панель работает и без iTerm2, адрес и лог.
uninstall.sh:launchctl bootout "gui/$(id -u)/com.andrey.claude-agents"(ошибку игнорировать) до блока остановки по порту; удалить plist и$HOME/.local/share/claude-agents;--purge— ещё и$HOME/Library/Logs/claude-agents.README.md: установка только за самим Mac; в таблице «Что ставится» — строки для копии службы, plist, лога; раздел «С iPad»: Termius → Port Forwarding → Local, local port8766, bind127.0.0.1, destination127.0.0.1:8766, открытьhttp://127.0.0.1:8766(именно127.0.0.1, неlocalhost); убрать упоминание темы профиля; «Если что-то не так» — лог службы иlaunchctl print gui/$(id -u)/com.andrey.claude-agents.CLAUDE.md/ARCHITECTURE.md: два процесса → служба launchd (сервер) + регистратор в iTerm2 + хук; команда тестовpython3 -m unittest discover -s tests -v; убрать упоминанияtheme_pollerиlive_sessions.
- Оба скрипта: первой исполняемой проверкой после
-
Step 4: Run tests and syntax checks Run:
python3 -m unittest discover -s tests -v && bash -n install.sh && bash -n uninstall.shExpected: всеOK, синтаксис без ошибок. -
Step 5: Commit
git add tests/test_install.py install.sh uninstall.sh README.md CLAUDE.md ARCHITECTURE.md git commit -m "feat: install panel as launchd service, refuse install over SSH (#4)"
Task 5: Фон панели на клиенте
Files:
- Create:
tests/test_appearance.py - Modify:
files/claude_agents.py(render_limits~1244–1248,render_page~840–1028) - Modify:
README.md(раздел «Работа с панелью»: ⚙)
Interfaces:
-
Consumes:
DEFAULT_THEME(Task 1). -
Produces:
BG_PRESETS = ("#002b36", "#1e1e1e", "#000000", "#2b2b3a", "#fdf6e3")LIGHT_BG_TEXT = "#3b4a50"CLIENT_COLORS_JS: str— JS-код с чистой функциейpanelColors(bg)→{bg, fg, dim}илиnull, еслиbgне#rrggbb.fg=LIGHT_BG_TEXT, если относительная яркость фона по WCAG > 0.4, иначеDEFAULT_THEME["fg"];dim= покомпонентноround(fg * 0.6 + bg * 0.4)(та же формула, что давала#668a91для профиля), строкой#rrggbbв нижнем регистре.- Ключ
localStorage:claude-panel-bg. - Разметка:
render_limitsдобавляет в строку.footпустой<span class="gear-slot"></span>;render_pageпосле#limitsвыводит статичный<div id="bgctl">с<span id="gear-home">, внутри которого лежит<span id="gear">⚙</span>, и скрытой строкой<div id="bgrow">с образцами<span class="sw" data-bg="#…">для каждогоBG_PRESETS,<input type="color" id="bg-custom">и<span id="bg-reset">сброс</span>.
-
Step 1: Write the failing tests в
tests/test_appearance.py:test_page_applies_saved_bg_before_body:html = ca.render_page();html.index("claude-panel-bg") < html.index("<body>");CLIENT_COLORS_JSцеликом входит вhtml; чтениеlocalStorageобёрнуто вtry(вhtmlдо<body>естьtry {).test_page_has_bg_controls: вhtmlестьid="bgctl",id="gear",id="bgrow",type="color",id="bg-reset", и для каждогоhexизBG_PRESETS—data-bg="{hex}";id="bgctl"идёт после закрытия#limits(не внутри перерисовываемого блока).test_limits_foot_has_gear_slot:ca.state["usage_updated"] = time.time(),usage_error = None→'class="gear-slot"'вca.render_limits(); восстановитьstate.test_panel_colors_js(@unittest.skipUnless(shutil.which("node"), "нет node")):node -eсCLIENT_COLORS_JS + "console.log(JSON.stringify([panelColors('#fdf6e3'), panelColors('#000000'), panelColors('nope'), panelColors('#002b36')]))"→[{"bg": "#fdf6e3", "fg": "#3b4a50", "dim": "#898f8b"}, {"bg": "#000000", "fg": "#aacace", "dim": "#66797c"}, null, {"bg": "#002b36", "fg": "#aacace", "dim": "#668a91"}].
-
Step 2: Run tests to verify they fail Run:
python3 -m unittest tests.test_appearance -vExpected: ERRORAttributeError: ... 'CLIENT_COLORS_JS'/ FAIL по разметке. -
Step 3: Implement в
files/claude_agents.py:- В
<head>после<style>:<script>сCLIENT_COLORS_JSиapplyBg(hex)— ставит--bg,--fg,--dimвdocument.documentElement.style; сразу читаетclaude-panel-bgвtry/catchи применяет, еслиpanelColorsнеnull. - В конце
<body>-скрипта: тап по#gearпереключает видимость#bgrow; тап по.sw→applyBg+ сохранить;inputна#bg-custom→ то же;#bg-reset→ удалить ключ, снять inline-переменные (removeProperty); каждая запись/удаление — вtry/catch.placeGear(): если в#limitsесть.gear-slot— перенести туда существующий узел#gear(appendChild), иначе вернуть в#gear-home; вызывать при загрузке и после каждой вставки вrefreshLimits. - CSS:
#gear,.sw,#bg-reset,#bg-custom— область нажатия не меньше 36×36 CSS px;.sw— круг с рамкойcolor-mix(in srgb, var(--fg) 35%, transparent);#bgrowпо умолчаниюdisplay:none, раскрытый —display:flex; gap:8px; align-items:center. README.md: строка про ⚙ — фон панели на этом устройстве, «сброс» — тема по умолчанию.
- В
-
Step 4: Run tests to verify they pass Run:
python3 -m unittest discover -s tests -vExpected: всеOK(test_panel_colors_jsне пропущен —nodeесть в/opt/homebrew/bin). -
Step 5: Commit
git add tests/test_appearance.py files/claude_agents.py README.md git commit -m "feat: client-side panel background with gear control (#4)"
Task 6: Приёмка на настоящем Mac (выполняет человек)
Задача меняет окружение пользователя (launchd, ~/.claude/settings.json, iTerm2), поэтому её не выполняют сабагенты SDD. Контроллер останавливается после финального ревью ветки и передаёт этот чеклист человеку; результаты (особенно п. 7) записываются в openspec/changes/headless-panel/design.md отдельным коммитом.
Files:
-
Modify:
openspec/changes/headless-panel/design.md(результат проверкиlocalStorageв Toolbelt, найденные отклонения) -
Step 1: За самим Mac, из worktree:
./install.sh→ все строки✓, в конце «служба запущена: http://127.0.0.1:8766/».launchctl print gui/$(id -u)/com.andrey.claude-agents | grep state→state = running.lsof -nP -iTCP:8766 -sTCP:LISTEN→ один процесс, адрес127.0.0.1:8766. Если macOS спросит доступ к Keychain дляsecurity— разрешить и отметить в design.md. -
Step 2: Полностью закрыть iTerm2 (⌘Q). С iPad через туннель Termius открыть
http://127.0.0.1:8766→ дерево сессий из tmux с моделями, контекстом, worktree; внизу лимиты и баланс. -
Step 3: С iPad: «↗ вкладка» у сессии → tmux переключился на её окно/панель;
⎇ папка→ в том же окне tmux открылась панель сlf. -
Step 4:
kill <PID службы>→ в течение ~30 с порт снова отвечает (curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8766/→200). -
Step 5: Запустить iTerm2 → Toolbelt → Claude показывает ту же страницу;
lsofпо-прежнему один слушатель (процесс службы, не iTerm2). -
Step 6: В Safari на iPad: ⚙ → светлый образец → фон светлый, текст тёмный; подождать минуту (автоперезагрузка) — фон держится без мелькания; «сброс» → тема по умолчанию; «свой цвет» открывает палитру iPad.
-
Step 7: В Toolbelt: выбрать фон, перезапустить iTerm2 → записать в design.md, сохранился ли выбор.
-
Step 8:
./install.shещё раз → одна служба, в~/.claude/settings.jsonхуки панели не задублированы (grep -c agents_panel_hook ~/.claude/settings.json— как до повтора). -
Step 9: Из SSH-сессии Termius:
./install.sh→ отказ со словами «за самим Mac», панель продолжает отвечать. -
Step 10 (по желанию):
./uninstall.sh→launchctl print gui/$(id -u)/com.andrey.claude-agentsсообщает, что службы нет; порт 8766 свободен; plist удалён. Затем./install.sh, чтобы вернуть панель. -
Step 11: Commit записей в design.md:
git add openspec/changes/headless-panel/design.md git commit -m "docs: acceptance results for headless panel (#4)"