mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
docs(skill): launch workers interactively and wait on --changed
The taught loop started its worker with `claude -p`, but headless print mode never stops to ask, so the `waiting` state steps 3-4 are built on could not arrive. Step 2 now launches interactively. Every wait after a send passes `--changed`, with the reason spelled out: without it the loop re-reads the state it just walked in on. The exit-1 "worker died" branch is documented too, and both guards are asserted in the skill-content test. Also gives the install round-trip test a Drop guard, so a panic cannot leave `CLAUDE_CONFIG_DIR` set for whatever runs next in the process.
This commit is contained in:
+1
-1
@@ -64,7 +64,7 @@ it never wraps or replaces the agent.
|
||||
- **Copy Session ID** — put the agent's native session id on the clipboard, beside *Copy Working Directory*, for pasting into `codex resume`, a bug report, or another tool
|
||||
- **Context feed** — palette commands send the current selection or the repo's `git diff` to the running agent as a ready-made prompt
|
||||
- **Tray icon** — a system tray / menu bar item that flips to an attention state the moment any agent needs your input; its menu lists every agent pane (brand avatar + status dot, click to reveal), switches the notification policy, and offers *Quit and Stop Daemon* alongside the plain session-keeping quit (`show_tray_icon`, on by default)
|
||||
- **`tty7 wait`** — the CLI's orchestration primitive: block until a pane's agent needs input or finishes its turn (`tty7 wait %3 --until waiting,done --timeout 600`, exit 124 on timeout), so one agent can sleep until its peer blocks on a permission prompt instead of screen-scraping — then `tty7 capture %3 --plain` to read the result
|
||||
- **`tty7 wait`** — the CLI's orchestration primitive: block until a pane's agent needs input or finishes its turn (`tty7 wait %3 --until waiting,done --changed --timeout 600`, exit 124 on timeout), so one agent can sleep until its peer blocks on a permission prompt instead of screen-scraping — then `tty7 capture %3 --plain` to read the result. The agent status is a level, not an event, so `--changed` ignores the state the pane was already in when the wait began; without it, the JSON's `stale` flag says whether the answer might belong to the previous turn
|
||||
- **Orchestration skill** — a switch (Settings → Agents) that installs a Claude Code skill (`~/.claude/skills/tty7-orchestration`) teaching a *primary* agent the delegation loop — spawn a worker pane, send it a bounded task, `wait` on it, capture the result. A skill rather than a global instruction on purpose: only its one-line description rides in context until explicitly invoked, and worker agents never inherit orchestration authority
|
||||
- **`tty7` on PATH** — the CLI ships inside every installer and is put on PATH at launch, so a script or a coding agent can drive tty7 from any terminal. Inside a tty7 pane it works regardless, since panes inherit the app's environment. On Unix it is a symlink into whichever of `/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, `~/bin`, `~/.cargo/bin` your PATH already covers; on Windows the install directory is appended to your user PATH, and the uninstaller takes it back out. A `tty7` you installed yourself is left alone, never replaced. Off via Settings → About or `install_cli_on_path: false` in `config.json`
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包
|
||||
- **复制 Session ID** —— 把 agent 的原生 session id 复制到剪贴板,就在 *Copy Working Directory* 旁边,方便粘进 `codex resume`、bug 报告或别的工具
|
||||
- **上下文回填** —— 面板命令把当前选区或仓库 `git diff` 打包成 prompt 直接喂给正在跑的 agent
|
||||
- **托盘图标** —— 系统托盘 / 菜单栏常驻图标,任何 agent 等你输入时立即切换为提醒态;菜单列出所有 agent pane(品牌头像 + 状态点,点击直达)、可切换通知策略,并在保留会话的普通退出之外提供 *Quit and Stop Daemon*(`show_tray_icon`,默认开启)
|
||||
- **`tty7 wait`** —— CLI 的编排原语:阻塞到某个 pane 的 agent 等待输入或完成一轮(`tty7 wait %3 --until waiting,done --timeout 600`,超时退出码 124),让一个 agent 睡到同伴卡在权限确认的那一刻,而不是抓屏猜——然后 `tty7 capture %3 --plain` 收结果
|
||||
- **`tty7 wait`** —— CLI 的编排原语:阻塞到某个 pane 的 agent 等待输入或完成一轮(`tty7 wait %3 --until waiting,done --changed --timeout 600`,超时退出码 124),让一个 agent 睡到同伴卡在权限确认的那一刻,而不是抓屏猜——然后 `tty7 capture %3 --plain` 收结果。agent 状态是电平不是边沿,所以 `--changed` 会忽略 wait 开始时 pane 本来就处在的那个状态;不加它的话,JSON 里的 `stale` 标记会告诉你这个答案是不是上一轮留下的
|
||||
- **Orchestration skill** —— 一个开关(设置 → Agents),安装一个 Claude Code skill(`~/.claude/skills/tty7-orchestration`),教 *primary* agent 完整的委派循环——开 worker pane、发一个边界清晰的任务、`wait` 等待、收结果。特意做成 skill 而非全局指令:平时只有一行描述占上下文,显式调用才加载全文,worker agent 也不会继承编排权限
|
||||
- **`tty7` 上 PATH** —— CLI 随每个安装包一起发布,启动时自动放到 PATH 上,脚本和 coding agent 在任何终端里都能驱动 tty7。tty7 自己的 pane 里则一定可用,因为 pane 继承 app 的环境。Unix 上是往 `/opt/homebrew/bin`、`/usr/local/bin`、`~/.local/bin`、`~/bin`、`~/.cargo/bin` 中你 PATH 已经覆盖的那个目录里放一个软链;Windows 上是把安装目录追加到用户 PATH,卸载时再摘掉。你自己装的 `tty7` 一律保持原样,不会被覆盖。关掉:设置 → About,或 `config.json` 里 `install_cli_on_path: false`
|
||||
|
||||
|
||||
@@ -49,17 +49,29 @@ pane. Every verb takes `--json`.
|
||||
## The delegation loop
|
||||
|
||||
1. Create a worker pane: `tty7 tab new --cwd DIR` — prints the pane id (`%N`)
|
||||
2. Start the worker: `tty7 send %N 'claude -p \"one bounded task\"' --enter`
|
||||
3. Sleep until it needs you: `tty7 wait %N --until waiting,done --timeout 600`
|
||||
2. Start the worker: `tty7 send %N 'claude \"one bounded task\"' --enter`
|
||||
- interactive, not `claude -p`: headless print mode never stops to ask,
|
||||
so the `waiting` state this loop turns on would never arrive
|
||||
3. Sleep until it needs you:
|
||||
`tty7 wait %N --until waiting,done --changed --timeout 600`
|
||||
- exit 0: the JSON report names the matched state, with the agent's
|
||||
message and native session id
|
||||
- exit 124: still working — wait again, or look in on it
|
||||
- exit 1 with `\"status\": \"exit\"`: the worker died; do not wait again
|
||||
4. If it is *waiting* (a permission prompt or question), read and answer it:
|
||||
`tty7 capture %N --plain`, then `tty7 send %N 'y' --enter` (or whatever
|
||||
the prompt asks)
|
||||
the prompt asks) — then go back to step 3
|
||||
5. When *done*, collect the result: `tty7 capture %N --plain`
|
||||
6. Clean up: `tty7 pane close %N`
|
||||
|
||||
Always pass `--changed` when you wait after sending something. The status the
|
||||
server keeps is a level, not an event: `done` stands until the next turn
|
||||
begins and `waiting` stands until the agent moves, so a plain `wait` issued
|
||||
right after a `send` answers with the *previous* turn's state before the
|
||||
worker has even read your input. `--changed` ignores the state the pane was
|
||||
already in. Without it, check `\"stale\": true` in the JSON before trusting a
|
||||
wake-up.
|
||||
|
||||
Run workers in parallel by repeating steps 1–2, then waiting on each pane.
|
||||
`tty7 ls` shows every workspace, tab and pane; `tty7 agents` shows every
|
||||
agent and its status at a glance.
|
||||
@@ -147,6 +159,43 @@ mod tests {
|
||||
for verb in ["tab new", "send %N", "wait %N", "capture %N", "pane close"] {
|
||||
assert!(SKILL.contains(verb), "skill body lost `{verb}`");
|
||||
}
|
||||
// The whole loop rests on `--changed`: without it a wait issued right
|
||||
// after a send answers with the previous turn's status.
|
||||
assert!(SKILL.contains("--changed"), "the loop lost --changed");
|
||||
// And the worker must be *launched* interactively — `claude -p` never
|
||||
// reaches the `waiting` state steps 3–4 are built on. The prose may
|
||||
// still name it; the command in step 2 may not start with it.
|
||||
assert!(
|
||||
!SKILL.contains("'claude -p"),
|
||||
"headless print mode cannot produce the `waiting` state this loop waits for"
|
||||
);
|
||||
}
|
||||
|
||||
/// Owns `CLAUDE_CONFIG_DIR` and a scratch directory for the length of a
|
||||
/// test, and puts both back on the way out — including on a panic, which
|
||||
/// a plain tail cleanup would skip, leaving the var set for whatever runs
|
||||
/// next in this process.
|
||||
struct ScratchConfigDir(PathBuf);
|
||||
|
||||
impl ScratchConfigDir {
|
||||
fn new(tag: &str) -> ScratchConfigDir {
|
||||
let dir =
|
||||
std::env::temp_dir().join(format!("tty7-skill-test-{}-{tag}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
// SAFETY: test-scoped env mutation. `skill_path` is the only
|
||||
// reader of this var in this binary, and the guard restores it.
|
||||
unsafe { std::env::set_var("CLAUDE_CONFIG_DIR", &dir) };
|
||||
ScratchConfigDir(dir)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScratchConfigDir {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: as above — undoing what `new` did.
|
||||
unsafe { std::env::remove_var("CLAUDE_CONFIG_DIR") };
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Install → installed → uninstall round-trips against a scratch
|
||||
@@ -154,10 +203,8 @@ mod tests {
|
||||
/// deleted. Env-var scoped: this test owns the var for its duration.
|
||||
#[test]
|
||||
fn install_roundtrip_and_foreign_file_safety() {
|
||||
let scratch = std::env::temp_dir().join(format!("tty7-skill-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&scratch).unwrap();
|
||||
// SAFETY: test-scoped env mutation; no other test reads this var.
|
||||
unsafe { std::env::set_var("CLAUDE_CONFIG_DIR", &scratch) };
|
||||
let guard = ScratchConfigDir::new("roundtrip");
|
||||
let scratch = guard.0.clone();
|
||||
|
||||
assert!(!installed());
|
||||
install().unwrap();
|
||||
@@ -180,8 +227,5 @@ mod tests {
|
||||
assert!(!installed(), "a foreign file is not a tty7 install");
|
||||
assert!(uninstall().is_err());
|
||||
assert!(path.exists(), "the user's file was deleted");
|
||||
|
||||
unsafe { std::env::remove_var("CLAUDE_CONFIG_DIR") };
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user