@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.
- package/.copilot/mcp-config.json +14 -0
- package/.copilot/skills/agent-collaboration/SKILL.md +42 -0
- package/.copilot/skills/agent-conduct/SKILL.md +24 -0
- package/.copilot/skills/architectural-proposals/SKILL.md +151 -0
- package/.copilot/skills/ci-validation-gates/SKILL.md +84 -0
- package/.copilot/skills/cli-wiring/SKILL.md +47 -0
- package/.copilot/skills/client-compatibility/SKILL.md +89 -0
- package/.copilot/skills/cross-mesh/SKILL.md +114 -0
- package/.copilot/skills/distributed-mesh/SKILL.md +287 -0
- package/.copilot/skills/distributed-mesh/mesh.json.example +30 -0
- package/.copilot/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/.copilot/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/.copilot/skills/docs-standards/SKILL.md +71 -0
- package/.copilot/skills/economy-mode/SKILL.md +101 -0
- package/.copilot/skills/external-comms/SKILL.md +331 -0
- package/.copilot/skills/gh-auth-isolation/SKILL.md +183 -0
- package/.copilot/skills/git-workflow/SKILL.md +206 -0
- package/.copilot/skills/github-multi-account/SKILL.md +95 -0
- package/.copilot/skills/history-hygiene/SKILL.md +36 -0
- package/.copilot/skills/humanizer/SKILL.md +107 -0
- package/.copilot/skills/init-mode/SKILL.md +101 -0
- package/.copilot/skills/mesh-conventions/SKILL.md +69 -0
- package/.copilot/skills/model-selection/SKILL.md +139 -0
- package/.copilot/skills/nap/SKILL.md +24 -0
- package/.copilot/skills/personal-mesh/SKILL.md +57 -0
- package/.copilot/skills/project-conventions/SKILL.md +56 -0
- package/.copilot/skills/release-process/SKILL.md +435 -0
- package/.copilot/skills/reskill/SKILL.md +92 -0
- package/.copilot/skills/reviewer-protocol/SKILL.md +79 -0
- package/.copilot/skills/secret-handling/SKILL.md +200 -0
- package/.copilot/skills/session-recovery/SKILL.md +155 -0
- package/.copilot/skills/test-discipline/SKILL.md +37 -0
- package/.copilot/skills/windows-compatibility/SKILL.md +74 -0
- package/.github/agents/mercury-mesh.agent.md +1732 -0
- package/.mesh/manifesto.md +66 -0
- package/.mesh/templates/casting/Futurama.json +10 -0
- package/.mesh/templates/casting-history.json +4 -0
- package/.mesh/templates/casting-policy.json +37 -0
- package/.mesh/templates/casting-reference.md +104 -0
- package/.mesh/templates/casting-registry.json +3 -0
- package/.mesh/templates/ceremonies.md +41 -0
- package/.mesh/templates/charter.md +56 -0
- package/.mesh/templates/constraint-tracking.md +38 -0
- package/.mesh/templates/cooperative-rate-limiting.md +229 -0
- package/.mesh/templates/copilot-instructions.md +50 -0
- package/.mesh/templates/department-backlog.md +15 -0
- package/.mesh/templates/department-charter.md +27 -0
- package/.mesh/templates/department-state.json +19 -0
- package/.mesh/templates/history.md +10 -0
- package/.mesh/templates/identity/now.md +9 -0
- package/.mesh/templates/identity/wisdom.md +15 -0
- package/.mesh/templates/interface-contract.md +26 -0
- package/.mesh/templates/issue-lifecycle.md +421 -0
- package/.mesh/templates/keda-scaler.md +166 -0
- package/.mesh/templates/machine-capabilities.md +77 -0
- package/.mesh/templates/mcp-config.md +90 -0
- package/.mesh/templates/mercury-mesh.agent.md +1732 -0
- package/.mesh/templates/multi-agent-format.md +28 -0
- package/.mesh/templates/orchestration-log.md +27 -0
- package/.mesh/templates/org-autonomy-spec.md +152 -0
- package/.mesh/templates/org-backlog-from-triage.js +199 -0
- package/.mesh/templates/org-runtime-reconcile.js +364 -0
- package/.mesh/templates/org-seed-runtime.js +238 -0
- package/.mesh/templates/org-status.js +193 -0
- package/.mesh/templates/org-structure.json +38 -0
- package/.mesh/templates/package.json +3 -0
- package/.mesh/templates/plugin-marketplace.md +49 -0
- package/.mesh/templates/ralph-circuit-breaker.md +313 -0
- package/.mesh/templates/ralph-triage.js +844 -0
- package/.mesh/templates/raw-agent-output.md +37 -0
- package/.mesh/templates/roster.md +60 -0
- package/.mesh/templates/routing.md +78 -0
- package/.mesh/templates/run-output.md +50 -0
- package/.mesh/templates/schedule.json +64 -0
- package/.mesh/templates/scribe-charter.md +119 -0
- package/.mesh/templates/skill.md +24 -0
- package/.mesh/templates/skills/agent-collaboration/SKILL.md +42 -0
- package/.mesh/templates/skills/agent-conduct/SKILL.md +24 -0
- package/.mesh/templates/skills/architectural-proposals/SKILL.md +151 -0
- package/.mesh/templates/skills/ci-validation-gates/SKILL.md +84 -0
- package/.mesh/templates/skills/cli-wiring/SKILL.md +47 -0
- package/.mesh/templates/skills/client-compatibility/SKILL.md +89 -0
- package/.mesh/templates/skills/cross-mesh/SKILL.md +114 -0
- package/.mesh/templates/skills/distributed-mesh/SKILL.md +287 -0
- package/.mesh/templates/skills/distributed-mesh/mesh.json.example +30 -0
- package/.mesh/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/.mesh/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/.mesh/templates/skills/docs-standards/SKILL.md +71 -0
- package/.mesh/templates/skills/economy-mode/SKILL.md +101 -0
- package/.mesh/templates/skills/external-comms/SKILL.md +331 -0
- package/.mesh/templates/skills/gh-auth-isolation/SKILL.md +183 -0
- package/.mesh/templates/skills/git-workflow/SKILL.md +204 -0
- package/.mesh/templates/skills/github-multi-account/SKILL.md +95 -0
- package/.mesh/templates/skills/history-hygiene/SKILL.md +36 -0
- package/.mesh/templates/skills/humanizer/SKILL.md +107 -0
- package/.mesh/templates/skills/init-mode/SKILL.md +101 -0
- package/.mesh/templates/skills/mesh-conventions/SKILL.md +69 -0
- package/.mesh/templates/skills/model-selection/SKILL.md +139 -0
- package/.mesh/templates/skills/nap/SKILL.md +24 -0
- package/.mesh/templates/skills/personal-mesh/SKILL.md +57 -0
- package/.mesh/templates/skills/project-conventions/SKILL.md +56 -0
- package/.mesh/templates/skills/release-process/SKILL.md +435 -0
- package/.mesh/templates/skills/reskill/SKILL.md +92 -0
- package/.mesh/templates/skills/reviewer-protocol/SKILL.md +79 -0
- package/.mesh/templates/skills/secret-handling/SKILL.md +200 -0
- package/.mesh/templates/skills/session-recovery/SKILL.md +155 -0
- package/.mesh/templates/skills/test-discipline/SKILL.md +37 -0
- package/.mesh/templates/skills/windows-compatibility/SKILL.md +74 -0
- package/.mesh/templates/workflows/mesh-ci.yml +24 -0
- package/.mesh/templates/workflows/mesh-docs.yml +54 -0
- package/.mesh/templates/workflows/mesh-heartbeat.yml +237 -0
- package/.mesh/templates/workflows/mesh-insider-release.yml +61 -0
- package/.mesh/templates/workflows/mesh-issue-assign.yml +243 -0
- package/.mesh/templates/workflows/mesh-label-enforce.yml +181 -0
- package/.mesh/templates/workflows/mesh-preview.yml +55 -0
- package/.mesh/templates/workflows/mesh-promote.yml +120 -0
- package/.mesh/templates/workflows/mesh-release.yml +77 -0
- package/.mesh/templates/workflows/mesh-triage.yml +383 -0
- package/.mesh/templates/workflows/sync-mesh-labels.yml +204 -0
- package/README.md +640 -0
- package/bin/mercury-mesh.cjs +317 -0
- package/docs/brand-language.md +287 -0
- package/docs/commander-onboarding.md +462 -0
- package/docs/mercury-mesh-runtime-rename-impact.md +148 -0
- package/docs/persona-manifesto.md +114 -0
- package/docs/scenarios/client-compatibility.md +59 -0
- package/index.cjs +41 -0
- 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`);
|