@bradygaster/squad-sdk 0.9.6-insider.3 → 0.10.0

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.
@@ -1,167 +1,167 @@
1
- name: Squad Heartbeat (Ralph)
2
- # ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all:
3
- # - templates/workflows/squad-heartbeat.yml (source template)
4
- # - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package)
5
- # - .squad/templates/workflows/squad-heartbeat.yml (installed template)
6
- # - .github/workflows/squad-heartbeat.yml (active workflow)
7
- # Run 'squad upgrade' to sync installed copies from source templates.
8
-
9
- on:
10
- # React to completed work or new squad work
11
- issues:
12
- types: [closed, labeled]
13
- pull_request:
14
- types: [closed]
15
-
16
- # Manual trigger
17
- workflow_dispatch:
18
-
19
- permissions:
20
- issues: write
21
- contents: read
22
- pull-requests: read
23
-
24
- jobs:
25
- heartbeat:
26
- runs-on: ubuntu-latest
27
- steps:
28
- - uses: actions/checkout@v4
29
-
30
- - name: Check triage script
31
- id: check-script
32
- run: |
33
- if [ -f ".squad/templates/ralph-triage.js" ]; then
34
- echo "has_script=true" >> $GITHUB_OUTPUT
35
- else
36
- echo "has_script=false" >> $GITHUB_OUTPUT
37
- echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install"
38
- fi
39
-
40
- - name: Ralph — Smart triage
41
- if: steps.check-script.outputs.has_script == 'true'
42
- env:
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- run: |
45
- node .squad/templates/ralph-triage.js \
46
- --squad-dir .squad \
47
- --output triage-results.json
48
-
49
- - name: Ralph — Apply triage decisions
50
- if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != ''
51
- uses: actions/github-script@v7
52
- with:
53
- script: |
54
- const fs = require('fs');
55
- const path = 'triage-results.json';
56
- if (!fs.existsSync(path)) {
57
- core.info('No triage results — board is clear');
58
- return;
59
- }
60
-
61
- const results = JSON.parse(fs.readFileSync(path, 'utf8'));
62
- if (results.length === 0) {
63
- core.info('📋 Board is clear — Ralph found no untriaged issues');
64
- return;
65
- }
66
-
67
- for (const decision of results) {
68
- try {
69
- await github.rest.issues.addLabels({
70
- owner: context.repo.owner,
71
- repo: context.repo.repo,
72
- issue_number: decision.issueNumber,
73
- labels: [decision.label]
74
- });
75
-
76
- await github.rest.issues.createComment({
77
- owner: context.repo.owner,
78
- repo: context.repo.repo,
79
- issue_number: decision.issueNumber,
80
- body: [
81
- '### 🔄 Ralph — Auto-Triage',
82
- '',
83
- `**Assigned to:** ${decision.assignTo}`,
84
- `**Reason:** ${decision.reason}`,
85
- `**Source:** ${decision.source}`,
86
- '',
87
- '> Ralph auto-triaged this issue using routing rules.',
88
- '> To reassign, swap the `squad:*` label.'
89
- ].join('\n')
90
- });
91
-
92
- core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`);
93
- } catch (e) {
94
- core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`);
95
- }
96
- }
97
-
98
- core.info(`🔄 Ralph triaged ${results.length} issue(s)`);
99
-
100
- # Copilot auto-assign step (uses PAT if available)
101
- - name: Ralph — Assign @copilot issues
102
- if: success()
103
- uses: actions/github-script@v7
104
- with:
105
- github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
106
- script: |
107
- const fs = require('fs');
108
-
109
- let teamFile = '.squad/team.md';
110
- if (!fs.existsSync(teamFile)) {
111
- teamFile = '.ai-team/team.md';
112
- }
113
- if (!fs.existsSync(teamFile)) return;
114
-
115
- const content = fs.readFileSync(teamFile, 'utf8');
116
-
117
- // Check if @copilot is on the team with auto-assign
118
- const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
119
- const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
120
- if (!hasCopilot || !autoAssign) return;
121
-
122
- // Find issues labeled squad:copilot with no assignee
123
- try {
124
- const { data: copilotIssues } = await github.rest.issues.listForRepo({
125
- owner: context.repo.owner,
126
- repo: context.repo.repo,
127
- labels: 'squad:copilot',
128
- state: 'open',
129
- per_page: 5
130
- });
131
-
132
- const unassigned = copilotIssues.filter(i =>
133
- !i.assignees || i.assignees.length === 0
134
- );
135
-
136
- if (unassigned.length === 0) {
137
- core.info('No unassigned squad:copilot issues');
138
- return;
139
- }
140
-
141
- // Get repo default branch
142
- const { data: repoData } = await github.rest.repos.get({
143
- owner: context.repo.owner,
144
- repo: context.repo.repo
145
- });
146
-
147
- for (const issue of unassigned) {
148
- try {
149
- await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
150
- owner: context.repo.owner,
151
- repo: context.repo.repo,
152
- issue_number: issue.number,
153
- assignees: ['copilot-swe-agent[bot]'],
154
- agent_assignment: {
155
- target_repo: `${context.repo.owner}/${context.repo.repo}`,
156
- base_branch: repoData.default_branch,
157
- 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.`
158
- }
159
- });
160
- core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
161
- } catch (e) {
162
- core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
163
- }
164
- }
165
- } catch (e) {
166
- core.info(`No squad:copilot label found or error: ${e.message}`);
167
- }
1
+ name: Squad Heartbeat (Ralph)
2
+ # ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all:
3
+ # - templates/workflows/squad-heartbeat.yml (source template)
4
+ # - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package)
5
+ # - .squad/templates/workflows/squad-heartbeat.yml (installed template)
6
+ # - .github/workflows/squad-heartbeat.yml (active workflow)
7
+ # Run 'squad upgrade' to sync installed copies from source templates.
8
+
9
+ on:
10
+ # React to completed work or new squad work
11
+ issues:
12
+ types: [closed, labeled]
13
+ pull_request:
14
+ types: [closed]
15
+
16
+ # Manual trigger
17
+ workflow_dispatch:
18
+
19
+ permissions:
20
+ issues: write
21
+ contents: read
22
+ pull-requests: read
23
+
24
+ jobs:
25
+ heartbeat:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - name: Check triage script
31
+ id: check-script
32
+ run: |
33
+ if [ -f ".squad/templates/ralph-triage.js" ]; then
34
+ echo "has_script=true" >> $GITHUB_OUTPUT
35
+ else
36
+ echo "has_script=false" >> $GITHUB_OUTPUT
37
+ echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install"
38
+ fi
39
+
40
+ - name: Ralph — Smart triage
41
+ if: steps.check-script.outputs.has_script == 'true'
42
+ env:
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ run: |
45
+ node .squad/templates/ralph-triage.js \
46
+ --squad-dir .squad \
47
+ --output triage-results.json
48
+
49
+ - name: Ralph — Apply triage decisions
50
+ if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != ''
51
+ uses: actions/github-script@v7
52
+ with:
53
+ script: |
54
+ const fs = require('fs');
55
+ const path = 'triage-results.json';
56
+ if (!fs.existsSync(path)) {
57
+ core.info('No triage results — board is clear');
58
+ return;
59
+ }
60
+
61
+ const results = JSON.parse(fs.readFileSync(path, 'utf8'));
62
+ if (results.length === 0) {
63
+ core.info('📋 Board is clear — Ralph found no untriaged issues');
64
+ return;
65
+ }
66
+
67
+ for (const decision of results) {
68
+ try {
69
+ await github.rest.issues.addLabels({
70
+ owner: context.repo.owner,
71
+ repo: context.repo.repo,
72
+ issue_number: decision.issueNumber,
73
+ labels: [decision.label]
74
+ });
75
+
76
+ await github.rest.issues.createComment({
77
+ owner: context.repo.owner,
78
+ repo: context.repo.repo,
79
+ issue_number: decision.issueNumber,
80
+ body: [
81
+ '### 🔄 Ralph — Auto-Triage',
82
+ '',
83
+ `**Assigned to:** ${decision.assignTo}`,
84
+ `**Reason:** ${decision.reason}`,
85
+ `**Source:** ${decision.source}`,
86
+ '',
87
+ '> Ralph auto-triaged this issue using routing rules.',
88
+ '> To reassign, swap the `squad:*` label.'
89
+ ].join('\n')
90
+ });
91
+
92
+ core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`);
93
+ } catch (e) {
94
+ core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`);
95
+ }
96
+ }
97
+
98
+ core.info(`🔄 Ralph triaged ${results.length} issue(s)`);
99
+
100
+ # Copilot auto-assign step (uses PAT if available)
101
+ - name: Ralph — Assign @copilot issues
102
+ if: success()
103
+ uses: actions/github-script@v7
104
+ with:
105
+ github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }}
106
+ script: |
107
+ const fs = require('fs');
108
+
109
+ let teamFile = '.squad/team.md';
110
+ if (!fs.existsSync(teamFile)) {
111
+ teamFile = '.ai-team/team.md';
112
+ }
113
+ if (!fs.existsSync(teamFile)) return;
114
+
115
+ const content = fs.readFileSync(teamFile, 'utf8');
116
+
117
+ // Check if @copilot is on the team with auto-assign
118
+ const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot');
119
+ const autoAssign = content.includes('<!-- copilot-auto-assign: true -->');
120
+ if (!hasCopilot || !autoAssign) return;
121
+
122
+ // Find issues labeled squad:copilot with no assignee
123
+ try {
124
+ const { data: copilotIssues } = await github.rest.issues.listForRepo({
125
+ owner: context.repo.owner,
126
+ repo: context.repo.repo,
127
+ labels: 'squad:copilot',
128
+ state: 'open',
129
+ per_page: 5
130
+ });
131
+
132
+ const unassigned = copilotIssues.filter(i =>
133
+ !i.assignees || i.assignees.length === 0
134
+ );
135
+
136
+ if (unassigned.length === 0) {
137
+ core.info('No unassigned squad:copilot issues');
138
+ return;
139
+ }
140
+
141
+ // Get repo default branch
142
+ const { data: repoData } = await github.rest.repos.get({
143
+ owner: context.repo.owner,
144
+ repo: context.repo.repo
145
+ });
146
+
147
+ for (const issue of unassigned) {
148
+ try {
149
+ await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
150
+ owner: context.repo.owner,
151
+ repo: context.repo.repo,
152
+ issue_number: issue.number,
153
+ assignees: ['copilot-swe-agent[bot]'],
154
+ agent_assignment: {
155
+ target_repo: `${context.repo.owner}/${context.repo.repo}`,
156
+ base_branch: repoData.default_branch,
157
+ 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.`
158
+ }
159
+ });
160
+ core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`);
161
+ } catch (e) {
162
+ core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`);
163
+ }
164
+ }
165
+ } catch (e) {
166
+ core.info(`No squad:copilot label found or error: ${e.message}`);
167
+ }