pi-lens 2.0.44 → 2.1.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.
Files changed (89) hide show
  1. package/README.md +89 -0
  2. package/clients/agent-behavior-client.js +110 -0
  3. package/clients/agent-behavior-client.test.js +94 -0
  4. package/clients/agent-behavior-client.test.ts +116 -0
  5. package/clients/agent-behavior-client.ts +152 -0
  6. package/clients/architect-client.js +1 -0
  7. package/clients/architect-client.ts +1 -0
  8. package/clients/ast-grep-client.js +3 -2
  9. package/clients/ast-grep-client.ts +9 -4
  10. package/clients/auto-loop.js +5 -1
  11. package/clients/auto-loop.ts +23 -7
  12. package/clients/biome-client.js +3 -12
  13. package/clients/biome-client.test.js +3 -1
  14. package/clients/biome-client.test.ts +3 -1
  15. package/clients/biome-client.ts +3 -12
  16. package/clients/cache-manager.js +245 -0
  17. package/clients/cache-manager.test.js +197 -0
  18. package/clients/cache-manager.test.ts +299 -0
  19. package/clients/cache-manager.ts +331 -0
  20. package/clients/complexity-client.js +3 -7
  21. package/clients/complexity-client.ts +12 -13
  22. package/clients/dependency-checker.js +26 -5
  23. package/clients/dependency-checker.ts +32 -6
  24. package/clients/dispatch/dispatcher.js +242 -0
  25. package/clients/dispatch/dispatcher.test.js +115 -0
  26. package/clients/dispatch/dispatcher.test.ts +148 -0
  27. package/clients/dispatch/dispatcher.ts +333 -0
  28. package/clients/dispatch/integration.js +58 -0
  29. package/clients/dispatch/integration.ts +71 -0
  30. package/clients/dispatch/plan.js +131 -0
  31. package/clients/dispatch/plan.ts +145 -0
  32. package/clients/dispatch/runners/architect.js +69 -0
  33. package/clients/dispatch/runners/architect.ts +87 -0
  34. package/clients/dispatch/runners/ast-grep.js +115 -0
  35. package/clients/dispatch/runners/ast-grep.ts +139 -0
  36. package/clients/dispatch/runners/biome.js +69 -0
  37. package/clients/dispatch/runners/biome.ts +93 -0
  38. package/clients/dispatch/runners/go-vet.js +72 -0
  39. package/clients/dispatch/runners/go-vet.ts +92 -0
  40. package/clients/dispatch/runners/index.js +22 -0
  41. package/clients/dispatch/runners/index.ts +24 -0
  42. package/clients/dispatch/runners/ruff.js +68 -0
  43. package/clients/dispatch/runners/ruff.ts +88 -0
  44. package/clients/dispatch/runners/rust-clippy.js +106 -0
  45. package/clients/dispatch/runners/rust-clippy.ts +131 -0
  46. package/clients/dispatch/runners/ts-lsp.js +53 -0
  47. package/clients/dispatch/runners/ts-lsp.ts +71 -0
  48. package/clients/dispatch/runners/type-safety.js +142 -0
  49. package/clients/dispatch/runners/type-safety.ts +183 -0
  50. package/clients/dispatch/runners/utils.js +53 -0
  51. package/clients/dispatch/runners/utils.ts +60 -0
  52. package/clients/dispatch/types.js +13 -0
  53. package/clients/dispatch/types.ts +152 -0
  54. package/clients/file-kinds.js +177 -0
  55. package/clients/file-kinds.test.js +169 -0
  56. package/clients/file-kinds.test.ts +210 -0
  57. package/clients/file-kinds.ts +216 -0
  58. package/clients/fix-scanners.js +23 -4
  59. package/clients/fix-scanners.ts +25 -7
  60. package/clients/jscpd-client.js +22 -1
  61. package/clients/jscpd-client.test.js +1 -1
  62. package/clients/jscpd-client.test.ts +1 -1
  63. package/clients/jscpd-client.ts +25 -1
  64. package/clients/metrics-history.js +3 -2
  65. package/clients/metrics-history.ts +3 -2
  66. package/clients/ruff-client.js +3 -2
  67. package/clients/ruff-client.ts +3 -2
  68. package/clients/rules-scanner.js +97 -0
  69. package/clients/rules-scanner.ts +120 -0
  70. package/clients/sanitize.js +291 -0
  71. package/clients/sanitize.test.js +177 -0
  72. package/clients/sanitize.test.ts +223 -0
  73. package/clients/sanitize.ts +356 -0
  74. package/clients/sg-runner.js +10 -3
  75. package/clients/sg-runner.ts +19 -5
  76. package/clients/todo-scanner.js +1 -1
  77. package/clients/todo-scanner.ts +1 -1
  78. package/clients/tool-availability.js +213 -0
  79. package/clients/tool-availability.ts +256 -0
  80. package/commands/booboo.js +114 -48
  81. package/commands/booboo.ts +119 -53
  82. package/commands/fix.js +13 -5
  83. package/commands/fix.ts +70 -29
  84. package/commands/refactor.js +3 -5
  85. package/commands/refactor.ts +4 -5
  86. package/default-architect.yaml +8 -5
  87. package/index.ts +365 -414
  88. package/package.json +14 -1
  89. package/rules/ast-grep-rules/rules/large-class.yml +1 -1
