Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/plugin-icon-human-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bradygaster/squad-sdk": patch
"@bradygaster/squad-cli": patch
---

The Claude Code plugin ships an icon, so `claude plugin validate` reports no warnings, and `.squad/templates/human-members.md`, listed in the template index but missing, ships again with the human sign-off gate.
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ scripts/release/manifest.schema.json text eol=lf
# no extension. Pin LF so Windows checkouts neither break it nor drift (#2443).
plugins/** text eol=lf
.claude-plugin/** text eol=lf
# Images are binary, including inside plugins/** above (the plugin icon): never convert line endings.
*.png binary -eol
*.jpg binary -eol
*.gif binary -eol
83 changes: 83 additions & 0 deletions .squad-templates/human-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Human Members

Reference for adding human team members to the Squad roster.

## Triggers

The coordinator enters the human-member flow when:

- User says **"add {name} as {role}"** or **"add a human for {domain}"**
- User says **"make {name} a reviewer"**
- Routing detects a human member as the target for work

## Comparison: AI Agent vs. Human Member

| | AI Agent | Human Member |
|---|----------|-------------|
| Badge | Role-specific emoji | 👤 Human |
| Charter | ✅ | ❌ |
| History | ✅ | ❌ |
| Spawned as sub-agent | ✅ | ❌ |
| Can review work | ✅ | ✅ |
| Casting (fictional name) | ✅ | ❌ (real name) |

## Adding a Human Member

Follow these steps:

1. **Use real name** — no casting. Badge: 👤 Human.
2. **Add to team.md roster:**
```markdown
| {Name} | {Role} | — | 👤 Human |
```
3. **Add routing entries to routing.md:**
```markdown
| {domain} | {Name} 👤 | {example tasks} |
```
4. **Do not create** `charter.md` or `history.md` for humans.
5. **Do not add** to casting registry.

## Routing to a Human

When work routes to a human member, the coordinator pauses. It never does that work itself or hands it to an AI member.

**Sign-off gates.** When `team.md`, `routing.md` or a decision says a human must approve or sign off on a kind of work (a release, version bump, tag, publish, merge or deploy), a request to do that work is not the sign-off. Before any gated step the coordinator names the rule and the human, asks whether the user is that human or has their explicit sign-off, proceeds only on an explicit yes, and records it in the decisions inbox. In a non-interactive run it stops at the gate and reports what is waiting and for whom.


1. **Present the work to the user:**
```
"{Name} needs to review this. [Context about what needs review]"
```
2. **Non-dependent work continues immediately** — other agents proceed with unrelated tasks.
3. **Stale reminder** after >1 turn without response:
```
"📌 Still waiting on {Name} for {thing}."
```

## Reviewing as a Human

- Humans can serve as reviewers in the review process.
- **Rejection lockout applies normally** — original author is locked out when reviewer rejects.
- User relays the human's verdict to the coordinator ("approved" or "rejected with changes").

## Decision Framework: When to Add a Human Member

| Scenario | Add to roster? | Why |
|----------|---------------|-----|
| Approves architecture decisions before implementation | ✅ Yes | Decision gate — team routes and waits |
| Reviews all docs PRs as standing reviewer | ✅ Yes | Recurring review gate |
| Occasionally reviews PRs when tagged | ❌ No | Use @mention on the PR |
| Files issues and contributes code | ❌ No | Normal GitHub collaboration |
| Makes final ship/no-ship call | ✅ Yes | Approval gate |

**Litmus test:** If you want agents to *stop and wait* for someone's input before proceeding on a specific type of work, add them. If they review asynchronously via GitHub, don't bother.

**Who is driving:** `git config user.name` tells the team who is probably at the keyboard. It is a hint, not a sign-off: for a gated step the coordinator still asks. Adding yourself to the roster is optional; it formalizes routing and tracking.

## Removing a Human

Same as removing any member:

1. Move entry to alumni section in team.md.
2. Remove from active roster.
3. Update routing.md to remove all routing entries.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ 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.
- **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.
- The Claude Code plugin has an icon (the Squad logo, 512 px), so `claude plugin validate` reports no warnings. Images are marked binary in `.gitattributes`, so the plugin folder's LF rule never rewrites them.
- `.squad/templates/human-members.md` ships again. The template index listed it, but it had been missing since 2025. It describes adding a human to the roster and now includes the sign-off gate: a request to do gated work is not the sign-off.
- `squad doctor` no longer fails the `squad_state` bridge check when the team folder has two spellings, such as macOS `/var` and `/private/var`. It compares the real paths.
- When `squad_state` is unavailable and `.squad/config.json` sets a non-local `stateBackend`, the non-Copilot coordinators stop and say so instead of writing `.squad/decisions.md` directly, which that backend never reads. Seen in a live test with `two-layer`.
- `squad init --client claude` tells you to run `claude --agent squad --mcp-config .mcp.json`, not Copilot. With several hosts it gives one line per host (#2453).
Expand Down
Binary file added docs/src/assets/images/squad-plugin-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions packages/squad-cli/templates/human-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Human Members

Reference for adding human team members to the Squad roster.

## Triggers

The coordinator enters the human-member flow when:

- User says **"add {name} as {role}"** or **"add a human for {domain}"**
- User says **"make {name} a reviewer"**
- Routing detects a human member as the target for work

## Comparison: AI Agent vs. Human Member

| | AI Agent | Human Member |
|---|----------|-------------|
| Badge | Role-specific emoji | 👤 Human |
| Charter | ✅ | ❌ |
| History | ✅ | ❌ |
| Spawned as sub-agent | ✅ | ❌ |
| Can review work | ✅ | ✅ |
| Casting (fictional name) | ✅ | ❌ (real name) |

## Adding a Human Member

Follow these steps:

1. **Use real name** — no casting. Badge: 👤 Human.
2. **Add to team.md roster:**
```markdown
| {Name} | {Role} | — | 👤 Human |
```
3. **Add routing entries to routing.md:**
```markdown
| {domain} | {Name} 👤 | {example tasks} |
```
4. **Do not create** `charter.md` or `history.md` for humans.
5. **Do not add** to casting registry.

## Routing to a Human

When work routes to a human member, the coordinator pauses. It never does that work itself or hands it to an AI member.

**Sign-off gates.** When `team.md`, `routing.md` or a decision says a human must approve or sign off on a kind of work (a release, version bump, tag, publish, merge or deploy), a request to do that work is not the sign-off. Before any gated step the coordinator names the rule and the human, asks whether the user is that human or has their explicit sign-off, proceeds only on an explicit yes, and records it in the decisions inbox. In a non-interactive run it stops at the gate and reports what is waiting and for whom.


1. **Present the work to the user:**
```
"{Name} needs to review this. [Context about what needs review]"
```
2. **Non-dependent work continues immediately** — other agents proceed with unrelated tasks.
3. **Stale reminder** after >1 turn without response:
```
"📌 Still waiting on {Name} for {thing}."
```

## Reviewing as a Human

- Humans can serve as reviewers in the review process.
- **Rejection lockout applies normally** — original author is locked out when reviewer rejects.
- User relays the human's verdict to the coordinator ("approved" or "rejected with changes").

## Decision Framework: When to Add a Human Member

| Scenario | Add to roster? | Why |
|----------|---------------|-----|
| Approves architecture decisions before implementation | ✅ Yes | Decision gate — team routes and waits |
| Reviews all docs PRs as standing reviewer | ✅ Yes | Recurring review gate |
| Occasionally reviews PRs when tagged | ❌ No | Use @mention on the PR |
| Files issues and contributes code | ❌ No | Normal GitHub collaboration |
| Makes final ship/no-ship call | ✅ Yes | Approval gate |

**Litmus test:** If you want agents to *stop and wait* for someone's input before proceeding on a specific type of work, add them. If they review asynchronously via GitHub, don't bother.

**Who is driving:** `git config user.name` tells the team who is probably at the keyboard. It is a hint, not a sign-off: for a gated step the coordinator still asks. Adding yourself to the roster is optional; it formalizes routing and tracking.

## Removing a Human

Same as removing any member:

1. Move entry to alumni section in team.md.
2. Remove from active roster.
3. Update routing.md to remove all routing entries.
9 changes: 9 additions & 0 deletions packages/squad-sdk/src/client/adapters/claude-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ export const CLAUDE_PLUGIN_AGENT = `${CLAUDE_PLUGIN_NAME}:squad`;
export const CLAUDE_PLUGIN_MCP_PREFIX = `mcp__plugin_${CLAUDE_PLUGIN_NAME}_squad_state__`;
/** Where the generated plugin lives in the Squad repository; the marketplace points here. */
export const CLAUDE_PLUGIN_SOURCE_DIR = 'plugins/squad';

