shakers-cli 0.0.0-stage → 0.14.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 (237) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1233 -2
  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 +99 -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 +124 -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 +216 -0
  20. package/bin/mcp.js +124 -0
  21. package/bin/mcpb.js +5 -0
  22. package/bin/me.js +69 -0
  23. package/bin/onboarding.js +134 -0
  24. package/bin/portfolios.js +69 -0
  25. package/bin/rate.js +70 -0
  26. package/bin/register.js +337 -0
  27. package/bin/report-html.js +134 -0
  28. package/bin/save.js +77 -0
  29. package/bin/shakers.js +363 -0
  30. package/bin/share.js +72 -0
  31. package/bin/show-project.js +72 -0
  32. package/bin/socials.js +70 -0
  33. package/bin/superadmin.js +351 -0
  34. package/bin/update-check-worker.js +70 -0
  35. package/bin/update.js +111 -0
  36. package/package.json +40 -4
  37. package/src/agent-category.js +13 -0
  38. package/src/agent-evaluation.js +177 -0
  39. package/src/agent-org-chart.js +355 -0
  40. package/src/agent-synthesis.js +132 -0
  41. package/src/agent-usage.js +118 -0
  42. package/src/agents-client.js +165 -0
  43. package/src/ai-fluency-cell.js +72 -0
  44. package/src/ai-profile-client.js +45 -0
  45. package/src/ai-profile-preview.js +154 -0
  46. package/src/ai.js +66 -0
  47. package/src/alma-client.js +123 -0
  48. package/src/alma-flow.js +32 -0
  49. package/src/alma-repo-context.js +47 -0
  50. package/src/alma-shell.js +161 -0
  51. package/src/ansi.js +62 -0
  52. package/src/applications-client.js +79 -0
  53. package/src/applications-flow.js +58 -0
  54. package/src/auth-client.js +144 -0
  55. package/src/auth-device-client.js +118 -0
  56. package/src/auth-method-picker.js +44 -0
  57. package/src/auth-methods.js +35 -0
  58. package/src/auth-session-store.js +215 -0
  59. package/src/authorship-mismatch.js +62 -0
  60. package/src/authorship.js +204 -0
  61. package/src/automations-detector.js +170 -0
  62. package/src/availability-client.js +93 -0
  63. package/src/availability-flow.js +124 -0
  64. package/src/backend-request.js +191 -0
  65. package/src/blind-approval-extract.js +48 -0
  66. package/src/brand-ansi.js +14 -0
  67. package/src/brand-svg.js +9 -0
  68. package/src/browser-tools-detector.js +21 -0
  69. package/src/build-next-level.js +56 -0
  70. package/src/certifications-client.js +89 -0
  71. package/src/certifications-flow.js +75 -0
  72. package/src/certify-dimension-client.js +163 -0
  73. package/src/certify-dimension-flow.js +135 -0
  74. package/src/certify-disclaimer.js +51 -0
  75. package/src/certify-remediation-prompt.js +23 -0
  76. package/src/cli-args.js +83 -0
  77. package/src/command-audit.js +105 -0
  78. package/src/config-dir.js +120 -0
  79. package/src/config.js +877 -0
  80. package/src/consent-flow.js +115 -0
  81. package/src/consent-skip.js +27 -0
  82. package/src/cv-file.js +62 -0
  83. package/src/cv-text.js +92 -0
  84. package/src/decisions-extract.js +43 -0
  85. package/src/detect-repos.js +145 -0
  86. package/src/detected-tools.js +15 -0
  87. package/src/detection-evidence.js +105 -0
  88. package/src/detection-noise.js +132 -0
  89. package/src/detectors.js +221 -0
  90. package/src/device-auth-store.js +100 -0
  91. package/src/device-login.js +120 -0
  92. package/src/email-verification.js +152 -0
  93. package/src/env-paths.js +30 -0
  94. package/src/find-projects-cache.js +89 -0
  95. package/src/find-projects-client.js +148 -0
  96. package/src/find-projects-flow.js +164 -0
  97. package/src/git-activity.js +124 -0
  98. package/src/git-log-prefetch.js +53 -0
  99. package/src/graph-certs.js +58 -0
  100. package/src/graph-generator.js +230 -0
  101. package/src/graph-scan.js +239 -0
  102. package/src/i18n.js +3341 -0
  103. package/src/inter-fonts.js +6 -0
  104. package/src/interactive-select.js +183 -0
  105. package/src/interview-client.js +226 -0
  106. package/src/interview-transcript.js +56 -0
  107. package/src/inventory-client.js +90 -0
  108. package/src/invitations-client.js +66 -0
  109. package/src/invitations-flow.js +50 -0
  110. package/src/lang-client.js +147 -0
  111. package/src/lang-codes.js +14 -0
  112. package/src/lang-flow.js +85 -0
  113. package/src/legal-notice.js +53 -0
  114. package/src/locale.js +72 -0
  115. package/src/markdown-ansi.js +50 -0
  116. package/src/maturity.js +111 -0
  117. package/src/mcp-ai-usage-result-tool.js +111 -0
  118. package/src/mcp-ai-usage-tool.js +401 -0
  119. package/src/mcp-alma-tools.js +95 -0
  120. package/src/mcp-applications-tools.js +44 -0
  121. package/src/mcp-auth-form.js +480 -0
  122. package/src/mcp-availability-tools.js +45 -0
  123. package/src/mcp-certifications-tools.js +44 -0
  124. package/src/mcp-certify-dimension-tools.js +66 -0
  125. package/src/mcp-choice.js +95 -0
  126. package/src/mcp-detector.js +223 -0
  127. package/src/mcp-device-login-tools.js +177 -0
  128. package/src/mcp-find-projects-tools.js +69 -0
  129. package/src/mcp-install.js +275 -0
  130. package/src/mcp-instructions.js +27 -0
  131. package/src/mcp-invitations-tools.js +44 -0
  132. package/src/mcp-lang-tools.js +41 -0
  133. package/src/mcp-onboarding-tools.js +379 -0
  134. package/src/mcp-portfolios-tools.js +50 -0
  135. package/src/mcp-profile-tools.js +48 -0
  136. package/src/mcp-prompts.js +73 -0
  137. package/src/mcp-rate-tools.js +44 -0
  138. package/src/mcp-register-tools.js +441 -0
  139. package/src/mcp-report-tools.js +105 -0
  140. package/src/mcp-role-tools.js +372 -0
  141. package/src/mcp-saved-positions-tools.js +59 -0
  142. package/src/mcp-server.js +290 -0
  143. package/src/mcp-show-project-tools.js +52 -0
  144. package/src/mcp-signup-client.js +118 -0
  145. package/src/mcp-signup-tools.js +1171 -0
  146. package/src/mcp-socials-tools.js +41 -0
  147. package/src/memory-structure-detector.js +108 -0
  148. package/src/model-call-record.js +198 -0
  149. package/src/onboarding-availability.js +26 -0
  150. package/src/onboarding-client.js +374 -0
  151. package/src/onboarding-flow.js +1162 -0
  152. package/src/onboarding-funnel.js +272 -0
  153. package/src/onboarding-livekit-client.js +102 -0
  154. package/src/onboarding-status-client.js +57 -0
  155. package/src/open-file.js +32 -0
  156. package/src/osc-link.js +16 -0
  157. package/src/portfolio-client.js +164 -0
  158. package/src/portfolio-git-info.js +61 -0
  159. package/src/portfolios-client.js +85 -0
  160. package/src/portfolios-flow.js +79 -0
  161. package/src/profile-client.js +133 -0
  162. package/src/profile-flow.js +115 -0
  163. package/src/profile-switch.js +46 -0
  164. package/src/prompt-select.js +66 -0
  165. package/src/rate-client.js +81 -0
  166. package/src/rate-flow.js +137 -0
  167. package/src/register-context.js +145 -0
  168. package/src/render-ai-fluency.js +266 -0
  169. package/src/render-certification.js +177 -0
  170. package/src/render-html.js +140 -0
  171. package/src/render-report-text.js +27 -0
  172. package/src/render-sheet.js +261 -0
  173. package/src/render-terminal.js +109 -0
  174. package/src/render-usage-report-text.js +155 -0
  175. package/src/repo-context.js +215 -0
  176. package/src/report-gating.js +49 -0
  177. package/src/report-store.js +226 -0
  178. package/src/report-theme.js +177 -0
  179. package/src/report-view-client.js +50 -0
  180. package/src/reveal-output.js +29 -0
  181. package/src/roadmap-content.js +453 -0
  182. package/src/roadmap-personalization.js +132 -0
  183. package/src/roadmap-prompt.js +154 -0
  184. package/src/roles-client.js +112 -0
  185. package/src/roles-flow.js +192 -0
  186. package/src/sanitize-network-text.js +36 -0
  187. package/src/saved-positions-client.js +71 -0
  188. package/src/saved-positions-flow.js +50 -0
  189. package/src/scan-exclusions.js +50 -0
  190. package/src/scanner.js +624 -0
  191. package/src/scope-signals.js +148 -0
  192. package/src/secret-prompt.js +72 -0
  193. package/src/session-gate.js +30 -0
  194. package/src/session-normalizers.js +174 -0
  195. package/src/session-refresh.js +51 -0
  196. package/src/session-scan.js +475 -0
  197. package/src/session-signals.js +36 -0
  198. package/src/shakers-wordmark.js +17 -0
  199. package/src/share-card.js +362 -0
  200. package/src/share-certification.js +111 -0
  201. package/src/share-consent-state.js +157 -0
  202. package/src/share-payload.js +300 -0
  203. package/src/share.js +185 -0
  204. package/src/show-project-client.js +58 -0
  205. package/src/show-project-flow.js +119 -0
  206. package/src/signup-client.js +103 -0
  207. package/src/signup-copy.js +450 -0
  208. package/src/signup-language.js +21 -0
  209. package/src/skills-client.js +102 -0
  210. package/src/socials-client.js +91 -0
  211. package/src/socials-flow.js +83 -0
  212. package/src/specificity-extract.js +41 -0
  213. package/src/start-add-agents.js +306 -0
  214. package/src/start-add-portfolio.js +388 -0
  215. package/src/start-add-skills.js +260 -0
  216. package/src/stdin-ask.js +129 -0
  217. package/src/steering-extract.js +43 -0
  218. package/src/strip-internal-comments.js +45 -0
  219. package/src/tech-detector.js +295 -0
  220. package/src/templates/report-sheet.html +572 -0
  221. package/src/terminal-agents.js +207 -0
  222. package/src/terminal-format.js +85 -0
  223. package/src/terminal-progress.js +106 -0
  224. package/src/terminal-sections.js +219 -0
  225. package/src/tier-analysis.js +126 -0
  226. package/src/tier-engine.js +114 -0
  227. package/src/tier-projection.js +117 -0
  228. package/src/transcript-turns.js +49 -0
  229. package/src/translations-client.js +84 -0
  230. package/src/typewriter.js +154 -0
  231. package/src/update-flow.js +223 -0
  232. package/src/update-notifier.js +113 -0
  233. package/src/usage-model-calls.js +119 -0
  234. package/src/usage-oneshot-commands.js +180 -0
  235. package/src/usage-repo-scope.js +162 -0
  236. package/src/usage-run-steps.js +354 -0
  237. package/src/work-streams.js +79 -0
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const { isValidEmail, recordConsent } = require('./share');
4
+ const {
5
+ getEmailVerificationRequestUrl,
6
+ getEmailVerificationVerifyUrl,
7
+ } = require('./config');
8
+ const { runEmailVerification } = require('./email-verification');
9
+ const { renderLegalNotice } = require('./legal-notice');
10
+
11
+ // Consent prompt (talents-ai-score, ADR-011 — revises issue 006 / ADR-007's disclosure wall; ADR-051 revises ADR-011's OWN ordering in turn, see below).
12
+
13
+ const MAX_ATTEMPTS = 5;
14
+
15
+ const YES_RE = /^(y|yes|s|si|sí)$/i;
16
+ const NO_RE = /^(n|no)$/i;
17
+
18
+ async function askYesNo(ask, question, catalog, { defaultNo = false } = {}) {
19
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
20
+ const raw = String(await ask(question)).trim();
21
+ if (defaultNo && raw === '') return false;
22
+ if (YES_RE.test(raw)) return true;
23
+ if (NO_RE.test(raw)) return false;
24
+ process.stdout.write(` ${catalog.consent.invalidAnswer}\n`);
25
+ }
26
+ return null;
27
+ }
28
+
29
+ async function askEmail(ask, catalog, question = catalog.consent.emailPrompt) {
30
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
31
+ const raw = String(await ask(question)).trim();
32
+ if (isValidEmail(raw)) return raw;
33
+ process.stdout.write(` ${catalog.consent.invalidEmail}\n`);
34
+ }
35
+ return null;
36
+ }
37
+
38
+ async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
39
+ return runEmailVerification({
40
+ email,
41
+ ask,
42
+ catalog,
43
+ requestUrl: getEmailVerificationRequestUrl(env),
44
+ verifyUrl: getEmailVerificationVerifyUrl(env),
45
+ });
46
+ }
47
+
48
+ // Runs the short consent-to-persist prompt + (if accepted) email collection + (skill-code-certification / ADR-006) EMAIL-OWNERSHIP VERIFICATION.
49
+ async function runConsentPrompt({
50
+ ask,
51
+ catalog,
52
+ env = process.env,
53
+ verifyEmail = defaultVerifyEmail,
54
+ loggedIn = false,
55
+ sessionEmail = null,
56
+ drainBeforeAsk = false,
57
+ profile = 'talent',
58
+ }) {
59
+ const c = catalog.consent;
60
+ // Presentation only (talents-ai-score): framed by src/legal-notice.js on a
61
+ // real TTY, byte-identical (no heading, no rule) otherwise.
62
+ process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
63
+
64
+ if (drainBeforeAsk && typeof ask.drain === 'function') ask.drain();
65
+
66
+ if (loggedIn && sessionEmail && isValidEmail(sessionEmail)) {
67
+ process.stdout.write(` ${c.savedUnderEmail(sessionEmail)}\n`);
68
+ }
69
+
70
+ // defaultNo: a blank Enter means "do not save" (legal requirement).
71
+ const accepted = await askYesNo(ask, c.persistQuestion, catalog, { defaultNo: true });
72
+ if (accepted === null) {
73
+ process.stdout.write(` ${c.notObtained}\n\n`);
74
+ return null;
75
+ }
76
+
77
+ if (!accepted) {
78
+ recordConsent('denied');
79
+ process.stdout.write(` ${c.deniedSaved}\n\n`);
80
+ return 'denied';
81
+ }
82
+
83
+ if (loggedIn) {
84
+ if (sessionEmail && isValidEmail(sessionEmail)) {
85
+ // Identity already proven by the login session — no email prompt, no OTP.
86
+ recordConsent('granted', sessionEmail, { verified: true });
87
+ process.stdout.write(` ${c.grantedSaved(sessionEmail)}\n\n`);
88
+ return 'granted';
89
+ }
90
+ // Logged in, but this session has no cached email — only happens against an older certs deployment that omitted `email` from the login response (see auth-session-store.js).
91
+ process.stdout.write(` ${c.noSessionEmail}\n\n`);
92
+ return null;
93
+ }
94
+
95
+ const email = await askEmail(ask, catalog, profile === 'external' ? c.emailPromptExternal : undefined);
96
+ if (!email) {
97
+ process.stdout.write(` ${c.notObtained}\n\n`);
98
+ return null;
99
+ }
100
+
101
+ process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
102
+
103
+ // Prove email ownership BEFORE persisting (ADR-006) — every profile, external included.
104
+ const verification = await verifyEmail({ email, ask, catalog, env });
105
+ if (verification && verification.verified) {
106
+ recordConsent('granted', email, { verified: true });
107
+ process.stdout.write(` ${c.grantedSaved(email)}\n\n`);
108
+ return 'granted';
109
+ }
110
+
111
+ process.stdout.write(` ${c.notObtained}\n\n`);
112
+ return null;
113
+ }
114
+
115
+ module.exports = { runConsentPrompt, defaultVerifyEmail };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ // Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently (talents-ai-score, DX).
4
+ function computeConsentSkip({ decision, emailVerified, stdinIsTTY, consentFilePath, catalog } = {}) {
5
+ const c = catalog && catalog.consent;
6
+
7
+ // Only a TERMINAL decision is skippable: an explicit DECLINE, or a GRANT whose email ownership was actually verified.
8
+ const terminal =
9
+ decision === 'denied' ||
10
+ (decision === 'granted' && emailVerified !== false);
11
+
12
+ if (terminal) {
13
+ const message = c && typeof c.skipAlreadyDecided === 'function'
14
+ ? c.skipAlreadyDecided(decision, consentFilePath)
15
+ : null;
16
+ return { skip: true, message };
17
+ }
18
+
19
+ if (!stdinIsTTY) {
20
+ const message = c ? c.nonInteractiveWarning || null : null;
21
+ return { skip: false, message };
22
+ }
23
+
24
+ return { skip: false, message: null };
25
+ }
26
+
27
+ module.exports = { computeConsentSkip };
package/src/cv-file.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { expandHome } = require('./env-paths');
6
+
7
+ // The talent names a file on their own machine. Read it only when the resolved file (symlinks
8
+ // followed) really is a CV: an allowed extension, a regular file, under the cap, and starting
9
+ // with that format's signature. Anything else is refused before it is loaded or sent.
10
+ const CV_MAX_BYTES = 10 * 1024 * 1024;
11
+
12
+ const CV_FORMATS = {
13
+ '.pdf': { contentType: 'application/pdf', signature: Buffer.from('%PDF-') },
14
+ '.doc': { contentType: 'application/msword', signature: Buffer.from([0xd0, 0xcf, 0x11, 0xe0]) },
15
+ '.docx': {
16
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
17
+ signature: Buffer.from([0x50, 0x4b, 0x03, 0x04]),
18
+ },
19
+ };
20
+
21
+ function readLocalCv(input, { extensions, badFormat }) {
22
+ if (typeof input !== 'string' || !input.trim()) return { ok: false, reason: 'cv-not-found' };
23
+ const given = expandHome(input.trim());
24
+ const ext = path.extname(given).toLowerCase();
25
+ if (!extensions.includes(ext)) return { ok: false, reason: badFormat };
26
+ const format = CV_FORMATS[ext];
27
+ let real;
28
+ try {
29
+ real = fs.realpathSync(given);
30
+ } catch {
31
+ return { ok: false, reason: 'cv-not-found' };
32
+ }
33
+ if (path.extname(real).toLowerCase() !== ext) return { ok: false, reason: badFormat };
34
+
35
+ let fd;
36
+ try {
37
+ fd = fs.openSync(real, 'r');
38
+ } catch {
39
+ return { ok: false, reason: 'cv-not-found' };
40
+ }
41
+ try {
42
+ const stat = fs.fstatSync(fd);
43
+ if (!stat.isFile()) return { ok: false, reason: 'cv-not-found' };
44
+ if (stat.size > CV_MAX_BYTES) return { ok: false, reason: 'cv-too-large' };
45
+ const data = Buffer.alloc(stat.size);
46
+ let read = 0;
47
+ while (read < stat.size) {
48
+ const n = fs.readSync(fd, data, read, stat.size - read, read);
49
+ if (n === 0) break;
50
+ read += n;
51
+ }
52
+ const content = data.subarray(0, read);
53
+ if (!content.subarray(0, format.signature.length).equals(format.signature)) return { ok: false, reason: badFormat };
54
+ return { ok: true, file: { filename: path.basename(given), contentType: format.contentType, data: content } };
55
+ } catch {
56
+ return { ok: false, reason: 'cv-not-found' };
57
+ } finally {
58
+ fs.closeSync(fd);
59
+ }
60
+ }
61
+
62
+ module.exports = { readLocalCv, CV_MAX_BYTES };
package/src/cv-text.js ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ // Plain text out of the talent's CV (PDF, DOC, DOCX), so the AI can pre-fill from
4
+ // what the CV says. Zero-dependency on purpose (install.sh copies the CLI without
5
+ // npm install): on macOS PDFs go through the system PDFKit (JXA) and Word files
6
+ // through textutil; elsewhere PDFs through poppler's pdftotext when installed.
7
+ // With none, the caller asks the talent to attach the CV to the chat instead.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { execFileSync } = require('child_process');
12
+ const { expandHome } = require('./env-paths');
13
+
14
+ const MAX_BYTES = 10 * 1024 * 1024;
15
+ const MAX_CHARS = 20000;
16
+
17
+ const JXA_PDF_TEXT = 'function run(argv){ObjC.import("PDFKit");'
18
+ + 'const d=$.PDFDocument.alloc.initWithURL($.NSURL.fileURLWithPath(argv[0]));'
19
+ + 'return d.isNil()?"":d.string.js}';
20
+
21
+ function run(cmd, args, exec) {
22
+ return String(exec(cmd, args, { encoding: 'utf8', timeout: 20000, maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }) || '');
23
+ }
24
+
25
+ function extractors(platform, file) {
26
+ const list = [];
27
+ if (/\.docx?$/i.test(file)) {
28
+ if (platform === 'darwin') list.push({ name: 'textutil', cmd: 'textutil', args: (f) => ['-convert', 'txt', '-stdout', f] });
29
+ return list;
30
+ }
31
+ if (platform === 'darwin') list.push({ name: 'pdfkit', cmd: 'osascript', args: (f) => ['-l', 'JavaScript', '-e', JXA_PDF_TEXT, f] });
32
+ list.push({ name: 'pdftotext', cmd: 'pdftotext', args: (f) => ['-layout', '-q', f, '-'] });
33
+ return list;
34
+ }
35
+
36
+ const fold = (text) => String(text || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
37
+
38
+ // Whether the CV text names the talent: every name token of 3+ letters present.
39
+ // Lets the AI drop a stranger's CV without showing its contents.
40
+ function mentionsName(text, talentName) {
41
+ const tokens = fold(talentName).split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
42
+ if (tokens.length === 0) return null;
43
+ const body = fold(text);
44
+ return tokens.every((t) => body.includes(t));
45
+ }
46
+
47
+ const uniq = (list) => [...new Set(list)];
48
+
49
+ // Contact links a CV header usually carries, so the AI can fill them directly.
50
+ function extractLinks(text) {
51
+ const t = String(text || '');
52
+ return {
53
+ linkedin: uniq((t.match(/(?:https?:\/\/)?(?:[a-z]{2,3}\.)?linkedin\.com\/in\/[A-Za-z0-9_%-]+/gi) || []).map((u) => `https://www.${u.replace(/^https?:\/\//i, '').replace(/^(?:[a-z]{2,3}\.)?linkedin/i, 'linkedin')}`)),
54
+ github: uniq((t.match(/(?:https?:\/\/)?github\.com\/[A-Za-z0-9-]+/gi) || []).map((u) => `https://${u.replace(/^https?:\/\//i, '')}`)),
55
+ emails: uniq(t.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g) || []),
56
+ phones: uniq((t.match(/\+\d{1,3}[\s.-]?\d[\d\s.-]{6,14}\d/g) || []).map((p) => p.replace(/[\s.-]+/g, ' ').trim())),
57
+ };
58
+ }
59
+
60
+ function readCvText(input, { platform = process.platform, exec = execFileSync, env = process.env, talentName = null } = {}) {
61
+ const file = expandHome(input, env);
62
+ if (typeof file !== 'string' || !path.isAbsolute(file) || !/\.(pdf|docx?)$/i.test(file)) return { ok: false, reason: 'not-a-cv-path' };
63
+ let stat;
64
+ try {
65
+ stat = fs.statSync(file);
66
+ } catch {
67
+ return { ok: false, reason: 'not-found' };
68
+ }
69
+ if (!stat.isFile()) return { ok: false, reason: 'not-found' };
70
+ if (stat.size > MAX_BYTES) return { ok: false, reason: 'too-large' };
71
+
72
+ for (const extractor of extractors(platform, file)) {
73
+ let text;
74
+ try {
75
+ text = run(extractor.cmd, extractor.args(file), exec).trim();
76
+ } catch {
77
+ continue;
78
+ }
79
+ if (!text) continue;
80
+ return {
81
+ ok: true,
82
+ extractor: extractor.name,
83
+ mentionsTalent: mentionsName(text, talentName),
84
+ links: extractLinks(text),
85
+ truncated: text.length > MAX_CHARS,
86
+ text: text.slice(0, MAX_CHARS),
87
+ };
88
+ }
89
+ return { ok: false, reason: 'no-text-extracted' };
90
+ }
91
+
92
+ module.exports = { readCvText, extractLinks, mentionsName, MAX_CHARS };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const { eachHumanText } = require('./transcript-turns');
4
+ const { scrubSecrets } = require('./agent-synthesis');
5
+
6
+ const MAX_EXCERPTS = 20;
7
+ const MAX_EXCERPT_LEN = 200;
8
+
9
+ const DECISION_RE =
10
+ /\b(decid\w*|decision|decisión|vamos a|vamos con|voy con|let'?s|we'?ll|we will|prefer|prefiero|option|opción|elij\w*|choose|chose|choice|pick|go with|descarta\w*|opta|trade-?off|instead of|en vez de|en lugar de|rather than|because|porque|reason|razón)\b/i;
11
+
12
+ function makeDecisionsAccumulator() {
13
+ let decisionExchangeCount = 0;
14
+ const excerpts = [];
15
+ const seen = new Set();
16
+ return {
17
+ add(text) {
18
+ if (!DECISION_RE.test(text)) return;
19
+ decisionExchangeCount += 1;
20
+ if (excerpts.length >= MAX_EXCERPTS) return;
21
+ const scrubbed = scrubSecrets(text)
22
+ .replace(/\s+/g, ' ')
23
+ .trim()
24
+ .slice(0, MAX_EXCERPT_LEN);
25
+ if (scrubbed && !seen.has(scrubbed)) {
26
+ seen.add(scrubbed);
27
+ excerpts.push(scrubbed);
28
+ }
29
+ },
30
+ result() {
31
+ if (decisionExchangeCount === 0) return null;
32
+ return { decisionExchangeCount, redactedDecisionExcerpts: excerpts };
33
+ },
34
+ };
35
+ }
36
+
37
+ function collectDecisions(env = process.env, selectedCwds = null) {
38
+ const acc = makeDecisionsAccumulator();
39
+ for (const text of eachHumanText(env, { selectedCwds })) acc.add(text);
40
+ return acc.result();
41
+ }
42
+
43
+ module.exports = { collectDecisions, makeDecisionsAccumulator };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+ const {
6
+ toolSources,
7
+ listSessionFiles,
8
+ readSessionObjects,
9
+ resolveFileCwd,
10
+ } = require('./session-scan');
11
+ const { normalizeRemote } = require('./authorship');
12
+
13
+ const GIT_TIMEOUT_MS = 15000;
14
+
15
+ function git(cwd, args) {
16
+ try {
17
+ const out = execFileSync('git', ['-C', cwd, ...args], {
18
+ encoding: 'utf8',
19
+ timeout: GIT_TIMEOUT_MS,
20
+ stdio: ['ignore', 'pipe', 'ignore'],
21
+ maxBuffer: 4 * 1024 * 1024,
22
+ });
23
+ return out ? out.trim() : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function repoIdentityForCwd(cwd, gitRunner = git) {
30
+ const toplevel = gitRunner(cwd, ['rev-parse', '--show-toplevel']) || null;
31
+ const remoteRaw = gitRunner(cwd, ['config', '--get', 'remote.origin.url']);
32
+ const remote = remoteRaw && remoteRaw.trim() ? normalizeRemote(remoteRaw.trim()) : null;
33
+ return { toplevel, remote };
34
+ }
35
+
36
+ function sessionCwdCounts(env = process.env) {
37
+ const counts = new Map();
38
+ let unassigned = 0;
39
+ for (const source of toolSources(env)) {
40
+ for (const file of listSessionFiles(source.dir)) {
41
+ const cwd = resolveFileCwd(readSessionObjects(file));
42
+ if (cwd === null) {
43
+ unassigned += 1;
44
+ continue;
45
+ }
46
+ counts.set(cwd, (counts.get(cwd) || 0) + 1);
47
+ }
48
+ }
49
+ return { counts, unassigned };
50
+ }
51
+
52
+ function labelFor(remote, groupKey) {
53
+ if (remote) return remote;
54
+ return path.basename(groupKey) || groupKey;
55
+ }
56
+
57
+ function detectRepos(env = process.env, { gitRunner = git } = {}) {
58
+ const { counts, unassigned } = sessionCwdCounts(env);
59
+
60
+ const byGroup = new Map();
61
+ for (const [cwd, sessionCount] of counts) {
62
+ const { toplevel, remote } = repoIdentityForCwd(cwd, gitRunner);
63
+ const groupKey = toplevel || cwd;
64
+ let group = byGroup.get(groupKey);
65
+ if (!group) {
66
+ group = {
67
+ toplevel: toplevel || null,
68
+ remote: remote || null,
69
+ cwds: new Set(),
70
+ sessionCount: 0,
71
+ resolved: !!toplevel,
72
+ };
73
+ byGroup.set(groupKey, group);
74
+ }
75
+ group.cwds.add(cwd);
76
+ group.sessionCount += sessionCount;
77
+ if (!group.remote && remote) group.remote = remote;
78
+ }
79
+
80
+ const repos = [...byGroup.entries()]
81
+ .map(([groupKey, g]) => ({
82
+ id: g.remote || g.toplevel || groupKey,
83
+ toplevel: g.toplevel,
84
+ remote: g.remote,
85
+ label: labelFor(g.remote, groupKey),
86
+ sessionCount: g.sessionCount,
87
+ cwds: [...g.cwds].sort(),
88
+ resolved: g.resolved,
89
+ }))
90
+ .sort(
91
+ (a, b) => b.sessionCount - a.sessionCount || a.label.localeCompare(b.label),
92
+ );
93
+
94
+ return { repos, unassignedSessionCount: unassigned };
95
+ }
96
+
97
+ function normalizeFlagToken(token) {
98
+ return String(token || '').trim().toLowerCase().replace(/\/+$/, '');
99
+ }
100
+
101
+ function repoMatchesToken(repo, token) {
102
+ const t = normalizeFlagToken(token);
103
+ if (!t) return false;
104
+ return [repo.id, repo.remote, repo.toplevel, repo.label, path.basename(repo.toplevel || repo.id)]
105
+ .filter((x) => typeof x === 'string' && x)
106
+ .some((x) => normalizeFlagToken(x) === t || normalizeFlagToken(x).endsWith(`/${t}`));
107
+ }
108
+
109
+ function matchReposByFlag(repos, tokens) {
110
+ const indices = new Set();
111
+ const unmatched = [];
112
+ for (const token of tokens || []) {
113
+ const idx = repos.findIndex((r) => repoMatchesToken(r, token));
114
+ if (idx === -1) unmatched.push(token);
115
+ else indices.add(idx);
116
+ }
117
+ return { indices: [...indices].sort((a, b) => a - b), unmatched };
118
+ }
119
+
120
+ function scopeFromSelection(repos, indices) {
121
+ const selectedCwds = new Set();
122
+ const selectedToplevels = [];
123
+ for (const i of indices) {
124
+ const repo = repos[i];
125
+ if (!repo) continue;
126
+ for (const c of repo.cwds) selectedCwds.add(c);
127
+ selectedToplevels.push(repo.toplevel || repo.id);
128
+ }
129
+ return { selectedCwds, selectedToplevels };
130
+ }
131
+
132
+ function indexOfCurrentRepo(repos, cwdToplevel) {
133
+ if (!cwdToplevel) return -1;
134
+ return repos.findIndex((r) => r.toplevel === cwdToplevel);
135
+ }
136
+
137
+ module.exports = {
138
+ detectRepos,
139
+ repoIdentityForCwd,
140
+ sessionCwdCounts,
141
+ matchReposByFlag,
142
+ repoMatchesToken,
143
+ scopeFromSelection,
144
+ indexOfCurrentRepo,
145
+ };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ // Single source of truth for turning a tools array ({id, name, detected}) into what the report shows.
4
+
5
+ function detectedTools(tools) {
6
+ return Array.isArray(tools) ? tools.filter((t) => t && t.detected === true) : [];
7
+ }
8
+
9
+ function detectedToolNames(tools) {
10
+ return detectedTools(tools)
11
+ .map((t) => (t && (t.name || t.id)) || null)
12
+ .filter(Boolean);
13
+ }
14
+
15
+ module.exports = { detectedTools, detectedToolNames };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const { collectAuthorship, attributeSample } = require('./authorship');
4
+ const { detectTechnologyManifests } = require('./tech-detector');
5
+ const { agentSourceFilesByName } = require('./agent-org-chart');
6
+ const { classifyVendored, collectProvenance } = require('./detection-noise');
7
+
8
+ // Per-CANDIDATE authorship + depth (hybrid evaluation, Slice 3c).
9
+
10
+ const SKILL_DEPTH_SATURATION = 3;
11
+ const AGENT_DEPTH_SATURATION = 8;
12
+ const DEPTH_FLOOR = 0.3;
13
+
14
+ function round2(n) {
15
+ return Math.round(n * 100) / 100;
16
+ }
17
+
18
+ // Maps a raw count to (0,1]: DEPTH_FLOOR..1, so a detected-but-shallow candidate
19
+ // is ordered DOWN without being excluded. `null` when there is no signal.
20
+ function normalizedDepth(signal, saturation) {
21
+ if (typeof signal !== 'number' || !Number.isFinite(signal) || signal <= 0) {
22
+ return null;
23
+ }
24
+ const ratio = Math.min(1, signal / saturation);
25
+ return round2(DEPTH_FLOOR + (1 - DEPTH_FLOOR) * ratio);
26
+ }
27
+
28
+ function recencyDaysForPaths(relPaths, authorship, nowSeconds) {
29
+ if (!Array.isArray(relPaths) || relPaths.length === 0) return null;
30
+ let mostRecent = null;
31
+ for (const p of relPaths) {
32
+ const t = authorship.mostRecentCommitTime(p);
33
+ if (typeof t === 'number' && Number.isFinite(t)) {
34
+ mostRecent = mostRecent === null ? t : Math.max(mostRecent, t);
35
+ }
36
+ }
37
+ if (mostRecent === null) return null;
38
+ return Math.max(0, Math.floor((nowSeconds - mostRecent) / 86400));
39
+ }
40
+
41
+ // Attributes a candidate's evidence files against the talent's own email via the ADR-017 gate.
42
+ function attributionCounts(relPaths, verifiedEmail, authorship) {
43
+ if (!Array.isArray(relPaths) || relPaths.length === 0) {
44
+ return { authoredFileCount: 0, totalFileCount: 0, authorshipKnown: false };
45
+ }
46
+ const result = attributeSample(
47
+ { files: relPaths.map((p) => ({ path: p })) },
48
+ verifiedEmail,
49
+ authorship,
50
+ );
51
+ const totalFileCount = result.fileAttribution.filter(
52
+ (f) => f.authors.length > 0,
53
+ ).length;
54
+ return {
55
+ authoredFileCount: result.attributableFiles.length,
56
+ totalFileCount,
57
+ authorshipKnown: authorship.available && totalFileCount > 0,
58
+ };
59
+ }
60
+
61
+ // Builds `report.detectionEvidence` = `{ skills, agents, provenance } | null`.
62
+ function collectDetectionEvidence(root, verifiedEmail, report) {
63
+ const scanRoot = root || process.cwd();
64
+ const authorship = collectAuthorship(scanRoot);
65
+ const nowSeconds = Math.floor(Date.now() / 1000);
66
+
67
+ const skills = detectTechnologyManifests(scanRoot).map(
68
+ ({ tech, manifestPaths }) => ({
69
+ tech,
70
+ ...attributionCounts(manifestPaths, verifiedEmail, authorship),
71
+ depth: normalizedDepth(manifestPaths.length, SKILL_DEPTH_SATURATION),
72
+ vendored: classifyVendored(manifestPaths),
73
+ recencyDays: recencyDaysForPaths(manifestPaths, authorship, nowSeconds),
74
+ }),
75
+ );
76
+
77
+ const sourceByName = agentSourceFilesByName(scanRoot);
78
+ const reportAgents =
79
+ report && Array.isArray(report.agents) ? report.agents : [];
80
+ const agents = reportAgents.map((agent) => {
81
+ const rel = sourceByName.get(agent.name) || null;
82
+ const tools = Array.isArray(agent.tools) ? agent.tools : [];
83
+ const evidencePaths = rel ? [rel] : [];
84
+ return {
85
+ name: agent.name,
86
+ tools,
87
+ model: agent.model || null,
88
+ ...attributionCounts(evidencePaths, verifiedEmail, authorship),
89
+ depth: normalizedDepth(tools.length, AGENT_DEPTH_SATURATION),
90
+ vendored: classifyVendored(evidencePaths),
91
+ recencyDays: recencyDaysForPaths(evidencePaths, authorship, nowSeconds),
92
+ };
93
+ });
94
+
95
+ if (skills.length === 0 && agents.length === 0) return null;
96
+ return { skills, agents, provenance: collectProvenance(scanRoot) };
97
+ }
98
+
99
+ module.exports = {
100
+ collectDetectionEvidence,
101
+ normalizedDepth,
102
+ SKILL_DEPTH_SATURATION,
103
+ AGENT_DEPTH_SATURATION,
104
+ DEPTH_FLOOR,
105
+ };