package/README.md CHANGED
@@ -16,6 +16,76 @@ pi install git:github.com/apmantza/pi-lens
16
16
 
17
17
  ---
18
18
 
19
+ ## What's New (v2.0)
20
+
21
+ ### Declarative Dispatch System
22
+
23
+ The core linting engine has been redesigned from ~400 lines of nested `if/else` blocks into a clean, extensible dispatch architecture:
24
+
25
+ ```
26
+ file → detectFileKind() → getRunnersForKind() → run all runners → aggregate output
27
+ ```
28
+
29
+ **Key improvements:**
30
+ - **Extensible**: Add new linters by dropping a runner file in `clients/dispatch/runners/` — no need to touch the core
31
+ - **Unified output**: All tools report through the same format (🔴 blocking, 🟡 warning, ✅ fixed)
32
+ - **Delta mode built-in**: Each runner supports baseline tracking to show only *new* violations
33
+ - **Conditional execution**: Runners can have `when` conditions (e.g., only run when `--autofix` is enabled)
34
+
35
+ **Runners:** `ts-lsp`, `biome`, `ruff`, `ast-grep`, `type-safety`, `architect`, `go-vet`, `rust-clippy`
36
+
37
+ ### Asynchronous Session Start
38
+
39
+ Session initialization now runs all scans concurrently with caching:
40
+
41
+ ```
42
+ session_start
43
+ ├─ TODO/FIXME scan (fast, uncached)
44
+ ├─ Knip dead code (cached 30 min)
45
+ ├─ jscpd duplicates (cached 30 min)
46
+ ├─ Type coverage (cached 30 min)
47
+ └─ Export scanning (async, for duplicate export detection)
48
+ ```
49
+
50
+ Each scan runs independently — expensive scans (jscpd, knip) are cached in `.pi-lens/cache/` with 30-minute TTL. The agent sees results immediately without waiting for slow tools.
51
+
52
+ ### Inline Messaging
53
+
54
+ Every `write` or `edit` operation returns structured feedback directly in the tool result:
55
+
56
+ ```typescript
57
+ // tool_result handler runs dispatchLint() → formats output → appends to result
58
+ return {
59
+ content: [...event.content, { type: "text", text: lspOutput }],
60
+ };
61
+ ```
62
+
63
+ **Message types:**
64
+ | Prefix | Meaning |
65
+ |--------|---------|
66
+ | 🔴 | Blocking error — must fix before continuing |
67
+ | 🟡 | Warning — should fix, but not blocking |
68
+ | ✅ | Auto-fixed issue — no action needed |
69
+ | 📊 | Silent metric — tracked but not shown |
70
+ | 📐 | Architectural rule — reference only |
71
+
72
+ ### File Type Detection
73
+
74
+ Centralized file-kind detection (`clients/file-kinds.ts`) replaces scattered regex checks:
75
+
76
+ ```typescript
77
+ const kind = detectFileKind(filePath); // "jsts" | "python" | "go" | "rust" | ...
78
+ const runners = getRunnersForKind(kind); // All applicable runners
79
+ ```
80
+
81
+ ### Project Rules Integration
82
+
83
+ pi-lens now scans for project-specific rule files (`.claude/rules/`, `.agents/rules/`, `CLAUDE.md`, `AGENTS.md`) at session start. These are surfaced in the system prompt so the agent knows to read them when relevant.
84
+
85
+ This works **alongside** pi-lens architect rules — your project's markdown rules provide general guidance, while pi-lens handles automated regex-based checks on every write.
86
+
87
+ ---
88
+
19
89
  ## Features
