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.
- package/LICENSE +59 -0
- package/README.md +1175 -0
- 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 +81 -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 +120 -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 +283 -0
- package/bin/mcp.js +98 -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 +325 -0
- package/bin/report-html.js +134 -0
- package/bin/save.js +77 -0
- package/bin/shakers.js +344 -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/package.json +40 -0
- 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 +214 -0
- package/src/authorship.js +203 -0
- package/src/automations-detector.js +170 -0
- package/src/availability-client.js +92 -0
- package/src/availability-flow.js +105 -0
- package/src/backend-request.js +170 -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 +217 -0
- package/src/certify-dimension-flow.js +193 -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/config-dir.js +120 -0
- package/src/config.js +778 -0
- package/src/consent-flow.js +115 -0
- package/src/consent-skip.js +27 -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 +118 -0
- package/src/email-verification.js +152 -0
- package/src/env-paths.js +16 -0
- package/src/find-projects-cache.js +89 -0
- package/src/find-projects-client.js +138 -0
- package/src/find-projects-flow.js +164 -0
- package/src/git-activity.js +123 -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 +3264 -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 +51 -0
- package/src/markdown-ansi.js +50 -0
- package/src/maturity.js +111 -0
- package/src/mcp-ai-usage-result-tool.js +109 -0
- package/src/mcp-ai-usage-tool.js +333 -0
- package/src/mcp-alma-tools.js +91 -0
- package/src/mcp-applications-tools.js +42 -0
- package/src/mcp-ask-tools.js +50 -0
- package/src/mcp-auth-form.js +282 -0
- package/src/mcp-availability-tools.js +43 -0
- package/src/mcp-certifications-tools.js +42 -0
- package/src/mcp-certify-dimension-tools.js +58 -0
- package/src/mcp-detector.js +223 -0
- package/src/mcp-device-login-tools.js +173 -0
- package/src/mcp-find-projects-tools.js +67 -0
- package/src/mcp-install.js +245 -0
- package/src/mcp-invitations-tools.js +42 -0
- package/src/mcp-lang-tools.js +39 -0
- package/src/mcp-onboarding-tools.js +388 -0
- package/src/mcp-portfolios-tools.js +46 -0
- package/src/mcp-profile-tools.js +46 -0
- package/src/mcp-rate-tools.js +42 -0
- package/src/mcp-register-tools.js +517 -0
- package/src/mcp-report-tools.js +101 -0
- package/src/mcp-role-tools.js +112 -0
- package/src/mcp-saved-positions-tools.js +55 -0
- package/src/mcp-server.js +170 -0
- package/src/mcp-show-project-tools.js +50 -0
- package/src/mcp-socials-tools.js +39 -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 +364 -0
- package/src/onboarding-flow.js +1078 -0
- package/src/onboarding-livekit-client.js +102 -0
- package/src/onboarding-status-client.js +42 -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 +125 -0
- package/src/profile-flow.js +115 -0
- package/src/profile-switch.js +46 -0
- package/src/prompt-select.js +29 -0
- package/src/rate-client.js +82 -0
- package/src/rate-flow.js +137 -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 +114 -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 +612 -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-refresh.js +51 -0
- package/src/session-scan.js +336 -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 +184 -0
- package/src/show-project-client.js +58 -0
- package/src/show-project-flow.js +119 -0
- package/src/signup-client.js +91 -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 +199 -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/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 +338 -0
- package/src/work-streams.js +74 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isValidEmail, recordConsent } = require('./share');
|
|
4
|
+
const {
|
|
5
|
+
getEmailVerificationRequestUrl,
|
|
6
|
+
getEmailVerificationVerifyUrl,
|
|
7
|
+
} = require('./config');
|
|
8
|
+
const { runEmailVerification } = require('./email-verification');
|
|
9
|
+
const { renderLegalNotice } = require('./legal-notice');
|
|
10
|
+
|
|
11
|
+
// Consent prompt (talents-ai-score, ADR-011 — revises issue 006 / ADR-007's disclosure wall; ADR-051 revises ADR-011's OWN ordering in turn, see below).
|
|
12
|
+
|
|
13
|
+
const MAX_ATTEMPTS = 5;
|
|
14
|
+
|
|
15
|
+
const YES_RE = /^(y|yes|s|si|sí)$/i;
|
|
16
|
+
const NO_RE = /^(n|no)$/i;
|
|
17
|
+
|
|
18
|
+
async function askYesNo(ask, question, catalog) {
|
|
19
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
20
|
+
const raw = String(await ask(question)).trim();
|
|
21
|
+
if (YES_RE.test(raw)) return true;
|
|
22
|
+
if (NO_RE.test(raw)) return false;
|
|
23
|
+
process.stdout.write(` ${catalog.consent.invalidAnswer}\n`);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function askEmail(ask, catalog, question = catalog.consent.emailPrompt) {
|
|
29
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
30
|
+
const raw = String(await ask(question)).trim();
|
|
31
|
+
if (isValidEmail(raw)) return raw;
|
|
32
|
+
process.stdout.write(` ${catalog.consent.invalidEmail}\n`);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function defaultVerifyEmail({ email, ask, catalog, env = process.env }) {
|
|
38
|
+
return runEmailVerification({
|
|
39
|
+
email,
|
|
40
|
+
ask,
|
|
41
|
+
catalog,
|
|
42
|
+
requestUrl: getEmailVerificationRequestUrl(env),
|
|
43
|
+
verifyUrl: getEmailVerificationVerifyUrl(env),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Runs the short consent-to-persist prompt + (if accepted) email collection + (skill-code-certification / ADR-006) EMAIL-OWNERSHIP VERIFICATION.
|
|
48
|
+
async function runConsentPrompt({
|
|
49
|
+
ask,
|
|
50
|
+
catalog,
|
|
51
|
+
env = process.env,
|
|
52
|
+
verifyEmail = defaultVerifyEmail,
|
|
53
|
+
loggedIn = false,
|
|
54
|
+
sessionEmail = null,
|
|
55
|
+
drainBeforeAsk = false,
|
|
56
|
+
profile = 'talent',
|
|
57
|
+
}) {
|
|
58
|
+
const c = catalog.consent;
|
|
59
|
+
// Presentation only (talents-ai-score): framed by src/legal-notice.js on a
|
|
60
|
+
// real TTY, byte-identical (no heading, no rule) otherwise.
|
|
61
|
+
process.stdout.write(`\n ${renderLegalNotice(c.persistIntro, catalog)}\n`);
|
|
62
|
+
|
|
63
|
+
if (drainBeforeAsk && typeof ask.drain === 'function') ask.drain();
|
|
64
|
+
|
|
65
|
+
const accepted = await askYesNo(ask, c.persistQuestion, catalog);
|
|
66
|
+
if (accepted === null) {
|
|
67
|
+
process.stdout.write(` ${c.notObtained}\n\n`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!accepted) {
|
|
72
|
+
recordConsent('denied');
|
|
73
|
+
process.stdout.write(` ${c.deniedSaved}\n\n`);
|
|
74
|
+
return 'denied';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (loggedIn) {
|
|
78
|
+
if (sessionEmail && isValidEmail(sessionEmail)) {
|
|
79
|
+
// Identity already proven by the login session — no email prompt, no OTP.
|
|
80
|
+
recordConsent('granted', sessionEmail, { verified: true });
|
|
81
|
+
process.stdout.write(` ${c.grantedSaved(sessionEmail)}\n\n`);
|
|
82
|
+
return 'granted';
|
|
83
|
+
}
|
|
84
|
+
// Logged in, but this session has no cached email — only happens against an older certs deployment that omitted `email` from the login response (see auth-session-store.js).
|
|
85
|
+
process.stdout.write(` ${c.noSessionEmail}\n\n`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const email = await askEmail(ask, catalog, profile === 'external' ? c.emailPromptExternal : undefined);
|
|
90
|
+
if (!email) {
|
|
91
|
+
process.stdout.write(` ${c.notObtained}\n\n`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (profile === 'external') {
|
|
96
|
+
recordConsent('granted', email, { verified: false });
|
|
97
|
+
process.stdout.write(` ${c.grantedSavedExternal(email)}\n\n`);
|
|
98
|
+
return 'granted';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prove email ownership BEFORE persisting anything (ADR-006).
|
|
102
|
+
const verification = await verifyEmail({ email, ask, catalog, env });
|
|
103
|
+
if (verification && verification.verified) {
|
|
104
|
+
recordConsent('granted', email, { verified: true });
|
|
105
|
+
process.stdout.write(` ${c.grantedSaved(email)}\n\n`);
|
|
106
|
+
return 'granted';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Not verified: persist nothing, re-ask next run. runEmailVerification
|
|
110
|
+
// already printed the specific reason (cancelled / technical / exhausted).
|
|
111
|
+
process.stdout.write(` ${c.notObtained}\n\n`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { runConsentPrompt, defaultVerifyEmail };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Explains WHY the consent-to-persist prompt is skipped (or might not complete) this run, instead of doing so silently (talents-ai-score, DX).
|
|
4
|
+
function computeConsentSkip({ decision, emailVerified, stdinIsTTY, consentFilePath, catalog } = {}) {
|
|
5
|
+
const c = catalog && catalog.consent;
|
|
6
|
+
|
|
7
|
+
// Only a TERMINAL decision is skippable: an explicit DECLINE, or a GRANT whose email ownership was actually verified.
|
|
8
|
+
const terminal =
|
|
9
|
+
decision === 'denied' ||
|
|
10
|
+
(decision === 'granted' && emailVerified !== false);
|
|
11
|
+
|
|
12
|
+
if (terminal) {
|
|
13
|
+
const message = c && typeof c.skipAlreadyDecided === 'function'
|
|
14
|
+
? c.skipAlreadyDecided(decision, consentFilePath)
|
|
15
|
+
: null;
|
|
16
|
+
return { skip: true, message };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!stdinIsTTY) {
|
|
20
|
+
const message = c ? c.nonInteractiveWarning || null : null;
|
|
21
|
+
return { skip: false, message };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { skip: false, message: null };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { computeConsentSkip };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { eachHumanText } = require('./transcript-turns');
|
|
4
|
+
const { scrubSecrets } = require('./agent-synthesis');
|
|
5
|
+
|
|
6
|
+
const MAX_EXCERPTS = 20;
|
|
7
|
+
const MAX_EXCERPT_LEN = 200;
|
|
8
|
+
|
|
9
|
+
const DECISION_RE =
|
|
10
|
+
/\b(decid\w*|decision|decisión|vamos a|vamos con|voy con|let'?s|we'?ll|we will|prefer|prefiero|option|opción|elij\w*|choose|chose|choice|pick|go with|descarta\w*|opta|trade-?off|instead of|en vez de|en lugar de|rather than|because|porque|reason|razón)\b/i;
|
|
11
|
+
|
|
12
|
+
function makeDecisionsAccumulator() {
|
|
13
|
+
let decisionExchangeCount = 0;
|
|
14
|
+
const excerpts = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
return {
|
|
17
|
+
add(text) {
|
|
18
|
+
if (!DECISION_RE.test(text)) return;
|
|
19
|
+
decisionExchangeCount += 1;
|
|
20
|
+
if (excerpts.length >= MAX_EXCERPTS) return;
|
|
21
|
+
const scrubbed = scrubSecrets(text)
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim()
|
|
24
|
+
.slice(0, MAX_EXCERPT_LEN);
|
|
25
|
+
if (scrubbed && !seen.has(scrubbed)) {
|
|
26
|
+
seen.add(scrubbed);
|
|
27
|
+
excerpts.push(scrubbed);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
result() {
|
|
31
|
+
if (decisionExchangeCount === 0) return null;
|
|
32
|
+
return { decisionExchangeCount, redactedDecisionExcerpts: excerpts };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectDecisions(env = process.env, selectedCwds = null) {
|
|
38
|
+
const acc = makeDecisionsAccumulator();
|
|
39
|
+
for (const text of eachHumanText(env, { selectedCwds })) acc.add(text);
|
|
40
|
+
return acc.result();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { collectDecisions, makeDecisionsAccumulator };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
toolSources,
|
|
7
|
+
listSessionFiles,
|
|
8
|
+
readSessionObjects,
|
|
9
|
+
resolveFileCwd,
|
|
10
|
+
} = require('./session-scan');
|
|
11
|
+
const { normalizeRemote } = require('./authorship');
|
|
12
|
+
|
|
13
|
+
const GIT_TIMEOUT_MS = 15000;
|
|
14
|
+
|
|
15
|
+
function git(cwd, args) {
|
|
16
|
+
try {
|
|
17
|
+
const out = execFileSync('git', ['-C', cwd, ...args], {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
timeout: GIT_TIMEOUT_MS,
|
|
20
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
21
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
22
|
+
});
|
|
23
|
+
return out ? out.trim() : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function repoIdentityForCwd(cwd, gitRunner = git) {
|
|
30
|
+
const toplevel = gitRunner(cwd, ['rev-parse', '--show-toplevel']) || null;
|
|
31
|
+
const remoteRaw = gitRunner(cwd, ['config', '--get', 'remote.origin.url']);
|
|
32
|
+
const remote = remoteRaw && remoteRaw.trim() ? normalizeRemote(remoteRaw.trim()) : null;
|
|
33
|
+
return { toplevel, remote };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionCwdCounts(env = process.env) {
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
let unassigned = 0;
|
|
39
|
+
for (const source of toolSources(env)) {
|
|
40
|
+
for (const file of listSessionFiles(source.dir)) {
|
|
41
|
+
const cwd = resolveFileCwd(readSessionObjects(file));
|
|
42
|
+
if (cwd === null) {
|
|
43
|
+
unassigned += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
counts.set(cwd, (counts.get(cwd) || 0) + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { counts, unassigned };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function labelFor(remote, groupKey) {
|
|
53
|
+
if (remote) return remote;
|
|
54
|
+
return path.basename(groupKey) || groupKey;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detectRepos(env = process.env, { gitRunner = git } = {}) {
|
|
58
|
+
const { counts, unassigned } = sessionCwdCounts(env);
|
|
59
|
+
|
|
60
|
+
const byGroup = new Map();
|
|
61
|
+
for (const [cwd, sessionCount] of counts) {
|
|
62
|
+
const { toplevel, remote } = repoIdentityForCwd(cwd, gitRunner);
|
|
63
|
+
const groupKey = toplevel || cwd;
|
|
64
|
+
let group = byGroup.get(groupKey);
|
|
65
|
+
if (!group) {
|
|
66
|
+
group = {
|
|
67
|
+
toplevel: toplevel || null,
|
|
68
|
+
remote: remote || null,
|
|
69
|
+
cwds: new Set(),
|
|
70
|
+
sessionCount: 0,
|
|
71
|
+
resolved: !!toplevel,
|
|
72
|
+
};
|
|
73
|
+
byGroup.set(groupKey, group);
|
|
74
|
+
}
|
|
75
|
+
group.cwds.add(cwd);
|
|
76
|
+
group.sessionCount += sessionCount;
|
|
77
|
+
if (!group.remote && remote) group.remote = remote;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const repos = [...byGroup.entries()]
|
|
81
|
+
.map(([groupKey, g]) => ({
|
|
82
|
+
id: g.remote || g.toplevel || groupKey,
|
|
83
|
+
toplevel: g.toplevel,
|
|
84
|
+
remote: g.remote,
|
|
85
|
+
label: labelFor(g.remote, groupKey),
|
|
86
|
+
sessionCount: g.sessionCount,
|
|
87
|
+
cwds: [...g.cwds].sort(),
|
|
88
|
+
resolved: g.resolved,
|
|
89
|
+
}))
|
|
90
|
+
.sort(
|
|
91
|
+
(a, b) => b.sessionCount - a.sessionCount || a.label.localeCompare(b.label),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { repos, unassignedSessionCount: unassigned };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeFlagToken(token) {
|
|
98
|
+
return String(token || '').trim().toLowerCase().replace(/\/+$/, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function repoMatchesToken(repo, token) {
|
|
102
|
+
const t = normalizeFlagToken(token);
|
|
103
|
+
if (!t) return false;
|
|
104
|
+
return [repo.id, repo.remote, repo.toplevel, repo.label, path.basename(repo.toplevel || repo.id)]
|
|
105
|
+
.filter((x) => typeof x === 'string' && x)
|
|
106
|
+
.some((x) => normalizeFlagToken(x) === t || normalizeFlagToken(x).endsWith(`/${t}`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function matchReposByFlag(repos, tokens) {
|
|
110
|
+
const indices = new Set();
|
|
111
|
+
const unmatched = [];
|
|
112
|
+
for (const token of tokens || []) {
|
|
113
|
+
const idx = repos.findIndex((r) => repoMatchesToken(r, token));
|
|
114
|
+
if (idx === -1) unmatched.push(token);
|
|
115
|
+
else indices.add(idx);
|
|
116
|
+
}
|
|
117
|
+
return { indices: [...indices].sort((a, b) => a - b), unmatched };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scopeFromSelection(repos, indices) {
|
|
121
|
+
const selectedCwds = new Set();
|
|
122
|
+
const selectedToplevels = [];
|
|
123
|
+
for (const i of indices) {
|
|
124
|
+
const repo = repos[i];
|
|
125
|
+
if (!repo) continue;
|
|
126
|
+
for (const c of repo.cwds) selectedCwds.add(c);
|
|
127
|
+
selectedToplevels.push(repo.toplevel || repo.id);
|
|
128
|
+
}
|
|
129
|
+
return { selectedCwds, selectedToplevels };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function indexOfCurrentRepo(repos, cwdToplevel) {
|
|
133
|
+
if (!cwdToplevel) return -1;
|
|
134
|
+
return repos.findIndex((r) => r.toplevel === cwdToplevel);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
detectRepos,
|
|
139
|
+
repoIdentityForCwd,
|
|
140
|
+
sessionCwdCounts,
|
|
141
|
+
matchReposByFlag,
|
|
142
|
+
repoMatchesToken,
|
|
143
|
+
scopeFromSelection,
|
|
144
|
+
indexOfCurrentRepo,
|
|
145
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Single source of truth for turning a tools array ({id, name, detected}) into what the report shows.
|
|
4
|
+
|
|
5
|
+
function detectedTools(tools) {
|
|
6
|
+
return Array.isArray(tools) ? tools.filter((t) => t && t.detected === true) : [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function detectedToolNames(tools) {
|
|
10
|
+
return detectedTools(tools)
|
|
11
|
+
.map((t) => (t && (t.name || t.id)) || null)
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { detectedTools, detectedToolNames };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { collectAuthorship, attributeSample } = require('./authorship');
|
|
4
|
+
const { detectTechnologyManifests } = require('./tech-detector');
|
|
5
|
+
const { agentSourceFilesByName } = require('./agent-org-chart');
|
|
6
|
+
const { classifyVendored, collectProvenance } = require('./detection-noise');
|
|
7
|
+
|
|
8
|
+
// Per-CANDIDATE authorship + depth (hybrid evaluation, Slice 3c).
|
|
9
|
+
|
|
10
|
+
const SKILL_DEPTH_SATURATION = 3;
|
|
11
|
+
const AGENT_DEPTH_SATURATION = 8;
|
|
12
|
+
const DEPTH_FLOOR = 0.3;
|
|
13
|
+
|
|
14
|
+
function round2(n) {
|
|
15
|
+
return Math.round(n * 100) / 100;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Maps a raw count to (0,1]: DEPTH_FLOOR..1, so a detected-but-shallow candidate
|
|
19
|
+
// is ordered DOWN without being excluded. `null` when there is no signal.
|
|
20
|
+
function normalizedDepth(signal, saturation) {
|
|
21
|
+
if (typeof signal !== 'number' || !Number.isFinite(signal) || signal <= 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const ratio = Math.min(1, signal / saturation);
|
|
25
|
+
return round2(DEPTH_FLOOR + (1 - DEPTH_FLOOR) * ratio);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function recencyDaysForPaths(relPaths, authorship, nowSeconds) {
|
|
29
|
+
if (!Array.isArray(relPaths) || relPaths.length === 0) return null;
|
|
30
|
+
let mostRecent = null;
|
|
31
|
+
for (const p of relPaths) {
|
|
32
|
+
const t = authorship.mostRecentCommitTime(p);
|
|
33
|
+
if (typeof t === 'number' && Number.isFinite(t)) {
|
|
34
|
+
mostRecent = mostRecent === null ? t : Math.max(mostRecent, t);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (mostRecent === null) return null;
|
|
38
|
+
return Math.max(0, Math.floor((nowSeconds - mostRecent) / 86400));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Attributes a candidate's evidence files against the talent's own email via the ADR-017 gate.
|
|
42
|
+
function attributionCounts(relPaths, verifiedEmail, authorship) {
|
|
43
|
+
if (!Array.isArray(relPaths) || relPaths.length === 0) {
|
|
44
|
+
return { authoredFileCount: 0, totalFileCount: 0, authorshipKnown: false };
|
|
45
|
+
}
|
|
46
|
+
const result = attributeSample(
|
|
47
|
+
{ files: relPaths.map((p) => ({ path: p })) },
|
|
48
|
+
verifiedEmail,
|
|
49
|
+
authorship,
|
|
50
|
+
);
|
|
51
|
+
const totalFileCount = result.fileAttribution.filter(
|
|
52
|
+
(f) => f.authors.length > 0,
|
|
53
|
+
).length;
|
|
54
|
+
return {
|
|
55
|
+
authoredFileCount: result.attributableFiles.length,
|
|
56
|
+
totalFileCount,
|
|
57
|
+
authorshipKnown: authorship.available && totalFileCount > 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Builds `report.detectionEvidence` = `{ skills, agents, provenance } | null`.
|
|
62
|
+
function collectDetectionEvidence(root, verifiedEmail, report) {
|
|
63
|
+
const scanRoot = root || process.cwd();
|
|
64
|
+
const authorship = collectAuthorship(scanRoot);
|
|
65
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
66
|
+
|
|
67
|
+
const skills = detectTechnologyManifests(scanRoot).map(
|
|
68
|
+
({ tech, manifestPaths }) => ({
|
|
69
|
+
tech,
|
|
70
|
+
...attributionCounts(manifestPaths, verifiedEmail, authorship),
|
|
71
|
+
depth: normalizedDepth(manifestPaths.length, SKILL_DEPTH_SATURATION),
|
|
72
|
+
vendored: classifyVendored(manifestPaths),
|
|
73
|
+
recencyDays: recencyDaysForPaths(manifestPaths, authorship, nowSeconds),
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const sourceByName = agentSourceFilesByName(scanRoot);
|
|
78
|
+
const reportAgents =
|
|
79
|
+
report && Array.isArray(report.agents) ? report.agents : [];
|
|
80
|
+
const agents = reportAgents.map((agent) => {
|
|
81
|
+
const rel = sourceByName.get(agent.name) || null;
|
|
82
|
+
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
83
|
+
const evidencePaths = rel ? [rel] : [];
|
|
84
|
+
return {
|
|
85
|
+
name: agent.name,
|
|
86
|
+
tools,
|
|
87
|
+
model: agent.model || null,
|
|
88
|
+
...attributionCounts(evidencePaths, verifiedEmail, authorship),
|
|
89
|
+
depth: normalizedDepth(tools.length, AGENT_DEPTH_SATURATION),
|
|
90
|
+
vendored: classifyVendored(evidencePaths),
|
|
91
|
+
recencyDays: recencyDaysForPaths(evidencePaths, authorship, nowSeconds),
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (skills.length === 0 && agents.length === 0) return null;
|
|
96
|
+
return { skills, agents, provenance: collectProvenance(scanRoot) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
collectDetectionEvidence,
|
|
101
|
+
normalizedDepth,
|
|
102
|
+
SKILL_DEPTH_SATURATION,
|
|
103
|
+
AGENT_DEPTH_SATURATION,
|
|
104
|
+
DEPTH_FLOOR,
|
|
105
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Anti-noise CLASSIFICATION for detected candidates (hybrid evaluation, Slice 3 — issue 019).
|
|
8
|
+
|
|
9
|
+
const GIT_TIMEOUT_MS = 15000;
|
|
10
|
+
const README_MAX_BYTES = 8 * 1024;
|
|
11
|
+
|
|
12
|
+
// Directory segments that mark a path as vendored / generated (dependency trees, build output, virtualenvs, package caches).
|
|
13
|
+
const VENDORED_SEGMENTS = new Set([
|
|
14
|
+
// JS / general
|
|
15
|
+
'node_modules', 'bower_components', '.pnp', 'jspm_packages',
|
|
16
|
+
// generic vendoring
|
|
17
|
+
'vendor', 'vendored', 'third_party', 'third-party', 'external', 'deps',
|
|
18
|
+
// build / generated output
|
|
19
|
+
'dist', 'build', 'out', 'generated', '__generated__',
|
|
20
|
+
'.next', '.nuxt', '.output', '.svelte-kit', '.turbo', '.cache',
|
|
21
|
+
// Python
|
|
22
|
+
'.venv', 'venv', 'virtualenv', 'site-packages', '__pycache__',
|
|
23
|
+
// iOS / macOS / other package managers
|
|
24
|
+
'pods', 'carthage',
|
|
25
|
+
// JVM / Rust build output
|
|
26
|
+
'target', '.gradle',
|
|
27
|
+
// IaC provider plugins
|
|
28
|
+
'.terraform',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// True when ANY segment of the (root-relative, POSIX) path is a vendored
|
|
32
|
+
// segment. Case-insensitive so `Pods`/`pods` both match.
|
|
33
|
+
function isVendoredPath(relPath) {
|
|
34
|
+
if (typeof relPath !== 'string' || !relPath) return false;
|
|
35
|
+
return relPath
|
|
36
|
+
.split('/')
|
|
37
|
+
.some((seg) => VENDORED_SEGMENTS.has(seg.toLowerCase()));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A candidate's evidence is VENDORED-dominated when it has at least one evidence path AND every one of them is vendored.
|
|
41
|
+
function classifyVendored(evidencePaths) {
|
|
42
|
+
if (!Array.isArray(evidencePaths) || evidencePaths.length === 0) return false;
|
|
43
|
+
return evidencePaths.every(isVendoredPath);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runGit(root, args) {
|
|
47
|
+
try {
|
|
48
|
+
return execFileSync('git', ['-C', root, ...args], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
timeout: GIT_TIMEOUT_MS,
|
|
51
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// LOW-CONFIDENCE fork signal: a git remote named `upstream` alongside `origin` is the canonical local convention for a fork's parent.
|
|
60
|
+
function detectFork(root) {
|
|
61
|
+
const out = runGit(root, ['remote']);
|
|
62
|
+
if (out === null) return { fork: false, forkSignal: null };
|
|
63
|
+
const remotes = new Set(
|
|
64
|
+
out
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((l) => l.trim())
|
|
67
|
+
.filter(Boolean),
|
|
68
|
+
);
|
|
69
|
+
if (remotes.has('upstream')) {
|
|
70
|
+
return { fork: true, forkSignal: 'upstream-remote' };
|
|
71
|
+
}
|
|
72
|
+
return { fork: false, forkSignal: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Scaffolder README signatures.
|
|
76
|
+
const BOILERPLATE_MARKERS = [
|
|
77
|
+
{ id: 'create-react-app', re: /bootstrapped with \[?create react app/i },
|
|
78
|
+
{ id: 'create-next-app', re: /bootstrapped with \[?`?create-next-app/i },
|
|
79
|
+
{ id: 'vite-starter', re: /this template provides a minimal setup to get .*working in vite/i },
|
|
80
|
+
{ id: 'angular-cli', re: /generated with \[?angular ?cli/i },
|
|
81
|
+
{ id: 'nuxt-starter', re: /nuxt[\s\d]*minimal starter/i },
|
|
82
|
+
{ id: 'create-svelte', re: /(create-svelte|everything you need to build a svelte project)/i },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const README_NAMES = ['README.md', 'README', 'readme.md', 'Readme.md'];
|
|
86
|
+
|
|
87
|
+
function readReadmeHead(root) {
|
|
88
|
+
for (const name of README_NAMES) {
|
|
89
|
+
const abs = path.join(root, name);
|
|
90
|
+
try {
|
|
91
|
+
const fd = fs.openSync(abs, 'r');
|
|
92
|
+
try {
|
|
93
|
+
const buf = Buffer.alloc(README_MAX_BYTES);
|
|
94
|
+
const bytes = fs.readSync(fd, buf, 0, README_MAX_BYTES, 0);
|
|
95
|
+
return buf.toString('utf8', 0, bytes);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// try the next candidate name
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Repo-level boilerplate flag from an unmodified scaffolder README.
|
|
107
|
+
function detectBoilerplate(root) {
|
|
108
|
+
const head = readReadmeHead(root);
|
|
109
|
+
if (!head) return { boilerplate: false, boilerplateMarker: null };
|
|
110
|
+
for (const { id, re } of BOILERPLATE_MARKERS) {
|
|
111
|
+
if (re.test(head)) return { boilerplate: true, boilerplateMarker: id };
|
|
112
|
+
}
|
|
113
|
+
return { boilerplate: false, boilerplateMarker: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Repo-level provenance context for the whole scan.
|
|
117
|
+
function collectProvenance(root) {
|
|
118
|
+
const scanRoot = root || process.cwd();
|
|
119
|
+
return {
|
|
120
|
+
...detectFork(scanRoot),
|
|
121
|
+
...detectBoilerplate(scanRoot),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
VENDORED_SEGMENTS,
|
|
127
|
+
isVendoredPath,
|
|
128
|
+
classifyVendored,
|
|
129
|
+
detectFork,
|
|
130
|
+
detectBoilerplate,
|
|
131
|
+
collectProvenance,
|
|
132
|
+
};
|