Skip to content

Commit ccd7f5a

Browse files
csharpfritzCopilot
andcommitted
chore(.squad): add Squad infrastructure files
Includes Squad CI workflows, agent definition, merge drivers, and template files from Squad initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 67cf2bc commit ccd7f5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+5121
-0
lines changed

.copilot/mcp-config.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"mcpServers": {
3+
"EXAMPLE-trello": {
4+
"command": "npx",
5+
"args": [
6+
"-y",
7+
"@trello/mcp-server"
8+
],
9+
"env": {
10+
"TRELLO_API_KEY": "${TRELLO_API_KEY}",
11+
"TRELLO_TOKEN": "${TRELLO_TOKEN}"
12+
}
13+
}
14+
}
15+
}

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Squad: union merge for append-only team state files
2+
.squad/decisions.md merge=union
3+
.squad/agents/*/history.md merge=union
4+
.squad/log/** merge=union
5+
.squad/orchestration-log/** merge=union

.github/agents/squad.agent.md

Lines changed: 1146 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/squad-ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Squad CI
2+
# dotnet project — configure build/test commands below
3+
4+
on:
5+
pull_request:
6+
branches: [dev, preview, main, insider]
7+
types: [opened, synchronize, reopened]
8+
push:
9+
branches: [dev, insider]
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
test:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Build and test
21+
run: |
22+
# TODO: Add your dotnet build/test commands here
23+
# Go: go test ./...
24+
# Python: pip install -r requirements.txt && pytest
25+
# .NET: dotnet test
26+
# Java (Maven): mvn test
27+
# Java (Gradle): ./gradlew test
28+
echo "No build commands configured — update squad-ci.yml"

.github/workflows/squad-docs.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Squad Docs — Build & Deploy
2+
# dotnet project — configure documentation build commands below
3+
4+
on:
5+
workflow_dispatch:
6+
push:
7+
branches: [preview]
8+
paths:
9+
- 'docs/**'
10+
- '.github/workflows/squad-docs.yml'
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Build docs
24+
run: |
25+
# TODO: Add your documentation build commands here
26+
# This workflow is optional — remove or customize it for your project
27+
echo "No docs build commands configured — update or remove squad-docs.yml"
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
name: Squad Heartbeat (Ralph)
2+
3+
on:
4+
schedule:
5+
# Every 30 minutes — adjust or remove if not needed
6+
- cron: '*/30 * * * *'
7+
8+
# React to completed work or new squad work
9+
issues:
10+
types: [closed, labeled]
11+
pull_request:
12+
types: [closed]
13+
14+
# Manual trigger
15+
workflow_dispatch:
16+
17+
permissions:
18+
issues: write
19+
contents: read
20+
pull-requests: read
21+
22+
jobs:
23+
heartbeat:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Ralph — Check for squad work
29+
uses: actions/github-script@v7
30+
with:
31+
script: |
32+
const fs = require('fs');
33+
34+
// Read team roster — check .squad/ first, fall back to .ai-team/
35+
let teamFile = '.squad/team.md';
36+
if (!fs.existsSync(teamFile)) {
37+
teamFile = '.ai-team/team.md';
38+
}
39+
if (!fs.existsSync(teamFile)) {
40+
core.info('No .squad/team.md or .ai-team/team.md found — Ralph has nothing to monitor');
41+
return;
42+
}
43+
44+
const content = fs.readFileSync(teamFile, 'utf8');
45+
46+
// Check if Ralph is on the roster
47+
if (!content.includes('Ralph') || !content.includes('🔄')) {
48+
core.info('Ralph not on roster — heartbeat disabled');
49+
return;
50+
}
51+
52+
// Parse members from roster
53+
const lines = content.split('\n');
54+
const members = [];
55+
let inMembersTable = false;
56+
for (const line of lines) {
57+
if (line.match(/^##\s+(Members|Team Roster)/i)) {
58+
inMembersTable = true;
59+
continue;
60+
}
61+
if (inMembersTable && line.startsWith('## ')) break;
62+
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
63+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
64+
if (cells.length >= 2 && !['Scribe', 'Ralph'].includes(cells[0])) {
65+
members.push({
66+
name: cells[0],
67+
role: cells[1],
68+
label: `squad:${cells[0].toLowerCase()}`
69+
});
70+
}
71+
}
72+
}
73+
74+
if (members.length === 0) {
75+
core.info('No squad members found — nothing to monitor');
76+
return;
77+
}
78+
79+
// 1. Find untriaged issues (labeled "squad" but no "squad:{member}" label)
80+
const { data: squadIssues } = await github.rest.issues.listForRepo({
81+
owner: context.repo.owner,
82+
repo: context.repo.repo,
83+
labels: 'squad',
84+
state: 'open',
85+
per_page: 20
86+
});
87+
88+
const memberLabels = members.map(m => m.label);
89+
const untriaged = squadIssues.filter(issue => {
90+
const issueLabels = issue.labels.map(l => l.name);
91+
return !memberLabels.some(ml => issueLabels.includes(ml));
92+
});
93+
94+
// 2. Find assigned but unstarted issues (has squad:{member} label, no assignee)
95+
const unstarted = [];
96+
for (const member of members) {
97+
try {
98+
const { data: memberIssues } = await github.rest.issues.listForRepo({
99+
owner: context.repo.owner,
100+
repo: context.repo.repo,
101+
labels: member.label,
102+
state: 'open',
103+
per_page: 10
104+
});
105+
for (const issue of memberIssues) {
106+
if (!issue.assignees || issue.assignees.length === 0) {
107+
unstarted.push({ issue, member });
108+
}
109+
}
110+
} catch (e) {
111+
// Label may not exist yet
112+
}
113+
}
114+
115+
// 3. Find squad issues missing triage verdict (no go:* label)
116+
const missingVerdict = squadIssues.filter(issue => {
117+
const labels = issue.labels.map(l => l.name);
118+
return !labels.some(l => l.startsWith('go:'));
119+
});
120+
121+
// 4. Find go:yes issues missing release target
122+
const goYesIssues = squadIssues.filter(issue => {
123+
const labels = issue.labels.map(l => l.name);
124+
return labels.includes('go:yes') && !labels.some(l => l.startsWith('release:'));
125+
});
126+
127+
// 4b. Find issues missing type: label
128+
const missingType = squadIssues.filter(issue => {
129+
const labels = issue.labels.map(l => l.name);
130+
return !labels.some(l => l.startsWith('type:'));
131+
});
132+
133+
// 5. Find open PRs that need attention
134+
const { data: openPRs } = await github.rest.pulls.list({
135+
owner: context.repo.owner,
136+
repo: context.repo.repo,
137+
state: 'open',
138+
per_page: 20
139+
});
140+
141+
const squadPRs = openPRs.filter(pr =>
142+
pr.labels.some(l => l.name.startsWith('squad'))
143+
);
144+
145+
// Build status summary
146+
const summary = [];
147+
if (untriaged.length > 0) {
148+
summary.push(`🔴 **${untriaged.length} untriaged issue(s)** need triage`);
149+
}
150+
if (unstarted.length > 0) {
151+
summary.push(`🟡 **${unstarted.length} assigned issue(s)** have no assignee`);
152+
}
153+
if (missingVerdict.length > 0) {
154+
summary.push(`⚪ **${missingVerdict.length} issue(s)** missing triage verdict (no \`go:\` label)`);
155+
}
156+
if (goYesIssues.length > 0) {
157+
summary.push(`⚪ **${goYesIssues.length} approved issue(s)** missing release target (no \`release:\` label)`);
158+
}
159+
if (missingType.length > 0) {
160+
summary.push(`⚪ **${missingType.length} issue(s)** missing \`type:\` label`);
161+
}
162+
if (squadPRs.length > 0) {
163+
const drafts = squadPRs.filter(pr => pr.draft).length;
164+
const ready = squadPRs.length - drafts;
165+
if (drafts > 0) summary.push(`🟡 **${drafts} draft PR(s)** in progress`);
166+
if (ready > 0) summary.push(`🟢 **${ready} PR(s)** open for review/merge`);
167+
}
168+
169+
if (summary.length === 0) {
170+
core.info('📋 Board is clear — Ralph found no pending work');
171+
return;
172+
}
173+
174+
core.info(`🔄 Ralph found work:\n${summary.join('\n')}`);
175+
176+
// Auto-triage untriaged issues
177+
for (const issue of untriaged) {
178+
const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase();
179+
let assignedMember = null;
180+
let reason = '';
181+
182+
// Simple keyword-based routing
183+
for (const member of members) {
184+
const role = member.role.toLowerCase();
185+
if ((role.includes('frontend') || role.includes('ui')) &&
186+
(issueText.includes('ui') || issueText.includes('frontend') ||
187+
issueText.includes('css') || issueText.includes('component'))) {
188+
assignedMember = member;
189+
reason = 'Matches frontend/UI domain';
190+
break;
191+
}
192+
if ((role.includes('backend') || role.includes('api') || role.includes('server')) &&
193+
(issueText.includes('api') || issueText.includes('backend') ||
194+
issueText.includes('database') || issueText.includes('endpoint'))) {
195+
assignedMember = member;
196+
reason = 'Matches backend/API domain';
197+
break;
198+
}
199+
if ((role.includes('test') || role.includes('qa')) &&
200+
(issueText.includes('test') || issueText.includes('bug') ||
201+
issueText.includes('fix') || issueText.includes('regression'))) {
202+
assignedMember = member;
203+
reason = 'Matches testing/QA domain';
204+
break;
205+
}
206+
}
207+
208+
// Default to Lead
209+
if (!assignedMember) {
210+
const lead = members.find(m =>
211+
m.role.toLowerCase().includes('lead') ||
212+
m.role.toLowerCase().includes('architect')
213+
);
214+
if (lead) {
215+
assignedMember = lead;
216+
reason = 'No domain match — routed to Lead';
217+
}
218+
}
219+
220+
if (assignedMember) {
221+
// Add member label
222+
await github.rest.issues.addLabels({
223+
owner: context.repo.owner,
224+
repo: context.repo.repo,
225+
issue_number: issue.number,
226+
labels: [assignedMember.label]
227+
});
228+
229+
// Post triage comment
230+
await github.rest.issues.createComment({
231+
owner: context.repo.owner,
232+
repo: context.repo.repo,
233+
issue_number: issue.number,
234+
body: [
235+
`### 🔄 Ralph — Auto-Triage`,
236+
'',
237+
`**Assigned to:** ${assignedMember.name} (${assignedMember.role})`,
238+
`**Reason:** ${reason}`,
239+
'',
240+
`> Ralph auto-triaged this issue via the squad heartbeat. To reassign, swap the \`squad:*\` label.`
241+
].join('\n')
242+
});
243+
244+
core.info(`Auto-triaged #${issue.number} → ${assignedMember.name}`);
245+
}
246+
}
247+
248+
# Copilot auto-assign step (uses PAT if available)
249+
- name: Ralph — Assign @copilot issues
250+
if: success()
251+
uses: actions/github-script@v7
252+
with:
253+
github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
254+
script: |
255+
const fs = require('fs');
256+
257+
let teamFile = '.squad/team.md';
258+
if (!fs.existsSync(teamFile)) {
259+
teamFile = '.ai-team/team.md';
260+
}
261+
if (!fs.existsSync(teamFile)) return;
262+
263+
const content = fs.readFileSync(teamFile, 'utf8');
264+
265+
// Check if @copilot is on the team with auto-assign
266+
const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
267+
const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
268+
if (!hasCopilot || !autoAssign) return;
269+
270+
// Find issues labeled squad:copilot with no assignee
271+
try {
272+
const { data: copilotIssues } = await github.rest.issues.listForRepo({
273+
owner: context.repo.owner,
274+
repo: context.repo.repo,
275+
labels: 'squad:copilot',
276+
state: 'open',
277+
per_page: 5
278+
});
279+
280+
const unassigned = copilotIssues.filter(i =>
281+
!i.assignees || i.assignees.length === 0
282+
);
283+
284+
if (unassigned.length === 0) {
285+
core.info('No unassigned squad:copilot issues');
286+
return;
287+
}
288+
289+
// Get repo default branch
290+
const { data: repoData } = await github.rest.repos.get({
291+
owner: context.repo.owner,
292+
repo: context.repo.repo
293+
});
294+
295+
for (const issue of unassigned) {
296+
try {
297+
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
298+
owner: context.repo.owner,
299+
repo: context.repo.repo,
300+
issue_number: issue.number,
301+
assignees: ['copilot-swe-agent[bot]'],
302+
agent_assignment: {
303+
target_repo: `${context.repo.owner}/${context.repo.repo}`,
304+
base_branch: repoData.default_branch,
305+
custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.`
306+
}
307+
});
308+
core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
309+
} catch (e) {
310+
core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
311+
}
312+
}
313+
} catch (e) {
314+
core.info(`No squad:copilot label found or error: ${e.message}`);
315+
}

0 commit comments

Comments
 (0)