archbyte 0.4.2 → 0.5.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.
Files changed (38) hide show
  1. package/README.md +9 -25
  2. package/bin/archbyte.js +6 -41
  3. package/dist/agents/static/component-detector.js +71 -107
  4. package/dist/agents/static/connection-mapper.js +24 -25
  5. package/dist/agents/static/deep-drill.d.ts +72 -0
  6. package/dist/agents/static/deep-drill.js +388 -0
  7. package/dist/agents/static/doc-parser.js +73 -48
  8. package/dist/agents/static/env-detector.js +3 -6
  9. package/dist/agents/static/event-detector.js +20 -26
  10. package/dist/agents/static/infra-analyzer.js +15 -1
  11. package/dist/agents/static/structure-scanner.js +56 -57
  12. package/dist/agents/static/taxonomy.d.ts +19 -0
  13. package/dist/agents/static/taxonomy.js +147 -0
  14. package/dist/agents/tools/local-fs.js +5 -2
  15. package/dist/cli/analyze.js +49 -27
  16. package/dist/cli/license-gate.js +47 -19
  17. package/dist/cli/run.js +117 -1
  18. package/dist/cli/setup.d.ts +6 -1
  19. package/dist/cli/setup.js +35 -16
  20. package/dist/cli/shared.d.ts +0 -11
  21. package/dist/cli/shared.js +0 -61
  22. package/dist/cli/workflow.js +8 -15
  23. package/dist/server/src/index.js +276 -168
  24. package/package.json +2 -2
  25. package/templates/archbyte.yaml +28 -7
  26. package/ui/dist/assets/index-BQouokNH.css +1 -0
  27. package/ui/dist/assets/index-QllGSFhe.js +72 -0
  28. package/ui/dist/index.html +2 -2
  29. package/dist/cli/arch-diff.d.ts +0 -38
  30. package/dist/cli/arch-diff.js +0 -61
  31. package/dist/cli/diff.d.ts +0 -10
  32. package/dist/cli/diff.js +0 -144
  33. package/dist/cli/patrol.d.ts +0 -18
  34. package/dist/cli/patrol.js +0 -596
  35. package/dist/cli/validate.d.ts +0 -53
  36. package/dist/cli/validate.js +0 -299
  37. package/ui/dist/assets/index-DDCNauh7.css +0 -1
  38. package/ui/dist/assets/index-DO4t5Xu1.js +0 -72
@@ -15,13 +15,35 @@ let config;
15
15
  let sseClients = new Set();
16
16
  let diagramWatcher = null;
17
17
  let isAnalyzing = false;
18
+ // Source file watcher — detects changes to notify UI for manual re-analysis
19
+ let sourceWatcher = null;
20
+ const pendingSourceChanges = new Map();
21
+ let changeDebounceTimer = null;
22
+ const CHANGE_DEBOUNCE_MS = 2500;
23
+ function pendingChangesPath() {
24
+ return path.join(config.workspaceRoot, ".archbyte", "pending-changes.json");
25
+ }
26
+ function savePendingChanges() {
27
+ const entries = Array.from(pendingSourceChanges.values());
28
+ writeFile(pendingChangesPath(), JSON.stringify(entries), "utf-8").catch(() => { });
29
+ }
30
+ function loadPendingChanges() {
31
+ try {
32
+ const raw = readFileSync(pendingChangesPath(), "utf-8");
33
+ const entries = JSON.parse(raw);
34
+ for (const entry of entries) {
35
+ pendingSourceChanges.set(entry.path, entry);
36
+ }
37
+ }
38
+ catch {
39
+ // No persisted changes — fresh start
40
+ }
41
+ }
18
42
  const sessionChanges = [];
19
43
  const MAX_SESSION_CHANGES = 50;
20
44
  let currentArchitecture = null;
21
45
  // Process tracking for run-from-UI
22
46
  const runningWorkflows = new Map();
23
- let patrolProcess = null;
24
- let patrolRunning = false;
25
47
  let chatProcess = null;
26
48
  // Resolve archbyte CLI binary path
