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
@@ -1,69 +1,12 @@
1
1
  'use strict';
2
2
 
3
- // Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js (structure refactor, issue 020).
3
+ // Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js.
4
4
 
5
- const { isRefusedWalkRoot } = require('./scan-exclusions');
6
- const {
7
- detectRepos,
8
- repoIdentityForCwd,
9
- matchReposByFlag,
10
- scopeFromSelection,
11
- indexOfCurrentRepo,
12
- } = require('./detect-repos');
5
+ const core = require('./repo-scope');
13
6
  const { promptSelect } = require('./prompt-select');
14
7
  const { createStdinAsk } = require('./stdin-ask');
15
8
 
16
- const MAX_MACHINE_WIDE_REPOS = 5;
17
- const SCOPE_MACHINE = 'machine';
18
- const SCOPE_REPO = 'repo';
19
-
20
- function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
21
- try {
22
- return repoIdentityFn(cwd).toplevel || cwd;
23
- } catch {
24
- return cwd;
25
- }
26
- }
27
-
28
- // The real git toplevel of `cwd`, or null when `cwd` is not a repo (or is a refused system root like `/`, the MCP `cwd=/` case).
29
- function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
30
- if (isRefusedWalkRoot(cwd)) return null;
31
- try {
32
- return repoIdentityFn(cwd).toplevel || null;
33
- } catch {
34
- return null;
35
- }
36
- }
37
-
38
- // Pure: builds the machine-wide root set.
39
- function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
40
- const ordered = [];
41
- const seen = new Set();
42
- const push = (t) => {
43
- if (typeof t !== 'string' || !t) return;
44
- if (isRefusedWalkRoot(t)) return;
45
- if (seen.has(t)) return;
46
- seen.add(t);
47
- ordered.push(t);
48
- };
49
- push(cwdRepoTop);
50
- for (const r of detectedRepos || []) push(r && r.toplevel);
51
- const capped = ordered.slice(0, cap);
52
- return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
53
- }
54
-
55
- function cwdOnlyScope(repos, cwdToplevel) {
56
- const idx = indexOfCurrentRepo(repos, cwdToplevel);
57
- if (idx === -1) {
58
- return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
59
- }
60
- const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
61
- return {
62
- selectedCwds,
63
- toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
64
- mode: 'cwd',
65
- };
66
- }
9
+ const { SCOPE_MACHINE, SCOPE_REPO, machineWideToplevels } = core;
67
10
 
68
11
  async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk } = {}) {
69
12
  const c = (catalog && catalog.cli) || {};
@@ -88,75 +31,14 @@ async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk
88
31
  }
89
32
  }
90
33
 
