archbyte 0.2.9 → 0.3.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.
@@ -6,7 +6,6 @@ export { createProvider, detectConfig } from "./providers/router.js";
6
6
  export { AnthropicProvider } from "./providers/anthropic.js";
7
7
  export { OpenAIProvider } from "./providers/openai.js";
8
8
  export { GoogleProvider } from "./providers/google.js";
9
- export { OllamaProvider } from "./providers/ollama.js";
10
9
  export { LocalFSBackend } from "./tools/local-fs.js";
11
10
  export { ClaudeCodeBackend } from "./tools/claude-code.js";
12
11
  export { AGENT_TOOLS } from "./tools/tool-definitions.js";
@@ -8,7 +8,6 @@ export { createProvider, detectConfig } from "./providers/router.js";
8
8
  export { AnthropicProvider } from "./providers/anthropic.js";
9
9
  export { OpenAIProvider } from "./providers/openai.js";
10
10
  export { GoogleProvider } from "./providers/google.js";
11
- export { OllamaProvider } from "./providers/ollama.js";
12
11
  // Tools
13
12
  export { LocalFSBackend } from "./tools/local-fs.js";
14
13
  export { ClaudeCodeBackend } from "./tools/claude-code.js";
@@ -3,10 +3,19 @@
3
3
  import { extractJSON } from "../response-parser.js";
4
4
  import { getPrompt } from "../../prompts.js";
5
5
  import "../../prompt-data.js";
