@bradygaster/squad-sdk 0.8.25 → 0.9.1

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 (228) hide show
  1. package/README.md +296 -296
  2. package/dist/adapter/client.d.ts +17 -0
  3. package/dist/adapter/client.d.ts.map +1 -1
  4. package/dist/adapter/client.js +101 -1
  5. package/dist/adapter/client.js.map +1 -1
  6. package/dist/agents/history-shadow.d.ts.map +1 -1
  7. package/dist/agents/history-shadow.js +129 -62
  8. package/dist/agents/history-shadow.js.map +1 -1
  9. package/dist/agents/index.d.ts +1 -0
  10. package/dist/agents/index.d.ts.map +1 -1
  11. package/dist/agents/index.js +2 -0
  12. package/dist/agents/index.js.map +1 -1
  13. package/dist/agents/model-selector.d.ts +2 -0
  14. package/dist/agents/model-selector.d.ts.map +1 -1
  15. package/dist/agents/model-selector.js +41 -35
  16. package/dist/agents/model-selector.js.map +1 -1
  17. package/dist/agents/personal.d.ts +35 -0
  18. package/dist/agents/personal.d.ts.map +1 -0
  19. package/dist/agents/personal.js +67 -0
  20. package/dist/agents/personal.js.map +1 -0
  21. package/dist/build/github-dist.js +42 -42
  22. package/dist/builders/index.d.ts +3 -2
  23. package/dist/builders/index.d.ts.map +1 -1
  24. package/dist/builders/index.js +28 -0
  25. package/dist/builders/index.js.map +1 -1
  26. package/dist/builders/types.d.ts +13 -0
  27. package/dist/builders/types.d.ts.map +1 -1
  28. package/dist/config/init.d.ts +8 -0
  29. package/dist/config/init.d.ts.map +1 -1
  30. package/dist/config/init.js +304 -193
  31. package/dist/config/init.js.map +1 -1
  32. package/dist/config/models.d.ts +112 -0
  33. package/dist/config/models.d.ts.map +1 -1
  34. package/dist/config/models.js +329 -18
  35. package/dist/config/models.js.map +1 -1
  36. package/dist/coordinator/index.js +2 -2
  37. package/dist/coordinator/index.js.map +1 -1
  38. package/dist/index.d.ts +8 -3
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/platform/azure-devops.d.ts +42 -0
  43. package/dist/platform/azure-devops.d.ts.map +1 -1
  44. package/dist/platform/azure-devops.js +75 -0
  45. package/dist/platform/azure-devops.js.map +1 -1
  46. package/dist/platform/comms-file-log.d.ts.map +1 -1
  47. package/dist/platform/comms-file-log.js +2 -1
  48. package/dist/platform/comms-file-log.js.map +1 -1
  49. package/dist/platform/index.d.ts +2 -1
  50. package/dist/platform/index.d.ts.map +1 -1
  51. package/dist/platform/index.js +1 -0
  52. package/dist/platform/index.js.map +1 -1
  53. package/dist/ralph/capabilities.d.ts +67 -0
  54. package/dist/ralph/capabilities.d.ts.map +1 -0
  55. package/dist/ralph/capabilities.js +111 -0
  56. package/dist/ralph/capabilities.js.map +1 -0
  57. package/dist/ralph/index.d.ts +2 -0
  58. package/dist/ralph/index.d.ts.map +1 -1
  59. package/dist/ralph/index.js +6 -5
  60. package/dist/ralph/index.js.map +1 -1
  61. package/dist/ralph/rate-limiting.d.ts +99 -0
  62. package/dist/ralph/rate-limiting.d.ts.map +1 -0
  63. package/dist/ralph/rate-limiting.js +170 -0
  64. package/dist/ralph/rate-limiting.js.map +1 -0
  65. package/dist/resolution.d.ts +24 -2
  66. package/dist/resolution.d.ts.map +1 -1
  67. package/dist/resolution.js +106 -6
  68. package/dist/resolution.js.map +1 -1
  69. package/dist/roles/catalog-categories.d.ts +146 -0
  70. package/dist/roles/catalog-categories.d.ts.map +1 -0
  71. package/dist/roles/catalog-categories.js +374 -0
  72. package/dist/roles/catalog-categories.js.map +1 -0
  73. package/dist/roles/catalog-engineering.d.ts +212 -0
  74. package/dist/roles/catalog-engineering.d.ts.map +1 -0
  75. package/dist/roles/catalog-engineering.js +549 -0
  76. package/dist/roles/catalog-engineering.js.map +1 -0
  77. package/dist/roles/catalog.d.ts +24 -0
  78. package/dist/roles/catalog.d.ts.map +1 -0
  79. package/dist/roles/catalog.js +28 -0
  80. package/dist/roles/catalog.js.map +1 -0
  81. package/dist/roles/index.d.ts +69 -0
  82. package/dist/roles/index.d.ts.map +1 -0
  83. package/dist/roles/index.js +197 -0
  84. package/dist/roles/index.js.map +1 -0
  85. package/dist/roles/types.d.ts +87 -0
  86. package/dist/roles/types.d.ts.map +1 -0
  87. package/dist/roles/types.js +14 -0
  88. package/dist/roles/types.js.map +1 -0
  89. package/dist/runtime/benchmarks.js +5 -5
  90. package/dist/runtime/benchmarks.js.map +1 -1
  91. package/dist/runtime/constants.d.ts +2 -2
  92. package/dist/runtime/constants.d.ts.map +1 -1
  93. package/dist/runtime/constants.js +5 -3
  94. package/dist/runtime/constants.js.map +1 -1
  95. package/dist/runtime/cross-squad.d.ts +118 -0
  96. package/dist/runtime/cross-squad.d.ts.map +1 -0
  97. package/dist/runtime/cross-squad.js +234 -0
  98. package/dist/runtime/cross-squad.js.map +1 -0
  99. package/dist/runtime/otel-init.d.ts +24 -17
  100. package/dist/runtime/otel-init.d.ts.map +1 -1
  101. package/dist/runtime/otel-init.js +29 -20
  102. package/dist/runtime/otel-init.js.map +1 -1
  103. package/dist/runtime/otel-metrics.d.ts +5 -0
  104. package/dist/runtime/otel-metrics.d.ts.map +1 -1
  105. package/dist/runtime/otel-metrics.js +54 -0
  106. package/dist/runtime/otel-metrics.js.map +1 -1
  107. package/dist/runtime/rework.d.ts +71 -0
  108. package/dist/runtime/rework.d.ts.map +1 -0
  109. package/dist/runtime/rework.js +107 -0
  110. package/dist/runtime/rework.js.map +1 -0
  111. package/dist/runtime/scheduler.d.ts +128 -0
  112. package/dist/runtime/scheduler.d.ts.map +1 -0
  113. package/dist/runtime/scheduler.js +427 -0
  114. package/dist/runtime/scheduler.js.map +1 -0
  115. package/dist/runtime/squad-observer.d.ts.map +1 -1
  116. package/dist/runtime/squad-observer.js +4 -0
  117. package/dist/runtime/squad-observer.js.map +1 -1
  118. package/dist/runtime/streaming.d.ts +2 -0
  119. package/dist/runtime/streaming.d.ts.map +1 -1
  120. package/dist/runtime/streaming.js +6 -0
  121. package/dist/runtime/streaming.js.map +1 -1
  122. package/dist/runtime/telemetry.d.ts +2 -0
  123. package/dist/runtime/telemetry.d.ts.map +1 -1
  124. package/dist/runtime/telemetry.js +6 -0
  125. package/dist/runtime/telemetry.js.map +1 -1
  126. package/dist/sharing/consult.d.ts +2 -2
  127. package/dist/sharing/consult.js +83 -83
  128. package/dist/sharing/consult.js.map +1 -1
  129. package/dist/sharing/export.d.ts.map +1 -1
  130. package/dist/sharing/export.js +17 -4
  131. package/dist/sharing/export.js.map +1 -1
  132. package/dist/skills/handler-types.d.ts +271 -0
  133. package/dist/skills/handler-types.d.ts.map +1 -0
  134. package/dist/skills/handler-types.js +31 -0
  135. package/dist/skills/handler-types.js.map +1 -0
  136. package/dist/skills/index.d.ts +3 -0
  137. package/dist/skills/index.d.ts.map +1 -1
  138. package/dist/skills/index.js +3 -0
  139. package/dist/skills/index.js.map +1 -1
  140. package/dist/skills/skill-script-loader.d.ts +65 -0
  141. package/dist/skills/skill-script-loader.d.ts.map +1 -0
  142. package/dist/skills/skill-script-loader.js +227 -0
  143. package/dist/skills/skill-script-loader.js.map +1 -0
  144. package/dist/skills/skill-source.d.ts.map +1 -1
  145. package/dist/skills/skill-source.js +5 -1
  146. package/dist/skills/skill-source.js.map +1 -1
  147. package/dist/tools/index.d.ts +10 -1
  148. package/dist/tools/index.d.ts.map +1 -1
  149. package/dist/tools/index.js +49 -8
  150. package/dist/tools/index.js.map +1 -1
  151. package/dist/upstream/resolver.d.ts.map +1 -1
  152. package/dist/upstream/resolver.js +14 -5
  153. package/dist/upstream/resolver.js.map +1 -1
  154. package/package.json +34 -3
  155. package/templates/casting/Futurama.json +10 -0
  156. package/templates/casting-history.json +4 -4
  157. package/templates/casting-policy.json +37 -35
  158. package/templates/casting-reference.md +104 -0
  159. package/templates/casting-registry.json +3 -3
  160. package/templates/ceremonies.md +41 -41
  161. package/templates/charter.md +53 -53
  162. package/templates/constraint-tracking.md +38 -38
  163. package/templates/cooperative-rate-limiting.md +229 -0
  164. package/templates/copilot-instructions.md +46 -46
  165. package/templates/history.md +10 -10
  166. package/templates/identity/now.md +9 -9
  167. package/templates/identity/wisdom.md +15 -15
  168. package/templates/issue-lifecycle.md +412 -0
  169. package/templates/keda-scaler.md +164 -0
  170. package/templates/machine-capabilities.md +75 -0
  171. package/templates/mcp-config.md +90 -98
  172. package/templates/multi-agent-format.md +28 -28
  173. package/templates/orchestration-log.md +27 -27
  174. package/templates/package.json +3 -0
  175. package/templates/plugin-marketplace.md +49 -49
  176. package/templates/ralph-circuit-breaker.md +313 -0
  177. package/templates/ralph-triage.js +543 -0
  178. package/templates/raw-agent-output.md +37 -37
  179. package/templates/roster.md +60 -60
  180. package/templates/routing.md +39 -54
  181. package/templates/run-output.md +50 -50
  182. package/templates/schedule.json +19 -0
  183. package/templates/scribe-charter.md +119 -119
  184. package/templates/skill.md +24 -24
  185. package/templates/skills/agent-collaboration/SKILL.md +42 -0
  186. package/templates/skills/agent-conduct/SKILL.md +24 -0
  187. package/templates/skills/architectural-proposals/SKILL.md +151 -0
  188. package/templates/skills/ci-validation-gates/SKILL.md +84 -0
  189. package/templates/skills/cli-wiring/SKILL.md +47 -0
  190. package/templates/skills/client-compatibility/SKILL.md +89 -0
  191. package/templates/skills/cross-squad/SKILL.md +114 -0
  192. package/templates/skills/distributed-mesh/SKILL.md +287 -0
  193. package/templates/skills/distributed-mesh/mesh.json.example +30 -0
  194. package/templates/skills/distributed-mesh/sync-mesh.ps1 +111 -0
  195. package/templates/skills/distributed-mesh/sync-mesh.sh +104 -0
  196. package/templates/skills/docs-standards/SKILL.md +71 -0
  197. package/templates/skills/economy-mode/SKILL.md +114 -0
  198. package/templates/skills/external-comms/SKILL.md +329 -0
  199. package/templates/skills/gh-auth-isolation/SKILL.md +183 -0
  200. package/templates/skills/git-workflow/SKILL.md +204 -0
  201. package/templates/skills/github-multi-account/SKILL.md +95 -0
  202. package/templates/skills/history-hygiene/SKILL.md +36 -0
  203. package/templates/skills/humanizer/SKILL.md +105 -0
  204. package/templates/skills/init-mode/SKILL.md +102 -0
  205. package/templates/skills/model-selection/SKILL.md +117 -0
  206. package/templates/skills/nap/SKILL.md +24 -0
  207. package/templates/skills/personal-squad/SKILL.md +57 -0
  208. package/templates/skills/project-conventions/SKILL.md +56 -56
  209. package/templates/skills/release-process/SKILL.md +423 -0
  210. package/templates/skills/reskill/SKILL.md +92 -0
  211. package/templates/skills/reviewer-protocol/SKILL.md +79 -0
  212. package/templates/skills/secret-handling/SKILL.md +200 -0
  213. package/templates/skills/session-recovery/SKILL.md +155 -0
  214. package/templates/skills/squad-conventions/SKILL.md +69 -0
  215. package/templates/skills/test-discipline/SKILL.md +37 -0
  216. package/templates/skills/windows-compatibility/SKILL.md +74 -0
  217. package/templates/squad.agent.md +1287 -1146
  218. package/templates/workflows/squad-ci.yml +24 -24
  219. package/templates/workflows/squad-docs.yml +54 -50
  220. package/templates/workflows/squad-heartbeat.yml +171 -316
  221. package/templates/workflows/squad-insider-release.yml +61 -61
  222. package/templates/workflows/squad-issue-assign.yml +161 -161
  223. package/templates/workflows/squad-label-enforce.yml +181 -181
  224. package/templates/workflows/squad-preview.yml +55 -55
  225. package/templates/workflows/squad-promote.yml +120 -120
  226. package/templates/workflows/squad-release.yml +77 -77
  227. package/templates/workflows/squad-triage.yml +260 -260
  228. package/templates/workflows/sync-squad-labels.yml +169 -169
