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.
- package/README.md +56 -1203
- package/bin/ai-usage.js +32 -58
- package/bin/login.js +10 -10
- package/bin/mcp.js +17 -12
- package/bin/mcpb.js +1 -1
- package/bin/onboarding.js +3 -2
- package/bin/register.js +19 -5
- package/bin/report-html.js +2 -2
- package/bin/shakers-cli.js +352 -0
- package/bin/shakers.js +20 -344
- package/bin/share.js +1 -1
- package/bin/superadmin.js +5 -5
- package/bin/version.js +3 -0
- package/package.json +12 -3
- package/src/account-marker.js +40 -0
- package/src/account-trace.js +23 -0
- package/src/agent-category.js +1 -1
- package/src/agent-evaluation.js +3 -3
- package/src/agent-org-chart.js +5 -5
- package/src/agent-synthesis.js +3 -3
- package/src/agent-usage.js +1 -1
- package/src/agents-client.js +4 -4
- package/src/ai-usage-review.js +101 -0
- package/src/ai.js +1 -1
- package/src/ansi.js +1 -1
- package/src/auth-client.js +18 -4
- package/src/auth-method-picker.js +1 -1
- package/src/auth-methods.js +1 -1
- package/src/auth-session-store.js +18 -8
- package/src/authorship-mismatch.js +1 -1
- package/src/authorship.js +2 -2
- package/src/automations-detector.js +1 -1
- package/src/backend-request.js +5 -3
- package/src/brand-ansi.js +1 -1
- package/src/browser-tools-detector.js +1 -1
- package/src/certify-disclaimer.js +1 -1
- package/src/certify-remediation-prompt.js +1 -1
- package/src/cli-args.js +2 -2
- package/src/command-audit.js +8 -5
- package/src/config-dir.js +1 -1
- package/src/config.js +114 -48
- package/src/consent-flow.js +7 -6
- package/src/consent-skip.js +1 -1
- package/src/detection-evidence.js +2 -2
- package/src/detection-noise.js +1 -1
- package/src/detectors.js +1 -1
- package/src/device-login.js +12 -1
- package/src/email-verification.js +3 -3
- package/src/env-paths.js +1 -1
- package/src/find-projects-client.js +12 -0
- package/src/find-projects-flow.js +1 -13
- package/src/git-activity.js +1 -1
- package/src/graph-certs.js +3 -3
- package/src/graph-scan.js +9 -9
- package/src/i18n.js +148 -138
- package/src/interactive-select.js +1 -1
- package/src/interview-client.js +6 -1
- package/src/interview-transcript.js +1 -1
- package/src/legal-notice.js +1 -1
- package/src/maturity.js +6 -6
- package/src/mcp-ai-usage-result-tool.js +34 -12
- package/src/mcp-ai-usage-tool.js +282 -163
- package/src/mcp-auth-form.js +7 -3
- package/src/mcp-detector.js +3 -3
- package/src/mcp-device-login-tools.js +2 -1
- package/src/mcp-find-projects-tools.js +1 -2
- package/src/mcp-install.js +279 -44
- package/src/mcp-instructions.js +1 -1
- package/src/mcp-onboarding-tools.js +4 -4
- package/src/mcp-prompts.js +4 -4
- package/src/mcp-register-tools.js +1 -0
- package/src/mcp-role-tools.js +24 -1
- package/src/mcp-server.js +5 -1
- package/src/mcp-signup-client.js +2 -2
- package/src/mcp-signup-tools.js +39 -9
- package/src/memory-structure-detector.js +1 -1
- package/src/model-call-record.js +2 -2
- package/src/onboarding-flow.js +63 -42
- package/src/onboarding-funnel.js +153 -20
- package/src/open-file.js +1 -1
- package/src/osc-link.js +1 -1
- package/src/portfolio-client.js +11 -1
- package/src/portfolio-git-info.js +1 -1
- package/src/profile-flow.js +1 -1
- package/src/profile-switch.js +5 -12
- package/src/render-certification.js +10 -10
- package/src/render-html.js +6 -6
- package/src/render-report-text.js +1 -1
- package/src/render-sheet.js +10 -10
- package/src/render-terminal.js +6 -6
- package/src/repo-scope.js +132 -0
- package/src/report-gating.js +5 -3
- package/src/report-store.js +13 -8
- package/src/roadmap-content.js +5 -5
- package/src/roadmap-personalization.js +3 -3
- package/src/roadmap-prompt.js +2 -2
- package/src/roles-flow.js +8 -5
- package/src/scan-exclusions.js +1 -1
- package/src/scanner.js +14 -14
- package/src/secret-prompt.js +1 -1
- package/src/session-gate.js +1 -1
- package/src/session-refresh.js +33 -3
- package/src/session-scan.js +1 -1
- package/src/shakers-wordmark.js +1 -1
- package/src/share-card.js +2 -2
- package/src/share-certification.js +4 -4
- package/src/share-consent-state.js +9 -9
- package/src/share-payload.js +7 -7
- package/src/share.js +7 -7
- package/src/signup-client.js +1 -1
- package/src/signup-copy.js +177 -2
- package/src/skills-client.js +1 -1
- package/src/start-add-agents.js +2 -2
- package/src/start-add-portfolio.js +3 -13
- package/src/start-add-skills.js +2 -2
- package/src/stdin-ask.js +16 -4
- package/src/strip-internal-comments.js +1 -1
- package/src/tech-detector.js +2 -2
- package/src/templates/report-sheet.html +11 -13
- package/src/terminal-agents.js +8 -8
- package/src/terminal-format.js +3 -3
- package/src/terminal-sections.js +11 -11
- package/src/tier-engine.js +3 -3
- package/src/tier-projection.js +2 -2
- package/src/transcript-turns.js +1 -1
- package/src/typewriter.js +1 -1
- package/src/usage-model-calls.js +6 -6
- package/src/usage-oneshot-commands.js +5 -5
- package/src/usage-repo-scope.js +11 -129
- package/src/usage-run-steps.js +82 -181
- package/src/usage-scan.js +235 -0
- package/src/work-streams.js +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isRefusedWalkRoot } = require('./scan-exclusions');
|
|
4
|
+
const {
|
|
5
|
+
detectRepos,
|
|
6
|
+
repoIdentityForCwd,
|
|
7
|
+
matchReposByFlag,
|
|
8
|
+
scopeFromSelection,
|
|
9
|
+
indexOfCurrentRepo,
|
|
10
|
+
} = require('./detect-repos');
|
|
11
|
+
|
|
12
|
+
const MAX_MACHINE_WIDE_REPOS = 5;
|
|
13
|
+
const SCOPE_MACHINE = 'machine';
|
|
14
|
+
const SCOPE_REPO = 'repo';
|
|
15
|
+
|
|
16
|
+
function currentRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
17
|
+
try {
|
|
18
|
+
return repoIdentityFn(cwd).toplevel || cwd;
|
|
19
|
+
} catch {
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 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).
|
|
25
|
+
function cwdRepoToplevel(cwd, repoIdentityFn = repoIdentityForCwd) {
|
|
26
|
+
if (isRefusedWalkRoot(cwd)) return null;
|
|
27
|
+
try {
|
|
28
|
+
return repoIdentityFn(cwd).toplevel || null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pure: builds the machine-wide root set.
|
|
35
|
+
function machineWideToplevels(cwdRepoTop, detectedRepos, cap = MAX_MACHINE_WIDE_REPOS) {
|
|
36
|
+
const ordered = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const push = (t) => {
|
|
39
|
+
if (typeof t !== 'string' || !t) return;
|
|
40
|
+
if (isRefusedWalkRoot(t)) return;
|
|
41
|
+
if (seen.has(t)) return;
|
|
42
|
+
seen.add(t);
|
|
43
|
+
ordered.push(t);
|
|
44
|
+
};
|
|
45
|
+
push(cwdRepoTop);
|
|
46
|
+
for (const r of detectedRepos || []) push(r && r.toplevel);
|
|
47
|
+
const capped = ordered.slice(0, cap);
|
|
48
|
+
return { toplevels: capped, total: ordered.length, capped: ordered.length > capped.length };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cwdOnlyScope(repos, cwdToplevel) {
|
|
52
|
+
const idx = indexOfCurrentRepo(repos, cwdToplevel);
|
|
53
|
+
if (idx === -1) {
|
|
54
|
+
return { selectedCwds: new Set(), toplevels: [cwdToplevel], mode: 'cwd' };
|
|
55
|
+
}
|
|
56
|
+
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, [idx]);
|
|
57
|
+
return {
|
|
58
|
+
selectedCwds,
|
|
59
|
+
toplevels: selectedToplevels.length ? selectedToplevels : [cwdToplevel],
|
|
60
|
+
mode: 'cwd',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Resolve which repos feed the AI-usage evaluation.
|
|
65
|
+
async function resolveRepoScope({
|
|
66
|
+
opts,
|
|
67
|
+
cwd,
|
|
68
|
+
injectedAsk,
|
|
69
|
+
catalog,
|
|
70
|
+
detectReposFn = detectRepos,
|
|
71
|
+
repoIdentityFn = repoIdentityForCwd,
|
|
72
|
+
chooseScope = null,
|
|
73
|
+
isTTY = !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
74
|
+
note = () => {},
|
|
75
|
+
}) {
|
|
76
|
+
const cwdToplevel = currentRepoToplevel(cwd, repoIdentityFn);
|
|
77
|
+
const cwdRepoTop = cwdRepoToplevel(cwd, repoIdentityFn);
|
|
78
|
+
const detect = () => {
|
|
79
|
+
try {
|
|
80
|
+
return detectReposFn(process.env);
|
|
81
|
+
} catch {
|
|
82
|
+
return { repos: [], unassignedSessionCount: 0 };
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 1. Explicit repo list always wins.
|
|
87
|
+
if (opts.repos) {
|
|
88
|
+
const { repos, unassignedSessionCount } = detect();
|
|
89
|
+
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
90
|
+
const { indices, unmatched } = matchReposByFlag(repos, opts.repos);
|
|
91
|
+
if (unmatched.length > 0) {
|
|
92
|
+
process.stderr.write(` ${catalog.cli.reposFlagUnmatched(unmatched.join(', '))}\n`);
|
|
93
|
+
}
|
|
94
|
+
if (indices.length > 0) {
|
|
95
|
+
const { selectedCwds, selectedToplevels } = scopeFromSelection(repos, indices);
|
|
96
|
+
return { selectedCwds, toplevels: [...new Set(selectedToplevels)], mode: 'flag' };
|
|
97
|
+
}
|
|
98
|
+
return cwdOnlyScope(repos, cwdToplevel);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const wantsMachine = opts.allRepos || opts.machine || opts.scope === SCOPE_MACHINE;
|
|
102
|
+
const wantsRepo = opts.repo || opts.scope === SCOPE_REPO;
|
|
103
|
+
|
|
104
|
+
let choice = null;
|
|
105
|
+
if (wantsMachine) choice = SCOPE_MACHINE;
|
|
106
|
+
else if (wantsRepo) choice = SCOPE_REPO;
|
|
107
|
+
else if (chooseScope && isTTY && !injectedAsk && !opts.json) {
|
|
108
|
+
choice = await chooseScope({ catalog, stdinIsTTY: true });
|
|
109
|
+
}
|
|
110
|
+
if (choice === null) choice = SCOPE_REPO;
|
|
111
|
+
|
|
112
|
+
if (choice === SCOPE_MACHINE) {
|
|
113
|
+
const { repos, unassignedSessionCount } = detect();
|
|
114
|
+
note(catalog.cli.reposDetectedNote(repos.length, unassignedSessionCount));
|
|
115
|
+
const mw = machineWideToplevels(cwdRepoTop, repos);
|
|
116
|
+
if (mw.capped) {
|
|
117
|
+
process.stderr.write(`\n ${catalog.cli.reposScopeCapped(mw.toplevels.length, mw.total)}\n`);
|
|
118
|
+
}
|
|
119
|
+
note(catalog.cli.reposScopeAll);
|
|
120
|
+
return {
|
|
121
|
+
selectedCwds: null,
|
|
122
|
+
toplevels: mw.toplevels.length ? mw.toplevels : [cwdToplevel],
|
|
123
|
+
mode: 'all',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Current repo.
|
|
128
|
+
const { repos } = detect();
|
|
129
|
+
return cwdOnlyScope(repos, cwdToplevel);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { resolveRepoScope, machineWideToplevels, SCOPE_MACHINE, SCOPE_REPO };
|
package/src/report-gating.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { loadAuthSession, sessionStatus, isPlatformTalent } = require('./auth-session-store');
|
|
4
4
|
const { getProfile } = require('./config');
|
|
5
|
+
const { hasAccountTrace } = require('./account-trace');
|
|
5
6
|
|
|
6
|
-
// THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY
|
|
7
|
+
// THE ONE LAYER THAT DECIDES WHAT THE REPORT SHOWS ACCORDING TO IDENTITY.
|
|
7
8
|
|
|
8
9
|
// Pure: identity in, policy out.
|
|
9
|
-
function resolveReportGate({ loggedIn = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
|
|
10
|
+
function resolveReportGate({ loggedIn = false, accountTrace = false, status = null, email = null, userType = null, profile = 'talent' } = {}) {
|
|
10
11
|
const showEverything = !loggedIn;
|
|
11
12
|
const resolvedProfile = profile === 'external' ? 'external' : 'talent';
|
|
12
13
|
return Object.freeze({
|
|
@@ -16,7 +17,7 @@ function resolveReportGate({ loggedIn = false, status = null, email = null, user
|
|
|
16
17
|
email: loggedIn && typeof email === 'string' && email ? email : null,
|
|
17
18
|
// --roadmap AND its server-side computation/egress (see bin/report.js): a
|
|
18
19
|
// logged-in Talent's roadmap is not merely hidden, it is not requested.
|
|
19
|
-
showRoadmap: showEverything,
|
|
20
|
+
showRoadmap: showEverything && !accountTrace,
|
|
20
21
|
// Per-agent "how to improve this agent" tips in the footprint report.
|
|
21
22
|
showAgentSuggestions: showEverything,
|
|
22
23
|
// The copyable remediation prompt in a `certify` verdict.
|
|
@@ -35,6 +36,7 @@ function reportGateForIdentity(env = process.env, now = Date.now()) {
|
|
|
35
36
|
const status = sessionStatus(session, now);
|
|
36
37
|
return resolveReportGate({
|
|
37
38
|
loggedIn: status === 'active',
|
|
39
|
+
accountTrace: hasAccountTrace(env),
|
|
38
40
|
status,
|
|
39
41
|
email: session ? session.email : null,
|
|
40
42
|
userType: session ? session.userType : null,
|
package/src/report-store.js
CHANGED
|
@@ -14,8 +14,8 @@ const SCHEMA_VERSION = 2;
|
|
|
14
14
|
function configDir() {
|
|
15
15
|
return getConfigDir(process.env);
|
|
16
16
|
}
|
|
17
|
-
function statePath() {
|
|
18
|
-
return path.join(
|
|
17
|
+
function statePath(env = process.env) {
|
|
18
|
+
return path.join(getConfigDir(env), 'report-state.json');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Stable, filesystem-safe per-project file name derived from the absolute path.
|
|
@@ -54,10 +54,10 @@ function migrateFromV1(parsed) {
|
|
|
54
54
|
return { schemaVersion: SCHEMA_VERSION, updatedAt: (parsed && parsed.updatedAt) || null, projects };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function loadState() {
|
|
57
|
+
function loadState(env = process.env) {
|
|
58
58
|
let parsed;
|
|
59
59
|
try {
|
|
60
|
-
parsed = JSON.parse(fs.readFileSync(statePath(), 'utf8'));
|
|
60
|
+
parsed = JSON.parse(fs.readFileSync(statePath(env), 'utf8'));
|
|
61
61
|
} catch {
|
|
62
62
|
return freshState();
|
|
63
63
|
}
|
|
@@ -84,7 +84,7 @@ function getOrCreateProject(state, absRoot) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
// Backward-compat: a project persisted before the two-backend chain
|
|
87
|
-
// existed
|
|
87
|
+
// existed.
|
|
88
88
|
if (!state.projects[absRoot].backendAcceptance) {
|
|
89
89
|
state.projects[absRoot].backendAcceptance = {};
|
|
90
90
|
}
|
|
@@ -98,7 +98,7 @@ function renderProjectHtml(project, lang, gate) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/* ---------- persist (state only) + materialize (render HTML) ---------- */
|
|
101
|
-
//
|
|
101
|
+
// Split: `footprint` and `certify` now PERSIST state only (no HTML file, no printed link).
|
|
102
102
|
|
|
103
103
|
function stampAndSaveState(state, absRoot) {
|
|
104
104
|
state.schemaVersion = SCHEMA_VERSION;
|
|
@@ -154,6 +154,10 @@ function recordBackendAcceptance({ root, kind, backend }) {
|
|
|
154
154
|
return stampAndSaveState(state, absRoot);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function hasRecordedUpload(env = process.env) {
|
|
158
|
+
return Object.values(loadState(env).projects).some((p) => !!(p && p.backendAcceptance && Object.keys(p.backendAcceptance).length));
|
|
159
|
+
}
|
|
160
|
+
|
|
157
161
|
function loadFootprint({ root } = {}) {
|
|
158
162
|
const absRoot = path.resolve(root || process.cwd());
|
|
159
163
|
const state = loadState();
|
|
@@ -179,12 +183,12 @@ function materializeProjectReport({ root, lang }) {
|
|
|
179
183
|
const state = loadState();
|
|
180
184
|
const project = state.projects[absRoot];
|
|
181
185
|
if (!hasRenderableData(project)) return { hasData: false };
|
|
182
|
-
//
|
|
186
|
+
// The shareable HTML is gated by identity AT GENERATION TIME — this is the moment the `report` command produces the file, so this is where the gate is resolved.
|
|
183
187
|
const gate = reportGateForIdentity();
|
|
184
188
|
return { hasData: true, ...writeProjectHtml(project, absRoot, lang, gate) };
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
// `upsertFootprint` AND `upsertCertification` WERE HERE, and
|
|
191
|
+
// `upsertFootprint` AND `upsertCertification` WERE HERE, and were retired.
|
|
188
192
|
|
|
189
193
|
function certKey(item) {
|
|
190
194
|
if (item && item.skillId != null) return `id:${item.skillId}`;
|
|
@@ -220,6 +224,7 @@ module.exports = {
|
|
|
220
224
|
loadAgentClassifications,
|
|
221
225
|
loadFootprint,
|
|
222
226
|
recordBackendAcceptance,
|
|
227
|
+
hasRecordedUpload,
|
|
223
228
|
materializeProjectReport,
|
|
224
229
|
certKey,
|
|
225
230
|
SCHEMA_VERSION,
|
package/src/roadmap-content.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Curated tier roadmap content
|
|
3
|
+
// Curated tier roadmap content.
|
|
4
4
|
|
|
5
5
|
const TIER_JUMPS_ES = {
|
|
6
6
|
T0: {
|
|
@@ -201,13 +201,13 @@ const T7_TERMINAL_ES = {
|
|
|
201
201
|
whatRemains: 'El tier ya no sube, pero el equipamiento no es el trabajo. A partir de aquí el margen está en usar lo que tienes, no en instalar más: documentar tu setup para que otros lo repliquen, podar lo que no usas, y afilar cada agente/comando con el trabajo real. La escala mide cómo tienes montado el banco; que salgan buenas piezas de él depende de ti.',
|
|
202
202
|
consolidationSteps: [
|
|
203
203
|
'Afina tus hooks y agentes con el trabajo real: recorta lo que estorba, acota los matchers y ajusta las tools/límites de cada agente a su rol.',
|
|
204
|
-
'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate
|
|
204
|
+
'Poda lo que no usas: revisa qué assets llevas tiempo sin tocar (el CLI expone recency/mtime de forma informativa, no como gate) y elimina lo redundante.',
|
|
205
205
|
'Mantén vivos tus ficheros de contexto (CLAUDE.md, reglas, comandos): actualízalos cuando cambien el stack o las convenciones, o dejan de ayudar.',
|
|
206
206
|
'Empaqueta y contribuye tus skills/comandos a un repo compartido del equipo para que tu setup escale más allá de tu máquina.',
|
|
207
207
|
'Documenta tu organigrama de agentes y compártelo como referencia interna para que otros lo repliquen.',
|
|
208
208
|
'Mide el impacto real (tiempo ahorrado, tareas delegadas de principio a fin) y usa ese dato para decidir qué merece seguir en tu banco.',
|
|
209
209
|
],
|
|
210
|
-
honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada
|
|
210
|
+
honestyNote: 'Estar en T7 significa que tienes el banco de trabajo mejor equipado que el CLI sabe medir. No significa que seas quien mejor usa la IA: el dato es una proxy de setup, indicativa y no verificada. No se lee como "calidad IA del Talent".',
|
|
211
211
|
};
|
|
212
212
|
|
|
213
213
|
const TIER_JUMPS_EN = {
|
|
@@ -409,13 +409,13 @@ const T7_TERMINAL_EN = {
|
|
|
409
409
|
whatRemains: "The tier no longer rises, but equipment isn't the work. From here the margin is in using what you have, not installing more: documenting your setup so others can replicate it, pruning what you don't use, and sharpening each agent/command with real work. The ladder measures how your bench is set up; whether good pieces come out of it is up to you.",
|
|
410
410
|
consolidationSteps: [
|
|
411
411
|
"Sharpen your hooks and agents against real work: trim what gets in the way, scope the matchers, and tune each agent's tools/limits to its role.",
|
|
412
|
-
"Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate
|
|
412
|
+
"Prune what you don't use: review which assets you haven't touched in a while (the CLI exposes recency/mtime informatively, not as a gate) and drop the redundant ones.",
|
|
413
413
|
'Keep your context files (CLAUDE.md, rules, commands) alive: update them when the stack or conventions change, or they stop helping.',
|
|
414
414
|
'Package and contribute your skills/commands to a shared team repo so your setup scales beyond your own machine.',
|
|
415
415
|
'Document your agent org chart and share it as an internal reference so others can replicate it.',
|
|
416
416
|
'Measure the real impact (time saved, tasks delegated end to end) and use that to decide what deserves to stay on your bench.',
|
|
417
417
|
],
|
|
418
|
-
honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified
|
|
418
|
+
honestyNote: 'Being at T7 means you have the best-equipped bench the CLI knows how to measure. It does not mean you\'re the one who uses AI best: the data is a proxy for setup, indicative and unverified. It\'s not read as "the Talent\'s AI quality".',
|
|
419
419
|
};
|
|
420
420
|
|
|
421
421
|
const CONTENT_BY_LANG = {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { requestBackend } = require('./backend-request');
|
|
4
4
|
const { reporterFor, REASON } = require('./model-call-record');
|
|
5
5
|
|
|
6
|
-
// Roadmap personalization client
|
|
6
|
+
// Roadmap personalization client.
|
|
7
7
|
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 8000;
|
|
9
9
|
|
|
@@ -38,7 +38,7 @@ function buildRoadmapPersonalizationRequest(curatedEntry, tierResult, report, lo
|
|
|
38
38
|
mistakes: curatedEntry.commonMistakes,
|
|
39
39
|
},
|
|
40
40
|
signals: buildRoadmapSignals(report, tierResult.signals),
|
|
41
|
-
//
|
|
41
|
+
// Detected report language for the personalized prose.
|
|
42
42
|
...(locale === 'es' || locale === 'en' ? { locale } : {}),
|
|
43
43
|
...(consentGranted === true ? { traceContentConsent: true } : {}),
|
|
44
44
|
};
|
|
@@ -66,7 +66,7 @@ function isValidPersonalizedRoadmap(parsed, curated) {
|
|
|
66
66
|
|
|
67
67
|
// Requests the roadmap personalization endpoint.
|
|
68
68
|
async function requestRoadmapPersonalization(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
|
|
69
|
-
//
|
|
69
|
+
// See agent-synthesis.js#requestAgentSynthesis for the contract.
|
|
70
70
|
const outcome = reporterFor(onOutcome, { timeoutMs });
|
|
71
71
|
if (!endpoint) {
|
|
72
72
|
outcome.skip(REASON.NO_ENDPOINT);
|
package/src/roadmap-prompt.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { tierName: getTierName } = require('./i18n');
|
|
4
4
|
|
|
5
|
-
// Deterministic, ready-to-paste "implementation prompt" (
|
|
5
|
+
// Deterministic, ready-to-paste "implementation prompt" ("next steps -> prompt").
|
|
6
6
|
|
|
7
7
|
const TEMPLATES = {
|
|
8
8
|
es: {
|
|
@@ -65,7 +65,7 @@ function buildImplementationPrompt(entry, report, maturity, lang) {
|
|
|
65
65
|
const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
|
|
66
66
|
const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
|
|
67
67
|
const tierKey = (maturity && maturity.tierKey) || entry.tierKey || '';
|
|
68
|
-
//
|
|
68
|
+
// NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
|
|
69
69
|
const tierName = getTierName(tierKey, lang);
|
|
70
70
|
|
|
71
71
|
const lines = [];
|
package/src/roles-flow.js
CHANGED
|
@@ -88,6 +88,7 @@ async function runAddRole({ io, session, catalog, deps = makeRolesDeps() }) {
|
|
|
88
88
|
// deduped, for setting/switching the MAIN role.
|
|
89
89
|
async function fetchHeldRoles({ deps, hubAccessToken }) {
|
|
90
90
|
const held = [];
|
|
91
|
+
const assigned = [];
|
|
91
92
|
let mainClusterId = null;
|
|
92
93
|
const certEndpoint = deps.getMyCertificationsEndpoint();
|
|
93
94
|
if (certEndpoint) {
|
|
@@ -99,10 +100,11 @@ async function fetchHeldRoles({ deps, hubAccessToken }) {
|
|
|
99
100
|
}
|
|
100
101
|
const assignedEndpoint = deps.getAssignedClustersEndpoint();
|
|
101
102
|
if (assignedEndpoint) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
103
|
+
const res = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
|
|
104
|
+
if (res.ok) assigned.push(...(res.clusters || []));
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
held.push(...assigned);
|
|
107
|
+
return { held: dedupeByCluster(held), assigned, mainClusterId };
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// `change-role`: switch the MAIN role to one of the roles the talent already holds.
|
|
@@ -130,12 +132,12 @@ async function runChangeMainRole({ io, session, catalog, deps = makeRolesDeps()
|
|
|
130
132
|
|
|
131
133
|
// The onboarding final step: pick a MAIN role from the roles resolved for the
|
|
132
134
|
// talent (interview recommendations + any held). Degrades gracefully to a skip.
|
|
133
|
-
async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps() }) {
|
|
135
|
+
async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps(), onRolesShown = () => {} }) {
|
|
134
136
|
const r = catalog.roles;
|
|
135
137
|
const hubAccessToken = session ? session.hubAccessToken : null;
|
|
136
138
|
|
|
137
139
|
io.section(r.mainRoleTitle);
|
|
138
|
-
const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
|
|
140
|
+
const { held, assigned, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
|
|
139
141
|
|
|
140
142
|
// Prefer the talent's own/recommended roles; fall back to the catalog for a
|
|
141
143
|
// fresh talent so a main role can always be set (picking one adds it first).
|
|
@@ -150,6 +152,7 @@ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRole
|
|
|
150
152
|
io.notify(r.mainRolePending);
|
|
151
153
|
return { ok: true, chosen: null };
|
|
152
154
|
}
|
|
155
|
+
onRolesShown({ suggested: assigned.filter((c) => c.type === 'RECOMMENDED').map((c) => c.clusterId), clustersCount: assigned.length });
|
|
153
156
|
const choice = await chooseRole({
|
|
154
157
|
io, roles, header: r.mainRolePrompt, hint: r.selectHint, initialClusterId: mainClusterId,
|
|
155
158
|
});
|
package/src/scan-exclusions.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
-
// Directory names never descended into when walking a project tree
|
|
5
|
+
// Directory names never descended into when walking a project tree.
|
|
6
6
|
const EXCLUDED_DIRS = new Set([
|
|
7
7
|
'node_modules', 'dist', 'build', '.git', 'vendor', 'coverage', '.next', 'out',
|
|
8
8
|
// Python
|
package/src/scanner.js
CHANGED
|
@@ -163,7 +163,7 @@ function aggregateFootprint(paths) {
|
|
|
163
163
|
return { bytes, files };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// Recency = ONLY mtime of already-detected config files/dirs
|
|
166
|
+
// Recency = ONLY mtime of already-detected config files/dirs; never reads
|
|
167
167
|
// log/history content.
|
|
168
168
|
|
|
169
169
|
function latestMtime(paths) {
|
|
@@ -317,8 +317,8 @@ const probes = {
|
|
|
317
317
|
}),
|
|
318
318
|
};
|
|
319
319
|
|
|
320
|
-
// Agent org-chart counts
|
|
321
|
-
//
|
|
320
|
+
// Agent org-chart counts: deterministic, structure+names only. PROJECT ∪ HOME
|
|
321
|
+
// since the tier engine's T6 (`agents >= 2`) needs the full picture.
|
|
322
322
|
|
|
323
323
|
function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
324
324
|
const home = getHomeDir();
|
|
@@ -330,7 +330,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
330
330
|
commands:
|
|
331
331
|
countFiles(path.join(root, '.claude', 'commands'), '.md') +
|
|
332
332
|
countFiles(path.join(home, '.claude', 'commands'), '.md'),
|
|
333
|
-
//
|
|
333
|
+
// The third copy of the MCP paths lived here.
|
|
334
334
|
mcpServers: (mcp && typeof mcp.total === 'number' ? mcp.total : 0),
|
|
335
335
|
hooks:
|
|
336
336
|
countJsonKeys(path.join(root, '.claude', 'settings.json'), 'hooks') +
|
|
@@ -342,7 +342,7 @@ function agentOrgChartCounts(root, agentsCount, mcp) {
|
|
|
342
342
|
|
|
343
343
|
function scan(options = {}) {
|
|
344
344
|
const root = options.root || process.cwd();
|
|
345
|
-
// MCP detection runs FIRST
|
|
345
|
+
// MCP detection runs FIRST, because the per-tool depth probes now read their `mcpServers` count from it.
|
|
346
346
|
const mcp = detectMcpServers(root);
|
|
347
347
|
const scanCtx = { mcp };
|
|
348
348
|
|
|
@@ -366,7 +366,7 @@ function scan(options = {}) {
|
|
|
366
366
|
// Config footprint: ONLY the aggregated size in bytes and file count, never paths or names.
|
|
367
367
|
footprint: null,
|
|
368
368
|
// Recency: ONLY the date derived from the most recent mtime among its
|
|
369
|
-
// already-detected config files
|
|
369
|
+
// already-detected config files. Never content, logs or history.
|
|
370
370
|
recency: { lastModified: null, daysSinceModified: null, bucket: null },
|
|
371
371
|
// Version: only if the tool was detected via a binary on PATH; runs
|
|
372
372
|
// THAT already-detected binary, with `--version` only.
|
|
@@ -397,7 +397,7 @@ function scan(options = {}) {
|
|
|
397
397
|
|
|
398
398
|
const detectedTools = tools.filter((t) => t.detected);
|
|
399
399
|
|
|
400
|
-
// Deterministic (no-LLM) agent org chart
|
|
400
|
+
// Deterministic (no-LLM) agent org chart: structure + names
|
|
401
401
|
// only (name, wired tools, model, hierarchy). Never descriptions/prompts.
|
|
402
402
|
const agents = parseAgentOrgChart(root);
|
|
403
403
|
|
|
@@ -405,7 +405,7 @@ function scan(options = {}) {
|
|
|
405
405
|
|
|
406
406
|
const rawDependencyNames = detectRawDependencyNames(root);
|
|
407
407
|
|
|
408
|
-
// Deterministic (no-LLM) memory STRUCTURE
|
|
408
|
+
// Deterministic (no-LLM) memory STRUCTURE: import count, nesting depth, sections, size — from known context files (project ∪ home).
|
|
409
409
|
const memory = analyzeMemoryStructure(root);
|
|
410
410
|
|
|
411
411
|
const automations = detectAutomations(root);
|
|
@@ -436,19 +436,19 @@ function scan(options = {}) {
|
|
|
436
436
|
categories: [...new Set(detectedTools.map((t) => t.category))],
|
|
437
437
|
},
|
|
438
438
|
tools,
|
|
439
|
-
// Agent org chart
|
|
439
|
+
// Agent org chart: structure + names only, never content.
|
|
440
440
|
agents,
|
|
441
441
|
agentCounts: agentOrgChartCounts(root, agents.length, mcp),
|
|
442
442
|
// Projects where each detected AI tool has been used (skill-code-
|
|
443
|
-
// Project technologies
|
|
443
|
+
// Project technologies: dependency manifest names only.
|
|
444
444
|
technologies,
|
|
445
|
-
// MCP servers by name/category
|
|
445
|
+
// MCP servers by name/category: never the raw config.
|
|
446
446
|
mcp,
|
|
447
|
-
// Memory structure
|
|
447
|
+
// Memory structure: imports/nesting/sections/size, never text.
|
|
448
448
|
memory,
|
|
449
|
-
// Automations
|
|
449
|
+
// Automations: script/scheduler signals, never raw content.
|
|
450
450
|
automations,
|
|
451
|
-
// Browser tools
|
|
451
|
+
// Browser tools: composition over technologies + mcp.
|
|
452
452
|
browserTools,
|
|
453
453
|
};
|
|
454
454
|
}
|
package/src/secret-prompt.js
CHANGED
package/src/session-gate.js
CHANGED
|
@@ -20,7 +20,7 @@ async function hasUsableSession(env = process.env, deps = {}) {
|
|
|
20
20
|
|
|
21
21
|
const session = load(env);
|
|
22
22
|
if (sessionStatus(session, now) === 'active') return true;
|
|
23
|
-
if (!session || !session.cookie) return false;
|
|
23
|
+
if (!session || (!session.cookie && !session.sessionToken)) return false;
|
|
24
24
|
|
|
25
25
|
let refreshed = null;
|
|
26
26
|
try { refreshed = await refresh(env, { now }); } catch { refreshed = null; }
|
package/src/session-refresh.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// On-demand hub-JWT minting for an email (better-auth) session.
|
|
4
4
|
|
|
5
5
|
const REFRESH_MARGIN_MS = 120 * 1000;
|
|
6
|
+
const REVOKED_STATUSES = new Set([401, 403]);
|
|
6
7
|
|
|
7
8
|
function jwtStillFresh(accessTokenExpiresAt, now, marginMs) {
|
|
8
9
|
const exp = Date.parse(String(accessTokenExpiresAt || ''));
|
|
@@ -14,6 +15,7 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
14
15
|
const {
|
|
15
16
|
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
16
17
|
saveAuthSession = require('./auth-session-store').saveAuthSession,
|
|
18
|
+
clearAuthSession = require('./auth-session-store').clearAuthSession,
|
|
17
19
|
getAuthTokenEndpoint = require('./config').getAuthTokenEndpoint,
|
|
18
20
|
requestAuthToken = require('./auth-client').requestAuthToken,
|
|
19
21
|
now = Date.now(),
|
|
@@ -22,13 +24,17 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
22
24
|
|
|
23
25
|
const session = loadAuthSession(env);
|
|
24
26
|
if (!session) return null;
|
|
25
|
-
if (!session.cookie) return session;
|
|
27
|
+
if (!session.cookie && !session.sessionToken) return session;
|
|
26
28
|
if (session.accessToken && jwtStillFresh(session.accessTokenExpiresAt, now, marginMs)) return session;
|
|
27
29
|
|
|
28
30
|
const endpoint = getAuthTokenEndpoint(env);
|
|
29
31
|
if (!endpoint) return session;
|
|
30
32
|
|
|
31
|
-
const tok = await requestAuthToken({ cookie: session.cookie }, { endpoint });
|
|
33
|
+
const tok = await requestAuthToken({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint });
|
|
34
|
+
if (!tok.ok && REVOKED_STATUSES.has(tok.status)) {
|
|
35
|
+
clearAuthSession(env);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
32
38
|
if (!tok.ok) return session;
|
|
33
39
|
|
|
34
40
|
try {
|
|
@@ -48,4 +54,28 @@ async function ensureFreshSession(env = process.env, deps = {}) {
|
|
|
48
54
|
return loadAuthSession(env);
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
function webOrigin(url) {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(url).origin;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function endSession(env = process.env, deps = {}) {
|
|
66
|
+
const {
|
|
67
|
+
loadAuthSession = require('./auth-session-store').loadAuthSession,
|
|
68
|
+
clearAuthSession = require('./auth-session-store').clearAuthSession,
|
|
69
|
+
getSignOutEndpoint = require('./config').getSignOutEndpoint,
|
|
70
|
+
getTalentProfileUrl = require('./config').getTalentProfileUrl,
|
|
71
|
+
requestSignOut = require('./auth-client').requestSignOut,
|
|
72
|
+
} = deps;
|
|
73
|
+
const session = loadAuthSession(env);
|
|
74
|
+
if (session && (session.sessionToken || session.cookie)) {
|
|
75
|
+
await requestSignOut({ cookie: session.cookie, sessionToken: session.sessionToken }, { endpoint: getSignOutEndpoint(env), origin: webOrigin(getTalentProfileUrl(env)) });
|
|
76
|
+
}
|
|
77
|
+
clearAuthSession(env);
|
|
78
|
+
return session !== null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { ensureFreshSession, endSession, jwtStillFresh, REFRESH_MARGIN_MS };
|
package/src/session-scan.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
parseAiderHistory,
|
|
13
13
|
} = require('./session-normalizers');
|
|
14
14
|
|
|
15
|
-
// GLOBAL, machine-wide inventory of AI coding-tool SESSIONS
|
|
15
|
+
// GLOBAL, machine-wide inventory of AI coding-tool SESSIONS.
|
|
16
16
|
|
|
17
17
|
const MAX_FILES_PER_TOOL = 2000;
|
|
18
18
|
const MAX_BYTES_PER_FILE = 8 * 1024 * 1024;
|
package/src/shakers-wordmark.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the
|
|
3
|
+
// Shakers hand-drawn "shakers" WORDMARK, inlined VERBATIM from the Shakers text-logo SVG (viewBox 0 0 87 18, 7 glyph <path>s).
|
|
4
4
|
|
|
5
5
|
const WORDMARK_VIEWBOX = { w: 87, h: 18 };
|
|
6
6
|
|
package/src/share-card.js
CHANGED
|
@@ -70,7 +70,7 @@ function loadProjectFootprint(root, { load = loadState } = {}) {
|
|
|
70
70
|
score: m.score,
|
|
71
71
|
level: m.level,
|
|
72
72
|
levelKey: m.key,
|
|
73
|
-
//
|
|
73
|
+
// Setup Level for the card pill (replaces the 0-4 band). Prefer the
|
|
74
74
|
// persisted `setupLevel`; derive from the tier for older stored reports.
|
|
75
75
|
setupLevelKey: (m.setupLevel && m.setupLevel.key)
|
|
76
76
|
|| (typeof m.tier === 'number' ? setupLevelForTier(m.tier).key : 'none'),
|
|
@@ -95,7 +95,7 @@ function buildCardModel(fp) {
|
|
|
95
95
|
const en = getCatalog('en');
|
|
96
96
|
const tierKey = fp.tierKey;
|
|
97
97
|
const tierName = (en.tierNames && en.tierNames[tierKey]) || tierKey;
|
|
98
|
-
//
|
|
98
|
+
// Setup Level label for the pill (English-fixed like the tier name).
|
|
99
99
|
const setupKey = fp.setupLevelKey || 'none';
|
|
100
100
|
const bandName = (en.setupLevels && en.setupLevels[setupKey] && en.setupLevels[setupKey].label) || '';
|
|
101
101
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Certification-persistence payload
|
|
3
|
+
// Certification-persistence payload, extracted from src/share.js.
|
|
4
4
|
|
|
5
5
|
const { scrubSecrets } = require('./agent-synthesis');
|
|
6
6
|
|
|
7
7
|
const CERTIFY_SEND_THROTTLE_MS = 60 * 60 * 1000;
|
|
8
8
|
const CERT_SCHEMA_VERSION = 1;
|
|
9
9
|
|
|
10
|
-
// Strict whitelist: the analyzed RESULT + observability provenance
|
|
10
|
+
// Strict whitelist: the analyzed RESULT + observability provenance, rebuilt field-by-field (never spread).
|
|
11
11
|
const CERT_DIMENSION_KEYS = ['idiomatic', 'correctness', 'depth', 'structure', 'testing'];
|
|
12
12
|
function deriveDimensionScores(dimensions) {
|
|
13
13
|
if (!dimensions || typeof dimensions !== 'object') return null;
|
|
@@ -74,13 +74,13 @@ function deriveCertificationPayload(items, { repository = null, commitRange = nu
|
|
|
74
74
|
note: typeof f.note === 'string' && f.note ? scrubSecrets(f.note) : null,
|
|
75
75
|
}))
|
|
76
76
|
: [],
|
|
77
|
-
//
|
|
77
|
+
// Rubric dimensions (0-4 or null) — the inputs the server used to
|
|
78
78
|
// compute the deterministic score. Rebuilt key-by-key, never spread.
|
|
79
79
|
dimensionScores: deriveDimensionScores(i.result.dimensions),
|
|
80
80
|
};
|
|
81
81
|
const certifyToken = certifyEvidenceTokenOf(i);
|
|
82
82
|
if (certifyToken) assessment.evidenceToken = certifyToken;
|
|
83
|
-
//
|
|
83
|
+
// The attested combined level, bound ONLY when an
|
|
84
84
|
// interview ran. Added conditionally so an item without one is byte-identical.
|
|
85
85
|
const token = interviewEvidenceTokenOf(i);
|
|
86
86
|
if (token) assessment.interviewEvidenceToken = token;
|