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
20 changes: 20 additions & 0 deletions .github/agents/squad.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ When triggered:

**Casting migration check:** If `.ai-team/team.md` exists but `.ai-team/casting/` does not, perform the migration described in "Casting & Persistent Naming → Migration — Already-Squadified Repos" before proceeding.

### Issue Awareness

**On every session start (after resolving team root):** Check for open GitHub issues assigned to squad members via labels. Use the GitHub CLI or API to list issues with `squad:*` labels:

```
gh issue list --label "squad:{member-name}" --state open --json number,title,labels,body --limit 10
```

For each squad member with assigned issues, note them in the session context. When presenting a catch-up or when the user asks for status, include pending issues:

```
📋 Open issues assigned to squad members:
🔧 {Backend} — #42: Fix auth endpoint timeout (squad:ripley)
⚛️ {Frontend} — #38: Add dark mode toggle (squad:dallas)
```

**Proactive issue pickup:** If a user starts a session and there are open `squad:{member}` issues, mention them: *"Hey {user}, {AgentName} has an open issue — #42: Fix auth endpoint timeout. Want them to pick it up?"*

**Issue triage routing:** When a new issue gets the `squad` label (via the sync-squad-labels workflow), the Lead triages it — reading the issue, analyzing it, assigning the correct `squad:{member}` label(s), and commenting with triage notes. The Lead can also reassign by swapping labels.

**⚡ Read `.ai-team/team.md` (roster), `.ai-team/routing.md` (routing), and `.ai-team/casting/registry.json` (persistent names) as parallel tool calls in a single turn. Do NOT read these sequentially.**

### Acknowledge Immediately — "Feels Heard"
Expand Down
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,51 @@ The Coordinator enforces this. No self-review of rejected work.

---

## Issue Assignment & Triage

Squad integrates with GitHub Issues. Label an issue with `squad` to trigger triage, or assign directly to a member with `squad:{name}`.

### How It Works

1. **Label an issue `squad`** — the Lead auto-triages it: reads the issue, determines who should handle it, applies the right `squad:{member}` label, and comments with triage notes.

2. **`squad:{member}` label applied** — the assigned member picks up the issue in their next Copilot session (or automatically if Copilot coding agent is enabled).

3. **Reassign** — remove the current `squad:*` label and add a different member's label.

### Labels

Labels are auto-created from your team roster via the `sync-squad-labels` workflow:

| Label | Purpose |
|-------|---------|
| `squad` | Triage inbox — Lead reviews and assigns |
| `squad:{name}` | Assigned to a specific squad member |

Labels sync automatically when `.ai-team/team.md` changes, or you can trigger the workflow manually.

### Workflows

Squad installs three GitHub Actions workflows:

| Workflow | Trigger | What it does |
|----------|---------|--------------|
| `sync-squad-labels.yml` | Push to `.ai-team/team.md`, manual | Creates/updates `squad:*` labels from roster |
| `squad-triage.yml` | `squad` label added to issue | Lead triages and assigns `squad:{member}` label |
| `squad-issue-assign.yml` | `squad:{member}` label added | Acknowledges assignment, queues for member |

### Prerequisites

