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
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const { isRefusedWalkRoot } = require('./scan-exclusions');
4
+ const {
5
+ detectRepos,
6
+ repoIdentityForCwd,
7
+ matchReposByFlag,
8
+ scopeFromSelection,
9
+ indexOfCurrentRepo,
10
+ } = require('./detect-repos');
11
+
12
+ const MAX_MACHINE_WIDE_REPOS = 5;
13
+ const SCOPE_MACHINE = 'machine';
14
+ const SCOPE_REPO = 'repo';
15
+
16
+ function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
17
+ try {
18
+ return repoIdentityFn(cwd).toplevel || cwd;
19
+ } catch {
20
+ return cwd;
21
+ }
22
+ }
23
+
24
+ // The real git toplevel of `cwd`, or null when `cwd` is not a repo (or is a refused system root like `/`, the MCP `cwd=/` case).
25
+ function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
26
+ if (isRefusedWalkRoot(cwd)) return null;
27
+ try {
28
+ return repoIdentityFn(cwd).toplevel || null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ // Pure: builds the machine-wide root set.
35
+ function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
36
+ const ordered = [];
37
+ const seen = new Set();
38
+ const push = (t) => {
39
+ if (typeof t !== 'string' || !t) return;
40
+ if (isRefusedWalkRoot(t)) return;
41
+ if (seen.has(t)) return;
42
+ seen.add(t);
43
+ ordered.push(t);
44
+ };
45
+ push(cwdRepoTop);
46
+ for (const r of detectedRepos || []) push(r && r.toplevel);
47
+ const capped = ordered.slice(0, cap);
48
+ return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
49
+ }
50
+
51
+ function cwdOnlyScope(repos, cwdToplevel) {
52
+ const idx = indexOfCurrentRepo(repos, cwdToplevel);
53
+ if (idx === -1) {
54
+ return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
55
+ }
56
+ const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
57
+ return {
58
+ selectedCwds,
59
+ toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
60
+ mode: 'cwd',
61
+ };
62
+ }
63
+
64
+ // Resolve which repos feed the AI-usage evaluation.
65
+ async function resolveRepoScope({
66
+ opts,
67
+ cwd,
68
+ injectedAsk,
69
+ catalog,
70
+ detectReposFn = detectRepos,
71
+ repoIdentityFn = repoIdentityForCwd,
72
+ chooseScope = null,
73
+ isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
74
+ note = () => {},
75
+ }) {
76
+ const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
77
+ const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
78
+ const detect = () => {
79
+ try {
80
+ return detectReposFn(process.env);
81
+ } catch {
82
+ return { repos: [], unassignedSessionCount: 0 };
83
+ }
84
+ };
85
+
86
+ // 1. Explicit repo list always wins.
87
+ if (opts.repos) {
88
+ const { repos, unassignedSessionCount } = detect();
89
+ note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
90
+ const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
91
+ if (unmatched.length > 0) {
92
+ process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
93
+ }
94
+ if (indices.length > 0) {
95
+ const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
96
+ return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
97
+ }
98
+ return cwdOnlyScope(repos, cwdToplevel);
99
+ }
100
+
101
+ const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
102
+ const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
103
+
104
+ let choice = null;
105
+ if (wantsMachine) choice = SCOPE_MACHINE;
106
+ else if (wantsRepo) choice = SCOPE_REPO;
107
+ else if (chooseScope && isTTY && !injectedAsk && !opts.json) {
108
+ choice = await chooseScope({ catalog, stdinIsTTY: true });
109
+ }
110
+ if (choice === null) choice = SCOPE_REPO;
111
+
112
+ if (choice === SCOPE_MACHINE) {
113
+ const { repos, unassignedSessionCount } = detect();
114
+ note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
115
+ const mw = machineWideToplevels(cwdRepoTop, repos);
116
+ if (mw.capped) {
117
+ process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
118
+ }
119
+ note(catalog.cli.reposScopeAll);
120
+ return {
121
+ selectedCwds: null,
122
+ toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
123
+ mode: 'all',
124
+ };
125
+ }
126
+
127
+ // Current repo.
128
+ const { repos } = detect();
129
+ return cwdOnlyScope(repos, cwdToplevel);
130
+ }
131
+
132
+ module.exports = { resolveRepoScope, machineWideToplevels, SCOPE_MACHINE, SCOPE_REPO };
@@ -3,7 +3,7 @@
3
3
  const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
