opencode-swarm 7.85.0 → 7.86.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.
@@ -1,8 +1,8 @@
1
1
  // @bun
2
2
  import {
3
3
  handleGuardrailExplain
4
- } from "./index-x7qck34v.js";
5
- import"./index-yhqt45de.js";
4
+ } from "./index-7r2b453y.js";
5
+ import"./index-5q66xc88.js";
6
6
  import"./index-vq2321gg.js";
7
7
  import"./index-yhsmmv2z.js";
8
8
  import"./index-d9fbxaqd.js";
@@ -879,7 +879,7 @@ var init_executor = __esm(() => {
879
879
  // package.json
880
880
  var package_default = {
881
881
  name: "opencode-swarm",
882
- version: "7.85.0",
882
+ version: "7.86.0",
883
883
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
884
884
  main: "dist/index.js",
885
885
  types: "dist/index.d.ts",
@@ -27978,7 +27978,7 @@ function buildDetailedHelp(commandName, entry) {
27978
27978
  async function handleHelpCommand(ctx) {
27979
27979
  const targetCommand = ctx.args.join(" ");
27980
27980
  if (!targetCommand) {
27981
- const { buildHelpText } = await import("./index-qqabjns2.js");
27981
+ const { buildHelpText } = await import("./index-jwz50183.js");
27982
27982
  return buildHelpText();
27983
27983
  }
27984
27984
  const tokens = targetCommand.split(/\s+/);
@@ -27987,7 +27987,7 @@ async function handleHelpCommand(ctx) {
27987
27987
  return _internals41.buildDetailedHelp(resolved.key, resolved.entry);
27988
27988
  }
27989
27989
  const similar = _internals41.findSimilarCommands(targetCommand);
27990
- const { buildHelpText: fullHelp } = await import("./index-qqabjns2.js");
27990
+ const { buildHelpText: fullHelp } = await import("./index-jwz50183.js");
27991
27991
  if (similar.length > 0) {
27992
27992
  return `Command '/swarm ${targetCommand}' not found.
27993
27993
 
@@ -28120,7 +28120,7 @@ var COMMAND_REGISTRY = {
28120
28120
  },
28121
28121
  "guardrail explain": {
28122
28122
  handler: async (ctx) => {
28123
- const { handleGuardrailExplain } = await import("./guardrail-explain-w4txg349.js");
28123
+ const { handleGuardrailExplain } = await import("./guardrail-explain-rtd1x26f.js");
28124
28124
  return handleGuardrailExplain(ctx.directory, ctx.args);
28125
28125
  },
28126
28126
  description: "Dry-run: show what the guardrails would do to a command or write target (executes nothing)",
@@ -12,7 +12,7 @@ import {
12
12
  detectPosixWrites,
13
13
  detectWindowsWrites,
14
14
  resolveWriteTargets
15
- } from "./index-yhqt45de.js";
15
+ } from "./index-5q66xc88.js";
16
16
  import {
17
17
  checkFileAuthority,
18
18
  classifyFile,
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  handleGuardrailExplain
4
- } from "./index-x7qck34v.js";
4
+ } from "./index-7r2b453y.js";
5
5
  import {
6
6
  handleGuardrailLog
7
7
  } from "./index-5cb86007.js";
@@ -76,7 +76,7 @@ import {
76
76
  handleWriteRetroCommand,
77
77
  normalizeSwarmCommandInput,
78
78
  resolveCommand
79
- } from "./index-yhqt45de.js";
79
+ } from "./index-5q66xc88.js";
80
80
  import"./index-vq2321gg.js";
81
81
  import"./index-yhsmmv2z.js";
82
82
  import"./index-d9fbxaqd.js";
package/dist/cli/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  getPluginLockFilePaths,
8
8
  package_default,
9
9
  resolveCommand
10
- } from "./index-yhqt45de.js";
10
+ } from "./index-5q66xc88.js";
11
11
  import"./index-vq2321gg.js";
12
12
  import"./index-yhsmmv2z.js";
13
13
  import"./index-d9fbxaqd.js";
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var package_default;
69
69
  var init_package = __esm(() => {
70
70
  package_default = {
71
71
  name: "opencode-swarm",
72
- version: "7.85.0",
72
+ version: "7.86.0",
73
73
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
74
74
  main: "dist/index.js",
75
75
  types: "dist/index.d.ts",
@@ -45068,7 +45068,7 @@ function recordSessionStart(directory, startMs) {
45068
45068
  const filePath = path32.join(sessionDir, SESSION_START_FILE);
45069
45069
  const line = JSON.stringify({ startMs, ts: Date.now() });
45070
45070
  mkdirSync11(sessionDir, { recursive: true });
45071
- writeFileSync6(filePath, line + `
45071
+ writeFileSync6(filePath, `${line}
45072
45072
  `, { flag: "a" });
45073
45073
  } catch {}
45074
45074
  }
@@ -113317,7 +113317,25 @@ function safeRealpathSync(targetPath, fallback, realpathResolver = realpathSync1
113317
113317
  // src/tools/repo-graph/types.ts
113318
113318
  import * as path117 from "node:path";
113319
113319
  var REPO_GRAPH_FILENAME = "repo-graph.json";
113320
- var GRAPH_SCHEMA_VERSION = "1.0.0";
113320
+ var GRAPH_SCHEMA_VERSION = "1.1.0";
113321
+ function isSchemaVersionAtLeast(version5, minimum) {
113322
+ const parse10 = (v) => v.split(".").map((part) => {
113323
+ const n = Number.parseInt(part, 10);
113324
+ return Number.isFinite(n) ? n : 0;
113325
+ });
113326
+ const a = parse10(version5 ?? "");
113327
+ const b = parse10(minimum);
113328
+ const len = Math.max(a.length, b.length);
113329
+ for (let i2 = 0;i2 < len; i2++) {
113330
+ const av = a[i2] ?? 0;
113331
+ const bv = b[i2] ?? 0;
113332
+ if (av > bv)
113333
+ return true;
113334
+ if (av < bv)
113335
+ return false;
113336
+ }
113337
+ return true;
113338
+ }
113321
113339
  var FILE_ROLE_VALUES = [
113322
113340
  "api_route",
113323
113341
  "middleware",
@@ -113492,6 +113510,19 @@ function validateGraphNode(node) {
113492
113510
  throw new Error(`Invalid node: imports contains control characters (file=${node.filePath}, value="${preview}")`);
113493
113511
  }
113494
113512
  }
113513
+ if (node.exportLines !== undefined) {
113514
+ if (typeof node.exportLines !== "object" || node.exportLines === null || Array.isArray(node.exportLines)) {
113515
+ throw new Error("Invalid node: exportLines must be an object");
113516
+ }
113517
+ for (const [name2, line] of Object.entries(node.exportLines)) {
113518
+ if (containsControlChars(name2)) {
113519
+ throw new Error("Invalid node: exportLines key contains control characters");
113520
+ }
113521
+ if (typeof line !== "number" || !Number.isFinite(line) || line < 0) {
113522
+ throw new Error("Invalid node: exportLines values must be non-negative numbers");
113523
+ }
113524
+ }
113525
+ }
113495
113526
  if (node.ontology !== undefined) {
113496
113527
  validateOntologyStrings(node);
113497
113528
  }
@@ -113609,6 +113640,19 @@ function validateGraphEdge(edge) {
113609
113640
  }
113610
113641
  }
113611
113642
  }
113643
+ if (edge.usedSymbols !== undefined) {
113644
+ if (!Array.isArray(edge.usedSymbols)) {
113645
+ throw new Error("Invalid edge: usedSymbols must be an array");
113646
+ }
113647
+ for (const symbol3 of edge.usedSymbols) {
113648
+ if (typeof symbol3 !== "string") {
113649
+ throw new Error("Invalid edge: usedSymbols must be an array of strings");
113650
+ }
113651
+ if (containsControlChars(symbol3)) {
113652
+ throw new Error("Invalid edge: usedSymbols contains control characters");
113653
+ }
113654
+ }
113655
+ }
113612
113656
  }
113613
113657
 
113614
113658
  // src/tools/repo-graph/builder.ts
@@ -113617,7 +113661,9 @@ var _internals74 = {
113617
113661
  extractTSSymbols,
113618
113662
  extractPythonSymbols,
113619
113663
  parseFileImports,
113620
- extractFileOntology
113664
+ extractFileOntology,
113665
+ stripComments: stripComments2,
113666
+ computeUsedSymbols
113621
113667
  };
113622
113668
  var SKIP_DIRECTORIES3 = new Set([
113623
113669
  "node_modules",
@@ -113914,11 +113960,89 @@ function parseFileImports(rawContent) {
113914
113960
  imports.push({
113915
113961
  specifier: modulePath,
113916
113962
  importType,
113917
- importedSymbols: parseImportedSymbols(matchedString, importType)
113963
+ importedSymbols: parseImportedSymbols(matchedString, importType),
113964
+ bindings: parseImportBindings(matchedString, importType),
113965
+ reExport: /^\s*export\b/.test(matchedString)
113918
113966
  });
113919
113967
  }
113920
113968
  return imports;
113921
113969
  }
113970
+ function parseImportBindings(matchedString, importType) {
113971
+ if (importType === "namespace")
113972
+ return [];
113973
+ if (importType === "default") {
113974
+ const defaultMatch = matchedString.match(/^import\s+(\w+)\s+from\s+['"`]/);
113975
+ return defaultMatch ? [{ imported: "default", local: defaultMatch[1] }] : [];
113976
+ }
113977
+ if (importType !== "named")
113978
+ return [];
113979
+ const braceMatch = matchedString.match(/\{\s*([\s\S]*?)\s*\}/);
113980
+ if (!braceMatch)
113981
+ return [];
113982
+ const bindings = [];
113983
+ const seen = new Set;
113984
+ for (const rawPart of braceMatch[1].split(",")) {
113985
+ const part = rawPart.trim().replace(/^type\s+/, "");
113986
+ if (!part)
113987
+ continue;
113988
+ const aliasSplit = part.split(/\s+as\s+/i);
113989
+ const imported = aliasSplit[0].trim();
113990
+ const local = (aliasSplit[1] ?? aliasSplit[0]).trim();
113991
+ if (!/^[A-Za-z_$][\w$]*$/.test(imported))
113992
+ continue;
113993
+ if (!/^[A-Za-z_$][\w$]*$/.test(local))
113994
+ continue;
113995
+ if (seen.has(imported))
113996
+ continue;
113997
+ seen.add(imported);
113998
+ bindings.push({ imported, local });
113999
+ }
114000
+ return bindings;
114001
+ }
114002
+ var SAFE_USAGE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
114003
+ function computeUsedSymbols(strippedContent, bindings) {
114004
+ if (bindings.length === 0)
114005
+ return [];
114006
+ const used = new Set;
114007
+ for (const binding of bindings) {
114008
+ if (!SAFE_USAGE_IDENTIFIER.test(binding.local)) {
114009
+ used.add(binding.imported);
114010
+ continue;
114011
+ }
114012
+ const re = new RegExp(`\\b${binding.local}\\b`, "g");
114013
+ let count = 0;
114014
+ for (const _match of strippedContent.matchAll(re)) {
114015
+ count++;
114016
+ if (count > 1)
114017
+ break;
114018
+ }
114019
+ if (count > 1)
114020
+ used.add(binding.imported);
114021
+ }
114022
+ return [...used].sort((a, b) => a.localeCompare(b));
114023
+ }
114024
+ function usedSymbolsForImport(parsed, strippedContent) {
114025
+ if (parsed.importType === "namespace" || parsed.importType === "sideeffect" || parsed.importType === "require") {
114026
+ return;
114027
+ }
114028
+ if (parsed.reExport) {
114029
+ return [...new Set(parsed.bindings.map((b) => b.imported))].sort((a, b) => a.localeCompare(b));
114030
+ }
114031
+ return computeUsedSymbols(strippedContent, parsed.bindings);
114032
+ }
114033
+ function collectExports(symbols2) {
114034
+ const exported = symbols2.filter((s) => s.exported);
114035
+ const exports = exported.map((s) => s.signature === `default ${s.name}` ? "default" : s.name);
114036
+ const exportLines = {};
114037
+ for (let i2 = 0;i2 < exported.length; i2++) {
114038
+ const s = exported[i2];
114039
+ const name2 = exports[i2];
114040
+ if (typeof s.line === "number" && Number.isFinite(s.line) && exportLines[name2] === undefined) {
114041
+ exportLines[name2] = s.line;
114042
+ }
114043
+ }
114044
+ return { exports, exportLines };
114045
+ }
113922
114046
  function parseImportedSymbols(matchedString, importType) {
113923
114047
  if (importType === "namespace")
113924
114048
  return ["*"];
@@ -114062,22 +114186,23 @@ function scanFile(filePath, absoluteRoot, maxFileSize) {
114062
114186
  }
114063
114187
  const ext = path118.extname(filePath).toLowerCase();
114064
114188
  let exports = [];
114189
+ let exportLines = {};
114065
114190
  try {
114066
114191
  if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
114067
114192
  const relativePath = path118.relative(absoluteRoot, filePath);
114068
- const symbols2 = _internals74.extractTSSymbols(relativePath, absoluteRoot);
114069
- exports = symbols2.filter((s) => s.exported).map((s) => s.name);
114193
+ ({ exports, exportLines } = collectExports(_internals74.extractTSSymbols(relativePath, absoluteRoot)));
114070
114194
  } else if (ext === ".py") {
114071
114195
  const relativePath = path118.relative(absoluteRoot, filePath);
114072
- const symbols2 = _internals74.extractPythonSymbols(relativePath, absoluteRoot);
114073
- exports = symbols2.filter((s) => s.exported).map((s) => s.name);
114196
+ ({ exports, exportLines } = collectExports(_internals74.extractPythonSymbols(relativePath, absoluteRoot)));
114074
114197
  }
114075
114198
  const parsedImports = _internals74.parseFileImports(content);
114199
+ const strippedForUsage = parsedImports.length > 0 ? _internals74.stripComments(content) : "";
114076
114200
  const moduleName = toModuleName(filePath, absoluteRoot);
114077
114201
  const node = {
114078
114202
  filePath,
114079
114203
  moduleName,
114080
114204
  exports,
114205
+ ...Object.keys(exportLines).length > 0 ? { exportLines } : {},
114081
114206
  imports: parsedImports.map((p) => p.specifier),
114082
114207
  language: getLanguage(filePath),
114083
114208
  mtime: fileStats.mtime.toISOString(),
@@ -114095,12 +114220,14 @@ function scanFile(filePath, absoluteRoot, maxFileSize) {
114095
114220
  for (const parsed of sortedImports) {
114096
114221
  const resolvedTarget = resolveModuleSpecifier(absoluteRoot, filePath, parsed.specifier);
114097
114222
  if (resolvedTarget !== null) {
114223
+ const usedSymbols = usedSymbolsForImport(parsed, strippedForUsage);
114098
114224
  edges.push({
114099
114225
  source: filePath,
114100
114226
  target: resolvedTarget,
114101
114227
  importSpecifier: parsed.specifier,
114102
114228
  importType: parsed.importType,
114103
- importedSymbols: parsed.importedSymbols
114229
+ importedSymbols: parsed.importedSymbols,
114230
+ ...usedSymbols !== undefined ? { usedSymbols } : {}
114104
114231
  });
114105
114232
  }
114106
114233
  }
@@ -114356,6 +114483,113 @@ function getSymbolConsumers(graph, filePath, symbolName) {
114356
114483
  refs.sort((a, b) => a.file.localeCompare(b.file));
114357
114484
  return refs;
114358
114485
  }
114486
+ function getCallers(graph, filePath, symbolName) {
114487
+ const node = getGraphNode(graph, filePath);
114488
+ if (!node)
114489
+ return [];
114490
+ const targetKey = normalizeGraphPath(node.filePath);
114491
+ const refs = [];
114492
+ const seen = new Set;
114493
+ for (const edge of graph.edges) {
114494
+ if (normalizeGraphPath(edge.target) !== targetKey)
114495
+ continue;
114496
+ const file3 = moduleNameForEdgePath(graph, edge.source);
114497
+ if (seen.has(file3))
114498
+ continue;
114499
+ if (edge.usedSymbols !== undefined) {
114500
+ if (edge.usedSymbols.includes(symbolName)) {
114501
+ seen.add(file3);
114502
+ refs.push({ file: file3, resolution: "used" });
114503
+ }
114504
+ continue;
114505
+ }
114506
+ const imported = edge.importedSymbols ?? [];
114507
+ if (edge.importType === "namespace" || imported.includes(symbolName)) {
114508
+ seen.add(file3);
114509
+ refs.push({ file: file3, resolution: "imported" });
114510
+ }
114511
+ }
114512
+ refs.sort((a, b) => a.file.localeCompare(b.file));
114513
+ return refs;
114514
+ }
114515
+ var DEAD_EXPORT_EXCLUDED_ROLES = new Set([
114516
+ "api_route",
114517
+ "cli_command",
114518
+ "test_file",
114519
+ "agent",
114520
+ "hook",
114521
+ "middleware"
114522
+ ]);
114523
+ function getDeadExports(graph, options) {
114524
+ if (!isSchemaVersionAtLeast(graph.schema_version, "1.1.0")) {
114525
+ return {
114526
+ schemaSupported: false,
114527
+ analyzedFiles: 0,
114528
+ skippedUnresolvable: 0,
114529
+ candidates: [],
114530
+ note: 'Graph predates schema 1.1.0 (no usedSymbols data). Run repo_map action="build" to enable dead_exports.'
114531
+ };
114532
+ }
114533
+ const usage = new Map;
114534
+ for (const edge of graph.edges) {
114535
+ const target = normalizeGraphPath(edge.target);
114536
+ let entry = usage.get(target);
114537
+ if (!entry) {
114538
+ entry = { used: new Set, unresolvable: false };
114539
+ usage.set(target, entry);
114540
+ }
114541
+ if (edge.importType === "namespace" || edge.importType === "sideeffect" || edge.importType === "require") {
114542
+ entry.unresolvable = true;
114543
+ } else if (edge.usedSymbols) {
114544
+ for (const symbol3 of edge.usedSymbols)
114545
+ entry.used.add(symbol3);
114546
+ }
114547
+ }
114548
+ const reverse = getReverseIndex(graph);
114549
+ const candidates = [];
114550
+ let analyzedFiles = 0;
114551
+ let skippedUnresolvable = 0;
114552
+ for (const node of Object.values(graph.nodes)) {
114553
+ if (node.exports.length === 0)
114554
+ continue;
114555
+ const key = normalizeGraphPath(node.filePath);
114556
+ const importerCount = reverse.get(key)?.length ?? 0;
114557
+ if (importerCount === 0)
114558
+ continue;
114559
+ const roles = node.ontology?.roles ?? [];
114560
+ if (roles.some((r) => DEAD_EXPORT_EXCLUDED_ROLES.has(r)))
114561
+ continue;
114562
+ const entry = usage.get(key);
114563
+ if (entry?.unresolvable) {
114564
+ skippedUnresolvable++;
114565
+ continue;
114566
+ }
114567
+ analyzedFiles++;
114568
+ const used = entry?.used ?? new Set;
114569
+ for (const symbol3 of node.exports) {
114570
+ if (symbol3 === "default")
114571
+ continue;
114572
+ if (used.has(symbol3))
114573
+ continue;
114574
+ candidates.push({
114575
+ file: node.moduleName,
114576
+ symbol: symbol3,
114577
+ line: node.exportLines?.[symbol3],
114578
+ importerCount
114579
+ });
114580
+ }
114581
+ }
114582
+ candidates.sort((a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol));
114583
+ const limit = options?.maxCandidates ?? 100;
114584
+ const truncated = candidates.length > limit;
114585
+ return {
114586
+ schemaSupported: true,
114587
+ analyzedFiles,
114588
+ skippedUnresolvable,
114589
+ candidates: candidates.slice(0, limit),
114590
+ note: "Advisory: exported symbols with no detected in-repo reference, limited to files imported by >=1 other file. " + "Regex analysis cannot see dynamic dispatch, string-keyed access, or namespace/barrel re-export usage; verify before removing." + (truncated ? ` Showing ${limit} of ${candidates.length}.` : "")
114591
+ };
114592
+ }
114359
114593
  function getBlastRadius(graph, filePaths, maxDepth = 3) {
114360
114594
  const targetNodes = filePaths.map((filePath) => getGraphNode(graph, filePath)).filter((node) => node !== undefined);
114361
114595
  const targets = targetNodes.map((node) => normalizeLookupPath(node.moduleName));
@@ -142569,7 +142803,9 @@ var VALID_ACTIONS = [
142569
142803
  "key_files",
142570
142804
  "ontology",
142571
142805
  "package_boundaries",
142572
- "preflight_packet"
142806
+ "preflight_packet",
142807
+ "callers",
142808
+ "dead_exports"
142573
142809
  ];
142574
142810
  var MAX_FILE_PATH_LENGTH3 = 500;
142575
142811
  var MAX_SYMBOL_LENGTH2 = 256;
@@ -142633,7 +142869,7 @@ async function loadOrError(directory, action) {
142633
142869
  }
142634
142870
  }
142635
142871
  var repo_map = createSwarmTool({
142636
- description: "Query the repository code graph for structural awareness before editing. " + 'Actions: "build" (build/refresh .swarm/repo-graph.json), "importers" (who imports a file), ' + '"dependencies" (what a file imports), "blast_radius" (transitive dependents + risk), ' + '"localization" (compact context block for a target file), "key_files" (top-N most-imported files), ' + '"ontology" (file roles/routes/data/security/findings), "package_boundaries" (inferred package/layer boundaries), ' + '"preflight_packet" (bounded ontology packet for planning). ' + "Use this before refactoring shared modules to avoid breaking unseen consumers.",
142872
+ description: "Query the repository code graph for structural awareness before editing. " + 'Actions: "build" (build/refresh .swarm/repo-graph.json), "importers" (who imports a file), ' + '"dependencies" (what a file imports), "blast_radius" (transitive dependents + risk), ' + '"localization" (compact context block for a target file), "key_files" (top-N most-imported files), ' + '"ontology" (file roles/routes/data/security/findings), "package_boundaries" (inferred package/layer boundaries), ' + '"preflight_packet" (bounded ontology packet for planning), ' + '"callers" (files that reference an exported symbol, call-site granularity; needs file+symbol), ' + '"dead_exports" (advisory: exported symbols with no detected in-repo reference). ' + "Use this before refactoring shared modules to avoid breaking unseen consumers. " + 'Note: "callers"/"dead_exports" use conservative regex analysis (TS/JS/Python) and cannot see ' + 'dynamic dispatch or namespace/barrel re-export usage; "dead_exports" results are review candidates, not delete directives.',
142637
142873
  args: {
142638
142874
  action: exports_external.enum([
142639
142875
  "build",
@@ -142644,12 +142880,14 @@ var repo_map = createSwarmTool({
142644
142880
  "key_files",
142645
142881
  "ontology",
142646
142882
  "package_boundaries",
142647
- "preflight_packet"
142648
- ]).describe('Query action: "build" | "importers" | "dependencies" | "blast_radius" | "localization" | "key_files" | "ontology" | "package_boundaries" | "preflight_packet"'),
142883
+ "preflight_packet",
142884
+ "callers",
142885
+ "dead_exports"
142886
+ ]).describe('Query action: "build" | "importers" | "dependencies" | "blast_radius" | "localization" | "key_files" | "ontology" | "package_boundaries" | "preflight_packet" | "callers" | "dead_exports"'),
142649
142887
  file: exports_external.string().optional().describe("Target file (workspace-relative or absolute). Required for importers/dependencies/localization/ontology. Optional for preflight_packet."),
142650
142888
  files: exports_external.array(exports_external.string()).optional().describe("Multiple target files for blast_radius/preflight_packet. If omitted, falls back to `file`."),
142651
- symbol: exports_external.string().optional().describe('When provided alongside `file` on action="importers", restrict to consumers of this exported symbol.'),
142652
- top_n: exports_external.number().int().min(1).max(100).optional().describe('For action="key_files" or "package_boundaries": number of entries to return (default 10).'),
142889
+ symbol: exports_external.string().optional().describe('Exported symbol name. Restricts consumers on action="importers"; required for action="callers".'),
142890
+ top_n: exports_external.number().int().min(1).max(100).optional().describe('For action="key_files"/"package_boundaries": entries to return (default 10). For action="dead_exports": max candidates (default 100).'),
142653
142891
  max_depth: exports_external.number().int().min(1).max(10).optional().describe('For action="blast_radius": max BFS depth (default 3).')
142654
142892
  },
142655
142893
  async execute(args2, directory, _ctx) {
@@ -142711,6 +142949,10 @@ var repo_map = createSwarmTool({
142711
142949
  stale
142712
142950
  });
142713
142951
  }
142952
+ if (action === "dead_exports") {
142953
+ const result = getDeadExports(graph, { maxCandidates: a.top_n ?? 100 });
142954
+ return ok(action, { ...result, stale });
142955
+ }
142714
142956
  if (action === "preflight_packet") {
142715
142957
  const inputs = a.files && a.files.length > 0 ? a.files : a.file ? [a.file] : [];
142716
142958
  for (const f of inputs) {
@@ -142770,6 +143012,22 @@ var repo_map = createSwarmTool({
142770
143012
  stale
142771
143013
  });
142772
143014
  }
143015
+ if (action === "callers") {
143016
+ if (a.symbol === undefined) {
143017
+ return err2(action, "callers requires `symbol` (the exported name)");
143018
+ }
143019
+ const sErr = validateSymbol(a.symbol);
143020
+ if (sErr)
143021
+ return err2(action, `invalid symbol: ${sErr}`);
143022
+ const callers = getCallers(graph, target, a.symbol);
143023
+ return ok(action, {
143024
+ target,
143025
+ symbol: a.symbol,
143026
+ count: callers.length,
143027
+ callers,
143028
+ stale
143029
+ });
143030
+ }
142773
143031
  if (action === "dependencies") {
142774
143032
  const deps = getDependencies(graph, target);
142775
143033
  return ok(action, {
@@ -26,6 +26,8 @@ export declare const _internals: {
26
26
  extractPythonSymbols: typeof extractPythonSymbols;
27
27
  parseFileImports: typeof parseFileImports;
28
28
  extractFileOntology: typeof extractFileOntology;
29
+ stripComments: typeof stripComments;
30
+ computeUsedSymbols: typeof computeUsedSymbols;
29
31
  };
30
32
  /**
31
33
  * Add or update a node in the graph.
@@ -66,6 +68,16 @@ export declare function resolveModuleSpecifier(workspaceRoot: string, sourceFile
66
68
  /**
67
69
  * A parsed import with its specifier and type.
68
70
  */
71
+ /**
72
+ * A single imported binding: the symbol's *exported* name in the target file
73
+ * and the *local* name it is bound to in the importing file (differs when an
74
+ * `as` alias or default import is used). Used to attribute call-site usage back
75
+ * to the correct exported symbol.
76
+ */
77
+ interface ImportBinding {
78
+ imported: string;
79
+ local: string;
80
+ }
69
81
  interface ParsedImport {
70
82
  /** The module specifier (e.g., './foo', 'lodash') */
71
83
  specifier: string;
@@ -73,8 +85,42 @@ interface ParsedImport {
73
85
  importType: 'default' | 'named' | 'namespace' | 'require' | 'sideeffect';
74
86
  /** Named imported symbols when statically detectable */
75
87
  importedSymbols: string[];
88
+ /** Alias-aware imported→local bindings for usage attribution */
89
+ bindings: ImportBinding[];
90
+ /** True for `export { x } from '...'` re-exports (symbols are re-exposed). */
91
+ reExport: boolean;
76
92
  }
93
+ /**
94
+ * Strip line (`//…`) and block (`/* … *\/`) comments from JS/TS source while
95
+ * preserving string, template-literal, and regex-literal contents (DD-C010).
96
+ * Import specifiers live inside string literals, so strings must be kept
97
+ * intact; only comment spans are removed. This is a bounded single-pass scanner
98
+ * — not a full parser (AST parsing in the repo-graph init path would violate
99
+ * AGENTS.md invariant 1) — and it eliminates the most common source of false
100
+ * import edges: import-like text inside comments (`// import x from "y"`).
101
+ *
102
+ * It is string-aware (a `//` inside `"http://…"` is not a comment) and
103
+ * regex-aware (a regex literal such as `/[/*]/` must not be mistaken for the
104
+ * start of a block comment, which would otherwise run to EOF and delete real
105
+ * imports). Regex-vs-division is disambiguated by the previous significant
106
+ * character (REGEX_ALLOWED_AFTER).
107
+ */
108
+ declare function stripComments(content: string): string;
77
109
  declare function parseFileImports(rawContent: string): ParsedImport[];
110
+ /**
111
+ * Conservatively determine which imported bindings are actually referenced in
112
+ * the importing file's body.
113
+ *
114
+ * Heuristic: in a well-formed import statement, each local binding name appears
115
+ * exactly once. Counting occurrences of the local name across the
116
+ * comment-stripped file content, a count > 1 means at least one body reference.
117
+ * Strings are intentionally *not* stripped, so the bias is toward "used" — a
118
+ * conservative direction that avoids false dead-export positives. Bindings whose
119
+ * local name cannot be safely word-boundary matched are assumed used.
120
+ *
121
+ * @returns the *exported* names (binding.imported) judged to be used.
122
+ */
123
+ declare function computeUsedSymbols(strippedContent: string, bindings: readonly ImportBinding[]): string[];
78
124
  /**
79
125
  * Result of scanning a single file for graph updates.
80
126
  */
@@ -1,10 +1,35 @@
1
- import type { BlastRadiusResult, FileOntology, FileReference, GraphNode, LocalizationBlock, PackageBoundarySummary, RepoGraph, SymbolReference } from './types';
1
+ import type { BlastRadiusResult, CallerReference, DeadExportsResult, FileOntology, FileReference, GraphNode, LocalizationBlock, PackageBoundarySummary, RepoGraph, SymbolReference } from './types';
2
2
  export declare function getGraphNode(graph: RepoGraph, input: string): GraphNode | undefined;
3
3
  export declare function resetQueryCache(): void;
4
4
  export declare function isGraphFresh(graph: RepoGraph | null, maxAgeMs?: number): boolean;
5
5
  export declare function getImporters(graph: RepoGraph, filePath: string): FileReference[];
6
6
  export declare function getDependencies(graph: RepoGraph, filePath: string): FileReference[];
7
7
  export declare function getSymbolConsumers(graph: RepoGraph, filePath: string, symbolName: string): SymbolReference[];
8
+ /**
9
+ * Files that actually *reference* an exported symbol of `filePath` — call-site
10
+ * granularity, not just "imports the file". On schema >= 1.1.0 graphs this uses
11
+ * per-edge `usedSymbols`; on older graphs (or namespace imports) it falls back
12
+ * to import-level matching, flagged via `resolution: 'imported'`.
13
+ */
14
+ export declare function getCallers(graph: RepoGraph, filePath: string, symbolName: string): CallerReference[];
15
+ export interface DeadExportsOptions {
16
+ /** Max candidates returned (default 100). */
17
+ maxCandidates?: number;
18
+ }
19
+ /**
20
+ * Conservatively detect exported symbols with no detected in-repo reference.
21
+ *
22
+ * Scoping for precision (advisory "candidate" output, never a delete directive):
23
+ * - Requires schema >= 1.1.0 (per-edge usedSymbols); otherwise returns
24
+ * schemaSupported=false so the caller can prompt a rebuild.
25
+ * - Only considers files imported by >= 1 other file — a file with no
26
+ * importers is a likely public-API entry / CLI / test, not dead code.
27
+ * - Skips files imported anywhere via namespace/side-effect/require/dynamic
28
+ * imports, where per-symbol usage is unresolvable.
29
+ * - Excludes framework-invoked roles (routes, CLIs, tests, agents, hooks,
30
+ * middleware) and the synthetic 'default' export.
31
+ */
32
+ export declare function getDeadExports(graph: RepoGraph, options?: DeadExportsOptions): DeadExportsResult;
8
33
  export declare function getBlastRadius(graph: RepoGraph, filePaths: string[], maxDepth?: number): BlastRadiusResult;
9
34
  export declare function getKeyFiles(graph: RepoGraph, topN?: number): GraphNode[];
10
35
  export declare function getFileOntology(graph: RepoGraph, filePath: string): FileOntology | null;
@@ -7,7 +7,23 @@
7
7
  * Every other submodule imports from here.
8
8
  */
9
9
  export declare const REPO_GRAPH_FILENAME = "repo-graph.json";
10
- export declare const GRAPH_SCHEMA_VERSION = "1.0.0";
10
+ /**
11
+ * Graph schema version.
12
+ *
13
+ * 1.1.0 added per-edge `usedSymbols` (imported symbols actually referenced in
14
+ * the importing file) and per-node `exportLines`, enabling the `callers` and
15
+ * `dead_exports` queries. Both fields are optional, so graphs written by older
16
+ * versions (1.0.0) still load — but `dead_exports` requires >= 1.1.0 data and
17
+ * self-gates via {@link isSchemaVersionAtLeast} rather than relying on the
18
+ * loader (which only checks that a version string is present, not its value).
19
+ */
20
+ export declare const GRAPH_SCHEMA_VERSION = "1.1.0";
21
+ /**
22
+ * Compare dotted numeric version strings (e.g. '1.1.0' >= '1.1.0').
23
+ * Missing/non-numeric segments are treated as 0. Returns true when `version`
24
+ * is greater than or equal to `minimum`.
25
+ */
26
+ export declare function isSchemaVersionAtLeast(version: string | undefined, minimum: string): boolean;
11
27
  export declare const FILE_ROLE_VALUES: readonly ["api_route", "middleware", "service_module", "data_module", "swarm_tool", "agent", "hook", "config", "schema", "test_file", "cli_command", "documentation", "source_module"];
12
28
  export type FileRole = (typeof FILE_ROLE_VALUES)[number];
13
29
  export declare const ROUTE_METHOD_VALUES: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "ALL"];
@@ -73,6 +89,13 @@ export interface GraphNode {
73
89
  moduleName: string;
74
90
  /** Exported symbols from this file */
75
91
  exports: string[];
92
+ /**
93
+ * Definition line for each exported symbol, keyed by symbol name (1-based).
94
+ * Optional and best-effort: present on graphs built at schema >= 1.1.0,
95
+ * absent for symbols whose line could not be determined. Used to point
96
+ * `dead_exports` candidates at a location.
97
+ */
98
+ exportLines?: Record<string, number>;
76
99
  /** Imported module specifiers */
77
100
  imports: string[];
78
101
  /** Language/extension of the file */
@@ -98,6 +121,14 @@ export interface GraphEdge {
98
121
  importType: ImportType;
99
122
  /** Named symbols imported from the target, when statically detectable */
100
123
  importedSymbols?: string[];
124
+ /**
125
+ * The subset of the target's exported symbols (by their *exported* name)
126
+ * that are actually referenced in the source file's body — not merely
127
+ * imported. Computed at build time via a conservative, alias-aware textual
128
+ * scan (schema >= 1.1.0). Absent on namespace/side-effect/require/dynamic
129
+ * imports, where individual symbol usage is not statically resolvable.
130
+ */
131
+ usedSymbols?: string[];
101
132
  }
102
133
  export interface FileReference {
103
134
  file: string;
@@ -109,6 +140,47 @@ export interface SymbolReference {
109
140
  line?: number;
110
141
  importedAs: string;
111
142
  }
143
+ /**
144
+ * A file that references a specific exported symbol of a target file.
145
+ * `resolution` records how confidently the usage was attributed:
146
+ * - 'used' → the symbol was found referenced in the source body
147
+ * - 'imported' → fallback for graphs predating usedSymbols (schema < 1.1.0);
148
+ * the symbol is imported but body usage was not analyzed
149
+ */
150
+ export interface CallerReference {
151
+ file: string;
152
+ resolution: 'used' | 'imported';
153
+ }
154
+ /**
155
+ * An exported symbol with no detected in-repo reference. Advisory only —
156
+ * regex-based analysis cannot see dynamic dispatch, string-keyed access, or
157
+ * usage through namespace/barrel re-exports, so this is a *candidate* for
158
+ * review, never a directive to delete.
159
+ */
160
+ export interface DeadExportCandidate {
161
+ /** Module name (workspace-relative) of the file that owns the export */
162
+ file: string;
163
+ /** The exported symbol name */
164
+ symbol: string;
165
+ /** Definition line, when known (from exportLines) */
166
+ line?: number;
167
+ /** How many other in-repo files import this file at all */
168
+ importerCount: number;
169
+ }
170
+ export interface DeadExportsResult {
171
+ /** False when the graph predates schema 1.1.0 (rebuild required). */
172
+ schemaSupported: boolean;
173
+ /** Files whose exports were analyzed (imported by >= 1 other file). */
174
+ analyzedFiles: number;
175
+ /**
176
+ * Files skipped because at least one importer used a namespace/side-effect/
177
+ * require/dynamic import, making per-symbol usage unresolvable.
178
+ */
179
+ skippedUnresolvable: number;
180
+ candidates: DeadExportCandidate[];
181
+ /** Human-readable note describing scope and limitations of the result. */
182
+ note: string;
183
+ }
112
184
  export interface BlastRadiusResult {
113
185
  target: string[];
114
186
  directDependents: string[];
@@ -20,8 +20,9 @@ export { clearCache, getCachedGraph, getCachedMtime, isDirty, markDirty, setCach
20
20
  export { updateGraphForFiles } from './repo-graph/incremental';
21
21
  export type { ExtractFileOntologyInput } from './repo-graph/ontology';
22
22
  export { extractFileOntology } from './repo-graph/ontology';
23
- export { buildOntologyPreflightPacket, getBlastRadius, getDependencies, getFileOntology, getGraphNode, getImporters, getKeyFiles, getLocalizationContext, getPackageBoundaries, getSymbolConsumers, isGraphFresh, resetQueryCache, } from './repo-graph/query';
23
+ export type { DeadExportsOptions } from './repo-graph/query';
24
+ export { buildOntologyPreflightPacket, getBlastRadius, getCallers, getDeadExports, getDependencies, getFileOntology, getGraphNode, getImporters, getKeyFiles, getLocalizationContext, getPackageBoundaries, getSymbolConsumers, isGraphFresh, resetQueryCache, } from './repo-graph/query';
24
25
  export { getGraphPath, loadGraph, loadGraphSync, loadOrCreateGraph, saveGraph, saveIfDirty, } from './repo-graph/storage';
25
- export type { BlastRadiusResult, BuildWorkspaceGraphOptions, ConventionFact, DataOperationFact, FileOntology, FileReference, FileRole, GraphEdge, GraphNode, LocalizationBlock, OntologyFinding, PackageBoundarySummary, RepoGraph, RouteFact, RouteMethod, SecurityFact, SymbolReference, } from './repo-graph/types';
26
- export { createEmptyGraph, GRAPH_SCHEMA_VERSION, normalizeGraphPath, REPO_GRAPH_FILENAME, updateGraphMetadata, } from './repo-graph/types';
26
+ export type { BlastRadiusResult, BuildWorkspaceGraphOptions, CallerReference, ConventionFact, DataOperationFact, DeadExportCandidate, DeadExportsResult, FileOntology, FileReference, FileRole, GraphEdge, GraphNode, LocalizationBlock, OntologyFinding, PackageBoundarySummary, RepoGraph, RouteFact, RouteMethod, SecurityFact, SymbolReference, } from './repo-graph/types';
27
+ export { createEmptyGraph, GRAPH_SCHEMA_VERSION, isSchemaVersionAtLeast, normalizeGraphPath, REPO_GRAPH_FILENAME, updateGraphMetadata, } from './repo-graph/types';
27
28
  export { validateGraphEdge, validateGraphNode, validateWorkspace, } from './repo-graph/validation';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.85.0",
3
+ "version": "7.86.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",