Remove orchestration skill setting

This commit is contained in:
l0ng-ai
2026-08-02 13:12:09 +08:00
parent 0a3accd10f
commit da6df709cd
7 changed files with 14 additions and 341 deletions
+6
View File
@@ -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 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). interface is documented in [skills/tty7/SKILL.md](skills/tty7/SKILL.md).
Install the skill with:
```sh
npx skills add l0ng-ai/tty7
```
## Benchmarks ## Benchmarks
Same machine, same day, same 155×40 grid — Apple M1 Pro, macOS 26.3.1, Same machine, same day, same 155×40 grid — Apple M1 Pro, macOS 26.3.1,
+6
View File
@@ -53,6 +53,12 @@
终端和快捷键参考:[docs/features.zh-CN.md](docs/features.zh-CN.md)。面向 agent 的 CLI 接口见 终端和快捷键参考:[docs/features.zh-CN.md](docs/features.zh-CN.md)。面向 agent 的 CLI 接口见
[skills/tty7/SKILL.md](skills/tty7/SKILL.md)。 [skills/tty7/SKILL.md](skills/tty7/SKILL.md)。
通过以下命令安装 skill
```sh
npx skills add l0ng-ai/tty7
```
## 基准测试 ## 基准测试
同一台机器、同一天、统一 155×40 网格 —— Apple M1 PromacOS 26.3.1 同一台机器、同一天、统一 155×40 网格 —— Apple M1 PromacOS 26.3.1
+2 -1
View File
@@ -1,6 +1,7 @@
--- ---
name: tty7 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 # Driving tty7 from the command line
-1
View File
@@ -5,7 +5,6 @@ pub mod agent_prompt;
pub mod cli_install; pub mod cli_install;
pub mod config; pub mod config;
pub mod keychain; pub mod keychain;
pub mod orchestration_skill;
pub mod session; pub mod session;
pub mod ssh_config; pub mod ssh_config;
pub mod update; pub mod update;
-231
View File
@@ -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 = "<!-- managed by tty7 (Settings → Agents); edits are overwritten -->";
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.
---
<!-- managed by tty7 (Settings → Agents); edits are overwritten -->
# 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 12, 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<PathBuf> {
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<PathBuf> {
#[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<String> {
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<String> {
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 34 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");
}
}
-58
View File
@@ -3518,8 +3518,6 @@ impl Tty7App {
agent_hooks_states: crate::ui::settings::AgentHooksView::Loading, agent_hooks_states: crate::ui::settings::AgentHooksView::Loading,
agent_hooks_seq: 0, agent_hooks_seq: 0,
agent_hooks_note: None, agent_hooks_note: None,
orchestration_skill: None,
orchestration_skill_note: None,
_subs: subs, _subs: subs,
}); });
let search_focus = self let search_focus = self
@@ -4170,7 +4168,6 @@ impl Tty7App {
s.recording = None; s.recording = None;
if target == SettingsSection::Agents { if target == SettingsSection::Agents {
s.agent_hooks_states = crate::ui::settings::AgentHooksView::Loading; s.agent_hooks_states = crate::ui::settings::AgentHooksView::Loading;
s.orchestration_skill_note = None;
} }
} }
self.ensure_agent_hooks_loaded(cx); self.ensure_agent_hooks_loaded(cx);
@@ -4183,7 +4180,6 @@ impl Tty7App {
.is_some_and(|s| s.section == SettingsSection::Agents) .is_some_and(|s| s.section == SettingsSection::Agents)
{ {
self.load_agent_hooks_states(cx); self.load_agent_hooks_states(cx);
self.load_orchestration_skill_state(cx);
} }
} }
@@ -4321,60 +4317,6 @@ impl Tty7App {
Some((host, Some(home))) 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<Self>) {
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<Self>) {
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( pub(crate) fn settings_install_agent_hooks(
&mut self, &mut self,
agent: crate::core::agent_hooks::HookAgent, agent: crate::core::agent_hooks::HookAgent,
-50
View File
@@ -434,10 +434,6 @@ pub(crate) struct SettingsState {
pub(crate) agent_hooks_states: AgentHooksView, pub(crate) agent_hooks_states: AgentHooksView,
pub(crate) agent_hooks_seq: u64, pub(crate) agent_hooks_seq: u64,
pub(crate) agent_hooks_note: Option<(crate::core::agent_hooks::HookAgent, String)>, 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<bool>,
pub(crate) orchestration_skill_note: Option<String>,
pub(crate) _subs: Vec<Subscription>, pub(crate) _subs: Vec<Subscription>,
} }
@@ -3563,14 +3559,6 @@ impl Tty7App {
), ),
None => (AgentHooksView::Loading, None, HostId::LOCAL), 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( let mut page = v_flex().child(self.section_intro(
"Agents", "Agents",
"Hook integrations give panes running these agents live session status \ "Hook integrations give panes running these agents live session status \
@@ -3578,44 +3566,6 @@ impl Tty7App {
cx, 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)); page = page.children(self.agent_hooks_machine_picker(selected_host, cx));
match view { match view {