shakers-cli 0.14.2 → 0.16.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 (123) hide show
  1. package/README.md +51 -1203
  2. package/bin/ai-usage.js +11 -56
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +13 -9
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +17 -4
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/agent-category.js +1 -1
  16. package/src/agent-evaluation.js +3 -3
  17. package/src/agent-org-chart.js +5 -5
  18. package/src/agent-synthesis.js +3 -3
  19. package/src/agent-usage.js +1 -1
  20. package/src/agents-client.js +4 -4
  21. package/src/ai.js +1 -1
  22. package/src/ansi.js +1 -1
  23. package/src/auth-client.js +18 -4
  24. package/src/auth-method-picker.js +1 -1
  25. package/src/auth-methods.js +1 -1
  26. package/src/auth-session-store.js +11 -8
  27. package/src/authorship-mismatch.js +1 -1
  28. package/src/authorship.js +2 -2
  29. package/src/automations-detector.js +1 -1
  30. package/src/backend-request.js +20 -3
  31. package/src/brand-ansi.js +1 -1
  32. package/src/browser-tools-detector.js +1 -1
  33. package/src/certify-disclaimer.js +1 -1
  34. package/src/certify-remediation-prompt.js +1 -1
  35. package/src/cli-args.js +2 -2
  36. package/src/command-audit.js +8 -5
  37. package/src/config-dir.js +1 -1
  38. package/src/config.js +112 -48
  39. package/src/consent-flow.js +4 -4
  40. package/src/consent-skip.js +1 -1
  41. package/src/detection-evidence.js +2 -2
  42. package/src/detection-noise.js +1 -1
  43. package/src/detectors.js +1 -1
  44. package/src/device-login.js +12 -1
  45. package/src/email-verification.js +3 -3
  46. package/src/env-paths.js +1 -1
  47. package/src/find-projects-client.js +12 -0
  48. package/src/find-projects-flow.js +1 -13
  49. package/src/git-activity.js +1 -1
  50. package/src/graph-certs.js +3 -3
  51. package/src/graph-scan.js +9 -9
  52. package/src/i18n.js +124 -124
  53. package/src/interactive-select.js +1 -1
  54. package/src/interview-client.js +6 -1
  55. package/src/interview-transcript.js +1 -1
  56. package/src/legal-notice.js +1 -1
  57. package/src/maturity.js +6 -6
  58. package/src/mcp-ai-usage-tool.js +1 -1
  59. package/src/mcp-auth-form.js +1 -1
  60. package/src/mcp-detector.js +3 -3
  61. package/src/mcp-device-login-tools.js +2 -1
  62. package/src/mcp-find-projects-tools.js +1 -2
  63. package/src/mcp-onboarding-tools.js +4 -4
  64. package/src/mcp-prompts.js +2 -2
  65. package/src/mcp-register-tools.js +1 -0
  66. package/src/mcp-server.js +5 -1
  67. package/src/mcp-signup-client.js +2 -2
  68. package/src/mcp-signup-tools.js +26 -2
  69. package/src/memory-structure-detector.js +1 -1
  70. package/src/model-call-record.js +2 -2
  71. package/src/onboarding-flow.js +16 -16
  72. package/src/onboarding-funnel.js +110 -17
  73. package/src/open-file.js +1 -1
  74. package/src/osc-link.js +1 -1
  75. package/src/portfolio-client.js +11 -1
  76. package/src/portfolio-git-info.js +1 -1
  77. package/src/profile-flow.js +1 -1
  78. package/src/profile-switch.js +5 -12
  79. package/src/render-certification.js +10 -10
  80. package/src/render-html.js +6 -6
  81. package/src/render-sheet.js +10 -10
  82. package/src/render-terminal.js +6 -6
  83. package/src/repo-scope.js +132 -0
  84. package/src/report-gating.js +1 -1
  85. package/src/report-store.js +6 -6
  86. package/src/roadmap-content.js +5 -5
  87. package/src/roadmap-personalization.js +3 -3
  88. package/src/roadmap-prompt.js +2 -2
  89. package/src/scan-exclusions.js +1 -1
  90. package/src/scanner.js +14 -14
  91. package/src/secret-prompt.js +1 -1
  92. package/src/session-gate.js +1 -1
  93. package/src/session-refresh.js +33 -3
  94. package/src/session-scan.js +1 -1
  95. package/src/shakers-wordmark.js +1 -1
  96. package/src/share-card.js +2 -2
  97. package/src/share-certification.js +4 -4
  98. package/src/share-consent-state.js +7 -7
  99. package/src/share-payload.js +7 -7
  100. package/src/share.js +7 -7
  101. package/src/signup-client.js +1 -1
  102. package/src/signup-copy.js +1 -1
  103. package/src/skills-client.js +1 -1
  104. package/src/start-add-agents.js +2 -2
  105. package/src/start-add-portfolio.js +3 -13
  106. package/src/start-add-skills.js +2 -2
  107. package/src/stdin-ask.js +3 -3
  108. package/src/strip-internal-comments.js +1 -1
  109. package/src/tech-detector.js +2 -2
  110. package/src/templates/report-sheet.html +11 -13
  111. package/src/terminal-agents.js +8 -8
  112. package/src/terminal-format.js +3 -3
  113. package/src/terminal-sections.js +11 -11
  114. package/src/tier-engine.js +3 -3
  115. package/src/tier-projection.js +2 -2
  116. package/src/transcript-turns.js +1 -1
  117. package/src/typewriter.js +1 -1
  118. package/src/usage-model-calls.js +6 -6
  119. package/src/usage-oneshot-commands.js +5 -5
  120. package/src/usage-repo-scope.js +11 -129
  121. package/src/usage-run-steps.js +32 -184
  122. package/src/usage-scan.js +235 -0
  123. package/src/work-streams.js +1 -1
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { getConfigDir } = require('./config-dir');
7
7
 