6
- function formatFileTree(ctx) {
6
+ function formatFileTree(ctx, affectedPaths) {
7
7
  // Show directories + key files only (not all 1000+ files) to stay within context limits
8
8
  const lines = [];
9
- const MAX_LINES = 300;
9
+ const MAX_LINES = affectedPaths ? 150 : 300;
10
+ function isAffectedSubtree(dirPath) {
11
+ if (!affectedPaths)
12
+ return true;
13
+ for (const p of affectedPaths) {
14
+ if (p.startsWith(dirPath + "/") || dirPath.startsWith(p + "/") || dirPath === p)
15
+ return true;
16
+ }
17
+ return false;
18
+ }
10
19
  function walk(entries, indent) {
11
20
  for (const e of entries) {
12
21
  if (lines.length >= MAX_LINES)
@@ -14,6 +23,11 @@ function formatFileTree(ctx) {
14
23
  if (e.type === "directory") {
15
24
  const fileCount = e.children?.filter((c) => c.type === "file").length ?? 0;
16
25
  const dirCount = e.children?.filter((c) => c.type === "directory").length ?? 0;
26
+ if (affectedPaths && indent >= 1 && !isAffectedSubtree(e.path)) {
27
+ // Collapse unchanged directories
28
+ lines.push(`${" ".repeat(indent)}${e.path}/ ... (${fileCount} files, unchanged)`);
29
+ continue;
30
+ }
17
31
  lines.push(`${" ".repeat(indent)}${e.path}/ (${fileCount} files, ${dirCount} dirs)`);
18
32
  if (e.children)
19
33
  walk(e.children, indent + 1);
@@ -48,8 +62,49 @@ export const componentIdentifier = {
48
62
  buildPrompt(ctx, priorResults) {
49
63
  const system = getPrompt("pipeline/component-identifier");
50
64
  const incremental = priorResults?.["__incremental__"];
51
- const fileTree = formatFileTree(ctx);
52
- const configs = formatConfigs(ctx);
65
+ // Build set of affected paths for tree filtering
66
+ let affectedPaths;
67
+ if (incremental) {
68
+ affectedPaths = new Set();
69
+ // Include paths of affected components
70
+ for (const compId of incremental.affectedComponents) {
71
+ const comp = incremental.existingSpec.components[compId];
72
+ if (comp?.path)
73
+ affectedPaths.add(comp.path);
74
+ }
75
+ // Include paths of neighbor components
76
+ for (const compId of incremental.neighborComponents) {
77
+ const comp = incremental.existingSpec.components[compId];
78
+ if (comp?.path)
79
+ affectedPaths.add(comp.path);
80
+ }
81
+ // Include directories of changed files
82
+ for (const f of incremental.changedFiles) {
83
+ const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
84
+ affectedPaths.add(dir);
85
+ }
86
+ }
87
+ const fileTree = formatFileTree(ctx, affectedPaths);
88
+ // Filter configs: in incremental mode, only include configs from affected dirs + root configs
89
+ let configs;
90
+ if (incremental && affectedPaths) {
91
+ const filteredConfigs = ctx.codeSamples.configFiles.filter((cf) => {
92
+ const dir = cf.path.includes("/") ? cf.path.substring(0, cf.path.lastIndexOf("/")) : ".";
93
+ // Always include root configs
94
+ if (!cf.path.includes("/"))
95
+ return true;
96
+ // Include configs from affected directories
97
+ for (const p of affectedPaths) {
98
+ if (dir.startsWith(p) || p.startsWith(dir))
99
+ return true;
100
+ }
101
+ return false;
102
+ });
103
+ configs = filteredConfigs.map((cf) => `--- ${cf.path} ---\n${cf.content}`).join("\n\n");
104
+ }
105
+ else {
106
+ configs = formatConfigs(ctx);
107
+ }
53
108
  const structureInfo = [
54
109
  `Project: ${ctx.structure.projectName || "(unknown)"}`,
55
110
  `Language: ${ctx.structure.language}`,
@@ -62,9 +117,13 @@ export const componentIdentifier = {
62
117
  const docNotes = ctx.docs.architectureNotes.length > 0
63
118
  ? `\n\nArchitecture notes from docs:\n${ctx.docs.architectureNotes.join("\n")}`
64
119
  : "";
65
- const dockerInfo = ctx.infra.docker.composeFile
66
- ? `\n\nDocker Compose services:\n${ctx.infra.docker.services.map((s) => `- ${s.name}${s.image ? ` (image: ${s.image})` : ""}${s.buildContext ? ` (build: ${s.buildContext})` : ""}${s.dependsOn?.length ? ` depends_on: ${s.dependsOn.join(", ")}` : ""}`).join("\n")}`
67
- : "";
120
+ // In incremental mode, only include Docker info if infra files changed
121
+ let dockerInfo = "";
122
+ if (ctx.infra.docker.composeFile) {
123
+ if (!incremental || incremental.categories.infraFiles.length > 0) {
124
+ dockerInfo = `\n\nDocker Compose services:\n${ctx.infra.docker.services.map((s) => `- ${s.name}${s.image ? ` (image: ${s.image})` : ""}${s.buildContext ? ` (build: ${s.buildContext})` : ""}${s.dependsOn?.length ? ` depends_on: ${s.dependsOn.join(", ")}` : ""}`).join("\n")}`;
125
+ }
126
+ }
68
127
  let incrementalInfo = "";
69
128
  if (incremental) {
70
129
  const existing = Object.entries(incremental.existingSpec.components)
@@ -76,7 +135,7 @@ ${existing}
76
135
  Changed files: ${incremental.changedFiles.join(", ")}
77
136
  Affected components: ${incremental.affectedComponents.length > 0 ? incremental.affectedComponents.join(", ") : "none (check for new components)"}
78
137
 
79
- IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones. Only re-analyze components whose paths overlap with the changed files.`;
138
+ IMPORTANT: Return ALL components — both existing unaffected ones AND updated/new ones. Focus: re-evaluate components in [${[...new Set([...incremental.affectedComponents, ...incremental.neighborComponents])].join(", ")}]. Return other components as-is.`;
80
139
  }
81
140
  const user = `Analyze this project and identify ALL architecturally significant components.
82
141
 
@@ -47,10 +47,36 @@ export const connectionMapper = {
47
47
  parts.push(`- ${ar.method} ${ar.path} (${ar.handlerFile})`);
48
48
  }
49
49
  }
50
- // Import map
51
- const importEntries = Object.entries(ctx.codeSamples.importMap);
50
+ // Import map — filter to affected + neighbor component dirs in incremental mode
51
+ const incremental = priorResults?.["__incremental__"];
52
+ let affectedDirs;
53
+ if (incremental) {
54
+ affectedDirs = new Set();
55
+ for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
56
+ const comp = incremental.existingSpec.components[compId];
57
+ if (comp?.path)
58
+ affectedDirs.add(comp.path);
59
+ }
60
+ for (const f of incremental.changedFiles) {
61
+ const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
62
+ affectedDirs.add(dir);
63
+ }
64
+ }
65
+ let importEntries = Object.entries(ctx.codeSamples.importMap);
66
+ if (affectedDirs && affectedDirs.size > 0) {
67
+ importEntries = importEntries.filter(([file]) => {
68
+ const dir = file.includes("/") ? file.substring(0, file.lastIndexOf("/")) : ".";
69
+ for (const ad of affectedDirs) {
70
+ if (dir.startsWith(ad) || ad.startsWith(dir))
71
+ return true;
72
+ }
73
+ return false;
74
+ });
75
+ }
52
76
  if (importEntries.length > 0) {
53
- parts.push(`\n## Import Graph (${importEntries.length} files)`);
77
+ const totalEntries = Object.keys(ctx.codeSamples.importMap).length;
78
+ const label = affectedDirs ? `${importEntries.length} of ${totalEntries} files, filtered to affected dirs` : `${totalEntries} files`;
79
+ parts.push(`\n## Import Graph (${label})`);
54
80
  // Summarize by directory to keep it concise
55
81
  const dirImports = {};
56
82
  for (const [file, imports] of importEntries) {
@@ -69,8 +95,8 @@ export const connectionMapper = {
69
95
  }
70
96
  }
71
97
  }
72
- // Docker depends_on
73
- if (ctx.infra.docker.composeFile) {
98
+ // Docker depends_on — only if infra changed (or full scan)
99
+ if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.infraFiles.length > 0)) {
74
100
  const deps = ctx.infra.docker.services
75
101
  .filter((s) => s.dependsOn?.length)
76
102
  .map((s) => `${s.name} → ${s.dependsOn.join(", ")}`);
@@ -78,15 +104,14 @@ export const connectionMapper = {
78
104
  parts.push(`\n## Docker depends_on\n${deps.join("\n")}`);
79
105
  }
80
106
  }
81
- // Kubernetes resources
82
- if (ctx.infra.kubernetes.resources.length > 0) {
107
+ // Kubernetes resources — only if infra changed (or full scan)
108
+ if (ctx.infra.kubernetes.resources.length > 0 && (!incremental || incremental.categories.infraFiles.length > 0)) {
83
109
  parts.push("\n## Kubernetes Resources");
84
110
  for (const r of ctx.infra.kubernetes.resources) {
85
111
  parts.push(`- ${r.kind}: ${r.name}${r.namespace ? ` (ns=${r.namespace})` : ""}`);
86
112
  }
87
113
  }
88
114
  // Incremental context
89
- const incremental = priorResults?.["__incremental__"];
90
115
  if (incremental) {
91
116
  const existingConns = incremental.existingSpec.connections
92
117
  .map((c) => `- ${c.from} → ${c.to} (${c.type}${c.async ? ", async" : ""}): ${c.description}`)
@@ -11,6 +11,20 @@ export const flowDetector = {
11
11
  buildPrompt(ctx, priorResults) {
12
12
  const system = getPrompt("pipeline/flow-detector");
13
13
  const incremental = priorResults?.["__incremental__"];
14
+ // Build set of affected directories for filtering
15
+ let affectedDirs;
16
+ if (incremental) {
17
+ affectedDirs = new Set();
18
+ for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
19
+ const comp = incremental.existingSpec.components[compId];
20
+ if (comp?.path)
21
+ affectedDirs.add(comp.path);
22
+ }
23
+ for (const f of incremental.changedFiles) {
24
+ const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
25
+ affectedDirs.add(dir);
26
+ }
27
+ }
14
28
  const parts = [];
15
29
  // Event patterns from static scanner
16
30
  if (ctx.events.hasEDA) {
@@ -18,8 +32,23 @@ export const flowDetector = {
18
32
  for (const p of ctx.events.patterns) {
19
33
  parts.push(` Technology: ${p.technology} (dep: ${p.dependency})`);
20
34
  }
21
- parts.push("\nEvent occurrences in code:");
22
- for (const e of ctx.events.events.slice(0, 30)) {
35
+ // Filter events to affected directories in incremental mode
36
+ let events = ctx.events.events;
37
+ if (affectedDirs && affectedDirs.size > 0) {
38
+ events = events.filter((e) => {
39
+ const dir = e.file.includes("/") ? e.file.substring(0, e.file.lastIndexOf("/")) : ".";
40
+ for (const ad of affectedDirs) {
41
+ if (dir.startsWith(ad) || ad.startsWith(dir))
42
+ return true;
43
+ }
44
+ return false;
45
+ });
46
+ parts.push(`\nEvent occurrences in affected files (${events.length} of ${ctx.events.events.length} total):`);
47
+ }
48
+ else {
49
+ parts.push("\nEvent occurrences in code:");
50
+ }
51
+ for (const e of events.slice(0, 30)) {
23
52
  parts.push(` [${e.type}] ${e.file}: ${e.pattern}`);
24
53
  }
25
54
  }
@@ -33,24 +62,48 @@ export const flowDetector = {
33
62
  parts.push(` ${ep.method} ${ep.path} — ${ep.description}`);
34
63
  }
35
64
  }
36
- // Route file samples
37
- const routeSamples = ctx.codeSamples.samples.filter((s) => s.category === "route-file");
65
+ // Route file samples — filter to affected directories in incremental mode
66
+ let routeSamples = ctx.codeSamples.samples.filter((s) => s.category === "route-file");
67
+ if (affectedDirs && affectedDirs.size > 0) {
68
+ const filtered = routeSamples.filter((s) => {
69
+ const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
70
+ for (const ad of affectedDirs) {
71
+ if (dir.startsWith(ad) || ad.startsWith(dir))
72
+ return true;
73
+ }
74
+ return false;
75
+ });
76
+ if (filtered.length > 0)
77
+ routeSamples = filtered;
78
+ }
38
79
  if (routeSamples.length > 0) {
39
80
  parts.push("\nRoute file excerpts:");
40
81
  for (const s of routeSamples.slice(0, 5)) {
41
82
  parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
42
83
  }
43
84
  }
44
- // Entry points for context
45
- const entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
85
+ // Entry points filter to affected directories in incremental mode
86
+ let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
87
+ if (affectedDirs && affectedDirs.size > 0) {
88
+ const filtered = entrySamples.filter((s) => {
89
+ const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
90
+ for (const ad of affectedDirs) {
91
+ if (dir.startsWith(ad) || ad.startsWith(dir))
92
+ return true;
93
+ }
94
+ return false;
95
+ });
96
+ if (filtered.length > 0)
97
+ entrySamples = filtered;
98
+ }
46
99
  if (entrySamples.length > 0) {
47
100
  parts.push("\nEntry point excerpts:");
48
101
  for (const s of entrySamples.slice(0, 3)) {
49
102
  parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
50
103
  }
51
104
  }
52
- // Docker info for queue/broker identification
53
- if (ctx.infra.docker.composeFile) {
105
+ // Docker info for queue/broker identification — only if event-related files changed
106
+ if (ctx.infra.docker.composeFile && (!incremental || incremental.categories.eventFiles.length > 0 || incremental.categories.infraFiles.length > 0)) {
54
107
  const queueServices = ctx.infra.docker.services.filter((s) => {
55
108
  const img = (s.image ?? "").toLowerCase();
56
109
  return img.includes("rabbit") || img.includes("kafka") || img.includes("redis") ||
@@ -11,6 +11,22 @@ export const serviceDescriber = {
11
11
  buildPrompt(ctx, priorResults) {
12
12
  const system = getPrompt("pipeline/service-describer");
13
13
  const incremental = priorResults?.["__incremental__"];
14
+ // Build affected dirs for filtering
15
+ let affectedDirs;
16
+ if (incremental) {
17
+ affectedDirs = new Set();
18
+ for (const compId of [...incremental.affectedComponents, ...incremental.neighborComponents]) {
19
+ const comp = incremental.existingSpec.components[compId];
20
+ if (comp?.path)
21
+ affectedDirs.add(comp.path);
22
+ }
23
+ for (const f of incremental.changedFiles) {
24
+ const dir = f.includes("/") ? f.substring(0, f.lastIndexOf("/")) : ".";
25
+ affectedDirs.add(dir);
26
+ }
27
+ }
28
+ const hasInfraChanges = incremental ? incremental.categories.infraFiles.length > 0 : true;
29
+ const hasConfigChanges = incremental ? incremental.categories.configFiles.length > 0 : true;
14
30
  const parts = [];
15
31
  // Structure info
16
32
  parts.push(`Project: ${ctx.structure.projectName || "(unknown)"}`);
@@ -24,8 +40,8 @@ export const serviceDescriber = {
24
40
  if (ctx.docs.externalDependencies.length > 0) {
25
41
  parts.push(`\nExternal dependencies mentioned: ${ctx.docs.externalDependencies.join(", ")}`);
26
42
  }
27
- // Docker services
28
- if (ctx.infra.docker.composeFile) {
43
+ // Docker services — only include if infra/config files changed (or full scan)
44
+ if (ctx.infra.docker.composeFile && (hasInfraChanges || hasConfigChanges)) {
29
45
  const svcInfo = ctx.infra.docker.services.map((s) => {
30
46
  const details = [s.name];
31
47
  if (s.image)
@@ -40,22 +56,34 @@ export const serviceDescriber = {
40
56
  });
41
57
  parts.push(`\nDocker services:\n${svcInfo.join("\n")}`);
42
58
  }
43
- // Env vars
44
- if (ctx.envs.environments.length > 0) {
59
+ // Env vars — only include if infra/config files changed (or full scan)
60
+ if (ctx.envs.environments.length > 0 && (hasInfraChanges || hasConfigChanges)) {
45
61
  for (const env of ctx.envs.environments) {
46
62
  parts.push(`\nEnv file "${env.name}" variables: ${env.variables.join(", ")}`);
47
63
  }
48
64
  }
49
- // Code samples (entry points)
50
- const entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
65
+ // Code samples (entry points) — filter to affected dirs in incremental mode
66
+ let entrySamples = ctx.codeSamples.samples.filter((s) => s.category === "entry-point");
67
+ if (affectedDirs && affectedDirs.size > 0) {
68
+ const filtered = entrySamples.filter((s) => {
69
+ const dir = s.path.includes("/") ? s.path.substring(0, s.path.lastIndexOf("/")) : ".";
70
+ for (const ad of affectedDirs) {
71
+ if (dir.startsWith(ad) || ad.startsWith(dir))
72
+ return true;
73
+ }
74
+ return false;
75
+ });
76
+ if (filtered.length > 0)
77
+ entrySamples = filtered;
78
+ }
51
79
  if (entrySamples.length > 0) {
52
80
  parts.push("\nEntry point excerpts:");
53
81
  for (const s of entrySamples.slice(0, 5)) {
54
82
  parts.push(`\n--- ${s.path} ---\n${s.excerpt}`);
55
83
  }
56
84
  }
57
- // Cloud info
58
- if (ctx.infra.cloud.provider) {
85
+ // Cloud info — only if infra changed (or full scan)
86
+ if (ctx.infra.cloud.provider && hasInfraChanges) {
59
87
  parts.push(`\nCloud: ${ctx.infra.cloud.provider}, services: ${ctx.infra.cloud.services.join(", ")}`);
60
88
  }
61
89
  // Incremental context
@@ -10,15 +10,36 @@ export const validator = {
10
10
  phase: "sequential",
11
11
  buildPrompt(ctx, priorResults) {
12
12
  const system = getPrompt("pipeline/validator");
13
+ const incremental = priorResults?.["__incremental__"];
14
+ // Build set of relevant component IDs for filtering
15
+ let relevantComponents;
16
+ if (incremental) {
17
+ relevantComponents = new Set([...incremental.affectedComponents, ...incremental.neighborComponents]);
18
+ }
13
19
  const parts = [];
14
- // Components
20
+ // Components — in incremental mode, show affected + neighbors in full, others as summary
15
21
  const components = priorResults?.["component-identifier"];
16
22
  if (components?.components) {
17
- parts.push("## Components");
18
- for (const c of components.components) {
19
- parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
20
- parts.push(` description: ${c.description || "(none)"}`);
21
- parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
23
+ if (relevantComponents && relevantComponents.size > 0) {
24
+ const relevant = components.components.filter((c) => relevantComponents.has(c.id));
25
+ const others = components.components.length - relevant.length;
26
+ parts.push("## Components (affected + neighbors)");
27
+ for (const c of relevant) {
28
+ parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
29
+ parts.push(` description: ${c.description || "(none)"}`);
30
+ parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
31
+ }
32
+ if (others > 0) {
33
+ parts.push(`\n(${others} other unchanged components omitted)`);
34
+ }
35
+ }
36
+ else {
37
+ parts.push("## Components");
38
+ for (const c of components.components) {
39
+ parts.push(`- ${c.id}: ${c.name} (type=${c.type}, layer=${c.layer}, path=${c.path})`);
40
+ parts.push(` description: ${c.description || "(none)"}`);
41
+ parts.push(` tech: [${(c.technologies || []).join(", ")}]`);
42
+ }
22
43
  }
23
44
  }
24
45
  // Services
@@ -35,11 +56,19 @@ export const validator = {
35
56
  parts.push(`- ${svc.id}: ${svc.name} (${svc.type}) — ${svc.description}`);
36
57
  }
37
58
  }
38
- // Connections
59
+ // Connections — in incremental mode, only show connections involving affected/neighbor components
39
60
  const connections = priorResults?.["connection-mapper"];
40
61
  if (connections?.connections) {
41
- parts.push("\n## Connections");
42
- for (const c of connections.connections) {
62
+ let conns = connections.connections;
63
+ if (relevantComponents && relevantComponents.size > 0) {
64
+ conns = conns.filter((c) => relevantComponents.has(c.from) || relevantComponents.has(c.to));
65
+ const omitted = connections.connections.length - conns.length;
66
+ parts.push(`\n## Connections (involving affected components${omitted > 0 ? `, ${omitted} others omitted` : ""})`);
67
+ }
68
+ else {
69
+ parts.push("\n## Connections");
70
+ }
71
+ for (const c of conns) {
43
72
  parts.push(`- ${c.from} → ${c.to} (${c.type}) ${c.async ? "[async]" : ""}: ${c.description}`);
44
73
  }
45
74
  }
@@ -11,4 +11,5 @@ export declare function runPipeline(ctx: StaticContext, provider: LLMProvider, c
11
11
  input: number;
12
12
  output: number;
13
13
  };
14
+ skippedAgents?: string[];
14
15
  }>;
@@ -15,6 +15,79 @@ function isAuthError(err) {
15
15
  const msg = err instanceof Error ? err.message : String(err);
16
16
  return /401|authentication_error|invalid.*api.?key|unauthorized|invalid.*key/i.test(msg);
17
17
  }
18
+ /**
19
+ * Determine if an agent can be skipped based on what changed.
20
+ * Returns a skip reason string if skippable, or null if the agent must run.
21
+ */
22
+ function shouldSkipAgent(agentId, inc) {
23
+ // Never skip if there are unmapped files — potential new components
24
+ if (inc.hasUnmappedFiles)
25
+ return null;
26
+ const cat = inc.categories;
27
+ const hasInfra = cat.infraFiles.length > 0;
28
+ const hasConfig = cat.configFiles.length > 0;
29
+ const hasEntryPoints = cat.entryPoints.length > 0;
30
+ const hasRoutes = cat.routeFiles.length > 0;
31
+ const hasEvents = cat.eventFiles.length > 0;
32
+ switch (agentId) {
33
+ case "service-describer":
34
+ // Skip when no infra/config/entry-point changes
35
+ if (!hasInfra && !hasConfig && !hasEntryPoints) {
36
+ return "no infra/config/entry-point changes";
37
+ }
38
+ return null;
39
+ case "flow-detector":
40
+ // Skip when no route/event/entry-point changes
41
+ if (!hasRoutes && !hasEvents && !hasEntryPoints) {
42
+ return "no route/event/entry-point changes";
43
+ }
44
+ return null;
45
+ case "validator":
46
+ // Skip when ≤1 affected component and no infra changes
47
+ if (inc.affectedComponents.length <= 1 && !hasInfra) {
48
+ return `only ${inc.affectedComponents.length} component affected, no infra changes`;
49
+ }
50
+ return null;
51
+ // component-identifier and connection-mapper never skip
52
+ default:
53
+ return null;
54
+ }
55
+ }
56
+ /** Build fallback data for a skipped agent from existing spec */
57
+ function getFallbackData(agentId, inc) {
58
+ switch (agentId) {
59
+ case "service-describer": {
60
+ const dbs = Object.entries(inc.existingSpec.databases).map(([id, d]) => ({
61
+ id, name: d.name, type: d.type, description: d.description, usedBy: [],
62
+ }));
63
+ const exts = Object.entries(inc.existingSpec["external-services"]).map(([id, s]) => ({
64
+ id, name: s.name, type: s.type, description: s.description, usedBy: [],
65
+ }));
66
+ return {
67
+ projectDescription: "",
68
+ primaryLanguage: "",
69
+ databases: dbs,
70
+ externalServices: exts,
71
+ };
72
+ }
73
+ case "flow-detector":
74
+ return {
75
+ eventConnections: [],
76
+ apiRoutes: [],
77
+ };
78
+ case "validator":
79
+ return {
80
+ componentTypeCorrections: {},
81
+ componentDescriptions: {},
82
+ addedConnections: [],
83
+ removedConnectionKeys: [],
84
+ confidence: 0.8,
85
+ issues: [],
86
+ };
87
+ default:
88
+ return null;
89
+ }
90
+ }
18
91
  /**
19
92
  * Run the multi-agent pipeline: 3 parallel fast agents → 2 sequential agents.
20
93
  * Each agent gets a single chat() call with pre-collected static context.
@@ -22,6 +95,7 @@ function isAuthError(err) {
22
95
  export async function runPipeline(ctx, provider, config, onProgress, incrementalContext) {
23
96
  const agentResults = {};
24
97
  const agentMeta = [];
98
+ const skippedAgents = [];
25
99
  // Pass incremental context to agents via priorResults
26
100
  if (incrementalContext) {
27
101
  agentResults["__incremental__"] = incrementalContext;
@@ -30,10 +104,27 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
30
104
  onProgress?.(`Phase 1: Running ${PARALLEL_AGENTS.length} agents in parallel...`);
31
105
  // Pass agentResults to parallel agents too (contains __incremental__ if set)
32
106
  const parallelPrior = incrementalContext ? agentResults : undefined;
33
- const parallelResults = await Promise.allSettled(PARALLEL_AGENTS.map((agent) => runAgent(agent, ctx, provider, config, parallelPrior, onProgress)));
107
+ // Check for agent skipping
108
+ const parallelTasks = PARALLEL_AGENTS.map((agent) => ({
109
+ agent,
110
+ skipReason: incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null,
111
+ }));
112
+ const parallelResults = await Promise.allSettled(parallelTasks.map(({ agent, skipReason }) => {
113
+ if (skipReason) {
114
+ // Skip this agent — use fallback data
115
+ skippedAgents.push(agent.id);
116
+ onProgress?.(` ${agent.name}: skipped (${skipReason})`);
117
+ const fallback = getFallbackData(agent.id, incrementalContext);
118
+ agentResults[agent.id] = fallback;
119
+ return Promise.resolve(null);
120
+ }
121
+ return runAgent(agent, ctx, provider, config, parallelPrior, onProgress);
122
+ }));
34
123
  let authFailed = false;
35
- for (let i = 0; i < PARALLEL_AGENTS.length; i++) {
36
- const agent = PARALLEL_AGENTS[i];
124
+ for (let i = 0; i < parallelTasks.length; i++) {
125
+ const { agent, skipReason } = parallelTasks[i];
126
+ if (skipReason)
127
+ continue; // Already handled
37
128
  const result = parallelResults[i];
38
129
  if (result.status === "fulfilled" && result.value) {
39
130
  agentResults[agent.id] = result.value.data;
@@ -55,6 +146,15 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
55
146
  // === Phase 2: Sequential agents ===
56
147
  onProgress?.(`Phase 2: Running ${SEQUENTIAL_AGENTS.length} agents sequentially...`);
57
148
  for (const agent of SEQUENTIAL_AGENTS) {
149
+ // Check for agent skipping
150
+ const skipReason = incrementalContext ? shouldSkipAgent(agent.id, incrementalContext) : null;
151
+ if (skipReason) {
152
+ skippedAgents.push(agent.id);
153
+ onProgress?.(` ${agent.name}: skipped (${skipReason})`);
154
+ const fallback = getFallbackData(agent.id, incrementalContext);
155
+ agentResults[agent.id] = fallback;
156
+ continue;
157
+ }
58
158
  try {
59
159
  const result = await runAgent(agent, ctx, provider, config, agentResults, onProgress);
60
160
  if (result) {
@@ -82,13 +182,29 @@ export async function runPipeline(ctx, provider, config, onProgress, incremental
82
182
  if (merged.validation.repairs.length > 0) {
83
183
  onProgress?.(`Applied ${merged.validation.repairs.length} deterministic repair(s)`);
84
184
  }
85
- // Log token usage
185
+ // Log per-agent + total token usage
86
186
  const totalInput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.input ?? 0), 0);
87
187
  const totalOutput = agentMeta.reduce((s, a) => s + (a.tokenUsage?.output ?? 0), 0);
88
- if (totalInput > 0) {
89
- onProgress?.(`Token usage: ${totalInput} in / ${totalOutput} out`);
188
+ if (totalInput > 0 || skippedAgents.length > 0) {
189
+ // Log to stderr so progress bar doesn't clobber it
190
+ console.error("\n Token usage breakdown:");
191
+ for (const a of agentMeta) {
192
+ const inp = a.tokenUsage?.input ?? 0;
193
+ const out = a.tokenUsage?.output ?? 0;
194
+ const dur = (a.duration / 1000).toFixed(1);
195
+ console.error(` ${a.agentId.padEnd(22)} ${String(inp).padStart(6)} in ${String(out).padStart(6)} out ${dur}s`);
196
+ }
197
+ for (const id of skippedAgents) {
198
+ console.error(` ${id.padEnd(22)} 0 in 0 out SKIPPED`);
199
+ }
200
+ console.error(` ${"TOTAL".padEnd(22)} ${String(totalInput).padStart(6)} in ${String(totalOutput).padStart(6)} out`);
201
+ if (skippedAgents.length > 0) {
202
+ console.error(` Skipped ${skippedAgents.length} agent(s): ${skippedAgents.join(", ")}`);
203
+ }
204
+ console.error();
205
+ onProgress?.(`Token usage: ${totalInput} in / ${totalOutput} out${skippedAgents.length > 0 ? ` (${skippedAgents.length} skipped)` : ""}`);
90
206
  }
91
- return { ...merged, tokenUsage: { input: totalInput, output: totalOutput } };
207
+ return { ...merged, tokenUsage: { input: totalInput, output: totalOutput }, skippedAgents: skippedAgents.length > 0 ? skippedAgents : undefined };
92
208
  }
93
209
  // Agents that produce large structured output (lists of components/connections) need more tokens
94
210
  const MAX_TOKENS = {
@@ -82,6 +82,14 @@ export interface ValidatorOutput {
82
82
  confidence: number;
83
83
  issues: string[];
84
84
  }
85
+ export interface CategorizedChanges {
86
+ entryPoints: string[];
87
+ routeFiles: string[];
88
+ configFiles: string[];
89
+ infraFiles: string[];
90
+ eventFiles: string[];
91
+ sourceFiles: string[];
92
+ }
85
93
  export interface IncrementalContext {
86
94
  existingSpec: {
87
95
  components: Record<string, {
@@ -123,6 +131,9 @@ export interface IncrementalContext {
123
131
  };
124
132
  changedFiles: string[];
125
133
  affectedComponents: string[];
134
+ categories: CategorizedChanges;
135
+ neighborComponents: string[];
136
+ hasUnmappedFiles: boolean;
126
137
  }
127
138
  export interface PipelineAgentResult {
128
139
  agentId: string;