diff --git a/README.md b/README.md index 2190b61..02a08d3 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Multiple agents with the same role get unique IDs automatically (`worker`, `work | Command | Description | |---------|-------------| | `squad init [--refresh-roles]` | Initialize workspace, create `.squad/`, add `.squad/` to `.gitignore`, and append squad guidance to `CLAUDE.md`, `AGENTS.md`, and `GEMINI.md` if missing. `--refresh-roles` rewrites only builtin `manager`/`worker`/`inspector` files under `.squad/roles/`. | -| `squad join [--role ] [--client ] [--protocol-version ]` | Join as agent (auto-suffixes if ID is taken; omitted capability metadata stays `NULL`) | +| `squad join [--role ] [--client ] [--protocol-version ]` | Join as agent (auto-suffixes if ID is taken; omitted capability metadata stays `NULL`) | | `squad leave ` | Archive agent and preserve unread work | | `squad agents [--all] [--json]` | List online agents (`--json` emits one JSON object per line including raw/effective capability fields and protocol-derived support booleans) | | `squad send [--task-id ] [--reply-to ] ` | Send a note (`@all` to broadcast, or `squad send [flags] --file ` to read from file/stdin) | @@ -152,6 +152,7 @@ Install the `/squad` slash command for your AI tools: ```bash squad setup # auto-detect and install for all found tools squad setup claude # install only for Claude Code +squad setup agy # install only for Antigravity squad setup --list # show supported platforms ``` @@ -163,6 +164,7 @@ Supported platforms: | Gemini CLI | `gemini` | `~/.gemini/commands/squad.toml` | | Codex CLI | `codex` | `~/.codex/skills/squad/SKILL.md` | | OpenCode | `opencode` | `~/.config/opencode/commands/squad.md` | +| Antigravity | `agy` | `~/.gemini/config/skills/squad/SKILL.md` | Once installed, use `/squad ` (or `$squad ` in Codex) in any project where `squad init` has been run. Generated slash templates automatically join with their platform client type and the current supported protocol version. diff --git a/README.zh-CN.md b/README.zh-CN.md index 273f0d6..5cde54c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -128,7 +128,7 @@ scripts/squad-tmux-launch.sh /path/to/project --dry-run | 命令 | 说明 | |------|------| | `squad init [--refresh-roles]` | 初始化工作区,创建 `.squad/`,将 `.squad/` 加入 `.gitignore`,并在缺失时向 `CLAUDE.md`、`AGENTS.md`、`GEMINI.md` 追加 squad 说明。`--refresh-roles` 只会重写 `.squad/roles/` 下内置的 `manager`/`worker`/`inspector` 文件。 | -| `squad join [--role ] [--client ] [--protocol-version ]` | 以 Agent 身份加入(ID 冲突时自动添加后缀;省略能力元数据时数据库存 `NULL`) | +| `squad join [--role ] [--client ] [--protocol-version ]` | 以 Agent 身份加入(ID 冲突时自动添加后缀;省略能力元数据时数据库存 `NULL`) | | `squad leave ` | 归档 Agent,并保留未读工作 | | `squad agents [--all] [--json]` | 列出在线 Agent(`--json` 每行输出一个 JSON 对象,包含原始/生效能力字段和基于协议版本推导的支持布尔值) | | `squad send [--task-id ] [--reply-to ] ` | 发送普通消息(`@all` 广播给所有人,或用 `squad send [flags] --file ` 从文件/标准输入读取内容) | @@ -152,6 +152,7 @@ scripts/squad-tmux-launch.sh /path/to/project --dry-run ```bash squad setup # 自动检测已安装的工具并安装 squad setup claude # 只安装到 Claude Code +squad setup agy # 只安装到 Antigravity squad setup --list # 查看支持的平台 ``` @@ -161,6 +162,7 @@ squad setup --list # 查看支持的平台 | Gemini CLI | `gemini` | `~/.gemini/commands/squad.toml` | | Codex CLI | `codex` | `~/.codex/skills/squad/SKILL.md` | | OpenCode | `opencode` | `~/.config/opencode/commands/squad.md` | +| Antigravity | `agy` | `~/.gemini/config/skills/squad/SKILL.md` | 安装后,在任何执行过 `squad init` 的项目中使用 `/squad <角色>`(Codex 中使用 `$squad <角色>`)即可。生成的 slash 模板会自动带上所属平台的 `client` 值和当前支持的协议版本。 diff --git a/src/main.rs b/src/main.rs index 75d082e..4e18ad2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ fn main() -> Result<()> { "join" => { let id = args.next().unwrap_or_default(); if id.is_empty() { - bail!("Usage: squad join [--role ] [--client ] [--protocol-version ]"); + bail!("Usage: squad join [--role ] [--client ] [--protocol-version ]"); } let options = parse_join_args(&id, args.collect())?; cmd_join(&id, &options) @@ -155,9 +155,10 @@ fn parse_join_args(id: &str, args: Vec) -> Result { bail!("--client requires a value"); } match value.as_str() { - "claude" | "gemini" | "codex" | "opencode" => { + "claude" | "gemini" | "codex" | "opencode" | "agy" => { options.client_type = Some(value.clone()) } + "antigravity" => options.client_type = Some("agy".to_string()), _ => bail!("invalid --client value: {value}"), } i += 2; @@ -1180,10 +1181,13 @@ fn cmd_doctor() -> Result<()> { let store = open_store(&workspace)?; // 1. Template diagnostics - let home = PathBuf::from(std::env::var("HOME").context("HOME not set")?); + let home = std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) + .context("HOME or USERPROFILE not set")?; let installed_platforms: Vec<&squad::setup::Platform> = squad::setup::PLATFORMS .iter() - .filter(|p| squad::setup::is_installed(p.binary)) + .filter(|p| squad::setup::is_platform_installed(p)) .collect(); let template_diags = squad::setup::diagnose_templates_for_platforms(&installed_platforms, &home)?; @@ -1229,7 +1233,7 @@ fn cmd_setup(target: Option<&str>) -> Result<()> { Some("--list") => { println!("Supported platforms:"); for p in squad::setup::PLATFORMS { - let status = if squad::setup::is_installed(p.binary) { + let status = if squad::setup::is_platform_installed(p) { "installed" } else { "not found" @@ -1241,7 +1245,7 @@ fn cmd_setup(target: Option<&str>) -> Result<()> { Some(name) => { let platform = squad::setup::PLATFORMS .iter() - .find(|p| p.name == name) + .find(|p| p.name == name || (p.name == "agy" && name == "antigravity")) .with_context(|| format!("unknown platform: {name}. Run 'squad setup --list'"))?; let path = squad::setup::install_for_platform(platform)?; println!("Installed squad for {} → {}", name, path.display()); @@ -1252,7 +1256,7 @@ fn cmd_setup(target: Option<&str>) -> Result<()> { let results = squad::setup::run_setup(); if results.is_empty() { println!("No supported AI tools found in PATH."); - println!("Supported: claude, gemini, codex, opencode"); + println!("Supported: claude, gemini, codex, opencode, agy"); return Ok(()); } for (name, path, result) in &results { @@ -1276,7 +1280,7 @@ const HELP_TEXT: &str = r#"squad — Multi-AI-agent terminal collaboration COMMANDS squad init [--refresh-roles] Initialize workspace (`--refresh-roles` rewrites builtin roles only) - squad join [--role ] [--client ] [--protocol-version ] + squad join [--role ] [--client ] [--protocol-version ] Join as agent (role defaults to id; omitted metadata stays NULL) squad leave Archive agent squad agents [--all] [--json] List online agents (`--all` includes archived agents; `--json` emits one JSON object per line with raw/effective capability fields) diff --git a/src/setup.rs b/src/setup.rs index 8c71e20..1d9b973 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -36,8 +36,80 @@ pub const PLATFORMS: &[Platform] = &[ command_path: ".config/opencode/commands/squad.md", content: SQUAD_MD_CONTENT, }, + Platform { + name: "agy", + binary: "agy", + command_path: ".gemini/config/skills/squad/SKILL.md", + content: SQUAD_AGY_CONTENT, + }, ]; +/// Antigravity Skills format (uses $ARGUMENTS, placed in ~/.gemini/config/skills/squad/SKILL.md) +pub const SQUAD_AGY_CONTENT: &str = r#"--- +name: squad +description: "Join squad multi-agent collaboration. Usage: /squad [custom-id]" +--- + +You are joining a squad multi-agent collaboration team. + +Your join arguments: $ARGUMENTS + +**IMPORTANT:** Do NOT run `squad $ARGUMENTS` or treat the arguments as a CLI subcommand. Instead, follow the setup steps below. + +## Phase 1: Setup (do this once) + +1. Parse your join arguments above. + + **If arguments are empty or missing:** + Run `squad roles` to list available roles, then ask the user which role they want to join as. Do NOT proceed until the user picks a role. + + **If arguments look like a role name** (1-2 words, e.g. "cto", "worker worker-2"): + - First word is your role — this can be ANY string: "cto", "ceo", "manager", "reviewer", etc. It does NOT need to appear in `squad roles` (that list only shows predefined templates). + - Optional second word is a custom agent ID + - If no custom ID provided, use the role name as your ID + - Examples: "manager" → id=manager, role=manager | "worker worker-2" → id=worker-2, role=worker | "cto" → id=cto, role=cto + + **If arguments look like natural language** (e.g. "加入团队,作为管理员", "join as tech lead and review PRs"): + - Extract the intended role from the text. Pick a short English role name (e.g. "manager", "reviewer", "cto"). + - Use that as your role and ID. + - If no role can be inferred, ask the user to clarify. + +2. Run `squad init` (safe to run — won't overwrite existing workspace). + +3. **Clean up stale agents from previous sessions:** + Run `squad agents` and check the output. + - If ALL agents show "stale" (no active agents), tell the user stale squad state was detected and ask the user whether they want to reset squad state with `squad clean` followed by `squad init`. Do NOT clean automatically. + - If some agents are active (a team is already running), skip cleanup and proceed. + +4. Run `squad join --role --client __SQUAD_CLIENT__ --protocol-version __SQUAD_PROTOCOL_VERSION__` to register yourself. + - Read the output line that says "Joined as ..." — that confirms your actual agent ID. + - If the ID was taken, squad auto-assigns a suffixed ID (e.g. worker-2). Use that ID for all commands. + - If role instructions are printed (=== Role Instructions ===), follow them. + - If no predefined template exists, interpret the role using your own knowledge. + +5. Run `squad agents` to see who else is on the team. + +6. **If any squad command returns "Session replaced":** another terminal took your ID. Re-join with a different ID (e.g. `squad join worker-2 --role worker --client __SQUAD_CLIENT__ --protocol-version __SQUAD_PROTOCOL_VERSION__`). + +## Phase 2: Enter Receive Mode (MANDATORY) + +**Immediately after setup, run `squad receive --wait` to start listening for messages.** Do NOT wait for the user to tell you — enter receive mode now. + +After receiving a message: +1. Execute the task or respond as appropriate for your role. +2. Report results using `squad send` or `squad task` commands. +3. Run `squad receive --wait` again to wait for the next message. + +If receive times out with no messages, run it again immediately. + +Other useful commands: +- `squad send ""` — send a message (use @all to broadcast) +- `squad task create --title ""` — create a structured task +- `squad agents` — see who is online +- `squad pending` — check all unread messages +- `squad history` — view message history +"#; + /// Codex Skills format (uses $ARGUMENTS, placed in ~/.agents/skills/squad/SKILL.md) pub const SQUAD_CODEX_CONTENT: &str = r#"--- name: squad @@ -308,7 +380,7 @@ pub fn check_and_update_commands() -> Vec<(String, PathBuf)> { let version = current_version(); let mut updated = Vec::new(); for platform in PLATFORMS { - if !is_installed(platform.binary) { + if !is_platform_installed(platform) { continue; } let path = match command_path(platform) { @@ -442,11 +514,22 @@ fn is_executable(path: &Path) -> bool { path.is_file() } +/// Check if a platform's binary (or supported alias) is installed. +pub fn is_platform_installed(platform: &Platform) -> bool { + if is_installed(platform.binary) { + return true; + } + if platform.name == "agy" && is_installed("antigravity") { + return true; + } + false +} + /// Detect which platforms are installed. pub fn detect_platforms() -> Vec<&'static Platform> { PLATFORMS .iter() - .filter(|p| is_installed(p.binary)) + .filter(|p| is_platform_installed(p)) .collect() } @@ -480,7 +563,7 @@ pub fn install_for_platform(platform: &Platform) -> Result<PathBuf> { pub fn run_setup() -> Vec<(String, PathBuf, Result<()>)> { let mut results = Vec::new(); for platform in PLATFORMS { - if !is_installed(platform.binary) { + if !is_platform_installed(platform) { continue; } match install_for_platform(platform) { diff --git a/tests/cli_test.rs b/tests/cli_test.rs index a7fd74c..dbebfd3 100644 --- a/tests/cli_test.rs +++ b/tests/cli_test.rs @@ -348,6 +348,48 @@ fn test_agents_human_output_shows_capability_metadata_when_present() { .stdout(predicate::str::contains("protocol: 2")); } +#[test] +fn test_join_with_agy_and_antigravity_client() { + let tmp = TempDir::new().unwrap(); + squad(tmp.path()).arg("init").assert().success(); + + squad(tmp.path()) + .args([ + "join", + "agy-agent", + "--role", + "worker", + "--client", + "agy", + "--protocol-version", + "2", + ]) + .assert() + .success(); + + squad(tmp.path()) + .args([ + "join", + "antigravity-agent", + "--role", + "manager", + "--client", + "antigravity", + "--protocol-version", + "2", + ]) + .assert() + .success(); + + squad(tmp.path()) + .arg("agents") + .assert() + .success() + .stdout(predicate::str::contains("agy-agent (role: worker) — active")) + .stdout(predicate::str::contains("client: agy")) + .stdout(predicate::str::contains("antigravity-agent (role: manager) — active")); +} + #[test] fn test_send_and_receive() { let tmp = TempDir::new().unwrap(); @@ -1434,6 +1476,7 @@ fn test_setup_templates_pass_client_and_protocol_metadata_on_join() { let codex = command_content(PLATFORMS.iter().find(|p| p.name == "codex").unwrap()); let opencode = command_content(PLATFORMS.iter().find(|p| p.name == "opencode").unwrap()); let gemini = command_content(PLATFORMS.iter().find(|p| p.name == "gemini").unwrap()); + let agy = command_content(PLATFORMS.iter().find(|p| p.name == "agy").unwrap()); assert!(claude.contains("--client claude")); assert!(claude.contains("--protocol-version 2")); @@ -1441,6 +1484,8 @@ fn test_setup_templates_pass_client_and_protocol_metadata_on_join() { assert!(opencode.contains("--client opencode")); assert!(gemini.contains("--client gemini")); assert!(gemini.contains("--protocol-version 2")); + assert!(agy.contains("--client agy")); + assert!(agy.contains("--protocol-version 2")); } #[test] diff --git a/tests/setup_test.rs b/tests/setup_test.rs index 740e5cf..170988a 100644 --- a/tests/setup_test.rs +++ b/tests/setup_test.rs @@ -1,7 +1,7 @@ use squad::setup::{ command_path, current_version, diagnose_templates_for_platforms, install_command, - install_for_platform, is_installed, PLATFORMS, SQUAD_CODEX_CONTENT, SQUAD_MD_CONTENT, - SQUAD_TOML_CONTENT, + install_for_platform, is_installed, PLATFORMS, SQUAD_AGY_CONTENT, SQUAD_CODEX_CONTENT, + SQUAD_MD_CONTENT, SQUAD_TOML_CONTENT, }; use std::sync::{Mutex, OnceLock}; use tempfile::TempDir; @@ -52,12 +52,13 @@ fn lock_env() -> std::sync::MutexGuard<'static, ()> { #[test] fn test_platforms_defined() { - assert!(PLATFORMS.len() >= 4); + assert!(PLATFORMS.len() >= 5); let names: Vec<&str> = PLATFORMS.iter().map(|p| p.name).collect(); assert!(names.contains(&"claude")); assert!(names.contains(&"gemini")); assert!(names.contains(&"codex")); assert!(names.contains(&"opencode")); + assert!(names.contains(&"agy")); } #[test] @@ -114,6 +115,41 @@ fn test_installed_codex_skill_frontmatter_is_valid_yaml() { assert_eq!(parsed["squad-version"], current_version()); } +#[test] +fn test_agy_skill_frontmatter_is_valid_yaml() { + let frontmatter = SQUAD_AGY_CONTENT + .strip_prefix("---\n") + .and_then(|content| content.split_once("\n---")) + .map(|(frontmatter, _)| frontmatter) + .unwrap(); + + serde_yaml::from_str::<serde_yaml::Value>(frontmatter).unwrap(); +} + +#[test] +fn test_installed_agy_skill_frontmatter_is_valid_yaml() { + let _lock = lock_env(); + let _env = EnvGuard::new(); + let tmp = TempDir::new().unwrap(); + std::env::set_var("HOME", tmp.path()); + + let agy = PLATFORMS + .iter() + .find(|platform| platform.name == "agy") + .unwrap(); + let path = install_for_platform(agy).unwrap(); + let content = std::fs::read_to_string(path).unwrap(); + let frontmatter = content + .strip_prefix("---\n") + .and_then(|content| content.split_once("\n---")) + .map(|(frontmatter, _)| frontmatter) + .unwrap(); + + let parsed = serde_yaml::from_str::<serde_yaml::Value>(frontmatter).unwrap(); + assert_eq!(parsed["name"], "squad"); + assert_eq!(parsed["squad-version"], current_version()); +} + #[test] fn test_command_path_falls_back_to_userprofile_when_home_is_missing() { let _lock = lock_env();