@@ -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
+ });
@@ -1,37 +1,37 @@
1
- # Raw Agent Output — Appendix Format
2
-
3
- > This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section
4
- > in any multi-agent artifact.
5
-
6
- ## Rules
7
-
8
- 1. **Verbatim only.** Paste the agent's response exactly as returned. No edits.
9
- 2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output.
10
- 3. **No rewriting.** Do not fix typos, grammar, formatting, or style.
11
- 4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks.
12
- 5. **One section per agent.** Each agent that contributed gets its own heading.
13
- 6. **Order matches work order.** List agents in the order they were spawned.
14
- 7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability.
15
-
16
- ## Format
17
-
18
- ```markdown
19
- ## APPENDIX: RAW AGENT OUTPUTS
20
-
21
- ### {Name} ({Role}) — Raw Output
22
-
23
- {Paste agent's verbatim response here, unedited}
24
-
25
- ### {Name} ({Role}) — Raw Output
26
-
27
- {Paste agent's verbatim response here, unedited}
28
- ```
29
-
30
- ## Why This Exists
31
-
32
- The appendix provides diagnostic integrity. It lets anyone verify:
33
- - What each agent actually said (vs. what the Coordinator assembled)
34
- - Whether the Coordinator faithfully represented agent work
35
- - What was lost or changed in synthesis
36
-
37
- Without raw outputs, multi-agent collaboration is unauditable.
1
+ # Raw Agent Output — Appendix Format
2
+
3
+ > This template defines the format for the `## APPENDIX: RAW AGENT OUTPUTS` section
4
+ > in any multi-agent artifact.
5
+
6
+ ## Rules
7
+
8
+ 1. **Verbatim only.** Paste the agent's response exactly as returned. No edits.
9
+ 2. **No summarizing.** Do not condense, paraphrase, or rephrase any part of the output.
10
+ 3. **No rewriting.** Do not fix typos, grammar, formatting, or style.
11
+ 4. **No code fences around the entire output.** The raw output is pasted as-is, not wrapped in ``` blocks.
12
+ 5. **One section per agent.** Each agent that contributed gets its own heading.
13
+ 6. **Order matches work order.** List agents in the order they were spawned.
14
+ 7. **Include all outputs.** Even if an agent's work was rejected, include their output for diagnostic traceability.
15
+
16
+ ## Format
17
+
18
+ ```markdown
19
+ ## APPENDIX: RAW AGENT OUTPUTS
20
+
21
+ ### {Name} ({Role}) — Raw Output
22
+
23
+ {Paste agent's verbatim response here, unedited}
24
+
25
+ ### {Name} ({Role}) — Raw Output
26
+
27
+ {Paste agent's verbatim response here, unedited}
28
+ ```
29
+
30
+ ## Why This Exists
31
+
32
+ The appendix provides diagnostic integrity. It lets anyone verify:
33
+ - What each agent actually said (vs. what the Coordinator assembled)
34
+ - Whether the Coordinator faithfully represented agent work
35
+ - What was lost or changed in synthesis
36
+
37
+ Without raw outputs, multi-agent collaboration is unauditable.