@bradygaster/squad-cli 0.8.5 → 0.8.16
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/README.md +2 -2
- package/dist/cli/commands/copilot-bridge.d.ts +42 -0
- package/dist/cli/commands/copilot-bridge.d.ts.map +1 -0
- package/dist/cli/commands/copilot-bridge.js +191 -0
- package/dist/cli/commands/copilot-bridge.js.map +1 -0
- package/dist/cli/commands/import.js.map +1 -1
- package/dist/cli/commands/rc-tunnel.d.ts +30 -0
- package/dist/cli/commands/rc-tunnel.d.ts.map +1 -0
- package/dist/cli/commands/rc-tunnel.js +107 -0
- package/dist/cli/commands/rc-tunnel.js.map +1 -0
- package/dist/cli/commands/rc.d.ts +13 -0
- package/dist/cli/commands/rc.d.ts.map +1 -0
- package/dist/cli/commands/rc.js +270 -0
- package/dist/cli/commands/rc.js.map +1 -0
- package/dist/cli/commands/start.d.ts +18 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +219 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/upstream.js.map +1 -1
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +181 -65
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/core/cast.d.ts +40 -0
- package/dist/cli/core/cast.d.ts.map +1 -0
- package/dist/cli/core/cast.js +442 -0
- package/dist/cli/core/cast.js.map +1 -0
- package/dist/cli/core/gh-cli.d.ts +25 -0
- package/dist/cli/core/gh-cli.d.ts.map +1 -1
- package/dist/cli/core/gh-cli.js +15 -1
- package/dist/cli/core/gh-cli.js.map +1 -1
- package/dist/cli/core/init.d.ts +9 -1
- package/dist/cli/core/init.d.ts.map +1 -1
- package/dist/cli/core/init.js +108 -13
- package/dist/cli/core/init.js.map +1 -1
- package/dist/cli/core/migrations.js.map +1 -1
- package/dist/cli/core/nap.d.ts +37 -0
- package/dist/cli/core/nap.d.ts.map +1 -0
- package/dist/cli/core/nap.js +528 -0
- package/dist/cli/core/nap.js.map +1 -0
- package/dist/cli/core/output.d.ts +5 -0
- package/dist/cli/core/output.d.ts.map +1 -1
- package/dist/cli/core/output.js +7 -0
- package/dist/cli/core/output.js.map +1 -1
- package/dist/cli/core/upgrade.d.ts +0 -1
- package/dist/cli/core/upgrade.d.ts.map +1 -1
- package/dist/cli/core/upgrade.js.map +1 -1
- package/dist/cli/core/version.js.map +1 -1
- package/dist/cli/shell/agent-status.d.ts +11 -0
- package/dist/cli/shell/agent-status.d.ts.map +1 -0
- package/dist/cli/shell/agent-status.js +26 -0
- package/dist/cli/shell/agent-status.js.map +1 -0
- package/dist/cli/shell/commands.d.ts +10 -0
- package/dist/cli/shell/commands.d.ts.map +1 -1
- package/dist/cli/shell/commands.js +143 -29
- package/dist/cli/shell/commands.js.map +1 -1
- package/dist/cli/shell/components/AgentPanel.d.ts +1 -4
- package/dist/cli/shell/components/AgentPanel.d.ts.map +1 -1
- package/dist/cli/shell/components/AgentPanel.js +88 -6
- package/dist/cli/shell/components/AgentPanel.js.map +1 -1
- package/dist/cli/shell/components/App.d.ts +11 -6
- package/dist/cli/shell/components/App.d.ts.map +1 -1
- package/dist/cli/shell/components/App.js +212 -35
- package/dist/cli/shell/components/App.js.map +1 -1
- package/dist/cli/shell/components/ErrorBoundary.d.ts +22 -0
- package/dist/cli/shell/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/cli/shell/components/ErrorBoundary.js +31 -0
- package/dist/cli/shell/components/ErrorBoundary.js.map +1 -0
- package/dist/cli/shell/components/InputPrompt.d.ts +3 -0
- package/dist/cli/shell/components/InputPrompt.d.ts.map +1 -1
- package/dist/cli/shell/components/InputPrompt.js +155 -13
- package/dist/cli/shell/components/InputPrompt.js.map +1 -1
- package/dist/cli/shell/components/MessageStream.d.ts +17 -4
- package/dist/cli/shell/components/MessageStream.d.ts.map +1 -1
- package/dist/cli/shell/components/MessageStream.js +215 -23
- package/dist/cli/shell/components/MessageStream.js.map +1 -1
- package/dist/cli/shell/components/Separator.d.ts +17 -0
- package/dist/cli/shell/components/Separator.d.ts.map +1 -0
- package/dist/cli/shell/components/Separator.js +10 -0
- package/dist/cli/shell/components/Separator.js.map +1 -0
- package/dist/cli/shell/components/ThinkingIndicator.d.ts +21 -0
- package/dist/cli/shell/components/ThinkingIndicator.d.ts.map +1 -0
- package/dist/cli/shell/components/ThinkingIndicator.js +102 -0
- package/dist/cli/shell/components/ThinkingIndicator.js.map +1 -0
- package/dist/cli/shell/components/index.d.ts +3 -0
- package/dist/cli/shell/components/index.d.ts.map +1 -1
- package/dist/cli/shell/components/index.js +2 -0
- package/dist/cli/shell/components/index.js.map +1 -1
- package/dist/cli/shell/coordinator.d.ts +10 -0
- package/dist/cli/shell/coordinator.d.ts.map +1 -1
- package/dist/cli/shell/coordinator.js +99 -4
- package/dist/cli/shell/coordinator.js.map +1 -1
- package/dist/cli/shell/error-messages.d.ts +21 -0
- package/dist/cli/shell/error-messages.d.ts.map +1 -0
- package/dist/cli/shell/error-messages.js +61 -0
- package/dist/cli/shell/error-messages.js.map +1 -0
- package/dist/cli/shell/index.d.ts +24 -3
- package/dist/cli/shell/index.d.ts.map +1 -1
- package/dist/cli/shell/index.js +943 -34
- package/dist/cli/shell/index.js.map +1 -1
- package/dist/cli/shell/lifecycle.d.ts +2 -0
- package/dist/cli/shell/lifecycle.d.ts.map +1 -1
- package/dist/cli/shell/lifecycle.js +59 -6
- package/dist/cli/shell/lifecycle.js.map +1 -1
- package/dist/cli/shell/memory.d.ts +6 -1
- package/dist/cli/shell/memory.d.ts.map +1 -1
- package/dist/cli/shell/memory.js +12 -1
- package/dist/cli/shell/memory.js.map +1 -1
- package/dist/cli/shell/router.d.ts +16 -0
- package/dist/cli/shell/router.d.ts.map +1 -1
- package/dist/cli/shell/router.js +27 -0
- package/dist/cli/shell/router.js.map +1 -1
- package/dist/cli/shell/session-store.d.ts +47 -0
- package/dist/cli/shell/session-store.d.ts.map +1 -0
- package/dist/cli/shell/session-store.js +125 -0
- package/dist/cli/shell/session-store.js.map +1 -0
- package/dist/cli/shell/sessions.d.ts +2 -0
- package/dist/cli/shell/sessions.d.ts.map +1 -1
- package/dist/cli/shell/sessions.js +19 -5
- package/dist/cli/shell/sessions.js.map +1 -1
- package/dist/cli/shell/shell-metrics.d.ts +34 -0
- package/dist/cli/shell/shell-metrics.d.ts.map +1 -0
- package/dist/cli/shell/shell-metrics.js +98 -0
- package/dist/cli/shell/shell-metrics.js.map +1 -0
- package/dist/cli/shell/spawn.d.ts.map +1 -1
- package/dist/cli/shell/spawn.js +20 -6
- package/dist/cli/shell/spawn.js.map +1 -1
- package/dist/cli/shell/terminal.d.ts +26 -0
- package/dist/cli/shell/terminal.d.ts.map +1 -1
- package/dist/cli/shell/terminal.js +65 -2
- package/dist/cli/shell/terminal.js.map +1 -1
- package/dist/cli/shell/theme-colors.d.ts +39 -0
- package/dist/cli/shell/theme-colors.d.ts.map +1 -0
- package/dist/cli/shell/theme-colors.js +39 -0
- package/dist/cli/shell/theme-colors.js.map +1 -0
- package/dist/cli/shell/types.d.ts +2 -0
- package/dist/cli/shell/types.d.ts.map +1 -1
- package/dist/cli/shell/useAnimation.d.ts +42 -0
- package/dist/cli/shell/useAnimation.d.ts.map +1 -0
- package/dist/cli/shell/useAnimation.js +139 -0
- package/dist/cli/shell/useAnimation.js.map +1 -0
- package/dist/cli-entry.d.ts +0 -7
- package/dist/cli-entry.d.ts.map +1 -1
- package/dist/cli-entry.js +701 -96
- package/dist/cli-entry.js.map +1 -1
- package/package.json +156 -140
- package/templates/orchestration-log.md +1 -1
- package/templates/package.json +3 -0
- package/templates/ralph-triage.js +543 -0
- package/templates/scribe-charter.md +1 -1
- package/templates/squad.agent.md +10 -10
- package/templates/workflows/squad-heartbeat.yml +52 -196
- 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
|
+
});
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
After every substantial work session:
|
|
24
24
|
|
|
25
|
-
1. **Log the session** to `.squad/log/{timestamp}-{topic}.md
|
|
25
|
+
1. **Log the session** to `.squad/log/{timestamp}-{topic}.md` (use filename-safe timestamps — replace colons with hyphens, e.g., `2026-02-23T20-16-27Z` not `2026-02-23T20:16:27Z`, for Windows compatibility):
|
|
26
26
|
- Who worked
|
|
27
27
|
- What was done
|
|
28
28
|
- Decisions made
|
package/templates/squad.agent.md
CHANGED
|
@@ -717,8 +717,8 @@ prompt: |
|
|
|
717
717
|
SPAWN MANIFEST: {spawn_manifest}
|
|
718
718
|
|
|
719
719
|
Tasks (in order):
|
|
720
|
-
1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use ISO 8601 UTC timestamp.
|
|
721
|
-
2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use ISO 8601 UTC timestamp.
|
|
720
|
+
1. ORCHESTRATION LOG: Write .squad/orchestration-log/{timestamp}-{agent}.md per agent. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z` not `2026-02-23T20:16:27Z`).
|
|
721
|
+
2. SESSION LOG: Write .squad/log/{timestamp}-{topic}.md. Brief. Use filename-safe ISO 8601 UTC timestamp (replace colons with hyphens, e.g., `2026-02-23T20-16-27Z`).
|
|
722
722
|
3. DECISION INBOX: Merge .squad/decisions/inbox/ → decisions.md, delete inbox files. Deduplicate.
|
|
723
723
|
4. CROSS-AGENT: Append team updates to affected agents' history.md.
|
|
724
724
|
5. DECISIONS ARCHIVE: If decisions.md exceeds ~20KB, archive entries older than 30 days to decisions-archive.md.
|
|
@@ -951,7 +951,7 @@ Ralph is a built-in squad member whose job is keeping tabs on work. **Ralph trac
|
|
|
951
951
|
|
|
952
952
|
**⚡ CRITICAL BEHAVIOR: When Ralph is active, the coordinator MUST NOT stop and wait for user input between work items. Ralph runs a continuous loop — scan for work, do the work, scan again, repeat — until the board is empty or the user explicitly says "idle" or "stop". This is not optional. If work exists, keep going. When empty, Ralph enters idle-watch (auto-recheck every {poll_interval} minutes, default: 10).**
|
|
953
953
|
|
|
954
|
-
**Between checks:** Ralph's in-session loop runs while work exists. For persistent polling when the board is clear, use `npx
|
|
954
|
+
**Between checks:** Ralph's in-session loop runs while work exists. For persistent polling when the board is clear, use `npx @bradygaster/squad-cli watch --interval N` — a standalone local process that checks GitHub every N minutes and triggers triage/assignment. See [Watch Mode](#watch-mode-squad-watch).
|
|
955
955
|
|
|
956
956
|
**On-demand reference:** Read `.squad/templates/ralph-reference.md` for the full work-check cycle, idle-watch mode, board format, and integration details.
|
|
957
957
|
|
|
@@ -1001,7 +1001,7 @@ gh pr list --state open --draft --json number,title,author,labels,checks --limit
|
|
|
1001
1001
|
| **Review feedback** | PR has `CHANGES_REQUESTED` review | Route feedback to PR author agent to address |
|
|
1002
1002
|
| **CI failures** | PR checks failing | Notify assigned agent to fix, or create a fix issue |
|
|
1003
1003
|
| **Approved PRs** | PR approved, CI green, ready to merge | Merge and close related issue |
|
|
1004
|
-
| **No work found** | All clear | Report: "📋 Board is clear. Ralph is idling." Suggest `npx
|
|
1004
|
+
| **No work found** | All clear | Report: "📋 Board is clear. Ralph is idling." Suggest `npx @bradygaster/squad-cli watch` for persistent polling. |
|
|
1005
1005
|
|
|
1006
1006
|
**Step 3 — Act on highest-priority item:**
|
|
1007
1007
|
- Process one category at a time, highest priority first (untriaged > assigned > CI failures > review feedback > approved PRs)
|
|
@@ -1027,9 +1027,9 @@ After every 3-5 rounds, pause and report before continuing:
|
|
|
1027
1027
|
Ralph's in-session loop processes work while it exists, then idles. For **persistent polling** between sessions or when you're away from the keyboard, use the `squad watch` CLI command:
|
|
1028
1028
|
|
|
1029
1029
|
```bash
|
|
1030
|
-
npx
|
|
1031
|
-
npx
|
|
1032
|
-
npx
|
|
1030
|
+
npx @bradygaster/squad-cli watch # polls every 10 minutes (default)
|
|
1031
|
+
npx @bradygaster/squad-cli watch --interval 5 # polls every 5 minutes
|
|
1032
|
+
npx @bradygaster/squad-cli watch --interval 30 # polls every 30 minutes
|
|
1033
1033
|
```
|
|
1034
1034
|
|
|
1035
1035
|
This runs as a standalone local process (not inside Copilot) that:
|
|
@@ -1043,7 +1043,7 @@ This runs as a standalone local process (not inside Copilot) that:
|
|
|
1043
1043
|
| Layer | When | How |
|
|
1044
1044
|
|-------|------|-----|
|
|
1045
1045
|
| **In-session** | You're at the keyboard | "Ralph, go" — active loop while work exists |
|
|
1046
|
-
| **Local watchdog** | You're away but machine is on | `npx
|
|
1046
|
+
| **Local watchdog** | You're away but machine is on | `npx @bradygaster/squad-cli watch --interval 10` |
|
|
1047
1047
|
| **Cloud heartbeat** | Fully unattended | `squad-heartbeat.yml` GitHub Actions cron |
|
|
1048
1048
|
|
|
1049
1049
|
### Ralph State
|
|
@@ -1079,9 +1079,9 @@ After the coordinator's step 6 ("Immediately assess: Does anything trigger follo
|
|
|
1079
1079
|
3. Follow-up work assessed → more agents if needed
|
|
1080
1080
|
4. Ralph scans GitHub again (Step 1) → IMMEDIATELY, no pause
|
|
1081
1081
|
5. More work found → repeat from step 2
|
|
1082
|
-
6. No more work → "📋 Board is clear. Ralph is idling." (suggest `npx
|
|
1082
|
+
6. No more work → "📋 Board is clear. Ralph is idling." (suggest `npx @bradygaster/squad-cli watch` for persistent polling)
|
|
1083
1083
|
|
|
1084
|
-
**Ralph does NOT ask "should I continue?" — Ralph KEEPS GOING.** Only stops on explicit "idle"/"stop" or session end. A clear board → idle-watch, not full stop. For persistent monitoring after the board clears, use `npx
|
|
1084
|
+
**Ralph does NOT ask "should I continue?" — Ralph KEEPS GOING.** Only stops on explicit "idle"/"stop" or session end. A clear board → idle-watch, not full stop. For persistent monitoring after the board clears, use `npx @bradygaster/squad-cli watch`.
|
|
1085
1085
|
|
|
1086
1086
|
These are intent signals, not exact strings — match the user's meaning, not their exact words.
|
|
1087
1087
|
|