shakers-cli 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +56 -1203
  2. package/bin/ai-usage.js +32 -58
  3. package/bin/login.js +10 -10
  4. package/bin/mcp.js +17 -12
  5. package/bin/mcpb.js +1 -1
  6. package/bin/onboarding.js +3 -2
  7. package/bin/register.js +19 -5
  8. package/bin/report-html.js +2 -2
  9. package/bin/shakers-cli.js +352 -0
  10. package/bin/shakers.js +20 -344
  11. package/bin/share.js +1 -1
  12. package/bin/superadmin.js +5 -5
  13. package/bin/version.js +3 -0
  14. package/package.json +12 -3
  15. package/src/account-marker.js +40 -0
  16. package/src/account-trace.js +23 -0
  17. package/src/agent-category.js +1 -1
  18. package/src/agent-evaluation.js +3 -3
  19. package/src/agent-org-chart.js +5 -5
  20. package/src/agent-synthesis.js +3 -3
  21. package/src/agent-usage.js +1 -1
  22. package/src/agents-client.js +4 -4
  23. package/src/ai-usage-review.js +101 -0
  24. package/src/ai.js +1 -1
  25. package/src/ansi.js +1 -1
  26. package/src/auth-client.js +18 -4
  27. package/src/auth-method-picker.js +1 -1
  28. package/src/auth-methods.js +1 -1
  29. package/src/auth-session-store.js +18 -8
  30. package/src/authorship-mismatch.js +1 -1
  31. package/src/authorship.js +2 -2
  32. package/src/automations-detector.js +1 -1
  33. package/src/backend-request.js +5 -3
  34. package/src/brand-ansi.js +1 -1
  35. package/src/browser-tools-detector.js +1 -1
  36. package/src/certify-disclaimer.js +1 -1
  37. package/src/certify-remediation-prompt.js +1 -1
  38. package/src/cli-args.js +2 -2
  39. package/src/command-audit.js +8 -5
  40. package/src/config-dir.js +1 -1
  41. package/src/config.js +114 -48
  42. package/src/consent-flow.js +7 -6
  43. package/src/consent-skip.js +1 -1
  44. package/src/detection-evidence.js +2 -2
  45. package/src/detection-noise.js +1 -1
  46. package/src/detectors.js +1 -1
  47. package/src/device-login.js +12 -1
  48. package/src/email-verification.js +3 -3
  49. package/src/env-paths.js +1 -1
  50. package/src/find-projects-client.js +12 -0
  51. package/src/find-projects-flow.js +1 -13
  52. package/src/git-activity.js +1 -1
  53. package/src/graph-certs.js +3 -3
  54. package/src/graph-scan.js +9 -9
  55. package/src/i18n.js +148 -138
  56. package/src/interactive-select.js +1 -1
  57. package/src/interview-client.js +6 -1
  58. package/src/interview-transcript.js +1 -1
  59. package/src/legal-notice.js +1 -1
  60. package/src/maturity.js +6 -6
  61. package/src/mcp-ai-usage-result-tool.js +34 -12
  62. package/src/mcp-ai-usage-tool.js +282 -163
  63. package/src/mcp-auth-form.js +7 -3
  64. package/src/mcp-detector.js +3 -3
  65. package/src/mcp-device-login-tools.js +2 -1
  66. package/src/mcp-find-projects-tools.js +1 -2
  67. package/src/mcp-install.js +279 -44
  68. package/src/mcp-instructions.js +1 -1
  69. package/src/mcp-onboarding-tools.js +4 -4
  70. package/src/mcp-prompts.js +4 -4
  71. package/src/mcp-register-tools.js +1 -0
  72. package/src/mcp-role-tools.js +24 -1
  73. package/src/mcp-server.js +5 -1
  74. package/src/mcp-signup-client.js +2 -2
  75. package/src/mcp-signup-tools.js +39 -9
  76. package/src/memory-structure-detector.js +1 -1
  77. package/src/model-call-record.js +2 -2
  78. package/src/onboarding-flow.js +63 -42
  79. package/src/onboarding-funnel.js +153 -20
  80. package/src/open-file.js +1 -1
  81. package/src/osc-link.js +1 -1
  82. package/src/portfolio-client.js +11 -1
  83. package/src/portfolio-git-info.js +1 -1
  84. package/src/profile-flow.js +1 -1
  85. package/src/profile-switch.js +5 -12
  86. package/src/render-certification.js +10 -10
  87. package/src/render-html.js +6 -6
  88. package/src/render-report-text.js +1 -1
  89. package/src/render-sheet.js +10 -10
  90. package/src/render-terminal.js +6 -6
  91. package/src/repo-scope.js +132 -0
  92. package/src/report-gating.js +5 -3
  93. package/src/report-store.js +13 -8
  94. package/src/roadmap-content.js +5 -5
  95. package/src/roadmap-personalization.js +3 -3
  96. package/src/roadmap-prompt.js +2 -2
  97. package/src/roles-flow.js +8 -5
  98. package/src/scan-exclusions.js +1 -1
  99. package/src/scanner.js +14 -14
  100. package/src/secret-prompt.js +1 -1
  101. package/src/session-gate.js +1 -1
  102. package/src/session-refresh.js +33 -3
  103. package/src/session-scan.js +1 -1
  104. package/src/shakers-wordmark.js +1 -1
  105. package/src/share-card.js +2 -2
  106. package/src/share-certification.js +4 -4
  107. package/src/share-consent-state.js +9 -9
  108. package/src/share-payload.js +7 -7
  109. package/src/share.js +7 -7
  110. package/src/signup-client.js +1 -1
  111. package/src/signup-copy.js +177 -2
  112. package/src/skills-client.js +1 -1
  113. package/src/start-add-agents.js +2 -2
  114. package/src/start-add-portfolio.js +3 -13
  115. package/src/start-add-skills.js +2 -2
  116. package/src/stdin-ask.js +16 -4
  117. package/src/strip-internal-comments.js +1 -1
  118. package/src/tech-detector.js +2 -2
  119. package/src/templates/report-sheet.html +11 -13
  120. package/src/terminal-agents.js +8 -8
  121. package/src/terminal-format.js +3 -3
  122. package/src/terminal-sections.js +11 -11
  123. package/src/tier-engine.js +3 -3
  124. package/src/tier-projection.js +2 -2
  125. package/src/transcript-turns.js +1 -1
  126. package/src/typewriter.js +1 -1
  127. package/src/usage-model-calls.js +6 -6
  128. package/src/usage-oneshot-commands.js +5 -5
  129. package/src/usage-repo-scope.js +11 -129
  130. package/src/usage-run-steps.js +82 -181
  131. package/src/usage-scan.js +235 -0
  132. package/src/work-streams.js +1 -1
