diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a54dde13..0fa10bef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ Claude Code is the first host after GitHub Copilot where Squad uses the host's o - `squad doctor` flags machine-specific launch commands and a missing launcher. Run `squad upgrade` once to fix an existing repository. - **Squad's own tools no longer prompt in Claude Code.** In a demo dry run, a fresh `squad init --client claude` project hit 13 permission prompts while casting the team, and the coordinator ended up hand-writing charters and asking the user to run `squad cast` themselves. `squad init` and `squad upgrade` now add a scoped allowlist to `.claude/settings.json`: the `squad_state` tools the coordinator uses, by name (never the experimental backend's approval tools), and `squad cast`, `squad client sync`, `squad lockout reject` and `squad lockout status` (the steps that release a lockout, `assign`, `submit`, `approve` and `override`, still ask), as `squad …` and, when `.mcp.json` runs Squad's own launcher, as `node .squad/squad.mjs …`. Nothing else is allowed, and `squad cast` now refuses to write through a symbolic link. Your own rules come first and are never touched; Squad records the rules it added in `.claude/.squad-permissions.json`, and rollback removes only those, and `"claudePermissions": "off"` in `.squad/config.json` turns it off. Claude Code applies project allow rules only after the folder is trusted (the trust prompt on your first interactive `claude` run there). `squad init` now says so, and if a cast is still denied the coordinator stops and asks instead of retrying variants or hand-writing the team. - **`squad_decide` says where a git-branch backend stored the decision.** In a live run on the `orphan` backend, the coordinator told the user the decision was in `.squad/decisions/inbox/…`, but it was only on the `squad-state` branch. The tool result now says the path is a key on that branch, so the coordinator reports it correctly. +- **Claude Code gets only its own Init Mode skill.** The Claude projection and plugin no longer include Copilot's template `init-mode` and `coordinator-init-mode` skills. Those skills wait for a confirmation through an `ask_user` tool Claude Code doesn't have and write the team by hand, which contradicts Claude's coordinator. Claude Code picks skills by their descriptions, so a request like "set up my team" could have loaded one of them. The coordinator uses `squad-coordinator-init-mode`, which is the only one live runs loaded. A Squad-generated copy left in `.claude/skills/` is removed on the next `squad upgrade`. Copilot is unchanged. - **The reviewer gate runs on its own and means it.** In the same dry run, the coordinator declared a bug fix done without the lead's review even though `routing.md` named the lead as the approval gate; when asked, the lead listed six known gaps against the issue and still approved; and a reviewer's own fix went unreviewed. The non-Copilot coordinators now send every code or test change to the reviewer before calling it done, unasked; the reviewer is never an author, judges against the issue and the team's recorded decisions (a known gap against either is a REJECT, not an advisory), and code a reviewer writes needs a different reviewer. The coordinator also names a recorded decision when it shapes the work ("Per the team decision of : …"). - **Coordinators wait for a human's sign-off.** When `team.md` or `routing.md` says a human must sign off on work (a release, tag, publish, merge or deploy), the Claude, Kiro, OpenCode, Cursor, Codex and Antigravity coordinators no longer treat the request itself as the sign-off. They name the rule and the human, ask whether you are that human or have their sign-off, go ahead only on an explicit yes, and record it in the decisions inbox. In a non-interactive run they stop at the gate. Seen in a live test: asked to "cut the 1.0.0 release", the Claude coordinator used to bump, tag and run `npm publish` itself. - `squad_state` works when the team folder is reached through a symlink, such as macOS `/tmp` (really `/private/tmp`) or a linked team root. Squad stores the folder's resolved path but was given paths in the original spelling, so it refused reads and writes there with "Path traversal blocked". Paths that really leave the team folder, including through a symlink inside it, are still refused. diff --git a/packages/squad-sdk/src/client/adapters/claude-plugin.ts b/packages/squad-sdk/src/client/adapters/claude-plugin.ts index 6d17c3371..a0cb7beec 100644 --- a/packages/squad-sdk/src/client/adapters/claude-plugin.ts +++ b/packages/squad-sdk/src/client/adapters/claude-plugin.ts @@ -17,6 +17,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { buildCoordinatorInstructions, claudeCoordinatorSkills } from './coordinator.js'; import { CLAUDE_HOOK_EVENTS, claudeHookMatcher, type ClaudeHookEvent } from './claude-hooks.js'; +import { CLAUDE_SKIPPED_TEMPLATE_SKILLS } from './claude-skills.js'; export { SQUAD_REPO_LAUNCHER_BOOTSTRAP, SQUAD_REPO_LAUNCHER_PATH, squadRepoLauncherArgs, squadRepoLauncherScript } from './repo-launcher.js'; /** Plugin name: the namespace of its agents, skills, commands and MCP server. */ @@ -588,7 +589,7 @@ export function buildClaudePlugin(opts: BuildClaudePluginOptions): ClaudePluginF } const templates = fs .readdirSync(opts.templateSkillsDir, { withFileTypes: true }) - .filter((e) => e.isDirectory() && NAME_RE.test(e.name) && !skillNames.has(e.name)) + .filter((e) => e.isDirectory() && NAME_RE.test(e.name) && !skillNames.has(e.name) && !CLAUDE_SKIPPED_TEMPLATE_SKILLS.has(e.name)) .map((e) => e.name) .sort(); for (const name of templates) { diff --git a/packages/squad-sdk/src/client/adapters/claude-skills.ts b/packages/squad-sdk/src/client/adapters/claude-skills.ts index 1951dea05..c1eeaa96f 100644 --- a/packages/squad-sdk/src/client/adapters/claude-skills.ts +++ b/packages/squad-sdk/src/client/adapters/claude-skills.ts @@ -19,6 +19,14 @@ export const CLAUDE_SKILLS_DIR = '.claude/skills'; const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +/** + * Template skills never projected for Claude Code. They are Copilot's Init Mode: wait for a + * confirmation through an `ask_user` tool Claude Code lacks, and write the team by hand. Claude's + * coordinator uses `squad-coordinator-init-mode` instead (auto-confirm, `squad cast`, stop on a + * denied cast), and a request like "set up my team" could otherwise load the wrong one. + */ +export const CLAUDE_SKIPPED_TEMPLATE_SKILLS: ReadonlySet = new Set(['init-mode', 'coordinator-init-mode']); + function dirs(abs: string): string[] { try { return fs @@ -64,7 +72,7 @@ export function projectClaudeSkills( const teamSkills = new Set(dirs(path.join(squadAbs, 'skills'))); const templatesAbs = path.join(squadAbs, 'templates', 'skills'); for (const name of opts.plugin ? [] : dirs(templatesAbs)) { - if (teamSkills.has(name) || current.has(name)) continue; + if (teamSkills.has(name) || current.has(name) || CLAUDE_SKIPPED_TEMPLATE_SKILLS.has(name)) continue; let skill: string; try { // A SKILL.md (or directory) linked outside the team directory is never copied into .claude/. diff --git a/plugins/squad/skills/coordinator-init-mode/SKILL.md b/plugins/squad/skills/coordinator-init-mode/SKILL.md deleted file mode 100644 index 42e2110cc..000000000 --- a/plugins/squad/skills/coordinator-init-mode/SKILL.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -name: "coordinator-init-mode" -description: "The complete two-phase Init Mode protocol the Squad coordinator runs when no team exists yet in the current repo. Phase 1 = propose the team (no files created, wait for user confirm). Phase 2 = create .squad/ scaffolding, casting state, .gitattributes for merge drivers, and the always-on built-ins (Scribe, Ralph, Rai, Fact Checker). Loaded on demand when the coordinator detects no .squad/team.md exists." -allowedTools: [] -confidence: high -domain: squad-internals -source: "Extracted from squad.agent.md as part of the slimming effort (bradygaster/squad#1308). Behaviour unchanged — coordinator loads this satellite skill when init mode is detected (no .squad/team.md present)." ---- - - -> **Load this skill when:** you detect that no `.squad/team.md` exists in the resolved team root — that means this is a fresh repo or a repo that has never been squadified, and Init Mode is the right path. The short stub in `squad.agent.md` tells you to load this skill; the full two-phase protocol lives here. -> -> **⚠️ Eager-execution exception:** Init Mode is the ONE exception to the eager-execution / parallel-fan-out doctrine. Phase 1 MUST end with a user confirmation before any file is created. Do not bypass. - -## Phase 1: Propose the Team - -No team exists yet. **Propose one — but DO NOT create any files until the user confirms.** - -1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey {user}, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email`** — email addresses are PII and must not be written to committed files. -2. Ask: *"What are you building? (language, stack, what it does)"* -3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see the canonical Casting reference at `.squad/templates/casting-reference.md`): - - Determine team size: pick **4–5 cast (user-domain) agents**, then add the **4 always-on built-ins** (Scribe + Ralph + Rai + Fact Checker — see their dedicated sections in `squad.agent.md`). A typical fresh squad has **8–9 total roster entries**, not 4–5. - - Determine assignment shape from the user's project description. - - Derive resonance signals from the session and repo context. - - Select a universe. Allocate character names from that universe. - - Scribe is always "Scribe" — exempt from casting. - - Ralph is always "Ralph" — exempt from casting. - - Rai is always "Rai" — exempt from casting. - - Fact Checker is always "Fact Checker" — exempt from casting (same pattern as Scribe / Ralph / Rai). -4. Propose the team with their cast names. Example (names will vary per cast): - -``` -🏗️ {CastName1} — Lead Scope, decisions, code review -⚛️ {CastName2} — Frontend Dev React, UI, components -🔧 {CastName3} — Backend Dev APIs, database, services -🧪 {CastName4} — Tester Tests, quality, edge cases -📋 Scribe — (silent) Memory, decisions, session logs -🔄 Ralph — (monitor) Work queue, backlog, keep-alive -🛡️ Rai — (background) RAI awareness, content safety -🔍 Fact Checker — (verifier) Verification + Devil's Advocate -``` - -5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu. - - The `question` must be **self-contained** because the client may render the blocking input request without the assistant text that preceded it. - - The `question` must reuse every roster line from the exact proposal emitted in step 4, in the same order. Do not regenerate, summarize, truncate, omit, or replace any roster row with an ellipsis. Every proposed member must appear with their emoji, cast name, role, and concise responsibility/scope — including all four always-on built-ins (Scribe, Ralph, Rai, Fact Checker). - - Shape: - - ``` - Confirm this team (universe: {ActualUniverse}): - - {Every roster line from step 4, copied in full and in order} - - Look right? - ``` - - Never use a bare "Look right?" question and never rely on preceding assistant output to identify the team being confirmed. - - - **choices:** `["Yes, hire this team", "Add someone", "Change a role"]` - -**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** - ---- - -## Phase 2: Create the Team - -**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). - -> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. **Do NOT enter Phase 2 until the user confirms.** - -6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: `team.md`, `routing.md`, `ceremonies.md`, `decisions.md`, `decisions/inbox/`, `casting/`, `agents/`, `orchestration-log/`, `skills/`, `log/`, `rai/`). - -**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). - -**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. Rai's charter is seeded from the `Rai-charter.md` template, and `.squad/rai/policy.md` is seeded from `rai-policy.md`. Fact Checker's charter is seeded from `fact-checker-charter.md` and `.squad/fact-checker/policy.md` is seeded from `fact-checker-policy.md`. - -**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. - -**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: - -``` -.squad/decisions.md merge=union -.squad/agents/*/history.md merge=union -.squad/log/** merge=union -.squad/orchestration-log/** merge=union -.squad/rai/audit-trail.md merge=union -``` - -The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. - -7. Say: *"✅ Team hired. Try: '{FirstCastName}, set up the project structure'"* - -8. **Post-setup input sources** (optional — ask after team is created, not during casting): - - **PRD/spec:** *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow. - - **GitHub issues:** *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow. - - **Human members:** *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section. - - **Copilot agent:** *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment. - - These are additive. **Don't block** — if the user skips or gives a task instead, proceed immediately. diff --git a/plugins/squad/skills/init-mode/SKILL.md b/plugins/squad/skills/init-mode/SKILL.md deleted file mode 100644 index 5a51b28b2..000000000 --- a/plugins/squad/skills/init-mode/SKILL.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -name: "init-mode" -description: "Team initialization flow (Phase 1 proposal + Phase 2 creation)" -domain: "orchestration" -confidence: "high" -source: "extracted" -tools: - - name: "ask_user" - description: "Confirm team roster with selectable menu" - when: "Phase 1 proposal — requires explicit user confirmation" ---- - - -## Context - -Init Mode activates when `.squad/team.md` does not exist, or exists but has zero roster entries under `## Members`. The coordinator proposes a team (Phase 1), waits for user confirmation, then creates the team structure (Phase 2). - -## Patterns - -### Phase 1: Propose the Team - -No team exists yet. Propose one — but **DO NOT create any files until the user confirms.** - -1. **Identify the user.** Run `git config user.name` to learn who you're working with. Use their name in conversation (e.g., *"Hey {user}, what are you building?"*). Store their name (NOT email) in `team.md` under Project Context. **Never read or store `git config user.email` — email addresses are PII and must not be written to committed files.** -2. Ask: *"What are you building? (language, stack, what it does)"* -3. **Cast the team.** Before proposing names, run the Casting & Persistent Naming algorithm (see that section): - - Determine team size (typically 4–5 + Scribe). - - **Default to descriptive names** (Lead, Frontend, Backend, Tester, etc.) unless the user requests a themed universe. - - If the user requests a specific universe (built-in or custom), allocate character names from that universe. For custom universes not in the allowlist, use your knowledge of the source material and apply spoiler-safety rules. - - If the user asks for themed names without specifying a universe, auto-select from built-in universes using the scoring algorithm (size_fit + shape_fit + resonance_fit + LRU). - - Scribe is always "Scribe" — exempt from casting. - - Ralph is always "Ralph" — exempt from casting. -4. Propose the team with their cast names. Example (names will vary per cast): - -``` -🏗️ Lead — Lead Scope, decisions, code review -⚛️ Frontend — Frontend Dev React, UI, components -🔧 Backend — Backend Dev APIs, database, services -🧪 Tester — Tester Tests, quality, edge cases -📋 Scribe — (silent) Memory, decisions, session logs -🔄 Ralph — (monitor) Work queue, backlog, keep-alive -``` - -5. Use the `ask_user` tool to confirm the roster. Provide choices so the user sees a selectable menu: - - **question:** *"Look right?"* - - **choices:** `["Yes, cast this team", "Add someone", "Change a role"]` - -**⚠️ STOP. Your response ENDS here. Do NOT proceed to Phase 2. Do NOT create any files or directories. Wait for the user's reply.** - -### Phase 2: Create the Team - -**Trigger:** The user replied to Phase 1 with confirmation ("yes", "looks good", or similar affirmative), OR the user's reply to Phase 1 is a task (treat as implicit "yes"). - -> If the user said "add someone" or "change a role," go back to Phase 1 step 3 and re-propose. Do NOT enter Phase 2 until the user confirms. - -6. Create the `.squad/` directory structure (see `.squad/templates/` for format guides or use the standard structure: team.md, routing.md, ceremonies.md, decisions.md, decisions/inbox/, casting/, agents/, orchestration-log/, skills/, log/). - -**Casting state initialization:** Copy `.squad/templates/casting-policy.json` to `.squad/casting/policy.json` (or create from defaults). Create `registry.json` (entries: persistent_name, universe, created_at, legacy_named: false, status: "active") and `history.json` (first assignment snapshot with unique assignment_id). - -**Seeding:** Each agent's `history.md` starts with the project description, tech stack, and the user's name so they have day-1 context. Agent folder names are the cast name in lowercase (e.g., `.squad/agents/ripley/`). The Scribe's charter includes maintaining `decisions.md` and cross-agent context sharing. - -**Team.md structure:** `team.md` MUST contain a section titled exactly `## Members` (not "## Team Roster" or other variations) containing the roster table. This header is hard-coded in GitHub workflows (`squad-heartbeat.yml`, `squad-issue-assign.yml`, `squad-triage.yml`, `sync-squad-labels.yml`) for label automation. If the header is missing or titled differently, label routing breaks. - -**Merge driver for append-only files:** Create or update `.gitattributes` at the repo root to enable conflict-free merging of `.squad/` state across branches: -``` -.squad/decisions.md merge=union -.squad/agents/*/history.md merge=union -.squad/log/** merge=union -.squad/orchestration-log/** merge=union -``` -The `union` merge driver keeps all lines from both sides, which is correct for append-only files. This makes worktree-local strategy work seamlessly when branches merge — decisions, memories, and logs from all branches combine automatically. - -7. Say: *"✅ Team cast. Try: '{FirstCastName}, set up the project structure'"* - -8. **Post-setup input sources** (optional — ask after team is created, not during casting): - - PRD/spec: *"Do you have a PRD or spec document? (file path, paste it, or skip)"* → If provided, follow PRD Mode flow - - GitHub issues: *"Is there a GitHub repo with issues I should pull from? (owner/repo, or skip)"* → If provided, follow GitHub Issues Mode flow - - Human members: *"Are any humans joining the team? (names and roles, or just AI for now)"* → If provided, add per Human Team Members section - - Copilot agent: *"Want to include @copilot? It can pick up issues autonomously. (yes/no)"* → If yes, follow Copilot Coding Agent Member section and ask about auto-assignment - - These are additive. Don't block — if the user skips or gives a task instead, proceed immediately. - -## Examples - -**Example flow:** -1. Coordinator detects no team.md → Init Mode -2. Runs `git config user.name` → "{user}" -3. Asks: *"Hey {user}, what are you building?"* -4. User: *"TypeScript CLI tool with GitHub API integration"* -5. Coordinator uses descriptive naming by default -6. Proposes: Lead (Lead), Frontend (Frontend Dev), Backend (Backend Dev), Tester (Tester), Scribe, Ralph -7. Uses `ask_user` with choices → user selects "Yes, cast this team" -8. Coordinator creates `.squad/` structure, initializes casting state, seeds agents -9. Says: *"✅ Team cast. Try: 'Lead, set up the project structure'"* - -## Anti-Patterns - -- ❌ Creating files before user confirms Phase 1 -- ❌ Mixing agents from different universes in the same cast -- ❌ Skipping the `ask_user` tool and assuming confirmation -- ❌ Proceeding to Phase 2 when user said "add someone" or "change a role" -- ❌ Using `## Team Roster` instead of `## Members` as the header (breaks GitHub workflows) -- ❌ Forgetting to initialize `.squad/casting/` state files -- ❌ Reading or storing `git config user.email` (PII violation) -- ❌ Rejecting a user's universe choice because it is not in the built-in allowlist -- ❌ Auto-selecting a themed universe when the user didn't ask for one (use descriptive names by default) diff --git a/test/claude-skills.test.ts b/test/claude-skills.test.ts index 1e0f244f7..cd7e6f9fe 100644 --- a/test/claude-skills.test.ts +++ b/test/claude-skills.test.ts @@ -120,3 +120,32 @@ describe('Claude skills projection', () => { expect(fs.existsSync(path.join(root, '.claude'))).toBe(false); }); }); + +describe("Copilot's template Init Mode skills are not projected for Claude Code", () => { + it('neither project mode nor the plugin carries init-mode or coordinator-init-mode; squad-coordinator-init-mode stays', async () => { + const { CLAUDE_SKIPPED_TEMPLATE_SKILLS } = await import('../packages/squad-sdk/src/client/adapters/claude-skills.js'); + expect([...CLAUDE_SKIPPED_TEMPLATE_SKILLS].sort()).toEqual(['coordinator-init-mode', 'init-mode']); + const pluginSkills = fs.readdirSync(path.resolve('plugins', 'squad', 'skills')).filter((d) => fs.existsSync(path.resolve('plugins', 'squad', 'skills', d, 'SKILL.md'))); + expect(pluginSkills).toContain('squad-coordinator-init-mode'); + expect(pluginSkills).not.toContain('init-mode'); + expect(pluginSkills).not.toContain('coordinator-init-mode'); + + // Project mode: the templates are present in .squad/templates/skills but are not projected. + const { projectClaudeSkills } = await import('../packages/squad-sdk/src/client/adapters/claude-skills.js'); + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'squad-claude-init-skills-')); + try { + for (const name of ['init-mode', 'coordinator-init-mode', 'git-workflow']) { + fs.mkdirSync(path.join(root, '.squad', 'templates', 'skills', name), { recursive: true }); + fs.writeFileSync(path.join(root, '.squad', 'templates', 'skills', name, 'SKILL.md'), `---\nname: ${name}\ndescription: x\n---\nBody.\n`); + } + const paths = projectClaudeSkills({ root, mcp: { command: 'squad', args: ['state-mcp'] } }, '', 'gen') + .filter((f) => f.kind === 'managed').map((f) => f.path); + expect(paths).toContain('.claude/skills/git-workflow/SKILL.md'); + expect(paths).toContain('.claude/skills/squad-coordinator-init-mode/SKILL.md'); + expect(paths).not.toContain('.claude/skills/init-mode/SKILL.md'); + expect(paths).not.toContain('.claude/skills/coordinator-init-mode/SKILL.md'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); +});