getprismo 0.1.24 → 0.1.26

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.
@@ -10,198 +10,19 @@ module.exports = function createContextOptimize(deps) {
10
10
  color,
11
11
  writeGeneratedFile,
12
12
  } = deps;
13
- const { execFileSync } = require("child_process");
14
13
 
15
- function findRepoFiles(result, predicate, limit = 40) {
16
- return result.files
17
- ? result.files.filter((file) => !file.ignored && file.kind !== "binary" && predicate(file.path, file)).slice(0, limit)
18
- : [];
19
- }
20
-
21
- function detectFrameworks(root, result) {
22
- const frameworks = new Set();
23
- const packageFiles = findRepoFiles(result, (rel) => path.basename(rel) === "package.json" && !rel.includes("node_modules/"), 12);
24
- for (const file of packageFiles) {
25
- const pkg = safeReadJson(path.join(root, file.path));
26
- const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
27
- if (deps.next) frameworks.add("Next.js");
28
- if (deps.react) frameworks.add("React");
29
- if (deps.vue || deps["@vue/runtime-core"]) frameworks.add("Vue");
30
- if (deps.svelte || deps["@sveltejs/kit"]) frameworks.add(deps["@sveltejs/kit"] ? "SvelteKit" : "Svelte");
31
- if (deps["solid-js"]) frameworks.add("Solid");
32
- if (deps.astro) frameworks.add("Astro");
33
- if (deps.nuxt) frameworks.add("Nuxt");
34
- if (deps.express) frameworks.add("Express");
35
- if (deps["@nestjs/core"]) frameworks.add("NestJS");
36
- if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
37
- if (deps.tailwindcss) frameworks.add("Tailwind");
38
- if (deps.typescript) frameworks.add("TypeScript");
39
- if (pkg) frameworks.add("Node.js");
40
- }
41
-
42
- const textFiles = new Map(result.files.map((file) => [file.path, file]));
43
- const requirements = [...textFiles.keys()].filter((rel) => rel.endsWith("requirements.txt"));
44
- for (const rel of requirements) {
45
- const text = readIfText(path.join(root, rel)) || "";
46
- if (/fastapi/i.test(text)) frameworks.add("FastAPI");
47
- if (/django/i.test(text)) frameworks.add("Django");
48
- if (/flask/i.test(text)) frameworks.add("Flask");
49
- if (/psycopg2|asyncpg|sqlalchemy/i.test(text)) frameworks.add("PostgreSQL");
50
- if (/redis/i.test(text)) frameworks.add("Redis");
51
- frameworks.add("Python");
52
- }
53
-
54
- const pyprojectFiles = [...textFiles.keys()].filter((rel) => rel.endsWith("pyproject.toml") && !rel.includes("node_modules/"));
55
- for (const rel of pyprojectFiles) {
56
- const text = readIfText(path.join(root, rel)) || "";
57
- if (/fastapi/i.test(text)) frameworks.add("FastAPI");
58
- if (/django/i.test(text)) frameworks.add("Django");
59
- if (/flask/i.test(text)) frameworks.add("Flask");
60
- if (/sqlalchemy/i.test(text)) frameworks.add("SQLAlchemy");
61
- if (/psycopg|asyncpg/i.test(text)) frameworks.add("PostgreSQL");
62
- if (/redis/i.test(text)) frameworks.add("Redis");
63
- if (/celery/i.test(text)) frameworks.add("Celery");
64
- frameworks.add("Python");
65
- }
66
-
67
- if (pyprojectFiles.length && !frameworks.has("Python")) frameworks.add("Python");
68
- const pythonFiles = [...textFiles.values()].filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)).slice(0, 80);
69
- for (const file of pythonFiles) {
70
- const text = readIfText(path.join(root, file.path), 128 * 1024) || "";
71
- if (/FastAPI\s*\(|from\s+fastapi\s+import|APIRouter\s*\(/.test(text)) frameworks.add("FastAPI");
72
- if (/from\s+django|import\s+django|DJANGO_SETTINGS_MODULE/.test(text)) frameworks.add("Django");
73
- if (/Flask\s*\(|from\s+flask\s+import/.test(text)) frameworks.add("Flask");
74
- if (/sqlalchemy|create_engine|SessionLocal/i.test(text)) frameworks.add("SQLAlchemy");
75
- }
76
- if (pythonFiles.length) frameworks.add("Python");
77
- if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
78
- if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
79
- if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
80
- if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
81
- if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
82
- if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
83
- if ([...textFiles.keys()].some((rel) => rel.includes("svelte.config."))) frameworks.add("SvelteKit");
84
- if ([...textFiles.keys()].some((rel) => rel.includes("astro.config."))) frameworks.add("Astro");
85
- if ([...textFiles.keys()].some((rel) => rel.includes("nuxt.config."))) frameworks.add("Nuxt");
86
- if ([...textFiles.keys()].some((rel) => rel.endsWith(".vue"))) frameworks.add("Vue");
87
- if ([...textFiles.keys()].some((rel) => rel.endsWith(".svelte"))) frameworks.add("Svelte");
88
- if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
89
- if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
90
- if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
91
- if ([...textFiles.keys()].some((rel) => /postgres|postgresql/i.test(rel))) frameworks.add("PostgreSQL");
92
- return Array.from(frameworks).sort();
93
- }
94
-
95
- function topLevelDirectories(root) {
96
- try {
97
- return fs.readdirSync(root, { withFileTypes: true })
98
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
99
- .map((entry) => entry.name)
100
- .sort();
101
- } catch {
102
- return [];
103
- }
104
- }
105
-
106
- function hasPath(result, matcher) {
107
- return result.files.some((file) => matcher(file.path));
108
- }
109
-
110
- function detectEntrypoints(result) {
111
- const candidates = [
112
- "backend/app/main.py",
113
- "backend/main.py",
114
- "app/main.py",
115
- "main.py",
116
- "manage.py",
117
- "frontend/src/app/page.tsx",
118
- "frontend/src/app/layout.tsx",
119
- "src/app/page.tsx",
120
- "src/main.tsx",
121
- "src/index.tsx",
122
- "src/App.vue",
123
- "src/App.svelte",
124
- "src/routes/+page.svelte",
125
- "src/pages/index.astro",
126
- "app.vue",
127
- "server.js",
128
- "index.js",
129
- "docker/docker-compose.yml",
130
- "docker-compose.yml",
131
- ];
132
- const paths = new Set(result.files.map((file) => file.path));
133
- const found = candidates.filter((candidate) => paths.has(candidate));
134
- if (!found.length) {
135
- const pyMain = result.files.find((f) => /^[^/]+\/__main__\.py$/.test(f.path));
136
- if (pyMain) found.push(pyMain.path);
137
- const pyInit = result.files.find((f) => /^[^/]+\/__init__\.py$/.test(f.path) && !f.path.includes("test"));
138
- if (pyInit && !found.length) found.push(pyInit.path);
139
- }
140
- return found;
141
- }
142
-
143
- function isNonSourcePath(rel) {
144
- if (/\.(test|spec|e2e)\.[jt]sx?$/.test(rel)) return true;
145
- return /^(docs|docs_src|examples|samples|tutorials|tests|test|spec|__tests__|fixtures)\//i.test(rel) ||
146
- /\/(docs|docs_src|examples|samples|tutorials|tests|test|__tests__|fixtures)\//.test(rel);
147
- }
148
-
149
- function detectBackendPaths(result) {
150
- const pythonText = (file) => readIfText(path.join(result.root, file.path), 256 * 1024) || "";
151
- const isRootPython = (rel) => /^[^/]+\.py$/.test(rel);
152
- const isPython = (rel) => rel.endsWith(".py");
153
- const hasFastApiSignal = (file) => /FastAPI\s*\(|APIRouter\s*\(|@app\.(get|post|put|patch|delete)|@router\.(get|post|put|patch|delete)/.test(pythonText(file));
154
- const api = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
155
- /(backend|app|src)\/.*(router|routes|api)\//.test(rel) ||
156
- /(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) ||
157
- /\/(routing|routers?)\.py$/.test(rel) ||
158
- (isRootPython(rel) && hasFastApiSignal(file))
159
- ), 30).map((f) => f.path);
160
- const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
161
- /(backend|app|src)\/.*(service|services|application)/.test(rel) ||
162
- /\/applications?\.py$/.test(rel) ||
163
- (isRootPython(rel) && /(service|worker|manager|client|heartbeat|memory|chat|tool|orchestrator|pipeline)/i.test(rel))
164
- ), 40).map((f) => f.path);
165
- const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
166
- /(backend|app|src)\/.*models\.py$/.test(rel) ||
167
- /(backend|app|src)\/.*schema/.test(rel) ||
168
- /\/models\.py$/.test(rel) ||
169
- (isRootPython(rel) && /(model|schema|entity|types?)/i.test(rel))
170
- ), 30).map((f) => f.path);
171
- const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
172
- /(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel) ||
173
- (isPython(rel) && /(sqlite|qdrant|neo4j|database|storage|repository|vector|graph|migration)/i.test(rel))
174
- ), 30).map((f) => f.path);
175
- const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
176
- /(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) ||
177
- (isRootPython(rel) && /(config|settings|env)/i.test(rel)) ||
178
- rel.endsWith("requirements.txt") ||
179
- rel === "pyproject.toml"
180
- ), 20).map((f) => f.path);
181
- const auth = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
182
- /(backend|app|src)\/.*auth/.test(rel) ||
183
- /\/security\.py$/.test(rel) ||
184
- (isPython(rel) && /(auth|security|permission|token|session|oauth|jwt|middleware)/i.test(rel)) ||
185
- (isRootPython(rel) && /(Depends|HTTPBearer|OAuth2|JWT|Authorization)/.test(pythonText(file)))
186
- ), 30).map((f) => f.path);
187
- return { api, services, models, db, config, auth };
188
- }
14
+ const {
15
+ detectBackendPaths,
16
+ detectEntrypoints,
17
+ detectFrameworks,
18
+ detectFrontendPaths,
19
+ getGitActivity,
20
+ hasPath,
21
+ isNonSourcePath,
22
+ topLevelDirectories,
23
+ topLoadBearing,
24
+ } = require("./context-detect")({ fs, path, safeReadJson, readIfText, formatBytes });
189
25
 