@@ -1,60 +1,32 @@
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,
11
+ isThrottled,
17
12
  } = require('./share');
18
13
  const { runConsentPrompt } = require('./consent-flow');
19
14
  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');
15
+ const { getAiProfileEndpoint } = require('./config');
41
16
  // "My work with AI" preview, read from the hub ai-profile (same as the front).
42
17
  const { resolveAiProfilePreview } = require('./ai-profile-preview');
43
18
  const { renderAiProfileTerminal } = require('./render-ai-fluency');
44
19
  const { ensureFreshSession } = require('./session-refresh');
45
20
  const { loadAuthSession, sessionStatus } = require('./auth-session-store');
46
21
  const { c: termColors } = require('./terminal-format');
47
- const { withStaticStatus, withSpinner } = require('./terminal-progress');
22
+ const { withSpinner } = require('./terminal-progress');
48
23
  const { computeConsentSkip } = require('./consent-skip');
49
- const { getRoadmapEntry } = require('./roadmap-content');
50
- const {
51
- maybeSynthesizeAgents,
52
- maybeEvaluateAgents,
53
- maybePersonalizeRoadmap,
54
- } = require('./usage-model-calls');
24
+ const { collectIdentityAndUsage, storedAiConsent, reanchorReusedGrant } = require('./usage-scan');
55
25
  const { doBuildNextLevel } = require('./usage-oneshot-commands');
