@mizyoel/mercury-mesh 0.9.4

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 (128) hide show
  1. package/.copilot/mcp-config.json +14 -0
  2. package/.copilot/skills/agent-collaboration/SKILL.md +42 -0
  3. package/.copilot/skills/agent-conduct/SKILL.md +24 -0
  4. package/.copilot/skills/architectural-proposals/SKILL.md +151 -0
  5. package/.copilot/skills/ci-validation-gates/SKILL.md +84 -0
  6. package/.copilot/skills/cli-wiring/SKILL.md +47 -0
  7. package/.copilot/skills/client-compatibility/SKILL.md +89 -0
  8. package/.copilot/skills/cross-mesh/SKILL.md +114 -0
  9. package/.copilot/skills/distributed-mesh/SKILL.md +287 -0
  10. package/.copilot/skills/distributed-mesh/mesh.json.example +30 -0
  11. package/.copilot/skills/distributed-mesh/sync-mesh.ps1 +111 -0
  12. package/.copilot/skills/distributed-mesh/sync-mesh.sh +104 -0
  13. package/.copilot/skills/docs-standards/SKILL.md +71 -0
  14. package/.copilot/skills/economy-mode/SKILL.md +101 -0
  15. package/.copilot/skills/external-comms/SKILL.md +331 -0
  16. package/.copilot/skills/gh-auth-isolation/SKILL.md +183 -0
  17. package/.copilot/skills/git-workflow/SKILL.md +206 -0
  18. package/.copilot/skills/github-multi-account/SKILL.md +95 -0
  19. package/.copilot/skills/history-hygiene/SKILL.md +36 -0
  20. package/.copilot/skills/humanizer/SKILL.md +107 -0
  21. package/.copilot/skills/init-mode/SKILL.md +101 -0
  22. package/.copilot/skills/mesh-conventions/SKILL.md +69 -0
  23. package/.copilot/skills/model-selection/SKILL.md +139 -0
  24. package/.copilot/skills/nap/SKILL.md +24 -0
  25. package/.copilot/skills/personal-mesh/SKILL.md +57 -0
  26. package/.copilot/skills/project-conventions/SKILL.md +56 -0
  27. package/.copilot/skills/release-process/SKILL.md +435 -0
  28. package/.copilot/skills/reskill/SKILL.md +92 -0
  29. package/.copilot/skills/reviewer-protocol/SKILL.md +79 -0
  30. package/.copilot/skills/secret-handling/SKILL.md +200 -0
  31. package/.copilot/skills/session-recovery/SKILL.md +155 -0
  32. package/.copilot/skills/test-discipline/SKILL.md +37 -0
  33. package/.copilot/skills/windows-compatibility/SKILL.md +74 -0
  34. package/.github/agents/mercury-mesh.agent.md +1732 -0
  35. package/.mesh/manifesto.md +66 -0
  36. package/.mesh/templates/casting/Futurama.json +10 -0
  37. package/.mesh/templates/casting-history.json +4 -0
  38. package/.mesh/templates/casting-policy.json +37 -0
  39. package/.mesh/templates/casting-reference.md +104 -0
  40. package/.mesh/templates/casting-registry.json +3 -0
  41. package/.mesh/templates/ceremonies.md +41 -0
  42. package/.mesh/templates/charter.md +56 -0
  43. package/.mesh/templates/constraint-tracking.md +38 -0
  44. package/.mesh/templates/cooperative-rate-limiting.md +229 -0
  45. package/.mesh/templates/copilot-instructions.md +50 -0
  46. package/.mesh/templates/department-backlog.md +15 -0
  47. package/.mesh/templates/department-charter.md +27 -0
  48. package/.mesh/templates/department-state.json +19 -0
  49. package/.mesh/templates/history.md +10 -0
  50. package/.mesh/templates/identity/now.md +9 -0
  51. package/.mesh/templates/identity/wisdom.md +15 -0
  52. package/.mesh/templates/interface-contract.md +26 -0
  53. package/.mesh/templates/issue-lifecycle.md +421 -0
  54. package/.mesh/templates/keda-scaler.md +166 -0
  55. package/.mesh/templates/machine-capabilities.md +77 -0
  56. package/.mesh/templates/mcp-config.md +90 -0
  57. package/.mesh/templates/mercury-mesh.agent.md +1732 -0
  58. package/.mesh/templates/multi-agent-format.md +28 -0
  59. package/.mesh/templates/orchestration-log.md +27 -0
  60. package/.mesh/templates/org-autonomy-spec.md +152 -0
  61. package/.mesh/templates/org-backlog-from-triage.js +199 -0
  62. package/.mesh/templates/org-runtime-reconcile.js +364 -0
  63. package/.mesh/templates/org-seed-runtime.js +238 -0
  64. package/.mesh/templates/org-status.js +193 -0
  65. package/.mesh/templates/org-structure.json +38 -0
  66. package/.mesh/templates/package.json +3 -0
  67. package/.mesh/templates/plugin-marketplace.md +49 -0
  68. package/.mesh/templates/ralph-circuit-breaker.md +313 -0
  69. package/.mesh/templates/ralph-triage.js +844 -0
  70. package/.mesh/templates/raw-agent-output.md +37 -0
  71. package/.mesh/templates/roster.md +60 -0
  72. package/.mesh/templates/routing.md +78 -0
  73. package/.mesh/templates/run-output.md +50 -0
  74. package/.mesh/templates/schedule.json +64 -0
  75. package/.mesh/templates/scribe-charter.md +119 -0
  76. package/.mesh/templates/skill.md +24 -0
  77. package/.mesh/templates/skills/agent-collaboration/SKILL.md +42 -0
  78. package/.mesh/templates/skills/agent-conduct/SKILL.md +24 -0
  79. package/.mesh/templates/skills/architectural-proposals/SKILL.md +151 -0
  80. package/.mesh/templates/skills/ci-validation-gates/SKILL.md +84 -0
  81. package/.mesh/templates/skills/cli-wiring/SKILL.md +47 -0
  82. package/.mesh/templates/skills/client-compatibility/SKILL.md +89 -0
  83. package/.mesh/templates/skills/cross-mesh/SKILL.md +114 -0
  84. package/.mesh/templates/skills/distributed-mesh/SKILL.md +287 -0
  85. package/.mesh/templates/skills/distributed-mesh/mesh.json.example +30 -0
  86. package/.mesh/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
  87. package/.mesh/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
  88. package/.mesh/templates/skills/docs-standards/SKILL.md +71 -0
  89. package/.mesh/templates/skills/economy-mode/SKILL.md +101 -0
  90. package/.mesh/templates/skills/external-comms/SKILL.md +331 -0
  91. package/.mesh/templates/skills/gh-auth-isolation/SKILL.md +183 -0
  92. package/.mesh/templates/skills/git-workflow/SKILL.md +204 -0
  93. package/.mesh/templates/skills/github-multi-account/SKILL.md +95 -0
  94. package/.mesh/templates/skills/history-hygiene/SKILL.md +36 -0
  95. package/.mesh/templates/skills/humanizer/SKILL.md +107 -0
  96. package/.mesh/templates/skills/init-mode/SKILL.md +101 -0
  97. package/.mesh/templates/skills/mesh-conventions/SKILL.md +69 -0
  98. package/.mesh/templates/skills/model-selection/SKILL.md +139 -0
  99. package/.mesh/templates/skills/nap/SKILL.md +24 -0
  100. package/.mesh/templates/skills/personal-mesh/SKILL.md +57 -0
  101. package/.mesh/templates/skills/project-conventions/SKILL.md +56 -0
  102. package/.mesh/templates/skills/release-process/SKILL.md +435 -0
  103. package/.mesh/templates/skills/reskill/SKILL.md +92 -0
  104. package/.mesh/templates/skills/reviewer-protocol/SKILL.md +79 -0
  105. package/.mesh/templates/skills/secret-handling/SKILL.md +200 -0
  106. package/.mesh/templates/skills/session-recovery/SKILL.md +155 -0
  107. package/.mesh/templates/skills/test-discipline/SKILL.md +37 -0
  108. package/.mesh/templates/skills/windows-compatibility/SKILL.md +74 -0
  109. package/.mesh/templates/workflows/mesh-ci.yml +24 -0
  110. package/.mesh/templates/workflows/mesh-docs.yml +54 -0
  111. package/.mesh/templates/workflows/mesh-heartbeat.yml +237 -0
  112. package/.mesh/templates/workflows/mesh-insider-release.yml +61 -0
  113. package/.mesh/templates/workflows/mesh-issue-assign.yml +243 -0
  114. package/.mesh/templates/workflows/mesh-label-enforce.yml +181 -0
  115. package/.mesh/templates/workflows/mesh-preview.yml +55 -0
  116. package/.mesh/templates/workflows/mesh-promote.yml +120 -0
  117. package/.mesh/templates/workflows/mesh-release.yml +77 -0
  118. package/.mesh/templates/workflows/mesh-triage.yml +383 -0
  119. package/.mesh/templates/workflows/sync-mesh-labels.yml +204 -0
  120. package/README.md +640 -0
  121. package/bin/mercury-mesh.cjs +317 -0
  122. package/docs/brand-language.md +287 -0
  123. package/docs/commander-onboarding.md +462 -0
  124. package/docs/mercury-mesh-runtime-rename-impact.md +148 -0
  125. package/docs/persona-manifesto.md +114 -0
  126. package/docs/scenarios/client-compatibility.md +59 -0
  127. package/index.cjs +41 -0
  128. package/package.json +43 -0