190
- function detectFrontendPaths(result) {
191
- const appSurface = /(^|\/)(frontend\/)?src\/(app|pages|routes)\//;
192
- const appFile = /(^|\/)(frontend\/)?src\/(App|main|index)\.(jsx?|tsx?|vue|svelte)$/;
193
- const componentFile = /(^|\/)(frontend\/)?src\/(components|ui|widgets)\//;
194
- const apiFile = /(^|\/)(frontend\/)?src\/(lib|hooks|composables|stores|services|api)\/.*(api|client|query|fetch|request|finops)/;
195
- const stylingFile = /tailwind\.config|globals\.css|app\.css|\.module\.css|\.scss$|(^|\/)(frontend\/)?src\/styles?\//;
196
- const stateFile = /providers?\.(tsx?|jsx?)|react-query|use[A-Z].*\.(ts|tsx|js|jsx)|(^|\/)(stores?|state|pinia|zustand|redux)\//;
197
- return {
198
- app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (appSurface.test(rel) || appFile.test(rel) || /(^|\/)app\.vue$/.test(rel) || /apps\/[^/]+\/(app|pages|routes|src\/app|src\/pages|src\/routes)\//.test(rel)), 24).map((f) => f.path),
199
- components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (componentFile.test(rel) || /apps\/[^/]+\/.*(components|ui|widgets)\//.test(rel) || /packages\/[^/]+\/.*(components|ui|widgets)\//.test(rel)), 24).map((f) => f.path),
200
- apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (apiFile.test(rel) || /apps\/[^/]+\/.*(lib|hooks|composables|services|api)\/.*(api|client|query|fetch|request)/.test(rel)), 24).map((f) => f.path),
201
- styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stylingFile.test(rel), 24).map((f) => f.path),
202
- state: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && stateFile.test(rel), 24).map((f) => f.path),
203
- };
204
- }
205
26
 