26
+ const { uploadFacts, withoutExcluded, summaryText, explanationText } = require('./ai-usage-review');
27
+ const { reviewCopy } = require('./signup-copy');
28
+ const { askYesNo } = require('./consent-flow');
56
29
 
57
- // Automatic, silent PERSISTING once consent is `granted` (ADR-007, gating revised by ADR-011: consent controls persistence only, never what's shown).
58
30
  async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
59
31
  try {
60
32
  const result = await autoShare(report, maturity, { root, bypassThrottle });
@@ -82,35 +54,17 @@ function sendStatusColor(reason) {
82
54
  return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
83
55
  }
84
56
 
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
57
  // The identity gate + local (no-network, no-LLM) agent-usage signal.
99
58
  function prepareIdentityAndUsage({ report, catalog, quiet = false }) {
100
- const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
101
- const gate = reportGateForIdentity();
102
-
59
+ const identity = collectIdentityAndUsage({ report });
103
60
  // --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 };
61
+ if (identity.gate.showFrameworkIntro && !quiet) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
62
+ return identity;
110
63
  }
111
64
 
112
65
  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).
66
+ // CONSENT GATE, MOVED TO BEFORE ANY AI/EGRESS CALL (explicit product decision, not a bug fix).
67
+ if (opts.json) return storedAiConsent({ gate });
114
68
  const state = loadConsentState();
115
69
  const decision = getConsentDecision(state);
116
70
  const consentSkip = computeConsentSkip({
@@ -122,143 +76,90 @@ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
122
76
  });
123
77
 
124
78
  let aiConsentGranted;
125
- // A REUSED grant (skip/--json) carries a stale consent email; a fresh prompt re-anchors itself.
79
+ // A REUSED grant (skip) carries a stale consent email; a fresh prompt re-anchors itself.
126
80
  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);