8
- // Names and values mirror works-frontend onboarding-funnel-enums.ts.
8
+ // Names and values mirror the web onboarding funnel enums.
9
9
 
10
10
  const FLOW_VERSION = 'signup_new_works_v1';
11
11
 
@@ -48,10 +48,32 @@ const WORK_SITUATION_VALUES = Object.freeze({
48
48
  OTHER: 'other',
49
49
  });
50
50
 
51
+ const PRE_ACCOUNT_EVENTS = Object.freeze({
52
+ start: { onboarding_step_viewed: [], onboarding_step_completed: ['time_on_step_ms'] },
53
+ account_creation: {
54
+ onboarding_step_viewed: ['auth_methods_offered'],
55
+ onboarding_step_errored: ['error_type'],
56
+ onboarding_step_exited: ['time_on_step_ms', 'exited_to_step_name', 'exited_to_step_index'],
57
+ },
58
+ });
59
+ const AUTH_METHODS = Object.freeze(['google', 'linkedin', 'email']);
60
+ const PRE_ACCOUNT_VALUES = Object.freeze({
61
+ time_on_step_ms: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
62
+ auth_methods_offered: (v) => Array.isArray(v) && v.every((m) => AUTH_METHODS.includes(m)),
63
+ error_type: (v) => ERROR_TYPES.includes(v),
64
+ exited_to_step_name: (v) => v === 'start',
65
+ exited_to_step_index: (v) => v === STEP_INDEX.start,
66
+ });
67
+ const COMMON_PROPERTIES = new Set(['step_name', 'step_index', 'flow_version', 'surface', 'mcp_client']);
68
+ const MAX_EVENT_AGE_MS = 7 * 24 * 60 * 60 * 1000;
69
+ const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
70
+
51
71
  const ONCE_PER_STEP = new Set(['onboarding_step_viewed', 'onboarding_step_completed']);
52
72
  const MAX_EVENTS_PER_REQUEST = 50;
53
73
  const SEND_TIMEOUT_MS = 3000;
74
+ const EXIT_FLUSH_CAP_MS = 1500;
54
75
  const DEVICE_FILE_NAME = 'analytics-device.json';
76
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
55
77
 
