@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.
Files changed (153) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/copilot-bridge.d.ts +42 -0
  3. package/dist/cli/commands/copilot-bridge.d.ts.map +1 -0
  4. package/dist/cli/commands/copilot-bridge.js +191 -0
  5. package/dist/cli/commands/copilot-bridge.js.map +1 -0
  6. package/dist/cli/commands/import.js.map +1 -1
  7. package/dist/cli/commands/rc-tunnel.d.ts +30 -0
  8. package/dist/cli/commands/rc-tunnel.d.ts.map +1 -0
  9. package/dist/cli/commands/rc-tunnel.js +107 -0
  10. package/dist/cli/commands/rc-tunnel.js.map +1 -0
  11. package/dist/cli/commands/rc.d.ts +13 -0
  12. package/dist/cli/commands/rc.d.ts.map +1 -0
  13. package/dist/cli/commands/rc.js +270 -0
  14. package/dist/cli/commands/rc.js.map +1 -0
  15. package/dist/cli/commands/start.d.ts +18 -0
  16. package/dist/cli/commands/start.d.ts.map +1 -0
  17. package/dist/cli/commands/start.js +219 -0
  18. package/dist/cli/commands/start.js.map +1 -0
  19. package/dist/cli/commands/upstream.js.map +1 -1
  20. package/dist/cli/commands/watch.d.ts +10 -0
  21. package/dist/cli/commands/watch.d.ts.map +1 -1
  22. package/dist/cli/commands/watch.js +181 -65
  23. package/dist/cli/commands/watch.js.map +1 -1
  24. package/dist/cli/core/cast.d.ts +40 -0
  25. package/dist/cli/core/cast.d.ts.map +1 -0
  26. package/dist/cli/core/cast.js +442 -0
  27. package/dist/cli/core/cast.js.map +1 -0
  28. package/dist/cli/core/gh-cli.d.ts +25 -0
  29. package/dist/cli/core/gh-cli.d.ts.map +1 -1
  30. package/dist/cli/core/gh-cli.js +15 -1
  31. package/dist/cli/core/gh-cli.js.map +1 -1
  32. package/dist/cli/core/init.d.ts +9 -1
  33. package/dist/cli/core/init.d.ts.map +1 -1
  34. package/dist/cli/core/init.js +108 -13
  35. package/dist/cli/core/init.js.map +1 -1
  36. package/dist/cli/core/migrations.js.map +1 -1
  37. package/dist/cli/core/nap.d.ts +37 -0
  38. package/dist/cli/core/nap.d.ts.map +1 -0
  39. package/dist/cli/core/nap.js +528 -0
  40. package/dist/cli/core/nap.js.map +1 -0
  41. package/dist/cli/core/output.d.ts +5 -0
  42. package/dist/cli/core/output.d.ts.map +1 -1
  43. package/dist/cli/core/output.js +7 -0
  44. package/dist/cli/core/output.js.map +1 -1
  45. package/dist/cli/core/upgrade.d.ts +0 -1
  46. package/dist/cli/core/upgrade.d.ts.map +1 -1
  47. package/dist/cli/core/upgrade.js.map +1 -1
  48. package/dist/cli/core/version.js.map +1 -1
  49. package/dist/cli/shell/agent-status.d.ts +11 -0
  50. package/dist/cli/shell/agent-status.d.ts.map +1 -0
  51. package/dist/cli/shell/agent-status.js +26 -0
  52. package/dist/cli/shell/agent-status.js.map +1 -0
  53. package/dist/cli/shell/commands.d.ts +10 -0
  54. package/dist/cli/shell/commands.d.ts.map +1 -1
  55. package/dist/cli/shell/commands.js +143 -29
  56. package/dist/cli/shell/commands.js.map +1 -1
  57. package/dist/cli/shell/components/AgentPanel.d.ts +1 -4
  58. package/dist/cli/shell/components/AgentPanel.d.ts.map +1 -1
  59. package/dist/cli/shell/components/AgentPanel.js +88 -6
  60. package/dist/cli/shell/components/AgentPanel.js.map +1 -1
  61. package/dist/cli/shell/components/App.d.ts +11 -6
  62. package/dist/cli/shell/components/App.d.ts.map +1 -1
  63. package/dist/cli/shell/components/App.js +212 -35
  64. package/dist/cli/shell/components/App.js.map +1 -1
  65. package/dist/cli/shell/components/ErrorBoundary.d.ts +22 -0
  66. package/dist/cli/shell/components/ErrorBoundary.d.ts.map +1 -0
  67. package/dist/cli/shell/components/ErrorBoundary.js +31 -0
  68. package/dist/cli/shell/components/ErrorBoundary.js.map +1 -0
  69. package/dist/cli/shell/components/InputPrompt.d.ts +3 -0
  70. package/dist/cli/shell/components/InputPrompt.d.ts.map +1 -1
  71. package/dist/cli/shell/components/InputPrompt.js +155 -13
  72. package/dist/cli/shell/components/InputPrompt.js.map +1 -1
  73. package/dist/cli/shell/components/MessageStream.d.ts +17 -4
  74. package/dist/cli/shell/components/MessageStream.d.ts.map +1 -1
  75. package/dist/cli/shell/components/MessageStream.js +215 -23
  76. package/dist/cli/shell/components/MessageStream.js.map +1 -1
  77. package/dist/cli/shell/components/Separator.d.ts +17 -0
  78. package/dist/cli/shell/components/Separator.d.ts.map +1 -0
  79. package/dist/cli/shell/components/Separator.js +10 -0
  80. package/dist/cli/shell/components/Separator.js.map +1 -0
  81. package/dist/cli/shell/components/ThinkingIndicator.d.ts +21 -0
  82. package/dist/cli/shell/components/ThinkingIndicator.d.ts.map +1 -0
  83. package/dist/cli/shell/components/ThinkingIndicator.js +102 -0
  84. package/dist/cli/shell/components/ThinkingIndicator.js.map +1 -0
  85. package/dist/cli/shell/components/index.d.ts +3 -0
  86. package/dist/cli/shell/components/index.d.ts.map +1 -1
  87. package/dist/cli/shell/components/index.js +2 -0
  88. package/dist/cli/shell/components/index.js.map +1 -1
  89. package/dist/cli/shell/coordinator.d.ts +10 -0
  90. package/dist/cli/shell/coordinator.d.ts.map +1 -1
  91. package/dist/cli/shell/coordinator.js +99 -4
  92. package/dist/cli/shell/coordinator.js.map +1 -1
  93. package/dist/cli/shell/error-messages.d.ts +21 -0
  94. package/dist/cli/shell/error-messages.d.ts.map +1 -0
  95. package/dist/cli/shell/error-messages.js +61 -0
  96. package/dist/cli/shell/error-messages.js.map +1 -0
  97. package/dist/cli/shell/index.d.ts +24 -3
  98. package/dist/cli/shell/index.d.ts.map +1 -1
  99. package/dist/cli/shell/index.js +943 -34
  100. package/dist/cli/shell/index.js.map +1 -1
  101. package/dist/cli/shell/lifecycle.d.ts +2 -0
  102. package/dist/cli/shell/lifecycle.d.ts.map +1 -1
  103. package/dist/cli/shell/lifecycle.js +59 -6
  104. package/dist/cli/shell/lifecycle.js.map +1 -1
  105. package/dist/cli/shell/memory.d.ts +6 -1
  106. package/dist/cli/shell/memory.d.ts.map +1 -1
  107. package/dist/cli/shell/memory.js +12 -1
  108. package/dist/cli/shell/memory.js.map +1 -1
  109. package/dist/cli/shell/router.d.ts +16 -0
  110. package/dist/cli/shell/router.d.ts.map +1 -1
  111. package/dist/cli/shell/router.js +27 -0
  112. package/dist/cli/shell/router.js.map +1 -1
  113. package/dist/cli/shell/session-store.d.ts +47 -0
  114. package/dist/cli/shell/session-store.d.ts.map +1 -0
  115. package/dist/cli/shell/session-store.js +125 -0
  116. package/dist/cli/shell/session-store.js.map +1 -0
  117. package/dist/cli/shell/sessions.d.ts +2 -0
  118. package/dist/cli/shell/sessions.d.ts.map +1 -1
  119. package/dist/cli/shell/sessions.js +19 -5
  120. package/dist/cli/shell/sessions.js.map +1 -1
  121. package/dist/cli/shell/shell-metrics.d.ts +34 -0
  122. package/dist/cli/shell/shell-metrics.d.ts.map +1 -0
  123. package/dist/cli/shell/shell-metrics.js +98 -0
  124. package/dist/cli/shell/shell-metrics.js.map +1 -0
  125. package/dist/cli/shell/spawn.d.ts.map +1 -1
  126. package/dist/cli/shell/spawn.js +20 -6
  127. package/dist/cli/shell/spawn.js.map +1 -1
  128. package/dist/cli/shell/terminal.d.ts +26 -0
  129. package/dist/cli/shell/terminal.d.ts.map +1 -1
  130. package/dist/cli/shell/terminal.js +65 -2
  131. package/dist/cli/shell/terminal.js.map +1 -1
  132. package/dist/cli/shell/theme-colors.d.ts +39 -0
  133. package/dist/cli/shell/theme-colors.d.ts.map +1 -0
  134. package/dist/cli/shell/theme-colors.js +39 -0
  135. package/dist/cli/shell/theme-colors.js.map +1 -0
  136. package/dist/cli/shell/types.d.ts +2 -0
  137. package/dist/cli/shell/types.d.ts.map +1 -1
  138. package/dist/cli/shell/useAnimation.d.ts +42 -0
  139. package/dist/cli/shell/useAnimation.d.ts.map +1 -0
  140. package/dist/cli/shell/useAnimation.js +139 -0
  141. package/dist/cli/shell/useAnimation.js.map +1 -0
  142. package/dist/cli-entry.d.ts +0 -7
  143. package/dist/cli-entry.d.ts.map +1 -1
  144. package/dist/cli-entry.js +701 -96
  145. package/dist/cli-entry.js.map +1 -1
  146. package/package.json +156 -140
  147. package/templates/orchestration-log.md +1 -1
  148. package/templates/package.json +3 -0
  149. package/templates/ralph-triage.js +543 -0
  150. package/templates/scribe-charter.md +1 -1
  151. package/templates/squad.agent.md +10 -10
  152. package/templates/workflows/squad-heartbeat.yml +52 -196
  153. 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
@@ -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 github:bradygaster/squad watch --interval N` — a standalone local process that checks GitHub every N minutes and triggers triage/assignment. See [Watch Mode](#watch-mode-squad-watch).
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 github:bradygaster/squad watch` for persistent polling. |
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 github:bradygaster/squad watch # polls every 10 minutes (default)
1031
- npx github:bradygaster/squad watch --interval 5 # polls every 5 minutes
1032
- npx github:bradygaster/squad watch --interval 30 # polls every 30 minutes
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 github:bradygaster/squad watch --interval 10` |
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 github:bradygaster/squad watch` for persistent polling)
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 github:bradygaster/squad watch`.
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