fix(squad): start coding-agent sessions for squad:copilot issues; rename go/no-go workflows #1541
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Squad Heartbeat (Ralph) | |
| # ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all: | |
| # - templates/workflows/squad-heartbeat.yml (source template) | |
| # - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package) | |
| # - .squad/templates/workflows/squad-heartbeat.yml (installed template) | |
| # - .github/workflows/squad-heartbeat.yml (active workflow) | |
| # Run 'squad upgrade' to sync installed copies from source templates. | |
| on: | |
| # React to completed work or new squad work | |
| issues: | |
| types: [closed, labeled] | |
| pull_request: | |
| types: [closed] | |
| # Manual trigger | |
| workflow_dispatch: | |
| permissions: | |
| issues: write | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| heartbeat: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Check triage script | |
| id: check-script | |
| run: | | |
| if [ -f ".squad/templates/ralph-triage.js" ]; then | |
| echo "has_script=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_script=false" >> $GITHUB_OUTPUT | |
| echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install" | |
| fi | |
| - name: Ralph — Smart triage | |
| if: steps.check-script.outputs.has_script == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| node .squad/templates/ralph-triage.js \ | |
| --squad-dir .squad \ | |
| --output triage-results.json | |
| - name: Ralph — Apply triage decisions | |
| if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = 'triage-results.json'; | |
| if (!fs.existsSync(path)) { | |
| core.info('No triage results — board is clear'); | |
| return; | |
| } | |
| const results = JSON.parse(fs.readFileSync(path, 'utf8')); | |
| if (results.length === 0) { | |
| core.info('📋 Board is clear — Ralph found no untriaged issues'); | |
| return; | |
| } | |
| for (const decision of results) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: decision.issueNumber, | |
| labels: [decision.label] | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: decision.issueNumber, | |
| body: [ | |
| '### 🔄 Ralph — Auto-Triage', | |
| '', | |
| `**Assigned to:** ${decision.assignTo}`, | |
| `**Reason:** ${decision.reason}`, | |
| `**Source:** ${decision.source}`, | |
| '', | |
| '> Ralph auto-triaged this issue using routing rules.', | |
| '> To reassign, swap the `squad:*` label.' | |
| ].join('\n') | |
| }); | |
| core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`); | |
| } catch (e) { | |
| core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`); | |
| } | |
| } | |
| core.info(`🔄 Ralph triaged ${results.length} issue(s)`); | |
| # Copilot auto-assign step (uses PAT if available) | |
| - name: Ralph — Assign @copilot issues | |
| if: success() | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| let teamFile = '.squad/team.md'; | |
| if (!fs.existsSync(teamFile)) { | |
| teamFile = '.ai-team/team.md'; | |
| } | |
| if (!fs.existsSync(teamFile)) return; | |
| const content = fs.readFileSync(teamFile, 'utf8'); | |
| // Check if @copilot is on the team with auto-assign | |
| const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot'); | |
| const autoAssign = content.includes('<!-- copilot-auto-assign: true -->'); | |
| if (!hasCopilot || !autoAssign) return; | |
| // Find issues labeled squad:copilot with no assignee | |
| try { | |
| const { data: copilotIssues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'squad:copilot', | |
| state: 'open', | |
| per_page: 5 | |
| }); | |
| // Include issues that don't yet have the coding agent assigned. | |
| // issue-go-yes always assigns the go:yes maintainer as owner, so an | |
| // issue almost always has a (human) assignee — filtering on "zero | |
| // assignees" would skip every routed issue. Filter on the agent's | |
| // presence instead; the POST below ADDS the bot as an extra assignee | |
| // without removing the human owner. | |
| const COPILOT_LOGINS = new Set(['copilot-swe-agent[bot]', 'copilot-swe-agent', 'copilot']); | |
| const needsAgent = copilotIssues.filter(i => | |
| !(i.assignees || []).some(a => COPILOT_LOGINS.has((a.login || '').toLowerCase())) | |
| ); | |
| if (needsAgent.length === 0) { | |
| core.info('No squad:copilot issues awaiting the coding agent'); | |
| return; | |
| } | |
| // Get repo default branch | |
| const { data: repoData } = await github.rest.repos.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }); | |
| for (const issue of needsAgent) { | |
| try { | |
| await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| assignees: ['copilot-swe-agent[bot]'], | |
| agent_assignment: { | |
| target_repo: `${context.repo.owner}/${context.repo.repo}`, | |
| base_branch: repoData.default_branch, | |
| 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.` | |
| } | |
| }); | |
| core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`); | |
| } catch (e) { | |
| core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`); | |
| } | |
| } | |
| } catch (e) { | |
| core.info(`No squad:copilot label found or error: ${e.message}`); | |
| } |