shakers-cli 0.0.0-stage → 0.14.1
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,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { scrubSecrets } = require('./agent-synthesis');
|
|
4
|
+
const { requestBackend } = require('./backend-request');
|
|
5
|
+
const { reporterFor, REASON } = require('./model-call-record');
|
|
6
|
+
|
|
7
|
+
// Agent definition-quality evaluation client (ADR-016, agent evaluation).
|
|
8
|
+
|
|
9
|
+
// HOW LONG TO WAIT FOR THE AGENT EVALUATION, and why it keeps growing.
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 150000;
|
|
11
|
+
const PROMPT_VERSION = 'agent-eval-v1';
|
|
12
|
+
|
|
13
|
+
// Cap each agent definition to a prefix before sending.
|
|
14
|
+
const MAX_DEFINITION_CHARS = 2000;
|
|
15
|
+
|
|
16
|
+
// Builds the frozen-contract body from the deterministic org chart (structure) + the per-agent definition text, scrubbing each definition.
|
|
17
|
+
function buildAgentEvaluationRequest(structuralAgents, definitionsByName, locale = null, consentGranted = false) {
|
|
18
|
+
const defMap = new Map(
|
|
19
|
+
(definitionsByName || []).map((d) => [d.name, d.definition != null ? d.definition : d.description]),
|
|
20
|
+
);
|
|
21
|
+
return {
|
|
22
|
+
agents: (structuralAgents || []).map((a) => ({
|
|
23
|
+
name: a.name,
|
|
24
|
+
// Scrub first, THEN cap to the prefix — so a secret straddling the cut is
|
|
25
|
+
// already redacted and can't leak a half-matched fragment.
|
|
26
|
+
definition: scrubSecrets(defMap.get(a.name) || '').slice(0, MAX_DEFINITION_CHARS),
|
|
27
|
+
tools: Array.isArray(a.tools) ? a.tools : [],
|
|
28
|
+
model: a.model || null,
|
|
29
|
+
parent: a.parent || null,
|
|
30
|
+
})),
|
|
31
|
+
promptVersion: PROMPT_VERSION,
|
|
32
|
+
// ADR-026: detected report language for the rationale + description prose.
|
|
33
|
+
...(locale === 'es' || locale === 'en' ? { locale } : {}),
|
|
34
|
+
...(consentGranted === true ? { traceContentConsent: true } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isValidEvaluationResponse(parsed) {
|
|
39
|
+
return !!parsed && typeof parsed === 'object' && Array.isArray(parsed.evaluations);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The three classification methods the server may report; anything else is
|
|
43
|
+
// coerced to 'unclassified' (never trust an unexpected value).
|
|
44
|
+
const CLASSIFICATION_METHODS = new Set(['deterministic', 'llm', 'unclassified']);
|
|
45
|
+
const UNCLASSIFIED = Object.freeze({
|
|
46
|
+
catalogId: null,
|
|
47
|
+
category: null,
|
|
48
|
+
role: null,
|
|
49
|
+
level: null,
|
|
50
|
+
method: 'unclassified',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function normalizeClassification(c) {
|
|
54
|
+
if (!c || typeof c !== 'object') return { ...UNCLASSIFIED };
|
|
55
|
+
const catalogId = typeof c.catalogId === 'string' && c.catalogId ? c.catalogId : null;
|
|
56
|
+
if (!catalogId) return { ...UNCLASSIFIED };
|
|
57
|
+
const method = CLASSIFICATION_METHODS.has(c.method) ? c.method : 'unclassified';
|
|
58
|
+
return {
|
|
59
|
+
catalogId,
|
|
60
|
+
category: typeof c.category === 'string' && c.category ? c.category : null,
|
|
61
|
+
role: typeof c.role === 'string' && c.role ? c.role : null,
|
|
62
|
+
level: typeof c.level === 'string' && c.level ? c.level : null,
|
|
63
|
+
// A real id but method 'unclassified' would be contradictory — treat an id
|
|
64
|
+
// that arrived without a usable method as LLM-inferred (the conservative read).
|
|
65
|
+
method: method === 'unclassified' ? 'llm' : method,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bounds the improvements to a clean string[] (max 3, trimmed, non-empty).
|
|
70
|
+
function normalizeImprovements(list) {
|
|
71
|
+
return (Array.isArray(list) ? list : [])
|
|
72
|
+
.filter((s) => typeof s === 'string' && s.trim())
|
|
73
|
+
.map((s) => s.trim())
|
|
74
|
+
.slice(0, 3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// WHICH AGENTS THE CALL LEFT OUT (`omittedAgentNames`, server contract 2026-08-03).
|
|
78
|
+
function normalizeOmittedAgentNames(parsed, requestBody, evaluations) {
|
|
79
|
+
const requested = new Set(
|
|
80
|
+
(requestBody && Array.isArray(requestBody.agents) ? requestBody.agents : [])
|
|
81
|
+
.map((a) => (a && typeof a.name === 'string' ? a.name : null))
|
|
82
|
+
.filter(Boolean),
|
|
83
|
+
);
|
|
84
|
+
const answered = new Set((evaluations || []).map((e) => e.name));
|
|
85
|
+
|
|
86
|
+
const declared = (parsed && Array.isArray(parsed.omittedAgentNames) ? parsed.omittedAgentNames : [])
|
|
87
|
+
.filter((n) => typeof n === 'string' && n.trim())
|
|
88
|
+
.map((n) => n.trim())
|
|
89
|
+
.filter((n) => requested.has(n));
|
|
90
|
+
|
|
91
|
+
const derived = [...requested].filter((n) => !answered.has(n));
|
|
92
|
+
|
|
93
|
+
// Deduped, and in the order the agents were REQUESTED so the list reads the same
|
|
94
|
+
// way the report lists them (never in the order a server happened to emit).
|
|
95
|
+
const union = new Set([...declared, ...derived]);
|
|
96
|
+
return [...requested].filter((n) => union.has(n));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Normalizes each evaluation.
|
|
100
|
+
function normalizeEvaluations(list) {
|
|
101
|
+
return (Array.isArray(list) ? list : [])
|
|
102
|
+
.filter((e) => e && typeof e.name === 'string')
|
|
103
|
+
.map((e) => ({
|
|
104
|
+
name: e.name,
|
|
105
|
+
rationale: typeof e.rationale === 'string' ? e.rationale : '',
|
|
106
|
+
// ADR-026: target-language one-line description; null when the server
|
|
107
|
+
// omitted it — the caller falls back to the verbatim phrase.
|
|
108
|
+
description: typeof e.description === 'string' && e.description ? e.description : null,
|
|
109
|
+
// v4 (agent classification): closest catalog agent + how it was matched.
|
|
110
|
+
classification: normalizeClassification(e.classification),
|
|
111
|
+
// v4: 2-3 concrete improvement tips (target language).
|
|
112
|
+
improvements: normalizeImprovements(e.improvements),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Requests the agent-evaluation endpoint.
|
|
117
|
+
async function requestAgentEvaluation(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
|
|
118
|
+
const outcome = reporterFor(onOutcome, { timeoutMs });
|
|
119
|
+
if (!endpoint) {
|
|
120
|
+
outcome.skip(REASON.NO_ENDPOINT);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const promptVersion = (requestBody && requestBody.promptVersion) || PROMPT_VERSION;
|
|
125
|
+
const locale = requestBody && (requestBody.locale === 'es' || requestBody.locale === 'en')
|
|
126
|
+
? requestBody.locale
|
|
127
|
+
: null;
|
|
128
|
+
const traceContentConsent = requestBody && requestBody.traceContentConsent === true;
|
|
129
|
+
const safeBody = {
|
|
130
|
+
promptVersion,
|
|
131
|
+
agents: Array.isArray(requestBody && requestBody.agents)
|
|
132
|
+
? requestBody.agents.map((a) => ({ ...a, definition: scrubSecrets(a.definition) }))
|
|
133
|
+
: [],
|
|
134
|
+
...(locale ? { locale } : {}),
|
|
135
|
+
...(traceContentConsent ? { traceContentConsent: true } : {}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let res;
|
|
139
|
+
try {
|
|
140
|
+
res = await requestBackend({ endpoint, body: safeBody, timeoutMs, trace: outcome.hops });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
outcome.failFromError(e);
|
|
143
|
+
return null; // network error or timeout on every hop: never breaks the local report
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (res.status < 200 || res.status >= 300) {
|
|
147
|
+
outcome.fail(REASON.HTTP, { status: res.status, backend: res.backend });
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(res.raw);
|
|
154
|
+
} catch {
|
|
155
|
+
outcome.fail(REASON.MALFORMED_JSON, { status: res.status, backend: res.backend });
|
|
156
|
+
return null; // malformed (non-JSON) body
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!isValidEvaluationResponse(parsed)) {
|
|
160
|
+
outcome.fail(REASON.UNEXPECTED_SHAPE, { status: res.status, backend: res.backend });
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const evaluations = normalizeEvaluations(parsed.evaluations);
|
|
165
|
+
const omittedAgentNames = normalizeOmittedAgentNames(parsed, safeBody, evaluations);
|
|
166
|
+
// A PARTIAL ANSWER IS NOT A FAILURE, and it is not a plain success either.
|
|
167
|
+
outcome.succeed({ status: res.status, backend: res.backend, omitted: omittedAgentNames });
|
|
168
|
+
return { evaluations, promptVersion, omittedAgentNames };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
normalizeOmittedAgentNames,
|
|
173
|
+
PROMPT_VERSION,
|
|
174
|
+
MAX_DEFINITION_CHARS,
|
|
175
|
+
buildAgentEvaluationRequest,
|
|
176
|
+
requestAgentEvaluation,
|
|
177
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getHomeDir } = require('./env-paths');
|
|
6
|
+
|
|
7
|
+
// Deterministic (no-LLM) parser of the talent's AI agent org chart (talents-ai-score, ADR-009).
|
|
8
|
+
|
|
9
|
+
// Safety caps mirror scanner.js's FOOTPRINT_MAX_* (avoid pathological scans,
|
|
10
|
+
// never a product requirement).
|
|
11
|
+
const AGENTS_MAX_FILES = 500;
|
|
12
|
+
const AGENTS_MAX_DEPTH = 6;
|
|
13
|
+
|
|
14
|
+
// Only these frontmatter keys are ever captured.
|
|
15
|
+
const WHITELISTED_KEYS = new Set(['name', 'tools', 'model', 'parent']);
|
|
16
|
+
|
|
17
|
+
function listAgentMarkdownFiles(dir, depth = 0, budget = { count: 0 }) {
|
|
18
|
+
const results = [];
|
|
19
|
+
if (depth > AGENTS_MAX_DEPTH || budget.count >= AGENTS_MAX_FILES) return results;
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
return results; // no .claude/agents directory: not an error, just nothing to parse
|
|
25
|
+
}
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (budget.count >= AGENTS_MAX_FILES) break;
|
|
28
|
+
if (entry.isSymbolicLink()) continue; // never follow symlinks (mirrors scanner.js)
|
|
29
|
+
const full = path.join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
results.push(...listAgentMarkdownFiles(full, depth + 1, budget));
|
|
32
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
33
|
+
budget.count += 1;
|
|
34
|
+
results.push(full);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stripQuotes(value) {
|
|
41
|
+
const v = String(value).trim();
|
|
42
|
+
if (v.length >= 2) {
|
|
43
|
+
const first = v[0];
|
|
44
|
+
const last = v[v.length - 1];
|
|
45
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
46
|
+
return v.slice(1, -1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return v;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Minimal, dependency-free frontmatter reader (zero-dependency invariant, same as the rest of this repo).
|
|
53
|
+
function parseFrontmatter(content, { includeDescription = false } = {}) {
|
|
54
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
const lines = match[1].split(/\r?\n/);
|
|
57
|
+
const data = {};
|
|
58
|
+
const captureKeys = includeDescription
|
|
59
|
+
? new Set([...WHITELISTED_KEYS, 'description'])
|
|
60
|
+
: WHITELISTED_KEYS;
|
|
61
|
+
let i = 0;
|
|
62
|
+
|
|
63
|
+
while (i < lines.length) {
|
|
64
|
+
const line = lines[i];
|
|
65
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
66
|
+
if (!kv) {
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const key = kv[1];
|
|
71
|
+
const rawValue = kv[2].trim();
|
|
72
|
+
i += 1;
|
|
73
|
+
|
|
74
|
+
// Block scalar (`|`, `>`, `|-`, `>-`, `|+`, `>+`): this is how `description` shows up in practice.
|
|
75
|
+
if (/^[|>][-+]?$/.test(rawValue)) {
|
|
76
|
+
const blockLines = [];
|
|
77
|
+
while (i < lines.length && (lines[i].trim() === '' || /^\s+/.test(lines[i]))) {
|
|
78
|
+
blockLines.push(lines[i].trim() === '' ? '' : lines[i].replace(/^\s+/, ''));
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
if (includeDescription && key === 'description') {
|
|
82
|
+
while (blockLines.length && blockLines[blockLines.length - 1] === '') blockLines.pop();
|
|
83
|
+
data[key] = blockLines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// YAML block list continuation ("key:" on its own line, followed by
|
|
89
|
+
// "- item" lines).
|
|
90
|
+
if (rawValue === '') {
|
|
91
|
+
const items = [];
|
|
92
|
+
while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
|
|
93
|
+
items.push(stripQuotes(lines[i].replace(/^\s*-\s+/, '')));
|
|
94
|
+
i += 1;
|
|
95
|
+
}
|
|
96
|
+
if (items.length && captureKeys.has(key)) data[key] = items;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!captureKeys.has(key)) continue; // never captured (e.g. an inline description, unless explicitly requested)
|
|
101
|
+
|
|
102
|
+
// Inline YAML list: `tools: [Read, Write]`
|
|
103
|
+
if (/^\[.*\]$/.test(rawValue)) {
|
|
104
|
+
data[key] = rawValue
|
|
105
|
+
.slice(1, -1)
|
|
106
|
+
.split(',')
|
|
107
|
+
.map((s) => stripQuotes(s))
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Comma-separated scalar (the format Claude Code's own docs use):
|
|
113
|
+
// `tools: Read, Write, Bash`
|
|
114
|
+
if (key === 'tools' && rawValue.includes(',')) {
|
|
115
|
+
data[key] = rawValue
|
|
116
|
+
.split(',')
|
|
117
|
+
.map((s) => stripQuotes(s))
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
data[key] = stripQuotes(rawValue);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parses one `.claude/agents/*.md` file into { name, tools, model, parent }.
|
|
129
|
+
function deriveAiProduct(filePath) {
|
|
130
|
+
const p = String(filePath || '').replace(/\\/g, '/');
|
|
131
|
+
if (p.includes('/.claude/agents/') || p.includes('/.claude/agents')) return 'claude-code';
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildAgentFromFrontmatter(fm, filePath) {
|
|
136
|
+
const fallbackName = path.basename(filePath, path.extname(filePath));
|
|
137
|
+
const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
|
|
138
|
+
|
|
139
|
+
const tools = Array.isArray(fm.tools) ? fm.tools : typeof fm.tools === 'string' ? [fm.tools] : [];
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
tools,
|
|
144
|
+
// AI product derived from the source path (see deriveAiProduct) — shown on
|
|
145
|
+
// the card in place of the LLM `model`.
|
|
146
|
+
aiProduct: deriveAiProduct(filePath),
|
|
147
|
+
model: typeof fm.model === 'string' && fm.model ? fm.model : null,
|
|
148
|
+
// Hierarchy (ADR-009): first honour an explicit `parent` frontmatter key when a project declares one.
|
|
149
|
+
parent: typeof fm.parent === 'string' && fm.parent ? fm.parent : null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `parseAgentFile` USED TO BE HERE (removed by issue 093's sweep): read one `.md` and build an agent from its frontmatter.
|
|
154
|
+
|
|
155
|
+
// Markdown body (everything after the closing ` ` of the frontmatter).
|
|
156
|
+
function bodyAfterFrontmatter(content) {
|
|
157
|
+
const m = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
158
|
+
return m ? content.slice(m[0].length) : '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const HIERARCHY_DIRECTS = /\b(defines?|sets?|directs?|approves?|coordinat\w+|orchestrat\w+|owns?)\b/i;
|
|
162
|
+
const HIERARCHY_EXECUTES = /\byou\s+(execute|produce|implement|carry\s+out)\b|\byou\s+don'?t\s+strateg|passes?\s+through\s+you|execution\s+agent/i;
|
|
163
|
+
const HIERARCHY_NEGATION = /\b(do\s+not|don'?t\s+touch|not\s+touch|never|nunca|no\s+toques)\b/i;
|
|
164
|
+
|
|
165
|
+
function deriveParentFromText(selfName, text, knownNames) {
|
|
166
|
+
if (!text) return null;
|
|
167
|
+
const units = text.split(/\r?\n|(?<=[.!?])\s+/);
|
|
168
|
+
for (const unit of units) {
|
|
169
|
+
if (HIERARCHY_NEGATION.test(unit)) continue;
|
|
170
|
+
if (!HIERARCHY_DIRECTS.test(unit) || !HIERARCHY_EXECUTES.test(unit)) continue;
|
|
171
|
+
const matches = [];
|
|
172
|
+
for (const other of knownNames) {
|
|
173
|
+
if (other === selfName) continue;
|
|
174
|
+
const re = new RegExp('\\b' + other.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
175
|
+
if (re.test(unit)) matches.push(other);
|
|
176
|
+
}
|
|
177
|
+
if (matches.length === 1) return matches[0]; // exactly one -> unambiguous
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Both `.claude/agents/` locations in scope order (project first, so it
|
|
183
|
+
// wins on a name collision when the results are merged/deduped below).
|
|
184
|
+
function agentDirs(root) {
|
|
185
|
+
return [path.join(root, '.claude', 'agents'), path.join(getHomeDir(), '.claude', 'agents')];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Per-repo AI-usage scoping: `.claude/agents/` across EVERY selected project root, PLUS home appended ONCE at the end.
|
|
189
|
+
function agentDirsForRoots(roots) {
|
|
190
|
+
const dirs = [];
|
|
191
|
+
for (const root of roots) dirs.push(path.join(root, '.claude', 'agents'));
|
|
192
|
+
dirs.push(path.join(getHomeDir(), '.claude', 'agents'));
|
|
193
|
+
return dirs;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Deterministic (no-LLM) agent org chart over a fixed list of `.claude/agents/` directories (in precedence order — first occurrence of a name wins).
|
|
197
|
+
function parseAgentOrgChartFromDirs(dirs) {
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
const agents = [];
|
|
200
|
+
// TRANSIENT prose (description + body) per agent, kept LOCAL to this function purely to derive parent edges below.
|
|
201
|
+
const textByName = new Map();
|
|
202
|
+
|
|
203
|
+
for (const dir of dirs) {
|
|
204
|
+
for (const file of listAgentMarkdownFiles(dir)) {
|
|
205
|
+
let content;
|
|
206
|
+
try {
|
|
207
|
+
content = fs.readFileSync(file, 'utf8');
|
|
208
|
+
} catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const fm = parseFrontmatter(content);
|
|
212
|
+
if (!fm) continue; // no frontmatter block: not a valid agent definition
|
|
213
|
+
const agent = buildAgentFromFrontmatter(fm, file);
|
|
214
|
+
if (seen.has(agent.name)) continue; // project wins on a name collision (dir order)
|
|
215
|
+
seen.add(agent.name);
|
|
216
|
+
agents.push(agent);
|
|
217
|
+
const fmWithDesc = parseFrontmatter(content, { includeDescription: true });
|
|
218
|
+
const desc = fmWithDesc && typeof fmWithDesc.description === 'string' ? fmWithDesc.description : '';
|
|
219
|
+
textByName.set(agent.name, `${desc}\n${bodyAfterFrontmatter(content)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Derive orchestrator->subagent edges from prose for agents that declare no explicit `parent` (the common case).
|
|
224
|
+
const knownNames = agents.map((a) => a.name);
|
|
225
|
+
for (const agent of agents) {
|
|
226
|
+
if (agent.parent) continue;
|
|
227
|
+
const derived = deriveParentFromText(agent.name, textByName.get(agent.name), knownNames);
|
|
228
|
+
if (derived && derived !== agent.name) agent.parent = derived;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Defensive: break a direct 2-cycle from mutual cues (A<->B).
|
|
232
|
+
const byName = new Map(agents.map((a) => [a.name, a]));
|
|
233
|
+
for (const agent of agents) {
|
|
234
|
+
if (!agent.parent) continue;
|
|
235
|
+
const p = byName.get(agent.parent);
|
|
236
|
+
if (p && p.parent === agent.name) p.parent = null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return agents;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Deterministic (no-LLM) agent org chart, scoped to `.claude/agents/` in BOTH the project root and the home directory (ADR-014, project ∪ home).
|
|
243
|
+
function parseAgentOrgChart(root) {
|
|
244
|
+
return parseAgentOrgChartFromDirs(agentDirs(root));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Per-repo AI-usage scoping: the org chart across ALL selected project roots (∪ home, once), deduped by name.
|
|
248
|
+
function parseAgentOrgChartForRoots(roots) {
|
|
249
|
+
return parseAgentOrgChartFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// talents-ai-score, ADR-010: returns `[{ name, description }]` — the ONLY function in this module that ever returns description/prompt content.
|
|
253
|
+
function parseAgentDescriptionsFromDirs(dirs) {
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
const result = [];
|
|
256
|
+
for (const dir of dirs) {
|
|
257
|
+
for (const file of listAgentMarkdownFiles(dir)) {
|
|
258
|
+
let content;
|
|
259
|
+
try {
|
|
260
|
+
content = fs.readFileSync(file, 'utf8');
|
|
261
|
+
} catch {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const fm = parseFrontmatter(content, { includeDescription: true });
|
|
265
|
+
if (!fm) continue;
|
|
266
|
+
const fallbackName = path.basename(file, path.extname(file));
|
|
267
|
+
const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
|
|
268
|
+
if (!seen.has(name)) {
|
|
269
|
+
seen.add(name);
|
|
270
|
+
result.push({ name, description: typeof fm.description === 'string' ? fm.description : '' });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseAgentDescriptions(root) {
|
|
278
|
+
return parseAgentDescriptionsFromDirs(agentDirs(root));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Per-repo AI-usage scoping: card-description fallbacks across ALL selected
|
|
282
|
+
// project roots (∪ home, once), same dedup/precedence as parseAgentOrgChartForRoots.
|
|
283
|
+
function parseAgentDescriptionsForRoots(roots) {
|
|
284
|
+
return parseAgentDescriptionsFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseAgentDefinitionsFromDirs(dirs) {
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const dir of dirs) {
|
|
291
|
+
for (const file of listAgentMarkdownFiles(dir)) {
|
|
292
|
+
let content;
|
|
293
|
+
try {
|
|
294
|
+
content = fs.readFileSync(file, 'utf8');
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const fm = parseFrontmatter(content, { includeDescription: true });
|
|
299
|
+
if (!fm) continue;
|
|
300
|
+
const fallbackName = path.basename(file, path.extname(file));
|
|
301
|
+
const name = typeof fm.name === 'string' && fm.name.trim() ? fm.name.trim() : fallbackName;
|
|
302
|
+
if (seen.has(name)) continue;
|
|
303
|
+
seen.add(name);
|
|
304
|
+
const description = typeof fm.description === 'string' ? fm.description : '';
|
|
305
|
+
const body = bodyAfterFrontmatter(content);
|
|
306
|
+
const definition = [description, body].map((s) => (s || '').trim()).filter(Boolean).join('\n\n');
|
|
307
|
+
result.push({ name, definition });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseAgentDefinitions(root) {
|
|
314
|
+
return parseAgentDefinitionsFromDirs(agentDirs(root));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Per-repo AI-usage scoping: full agent definitions (for the quality-evaluation
|
|
318
|
+
// call) across ALL selected project roots (∪ home, once), same dedup/precedence.
|
|
319
|
+
function parseAgentDefinitionsForRoots(roots) {
|
|
320
|
+
return parseAgentDefinitionsFromDirs(agentDirsForRoots(Array.isArray(roots) ? roots : [roots]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function agentSourceFilesByName(root) {
|
|
324
|
+
const byName = new Map();
|
|
325
|
+
const dirs = agentDirs(root);
|
|
326
|
+
dirs.forEach((dir, dirIndex) => {
|
|
327
|
+
for (const file of listAgentMarkdownFiles(dir)) {
|
|
328
|
+
let content;
|
|
329
|
+
try {
|
|
330
|
+
content = fs.readFileSync(file, 'utf8');
|
|
331
|
+
} catch {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const fm = parseFrontmatter(content);
|
|
335
|
+
if (!fm) continue;
|
|
336
|
+
const agent = buildAgentFromFrontmatter(fm, file);
|
|
337
|
+
if (byName.has(agent.name)) continue; // project (dirIndex 0) wins on collision
|
|
338
|
+
const rel =
|
|
339
|
+
dirIndex === 0 ? path.relative(root, file).split(path.sep).join('/') : null;
|
|
340
|
+
byName.set(agent.name, rel);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return byName;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
parseAgentOrgChart,
|
|
348
|
+
parseAgentOrgChartForRoots,
|
|
349
|
+
parseFrontmatter,
|
|
350
|
+
parseAgentDescriptions,
|
|
351
|
+
parseAgentDescriptionsForRoots,
|
|
352
|
+
parseAgentDefinitions,
|
|
353
|
+
parseAgentDefinitionsForRoots,
|
|
354
|
+
agentSourceFilesByName,
|
|
355
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requestBackend } = require('./backend-request');
|
|
4
|
+
const { reporterFor, REASON } = require('./model-call-record');
|
|
5
|
+
|
|
6
|
+
// Agent synthesis client (talents-ai-score, ADR-010 / ADR-011).
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
9
|
+
|
|
10
|
+
/* ---------- scrub (mandatory mitigation) ---------- */
|
|
11
|
+
|
|
12
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g;
|
|
13
|
+
const BEARER_RE = /\bBearer\s+\S{10,}/gi;
|
|
14
|
+
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{16,}\b/g;
|
|
15
|
+
const AWS_KEY_RE = /\bAKIA[0-9A-Z]{16}\b/g;
|
|
16
|
+
const GENERIC_LONG_TOKEN_RE = /\b[A-Za-z0-9_-]{32,}\b/g;
|
|
17
|
+
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
18
|
+
const WINDOWS_PATH_RE = /[A-Za-z]:\\(?:[^\s\\]+\\)*[^\s\\]+/g;
|
|
19
|
+
const UNIX_PATH_RE = /(?:\/[A-Za-z0-9_.-]+){2,}/g;
|
|
20
|
+
|
|
21
|
+
const KEY_PREFIX_RE = /\b(?:sk|pk|rk|ghp|gho|ghu|ghs|ghr|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g; // OpenAI/Stripe/GitHub/Slack-style prefixed tokens
|
|
22
|
+
const GITHUB_PAT_RE = /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g;
|
|
23
|
+
const GOOGLE_API_KEY_RE = /\bAIza[0-9A-Za-z_-]{35}\b/g;
|
|
24
|
+
const SENDGRID_KEY_RE = /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g;
|
|
25
|
+
const BASE64_SECRET_RE = /\b[A-Za-z0-9+/]{40,}={0,2}\b/g; // base64-inclusive high-entropy blob (e.g. an AWS secret access key)
|
|
26
|
+
const JWT_LIKE_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g; // dot-separated JWT-shaped triplet not starting with `eyJ`
|
|
27
|
+
const PEM_BLOCK_RE = /-----BEGIN[ A-Z]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z]*PRIVATE KEY-----/g;
|
|
28
|
+
const CONN_STRING_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]{1,15}:\/\/)[^\s'"/:@]+:[^\s'"@]+@/g; // scheme://user:pass@ -> scheme://[REDACTED]@
|
|
29
|
+
const GENERIC_KV_RE = /\b(?:password|secret|token|api[_-]?key|bearer)\s*[:=]\s*\S+/gi;
|
|
30
|
+
|
|
31
|
+
// Heuristic, best-effort redaction — an "invisible safety net" (ADR-011), not a substitute for the talent's own judgment about what they write in agent descriptions.
|
|
32
|
+
function scrubSecrets(text) {
|
|
33
|
+
if (typeof text !== 'string' || !text) return '';
|
|
34
|
+
let out = text;
|
|
35
|
+
out = out.replace(PEM_BLOCK_RE, '[REDACTED]');
|
|
36
|
+
out = out.replace(CONN_STRING_RE, '$1[REDACTED]@');
|
|
37
|
+
out = out.replace(JWT_RE, '[REDACTED]');
|
|
38
|
+
out = out.replace(BEARER_RE, 'Bearer [REDACTED]');
|
|
39
|
+
out = out.replace(OPENAI_KEY_RE, '[REDACTED]');
|
|
40
|
+
out = out.replace(AWS_KEY_RE, '[REDACTED]');
|
|
41
|
+
out = out.replace(KEY_PREFIX_RE, '[REDACTED]');
|
|
42
|
+
out = out.replace(GITHUB_PAT_RE, '[REDACTED]');
|
|
43
|
+
out = out.replace(GOOGLE_API_KEY_RE, '[REDACTED]');
|
|
44
|
+
out = out.replace(SENDGRID_KEY_RE, '[REDACTED]');
|
|
45
|
+
out = out.replace(JWT_LIKE_RE, '[REDACTED]');
|
|
46
|
+
out = out.replace(GENERIC_LONG_TOKEN_RE, '[REDACTED]');
|
|
47
|
+
out = out.replace(BASE64_SECRET_RE, '[REDACTED]');
|
|
48
|
+
out = out.replace(GENERIC_KV_RE, '[REDACTED]');
|
|
49
|
+
out = out.replace(EMAIL_RE, '[REDACTED]');
|
|
50
|
+
out = out.replace(WINDOWS_PATH_RE, '[REDACTED]');
|
|
51
|
+
out = out.replace(UNIX_PATH_RE, '[REDACTED]');
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---------- request builder ---------- */
|
|
56
|
+
|
|
57
|
+
function buildSynthesisRequest(structuralAgents, descriptionsByName, consentGranted = false) {
|
|
58
|
+
const descMap = new Map((descriptionsByName || []).map((d) => [d.name, d.description]));
|
|
59
|
+
return {
|
|
60
|
+
agents: (structuralAgents || []).map((a) => ({
|
|
61
|
+
name: a.name,
|
|
62
|
+
description: scrubSecrets(descMap.get(a.name) || ''),
|
|
63
|
+
tools: Array.isArray(a.tools) ? a.tools : [],
|
|
64
|
+
model: a.model || null,
|
|
65
|
+
parent: a.parent || null,
|
|
66
|
+
})),
|
|
67
|
+
...(consentGranted === true ? { traceContentConsent: true } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ---------- network (via the shared PRIMARY -> FALLBACK chain) ---------- */
|
|
72
|
+
|
|
73
|
+
// Validates the minimal expected shape: `{agents: [...], edges: [...]}` with `agents` an array (edges defaults to [] if absent/invalid — some diagrams may legitimately have none).
|
|
74
|
+
function isValidSynthesisResponse(parsed) {
|
|
75
|
+
return !!parsed && typeof parsed === 'object' && Array.isArray(parsed.agents);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Requests the agent-synthesis endpoint.
|
|
79
|
+
async function requestAgentSynthesis(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
|
|
80
|
+
// Issue 109: the four `return null`s below are four DIFFERENT situations that used to be one.
|
|
81
|
+
const outcome = reporterFor(onOutcome, { timeoutMs });
|
|
82
|
+
if (!endpoint) {
|
|
83
|
+
outcome.skip(REASON.NO_ENDPOINT);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const safeBody = {
|
|
88
|
+
...requestBody,
|
|
89
|
+
agents: Array.isArray(requestBody && requestBody.agents)
|
|
90
|
+
? requestBody.agents.map((a) => ({ ...a, description: scrubSecrets(a.description) }))
|
|
91
|
+
: [],
|
|
92
|
+
};
|
|
93
|
+
if (safeBody.traceContentConsent !== true) delete safeBody.traceContentConsent;
|
|
94
|
+
|
|
95
|
+
let res;
|
|
96
|
+
try {
|
|
97
|
+
res = await requestBackend({ endpoint, body: safeBody, timeoutMs, trace: outcome.hops });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
outcome.failFromError(e);
|
|
100
|
+
return null; // network error or timeout on every hop: never breaks the local report
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (res.status < 200 || res.status >= 300) {
|
|
104
|
+
outcome.fail(REASON.HTTP, { status: res.status, backend: res.backend });
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(res.raw);
|
|
111
|
+
} catch {
|
|
112
|
+
outcome.fail(REASON.MALFORMED_JSON, { status: res.status, backend: res.backend });
|
|
113
|
+
return null; // malformed (non-JSON) response body
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isValidSynthesisResponse(parsed)) {
|
|
117
|
+
outcome.fail(REASON.UNEXPECTED_SHAPE, { status: res.status, backend: res.backend });
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
outcome.succeed({ status: res.status, backend: res.backend });
|
|
122
|
+
return {
|
|
123
|
+
agents: parsed.agents,
|
|
124
|
+
edges: Array.isArray(parsed.edges) ? parsed.edges : [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
scrubSecrets,
|
|
130
|
+
buildSynthesisRequest,
|
|
131
|
+
requestAgentSynthesis,
|
|
132
|
+
};
|