/**
* Binary files the plugin ships as they are, keyed by their path in the plugin, valued by their
* source in the repository. `scripts/build-claude-plugin.mjs` copies them; they are not generated.
* The icon (512 px square PNG) is the one `claude plugin validate` looks for at `.claude-plugin/icon.png`.
*/
export const CLAUDE_PLUGIN_STATIC_FILES: Readonly<Record<string, string>> = {
'.claude-plugin/icon.png': 'docs/src/assets/images/squad-plugin-icon.png',
};
/** Marketplace name: users install `squad@squad`. */
export const CLAUDE_MARKETPLACE_NAME = 'squad';

Expand Down
83 changes: 83 additions & 0 deletions packages/squad-sdk/templates/human-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Human Members

Reference for adding human team members to the Squad roster.

## Triggers

The coordinator enters the human-member flow when:

- User says **"add {name} as {role}"** or **"add a human for {domain}"**
- User says **"make {name} a reviewer"**
- Routing detects a human member as the target for work

## Comparison: AI Agent vs. Human Member

| | AI Agent | Human Member |
|---|----------|-------------|
| Badge | Role-specific emoji | 👤 Human |
| Charter | ✅ | ❌ |
| History | ✅ | ❌ |
| Spawned as sub-agent | ✅ | ❌ |
| Can review work | ✅ | ✅ |
| Casting (fictional name) | ✅ | ❌ (real name) |