27
49
  function getArchbyteBin() {
@@ -30,7 +52,7 @@ function getArchbyteBin() {
30
52
  }
31
53
  catch {
32
54
  // Fallback: resolve relative to this package
33
- return path.resolve(__dirname, "../../../cli/dist/index.js");
55
+ return path.resolve(__dirname, "../../../bin/archbyte.js");
34
56
  }
35
57
  }
36
58
  // Broadcast ops event to SSE clients
@@ -257,81 +279,76 @@ function createHttpServer() {
257
279
  }));
258
280
  return;
259
281
  }
260
- // API: Impact analysisblast radius BFS from a node
261
- if (url.startsWith("/api/impact/") && req.method === "GET") {
282
+ // API: Deep Drillfocused static analysis of a single component
283
+ if (url.startsWith("/api/component/") && url.includes("/deep") && req.method === "GET") {
262
284
  const arch = currentArchitecture || (await loadArchitecture());
263
285
  if (!arch) {
264
286
  res.writeHead(404, { "Content-Type": "application/json" });
265
287
  res.end(JSON.stringify({ error: "No architecture loaded" }));
266
288
  return;
267
289
  }
268
- const urlParts = url.split("?");
269
- const nodeId = decodeURIComponent(urlParts[0].replace("/api/impact/", ""));
270
- const params = new URL(url, `http://localhost:${config.port}`).searchParams;
271
- const depth = Math.min(Math.max(parseInt(params.get("depth") || "2", 10) || 2, 1), 5);
272
- const direction = params.get("direction") || "both";
273
- // Build adjacency lists
274
- const forward = new Map();
275
- const reverse = new Map();
276
- for (const nd of arch.nodes) {
277
- forward.set(nd.id, []);
278
- reverse.set(nd.id, []);
290
+ // Extract component ID: /api/component/{id}/deep
291
+ const urlPath = url.split("?")[0];
292
+ const match = urlPath.match(/^\/api\/component\/(.+)\/deep$/);
293
+ if (!match) {
294
+ res.writeHead(400, { "Content-Type": "application/json" });
295
+ res.end(JSON.stringify({ error: "Invalid URL format" }));
296
+ return;
279
297
  }
280
- for (const edge of arch.edges) {
281
- forward.get(edge.source)?.push(edge.target);
282
- reverse.get(edge.target)?.push(edge.source);
298
+ const componentId = decodeURIComponent(match[1]);
299
+ const node = arch.nodes.find((n) => n.id === componentId);
300
+ if (!node) {
301
+ res.writeHead(404, { "Content-Type": "application/json" });
302
+ res.end(JSON.stringify({ error: `Component not found: ${componentId}` }));
303
+ return;
283
304
  }
284
- const nodeMap = new Map();
285
- for (const nd of arch.nodes)
286
- nodeMap.set(nd.id, nd);
287
- // BFS helper
288
- function bfs(startId, adj, maxDepth) {
289
- const visited = new Map();
290
- visited.set(startId, 0);
291
- const queue = [{ id: startId, d: 0 }];
292
- const result = [];
293
- while (queue.length > 0) {
294
- const { id, d } = queue.shift();
295
- if (d >= maxDepth)
296
- continue;
297
- for (const neighbor of adj.get(id) || []) {
298
- if (!visited.has(neighbor)) {
299
- const nd = d + 1;
300
- visited.set(neighbor, nd);
301
- result.push({ id: neighbor, label: nodeMap.get(neighbor)?.label.split("\n")[0] || neighbor, depth: nd });
302
- queue.push({ id: neighbor, d: nd });
303
- }
305
+ const nodePath = node.path;
306
+ if (!nodePath) {
307
+ res.writeHead(400, { "Content-Type": "application/json" });
308
+ res.end(JSON.stringify({ error: `Component "${node.label}" has no path` }));
309
+ return;
310
+ }
311
+ try {
312
+ const { runDeepDrill } = await import("../../agents/static/deep-drill.js");
313
+ const result = await runDeepDrill(config.workspaceRoot, componentId, node.label.split("\n")[0], nodePath);
314
+ // Enrich with connection data from architecture edges
315
+ const outgoing = [];
316
+ const incoming = [];
317
+ const nodeMap = new Map();
318
+ for (const n of arch.nodes)
319
+ nodeMap.set(n.id, n);
320
+ for (const edge of arch.edges) {
321
+ if (edge.source === componentId) {
322
+ const target = nodeMap.get(edge.target);
323
+ outgoing.push({
324
+ targetId: edge.target,
325
+ targetName: target?.label.split("\n")[0] || edge.target,
326
+ type: edge.label || "depends-on",
327
+ description: edge.label || "",
328
+ });
329
+ }
330
+ if (edge.target === componentId) {
331
+ const source = nodeMap.get(edge.source);
332
+ incoming.push({
333
+ sourceId: edge.source,
334
+ sourceName: source?.label.split("\n")[0] || edge.source,
335
+ type: edge.label || "depends-on",
336
+ description: edge.label || "",
337
+ });
304
338
  }
305
339
  }
306
- return result;
307
- }
308
- const upstream = (direction === "downstream") ? [] : bfs(nodeId, reverse, depth);
309
- const downstream = (direction === "upstream") ? [] : bfs(nodeId, forward, depth);
310
- // Collect all affected node IDs (including origin)
311
- const affectedSet = new Set([nodeId]);
312
- for (const n of upstream)
313
- affectedSet.add(n.id);
314
- for (const n of downstream)
315
- affectedSet.add(n.id);
316
- // Affected edges: edges between any two nodes in the affected set
317
- const affectedEdges = [];
318
- for (const edge of arch.edges) {
319
- if (affectedSet.has(edge.source) && affectedSet.has(edge.target)) {
320
- affectedEdges.push(edge.id);
321
- }
340
+ result.connections = { outgoing, incoming };
341
+ res.writeHead(200, { "Content-Type": "application/json" });
342
+ res.end(JSON.stringify(result));
343
+ }
344
+ catch (error) {
345
+ res.writeHead(500, { "Content-Type": "application/json" });
346
+ res.end(JSON.stringify({ error: String(error) }));
322
347
  }
323
- res.writeHead(200, { "Content-Type": "application/json" });
324
- res.end(JSON.stringify({
325
- nodeId,
326
- upstream,
327
- downstream,
328
- affectedEdges,
329
- totalAffected: affectedSet.size - 1,
330
- }));
331
348
  return;
332
349
  }
333
350
  // API: Audit — unified validation + health endpoint
334
- if ((url === "/api/audit" || url === "/api/validate") && req.method === "GET") {
351
+ if (url === "/api/audit" && req.method === "GET") {
335
352
  const arch = currentArchitecture || (await loadArchitecture());
336
353
  if (!arch) {
337
354
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -937,51 +954,13 @@ function createHttpServer() {
937
954
  res.end(content);
938
955
  return;
939
956
  }
940
- // API: Patrol history
941
- if (url === "/api/patrol/latest" && req.method === "GET") {
942
- const latestPath = path.join(config.workspaceRoot, ".archbyte/patrols/latest.json");
943
- if (!existsSync(latestPath)) {
944
- res.writeHead(200, { "Content-Type": "application/json" });
945
- res.end(JSON.stringify(null));
946
- return;
947
- }
948
- try {
949
- const content = readFileSync(latestPath, "utf-8");
950
- res.writeHead(200, { "Content-Type": "application/json" });
951
- res.end(content);
952
- }
953
- catch {
954
- res.writeHead(200, { "Content-Type": "application/json" });
955
- res.end(JSON.stringify(null));
956
- }
957
- return;
958
- }
959
- if (url === "/api/patrol/history" && req.method === "GET") {
960
- const historyPath = path.join(config.workspaceRoot, ".archbyte/patrols/history.jsonl");
961
- if (!existsSync(historyPath)) {
962
- res.writeHead(200, { "Content-Type": "application/json" });
963
- res.end(JSON.stringify([]));
964
- return;
965
- }
966
- try {
967
- const content = readFileSync(historyPath, "utf-8");
968
- const records = content.trim().split("\n").filter(Boolean).slice(-50).map((line) => JSON.parse(line));
969
- res.writeHead(200, { "Content-Type": "application/json" });
970
- res.end(JSON.stringify(records));
971
- }
972
- catch {
973
- res.writeHead(200, { "Content-Type": "application/json" });
974
- res.end(JSON.stringify([]));
975
- }
976
- return;
977
- }
978
957
  // API: Workflow list and status
979
958
  if (url === "/api/workflow/list" && req.method === "GET") {
980
959
  const workflows = [];
981
960
  // Built-in workflows
982
961
  const builtins = [
983
- { id: "full-analysis", name: "Full Analysis Pipeline", description: "Complete architecture pipeline", steps: ["generate", "validate", "stats", "export"] },
984
- { id: "ci-check", name: "CI Architecture Check", description: "Lightweight CI pipeline", steps: ["validate"] },
962
+ { id: "full-analysis", name: "Full Analysis Pipeline", description: "Complete architecture pipeline", steps: ["generate", "stats", "export"] },
963
+ { id: "ci-check", name: "CI Architecture Check", description: "Lightweight CI pipeline", steps: ["stats"] },
985
964
  { id: "drift-check", name: "Architecture Drift Check", description: "Check for architecture drift", steps: ["snapshot", "generate", "diff"] },
986
965
  ];
987
966
  for (const b of builtins) {
@@ -1168,6 +1147,18 @@ function createHttpServer() {
1168
1147
  res.end(JSON.stringify({ running: runningWorkflows.has("__generate__") }));
1169
1148
  return;
1170
1149
  }
1150
+ // API: Reload — re-read architecture, reconcile pending changes
1151
+ if (url === "/api/reload" && req.method === "POST") {
1152
+ currentArchitecture = await loadArchitecture();
1153
+ reconcilePendingWithGit();
1154
+ broadcastUpdate();
1155
+ if (pendingSourceChanges.size > 0) {
1156
+ broadcastPendingChanges();
1157
+ }
1158
+ res.writeHead(200, { "Content-Type": "application/json" });
1159
+ res.end(JSON.stringify({ ok: true }));
1160
+ return;
1161
+ }
1171
1162
  // API: Run analyze (static or LLM) + generate
1172
1163
  if (url === "/api/analyze" && req.method === "POST") {
1173
1164
  if (isAnalyzing) {
@@ -1197,6 +1188,21 @@ function createHttpServer() {
1197
1188
  res.end(JSON.stringify(sessionChanges));
1198
1189
  return;
1199
1190
  }
1191
+ // API: Pending source file changes — hydrates UI on reconnect
1192
+ if (url === "/api/changes/pending" && req.method === "GET") {
1193
+ const changes = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
1194
+ const count = changes.length;
1195
+ const { componentChanges, unmapped } = mapFilesToComponents(changes, currentArchitecture);
1196
+ res.writeHead(200, { "Content-Type": "application/json" });
1197
+ res.end(JSON.stringify({
1198
+ count,
1199
+ files: changes.slice(0, 20).map((c) => ({ event: c.event, path: c.path })),
1200
+ componentChanges,
1201
+ unmapped,
1202
+ truncated: count > 20,
1203
+ }));
1204
+ return;
1205
+ }
1200
1206
  // API: Run workflow
1201
1207
  if (url.startsWith("/api/workflow/run/") && req.method === "POST") {
1202
1208
  const id = url.split("/").pop();
@@ -1269,56 +1275,6 @@ function createHttpServer() {
1269
1275
  });
1270
1276
  return;
1271
1277
  }
1272
- // API: Start patrol
1273
- if (url === "/api/patrol/start" && req.method === "POST") {
1274
- if (patrolRunning) {
1275
- res.writeHead(409, { "Content-Type": "application/json" });
1276
- res.end(JSON.stringify({ error: "Patrol already running" }));
1277
- return;
1278
- }
1279
- let body = "";
1280
- req.on("data", (chunk) => { body += chunk.toString(); });
1281
- req.on("end", () => {
1282
- const { interval } = JSON.parse(body || "{}");
1283
- const bin = getArchbyteBin();
1284
- const args = [bin, "patrol"];
1285
- if (interval)
1286
- args.push("--interval", String(interval));
1287
- patrolProcess = spawn(process.execPath, args, {
1288
- cwd: config.workspaceRoot,
1289
- stdio: ["ignore", "inherit", "inherit"],
1290
- env: { ...process.env, FORCE_COLOR: "1" },
1291
- });
1292
- patrolRunning = true;
1293
- broadcastOpsEvent({ type: "patrol:started" });
1294
- patrolProcess.on("close", () => {
1295
- patrolProcess = null;
1296
- patrolRunning = false;
1297
- broadcastOpsEvent({ type: "patrol:stopped" });
1298
- });
1299
- res.writeHead(200, { "Content-Type": "application/json" });
1300
- res.end(JSON.stringify({ ok: true }));
1301
- });
1302
- return;
1303
- }
1304
- // API: Stop patrol
1305
- if (url === "/api/patrol/stop" && req.method === "POST") {
1306
- if (patrolProcess) {
1307
- patrolProcess.kill("SIGTERM");
1308
- patrolProcess = null;
1309
- patrolRunning = false;
1310
- broadcastOpsEvent({ type: "patrol:stopped" });
1311
- }
1312
- res.writeHead(200, { "Content-Type": "application/json" });
1313
- res.end(JSON.stringify({ ok: true }));
1314
- return;
1315
- }
1316
- // API: Patrol running status
1317
- if (url === "/api/patrol/running" && req.method === "GET") {
1318
- res.writeHead(200, { "Content-Type": "application/json" });
1319
- res.end(JSON.stringify({ running: patrolRunning }));
1320
- return;
1321
- }
1322
1278
  // API: Config — read project config (including provider settings)
1323
1279
  if (url === "/api/config" && req.method === "GET") {
1324
1280
  const configPath = path.join(config.workspaceRoot, ".archbyte/config.json");
@@ -1732,12 +1688,23 @@ function diffArchitectures(prev, curr) {
1732
1688
  .map((e) => ({ source: e.source, target: e.target, label: e.label }));
1733
1689
  return { addedNodes, removedNodes, modifiedNodes, addedEdges, removedEdges };
1734
1690
  }
1735
- // Run analyze → generate pipeline (used by /api/analyze and patrol)
1691
+ // Run analyze → generate pipeline (used by /api/analyze)
1736
1692
  function runAnalyzePipeline(mode = "static", fileChanges) {
1737
1693
  if (isAnalyzing)
1738
1694
  return;
1739
1695
  isAnalyzing = true;
1740
1696
  const pipelineStart = Date.now();
1697
+ // Capture and clear pending source changes
1698
+ if (!fileChanges) {
1699
+ fileChanges = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
1700
+ }
1701
+ pendingSourceChanges.clear();
1702
+ savePendingChanges();
1703
+ if (changeDebounceTimer) {
1704
+ clearTimeout(changeDebounceTimer);
1705
+ changeDebounceTimer = null;
1706
+ }
1707
+ broadcastOpsEvent({ type: "changes:cleared" });
1741
1708
  // Snapshot current architecture for diffing after pipeline completes
1742
1709
  const prevArchitecture = currentArchitecture
1743
1710
  ? { ...currentArchitecture, nodes: [...currentArchitecture.nodes], edges: [...currentArchitecture.edges] }
@@ -1752,10 +1719,14 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
1752
1719
  stdio: ["ignore", "pipe", "pipe"],
1753
1720
  env: { ...process.env, FORCE_COLOR: "0" },
1754
1721
  });
1722
+ let analyzeStderr = "";
1723
+ analyzeChild.stdout?.on("data", (d) => process.stderr.write(`[analyze] ${d}`));
1724
+ analyzeChild.stderr?.on("data", (d) => { analyzeStderr += d; process.stderr.write(`[analyze] ${d}`); });
1755
1725
  analyzeChild.on("close", (analyzeCode) => {
1756
1726
  if (analyzeCode !== 0) {
1757
1727
  isAnalyzing = false;
1758
- broadcastOpsEvent({ type: "analyzing:finished", code: analyzeCode, success: false });
1728
+ const errMsg = analyzeStderr.trim().split("\n").pop() || `Analyze failed (exit ${analyzeCode})`;
1729
+ broadcastOpsEvent({ type: "analyzing:finished", code: analyzeCode, success: false, error: errMsg });
1759
1730
  return;
1760
1731
  }
1761
1732
  // Chain: generate after successful analyze
@@ -1764,6 +1735,8 @@ function runAnalyzePipeline(mode = "static", fileChanges) {
1764
1735
  stdio: ["ignore", "pipe", "pipe"],
1765
1736
  env: { ...process.env, FORCE_COLOR: "0" },
1766
1737
  });
1738
+ genChild.stdout?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
1739
+ genChild.stderr?.on("data", (d) => process.stderr.write(`[generate] ${d}`));
1767
1740
  genChild.on("close", async (genCode) => {
1768
1741
  isAnalyzing = false;
1769
1742
  const durationMs = Date.now() - pipelineStart;
@@ -1828,6 +1801,144 @@ function setupWatcher() {
1828
1801
  broadcastUpdate();
1829
1802
  });
1830
1803
  }
1804
+ // Map changed file paths to architecture components using longest-prefix matching
1805
+ function mapFilesToComponents(changes, arch) {
1806
+ if (!arch || arch.nodes.length === 0) {
1807
+ return { componentChanges: [], unmapped: changes.map((c) => ({ path: c.path, event: c.event })) };
1808
+ }
1809
+ // Build sorted list of component paths (longest first for greedy matching)
1810
+ const componentPaths = arch.nodes
1811
+ .filter((n) => n.path)
1812
+ .map((n) => ({ id: n.id, name: n.label.split("\n")[0], path: n.path }))
1813
+ .sort((a, b) => b.path.length - a.path.length);
1814
+ const groups = new Map();
1815
+ const unmapped = [];
1816
+ for (const change of changes) {
1817
+ let matched = false;
1818
+ for (const comp of componentPaths) {
1819
+ const prefix = comp.path.endsWith("/") ? comp.path : `${comp.path}/`;
1820
+ if (change.path.startsWith(prefix) || change.path === comp.path) {
1821
+ if (!groups.has(comp.id)) {
1822
+ groups.set(comp.id, { componentId: comp.id, componentName: comp.name, files: [] });
1823
+ }
1824
+ groups.get(comp.id).files.push({ path: change.path, event: change.event });
1825
+ matched = true;
1826
+ break;
1827
+ }
1828
+ }
1829
+ if (!matched) {
1830
+ unmapped.push({ path: change.path, event: change.event });
1831
+ }
1832
+ }
1833
+ return {
1834
+ componentChanges: Array.from(groups.values()),
1835
+ unmapped,
1836
+ };
1837
+ }
1838
+ // Broadcast pending source changes to SSE clients
1839
+ function broadcastPendingChanges() {
1840
+ const changes = Array.from(pendingSourceChanges.values()).map((c) => ({ event: c.event, path: c.path }));
1841
+ const count = changes.length;
1842
+ const truncated = count > 20;
1843
+ const { componentChanges, unmapped } = mapFilesToComponents(changes, currentArchitecture);
1844
+ broadcastOpsEvent({
1845
+ type: "changes:detected",
1846
+ count,
1847
+ files: changes.slice(0, 20).map((c) => c.path),
1848
+ componentChanges,
1849
+ unmapped,
1850
+ truncated,
1851
+ });
1852
+ }
1853
+ // Setup source file watcher — watches workspace for code changes
1854
+ function setupSourceWatcher() {
1855
+ sourceWatcher = watch(config.workspaceRoot, {
1856
+ ignoreInitial: true,
1857
+ ignored: [
1858
+ /(^|[/\\])\../, // dotfiles/dirs
1859
+ "**/node_modules/**",
1860
+ "**/dist/**",
1861
+ "**/build/**",
1862
+ "**/out/**",
1863
+ "**/.archbyte/**",
1864
+ "**/coverage/**",
1865
+ "**/*.lock",
1866
+ "**/package-lock.json",
1867
+ "**/__pycache__/**",
1868
+ "**/target/**",
1869
+ "**/vendor/**",
1870
+ ],
1871
+ depth: 10,
1872
+ });
1873
+ sourceWatcher.on("all", (event, filePath) => {
1874
+ if (event !== "change" && event !== "add" && event !== "unlink")
1875
+ return;
1876
+ const relativePath = path.relative(config.workspaceRoot, filePath);
1877
+ pendingSourceChanges.set(relativePath, {
1878
+ event,
1879
+ path: relativePath,
1880
+ timestamp: Date.now(),
1881
+ });
1882
+ // Debounce: reset timer on each change, broadcast after settling
1883
+ if (changeDebounceTimer)
1884
+ clearTimeout(changeDebounceTimer);
1885
+ changeDebounceTimer = setTimeout(() => {
1886
+ changeDebounceTimer = null;
1887
+ broadcastPendingChanges();
1888
+ savePendingChanges();
1889
+ }, CHANGE_DEBOUNCE_MS);
1890
+ });
1891
+ }
1892
+ // Watch for git commits — reconcile pending changes against actual dirty files
1893
+ let gitWatcher = null;
1894
+ let gitDebounceTimer = null;
1895
+ function setupGitWatcher() {
1896
+ const gitIndex = path.join(config.workspaceRoot, ".git", "index");
1897
+ if (!existsSync(gitIndex))
1898
+ return;
1899
+ gitWatcher = watch(gitIndex, { ignoreInitial: true, depth: 0 });
1900
+ gitWatcher.on("change", () => {
1901
+ // Debounce — git writes the index multiple times during a commit
1902
+ if (gitDebounceTimer)
1903
+ clearTimeout(gitDebounceTimer);
1904
+ gitDebounceTimer = setTimeout(() => {
1905
+ gitDebounceTimer = null;
1906
+ reconcilePendingWithGit();
1907
+ }, 1000);
1908
+ });
1909
+ }
1910
+ function reconcilePendingWithGit() {
1911
+ if (pendingSourceChanges.size === 0)
1912
+ return;
1913
+ try {
1914
+ // Get files that are still dirty (modified, untracked, staged)
1915
+ const output = execSync("git status --porcelain", {
1916
+ cwd: config.workspaceRoot,
1917
+ encoding: "utf-8",
1918
+ timeout: 5000,
1919
+ });
1920
+ const dirtyFiles = new Set(output.trim().split("\n").filter(Boolean).map((line) => line.slice(3).trim()));
1921
+ let changed = false;
1922
+ for (const [filePath] of pendingSourceChanges) {
1923
+ if (!dirtyFiles.has(filePath)) {
1924
+ pendingSourceChanges.delete(filePath);
1925
+ changed = true;
1926
+ }
1927
+ }
1928
+ if (changed) {
1929
+ savePendingChanges();
1930
+ if (pendingSourceChanges.size === 0) {
1931
+ broadcastOpsEvent({ type: "changes:cleared" });
1932
+ }
1933
+ else {
1934
+ broadcastPendingChanges();
1935
+ }
1936
+ }
1937
+ }
1938
+ catch {
1939
+ // Not a git repo or git not available — ignore
1940
+ }
1941
+ }
1831
1942
  // Graceful shutdown
1832
1943
  function setupShutdown() {
1833
1944
  const shutdown = async () => {
@@ -1840,14 +1951,6 @@ function setupShutdown() {
1840
1951
  catch { }
1841
1952
  }
1842
1953
  runningWorkflows.clear();
1843
- if (patrolProcess) {
1844
- try {
1845
- patrolProcess.kill("SIGTERM");
1846
- }
1847
- catch { }
1848
- patrolProcess = null;
1849
- patrolRunning = false;
1850
- }
1851
1954
  if (chatProcess) {
1852
1955
  try {
1853
1956
  chatProcess.kill("SIGTERM");
@@ -1862,7 +1965,9 @@ function setupShutdown() {
1862
1965
  catch { }
1863
1966
  }
1864
1967
  sseClients.clear();
1968
+ await sourceWatcher?.close();
1865
1969
  await diagramWatcher?.close();
1970
+ await gitWatcher?.close();
1866
1971
  httpServer?.close();
1867
1972
  process.exit(0);
1868
1973
  };
@@ -1878,8 +1983,7 @@ function loadLicenseInfo() {
1878
1983
  tier: "free",
1879
1984
  features: {
1880
1985
  analyze: true,
1881
- validate: false,
1882
- patrol: false,
1986
+ audit: false,
1883
1987
  workflows: false,
1884
1988
  chat: false,
1885
1989
  premiumAgents: false,
@@ -1931,8 +2035,7 @@ function loadLicenseInfo() {
1931
2035
  tier: isPremium ? "premium" : "free",
1932
2036
  features: {
1933
2037
  analyze: true,
1934
- validate: isPremium,
1935
- patrol: isPremium,
2038
+ audit: isPremium,
1936
2039
  workflows: isPremium,
1937
2040
  chat: isPremium,
1938
2041
  premiumAgents: isPremium,
@@ -1955,7 +2058,12 @@ export async function startServer(cfg) {
1955
2058
  console.error("[archbyte] Failed to start HTTP server:", err);
1956
2059
  process.exit(1);
1957
2060
  }
2061
+ currentArchitecture = await loadArchitecture();
1958
2062
  setupWatcher();
2063
+ loadPendingChanges();
2064
+ reconcilePendingWithGit();
2065
+ setupSourceWatcher();
2066
+ setupGitWatcher();
1959
2067
  console.error(`[archbyte] Serving ${config.name}`);
1960
2068
  console.error(`[archbyte] Diagram: ${config.diagramPath}`);
1961
2069
  // Listen for 'q' keypress to quit gracefully
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@anthropic-ai/sdk": "^0.74.0",
39
- "@google/genai": "^1.41.0",
39
+ "@google/genai": "^1.42.0",
40
40
  "@modelcontextprotocol/sdk": "^1.26.0",
41
41
  "chalk": "^5.3.0",
42
42
  "chokidar": "^3.5.3",
@@ -102,10 +102,31 @@ rules:
102
102
  # to: { type: external }
103
103
  # level: warn
104
104
 
105
- # ── Patrol ──
106
- # Configure the continuous monitoring daemon (archbyte patrol).
107
- # patrol:
108
- # ignore:
109
- # - "docs/"
110
- # - "*.md"
111
- # - "scripts/"
105
+ # ── Workflows ──
106
+ # Composable multi-step architecture pipelines.
107
+ # Run built-in workflows:
108
+ # archbyte workflow --list # see available workflows
109
+ # archbyte workflow --run full-analysis
110
+ # archbyte workflow --run ci-check
111
+ # archbyte workflow --run drift-check
112
+ #
113
+ # Create custom workflows in .archbyte/workflows/:
114
+ # archbyte workflow --create "My Pipeline"
115
+ #
116
+ # Custom workflow format (.archbyte/workflows/my-pipeline.yaml):
117
+ # id: my-pipeline
118
+ # name: "My Pipeline"
119
+ # description: "Custom architecture pipeline"
120
+ # steps:
121
+ # - id: generate
122
+ # name: "Generate Diagram"
123
+ # command: "archbyte generate"
124
+ # needs: []
125
+ # - id: validate
126
+ # name: "Validate"
127
+ # command: "archbyte validate"
128
+ # needs: [generate]
129
+ # - id: export
130
+ # name: "Export"
131
+ # command: "archbyte export --format mermaid"
132
+ # needs: [validate]