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
@@ -3,7 +3,7 @@
3
3
  const { palette, styleQuestion } = require('./ansi');
4
4
  const { BRAND_ANSI } = require('./brand-ansi');
5
5
 
6
- // Zero-dependency interactive multi-select (skill-code-certification, issue 011).
6
+ // Zero-dependency interactive multi-select.
7
7
 
8
8
  // COLOUR goes through the one gate (src/ansi.js).
9
9
  const ANSI = palette({
@@ -100,7 +100,7 @@ class InterviewClient {
100
100
 
101
101
  // The end of the interview is a protocol message, not a sentence: matching the
102
102
  // goodbye text never fired (certs sends no closingMessage), so the CLI kept
103
- // asking for answers until its turn-timeout (new-works, 2026-09-25).
103
+ // asking for answers until its turn-timeout.
104
104
  if (RoomEvent && RoomEvent.DataReceived && typeof room.on === 'function') {
105
105
  room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {
106
106
  if (topic !== PROTOCOL_TOPIC) return;
@@ -216,8 +216,13 @@ class InterviewClient {
216
216
  }
217
217
  }
218
218
 
219
+ function createInterviewClient(opts) {
220
+ return new InterviewClient(opts);
221
+ }
222
+
219
223
  module.exports = {
220
224
  InterviewClient,
225
+ createInterviewClient,
221
226
  LiveKitNotInstalledError,
222
227
  loadLiveKitSdk,
223
228
  CHAT_TOPIC,
@@ -4,7 +4,7 @@
4
4
 
5
5
  // Upper case: certs validates the role against its AGENT/USER enum and 400s the
6
6
  // whole PATCH otherwise, so every terminal onboarding lost its transcript
7
- // ("no se pudo guardar la transcripcion (http-400)", new-works 2026-09-25).
7
+ // ("no se pudo guardar la transcripcion (http-400)").
8
8
  const ROLE_AGENT = 'AGENT';
9
9
  const ROLE_USER = 'USER';
10
10
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // Shared PRESENTATION for the CLI's legal/consent disclosures (talents-ai-score).
3
+ // Shared PRESENTATION for the CLI's legal/consent disclosures.
4
4
 
5
5
  const { palette, colorEnabled } = require('./ansi');
6
6
  const { BRAND_ANSI } = require('./brand-ansi');
package/src/maturity.js CHANGED
@@ -12,7 +12,7 @@ const LEVELS = [
12
12
  { level: 4, key: 'orchestrator', name: 'Orquestador', emoji: '●' },
13
13
  ];
14
14
 
15
- // DECISION (talents-ai-score, signal expansion): the level definitions (LEVELS, HANDOFF §4) have NOT been touched.
15
+ // DECISION: the level definitions (LEVELS) have NOT been touched.
16
16
  function depthTotals(tools) {
17
17
  let instructions = 0; // project instructions/rules files
18
18
  let mcp = 0; // configured MCP servers
@@ -30,7 +30,7 @@ function depthTotals(tools) {
30
30
  return { instructions, mcp, custom, hooks };
31
31
  }
32
32
 
33
- // Score model 0-100 for the visual meter — RECALIBRATED (ADR-008, skill-code-certification).
33
+ // Score model 0-100 for the visual meter — RECALIBRATED.
34
34
  const SCORE_MODEL = [
35
35
  { key: 'breadth', weight: 12, full: 3 }, // number of AI tools detected
36
36
  { key: 'context', weight: 16, full: 2 }, // instructions + config files
@@ -61,7 +61,7 @@ function classify(report) {
61
61
  ? report.agentCounts.agents
62
62
  : 0;
63
63
 
64
- // Score scope reverted to PROJECT ∪ HOME (skill-code-certification / ADR-010, which reverts ADR-009).
64
+ // Score scope reverted to PROJECT ∪ HOME.
65
65
  const score = computeScore({
66
66
  breadth,
67
67
  context: d.instructions, // depthTotals already folds `config` into instructions
@@ -72,7 +72,7 @@ function classify(report) {
72
72
  multiAgent: agentCount,
73
73
  });
74
74
 
75
- // Band 0-4 derived from the tier engine (issue 019, single source of truth).
75
+ // Band 0-4 derived from the tier engine (single source of truth).
76
76
  const tierResult = computeTierResult(report);
77
77
  const level = tierResult.band;
78
78
  const meta = LEVELS[level];
@@ -87,11 +87,11 @@ function classify(report) {
87
87
  depth: d,
88
88
  hasAgentic,
89
89
  next: nextStep(level, { breadth, ...d, hasAgentic }),
90
- // Tier (T0-T7, issue 019): the fine-grained axis the band is derived from.
90
+ // Tier (T0-T7): the fine-grained axis the band is derived from.
91
91
  tier: tierResult.tier,
92
92
  tierKey: tierResult.tierKey,
93
93
  tierName: tierResult.tierName,
94
- // Setup Level (ADR-016): the 3-value rollup shown to the talent, derived
94
+ // Setup Level: the 3-value rollup shown to the talent, derived
95
95
  // from the tier. `{key, code, rank, emoji}`; label localized via i18n.
96
96
  setupLevel: tierResult.setupLevel,
97
97
  };
@@ -77,7 +77,7 @@ function buildArgv({ root, repoScope, lang }) {
77
77
 
78
78
  function makeAiUsageTool(deps = {}) {
79
79
  const {
80
- scanUsage = (argv) => require('../bin/ai-usage').scanUsage(argv),
80
+ scanUsage = (argv) => require('./usage-scan').scanUsage(argv),
81
81
  autoShare = require('./share').autoShare,
82
82
  recordConsent = require('./share').recordConsent,
83
83
  isValidEmail = require('./share').isValidEmail,
@@ -244,7 +244,7 @@ const LINKEDIN_IN = '<svg class="g" viewBox="0 0 24 24" aria-hidden="true">'
244
244
  // Each window offers these methods, in this order. The sign-up window has no LinkedIn: only Google carries the claim code there.
245
245
  const WINDOW_METHODS = { login: ['google', 'linkedin', 'email'], register: ['google', 'email'] };
246
246
 
247
- // The method-choice screen (talents-ai-score) — the FIRST screen of the loopback form when more than one method is offered for this flow.
247
+ // The method-choice screen — the FIRST screen of the loopback form when more than one method is offered for this flow.
248
248
  function methodChoiceHtml(mode, lang, methods) {
249
249
  const c = copy(lang);
250
250
  const title = mode === 'register' ? c.chooseTitleRegister : c.chooseTitleLogin;
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getHomeDir } = require('./env-paths');
6
6
 
7
- // KEYWORD -> { SERVICE LABEL, CATEGORY } (issue 110).
7
+ // KEYWORD -> { SERVICE LABEL, CATEGORY }.
8
8
  const SERVICE_CATALOG = [
9
9
  // --- data ---
10
10
  { kw: 'postgresql', label: 'Postgres', category: 'data' },
@@ -87,7 +87,7 @@ function serviceForMcpServerName(name) {
87
87
  return hit && hit.label ? hit.label : null;
88
88
  }
89
89
 
90
- // THE single inventory of MCP config locations (issue 112).
90
+ // THE single inventory of MCP config locations.
91
91
  const MCP_CONFIG_LOCATIONS = [
92
92
  // --- Claude Code (already read before 112) ---
93
93
  { tool: 'claude-code', scope: 'project', keys: ['mcpServers'], confidence: 'high', file: (root) => path.join(root, '.mcp.json') },
@@ -181,7 +181,7 @@ function detectMcpServers(root) {
181
181
  // 'project' wins when a server is declared in both: the most specific
182
182
  // scope is the honest label for "this repo uses it".
183
183
  scope: entry.scopes.has('project') ? 'project' : 'home',
184
- // Issue 110: the PRODUCT, derived from the name only. `null` when the
184
+ // The PRODUCT, derived from the name only. `null` when the
185
185
  // name carries none — a state, not a hole.
186
186
  service: serviceForMcpServerName(name),
187
187
  };
@@ -24,6 +24,7 @@ function makeDeviceLoginTools(deps = {}) {
24
24
  pollDeviceToken = require('./device-login').pollDeviceToken,
25
25
  finalizeDeviceSession = require('./device-login').finalizeDeviceSession,
26
26
  appendProviderParam = require('./device-login').appendProviderParam,
27
+ deviceSessionRecord = require('./device-login').deviceSessionRecord,
27
28
  savePendingDevice = require('./device-auth-store').savePendingDevice,
28
29
  loadPendingDevice = require('./device-auth-store').loadPendingDevice,
29
30
  clearPendingDevice = require('./device-auth-store').clearPendingDevice,
@@ -144,7 +145,7 @@ function makeDeviceLoginTools(deps = {}) {
144
145
  return { ok: false, status: 'error', reason: fin.reason };
145
146
  }
146
147
  try {
147
- saveAuthSession({ accessToken: fin.token, hubAccessToken: fin.token, email: res.email || pending.email || null });
148
+ saveAuthSession(deviceSessionRecord({ token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, email: res.email || pending.email || null }));
148
149
  } catch {
149
150
  clearPendingDevice();
150
151
  return { ok: false, status: 'error', reason: 'session-persist-failed' };
@@ -3,8 +3,7 @@
3
3
  // MCP: list the Positions/Projects available to the talent (Find Positions),
4
4
  // paginated + filtered server-side. Same params as the CLI so an agent can page.
5
5
 
6
- const { TABS, ATTENDANCE, DEFAULT_LIMIT } = require('./find-projects-client');
7
- const { splitRecommended } = require('./find-projects-flow');
6
+ const { TABS, ATTENDANCE, DEFAULT_LIMIT, splitRecommended } = require('./find-projects-client');
8
7
 
9
8
  const FIND_PROJECTS_SCHEMA = {
10
9
  type: 'object',
@@ -76,6 +76,7 @@ function makeOnboardingTools(deps = {}) {
76
76
  requestDiscoveredInventory = require('./inventory-client').requestDiscoveredInventory,
77
77
  requestDiscoveredInventoryWithRefresh = require('./inventory-client').requestDiscoveredInventoryWithRefresh,
78
78
  ensureFreshSession = require('./session-refresh').ensureFreshSession,
79
+ endSession = require('./session-refresh').endSession,
79
80
  requestDeclareSkill = require('./skills-client').requestDeclareSkill,
80
81
  requestDeclareAgent = require('./agents-client').requestDeclareAgent,
81
82
  requestListAgents = require('./agents-client').requestListAgents,
@@ -86,7 +87,7 @@ function makeOnboardingTools(deps = {}) {
86
87
  getRemoteUrl = require('./portfolio-git-info').getRemoteUrl,
87
88
  getCommitDateRange = require('./portfolio-git-info').getCommitDateRange,
88
89
  consentCopy = () => require('./signup-copy').signupCopy(require('./signup-language').signupLanguage(require('./i18n').detectFlowLang())).consent,
89
- normalizeDomain = require('./start-add-portfolio').normalizeDomain,
90
+ normalizeDomain = require('./portfolio-client').normalizeDomain,
90
91
  isValidEmail = require('./share').isValidEmail,
91
92
  normalizeEmail = require('./share').normalizeEmail,
92
93
  } = deps;
@@ -115,8 +116,7 @@ function makeOnboardingTools(deps = {}) {
115
116
  };
116
117
 
117
118
  async function logout() {
118
- const wasLoggedIn = loadAuthSession() !== null;
119
- try { clearAuthSession(); } catch { /* idempotent — nothing to clear */ }
119
+ const wasLoggedIn = await endSession(process.env, { loadAuthSession, clearAuthSession });
120
120
  return { ok: true, wasLoggedIn };
121
121
  }
122
122
 
@@ -251,7 +251,7 @@ function makeOnboardingTools(deps = {}) {
251
251
  const relatedInput = Array.isArray(args.relatedPortfolios) ? args.relatedPortfolios : [];
252
252
  if (relatedInput.length === 0 || !agentId) return { ...base, portfolios };
253
253
 
254
- // ADR-041 replace-set: validate every portfolioId against the talent's own
254
+ // Replace-set: validate every portfolioId against the talent's own
255
255
  // list, then PATCH the whole set in ONE call.
256
256
  const items = [];
257
257
  const relateFailures = [];
@@ -37,10 +37,10 @@ const PROMPTS = [
37
37
  },
38
38
  {
39
39
  name: 'certify',
40
- description: 'See which role dimensions you can certify and start a certification.',
40
+ description: 'See which role dimensions you can certify; the certification itself is taken on the Shakers web.',
41
41
  tools: ['list_certifiable_dimensions'],
42
42
  buildMessages: () => userText(
43
- 'Help me certify on Shakers: call `list_certifiable_dimensions`, present the dimensions I can certify by name, and guide me through certifying the one I pick.',
43
+ 'Help me certify on Shakers: call `list_certifiable_dimensions` and present the dimensions I can certify by name, and when I can retry the others. The certification interview is taken on the Shakers web, not in this chat: offer to open it with `open_web`. Never show internal ids/codes.',
44
44
  ),
45
45
  },
46
46
  {
@@ -204,6 +204,7 @@ function makeRegisterTools(deps = {}) {
204
204
 
205
205
  // Creates the interview, joins its room and reads the greeting; `room` lets a timed-out caller close what opened late.
206
206
  async function openRoom(args, room) {
207
+ if (typeof resolved.makeInterviewClient !== 'function') return onWeb('connect', 'livekit-not-installed');
207
208
  const start = await flow.createAndStartLivekitOnboarding(resolved, {
208
209
  candidateId: args.candidateId,
209
210
  language: args.language || lang,
package/src/mcp-server.js CHANGED
@@ -34,7 +34,7 @@ function errorResponse(id, code, message, data) {
34
34
  return { jsonrpc: JSONRPC, id: id === undefined ? null : id, error };
35
35
  }
36
36
 
37
- function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
37
+ function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVersion, beforeCall = null, afterCall = null, onInitialize = null, instructions = null, elicitationTimeoutMs = ELICITATION_TIMEOUT_MS } = {}) {
38
38
  const registry = new Map();
39
39
  for (const t of tools) {
40
40
  if (!t || !t.name || typeof t.handler !== 'function') {
@@ -185,6 +185,10 @@ function createMcpServer({ tools = [], prompts = [], serverInfo = {}, protocolVe
185
185
  content: [{ type: 'text', text: `${name} tool error: ${e && e.message ? e.message : String(e)}` }],
186
186
  isError: true,
187
187
  };
188
+ } finally {
189
+ if (typeof afterCall === 'function') {
190
+ try { afterCall(name); } catch { /* hook must never break a tool call */ }
191
+ }
188
192
  }
189
193
  }
190
194
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  const { postJsonWithTimeout, postMultipartWithTimeout } = require('./backend-request');
4
4
 
5
- // Hub calls of the MCP sign-up (ticket 39540): public sign-up + claim code, the signed-in talent's import status, and the one-time web login token.
5
+ // Hub calls of the MCP sign-up: public sign-up + claim code, the signed-in talent's import status, and the one-time web login token.
6
6
 
7
7
  const SIGNUP_TIMEOUT_MS = 60000;
8
8
  const DEFAULT_TIMEOUT_MS = 15000;
9
9
 
10
- // Hub's coded errors on the sign-up, verified against the works-ai exception filter (hub MR !2198); none says whether an account exists.
10
+ // Hub's coded errors on the sign-up; none says whether an account exists.
11
11
  const CODE_TO_REASON = {
12
12
  MISSING_SOURCE: 'missing-source',
13
13
  INVALID_CV: 'invalid-cv',
@@ -13,7 +13,7 @@ const { askAfterTexts, askConsent, optionFor, queueNotice, takeNotices } = requi
13
13
  const { expandHome, tildify } = require('./env-paths');
14
14
  const { NOOP_FUNNEL, weeklyHours, rateTypes, workSituationValue } = require('./onboarding-funnel');
15
15
 
16
- // MCP sign-up (ticket 39540): hub imports into an UNREGISTERED profile that the local window claims with a code kept only in memory.
16
+ // MCP sign-up: hub imports into an UNREGISTERED profile that the local window claims with a code kept only in memory.
17
17
 
18
18
  const SIGNUP_WINDOW_TIMEOUT_MS = 15 * 60 * 1000;
19
19
  // The browser opens this long after the tool answers, so the AI's reply is on screen first.
@@ -302,6 +302,7 @@ function makeSignupTools(deps = {}) {
302
302
  pollDeviceToken = require('./device-login').pollDeviceToken,
303
303
  finalizeDeviceSession = require('./device-login').finalizeDeviceSession,
304
304
  appendProviderParam = require('./device-login').appendProviderParam,
305
+ deviceSessionRecord = require('./device-login').deviceSessionRecord,
305
306
  buildRegistrationContext = require('./signup-client').buildRegistrationContext,
306
307
  runLoopbackAuth = require('./mcp-auth-form').runLoopbackAuth,
307
308
  checkOnboardingCompleted = require('./onboarding-availability').checkOnboardingCompleted,
@@ -347,6 +348,10 @@ function makeSignupTools(deps = {}) {
347
348
  funnel.completed('account_creation', { auth_method: next.via, is_returning_account: next.state === 'existing' });
348
349
  funnel.identified();
349
350
  }
351
+ if (next.state === 'failed' && before !== 'failed') {
352
+ funnel.errored('account_creation', next.reason);
353
+ funnel.flush();
354
+ }
350
355
  if (state.wake) state.wake();
351
356
  }
352
357
 
@@ -385,7 +390,7 @@ function makeSignupTools(deps = {}) {
385
390
  }
386
391
  const email = res.email || emailFromHubToken(fin.token);
387
392
  try {
388
- resolved.saveAuthSession({ accessToken: fin.token, hubAccessToken: fin.token, email });
393
+ resolved.saveAuthSession(deviceSessionRecord({ token: fin.token, sessionToken: res.accessToken, sessionExpiresIn: res.expiresIn, email }));
389
394
  } catch {
390
395
  setAccount({ state: 'failed', reason: 'session-persist-failed' });
391
396
  return;
@@ -576,6 +581,7 @@ function makeSignupTools(deps = {}) {
576
581
  return { ok: false, reason: 'draft-sources-required', message: "The draft needs sources: 'cv' if you read their CV, 'linkedin' only if you have their LinkedIn URL. Then call signup_draft again." };
577
582
  }
578
583
  if (!state.firstDraft) state.firstDraft = draft;
584
+ trackDraftShown(draft);
579
585
  const asked = await askAfterTexts(ctx, {
580
586
  tool: 'signup_draft',
581
587
  texts: [renderDraft(lang(), draft), legalCopy(lang()).signupNotice(sources), knownAccount() ? c.windowNoticeSignIn : c.windowNotice],
@@ -951,6 +957,20 @@ function makeSignupTools(deps = {}) {
951
957
  return c.ratesSaved(shown, !(state.draft && state.draft.ratesFromTalent));
952
958
  }
953
959
 
960
+ function trackDraftShown(draft) {
961
+ funnel.viewed('work_situation', { is_prefilled: true });
962
+ funnel.viewed('availability');
963
+ funnel.viewed('rates', { suggested_rate_shown: draft.ratesFromTalent !== true });
964
+ funnel.viewed('languages');
965
+ funnel.viewed('location');
966
+ funnel.viewed('profile_summary');
967
+ }
968
+
969
+ function draftFieldsEdited() {
970
+ if (!state.firstDraft || !state.draft) return undefined;
971
+ return DRAFT_FIELDS.filter((key) => String(state.firstDraft[key] ?? '') !== String(state.draft[key] ?? '')).length;
972
+ }
973
+
954
974
  function wasEdited(step) {
955
975
  if (!state.firstDraft || !state.draft) return undefined;
956
976
  return STEP_DRAFT_FIELDS[step].some((key) => String(state.firstDraft[key] ?? '') !== String(state.draft[key] ?? ''));
@@ -982,6 +1002,10 @@ function makeSignupTools(deps = {}) {
982
1002
  const codes = details.languages.map((row) => String(row.language || '').trim().toLowerCase()).filter(Boolean);
983
1003
  step('languages', { languages_count: codes.length, languages: codes });
984
1004
  }
1005
+ const sections = Object.entries(results).filter(([name]) => name !== 'phone');
1006
+ if (sections.length && sections.every(([, r]) => r.ok === true)) {
1007
+ funnel.completed('profile_summary', { fields_edited_count: draftFieldsEdited() });
1008
+ }
985
1009
  funnel.flush();
986
1010
  }
987
1011
 
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getHomeDir } = require('./env-paths');
6
6
 
7
- // Deterministic (no-LLM) memory STRUCTURE detector (talents-ai-score, issue 016 / ADR-013-014).
7
+ // Deterministic (no-LLM) memory STRUCTURE detector.
8
8
 
9
9
  const MAX_FILE_BYTES = 2_000_000; // safety cap: never read a pathologically huge file
10
10
  const MAX_IMPORT_DEPTH = 5; // mirrors Claude Code's own documented import-hop limit
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // What happened with each model call, written down (talents-ai-score, issue 109).
3
+ // What happened with each model call, written down.
4
4
 
5
5
  // The three keys.
6
6
  const MODEL_CALL = Object.freeze({
@@ -163,7 +163,7 @@ function callPartial(report, key) {
163
163
  return !!c && c.ok === true && Array.isArray(c.omitted) && c.omitted.length > 0;
164
164
  }
165
165
 
166
- // THE CALL LOST TIME TO A DEADLINE — and this is NOT `reason === TIMEOUT` (issue 116, found by measuring, not by reading).
166
+ // THE CALL LOST TIME TO A DEADLINE — and this is NOT `reason === TIMEOUT` (found by measuring, not by reading).
167
167
  function callTimedOut(report, key) {
168
168
  const c = modelCall(report, key);
169
169
  if (!c || c.ok !== false) return false;
@@ -168,7 +168,7 @@ function normalizeAmount(value) {
168
168
  return cents / 100;
169
169
  }
170
170
 
171
- // Pricing input (ADR-024).
171
+ // Pricing input.
172
172
  function validatePricing(input = {}) {
173
173
  const fullSelected = input.fullTimeSelected === true;
174
174
  const partSelected = input.partTimeSelected === true;
@@ -212,7 +212,7 @@ function buildPricingUpsert({ fullSelected, fullAmount, fullCurrency, partSelect
212
212
  };
213
213
  }
214
214
 
215
- // Availability + location (ADR-028).
215
+ // Availability + location.
216
216
  function validateAvailability(input = {}) {
217
217
  const body = { confirmed: true };
218
218
  if (typeof input.available === 'boolean') body.available = input.available;
@@ -299,7 +299,6 @@ function makeDeps(overrides = {}) {
299
299
  runMainRoleStep: require('./roles-flow').runOnboardingMainRoleStep,
300
300
  makeRolesDeps: require('./roles-flow').makeRolesDeps,
301
301
  requestDiscoveredInventory: inventory.requestDiscoveredInventory,
302
- makeInterviewClient: (opts) => new (require('./interview-client').InterviewClient)(opts),
303
302
  // Used by reattachConsentIdentity (A) to re-anchor the ingest identity.
304
303
  recordConsent: require('./share-consent-state').recordConsent,
305
304
  ...overrides,
@@ -371,7 +370,7 @@ async function establishSessionFromCredentials(deps, { email, password } = {}) {
371
370
 
372
371
  // Register a talent via a social provider (device flow, RFC 8628); a live session results either way.
373
372
  async function registerWithSocialAccount(deps, { provider = 'google', freelanceType, language } = {}, { openBrowser, onAuthUrl } = {}) {
374
- const { appendProviderParam } = require('./device-login');
373
+ const { appendProviderParam, deviceSessionRecord } = require('./device-login');
375
374
  const authorizeEndpoint = deps.getDeviceAuthorizeEndpoint();
376
375
  const tokenEndpoint = deps.getDeviceTokenEndpoint();
377
376
  const authTokenEndpoint = typeof deps.getAuthTokenEndpoint === 'function' ? deps.getAuthTokenEndpoint() : null;
@@ -397,13 +396,8 @@ async function registerWithSocialAccount(deps, { provider = 'google', freelanceT
397
396
  );
398
397
  if (!result.ok) return { ok: false, reason: result.reason || 'register-failed' };
399
398
 
400
- // Persist the API JWT the exchange minted (no cookie), like the retired Google flow did.
401
399
  try {
402
- deps.saveAuthSession({
403
- accessToken: result.token,
404
- hubAccessToken: result.token,
405
- email: result.email || null,
406
- });
400
+ deps.saveAuthSession(deviceSessionRecord(result));
407
401
  } catch {
408
402
  return { ok: false, reason: 'session-persist-failed' };
409
403
  }
@@ -698,6 +692,12 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
698
692
  funnel.completed('start');
699
693
  io.section(c.signupTitle);
700
694
  funnel.viewed('account_creation');
695
+ funnel.flush();
696
+ const accountFailed = (reason) => {
697
+ funnel.errored('account_creation', reason);
698
+ funnel.flush();
699
+ return { ok: false, reason, step: 'signup' };
700
+ };
701
701
 
702
702
  if (method === 'google' || method === 'linkedin') {
703
703
  // The provider supplies name/email; freelanceType is derived later from work situation.
@@ -709,7 +709,7 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
709
709
  );
710
710
  if (!reg.ok) {
711
711
  io.error(c.signupError);
712
- return { ok: false, reason: reg.reason, step: 'signup' };
712
+ return accountFailed(reg.reason);
713
713
  }
714
714
  funnel.completed('account_creation', { auth_method: method, is_returning_account: reg.kind === 'account-already-exists' });
715
715
  funnel.identified();
@@ -721,23 +721,23 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
721
721
  notifyOk(io, c.accountReady);
722
722
  } else {
723
723
  const account = await io.askSignUp(c);
724
- if (!account || !account.email) return { ok: false, reason: 'missing-fields', step: 'signup' };
724
+ if (!account || !account.email) return accountFailed('missing-fields');
725
725
  if (!isAcceptablePassword(account.password)) {
726
726
  io.error(c.weakPassword);
727
- return { ok: false, reason: 'weak-password', step: 'signup' };
727
+ return accountFailed('weak-password');
728
728
  }
729
729
 
730
730
  const su = await signUp(deps, { ...account, preferredLanguage: account.preferredLanguage || io.lang });
731
731
  if (!su.ok) {
732
732
  io.error(su.message || c.signupError);
733
- return { ok: false, reason: su.reason, step: 'signup' };
733
+ return accountFailed(su.reason);
734
734
  }
735
735
 
736
736
  if (su.accountExists) {
737
737
  // Existing account: sign them in, then STOP — no wizard re-run.
738
738
  io.warn(c.accountExists);
739
739
  const logged = await io.login(account.email);
740
- if (!logged) return { ok: false, reason: 'login-failed', step: 'signup' };
740
+ if (!logged) return accountFailed('login-failed');
741
741
  funnel.completed('account_creation', { auth_method: 'email', is_returning_account: true });
742
742
  funnel.identified();
743
743
  return { ok: true, alreadyRegistered: true };
@@ -745,7 +745,7 @@ async function runOnboarding(io, deps = makeDeps(), { method = 'email', openBrow
745
745
  const sess = await establishSessionFromCredentials(deps, { email: account.email, password: account.password });
746
746
  if (!sess.ok) {
747
747
  io.error(c.sessionError);
748
- return { ok: false, reason: sess.reason, step: 'signup' };
748
+ return accountFailed(sess.reason);
749
749
  }
750
750
  funnel.completed('account_creation', { auth_method: 'email', is_returning_account: false });
751
751
  funnel.identified();