- GitHub Actions must be enabled on the repository
- The `GITHUB_TOKEN` needs `issues: write` and `contents: read` permissions
- For automated issue work: [Copilot coding agent](https://docs.github.com/en/copilot) must be enabled on the repo

### Session Awareness

The coordinator checks for open `squad:{member}` issues at session start and will mention them: *"Hey {user}, {AgentName} has an open issue — #42: Fix auth endpoint timeout. Want them to pick it up?"*

---

## Install

```bash
Expand All @@ -252,7 +297,7 @@ Already have Squad? Update Squad-owned files to the latest version without touch
npx github:bradygaster/squad upgrade
```

This overwrites `squad.agent.md` and `.ai-team-templates/`. It never touches `.ai-team/` — your team's knowledge, decisions, and casting are safe.
This overwrites `squad.agent.md`, `.ai-team-templates/`, and squad workflow files in `.github/workflows/`. It never touches `.ai-team/` — your team's knowledge, decisions, and casting are safe.

---

Expand Down
32 changes: 32 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,38 @@ if (isUpgrade) {
console.log(`${GREEN}✓${RESET} .ai-team-templates/`);
}

// Copy workflow templates (Squad-owned — overwrite on upgrade)
const workflowsSrc = path.join(root, 'templates', 'workflows');
const workflowsDest = path.join(dest, '.github', 'workflows');

if (fs.existsSync(workflowsSrc) && fs.statSync(workflowsSrc).isDirectory()) {
const workflowFiles = fs.readdirSync(workflowsSrc).filter(f => f.endsWith('.yml'));

if (isUpgrade) {
fs.mkdirSync(workflowsDest, { recursive: true });
for (const file of workflowFiles) {
fs.copyFileSync(path.join(workflowsSrc, file), path.join(workflowsDest, file));
}
console.log(`${GREEN}✓${RESET} ${BOLD}upgraded${RESET} squad workflow files (${workflowFiles.length} workflows)`);
} else {
fs.mkdirSync(workflowsDest, { recursive: true });
let copied = 0;
for (const file of workflowFiles) {
const destFile = path.join(workflowsDest, file);
if (fs.existsSync(destFile)) {
console.log(`${DIM}${file} already exists — skipping (run 'upgrade' to update)${RESET}`);
} else {
fs.copyFileSync(path.join(workflowsSrc, file), destFile);
console.log(`${GREEN}✓${RESET} .github/workflows/${file}`);
copied++;
}
}
if (copied === 0 && workflowFiles.length > 0) {
console.log(`${DIM}all squad workflows already exist — skipping${RESET}`);
}
}
}

if (isUpgrade) {
console.log(`\n${DIM}.ai-team/ untouched — your team state is safe${RESET}`);
}
Expand Down
15 changes: 15 additions & 0 deletions templates/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ How to decide who handles what.
| Scope & priorities | {Name} | What to build next, trade-offs, decisions |
| Session logging | Scribe | Automatic — never needs routing |

## Issue Routing

| Label | Action | Who |
|-------|--------|-----|
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
| `squad:{name}` | Pick up issue and complete the work | Named member |

### How Issue Assignment Works

1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
3. Members can reassign by removing their label and adding another member's label.
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.

## Rules

1. **Eager by default** — spawn all agents who could usefully start work, including anticipatory downstream work.
Expand All @@ -22,3 +36,4 @@ How to decide who handles what.
4. **When two agents could handle it**, pick the one whose domain is the primary concern.
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
90 changes: 90 additions & 0 deletions templates/workflows/squad-issue-assign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Squad Issue Assign

on:
issues:
types: [labeled]

permissions:
issues: write
contents: read

jobs:
assign-work:
# Only trigger on squad:{member} labels (not the base "squad" label)
if: startsWith(github.event.label.name, 'squad:')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Identify assigned member and trigger work
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const issue = context.payload.issue;
const label = context.payload.label.name;

// Extract member name from label (e.g., "squad:ripley" → "ripley")
const memberName = label.replace('squad:', '').toLowerCase();

// Read team roster to find the member
const teamFile = '.ai-team/team.md';
if (!fs.existsSync(teamFile)) {
core.warning('No .ai-team/team.md found — cannot assign work');
return;
}

const content = fs.readFileSync(teamFile, 'utf8');
const lines = content.split('\n');

let assignedMember = null;
let inMembersTable = false;
for (const line of lines) {
if (line.startsWith('## Members')) {
inMembersTable = true;
continue;
}
if (inMembersTable && line.startsWith('## ')) {
break;
}
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
if (cells.length >= 2 && cells[0].toLowerCase() === memberName) {
assignedMember = { name: cells[0], role: cells[1] };
break;
}
}
}

if (!assignedMember) {
core.warning(`No member found matching label "${label}"`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `⚠️ No squad member found matching label \`${label}\`. Check \`.ai-team/team.md\` for valid member names.`
});
return;
}

// Post assignment acknowledgment
const comment = [
`### 📋 Assigned to ${assignedMember.name} (${assignedMember.role})`,
'',
`**Issue:** #${issue.number} — ${issue.title}`,
'',
`${assignedMember.name} will pick this up in the next Copilot session.`,
'',
`> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`,
`> Otherwise, start a Copilot session and say:`,
`> \`${assignedMember.name}, work on issue #${issue.number}\``,
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: comment
});

core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`);
Loading