20
90
 
21
91
  ### On every write / edit
@@ -80,6 +150,25 @@ On every new session, scans run silently in the background. Data is cached for r
80
150
  | **jscpd** | Duplicate detection on write; `/lens-booboo` reports |
81
151
  | **type-coverage** | `/lens-booboo` reports |
82
152
  | **Complexity baselines** | Regressed/improved delta tracking via `/lens-metrics` |
153
+ | **Project rules** | Scans for `.claude/rules/`, `.agents/rules/`, `CLAUDE.md`, `AGENTS.md` |
154
+
155
+ ### Project Rules Integration
156
+
157
+ pi-lens scans for project-specific rule files at session start. If found, they're listed in the system prompt so the agent knows to read them when relevant:
158
+
159
+ ```
160
+ 📋 Project rules found: 2 file(s) in .claude/rules, root. These apply alongside pi-lens defaults.
161
+ ```
162
+
163
+ **Scanned locations:**
164
+ | Location | Description |
165
+ |----------|-------------|
166
+ | `.claude/rules/` | Claude Code rule files (recursive) |
167
+ | `.agents/rules/` | Generic agent rule files (recursive) |
168
+ | `CLAUDE.md` | Claude Code project context |
169
+ | `AGENTS.md` | Generic agent context |
170
+
171
+ These files provide **general project guidance** (coding conventions, workflow rules, architecture notes). They coexist with pi-lens architect rules — your rules inform the agent's behavior, while pi-lens provides automated regex-based checks on every write.
83
172
 
84
173
  ### On-demand commands
85
174
 
