Skip to content

Commit f7a905e

Browse files
committed
Revert "chore: remove all squad files and workflows from dev"
This reverts commit 4809267.
1 parent 4809267 commit f7a905e

Some content is hidden

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

68 files changed

+6795
-0
lines changed

.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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Squad CI
2+
3+
on:
4+
pull_request:
5+
branches: [dev, preview, main, insider]
6+
types: [opened, synchronize, reopened]
7+
push:
8+
branches: [dev, insider]
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: 22
22+
23+
- name: Run tests
24+
run: node --test test/*.test.js

.github/workflows/squad-docs.yml

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

0 commit comments

Comments
 (0)