4
4
  const { getProfile } = require('./config');
5
5
 
6
- // THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY (talents-ai-score, issue 123 / ADR-044).
6
+ // THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY.
7
7
 
8
8
  // Pure: identity in, policy out.
9
9
  function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
@@ -14,8 +14,8 @@ const SCHEMA_VERSION = 2;
14
14
  function configDir() {
15
15
  return getConfigDir(process.env);
16
16
  }
17
- function statePath() {
18
- return path.join(configDir(), 'report-state.json');
17
+ function statePath(env = process.env) {
18
+ return path.join(getConfigDir(env), 'report-state.json');
19
19
  }
20
20
 
21
21
  // Stable, filesystem-safe per-project file name derived from the absolute path.
@@ -84,7 +84,7 @@ function getOrCreateProject(state, absRoot) {
84
84
  };
85
85
  }
86
86
  // Backward-compat: a project persisted before the two-backend chain
87
- // existed (talents-ai-score, ADR-020/021).
87
+ // existed.
88
88
  if (!state.projects[absRoot].backendAcceptance) {
89
89
  state.projects[absRoot].backendAcceptance = {};
90
90
  }
@@ -98,7 +98,7 @@ function renderProjectHtml(project, lang, gate) {
98
98
  }
99
99
 
100
100
  /* ---------- persist (state only) + materialize (render HTML) ---------- */
101
- // ADR-016 split: `footprint` and `certify` now PERSIST state only (no HTML file, no printed link).
101
+ // Split: `footprint` and `certify` now PERSIST state only (no HTML file, no printed link).
102
102
 
