shakers-cli 0.15.0 → 0.17.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 (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  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/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. 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 };
@@ -2,11 +2,12 @@
2
2
 
3
3
  const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
4
4
  const { getProfile } = require('./config');
5
+ const { hasAccountTrace } = require('./account-trace');
5
6
 
6
- // THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY (talents-ai-score, issue 123 / ADR-044).
7
+ // THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY.
7
8
 
8
9
  // Pure: identity in, policy out.
9
- function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
10
+ function resolveReportGate({ loggedIn = false, accountTrace = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
10
11
  const showEverything = !loggedIn;
11
12
  const resolvedProfile = profile === 'external' ? 'external' : 'talent';
12
13
  return Object.freeze({
@@ -16,7 +17,7 @@ function resolveReportGate({ loggedIn = false, status = null, email = null, user
16
17
  email: loggedIn && typeof email === 'string' && email ? email : null,
17
18
  // --roadmap AND its server-side computation/egress (see bin/report.js): a
18
19
  // logged-in Talent's roadmap is not merely hidden, it is not requested.
19
- showRoadmap: showEverything,
20
+ showRoadmap: showEverything && !accountTrace,
20
21
  // Per-agent "how to improve this agent" tips in the footprint report.
21
22
  showAgentSuggestions: showEverything,
22
23
  // The copyable remediation prompt in a `certify` verdict.
@@ -35,6 +36,7 @@ function reportGateForIdentity(env = process.env, now = Date.now()) {
35
36
  const status = sessionStatus(session, now);
36
37
  return resolveReportGate({
37
38
  loggedIn: status === 'active',
39
+ accountTrace: hasAccountTrace(env),
38
40
  status,
39
41
  email: session ? session.email : null,
40
42
  userType: session ? session.userType : null,
@@ -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.
@@ -54,10 +54,10 @@ function migrateFromV1(parsed) {
54
54
  return { schemaVersion: SCHEMA_VERSION, updatedAt: (parsed && parsed.updatedAt) || null, projects };
55
55
  }
56
56
 
57
- function loadState() {
57
+ function loadState(env = process.env) {
58
58
  let parsed;
59
59
  try {
60
- parsed = JSON.parse(fs.readFileSync(statePath(), 'utf8'));
60
+ parsed = JSON.parse(fs.readFileSync(statePath(env), 'utf8'));
61
61
  } catch {
62
62
  return freshState();
63
63
  }
@@ -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;
@@ -154,6 +154,10 @@ function recordBackendAcceptance({ root, kind, backend }) {
154
154
  return stampAndSaveState(state, absRoot);
155
155
  }
156
156
 
157
+ function hasRecordedUpload(env = process.env) {
158
+ return Object.values(loadState(env).projects).some((p) => !!(p && p.backendAcceptance && Object.keys(p.backendAcceptance).length));
159
+ }
160
+
157
161
  function loadFootprint({ root } = {}) {
158
162
  const absRoot = path.resolve(root || process.cwd());
159
163
  const state = loadState();
@@ -179,12 +183,12 @@ function materializeProjectReport({ root, lang }) {
179
183
  const state = loadState();
180
184
  const project = state.projects[absRoot];
181
185
  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.
186
+ // 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
187
  const gate = reportGateForIdentity();
184
188
  return { hasData: true, ...writeProjectHtml(project, absRoot, lang, gate) };
185
189
  }
186
190
 
187
- // `upsertFootprint` AND `upsertCertification` WERE HERE, and ADR-035 retired them.
191
+ // `upsertFootprint` AND `upsertCertification` WERE HERE, and were retired.
188
192
 
189
193
  function certKey(item) {
190
194
  if (item && item.skillId != null) return `id:${item.skillId}`;
@@ -220,6 +224,7 @@ module.exports = {
220
224
  loadAgentClassifications,
221
225
  loadFootprint,
222
226
  recordBackendAcceptance,
227
+ hasRecordedUpload,
223
228
  materializeProjectReport,
224
229
  certKey,
225
230
  SCHEMA_VERSION,
@@ -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 = [];
package/src/roles-flow.js CHANGED
@@ -88,6 +88,7 @@ async function runAddRole({ io, session, catalog, deps = makeRolesDeps() }) {
88
88
  // deduped, for setting/switching the MAIN role.
89
89
  async function fetchHeldRoles({ deps, hubAccessToken }) {
90
90
  const held = [];
91
+ const assigned = [];
91
92
  let mainClusterId = null;
92
93
  const certEndpoint = deps.getMyCertificationsEndpoint();
93
94
  if (certEndpoint) {
@@ -99,10 +100,11 @@ async function fetchHeldRoles({ deps, hubAccessToken }) {
99
100
  }
100
101
  const assignedEndpoint = deps.getAssignedClustersEndpoint();
101
102
  if (assignedEndpoint) {
102
- const assigned = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
103
- if (assigned.ok) for (const c of assigned.clusters || []) held.push(c);
103
+ const res = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
104
+ if (res.ok) assigned.push(...(res.clusters || []));
104
105
  }
105
- return { held: dedupeByCluster(held), mainClusterId };
106
+ held.push(...assigned);
107
+ return { held: dedupeByCluster(held), assigned, mainClusterId };
106
108
  }
107
109
 
108
110
  // `change-role`: switch the MAIN role to one of the roles the talent already holds.
@@ -130,12 +132,12 @@ async function runChangeMainRole({ io, session, catalog, deps = makeRolesDeps()
130
132
 
131
133
  // The onboarding final step: pick a MAIN role from the roles resolved for the
132
134
  // talent (interview recommendations + any held). Degrades gracefully to a skip.
133
- async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps() }) {
135
+ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps(), onRolesShown = () => {} }) {
134
136
  const r = catalog.roles;
135
137
  const hubAccessToken = session ? session.hubAccessToken : null;
136
138
 
137
139
  io.section(r.mainRoleTitle);
138
- const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
140
+ const { held, assigned, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
139
141
 
140
142
  // Prefer the talent's own/recommended roles; fall back to the catalog for a
141
143
  // fresh talent so a main role can always be set (picking one adds it first).
@@ -150,6 +152,7 @@ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRole
150
152
  io.notify(r.mainRolePending);
151
153
  return { ok: true, chosen: null };
152
154
  }
155
+ onRolesShown({ suggested: assigned.filter((c) => c.type === 'RECOMMENDED').map((c) => c.clusterId), clustersCount: assigned.length });
153
156
  const choice = await chooseRole({
154
157
  io, roles, header: r.mainRolePrompt, hint: r.selectHint, initialClusterId: mainClusterId,
155
158
  });
@@ -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;