shakers-cli-beta 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +81 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +120 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getHomeDir } = require('./env-paths');
6
+
7
+ // Deterministic (no-LLM) parser of the talent's AI agent org chart (talents-ai-score, ADR-009).
8
+
9
+ // Safety caps mirror scanner.js's FOOTPRINT_MAX_* (avoid pathological scans,
10
+ // never a product requirement).
11
+ const AGENTS_MAX_FILES = 500;
12
+ const AGENTS_MAX_DEPTH = 6;
13
+
14
+ // Only these frontmatter keys are ever captured.
15
+ const WHITELISTED_KEYS = new Set(['name', 'tools', 'model', 'parent']);
16
+
17
+ function listAgentMarkdownFiles(dir, depth = 0, budget = { count: 0 }) {
18
+ const results = [];
19
+ if (depth > AGENTS_MAX_DEPTH || budget.count >= AGENTS_MAX_FILES) return results;
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(dir, { withFileTypes: true });
23
+ } catch {
24
+ return results; // no .claude/agents directory: not an error, just nothing to parse
25
+ }
26
+ for (const entry of entries) {
27
+ if (budget.count >= AGENTS_MAX_FILES) break;
28
+ if (entry.isSymbolicLink()) continue; // never follow symlinks (mirrors scanner.js)
29
+ const full = path.join(dir, entry.name);
30
+ if (entry.isDirectory()) {
31
+ results.push(...listAgentMarkdownFiles(full, depth + 1, budget));
32
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
33
+ budget.count += 1;
34
+ results.push(full);
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+
40
+ function stripQuotes(value) {
41
+ const v = String(value).trim();
42
+ if (v.length >= 2) {
43
+ const first = v[0];
44
+ const last = v[v.length - 1];
45
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
46
+ return v.slice(1, -1);
47
+ }
48
+ }
49
+ return v;
50
+ }
51
+
52
+ // Minimal, dependency-free frontmatter reader (zero-dependency invariant, same as the rest of this repo).
53
+ function parseFrontmatter(content, { includeDescription = false } = {}) {
54
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
55
+ if (!match) return null;
56
+ const lines = match[1].split(/\r?\n/);
57
+ const data = {};
58
+ const captureKeys = includeDescription
59
+ ? new Set([...WHITELISTED_KEYS, 'description'])
60
+ : WHITELISTED_KEYS;
61
+ let i = 0;
62
+
63
+ while (i < lines.length) {
64
+ const line = lines[i];
65
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
66
+ if (!kv) {
67
+ i += 1;
68
+ continue;
69
+ }
70
+ const key = kv[1];
71
+ const rawValue = kv[2].trim();
72
+ i += 1;
73
+
74
+ // Block scalar (`|`, `>`, `|-`, `>-`, `|+`, `>+`): this is how `description` shows up in practice.
75
+ if (/^[|>][-+]?$/.test(rawValue)) {
76
+ const blockLines = [];
77
+ while (i < lines.length && (lines[i].trim() === '' || /^\s+/.test(lines[i]))) {
78
+ blockLines.push(lines[i].trim() === '' ? '' : lines[i].replace(/^\s+/, ''));
79
+ i += 1;
80
+ }
81
+ if (includeDescription && key === 'description') {
82
+ while (blockLines.length && blockLines[blockLines.length - 1] === '') blockLines.pop();
83
+ data[key] = blockLines.join('\n');
84
+ }
85
+ continue;
86
+ }
87
+
88
+ // YAML block list continuation ("key:" on its own line, followed by
89
+ // "- item" lines).
90
+ if (rawValue === '') {
91
+ const items = [];
92
+ while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
93
+ items.push(stripQuotes(lines[i].replace(/^\s*-\s+/, '')));
94
+ i += 1;
95
+ }
96
+ if (items.length && captureKeys.has(key)) data[key] = items;
97
+ continue;
98
+ }
99
+
100
+ if (!captureKeys.has(key)) continue; // never captured (e.g. an inline description, unless explicitly requested)
101
+
102
+ // Inline YAML list: `tools: [Read, Write]`
103
+ if (/^\[.*\]$/.test(rawValue)) {
104
+ data[key] = rawValue
105
+ .slice(1, -1)
106
+ .split(',')
107
+ .map((s) => stripQuotes(s))
108
+ .filter(Boolean);
109
+ continue;
110
+ }
111
+
112
+ // Comma-separated scalar (the format Claude Code's own docs use):
113
+ // `tools: Read, Write, Bash`
114
+ if (key === 'tools' && rawValue.includes(',')) {
115
+ data[key] = rawValue
116
+ .split(',')
117
+ .map((s) => stripQuotes(s))
118
+ .filter(Boolean);
119
+ continue;
120
+ }
121
+
122
+ data[key] = stripQuotes(rawValue);
123
+ }
124
+
125
+ return data;
126
+ }
127
+
128
+ // Parses one `.claude/agents/*.md` file into { name, tools, model, parent }.
129
+ function deriveAiProduct(filePath) {
130
+ const p = String(filePath || '').replace(/\\/g, '/');
131
+ if (p.includes('/.claude/agents/') || p.includes('/.claude/agents')) return 'claude-code';
132
+ return null;
133
+ }
134
+
135
+ function buildAgentFromFrontmatter(fm, filePath) {
136
+ const fallbackName = path.basename(filePath, path.extname(filePath));
137
+ const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
138
+
139
+ const tools = Array.isArray(fm.tools) ? fm.tools : typeof fm.tools === 'string' ? [fm.tools] : [];
140
+
141
+ return {
142
+ name,
143
+ tools,
144
+ // AI product derived from the source path (see deriveAiProduct) — shown on
145
+ // the card in place of the LLM `model`.
146
+ aiProduct: deriveAiProduct(filePath),
147
+ model: typeof fm.model === 'string' && fm.model ? fm.model : null,
148
+ // Hierarchy (ADR-009): first honour an explicit `parent` frontmatter key when a project declares one.
149
+ parent: typeof fm.parent === 'string' && fm.parent ? fm.parent : null,
150
+ };
151
+ }
152
+
153
+ // `parseAgentFile` USED TO BE HERE (removed by issue 093's sweep): read one `.md` and build an agent from its frontmatter.
154
+
155
+ // Markdown body (everything after the closing ` ` of the frontmatter).
156
+ function bodyAfterFrontmatter(content) {
157
+ const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
158
+ return m ? content.slice(m[0].length) : '';
159
+ }
160
+
161
+ const HIERARCHY_DIRECTS = /\b(defines?|sets?|directs?|approves?|coordinat\w+|orchestrat\w+|owns?)\b/i;
162
+ const HIERARCHY_EXECUTES = /\byou\s+(execute|produce|implement|carry\s+out)\b|\byou\s+don'?t\s+strateg|passes?\s+through\s+you|execution\s+agent/i;
163
+ const HIERARCHY_NEGATION = /\b(do\s+not|don'?t\s+touch|not\s+touch|never|nunca|no\s+toques)\b/i;
164
+
165
+ function deriveParentFromText(selfName, text, knownNames) {
166
+ if (!text) return null;
167
+ const units = text.split(/\r?\n|(?<=[.!?])\s+/);
168
+ for (const unit of units) {
169
+ if (HIERARCHY_NEGATION.test(unit)) continue;
170
+ if (!HIERARCHY_DIRECTS.test(unit) || !HIERARCHY_EXECUTES.test(unit)) continue;
171
+ const matches = [];
172
+ for (const other of knownNames) {
173
+ if (other === selfName) continue;
174
+ const re = new RegExp('\\b' + other.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
175
+ if (re.test(unit)) matches.push(other);
176
+ }
177
+ if (matches.length === 1) return matches[0]; // exactly one -> unambiguous
178
+ }
179
+ return null;
180
+ }
181
+
182
+ // Both `.claude/agents/` locations in scope order (project first, so it
183
+ // wins on a name collision when the results are merged/deduped below).
184
+ function agentDirs(root) {
185
+ return [path.join(root, '.claude', 'agents'), path.join(getHomeDir(), '.claude', 'agents')];
186
+ }
187
+
188
+ // Per-repo AI-usage scoping: `.claude/agents/` across EVERY selected project root, PLUS home appended ONCE at the end.
189
+ function agentDirsForRoots(roots) {
190
+ const dirs = [];
191
+ for (const root of roots) dirs.push(path.join(root, '.claude', 'agents'));
192
+ dirs.push(path.join(getHomeDir(), '.claude', 'agents'));
193
+ return dirs;
194
+ }
195
+
196
+ // Deterministic (no-LLM) agent org chart over a fixed list of `.claude/agents/` directories (in precedence order — first occurrence of a name wins).
197
+ function parseAgentOrgChartFromDirs(dirs) {
198
+ const seen = new Set();
199
+ const agents = [];
200
+ // TRANSIENT prose (description + body) per agent, kept LOCAL to this function purely to derive parent edges below.
201
+ const textByName = new Map();
202
+
203
+ for (const dir of dirs) {
204
+ for (const file of listAgentMarkdownFiles(dir)) {
205
+ let content;
206
+ try {
207
+ content = fs.readFileSync(file, 'utf8');
208
+ } catch {
209
+ continue;
210
+ }
211
+ const fm = parseFrontmatter(content);
212
+ if (!fm) continue; // no frontmatter block: not a valid agent definition
213
+ const agent = buildAgentFromFrontmatter(fm, file);
214
+ if (seen.has(agent.name)) continue; // project wins on a name collision (dir order)
215
+ seen.add(agent.name);
216
+ agents.push(agent);
217
+ const fmWithDesc = parseFrontmatter(content, { includeDescription: true });
218
+ const desc = fmWithDesc && typeof fmWithDesc.description === 'string' ? fmWithDesc.description : '';
219
+ textByName.set(agent.name, `${desc}\n${bodyAfterFrontmatter(content)}`);
220
+ }
221
+ }
222
+
223
+ // Derive orchestrator->subagent edges from prose for agents that declare no explicit `parent` (the common case).
224
+ const knownNames = agents.map((a) => a.name);
225
+ for (const agent of agents) {
226
+ if (agent.parent) continue;
227
+ const derived = deriveParentFromText(agent.name, textByName.get(agent.name), knownNames);
228
+ if (derived && derived !== agent.name) agent.parent = derived;
229
+ }
230
+
231
+ // Defensive: break a direct 2-cycle from mutual cues (A<->B).
232
+ const byName = new Map(agents.map((a) => [a.name, a]));
233
+ for (const agent of agents) {
234
+ if (!agent.parent) continue;
235
+ const p = byName.get(agent.parent);
236
+ if (p && p.parent === agent.name) p.parent = null;
237
+ }
238
+
239
+ return agents;
240
+ }
241
+
242
+ // Deterministic (no-LLM) agent org chart, scoped to `.claude/agents/` in BOTH the project root and the home directory (ADR-014, project ∪ home).
243
+ function parseAgentOrgChart(root) {
244
+ return parseAgentOrgChartFromDirs(agentDirs(root));
245
+ }
246
+
247
+ // Per-repo AI-usage scoping: the org chart across ALL selected project roots (∪ home, once), deduped by name.
248
+ function parseAgentOrgChartForRoots(roots) {
249
+ return parseAgentOrgChartFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
250
+ }
251
+
252
+ // talents-ai-score, ADR-010: returns `[{ name, description }]` — the ONLY function in this module that ever returns description/prompt content.
253
+ function parseAgentDescriptionsFromDirs(dirs) {
254
+ const seen = new Set();
255
+ const result = [];
256
+ for (const dir of dirs) {
257
+ for (const file of listAgentMarkdownFiles(dir)) {
258
+ let content;
259
+ try {
260
+ content = fs.readFileSync(file, 'utf8');
261
+ } catch {
262
+ continue;
263
+ }
264
+ const fm = parseFrontmatter(content, { includeDescription: true });
265
+ if (!fm) continue;
266
+ const fallbackName = path.basename(file, path.extname(file));
267
+ const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
268
+ if (!seen.has(name)) {
269
+ seen.add(name);
270
+ result.push({ name, description: typeof fm.description === 'string' ? fm.description : '' });
271
+ }
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+
277
+ function parseAgentDescriptions(root) {
278
+ return parseAgentDescriptionsFromDirs(agentDirs(root));
279
+ }
280
+
281
+ // Per-repo AI-usage scoping: card-description fallbacks across ALL selected
282
+ // project roots (∪ home, once), same dedup/precedence as parseAgentOrgChartForRoots.
283
+ function parseAgentDescriptionsForRoots(roots) {
284
+ return parseAgentDescriptionsFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
285
+ }
286
+
287
+ function parseAgentDefinitionsFromDirs(dirs) {
288
+ const seen = new Set();
289
+ const result = [];
290
+ for (const dir of dirs) {
291
+ for (const file of listAgentMarkdownFiles(dir)) {
292
+ let content;
293
+ try {
294
+ content = fs.readFileSync(file, 'utf8');
295
+ } catch {
296
+ continue;
297
+ }
298
+ const fm = parseFrontmatter(content, { includeDescription: true });
299
+ if (!fm) continue;
300
+ const fallbackName = path.basename(file, path.extname(file));
301
+ const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
302
+ if (seen.has(name)) continue;
303
+ seen.add(name);
304
+ const description = typeof fm.description === 'string' ? fm.description : '';
305
+ const body = bodyAfterFrontmatter(content);
306
+ const definition = [description, body].map((s) => (s || '').trim()).filter(Boolean).join('\n\n');
307
+ result.push({ name, definition });
308
+ }
309
+ }
310
+ return result;
311
+ }
312
+
313
+ function parseAgentDefinitions(root) {
314
+ return parseAgentDefinitionsFromDirs(agentDirs(root));
315
+ }
316
+
317
+ // Per-repo AI-usage scoping: full agent definitions (for the quality-evaluation
318
+ // call) across ALL selected project roots (∪ home, once), same dedup/precedence.
319
+ function parseAgentDefinitionsForRoots(roots) {
320
+ return parseAgentDefinitionsFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
321
+ }
322
+
323
+ function agentSourceFilesByName(root) {
324
+ const byName = new Map();
325
+ const dirs = agentDirs(root);
326
+ dirs.forEach((dir, dirIndex) => {
327
+ for (const file of listAgentMarkdownFiles(dir)) {
328
+ let content;
329
+ try {
330
+ content = fs.readFileSync(file, 'utf8');
331
+ } catch {
332
+ continue;
333
+ }
334
+ const fm = parseFrontmatter(content);
335
+ if (!fm) continue;
336
+ const agent = buildAgentFromFrontmatter(fm, file);
337
+ if (byName.has(agent.name)) continue; // project (dirIndex 0) wins on collision
338
+ const rel =
339
+ dirIndex === 0 ? path.relative(root, file).split(path.sep).join('/') : null;
340
+ byName.set(agent.name, rel);
341
+ }
342
+ });
343
+ return byName;
344
+ }
345
+
346
+ module.exports = {
347
+ parseAgentOrgChart,
348
+ parseAgentOrgChartForRoots,
349
+ parseFrontmatter,
350
+ parseAgentDescriptions,
351
+ parseAgentDescriptionsForRoots,
352
+ parseAgentDefinitions,
353
+ parseAgentDefinitionsForRoots,
354
+ agentSourceFilesByName,
355
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const { requestBackend } = require('./backend-request');
4
+ const { reporterFor, REASON } = require('./model-call-record');
5
+
6
+ // Agent synthesis client (talents-ai-score, ADR-010 / ADR-011).
7
+
8
+ const DEFAULT_TIMEOUT_MS = 8000;
9
+
10
+ /* ---------- scrub (mandatory mitigation) ---------- */
11
+
12
+ const JWT_RE = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g;
13
+ const BEARER_RE = /\bBearer\s+\S{10,}/gi;
14
+ const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{16,}\b/g;
15
+ const AWS_KEY_RE = /\bAKIA[0-9A-Z]{16}\b/g;
16
+ const GENERIC_LONG_TOKEN_RE = /\b[A-Za-z0-9_-]{32,}\b/g;
17
+ const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
18
+ const WINDOWS_PATH_RE = /[A-Za-z]:\\(?:[^\s\\]+\\)*[^\s\\]+/g;
19
+ const UNIX_PATH_RE = /(?:\/[A-Za-z0-9_.-]+){2,}/g;
20
+
21
+ const KEY_PREFIX_RE = /\b(?:sk|pk|rk|ghp|gho|ghu|ghs|ghr|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g; // OpenAI/Stripe/GitHub/Slack-style prefixed tokens
22
+ const GITHUB_PAT_RE = /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g;
23
+ const GOOGLE_API_KEY_RE = /\bAIza[0-9A-Za-z_-]{35}\b/g;
24
+ const SENDGRID_KEY_RE = /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g;
25
+ const BASE64_SECRET_RE = /\b[A-Za-z0-9+/]{40,}={0,2}\b/g; // base64-inclusive high-entropy blob (e.g. an AWS secret access key)
26
+ const JWT_LIKE_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g; // dot-separated JWT-shaped triplet not starting with `eyJ`
27
+ const PEM_BLOCK_RE = /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/g;
28
+ const CONN_STRING_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]{1,15}:\/\/)[^\s'"/:@]+:[^\s'"@]+@/g; // scheme://user:pass@ -> scheme://[REDACTED]@
29
+ const GENERIC_KV_RE = /\b(?:password|secret|token|api[_-]?key|bearer)\s*[:=]\s*\S+/gi;
30
+
31
+ // Heuristic, best-effort redaction — an "invisible safety net" (ADR-011), not a substitute for the talent's own judgment about what they write in agent descriptions.
32
+ function scrubSecrets(text) {
33
+ if (typeof text !== 'string' || !text) return '';
34
+ let out = text;
35
+ out = out.replace(PEM_BLOCK_RE, '[REDACTED]');
36
+ out = out.replace(CONN_STRING_RE, '$1[REDACTED]@');
37
+ out = out.replace(JWT_RE, '[REDACTED]');
38
+ out = out.replace(BEARER_RE, 'Bearer [REDACTED]');
39
+ out = out.replace(OPENAI_KEY_RE, '[REDACTED]');
40
+ out = out.replace(AWS_KEY_RE, '[REDACTED]');
41
+ out = out.replace(KEY_PREFIX_RE, '[REDACTED]');
42
+ out = out.replace(GITHUB_PAT_RE, '[REDACTED]');
43
+ out = out.replace(GOOGLE_API_KEY_RE, '[REDACTED]');
44
+ out = out.replace(SENDGRID_KEY_RE, '[REDACTED]');
45
+ out = out.replace(JWT_LIKE_RE, '[REDACTED]');
46
+ out = out.replace(GENERIC_LONG_TOKEN_RE, '[REDACTED]');
47
+ out = out.replace(BASE64_SECRET_RE, '[REDACTED]');
48
+ out = out.replace(GENERIC_KV_RE, '[REDACTED]');
49
+ out = out.replace(EMAIL_RE, '[REDACTED]');
50
+ out = out.replace(WINDOWS_PATH_RE, '[REDACTED]');
51
+ out = out.replace(UNIX_PATH_RE, '[REDACTED]');
52
+ return out;
53
+ }
54
+
55
+ /* ---------- request builder ---------- */
56
+
57
+ function buildSynthesisRequest(structuralAgents, descriptionsByName, consentGranted = false) {
58
+ const descMap = new Map((descriptionsByName || []).map((d) => [d.name, d.description]));
59
+ return {
60
+ agents: (structuralAgents || []).map((a) => ({
61
+ name: a.name,
62
+ description: scrubSecrets(descMap.get(a.name) || ''),
63
+ tools: Array.isArray(a.tools) ? a.tools : [],
64
+ model: a.model || null,
65
+ parent: a.parent || null,
66
+ })),
67
+ ...(consentGranted === true ? { traceContentConsent: true } : {}),
68
+ };
69
+ }
70
+
71
+ /* ---------- network (via the shared PRIMARY -> FALLBACK chain) ---------- */
72
+
73
+ // Validates the minimal expected shape: `{agents: [...], edges: [...]}` with `agents` an array (edges defaults to [] if absent/invalid — some diagrams may legitimately have none).
74
+ function isValidSynthesisResponse(parsed) {
75
+ return !!parsed && typeof parsed === 'object' && Array.isArray(parsed.agents);
76
+ }
77
+
78
+ // Requests the agent-synthesis endpoint.
79
+ async function requestAgentSynthesis(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
80
+ // Issue 109: the four `return null`s below are four DIFFERENT situations that used to be one.
81
+ const outcome = reporterFor(onOutcome, { timeoutMs });
82
+ if (!endpoint) {
83
+ outcome.skip(REASON.NO_ENDPOINT);
84
+ return null;
85
+ }
86
+
87
+ const safeBody = {
88
+ ...requestBody,
89
+ agents: Array.isArray(requestBody && requestBody.agents)
90
+ ? requestBody.agents.map((a) => ({ ...a, description: scrubSecrets(a.description) }))
91
+ : [],
92
+ };
93
+ if (safeBody.traceContentConsent !== true) delete safeBody.traceContentConsent;
94
+
95
+ let res;
96
+ try {
97
+ res = await requestBackend({ endpoint, body: safeBody, timeoutMs, trace: outcome.hops });
98
+ } catch (e) {
99
+ outcome.failFromError(e);
100
+ return null; // network error or timeout on every hop: never breaks the local report
101
+ }
102
+
103
+ if (res.status < 200 || res.status >= 300) {
104
+ outcome.fail(REASON.HTTP, { status: res.status, backend: res.backend });
105
+ return null;
106
+ }
107
+
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(res.raw);
111
+ } catch {
112
+ outcome.fail(REASON.MALFORMED_JSON, { status: res.status, backend: res.backend });
113
+ return null; // malformed (non-JSON) response body
114
+ }
115
+
116
+ if (!isValidSynthesisResponse(parsed)) {
117
+ outcome.fail(REASON.UNEXPECTED_SHAPE, { status: res.status, backend: res.backend });
118
+ return null;
119
+ }
120
+
121
+ outcome.succeed({ status: res.status, backend: res.backend });
122
+ return {
123
+ agents: parsed.agents,
124
+ edges: Array.isArray(parsed.edges) ? parsed.edges : [],
125
+ };
126
+ }
127
+
128
+ module.exports = {
129
+ scrubSecrets,
130
+ buildSynthesisRequest,
131
+ requestAgentSynthesis,
132
+ };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getHomeDir } = require('./env-paths');
6
+
7
+ // Local Claude Code agent-usage signal (ADR-016, agent evaluation feature).
8
+
9
+ const MAX_FILES = 500; // cap total session files scanned (bounded work)
10
+ const MAX_BYTES_PER_FILE = 8 * 1024 * 1024; // read at most an 8MB head per file
11
+ const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
12
+
13
+ function historyRoot(env = process.env) {
14
+ return path.join(getHomeDir(env), '.claude', 'projects');
15
+ }
16
+
17
+ function listSessionFiles(root) {
18
+ const out = [];
19
+ let dirs;
20
+ try {
21
+ dirs = fs.readdirSync(root);
22
+ } catch {
23
+ return out; // no history dir on this machine — graceful
24
+ }
25
+ for (const d of dirs) {
26
+ const dirPath = path.join(root, d);
27
+ let files;
28
+ try {
29
+ files = fs.readdirSync(dirPath);
30
+ } catch {
31
+ continue;
32
+ }
33
+ for (const f of files) {
34
+ if (f.endsWith('.jsonl')) {
35
+ out.push(path.join(dirPath, f));
36
+ if (out.length >= MAX_FILES) return out;
37
+ }
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ // Parse ONE jsonl line, tallying any Agent/Task tool_use subagent_type into `counts`.
44
+ function countInvocation(line, counts) {
45
+ if (!line || !line.includes('subagent_type')) return;
46
+ let o;
47
+ try {
48
+ o = JSON.parse(line);
49
+ } catch {
50
+ return; // truncated / malformed line — skip, never throw
51
+ }
52
+ const msg = o && o.message;
53
+ const content = msg && Array.isArray(msg.content) ? msg.content : null;
54
+ if (!content) return;
55
+ for (const c of content) {
56
+ if (c && c.type === 'tool_use' && AGENT_TOOL_NAMES.has(c.name)) {
57
+ const t = c.input && typeof c.input.subagent_type === 'string' ? c.input.subagent_type.trim() : '';
58
+ if (t) counts[t] = (counts[t] || 0) + 1;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Read a bounded head of a session file and tally invocations.
64
+ function scanFile(file, counts) {
65
+ let fd;
66
+ try {
67
+ fd = fs.openSync(file, 'r');
68
+ } catch {
69
+ return;
70
+ }
71
+ try {
72
+ const { size } = fs.fstatSync(fd);
73
+ const readLen = Math.min(size, MAX_BYTES_PER_FILE);
74
+ const buf = Buffer.allocUnsafe(readLen);
75
+ fs.readSync(fd, buf, 0, readLen, 0);
76
+ const lines = buf.toString('utf8').split('\n');
77
+ const complete = size > MAX_BYTES_PER_FILE ? lines.slice(0, -1) : lines;
78
+ for (const ln of complete) countInvocation(ln, counts);
79
+ } catch {
80
+ /* unreadable mid-stream — skip this file */
81
+ } finally {
82
+ try {
83
+ fs.closeSync(fd);
84
+ } catch {
85
+ /* already closed */
86
+ }
87
+ }
88
+ }
89
+
90
+ // Scan the whole local history.
91
+ function collectClaudeAgentUsage(env = process.env) {
92
+ const files = listSessionFiles(historyRoot(env));
93
+ if (!files.length) {
94
+ return { available: false, byAgent: {}, totalInvocations: 0, sessionsScanned: 0 };
95
+ }
96
+ const counts = {};
97
+ for (const f of files) scanFile(f, counts);
98
+ const totalInvocations = Object.values(counts).reduce((a, b) => a + b, 0);
99
+ return { available: true, byAgent: counts, totalInvocations, sessionsScanned: files.length };
100
+ }
101
+
102
+ // Annotate the DETECTED local agents with their usage count (exact match of the agent `name` against the recorded `subagent_type`).
103
+ function collectAgentUsage(agents, env = process.env) {
104
+ const usage = collectClaudeAgentUsage(env);
105
+ const byAgent = {};
106
+ for (const a of agents || []) {
107
+ if (!a || !a.name) continue;
108
+ byAgent[a.name] = usage.available ? usage.byAgent[a.name] || 0 : null;
109
+ }
110
+ return {
111
+ available: usage.available,
112
+ byAgent,
113
+ totalInvocations: usage.totalInvocations,
114
+ sessionsScanned: usage.sessionsScanned,
115
+ };
116
+ }
117
+
118
+ module.exports = { collectClaudeAgentUsage, collectAgentUsage, historyRoot };