archbyte 0.4.0 → 0.4.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.
@@ -3,8 +3,57 @@ import * as fs from "fs";
3
3
  import { execSync } from "child_process";
4
4
  import chalk from "chalk";
5
5
  import { runValidation } from "./validate.js";
6
- import { loadMetadata } from "./yaml-io.js";
6
+ import { loadPatrolIgnore } from "./shared.js";
7
+ import { loadArchitectureJSON, diffArchitectures, hasStructuralChanges } from "./arch-diff.js";
7
8
  const PATROL_DIR = ".archbyte/patrols";
9
+ /**
10
+ * Convert a gitignore-style pattern to chokidar glob(s).
11
+ */
12
+ function toChokidarGlobs(line) {
13
+ // Directory pattern: "dist/" → "**/dist/**"
14
+ if (line.endsWith("/")) {
15
+ return [`**/${line.slice(0, -1)}/**`];
16
+ }
17
+ // Glob with extension: "*.pyc" → "**/*.pyc"
18
+ if (line.startsWith("*.")) {
19
+ return [`**/${line}`];
20
+ }
21
+ // Already a glob — pass through
22
+ if (line.includes("*") || line.includes("?")) {
23
+ return [line];
24
+ }
25
+ // Plain name: treat as both file and directory
26
+ return [`**/${line}`, `**/${line}/**`];
27
+ }
28
+ /**
29
+ * Build chokidar ignored list from three layers:
30
+ * 1. Baseline: hidden dirs + node_modules (always)
31
+ * 2. .gitignore patterns (if present)
32
+ * 3. archbyte.yaml patrol.ignore patterns (if configured)
33
+ */
34
+ function buildWatchIgnored(configPath) {
35
+ const ignored = [
36
+ /(^|[/\\])\./, // hidden files/dirs (.git, .archbyte, .venv, .next, etc.)
37
+ "**/node_modules/**",
38
+ ];
39
+ // Layer 2: .gitignore
40
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
41
+ if (fs.existsSync(gitignorePath)) {
42
+ const lines = fs.readFileSync(gitignorePath, "utf-8").split("\n");
43
+ for (const raw of lines) {
44
+ const line = raw.trim();
45
+ if (!line || line.startsWith("#") || line.startsWith("!"))
46
+ continue;
47
+ ignored.push(...toChokidarGlobs(line));
48
+ }
49
+ }
50
+ // Layer 3: archbyte.yaml patrol.ignore
51
+ const userPatterns = loadPatrolIgnore(configPath);
52
+ for (const pattern of userPatterns) {
53
+ ignored.push(...toChokidarGlobs(pattern));
54
+ }
55
+ return ignored;
56
+ }
8
57
  const HISTORY_FILE = "history.jsonl";
9
58
  const LATEST_FILE = "latest.json";
