From da6df709cde58fa768ac3d12771e98ef7a6eff84 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:12:09 +0800 Subject: [PATCH] Remove orchestration skill setting --- README.md | 6 + README.zh-CN.md | 6 + skills/tty7/SKILL.md | 3 +- src/core/mod.rs | 1 - src/core/orchestration_skill.rs | 231 -------------------------------- src/ui/app.rs | 58 -------- src/ui/settings.rs | 50 ------- 7 files changed, 14 insertions(+), 341 deletions(-) delete mode 100644 src/core/orchestration_skill.rs diff --git a/README.md b/README.md index 928ac126..9accb3d5 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ Native builds for each platform on [**Releases**](https://github.com/l0ng-ai/tty Terminal and keybinding reference: [docs/features.md](docs/features.md). The agent-facing CLI interface is documented in [skills/tty7/SKILL.md](skills/tty7/SKILL.md). +Install the skill with: + +```sh +npx skills add l0ng-ai/tty7 +``` + ## Benchmarks Same machine, same day, same 155×40 grid — Apple M1 Pro, macOS 26.3.1, diff --git a/README.zh-CN.md b/README.zh-CN.md index 728c610c..ae575126 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -53,6 +53,12 @@ 终端和快捷键参考:[docs/features.zh-CN.md](docs/features.zh-CN.md)。面向 agent 的 CLI 接口见 [skills/tty7/SKILL.md](skills/tty7/SKILL.md)。 +通过以下命令安装 skill: + +```sh +npx skills add l0ng-ai/tty7 +``` + ## 基准测试 同一台机器、同一天、统一 155×40 网格 —— Apple M1 Pro,macOS 26.3.1, diff --git a/skills/tty7/SKILL.md b/skills/tty7/SKILL.md index 28a6d0de..8a12f68b 100644 --- a/skills/tty7/SKILL.md +++ b/skills/tty7/SKILL.md @@ -1,6 +1,7 @@ --- name: tty7 -description: Drive the tty7 terminal workbench from the shell with the `tty7` binary — list workspaces/tabs/panes, split a pane, send keystrokes into one, capture what is on a pane's screen, run a command in a real PTY and pass its exit code through, see which coding agents are running and which ports a pane is listening on. Use this whenever tty7, panes, workspaces, or `%42`/`@7`/"the other pane"/"the other agent" come up; whenever you need to start something long-running or interactive (dev server, REPL, ssh session, `tail -f`, a TUI) that should not sit blocking your Bash tool; whenever a program needs a real terminal to behave the way the user sees it; and whenever you need to look at or report on what is running in some *other* terminal on this machine. Cheap to check: if `$TTY7_PANE` is set you are already inside tty7 and every command here works with no setup. +description: >- + Drive the tty7 terminal workbench from the shell with the `tty7` binary — list workspaces/tabs/panes, split a pane, send keystrokes into one, capture what is on a pane's screen, run a command in a real PTY and pass its exit code through, see which coding agents are running and which ports a pane is listening on. Use this whenever tty7, panes, workspaces, or `%42`/`@7`/"the other pane"/"the other agent" come up; whenever you need to start something long-running or interactive (dev server, REPL, ssh session, `tail -f`, a TUI) that should not sit blocking your Bash tool; whenever a program needs a real terminal to behave the way the user sees it; and whenever you need to look at or report on what is running in some *other* terminal on this machine. Cheap to check: if `$TTY7_PANE` is set you are already inside tty7 and every command here works with no setup. --- # Driving tty7 from the command line diff --git a/src/core/mod.rs b/src/core/mod.rs index 5b23c05d..22fb526f 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,7 +5,6 @@ pub mod agent_prompt; pub mod cli_install; pub mod config; pub mod keychain; -pub mod orchestration_skill; pub mod session; pub mod ssh_config; pub mod update; diff --git a/src/core/orchestration_skill.rs b/src/core/orchestration_skill.rs deleted file mode 100644 index 0725d2d9..00000000 --- a/src/core/orchestration_skill.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! The tty7 orchestration *skill*: a Claude Code skill file describing how a -//! primary agent delegates work to worker panes over the session CLI -//! (`tab new` / `send` / `wait` / `capture`), installed at -//! `~/.claude/skills/tty7-orchestration/SKILL.md`. -//! -//! A skill, deliberately not a global instruction. An earlier cut of this -//! feature appended guidance to `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md`, -//! which taxed every session's context window and — worse — encouraged *every* -//! agent to discover and orchestrate its neighbours. The common shape is -//! primary → workers: one agent owns decomposition, dispatch, waiting and -//! aggregation, and the workers just do bounded tasks. A skill fits that -//! exactly: only its one-line description rides in context until the user or -//! the primary agent explicitly reaches for it, and workers never see it. -//! -//! The file is wholly tty7-owned (marker inside, checked before any delete), -//! so install is a plain overwrite — also the version-refresh path — and -//! uninstall removes the file, never guessing at merged user edits. - -use std::path::PathBuf; - -/// Ownership marker. Uninstall refuses to delete a file without it, so a -/// hand-written skill that happens to share the directory name survives. -const MARKER: &str = ""; - -const SKILL_DIR: &str = "tty7-orchestration"; - -/// The skill itself. The frontmatter description is what Claude Code matches -/// against a session's intent, so it names the *tasks* that should trigger it; -/// the body can afford real workflow detail because it only loads on use. -const SKILL: &str = "\ ---- -name: tty7-orchestration -description: Delegate work to other coding agents running in tty7 terminal panes — spawn a worker pane, send it a prompt, wait until it needs input or finishes, and capture its output. Use when asked to parallelize work across agents, run an agent team, or drive another terminal session in tty7. ---- - - - -# Orchestrating tty7 sessions - -You are the primary agent; panes you create are workers. Keep workers -bounded: give each a self-contained task, and keep decomposition, waiting, -and aggregation here. Do not hand workers orchestration duties — a worker -that finishes its task and stops is what keeps an agent team debuggable. - -Prerequisites: you are inside tty7 (the `TTY7` env var is set) and the -`tty7` CLI is on PATH. `%N` addresses a pane by id; `$TTY7_PANE` is your own -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 \"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) — 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. -"; - -/// The skill's install path: `~/.claude/skills/tty7-orchestration/SKILL.md`, -/// honoring the same `CLAUDE_CONFIG_DIR` override the hooks installer honors. -fn skill_path() -> Option { - let base = if let Some(dir) = std::env::var_os("CLAUDE_CONFIG_DIR").filter(|d| !d.is_empty()) { - PathBuf::from(dir) - } else { - home_dir()?.join(".claude") - }; - Some(base.join("skills").join(SKILL_DIR).join("SKILL.md")) -} - -fn home_dir() -> Option { - #[cfg(unix)] - { - std::env::var_os("HOME").map(PathBuf::from) - } - #[cfg(not(unix))] - { - std::env::var_os("USERPROFILE").map(PathBuf::from) - } -} - -/// Install (or refresh) the skill. A plain overwrite: the file is wholly -/// tty7-owned, and this doubling as the version-refresh path is the point. -pub fn install() -> anyhow::Result { - let path = skill_path().ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?; - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - crate::core::config::write_atomic(&path, SKILL.as_bytes())?; - Ok("Installed".to_string()) -} - -/// Remove the skill — but only a file carrying the ownership marker, so a -/// user's own `tty7-orchestration` skill is never deleted by tty7. The -/// directory goes too once empty; an empty skill dir would read as a broken -/// skill in Claude Code's listing. -pub fn uninstall() -> anyhow::Result { - let path = skill_path().ok_or_else(|| anyhow::anyhow!("cannot resolve home directory"))?; - match std::fs::read_to_string(&path) { - Ok(content) if content.contains(MARKER) => { - std::fs::remove_file(&path)?; - if let Some(dir) = path.parent() { - let _ = std::fs::remove_dir(dir); // fails non-empty; that's the guard - } - Ok("Removed".to_string()) - } - Ok(_) => anyhow::bail!( - "{} exists but was not installed by tty7 — not touching it", - path.display() - ), - Err(_) => Ok("Removed".to_string()), - } -} - -/// Whether the tty7-owned skill is currently installed. -pub fn installed() -> bool { - skill_path().is_some_and(|p| { - std::fs::read_to_string(p) - .map(|s| s.contains(MARKER)) - .unwrap_or(false) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The skill text itself is load-bearing: the frontmatter must parse as a - /// skill (name + description) and the body must carry the ownership - /// marker `uninstall` keys on. - #[test] - fn skill_content_is_well_formed() { - assert!(SKILL.starts_with("---\nname: tty7-orchestration\n")); - assert!(SKILL.contains("\ndescription: ")); - // Frontmatter is closed before the body starts. - assert_eq!(SKILL.matches("\n---\n").count(), 1); - assert!(SKILL.contains(MARKER)); - // The loop teaches the four primitives, not a stale verb set. - 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 - /// `CLAUDE_CONFIG_DIR`; a foreign (marker-less) file is refused, not - /// deleted. Env-var scoped: this test owns the var for its duration. - #[test] - fn install_roundtrip_and_foreign_file_safety() { - let guard = ScratchConfigDir::new("roundtrip"); - let scratch = guard.0.clone(); - - assert!(!installed()); - install().unwrap(); - assert!(installed()); - let path = scratch.join("skills").join(SKILL_DIR).join("SKILL.md"); - assert_eq!(std::fs::read_to_string(&path).unwrap(), SKILL); - - // Re-install is the refresh path: same content, no error. - install().unwrap(); - assert!(installed()); - - uninstall().unwrap(); - assert!(!installed()); - assert!(!path.exists()); - assert!(!path.parent().unwrap().exists(), "empty skill dir lingers"); - - // A user's own skill under our name must survive an uninstall. - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::fs::write(&path, "---\nname: tty7-orchestration\n---\nmy own\n").unwrap(); - assert!(!installed(), "a foreign file is not a tty7 install"); - assert!(uninstall().is_err()); - assert!(path.exists(), "the user's file was deleted"); - } -} diff --git a/src/ui/app.rs b/src/ui/app.rs index 332d1250..7d79a9c9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3518,8 +3518,6 @@ impl Tty7App { agent_hooks_states: crate::ui::settings::AgentHooksView::Loading, agent_hooks_seq: 0, agent_hooks_note: None, - orchestration_skill: None, - orchestration_skill_note: None, _subs: subs, }); let search_focus = self @@ -4170,7 +4168,6 @@ impl Tty7App { s.recording = None; if target == SettingsSection::Agents { s.agent_hooks_states = crate::ui::settings::AgentHooksView::Loading; - s.orchestration_skill_note = None; } } self.ensure_agent_hooks_loaded(cx); @@ -4183,7 +4180,6 @@ impl Tty7App { .is_some_and(|s| s.section == SettingsSection::Agents) { self.load_agent_hooks_states(cx); - self.load_orchestration_skill_state(cx); } } @@ -4321,60 +4317,6 @@ impl Tty7App { Some((host, Some(home))) } - /// Settings → Agents: the "Orchestration skill" switch — install or - /// remove the Claude Code skill that teaches a primary agent the - /// delegation workflow over the session CLI. - /// - /// Off the UI thread and with the outcome surfaced, like the hook rows: - /// the one error a user actually hits — uninstall refusing a file tty7 - /// did not write — is exactly the case where a switch that silently - /// springs back explains nothing. - pub(crate) fn set_orchestration_skill(&mut self, on: bool, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let result = cx - .background_spawn(async move { - if on { - crate::core::orchestration_skill::install() - } else { - crate::core::orchestration_skill::uninstall() - } - }) - .await; - let note = match result { - Ok(summary) => summary, - Err(e) => { - log::warn!("orchestration-skill change failed: {e}"); - format!("Failed: {e}") - } - }; - let _ = this.update(cx, |this, cx| { - if let Some(s) = this.settings.as_mut() { - s.orchestration_skill_note = Some(note); - } - this.load_orchestration_skill_state(cx); - }); - }) - .detach(); - } - - /// Re-read the skill's presence from disk into `SettingsState`. The file - /// is the truth — an edit made outside this panel shows up here — but it - /// is read on open and after a change, never once per rendered frame. - fn load_orchestration_skill_state(&mut self, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let installed = cx - .background_spawn(async { crate::core::orchestration_skill::installed() }) - .await; - let _ = this.update(cx, |this, cx| { - if let Some(s) = this.settings.as_mut() { - s.orchestration_skill = Some(installed); - } - cx.notify(); - }); - }) - .detach(); - } - pub(crate) fn settings_install_agent_hooks( &mut self, agent: crate::core::agent_hooks::HookAgent, diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 48016acf..0bbb185d 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -434,10 +434,6 @@ pub(crate) struct SettingsState { pub(crate) agent_hooks_states: AgentHooksView, pub(crate) agent_hooks_seq: u64, pub(crate) agent_hooks_note: Option<(crate::core::agent_hooks::HookAgent, String)>, - /// Whether the orchestration skill is on disk. `None` until the first - /// read lands — cached rather than probed per frame, like the hook rows. - pub(crate) orchestration_skill: Option, - pub(crate) orchestration_skill_note: Option, pub(crate) _subs: Vec, } @@ -3563,14 +3559,6 @@ impl Tty7App { ), None => (AgentHooksView::Loading, None, HostId::LOCAL), }; - let (skill_on, skill_note) = match self.active_settings() { - Some(s) => ( - s.orchestration_skill.unwrap_or(false), - s.orchestration_skill_note.clone(), - ), - None => (false, None), - }; - let mut page = v_flex().child(self.section_intro( "Agents", "Hook integrations give panes running these agents live session status \ @@ -3578,44 +3566,6 @@ impl Tty7App { cx, )); - // The orchestration skill (see `core::orchestration_skill`): a Claude - // Code skill a primary agent invokes explicitly to delegate work to - // worker panes over the session CLI. The filesystem stays the truth — - // an edit made outside this panel shows up here — but it is read into - // `SettingsState` when the page opens, not probed once per frame. - // Above the machine picker: the skill lives on this machine's disk - // regardless of which host's hooks are managed below. - let skill_switch = - v_flex() - .items_end() - .gap_1() - .child( - crate::ui::theme::switch("orchestration-skill", cx) - .checked(skill_on) - .on_click(cx.listener(|this, on: &bool, _w, cx| { - this.set_orchestration_skill(*on, cx) - })), - ) - .when_some(skill_note, |col, text| { - col.child( - div() - .max_w_80() - .text_xs() - .text_right() - .text_color(muted_fg) - .child(text), - ) - }) - .into_any_element(); - page = page.child(self.settings_row( - "Orchestration skill", - "Install a Claude Code skill (~/.claude/skills/tty7-orchestration) that teaches a \ - primary agent to delegate work to worker panes over the `tty7` CLI — spawn, send, \ - wait, capture — invoked explicitly, never injected globally", - skill_switch, - cx, - )); - page = page.children(self.agent_hooks_machine_picker(selected_host, cx)); match view {