91
- // Resolve which repos feed the AI-usage evaluation.
92
- async function resolveRepoScope({
93
- opts,
94
- cwd,
95
- injectedAsk,
96
- catalog,
97
- detectReposFn = detectRepos,
98
- repoIdentityFn = repoIdentityForCwd,
99
- chooseScope = promptScope,
100
- isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
101
- }) {
102
- const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
103
- const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
104
- const note = (s) => {
105
- if (!opts.json) process.stdout.write(`\n ${s}\n`);
106
- };
107
-
108
- const detect = () => {
109
- try {
110
- return detectReposFn(process.env);
111
- } catch {
112
- return { repos: [], unassignedSessionCount: 0 };
113
- }
114
- };
115
-
116
- // 1. Explicit repo list always wins.
117
- if (opts.repos) {
118
- const { repos, unassignedSessionCount } = detect();
119
- note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
120
- const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
121
- if (unmatched.length > 0) {
122
- process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
123
- }
124
- if (indices.length > 0) {
125
- const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
126
- return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
127
- }
128
- return cwdOnlyScope(repos, cwdToplevel);
129
- }
130
-
131
- const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
132
- const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
133
-
134
- let choice = null;
135
- if (wantsMachine) choice = SCOPE_MACHINE;
136
- else if (wantsRepo) choice = SCOPE_REPO;
137
- else if (isTTY && !injectedAsk && !opts.json) {
138
- choice = await chooseScope({ catalog, stdinIsTTY: true });
139
- }
140
- if (choice === null) choice = SCOPE_REPO;
141
-
142
- if (choice === SCOPE_MACHINE) {
143
- const { repos, unassignedSessionCount } = detect();
144
- note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
145
- const mw = machineWideToplevels(cwdRepoTop, repos);
146
- if (mw.capped) {
147
- process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
148
- }
149
- note(catalog.cli.reposScopeAll);
150
- return {
151
- selectedCwds: null,
152
- toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
153
- mode: 'all',
154
- };
155
- }
156
-
157
- // Current repo.
158
- const { repos } = detect();
159
- return cwdOnlyScope(repos, cwdToplevel);
34
+ function resolveRepoScope(args) {
35
+ return core.resolveRepoScope({
36
+ ...args,
37
+ chooseScope: args.chooseScope || promptScope,
38
+ note: (s) => {
39
+ if (!args.opts.json) process.stdout.write(`\n ${s}\n`);
40
+ },
41
+ });
160
42
  }
161
43
 
162
44
  module.exports = { resolveRepoScope, machineWideToplevels, promptScope };
@@ -1,60 +1,29 @@
1
1
  'use strict';
2
2
 
3
- const { scan, scanForRoots } = require('./scanner');
4
- const { classify } = require('./maturity');
5
3
  const { renderTerminal } = require('./render-terminal');
6
- const { reportGateForIdentity } = require('./report-gating');
7
4
  const { revealText } = require('./typewriter');
8
5
  const { persistFootprint } = require('./report-store');
9
6
  const {
10
7
  loadConsentState,
11
8
  getConsentDecision,
12
- recordConsent,
13
- isValidEmail,
14
- normalizeEmail,
15
9
  autoShare,
16
10
  consentPath,
17
11
  } = require('./share');
18
12
  const { runConsentPrompt } = require('./consent-flow');
19
13
  const { createStdinAsk } = require('./stdin-ask');
20
- const {
21
- parseAgentDescriptions,
22
- parseAgentOrgChartForRoots,
23
- parseAgentDescriptionsForRoots,
24
- parseAgentDefinitionsForRoots,
25
- } = require('./agent-org-chart');
26
- const { collectAgentUsage } = require('./agent-usage');
27
- const { collectSessionSignals } = require('./session-signals');
28
- const {
29
- collectScopedGitActivity,
30
- collectScopedWorkStreams,
31
- collectScopedDetectionEvidence,
32
- } = require('./scope-signals');
33
- const { detectAuthorshipMismatch } = require('./authorship-mismatch');
34
- const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
35
- const {
36
- getSynthesisEndpoint,
37
- getRoadmapEndpoint,
38
- getAgentEvaluationEndpoint,
39
- getAiProfileEndpoint,
40
- } = require('./config');
14
+ const { getAiProfileEndpoint } = require('./config');
41
15
  // "My work with AI" preview, read from the hub ai-profile (same as the front).
42
16
  const { resolveAiProfilePreview } = require('./ai-profile-preview');
43
17
  const { renderAiProfileTerminal } = require('./render-ai-fluency');
44
18
  const { ensureFreshSession } = require('./session-refresh');
45
19
  const { loadAuthSession, sessionStatus } = require('./auth-session-store');
46
20
  const { c: termColors } = require('./terminal-format');
47
- const { withStaticStatus, withSpinner } = require('./terminal-progress');
21
+ const { withSpinner } = require('./terminal-progress');
48
22
  const { computeConsentSkip } = require('./consent-skip');
49
- const { getRoadmapEntry } = require('./roadmap-content');
50
- const {
51
- maybeSynthesizeAgents,
52
- maybeEvaluateAgents,
53
- maybePersonalizeRoadmap,
54
- } = require('./usage-model-calls');
23
+ const { collectIdentityAndUsage, storedAiConsent, reanchorReusedGrant } = require('./usage-scan');
55
24
  const { doBuildNextLevel } = require('./usage-oneshot-commands');
56
25
 
57
- // Automatic, silent PERSISTING once consent is `granted` (ADR-007, gating revised by ADR-011: consent controls persistence only, never what's shown).
26
+ // Automatic, silent PERSISTING once consent is `granted` (consent controls persistence only, never what's shown).
58
27
  async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
59
28
  try {
60
29
  const result = await autoShare(report, maturity, { root, bypassThrottle });
@@ -82,35 +51,17 @@ function sendStatusColor(reason) {
82
51
  return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
83
52
  }
84
53
 
85
- function performScan({ opts, catalog }) {
86
- const root = opts.root;
87
- const report = withStaticStatus(catalog.cli.scanningLabel, () => scan({ root }));
88
- // `let`, not `const`: this is the cwd tier.
89
- const maturity = classify(report);
90
-
91
- if (Array.isArray(report.agents) && report.agents.length > 0) {
92
- report.agentDescriptions = parseAgentDescriptions(root || process.cwd());
93
- }
94
-
95
- return { report, maturity, root, rootDir: root || process.cwd() };
96
- }
97
-
98
54
  // The identity gate + local (no-network, no-LLM) agent-usage signal.
99
55
  function prepareIdentityAndUsage({ report, catalog, quiet = false }) {
100
- const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
101
- const gate = reportGateForIdentity();
102
-
56
+ const identity = collectIdentityAndUsage({ report });
103
57
  // --json owns stdout: one parseable document, and the MCP's JSON-RPC channel.
104
- if (gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
105
-
106
- // Local, no network, no LLM: how often each detected agent was invoked in the local Claude Code history.
107
- if (hasAgents) report.agentUsage = collectAgentUsage(report.agents);
108
-
109
- return { gate, hasAgents };
58
+ if (identity.gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
59
+ return identity;
110
60
  }
111
61
 
112
62
  async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
113
- // CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (talents-ai-score, ADR-051 — explicit product decision that AMENDS ADR-010/011/003 on purpose, not a bug fix).
63
+ // CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (explicit product decision, not a bug fix).
64
+ if (opts.json) return storedAiConsent({ gate });
114
65
  const state = loadConsentState();
115
66
  const decision = getConsentDecision(state);
116
67
  const consentSkip = computeConsentSkip({
@@ -122,143 +73,43 @@ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
122
73
  });
123
74
 
124
75
  let aiConsentGranted;
125
- // A REUSED grant (skip/--json) carries a stale consent email; a fresh prompt re-anchors itself.
76
+ // A REUSED grant (skip) carries a stale consent email; a fresh prompt re-anchors itself.
126
77
  let reusedStoredConsent = false;
127
- if (opts.json) {
128
- // `--json` never had an interactive prompt — its whole point is a single parseable stdout document, and it does not gain one now.
129
- aiConsentGranted = decision === 'granted' && (!state || state.emailVerified !== false);
78
+ if (consentSkip.message) {
79
+ process.stdout.write(`\n ${consentSkip.message}\n`);
80
+ }
81
+ if (consentSkip.skip) {
82
+ // A TERMINAL decision already exists (a prior run's explicit grant or
83
+ // denial) — never re-asked.
84
+ aiConsentGranted = decision === 'granted';
130
85
  reusedStoredConsent = true;
131
86
  } else {
132
- if (consentSkip.message) {
133
- process.stdout.write(`\n ${consentSkip.message}\n`);
134
- }
135
- if (consentSkip.skip) {
136
- // A TERMINAL decision already exists (a prior run's explicit grant or
137
- // denial) — never re-asked, per ADR-007/011.
138
- aiConsentGranted = decision === 'granted';
139
- reusedStoredConsent = true;
140
- } else {
141
- const ask = injectedAsk || createStdinAsk();
142
- let result;
143
- try {
144
- result = await runConsentPrompt({
145
- ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
146
- });
147
- } finally {
148
- if (!injectedAsk) ask.close();
149
- }
150
- aiConsentGranted = result === 'granted';
151
- }
152
- if (!aiConsentGranted) {
153
- // No early return (reverted 8867450's wall): the local report still renders further down, same as ADR-011 always promised.
154
- process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
87
+ const ask = injectedAsk || createStdinAsk();
88
+ let result;
89
+ try {
90
+ result = await runConsentPrompt({
91
+ ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
92
+ });
93
+ } finally {
94
+ if (!injectedAsk) ask.close();
155
95
  }
96
+ aiConsentGranted = result === 'granted';
156
97
  }
157
- // Re-anchor a reused grant to the session's login-proven email (ADR-006): else a stale consent email ingests the report under another account and this session's AI profile never populates.
158
- if (reusedStoredConsent && aiConsentGranted && gate.loggedIn
159
- && isValidEmail(gate.email) && state && state.email !== normalizeEmail(gate.email)) {
160
- recordConsent('granted', gate.email, { verified: true });
98
+ if (!aiConsentGranted) {
99
+ // No early return: the local report still renders further down.
100
+ process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
161
101
  }
102
+ if (reusedStoredConsent && aiConsentGranted) reanchorReusedGrant({ gate, state });
162
103
  return aiConsentGranted;
163
104
  }
164
105
 
165
- function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
166
- withStaticStatus(catalog.cli.aggregatingLabel, () => {
167
- report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
168
- // Display-only transparency warning (never feeds scoring, see authorship-mismatch.js).
169
- report.authorshipMismatch = detectAuthorshipMismatch(scope.toplevels, consentEmail, report.gitActivity);
170
- const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
171
- report.sessions = sessionSignals.sessions;
172
- report.workStreams = collectScopedWorkStreams(scope.toplevels);
173
- report.steering = sessionSignals.steering;
174
- report.decisions = sessionSignals.decisions;
175
- report.detectionEvidence = collectScopedDetectionEvidence(
176
- scope.toplevels,
177
- consentEmail,
178
- report,
179
- );
180
-
181
- const scoped = scanForRoots(agentRoots, { root });
182
- report.tools = scoped.tools;
183
- report.summary = scoped.summary;
184
- report.technologies = scoped.technologies;
185
- report.mcp = scoped.mcp;
186
- report.memory = scoped.memory;
187
- report.automations = scoped.automations;
188
- report.browserTools = scoped.browserTools;
189
- report.agentCounts = scoped.agentCounts;
190
- report.environment = scoped.environment;
191
-
192
- report.agents = parseAgentOrgChartForRoots(agentRoots);
193
- report.agentCounts.agents = report.agents.length;
194
- if (report.agents.length > 0) {
195
- report.agentDescriptions = parseAgentDescriptionsForRoots(agentRoots);
196
- report.agentDefinitions = parseAgentDefinitionsForRoots(agentRoots);
197
- report.agentUsage = collectAgentUsage(report.agents);
198
- }
199
- });
200
- }
201
-
202
- async function runModelCalls({
203
- report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
204
- }) {
205
- // Which of the three will genuinely be ATTEMPTED.
206
- const willAttemptSynthesis = aiConsentGranted && hasAgents && !!getSynthesisEndpoint();
207
- const willAttemptEvaluation = aiConsentGranted && hasAgents && !!getAgentEvaluationEndpoint();
208
- const roadmapEntryForTier = maturity.tierKey ? getRoadmapEntry(maturity.tierKey, lang) : null;
209
- const willAttemptRoadmapPersonalization =
210
- aiConsentGranted
211
- && gate.showRoadmap
212
- && !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
213
- && !!getRoadmapEndpoint();
214
-
215
- // THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL (issue 116).
216
- const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
217
-
218
- // talents-ai-score, ADR-051: `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
219
- let synthesis = null;
220
- let evaluation = null;
221
- let roadmapPersonalization = null;
222
- if (aiConsentGranted && !opts.noAi) {
223
- synthesis = await runPhase(
224
- willAttemptSynthesis,
225
- catalog.cli.synthesizingLabel,
226
- () => maybeSynthesizeAgents(report, rootDir),
227
- );
228
- evaluation = hasAgents
229
- ? await runPhase(
230
- willAttemptEvaluation,
231
- catalog.cli.evaluatingAgentsLabel,
232
- () => maybeEvaluateAgents(report, agentRoots, lang),
233
- )
234
- : null;
235
- // issue 123 / ADR-044: `runPhase` runs `call()` whether or not a spinner is shown, so gating the roadmap CANNOT be done by flipping `willAttempt` — that only hides the spinner.
236
- roadmapPersonalization = gate.showRoadmap
237
- ? await runPhase(
238
- willAttemptRoadmapPersonalization,
239
- catalog.cli.personalizingRoadmapLabel,
240
- () => maybePersonalizeRoadmap(report, maturity, lang),
241
- )
242
- : null;
243
- } else {
244
- const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
245
- attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
246
- attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
247
- attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
248
- }
249
-
250
- if (synthesis) report.agentSynthesis = synthesis;
251
- if (evaluation) report.agentEvaluation = evaluation;
252
- if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
253
- }
254
-
255
106
  async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
256
107
  if (opts.json) {
257
108
  process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
258
109
  return;
259
110
  }
260
111
 
261
- // Issue 103: revealed by SECTIONS instead of landing in one write.
112
+ // Revealed by SECTIONS instead of landing in one write.
262
113
  if (gate.sessionStatus === 'expired') {
263
114
  process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
264
115
  }
@@ -276,7 +127,7 @@ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, cat
276
127
  }
277
128
  }
278
129
 
279
- // "Construir el siguiente nivel ahora" (issue 021): optional, only when
130
+ // "Construir el siguiente nivel ahora": optional, only when
280
131
  // explicitly requested — never part of a normal run otherwise.
281
132
  if (opts.buildNextLevel) {
282
133
  doBuildNextLevel(root, maturity, opts.force, catalog);
@@ -343,11 +194,8 @@ async function maybeRenderAiProfile({ report, maturity, lang, root, save, submit
343
194
  }
344
195
 
345
196
  module.exports = {
346
- performScan,
347
197
  prepareIdentityAndUsage,
348
198
  resolveAiConsent,
349
- runAggregation,
350
- runModelCalls,
351
199
  renderAndFinalize,
352
200
  shouldRenderAiProfile,
353
201
  maybeRenderAiProfile,
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ const { scan, scanForRoots } = require('./scanner');
4
+ const { classify } = require('./maturity');
5
+ const { reportGateForIdentity } = require('./report-gating');
6
+ const {
7
+ loadConsentState,
8
+ getConsentDecision,
9
+ recordConsent,
10
+ isValidEmail,
11
+ normalizeEmail,
12
+ } = require('./share');
13
+ const {
14
+ parseAgentDescriptions,
15
+ parseAgentOrgChartForRoots,
16
+ parseAgentDescriptionsForRoots,
17
+ parseAgentDefinitionsForRoots,
18
+ } = require('./agent-org-chart');
19
+ const { collectAgentUsage } = require('./agent-usage');
20
+ const { collectSessionSignals } = require('./session-signals');
21
+ const {
22
+ collectScopedGitActivity,
23
+ collectScopedWorkStreams,
24
+ collectScopedDetectionEvidence,
25
+ } = require('./scope-signals');
26
+ const { detectAuthorshipMismatch } = require('./authorship-mismatch');
27
+ const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
28
+ const {
29
+ getSynthesisEndpoint,
30
+ getRoadmapEndpoint,
31
+ getAgentEvaluationEndpoint,
32
+ } = require('./config');
33
+ const { withStaticStatus, withSpinner } = require('./terminal-progress');
34
+ const { getRoadmapEntry } = require('./roadmap-content');
35
+ const {
36
+ maybeSynthesizeAgents,
37
+ maybeEvaluateAgents,
38
+ maybePersonalizeRoadmap,
39
+ } = require('./usage-model-calls');
40
+ const { parseArgs } = require('./cli-args');
41
+ const { detectReportLang, getCatalog } = require('./i18n');
42
+ const { prefetchGitLogs } = require('./git-log-prefetch');
43
+ const { GIT_ACTIVITY_LOG_ARGS } = require('./git-activity');
44
+ const { WORK_STREAMS_LOG_ARGS } = require('./work-streams');
45
+ const { AUTHORSHIP_LOG_ARGS } = require('./authorship');
46
+ const { resolveRepoScope } = require('./repo-scope');
47
+
48
+ function performScan({ opts, catalog }) {
49
+ const root = opts.root;
50
+ const report = withStaticStatus(catalog.cli.scanningLabel, () => scan({ root }));
51
+ // `let`, not `const`: this is the cwd tier.
52
+ const maturity = classify(report);
53
+
54
+ if (Array.isArray(report.agents) && report.agents.length > 0) {
55
+ report.agentDescriptions = parseAgentDescriptions(root || process.cwd());
56
+ }
57
+
58
+ return { report, maturity, root, rootDir: root || process.cwd() };
59
+ }
60
+
61
+ function collectIdentityAndUsage({ report }) {
62
+ const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
63
+ const gate = reportGateForIdentity();
64
+ if (hasAgents) report.agentUsage = collectAgentUsage(report.agents);
65
+ return { gate, hasAgents };
66
+ }
67
+
68
+ // Re-anchor a reused grant to the session's login-proven email: else a stale consent email ingests the report under another account and this session's AI profile never populates.
69
+ function reanchorReusedGrant({ gate, state }) {
70
+ if (gate.loggedIn && isValidEmail(gate.email) && state && state.email !== normalizeEmail(gate.email)) {
71
+ recordConsent('granted', gate.email, { verified: true });
72
+ }
73
+ }
74
+
75
+ // `--json` never had an interactive prompt — its whole point is a single parseable stdout document, and it does not gain one now.
76
+ function storedAiConsent({ gate }) {
77
+ const state = loadConsentState();
78
+ const granted = getConsentDecision(state) === 'granted' && (!state || state.emailVerified !== false);
79
+ if (granted) reanchorReusedGrant({ gate, state });
80
+ return granted;
81
+ }
82
+
83
+ function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
84
+ withStaticStatus(catalog.cli.aggregatingLabel, () => {
85
+ report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
86
+ // Display-only transparency warning (never feeds scoring, see authorship-mismatch.js).
87
+ report.authorshipMismatch = detectAuthorshipMismatch(scope.toplevels, consentEmail, report.gitActivity);
88
+ const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
89
+ report.sessions = sessionSignals.sessions;
90
+ report.workStreams = collectScopedWorkStreams(scope.toplevels);
91
+ report.steering = sessionSignals.steering;
92
+ report.decisions = sessionSignals.decisions;
93
+ report.detectionEvidence = collectScopedDetectionEvidence(
94
+ scope.toplevels,
95
+ consentEmail,
96
+ report,
97
+ );
98
+
99
+ const scoped = scanForRoots(agentRoots, { root });
100
+ report.tools = scoped.tools;
101
+ report.summary = scoped.summary;
102
+ report.technologies = scoped.technologies;
103
+ report.mcp = scoped.mcp;
104
+ report.memory = scoped.memory;
105
+ report.automations = scoped.automations;
106
+ report.browserTools = scoped.browserTools;
107
+ report.agentCounts = scoped.agentCounts;
108
+ report.environment = scoped.environment;
109
+
110
+ report.agents = parseAgentOrgChartForRoots(agentRoots);
111
+ report.agentCounts.agents = report.agents.length;
112
+ if (report.agents.length > 0) {
113
+ report.agentDescriptions = parseAgentDescriptionsForRoots(agentRoots);
114
+ report.agentDefinitions = parseAgentDefinitionsForRoots(agentRoots);
115
+ report.agentUsage = collectAgentUsage(report.agents);
116
+ }
117
+ });
118
+ }
119
+
120
+ async function runModelCalls({
121
+ report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
122
+ }) {
123
+ // Which of the three will genuinely be ATTEMPTED.
124
+ const willAttemptSynthesis = aiConsentGranted && hasAgents && !!getSynthesisEndpoint();
125
+ const willAttemptEvaluation = aiConsentGranted && hasAgents && !!getAgentEvaluationEndpoint();
126
+ const roadmapEntryForTier = maturity.tierKey ? getRoadmapEntry(maturity.tierKey, lang) : null;
127
+ const willAttemptRoadmapPersonalization =
128
+ aiConsentGranted
129
+ && gate.showRoadmap
130
+ && !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
131
+ && !!getRoadmapEndpoint();
132
+
133
+ // THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL.
134
+ const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
135
+
136
+ // `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
137
+ let synthesis = null;
138
+ let evaluation = null;
139
+ let roadmapPersonalization = null;
140
+ if (aiConsentGranted && !opts.noAi) {
141
+ synthesis = await runPhase(
142
+ willAttemptSynthesis,
143
+ catalog.cli.synthesizingLabel,
144
+ () => maybeSynthesizeAgents(report, rootDir),
145
+ );
146
+ evaluation = hasAgents
147
+ ? await runPhase(
148
+ willAttemptEvaluation,
149
+ catalog.cli.evaluatingAgentsLabel,
150
+ () => maybeEvaluateAgents(report, agentRoots, lang),
151
+ )
152
+ : null;
153
+ // `runPhase` runs `call()` whether or not a spinner is shown, so gating the roadmap CANNOT be done by flipping `willAttempt` — that only hides the spinner.
154
+ roadmapPersonalization = gate.showRoadmap
155
+ ? await runPhase(
156
+ willAttemptRoadmapPersonalization,
157
+ catalog.cli.personalizingRoadmapLabel,
158
+ () => maybePersonalizeRoadmap(report, maturity, lang),
159
+ )
160
+ : null;
161
+ } else {
162
+ const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
163
+ attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
164
+ attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
165
+ attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
166
+ }
167
+
168
+ if (synthesis) report.agentSynthesis = synthesis;
169
+ if (evaluation) report.agentEvaluation = evaluation;
170
+ if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
171
+ }
172
+
173
+ // Steps 1-6 of a run: scan, scope, aggregate, classify; nothing is rendered or sent.
174
+ async function computeUsage({
175
+ opts,
176
+ catalog,
177
+ lang,
178
+ inRegistration = false,
179
+ prepareIdentity = collectIdentityAndUsage,
180
+ resolveConsent = storedAiConsent,
181
+ resolveScope = resolveRepoScope,
182
+ }) {
183
+ // 1. Local scan + classify (always computed and shown).
184
+ const { report, root, rootDir } = performScan({ opts, catalog });
185
+
186
+ // 2. Identity gate + local agent-usage signal. Roadmap only for anonymous runs
187
+ // (gate.showRoadmap = !loggedIn) AND never during register/onboarding.
188
+ const { gate: identityGate } = prepareIdentity({ report, catalog, quiet: opts.json === true });
189
+ const gate = (inRegistration && identityGate.showRoadmap)
190
+ ? { ...identityGate, showRoadmap: false }
191
+ : identityGate;
192
+
193
+ // 3. Consent gate before any AI/egress call; a decline still renders.
194
+ const aiConsentGranted = await resolveConsent({ opts, catalog, gate });
195
+
196
+ // 4. Repo scoping (exclude client work).
197
+ const scope = await resolveScope({ opts, cwd: rootDir, catalog });
198
+ const consentEmail = (loadConsentState() || {}).email || null;
199
+ const agentRoots = scope.toplevels;
200
+
201
+ // 5. Aggregate scoped signals + rebuild scan/org-chart over the selection, re-classify.
202
+ await prefetchGitLogs(agentRoots, [GIT_ACTIVITY_LOG_ARGS, WORK_STREAMS_LOG_ARGS, AUTHORSHIP_LOG_ARGS]);
203
+ runAggregation({ report, scope, consentEmail, agentRoots, root, catalog });
204
+ const maturity = classify(report);
205
+ const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
206
+ report.analyzedRepos =
207
+ scope.mode === 'all'
208
+ ? null
209
+ : scope.toplevels.map((tl) => tl.split(/[/\\]/).pop() || tl);
210
+
211
+ // 6. The three ephemeral model calls, serialized and gated by consent.
212
+ await runModelCalls({
213
+ report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
214
+ });
215
+ return { report, maturity, root, gate };
216
+ }
217
+
218
+ // The MCP's scan: same steps, returned instead of printed, since stdout is its JSON-RPC channel.
219
+ async function scanUsage(argv) {
220
+ const opts = parseArgs(argv);
221
+ const lang = opts.lang || detectReportLang();
222
+ const { report, maturity } = await computeUsage({ opts, catalog: getCatalog(lang), lang });
223
+ return { report, maturity };
224
+ }
225
+
226
+ module.exports = {
227
+ performScan,
228
+ collectIdentityAndUsage,
229
+ reanchorReusedGrant,
230
+ storedAiConsent,
231
+ runAggregation,
232
+ runModelCalls,
233
+ computeUsage,
234
+ scanUsage,
235
+ };