shakers-cli 0.0.0-stage → 0.14.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.
- package/LICENSE +59 -0
- package/README.md +1233 -2
- package/bin/add-agent.js +88 -0
- package/bin/add-project.js +88 -0
- package/bin/add-role.js +61 -0
- package/bin/add-skill.js +88 -0
- package/bin/ai-usage.js +99 -0
- package/bin/alma.js +99 -0
- package/bin/applications.js +69 -0
- package/bin/availability.js +70 -0
- package/bin/certifications.js +69 -0
- package/bin/certify.js +73 -0
- package/bin/change-role.js +61 -0
- package/bin/config.js +124 -0
- package/bin/experiences.js +69 -0
- package/bin/find-projects.js +89 -0
- package/bin/invitations.js +69 -0
- package/bin/lang.js +70 -0
- package/bin/login.js +216 -0
- package/bin/mcp.js +124 -0
- package/bin/mcpb.js +5 -0
- package/bin/me.js +69 -0
- package/bin/onboarding.js +134 -0
- package/bin/portfolios.js +69 -0
- package/bin/rate.js +70 -0
- package/bin/register.js +337 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +363 -0
- package/bin/share.js +72 -0
- package/bin/show-project.js +72 -0
- package/bin/socials.js +70 -0
- package/bin/superadmin.js +351 -0
- package/bin/update-check-worker.js +70 -0
- package/bin/update.js +111 -0
- package/package.json +40 -4
- package/src/agent-category.js +13 -0
- package/src/agent-evaluation.js +177 -0
- package/src/agent-org-chart.js +355 -0
- package/src/agent-synthesis.js +132 -0
- package/src/agent-usage.js +118 -0
- package/src/agents-client.js +165 -0
- package/src/ai-fluency-cell.js +72 -0
- package/src/ai-profile-client.js +45 -0
- package/src/ai-profile-preview.js +154 -0
- package/src/ai.js +66 -0
- package/src/alma-client.js +123 -0
- package/src/alma-flow.js +32 -0
- package/src/alma-repo-context.js +47 -0
- package/src/alma-shell.js +161 -0
- package/src/ansi.js +62 -0
- package/src/applications-client.js +79 -0
- package/src/applications-flow.js +58 -0
- package/src/auth-client.js +144 -0
- package/src/auth-device-client.js +118 -0
- package/src/auth-method-picker.js +44 -0
- package/src/auth-methods.js +35 -0
- package/src/auth-session-store.js +215 -0
- package/src/authorship-mismatch.js +62 -0
- package/src/authorship.js +204 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +93 -0
- package/src/availability-flow.js +124 -0
- package/src/backend-request.js +191 -0
- package/src/blind-approval-extract.js +48 -0
- package/src/brand-ansi.js +14 -0
- package/src/brand-svg.js +9 -0
- package/src/browser-tools-detector.js +21 -0
- package/src/build-next-level.js +56 -0
- package/src/certifications-client.js +89 -0
- package/src/certifications-flow.js +75 -0
- package/src/certify-dimension-client.js +163 -0
- package/src/certify-dimension-flow.js +135 -0
- package/src/certify-disclaimer.js +51 -0
- package/src/certify-remediation-prompt.js +23 -0
- package/src/cli-args.js +83 -0
- package/src/command-audit.js +105 -0
- package/src/config-dir.js +120 -0
- package/src/config.js +877 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -0
- package/src/cv-file.js +62 -0
- package/src/cv-text.js +92 -0
- package/src/decisions-extract.js +43 -0
- package/src/detect-repos.js +145 -0
- package/src/detected-tools.js +15 -0
- package/src/detection-evidence.js +105 -0
- package/src/detection-noise.js +132 -0
- package/src/detectors.js +221 -0
- package/src/device-auth-store.js +100 -0
- package/src/device-login.js +120 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +30 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +148 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +124 -0
- package/src/git-log-prefetch.js +53 -0
- package/src/graph-certs.js +58 -0
- package/src/graph-generator.js +230 -0
- package/src/graph-scan.js +239 -0
- package/src/i18n.js +3341 -0
- package/src/inter-fonts.js +6 -0
- package/src/interactive-select.js +183 -0
- package/src/interview-client.js +226 -0
- package/src/interview-transcript.js +56 -0
- package/src/inventory-client.js +90 -0
- package/src/invitations-client.js +66 -0
- package/src/invitations-flow.js +50 -0
- package/src/lang-client.js +147 -0
- package/src/lang-codes.js +14 -0
- package/src/lang-flow.js +85 -0
- package/src/legal-notice.js +53 -0
- package/src/locale.js +72 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +111 -0
- package/src/mcp-ai-usage-tool.js +401 -0
- package/src/mcp-alma-tools.js +95 -0
- package/src/mcp-applications-tools.js +44 -0
- package/src/mcp-auth-form.js +480 -0
- package/src/mcp-availability-tools.js +45 -0
- package/src/mcp-certifications-tools.js +44 -0
- package/src/mcp-certify-dimension-tools.js +66 -0
- package/src/mcp-choice.js +95 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +177 -0
- package/src/mcp-find-projects-tools.js +69 -0
- package/src/mcp-install.js +275 -0
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-invitations-tools.js +44 -0
- package/src/mcp-lang-tools.js +41 -0
- package/src/mcp-onboarding-tools.js +379 -0
- package/src/mcp-portfolios-tools.js +50 -0
- package/src/mcp-profile-tools.js +48 -0
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +44 -0
- package/src/mcp-register-tools.js +441 -0
- package/src/mcp-report-tools.js +105 -0
- package/src/mcp-role-tools.js +372 -0
- package/src/mcp-saved-positions-tools.js +59 -0
- package/src/mcp-server.js +290 -0
- package/src/mcp-show-project-tools.js +52 -0
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +1171 -0
- package/src/mcp-socials-tools.js +41 -0
- package/src/memory-structure-detector.js +108 -0
- package/src/model-call-record.js +198 -0
- package/src/onboarding-availability.js +26 -0
- package/src/onboarding-client.js +374 -0
- package/src/onboarding-flow.js +1162 -0
- package/src/onboarding-funnel.js +272 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +57 -0
- package/src/open-file.js +32 -0
- package/src/osc-link.js +16 -0
- package/src/portfolio-client.js +164 -0
- package/src/portfolio-git-info.js +61 -0
- package/src/portfolios-client.js +85 -0
- package/src/portfolios-flow.js +79 -0
- package/src/profile-client.js +133 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +66 -0
- package/src/rate-client.js +81 -0
- package/src/rate-flow.js +137 -0
- package/src/register-context.js +145 -0
- package/src/render-ai-fluency.js +266 -0
- package/src/render-certification.js +177 -0
- package/src/render-html.js +140 -0
- package/src/render-report-text.js +27 -0
- package/src/render-sheet.js +261 -0
- package/src/render-terminal.js +109 -0
- package/src/render-usage-report-text.js +155 -0
- package/src/repo-context.js +215 -0
- package/src/report-gating.js +49 -0
- package/src/report-store.js +226 -0
- package/src/report-theme.js +177 -0
- package/src/report-view-client.js +50 -0
- package/src/reveal-output.js +29 -0
- package/src/roadmap-content.js +453 -0
- package/src/roadmap-personalization.js +132 -0
- package/src/roadmap-prompt.js +154 -0
- package/src/roles-client.js +112 -0
- package/src/roles-flow.js +192 -0
- package/src/sanitize-network-text.js +36 -0
- package/src/saved-positions-client.js +71 -0
- package/src/saved-positions-flow.js +50 -0
- package/src/scan-exclusions.js +50 -0
- package/src/scanner.js +624 -0
- package/src/scope-signals.js +148 -0
- package/src/secret-prompt.js +72 -0
- package/src/session-gate.js +30 -0
- package/src/session-normalizers.js +174 -0
- package/src/session-refresh.js +51 -0
- package/src/session-scan.js +475 -0
- package/src/session-signals.js +36 -0
- package/src/shakers-wordmark.js +17 -0
- package/src/share-card.js +362 -0
- package/src/share-certification.js +111 -0
- package/src/share-consent-state.js +157 -0
- package/src/share-payload.js +300 -0
- package/src/share.js +185 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +103 -0
- package/src/signup-copy.js +450 -0
- package/src/signup-language.js +21 -0
- package/src/skills-client.js +102 -0
- package/src/socials-client.js +91 -0
- package/src/socials-flow.js +83 -0
- package/src/specificity-extract.js +41 -0
- package/src/start-add-agents.js +306 -0
- package/src/start-add-portfolio.js +388 -0
- package/src/start-add-skills.js +260 -0
- package/src/stdin-ask.js +129 -0
- package/src/steering-extract.js +43 -0
- package/src/strip-internal-comments.js +45 -0
- package/src/tech-detector.js +295 -0
- package/src/templates/report-sheet.html +572 -0
- package/src/terminal-agents.js +207 -0
- package/src/terminal-format.js +85 -0
- package/src/terminal-progress.js +106 -0
- package/src/terminal-sections.js +219 -0
- package/src/tier-analysis.js +126 -0
- package/src/tier-engine.js +114 -0
- package/src/tier-projection.js +117 -0
- package/src/transcript-turns.js +49 -0
- package/src/translations-client.js +84 -0
- package/src/typewriter.js +154 -0
- package/src/update-flow.js +223 -0
- package/src/update-notifier.js +113 -0
- package/src/usage-model-calls.js +119 -0
- package/src/usage-oneshot-commands.js +180 -0
- package/src/usage-repo-scope.js +162 -0
- package/src/usage-run-steps.js +354 -0
- package/src/work-streams.js +79 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Repo-scoping for the `ai-usage` command, extracted from bin/ai-usage.js (structure refactor, issue 020).
|
|
4
|
+
|
|
5
|
+
const { isRefusedWalkRoot } = require('./scan-exclusions');
|
|
6
|
+
const {
|
|
7
|
+
detectRepos,
|
|
8
|
+
repoIdentityForCwd,
|
|
9
|
+
matchReposByFlag,
|
|
10
|
+
scopeFromSelection,
|
|
11
|
+
indexOfCurrentRepo,
|
|
12
|
+
} = require('./detect-repos');
|
|
13
|
+
const { promptSelect } = require('./prompt-select');
|
|
14
|
+
const { createStdinAsk } = require('./stdin-ask');
|
|
15
|
+
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
async function promptScope({ catalog, stdinIsTTY = true, mkAsk = createStdinAsk } = {}) {
|
|
69
|
+
const c = (catalog && catalog.cli) || {};
|
|
70
|
+
const items = [
|
|
71
|
+
{ key: SCOPE_MACHINE, label: c.reposScopeOptionMachine || 'Whole machine' },
|
|
72
|
+
{ key: SCOPE_REPO, label: c.reposScopeOptionRepo || 'Current repo' },
|
|
73
|
+
];
|
|
74
|
+
const ask = mkAsk();
|
|
75
|
+
try {
|
|
76
|
+
const picked = await promptSelect({
|
|
77
|
+
ask,
|
|
78
|
+
stdinIsTTY,
|
|
79
|
+
out: (s) => process.stdout.write(`${s}\n`),
|
|
80
|
+
items,
|
|
81
|
+
labelFor: (it) => it.label,
|
|
82
|
+
header: c.reposScopePromptHeader || '',
|
|
83
|
+
hint: c.reposScopePromptHint || '',
|
|
84
|
+
});
|
|
85
|
+
return picked ? picked.key : null;
|
|
86
|
+
} finally {
|
|
87
|
+
if (ask && typeof ask.close === 'function') ask.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
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);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { resolveRepoScope, machineWideToplevels, promptScope };
|
|
@@ -0,0 +1,354 @@
|
|
|
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
|
+
recordConsent,
|
|
13
|
+
isValidEmail,
|
|
14
|
+
normalizeEmail,
|
|
15
|
+
autoShare,
|
|
16
|
+
consentPath,
|
|
17
|
+
} = require('./share');
|
|
18
|
+
const { runConsentPrompt } = require('./consent-flow');
|
|
19
|
+
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');
|
|
41
|
+
// "My work with AI" preview, read from the hub ai-profile (same as the front).
|
|
42
|
+
const { resolveAiProfilePreview } = require('./ai-profile-preview');
|
|
43
|
+
const { renderAiProfileTerminal } = require('./render-ai-fluency');
|
|
44
|
+
const { ensureFreshSession } = require('./session-refresh');
|
|
45
|
+
const { loadAuthSession, sessionStatus } = require('./auth-session-store');
|
|
46
|
+
const { c: termColors } = require('./terminal-format');
|
|
47
|
+
const { withStaticStatus, withSpinner } = require('./terminal-progress');
|
|
48
|
+
const { computeConsentSkip } = require('./consent-skip');
|
|
49
|
+
const { getRoadmapEntry } = require('./roadmap-content');
|
|
50
|
+
const {
|
|
51
|
+
maybeSynthesizeAgents,
|
|
52
|
+
maybeEvaluateAgents,
|
|
53
|
+
maybePersonalizeRoadmap,
|
|
54
|
+
} = require('./usage-model-calls');
|
|
55
|
+
const { doBuildNextLevel } = require('./usage-oneshot-commands');
|
|
56
|
+
|
|
57
|
+
// Automatic, silent PERSISTING once consent is `granted` (ADR-007, gating revised by ADR-011: consent controls persistence only, never what's shown).
|
|
58
|
+
async function maybeAutoShare(report, maturity, root, catalog, { bypassThrottle = false } = {}) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await autoShare(report, maturity, { root, bypassThrottle });
|
|
61
|
+
if (result && result.ok && result.backend) {
|
|
62
|
+
process.stdout.write(` ${catalog.backendChain.saved(result.backend)}\n`);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
// Surface why a send was skipped/failed (user-language, stderr); null = quiet.
|
|
66
|
+
if (result && result.reason) {
|
|
67
|
+
const msg = catalog.sendStatus.reason(result.reason);
|
|
68
|
+
if (msg) process.stderr.write(` ${sendStatusColor(result.reason)}${msg}${termColors.reset}\n`);
|
|
69
|
+
}
|
|
70
|
+
return result || { ok: false, reason: null };
|
|
71
|
+
} catch {
|
|
72
|
+
try {
|
|
73
|
+
const msg = catalog.sendStatus.reason('error');
|
|
74
|
+
if (msg) process.stderr.write(` ${termColors.danger}${msg}${termColors.reset}\n`);
|
|
75
|
+
} catch { /* never break the local report over a notice */ }
|
|
76
|
+
return { ok: false, reason: 'error' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const SEND_WARN_REASONS = new Set(['throttled', 'email-unverified', 'no-email', 'no-endpoint-configured']);
|
|
81
|
+
function sendStatusColor(reason) {
|
|
82
|
+
return SEND_WARN_REASONS.has(reason) ? termColors.warning : termColors.danger;
|
|
83
|
+
}
|
|
84
|
+
|
|
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
|
+
// The identity gate + local (no-network, no-LLM) agent-usage signal.
|
|
99
|
+
function prepareIdentityAndUsage({ report, catalog, quiet = false }) {
|
|
100
|
+
const hasAgents = Array.isArray(report.agents) && report.agents.length > 0;
|
|
101
|
+
const gate = reportGateForIdentity();
|
|
102
|
+
|
|
103
|
+
// --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 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
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).
|
|
114
|
+
const state = loadConsentState();
|
|
115
|
+
const decision = getConsentDecision(state);
|
|
116
|
+
const consentSkip = computeConsentSkip({
|
|
117
|
+
decision,
|
|
118
|
+
emailVerified: state ? state.emailVerified : undefined,
|
|
119
|
+
stdinIsTTY: !!process.stdin.isTTY,
|
|
120
|
+
consentFilePath: consentPath(),
|
|
121
|
+
catalog,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let aiConsentGranted;
|
|
125
|
+
// A REUSED grant (skip/--json) carries a stale consent email; a fresh prompt re-anchors itself.
|
|
126
|
+
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);
|
|
130
|
+
reusedStoredConsent = true;
|
|
131
|
+
} 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`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
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 });
|
|
161
|
+
}
|
|
162
|
+
return aiConsentGranted;
|
|
163
|
+
}
|
|
164
|
+
|
|
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
|
+
async function renderAndFinalize({ report, maturity, lang, opts, root, gate, catalog, bypassThrottle = false }) {
|
|
256
|
+
if (opts.json) {
|
|
257
|
+
process.stdout.write(JSON.stringify({ report, maturity }, null, 2) + '\n');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Issue 103: revealed by SECTIONS instead of landing in one write.
|
|
262
|
+
if (gate.sessionStatus === 'expired') {
|
|
263
|
+
process.stdout.write(`\n ${termColors.warning}${catalog.terminal.sessionExpiredNotice}${termColors.reset}\n`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await revealText(renderTerminal(report, maturity, lang, { showRoadmap: opts.roadmap, gate }) + '\n', {
|
|
267
|
+
stream: process.stdout,
|
|
268
|
+
animate: opts.noAnimation ? false : null,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (opts.save) {
|
|
272
|
+
try {
|
|
273
|
+
persistFootprint({ root, report, maturity });
|
|
274
|
+
} catch {
|
|
275
|
+
// Never break the local run over a failed state write.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// "Construir el siguiente nivel ahora" (issue 021): optional, only when
|
|
280
|
+
// explicitly requested — never part of a normal run otherwise.
|
|
281
|
+
if (opts.buildNextLevel) {
|
|
282
|
+
doBuildNextLevel(root, maturity, opts.force, catalog);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Auto-send at the end (bypassThrottle only true on the register path).
|
|
286
|
+
const shareResult = await maybeAutoShare(report, maturity, root, catalog, { bypassThrottle });
|
|
287
|
+
|
|
288
|
+
// "My work with AI" preview, best-effort after the send; never blocks/breaks the run.
|
|
289
|
+
await maybeRenderAiProfile({ report, maturity, lang, root, save: opts.save, submitOk: !!(shareResult && shareResult.ok), catalog });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A `ready` profile always shows; otherwise the "being evaluated"/setup-only note only shows when this run's submit succeeded.
|
|
293
|
+
function shouldRenderAiProfile({ submitOk, status } = {}) {
|
|
294
|
+
return status === 'ready' || !!submitOk;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Previews the hub "My work with AI" profile (matrix + vision E + howIWork F +
|
|
298
|
+
// traction). Shared source with the MCP tool. Setup-only without session/consent.
|
|
299
|
+
async function maybeRenderAiProfile({ report, maturity, lang, root, save, submitOk = true, catalog }, deps = {}) {
|
|
300
|
+
const {
|
|
301
|
+
resolve = resolveAiProfilePreview,
|
|
302
|
+
refreshToken = async () => {
|
|
303
|
+
const fresh = await ensureFreshSession(process.env);
|
|
304
|
+
return fresh ? { accessToken: fresh.accessToken, hubAccessToken: fresh.hubAccessToken } : null;
|
|
305
|
+
},
|
|
306
|
+
spin = withSpinner,
|
|
307
|
+
out = process.stdout,
|
|
308
|
+
loadSession = loadAuthSession,
|
|
309
|
+
consentDecision = () => getConsentDecision(loadConsentState()),
|
|
310
|
+
} = deps;
|
|
311
|
+
try {
|
|
312
|
+
const session = loadSession();
|
|
313
|
+
const hasSession = sessionStatus(session) === 'active';
|
|
314
|
+
const consentGranted = consentDecision() === 'granted';
|
|
315
|
+
if (!hasSession || !consentGranted) return;
|
|
316
|
+
|
|
317
|
+
const result = await spin(
|
|
318
|
+
catalog.terminal.aiProfileGenerating,
|
|
319
|
+
() => resolve({ session, endpoint: getAiProfileEndpoint() }, { refreshToken }),
|
|
320
|
+
);
|
|
321
|
+
// A failed submit means nothing is in flight — don't tease "being evaluated"/setup-only.
|
|
322
|
+
if (!shouldRenderAiProfile({ submitOk, status: result && result.status })) return;
|
|
323
|
+
|
|
324
|
+
if (result.status === 'ready') {
|
|
325
|
+
// Re-persist so the HTML report and later state reads show the preview too.
|
|
326
|
+
report.aiWorkPreview = result.preview;
|
|
327
|
+
if (save) {
|
|
328
|
+
try {
|
|
329
|
+
persistFootprint({ root, report, maturity });
|
|
330
|
+
} catch {
|
|
331
|
+
// never break the run over a state re-write
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const block = renderAiProfileTerminal(result, { lang, c: termColors });
|
|
335
|
+
if (block) out.write(`\n${block}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
out.write(`\n ${catalog.terminal.aiProfilePendingRetry}\n`);
|
|
340
|
+
} catch {
|
|
341
|
+
// Never break the local run over the (optional) preview.
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
performScan,
|
|
347
|
+
prepareIdentityAndUsage,
|
|
348
|
+
resolveAiConsent,
|
|
349
|
+
runAggregation,
|
|
350
|
+
runModelCalls,
|
|
351
|
+
renderAndFinalize,
|
|
352
|
+
shouldRenderAiProfile,
|
|
353
|
+
maybeRenderAiProfile,
|
|
354
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const { isGitRepo } = require('./git-activity');
|
|
5
|
+
const { takePrefetchedGitLog } = require('./git-log-prefetch');
|
|
6
|
+
|
|
7
|
+
// Deterministic WORK-STREAM structure for the project where `shakers ai-usage` runs (hybrid evaluation, Slice 2).
|
|
8
|
+
|
|
9
|
+
const GIT_TIMEOUT_MS = 15000;
|
|
10
|
+
const SESSION_GAP_SECONDS = 4 * 3600;
|
|
11
|
+
const SECONDS_PER_DAY = 86400;
|
|
12
|
+
|
|
13
|
+
const WORK_STREAMS_LOG_ARGS = ['log', '--no-merges', '--pretty=format:%at'];
|
|
14
|
+
|
|
15
|
+
function runGitTimestamps(root) {
|
|
16
|
+
const prefetched = takePrefetchedGitLog(root, WORK_STREAMS_LOG_ARGS);
|
|
17
|
+
if (prefetched !== undefined) return prefetched;
|
|
18
|
+
try {
|
|
19
|
+
return execFileSync(
|
|
20
|
+
'git',
|
|
21
|
+
['-C', root, ...WORK_STREAMS_LOG_ARGS],
|
|
22
|
+
{
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
timeout: GIT_TIMEOUT_MS,
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function round2(n) {
|
|
35
|
+
return Math.round(n * 100) / 100;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function collectWorkStreams(root) {
|
|
39
|
+
if (!isGitRepo(root)) return null;
|
|
40
|
+
|
|
41
|
+
const log = runGitTimestamps(root);
|
|
42
|
+
if (log === null) return null;
|
|
43
|
+
|
|
44
|
+
const timestamps = log
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map((line) => Number.parseInt(line.trim(), 10))
|
|
47
|
+
.filter((n) => Number.isFinite(n))
|
|
48
|
+
.sort((a, b) => a - b);
|
|
49
|
+
if (timestamps.length === 0) return null;
|
|
50
|
+
|
|
51
|
+
const streams = [];
|
|
52
|
+
let current = null;
|
|
53
|
+
for (const ts of timestamps) {
|
|
54
|
+
if (current && ts - current.last <= SESSION_GAP_SECONDS) {
|
|
55
|
+
current.last = ts;
|
|
56
|
+
current.count += 1;
|
|
57
|
+
} else {
|
|
58
|
+
current = { first: ts, last: ts, count: 1 };
|
|
59
|
+
streams.push(current);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const streamCount = streams.length;
|
|
64
|
+
const spanDaysOf = (s) => Math.floor((s.last - s.first) / SECONDS_PER_DAY);
|
|
65
|
+
const multiDayStreams = streams.filter((s) => spanDaysOf(s) >= 1).length;
|
|
66
|
+
const maxStreamSpanDays = streams.reduce(
|
|
67
|
+
(max, s) => Math.max(max, spanDaysOf(s)),
|
|
68
|
+
0,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
streamCount,
|
|
73
|
+
multiDayStreams,
|
|
74
|
+
avgCommitsPerSession: round2(timestamps.length / streamCount),
|
|
75
|
+
maxStreamSpanDays,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { collectWorkStreams, SESSION_GAP_SECONDS, WORK_STREAMS_LOG_ARGS };
|