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
package/src/config.js CHANGED
@@ -4,8 +4,9 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { readEnv } = require('./env-paths');
6
6
  const { getConfigDir } = require('./config-dir');
7
+ const { recordAccountMarker } = require('./account-marker');
7
8
 
8
- // Persistent CLI config file (skill-code-certification, endpoint-config task).
9
+ // Persistent CLI config file.
9
10
  function configDir(env = process.env) {
10
11
  return getConfigDir(env);
11
12
  }
@@ -23,7 +24,7 @@ function loadConfigFile(env = process.env) {
23
24
  }
24
25
  }
25
26
 
26
- // DISTRIBUTION PROFILE (talents-ai-score, ADR-058 fase 1).
27
+ // DISTRIBUTION PROFILE.
27
28
  const VALID_PROFILES = new Set(['external', 'talent']);
28
29
  const PROFILE_DEFAULT_WHEN_ABSENT = 'talent';
29
30
 
@@ -48,7 +49,7 @@ function getUiLang(env = process.env) {
48
49
  return null;
49
50
  }
50
51
 
51
- // config.json holds the superadmin session token (ADR-027) and consent/ endpoint data, so it must never be briefly world/group-readable.
52
+ // config.json holds the superadmin session token and consent/ endpoint data, so it must never be briefly world/group-readable.
52
53
  function saveConfigFile(config, env = process.env) {
53
54
  const dir = configDir(env);
54
55
  fs.mkdirSync(dir, { recursive: true });
@@ -56,7 +57,7 @@ function saveConfigFile(config, env = process.env) {
56
57
  try { fs.chmodSync(configFilePath(env), 0o600); } catch { /* e.g. Windows */ }
57
58
  }
58
59
 
59
- // Endpoint safety (skill-code-certification, endpoint-config task) — BOUNDED.
60
+ // Endpoint safety — BOUNDED.
60
61
  const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
61
62
  const SHAKERS_CLI_DOMAINS = [];
62
63
 
@@ -120,7 +121,7 @@ function resolveIngestEndpoint(env = process.env) {
120
121
  return { endpoint: null, source: 'none' };
121
122
  }
122
123
 
123
- // Baked backend targets per distribution flavor (talents-ai-score, ADR-007/065).
124
+ // Baked backend targets per distribution flavor.
124
125
  // Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
125
126
  // still win at each getter (env > config.json > baked profile).
126
127
  const ENV_PROFILES = {
@@ -130,41 +131,86 @@ const ENV_PROFILES = {
130
131
  profileUrl: 'https://app.shakersworks.com/login',
131
132
  almaBase: 'https://app.shakersworks.com',
132
133
  },
134
+ dev: {
135
+ certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
136
+ hubBase: 'https://api.hub.dev.shakersworks.com/api/v1',
137
+ profileUrl: 'https://works.dev.shakersworks.com/login',
138
+ almaBase: 'https://works.dev.shakersworks.com',
139
+ },
140
+ local: {
141
+ certsBase: 'http://localhost:3004/api/v1',
142
+ hubBase: 'http://localhost:3001/api/v1',
143
+ profileUrl: 'http://localhost:8081/login',
144
+ },
145
+ };
146
+
147
+ const RETIRED_ENV_PROFILES = {
133
148
  staging: {
134
149
  certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
135
150
  hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
136
151
  profileUrl: 'https://works.staging.shakersworks.com/login',
137
152
  almaBase: 'https://works.staging.shakersworks.com',
138
153
  },
139
- dev: {
140
- certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
154
+ 'dev-new-works': {
141
155
  hubBase: 'https://api.hub.new-works.dev.shakersworks.com/api/v1',
142
156
  profileUrl: 'https://works.new-works.dev.shakersworks.com/login',
143
- },
144
- local: {
145
- certsBase: 'http://localhost:3004/api/v1',
146
- hubBase: 'http://localhost:3001/api/v1',
147
- profileUrl: 'http://localhost:8081/login',
157
+ almaBase: 'https://works.new-works.dev.shakersworks.com',
148
158
  },
149
159
  };
150
160
 
151
161
  // Which flavor is baked: SHAKERS_CLI_ENV override wins, else derived from the
152
- // published package name (public -> prod, beta -> staging, private -> dev), else dev (safe default).
153
- const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'staging', '@shakers/shakers-cli': 'dev' };
162
+ // published package name (shakers-cli -> prod, shakers-cli-beta -> dev), else dev (safe default).
163
+ const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'dev' };
154
164
  const DEFAULT_ENV_PROFILE = 'dev';
155
165
 
156
- function bakedEnvProfileName() {
166
+ function envProfileForPackageName(name) {
167
+ return PACKAGE_NAME_ENV[name] || DEFAULT_ENV_PROFILE;
168
+ }
169
+
170
+ function readPackageJson() {
157
171
  try {
158
- const name = require('../package.json').name;
159
- if (PACKAGE_NAME_ENV[name]) return PACKAGE_NAME_ENV[name];
160
- } catch { /* package.json unreadable — fall through to the safe default */ }
161
- return DEFAULT_ENV_PROFILE;
172
+ return require('../package.json');
173
+ } catch {
174
+ return {};
175
+ }
176
+ }
177
+
178
+ function bakedEnvProfileName() {
179
+ return envProfileForPackageName(readPackageJson().name);
180
+ }
181
+
182
+ function envOverrideVariable(env) {
183
+ const next = env.SHAKERS_CLI_ENV;
184
+ return next !== undefined && next !== '' ? 'SHAKERS_CLI_ENV' : 'AI_FOOTPRINT_ENV';
185
+ }
186
+
187
+ class UnknownEnvProfileError extends Error {
188
+ constructor(variable, value) {
189
+ const known = Object.keys(ENV_PROFILES).join(', ');
190
+ super(RETIRED_ENV_PROFILES[value]
191
+ ? `${variable}=${value} is a retired profile. Use one of ${known}, or unset it.`
192
+ : `${variable}=${value} is not a known profile. Use one of ${known}, or unset it.`);
193
+ this.name = 'UnknownEnvProfileError';
194
+ this.variable = variable;
195
+ this.value = value;
196
+ }
162
197
  }
163
198
 
164
199
  function getEnvProfileName(env = process.env) {
165
- const override = readEnv(env, 'ENV');
166
- if (override && ENV_PROFILES[String(override).trim()]) return String(override).trim();
167
- return bakedEnvProfileName();
200
+ const override = String(readEnv(env, 'ENV') || '').trim();
201
+ if (!override) return bakedEnvProfileName();
202
+ if (ENV_PROFILES[override]) return override;
203
+ throw new UnknownEnvProfileError(envOverrideVariable(env), override);
204
+ }
205
+
206
+ function envProfileProblem(env = process.env) {
207
+ try {
208
+ getEnvProfileName(env);
209
+ return null;
210
+ } catch (e) {
211
+ if (e instanceof UnknownEnvProfileError) return e.message;
212
+ throw e;
213
+ }
168
214
  }
169
215
 