@@ -0,0 +1,110 @@
1
+ /**
2
+ * AgentBehaviorClient for pi-lens
3
+ *
4
+ * Tracks tool call sequences and flags anti-patterns in real-time:
5
+ * - Blind writes: editing or writing without reading first
6
+ * - Thrashing: repeated identical tool calls with no progress
7
+ *
8
+ * No external dependencies — purely tracks tool call history.
9
+ */
10
+ // --- Constants ---
11
+ const WRITE_OPS = new Set(["edit", "write", "multiedit"]);
12
+ const READ_OPS = new Set(["read", "bash", "grep", "glob", "find", "rg"]);
13
+ const BLIND_WRITE_WINDOW = 5; // Check last N tool calls for a read
14
+ const THRASH_THRESHOLD = 3; // Flag after N consecutive identical tools
15
+ const THRASH_TIMEOUT_MS = 30000; // Reset thrash counter if gap > 30s
16
+ // --- Client ---
17
+ export class AgentBehaviorClient {
18
+ constructor() {
19
+ this.toolHistory = [];
20
+ this.consecutiveCount = 0;
21
+ this.lastToolName = null;
22
+ this.lastToolTimestamp = 0;
23
+ // Per-file tracking
24
+ this.fileEditCount = new Map();
25
+ }
26
+ /**
27
+ * Record a tool call and return any warnings triggered.
28
+ * Called from tool_result handler.
29
+ */
30
+ recordToolCall(toolName, filePath) {
31
+ const warnings = [];
32
+ const now = Date.now();
33
+ // Track consecutive identical tools (thrashing)
34
+ if (toolName === this.lastToolName &&
35
+ now - this.lastToolTimestamp < THRASH_TIMEOUT_MS) {
36
+ this.consecutiveCount++;
37
+ }
38
+ else {
39
+ this.consecutiveCount = 1;
40
+ }
41
+ this.lastToolName = toolName;
42
+ this.lastToolTimestamp = now;
43
+ // Check for thrashing
44
+ if (this.consecutiveCount === THRASH_THRESHOLD) {
45
+ warnings.push({
46
+ type: "thrashing",
47
+ message: `🔴 THRASHING — ${THRASH_THRESHOLD} consecutive \`${toolName}\` calls with no other action. Consider fixing the root cause instead of re-running.`,
48
+ severity: "error",
49
+ details: {
50
+ toolName,
51
+ callCount: this.consecutiveCount,
52
+ },
53
+ });
54
+ }
55
+ // Check for blind writes
56
+ if (WRITE_OPS.has(toolName)) {
57
+ const recentWindow = this.toolHistory.slice(-BLIND_WRITE_WINDOW);
58
+ const hasRecentRead = recentWindow.some((r) => READ_OPS.has(r.toolName));
59
+ if (!hasRecentRead && recentWindow.length > 0) {
60
+ // Count how many writes in the window without reads
61
+ const writesWithoutRead = recentWindow.filter((r) => WRITE_OPS.has(r.toolName)).length;
62
+ if (writesWithoutRead >= 2) {
63
+ warnings.push({
64
+ type: "blind-write",
65
+ message: `⚠ BLIND WRITE — editing \`${filePath ?? "file"}\` without reading in the last ${BLIND_WRITE_WINDOW} tool calls. Read the file first to avoid assumptions.`,
66
+ severity: "warning",
67
+ details: {
68
+ filePath,
69
+ windowSize: recentWindow.length,
70
+ },
71
+ });
72
+ }
73
+ }
74
+ // Track edits per file
75
+ if (filePath) {
76
+ this.fileEditCount.set(filePath, (this.fileEditCount.get(filePath) ?? 0) + 1);
77
+ }
78
+ }
79
+ // Add to history (keep last 50 entries)
80
+ this.toolHistory.push({ toolName, filePath, timestamp: now });
81
+ if (this.toolHistory.length > 50) {
82
+ this.toolHistory = this.toolHistory.slice(-50);
83
+ }
84
+ return warnings;
85
+ }
86
+ /**
87
+ * Format warnings for LLM consumption.
88
+ */
89
+ formatWarnings(warnings) {
90
+ if (warnings.length === 0)
91
+ return "";
92
+ return warnings.map((w) => w.message).join("\n");
93
+ }
94
+ /**
95
+ * Get edit count for a file in this session.
96
+ */
97
+ getEditCount(filePath) {
98
+ return this.fileEditCount.get(filePath) ?? 0;
99
+ }
100
+ /**
101
+ * Reset state (e.g., on session start).
102
+ */
103
+ reset() {
104
+ this.toolHistory = [];
105
+ this.consecutiveCount = 0;
106
+ this.lastToolName = null;
107
+ this.lastToolTimestamp = 0;
108
+ this.fileEditCount.clear();
109
+ }
110
+ }
@@ -0,0 +1,94 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { AgentBehaviorClient } from "./agent-behavior-client.js";
3
+ describe("AgentBehaviorClient", () => {
4
+ let client;
5
+ beforeEach(() => {
6
+ client = new AgentBehaviorClient();
7
+ client.reset();
8
+ });
9
+ describe("blind write detection", () => {
10
+ it("should NOT warn when read precedes write", () => {
11
+ client.recordToolCall("read", "src/file.ts");
12
+ client.recordToolCall("edit", "src/file.ts");
13
+ const warnings = client.recordToolCall("write", "src/file.ts");
14
+ expect(warnings).toHaveLength(0);
15
+ });
16
+ it("should warn when multiple writes happen without reads", () => {
17
+ // First write is OK (no history)
18
+ client.recordToolCall("write", "src/file1.ts");
19
+ // Second write - still in window, accumulates
20
+ client.recordToolCall("edit", "src/file2.ts");
21
+ // Third write without any read - now we have a pattern
22
+ const warnings = client.recordToolCall("edit", "src/file3.ts");
23
+ expect(warnings).toHaveLength(1);
24
+ expect(warnings[0].type).toBe("blind-write");
25
+ });
26
+ it("should not warn for single write with no history", () => {
27
+ const warnings = client.recordToolCall("write", "src/file.ts");
28
+ expect(warnings).toHaveLength(0);
29
+ });
30
+ });
31
+ describe("thrashing detection", () => {
32
+ it("should warn after 3 consecutive identical tool calls", () => {
33
+ client.recordToolCall("bash", undefined);
34
+ // Second call - no warning yet
35
+ let warnings = client.recordToolCall("bash", undefined);
36
+ expect(warnings).toHaveLength(0);
37
+ // Third consecutive - should warn
38
+ warnings = client.recordToolCall("bash", undefined);
39
+ expect(warnings).toHaveLength(1);
40
+ expect(warnings[0].type).toBe("thrashing");
41
+ expect(warnings[0].details.callCount).toBe(3);
42
+ });
43
+ it("should NOT warn for different tool calls", () => {
44
+ client.recordToolCall("read", "src/file.ts");
45
+ client.recordToolCall("bash", "npm test");
46
+ const warnings = client.recordToolCall("edit", "src/file.ts");
47
+ expect(warnings).toHaveLength(0);
48
+ });
49
+ it("should reset count when tool changes", () => {
50
+ client.recordToolCall("bash", undefined);
51
+ client.recordToolCall("bash", undefined);
52
+ // Different tool resets the count
53
+ client.recordToolCall("read", "src/file.ts");
54
+ // Now start new consecutive sequence
55
+ client.recordToolCall("bash", undefined);
56
+ const warnings = client.recordToolCall("bash", undefined);
57
+ expect(warnings).toHaveLength(0); // Only 2 consecutive, not 3
58
+ });
59
+ });
60
+ describe("edit counting", () => {
61
+ it("should track edit count per file", () => {
62
+ client.recordToolCall("edit", "src/a.ts");
63
+ client.recordToolCall("edit", "src/a.ts");
64
+ client.recordToolCall("edit", "src/b.ts");
65
+ expect(client.getEditCount("src/a.ts")).toBe(2);
66
+ expect(client.getEditCount("src/b.ts")).toBe(1);
67
+ expect(client.getEditCount("src/c.ts")).toBe(0);
68
+ });
69
+ });
70
+ describe("formatWarnings", () => {
71
+ it("should format multiple warnings", () => {
72
+ const warnings = [
73
+ {
74
+ type: "blind-write",
75
+ message: "⚠ BLIND WRITE — editing file",
76
+ severity: "warning",
77
+ details: {},
78
+ },
79
+ {
80
+ type: "thrashing",
81
+ message: "🔴 THRASHING — 3 consecutive calls",
82
+ severity: "error",
83
+ details: {},
84
+ },
85
+ ];
86
+ const formatted = client.formatWarnings(warnings);
87
+ expect(formatted).toContain("BLIND WRITE");
88
+ expect(formatted).toContain("THRASHING");
89
+ });
90
+ it("should return empty string for no warnings", () => {
91
+ expect(client.formatWarnings([])).toBe("");
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,116 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { AgentBehaviorClient } from "./agent-behavior-client.js";
3
+
4
+ describe("AgentBehaviorClient", () => {
5
+ let client: AgentBehaviorClient;
6
+
7
+ beforeEach(() => {
8
+ client = new AgentBehaviorClient();
9
+ client.reset();
10
+ });
11
+
12
+ describe("blind write detection", () => {
13
+ it("should NOT warn when read precedes write", () => {
14
+ client.recordToolCall("read", "src/file.ts");
15
+ client.recordToolCall("edit", "src/file.ts");
16
+
17
+ const warnings = client.recordToolCall("write", "src/file.ts");
18
+ expect(warnings).toHaveLength(0);
19
+ });
20
+
21
+ it("should warn when multiple writes happen without reads", () => {
22
+ // First write is OK (no history)
23
+ client.recordToolCall("write", "src/file1.ts");
24
+
25
+ // Second write - still in window, accumulates
26
+ client.recordToolCall("edit", "src/file2.ts");
27
+
28
+ // Third write without any read - now we have a pattern
29
+ const warnings = client.recordToolCall("edit", "src/file3.ts");
30
+ expect(warnings).toHaveLength(1);
31
+ expect(warnings[0].type).toBe("blind-write");
32
+ });
33
+
34
+ it("should not warn for single write with no history", () => {
35
+ const warnings = client.recordToolCall("write", "src/file.ts");
36
+ expect(warnings).toHaveLength(0);
37
+ });
38
+ });
39
+
40
+ describe("thrashing detection", () => {
41
+ it("should warn after 3 consecutive identical tool calls", () => {
42
+ client.recordToolCall("bash", undefined);
43
+
44
+ // Second call - no warning yet
45
+ let warnings = client.recordToolCall("bash", undefined);
46
+ expect(warnings).toHaveLength(0);
47
+
48
+ // Third consecutive - should warn
49
+ warnings = client.recordToolCall("bash", undefined);
50
+ expect(warnings).toHaveLength(1);
51
+ expect(warnings[0].type).toBe("thrashing");
52
+ expect(warnings[0].details.callCount).toBe(3);
53
+ });
54
+
55
+ it("should NOT warn for different tool calls", () => {
56
+ client.recordToolCall("read", "src/file.ts");
57
+ client.recordToolCall("bash", "npm test");
58
+
59
+ const warnings = client.recordToolCall("edit", "src/file.ts");
60
+ expect(warnings).toHaveLength(0);
61
+ });
62
+
63
+ it("should reset count when tool changes", () => {
64
+ client.recordToolCall("bash", undefined);
65
+ client.recordToolCall("bash", undefined);
66
+
67
+ // Different tool resets the count
68
+ client.recordToolCall("read", "src/file.ts");
69
+
70
+ // Now start new consecutive sequence
71
+ client.recordToolCall("bash", undefined);
72
+
73
+ const warnings = client.recordToolCall("bash", undefined);
74
+ expect(warnings).toHaveLength(0); // Only 2 consecutive, not 3
75
+ });
76
+ });
77
+
78
+ describe("edit counting", () => {
79
+ it("should track edit count per file", () => {
80
+ client.recordToolCall("edit", "src/a.ts");
81
+ client.recordToolCall("edit", "src/a.ts");
82
+ client.recordToolCall("edit", "src/b.ts");
83
+
84
+ expect(client.getEditCount("src/a.ts")).toBe(2);
85
+ expect(client.getEditCount("src/b.ts")).toBe(1);
86
+ expect(client.getEditCount("src/c.ts")).toBe(0);
87
+ });
88
+ });
89
+
90
+ describe("formatWarnings", () => {
91
+ it("should format multiple warnings", () => {
92
+ const warnings = [
93
+ {
94
+ type: "blind-write" as const,
95
+ message: "⚠ BLIND WRITE — editing file",
96
+ severity: "warning" as const,
97
+ details: {},
98
+ },
99
+ {
100
+ type: "thrashing" as const,
101
+ message: "🔴 THRASHING — 3 consecutive calls",
102
+ severity: "error" as const,
103
+ details: {},
104
+ },
105
+ ];
106
+
107
+ const formatted = client.formatWarnings(warnings);
108
+ expect(formatted).toContain("BLIND WRITE");
109
+ expect(formatted).toContain("THRASHING");
110
+ });
111
+
112
+ it("should return empty string for no warnings", () => {
113
+ expect(client.formatWarnings([])).toBe("");
114
+ });
115
+ });
116
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * AgentBehaviorClient for pi-lens
3
+ *
4
+ * Tracks tool call sequences and flags anti-patterns in real-time:
5
+ * - Blind writes: editing or writing without reading first
6
+ * - Thrashing: repeated identical tool calls with no progress
7
+ *
8
+ * No external dependencies — purely tracks tool call history.
9
+ */
10
+
11
+ // --- Types ---
12
+
13
+ export type BehaviorWarning = {
14
+ type: "blind-write" | "thrashing";
15
+ message: string;
16
+ severity: "warning" | "error";
17
+ details: {
18
+ filePath?: string;
19
+ callCount?: number;
20
+ toolName?: string;
21
+ windowSize?: number;
22
+ };
23
+ };
24
+
25
+ interface ToolCallRecord {
26
+ toolName: string;
27
+ filePath?: string;
28
+ timestamp: number;
29
+ }
30
+
31
+ // --- Constants ---
32
+
33
+ const WRITE_OPS = new Set(["edit", "write", "multiedit"]);
34
+ const READ_OPS = new Set(["read", "bash", "grep", "glob", "find", "rg"]);
35
+
36
+ const BLIND_WRITE_WINDOW = 5; // Check last N tool calls for a read
37
+ const THRASH_THRESHOLD = 3; // Flag after N consecutive identical tools
38
+ const THRASH_TIMEOUT_MS = 30_000; // Reset thrash counter if gap > 30s
39
+
40
+ // --- Client ---
41
+
42
+ export class AgentBehaviorClient {
43
+ private toolHistory: ToolCallRecord[] = [];
44
+ private consecutiveCount = 0;
45
+ private lastToolName: string | null = null;
46
+ private lastToolTimestamp = 0;
47
+
48
+ // Per-file tracking
49
+ private fileEditCount = new Map<string, number>();
50
+
51
+ /**
52
+ * Record a tool call and return any warnings triggered.
53
+ * Called from tool_result handler.
54
+ */
55
+ recordToolCall(toolName: string, filePath?: string): BehaviorWarning[] {
56
+ const warnings: BehaviorWarning[] = [];
57
+ const now = Date.now();
58
+
59
+ // Track consecutive identical tools (thrashing)
60
+ if (
61
+ toolName === this.lastToolName &&
62
+ now - this.lastToolTimestamp < THRASH_TIMEOUT_MS
63
+ ) {
64
+ this.consecutiveCount++;
65
+ } else {
66
+ this.consecutiveCount = 1;
67
+ }
68
+ this.lastToolName = toolName;
69
+ this.lastToolTimestamp = now;
70
+
71
+ // Check for thrashing
72
+ if (this.consecutiveCount === THRASH_THRESHOLD) {
73
+ warnings.push({
74
+ type: "thrashing",
75
+ message: `🔴 THRASHING — ${THRASH_THRESHOLD} consecutive \`${toolName}\` calls with no other action. Consider fixing the root cause instead of re-running.`,
76
+ severity: "error",
77
+ details: {
78
+ toolName,
79
+ callCount: this.consecutiveCount,
80
+ },
81
+ });
82
+ }
83
+
84
+ // Check for blind writes
85
+ if (WRITE_OPS.has(toolName)) {
86
+ const recentWindow = this.toolHistory.slice(-BLIND_WRITE_WINDOW);
87
+ const hasRecentRead = recentWindow.some((r) => READ_OPS.has(r.toolName));
88
+
89
+ if (!hasRecentRead && recentWindow.length > 0) {
90
+ // Count how many writes in the window without reads
91
+ const writesWithoutRead = recentWindow.filter((r) =>
92
+ WRITE_OPS.has(r.toolName),
93
+ ).length;
94
+
95
+ if (writesWithoutRead >= 2) {
96
+ warnings.push({
97
+ type: "blind-write",
98
+ message: `⚠ BLIND WRITE — editing \`${filePath ?? "file"}\` without reading in the last ${BLIND_WRITE_WINDOW} tool calls. Read the file first to avoid assumptions.`,
99
+ severity: "warning",
100
+ details: {
101
+ filePath,
102
+ windowSize: recentWindow.length,
103
+ },
104
+ });
105
+ }
106
+ }
107
+
108
+ // Track edits per file
109
+ if (filePath) {
110
+ this.fileEditCount.set(
111
+ filePath,
112
+ (this.fileEditCount.get(filePath) ?? 0) + 1,
113
+ );
114
+ }
115
+ }
116
+
117
+ // Add to history (keep last 50 entries)
118
+ this.toolHistory.push({ toolName, filePath, timestamp: now });
119
+ if (this.toolHistory.length > 50) {
120
+ this.toolHistory = this.toolHistory.slice(-50);
121
+ }
122
+
123
+ return warnings;
124
+ }
125
+
126
+ /**
127
+ * Format warnings for LLM consumption.
128
+ */
129
+ formatWarnings(warnings: BehaviorWarning[]): string {
130
+ if (warnings.length === 0) return "";
131
+
132
+ return warnings.map((w) => w.message).join("\n");
133
+ }
134
+
135
+ /**
136
+ * Get edit count for a file in this session.
137
+ */
138
+ getEditCount(filePath: string): number {
139
+ return this.fileEditCount.get(filePath) ?? 0;
140
+ }
141
+
142
+ /**
143
+ * Reset state (e.g., on session start).
144
+ */
145
+ reset(): void {
146
+ this.toolHistory = [];
147
+ this.consecutiveCount = 0;
148
+ this.lastToolName = null;
149
+ this.lastToolTimestamp = 0;
150
+ this.fileEditCount.clear();
151
+ }
152
+ }
@@ -15,6 +15,7 @@ import { minimatch } from "minimatch";
15
15
  export class ArchitectClient {
16
16
  constructor(verbose = false) {
17
17
  this.config = null;
18
+ this.configPath = null;
18
19
  this.isUserConfig = false;
19
20
  this.log = verbose
20
21
  ? (msg) => console.error(`[architect] ${msg}`)
@@ -44,6 +44,7 @@ export interface FileArchitectResult {
44
44
 
45
45
  export class ArchitectClient {
46
46
  private config: ArchitectConfig | null = null;
47
+ private configPath: string | null = null;
47
48
  private isUserConfig: boolean = false;
48
49
  private log: (msg: string) => void;
49
50
 
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { AstGrepParser } from "./ast-grep-parser.js";
14
14
  import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
15
  import { SgRunner } from "./sg-runner.js";
16
- const getExtensionDir = () => {
16
+ const _getExtensionDir = () => {
17
17
  if (typeof __dirname !== "undefined") {
18
18
  return __dirname;
19
19
  }
@@ -107,7 +107,8 @@ message: found
107
107
  if (!name)
108
108
  continue;
109
109
  const signature = this.normalizeFunction(item.text);
110
- const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) + 1;
110
+ const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
111
+ 1;
111
112
  const group = grouped.get(signature) ?? [];
112
113
  group.push({ name, file: item.file, line });
113
114
  grouped.set(signature, group);
@@ -13,9 +13,9 @@ import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
14
  import { AstGrepParser } from "./ast-grep-parser.js";
15
15
  import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
16
- import { SgRunner, type SgMatch } from "./sg-runner.js";
16
+ import { type SgMatch, SgRunner } from "./sg-runner.js";
17
17
 
18
- const getExtensionDir = () => {
18
+ const _getExtensionDir = () => {
19
19
  if (typeof __dirname !== "undefined") {
20
20
  return __dirname;
21
21
  }
@@ -174,14 +174,19 @@ message: found
174
174
  pattern: string;
175
175
  functions: Array<{ name: string; file: string; line: number }>;
176
176
  }> {
177
- const grouped = new Map<string, Array<{ name: string; file: string; line: number }>>();
177
+ const grouped = new Map<
178
+ string,
179
+ Array<{ name: string; file: string; line: number }>
180
+ >();
178
181
 
179
182
  for (const item of matches) {
180
183
  const name = this.extractFunctionName(item.text);
181
184
  if (!name) continue;
182
185
 
183
186
  const signature = this.normalizeFunction(item.text);
184
- const line = (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) + 1;
187
+ const line =
188
+ (item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0) +
189
+ 1;
185
190
 
186
191
  const group = grouped.get(signature) ?? [];
187
192
  group.push({ name, file: item.file, line });
@@ -25,7 +25,11 @@ export function createAutoLoop(pi, config) {
25
25
  };
26
26
  const stop = (ctx, reason) => {
27
27
  const wasActive = state.active;
28
- state = { active: false, iteration: 0, maxIterations: config.maxIterations };
28
+ state = {
29
+ active: false,
30
+ iteration: 0,
31
+ maxIterations: config.maxIterations,
32
+ };
29
33
  updateStatus(ctx);
30
34
  if (wasActive) {
31
35
  ctx.ui.notify(`✅ ${config.name} loop ${reason}`, "info");