Repository navigation
236 lines (210 loc) Β· 9.62 KB
/
Copy pathsync-squad-labels.yml
File metadata and controls
236 lines (210 loc) Β· 9.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
name: Sync Squad Labels
on:
push:
paths:
- '.squad/team.md'
- '.ai-team/team.md'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
permissions:
issues: write
contents: read
jobs:
sync-labels:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
- name: Parse roster and sync labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9
with:
script: |
const fs = require('fs');
let teamFile = '.squad/team.md';
if (!fs.existsSync(teamFile)) {
teamFile = '.ai-team/team.md';
}
if (!fs.existsSync(teamFile)) {
core.info('No .squad/team.md or .ai-team/team.md found β skipping label sync');
return;
}
const content = fs.readFileSync(teamFile, 'utf8');
const lines = content.split('\n');
// Parse the Members table for agent names
const members = [];
let inMembersTable = false;
for (const line of lines) {
if (line.match(/^##\s+(Members|Team Roster)/i)) {
inMembersTable = true;
continue;
}
if (inMembersTable && line.startsWith('## ')) {
break;
}
if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
if (cells.length >= 2 && cells[0] !== 'Scribe') {
members.push({
name: cells[0],
role: cells[1]
});
}
}
}
core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`);
// Check if @copilot is on the team
const hasCopilot = content.includes('π€ Coding Agent');
// Define label color palette for squad labels
const SQUAD_COLOR = '9B8FCC';
const COPILOT_COLOR = '10b981';
// Curated palette of visually distinct colors for squad member labels.
// These have good contrast with white text and avoid colors used by
// other label groups (go:*, release:*, type:*, priority:*, status:*).
const SQUAD_MEMBER_PALETTE = [
'E06C75', // soft red
'E5C07B', // gold
'56B6C2', // teal
'C678DD', // purple
'61AFEF', // blue
'98C379', // green
'D19A66', // orange
'BE5046', // rust
'7EC8E3', // sky blue
'9ECE6A', // lime
'F7768E', // pink
'FF9E64', // tangerine
'7AA2F7', // periwinkle
'BB9AF7', // lavender
'73DACA', // mint
'2AC3DE', // cyan
'DB4B4B', // crimson
'E0AF68', // amber
'449DAB', // dark teal
'A9DC76', // chartreuse
];
// Deterministic color from palette based on agent slug hash
const usedColors = new Set();
function getAgentColor(slug) {
let hash = 0;
for (const char of slug) {
hash = ((hash << 5) - hash) + char.charCodeAt(0);
hash = hash & hash;
}
let index = Math.abs(hash) % SQUAD_MEMBER_PALETTE.length;
// Linear probe to avoid collisions
let attempts = 0;
while (usedColors.has(SQUAD_MEMBER_PALETTE[index]) && attempts < SQUAD_MEMBER_PALETTE.length) {
index = (index + 1) % SQUAD_MEMBER_PALETTE.length;
attempts++;
}
usedColors.add(SQUAD_MEMBER_PALETTE[index]);
return SQUAD_MEMBER_PALETTE[index];
}
// Define go: and release: labels (static)
const GO_LABELS = [
{ name: 'go:yes', color: '0E8A16', description: 'Ready to implement' },
{ name: 'go:no', color: 'B60205', description: 'Not pursuing' },
{ name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' }
];
const RELEASE_LABELS = [
{ name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' },
{ name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' },
{ name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' },
{ name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' },
{ name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' }
];
const TYPE_LABELS = [
{ name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
{ name: 'type:bug', color: 'FF0422', description: 'Something broken' },
{ name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation β produces a plan, not code' },
{ name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' },
{ name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' },
{ name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }
];
// High-signal labels β these MUST visually dominate all others
const SIGNAL_LABELS = [
{ name: 'bug', color: 'FF0422', description: 'Something isn\'t working' },
{ name: 'feedback', color: '00E5FF', description: 'User feedback β high signal, needs attention' },
// Detection taxonomy: gh-aw threat detection infrastructure failure.
// Applied by upstream process_safe_outputs when conclusion=warning/parse_error.
// Distinct from the upstream threat label (genuine content threat) β see issue #1701.
{ name: 'agentic-detection-failed', color: 'E8B84B', description: 'gh-aw detection infrastructure failure β threat content was not evaluated' }
];
const PRIORITY_LABELS = [
{ name: 'priority:p0', color: 'B60205', description: 'Blocking release' },
{ name: 'priority:p1', color: 'D93F0B', description: 'This sprint' },
{ name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' }
];
function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }
function assertUniqueMemberSlugs(teamMembers) {
const memberBySlug = new Map();
for (const member of teamMembers) {
const slug = slugify(member.name);
if (memberBySlug.has(slug)) {
throw new Error(`Squad member label slug "${slug}" is ambiguous: "${memberBySlug.get(slug)}" and "${member.name}"`);
}
memberBySlug.set(slug, member.name);
}
}
assertUniqueMemberSlugs(members);
// Ensure the base "squad" triage label exists
const labels = [
{ name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox β Lead will assign to a member' }
];
for (const member of members) {
const slug = slugify(member.name);
labels.push({
name: `squad:${slug}`,
color: getAgentColor(slug),
description: `Assigned to ${member.name} (${member.role})`
});
}
// Add @copilot label if coding agent is on the team
if (hasCopilot) {
labels.push({
name: 'squad:copilot',
color: COPILOT_COLOR,
description: 'Assigned to @copilot (Coding Agent) for autonomous work'
});
}
// Add go:, release:, type:, priority:, and high-signal labels
labels.push(...GO_LABELS);
labels.push(...RELEASE_LABELS);
labels.push(...TYPE_LABELS);
labels.push(...PRIORITY_LABELS);
labels.push(...SIGNAL_LABELS);
// Sync labels (create or update)
for (const label of labels) {
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name
});
// Label exists β update it
await github.rest.issues.updateLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
description: label.description
});
core.info(`Updated label: ${label.name}`);
} catch (err) {
if (err.status === 404) {
// Label doesn't exist β create it
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: label.name,
color: label.color,
description: label.description
});
core.info(`Created label: ${label.name}`);
} else {
throw err;
}
}
}
core.info(`Label sync complete: ${labels.length} labels synced`);