@@ -0,0 +1,383 @@
1
+ name: Mercury Mesh Triage
2
+
3
+ on:
4
+ issues:
5
+ types: [labeled]
6
+
7
+ permissions:
8
+ issues: write
9
+ contents: read
10
+
11
+ jobs:
12
+ triage:
13
+ if: github.event.label.name == 'mesh'
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Triage issue via Lead agent
19
+ uses: actions/github-script@v7
20
+ with:
21
+ script: |
22
+ const fs = require('fs');
23
+ const issue = context.payload.issue;
24
+ const runtimeCandidates = ['.mesh', '.mercury', '.ai-team'];
25
+ const stateCandidates = ['.mesh', '.mercury'];
26
+ const findFirst = (candidates) => candidates.find((candidate) => fs.existsSync(candidate)) || null;
27
+ const currentBaseLabel = 'mesh';
28
+
29
+ const teamFile = findFirst(runtimeCandidates.map((candidate) => `${candidate}/team.md`));
30
+ if (!teamFile || !fs.existsSync(teamFile)) {
31
+ core.warning('No .mesh/team.md, .mercury/team.md, or .ai-team/team.md found — cannot run Mercury Mesh triage');
32
+ return;
33
+ }
34
+
35
+ const runtimeDir = teamFile.slice(0, teamFile.lastIndexOf('/'));
36
+ const stateDir = runtimeDir !== '.ai-team'
37
+ ? runtimeDir
38
+ : (findFirst(stateCandidates.map((candidate) => `${candidate}/config.json`)) || '.mesh/config.json').replace(/\/config\.json$/, '');
39
+ const labelPrefix = 'mesh';
40
+
41
+ const content = fs.readFileSync(teamFile, 'utf8');
42
+ const lines = content.split('\n');
43
+
44
+ // Check if @copilot is on the team
45
+ const hasCopilot = content.includes('🤖 Coding Agent');
46
+ const copilotAutoAssign = content.includes('<!-- copilot-auto-assign: true -->');
47
+
48
+ // Parse @copilot capability profile
49
+ let goodFitKeywords = [];
50
+ let needsReviewKeywords = [];
51
+ let notSuitableKeywords = [];
52
+
53
+ if (hasCopilot) {
54
+ // Extract capability tiers from team.md
55
+ const goodFitMatch = content.match(/🟢\s*Good fit[^:]*:\s*(.+)/i);
56
+ const needsReviewMatch = content.match(/🟡\s*Needs review[^:]*:\s*(.+)/i);
57
+ const notSuitableMatch = content.match(/🔴\s*Not suitable[^:]*:\s*(.+)/i);
58
+
59
+ if (goodFitMatch) {
60
+ goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim());
61
+ } else {
62
+ goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation'];
63
+ }
64
+ if (needsReviewMatch) {
65
+ needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim());
66
+ } else {
67
+ needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration'];
68
+ }
69
+ if (notSuitableMatch) {
70
+ notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim());
71
+ } else {
72
+ notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance'];
73
+ }
74
+ }
75
+
76
+ const members = [];
77
+ let inMembersTable = false;
78
+ for (const line of lines) {
79
+ if (line.match(/^##\s+(Members|Team Roster)/i)) {
80
+ inMembersTable = true;
81
+ continue;
82
+ }
83
+ if (inMembersTable && line.startsWith('## ')) {
84
+ break;
85
+ }
86
+ if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
87
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
88
+ if (cells.length >= 2 && cells[0] !== 'Scribe') {
89
+ members.push({
90
+ name: cells[0],
91
+ role: cells[1]
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ const routingFile = findFirst([`${runtimeDir}/routing.md`, '.ai-team/routing.md']) || `${runtimeDir}/routing.md`;
98
+ let routingContent = '';
99
+ if (fs.existsSync(routingFile)) {
100
+ routingContent = fs.readFileSync(routingFile, 'utf8');
101
+ }
102
+
103
+ let orgConfig = null;
104
+ let orgStructure = null;
105
+ try {
106
+ if (fs.existsSync(`${stateDir}/config.json`)) {
107
+ orgConfig = JSON.parse(fs.readFileSync(`${stateDir}/config.json`, 'utf8'));
108
+ }
109
+ } catch (err) {
110
+ core.warning(`Could not parse ${stateDir}/config.json: ${err.message}`);
111
+ }
112
+ try {
113
+ if (fs.existsSync(`${stateDir}/org/structure.json`)) {
114
+ orgStructure = JSON.parse(fs.readFileSync(`${stateDir}/org/structure.json`, 'utf8'));
115
+ }
116
+ } catch (err) {
117
+ core.warning(`Could not parse ${stateDir}/org/structure.json: ${err.message}`);
118
+ }
119
+
120
+ const normalize = (value = '') => value.toLowerCase().replace(/[^\w@-]+/g, ' ').trim();
121
+ const memberKey = (value = '') => normalize(value);
122
+ const memberByName = new Map(members.map(member => [memberKey(member.name), member]));
123
+ const memberDepartment = new Map();
124
+ const orgModeEnabled = !!(orgConfig && orgConfig.orgMode && orgStructure && Array.isArray(orgStructure.departments));
125
+ const departments = orgModeEnabled ? orgStructure.departments : [];
126
+
127
+ for (const dept of departments) {
128
+ const allNames = [...(dept.members || []), dept.lead].filter(Boolean);
129
+ for (const name of allNames) {
130
+ memberDepartment.set(memberKey(String(name)), dept);
131
+ }
132
+ }
133
+
134
+ const scoreMemberForIssue = (member, text) => {
135
+ const role = member.role.toLowerCase();
136
+ let score = 0;
137
+
138
+ if ((role.includes('lead') || role.includes('architect')) &&
139
+ (text.includes('architecture') || text.includes('design') || text.includes('structure') || text.includes('trade-off'))) {
140
+ score += 4;
141
+ }
142
+ if ((role.includes('codebase') || role.includes('analyst')) &&
143
+ (text.includes('analysis') || text.includes('analyze') || text.includes('flow') || text.includes('trace') || text.includes('codebase'))) {
144
+ score += 3;
145
+ }
146
+ if (role.includes('systems') &&
147
+ (text.includes('system') || text.includes('tooling') || text.includes('dependency') || text.includes('workflow') || text.includes('config'))) {
148
+ score += 3;
149
+ }
150
+ if ((role.includes('ux') || role.includes('ui')) &&
151
+ (text.includes('ux') || text.includes('ui') || text.includes('design') || text.includes('usability') || text.includes('surface'))) {
152
+ score += 3;
153
+ }
154
+ if ((role.includes('frontend') || role.includes('ui')) &&
155
+ (text.includes('frontend') || text.includes('css') || text.includes('layout') || text.includes('component'))) {
156
+ score += 3;
157
+ }
158
+ if ((role.includes('backend') || role.includes('api') || role.includes('server')) &&
159
+ (text.includes('backend') || text.includes('api') || text.includes('database') || text.includes('endpoint') || text.includes('auth'))) {
160
+ score += 3;
161
+ }
162
+ if ((role.includes('test') || role.includes('qa') || role.includes('quality')) &&
163
+ (text.includes('test') || text.includes('bug') || text.includes('fix') || text.includes('coverage'))) {
164
+ score += 3;
165
+ }
166
+
167
+ return score;
168
+ };
169
+
170
+ const findBestDepartmentMatch = (text) => {
171
+ let bestDept = null;
172
+ let bestScore = 0;
173
+ for (const dept of departments) {
174
+ const keywords = [...(dept.domain || []), ...(dept.routingKeywords || [])]
175
+ .map(keyword => String(keyword).toLowerCase())
176
+ .filter(Boolean);
177
+ const matched = keywords.filter(keyword => text.includes(keyword));
178
+ if (matched.length > bestScore) {
179
+ bestDept = dept;
180
+ bestScore = matched.length;
181
+ }
182
+ }
183
+ return bestScore > 0 ? bestDept : null;
184
+ };
185
+
186
+ const findBestMemberInDepartment = (dept, text) => {
187
+ const deptNames = new Set([...(dept.members || []), dept.lead].filter(Boolean).map(name => memberKey(String(name))));
188
+ const deptMembers = members.filter(member => deptNames.has(memberKey(member.name)));
189
+ let bestMember = null;
190
+ let bestScore = -1;
191
+
192
+ for (const member of deptMembers) {
193
+ const score = scoreMemberForIssue(member, text);
194
+ if (score > bestScore) {
195
+ bestMember = member;
196
+ bestScore = score;
197
+ }
198
+ }
199
+
200
+ if (bestMember) return bestMember;
201
+ return memberByName.get(memberKey(String(dept.lead))) || deptMembers[0] || null;
202
+ };
203
+
204
+ // Find the Lead
205
+ const lead = members.find(m =>
206
+ m.role.toLowerCase().includes('lead') ||
207
+ m.role.toLowerCase().includes('architect') ||
208
+ m.role.toLowerCase().includes('coordinator')
209
+ );
210
+
211
+ if (!lead) {
212
+ core.warning('No Lead role found in team roster — cannot triage');
213
+ return;
214
+ }
215
+
216
+ // Build triage context
217
+ const memberList = members.map(m =>
218
+ `- **${m.name}** (${m.role}) → label: \`${labelPrefix}:${m.name.toLowerCase()}\``
219
+ ).join('\n');
220
+
221
+ // Determine best assignee based on issue content and routing
222
+ const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase();
223
+
224
+ let assignedMember = null;
225
+ let assignedDepartment = null;
226
+ let triageReason = '';
227
+ let copilotTier = null;
228
+
229
+ // First, evaluate @copilot fit if enabled
230
+ if (hasCopilot) {
231
+ const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw));
232
+ const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw));
233
+ const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw));
234
+
235
+ if (isGoodFit) {
236
+ copilotTier = 'good-fit';
237
+ assignedMember = { name: '@copilot', role: 'Coding Agent' };
238
+ triageReason = '🟢 Good fit for @copilot — matches capability profile';
239
+ } else if (isNeedsReview) {
240
+ copilotTier = 'needs-review';
241
+ assignedMember = { name: '@copilot', role: 'Coding Agent' };
242
+ triageReason = '🟡 Routing to @copilot (needs review) — a Mercury Mesh member should review the PR';
243
+ } else if (isNotSuitable) {
244
+ copilotTier = 'not-suitable';
245
+ // Fall through to normal routing
246
+ }
247
+ }
248
+
249
+ // If not routed to @copilot, use keyword-based routing
250
+ if (!assignedMember) {
251
+ for (const member of members) {
252
+ const role = member.role.toLowerCase();
253
+ if ((role.includes('frontend') || role.includes('ui')) &&
254
+ (issueText.includes('ui') || issueText.includes('frontend') ||
255
+ issueText.includes('css') || issueText.includes('component') ||
256
+ issueText.includes('button') || issueText.includes('page') ||
257
+ issueText.includes('layout') || issueText.includes('design'))) {
258
+ assignedMember = member;
259
+ triageReason = 'Issue relates to frontend/UI work';
260
+ break;
261
+ }
262
+ if ((role.includes('backend') || role.includes('api') || role.includes('server')) &&
263
+ (issueText.includes('api') || issueText.includes('backend') ||
264
+ issueText.includes('database') || issueText.includes('endpoint') ||
265
+ issueText.includes('server') || issueText.includes('auth'))) {
266
+ assignedMember = member;
267
+ triageReason = 'Issue relates to backend/API work';
268
+ break;
269
+ }
270
+ if ((role.includes('test') || role.includes('qa') || role.includes('quality')) &&
271
+ (issueText.includes('test') || issueText.includes('bug') ||
272
+ issueText.includes('fix') || issueText.includes('regression') ||
273
+ issueText.includes('coverage'))) {
274
+ assignedMember = member;
275
+ triageReason = 'Issue relates to testing/quality work';
276
+ break;
277
+ }
278
+ if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) &&
279
+ (issueText.includes('deploy') || issueText.includes('ci') ||
280
+ issueText.includes('pipeline') || issueText.includes('docker') ||
281
+ issueText.includes('infrastructure'))) {
282
+ assignedMember = member;
283
+ triageReason = 'Issue relates to DevOps/infrastructure work';
284
+ break;
285
+ }
286
+ }
287
+ }
288
+
289
+ if (!assignedMember && orgModeEnabled) {
290
+ const matchedDepartment = findBestDepartmentMatch(issueText);
291
+ if (matchedDepartment) {
292
+ assignedDepartment = matchedDepartment;
293
+ assignedMember = findBestMemberInDepartment(matchedDepartment, issueText);
294
+ if (assignedMember) {
295
+ triageReason = `Hierarchy match — routed via ${matchedDepartment.name}`;
296
+ }
297
+ }
298
+ }
299
+
300
+ // Default to Lead if no routing match
301
+ if (!assignedMember) {
302
+ assignedMember = lead;
303
+ triageReason = 'No specific domain match — assigned to Lead for further analysis';
304
+ }
305
+
306
+ const isCopilot = assignedMember.name === '@copilot';
307
+ if (!assignedDepartment && !isCopilot && orgModeEnabled) {
308
+ assignedDepartment = memberDepartment.get(memberKey(assignedMember.name)) || null;
309
+ }
310
+ const assignLabel = isCopilot ? `${labelPrefix}:copilot` : `${labelPrefix}:${assignedMember.name.toLowerCase()}`;
311
+ const labelsToAdd = [assignLabel];
312
+ if (assignedDepartment && assignedDepartment.id) {
313
+ labelsToAdd.push(`dept:${String(assignedDepartment.id).toLowerCase()}`);
314
+ }
315
+
316
+ // Add the member-specific label
317
+ await github.rest.issues.addLabels({
318
+ owner: context.repo.owner,
319
+ repo: context.repo.repo,
320
+ issue_number: issue.number,
321
+ labels: labelsToAdd
322
+ });
323
+
324
+ // Apply default triage verdict
325
+ await github.rest.issues.addLabels({
326
+ owner: context.repo.owner,
327
+ repo: context.repo.repo,
328
+ issue_number: issue.number,
329
+ labels: ['go:needs-research']
330
+ });
331
+
332
+ // Auto-assign @copilot if enabled
333
+ if (isCopilot && copilotAutoAssign) {
334
+ try {
335
+ await github.rest.issues.addAssignees({
336
+ owner: context.repo.owner,
337
+ repo: context.repo.repo,
338
+ issue_number: issue.number,
339
+ assignees: ['copilot']
340
+ });
341
+ } catch (err) {
342
+ core.warning(`Could not auto-assign @copilot: ${err.message}`);
343
+ }
344
+ }
345
+
346
+ // Build copilot evaluation note
347
+ let copilotNote = '';
348
+ if (hasCopilot && !isCopilot) {
349
+ if (copilotTier === 'not-suitable') {
350
+ copilotNote = `\n\n**@copilot evaluation:** 🔴 Not suitable — issue involves work outside the coding agent's capability profile.`;
351
+ } else {
352
+ copilotNote = `\n\n**@copilot evaluation:** No strong capability match — routed to a Mercury Mesh specialist.`;
353
+ }
354
+ }
355
+
356
+ // Post triage comment
357
+ const comment = [
358
+ `### 🏗️ Mercury Mesh Triage — ${lead.name} (${lead.role})`,
359
+ '',
360
+ `**Issue:** #${issue.number} — ${issue.title}`,
361
+ `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`,
362
+ assignedDepartment ? `**Department:** ${assignedDepartment.name}` : '',
363
+ `**Reason:** ${triageReason}`,
364
+ copilotTier === 'needs-review' ? `\n⚠️ **PR review recommended** — a Mercury Mesh member should review @copilot's work on this one.` : '',
365
+ copilotNote,
366
+ '',
367
+ `---`,
368
+ '',
369
+ `**Bridge roster:**`,
370
+ memberList,
371
+ hasCopilot ? `- **@copilot** (Coding Agent) → label: \`${labelPrefix}:copilot\`` : '',
372
+ '',
373
+ `> To reassign, remove the current \`${labelPrefix}:*\` label and add the correct one. Keep any \`dept:*\` labels as metadata unless the routing changes.`,
374
+ ].filter(Boolean).join('\n');
375
+
376
+ await github.rest.issues.createComment({
377
+ owner: context.repo.owner,
378
+ repo: context.repo.repo,
379
+ issue_number: issue.number,
380
+ body: comment
381
+ });
382
+
383
+ core.info(`Triaged issue #${issue.number} → ${assignedMember.name} (${assignLabel})`);
@@ -0,0 +1,204 @@
1
+ name: Sync Mercury Mesh Labels
2
+
3
+ on:
4
+ push:
5
+ paths:
6
+ - '.mesh/team.md'
7
+ - '.mesh/org/structure.json'
8
+ - '.mercury/team.md'
9
+ - '.mercury/org/structure.json'
10
+ - '.ai-team/team.md'
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ issues: write
15
+ contents: read
16
+
17
+ jobs:
18
+ sync-labels:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Parse roster and sync labels
24
+ uses: actions/github-script@v7
25
+ with:
26
+ script: |
27
+ const fs = require('fs');
28
+ const runtimeCandidates = ['.mesh', '.mercury', '.ai-team'];
29
+ const stateCandidates = ['.mesh', '.mercury'];
30
+ const findFirst = (candidates) => candidates.find((candidate) => fs.existsSync(candidate)) || null;
31
+ const teamFile = findFirst(runtimeCandidates.map((candidate) => `${candidate}/team.md`));
32
+
33
+ if (!teamFile || !fs.existsSync(teamFile)) {
34
+ core.info('No runtime roster found — skipping Mercury Mesh label sync');
35
+ return;
36
+ }
37
+
38
+ const runtimeDir = teamFile.slice(0, teamFile.lastIndexOf('/'));
39
+ const stateDir = runtimeDir !== '.ai-team'
40
+ ? runtimeDir
41
+ : (findFirst(stateCandidates.map((candidate) => `${candidate}/config.json`)) || '.mesh/config.json').replace(/\/config\.json$/, '');
42
+ const content = fs.readFileSync(teamFile, 'utf8');
43
+ const lines = content.split('\n');
44
+ let orgStructure = null;
45
+ const orgStructurePath = `${stateDir}/org/structure.json`;
46
+ if (fs.existsSync(orgStructurePath)) {
47
+ try {
48
+ orgStructure = JSON.parse(fs.readFileSync(orgStructurePath, 'utf8'));
49
+ } catch (err) {
50
+ core.warning(`Could not parse ${orgStructurePath}: ${err.message}`);
51
+ }
52
+ }
53
+
54
+ // Parse the Members table for agent names
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('## ')) {
63
+ break;
64
+ }
65
+ if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) {
66
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
67
+ if (cells.length >= 2 && cells[0] !== 'Scribe') {
68
+ members.push({
69
+ name: cells[0],
70
+ role: cells[1]
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ core.info(`Found ${members.length} Mercury Mesh members: ${members.map(m => m.name).join(', ')}`);
77
+
78
+ // Check if @copilot is on the team
79
+ const hasCopilot = content.includes('🤖 Coding Agent');
80
+
81
+ // Define color palette for Mercury Mesh labels
82
+ const MESH_COLOR = '9B8FCC';
83
+ const MEMBER_COLOR = '9B8FCC';
84
+ const COPILOT_COLOR = '10b981';
85
+ const DEPT_COLOR = 'A2EEEF';
86
+ const ESCALATE_LEAD_COLOR = 'FBCA04';
87
+ const ESCALATE_COORDINATOR_COLOR = 'D93F0B';
88
+
89
+ // Define go: and release: labels (static)
90
+ const GO_LABELS = [
91
+ { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' },
92
+ { name: 'go:no', color: 'B60205', description: 'Not pursuing' },
93
+ { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' }
94
+ ];
95
+
96
+ const RELEASE_LABELS = [
97
+ { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' },
98
+ { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' },
99
+ { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' },
100
+ { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' },
101
+ { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' }
102
+ ];
103
+
104
+ const TYPE_LABELS = [
105
+ { name: 'type:feature', color: 'DDD1F2', description: 'New capability' },
106
+ { name: 'type:bug', color: 'FF0422', description: 'Something broken' },
107
+ { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' },
108
+ { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' },
109
+ { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' },
110
+ { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' }
111
+ ];
112
+
113
+ // High-signal labels — these MUST visually dominate all others
114
+ const SIGNAL_LABELS = [
115
+ { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' },
116
+ { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' }
117
+ ];
118
+
119
+ const PRIORITY_LABELS = [
120
+ { name: 'priority:p0', color: 'B60205', description: 'Blocking release' },
121
+ { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' },
122
+ { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' }
123
+ ];
124
+
125
+ // Mercury Mesh label family.
126
+ const labels = [
127
+ { name: 'mesh', color: MESH_COLOR, description: 'Mercury Mesh triage inbox' }
128
+ ];
129
+
130
+ for (const member of members) {
131
+ labels.push({
132
+ name: `mesh:${member.name.toLowerCase()}`,
133
+ color: MEMBER_COLOR,
134
+ description: `Assigned to ${member.name} (${member.role})`
135
+ });
136
+ }
137
+
138
+ // Add @copilot label if coding agent is on the team
139
+ if (hasCopilot) {
140
+ labels.push({
141
+ name: 'mesh:copilot',
142
+ color: COPILOT_COLOR,
143
+ description: 'Assigned to @copilot (Coding Agent) for Mercury Mesh autonomous work'
144
+ });
145
+ }
146
+
147
+ if (orgStructure && Array.isArray(orgStructure.departments)) {
148
+ for (const dept of orgStructure.departments) {
149
+ if (!dept || !dept.id) continue;
150
+ labels.push({
151
+ name: `dept:${String(dept.id).toLowerCase()}`,
152
+ color: DEPT_COLOR,
153
+ description: `Department: ${dept.name || dept.id} (lead: ${dept.lead || 'n/a'})`
154
+ });
155
+ }
156
+
157
+ labels.push(
158
+ { name: 'escalate:lead', color: ESCALATE_LEAD_COLOR, description: 'Escalated to department lead' },
159
+ { name: 'escalate:coordinator', color: ESCALATE_COORDINATOR_COLOR, description: 'Escalated to coordinator/org level' }
160
+ );
161
+ }
162
+
163
+ // Add go:, release:, type:, priority:, and high-signal labels
164
+ labels.push(...GO_LABELS);
165
+ labels.push(...RELEASE_LABELS);
166
+ labels.push(...TYPE_LABELS);
167
+ labels.push(...PRIORITY_LABELS);
168
+ labels.push(...SIGNAL_LABELS);
169
+
170
+ // Sync labels (create or update)
171
+ for (const label of labels) {
172
+ try {
173
+ await github.rest.issues.getLabel({
174
+ owner: context.repo.owner,
175
+ repo: context.repo.repo,
176
+ name: label.name
177
+ });
178
+ // Label exists — update it
179
+ await github.rest.issues.updateLabel({
180
+ owner: context.repo.owner,
181
+ repo: context.repo.repo,
182
+ name: label.name,
183
+ color: label.color,
184
+ description: label.description
185
+ });
186
+ core.info(`Updated label: ${label.name}`);
187
+ } catch (err) {
188
+ if (err.status === 404) {
189
+ // Label doesn't exist — create it
190
+ await github.rest.issues.createLabel({
191
+ owner: context.repo.owner,
192
+ repo: context.repo.repo,
193
+ name: label.name,
194
+ color: label.color,
195
+ description: label.description
196
+ });
197
+ core.info(`Created label: ${label.name}`);
198
+ } else {
199
+ throw err;
200
+ }
201
+ }
202
+ }
203
+
204
+ core.info(`Label sync complete: ${labels.length} labels synced`);