archbyte 0.2.9 → 0.3.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.
@@ -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;
@@ -7,12 +7,12 @@ export class OpenAIProvider {
7
7
  }
8
8
  async chat(params) {
9
9
  const messages = [
10
- { role: "system", content: params.system },
10
+ { role: "developer", content: params.system },
11
11
  ...params.messages.map((m) => this.toOpenAIMessage(m)),
12
12
  ];
13
13
  const response = await this.client.chat.completions.create({
14
- model: params.model ?? "gpt-4o",
15
- max_tokens: params.maxTokens ?? 8192,
14
+ model: params.model ?? "gpt-5.2",
15
+ max_completion_tokens: params.maxTokens ?? 8192,
16
16
  messages,
17
17
  ...(params.tools?.length
18
18
  ? {
@@ -54,12 +54,12 @@ export class OpenAIProvider {
54
54
  }
55
55
  async *stream(params) {
56
56
  const messages = [
57
- { role: "system", content: params.system },
57
+ { role: "developer", content: params.system },
58
58
  ...params.messages.map((m) => this.toOpenAIMessage(m)),
59
59
  ];
60
60
  const stream = await this.client.chat.completions.create({
61
- model: params.model ?? "gpt-4o",
62
- max_tokens: params.maxTokens ?? 8192,
61
+ model: params.model ?? "gpt-5.2",
62
+ max_completion_tokens: params.maxTokens ?? 8192,
63
63
  messages,
64
64
  stream: true,
65
65
  ...(params.tools?.length
@@ -7,9 +7,9 @@ export const MODEL_MAP = {
7
7
  advanced: "claude-opus-4-6",
8
8
  },
9
9
  openai: {
10
- fast: "gpt-5.2-codex",
10
+ fast: "gpt-5.2",
11
11
  standard: "gpt-5.2",
12
- advanced: "o3",
12
+ advanced: "gpt-5.2",
13
13
  },
14
14
  google: {
15
15
  fast: "gemini-2.5-flash",
@@ -5,7 +5,7 @@ import chalk from "chalk";
5
5
  import { resolveConfig } from "./config.js";
6
6
  import { recordUsage } from "./license-gate.js";
7
7
  import { staticResultToSpec, writeSpec, writeMetadata, loadSpec, loadMetadata } from "./yaml-io.js";
8
- import { getChangedFiles, mapFilesToComponents, shouldRunAgents, isGitAvailable } from "./incremental.js";
8
+ import { getChangedFiles, mapFilesToComponents, shouldRunAgents, isGitAvailable, categorizeChanges, computeNeighbors, getCommitCount } from "./incremental.js";
9
9
  import { progressBar } from "./ui.js";
10
10
  export async function handleAnalyze(options) {
11
11
  const rootDir = options.dir ? path.resolve(options.dir) : process.cwd();
@@ -115,30 +115,63 @@ export async function handleAnalyze(options) {
115
115
  const priorSpec = loadSpec(rootDir);
116
116
  const priorMeta = loadMetadata(rootDir);
117
117
  if (priorSpec && !options.force && isGitAvailable(rootDir) && priorMeta?.lastCommit) {
118
- const changedFiles = getChangedFiles(rootDir, priorMeta.lastCommit);
119
- if (changedFiles.length === 0) {
120
- console.log(chalk.green("No changes detected since last scan. Use --force to re-scan."));
118
+ // Staleness detection: force full scan after 20+ commits
119
+ const commitsSinceScan = getCommitCount(rootDir, priorMeta.lastCommit);
120
+ if (commitsSinceScan >= 20) {
121
+ console.log(chalk.yellow(`${commitsSinceScan} commits since last scan — forcing full re-scan for accuracy.`));
121
122
  console.log();
122
- return;
123
123
  }
124
- const { affected, unmapped } = mapFilesToComponents(changedFiles, priorSpec);
125
- if (!shouldRunAgents(affected, unmapped)) {
126
- console.log(chalk.green("Only config changes detected. No re-scan needed. Use --force to re-scan."));
124
+ else {
125
+ const changedFiles = getChangedFiles(rootDir, priorMeta.lastCommit);
126
+ if (changedFiles.length === 0) {
127
+ console.log(chalk.green("No changes detected since last scan. Use --force to re-scan."));
128
+ console.log();
129
+ return;
130
+ }
131
+ const { affected, unmapped } = mapFilesToComponents(changedFiles, priorSpec);
132
+ if (!shouldRunAgents(affected, unmapped)) {
133
+ console.log(chalk.green("Only config changes detected. No re-scan needed. Use --force to re-scan."));
134
+ console.log();
135
+ return;
136
+ }
137
+ const categories = categorizeChanges(changedFiles);
138
+ const neighborComponents = computeNeighbors(affected, priorSpec);
139
+ incrementalContext = {
140
+ existingSpec: priorSpec,
141
+ changedFiles,
142
+ affectedComponents: [...affected],
143
+ categories,
144
+ neighborComponents,
145
+ hasUnmappedFiles: unmapped.length > 0,
146
+ };
147
+ console.log(chalk.bold("Incremental scan:"));
148
+ console.log(chalk.gray(` Changed files: ${changedFiles.length}`));
149
+ console.log(chalk.gray(` Affected components: ${affected.size > 0 ? [...affected].join(", ") : "none (new files)"}`));
150
+ if (neighborComponents.length > 0) {
151
+ console.log(chalk.gray(` Neighbor components: ${neighborComponents.join(", ")}`));
152
+ }
153
+ if (unmapped.length > 0) {
154
+ console.log(chalk.gray(` Unmapped files: ${unmapped.length} (potential new components — all agents will run)`));
155
+ }
156
+ // Summarize categorized changes
157
+ const catSummary = [];
158
+ if (categories.entryPoints.length)
159
+ catSummary.push(`${categories.entryPoints.length} entry`);
160
+ if (categories.routeFiles.length)
161
+ catSummary.push(`${categories.routeFiles.length} route`);
162
+ if (categories.infraFiles.length)
163
+ catSummary.push(`${categories.infraFiles.length} infra`);
164
+ if (categories.configFiles.length)
165
+ catSummary.push(`${categories.configFiles.length} config`);
166
+ if (categories.eventFiles.length)
167
+ catSummary.push(`${categories.eventFiles.length} event`);
168
+ if (categories.sourceFiles.length)
169
+ catSummary.push(`${categories.sourceFiles.length} source`);
170
+ if (catSummary.length > 0) {
171
+ console.log(chalk.gray(` Categories: ${catSummary.join(", ")}`));
172
+ }
127
173
  console.log();
128
- return;
129
174
  }
130
- incrementalContext = {
131
- existingSpec: priorSpec,
132
- changedFiles,
133
- affectedComponents: [...affected],
134
- };
135
- console.log(chalk.bold("Incremental scan:"));
136
- console.log(chalk.gray(` Changed files: ${changedFiles.length}`));
137
- console.log(chalk.gray(` Affected components: ${affected.size > 0 ? [...affected].join(", ") : "none (new files)"}`));
138
- if (unmapped.length > 0) {
139
- console.log(chalk.gray(` Unmapped files: ${unmapped.length} (potential new components)`));
140
- }
141
- console.log();
142
175
  }
143
176
  // 4. Run static context collection → LLM pipeline
144
177
  const progress = progressBar(7);
@@ -235,13 +268,21 @@ export async function handleAnalyze(options) {
235
268
  if (result.tokenUsage) {
236
269
  meta.tokenUsage = { input: result.tokenUsage.input, output: result.tokenUsage.output };
237
270
  }
271
+ if (incrementalContext) {
272
+ meta.incrementalMode = true;
273
+ meta.changedFiles = incrementalContext.changedFiles.length;
274
+ meta.affectedComponents = incrementalContext.affectedComponents;
275
+ }
276
+ if (result.skippedAgents && result.skippedAgents.length > 0) {
277
+ meta.skippedAgents = result.skippedAgents;
278
+ }
238
279
  writeAnalysis(rootDir, analysis);
239
280
  writeAnalysisStatus(rootDir, "success");
240
281
  // Dual-write: archbyte.yaml + metadata.json
241
282
  const existingSpec = loadSpec(rootDir);
242
283
  const spec = staticResultToSpec(result, rootDir, existingSpec?.rules);
243
284
  writeSpec(rootDir, spec);
244
- writeScanMetadata(rootDir, duration, "pipeline", ctx.fileTree.totalFiles, result.tokenUsage);
285
+ writeScanMetadata(rootDir, duration, "pipeline", ctx.fileTree.totalFiles, result.tokenUsage, incrementalContext ? true : undefined, result.skippedAgents);
245
286
  progress.update(6, "Generating diagram...");
246
287
  await autoGenerate(rootDir, options);
247
288
  progress.done("Analysis complete");
@@ -283,7 +324,7 @@ function getGitCommit() {
283
324
  return undefined;
284
325
  }
285
326
  }
286
- function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage) {
327
+ function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage, incrementalMode, skippedAgents) {
287
328
  const meta = {
288
329
  analyzedAt: new Date().toISOString(),
289
330
  durationMs,
@@ -294,6 +335,10 @@ function writeScanMetadata(rootDir, durationMs, mode, filesScanned, tokenUsage)
294
335
  meta.filesScanned = filesScanned;
295
336
  if (tokenUsage)
296
337
  meta.tokenUsage = tokenUsage;
338
+ if (incrementalMode)
339
+ meta.incrementalMode = true;
340
+ if (skippedAgents && skippedAgents.length > 0)
341
+ meta.skippedAgents = skippedAgents;
297
342
  writeMetadata(rootDir, meta);
298
343
  }
299
344
  async function autoGenerate(rootDir, options) {
package/dist/cli/auth.js CHANGED
@@ -46,8 +46,8 @@ export async function handleLogin(provider) {
46
46
  console.log(chalk.green(`Logged in as ${chalk.bold(credentials.email)} (${credentials.tier} tier)`));
47
47
  console.log(chalk.gray(`Credentials saved to ${CREDENTIALS_PATH}`));
48
48
  console.log();
49
- console.log(chalk.gray("Your credentials and API keys are stored locally on your machine."));
50
- console.log(chalk.gray("ArchByte never transmits or stores your model provider keys."));
49
+ console.log(chalk.gray("Login credentials are stored in ~/.archbyte/ (global) never inside your project."));
50
+ console.log(chalk.gray("Your API keys and credentials never leave this machine."));
51
51
  }
52
52
  catch (err) {
53
53
  console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
@@ -21,3 +21,24 @@ export declare function shouldRunAgents(affected: Set<string>, unmapped: string[
21
21
  * Check if git is available and this is a git repo.
22
22
  */
23
23
  export declare function isGitAvailable(rootDir: string): boolean;
24
+ /**
25
+ * Categorize changed files by domain relevance for agent filtering.
26
+ */
27
+ export interface CategorizedChanges {
28
+ entryPoints: string[];
29
+ routeFiles: string[];
30
+ configFiles: string[];
31
+ infraFiles: string[];
32
+ eventFiles: string[];
33
+ sourceFiles: string[];
34
+ }
35
+ export declare function categorizeChanges(changedFiles: string[]): CategorizedChanges;
36
+ /**
37
+ * Find 1-layer neighbor components: components connected to affected components.
38
+ * Ensures cross-component connections aren't missed during incremental scans.
39
+ */
40
+ export declare function computeNeighbors(affectedComponents: Set<string>, spec: ArchbyteSpec): string[];
41
+ /**
42
+ * Count commits since a given commit hash. Used for staleness detection.
43
+ */
44
+ export declare function getCommitCount(rootDir: string, sinceCommit: string): number;
@@ -9,7 +9,7 @@ export function getChangedFiles(rootDir, sinceCommit) {
9
9
  try {
10
10
  const files = [];
11
11
  if (sinceCommit) {
12
- // Changed files since last scan commit
12
+ // Changed files since last scan commit (committed changes)
13
13
  const diff = execSync(`git diff --name-only ${sinceCommit}..HEAD`, {
14
14
  cwd: rootDir,
15
15
  encoding: "utf-8",
@@ -19,6 +19,30 @@ export function getChangedFiles(rootDir, sinceCommit) {
19
19
  files.push(...diff.split("\n").filter(Boolean));
20
20
  }
21
21
  }
22
+ // Include unstaged modifications to tracked files
23
+ const modified = execSync("git diff --name-only", {
24
+ cwd: rootDir,
25
+ encoding: "utf-8",
26
+ stdio: ["pipe", "pipe", "pipe"],
27
+ }).trim();
28
+ if (modified) {
29
+ for (const f of modified.split("\n").filter(Boolean)) {
30
+ if (!files.includes(f))
31
+ files.push(f);
32
+ }
33
+ }
34
+ // Include staged but uncommitted changes
35
+ const staged = execSync("git diff --name-only --cached", {
36
+ cwd: rootDir,
37
+ encoding: "utf-8",
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ }).trim();
40
+ if (staged) {
41
+ for (const f of staged.split("\n").filter(Boolean)) {
42
+ if (!files.includes(f))
43
+ files.push(f);
44
+ }
45
+ }
22
46
  // Also include untracked files
23
47
  const untracked = execSync("git ls-files --others --exclude-standard", {
24
48
  cwd: rootDir,
@@ -100,6 +124,72 @@ export function isGitAvailable(rootDir) {
100
124
  return false;
101
125
  }
102
126
  }
127
+ export function categorizeChanges(changedFiles) {
128
+ const categories = {
129
+ entryPoints: [],
130
+ routeFiles: [],
131
+ configFiles: [],
132
+ infraFiles: [],
133
+ eventFiles: [],
134
+ sourceFiles: [],
135
+ };
136
+ for (const file of changedFiles) {
137
+ const name = file.split("/").pop() ?? "";
138
+ const lower = file.toLowerCase();
139
+ if (isInfraFile(lower, name)) {
140
+ categories.infraFiles.push(file);
141
+ }
142
+ else if (isConfigFile(name)) {
143
+ categories.configFiles.push(file);
144
+ }
145
+ else if (isEntryPoint(name)) {
146
+ categories.entryPoints.push(file);
147
+ }
148
+ else if (isRouteFile(lower, name)) {
149
+ categories.routeFiles.push(file);
150
+ }
151
+ else if (isEventFile(lower, name)) {
152
+ categories.eventFiles.push(file);
153
+ }
154
+ else if (isSourceFile(name)) {
155
+ categories.sourceFiles.push(file);
156
+ }
157
+ // else: non-code file (docs, images, etc.) — ignored
158
+ }
159
+ return categories;
160
+ }
161
+ /**
162
+ * Find 1-layer neighbor components: components connected to affected components.
163
+ * Ensures cross-component connections aren't missed during incremental scans.
164
+ */
165
+ export function computeNeighbors(affectedComponents, spec) {
166
+ const neighbors = new Set();
167
+ for (const conn of spec.connections) {
168
+ if (affectedComponents.has(conn.from) && !affectedComponents.has(conn.to)) {
169
+ neighbors.add(conn.to);
170
+ }
171
+ if (affectedComponents.has(conn.to) && !affectedComponents.has(conn.from)) {
172
+ neighbors.add(conn.from);
173
+ }
174
+ }
175
+ return [...neighbors];
176
+ }
177
+ /**
178
+ * Count commits since a given commit hash. Used for staleness detection.
179
+ */
180
+ export function getCommitCount(rootDir, sinceCommit) {
181
+ try {
182
+ const output = execSync(`git rev-list --count ${sinceCommit}..HEAD`, {
183
+ cwd: rootDir,
184
+ encoding: "utf-8",
185
+ stdio: ["pipe", "pipe", "pipe"],
186
+ }).trim();
187
+ return parseInt(output, 10) || 0;
188
+ }
189
+ catch {
190
+ return 0;
191
+ }
192
+ }
103
193
  // Helpers
104
194
  function normalizePath(p) {
105
195
  // Remove trailing slashes, normalize
@@ -109,3 +199,29 @@ function isConfigOnly(filePath) {
109
199
  const name = filePath.split("/").pop() ?? "";
110
200
  return /^(\.gitignore|\.eslintrc|\.prettierrc|tsconfig|package\.json|package-lock|yarn\.lock|pnpm-lock|archbyte\.yaml|README|LICENSE|CHANGELOG|\.env)/i.test(name);
111
201
  }
202
+ function isInfraFile(lower, name) {
203
+ return /dockerfile|docker-compose|compose\.ya?ml|\.dockerignore/i.test(name) ||
204
+ lower.includes("k8s/") || lower.includes("kubernetes/") ||
205
+ lower.includes("terraform/") || lower.includes("helm/") ||
206
+ /\.(tf|hcl)$/.test(name);
207
+ }
208
+ function isConfigFile(name) {
209
+ return /^(package\.json|tsconfig|\.env|\.eslintrc|\.prettierrc|pyproject\.toml|Cargo\.toml|go\.mod|archbyte\.yaml)/i.test(name);
210
+ }
211
+ function isEntryPoint(name) {
212
+ return /^(index|main|app|server|entry)\.(ts|js|tsx|jsx|py|go|rs)$/i.test(name);
213
+ }
214
+ function isRouteFile(lower, name) {
215
+ return lower.includes("route") || lower.includes("router") ||
216
+ lower.includes("controller") || lower.includes("handler") ||
217
+ /\.(routes|controller|handler)\.(ts|js)$/i.test(name);
218
+ }
219
+ function isEventFile(lower, name) {
220
+ return lower.includes("event") || lower.includes("listener") ||
221
+ lower.includes("subscriber") || lower.includes("consumer") ||
222
+ lower.includes("producer") || lower.includes("queue") ||
223
+ /\.(events?|listener|subscriber|consumer|producer)\.(ts|js)$/i.test(name);
224
+ }
225
+ function isSourceFile(name) {
226
+ return /\.(ts|tsx|js|jsx|py|go|rs|java|kt|rb|cs|php|swift|dart)$/i.test(name);
227
+ }
package/dist/cli/serve.js CHANGED
@@ -2,12 +2,51 @@ import * as path from "path";
2
2
  import * as fs from "fs";
3
3
  import chalk from "chalk";
4
4
  import { DEFAULT_PORT } from "./constants.js";
5
+ import { select } from "./ui.js";
6
+ function askPort(prompt) {
7
+ return new Promise((resolve) => {
8
+ process.stdout.write(prompt);
9
+ const stdin = process.stdin;
10
+ const wasRaw = stdin.isRaw;
11
+ stdin.setRawMode(true);
12
+ stdin.resume();
13
+ stdin.setEncoding("utf8");
14
+ let input = "";
15
+ const onData = (data) => {
16
+ for (const ch of data) {
17
+ if (ch === "\n" || ch === "\r" || ch === "\u0004") {
18
+ stdin.setRawMode(wasRaw ?? false);
19
+ stdin.pause();
20
+ stdin.removeListener("data", onData);
21
+ process.stdout.write("\n");
22
+ resolve(input);
23
+ return;
24
+ }
25
+ else if (ch === "\u0003") {
26
+ process.stdout.write("\n");
27
+ process.exit(0);
28
+ }
29
+ else if (ch === "\u007F" || ch === "\b") {
30
+ if (input.length > 0) {
31
+ input = input.slice(0, -1);
32
+ process.stdout.write("\b \b");
33
+ }
34
+ }
35
+ else if (ch >= "0" && ch <= "9") {
36
+ input += ch;
37
+ process.stdout.write(ch);
38
+ }
39
+ }
40
+ };
41
+ stdin.on("data", onData);
42
+ });
43
+ }
5
44
  /**
6
45
  * Start the ArchByte UI server
7
46
  */
8
47
  export async function handleServe(options) {
9
48
  const rootDir = process.cwd();
10
- const port = options.port || DEFAULT_PORT;
49
+ let port = options.port || DEFAULT_PORT;
11
50
  const diagramPath = options.diagram
12
51
  ? path.resolve(rootDir, options.diagram)
13
52
  : path.join(rootDir, ".archbyte", "architecture.json");
@@ -25,7 +64,6 @@ export async function handleServe(options) {
25
64
  }
26
65
  console.log(chalk.cyan(`🏗️ ArchByte - See what agents build. As they build it.`));
27
66
  console.log(chalk.gray(`Diagram: ${path.relative(rootDir, diagramPath)}`));
28
- console.log(chalk.gray(`Port: ${port}`));
29
67
  console.log();
30
68
  // Check if diagram exists
31
69
  if (!fs.existsSync(diagramPath)) {
@@ -34,26 +72,66 @@ export async function handleServe(options) {
34
72
  console.log();
35
73
  }
36
74
  const { startServer } = await import("../server/src/index.js");
37
- const maxRetries = 10;
38
- for (let attempt = 0; attempt < maxRetries; attempt++) {
39
- const tryPort = port + attempt;
75
+ // Try to start, handle port conflicts interactively
76
+ while (true) {
40
77
  try {
41
- if (attempt > 0) {
42
- console.log(chalk.gray(`Port ${tryPort - 1} in use, trying ${tryPort}...`));
43
- }
44
- console.log(chalk.green(`UI: http://localhost:${tryPort}`));
78
+ console.log(chalk.green(`UI: http://localhost:${port}`));
45
79
  console.log();
46
80
  await startServer({
47
81
  name: projectName,
48
82
  diagramPath,
49
83
  workspaceRoot: rootDir,
50
- port: tryPort,
84
+ port,
51
85
  });
52
86
  return;
53
87
  }
54
88
  catch (error) {
55
89
  if (error instanceof Error && error.code === "EADDRINUSE") {
56
- continue;
90
+ console.log(chalk.yellow(` Port ${port} is already in use.`));
91
+ console.log();
92
+ if (!process.stdin.isTTY) {
93
+ // Non-interactive — just try next port
94
+ port++;
95
+ continue;
96
+ }
97
+ const nextPort = port + 1;
98
+ const choice = await select(" What would you like to do?", [
99
+ `Use next available port (${nextPort})`,
100
+ "Enter a custom port",
101
+ "Kill the process on this port and retry",
102
+ "Exit",
103
+ ]);
104
+ if (choice === 0) {
105
+ port = nextPort;
106
+ continue;
107
+ }
108
+ else if (choice === 1) {
109
+ const input = await askPort(chalk.bold(" Port: "));
110
+ const parsed = parseInt(input, 10);
111
+ if (!parsed || parsed < 1 || parsed > 65535) {
112
+ console.log(chalk.red(" Invalid port number."));
113
+ continue;
114
+ }
115
+ port = parsed;
116
+ continue;
117
+ }
118
+ else if (choice === 2) {
119
+ try {
120
+ const { execSync } = await import("child_process");
121
+ execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: "ignore" });
122
+ console.log(chalk.green(` Killed process on port ${port}.`));
123
+ // Small delay for OS to release the port
124
+ await new Promise((r) => setTimeout(r, 500));
125
+ continue;
126
+ }
127
+ catch {
128
+ console.log(chalk.red(` Could not kill process on port ${port}. Try manually: lsof -ti:${port} | xargs kill -9`));
129
+ continue;
130
+ }
131
+ }
132
+ else {
133
+ process.exit(0);
134
+ }
57
135
  }
58
136
  if (error instanceof Error) {
59
137
  console.error(chalk.red(`Error: ${error.message}`));
@@ -61,6 +139,4 @@ export async function handleServe(options) {
61
139
  process.exit(1);
62
140
  }
63
141
  }
64
- console.error(chalk.red(`All ports ${port}-${port + maxRetries - 1} are in use.`));
65
- process.exit(1);
66
142
  }
package/dist/cli/setup.js CHANGED
@@ -16,7 +16,7 @@ const PROVIDERS = [
16
16
  ];
17
17
  const PROVIDER_MODELS = {
18
18
  anthropic: [
19
- { id: "", label: "Default (recommended)", hint: "Haiku for fast agents, Sonnet for connections" },
19
+ { id: "", label: "Default (recommended)", hint: "Opus for all agents" },
20
20
  { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
21
21
  { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", hint: "Balanced, great quality" },
22
22
  { id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
@@ -292,8 +292,16 @@ export async function handleSetup() {
292
292
  console.log(chalk.green(` ✓ Model: ${chosen.label}`));
293
293
  }
294
294
  else {
295
- delete profiles[provider].model;
296
- console.log(chalk.green(` ✓ Model: Default (auto per agent tier)`));
295
+ // Save the actual default model so config is explicit
296
+ const defaultModels = {
297
+ anthropic: "claude-opus-4-6",
298
+ openai: "gpt-5.2",
299
+ google: "gemini-2.5-pro",
300
+ ollama: "qwen2.5-coder",
301
+ };
302
+ const defaultModel = defaultModels[provider] ?? resolveModel(provider, "standard");
303
+ profiles[provider].model = defaultModel;
304
+ console.log(chalk.green(` ✓ Model: ${defaultModel} (default)`));
297
305
  }
298
306
  }
299
307
  config.profiles = profiles;
@@ -329,13 +337,11 @@ export async function handleSetup() {
329
337
  delete config.ollamaBaseUrl;
330
338
  // Save config
331
339
  saveConfig(config);
332
- console.log(chalk.gray(`\n Config saved to ${CONFIG_PATH}`));
333
- console.log(chalk.gray(" Your API key is stored locally on this machine and never sent to ArchByte."));
334
- console.log(chalk.gray(" All model calls go directly from your machine to your provider."));
335
340
  // Generate archbyte.yaml in .archbyte/ if it doesn't exist
336
341
  const projectDir = process.cwd();
337
342
  const archbyteDir = path.join(projectDir, ".archbyte");
338
343
  const yamlPath = path.join(archbyteDir, "archbyte.yaml");
344
+ let yamlCreated = false;
339
345
  if (!fs.existsSync(yamlPath)) {
340
346
  if (!fs.existsSync(archbyteDir)) {
341
347
  fs.mkdirSync(archbyteDir, { recursive: true });
@@ -355,21 +361,39 @@ export async function handleSetup() {
355
361
  let template = fs.readFileSync(templatePath, "utf-8");
356
362
  template = template.replace("name: my-project", `name: ${projectName}`);
357
363
  fs.writeFileSync(yamlPath, template, "utf-8");
358
- console.log(chalk.green(` Created .archbyte/archbyte.yaml`));
359
- }
360
- else {
361
- console.log(chalk.gray(` .archbyte/archbyte.yaml already exists`));
364
+ yamlCreated = true;
362
365
  }
363
366
  // Generate README.md in .archbyte/
364
367
  writeArchbyteReadme(archbyteDir);
368
+ // ─── Summary ───
369
+ const dim = chalk.gray;
370
+ const sep = dim(" ───");
371
+ console.log();
372
+ console.log(chalk.bold.green(" ✓ Setup complete"));
373
+ console.log();
374
+ console.log(sep);
375
+ console.log();
376
+ console.log(dim(" ~/.archbyte/") + " API keys, login, config " + dim("(global)"));
377
+ console.log(dim(" .archbyte/") + " Analysis, diagrams " + dim("(per project)"));
378
+ console.log();
379
+ console.log(dim(" Your keys never leave this machine."));
380
+ console.log(dim(" All model calls go directly to your provider."));
365
381
  console.log();
366
- console.log(chalk.green(" Setup complete!"));
382
+ console.log(sep);
367
383
  console.log();
368
- console.log(chalk.bold(" Next: ") + chalk.cyan("archbyte run") + chalk.bold(" to analyze your codebase."));
369
- console.log(chalk.gray(" Run from your project root, or use ") + chalk.cyan("archbyte run -d /path/to/project"));
384
+ if (yamlCreated) {
385
+ console.log(chalk.green(" + ") + "Created " + chalk.cyan(".archbyte/archbyte.yaml"));
386
+ }
387
+ else {
388
+ console.log(dim(" .archbyte/archbyte.yaml already exists"));
389
+ }
390
+ console.log();
391
+ console.log(sep);
392
+ console.log();
393
+ console.log(" " + chalk.bold("Next"));
394
+ console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
370
395
  if (hasClaude || hasCodex) {
371
- console.log();
372
- console.log(chalk.bold(" MCP: ") + chalk.cyan("archbyte mcp install") + chalk.bold(" to use ArchByte from your AI tool."));
396
+ console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
373
397
  }
374
398
  console.log();
375
399
  }
@@ -71,6 +71,8 @@ export interface ScanMetadata {
71
71
  };
72
72
  validation?: unknown;
73
73
  gaps?: unknown;
74
+ incrementalMode?: boolean;
75
+ skippedAgents?: string[];
74
76
  }
75
77
  export declare function loadSpec(rootDir: string): ArchbyteSpec | null;
76
78
  export declare function writeSpec(rootDir: string, spec: ArchbyteSpec): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {