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
package/src/config.js CHANGED
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const { readEnv } = require('./env-paths');
6
6
  const { getConfigDir } = require('./config-dir');
7
7
 
8
- // Persistent CLI config file (skill-code-certification, endpoint-config task).
8
+ // Persistent CLI config file.
9
9
  function configDir(env = process.env) {
10
10
  return getConfigDir(env);
11
11
  }
@@ -23,7 +23,7 @@ function loadConfigFile(env = process.env) {
23
23
  }
24
24
  }
25
25
 
26
- // DISTRIBUTION PROFILE (talents-ai-score, ADR-058 fase 1).
26
+ // DISTRIBUTION PROFILE.
27
27
  const VALID_PROFILES = new Set(['external', 'talent']);
28
28
  const PROFILE_DEFAULT_WHEN_ABSENT = 'talent';
29
29
 
@@ -48,7 +48,7 @@ function getUiLang(env = process.env) {
48
48
  return null;
49
49
  }
50
50
 
51
- // config.json holds the superadmin session token (ADR-027) and consent/ endpoint data, so it must never be briefly world/group-readable.
51
+ // config.json holds the superadmin session token and consent/ endpoint data, so it must never be briefly world/group-readable.
52
52
  function saveConfigFile(config, env = process.env) {
53
53
  const dir = configDir(env);
54
54
  fs.mkdirSync(dir, { recursive: true });
@@ -56,7 +56,7 @@ function saveConfigFile(config, env = process.env) {
56
56
  try { fs.chmodSync(configFilePath(env), 0o600); } catch { /* e.g. Windows */ }
57
57
  }
58
58
 
59
- // Endpoint safety (skill-code-certification, endpoint-config task) — BOUNDED.
59
+ // Endpoint safety — BOUNDED.
60
60
  const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
61
61
  const SHAKERS_CLI_DOMAINS = [];
62
62
 
@@ -120,7 +120,7 @@ function resolveIngestEndpoint(env = process.env) {
120
120
  return { endpoint: null, source: 'none' };
121
121
  }
122
122
 
123
- // Baked backend targets per distribution flavor (talents-ai-score, ADR-007/065).
123
+ // Baked backend targets per distribution flavor.
124
124
  // Only the DEFAULT bases; per-endpoint SHAKERS_CLI_* env and config.json overrides
125
125
  // still win at each getter (env > config.json > baked profile).
126
126
  const ENV_PROFILES = {
@@ -130,41 +130,86 @@ const ENV_PROFILES = {
130
130
  profileUrl: 'https://app.shakersworks.com/login',
131
131
  almaBase: 'https://app.shakersworks.com',
132
132
  },
133
+ dev: {
134
+ certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
135
+ hubBase: 'https://api.hub.dev.shakersworks.com/api/v1',
136
+ profileUrl: 'https://works.dev.shakersworks.com/login',
137
+ almaBase: 'https://works.dev.shakersworks.com',
138
+ },
139
+ local: {
140
+ certsBase: 'http://localhost:3004/api/v1',
141
+ hubBase: 'http://localhost:3001/api/v1',
142
+ profileUrl: 'http://localhost:8081/login',
143
+ },
144
+ };
145
+
146
+ const RETIRED_ENV_PROFILES = {
133
147
  staging: {
134
148
  certsBase: 'https://api.staging.certifications.shakersworks.com/api/v1',
135
149
  hubBase: 'https://api.hub.staging.shakersworks.com/api/v1',
136
150
  profileUrl: 'https://works.staging.shakersworks.com/login',
137
151
  almaBase: 'https://works.staging.shakersworks.com',
138
152
  },
139
- dev: {
140
- certsBase: 'https://api.dev.certifications.shakersworks.com/api/v1',
153
+ 'dev-new-works': {
141
154
  hubBase: 'https://api.hub.new-works.dev.shakersworks.com/api/v1',
142
155
  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',
156
+ almaBase: 'https://works.new-works.dev.shakersworks.com',
148
157
  },
149
158
  };
150
159
 
151
160
  // 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' };
161
+ // published package name (shakers-cli -> prod, shakers-cli-beta -> dev), else dev (safe default).
162
+ const PACKAGE_NAME_ENV = { 'shakers-cli': 'prod', 'shakers-cli-beta': 'dev' };
154
163
  const DEFAULT_ENV_PROFILE = 'dev';
155
164
 
156
- function bakedEnvProfileName() {
165
+ function envProfileForPackageName(name) {
166
+ return PACKAGE_NAME_ENV[name] || DEFAULT_ENV_PROFILE;
167
+ }
168
+
169
+ function readPackageJson() {
157
170
  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;
171
+ return require('../package.json');
172
+ } catch {
173
+ return {};
174
+ }
175
+ }
176
+
177
+ function bakedEnvProfileName() {
178
+ return envProfileForPackageName(readPackageJson().name);
179
+ }
180
+
181
+ function envOverrideVariable(env) {
182
+ const next = env.SHAKERS_CLI_ENV;
183
+ return next !== undefined && next !== '' ? 'SHAKERS_CLI_ENV' : 'AI_FOOTPRINT_ENV';
184
+ }
185
+
186
+ class UnknownEnvProfileError extends Error {
187
+ constructor(variable, value) {
188
+ const known = Object.keys(ENV_PROFILES).join(', ');
189
+ super(RETIRED_ENV_PROFILES[value]
190
+ ? `${variable}=${value} is a retired profile. Use one of ${known}, or unset it.`
191
+ : `${variable}=${value} is not a known profile. Use one of ${known}, or unset it.`);
192
+ this.name = 'UnknownEnvProfileError';
193
+ this.variable = variable;
194
+ this.value = value;
195
+ }
162
196
  }
163
197
 
164
198
  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();
199
+ const override = String(readEnv(env, 'ENV') || '').trim();
200
+ if (!override) return bakedEnvProfileName();
201
+ if (ENV_PROFILES[override]) return override;
202
+ throw new UnknownEnvProfileError(envOverrideVariable(env), override);
203
+ }
204
+
205
+ function envProfileProblem(env = process.env) {
206
+ try {
207
+ getEnvProfileName(env);
208
+ return null;
209
+ } catch (e) {
210
+ if (e instanceof UnknownEnvProfileError) return e.message;
211
+ throw e;
212
+ }
168
213
  }
169
214
 
170
215
  function getEnvProfile(env = process.env) {
@@ -205,7 +250,7 @@ function getCertsBase(env = process.env) {
205
250
  if (ingest) return ingest.replace(/\/usage\/reports\/?$/, '').replace(/\/+$/, '');
206
251
  // No explicit override anywhere -> the baked flavor profile (never null when a
207
252
  // profile exists), so switching the published flavor "just works" without a
208
- // seeded config.json (ADR-065 revised).
253
+ // seeded config.json.
209
254
  const baked = getEnvProfile(env).certsBase;
210
255
  return baked ? baked.replace(/\/+$/, '') : null;
211
256
  }
@@ -217,11 +262,11 @@ function getIngestEndpoint(env = process.env) {
217
262
  return base ? `${base}/usage/reports` : null;
218
263
  }
219
264
 
220
- // Agent-synthesis endpoint (talents-ai-score, ADR-010/ADR-011).
265
+ // Agent-synthesis endpoint.
221
266
  function getSynthesisEndpoint(env = process.env) {
222
267
  const value = readEnv(env, 'SYNTHESIS_ENDPOINT');
223
268
  if (value && value.trim()) return value.trim();
224
- // Issue 111: derived as an ingest sibling when the override is unset.
269
+ // Derived as an ingest sibling when the override is unset.
225
270
  return deriveFromIngest(env, 'agent-synthesis');
226
271
  }
227
272
 
@@ -231,7 +276,7 @@ function getRoadmapEndpoint(env = process.env) {
231
276
  return deriveFromIngest(env, 'roadmap-personalize');
232
277
  }
233
278
 
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.
279
+ // 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
280
  function deriveFromIngest(env, relativePath) {
236
281
  const ingest = getIngestEndpoint(env);
237
282
  if (!ingest) return null;
@@ -254,7 +299,7 @@ function getAgentEvaluationEndpoint(env = process.env) {
254
299
  return deriveFromIngest(env, 'agent-evaluation');
255
300
  }
256
301
 
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}`.
302
+ // Login endpoint — better-auth email sign-in: `POST {hubBase}/auth/sign-in/email` with `{email,password}`.
258
303
  function getLoginEndpoint(env = process.env) {
259
304
  const explicit = readEnv(env, 'LOGIN_ENDPOINT');
260
305
  if (explicit && explicit.trim()) return explicit.trim();
@@ -262,6 +307,10 @@ function getLoginEndpoint(env = process.env) {
262
307
  }
263
308
 
264
309
  // better-auth JWT endpoint: `GET {hubBase}/auth/token` with the session cookie → `{token}` (ES256/JWKS, ~15-min TTL).
310
+ function getSignOutEndpoint(env = process.env) {
311
+ return deriveFromHub(env, 'auth/sign-out');
312
+ }
313
+
265
314
  function getAuthTokenEndpoint(env = process.env) {
266
315
  const explicit = readEnv(env, 'AUTH_TOKEN_ENDPOINT');
267
316
  if (explicit && explicit.trim()) return explicit.trim();
@@ -462,14 +511,14 @@ function getAiProfileEndpoint(env = process.env) {
462
511
  return deriveFromHub(env, 'works/talent/me/ai-profile');
463
512
  }
464
513
 
465
- // Evidence emission endpoint for the `cli_scan` instrument (talents-ai-score Phase 2).
514
+ // Evidence emission endpoint for the `cli_scan` instrument.
466
515
  function getEvidenceCliScanEndpoint(env = process.env) {
467
516
  const explicit = readEnv(env, 'EVIDENCE_CLI_SCAN_ENDPOINT');
468
517
  if (explicit && explicit.trim()) return explicit.trim();
469
518
  return deriveFromIngest(env, '../evidence/cli-scan');
470
519
  }
471
520
 
472
- // Talent-portfolio endpoints (talents-ai-score, ADR-059 — `start`'s "Add project to portfolio" route, talent-only).
521
+ // Talent-portfolio endpoints (`start`'s "Add project to portfolio" route, talent-only).
473
522
  function getPortfoliosDeclareEndpoint(env = process.env) {
474
523
  const explicit = readEnv(env, 'PORTFOLIOS_DECLARE_ENDPOINT');
475
524
  if (explicit && explicit.trim()) return explicit.trim();
@@ -512,7 +561,7 @@ function getAgentsDraftFieldsEndpoint(env = process.env) {
512
561
  return deriveFromIngest(env, '../works/talents/me/agents/draft-fields');
513
562
  }
514
563
 
515
- // Agent<->experience relate endpoint (ADR-041, `start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
564
+ // Agent<->experience relate endpoint (`start`'s "Add agent" relate step / MCP `add_agent.relatedPortfolios`).
516
565
  function getAgentPortfoliosEndpoint(agentId, env = process.env) {
517
566
  // Null on missing/non-scalar id: no bogus URL, and keeps config.test.js's `fn(env)`
518
567
  // getter sweep honest when the env object lands in `agentId`.
@@ -520,7 +569,7 @@ function getAgentPortfoliosEndpoint(agentId, env = process.env) {
520
569
  return deriveFromHub(env, `works/talent/me/agents/${encodeURIComponent(agentId)}/portfolios`);
521
570
  }
522
571
 
523
- // Onboarding endpoints (evaluation-mcp, ADR-011..015/024 — the register flow).
572
+ // Onboarding endpoints (the register flow).
524
573
  function getHubBase(env = process.env) {
525
574
  const explicit = readEnv(env, 'HUB_BASE');
526
575
  if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
@@ -551,7 +600,7 @@ function getImportProfileEndpoint(env = process.env) {
551
600
  return deriveFromHub(env, 'works/talent/me/import-profile');
552
601
  }
553
602
 
554
- // MCP sign-up (ticket 39540): public UNREGISTERED + claim code, its import status, and the one-time web login.
603
+ // MCP sign-up: public UNREGISTERED + claim code, its import status, and the one-time web login.
555
604
  function getMcpSignupEndpoint(env = process.env) {
556
605
  return deriveFromHub(env, 'works-ai/talents/mcp-signup');
557
606
  }
@@ -564,11 +613,20 @@ function getOnboardingEventsEndpoint(env = process.env) {
564
613
  return deriveFromHub(env, 'works/talent/me/onboarding-events');
565
614
  }
566
615
 
616
+ const AMPLITUDE_HTTP_API = 'https://api.eu.amplitude.com/2/httpapi';
617
+
618
+ function getAmplitudeIngestion(env = process.env) {
619
+ const stamped = readPackageJson().amplitude;
620
+ if (!stamped || typeof stamped.apiKey !== 'string' || !stamped.apiKey.trim()) return null;
621
+ if (stamped.env !== getEnvProfileName(env)) return null;
622
+ return { endpoint: AMPLITUDE_HTTP_API, apiKey: stamped.apiKey.trim() };
623
+ }
624
+
567
625
  function getOneTimeTokenEndpoint(env = process.env) {
568
626
  return deriveFromHub(env, 'auth/one-time-token/generate');
569
627
  }
570
628
 
571
- // The ONE place the web route that redeems a one-time token is spelled (the route itself is a later works-frontend ticket).
629
+ // The ONE place the web route that redeems a one-time token is spelled.
572
630
  function getOneTimeLoginUrl(token, env = process.env) {
573
631
  if (typeof token !== 'string' || !token) return null;
574
632
  const profile = getTalentProfileUrl(env);
@@ -675,7 +733,7 @@ function getOnboardingStatusEndpoint(env = process.env) {
675
733
  return base ? `${base}/interviews/onboarding/status` : null;
676
734
  }
677
735
 
678
- // Talent profile web URL (evaluation-mcp, ADR-011 — the step-5 skip fallback).
736
+ // Talent profile web URL (the step-5 skip fallback).
679
737
  function getTalentProfileUrl(env = process.env) {
680
738
  const explicit = readEnv(env, 'PROFILE_URL');
681
739
  if (explicit && explicit.trim()) return explicit.trim().replace(/\/+$/, '');
@@ -689,14 +747,14 @@ function getTalentProfileUrl(env = process.env) {
689
747
  // user overrides of these, never the baked profile itself.
690
748
  const PROFILE_ENDPOINT_KEYS = Object.keys(ENV_PROFILES[DEFAULT_ENV_PROFILE]);
691
749
 
692
- // Boot migration (ADR-065 revised). The CLI used to SEED config.json with the
750
+ // Boot migration. The CLI used to SEED config.json with the
693
751
  // baked bases on first run; the getters now fall back to getEnvProfile() directly,
694
752
  // 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
753
+ // a config.json seeded by a previous flavor (e.g. the beta when it baked staging)
754
+ // survives into a new flavor (the beta now baking dev) and pins the old endpoints
697
755
  // forever, since config.json wins over the baked profile. This sweep removes any
698
756
  // 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
757
+ // (current or retired) OTHER than the baked one — i.e. a value that could only have gotten there by the
700
758
  // old seeding of a different flavor. A value that matches no known profile is a
701
759
  // genuine user override (set via `shakers config set`) and is left untouched. We
702
760
  // never touch env, auth-session.json or consent.json, and never remove the dir.
@@ -708,7 +766,7 @@ function sanitizeStaleBakedEndpoints(env = process.env) {
708
766
  const current = config[key];
709
767
  if (typeof current !== 'string' || !current.trim()) continue;
710
768
  const normalized = current.trim().replace(/\/+$/, '');
711
- const matchesForeignProfile = Object.entries(ENV_PROFILES).some(
769
+ const matchesForeignProfile = Object.entries({ ...RETIRED_ENV_PROFILES, ...ENV_PROFILES }).some(
712
770
  ([name, profile]) =>
713
771
  name !== bakedName && String(profile[key]).replace(/\/+$/, '') === normalized,
714
772
  );
@@ -735,12 +793,12 @@ function getSuperadminSessionEndpoint(env = process.env) {
735
793
  return deriveFromIngest(env, 'superadmin/session');
736
794
  }
737
795
 
738
- // Superadmin READ-ONLY certification inspection endpoint (ADR-025, NON-PROD only) — a sibling of the ingest endpoint.
796
+ // Superadmin READ-ONLY certification inspection endpoint (NON-PROD only) — a sibling of the ingest endpoint.
739
797
  function getInspectCertificationsEndpoint(env = process.env) {
740
798
  return deriveFromIngest(env, 'superadmin/inspect-certifications');
741
799
  }
742
800
 
743
- // ADR-027 superadmin SESSION persistence.
801
+ // Superadmin SESSION persistence.
744
802
  function loadSuperadminSession(env = process.env) {
745
803
  const config = loadConfigFile(env);
746
804
  const s = config && typeof config === 'object' ? config.superadminSession : null;
@@ -774,6 +832,7 @@ module.exports = {
774
832
  getAgentEvaluationEndpoint,
775
833
  getLoginEndpoint,
776
834
  getAuthTokenEndpoint,
835
+ getSignOutEndpoint,
777
836
  getCompleteRegistrationEndpoint,
778
837
  getDeviceAuthorizeEndpoint,
779
838
  getDeviceTokenEndpoint,
@@ -807,19 +866,19 @@ module.exports = {
807
866
  getUsageDiscoveredInventoryEndpoint,
808
867
  getUsageReportEndpoint,
809
868
  getAiProfileEndpoint,
810
- // Evidence emission (talents-ai-score Phase 2, evidence-based evaluation).
869
+ // Evidence emission.
811
870
  getEvidenceCliScanEndpoint,
812
- // Talent-portfolio endpoints (ADR-059).
871
+ // Talent-portfolio endpoints.
813
872
  getPortfoliosDeclareEndpoint,
814
873
  getPortfoliosListEndpoint,
815
874
  getPortfolioDraftDescriptionEndpoint,
816
875
  getPortfolioUpdateEndpoint,
817
- // Talent-agent endpoints (talents-ai-score Phase 2 — "Add agent to profile").
876
+ // Talent-agent endpoints ("Add agent to profile").
818
877
  getAgentsDeclareEndpoint,
819
878
  getAgentsListEndpoint,
820
879
  getAgentsDraftFieldsEndpoint,
821
880
  getAgentPortfoliosEndpoint,
822
- // Onboarding / register flow (evaluation-mcp, ADR-011..015/024).
881
+ // Onboarding / register flow.
823
882
  getCertsBase,
824
883
  getHubBase,
825
884
  sanitizeStaleBakedEndpoints,
@@ -828,6 +887,8 @@ module.exports = {
828
887
  getMcpSignupEndpoint,
829
888
  getMyImportStatusEndpoint,
830
889
  getOnboardingEventsEndpoint,
890
+ getAmplitudeIngestion,
891
+ AMPLITUDE_HTTP_API,
831
892
  getOneTimeTokenEndpoint,
832
893
  getOneTimeLoginUrl,
833
894
  getProfessionalDetailsEndpoint,
@@ -849,7 +910,7 @@ module.exports = {
849
910
  getEmailVerificationVerifyUrl,
850
911
  getSuperadminSessionEndpoint,
851
912
  getInspectCertificationsEndpoint,
852
- // ADR-027 superadmin session persistence.
913
+ // Superadmin session persistence.
853
914
  loadSuperadminSession,
854
915
  saveSuperadminSession,
855
916
  clearSuperadminSession,
@@ -863,15 +924,18 @@ module.exports = {
863
924
  setIngestEndpoint,
864
925
  hasRetiredFallbackConfig,
865
926
  resolveIngestEndpoint,
866
- // Distribution profile (ADR-058 fase 1).
927
+ // Distribution profile.
867
928
  VALID_PROFILES,
868
929
  getProfile,
869
930
  isTalentProfile,
870
931
  // UI language preference (env SHAKERS_CLI_LANG > config.json `lang`).
871
932
  VALID_UI_LANGS,
872
933
  getUiLang,
873
- // Baked backend target flavor (staging | dev | local).
934
+ // Baked backend target flavor (prod | dev | local).
874
935
  ENV_PROFILES,
875
936
  getEnvProfileName,
876
937
  getEnvProfile,
938
+ envProfileForPackageName,
939
+ envProfileProblem,
940
+ UnknownEnvProfileError,
877
941
  };
@@ -8,7 +8,7 @@ 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
 
@@ -45,7 +45,7 @@ async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
45
45
  });
46
46
  }
47
47
 
48
- // Runs the short consent-to-persist prompt + (if accepted) email collection + (skill-code-certification / ADR-006) EMAIL-OWNERSHIP VERIFICATION.
48
+ // Runs the short consent-to-persist prompt + (if accepted) email collection + EMAIL-OWNERSHIP VERIFICATION.
49
49
  async function runConsentPrompt({
50
50
  ask,
51
51
  catalog,
@@ -57,7 +57,7 @@ async function runConsentPrompt({
57
57
  profile = 'talent',
58
58
  }) {
59
59
  const c = catalog.consent;
60
- // Presentation only (talents-ai-score): framed by src/legal-notice.js on a
60
+ // Presentation only: framed by src/legal-notice.js on a
61
61
  // real TTY, byte-identical (no heading, no rule) otherwise.
62
62
  process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
63
63
 
@@ -100,7 +100,7 @@ async function runConsentPrompt({
100
100
 
101
101
  process.stdout.write(` ${c.savedUnderEmail(email)}\n`);
102
102
 
103
- // Prove email ownership BEFORE persisting (ADR-006) — every profile, external included.
103
+ // Prove email ownership BEFORE persisting — every profile, external included.
104
104
  const verification = await verifyEmail({ email, ask, catalog, env });
105
105
  if (verification && verification.verified) {
106
106
  recordConsent('granted', email, { verified: true });
@@ -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))