206
27
  function createOptimizeContext(rootDir = process.cwd(), scope = null) {
207
28
  const scan = scanRepo(rootDir);
@@ -253,140 +74,6 @@ function mdList(items, empty = "None detected.") {
253
74
  return items.map((item) => `- \`${item}\``).join("\n");
254
75
  }
255
76
 
256
- function moduleNameForPath(rel) {
257
- return rel
258
- .replace(/\.[^.]+$/, "")
259
- .replace(/\/__init__$/, "")
260
- .replace(/\/index$/, "")
261
- .replace(/\//g, ".");
262
- }
263
-
264
- function basenameStem(rel) {
265
- return path.basename(rel).replace(/\.[^.]+$/, "");
266
- }
267
-
268
- function extractPythonImports(text) {
269
- const imports = new Set();
270
- const importRe = /^\s*import\s+([a-zA-Z0-9_.,\s]+)/gm;
271
- const fromRe = /^\s*from\s+([a-zA-Z0-9_.]+)\s+import\s+/gm;
272
- let match;
273
- while ((match = importRe.exec(text))) {
274
- match[1].split(",").map((part) => part.trim().split(/\s+as\s+/)[0]).filter(Boolean).forEach((name) => imports.add(name));
275
- }
276
- while ((match = fromRe.exec(text))) imports.add(match[1]);
277
- return imports;
278
- }
279
-
280
- function resolveJsImport(fromRel, specifier, sourcePaths) {
281
- if (!specifier.startsWith(".")) return null;
282
- const fromDir = path.posix.dirname(fromRel);
283
- const base = path.posix.normalize(path.posix.join(fromDir, specifier));
284
- const candidates = [
285
- base,
286
- `${base}.ts`,
287
- `${base}.tsx`,
288
- `${base}.js`,
289
- `${base}.jsx`,
290
- `${base}/index.ts`,
291
- `${base}/index.tsx`,
292
- `${base}/index.js`,
293
- `${base}/index.jsx`,
294
- ];
295
- return candidates.find((candidate) => sourcePaths.has(candidate)) || null;
296
- }
297
-
298
- function extractJsImports(fromRel, text, sourcePaths) {
299
- const imports = new Set();
300
- const importRe = /\bfrom\s+["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
301
- let match;
302
- while ((match = importRe.exec(text))) {
303
- const specifier = match[1] || match[2] || match[3];
304
- const resolved = resolveJsImport(fromRel, specifier, sourcePaths);
305
- if (resolved) imports.add(resolved);
306
- }
307
- return imports;
308
- }
309
-
310
- function getGitActivity(root) {
311
- const activity = new Map();
312
- try {
313
- const output = execFileSync("git", ["-C", root, "log", "--since=180 days ago", "--name-only", "--format=%ct"], {
314
- encoding: "utf8",
315
- maxBuffer: 8 * 1024 * 1024,
316
- stdio: ["ignore", "pipe", "ignore"],
317
- });
318
- let currentTs = 0;
319
- for (const rawLine of output.split(/\r?\n/)) {
320
- const line = rawLine.trim();
321
- if (!line) continue;
322
- if (/^\d{9,}$/.test(line)) {
323
- currentTs = Number(line);
324
- continue;
325
- }
326
- const rel = line.replace(/\\/g, "/");
327
- const previous = activity.get(rel) || { touches: 0, lastTouched: 0 };
328
- previous.touches += 1;
329
- previous.lastTouched = Math.max(previous.lastTouched, currentTs);
330
- activity.set(rel, previous);
331
- }
332
- } catch {
333
- return activity;
334
- }
335
- return activity;
336
- }
337
-
338
- function formatGitActivity(activity) {
339
- if (!activity || !activity.touches) return "no recent git touches";
340
- const date = activity.lastTouched ? new Date(activity.lastTouched * 1000).toISOString().slice(0, 10) : "unknown date";
341
- return `${activity.touches} recent git touch${activity.touches === 1 ? "" : "es"}, last ${date}`;
342
- }
343
-
344
- function topLoadBearing(root, files, allFiles, gitActivity, limit = 8) {
345
- const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path));
346
- const sourcePaths = new Set(textFiles.map((file) => file.path));
347
- const pythonModuleToPath = new Map();
348
- for (const file of textFiles.filter((candidate) => candidate.path.endsWith(".py"))) {
349
- pythonModuleToPath.set(moduleNameForPath(file.path), file.path);
350
- pythonModuleToPath.set(basenameStem(file.path), file.path);
351
- }
352
-
353
- const importRefs = new Map();
354
- const textRefs = new Map();
355
- const corpus = textFiles.map((file) => {
356
- const text = readIfText(path.join(root, file.path), 512 * 1024) || "";
357
- if (file.path.endsWith(".py")) {
358
- for (const imported of extractPythonImports(text)) {
359
- const target = pythonModuleToPath.get(imported) || pythonModuleToPath.get(imported.split(".").pop());
360
- if (target && target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
361
- }
362
- } else if (/\.(tsx?|jsx?|mjs|cjs|vue|svelte)$/.test(file.path)) {
363
- for (const target of extractJsImports(file.path, text, sourcePaths)) {
364
- if (target !== file.path) importRefs.set(target, (importRefs.get(target) || 0) + 1);
365
- }
366
- }
367
- return `${file.path}\n${text}`;
368
- });
369
-
370
- for (const file of files || []) {
371
- const base = basenameStem(file.path);
372
- const importName = base.replace(/[-.]/g, "_");
373
- const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
374
- textRefs.set(file.path, refs);
375
- }
376
-
377
- return [...(files || [])]
378
- .filter((file) => !file.ignored && file.kind !== "binary")
379
- .map((file) => {
380
- const imports = importRefs.get(file.path) || 0;
381
- const refs = textRefs.get(file.path) || 0;
382
- const git = gitActivity.get(file.path) || { touches: 0, lastTouched: 0 };
383
- const score = imports * 1000 + refs * 25 + Math.min(git.touches, 20) * 10 + Math.log10(file.size + 1);
384
- return { file, imports, refs, git, score };
385
- })
386
- .sort((a, b) => b.score - a.score || b.file.size - a.file.size)
387
- .slice(0, limit)
388
- .map(({ file, imports, refs, git }) => `${file.path} (${imports} import ref${imports === 1 ? "" : "s"}, ${refs} text reference signal${refs === 1 ? "" : "s"}, ${formatGitActivity(git)}, ${formatBytes(file.size)})`);
389
- }
390
77
 
391
78
  function inferGaps(ctx) {
392
79
  const gaps = [];
@@ -324,6 +324,7 @@ function toDoctorJsonPayload(result) {
324
324
  confidence: usage.confidence,
325
325
  totals: usage.totals,
326
326
  sources: usage.sources,
327
+ multiAgent: usage.multiAgent || null,
327
328
  }
328
329
  : null;
329
330
  return {
@@ -381,6 +382,13 @@ function renderDoctorTerminal(result) {
381
382
  if (result.before.realUsage && result.before.realUsage.sessions.length) {
382
383
  lines.push(`Local usage: ${formatTokenCount(result.before.realUsage.totals.displayTokens)} tokens across ${result.before.realUsage.sessions.length} recent session(s)`);
383
384
  }
385
+ const multiAgent = result.before.realUsage && result.before.realUsage.multiAgent;
386
+ if (multiAgent && multiAgent.agentCount > 1) {
387
+ lines.push(`Multi-agent: ${multiAgent.agentCount} agents visible; highest pressure ${multiAgent.highestPressure}`);
388
+ if (multiAgent.coordinationWarnings && multiAgent.coordinationWarnings[0]) {
389
+ lines.push(`Coordination warning: ${multiAgent.coordinationWarnings[0]}`);
390
+ }
391
+ }
384
392
  if (!(result.dryRun && result.applySuggestions)) lines.push(`Estimated exposed context reduction: ${result.exposedTokenReductionPercent}%`);
385
393
  if (!result.dryRun) {
386
394
  lines.push(`Payoff: ${scoreDelta > 0 ? `repo is ${scoreDelta} points cleaner for AI coding sessions` : "safe fixes applied; remaining risk needs manual cleanup"}`);
@@ -457,6 +465,10 @@ function renderDevTerminal(result) {
457
465
  } else if (result.scan.realUsage) {
458
466
  lines.push("Real local usage: no matching local sessions found for this repo");
459
467
  }
468
+ const multiAgent = result.scan.realUsage && result.scan.realUsage.multiAgent;
469
+ if (multiAgent && multiAgent.agentCount > 1) {
470
+ lines.push(`Multi-agent: ${multiAgent.agentCount} agents visible; highest pressure ${multiAgent.highestPressure}`);
471
+ }
460
472
  lines.push("");
461
473
  lines.push("Generated:");
462
474
  result.optimize.generatedFiles.slice(0, 8).forEach((file) => lines.push(`- ${file}`));
@@ -69,6 +69,11 @@ function createMcpTools(deps) {
69
69
  tool: { type: "string", enum: ["all", "codex", "claude"], description: "Which local session logs to inspect." },
70
70
  limit: limitProperty,
71
71
  }),
72
+ makeTool("prismo_multi_agent_watch", "Return multi-agent coordination risks across visible local Codex/Claude sessions.", {
73
+ path: pathProperty,
74
+ tool: { type: "string", enum: ["all", "codex", "claude"], description: "Which local session logs to inspect." },
75
+ limit: limitProperty,
76
+ }),
72
77
  makeTool("prismo_shield_run", "Run a noisy command through Prismo shield and store full output locally.", {
73
78
  path: pathProperty,
74
79
  command: {
@@ -131,11 +136,27 @@ function createMcpTools(deps) {
131
136
  const summary = getUsageSummary({
132
137
  cwd: target,
133
138
  limit: Number(args.limit) || 3,
134
- usageTool: args.tool || "all",
139
+ tool: args.tool || "all",
135
140
  });
136
141
  return createTextResult(summary);
137
142
  }
138
143
 
144
+ if (name === "prismo_multi_agent_watch") {
145
+ const summary = getUsageSummary({
146
+ cwd: target,
147
+ limit: Number(args.limit) || 8,
148
+ tool: args.tool || "all",
149
+ });
150
+ return createTextResult({
151
+ schemaVersion: 1,
152
+ generatedAt: summary.generatedAt,
153
+ scannedPath: summary.scannedPath,
154
+ tool: summary.tool,
155
+ totals: summary.totals,
156
+ multiAgent: summary.multiAgent,
157
+ });
158
+ }
159
+
139
160
  if (name === "prismo_shield_run") {
140
161
  return createTextResult(runShield(target, args.command));
141
162
  }
@@ -265,6 +286,7 @@ async function runMcpDoctor(deps) {
265
286
  "prismo_scan",
266
287
  "prismo_doctor_dry_run",
267
288
  "prismo_watch_snapshot",
289
+ "prismo_multi_agent_watch",
268
290
  "prismo_shield_run",
269
291
  "prismo_shield_search",
270
292
  "prismo_shield_last",
@@ -315,7 +337,7 @@ async function runMcpDoctor(deps) {
315
337
  next: [
316
338
  "Add the config snippet to your MCP-compatible client.",
317
339
  "Restart the client and confirm prismodev appears in the MCP tool list.",
318
- "Ask the agent to call prismo_scan or prismo_shield_run.",
340
+ "Ask the agent to call prismo_scan, prismo_multi_agent_watch, or prismo_shield_run.",
319
341
  ],
320
342
  };
321
343
  }
@@ -45,6 +45,10 @@ function renderTerminalReport(result, options = {}) {
45
45
  if (result.realUsage && result.realUsage.sessions.length) {
46
46
  lines.push(`- Real local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
47
47
  lines.push(`- Usage confidence: ${result.realUsage.confidence}`);
48
+ if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
49
+ lines.push(`- Multi-agent: ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
50
+ if (result.realUsage.multiAgent.coordinationWarnings[0]) lines.push(`- Coordination warning: ${result.realUsage.multiAgent.coordinationWarnings[0]}`);
51
+ }
48
52
  } else if (result.realUsage) {
49
53
  lines.push("- Real local usage: no matching local Codex/Claude Code sessions found for this repo");
50
54
  }
@@ -116,6 +120,9 @@ function renderOptimizerFitTerminal(result, options = {}) {
116
120
  lines.push(`Primary bottleneck: ${color(fit.summary, tone, useColor)}`);
117
121
  if (result.realUsage && result.realUsage.sessions.length) {
118
122
  lines.push(`Local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
123
+ if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
124
+ lines.push(`Multi-agent: ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
125
+ }
119
126
  } else if (result.realUsage) {
120
127
  lines.push("Local usage: no matching local Claude/Codex sessions found");
121
128
  }
@@ -261,6 +268,9 @@ function renderMarkdownReport(result) {
261
268
  if (result.realUsage) {
262
269
  lines.push(`- **Real Local Usage:** ${result.realUsage.totals.displayTokens.toLocaleString()} tokens across ${result.realUsage.sessions.length} session(s)`);
263
270
  lines.push(`- **Usage Confidence:** ${result.realUsage.confidence}`);
271
+ if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
272
+ lines.push(`- **Multi-Agent:** ${result.realUsage.multiAgent.agentCount} agents visible; highest pressure ${result.realUsage.multiAgent.highestPressure}`);
273
+ }
264
274
  }
265
275
  lines.push("");
266
276
  lines.push("Estimates are based on local file-size and configuration heuristics. They are not provider billing data and are not guaranteed savings.");
@@ -346,6 +356,20 @@ function renderMarkdownReport(result) {
346
356
  lines.push(`- Estimated tool/output tokens: ${result.realUsage.totals.toolTokens.toLocaleString()}`);
347
357
  lines.push(`- Confidence: ${result.realUsage.confidence}`);
348
358
  lines.push("");
359
+ if (result.realUsage.multiAgent && result.realUsage.multiAgent.agentCount > 1) {
360
+ lines.push("### Multi-Agent Coordination");
361
+ lines.push("");
362
+ lines.push(`- Agents visible: ${result.realUsage.multiAgent.agentCount}`);
363
+ lines.push(`- Highest pressure: ${result.realUsage.multiAgent.highestPressure}`);
364
+ result.realUsage.multiAgent.coordinationWarnings.slice(0, 5).forEach((warning) => lines.push(`- ${warning}`));
365
+ if (result.realUsage.multiAgent.sharedFiles.length) {
366
+ lines.push(`- Shared repeated files: ${result.realUsage.multiAgent.sharedFiles.slice(0, 5).map((item) => `\`${item.path}\` (${item.agents} agents)`).join(", ")}`);
367
+ }
368
+ if (result.realUsage.multiAgent.sharedArtifacts.length) {
369
+ lines.push(`- Shared artifact leaks: ${result.realUsage.multiAgent.sharedArtifacts.slice(0, 5).map((item) => `${item.type} (${item.agents} agents)`).join(", ")}`);
370
+ }
371
+ lines.push("");
372
+ }
349
373
  result.realUsage.sessions.slice(0, 5).forEach((session, index) => {
350
374
  lines.push(`${index + 1}. ${session.tool} - ${session.title || session.sessionId}`);
351
375
  lines.push(` - Tokens: ${session.displayTokens.toLocaleString()} (${session.confidence})`);