@bradygaster/squad-sdk 0.8.25 → 0.9.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.
- package/dist/adapter/client.d.ts +17 -0
- package/dist/adapter/client.d.ts.map +1 -1
- package/dist/adapter/client.js +101 -1
- package/dist/adapter/client.js.map +1 -1
- package/dist/agents/history-shadow.d.ts.map +1 -1
- package/dist/agents/history-shadow.js +99 -32
- package/dist/agents/history-shadow.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/model-selector.d.ts +2 -0
- package/dist/agents/model-selector.d.ts.map +1 -1
- package/dist/agents/model-selector.js +41 -35
- package/dist/agents/model-selector.js.map +1 -1
- package/dist/agents/personal.d.ts +35 -0
- package/dist/agents/personal.d.ts.map +1 -0
- package/dist/agents/personal.js +67 -0
- package/dist/agents/personal.js.map +1 -0
- package/dist/builders/index.d.ts +3 -2
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +28 -0
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/types.d.ts +13 -0
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/config/init.d.ts +8 -0
- package/dist/config/init.d.ts.map +1 -1
- package/dist/config/init.js +131 -20
- package/dist/config/init.js.map +1 -1
- package/dist/config/models.d.ts +112 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +329 -18
- package/dist/config/models.js.map +1 -1
- package/dist/coordinator/index.js +2 -2
- package/dist/coordinator/index.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/platform/azure-devops.d.ts +42 -0
- package/dist/platform/azure-devops.d.ts.map +1 -1
- package/dist/platform/azure-devops.js +75 -0
- package/dist/platform/azure-devops.js.map +1 -1
- package/dist/platform/comms-file-log.d.ts.map +1 -1
- package/dist/platform/comms-file-log.js +2 -1
- package/dist/platform/comms-file-log.js.map +1 -1
- package/dist/platform/index.d.ts +2 -1
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +1 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/ralph/capabilities.d.ts +67 -0
- package/dist/ralph/capabilities.d.ts.map +1 -0
- package/dist/ralph/capabilities.js +111 -0
- package/dist/ralph/capabilities.js.map +1 -0
- package/dist/ralph/index.d.ts +2 -0
- package/dist/ralph/index.d.ts.map +1 -1
- package/dist/ralph/index.js +6 -5
- package/dist/ralph/index.js.map +1 -1
- package/dist/ralph/rate-limiting.d.ts +99 -0
- package/dist/ralph/rate-limiting.d.ts.map +1 -0
- package/dist/ralph/rate-limiting.js +170 -0
- package/dist/ralph/rate-limiting.js.map +1 -0
- package/dist/resolution.d.ts +24 -2
- package/dist/resolution.d.ts.map +1 -1
- package/dist/resolution.js +106 -6
- package/dist/resolution.js.map +1 -1
- package/dist/roles/catalog-categories.d.ts +146 -0
- package/dist/roles/catalog-categories.d.ts.map +1 -0
- package/dist/roles/catalog-categories.js +374 -0
- package/dist/roles/catalog-categories.js.map +1 -0
- package/dist/roles/catalog-engineering.d.ts +212 -0
- package/dist/roles/catalog-engineering.d.ts.map +1 -0
- package/dist/roles/catalog-engineering.js +549 -0
- package/dist/roles/catalog-engineering.js.map +1 -0
- package/dist/roles/catalog.d.ts +24 -0
- package/dist/roles/catalog.d.ts.map +1 -0
- package/dist/roles/catalog.js +28 -0
- package/dist/roles/catalog.js.map +1 -0
- package/dist/roles/index.d.ts +69 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/dist/roles/index.js +197 -0
- package/dist/roles/index.js.map +1 -0
- package/dist/roles/types.d.ts +87 -0
- package/dist/roles/types.d.ts.map +1 -0
- package/dist/roles/types.js +14 -0
- package/dist/roles/types.js.map +1 -0
- package/dist/runtime/benchmarks.js +5 -5
- package/dist/runtime/benchmarks.js.map +1 -1
- package/dist/runtime/constants.d.ts +2 -2
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +5 -3
- package/dist/runtime/constants.js.map +1 -1
- package/dist/runtime/cross-squad.d.ts +118 -0
- package/dist/runtime/cross-squad.d.ts.map +1 -0
- package/dist/runtime/cross-squad.js +234 -0
- package/dist/runtime/cross-squad.js.map +1 -0
- package/dist/runtime/otel-init.d.ts +24 -17
- package/dist/runtime/otel-init.d.ts.map +1 -1
- package/dist/runtime/otel-init.js +29 -20
- package/dist/runtime/otel-init.js.map +1 -1
- package/dist/runtime/otel-metrics.d.ts +5 -0
- package/dist/runtime/otel-metrics.d.ts.map +1 -1
- package/dist/runtime/otel-metrics.js +54 -0
- package/dist/runtime/otel-metrics.js.map +1 -1
- package/dist/runtime/rework.d.ts +71 -0
- package/dist/runtime/rework.d.ts.map +1 -0
- package/dist/runtime/rework.js +107 -0
- package/dist/runtime/rework.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +128 -0
- package/dist/runtime/scheduler.d.ts.map +1 -0
- package/dist/runtime/scheduler.js +427 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/squad-observer.d.ts.map +1 -1
- package/dist/runtime/squad-observer.js +4 -0
- package/dist/runtime/squad-observer.js.map +1 -1
- package/dist/runtime/streaming.d.ts +2 -0
- package/dist/runtime/streaming.d.ts.map +1 -1
- package/dist/runtime/streaming.js +6 -0
- package/dist/runtime/streaming.js.map +1 -1
- package/dist/runtime/telemetry.d.ts +2 -0
- package/dist/runtime/telemetry.d.ts.map +1 -1
- package/dist/runtime/telemetry.js +6 -0
- package/dist/runtime/telemetry.js.map +1 -1
- package/dist/sharing/consult.d.ts +2 -2
- package/dist/sharing/consult.js +6 -6
- package/dist/sharing/consult.js.map +1 -1
- package/dist/sharing/export.d.ts.map +1 -1
- package/dist/sharing/export.js +17 -4
- package/dist/sharing/export.js.map +1 -1
- package/dist/skills/handler-types.d.ts +271 -0
- package/dist/skills/handler-types.d.ts.map +1 -0
- package/dist/skills/handler-types.js +31 -0
- package/dist/skills/handler-types.js.map +1 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +3 -0
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/skill-script-loader.d.ts +65 -0
- package/dist/skills/skill-script-loader.d.ts.map +1 -0
- package/dist/skills/skill-script-loader.js +227 -0
- package/dist/skills/skill-script-loader.js.map +1 -0
- package/dist/skills/skill-source.d.ts.map +1 -1
- package/dist/skills/skill-source.js +5 -1
- package/dist/skills/skill-source.js.map +1 -1
- package/dist/tools/index.d.ts +10 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +49 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/upstream/resolver.d.ts.map +1 -1
- package/dist/upstream/resolver.js +14 -5
- package/dist/upstream/resolver.js.map +1 -1
- package/package.json +34 -3
- package/templates/casting/Futurama.json +10 -0
- package/templates/casting-policy.json +4 -2
- package/templates/casting-reference.md +104 -0
- package/templates/cooperative-rate-limiting.md +229 -0
- package/templates/issue-lifecycle.md +412 -0
- package/templates/keda-scaler.md +164 -0
- package/templates/machine-capabilities.md +75 -0
- package/templates/mcp-config.md +0 -8
- package/templates/orchestration-log.md +27 -27
- package/templates/package.json +3 -0
- package/templates/ralph-circuit-breaker.md +313 -0
- package/templates/ralph-triage.js +543 -0
- package/templates/routing.md +5 -20
- package/templates/schedule.json +19 -0
- package/templates/scribe-charter.md +1 -1
- package/templates/skills/agent-collaboration/SKILL.md +42 -0
- package/templates/skills/agent-conduct/SKILL.md +24 -0
- package/templates/skills/architectural-proposals/SKILL.md +151 -0
- package/templates/skills/ci-validation-gates/SKILL.md +84 -0
- package/templates/skills/cli-wiring/SKILL.md +47 -0
- package/templates/skills/client-compatibility/SKILL.md +89 -0
- package/templates/skills/cross-squad/SKILL.md +114 -0
- package/templates/skills/distributed-mesh/SKILL.md +287 -0
- package/templates/skills/distributed-mesh/mesh.json.example +30 -0
- package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
- package/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
- package/templates/skills/docs-standards/SKILL.md +71 -0
- package/templates/skills/economy-mode/SKILL.md +114 -0
- package/templates/skills/external-comms/SKILL.md +329 -0
- package/templates/skills/gh-auth-isolation/SKILL.md +183 -0
- package/templates/skills/git-workflow/SKILL.md +204 -0
- package/templates/skills/github-multi-account/SKILL.md +95 -0
- package/templates/skills/history-hygiene/SKILL.md +36 -0
- package/templates/skills/humanizer/SKILL.md +105 -0
- package/templates/skills/init-mode/SKILL.md +102 -0
- package/templates/skills/model-selection/SKILL.md +117 -0
- package/templates/skills/nap/SKILL.md +24 -0
- package/templates/skills/personal-squad/SKILL.md +57 -0
- package/templates/skills/release-process/SKILL.md +423 -0
- package/templates/skills/reskill/SKILL.md +92 -0
- package/templates/skills/reviewer-protocol/SKILL.md +79 -0
- package/templates/skills/secret-handling/SKILL.md +200 -0
- package/templates/skills/session-recovery/SKILL.md +155 -0
- package/templates/skills/squad-conventions/SKILL.md +69 -0
- package/templates/skills/test-discipline/SKILL.md +37 -0
- package/templates/skills/windows-compatibility/SKILL.md +74 -0
- package/templates/squad.agent.md +1287 -1146
- package/templates/workflows/squad-docs.yml +8 -4
- package/templates/workflows/squad-heartbeat.yml +55 -200
- package/templates/workflows/squad-insider-release.yml +1 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ralph Triage Script — Standalone CJS implementation
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ SYNC NOTICE: This file ports triage logic from the SDK source:
|
|
6
|
+
* packages/squad-sdk/src/ralph/triage.ts
|
|
7
|
+
*
|
|
8
|
+
* Any changes to routing/triage logic MUST be applied to BOTH files.
|
|
9
|
+
* The SDK module is the canonical implementation; this script exists
|
|
10
|
+
* for zero-dependency use in GitHub Actions workflows.
|
|
11
|
+
*
|
|
12
|
+
* To verify parity: npm test -- test/ralph-triage.test.ts
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const https = require('node:https');
|
|
19
|
+
const { execSync } = require('node:child_process');
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
let squadDir = '.squad';
|
|
23
|
+
let output = 'triage-results.json';
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
if (arg === '--squad-dir') {
|
|
28
|
+
squadDir = argv[i + 1];
|
|
29
|
+
i += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === '--output') {
|
|
33
|
+
output = argv[i + 1];
|
|
34
|
+
i += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (arg === '--help' || arg === '-h') {
|
|
38
|
+
printUsage();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!squadDir) throw new Error('--squad-dir requires a value');
|
|
45
|
+
if (!output) throw new Error('--output requires a value');
|
|
46
|
+
|
|
47
|
+
return { squadDir, output };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function printUsage() {
|
|
51
|
+
console.log('Usage: node .squad/templates/ralph-triage.js --squad-dir .squad --output triage-results.json');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeEol(content) {
|
|
55
|
+
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseRoutingRules(routingMd) {
|
|
59
|
+
const table = parseTableSection(routingMd, /^##\s*work\s*type\s*(?:→|->)\s*agent\b/i);
|
|
60
|
+
if (!table) return [];
|
|
61
|
+
|
|
62
|
+
const workTypeIndex = findColumnIndex(table.headers, ['work type', 'type']);
|
|
63
|
+
const agentIndex = findColumnIndex(table.headers, ['agent', 'route to', 'route']);
|
|
64
|
+
const examplesIndex = findColumnIndex(table.headers, ['examples', 'example']);
|
|
65
|
+
|
|
66
|
+
if (workTypeIndex < 0 || agentIndex < 0) return [];
|
|
67
|
+
|
|
68
|
+
const rules = [];
|
|
69
|
+
for (const row of table.rows) {
|
|
70
|
+
const workType = cleanCell(row[workTypeIndex] || '');
|
|
71
|
+
const agentName = cleanCell(row[agentIndex] || '');
|
|
72
|
+
const keywords = splitKeywords(examplesIndex >= 0 ? row[examplesIndex] : '');
|
|
73
|
+
if (!workType || !agentName) continue;
|
|
74
|
+
rules.push({ workType, agentName, keywords });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return rules;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseModuleOwnership(routingMd) {
|
|
81
|
+
const table = parseTableSection(routingMd, /^##\s*module\s*ownership\b/i);
|
|
82
|
+
if (!table) return [];
|
|
83
|
+
|
|
84
|
+
const moduleIndex = findColumnIndex(table.headers, ['module', 'path']);
|
|
85
|
+
const primaryIndex = findColumnIndex(table.headers, ['primary']);
|
|
86
|
+
const secondaryIndex = findColumnIndex(table.headers, ['secondary']);
|
|
87
|
+
|
|
88
|
+
if (moduleIndex < 0 || primaryIndex < 0) return [];
|
|
89
|
+
|
|
90
|
+
const modules = [];
|
|
91
|
+
for (const row of table.rows) {
|
|
92
|
+
const modulePath = normalizeModulePath(row[moduleIndex] || '');
|
|
93
|
+
const primary = cleanCell(row[primaryIndex] || '');
|
|
94
|
+
const secondaryRaw = cleanCell(secondaryIndex >= 0 ? row[secondaryIndex] || '' : '');
|
|
95
|
+
const secondary = normalizeOptionalOwner(secondaryRaw);
|
|
96
|
+
|
|
97
|
+
if (!modulePath || !primary) continue;
|
|
98
|
+
modules.push({ modulePath, primary, secondary });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return modules;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseRoster(teamMd) {
|
|
105
|
+
const table =
|
|
106
|
+
parseTableSection(teamMd, /^##\s*members\b/i) ||
|
|
107
|
+
parseTableSection(teamMd, /^##\s*team\s*roster\b/i);
|
|
108
|
+
|
|
109
|
+
if (!table) return [];
|
|
110
|
+
|
|
111
|
+
const nameIndex = findColumnIndex(table.headers, ['name']);
|
|
112
|
+
const roleIndex = findColumnIndex(table.headers, ['role']);
|
|
113
|
+
if (nameIndex < 0 || roleIndex < 0) return [];
|
|
114
|
+
|
|
115
|
+
const excluded = new Set(['scribe', 'ralph']);
|
|
116
|
+
const members = [];
|
|
117
|
+
|
|
118
|
+
for (const row of table.rows) {
|
|
119
|
+
const name = cleanCell(row[nameIndex] || '');
|
|
120
|
+
const role = cleanCell(row[roleIndex] || '');
|
|
121
|
+
if (!name || !role) continue;
|
|
122
|
+
if (excluded.has(name.toLowerCase())) continue;
|
|
123
|
+
|
|
124
|
+
members.push({
|
|
125
|
+
name,
|
|
126
|
+
role,
|
|
127
|
+
label: `squad:${name.toLowerCase()}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return members;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function triageIssue(issue, rules, modules, roster) {
|
|
135
|
+
const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase();
|
|
136
|
+
const normalizedIssueText = normalizeTextForPathMatch(issueText);
|
|
137
|
+
|
|
138
|
+
const bestModule = findBestModuleMatch(normalizedIssueText, modules);
|
|
139
|
+
if (bestModule) {
|
|
140
|
+
const primaryMember = findMember(bestModule.primary, roster);
|
|
141
|
+
if (primaryMember) {
|
|
142
|
+
return {
|
|
143
|
+
agent: primaryMember,
|
|
144
|
+
reason: `Matched module path "${bestModule.modulePath}" to primary owner "${bestModule.primary}"`,
|
|
145
|
+
source: 'module-ownership',
|
|
146
|
+
confidence: 'high',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (bestModule.secondary) {
|
|
151
|
+
const secondaryMember = findMember(bestModule.secondary, roster);
|
|
152
|
+
if (secondaryMember) {
|
|
153
|
+
return {
|
|
154
|
+
agent: secondaryMember,
|
|
155
|
+
reason: `Matched module path "${bestModule.modulePath}" to secondary owner "${bestModule.secondary}"`,
|
|
156
|
+
source: 'module-ownership',
|
|
157
|
+
confidence: 'medium',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bestRule = findBestRuleMatch(issueText, rules);
|
|
164
|
+
if (bestRule) {
|
|
165
|
+
const agent = findMember(bestRule.rule.agentName, roster);
|
|
166
|
+
if (agent) {
|
|
167
|
+
return {
|
|
168
|
+
agent,
|
|
169
|
+
reason: `Matched routing keyword(s): ${bestRule.matchedKeywords.join(', ')}`,
|
|
170
|
+
source: 'routing-rule',
|
|
171
|
+
confidence: bestRule.matchedKeywords.length >= 2 ? 'high' : 'medium',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const roleMatch = findRoleKeywordMatch(issueText, roster);
|
|
177
|
+
if (roleMatch) {
|
|
178
|
+
return {
|
|
179
|
+
agent: roleMatch.agent,
|
|
180
|
+
reason: roleMatch.reason,
|
|
181
|
+
source: 'role-keyword',
|
|
182
|
+
confidence: 'medium',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lead = findLeadFallback(roster);
|
|
187
|
+
if (!lead) return null;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
agent: lead,
|
|
191
|
+
reason: 'No module, routing, or role keyword match — routed to Lead/Architect',
|
|
192
|
+
source: 'lead-fallback',
|
|
193
|
+
confidence: 'low',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseTableSection(markdown, sectionHeader) {
|
|
198
|
+
const lines = normalizeEol(markdown).split('\n');
|
|
199
|
+
let inSection = false;
|
|
200
|
+
const tableLines = [];
|
|
201
|
+
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
if (!inSection && sectionHeader.test(trimmed)) {
|
|
205
|
+
inSection = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (inSection && /^##\s+/.test(trimmed)) break;
|
|
209
|
+
if (inSection && trimmed.startsWith('|')) tableLines.push(trimmed);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (tableLines.length === 0) return null;
|
|
213
|
+
|
|
214
|
+
let headers = null;
|
|
215
|
+
const rows = [];
|
|
216
|
+
|
|
217
|
+
for (const line of tableLines) {
|
|
218
|
+
const cells = parseTableLine(line);
|
|
219
|
+
if (cells.length === 0) continue;
|
|
220
|
+
if (cells.every((cell) => /^:?-{2,}:?$/.test(cell))) continue;
|
|
221
|
+
|
|
222
|
+
if (!headers) {
|
|
223
|
+
headers = cells;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
rows.push(cells);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!headers) return null;
|
|
231
|
+
return { headers, rows };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseTableLine(line) {
|
|
235
|
+
return line
|
|
236
|
+
.replace(/^\|/, '')
|
|
237
|
+
.replace(/\|$/, '')
|
|
238
|
+
.split('|')
|
|
239
|
+
.map((cell) => cell.trim());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findColumnIndex(headers, candidates) {
|
|
243
|
+
const normalizedHeaders = headers.map((header) => cleanCell(header).toLowerCase());
|
|
244
|
+
for (const candidate of candidates) {
|
|
245
|
+
const index = normalizedHeaders.findIndex((header) => header.includes(candidate));
|
|
246
|
+
if (index >= 0) return index;
|
|
247
|
+
}
|
|
248
|
+
return -1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function cleanCell(value) {
|
|
252
|
+
return value
|
|
253
|
+
.replace(/`/g, '')
|
|
254
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
255
|
+
.trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function splitKeywords(examplesCell) {
|
|
259
|
+
if (!examplesCell) return [];
|
|
260
|
+
return examplesCell
|
|
261
|
+
.split(',')
|
|
262
|
+
.map((keyword) => cleanCell(keyword))
|
|
263
|
+
.filter((keyword) => keyword.length > 0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeOptionalOwner(owner) {
|
|
267
|
+
if (!owner) return null;
|
|
268
|
+
if (/^[-—–]+$/.test(owner)) return null;
|
|
269
|
+
return owner;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeModulePath(modulePath) {
|
|
273
|
+
return cleanCell(modulePath).replace(/\\/g, '/').toLowerCase();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function normalizeTextForPathMatch(text) {
|
|
277
|
+
return text.replace(/\\/g, '/').replace(/`/g, '');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeName(value) {
|
|
281
|
+
return cleanCell(value)
|
|
282
|
+
.toLowerCase()
|
|
283
|
+
.replace(/[^\w@\s-]/g, '')
|
|
284
|
+
.replace(/\s+/g, ' ')
|
|
285
|
+
.trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findMember(target, roster) {
|
|
289
|
+
const normalizedTarget = normalizeName(target);
|
|
290
|
+
if (!normalizedTarget) return null;
|
|
291
|
+
|
|
292
|
+
for (const member of roster) {
|
|
293
|
+
if (normalizeName(member.name) === normalizedTarget) return member;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const member of roster) {
|
|
297
|
+
if (normalizeName(member.role) === normalizedTarget) return member;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const member of roster) {
|
|
301
|
+
const memberName = normalizeName(member.name);
|
|
302
|
+
if (normalizedTarget.includes(memberName) || memberName.includes(normalizedTarget)) {
|
|
303
|
+
return member;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const member of roster) {
|
|
308
|
+
const memberRole = normalizeName(member.role);
|
|
309
|
+
if (normalizedTarget.includes(memberRole) || memberRole.includes(normalizedTarget)) {
|
|
310
|
+
return member;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function findBestModuleMatch(issueText, modules) {
|
|
318
|
+
let best = null;
|
|
319
|
+
let bestLength = -1;
|
|
320
|
+
|
|
321
|
+
for (const module of modules) {
|
|
322
|
+
const modulePath = normalizeModulePath(module.modulePath);
|
|
323
|
+
if (!modulePath) continue;
|
|
324
|
+
if (!issueText.includes(modulePath)) continue;
|
|
325
|
+
|
|
326
|
+
if (modulePath.length > bestLength) {
|
|
327
|
+
best = module;
|
|
328
|
+
bestLength = modulePath.length;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return best;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function findBestRuleMatch(issueText, rules) {
|
|
336
|
+
let best = null;
|
|
337
|
+
let bestScore = 0;
|
|
338
|
+
|
|
339
|
+
for (const rule of rules) {
|
|
340
|
+
const matchedKeywords = rule.keywords
|
|
341
|
+
.map((keyword) => keyword.toLowerCase())
|
|
342
|
+
.filter((keyword) => keyword.length > 0 && issueText.includes(keyword));
|
|
343
|
+
|
|
344
|
+
if (matchedKeywords.length === 0) continue;
|
|
345
|
+
|
|
346
|
+
const score =
|
|
347
|
+
matchedKeywords.length * 100 + matchedKeywords.reduce((sum, keyword) => sum + keyword.length, 0);
|
|
348
|
+
if (score > bestScore) {
|
|
349
|
+
best = { rule, matchedKeywords };
|
|
350
|
+
bestScore = score;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return best;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function findRoleKeywordMatch(issueText, roster) {
|
|
358
|
+
for (const member of roster) {
|
|
359
|
+
const role = member.role.toLowerCase();
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
(role.includes('frontend') || role.includes('ui')) &&
|
|
363
|
+
(issueText.includes('ui') || issueText.includes('frontend') || issueText.includes('css'))
|
|
364
|
+
) {
|
|
365
|
+
return { agent: member, reason: 'Matched frontend/UI role keywords' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
(role.includes('backend') || role.includes('api') || role.includes('server')) &&
|
|
370
|
+
(issueText.includes('api') || issueText.includes('backend') || issueText.includes('database'))
|
|
371
|
+
) {
|
|
372
|
+
return { agent: member, reason: 'Matched backend/API role keywords' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
(role.includes('test') || role.includes('qa')) &&
|
|
377
|
+
(issueText.includes('test') || issueText.includes('bug') || issueText.includes('fix'))
|
|
378
|
+
) {
|
|
379
|
+
return { agent: member, reason: 'Matched testing/QA role keywords' };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function findLeadFallback(roster) {
|
|
387
|
+
return (
|
|
388
|
+
roster.find((member) => {
|
|
389
|
+
const role = member.role.toLowerCase();
|
|
390
|
+
return role.includes('lead') || role.includes('architect');
|
|
391
|
+
}) || null
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function parseOwnerRepoFromRemote(remoteUrl) {
|
|
396
|
+
const sshMatch = remoteUrl.match(/^git@[^:]+:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
397
|
+
if (sshMatch) return { owner: sshMatch[1], repo: sshMatch[2] };
|
|
398
|
+
|
|
399
|
+
if (remoteUrl.startsWith('http://') || remoteUrl.startsWith('https://') || remoteUrl.startsWith('ssh://')) {
|
|
400
|
+
const parsed = new URL(remoteUrl);
|
|
401
|
+
const parts = parsed.pathname.replace(/^\/+/, '').replace(/\.git$/, '').split('/');
|
|
402
|
+
if (parts.length >= 2) {
|
|
403
|
+
return { owner: parts[0], repo: parts[1] };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
throw new Error(`Unable to parse owner/repo from remote URL: ${remoteUrl}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function getOwnerRepoFromGit() {
|
|
411
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
412
|
+
return parseOwnerRepoFromRemote(remoteUrl);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function githubRequestJson(pathname, token) {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const req = https.request(
|
|
418
|
+
{
|
|
419
|
+
hostname: 'api.github.com',
|
|
420
|
+
method: 'GET',
|
|
421
|
+
path: pathname,
|
|
422
|
+
headers: {
|
|
423
|
+
Accept: 'application/vnd.github+json',
|
|
424
|
+
Authorization: `Bearer ${token}`,
|
|
425
|
+
'User-Agent': 'squad-ralph-triage',
|
|
426
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
(res) => {
|
|
430
|
+
let body = '';
|
|
431
|
+
res.setEncoding('utf8');
|
|
432
|
+
res.on('data', (chunk) => {
|
|
433
|
+
body += chunk;
|
|
434
|
+
});
|
|
435
|
+
res.on('end', () => {
|
|
436
|
+
if ((res.statusCode || 500) >= 400) {
|
|
437
|
+
reject(new Error(`GitHub API ${res.statusCode}: ${body}`));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
resolve(JSON.parse(body));
|
|
442
|
+
} catch (error) {
|
|
443
|
+
reject(new Error(`Failed to parse GitHub response: ${error.message}`));
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
req.on('error', reject);
|
|
449
|
+
req.end();
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function fetchSquadIssues(owner, repo, token) {
|
|
454
|
+
const all = [];
|
|
455
|
+
let page = 1;
|
|
456
|
+
const perPage = 100;
|
|
457
|
+
|
|
458
|
+
for (;;) {
|
|
459
|
+
const query = new URLSearchParams({
|
|
460
|
+
state: 'open',
|
|
461
|
+
labels: 'squad',
|
|
462
|
+
per_page: String(perPage),
|
|
463
|
+
page: String(page),
|
|
464
|
+
});
|
|
465
|
+
const issues = await githubRequestJson(`/repos/${owner}/${repo}/issues?${query.toString()}`, token);
|
|
466
|
+
if (!Array.isArray(issues) || issues.length === 0) break;
|
|
467
|
+
all.push(...issues);
|
|
468
|
+
if (issues.length < perPage) break;
|
|
469
|
+
page += 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return all;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function issueHasLabel(issue, labelName) {
|
|
476
|
+
const target = labelName.toLowerCase();
|
|
477
|
+
return (issue.labels || []).some((label) => {
|
|
478
|
+
if (!label) return false;
|
|
479
|
+
const name = typeof label === 'string' ? label : label.name;
|
|
480
|
+
return typeof name === 'string' && name.toLowerCase() === target;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function isUntriagedIssue(issue, memberLabels) {
|
|
485
|
+
if (issue.pull_request) return false;
|
|
486
|
+
if (!issueHasLabel(issue, 'squad')) return false;
|
|
487
|
+
return !memberLabels.some((label) => issueHasLabel(issue, label));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function main() {
|
|
491
|
+
const args = parseArgs(process.argv.slice(2));
|
|
492
|
+
const token = process.env.GITHUB_TOKEN;
|
|
493
|
+
if (!token) {
|
|
494
|
+
throw new Error('GITHUB_TOKEN is required');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const squadDir = path.resolve(process.cwd(), args.squadDir);
|
|
498
|
+
const teamMd = fs.readFileSync(path.join(squadDir, 'team.md'), 'utf8');
|
|
499
|
+
const routingMd = fs.readFileSync(path.join(squadDir, 'routing.md'), 'utf8');
|
|
500
|
+
|
|
501
|
+
const roster = parseRoster(teamMd);
|
|
502
|
+
const rules = parseRoutingRules(routingMd);
|
|
503
|
+
const modules = parseModuleOwnership(routingMd);
|
|
504
|
+
|
|
505
|
+
const { owner, repo } = getOwnerRepoFromGit();
|
|
506
|
+
const openSquadIssues = await fetchSquadIssues(owner, repo, token);
|
|
507
|
+
|
|
508
|
+
const memberLabels = roster.map((member) => member.label);
|
|
509
|
+
const untriaged = openSquadIssues.filter((issue) => isUntriagedIssue(issue, memberLabels));
|
|
510
|
+
|
|
511
|
+
const results = [];
|
|
512
|
+
for (const issue of untriaged) {
|
|
513
|
+
const decision = triageIssue(
|
|
514
|
+
{
|
|
515
|
+
number: issue.number,
|
|
516
|
+
title: issue.title || '',
|
|
517
|
+
body: issue.body || '',
|
|
518
|
+
labels: [],
|
|
519
|
+
},
|
|
520
|
+
rules,
|
|
521
|
+
modules,
|
|
522
|
+
roster,
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
if (!decision) continue;
|
|
526
|
+
results.push({
|
|
527
|
+
issueNumber: issue.number,
|
|
528
|
+
assignTo: decision.agent.name,
|
|
529
|
+
label: decision.agent.label,
|
|
530
|
+
reason: decision.reason,
|
|
531
|
+
source: decision.source,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const outputPath = path.resolve(process.cwd(), args.output);
|
|
536
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
537
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(results, null, 2)}\n`, 'utf8');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
main().catch((error) => {
|
|
541
|
+
console.error(error.message);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
});
|
package/templates/routing.md
CHANGED
|
@@ -12,35 +12,21 @@ How to decide who handles what.
|
|
|
12
12
|
| Code review | {Name} | Review PRs, check quality, suggest improvements |
|
|
13
13
|
| Testing | {Name} | Write tests, find edge cases, verify fixes |
|
|
14
14
|
| Scope & priorities | {Name} | What to build next, trade-offs, decisions |
|
|
15
|
-
| Async issue work (bugs, tests, small features) | @copilot 🤖 | Well-defined tasks matching capability profile |
|
|
16
15
|
| Session logging | Scribe | Automatic — never needs routing |
|
|
17
16
|
|
|
18
17
|
## Issue Routing
|
|
19
18
|
|
|
20
19
|
| Label | Action | Who |
|
|
21
20
|
|-------|--------|-----|
|
|
22
|
-
| `squad` | Triage: analyze issue,
|
|
21
|
+
| `squad` | Triage: analyze issue, assign `squad:{member}` label | Lead |
|
|
23
22
|
| `squad:{name}` | Pick up issue and complete the work | Named member |
|
|
24
|
-
| `squad:copilot` | Assign to @copilot for autonomous work (if enabled) | @copilot 🤖 |
|
|
25
23
|
|
|
26
24
|
### How Issue Assignment Works
|
|
27
25
|
|
|
28
|
-
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content,
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
31
|
-
4.
|
|
32
|
-
5. Members can reassign by removing their label and adding another member's label.
|
|
33
|
-
6. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
|
34
|
-
|
|
35
|
-
### Lead Triage Guidance for @copilot
|
|
36
|
-
|
|
37
|
-
When triaging, the Lead should ask:
|
|
38
|
-
|
|
39
|
-
1. **Is this well-defined?** Clear title, reproduction steps or acceptance criteria, bounded scope → likely 🟢
|
|
40
|
-
2. **Does it follow existing patterns?** Adding a test, fixing a known bug, updating a dependency → likely 🟢
|
|
41
|
-
3. **Does it need design judgment?** Architecture, API design, UX decisions → likely 🔴
|
|
42
|
-
4. **Is it security-sensitive?** Auth, encryption, access control → always 🔴
|
|
43
|
-
5. **Is it medium complexity with specs?** Feature with clear requirements, refactoring with tests → likely 🟡
|
|
26
|
+
1. When a GitHub issue gets the `squad` label, the **Lead** triages it — analyzing content, assigning the right `squad:{member}` label, and commenting with triage notes.
|
|
27
|
+
2. When a `squad:{member}` label is applied, that member picks up the issue in their next session.
|
|
28
|
+
3. Members can reassign by removing their label and adding another member's label.
|
|
29
|
+
4. The `squad` label is the "inbox" — untriaged issues waiting for Lead review.
|
|
44
30
|
|
|
45
31
|
## Rules
|
|
46
32
|
|
|
@@ -51,4 +37,3 @@ When triaging, the Lead should ask:
|
|
|
51
37
|
5. **"Team, ..." → fan-out.** Spawn all relevant agents in parallel as `mode: "background"`.
|
|
52
38
|
6. **Anticipate downstream work.** If a feature is being built, spawn the tester to write test cases from requirements simultaneously.
|
|
53
39
|
7. **Issue-labeled work** — when a `squad:{member}` label is applied to an issue, route to that member. The Lead handles all `squad` (base label) triage.
|
|
54
|
-
8. **@copilot routing** — when evaluating issues, check @copilot's capability profile in `team.md`. Route 🟢 good-fit tasks to `squad:copilot`. Flag 🟡 needs-review tasks for PR review. Keep 🔴 not-suitable tasks with squad members.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"schedules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "ralph-heartbeat",
|
|
6
|
+
"name": "Ralph Heartbeat",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"trigger": {
|
|
9
|
+
"type": "interval",
|
|
10
|
+
"intervalSeconds": 300
|
|
11
|
+
},
|
|
12
|
+
"task": {
|
|
13
|
+
"type": "workflow",
|
|
14
|
+
"ref": ".github/workflows/squad-heartbeat.yml"
|
|
15
|
+
},
|
|
16
|
+
"providers": ["local-polling", "github-actions"]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "agent-collaboration"
|
|
3
|
+
description: "Standard collaboration patterns for all squad agents — worktree awareness, decisions, cross-agent communication"
|
|
4
|
+
domain: "team-workflow"
|
|
5
|
+
confidence: "high"
|
|
6
|
+
source: "extracted from charter boilerplate — identical content in 18+ agent charters"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Every agent on the team follows identical collaboration patterns for worktree awareness, decision recording, and cross-agent communication. These were previously duplicated in every charter's Collaboration section (~300 bytes × 18 agents = ~5.4KB of redundant context). Now centralized here.
|
|
12
|
+
|
|
13
|
+
The coordinator's spawn prompt already instructs agents to read decisions.md and their history.md. This skill adds the patterns for WRITING decisions and requesting help.
|
|
14
|
+
|
|
15
|
+
## Patterns
|
|
16
|
+
|
|
17
|
+
### Worktree Awareness
|
|
18
|
+
Use the `TEAM ROOT` path provided in your spawn prompt. All `.squad/` paths are relative to this root. If TEAM ROOT is not provided (rare), run `git rev-parse --show-toplevel` as fallback. Never assume CWD is the repo root.
|
|
19
|
+
|
|
20
|
+
### Decision Recording
|
|
21
|
+
After making a decision that affects other team members, write it to:
|
|
22
|
+
`.squad/decisions/inbox/{your-name}-{brief-slug}.md`
|
|
23
|
+
|
|
24
|
+
Format:
|
|
25
|
+
```
|
|
26
|
+
### {date}: {decision title}
|
|
27
|
+
**By:** {Your Name}
|
|
28
|
+
**What:** {the decision}
|
|
29
|
+
**Why:** {rationale}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Cross-Agent Communication
|
|
33
|
+
If you need another team member's input, say so in your response. The coordinator will bring them in. Don't try to do work outside your domain.
|
|
34
|
+
|
|
35
|
+
### Reviewer Protocol
|
|
36
|
+
If you have reviewer authority and reject work: the original author is locked out from revising that artifact. A different agent must own the revision. State who should revise in your rejection response.
|
|
37
|
+
|
|
38
|
+
## Anti-Patterns
|
|
39
|
+
- Don't read all agent charters — you only need your own context + decisions.md
|
|
40
|
+
- Don't write directly to `.squad/decisions.md` — always use the inbox drop-box
|
|
41
|
+
- Don't modify other agents' history.md files — that's Scribe's job
|
|
42
|
+
- Don't assume CWD is the repo root — always use TEAM ROOT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "agent-conduct"
|
|
3
|
+
description: "Shared hard rules enforced across all squad agents"
|
|
4
|
+
domain: "team-governance"
|
|
5
|
+
confidence: "high"
|
|
6
|
+
source: "reskill extraction — Product Isolation Rule and Peer Quality Check appeared in all 20 agent charters"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Every squad agent must follow these two hard rules. They were previously duplicated in every charter. Now they live here as a shared skill, loaded once.
|
|
12
|
+
|
|
13
|
+
## Patterns
|
|
14
|
+
|
|
15
|
+
### Product Isolation Rule (hard rule)
|
|
16
|
+
Tests, CI workflows, and product code must NEVER depend on specific agent names from any particular squad. "Our squad" must not impact "the squad." No hardcoded references to agent names (Flight, EECOM, FIDO, etc.) in test assertions, CI configs, or product logic. Use generic/parameterized values. If a test needs agent names, use obviously-fake test fixtures (e.g., "test-agent-1", "TestBot").
|
|
17
|
+
|
|
18
|
+
### Peer Quality Check (hard rule)
|
|
19
|
+
Before finishing work, verify your changes don't break existing tests. Run the test suite for files you touched. If CI has been failing, check your changes aren't contributing to the problem. When you learn from mistakes, update your history.md.
|
|
20
|
+
|
|
21
|
+
## Anti-Patterns
|
|
22
|
+
- Don't hardcode dev team agent names in product code or tests
|
|
23
|
+
- Don't skip test verification before declaring work done
|
|
24
|
+
- Don't ignore pre-existing CI failures that your changes may worsen
|