@bradygaster/squad-sdk 0.8.17 → 0.8.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/client.d.ts.map +1 -1
- package/dist/adapter/client.js +2 -0
- package/dist/adapter/client.js.map +1 -1
- package/dist/config/init.d.ts +43 -2
- package/dist/config/init.d.ts.map +1 -1
- package/dist/config/init.js +389 -46
- package/dist/config/init.js.map +1 -1
- package/dist/index.d.ts +8 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -9
- package/dist/index.js.map +1 -1
- package/dist/ralph/index.js +5 -5
- package/dist/ralph/index.js.map +1 -1
- package/dist/remote/bridge.d.ts +2 -0
- package/dist/remote/bridge.d.ts.map +1 -1
- package/dist/remote/bridge.js +34 -4
- package/dist/remote/bridge.js.map +1 -1
- package/dist/resolution.d.ts +13 -0
- package/dist/resolution.d.ts.map +1 -1
- package/dist/resolution.js +9 -1
- package/dist/resolution.js.map +1 -1
- package/dist/sharing/consult.d.ts +226 -0
- package/dist/sharing/consult.d.ts.map +1 -0
- package/dist/sharing/consult.js +818 -0
- package/dist/sharing/consult.js.map +1 -0
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/package.json +4 -2
- package/templates/casting-history.json +4 -0
- package/templates/casting-policy.json +35 -0
- package/templates/casting-registry.json +3 -0
- package/templates/ceremonies.md +41 -0
- package/templates/charter.md +53 -0
- package/templates/constraint-tracking.md +38 -0
- package/templates/copilot-instructions.md +46 -0
- package/templates/history.md +10 -0
- package/templates/identity/now.md +9 -0
- package/templates/identity/wisdom.md +15 -0
- package/templates/mcp-config.md +98 -0
- package/templates/multi-agent-format.md +28 -0
- package/templates/orchestration-log.md +27 -0
- package/templates/plugin-marketplace.md +49 -0
- package/templates/raw-agent-output.md +37 -0
- package/templates/roster.md +60 -0
- package/templates/routing.md +54 -0
- package/templates/run-output.md +50 -0
- package/templates/scribe-charter.md +119 -0
- package/templates/skill.md +24 -0
- package/templates/skills/project-conventions/SKILL.md +56 -0
- package/templates/squad.agent.md +1146 -0
- package/templates/workflows/squad-ci.yml +24 -0
- package/templates/workflows/squad-docs.yml +50 -0
- package/templates/workflows/squad-heartbeat.yml +316 -0
- package/templates/workflows/squad-insider-release.yml +61 -0
- package/templates/workflows/squad-issue-assign.yml +161 -0
- package/templates/workflows/squad-label-enforce.yml +181 -0
- package/templates/workflows/squad-main-guard.yml +129 -0
- package/templates/workflows/squad-preview.yml +55 -0
- package/templates/workflows/squad-promote.yml +121 -0
- package/templates/workflows/squad-release.yml +77 -0
- package/templates/workflows/squad-triage.yml +260 -0
- package/templates/workflows/sync-squad-labels.yml +169 -0
|
@@ -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
|
|
@@ -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
|
|
@@ -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,0 +1,61 @@
|
|
|
1
|
+
name: Squad Insider Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [insider]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: node --test test/*.test.js
|
|
24
|
+
|
|
25
|
+
- name: Read version from package.json
|
|
26
|
+
id: version
|
|
27
|
+
run: |
|
|
28
|
+
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
29
|
+
SHORT_SHA=$(git rev-parse --short HEAD)
|
|
30
|
+
INSIDER_VERSION="${VERSION}-insider+${SHORT_SHA}"
|
|
31
|
+
INSIDER_TAG="v${INSIDER_VERSION}"
|
|
32
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
33
|
+
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
|
34
|
+
echo "insider_version=$INSIDER_VERSION" >> "$GITHUB_OUTPUT"
|
|
35
|
+
echo "insider_tag=$INSIDER_TAG" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "📦 Base Version: $VERSION (Short SHA: $SHORT_SHA)"
|
|
37
|
+
echo "🏷️ Insider Version: $INSIDER_VERSION"
|
|
38
|
+
echo "🔖 Insider Tag: $INSIDER_TAG"
|
|
39
|
+
|
|
40
|
+
- name: Create git tag
|
|
41
|
+
run: |
|
|
42
|
+
git config user.name "github-actions[bot]"
|
|
43
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
44
|
+
git tag -a "${{ steps.version.outputs.insider_tag }}" -m "Insider Release ${{ steps.version.outputs.insider_tag }}"
|
|
45
|
+
git push origin "${{ steps.version.outputs.insider_tag }}"
|
|
46
|
+
|
|
47
|
+
- name: Create GitHub Release
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
50
|
+
run: |
|
|
51
|
+
gh release create "${{ steps.version.outputs.insider_tag }}" \
|
|
52
|
+
--title "${{ steps.version.outputs.insider_tag }}" \
|
|
53
|
+
--notes "This is an insider/development build of Squad. Install with:\`\`\`bash\nnpx github:bradygaster/squad#${{ steps.version.outputs.insider_tag }}\n\`\`\`\n\n**Note:** Insider builds may be unstable and are intended for early adopters and testing only." \
|
|
54
|
+
--prerelease
|
|
55
|
+
|
|
56
|
+
- name: Verify release
|
|
57
|
+
env:
|
|
58
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
59
|
+
run: |
|
|
60
|
+
gh release view "${{ steps.version.outputs.insider_tag }}"
|
|
61
|
+
echo "✅ Insider Release ${{ steps.version.outputs.insider_tag }} created and verified."
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
name: Squad Issue Assign
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [labeled]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
issues: write
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
assign-work:
|
|
13
|
+
# Only trigger on squad:{member} labels (not the base "squad" label)
|
|
14
|
+
if: startsWith(github.event.label.name, 'squad:')
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Identify assigned member and trigger work
|
|
20
|
+
uses: actions/github-script@v7
|
|
21
|
+
with:
|
|
22
|
+
script: |
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const issue = context.payload.issue;
|
|
25
|
+
const label = context.payload.label.name;
|
|
26
|
+
|
|
27
|
+
// Extract member name from label (e.g., "squad:ripley" → "ripley")
|
|
28
|
+
const memberName = label.replace('squad:', '').toLowerCase();
|
|
29
|
+
|
|
30
|
+
// Read team roster — check .squad/ first, fall back to .ai-team/
|
|
31
|
+
let teamFile = '.squad/team.md';
|
|
32
|
+
if (!fs.existsSync(teamFile)) {
|
|
33
|
+
teamFile = '.ai-team/team.md';
|
|
34
|
+
}
|
|
35
|
+
if (!fs.existsSync(teamFile)) {
|
|
36
|
+
core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(teamFile, 'utf8');
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
|
|
43
|
+
// Check if this is a coding agent assignment
|
|
44
|
+
const isCopilotAssignment = memberName === 'copilot';
|
|
45
|
+
|
|
46
|
+
let assignedMember = null;
|
|
47
|
+
if (isCopilotAssignment) {
|
|
48
|
+
assignedMember = { name: '@copilot', role: 'Coding Agent' };
|
|
49
|
+
} else {
|
|
50
|
+
let inMembersTable = false;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (line.match(/^##\s+(Members|Team Roster)/i)) {
|
|
53
|
+
inMembersTable = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (inMembersTable && line.startsWith('## ')) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
|
|
60
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
61
|
+
if (cells.length >= 2 && cells[0].toLowerCase() === memberName) {
|
|
62
|
+
assignedMember = { name: cells[0], role: cells[1] };
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!assignedMember) {
|
|
70
|
+
core.warning(`No member found matching label "${label}"`);
|
|
71
|
+
await github.rest.issues.createComment({
|
|
72
|
+
owner: context.repo.owner,
|
|
73
|
+
repo: context.repo.repo,
|
|
74
|
+
issue_number: issue.number,
|
|
75
|
+
body: `⚠️ No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.`
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Post assignment acknowledgment
|
|
81
|
+
let comment;
|
|
82
|
+
if (isCopilotAssignment) {
|
|
83
|
+
comment = [
|
|
84
|
+
`### 🤖 Routed to @copilot (Coding Agent)`,
|
|
85
|
+
'',
|
|
86
|
+
`**Issue:** #${issue.number} — ${issue.title}`,
|
|
87
|
+
'',
|
|
88
|
+
`@copilot has been assigned and will pick this up automatically.`,
|
|
89
|
+
'',
|
|
90
|
+
`> The coding agent will create a \`copilot/*\` branch and open a draft PR.`,
|
|
91
|
+
`> Review the PR as you would any team member's work.`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
} else {
|
|
94
|
+
comment = [
|
|
95
|
+
`### 📋 Assigned to ${assignedMember.name} (${assignedMember.role})`,
|
|
96
|
+
'',
|
|
97
|
+
`**Issue:** #${issue.number} — ${issue.title}`,
|
|
98
|
+
'',
|
|
99
|
+
`${assignedMember.name} will pick this up in the next Copilot session.`,
|
|
100
|
+
'',
|
|
101
|
+
`> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`,
|
|
102
|
+
`> Otherwise, start a Copilot session and say:`,
|
|
103
|
+
`> \`${assignedMember.name}, work on issue #${issue.number}\``,
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await github.rest.issues.createComment({
|
|
108
|
+
owner: context.repo.owner,
|
|
109
|
+
repo: context.repo.repo,
|
|
110
|
+
issue_number: issue.number,
|
|
111
|
+
body: comment
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`);
|
|
115
|
+
|
|
116
|
+
# Separate step: assign @copilot using PAT (required for coding agent)
|
|
117
|
+
- name: Assign @copilot coding agent
|
|
118
|
+
if: github.event.label.name == 'squad:copilot'
|
|
119
|
+
uses: actions/github-script@v7
|
|
120
|
+
with:
|
|
121
|
+
github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }}
|
|
122
|
+
script: |
|
|
123
|
+
const owner = context.repo.owner;
|
|
124
|
+
const repo = context.repo.repo;
|
|
125
|
+
const issue_number = context.payload.issue.number;
|
|
126
|
+
|
|
127
|
+
// Get the default branch name (main, master, etc.)
|
|
128
|
+
const { data: repoData } = await github.rest.repos.get({ owner, repo });
|
|
129
|
+
const baseBranch = repoData.default_branch;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
issue_number,
|
|
136
|
+
assignees: ['copilot-swe-agent[bot]'],
|
|
137
|
+
agent_assignment: {
|
|
138
|
+
target_repo: `${owner}/${repo}`,
|
|
139
|
+
base_branch: baseBranch,
|
|
140
|
+
custom_instructions: '',
|
|
141
|
+
custom_agent: '',
|
|
142
|
+
model: ''
|
|
143
|
+
},
|
|
144
|
+
headers: {
|
|
145
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
core.warning(`Assignment with agent_assignment failed: ${err.message}`);
|
|
151
|
+
// Fallback: try without agent_assignment
|
|
152
|
+
try {
|
|
153
|
+
await github.rest.issues.addAssignees({
|
|
154
|
+
owner, repo, issue_number,
|
|
155
|
+
assignees: ['copilot-swe-agent']
|
|
156
|
+
});
|
|
157
|
+
core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`);
|
|
158
|
+
} catch (err2) {
|
|
159
|
+
core.warning(`Fallback also failed: ${err2.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|