103
103
  function stampAndSaveState(state, absRoot) {
104
104
  state.schemaVersion = SCHEMA_VERSION;
@@ -179,12 +179,12 @@ function materializeProjectReport({ root, lang }) {
179
179
  const state = loadState();
180
180
  const project = state.projects[absRoot];
181
181
  if (!hasRenderableData(project)) return { hasData: false };
182
- // issue 123 / ADR-044: the shareable HTML is gated by identity AT GENERATION TIME — this is the moment the `report` command produces the file, so this is where the gate is resolved.
182
+ // The shareable HTML is gated by identity AT GENERATION TIME — this is the moment the `report` command produces the file, so this is where the gate is resolved.
183
183
  const gate = reportGateForIdentity();
184
184
  return { hasData: true, ...writeProjectHtml(project, absRoot, lang, gate) };
185
185
  }
186
186
 
187
- // `upsertFootprint` AND `upsertCertification` WERE HERE, and ADR-035 retired them.
187
+ // `upsertFootprint` AND `upsertCertification` WERE HERE, and were retired.
188
188
 
189
189
  function certKey(item) {
190
190
  if (item && item.skillId != null) return `id:${item.skillId}`;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Curated tier roadmap content (talents-ai-score, issue 020 / ADR-013/014).
3
+ // Curated tier roadmap content.
4
4
 
5
5
  const TIER_JUMPS_ES = {
6
6
  T0: {
@@ -201,13 +201,13 @@ const T7_TERMINAL_ES = {
201
201
  whatRemains: 'El tier ya no sube, pero el equipamiento no es el trabajo. A partir de aquí el margen está en usar lo que tienes, no en instalar más: documentar tu setup para que otros lo repliquen, podar lo que no usas, y afilar cada agente/comando con el trabajo real. La escala mide cómo tienes montado el banco; que salgan buenas piezas de él depende de ti.',
202
202
  consolidationSteps: [
203
203
  'Afina tus hooks y agentes con el trabajo real: recorta lo que estorba, acota los matchers y ajusta las tools/límites de cada agente a su rol.',
204
- 'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate — ADR-003) y elimina lo redundante.',
204
+ 'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate) y elimina lo redundante.',
205
205
  'Mantén vivos tus ficheros de contexto (CLAUDE.md, reglas, comandos): actualízalos cuando cambien el stack o las convenciones, o dejan de ayudar.',
206
206
  'Empaqueta y contribuye tus skills/comandos a un repo compartido del equipo para que tu setup escale más allá de tu máquina.',
207
207
  'Documenta tu organigrama de agentes y compártelo como referencia interna para que otros lo repliquen.',
208
208
  'Mide el impacto real (tiempo ahorrado, tareas delegadas de principio a fin) y usa ese dato para decidir qué merece seguir en tu banco.',
209
209
  ],
210
- honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada (ADR-007). No se lee como "calidad IA del Talent".',
210
+ honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada. No se lee como "calidad IA del Talent".',
211
211
  };
212
212
 
213
213
  const TIER_JUMPS_EN = {
@@ -409,13 +409,13 @@ const T7_TERMINAL_EN = {
409
409
  whatRemains: "The tier no longer rises, but equipment isn't the work. From here the margin is in using what you have, not installing more: documenting your setup so others can replicate it, pruning what you don't use, and sharpening each agent/command with real work. The ladder measures how your bench is set up; whether good pieces come out of it is up to you.",
410
410
  consolidationSteps: [
411
411
  "Sharpen your hooks and agents against real work: trim what gets in the way, scope the matchers, and tune each agent's tools/limits to its role.",
412
- "Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate — ADR-003) and drop the redundant ones.",
412
+ "Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate) and drop the redundant ones.",
413
413
  'Keep your context files (CLAUDE.md, rules, commands) alive: update them when the stack or conventions change, or they stop helping.',
414
414
  'Package and contribute your skills/commands to a shared team repo so your setup scales beyond your own machine.',
415
415
  'Document your agent org chart and share it as an internal reference so others can replicate it.',
416
416
  'Measure the real impact (time saved, tasks delegated end to end) and use that to decide what deserves to stay on your bench.',
417
417
  ],
418
- honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified (ADR-007). It\'s not read as "the Talent\'s AI quality".',
418
+ honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified. It\'s not read as "the Talent\'s AI quality".',
419
419
  };
420
420
 
421
421
  const CONTENT_BY_LANG = {
@@ -3,7 +3,7 @@
3
3
  const { requestBackend } = require('./backend-request');
4
4
  const { reporterFor, REASON } = require('./model-call-record');
5
5
 
6
- // Roadmap personalization client (talents-ai-score, ADR-015).
6
+ // Roadmap personalization client.
7
7
 
8
8
  const DEFAULT_TIMEOUT_MS = 8000;
9
9
 
@@ -38,7 +38,7 @@ function buildRoadmapPersonalizationRequest(curatedEntry, tierResult, report, lo
38
38
  mistakes: curatedEntry.commonMistakes,
39
39
  },
40
40
  signals: buildRoadmapSignals(report, tierResult.signals),
41
- // ADR-026: detected report language for the personalized prose.
41
+ // Detected report language for the personalized prose.
42
42
  ...(locale === 'es' || locale === 'en' ? { locale } : {}),
43
43
  ...(consentGranted === true ? { traceContentConsent: true } : {}),
44
44
  };
@@ -66,7 +66,7 @@ function isValidPersonalizedRoadmap(parsed, curated) {
66
66
 
67
67
  // Requests the roadmap personalization endpoint.
68
68
  async function requestRoadmapPersonalization(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
69
- // Issue 109 — see agent-synthesis.js#requestAgentSynthesis for the contract.
69
+ // See agent-synthesis.js#requestAgentSynthesis for the contract.
70
70
  const outcome = reporterFor(onOutcome, { timeoutMs });
71
71
  if (!endpoint) {
72
72
  outcome.skip(REASON.NO_ENDPOINT);
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { tierName: getTierName } = require('./i18n');
4
4
 
5
- // Deterministic, ready-to-paste "implementation prompt" (talents-ai-score: "next steps -> prompt").
5
+ // Deterministic, ready-to-paste "implementation prompt" ("next steps -> prompt").
6
6
 
7
7
  const TEMPLATES = {
8
8
  es: {
@@ -65,7 +65,7 @@ function buildImplementationPrompt(entry, report, maturity, lang) {
65
65
  const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
66
66
  const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
67
67
  const tierKey = (maturity && maturity.tierKey) || entry.tierKey || '';
68
- // talents-ai-score, i18n audit: NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
68
+ // NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
69
69
  const tierName = getTierName(tierKey, lang);
70
70
 
71
71
  const lines = [];
@@ -2,7 +2,7 @@
2
2
 
3
3
  const path = require('path');
4
4
 
5
- // Directory names never descended into when walking a project tree (skill-code-certification, issue 008).
5
+ // Directory names never descended into when walking a project tree.
6
6
  const EXCLUDED_DIRS = new Set([
7
7
  'node_modules', 'dist', 'build', '.git', 'vendor', 'coverage', '.next', 'out',
8
8
  // Python
package/src/scanner.js CHANGED
@@ -163,7 +163,7 @@ function aggregateFootprint(paths) {
163
163
  return { bytes, files };
164
164
  }
165
165
 
166
- // Recency = ONLY mtime of already-detected config files/dirs (ADR-003); never reads
166
+ // Recency = ONLY mtime of already-detected config files/dirs; never reads
167
167
  // log/history content.
168
168
 
169
169
  function latestMtime(paths) {
@@ -317,8 +317,8 @@ const probes = {
317
317
  }),
318
318
  };
319
319
 
320
- // Agent org-chart counts (ADR-009): deterministic, structure+names only. PROJECT ∪ HOME
321
- // per ADR-014, since the tier engine's T6 (`agents >= 2`) needs the full picture.
320
+ // Agent org-chart counts: deterministic, structure+names only. PROJECT ∪ HOME
321
+ // since the tier engine's T6 (`agents >= 2`) needs the full picture.
322
322
 
323
323
  function agentOrgChartCounts(root, agentsCount, mcp) {
324
324
  const home = getHomeDir();
@@ -330,7 +330,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
330
330
  commands:
331
331
  countFiles(path.join(root, '.claude', 'commands'), '.md') +
332
332
  countFiles(path.join(home, '.claude', 'commands'), '.md'),
333
- // Issue 112: the third copy of the MCP paths lived here.
333
+ // The third copy of the MCP paths lived here.
334
334
  mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
335
335
  hooks:
336
336
  countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
@@ -342,7 +342,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
342
342
 
343
343
  function scan(options = {}) {
344
344
  const root = options.root || process.cwd();
345
- // MCP detection runs FIRST (issue 112), because the per-tool depth probes now read their `mcpServers` count from it.
345
+ // MCP detection runs FIRST, because the per-tool depth probes now read their `mcpServers` count from it.
346
346
  const mcp = detectMcpServers(root);
347
347
  const scanCtx = { mcp };
348
348
 
@@ -366,7 +366,7 @@ function scan(options = {}) {
366
366
  // Config footprint: ONLY the aggregated size in bytes and file count, never paths or names.
367
367
  footprint: null,
368
368
  // Recency: ONLY the date derived from the most recent mtime among its
369
- // already-detected config files (ADR-003). Never content, logs or history.
369
+ // already-detected config files. Never content, logs or history.
370
370
  recency: { lastModified: null, daysSinceModified: null, bucket: null },
371
371
  // Version: only if the tool was detected via a binary on PATH; runs
372
372
  // THAT already-detected binary, with `--version` only.
@@ -397,7 +397,7 @@ function scan(options = {}) {
397
397
 
398
398
  const detectedTools = tools.filter((t) => t.detected);
399
399
 
400
- // Deterministic (no-LLM) agent org chart (ADR-009): structure + names
400
+ // Deterministic (no-LLM) agent org chart: structure + names
401
401
  // only (name, wired tools, model, hierarchy). Never descriptions/prompts.
402
402
  const agents = parseAgentOrgChart(root);
403
403
 
@@ -405,7 +405,7 @@ function scan(options = {}) {
405
405
 
406
406
  const rawDependencyNames = detectRawDependencyNames(root);
407
407
 
408
- // Deterministic (no-LLM) memory STRUCTURE (talents-ai-score, issue 016 / ADR-013-014): import count, nesting depth, sections, size — from known context files (project ∪ home).
408
+ // Deterministic (no-LLM) memory STRUCTURE: import count, nesting depth, sections, size — from known context files (project ∪ home).
409
409
  const memory = analyzeMemoryStructure(root);
410
410
 
411
411
  const automations = detectAutomations(root);
@@ -436,19 +436,19 @@ function scan(options = {}) {
436
436
  categories: [...new Set(detectedTools.map((t) => t.category))],
437
437
  },
438
438
  tools,
439
- // Agent org chart (ADR-009): structure + names only, never content.
439
+ // Agent org chart: structure + names only, never content.
440
440
  agents,
441
441
  agentCounts: agentOrgChartCounts(root, agents.length, mcp),
442
442
  // Projects where each detected AI tool has been used (skill-code-
443
- // Project technologies (ADR-012): dependency manifest names only.
443
+ // Project technologies: dependency manifest names only.
444
444
  technologies,
445
- // MCP servers by name/category (issue 015): never the raw config.
445
+ // MCP servers by name/category: never the raw config.
446
446
  mcp,
447
- // Memory structure (issue 016): imports/nesting/sections/size, never text.
447
+ // Memory structure: imports/nesting/sections/size, never text.
448
448
  memory,
449
- // Automations (issue 017): script/scheduler signals, never raw content.
449
+ // Automations: script/scheduler signals, never raw content.
450
450
  automations,
451
- // Browser tools (issue 018): composition over technologies + mcp.
451
+ // Browser tools: composition over technologies + mcp.
452
452
  browserTools,
453
453
  };
454
454
  }
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // SECRET PROMPT — reads a secret WITHOUT painting it (issue 102).
3
+ // SECRET PROMPT — reads a secret WITHOUT painting it.
4
4
 
5
5
  const CTRL_C = '\u0003';
6
6
  const CTRL_D = '\u0004';
@@ -20,7 +20,7 @@ async function hasUsableSession(env = process.env, deps = {}) {
20
20
 
21
21
  const session = load(env);
22
22
  if (sessionStatus(session, now) === 'active') return true;
23
- if (!session || !session.cookie) return false;
23
+ if (!session || (!session.cookie && !session.sessionToken)) return false;
24
24
 
25
25
  let refreshed = null;
26
26
  try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
@@ -3,6 +3,7 @@
3
3
  // On-demand hub-JWT minting for an email (better-auth) session.
4
4
 
5
5
  const REFRESH_MARGIN_MS = 120 * 1000;
6
+ const REVOKED_STATUSES = new Set([401, 403]);
6
7
 
7
8
  function jwtStillFresh(accessTokenExpiresAt, now, marginMs) {
8
9
  const exp = Date.parse(String(accessTokenExpiresAt || ''));
@@ -14,6 +15,7 @@ async function ensureFreshSession(env = process.env, deps = {}) {
14
15
  const {
15
16
  loadAuthSession = require('./auth-session-store').loadAuthSession,
16
17
  saveAuthSession = require('./auth-session-store').saveAuthSession,
18
+ clearAuthSession = require('./auth-session-store').clearAuthSession,
17
19
  getAuthTokenEndpoint = require('./config').getAuthTokenEndpoint,
18
20
  requestAuthToken = require('./auth-client').requestAuthToken,
19
21
  now = Date.now(),
@@ -22,13 +24,17 @@ async function ensureFreshSession(env = process.env, deps = {}) {
22
24
 
23
25
  const session = loadAuthSession(env);
24
26
  if (!session) return null;
25
- if (!session.cookie) return session;
27
+ if (!session.cookie && !session.sessionToken) return session;
26
28
  if (session.accessToken && jwtStillFresh(session.accessTokenExpiresAt, now, marginMs)) return session;
27
29
 
28
30
  const endpoint = getAuthTokenEndpoint(env);
29
31
  if (!endpoint) return session;
30
32
 
31
- const tok = await requestAuthToken({ cookie: session.cookie }, { endpoint });
33
+ const tok = await requestAuthToken({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint });
34
+ if (!tok.ok && REVOKED_STATUSES.has(tok.status)) {
35
+ clearAuthSession(env);
36
+ return null;
37
+ }
32
38
  if (!tok.ok) return session;
33
39
 
34
40
  try {
@@ -48,4 +54,28 @@ async function ensureFreshSession(env = process.env, deps = {}) {
48
54
  return loadAuthSession(env);
49
55
  }
50
56
 
51
- module.exports = { ensureFreshSession, jwtStillFresh, REFRESH_MARGIN_MS };
57
+ function webOrigin(url) {
58
+ try {
59
+ return new URL(url).origin;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ async function endSession(env = process.env, deps = {}) {
66
+ const {
67
+ loadAuthSession = require('./auth-session-store').loadAuthSession,
68
+ clearAuthSession = require('./auth-session-store').clearAuthSession,
69
+ getSignOutEndpoint = require('./config').getSignOutEndpoint,
70
+ getTalentProfileUrl = require('./config').getTalentProfileUrl,
71
+ requestSignOut = require('./auth-client').requestSignOut,
72
+ } = deps;
73
+ const session = loadAuthSession(env);
74
+ if (session && (session.sessionToken || session.cookie)) {
75
+ await requestSignOut({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint: getSignOutEndpoint(env), origin: webOrigin(getTalentProfileUrl(env)) });
76
+ }
77
+ clearAuthSession(env);
78
+ return session !== null;
79
+ }
80
+
81
+ module.exports = { ensureFreshSession, endSession, jwtStillFresh, REFRESH_MARGIN_MS };
@@ -12,7 +12,7 @@ const {
12
12
  parseAiderHistory,
13
13
  } = require('./session-normalizers');
14
14
 
15
- // GLOBAL, machine-wide inventory of AI coding-tool SESSIONS (hybrid evaluation, Slice 1).
15
+ // GLOBAL, machine-wide inventory of AI coding-tool SESSIONS.
16
16
 
17
17
  const MAX_FILES_PER_TOOL = 2000;
18
18
  const MAX_BYTES_PER_FILE = 8 * 1024 * 1024;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the design asset shakers-hub-frontend/apps/hub/public/images/shakers-text-logo.svg (viewBox 0 0 87 18, 7 glyph <path>s).
3
+ // Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the Shakers text-logo SVG (viewBox 0 0 87 18, 7 glyph <path>s).
4
4
 
5
5
  const WORDMARK_VIEWBOX = { w: 87, h: 18 };
6
6
 
package/src/share-card.js CHANGED
@@ -70,7 +70,7 @@ function loadProjectFootprint(root, { load = loadState } = {}) {
70
70
  score: m.score,
71
71
  level: m.level,
72
72
  levelKey: m.key,
73
- // ADR-016: Setup Level for the card pill (replaces the 0-4 band). Prefer the
73
+ // Setup Level for the card pill (replaces the 0-4 band). Prefer the
74
74
  // persisted `setupLevel`; derive from the tier for older stored reports.
75
75
  setupLevelKey: (m.setupLevel && m.setupLevel.key)
76
76
  || (typeof m.tier === 'number' ? setupLevelForTier(m.tier).key : 'none'),
@@ -95,7 +95,7 @@ function buildCardModel(fp) {
95
95
  const en = getCatalog('en');
96
96
  const tierKey = fp.tierKey;
97
97
  const tierName = (en.tierNames && en.tierNames[tierKey]) || tierKey;
98
- // ADR-016: Setup Level label for the pill (English-fixed like the tier name).
98
+ // Setup Level label for the pill (English-fixed like the tier name).
99
99
  const setupKey = fp.setupLevelKey || 'none';
100
100
  const bandName = (en.setupLevels && en.setupLevels[setupKey] && en.setupLevels[setupKey].label) || '';
101
101
 
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- // Certification-persistence payload (skill-code-certification, issue 005), extracted from src/share.js (structure refactor, issue 020).
3
+ // Certification-persistence payload, extracted from src/share.js.
4
4
 
5
5
  const { scrubSecrets } = require('./agent-synthesis');
6
6
 
7
7
  const CERTIFY_SEND_THROTTLE_MS = 60 * 60 * 1000;
8
8
  const CERT_SCHEMA_VERSION = 1;
9
9
 
10
- // Strict whitelist: the analyzed RESULT + observability provenance (ADR-017), rebuilt field-by-field (never spread).
10
+ // Strict whitelist: the analyzed RESULT + observability provenance, rebuilt field-by-field (never spread).
11
11
  const CERT_DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
12
12
  function deriveDimensionScores(dimensions) {
13
13
  if (!dimensions || typeof dimensions !== 'object') return null;
@@ -74,13 +74,13 @@ function deriveCertificationPayload(items, { repository = null, commitRange = nu
74
74
  note: typeof f.note === 'string' && f.note ? scrubSecrets(f.note) : null,
75
75
  }))
76
76
  : [],
77
- // ADR-024 rubric dimensions (0-4 or null) — the inputs the server used to
77
+ // Rubric dimensions (0-4 or null) — the inputs the server used to
78
78
  // compute the deterministic score. Rebuilt key-by-key, never spread.
79
79
  dimensionScores: deriveDimensionScores(i.result.dimensions),
80
80
  };
81
81
  const certifyToken = certifyEvidenceTokenOf(i);
82
82
  if (certifyToken) assessment.evidenceToken = certifyToken;
83
- // ADR-049 / issue 128: the attested combined level, bound ONLY when an
83
+ // The attested combined level, bound ONLY when an
84
84
  // interview ran. Added conditionally so an item without one is byte-identical.
85
85
  const token = interviewEvidenceTokenOf(i);
86
86
  if (token) assessment.interviewEvidenceToken = token;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Consent state + email identity + client-side throttle, extracted from src/share.js (structure refactor, issue 020).
3
+ // Consent state + email identity + client-side throttle, extracted from src/share.js.
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
@@ -10,8 +10,8 @@ function configDir() {
10
10
  return getConfigDir(process.env);
11
11
  }
12
12
 
13
- function consentPath() {
14
- return path.join(configDir(), 'consent.json');
13
+ function consentPath(env = process.env) {
14
+ return path.join(getConfigDir(env), 'consent.json');
15
15
  }
16
16
 
17
17
  // Client-side throttle: don't retry a submission if the last one was less than 1h ago.
@@ -70,7 +70,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
70
70
  state.consent = decision;
71
71
  if (email !== null) state.email = normalizeEmail(email);
72
72
  if (state.lastSentAt === undefined) state.lastSentAt = null;
73
- // Email-ownership verification flag (skill-code-certification / ADR-006).
73
+ // Email-ownership verification flag.
74
74
  if (decision === 'granted' && verified !== undefined) {
75
75
  state.emailVerified = verified === true;
76
76
  }
@@ -78,7 +78,7 @@ function recordConsent(decision, email = null, { verified } = {}) {
78
78
  return state;
79
79
  }
80
80
 
81
- // consent management (issue 007: status / revoke / change email) One-shot actions (do not scan), same pattern the retired `--enroll` used.
81
+ // consent management (status / revoke / change email). One-shot actions (do not scan), same pattern the retired `--enroll` used.
82
82
 
83
83
  // Read-only snapshot for `--consent-status`. Never throws.
84
84
  function getConsentStatus() {
@@ -100,7 +100,7 @@ function revokeConsent() {
100
100
  function resetConsent() {
101
101
  const state = loadConsentState() || {};
102
102
  delete state.consent;
103
- // A re-grant is "concede de cero" (ADR-006): drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
103
+ // A re-grant is "concede de cero": drop the verification flag so the OTP runs again from scratch and nothing is sent under the old email until it's re-verified.
104
104
  delete state.emailVerified;
105
105
  if (state.lastSentAt === undefined) state.lastSentAt = null;
106
106
  saveConsentState(state);
@@ -121,7 +121,7 @@ function setEmail(newEmail) {
121
121
  }
122
122
  const state = loadConsentState() || {};
123
123
  state.email = normalizeEmail(newEmail);
124
- // A new email is unverified until proven (ADR-006): reset the flag so
124
+ // A new email is unverified until proven: reset the flag so
125
125
  // autoShare won't send under an address whose ownership wasn't checked.
126
126
  state.emailVerified = false;
127
127
  saveConsentState(state);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js (structure refactor, issue 020).
3
+ // The derived-payload whitelist for the `usage/reports` ingest, extracted from src/share.js.
4
4
 
5
5
  const { scrubSecrets } = require('./agent-synthesis');
6
6
  const { MAX_DEFINITION_CHARS } = require('./agent-evaluation');
@@ -13,7 +13,7 @@ function whitelistSchedulerProbe(probe) {
13
13
  };
14
14
  }
15
15
 
16
- // WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST (issue 115).
16
+ // WHY ONE MORE FIELD IS ADMISSIBLE ON THE AGENT WHITELIST.
17
17
  function agentCatalogIdsByName(report) {
18
18
  const evaluations =
19
19
  report.agentEvaluation && Array.isArray(report.agentEvaluation.evaluations)
@@ -86,7 +86,7 @@ function derivePayload(report, maturity) {
86
86
  tools: Array.isArray(a.tools) ? a.tools : [],
87
87
  model: a.model || null,
88
88
  parent: a.parent || null,
89
- // issue 115 — the ONLY classification field that travels.
89
+ // The ONLY classification field that travels.
90
90
  catalogId: catalogIdsByName.get(a.name) || null,
91
91
  // agent-classifier fix — the agent's own definition (frontmatter description + body), scrubbed + capped.
92
92
  ...(definitionsByName.get(a.name)
@@ -132,14 +132,14 @@ function derivePayload(report, maturity) {
132
132
  mcp: Array.isArray(browserToolsVia.mcp) && browserToolsVia.mcp.length > 0,
133
133
  },
134
134
  },
135
- // Hybrid evaluation, Slice 1: two DETERMINISTIC behavioural blocks (no LLM).
135
+ // Two DETERMINISTIC behavioural blocks (no LLM).
136
136
  gitActivity: whitelistGitActivity(report.gitActivity),
137
137
  sessions: whitelistSessions(report.sessions),
138
- // Hybrid evaluation, Slice 2: one DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
138
+ // One DETERMINISTIC block (workStreams) and two REDACTED-excerpt blocks (steering, decisions).
139
139
  workStreams: whitelistWorkStreams(report.workStreams),
140
140
  steering: whitelistSteering(report.steering),
141
141
  decisions: whitelistDecisions(report.decisions),
142
- // Hybrid evaluation, Slice 3c: PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
142
+ // PER-CANDIDATE authorship + depth (per detected skill, per detected agent).
143
143
  detectionEvidence: whitelistDetectionEvidence(report.detectionEvidence),
144
144
  };
145
145
  }
@@ -176,7 +176,7 @@ function whitelistDetectionEvidence(evidence) {
176
176
  return { skills, agents, provenance: whitelistProvenance(evidence.provenance) };
177
177
  }
178
178
 
179
- // Re-derives the repo-level anti-noise `provenance` (Slice 3): the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
179
+ // Re-derives the repo-level anti-noise `provenance`: the (weak) fork signal and the (conservative) boilerplate/tutorial signal.
180
180
  function whitelistProvenance(provenance) {
181
181
  if (!provenance || typeof provenance !== 'object') return null;
182
182
  const marker =