170
216
  function getEnvProfile(env = process.env) {
@@ -205,7 +251,7 @@ function getCertsBase(env = process.env) {
205
251
  if (ingest) return ingest.replace(/\/usage\/reports\/?$/, '').replace(/\/+$/, '');
206
252
  // No explicit override anywhere -> the baked flavor profile (never null when a
207
253
  // profile exists), so switching the published flavor "just works" without a
208
- // seeded config.json (ADR-065 revised).
254
+ // seeded config.json.
209
255
  const baked = getEnvProfile(env).certsBase;
210
256
  return baked ? baked.replace(/\/+$/, '') : null;
211
257
  }
@@ -217,11 +263,11 @@ function getIngestEndpoint(env = process.env) {
217
263
  return base ? `${base}/usage/reports` : null;
218
264
  }
219
265
 
220
- // Agent-synthesis endpoint (talents-ai-score, ADR-010/ADR-011).
266
+ // Agent-synthesis endpoint.
221
267
  function getSynthesisEndpoint(env = process.env) {
222
268
  const value = readEnv(env, 'SYNTHESIS_ENDPOINT');
223
269
  if (value && value.trim()) return value.trim();
224
- // Issue 111: derived as an ingest sibling when the override is unset.
270
+ // Derived as an ingest sibling when the override is unset.
225
271
  return deriveFromIngest(env, 'agent-synthesis');
226
272
  }
227
273
 
@@ -231,7 +277,7 @@ function getRoadmapEndpoint(env = process.env) {
231
277
  return deriveFromIngest(env, 'roadmap-personalize');
232
278
  }
233
279
 
234
- // Email-verification endpoints (ADR-006): the OTP "prove you own this email" step that gates PERSISTENCE. Sibling-of-ingest resolution (`new URL(relative, base)`); unset ingest -> null.
280
+ // Email-verification endpoints: the OTP "prove you own this email" step that gates PERSISTENCE. Sibling-of-ingest resolution (`new URL(relative, base)`); unset ingest -> null.
235
281
  function deriveFromIngest(env, relativePath) {
236
282
  const ingest = getIngestEndpoint(env);
237
283
  if (!ingest) return null;
@@ -254,7 +300,7 @@ function getAgentEvaluationEndpoint(env = process.env) {
254
300
  return deriveFromIngest(env, 'agent-evaluation');
255
301
  }
256
302
 
257
- // Login endpoint — better-auth email sign-in (owner decision, supersedes ADR-031 for the email path): `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
303
+ // Login endpoint — better-auth email sign-in: `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
258
304
  function getLoginEndpoint(env = process.env) {
259
305
  const explicit = readEnv(env, 'LOGIN_ENDPOINT');
260
306
  if (explicit && explicit.trim()) return explicit.trim();
@@ -262,6 +308,10 @@ function getLoginEndpoint(env = process.env) {
262
308
  }
263
309
 
264
310
  // better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
311
+ function getSignOutEndpoint(env = process.env) {
312
+ return deriveFromHub(env, 'auth/sign-out');
313
+ }
314
+
265
315
  function getAuthTokenEndpoint(env = process.env) {
266
316
  const explicit = readEnv(env, 'AUTH_TOKEN_ENDPOINT');
267
317
  if (explicit && explicit.trim()) return explicit.trim();
@@ -462,14 +512,14 @@ function getAiProfileEndpoint(env = process.env) {
462
512
  return deriveFromHub(env, 'works/talent/me/ai-profile');
463
513
  }
464
514
 
465
- // Evidence emission endpoint for the `cli_scan` instrument (talents-ai-score Phase 2).
515
+ // Evidence emission endpoint for the `cli_scan` instrument.
466
516
  function getEvidenceCliScanEndpoint(env = process.env) {
467
517
  const explicit = readEnv(env, 'EVIDENCE_CLI_SCAN_ENDPOINT');
468
518
  if (explicit && explicit.trim()) return explicit.trim();
469
519
  return deriveFromIngest(env, '../evidence/cli-scan');
470
520
  }
471
521
 
472
- // Talent-portfolio endpoints (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, talent-only).
522
+ // Talent-portfolio endpoints (`start`'s "Add project to portfolio" route, talent-only).
473
523
  function getPortfoliosDeclareEndpoint(env = process.env) {
474
524
  const explicit = readEnv(env, 'PORTFOLIOS_DECLARE_ENDPOINT');
475
525
  if (explicit && explicit.trim()) return explicit.trim();
@@ -512,7 +562,7 @@ function getAgentsDraftFieldsEndpoint(env = process.env) {
512
562
  return deriveFromIngest(env, '../works/talents/me/agents/draft-fields');
513
563
  }
514
564
 
515
- // Agent<->experience relate endpoint (ADR-041, `start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
565
+ // Agent<->experience relate endpoint (`start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
516
566
  function getAgentPortfoliosEndpoint(agentId, env = process.env) {
517
567
  // Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
518
568
  // getter sweep honest when the env object lands in `agentId`.
@@ -520,7 +570,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
520
570
  return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
521
571
  }
522
572
 
523
- // Onboarding endpoints (evaluation-mcp, ADR-011..015/024 — the register flow).
573
+ // Onboarding endpoints (the register flow).
524
574
  function getHubBase(env = process.env) {
525
575
  const explicit = readEnv(env, 'HUB_BASE');
526
576
  if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
@@ -551,7 +601,7 @@ function getImportProfileEndpoint(env = process.env) {
551
601
  return deriveFromHub(env, 'works/talent/me/import-profile');
552
602
  }
553
603
 
554
- // MCP sign-up (ticket 39540): public UNREGISTERED + claim code, its import status, and the one-time web login.
604
+ // MCP sign-up: public UNREGISTERED + claim code, its import status, and the one-time web login.
555
605
  function getMcpSignupEndpoint(env = process.env) {
556
606
  return deriveFromHub(env, 'works-ai/talents/mcp-signup');
557
607
  }
@@ -564,11 +614,20 @@ function getOnboardingEventsEndpoint(env = process.env) {
564
614
  return deriveFromHub(env, 'works/talent/me/onboarding-events');
565
615
  }
566
616
 
617
+ const AMPLITUDE_HTTP_API = 'https://api.eu.amplitude.com/2/httpapi';
618
+
619
+ function getAmplitudeIngestion(env = process.env) {
620
+ const stamped = readPackageJson().amplitude;
621
+ if (!stamped || typeof stamped.apiKey !== 'string' || !stamped.apiKey.trim()) return null;
622
+ if (stamped.env !== getEnvProfileName(env)) return null;
623
+ return { endpoint: AMPLITUDE_HTTP_API, apiKey: stamped.apiKey.trim() };
624
+ }
625
+
567
626
  function getOneTimeTokenEndpoint(env = process.env) {
568
627
  return deriveFromHub(env, 'auth/one-time-token/generate');
569
628
  }
570
629
 
571
- // The ONE place the web route that redeems a one-time token is spelled (the route itself is a later works-frontend ticket).
630
+ // The ONE place the web route that redeems a one-time token is spelled.
572
631
  function getOneTimeLoginUrl(token, env = process.env) {
573
632
  if (typeof token !== 'string' || !token) return null;
574
633
  const profile = getTalentProfileUrl(env);
@@ -675,7 +734,7 @@ function getOnboardingStatusEndpoint(env = process.env) {
675
734
  return base ? `${base}/interviews/onboarding/status` : null;
676
735
  }
677
736
 
678
- // Talent profile web URL (evaluation-mcp, ADR-011 — the step-5 skip fallback).
737
+ // Talent profile web URL (the step-5 skip fallback).
679
738
  function getTalentProfileUrl(env = process.env) {
680
739
  const explicit = readEnv(env, 'PROFILE_URL');
681
740
  if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
@@ -689,14 +748,14 @@ function getTalentProfileUrl(env = process.env) {
689
748
  // user overrides of these, never the baked profile itself.
690
749
  const PROFILE_ENDPOINT_KEYS = Object.keys(ENV_PROFILES[DEFAULT_ENV_PROFILE]);
691
750
 
692
- // Boot migration (ADR-065 revised). The CLI used to SEED config.json with the
751
+ // Boot migration. The CLI used to SEED config.json with the
693
752
  // baked bases on first run; the getters now fall back to getEnvProfile() directly,
694
753
  // so seeding is unnecessary AND harmful: `npm uninstall` leaves the config dir, so
695
- // a config.json seeded by a previous flavor (e.g. the private @shakers build → dev)
696
- // survives into a new flavor (the public beta → staging) and pins the old endpoints
754
+ // a config.json seeded by a previous flavor (e.g. the beta when it baked staging)
755
+ // survives into a new flavor (the beta now baking dev) and pins the old endpoints
697
756
  // forever, since config.json wins over the baked profile. This sweep removes any
698
757
  // endpoint key whose value is byte-identical to that SAME field in a KNOWN profile
699
- // OTHER than the baked one — i.e. a value that could only have gotten there by the
758
+ // (current or retired) OTHER than the baked one — i.e. a value that could only have gotten there by the
700
759
  // old seeding of a different flavor. A value that matches no known profile is a
701
760
  // genuine user override (set via `shakers config set`) and is left untouched. We
702
761
  // never touch env, auth-session.json or consent.json, and never remove the dir.
@@ -708,7 +767,7 @@ function sanitizeStaleBakedEndpoints(env = process.env) {
708
767
  const current = config[key];
709
768
  if (typeof current !== 'string' || !current.trim()) continue;
710
769
  const normalized = current.trim().replace(/\/+$/, '');
711
- const matchesForeignProfile = Object.entries(ENV_PROFILES).some(
770
+ const matchesForeignProfile = Object.entries({ ...RETIRED_ENV_PROFILES, ...ENV_PROFILES }).some(
712
771
  ([name, profile]) =>
713
772
  name !== bakedName && String(profile[key]).replace(/\/+$/, '') === normalized,
714
773
  );
@@ -735,12 +794,12 @@ function getSuperadminSessionEndpoint(env = process.env) {
735
794
  return deriveFromIngest(env, 'superadmin/session');
736
795
  }
737
796
 
738
- // Superadmin READ-ONLY certification inspection endpoint (ADR-025, NON-PROD only) — a sibling of the ingest endpoint.
797
+ // Superadmin READ-ONLY certification inspection endpoint (NON-PROD only) — a sibling of the ingest endpoint.
739
798
  function getInspectCertificationsEndpoint(env = process.env) {
740
799
  return deriveFromIngest(env, 'superadmin/inspect-certifications');
741
800
  }
742
801
 
743
- // ADR-027 superadmin SESSION persistence.
802
+ // Superadmin SESSION persistence.
744
803
  function loadSuperadminSession(env = process.env) {
745
804
  const config = loadConfigFile(env);
746
805
  const s = config && typeof config === 'object' ? config.superadminSession : null;
@@ -757,6 +816,7 @@ function saveSuperadminSession(session, env = process.env) {
757
816
  expiresAt: session.expiresAt || null,
758
817
  };
759
818
  saveConfigFile(config, env);
819
+ recordAccountMarker('superadmin', env);
760
820
  }
761
821
 
762
822
  function clearSuperadminSession(env = process.env) {
@@ -774,6 +834,7 @@ module.exports = {
774
834
  getAgentEvaluationEndpoint,
775
835
  getLoginEndpoint,
776
836
  getAuthTokenEndpoint,
837
+ getSignOutEndpoint,
777
838
  getCompleteRegistrationEndpoint,
778
839
  getDeviceAuthorizeEndpoint,
779
840
  getDeviceTokenEndpoint,
@@ -807,19 +868,19 @@ module.exports = {
807
868
  getUsageDiscoveredInventoryEndpoint,
808
869
  getUsageReportEndpoint,
809
870
  getAiProfileEndpoint,
810
- // Evidence emission (talents-ai-score Phase 2, evidence-based evaluation).
871
+ // Evidence emission.
811
872
  getEvidenceCliScanEndpoint,
812
- // Talent-portfolio endpoints (ADR-059).
873
+ // Talent-portfolio endpoints.
813
874
  getPortfoliosDeclareEndpoint,
814
875
  getPortfoliosListEndpoint,
815
876
  getPortfolioDraftDescriptionEndpoint,
816
877
  getPortfolioUpdateEndpoint,
817
- // Talent-agent endpoints (talents-ai-score Phase 2 — "Add agent to profile").
878
+ // Talent-agent endpoints ("Add agent to profile").
818
879
  getAgentsDeclareEndpoint,
819
880
  getAgentsListEndpoint,
820
881
  getAgentsDraftFieldsEndpoint,
821
882
  getAgentPortfoliosEndpoint,
822
- // Onboarding / register flow (evaluation-mcp, ADR-011..015/024).
883
+ // Onboarding / register flow.
823
884
  getCertsBase,
824
885
  getHubBase,
825
886
  sanitizeStaleBakedEndpoints,
@@ -828,6 +889,8 @@ module.exports = {
828
889
  getMcpSignupEndpoint,
829
890
  getMyImportStatusEndpoint,
830
891
  getOnboardingEventsEndpoint,
892
+ getAmplitudeIngestion,
893
+ AMPLITUDE_HTTP_API,
831
894
  getOneTimeTokenEndpoint,
832
895
  getOneTimeLoginUrl,
833
896
  getProfessionalDetailsEndpoint,
@@ -849,7 +912,7 @@ module.exports = {
849
912
  getEmailVerificationVerifyUrl,
850
913
  getSuperadminSessionEndpoint,
851
914
  getInspectCertificationsEndpoint,
852
- // ADR-027 superadmin session persistence.
915
+ // Superadmin session persistence.
853
916
  loadSuperadminSession,
854
917
  saveSuperadminSession,
855
918
  clearSuperadminSession,
@@ -863,15 +926,18 @@ module.exports = {
863
926
  setIngestEndpoint,
864
927
  hasRetiredFallbackConfig,
865
928
  resolveIngestEndpoint,
866
- // Distribution profile (ADR-058 fase 1).
929
+ // Distribution profile.
867
930
  VALID_PROFILES,
868
931
  getProfile,
869
932
  isTalentProfile,
870
933
  // UI language preference (env SHAKERS_CLI_LANG > config.json `lang`).
871
934
  VALID_UI_LANGS,
872
935
  getUiLang,
873
- // Baked backend target flavor (staging | dev | local).
936
+ // Baked backend target flavor (prod | dev | local).
874
937
  ENV_PROFILES,
875
938
  getEnvProfileName,
876
939
  getEnvProfile,
940
+ envProfileForPackageName,
941
+ envProfileProblem,
942
+ UnknownEnvProfileError,
877
943
  };
@@ -8,17 +8,18 @@ const {
8
8
  const { runEmailVerification } = require('./email-verification');
9
9
  const { renderLegalNotice } = require('./legal-notice');
10
10
 
11
- // Consent prompt (talents-ai-score, ADR-011 — revises issue 006 / ADR-007's disclosure wall; ADR-051 revises ADR-011's OWN ordering in turn, see below).
11
+ // Consent prompt (replaces the earlier disclosure wall; its ordering is revised in turn, see below).
12
12
 
13
13
  const MAX_ATTEMPTS = 5;
14
14
 
15
15
  const YES_RE = /^(y|yes|s|si|sí)$/i;
16
16
  const NO_RE = /^(n|no)$/i;
17
17
 
18
- async function askYesNo(ask, question, catalog, { defaultNo = false } = {}) {
18
+ async function askYesNo(ask, question, catalog, { defaultNo = false, defaultYes = false } = {}) {
19
19
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
20
20
  const raw = String(await ask(question)).trim();
21
21
  if (defaultNo && raw === '') return false;
22
+ if (defaultYes && raw === '') return true;
22
23
  if (YES_RE.test(raw)) return true;
23
24
  if (NO_RE.test(raw)) return false;
24
25
  process.stdout.write(` ${catalog.consent.invalidAnswer}\n`);
@@ -45,7 +46,7 @@ async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
45
46
  });
46
47
  }
47
48
 
48
- // Runs the short consent-to-persist prompt + (if accepted) email collection + (skill-code-certification / ADR-006) EMAIL-OWNERSHIP VERIFICATION.
49
+ // Runs the short consent-to-persist prompt + (if accepted) email collection + EMAIL-OWNERSHIP VERIFICATION.
49
50
  async function runConsentPrompt({
50
51
  ask,
51
52
  catalog,
@@ -57,7 +58,7 @@ async function runConsentPrompt({
57
58
  profile = 'talent',
58
59
  }) {
59
60
  const c = catalog.consent;
60
- // Presentation only (talents-ai-score): framed by src/legal-notice.js on a
61
+ // Presentation only: framed by src/legal-notice.js on a
61
62
  // real TTY, byte-identical (no heading, no rule) otherwise.
62
63
  process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
63
64
 
@@ -100,7 +101,7 @@ async function runConsentPrompt({
100
101
 
101
102
  process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
102
103
 
103
- // Prove email ownership BEFORE persisting (ADR-006) — every profile, external included.
104
+ // Prove email ownership BEFORE persisting — every profile, external included.
104
105
  const verification = await verifyEmail({ email, ask, catalog, env });
105
106
  if (verification && verification.verified) {
106
107
  recordConsent('granted', email, { verified: true });
@@ -112,4 +113,4 @@ async function runConsentPrompt({
112
113
  return null;
113
114
  }
114
115
 
115
- module.exports = { runConsentPrompt, defaultVerifyEmail };
116
+ module.exports = { runConsentPrompt, defaultVerifyEmail, askYesNo };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently (talents-ai-score, DX).
3
+ // Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently.
4
4
  function computeConsentSkip({ decision, emailVerified, stdinIsTTY, consentFilePath, catalog } = {}) {
5
5
  const c = catalog && catalog.consent;
6
6
 
@@ -5,7 +5,7 @@ const { detectTechnologyManifests } = require('./tech-detector');
5
5
  const { agentSourceFilesByName } = require('./agent-org-chart');
6
6
  const { classifyVendored, collectProvenance } = require('./detection-noise');
7
7
 
8
- // Per-CANDIDATE authorship + depth (hybrid evaluation, Slice 3c).
8
+ // Per-CANDIDATE authorship + depth.
9
9
 
10
10
  const SKILL_DEPTH_SATURATION = 3;
11
11
  const AGENT_DEPTH_SATURATION = 8;
@@ -38,7 +38,7 @@ function recencyDaysForPaths(relPaths, authorship, nowSeconds) {
38
38
  return Math.max(0, Math.floor((nowSeconds - mostRecent) / 86400));
39
39
  }
40
40
 
41
- // Attributes a candidate's evidence files against the talent's own email via the ADR-017 gate.
41
+ // Attributes a candidate's evidence files against the talent's own email via the authorship gate.
42
42
  function attributionCounts(relPaths, verifiedEmail, authorship) {
43
43
  if (!Array.isArray(relPaths) || relPaths.length === 0) {
44
44
  return { authoredFileCount: 0, totalFileCount: 0, authorshipKnown: false };
@@ -4,7 +4,7 @@ const { execFileSync } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
- // Anti-noise CLASSIFICATION for detected candidates (hybrid evaluation, Slice 3 — issue 019).
7
+ // Anti-noise CLASSIFICATION for detected candidates.
8
8
 
9
9
  const GIT_TIMEOUT_MS = 15000;
10
10
  const README_MAX_BYTES = 8 * 1024;
package/src/detectors.js CHANGED
@@ -147,7 +147,7 @@ const detectors = [
147
147
  ],
148
148
  },
149
149
 
150
- // ---- Expansion (talents-ai-score, more signals) — confidence per entry below ----
150
+ // ---- Expansion — confidence per entry below ----
151
151
  {
152
152
  id: 'amazon-q-developer',
153
153
  name: 'Amazon Q Developer',
@@ -95,7 +95,7 @@ async function runDeviceLogin(
95
95
  timeoutMs,
96
96
  });
97
97
  if (!fin.ok) return { ok: false, reason: fin.reason };
98
- return { ok: true, token: fin.token, sessionToken: res.accessToken, isNewUser: res.isNewUser, email: res.email || null };
98
+ return { ok: true, token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, isNewUser: res.isNewUser, email: res.email || null };
99
99
  }
100
100
  if (res.status === 'pending') continue;
101
101
  if (res.status === 'slow_down') {
@@ -108,7 +108,18 @@ async function runDeviceLogin(
108
108
  }
109
109
  }
110
110
 
111
+ function deviceSessionRecord({ token, sessionToken = null, sessionExpiresIn = null, email = null }, now = Date.now()) {
112
+ return {
113
+ accessToken: token,
114
+ hubAccessToken: token,
115
+ sessionToken: sessionToken || null,
116
+ sessionExpiresAt: sessionExpiresIn ? new Date(now + sessionExpiresIn * 1000).toISOString() : null,
117
+ email: email || null,
118
+ };
119
+ }
120
+
111
121
  module.exports = {
122
+ deviceSessionRecord,
112
123
  startDeviceLogin,
113
124
  pollDeviceToken,
114
125
  finalizeDeviceSession,
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { requestBackend } = require('./backend-request');
4
4
 
5
- // Email-verification client + wait-mode loop (skill-code-certification, ADR-006).
5
+ // Email-verification client + wait-mode loop.
6
6
 
7
7
  const DEFAULT_TIMEOUT_MS = 15000;
8
8
  // Bounded so a Talent who can never produce a matching code can't loop forever — after this many CODE-ENTRY attempts we give up (nothing persisted, asked again next run).
@@ -71,7 +71,7 @@ function classifyVerifyReason(reason) {
71
71
  }
72
72
 
73
73
  // The interactive "modo espera": send a code, then loop reading the pasted code until it verifies, the Talent cancels, or attempts run out.
74
- // talents-ai-score, ADR-042: `requestUrl`/`verifyUrl` address the ONE backend (the certifications service).
74
+ // `requestUrl`/`verifyUrl` address the ONE backend (the certifications service).
75
75
  async function runEmailVerification({
76
76
  email,
77
77
  ask,
@@ -103,7 +103,7 @@ async function runEmailVerification({
103
103
  write(` ${v.waitHint}\n`);
104
104
 
105
105
  for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS; ) {
106
- // The OTP is a secret too (issue 102): same reader, same echo, and the fix is the same call.
106
+ // The OTP is a secret too: same reader, same echo, and the fix is the same call.
107
107
  const readCode = typeof ask.secret === 'function' ? ask.secret.bind(ask) : ask;
108
108
  const raw = String(await readCode(v.codePrompt)).trim();
109
109
 
package/src/env-paths.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const os = require('os');
4
4
 
5
- // Shared home-directory + env resolution (talents-ai-score, ADR-014 — "Alcance del tier: Talento = proyecto ∪ home", applied uniformly to every category).
5
+ // Shared home-directory + env resolution (tier scope = project ∪ home, applied uniformly to every category).
6
6
  function readEnv(env, suffix) {
7
7
  const next = env[`SHAKERS_CLI_${suffix}`];
8
8
  if (next !== undefined && next !== '') return next;
@@ -136,6 +136,17 @@ async function countOpenPositions(deps = {}, { hubAccessToken, clusterId, timeou
136
136
  return total === null ? { ok: false, reason: 'no-total' } : { ok: true, total };
137
137
  }
138
138
 
139
+ // `--recommended` is a CLIENT-SIDE filter over the fetched page (the hub has no
140
+ // server-side "recommended" criterion — `match` is output-only). Recommended =
141
+ // items that have a match. If none on this page, fall back to the available
142
+ // items so the user is never left with an empty screen.
143
+ function splitRecommended(items, recommended) {
144
+ if (!recommended) return { shown: items, mode: null };
145
+ const rec = items.filter((it) => it.match != null);
146
+ if (rec.length > 0) return { shown: rec, mode: 'recommended' };
147
+ return { shown: items, mode: 'fallback' };
148
+ }
149
+
139
150
  module.exports = {
140
151
  TABS,
141
152
  countOpenPositions,
@@ -145,4 +156,5 @@ module.exports = {
145
156
  normalizeItem,
146
157
  requestFindProjects,
147
158
  fetchFindProjects,
159
+ splitRecommended,
148
160
  };
@@ -3,7 +3,7 @@
3
3
  // `find-projects` flow: list the Positions/Projects available to the talent,
4
4
  // paginated and filtered server-side via the endpoint's `criteria`. Read-only.
5
5
 
6
- const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
6
+ const { TABS, ATTENDANCE, DEFAULT_LIMIT, splitRecommended } = require('./find-projects-client');
7
7
  const { styleLabelPrefix } = require('./ansi');
8
8
 
9
9
  function makeFindProjectsDeps(overrides = {}) {
@@ -30,17 +30,6 @@ function resolveQuery(opts = {}) {
30
30
  return { tab, limit, page, offset, attendance, country, attendanceInvalid, countryInvalid, recommended: !!opts.recommended };
31
31
  }
32
32
 
33
- // `--recommended` is a CLIENT-SIDE filter over the fetched page (the hub has no
34
- // server-side "recommended" criterion — `match` is output-only). Recommended =
35
- // items that have a match. If none on this page, fall back to the available
36
- // items so the user is never left with an empty screen.
37
- function splitRecommended(items, recommended) {
38
- if (!recommended) return { shown: items, mode: null };
39
- const rec = items.filter((it) => it.match != null);
40
- if (rec.length > 0) return { shown: rec, mode: 'recommended' };
41
- return { shown: items, mode: 'fallback' };
42
- }
43
-
44
33
  function budgetLine(item, fp) {
45
34
  if (item.budget && item.budget.display) return item.budget.display;
46
35
  if (item.budget && (item.budget.from != null || item.budget.to != null)) {
@@ -157,7 +146,6 @@ module.exports = {
157
146
  makeFindProjectsDeps,
158
147
  runFindProjects,
159
148
  resolveQuery,
160
- splitRecommended,
161
149
  formatItem,
162
150
  paginationFooter,
163
151
  recommendedFooter,
@@ -55,7 +55,7 @@ function round2(n) {
55
55
  return Math.round(n * 100) / 100;
56
56
  }
57
57
 
58
- // Deterministic git-activity signals for the project where `shakers ai-usage` runs (hybrid evaluation, Slice 1).
58
+ // Deterministic git-activity signals for the project where `shakers ai-usage` runs.
59
59
  function collectGitActivity(root, verifiedEmail) {
60
60
  if (!isGitRepo(root)) return null;
61
61
 
@@ -24,10 +24,10 @@ function buildCertsPayload(project, lang) {
24
24
  .filter(Boolean)
25
25
  .map((item) => {
26
26
  const res = item.result || {};
27
- // `skillName` and `technology` both come off the wire (the service's certifiable list / verdict), and `technology` was one of the two fields issue 055 was opened for.
27
+ // `skillName` and `technology` both come off the wire (the service's certifiable list / verdict).
28
28
  const base = sanitizeRenderText(item.skillName) || (item.skillId != null ? String(item.skillId) : 'Skill');
29
29
  const name = item.technology ? `${base} · ${sanitizeRenderText(item.technology)}` : base;
30
- // ADR-016: named level (Middle/Senior/Expert) instead of the numeric grade.
30
+ // Named level (Middle/Senior/Expert) instead of the numeric grade.
31
31
  const skillReport = (t.certify && t.certify.report) || {};
32
32
  const skillLevelNames = skillReport.skillLevels || {};
33
33
  const { key: levelKey, band } = levelInfoFromCombinedLevel(res.combinedLevel) || skillLevelForScore(res.score);
@@ -36,7 +36,7 @@ function buildCertsPayload(project, lang) {
36
36
  band, // retained for the badge colour (unchanged palette)
37
37
  levelKey,
38
38
  levelName: levelKey ? (skillLevelNames[levelKey] || levelKey) : (skillLevelNames.na || '—'),
39
- // Model-written text off the wire; sanitised (issue 055 — left raw by 052).
39
+ // Model-written text off the wire; sanitised.
40
40
  rationale: typeof res.rationale === 'string' ? sanitizeRenderText(res.rationale) : '',
41
41
  improvements: Array.isArray(res.improvements)
42
42
  ? res.improvements.map((i) => (typeof i === 'string' ? sanitizeRenderText(i) : i))