81
+ if (consentSkip.message) {
82
+ process.stdout.write(`\n ${consentSkip.message}\n`);
83
+ }
84
+ if (consentSkip.skip) {
85
+ // A TERMINAL decision already exists (a prior run's explicit grant or
86
+ // denial) — never re-asked.
87
+ aiConsentGranted = decision === 'granted';
130
88
  reusedStoredConsent = true;
131
89
  } 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`);
90
+ const ask = injectedAsk || createStdinAsk();
91
+ let result;
92
+ try {
93
+ result = await runConsentPrompt({
94
+ ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
95
+ });
96
+ } finally {
97
+ if (!injectedAsk) ask.close();
155
98
  }
99
+ aiConsentGranted = result === 'granted';
156
100
  }
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 });
101
+ if (!aiConsentGranted) {
102
+ // No early return: the local report still renders further down.
103
+ process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
161
104
  }
105
+ if (reusedStoredConsent && aiConsentGranted) reanchorReusedGrant({ gate, state });
162
106
  return aiConsentGranted;
163
107
  }
164
108
 
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
- });
109
+ function matchNames(raw, known) {
110
+ const wanted = String(raw || '').split(',').map((x) => x.trim()).filter(Boolean);
111
+ const byLower = new Map(known.map((name) => [name.toLowerCase(), name]));
112
+ return {
113
+ matched: wanted.map((w) => byLower.get(w.toLowerCase())).filter(Boolean),
114
+ unknown: wanted.filter((w) => !byLower.has(w.toLowerCase())),
115
+ };
200
116
  }
201
117
 
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));
118
+ async function askExcludedAgents(ask, c, known, out) {
119
+ for (let attempt = 0; attempt < 3; attempt += 1) {
120
+ const { matched, unknown } = matchNames(await ask(c.cliExcludeAgents), known);
121
+ if (unknown.length === 0) return matched;
122
+ out.write(` ${c.cliUnknownAgents(unknown.join(', '), known.join(', '))}\n`);
248
123
  }
124
+ return null;
125
+ }
249
126
 
250
- if (synthesis) report.agentSynthesis = synthesis;
251
- if (evaluation) report.agentEvaluation = evaluation;
252
- if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
127
+ async function reviewUpload({ report, maturity, repos = [], lang, catalog, injectedAsk = null, out = process.stdout, bypassThrottle = false }) {
128
+ const state = loadConsentState();
129
+ if (getConsentDecision(state) !== 'granted' || !state || state.emailVerified === false || !state.email || (!bypassThrottle && isThrottled(state))) {
130
+ return { approved: true, report, maturity };
131
+ }
132
+ const c = reviewCopy(lang);
133
+ const facts = uploadFacts({ report, maturity, repos });
134
+ out.write(`\n${summaryText(lang, facts)}\n\n${explanationText(lang)}\n\n`);
135
+ if (facts.repos.length > 1) out.write(`${c.cliRepoHint}\n\n`);
136
+ const ask = injectedAsk || createStdinAsk();
137
+ try {
138
+ const approved = await askYesNo(ask, c.cliQuestion, catalog, { defaultNo: true });
139
+ if (!approved) {
140
+ out.write(` ${c.declined}\n`);
141
+ return { approved: false };
142
+ }
143
+ const agents = facts.agents.length > 0 ? await askExcludedAgents(ask, c, facts.agents, out) : [];
144
+ if (agents === null) {
145
+ out.write(` ${c.declined}\n`);
146
+ return { approved: false };
147
+ }
148
+ const hasExcerpts = facts.conversationExcerpts + facts.commandExcerpts > 0;
149
+ const excerpts = hasExcerpts ? (await askYesNo(ask, c.cliIncludeExcerpts, catalog, { defaultYes: true })) === true : true;
150
+ return { approved: true, ...withoutExcluded(report, { agents, excerpts }) };
151
+ } finally {
152
+ if (!injectedAsk) ask.close();
153
+ }
253
154
  }
254
155
 
255
- async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
156
+ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false, repos = [], injectedAsk = null }) {
256
157
  if (opts.json) {
257
158
  process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
258
159
  return;
259
160
  }
260
161
 
261
- // Issue 103: revealed by SECTIONS instead of landing in one write.
162
+ // Revealed by SECTIONS instead of landing in one write.
262
163
  if (gate.sessionStatus === 'expired') {
263
164
  process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
264
165
  }
@@ -276,14 +177,16 @@ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, cat
276
177
  }
277
178
  }
278
179
 
279
- // "Construir el siguiente nivel ahora" (issue 021): optional, only when
180
+ // "Construir el siguiente nivel ahora": optional, only when
280
181
  // explicitly requested — never part of a normal run otherwise.
281
182
  if (opts.buildNextLevel) {
282
183
  doBuildNextLevel(root, maturity, opts.force, catalog);
283
184
  }
284
185
 
285
- // Auto-send at the end (bypassThrottle only true on the register path).
286
- const shareResult = await maybeAutoShare(report, maturity, root, catalog, { bypassThrottle });
186
+ const reviewed = await reviewUpload({ report, maturity, repos, lang, catalog, injectedAsk, bypassThrottle });
187
+ if (!reviewed.approved) return;
188
+ const shareResult = await maybeAutoShare(reviewed.report, reviewed.maturity, root, catalog, { bypassThrottle });
189
+ if (shareResult && shareResult.ok) process.stdout.write(`\n ${reviewCopy(lang).afterwards}\n`);
287
190
 
288
191
  // "My work with AI" preview, best-effort after the send; never blocks/breaks the run.
289
192
  await maybeRenderAiProfile({ report, maturity, lang, root, save: opts.save, submitOk: !!(shareResult && shareResult.ok), catalog });
@@ -343,11 +246,9 @@ async function maybeRenderAiProfile({ report, maturity, lang, root, save, submit
343
246
  }
344
247
 
345
248
  module.exports = {
346
- performScan,
249
+ reviewUpload,
347
250
  prepareIdentityAndUsage,
348
251
  resolveAiConsent,
349
- runAggregation,
350
- runModelCalls,
351
252
  renderAndFinalize,
352
253
  shouldRenderAiProfile,
353
254
  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 roadmapRequested = opts.roadmap === true && gate.showRoadmap;
128
+ const willAttemptRoadmapPersonalization =
129
+ aiConsentGranted
130
+ && roadmapRequested
131
+ && !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
132
+ && !!getRoadmapEndpoint();
133
+
134
+ // THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL.
135
+ const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
136
+
137
+ // `aiConsentGranted` is `false` whenever consent for this run was declined, denied in the past, or (on `--json`) simply never granted.
138
+ let synthesis = null;
139
+ let evaluation = null;
140
+ let roadmapPersonalization = null;
141
+ if (aiConsentGranted && !opts.noAi) {
142
+ synthesis = await runPhase(
143
+ willAttemptSynthesis,
144
+ catalog.cli.synthesizingLabel,
145
+ () => maybeSynthesizeAgents(report, rootDir),
146
+ );
147
+ evaluation = hasAgents
148
+ ? await runPhase(
149
+ willAttemptEvaluation,
150
+ catalog.cli.evaluatingAgentsLabel,
151
+ () => maybeEvaluateAgents(report, agentRoots, lang),
152
+ )
153
+ : null;
154
+ // `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.
155
+ roadmapPersonalization = roadmapRequested
156
+ ? await runPhase(
157
+ willAttemptRoadmapPersonalization,
158
+ catalog.cli.personalizingRoadmapLabel,
159
+ () => maybePersonalizeRoadmap(report, maturity, lang),
160
+ )
161
+ : null;
162
+ } else {
163
+ const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
164
+ attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
165
+ attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
166
+ attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
167
+ }
168
+
169
+ if (synthesis) report.agentSynthesis = synthesis;
170
+ if (evaluation) report.agentEvaluation = evaluation;
171
+ if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
172
+ }
173
+
174
+ // Steps 1-6 of a run: scan, scope, aggregate, classify; nothing is rendered or sent.
175
+ async function computeUsage({
176
+ opts,
177
+ catalog,
178
+ lang,
179
+ inRegistration = false,
180
+ prepareIdentity = collectIdentityAndUsage,
181
+ resolveConsent = storedAiConsent,
182
+ resolveScope = resolveRepoScope,
183
+ }) {
184
+ // 1. Local scan + classify (always computed and shown).
185
+ const { report, root, rootDir } = performScan({ opts, catalog });
186
+
187
+ // 2. Identity gate + local agent-usage signal.
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, repos: scope.toplevels };
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), roadmap: false };
221
+ const lang = opts.lang || detectReportLang();
222
+ const { report, maturity, repos } = await computeUsage({ opts, catalog: getCatalog(lang), lang });
223
+ return { report, maturity, repos };
224
+ }
225
+
226
+ module.exports = {
227
+ performScan,
228
+ collectIdentityAndUsage,
229
+ reanchorReusedGrant,
230
+ storedAiConsent,
231
+ runAggregation,
232
+ runModelCalls,
233
+ computeUsage,
234
+ scanUsage,
235
+ };
@@ -4,7 +4,7 @@ const { execFileSync } = require('child_process');
4
4
  const { isGitRepo } = require('./git-activity');
5
5
  const { takePrefetchedGitLog } = require('./git-log-prefetch');
6
6
 
7
- // Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs (hybrid evaluation, Slice 2).
7
+ // Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs.
8
8
 
9
9
  const GIT_TIMEOUT_MS = 15000;
10
10
  const SESSION_GAP_SECONDS = 4 * 3600;