56
78
  function mcpClientFor(clientName) {
57
79
  const name = typeof clientName === 'string' ? clientName.toLowerCase() : '';
@@ -83,7 +105,8 @@ function rateTypes({ partTimeSelected, fullTimeSelected } = {}) {
83
105
 
84
106
  function errorTypeFor(reason) {
85
107
  const r = String(reason || '');
86
- if (r === 'no-session' || r === 'no-hub-token' || r === 'http-401' || r === 'http-403') return 'auth';
108
+ if (r === 'no-session' || r === 'no-hub-token' || r === 'http-401' || r === 'http-403' || r === 'login-failed' || r.startsWith('google-')) return 'auth';
109
+ if (r === 'missing-fields' || r === 'weak-password') return 'validation';
87
110
  if (r === 'timeout') return 'timeout';
88
111
  if (r === 'no-endpoint') return 'server';
89
112
  if (r.startsWith('bad-') || r.startsWith('no-') || r === 'duplicate-language' || r === 'unresolved-languages') return 'validation';
@@ -126,12 +149,27 @@ function loadOrCreateDeviceId(env = process.env, { fs: fsImpl = fs } = {}) {
126
149
  }
127
150
  }
128
151
 
129
- async function defaultSend({ endpoint, events, hubAccessToken }) {
152
+ async function postEvents(endpoint, body, headers) {
130
153
  const { postJsonWithTimeout } = require('./backend-request');
131
- const res = await postJsonWithTimeout(endpoint, { events }, SEND_TIMEOUT_MS, 'POST', null, { Authorization: `Bearer ${hubAccessToken}` });
154
+ const res = await postJsonWithTimeout(endpoint, body, SEND_TIMEOUT_MS, 'POST', null, headers);
132
155
  return { ok: res.status >= 200 && res.status < 300, status: res.status };
133
156
  }
134
157
 
158
+ function defaultSend({ endpoint, events, hubAccessToken }) {
159
+ return postEvents(endpoint, { events }, { Authorization: `Bearer ${hubAccessToken}` });
160
+ }
161
+
162
+ function defaultSendToAmplitude({ endpoint, apiKey, events }) {
163
+ return postEvents(endpoint, { api_key: apiKey, events }, null);
164
+ }
165
+
166
+ function isPreAccount(item) {
167
+ const step = PRE_ACCOUNT_EVENTS[item.properties.step_name];
168
+ const allowed = step && step[item.eventType];
169
+ return !!allowed && Object.entries(item.properties)
170
+ .every(([key, value]) => COMMON_PROPERTIES.has(key) || (allowed.includes(key) && PRE_ACCOUNT_VALUES[key](value)));
171
+ }
172
+
135
173
  function defaultHubToken() {
136
174
  const { loadAuthSession, sessionStatus } = require('./auth-session-store');
137
175
  const session = loadAuthSession();
@@ -143,7 +181,9 @@ function createFunnel({
143
181
  hubToken = defaultHubToken,
144
182
  deviceId = () => loadOrCreateDeviceId(),
145
183
  endpoint = () => require('./config').getOnboardingEventsEndpoint(),
184
+ amplitude = () => require('./config').getAmplitudeIngestion(),
146
185
  send = defaultSend,
186
+ sendToAmplitude = defaultSendToAmplitude,
147
187
  now = () => Date.now(),
148
188
  uuid = () => crypto.randomUUID(),
149
189
  } = {}) {
@@ -172,34 +212,78 @@ function createFunnel({
172
212
  return true;
173
213
  }
174
214
 
215
+ function currentDeviceId() {
216
+ if (cachedDeviceId === undefined) cachedDeviceId = safe(deviceId);
217
+ return cachedDeviceId;
218
+ }
219
+
175
220
  // Funnel order, each time strictly after the previous one sent.
176
221
  function stamp(batch) {
177
222
  const ordered = [...batch].sort((a, b) => a.properties.step_index - b.properties.step_index || a.seq - b.seq);
178
- const id = cachedDeviceId === undefined ? (cachedDeviceId = safe(deviceId)) : cachedDeviceId;
179
- return ordered.map((e) => {
180
- lastTime = Math.max(e.at, lastTime + 1);
181
- return { event_type: e.eventType, insert_id: e.insertId, time: lastTime, ...(id ? { device_id: id } : {}), properties: e.properties };
223
+ return ordered.map((item) => {
224
+ if (item.time === undefined) {
225
+ lastTime = Math.max(item.at, lastTime + 1);
226
+ item.time = lastTime;
227
+ }
228
+ return { item, time: item.time };
182
229
  });
183
230
  }
184
231
 
185
- function flush() {
186
- if (!ready || !pending.length) return Promise.resolve();
232
+ function post(stamped, toWire, deliver) {
233
+ const sends = [];
234
+ for (let i = 0; i < stamped.length; i += MAX_EVENTS_PER_REQUEST) {
235
+ const chunk = stamped.slice(i, i + MAX_EVENTS_PER_REQUEST);
236
+ sends.push(Promise.resolve()
237
+ .then(() => deliver(chunk.map(toWire)))
238
+ .then((res) => !!(res && res.ok), () => false)
239
+ .then((ok) => (ok ? [] : chunk.map((e) => e.item))));
240
+ }
241
+ return Promise.all(sends).then((failed) => failed.flat());
242
+ }
243
+
244
+ function flushAsTalent() {
187
245
  const token = safe(hubToken);
188
246
  const url = safe(endpoint);
189
247
  // Never send under another account signed in since.
190
248
  if (!token || !url || accountOf(token) !== account) return Promise.resolve();
191
- const events = stamp(pending);
249
+ const batch = pending;
192
250
  pending = [];
193
- const sends = [];
194
- for (let i = 0; i < events.length; i += MAX_EVENTS_PER_REQUEST) {
195
- sends.push(Promise.resolve().then(() => send({ endpoint: url, events: events.slice(i, i + MAX_EVENTS_PER_REQUEST), hubAccessToken: token })).catch(() => null));
196
- }
197
- const done = Promise.all(sends).then(() => undefined, () => undefined);
251
+ const id = currentDeviceId();
252
+ const toWire = ({ item, time }) => ({ event_type: item.eventType, insert_id: item.insertId, time, ...(id ? { device_id: id } : {}), properties: item.properties });
253
+ return post(stamp(batch), toWire, (events) => send({ endpoint: url, events, hubAccessToken: token })).then(() => undefined);
254
+ }
255
+
256
+ function flushToAmplitude() {
257
+ const id = currentDeviceId();
258
+ const target = safe(amplitude);
259
+ if (!id || !UUID_RE.test(id) || !target) return Promise.resolve();
260
+ const batch = pending.filter((e) => !e.directFailed && isPreAccount(e));
261
+ if (!batch.length) return Promise.resolve();
262
+ pending = pending.filter((e) => !batch.includes(e));
263
+ const at = now();
264
+ const stamped = stamp(batch);
265
+ const inWindow = ({ time }) => time >= at - MAX_EVENT_AGE_MS && time <= at + MAX_CLOCK_SKEW_MS;
266
+ const outOfWindow = stamped.filter((e) => !inWindow(e)).map((e) => e.item);
267
+ const toWire = ({ item, time }) => ({ event_type: item.eventType, device_id: id, insert_id: item.insertId, time, event_properties: item.properties });
268
+ return post(stamped.filter(inWindow), toWire, (events) => sendToAmplitude({ endpoint: target.endpoint, apiKey: target.apiKey, events })).then((failed) => {
269
+ const waiting = [...outOfWindow, ...failed];
270
+ if (!waiting.length) return undefined;
271
+ for (const e of waiting) e.directFailed = true;
272
+ pending.push(...waiting);
273
+ return ready ? flush() : undefined;
274
+ });
275
+ }
276
+
277
+ function flush() {
278
+ if (!pending.length) return Promise.resolve();
279
+ const done = (ready ? flushAsTalent() : flushToAmplitude()).then(() => undefined, () => undefined);
198
280
  inflight.add(done);
199
281
  done.then(() => inflight.delete(done));
200
282
  return done;
201
283
  }
202
284
 
285
+ const idle = () => Promise.all([...inflight]).then(() => undefined);
286
+
203
287
  return {
204
288
  surface,
205
289
  begin() {
@@ -221,7 +305,13 @@ function createFunnel({
221
305
  return flush();
222
306
  },
223
307
  flush,
224
- idle: () => Promise.all([...inflight]).then(() => undefined),
308
+ idle,
309
+ settle(capMs = EXIT_FLUSH_CAP_MS) {
310
+ flush();
311
+ let timer;
312
+ const cap = new Promise((resolve) => { timer = setTimeout(resolve, capMs); });
313
+ return Promise.race([idle(), cap]).then(() => clearTimeout(timer));
314
+ },
225
315
  };
226
316
  }
227
317
 
@@ -248,6 +338,7 @@ const NOOP_FUNNEL = Object.freeze({
248
338
  identified: () => Promise.resolve(),
249
339
  flush: () => Promise.resolve(),
250
340
  idle: () => Promise.resolve(),
341
+ settle: () => Promise.resolve(),
251
342
  });
252
343
 
253
344
  module.exports = {
@@ -259,6 +350,8 @@ module.exports = {
259
350
  SOURCE_TYPES,
260
351
  ERROR_TYPES,
261
352
  MAX_EVENTS_PER_REQUEST,
353
+ EXIT_FLUSH_CAP_MS,
354
+ PRE_ACCOUNT_EVENTS,
262
355
  DEVICE_FILE_NAME,
263
356
  NOOP_FUNNEL,
264
357
  createFunnel,
package/src/open-file.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { spawn } = require('child_process');
4
4
 
5
- // Zero-dependency, best-effort "open this file in the OS default app" (ADR-016, used by the `report` command to open the generated HTML in the browser).
5
+ // Zero-dependency, best-effort "open this file in the OS default app" (used by the `report` command to open the generated HTML in the browser).
6
6
  function openPath(target) {
7
7
  let cmd;
8
8
  let args;
package/src/osc-link.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // OSC 8 terminal hyperlink helper (talents-ai-score, user request: links were printed as plain text and could NOT be clicked in iTerm2).
3
+ // OSC 8 terminal hyperlink helper (links were printed as plain text and could NOT be clicked in iTerm2).
4
4
 
5
5
  const OSC = '\x1b]8;;';
6
6
  const ST = '\x1b\\';
@@ -3,7 +3,7 @@
3
3
  const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
4
4
  const { reasonForError, WORKS_CODE_TO_REASON } = require('./skills-client');
5
5
 
6
- // Talent-portfolio client (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, src/start-add-portfolio.js).
6
+ // Talent-portfolio client (`start`'s "Add project to portfolio" route, src/start-add-portfolio.js).
7
7
 
8
8
  const DEFAULT_TIMEOUT_MS = 20000;
9
9
 
@@ -155,10 +155,20 @@ async function requestUpdatePortfolioSkills(
155
155
  return { ok: true };
156
156
  }
157
157
 
158
+ function normalizeDomain(raw) {
159
+ let s = String(raw || '').trim().toLowerCase();
160
+ if (!s) return '';
161
+ s = s.replace(/^https?:\/\//, '');
162
+ s = s.split('/')[0].split('?')[0].split('#')[0];
163
+ s = s.replace(/^www\./, '');
164
+ return s;
165
+ }
166
+
158
167
  module.exports = {
159
168
  requestListPortfolios,
160
169
  requestDeclarePortfolio,
161
170
  requestDraftDescription,
162
171
  requestUpdatePortfolioSkills,
163
172
  WORKS_CODE_TO_REASON,
173
+ normalizeDomain,
164
174
  };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { execFileSync } = require('child_process');
4
4
 
5
- // Git-derived signals for the "Add project to portfolio" route (talents-ai- score, ADR-059): the remote URL (step 4) and the first/last-commit date range (auto `startDate`/`endDate`).
5
+ // Git-derived signals for the "Add project to portfolio" route: the remote URL (step 4) and the first/last-commit date range (auto `startDate`/`endDate`).
6
6
 
7
7
  const GIT_TIMEOUT_MS = 15000;
8
8
 
@@ -62,7 +62,7 @@ async function runProfile({ io, rawOut, session, catalog, opts = {}, deps = make
62
62
  const agentsCount = aiProfile ? aiProfile.agentsOnProfile : null;
63
63
 
64
64
  if (opts.json) {
65
- (rawOut || ((s) => process.stdout.write(s)))(`${JSON.stringify({
65
+ rawOut(`${JSON.stringify({
66
66
  ok: true, mainRole, headline, completedProfilePercentage: completion, freelanceType,
67
67
  languageCodes, skillsCount, agentsCount, availability: availabilitySummary, aiProfile, pricing,
68
68
  }, null, 2)}\n`);
@@ -1,18 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const path = require('path');
5
4
 
6
- const { getConfigDir } = require('./config-dir');
7
5
  const { loadConfigFile, saveConfigFile, VALID_PROFILES } = require('./config');
8
6
  const { clearAuthSession } = require('./auth-session-store');
9
-
10
- function reportStatePath(env) {
11
- return path.join(getConfigDir(env), 'report-state.json');
12
- }
13
- function consentStatePath(env) {
14
- return path.join(getConfigDir(env), 'consent.json');
15
- }
7
+ const { consentPath } = require('./share-consent-state');
8
+ const { statePath } = require('./report-store');
16
9
 
17
10
  // Best-effort delete: a file that doesn't exist (nothing to purge yet, e.g.
18
11
  // a superadmin who never ran `usage` in this profile) is not a failure.
@@ -35,12 +28,12 @@ function switchProfile(target, env = process.env) {
35
28
  saveConfigFile(config, env);
36
29
 
37
30
  const purged = {
38
- reportState: unlinkIfExists(reportStatePath(env)),
39
- consent: unlinkIfExists(consentStatePath(env)),
31
+ reportState: unlinkIfExists(statePath(env)),
32
+ consent: unlinkIfExists(consentPath(env)),
40
33
  authSession: clearAuthSession(env),
41
34
  };
42
35
 
43
36
  return { ok: true, from, to: target, purged };
44
37
  }
45
38
 
46
- module.exports = { switchProfile, reportStatePath, consentStatePath };
39
+ module.exports = { switchProfile };
@@ -26,7 +26,7 @@ function anyTruncated(items) {
26
26
  return (items || []).some((i) => i && i.sampling && i.sampling.truncated);
27
27
  }
28
28
 
29
- // ADR-024 rubric dimension order (terminal).
29
+ // Rubric dimension order (terminal).
30
30
  const DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
31
31
 
32
32
  // Score band -> semantic (shared with the HTML/graph CSS class via `graph-certs.js`, which still imports this).
@@ -37,7 +37,7 @@ function scoreBand(score) {
37
37
  return 'low';
38
38
  }
39
39
 
40
- // ADR-016: named skill level (by decision power, not points) shown INSTEAD of the numeric grade.
40
+ // Named skill level (by decision power, not points) shown INSTEAD of the numeric grade.
41
41
  function skillLevelForScore(score) {
42
42
  const band = scoreBand(score);
43
43
  if (typeof score !== 'number') return { key: null, band };
@@ -67,18 +67,18 @@ function renderCertificationTerminal(certification, lang, opts = {}) {
67
67
  const r = getCatalog(lang).certify.report;
68
68
  const items = Array.isArray(certification && certification.items) ? certification.items : [];
69
69
  const lines = [];
70
- // issue 123 / ADR-044: the SAME identity gate the footprint report reads.
70
+ // The SAME identity gate the footprint report reads.
71
71
  const gate = opts.gate || DEFAULT_REPORT_GATE;
72
72
 
73
73
  // Header stands out; disclaimer + cost note are deliberately quiet (dim) so
74
- // the per-Skill results below are visually dominant (issue 011 feedback).
74
+ // the per-Skill results below are visually dominant.
75
75
  lines.push(`${C.bold}${C.primary}${r.heading}${C.reset}`);
76
- // talents-ai-score, ADR-051: this whole render is now a FALLBACK — reached only when the mandatory interview did not complete.
76
+ // This whole render is now a FALLBACK — reached only when the mandatory interview did not complete.
77
77
  if (opts.noInterview && r.noInterviewNote) lines.push(`${C.bold}${C.warning}${r.noInterviewNote}${C.reset}`);
78
78
  lines.push('');
79
79
  lines.push(`${C.dim}${r.disclaimer}${C.reset}`);
80
80
  lines.push(`${C.dim}${r.costNote}${C.reset}`);
81
- // ADR-025 authorship receipt (run-level): repo + commit range + honest note,
81
+ // Authorship receipt (run-level): repo + commit range + honest note,
82
82
  // shown once. Attribution is based on git authorship, NOT cryptographic proof.
83
83
  const authorship = (certification && certification.authorship) || null;
84
84
  const rc = r.receipt;
@@ -114,10 +114,10 @@ function renderCertificationTerminal(certification, lang, opts = {}) {
114
114
  continue;
115
115
  }
116
116
 
117
- // ADR-016: the HEADLINE is the named level (Middle/Senior/Expert) instead of the numeric 0-100 grade.
117
+ // The HEADLINE is the named level (Middle/Senior/Expert) instead of the numeric 0-100 grade.
118
118
  const { key: levelKey } = skillLevelForScore(item.result.score);
119
119
  lines.push(`${C.primary}│${C.reset} ${C.bold}${C.accent}${r.levelLine(skillLevelLabel(levelKey, r))}${C.reset}`);
120
- // ADR-024: show the anchored rubric dimensions so the level is explainable.
120
+ // Show the anchored rubric dimensions so the level is explainable.
121
121
  if (item.result.dimensions && r.dimensionsLabel && r.dimensionLabels) {
122
122
  lines.push(`${C.primary}│${C.reset} ${C.bold}${r.dimensionsLabel}:${C.reset}`);
123
123
  for (const key of DIMENSION_KEYS) {
@@ -138,7 +138,7 @@ function renderCertificationTerminal(certification, lang, opts = {}) {
138
138
  const tag = item.sampling.truncated ? ` ${r.partialTag}` : '';
139
139
  lines.push(`${C.primary}│${C.reset} ${C.dim}${summary}${tag}${C.reset}`);
140
140
  }
141
- // ADR-025 per-Skill authorship receipt (compact in terminal): attributed
141
+ // Per-Skill authorship receipt (compact in terminal): attributed
142
142
  // file count + the author emails confirmed against the identity.
143
143
  if (rc && Array.isArray(item.fileAttribution) && item.fileAttribution.length > 0) {
144
144
  const attributed = item.fileAttribution.filter((f) => f.attributed).length;
@@ -151,7 +151,7 @@ function renderCertificationTerminal(certification, lang, opts = {}) {
151
151
  }
152
152
  }
153
153
 
154
- // Remediation prompt (issue 011): a clearly delimited copyable block.
154
+ // Remediation prompt: a clearly delimited copyable block.
155
155
  const remediation = gate.showCertifyRemediation ? buildRemediationPrompt(item, lang) : null;
156
156
  if (remediation) {
157
157
  lines.push(`${C.primary}│${C.reset}`);
@@ -8,7 +8,7 @@ function normalizeAgentName(name) {
8
8
  return String(name || '').trim().toLowerCase().replace(/^[`'"*]+|[`'"*]+$/g, '');
9
9
  }
10
10
 
11
- // talents-ai-score, description-always-present (real-browser user feedback): TWO earlier approaches were tried and rejected before this one.
11
+ // TWO earlier approaches were tried and rejected before this one.
12
12
 
13
13
  function humanizeAgentName(name) {
14
14
  const spaced = String(name || '').replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
@@ -38,9 +38,9 @@ function excerptForCard(text, maxLen = CARD_EXCERPT_MAX_LEN) {
38
38
  return `${text.slice(0, maxLen).trim()}…`;
39
39
  }
40
40
 
41
- // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here — issue 055.
41
+ // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here.
42
42
 
43
- // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here — issue 055.
43
+ // Every card field that is TALENT-AUTHORED or came OFF THE WIRE, neutralised once, here.
44
44
  function sanitizeCardText(v) {
45
45
  return typeof v === 'string' ? sanitizeRenderText(v) : v;
46
46
  }
@@ -58,7 +58,7 @@ function buildAgentCardTree(report, t) {
58
58
  const rawDescByName = new Map(rawDescriptions.map((d) => [normalizeAgentName(d.name), d.description]));
59
59
  const byName = new Set(agents.map((a) => a.name));
60
60
 
61
- // ADR-016 agent evaluation: the ephemeral LLM definition-quality score (+ rationale) keyed by normalized name, and the LOCAL usage count keyed by exact name.
61
+ // Agent evaluation: the ephemeral LLM definition-quality score (+ rationale) keyed by normalized name, and the LOCAL usage count keyed by exact name.
62
62
  const evaluations =
63
63
  report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
64
64
  ? report.agentEvaluation.evaluations
@@ -111,7 +111,7 @@ function buildAgentCardTree(report, t) {
111
111
  symbolicName: sanitizeCardText(synth && synth.symbolicName ? synth.symbolicName : null),
112
112
  whatItDoes: sanitizeCardText(whatItDoes),
113
113
  tools: sanitizeCardList(tools),
114
- // The declared LLM model (issue 089).
114
+ // The declared LLM model.
115
115
  model: sanitizeCardText(a.model),
116
116
  aiProduct,
117
117
  parent: sanitizeCardText(parentKey),
@@ -120,7 +120,7 @@ function buildAgentCardTree(report, t) {
120
120
  ? { ...classification, role: sanitizeCardText(classification.role) }
121
121
  : classification,
122
122
  improvements: sanitizeCardList(improvements),
123
- // Issue 106's provenance rule, applied to the fourth state: ONE field, on the card both surfaces read, so the terminal and the shared report cannot disagree about the same agent.
123
+ // The provenance rule, applied to the fourth state: ONE field, on the card both surfaces read, so the terminal and the shared report cannot disagree about the same agent.
124
124
  evaluationState,
125
125
  };
126
126
  });
@@ -21,7 +21,7 @@ function aiFluencyHtml(report, lang) {
21
21
 
22
22
  const TEMPLATE_PATH = path.join(__dirname, 'templates', 'report-sheet.html');
23
23
  let _tpl = null;
24
- // Issue 098: the template's comments are OURS and this document is the one the talent shares, so they are swept here — at load, before any talent data is in the string.
24
+ // The template's comments are OURS and this document is the one the talent shares, so they are swept here — at load, before any talent data is in the string.
25
25
  function template() {
26
26
  if (_tpl == null) _tpl = stripTemplateComments(fs.readFileSync(TEMPLATE_PATH, 'utf8'));
27
27
  return _tpl;
@@ -54,7 +54,7 @@ function tierName(fp, t) {
54
54
 
55
55
  function bandClass(b) { return b === 'high' ? 'band-high' : b === 'mid' ? 'band-mid' : 'band-low'; }
56
56
 
57
- // Localized Setup Level label from the drawer payload (ADR-016): prefer the
57
+ // Localized Setup Level label from the drawer payload: prefer the
58
58
  // i18n `setupLevels` catalog by key, fall back to the label baked into `fp`.
59
59
  function setupLabel(fp, t) {
60
60
  const key = (fp.setup && fp.setup.key) || 'none';
@@ -65,7 +65,7 @@ function setupLabel(fp, t) {
65
65
  function heroHtml(fp, c, t) {
66
66
  const rank = (fp.setup && typeof fp.setup.rank === 'number') ? fp.setup.rank : 0;
67
67
  const label = setupLabel(fp, t);
68
- // Four pips: Not certified + S1/S2/S3, current filled (ADR-016).
68
+ // Four pips: Not certified + S1/S2/S3, current filled.
69
69
  const pills = Array.from({ length: 4 }, (_, i) => `<span class="${i <= rank ? 'on' : ''}"></span>`).join('');
70
70
  const chipK = esc(fp.tier.key && fp.tier.key !== 'none' ? fp.tier.key : (fp.setup && fp.setup.code) || '');
71
71
  return `<div class="card reveal">
@@ -86,7 +86,7 @@ function heroHtml(fp, c, t) {
86
86
  </div>`;
87
87
  }
88
88
 
89
- // The Setup Level ladder (ADR-016), with each rung's tiers and short meaning inline — meanings from the localized i18n `setupLevels` catalog (never invented).
89
+ // The Setup Level ladder, with each rung's tiers and short meaning inline — meanings from the localized i18n `setupLevels` catalog (never invented).
90
90
  function ladderHtml(fp, c, t) {
91
91
  const cur = (fp.setup && typeof fp.setup.rank === 'number') ? fp.setup.rank : 0;
92
92
  const catalog = (t && t.setupLevels) || {};
@@ -111,7 +111,7 @@ function chipsCard(title, sub, items, withDot) {
111
111
  return `<div class="card reveal"><h3>${esc(title)}</h3><p class="sub">${esc(sub)}</p><div class="chips">${body}</div></div>`;
112
112
  }
113
113
 
114
- // MCP SERVICES (issue 110).
114
+ // MCP SERVICES.
115
115
  function mcpCard(mcp, c) {
116
116
  const services = (mcp && Array.isArray(mcp.services) ? mcp.services : []);
117
117
  const unidentified = (mcp && typeof mcp.unidentified === 'number') ? mcp.unidentified : 0;
@@ -129,17 +129,17 @@ function mcpCard(mcp, c) {
129
129
  return `<div class="card reveal"><h3>${esc(c.mcpT)}</h3><p class="sub">${esc(c.mcpS)}</p><div class="chips">${chips}</div>${note}</div>`;
130
130
  }
131
131
 
132
- // DETECTED AGENTS, left column (issue 089).
132
+ // DETECTED AGENTS, left column.
133
133
  function agentRow(a, c, t) {
134
134
  const cats = (t.classification && t.classification.categories) || {};
135
135
  const model = a.model ? `<span class="model">${esc(a.model)}</span>` : `<span class="model" style="opacity:.6">${esc(c.agentNoModel)}</span>`;
136
136
  // The category is the agent → catalog-seed association (per agent), not the Talent's tier.
137
137
  const catLabel = a.category ? (cats[a.category] || sanitizeRenderText(a.category)) : null;
138
138
  // ONE COLOUR PER CATEGORY (user request), and THREE distinct facts, not two: - a known catalog category -> its own colour token (`.cat-developer` …).
139
- // FOUR facts now, not three (issue 120).
139
+ // FOUR facts now, not three.
140
140
  const coloured = !!(a.category && cats[a.category] && !isFloorCategory(a.category));
141
141
  const catClass = coloured ? `cat cat-${a.category}` : 'cat cat-none';
142
- // Issue 106: the empty badge says WHICH kind of empty it is.
142
+ // The empty badge says WHICH kind of empty it is.
143
143
  const cls = t.classification || {};
144
144
  const emptyLabel = a.evaluationState === 'omitted'
145
145
  ? cls.agentEvalOmitted
@@ -163,7 +163,7 @@ function agentRow(a, c, t) {
163
163
 
164
164
  function agentsCard(agents, c, t) {
165
165
  const list = Array.isArray(agents) ? agents : [];
166
- // SAID ONCE, VISIBLY (issue 106): if nobody was evaluated, the reason the badges are empty is that the run could not classify, not that the agents have no category.
166
+ // SAID ONCE, VISIBLY: if nobody was evaluated, the reason the badges are empty is that the run could not classify, not that the agents have no category.
167
167
  const noneEvaluated = list.length > 0 && list.every((a) => a.evaluated === false);
168
168
  // AND THE PARTIAL RUN, NAMED (2026-08-03).
169
169
  const omitted = list.filter((a) => a.evaluationState === 'omitted').map((a) => a.name);
@@ -196,7 +196,7 @@ function renderSheet(project, lang, gate = DEFAULT_REPORT_GATE) {
196
196
  const t = getCatalog(L);
197
197
  const c = t.sheet;
198
198
  const hasFoot = !!(project && project.footprint && project.footprint.report);
199
- // `t` is passed now (issue 089): the drawer's agent rows use the catalog for the
199
+ // `t` is passed now: the drawer's agent rows use the catalog for the
200
200
  // description fallback and nothing else — it stays optional for the graph report.
201
201
  const fp = hasFoot ? buildFootprintDrawer(project.footprint.report, project.footprint.maturity, t) : null;
202
202
  // buildCertsPayload returns null when the project has no Skill certs — the sheet still renders both columns, so normalize to an empty list (clean state).
@@ -13,7 +13,7 @@ const {
13
13
  printRoadmap,
14
14
  } = require('./terminal-sections');
15
15
 
16
- // talents-ai-score: terminal parity with the HTML report.
16
+ // Terminal parity with the HTML report.
17
17
 
18
18
  // `lang` ('es'|'en', see src/i18n.js) decides the text catalog.
19
19
  function renderTerminal(report, maturity, lang, opts = {}) {
@@ -21,10 +21,10 @@ function renderTerminal(report, maturity, lang, opts = {}) {
21
21
  const lines = [];
22
22
  const p = (s = '') => lines.push(s);
23
23
 
24
- // issue 123 / ADR-044: the ONE identity gate.
24
+ // The ONE identity gate.
25
25
  const gate = opts.gate || DEFAULT_REPORT_GATE;
26
26
 
27
- // ADR-016: Setup Level (S1-S3 / Not certified) replaces the 0-4 band label.
27
+ // Setup Level (S1-S3 / Not certified) replaces the 0-4 band label.
28
28
  const setupKey = (maturity.setupLevel && maturity.setupLevel.key) || 'none';
29
29
  const setupLabel = (t.setupLevels && t.setupLevels[setupKey] && t.setupLevels[setupKey].label) || setupKey;
30
30
 
@@ -47,7 +47,7 @@ function renderTerminal(report, maturity, lang, opts = {}) {
47
47
  sep(p);
48
48
  const tierName = (maturity.tierKey && t.tierNames[maturity.tierKey]) || maturity.tierName || '';
49
49
  const tierBit = maturity.tierKey ? t.terminal.tierInline(maturity.tierKey, tierName) : '';
50
- // This report's ONE headline (dueño, 2026-08-11): the Setup Level IS the evaluation's main result, so it gets `accent` (lime) — and nothing else on this screen does.
50
+ // This report's ONE headline: the Setup Level IS the evaluation's main result, so it gets `accent` (lime) — and nothing else on this screen does.
51
51
  const setupColor = setupKey === 'none' ? c.white : c.accent;
52
52
  p(` ${c.bold}${setupColor}${t.terminal.setupLevel(setupLabel)}${c.reset}${c.muted}${tierBit}${c.reset}`);
53
53
  p(` ${c.primary}${bar(maturity.score)}${c.reset} ${c.dim}${maturity.score}/100${c.reset}`);
@@ -77,7 +77,7 @@ function renderTerminal(report, maturity, lang, opts = {}) {
77
77
  }
78
78
  }
79
79
 
80
- // 4b. MCP services (issue 110) — ALWAYS printed, empty state included, because
80
+ // 4b. MCP services — ALWAYS printed, empty state included, because
81
81
  // its absence is a tier criterion the talent needs to know about.
82
82
  sep(p);
83
83
  printMcpServices(report, t, p);
@@ -105,5 +105,5 @@ function renderTerminal(report, maturity, lang, opts = {}) {
105
105
  return lines.join('\n');
106
106
  }
107
107
 
108
- // `wrap` is re-exported for its own unit test (issue 087): it is the one piece of the formatting with edge cases worth pinning directly.
108
+ // `wrap` is re-exported for its own unit test: it is the one piece of the formatting with edge cases worth pinning directly.
109
109
  module.exports = { renderTerminal, wrap, printAgents };