10
59
  function parseInterval(str) {
@@ -41,63 +90,240 @@ function saveRecord(patrolDir, record) {
41
90
  // Append to history
42
91
  fs.appendFileSync(path.join(patrolDir, HISTORY_FILE), JSON.stringify(record) + "\n", "utf-8");
43
92
  }
44
- function diffViolations(previous, current) {
45
- const key = (v) => `${v.rule}:${v.message}`;
93
+ function diffIssues(previous, current) {
94
+ const key = (i) => `${i.type}:${i.rule || i.metric}:${i.message}`;
46
95
  const prevKeys = new Set(previous.map(key));
47
96
  const currKeys = new Set(current.map(key));
48
97
  return {
49
- newViolations: current.filter((v) => !prevKeys.has(key(v))),
50
- resolvedViolations: previous.filter((v) => !currKeys.has(key(v))),
98
+ newIssues: current.filter((i) => !prevKeys.has(key(i))),
99
+ resolvedIssues: previous.filter((i) => !currKeys.has(key(i))),
51
100
  };
52
101
  }
53
- function getCurrentCommit() {
102
+ /**
103
+ * Load token usage from analysis.json metadata
104
+ */
105
+ function loadTokenUsage() {
54
106
  try {
55
- return execSync("git rev-parse HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
107
+ const analysisPath = path.join(process.cwd(), ".archbyte", "analysis.json");
108
+ if (!fs.existsSync(analysisPath))
109
+ return null;
110
+ const analysis = JSON.parse(fs.readFileSync(analysisPath, "utf-8"));
111
+ const tokenUsage = analysis.metadata?.tokenUsage;
112
+ if (tokenUsage?.input && tokenUsage?.output) {
113
+ return { input: tokenUsage.input, output: tokenUsage.output };
114
+ }
115
+ return null;
56
116
  }
57
117
  catch {
58
118
  return null;
59
119
  }
60
120
  }
61
- async function maybeRescan(options) {
62
- if (!options.rescan)
63
- return false;
64
- const rootDir = process.cwd();
65
- const meta = loadMetadata(rootDir);
66
- const currentCommit = getCurrentCommit();
67
- if (!currentCommit || !meta?.lastCommit || currentCommit === meta.lastCommit) {
68
- return false;
121
+ // ─── Git change tracking ───
122
+ function gitExec(cmd, rootDir) {
123
+ try {
124
+ return execSync(cmd, {
125
+ cwd: rootDir,
126
+ encoding: "utf-8",
127
+ stdio: ["pipe", "pipe", "pipe"],
128
+ }).trim();
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ function captureGitState(rootDir, patrolDir) {
135
+ const commitHash = gitExec("git rev-parse HEAD", rootDir);
136
+ if (!commitHash)
137
+ return undefined;
138
+ const previous = loadLatestRecord(patrolDir);
139
+ const previousCommitHash = previous?.git?.commitHash;
140
+ let changedFiles = [];
141
+ let commitCount = 0;
142
+ if (previousCommitHash && previousCommitHash !== commitHash) {
143
+ const countStr = gitExec(`git rev-list --count ${previousCommitHash}..HEAD`, rootDir);
144
+ if (countStr)
145
+ commitCount = parseInt(countStr, 10) || 0;
146
+ const diff = gitExec(`git diff --name-only ${previousCommitHash}..HEAD`, rootDir);
147
+ if (diff)
148
+ changedFiles = diff.split("\n").filter(Boolean);
69
149
  }
70
- // Commit changed run incremental analyze
71
- console.log(chalk.gray(` Rescan: commit changed (${meta.lastCommit.slice(0, 7)} → ${currentCommit.slice(0, 7)})`));
150
+ // Include uncommitted changes (staged + unstaged + untracked)
151
+ const unstaged = gitExec("git diff --name-only", rootDir);
152
+ if (unstaged) {
153
+ for (const f of unstaged.split("\n").filter(Boolean)) {
154
+ if (!changedFiles.includes(f))
155
+ changedFiles.push(f);
156
+ }
157
+ }
158
+ const staged = gitExec("git diff --name-only --cached", rootDir);
159
+ if (staged) {
160
+ for (const f of staged.split("\n").filter(Boolean)) {
161
+ if (!changedFiles.includes(f))
162
+ changedFiles.push(f);
163
+ }
164
+ }
165
+ const untracked = gitExec("git ls-files --others --exclude-standard", rootDir);
166
+ if (untracked) {
167
+ for (const f of untracked.split("\n").filter(Boolean)) {
168
+ if (!changedFiles.includes(f))
169
+ changedFiles.push(f);
170
+ }
171
+ }
172
+ return {
173
+ commitHash,
174
+ previousCommitHash,
175
+ commitCount,
176
+ changedFiles: changedFiles.slice(0, 100),
177
+ changedFileCount: changedFiles.length,
178
+ };
179
+ }
180
+ // ─── Structural diff → issues ───
181
+ function structuralDiffToIssues(diff) {
182
+ const issues = [];
183
+ for (const node of diff.addedNodes) {
184
+ issues.push({
185
+ type: 'structural',
186
+ metric: 'component-added',
187
+ level: 'warn',
188
+ message: `New component: "${node.label}" (${node.type}, ${node.layer})`,
189
+ nodeIds: [node.id],
190
+ });
191
+ }
192
+ for (const node of diff.removedNodes) {
193
+ issues.push({
194
+ type: 'structural',
195
+ metric: 'component-removed',
196
+ level: 'warn',
197
+ message: `Removed component: "${node.label}" (${node.type}, ${node.layer})`,
198
+ nodeIds: [node.id],
199
+ });
200
+ }
201
+ for (const mod of diff.modifiedNodes) {
202
+ issues.push({
203
+ type: 'structural',
204
+ metric: 'component-modified',
205
+ level: 'warn',
206
+ message: `"${mod.id}" ${mod.field}: "${mod.from}" -> "${mod.to}"`,
207
+ nodeIds: [mod.id],
208
+ });
209
+ }
210
+ if (diff.addedEdges.length > 0 || diff.removedEdges.length > 0) {
211
+ issues.push({
212
+ type: 'structural',
213
+ metric: 'connections-changed',
214
+ level: 'warn',
215
+ message: `Connections: +${diff.addedEdges.length} added, -${diff.removedEdges.length} removed`,
216
+ });
217
+ }
218
+ return issues;
219
+ }
220
+ // ─── Metrics from validation ───
221
+ function computeMetrics(result) {
222
+ return {
223
+ circularDependencies: result.violations.filter((v) => v.rule === "no-circular-deps").length,
224
+ highDegreeHubCount: result.violations.filter((v) => v.rule === "max-connections").length,
225
+ disconnectedComponentCount: result.violations.filter((v) => v.rule === "no-orphans").length,
226
+ };
227
+ }
228
+ /**
229
+ * Run a full patrol cycle: snapshot → analyze → generate → validate → diff.
230
+ */
231
+ async function runPatrolCycle(options, patrolDir) {
232
+ const cycleStart = Date.now();
233
+ const rootDir = process.cwd();
234
+ // 0. Snapshot architecture before cycle for structural diff
235
+ const prevArch = loadArchitectureJSON(rootDir);
236
+ // 0b. Capture git state
237
+ const git = captureGitState(rootDir, patrolDir);
238
+ // 1. Run full analyze (static context → LLM pipeline, incremental)
72
239
  try {
73
240
  const { handleAnalyze } = await import("./analyze.js");
74
- await handleAnalyze({ verbose: false });
75
- return true;
241
+ await handleAnalyze({ verbose: false, skipServeHint: true });
76
242
  }
77
243
  catch (err) {
78
- console.error(chalk.yellow(` Rescan failed: ${err instanceof Error ? err.message : String(err)}`));
79
- return false;
244
+ console.error(chalk.yellow(` Analyze failed: ${err instanceof Error ? err.message : String(err)}`));
80
245
  }
81
- }
82
- function runPatrolCycle(options, patrolDir) {
246
+ // 2. Generate diagram from updated analysis
247
+ try {
248
+ const { handleGenerate } = await import("./generate.js");
249
+ await handleGenerate({ verbose: false });
250
+ }
251
+ catch (err) {
252
+ console.error(chalk.yellow(` Generate failed: ${err instanceof Error ? err.message : String(err)}`));
253
+ }
254
+ // 3. Compute structural diff
255
+ const currArch = loadArchitectureJSON(rootDir);
256
+ let structuralDiff;
257
+ if (prevArch && currArch) {
258
+ const diff = diffArchitectures(prevArch, currArch);
259
+ if (hasStructuralChanges(diff)) {
260
+ structuralDiff = diff;
261
+ }
262
+ }
263
+ // 4. Validate (get rule violations)
83
264
  const previous = loadLatestRecord(patrolDir);
84
265
  const result = runValidation({
85
266
  diagram: options.diagram,
86
267
  config: options.config,
87
268
  });
88
- const { newViolations, resolvedViolations } = previous
89
- ? diffViolations(previous.violations, result.violations)
90
- : { newViolations: result.violations, resolvedViolations: [] };
269
+ // Convert rule violations to audit issues
270
+ const ruleIssues = result.violations.map((v) => ({
271
+ type: 'rule-violation',
272
+ rule: v.rule,
273
+ level: v.level,
274
+ message: v.message,
275
+ ruleType: 'builtin',
276
+ }));
277
+ // Convert structural diff to audit issues
278
+ const structuralIssues = structuralDiff ? structuralDiffToIssues(structuralDiff) : [];
279
+ // Merge all issues
280
+ const allIssues = [...ruleIssues, ...structuralIssues];
281
+ // Diff against previous cycle
282
+ const prevIssues = previous?.issues ?? [];
283
+ const { newIssues, resolvedIssues } = diffIssues(prevIssues, allIssues);
284
+ // 5. Extract token usage
285
+ const tokenData = loadTokenUsage();
286
+ // 6. Build record with all fields (structured + flattened for UI)
91
287
  const record = {
92
288
  timestamp: new Date().toISOString(),
93
- passed: result.errors === 0,
289
+ passed: result.errors === 0 && structuralIssues.length === 0,
290
+ issues: allIssues,
291
+ summary: {
292
+ ruleViolations: ruleIssues.length,
293
+ structuralIssues: structuralIssues.length,
294
+ totalIssues: allIssues.length,
295
+ },
296
+ newIssues,
297
+ resolvedIssues,
298
+ metrics: computeMetrics(result),
299
+ durationMs: Date.now() - cycleStart,
300
+ tokenUsage: tokenData
301
+ ? {
302
+ inputTokens: tokenData.input,
303
+ outputTokens: tokenData.output,
304
+ totalTokens: tokenData.input + tokenData.output,
305
+ }
306
+ : undefined,
307
+ // Structural diff
308
+ structuralDiff,
309
+ // Git context
310
+ git,
311
+ // Architecture counts
312
+ totalNodes: currArch?.nodes.length ?? 0,
313
+ totalEdges: currArch?.edges.length ?? 0,
314
+ // Flattened for UI (backward compat)
94
315
  errors: result.errors,
95
316
  warnings: result.warnings,
96
- violations: result.violations,
97
- newViolations,
98
- resolvedViolations,
99
- totalNodes: result.totalNodes,
100
- totalEdges: result.totalEdges,
317
+ newViolations: newIssues.map((i) => ({
318
+ rule: i.rule || i.metric || "",
319
+ level: i.level,
320
+ message: i.message,
321
+ })),
322
+ resolvedViolations: resolvedIssues.map((i) => ({
323
+ rule: i.rule || i.metric || "",
324
+ level: i.level,
325
+ message: i.message,
326
+ })),
101
327
  };
102
328
  saveRecord(patrolDir, record);
103
329
  return record;
@@ -105,23 +331,66 @@ function runPatrolCycle(options, patrolDir) {
105
331
  function printPatrolResult(record, cycleNum) {
106
332
  const time = new Date(record.timestamp).toLocaleTimeString();
107
333
  const status = record.passed ? chalk.green("HEALTHY") : chalk.red("VIOLATION");
334
+ const duration = record.durationMs ? `${(record.durationMs / 1000).toFixed(1)}s` : "?";
108
335
  console.log();
109
- console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status}`));
110
- if (record.newViolations.length > 0) {
111
- console.log(chalk.red(` ${record.newViolations.length} new violation(s):`));
112
- for (const v of record.newViolations) {
113
- const icon = v.level === "error" ? chalk.red("!!") : chalk.yellow("!!");
114
- console.log(chalk.gray(` ${icon} [${v.rule}] ${v.message}`));
336
+ console.log(chalk.bold.cyan(` Patrol #${cycleNum} | ${time} | ${status} | ${duration}`));
337
+ console.log(chalk.gray(` Architecture: ${record.totalNodes} nodes, ${record.totalEdges} edges`));
338
+ // Token usage
339
+ if (record.tokenUsage) {
340
+ const tokens = record.tokenUsage;
341
+ console.log(chalk.gray(` Tokens: ${tokens.inputTokens.toLocaleString()} in + ${tokens.outputTokens.toLocaleString()} out = ${tokens.totalTokens.toLocaleString()} total`));
342
+ }
343
+ // Git changes
344
+ if (record.git && (record.git.commitCount > 0 || record.git.changedFileCount > 0)) {
345
+ const parts = [];
346
+ if (record.git.commitCount > 0)
347
+ parts.push(`${record.git.commitCount} commit(s)`);
348
+ parts.push(`${record.git.changedFileCount} file(s) changed`);
349
+ console.log(chalk.gray(` Git: ${parts.join(", ")}`));
350
+ }
351
+ // Structural changes
352
+ if (record.structuralDiff && hasStructuralChanges(record.structuralDiff)) {
353
+ const d = record.structuralDiff;
354
+ console.log(chalk.bold.yellow(` Architecture drift detected:`));
355
+ for (const n of d.addedNodes) {
356
+ console.log(chalk.green(` + ${n.label} (${n.type})`));
357
+ }
358
+ for (const n of d.removedNodes) {
359
+ console.log(chalk.red(` - ${n.label} (${n.type})`));
360
+ }
361
+ for (const m of d.modifiedNodes) {
362
+ console.log(chalk.yellow(` ~ ${m.id}: ${m.field} "${m.from}" -> "${m.to}"`));
363
+ }
364
+ if (d.addedEdges.length > 0) {
365
+ console.log(chalk.green(` + ${d.addedEdges.length} new connection(s)`));
366
+ }
367
+ if (d.removedEdges.length > 0) {
368
+ console.log(chalk.red(` - ${d.removedEdges.length} removed connection(s)`));
115
369
  }
116
370
  }
117
- if (record.resolvedViolations.length > 0) {
118
- console.log(chalk.green(` ${record.resolvedViolations.length} resolved:`));
119
- for (const v of record.resolvedViolations) {
120
- console.log(chalk.green(` -- [${v.rule}] ${v.message}`));
371
+ // New rule violations
372
+ const newRuleIssues = record.newIssues.filter((i) => i.type === "rule-violation");
373
+ if (newRuleIssues.length > 0) {
374
+ console.log(chalk.red(` ${newRuleIssues.length} new rule violation(s):`));
375
+ for (const issue of newRuleIssues) {
376
+ const icon = issue.level === "error" ? chalk.red("!!") : chalk.yellow("!!");
377
+ const label = issue.rule || issue.metric || "unknown";
378
+ console.log(chalk.gray(` ${icon} [${label}] ${issue.message}`));
121
379
  }
122
380
  }
123
- if (record.newViolations.length === 0 && record.resolvedViolations.length === 0) {
124
- console.log(chalk.gray(` No changes. ${record.errors} errors, ${record.warnings} warnings`));
381
+ // Resolved issues
382
+ if (record.resolvedIssues.length > 0) {
383
+ console.log(chalk.green(` ${record.resolvedIssues.length} resolved:`));
384
+ for (const issue of record.resolvedIssues) {
385
+ const label = issue.rule || issue.metric || "unknown";
386
+ console.log(chalk.green(` -- [${label}] ${issue.message}`));
387
+ }
388
+ }
389
+ // No changes at all
390
+ if (record.newIssues.length === 0 &&
391
+ record.resolvedIssues.length === 0 &&
392
+ (!record.structuralDiff || !hasStructuralChanges(record.structuralDiff))) {
393
+ console.log(chalk.gray(` No changes. ${record.summary.ruleViolations} rules, ${record.summary.structuralIssues} structural`));
125
394
  }
126
395
  }
127
396
  function printHistory(patrolDir) {
@@ -155,34 +424,43 @@ function printHistory(patrolDir) {
155
424
  console.log(` Health: ${sparkline} (last ${records.length} patrols)`);
156
425
  console.log();
157
426
  // Table
158
- console.log(chalk.gray(" Time Status Errors Warnings New Resolved"));
159
- console.log(chalk.gray(" " + "-".repeat(68)));
427
+ console.log(chalk.gray(" Time Status Nodes Edges Rules Struct New Resolved Git"));
428
+ console.log(chalk.gray(" " + "-".repeat(100)));
160
429
  for (const r of records) {
161
430
  const time = new Date(r.timestamp).toLocaleString().padEnd(20);
162
431
  const status = r.passed ? chalk.green("PASS ") : chalk.red("FAIL ");
163
- const errors = String(r.errors).padEnd(8);
164
- const warnings = String(r.warnings).padEnd(10);
165
- const newV = String(r.newViolations.length).padEnd(5);
166
- const resolved = String(r.resolvedViolations.length);
167
- console.log(` ${time} ${status} ${errors}${warnings}${newV}${resolved}`);
432
+ const nodes = String(r.totalNodes ?? "?").padEnd(6);
433
+ const edges = String(r.totalEdges ?? "?").padEnd(6);
434
+ const rules = String(r.summary?.ruleViolations ?? 0).padEnd(6);
435
+ const structural = String(r.summary?.structuralIssues ?? 0).padEnd(8);
436
+ const newV = String(r.newIssues?.length ?? 0).padEnd(5);
437
+ const resolved = String(r.resolvedIssues?.length ?? 0).padEnd(10);
438
+ const gitInfo = r.git
439
+ ? `${r.git.commitCount}c/${r.git.changedFileCount}f`
440
+ : "-";
441
+ console.log(` ${time} ${status} ${nodes}${edges}${rules}${structural}${newV}${resolved}${gitInfo}`);
168
442
  }
169
443
  console.log();
170
444
  // Summary
171
445
  const totalPatrols = lines.length;
172
- const failedPatrols = lines.filter((l) => !JSON.parse(l).passed).length;
173
- const healthPct = Math.round(((totalPatrols - failedPatrols) / totalPatrols) * 100);
446
+ const failedPatrols = records.filter((r) => !r.passed).length;
447
+ const healthPct = totalPatrols > 0
448
+ ? Math.round(((totalPatrols - failedPatrols) / totalPatrols) * 100)
449
+ : 100;
174
450
  console.log(` Total patrols: ${totalPatrols} | Health rate: ${healthPct}% | Failed: ${failedPatrols}`);
175
451
  console.log();
176
452
  }
177
453
  function handleViolationAction(record, action) {
178
- if (record.newViolations.length === 0)
454
+ if (record.newIssues.length === 0 && (!record.structuralDiff || !hasStructuralChanges(record.structuralDiff)))
179
455
  return;
180
456
  switch (action) {
181
457
  case "json":
182
458
  console.log(JSON.stringify({
183
- event: "patrol-violation",
459
+ event: "patrol-issue",
184
460
  timestamp: record.timestamp,
185
- newViolations: record.newViolations,
461
+ newIssues: record.newIssues,
462
+ structuralDiff: record.structuralDiff,
463
+ git: record.git,
186
464
  }));
187
465
  break;
188
466
  case "log":
@@ -193,8 +471,9 @@ function handleViolationAction(record, action) {
193
471
  }
194
472
  /**
195
473
  * Run the architecture patrol daemon.
196
- * Inspired by Gastown's patrol loop pattern cyclic monitoring
197
- * that detects drift and reports violations.
474
+ * Each cycle: snapshot analyze (incremental) generate → validate → diff
475
+ *
476
+ * --once: run a single cycle then exit (used by UI "Run Now")
198
477
  */
199
478
  export async function handlePatrol(options) {
200
479
  const projectName = process.cwd().split("/").pop() || "project";
@@ -204,25 +483,98 @@ export async function handlePatrol(options) {
204
483
  printHistory(patrolDir);
205
484
  return;
206
485
  }
486
+ const action = options.onViolation || "log";
487
+ // Run initial cycle immediately
488
+ let cycleNum = 1;
489
+ // Watch mode: trigger patrol cycle on source file changes
490
+ if (options.watch) {
491
+ const chokidar = await import("chokidar");
492
+ const userPatterns = loadPatrolIgnore(options.config);
493
+ console.log();
494
+ console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
495
+ console.log(chalk.gray(` Mode: watch | On violation: ${action}`));
496
+ console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
497
+ console.log(chalk.gray(` History: archbyte patrol --history`));
498
+ if (userPatterns.length > 0) {
499
+ console.log(chalk.gray(` Ignoring: ${userPatterns.join(", ")} (from archbyte.yaml)`));
500
+ }
501
+ else {
502
+ console.log(chalk.gray(` Tip: Add patrol.ignore in archbyte.yaml to exclude paths from watching`));
503
+ }
504
+ console.log(chalk.gray(" Press Ctrl+C to stop."));
505
+ // Set up watcher FIRST — its file handles keep the event loop alive
506
+ // so the process can't exit during the initial cycle.
507
+ let debounceTimer = null;
508
+ let running = false;
509
+ const triggerCycle = () => {
510
+ if (debounceTimer)
511
+ clearTimeout(debounceTimer);
512
+ debounceTimer = setTimeout(async () => {
513
+ if (running)
514
+ return;
515
+ running = true;
516
+ cycleNum++;
517
+ try {
518
+ const record = await runPatrolCycle(options, patrolDir);
519
+ printPatrolResult(record, cycleNum);
520
+ handleViolationAction(record, action);
521
+ }
522
+ catch (err) {
523
+ console.error(chalk.red(` Patrol cycle #${cycleNum} failed: ${err}`));
524
+ }
525
+ finally {
526
+ running = false;
527
+ }
528
+ }, 500);
529
+ };
530
+ const watcher = chokidar.watch(".", {
531
+ cwd: process.cwd(),
532
+ ignoreInitial: true,
533
+ ignored: buildWatchIgnored(options.config),
534
+ });
535
+ watcher.on("change", triggerCycle);
536
+ watcher.on("add", triggerCycle);
537
+ watcher.on("unlink", triggerCycle);
538
+ const shutdown = () => {
539
+ watcher.close();
540
+ if (debounceTimer)
541
+ clearTimeout(debounceTimer);
542
+ console.log();
543
+ console.log(chalk.gray(` Patrol stopped after ${cycleNum} cycles.`));
544
+ process.exit(0);
545
+ };
546
+ process.on("SIGINT", shutdown);
547
+ process.on("SIGTERM", shutdown);
548
+ // Now run initial cycle — watcher is already anchoring the event loop
549
+ try {
550
+ const record = await runPatrolCycle(options, patrolDir);
551
+ printPatrolResult(record, cycleNum);
552
+ handleViolationAction(record, action);
553
+ }
554
+ catch (err) {
555
+ console.error(chalk.red(` Initial patrol cycle failed: ${err}`));
556
+ }
557
+ console.log(chalk.gray(" Watching for changes..."));
558
+ await new Promise(() => { });
559
+ return;
560
+ }
561
+ // Timer mode (default): poll on interval
207
562
  const intervalMs = parseInterval(options.interval || "5m");
208
563
  const intervalStr = options.interval || "5m";
209
- const action = options.onViolation || "log";
210
564
  console.log();
211
565
  console.log(chalk.bold.cyan(` ArchByte Patrol: ${projectName}`));
212
566
  console.log(chalk.gray(` Interval: ${intervalStr} | On violation: ${action}`));
213
- console.log(chalk.gray(` History: ${path.join(PATROL_DIR, HISTORY_FILE)}`));
567
+ console.log(chalk.gray(` Detects: rule violations, architecture drift, git changes`));
568
+ console.log(chalk.gray(` History: archbyte patrol --history`));
214
569
  console.log(chalk.gray(" Press Ctrl+C to stop."));
215
- // Run initial cycle immediately
216
- let cycleNum = 1;
217
- const record = runPatrolCycle(options, patrolDir);
570
+ const record = await runPatrolCycle(options, patrolDir);
218
571
  printPatrolResult(record, cycleNum);
219
572
  handleViolationAction(record, action);
220
573
  // Patrol loop
221
574
  const timer = setInterval(async () => {
222
575
  cycleNum++;
223
576
  try {
224
- await maybeRescan(options);
225
- const record = runPatrolCycle(options, patrolDir);
577
+ const record = await runPatrolCycle(options, patrolDir);
226
578
  printPatrolResult(record, cycleNum);
227
579
  handleViolationAction(record, action);
228
580
  }
package/dist/cli/setup.js CHANGED
@@ -211,7 +211,7 @@ export async function handleSetup() {
211
211
  console.log();
212
212
  console.log(chalk.bold.cyan("ArchByte Setup"));
213
213
  console.log(chalk.gray("Configure your model provider and API key.\n"));
214
- // Detect AI coding tools — suggest MCP instead of BYOK
214
+ // Detect AI coding tools
215
215
  const hasClaude = isInPath("claude");
216
216
  const codexDir = path.join(CONFIG_DIR, "../.codex");
217
217
  const hasCodex = fs.existsSync(codexDir);
@@ -303,9 +303,6 @@ export async function handleSetup() {
303
303
  console.log(" " + chalk.bold("Next steps"));
304
304
  console.log();
305
305
  console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
306
- if (hasCodex) {
307
- console.log(" " + chalk.cyan("archbyte mcp install") + " Use from Codex CLI");
308
- }
309
306
  console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
310
307
  console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
311
308
  console.log();
@@ -313,8 +310,7 @@ export async function handleSetup() {
313
310
  }
314
311
  if (choice === "codex") {
315
312
  // TODO: Add Codex SDK provider when available
316
- console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now."));
317
- console.log(chalk.gray(" In the meantime, use archbyte mcp install to run ArchByte from Codex.\n"));
313
+ console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now.\n"));
318
314
  }
319
315
  // User chose BYOK — continue to normal provider selection below
320
316
  if (choice === "byok")
@@ -596,7 +592,6 @@ export async function handleSetup() {
596
592
  console.log();
597
593
  console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
598
594
  if (hasClaude || hasCodex) {
599
- console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
600
595
  }
601
596
  console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
602
597
  console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
@@ -64,5 +64,16 @@ export declare function parseRulesFromYaml(content: string): RuleConfig;
64
64
  * level: error
65
65
  */
66
66
  export declare function parseCustomRulesFromYaml(content: string): CustomRule[];
67
+ /**
68
+ * Parse the patrol.ignore list from archbyte.yaml.
69
+ * Returns user-defined glob patterns for watch mode to ignore.
70
+ *
71
+ * patrol:
72
+ * ignore:
73
+ * - "docs/"
74
+ * - "*.md"
75
+ * - "build/"
76
+ */
77
+ export declare function loadPatrolIgnore(configPath?: string): string[];
67
78
  export declare function getRuleLevel(config: RuleConfig, rule: keyof RuleConfig, defaultLevel: RuleLevel): RuleLevel;
68
79
  export declare function getThreshold(config: RuleConfig, rule: "max-connections", defaultVal: number): number;