@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.
Files changed (64) hide show
  1. package/dist/adapter/client.d.ts.map +1 -1
  2. package/dist/adapter/client.js +2 -0
  3. package/dist/adapter/client.js.map +1 -1
  4. package/dist/config/init.d.ts +43 -2
  5. package/dist/config/init.d.ts.map +1 -1
  6. package/dist/config/init.js +389 -46
  7. package/dist/config/init.js.map +1 -1
  8. package/dist/index.d.ts +8 -13
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -9
  11. package/dist/index.js.map +1 -1
  12. package/dist/ralph/index.js +5 -5
  13. package/dist/ralph/index.js.map +1 -1
  14. package/dist/remote/bridge.d.ts +2 -0
  15. package/dist/remote/bridge.d.ts.map +1 -1
  16. package/dist/remote/bridge.js +34 -4
  17. package/dist/remote/bridge.js.map +1 -1
  18. package/dist/resolution.d.ts +13 -0
  19. package/dist/resolution.d.ts.map +1 -1
  20. package/dist/resolution.js +9 -1
  21. package/dist/resolution.js.map +1 -1
  22. package/dist/sharing/consult.d.ts +226 -0
  23. package/dist/sharing/consult.d.ts.map +1 -0
  24. package/dist/sharing/consult.js +818 -0
  25. package/dist/sharing/consult.js.map +1 -0
  26. package/dist/sharing/index.d.ts +2 -1
  27. package/dist/sharing/index.d.ts.map +1 -1
  28. package/dist/sharing/index.js +2 -1
  29. package/dist/sharing/index.js.map +1 -1
  30. package/package.json +4 -2
  31. package/templates/casting-history.json +4 -0
  32. package/templates/casting-policy.json +35 -0
  33. package/templates/casting-registry.json +3 -0
  34. package/templates/ceremonies.md +41 -0
  35. package/templates/charter.md +53 -0
  36. package/templates/constraint-tracking.md +38 -0
  37. package/templates/copilot-instructions.md +46 -0
  38. package/templates/history.md +10 -0
  39. package/templates/identity/now.md +9 -0
  40. package/templates/identity/wisdom.md +15 -0
  41. package/templates/mcp-config.md +98 -0
  42. package/templates/multi-agent-format.md +28 -0
  43. package/templates/orchestration-log.md +27 -0
  44. package/templates/plugin-marketplace.md +49 -0
  45. package/templates/raw-agent-output.md +37 -0
  46. package/templates/roster.md +60 -0
  47. package/templates/routing.md +54 -0
  48. package/templates/run-output.md +50 -0
  49. package/templates/scribe-charter.md +119 -0
  50. package/templates/skill.md +24 -0
  51. package/templates/skills/project-conventions/SKILL.md +56 -0
  52. package/templates/squad.agent.md +1146 -0
  53. package/templates/workflows/squad-ci.yml +24 -0
  54. package/templates/workflows/squad-docs.yml +50 -0
  55. package/templates/workflows/squad-heartbeat.yml +316 -0
  56. package/templates/workflows/squad-insider-release.yml +61 -0
  57. package/templates/workflows/squad-issue-assign.yml +161 -0
  58. package/templates/workflows/squad-label-enforce.yml +181 -0
  59. package/templates/workflows/squad-main-guard.yml +129 -0
  60. package/templates/workflows/squad-preview.yml +55 -0
  61. package/templates/workflows/squad-promote.yml +121 -0
  62. package/templates/workflows/squad-release.yml +77 -0
  63. package/templates/workflows/squad-triage.yml +260 -0
  64. 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
+ }