## Adding a Human Member

Follow these steps:

1. **Use real name** — no casting. Badge: 👤 Human.
2. **Add to team.md roster:**
```markdown
| {Name} | {Role} | — | 👤 Human |
```
3. **Add routing entries to routing.md:**
```markdown
| {domain} | {Name} 👤 | {example tasks} |
```
4. **Do not create** `charter.md` or `history.md` for humans.
5. **Do not add** to casting registry.

## Routing to a Human

When work routes to a human member, the coordinator pauses. It never does that work itself or hands it to an AI member.

**Sign-off gates.** When `team.md`, `routing.md` or a decision says a human must approve or sign off on a kind of work (a release, version bump, tag, publish, merge or deploy), a request to do that work is not the sign-off. Before any gated step the coordinator names the rule and the human, asks whether the user is that human or has their explicit sign-off, proceeds only on an explicit yes, and records it in the decisions inbox. In a non-interactive run it stops at the gate and reports what is waiting and for whom.


1. **Present the work to the user:**
```
"{Name} needs to review this. [Context about what needs review]"
```
2. **Non-dependent work continues immediately** — other agents proceed with unrelated tasks.
3. **Stale reminder** after >1 turn without response:
```
"📌 Still waiting on {Name} for {thing}."
```

## Reviewing as a Human

- Humans can serve as reviewers in the review process.
- **Rejection lockout applies normally** — original author is locked out when reviewer rejects.
- User relays the human's verdict to the coordinator ("approved" or "rejected with changes").

## Decision Framework: When to Add a Human Member

| Scenario | Add to roster? | Why |
|----------|---------------|-----|
| Approves architecture decisions before implementation | ✅ Yes | Decision gate — team routes and waits |
| Reviews all docs PRs as standing reviewer | ✅ Yes | Recurring review gate |
| Occasionally reviews PRs when tagged | ❌ No | Use @mention on the PR |
| Files issues and contributes code | ❌ No | Normal GitHub collaboration |
| Makes final ship/no-ship call | ✅ Yes | Approval gate |

**Litmus test:** If you want agents to *stop and wait* for someone's input before proceeding on a specific type of work, add them. If they review asynchronously via GitHub, don't bother.

