shakers-cli-beta 0.8.0-beta.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 (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +81 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +120 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,338 @@
1
+ 'use strict';
2
+
3
+ const { scan, scanForRoots } = require('./scanner');
4
+ const { classify } = require('./maturity');
5
+ const { renderTerminal } = require('./render-terminal');
6
+ const { reportGateForIdentity } = require('./report-gating');
7
+ const { revealText } = require('./typewriter');
8
+ const { persistFootprint } = require('./report-store');
9
+ const {
10
+ loadConsentState,
11
+ getConsentDecision,
12
+ autoShare,
13
+ consentPath,
14
+ } = require('./share');
15
+ const { runConsentPrompt } = require('./consent-flow');
16
+ const { createStdinAsk } = require('./stdin-ask');
17
+ const {
18
+ parseAgentDescriptions,
19
+ parseAgentOrgChartForRoots,
20
+ parseAgentDescriptionsForRoots,
21
+ parseAgentDefinitionsForRoots,
22
+ } = require('./agent-org-chart');
23
+ const { collectAgentUsage } = require('./agent-usage');
24
+ const { collectSessionSignals } = require('./session-signals');
25
+ const {
26
+ collectScopedGitActivity,
27
+ collectScopedWorkStreams,
28
+ collectScopedDetectionEvidence,
29
+ } = require('./scope-signals');
30
+ const { MODEL_CALL, REASON, skipped, attachModelCall } = require('./model-call-record');
31
+ const {
32
+ getSynthesisEndpoint,
33
+ getRoadmapEndpoint,
34
+ getAgentEvaluationEndpoint,
35
+ getAiProfileEndpoint,
36
+ } = require('./config');
37
+ // "My work with AI" preview, read from the hub ai-profile (same as the front).
38
+ const { resolveAiProfilePreview } = require('./ai-profile-preview');
39
+ const { renderAiProfileTerminal } = require('./render-ai-fluency');
40
+ const { ensureFreshSession } = require('./session-refresh');
41
+ const { loadAuthSession, sessionStatus } = require('./auth-session-store');
42
+ const { c: termColors } = require('./terminal-format');
43
+ const { withStaticStatus, withSpinner } = require('./terminal-progress');
44
+ const { computeConsentSkip } = require('./consent-skip');
45
+ const { getRoadmapEntry } = require('./roadmap-content');
46
+ const {
47
+ maybeSynthesizeAgents,
48
+ maybeEvaluateAgents,
49
+ maybePersonalizeRoadmap,
50
+ } = require('./usage-model-calls');
51
+ const { doBuildNextLevel } = require('./usage-oneshot-commands');
52
+
53
+ // Automatic, silent PERSISTING once consent is `granted` (ADR-007, gating revised by ADR-011: consent controls persistence only, never what's shown).
54
+ async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
55
+ try {
56
+ const result = await autoShare(report, maturity, { root, bypassThrottle });
57
+ if (result && result.ok && result.backend) {
58
+ process.stdout.write(` ${catalog.backendChain.saved(result.backend)}\n`);
59
+ return result;
60
+ }
61
+ // Surface why a send was skipped/failed (user-language, stderr); null = quiet.
62
+ if (result && result.reason) {
63
+ const msg = catalog.sendStatus.reason(result.reason);
64
+ if (msg) process.stderr.write(` ${sendStatusColor(result.reason)}${msg}${termColors.reset}\n`);
65
+ }
66
+ return result || { ok: false, reason: null };
67
+ } catch {
68
+ try {
69
+ const msg = catalog.sendStatus.reason('error');
70
+ if (msg) process.stderr.write(` ${termColors.danger}${msg}${termColors.reset}\n`);
71
+ } catch { /* never break the local report over a notice */ }
72
+ return { ok: false, reason: 'error' };
73
+ }
74
+ }
75
+
76
+ const SEND_WARN_REASONS = new Set(['throttled', 'email-unverified', 'no-email', 'no-endpoint-configured']);
77
+ function sendStatusColor(reason) {
78
+ return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
79
+ }
80
+
81
+ function performScan({ opts, catalog }) {
82
+ const root = opts.root;
83
+ const report = withStaticStatus(catalog.cli.scanningLabel, () => scan({ root }));
84
+ // `let`, not `const`: this is the cwd tier.
85
+ const maturity = classify(report);
86
+
87
+ if (Array.isArray(report.agents) && report.agents.length > 0) {
88
+ report.agentDescriptions = parseAgentDescriptions(root || process.cwd());
89
+ }
90
+
91
+ return { report, maturity, root, rootDir: root || process.cwd() };
92
+ }
93
+
94
+ // The identity gate + local (no-network, no-LLM) agent-usage signal.
95
+ function prepareIdentityAndUsage({ report, catalog }) {
96
+ const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
97
+ const gate = reportGateForIdentity();
98
+
99
+ if (gate.showFrameworkIntro) process.stdout.write(`\n ${catalog.cli.frameworkIntroUsage}\n`);
100
+
101
+ // Local, no network, no LLM: how often each detected agent was invoked in the local Claude Code history.
102
+ if (hasAgents) report.agentUsage = collectAgentUsage(report.agents);
103
+
104
+ return { gate, hasAgents };
105
+ }
106
+
107
+ async function resolveAiConsent({ opts, catalog, gate, injectedAsk }) {
108
+ // 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).
109
+ const state = loadConsentState();
110
+ const decision = getConsentDecision(state);
111
+ const consentSkip = computeConsentSkip({
112
+ decision,
113
+ emailVerified: state ? state.emailVerified : undefined,
114
+ stdinIsTTY: !!process.stdin.isTTY,
115
+ consentFilePath: consentPath(),
116
+ catalog,
117
+ });
118
+
119
+ let aiConsentGranted;
120
+ if (opts.json) {
121
+ // `--json` never had an interactive prompt — its whole point is a single parseable stdout document, and it does not gain one now.
122
+ aiConsentGranted = decision === 'granted' && (!state || state.emailVerified !== false);
123
+ } else {
124
+ if (consentSkip.message) {
125
+ process.stdout.write(`\n ${consentSkip.message}\n`);
126
+ }
127
+ if (consentSkip.skip) {
128
+ // A TERMINAL decision already exists (a prior run's explicit grant or
129
+ // denial) — never re-asked, per ADR-007/011.
130
+ aiConsentGranted = decision === 'granted';
131
+ } else {
132
+ const ask = injectedAsk || createStdinAsk();
133
+ let result;
134
+ try {
135
+ result = await runConsentPrompt({
136
+ ask, catalog, loggedIn: gate.loggedIn, sessionEmail: gate.email, profile: gate.profile,
137
+ });
138
+ } finally {
139
+ if (!injectedAsk) ask.close();
140
+ }
141
+ aiConsentGranted = result === 'granted';
142
+ }
143
+ if (!aiConsentGranted) {
144
+ // No early return (reverted 8867450's wall): the local report still renders further down, same as ADR-011 always promised.
145
+ process.stdout.write(`\n ${termColors.warning}${catalog.consent.noReportWithoutConsent}${termColors.reset}\n\n`);
146
+ }
147
+ }
148
+ return aiConsentGranted;
149
+ }
150
+
151
+ function runAggregation({ report, scope, consentEmail, agentRoots, root, catalog }) {
152
+ withStaticStatus(catalog.cli.aggregatingLabel, () => {
153
+ report.gitActivity = collectScopedGitActivity(scope.toplevels, consentEmail);
154
+ const sessionSignals = collectSessionSignals(process.env, scope.selectedCwds);
155
+ report.sessions = sessionSignals.sessions;
156
+ report.workStreams = collectScopedWorkStreams(scope.toplevels);
157
+ report.steering = sessionSignals.steering;
158
+ report.decisions = sessionSignals.decisions;
159
+ report.detectionEvidence = collectScopedDetectionEvidence(
160
+ scope.toplevels,
161
+ consentEmail,
162
+ report,
163
+ );
164
+
165
+ const scoped = scanForRoots(agentRoots, { root });
166
+ report.tools = scoped.tools;
167
+ report.summary = scoped.summary;
168
+ report.technologies = scoped.technologies;
169
+ report.mcp = scoped.mcp;
170
+ report.memory = scoped.memory;
171
+ report.automations = scoped.automations;
172
+ report.browserTools = scoped.browserTools;
173
+ report.agentCounts = scoped.agentCounts;
174
+ report.environment = scoped.environment;
175
+
176
+ report.agents = parseAgentOrgChartForRoots(agentRoots);
177
+ report.agentCounts.agents = report.agents.length;
178
+ if (report.agents.length > 0) {
179
+ report.agentDescriptions = parseAgentDescriptionsForRoots(agentRoots);
180
+ report.agentDefinitions = parseAgentDefinitionsForRoots(agentRoots);
181
+ report.agentUsage = collectAgentUsage(report.agents);
182
+ }
183
+ });
184
+ }
185
+
186
+ async function runModelCalls({
187
+ report, maturity, aiConsentGranted, opts, gate, hasAgents, agentRoots, rootDir, lang, catalog,
188
+ }) {
189
+ // Which of the three will genuinely be ATTEMPTED.
190
+ const willAttemptSynthesis = aiConsentGranted && hasAgents && !!getSynthesisEndpoint();
191
+ const willAttemptEvaluation = aiConsentGranted && hasAgents && !!getAgentEvaluationEndpoint();
192
+ const roadmapEntryForTier = maturity.tierKey ? getRoadmapEntry(maturity.tierKey, lang) : null;
193
+ const willAttemptRoadmapPersonalization =
194
+ aiConsentGranted
195
+ && gate.showRoadmap
196
+ && !!roadmapEntryForTier && !roadmapEntryForTier.maxTier
197
+ && !!getRoadmapEndpoint();
198
+
199
+ // THE THREE MODEL CALLS RUN ONE AFTER ANOTHER, NOT IN PARALLEL (issue 116).
200
+ const runPhase = (willAttempt, label, call) => (willAttempt ? withSpinner(label, call) : call());
201
+
202
+ // 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.
203
+ let synthesis = null;
204
+ let evaluation = null;
205
+ let roadmapPersonalization = null;
206
+ if (aiConsentGranted && !opts.noAi) {
207
+ synthesis = await runPhase(
208
+ willAttemptSynthesis,
209
+ catalog.cli.synthesizingLabel,
210
+ () => maybeSynthesizeAgents(report, rootDir),
211
+ );
212
+ evaluation = hasAgents
213
+ ? await runPhase(
214
+ willAttemptEvaluation,
215
+ catalog.cli.evaluatingAgentsLabel,
216
+ () => maybeEvaluateAgents(report, agentRoots, lang),
217
+ )
218
+ : null;
219
+ // 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.
220
+ roadmapPersonalization = gate.showRoadmap
221
+ ? await runPhase(
222
+ willAttemptRoadmapPersonalization,
223
+ catalog.cli.personalizingRoadmapLabel,
224
+ () => maybePersonalizeRoadmap(report, maturity, lang),
225
+ )
226
+ : null;
227
+ } else {
228
+ const skipReason = aiConsentGranted ? REASON.NOT_APPLICABLE : REASON.NO_CONSENT;
229
+ attachModelCall(report, MODEL_CALL.SYNTHESIS, skipped(skipReason));
230
+ attachModelCall(report, MODEL_CALL.EVALUATION, skipped(skipReason));
231
+ attachModelCall(report, MODEL_CALL.ROADMAP, skipped(skipReason));
232
+ }
233
+
234
+ if (synthesis) report.agentSynthesis = synthesis;
235
+ if (evaluation) report.agentEvaluation = evaluation;
236
+ if (roadmapPersonalization) report.roadmapPersonalization = roadmapPersonalization;
237
+ }
238
+
239
+ async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
240
+ if (opts.json) {
241
+ process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
242
+ return;
243
+ }
244
+
245
+ // Issue 103: revealed by SECTIONS instead of landing in one write.
246
+ if (gate.sessionStatus === 'expired') {
247
+ process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
248
+ }
249
+
250
+ await revealText(renderTerminal(report, maturity, lang, { showRoadmap: opts.roadmap, gate }) + '\n', {
251
+ stream: process.stdout,
252
+ animate: opts.noAnimation ? false : null,
253
+ });
254
+
255
+ if (opts.save) {
256
+ try {
257
+ persistFootprint({ root, report, maturity });
258
+ } catch {
259
+ // Never break the local run over a failed state write.
260
+ }
261
+ }
262
+
263
+ // "Construir el siguiente nivel ahora" (issue 021): optional, only when
264
+ // explicitly requested — never part of a normal run otherwise.
265
+ if (opts.buildNextLevel) {
266
+ doBuildNextLevel(root, maturity, opts.force, catalog);
267
+ }
268
+
269
+ // Auto-send at the end (bypassThrottle only true on the register path).
270
+ const shareResult = await maybeAutoShare(report, maturity, root, catalog, { bypassThrottle });
271
+
272
+ // "My work with AI" preview, best-effort after the send; never blocks/breaks the run.
273
+ await maybeRenderAiProfile({ report, maturity, lang, root, save: opts.save, submitOk: !!(shareResult && shareResult.ok), catalog });
274
+ }
275
+
276
+ // A `ready` profile always shows; otherwise the "being evaluated"/setup-only note only shows when this run's submit succeeded.
277
+ function shouldRenderAiProfile({ submitOk, status } = {}) {
278
+ return status === 'ready' || !!submitOk;
279
+ }
280
+
281
+ // Previews the hub "My work with AI" profile (matrix + vision E + howIWork F +
282
+ // traction). Shared source with the MCP tool. Setup-only without session/consent.
283
+ async function maybeRenderAiProfile({ report, maturity, lang, root, save, submitOk = true, catalog }, deps = {}) {
284
+ const {
285
+ resolve = resolveAiProfilePreview,
286
+ refreshToken = async () => {
287
+ const fresh = await ensureFreshSession(process.env);
288
+ return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
289
+ },
290
+ spin = withSpinner,
291
+ out = process.stdout,
292
+ loadSession = loadAuthSession,
293
+ consentDecision = () => getConsentDecision(loadConsentState()),
294
+ } = deps;
295
+ try {
296
+ const session = loadSession();
297
+ const hasSession = sessionStatus(session) === 'active';
298
+ const consentGranted = consentDecision() === 'granted';
299
+ if (!hasSession || !consentGranted) return;
300
+
301
+ const result = await spin(
302
+ catalog.terminal.aiProfileGenerating,
303
+ () => resolve({ session, endpoint: getAiProfileEndpoint() }, { refreshToken }),
304
+ );
305
+ // A failed submit means nothing is in flight — don't tease "being evaluated"/setup-only.
306
+ if (!shouldRenderAiProfile({ submitOk, status: result && result.status })) return;
307
+
308
+ if (result.status === 'ready') {
309
+ // Re-persist so the HTML report and later state reads show the preview too.
310
+ report.aiWorkPreview = result.preview;
311
+ if (save) {
312
+ try {
313
+ persistFootprint({ root, report, maturity });
314
+ } catch {
315
+ // never break the run over a state re-write
316
+ }
317
+ }
318
+ const block = renderAiProfileTerminal(result, { lang, c: termColors });
319
+ if (block) out.write(`\n${block}\n`);
320
+ return;
321
+ }
322
+
323
+ out.write(`\n ${catalog.terminal.aiProfilePendingRetry}\n`);
324
+ } catch {
325
+ // Never break the local run over the (optional) preview.
326
+ }
327
+ }
328
+
329
+ module.exports = {
330
+ performScan,
331
+ prepareIdentityAndUsage,
332
+ resolveAiConsent,
333
+ runAggregation,
334
+ runModelCalls,
335
+ renderAndFinalize,
336
+ shouldRenderAiProfile,
337
+ maybeRenderAiProfile,
338
+ };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const { isGitRepo } = require('./git-activity');
5
+
6
+ // Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs (hybrid evaluation, Slice 2).
7
+
8
+ const GIT_TIMEOUT_MS = 15000;
9
+ const SESSION_GAP_SECONDS = 4 * 3600;
10
+ const SECONDS_PER_DAY = 86400;
11
+
12
+ function runGitTimestamps(root) {
13
+ try {
14
+ return execFileSync(
15
+ 'git',
16
+ ['-C', root, 'log', '--no-merges', '--pretty=format:%at'],
17
+ {
18
+ encoding: 'utf8',
19
+ timeout: GIT_TIMEOUT_MS,
20
+ stdio: ['ignore', 'pipe', 'ignore'],
21
+ maxBuffer: 64 * 1024 * 1024,
22
+ },
23
+ );
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function round2(n) {
30
+ return Math.round(n * 100) / 100;
31
+ }
32
+
33
+ function collectWorkStreams(root) {
34
+ if (!isGitRepo(root)) return null;
35
+
36
+ const log = runGitTimestamps(root);
37
+ if (log === null) return null;
38
+
39
+ const timestamps = log
40
+ .split('\n')
41
+ .map((line) => Number.parseInt(line.trim(), 10))
42
+ .filter((n) => Number.isFinite(n))
43
+ .sort((a, b) => a - b);
44
+ if (timestamps.length === 0) return null;
45
+
46
+ const streams = [];
47
+ let current = null;
48
+ for (const ts of timestamps) {
49
+ if (current && ts - current.last <= SESSION_GAP_SECONDS) {
50
+ current.last = ts;
51
+ current.count += 1;
52
+ } else {
53
+ current = { first: ts, last: ts, count: 1 };
54
+ streams.push(current);
55
+ }
56
+ }
57
+
58
+ const streamCount = streams.length;
59
+ const spanDaysOf = (s) => Math.floor((s.last - s.first) / SECONDS_PER_DAY);
60
+ const multiDayStreams = streams.filter((s) => spanDaysOf(s) >= 1).length;
61
+ const maxStreamSpanDays = streams.reduce(
62
+ (max, s) => Math.max(max, spanDaysOf(s)),
63
+ 0,
64
+ );
65
+
66
+ return {
67
+ streamCount,
68
+ multiDayStreams,
69
+ avgCommitsPerSession: round2(timestamps.length / streamCount),
70
+ maxStreamSpanDays,
71
+ };
72
+ }
73
+
74
+ module.exports = { collectWorkStreams, SESSION_GAP_SECONDS };