**Who is driving:** `git config user.name` tells the team who is probably at the keyboard. It is a hint, not a sign-off: for a gated step the coordinator still asks. Adding yourself to the roster is optional; it formalizes routing and tracking.

## Removing a Human

Same as removing any member:

1. Move entry to alumni section in team.md.
2. Remove from active roster.
3. Update routing.md to remove all routing entries.
Binary file added plugins/squad/.claude-plugin/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 15 additions & 4 deletions scripts/build-claude-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (!fs.existsSync(SDK)) {
console.error(`build-claude-plugin: ${path.relative(ROOT, SDK)} is missing; run \`npm run build\` first.`);
process.exit(1);
}
const { buildClaudePlugin, buildClaudeMarketplace, CLAUDE_PLUGIN_SOURCE_DIR } = await import(pathToFileURL(SDK).href);
const { buildClaudePlugin, buildClaudeMarketplace, CLAUDE_PLUGIN_SOURCE_DIR, CLAUDE_PLUGIN_STATIC_FILES } = await import(pathToFileURL(SDK).href);

const args = process.argv.slice(2);
const check = args.includes('--check');
Expand Down Expand Up @@ -56,21 +56,27 @@ function listTree(dir, rel = '') {
}

const wanted = new Map(files.map((f) => [f.path, f]));
// Binary files shipped as they are (the icon): copied from the repository, compared byte for byte.
const statics = Object.entries(CLAUDE_PLUGIN_STATIC_FILES).map(([rel, src]) => ({ rel, bytes: fs.readFileSync(path.join(ROOT, src)) }));
const stale = [];
for (const rel of listTree(pluginDir)) if (!wanted.has(rel)) stale.push(rel);
for (const rel of listTree(pluginDir)) if (!wanted.has(rel) && !(rel in CLAUDE_PLUGIN_STATIC_FILES)) stale.push(rel);
const changed = [];
for (const f of files) {
const abs = path.join(pluginDir, f.path);
const current = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : undefined;
if (current !== f.content) changed.push(f.path);
}
for (const { rel, bytes } of statics) {
const abs = path.join(pluginDir, rel);
if (!fs.existsSync(abs) || !fs.readFileSync(abs).equals(bytes)) changed.push(rel);
}
const marketplaceStale = !fs.existsSync(marketplacePath) || fs.readFileSync(marketplacePath, 'utf8') !== marketplace;

if (check) {
const problems = [...changed.map((p) => `changed: ${p}`), ...stale.map((p) => `extra: ${p}`)];
if (marketplaceStale) problems.push('changed: .claude-plugin/marketplace.json');
// Marketplace installs clone the repository: a generated file git ignores never reaches users.
const ignored = spawnSync('git', ['check-ignore', '--no-index', '--', ...files.map((f) => path.join(CLAUDE_PLUGIN_SOURCE_DIR, f.path))], {
const ignored = spawnSync('git', ['check-ignore', '--no-index', '--', ...[...files.map((f) => f.path), ...statics.map((x) => x.rel)].map((p) => path.join(CLAUDE_PLUGIN_SOURCE_DIR, p))], {
cwd: ROOT,
encoding: 'utf8',
});
Expand All @@ -90,8 +96,13 @@ for (const f of files) {
fs.writeFileSync(abs, f.content, 'utf8');
if (f.executable) fs.chmodSync(abs, 0o755);
}
for (const { rel, bytes } of statics) {
const abs = path.join(pluginDir, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, bytes);
}
fs.mkdirSync(path.dirname(marketplacePath), { recursive: true });
fs.writeFileSync(marketplacePath, marketplace, 'utf8');
console.log(
`build-claude-plugin: wrote ${files.length} files to ${CLAUDE_PLUGIN_SOURCE_DIR} (${changed.length} changed, ${stale.length} removed), version ${version}.`,
`build-claude-plugin: wrote ${files.length + statics.length} files to ${CLAUDE_PLUGIN_SOURCE_DIR} (${changed.length} changed, ${stale.length} removed), version ${version}.`,
);
Loading
Loading