pi-lens 2.2.9 → 3.0.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 (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
package/index.ts CHANGED
@@ -8,21 +8,30 @@ import { AgentBehaviorClient } from "./clients/agent-behavior-client.js";
8
8
  import { ArchitectClient } from "./clients/architect-client.js";
9
9
  import { AstGrepClient } from "./clients/ast-grep-client.js";
10
10
  import { BiomeClient } from "./clients/biome-client.js";
11
+ import {
12
+ enableDebug as enableBusDebug,
13
+ FileModified,
14
+ initBusIntegration,
15
+ SessionStarted,
16
+ TurnEnded,
17
+ } from "./clients/bus/index.js";
11
18
  import { CacheManager } from "./clients/cache-manager.js";
12
19
  import { ComplexityClient } from "./clients/complexity-client.js";
13
20
  import { DependencyChecker } from "./clients/dependency-checker.js";
21
+ import { dispatchLintWithBus } from "./clients/dispatch/bus-dispatcher.js";
14
22
  import { dispatchLint } from "./clients/dispatch/integration.js";
23
+ import {
24
+ getFormatService,
25
+ resetFormatService,
26
+ } from "./clients/format-service.js";
15
27
  import { GoClient } from "./clients/go-client.js";
28
+ import { ensureTool } from "./clients/installer/index.js";
16
29
  import { buildInterviewer } from "./clients/interviewer.js";
17
30
  import { JscpdClient } from "./clients/jscpd-client.js";
18
31
  import { KnipClient } from "./clients/knip-client.js";
32
+ import { getLSPService, resetLSPService } from "./clients/lsp/index.js";
19
33
  import { MetricsClient } from "./clients/metrics-client.js";
20
- import {
21
- captureSnapshot,
22
- captureSnapshots,
23
- formatTrendCell,
24
- getTrendSummary,
25
- } from "./clients/metrics-history.js";
34
+ import { captureSnapshot } from "./clients/metrics-history.js";
26
35
  import { RuffClient } from "./clients/ruff-client.js";
27
36
  import {
28
37
  formatRulesForPrompt,
@@ -32,14 +41,13 @@ import {
32
41
  import { RustClient } from "./clients/rust-client.js";
33
42
  import { getSourceFiles } from "./clients/scan-utils.js";
34
43
  import { formatSecrets, scanForSecrets } from "./clients/secrets-scanner.js";
44
+ import { dispatchLintWithEffect } from "./clients/services/effect-integration.js";
35
45
  import { TestRunnerClient } from "./clients/test-runner-client.js";
36
46
  import { TodoScanner } from "./clients/todo-scanner.js";
37
47
  import { TypeCoverageClient } from "./clients/type-coverage-client.js";
38
48
  import { TypeScriptClient } from "./clients/typescript-client.js";
39
49
  import { handleBooboo } from "./commands/booboo.js";
40
- import { handleFix } from "./commands/fix.js";
41
- import { handleRate } from "./commands/rate.js";
42
- import { handleRefactor, initRefactorLoop } from "./commands/refactor.js";
50
+ import { initRefactorLoop } from "./commands/refactor.js";
43
51
 
44
52
  /** Parse a diff to extract modified line ranges in the new file.
45
53
  * Handles pi's custom diff format:
@@ -133,7 +141,7 @@ export default function (pi: ExtensionAPI) {
133
141
  const agentBehaviorClient = new AgentBehaviorClient();
134
142
  const cacheManager = new CacheManager();
135
143
 
136
- // --- Initialize auto-loops (must be early for event handlers) ---
144
+ // --- Initialize auto-loops ---
137
145
  initRefactorLoop(pi);
138
146
 
139
147
  // --- Flags ---
@@ -150,6 +158,12 @@ export default function (pi: ExtensionAPI) {
150
158
  default: false,
151
159
  });
152
160
 
161
+ pi.registerFlag("no-oxlint", {
162
+ description: "Disable Oxlint fast JS/TS linter",
163
+ type: "boolean",
164
+ default: false,
165
+ });
166
+
153
167
  pi.registerFlag("no-ast-grep", {
154
168
  description: "Disable ast-grep structural analysis",
155
169
  type: "boolean",
@@ -162,6 +176,12 @@ export default function (pi: ExtensionAPI) {
162
176
  default: false,
163
177
  });
164
178
 
179
+ pi.registerFlag("no-shellcheck", {
180
+ description: "Disable shellcheck for shell scripts",
181
+ type: "boolean",
182
+ default: false,
183
+ });
184
+
165
185
  pi.registerFlag("no-lsp", {
166
186
  description: "Disable TypeScript LSP",
167
187
  type: "boolean",
@@ -174,17 +194,32 @@ export default function (pi: ExtensionAPI) {
174
194
  default: false,
175
195
  });
176
196
 
177
- pi.registerFlag("autofix-biome", {
197
+ pi.registerFlag("no-autoformat", {
178
198
  description:
179
- "Auto-fix Biome lint/format issues on write (applies --write --unsafe)",
199
+ "Disable automatic formatting on file write (formatters run by default)",
180
200
  type: "boolean",
181
201
  default: false,
182
202
  });
183
203
 
184
- pi.registerFlag("autofix-ruff", {
185
- description: "Auto-fix Ruff lint/format issues on write",
204
+ pi.registerFlag("no-autofix", {
205
+ description:
206
+ "Disable auto-fixing of lint issues (Biome, Ruff). Use --no-autofix-biome or --no-autofix-ruff for individual control.",
186
207
  type: "boolean",
187
- default: true,
208
+ default: false,
209
+ });
210
+
211
+ pi.registerFlag("no-autofix-biome", {
212
+ description:
213
+ "Disable Biome auto-fix on write (Biome autofix is enabled by default)",
214
+ type: "boolean",
215
+ default: false,
216
+ });
217
+
218
+ pi.registerFlag("no-autofix-ruff", {
219
+ description:
220
+ "Disable Ruff auto-fix on write (Ruff autofix is enabled by default)",
221
+ type: "boolean",
222
+ default: false,
188
223
  });
189
224
 
190
225
  pi.registerFlag("no-tests", {
@@ -212,6 +247,39 @@ export default function (pi: ExtensionAPI) {
212
247
  default: false,
213
248
  });
214
249
 
250
+ // Internal flag for development/debugging (not surfaced to users)
251
+ pi.registerFlag("lens-bus", {
252
+ description: "[Internal] Enable event bus system",
253
+ type: "boolean",
254
+ default: false,
255
+ });
256
+
257
+ pi.registerFlag("lens-bus-debug", {
258
+ description: "Enable verbose bus event logging",
259
+ type: "boolean",
260
+ default: false,
261
+ });
262
+
263
+ pi.registerFlag("lens-effect", {
264
+ description: "Enable Effect-TS concurrent runner execution (Phase 2)",
265
+ type: "boolean",
266
+ default: false,
267
+ });
268
+
269
+ pi.registerFlag("lens-lsp", {
270
+ description:
271
+ "Enable LSP (Language Server Protocol) for semantic analysis (Phase 3)",
272
+ type: "boolean",
273
+ default: false,
274
+ });
275
+
276
+ pi.registerFlag("auto-install", {
277
+ description:
278
+ "Auto-install missing LSP servers without prompting (for Go, Rust, YAML, JSON, Bash)",
279
+ type: "boolean",
280
+ default: false,
281
+ });
282
+
215
283
  // --- Commands ---
216
284
 
217
285
  pi.registerCommand("lens-booboo", {
@@ -235,404 +303,7 @@ export default function (pi: ExtensionAPI) {
235
303
  ),
236
304
  });
237
305
 
238
- // --- Rule action map for lens-booboo-fix ---
239
- // Rules marked "skip" are architectural — they need deliberate user decisions.
240
- // They are excluded from inline tool_result hard stops (use /lens-refactor instead).
241
- const RULE_ACTIONS: Record<
242
- string,
243
- { type: "biome" | "agent" | "skip"; note: string }
244
- > = {
245
- "no-lonely-if": { type: "biome", note: "auto-fixed by Biome --write" },
246
- "empty-catch": {
247
- type: "agent",
248
- note: "Add this.log('Error: ' + err.message) to the catch block",
249
- },
250
- "no-console-log": {
251
- type: "agent",
252
- note: "Remove or replace with class logger method",
253
- },
254
- "no-debugger": { type: "agent", note: "Remove the debugger statement" },
255
- "no-return-await": {
256
- type: "agent",
257
- note: "Remove the unnecessary `return await`",
258
- },
259
- "nested-ternary": {
260
- type: "agent",
261
- note: "Extract to if/else or a named variable",
262
- },
263
- "no-throw-string": {
264
- type: "agent",
265
- note: "Wrap in `new Error(...)` instead of throwing a string",
266
- },
267
- "no-star-imports": {
268
- type: "skip",
269
- note: "Requires knowing which exports are actually used.",
270
- },
271
- "no-as-any": {
272
- type: "skip",
273
- note: "Replacing `as any` requires knowing the correct type.",
274
- },
275
- "no-non-null-assertion": {
276
- type: "skip",
277
- note: "Each `!` needs nullability analysis in context.",
278
- },
279
- "large-class": {
280
- type: "skip",
281
- note: "Splitting a class requires architectural decisions.",
282
- },
283
- "long-method": {
284
- type: "skip",
285
- note: "Extraction requires understanding the function's purpose.",
286
- },
287
- "long-parameter-list": {
288
- type: "skip",
289
- note: "Redesigning the signature requires an API decision.",
290
- },
291
- "no-shadow": {
292
- type: "skip",
293
- note: "Renaming requires understanding all variable scopes.",
294
- },
295
- "no-process-env": {
296
- type: "skip",
297
- note: "Using process.env directly makes code untestable. Use DI or a config module.",
298
- },
299
- "no-param-reassign": {
300
- type: "agent",
301
- note: "Create a new variable instead of reassigning the parameter.",
302
- },
303
- "no-single-char-var": {
304
- type: "skip",
305
- note: "Renaming requires understanding the variable's purpose.",
306
- },
307
- "switch-without-default": {
308
- type: "agent",
309
- note: "Add a default case to handle unexpected values.",
310
- },
311
- "no-architecture-violation": {
312
- type: "skip",
313
- note: "Layer boundary violations require architectural decisions.",
314
- },
315
- "switch-exhaustiveness": {
316
- type: "agent",
317
- note: "Add the missing case(s) or a default clause to handle all union values.",
318
- },
319
- };
320
-
321
- // Derived from RULE_ACTIONS — used to suppress architectural rules from inline hard stops.
322
- const SKIP_RULES = new Set(
323
- Object.entries(RULE_ACTIONS)
324
- .filter(([, v]) => v.type === "skip")
325
- .map(([k]) => k),
326
- );
327
-
328
- pi.registerCommand("lens-booboo-fix", {
329
- description:
330
- "Iterative fix loop: auto-fixes Biome/Ruff, then generates a per-issue plan for agent to execute. Run repeatedly until clean. Usage: /lens-booboo-fix [path] [--reset]",
331
- handler: (args, ctx) =>
332
- handleFix(
333
- args,
334
- ctx,
335
- {
336
- tsClient,
337
- astGrep: astGrepClient,
338
- ruff: ruffClient,
339
- biome: biomeClient,
340
- knip: knipClient,
341
- jscpd: jscpdClient,
342
- complexity: complexityClient,
343
- },
344
- pi,
345
- RULE_ACTIONS,
346
- ),
347
- });
348
-
349
- pi.registerCommand("lens-booboo-refactor", {
350
- description:
351
- "Interactive architectural refactor: scans for worst offender, opens a browser interview with options + recommendation, then steers the agent with your decision. Usage: /lens-booboo-refactor [path]",
352
- handler: (args, ctx) =>
353
- handleRefactor(
354
- args,
355
- ctx,
356
- {
357
- astGrep: astGrepClient,
358
- complexity: complexityClient,
359
- architect: architectClient,
360
- },
361
- pi,
362
- SKIP_RULES,
363
- RULE_ACTIONS,
364
- ),
365
- });
366
-
367
- pi.registerCommand("lens-metrics", {
368
- description:
369
- "Measure complexity metrics for all files and export to report.md. Usage: /lens-metrics [path]",
370
- handler: async (args, ctx) => {
371
- const targetPath = args.trim() || ctx.cwd || process.cwd();
372
- ctx.ui.notify("📊 Measuring code metrics...", "info");
373
-
374
- const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
375
- const timestamp = new Date()
376
- .toISOString()
377
- .replace(/[:.]/g, "-")
378
- .slice(0, 19);
379
- const projectName = path.basename(process.cwd());
380
-
381
- const results: import("./clients/complexity-client.js").FileComplexity[] =
382
- [];
383
-
384
- const isTsProject = nodeFs.existsSync(
385
- path.join(targetPath, "tsconfig.json"),
386
- );
387
- const files = getSourceFiles(targetPath, isTsProject);
388
- for (const fullPath of files) {
389
- if (complexityClient.isSupportedFile(fullPath)) {
390
- const metrics = complexityClient.analyzeFile(fullPath);
391
- if (metrics) {
392
- results.push(metrics);
393
- }
394
- }
395
- }
396
-
397
- if (results.length === 0) {
398
- ctx.ui.notify("No supported files found to analyze", "warning");
399
- return;
400
- }
401
-
402
- // Calculate aggregates
403
- const avgMI =
404
- results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
405
- results.length;
406
- const avgCognitive =
407
- results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
408
- const avgCyclomatic =
409
- results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
410
- results.length;
411
- const avgFunctionLength =
412
- results.reduce((a, b) => a + b.avgFunctionLength, 0) / results.length;
413
- const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
414
- const maxCognitive = Math.max(
415
- ...results.map((r) => r.cognitiveComplexity),
416
- );
417
- const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
418
- const totalFunctions = results.reduce((a, b) => a + b.functionCount, 0);
419
- const totalLOC = results.reduce((a, b) => a + b.linesOfCode, 0);
420
-
421
- // Grade distribution
422
- const grades = results.map((r) => {
423
- const mi = r.maintainabilityIndex;
424
- if (mi >= 80) return { letter: "A", color: "🟢" };
425
- if (mi >= 60) return { letter: "B", color: "🟡" };
426
- if (mi >= 40) return { letter: "C", color: "🟠" };
427
- if (mi >= 20) return { letter: "D", color: "🔴" };
428
- return { letter: "F", color: "⚫" };
429
- });
430
-
431
- const gradeCount = { A: 0, B: 0, C: 0, D: 0, F: 0 };
432
- for (const g of grades) {
433
- gradeCount[g.letter as keyof typeof gradeCount]++;
434
- }
435
-
436
- // Capture snapshots for history tracking
437
- const history = captureSnapshots(
438
- results.map((r) => ({
439
- filePath: r.filePath,
440
- metrics: {
441
- maintainabilityIndex: r.maintainabilityIndex,
442
- cognitiveComplexity: r.cognitiveComplexity,
443
- maxNestingDepth: r.maxNestingDepth,
444
- linesOfCode: r.linesOfCode,
445
- },
446
- })),
447
- );
448
-
449
- // Build report
450
- let report = `# Code Metrics Report: ${projectName}\n\n`;
451
- report += `**Generated:** ${new Date().toISOString()}\n\n`;
452
- report += `**Path:** \`${targetPath}\`\n\n`;
453
- report += `---\n\n`;
454
-
455
- // AI slop aggregates
456
- const totalAISlopWarnings = results.reduce((a, b) => {
457
- return a + complexityClient.checkThresholds(b).length;
458
- }, 0);
459
- const totalEmojiComments = results.reduce(
460
- (a, b) => a + b.aiCommentPatterns,
461
- 0,
462
- );
463
- const totalTryCatch = results.reduce((a, b) => a + b.tryCatchCount, 0);
464
- const totalSingleUse = results.reduce(
465
- (a, b) => a + b.singleUseFunctions,
466
- 0,
467
- );
468
- const maxParams = Math.max(...results.map((r) => r.maxParamsInFunction));
469
-
470
- // Summary
471
- report += `## Summary\n\n`;
472
- report += `| Metric | Value |\n`;
473
- report += `|--------|-------|\n`;
474
- report += `| Files Analyzed | ${results.length} |\n`;
475
- report += `| Total Functions | ${totalFunctions} |\n`;
476
- report += `| Total Lines of Code | ${totalLOC.toLocaleString()} |\n`;
477
- report += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
478
- report += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
479
- report += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
480
- report += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
481
- report += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
482
- report += `| Max Nesting Depth | ${maxNesting} |\n`;
483
- report += `| Avg Function Length | ${avgFunctionLength.toFixed(1)} lines |\n\n`;
484
-
485
- // AI Slop Summary
486
- report += `## AI Slop Indicators (Aggregate)\n\n`;
487
- report += `| Indicator | Count |\n`;
488
- report += `|-----------|-------|\n`;
489
- report += `| Total Warnings | ${totalAISlopWarnings} |\n`;
490
- report += `| Emoji/Boilerplate Comments | ${totalEmojiComments} |\n`;
491
- report += `| Try/Catch Blocks | ${totalTryCatch} |\n`;
492
- report += `| Single-Use Helper Functions | ${totalSingleUse} |\n`;
493
- report += `| Max Function Parameters | ${maxParams} |\n\n`;
494
-
495
- // Grade distribution
496
- report += `## Maintainability Grade Distribution\n\n`;
497
- report += `| Grade | Count | Percentage |\n`;
498
- report += `|-------|-------|------------|\n`;
499
- for (const [grade, count] of Object.entries(gradeCount)) {
500
- const pct = ((count / results.length) * 100).toFixed(1);
501
- const gradeIcons: Record<string, string> = {
502
- A: "🟢",
503
- B: "🟡",
504
- C: "🟠",
505
- D: "🔴",
506
- };
507
- const gradeThresholds: Record<string, number> = {
508
- A: 80,
509
- B: 60,
510
- C: 40,
511
- D: 20,
512
- };
513
- const icon = gradeIcons[grade] ?? "⚫";
514
- const threshold = gradeThresholds[grade] ?? 0;
515
- report += `| ${icon} ${grade} (MI ≥ ${threshold}) | ${count} | ${pct}% |\n`;
516
- }
517
- report += `\n`;
518
-
519
- // All files table (sorted by MI ascending)
520
- report += `## All Files\n\n`;
521
- report += `| Grade | File | MI | Cognitive | LOC | Entropy | Trend |\n`;
522
- report += `|-------|------|-----|-----------|-----|---------|-------|\n`;
523
-
524
- const sorted = [...results].sort(
525
- (a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
526
- );
527
- for (const f of sorted) {
528
- const mi = f.maintainabilityIndex;
529
- let grade: string;
530
- if (mi >= 80) grade = "🟢 A";
531
- else if (mi >= 60) grade = "🟡 B";
532
- else if (mi >= 40) grade = "🟠 C";
533
- else if (mi >= 20) grade = "🔴 D";
534
- else grade = "⚫ F";
535
-
536
- // Make path relative for readability
537
- const relPath = path.relative(targetPath, f.filePath);
538
- const trendCell = formatTrendCell(f.filePath, history);
539
- const entropyCell = f.codeEntropy > 0 ? f.codeEntropy.toFixed(2) : "—";
540
-
541
- report += `| ${grade} | ${relPath} | ${mi.toFixed(1)} | ${f.cognitiveComplexity} | ${f.linesOfCode} | ${entropyCell} | ${trendCell} |\n`;
542
- }
543
- report += `\n`;
544
-
545
- // Trend Summary
546
- const trendSummary = getTrendSummary(history);
547
- report += `## Trend Summary\n\n`;
548
- report += `| Trend | Count |\n`;
549
- report += `|-------|-------|\n`;
550
- report += `| 📈 Improving | ${trendSummary.improving} |\n`;
551
- report += `| ➡️ Stable | ${trendSummary.stable} |\n`;
552
- report += `| 📉 Regressing | ${trendSummary.regressing} |\n\n`;
553
-
554
- if (trendSummary.worstRegressions.length > 0) {
555
- report += `### Top Regressions\n\n`;
556
- report += `Files with largest MI decline since last scan:\n\n`;
557
- for (const r of trendSummary.worstRegressions) {
558
- report += `- **${r.file}**: MI ${r.miDelta > 0 ? "+" : ""}${r.miDelta}\n`;
559
- }
560
- report += `\n`;
561
- }
562
-
563
- // Top 10 worst files (actionable)
564
- report += `## Top 10 Files Needing Attention\n\n`;
565
- report += `These files have the lowest maintainability scores:\n\n`;
566
- for (let i = 0; i < Math.min(10, sorted.length); i++) {
567
- const f = sorted[i];
568
- const relPath = path.relative(targetPath, f.filePath);
569
- const warnings: string[] = [];
570
-
571
- if (f.maintainabilityIndex < 20) warnings.push("🔴 Critical: MI < 20");
572
- else if (f.maintainabilityIndex < 40) warnings.push("🟠 Low: MI < 40");
573
- if (f.cognitiveComplexity > 50)
574
- warnings.push(`High cognitive (${f.cognitiveComplexity})`);
575
- if (f.maxNestingDepth > 5)
576
- warnings.push(`Deep nesting (${f.maxNestingDepth})`);
577
- if (f.maxFunctionLength > 50)
578
- warnings.push(`Long functions (max ${f.maxFunctionLength})`);
579
-
580
- // AI slop indicators
581
- const slopWarnings = complexityClient.checkThresholds(f);
582
- for (const w of slopWarnings) {
583
- if (
584
- w.includes("AI-style") ||
585
- w.includes("try/catch") ||
586
- w.includes("single-use") ||
587
- w.includes("parameter list")
588
- ) {
589
- warnings.push(`🤖 ${w.split(" — ")[0]}`);
590
- }
591
- }
592
-
593
- report += `${i + 1}. **${relPath}** — MI: ${f.maintainabilityIndex.toFixed(1)}\n`;
594
- if (warnings.length > 0) {
595
- report += ` - ${warnings.join(", ")}\n`;
596
- }
597
- }
598
- report += `\n`;
599
-
600
- // Save report
601
- nodeFs.mkdirSync(reviewDir, { recursive: true });
602
-
603
- const reportPath = path.join(reviewDir, `metrics-${timestamp}.md`);
604
- nodeFs.writeFileSync(reportPath, report, "utf-8");
605
-
606
- // Also save latest.md for easy access
607
- const latestPath = path.join(reviewDir, "latest.md");
608
- nodeFs.writeFileSync(latestPath, report, "utf-8");
609
-
610
- // Console summary
611
- const summary = [
612
- `📊 Metrics Report`,
613
- ` ${results.length} files, ${totalLOC.toLocaleString()} LOC, ${totalFunctions} functions`,
614
- ` MI: ${avgMI.toFixed(1)} avg (${gradeCount.A}A ${gradeCount.B}B ${gradeCount.C}C ${gradeCount.D}D ${gradeCount.F}F)`,
615
- ` Cognitive: ${avgCognitive.toFixed(1)} avg, ${maxCognitive} max`,
616
- `📄 Saved: ${reportPath}`,
617
- ].join("\n");
618
-
619
- ctx.ui.notify(summary, "info");
620
- },
621
- });
622
-
623
- pi.registerCommand("lens-rate", {
624
- description:
625
- "Show code quality score with visual breakdown. Usage: /lens-rate [path]",
626
- handler: async (args, ctx) => {
627
- const result = await handleRate(args, ctx, {
628
- complexity: complexityClient,
629
- knip: knipClient,
630
- typeCoverage: typeCoverageClient,
631
- architect: architectClient,
632
- });
633
- ctx.ui.notify(result, "info");
634
- },
635
- });
306
+ // DISABLED: lens-booboo-fix command - disabled per user request
636
307
 
637
308
  pi.registerCommand("lens-tdi", {
638
309
  description:
@@ -914,13 +585,16 @@ export default function (pi: ExtensionAPI) {
914
585
  // Delta baselines: store pre-write diagnostics to diff against post-write
915
586
  const astGrepBaselines = new Map<
916
587
  string,
917
- import("./clients/ast-grep-client.js").AstGrepDiagnostic[]
588
+ import("./clients/ast-grep-types.js").AstGrepDiagnostic[]
918
589
  >();
919
590
  const biomeBaselines = new Map<
920
591
  string,
921
592
  import("./clients/biome-client.js").BiomeDiagnostic[]
922
593
  >();
923
594
 
595
+ // Track files already auto-fixed this turn to prevent fix loops
596
+ const fixedThisTurn = new Set<string>();
597
+
924
598
  // Project rules scan result (from .claude/rules, .agents/rules, etc.)
925
599
  let projectRulesScan: RuleScanResult = { rules: [], hasCustomRules: false };
926
600
 
@@ -930,6 +604,19 @@ export default function (pi: ExtensionAPI) {
930
604
  _verbose = !!pi.getFlag("lens-verbose");
931
605
  dbg("session_start fired");
932
606
 
607
+ // Initialize event bus system (Phase 1)
608
+ const busEnabled = pi.getFlag("lens-bus");
609
+ if (busEnabled) {
610
+ const busDebug = pi.getFlag("lens-bus-debug");
611
+ initBusIntegration(pi, { debug: !!busDebug });
612
+ if (busDebug) enableBusDebug(true);
613
+ SessionStarted.publish({
614
+ cwd: ctx.cwd ?? process.cwd(),
615
+ timestamp: Date.now(),
616
+ });
617
+ dbg("session_start: bus integration initialized");
618
+ }
619
+
933
620
  // Reset session state
934
621
  metricsClient.reset();
935
622
  complexityBaselines.clear();
@@ -948,6 +635,23 @@ export default function (pi: ExtensionAPI) {
948
635
  log(`Active tools: ${tools.join(", ")}`);
949
636
  dbg(`session_start tools: ${tools.join(", ")}`);
950
637
 
638
+ // Pre-install TypeScript LSP if --lens-lsp flag is set (avoid delay on first use)
639
+ if (pi.getFlag("lens-lsp")) {
640
+ dbg("session_start: pre-installing TypeScript LSP...");
641
+ // Fire-and-forget: don't block session start, just warm up the cache
642
+ ensureTool("typescript-language-server")
643
+ .then((path) => {
644
+ if (path) {
645
+ dbg(`session_start: TypeScript LSP ready at ${path}`);
646
+ } else {
647
+ console.error("[lens] TypeScript LSP installation failed");
648
+ }
649
+ })
650
+ .catch((err) => {
651
+ console.error("[lens] TypeScript LSP pre-install error:", err);
652
+ });
653
+ }
654
+
951
655
  const cwd = ctx.cwd ?? process.cwd();
952
656
  projectRoot = cwd; // Module-level for architect client
953
657
  dbg(`session_start cwd: ${cwd}`);
@@ -1160,10 +864,27 @@ export default function (pi: ExtensionAPI) {
1160
864
  if (/\.(ts|tsx|js|jsx)$/.test(filePath) && !pi.getFlag("no-lsp")) {
1161
865
  tsClient.updateFile(filePath, nodeFs.readFileSync(filePath, "utf-8"));
1162
866
  const diags = tsClient.getDiagnostics(filePath);
867
+ const fixes = tsClient.getAllCodeFixes(filePath);
1163
868
  if (diags.length > 0) {
1164
- hints.push(
1165
- `⚠ Pre-write: file already has ${diags.length} TypeScript error(s) — fix before adding more`,
1166
- );
869
+ const errorDiags = diags.filter((d) => d.severity === 1);
870
+ if (errorDiags.length > 0) {
871
+ hints.push(
872
+ `⚠ Pre-write: file has ${errorDiags.length} TypeScript error(s):`,
873
+ );
874
+ // Show first 3 errors with quick fixes
875
+ for (const d of errorDiags.slice(0, 3)) {
876
+ const lineFixes = fixes.get(d.range.start.line);
877
+ const fixHint = lineFixes?.[0]?.description
878
+ ? ` 💡 ${lineFixes[0].description}`
879
+ : "";
880
+ hints.push(
881
+ ` L${d.range.start.line + 1}: ${d.message.split("\n")[0].substring(0, 80)}${fixHint}`,
882
+ );
883
+ }
884
+ if (errorDiags.length > 3) {
885
+ hints.push(` ... and ${errorDiags.length - 3} more errors`);
886
+ }
887
+ }
1167
888
  }
1168
889
  }
1169
890
 
@@ -1195,17 +916,8 @@ export default function (pi: ExtensionAPI) {
1195
916
  );
1196
917
  }
1197
918
 
1198
- // Architectural rules pre-write hints
1199
- if (architectClient.hasConfig()) {
1200
- const relPath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
1201
- const archHints = architectClient.getHints(relPath);
1202
- if (archHints.length > 0) {
1203
- hints.push(`📐 Architectural rules for ${relPath}:`);
1204
- for (const h of archHints) {
1205
- hints.push(` → ${h}`);
1206
- }
1207
- }
1208
- }
919
+ // Architectural rules: Skip pre-write hints (too noisy)
920
+ // Post-write violations will be shown via architect runner in dispatch
1209
921
 
1210
922
  dbg(` pre-write hints: ${hints.length} — ${hints.join(" | ") || "none"}`);
1211
923
  if (hints.length > 0) {
@@ -1301,6 +1013,58 @@ export default function (pi: ExtensionAPI) {
1301
1013
  void err;
1302
1014
  }
1303
1015
 
1016
+ // --- Auto-format on write (default enabled) ---
1017
+ // Runs detected formatters concurrently via Effect-TS
1018
+ let formatChanged = false;
1019
+ if (!pi.getFlag("no-autoformat") && fileContent) {
1020
+ const formatService = getFormatService();
1021
+ try {
1022
+ // Record file read to establish FileTime baseline before formatting
1023
+ // This prevents "modified externally" false positives when agent writes file
1024
+ formatService.recordRead(filePath);
1025
+ const result = await formatService.formatFile(filePath);
1026
+ if (result.anyChanged) {
1027
+ formatChanged = true;
1028
+ dbg(
1029
+ `autoformat: ${result.formatters.map((f) => `${f.name}(${f.changed ? "changed" : "unchanged"})`).join(", ")}`,
1030
+ );
1031
+ // Re-read content after formatting for downstream processing
1032
+ fileContent = nodeFs.readFileSync(filePath, "utf-8");
1033
+ }
1034
+ } catch (err) {
1035
+ dbg(`autoformat error: ${err}`);
1036
+ }
1037
+ }
1038
+
1039
+ // --- Publish file modified event to bus (Phase 1) ---
1040
+ if (pi.getFlag("lens-bus")) {
1041
+ FileModified.publish({
1042
+ filePath,
1043
+ content: fileContent,
1044
+ changeType: event.toolName === "edit" ? "edit" : "write",
1045
+ });
1046
+ }
1047
+
1048
+ // --- LSP integration (Phase 3) ---
1049
+ if (pi.getFlag("lens-lsp") && fileContent) {
1050
+ const lspService = getLSPService();
1051
+ lspService
1052
+ .hasLSP(filePath)
1053
+ .then(async (hasLSP) => {
1054
+ if (hasLSP) {
1055
+ // Open or update file in LSP
1056
+ if (event.toolName === "write") {
1057
+ await lspService.openFile(filePath, fileContent);
1058
+ } else {
1059
+ await lspService.updateFile(filePath, fileContent);
1060
+ }
1061
+ }
1062
+ })
1063
+ .catch((err) => {
1064
+ dbg(`LSP error: ${err}`);
1065
+ });
1066
+ }
1067
+
1304
1068
  // --- Secrets scan (blocking - must check before other linting) ---
1305
1069
  if (fileContent) {
1306
1070
  const secretFindings = scanForSecrets(fileContent, filePath);
@@ -1318,14 +1082,142 @@ export default function (pi: ExtensionAPI) {
1318
1082
 
1319
1083
  let lspOutput = preHint ? `\n\n${preHint}` : "";
1320
1084
 
1085
+ // --- Auto-fix on write (safely - track to prevent loops) ---
1086
+ // Apply fixes BEFORE dispatch so dispatch only reports remaining issues
1087
+ // Autofix is enabled by default, use --no-autofix to disable
1088
+ const noAutofix = pi.getFlag("no-autofix");
1089
+ const noAutofixBiome = pi.getFlag("no-autofix-biome");
1090
+ const noAutofixRuff = pi.getFlag("no-autofix-ruff");
1091
+ let fixedCount = 0;
1092
+
1093
+ if (!fixedThisTurn.has(filePath) && !noAutofix) {
1094
+ // Python: Ruff auto-fix (enabled by default)
1095
+ if (
1096
+ !noAutofixRuff &&
1097
+ ruffClient.isAvailable() &&
1098
+ ruffClient.isPythonFile(filePath)
1099
+ ) {
1100
+ const result = ruffClient.fixFile(filePath);
1101
+ if (result.success && result.fixed > 0) {
1102
+ fixedCount += result.fixed;
1103
+ fixedThisTurn.add(filePath);
1104
+ dbg(`autofix: ruff fixed ${result.fixed} issue(s) in ${filePath}`);
1105
+ }
1106
+ }
1107
+
1108
+ // JS/TS/JSON: Biome auto-fix (enabled by default)
1109
+ if (
1110
+ !noAutofixBiome &&
1111
+ biomeClient.isAvailable() &&
1112
+ biomeClient.isSupportedFile(filePath)
1113
+ ) {
1114
+ const result = biomeClient.fixFile(filePath);
1115
+ if (result.success && result.fixed > 0) {
1116
+ fixedCount += result.fixed;
1117
+ fixedThisTurn.add(filePath);
1118
+ dbg(`autofix: biome fixed ${result.fixed} issue(s) in ${filePath}`);
1119
+ }
1120
+ }
1121
+ }
1122
+
1321
1123
  // --- Declarative dispatch: run all applicable lint tools ---
1322
1124
  // Phase 2: Replaced ~400 lines of if/else with unified dispatch system
1323
1125
  dbg(`dispatch: running lint tools for ${filePath}`);
1324
- const dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
1126
+
1127
+ // Select dispatcher based on flags:
1128
+ // - lens-effect: Effect-TS concurrent execution (Phase 2)
1129
+ // - lens-bus: Bus-enabled dispatcher (Phase 1)
1130
+ // - default: Original sequential dispatcher
1131
+ let dispatchOutput: string;
1132
+ if (pi.getFlag("lens-effect")) {
1133
+ dispatchOutput = await dispatchLintWithEffect(filePath, projectRoot, pi);
1134
+ } else if (pi.getFlag("lens-bus")) {
1135
+ dispatchOutput = await dispatchLintWithBus(filePath, projectRoot, pi);
1136
+ } else {
1137
+ dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
1138
+ }
1139
+
1325
1140
  if (dispatchOutput) {
1326
1141
  lspOutput += `\n\n${dispatchOutput}`;
1327
1142
  }
1328
1143
 
1144
+ // Report autofix results
1145
+ if (fixedCount > 0) {
1146
+ lspOutput += `\n\n✅ Auto-fixed ${fixedCount} issue(s) in ${path.basename(filePath)}`;
1147
+ }
1148
+
1149
+ // Warn agent if file was modified by auto-format or auto-fix
1150
+ // This ensures they know to re-read before next edit
1151
+ if (formatChanged || fixedCount > 0) {
1152
+ lspOutput += `\n\n⚠️ **File modified by auto-format/fix. Re-read before next edit.**`;
1153
+ }
1154
+
1155
+ // --- Test runner: run corresponding tests on write ---
1156
+ if (!pi.getFlag("no-tests")) {
1157
+ const testInfo = testRunnerClient.findTestFile(filePath, cwd);
1158
+ if (testInfo) {
1159
+ dbg(
1160
+ `test-runner: found test file ${testInfo.testFile} for ${filePath}`,
1161
+ );
1162
+ const detectedRunner = testRunnerClient.detectRunner(cwd);
1163
+ if (detectedRunner) {
1164
+ const testResult = testRunnerClient.runTestFile(
1165
+ testInfo.testFile,
1166
+ cwd,
1167
+ detectedRunner.runner,
1168
+ detectedRunner.config,
1169
+ );
1170
+ if (testResult && !testResult.error) {
1171
+ const testOutput = testRunnerClient.formatResult(testResult);
1172
+ if (testOutput) {
1173
+ lspOutput += `\n\n${testOutput}`;
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ // --- TypeScript Language Service diagnostics (post-write) ---
1181
+ // Fast semantic analysis for type errors, unused vars, etc.
1182
+ if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
1183
+ try {
1184
+ const tsClient = new TypeScriptClient();
1185
+
1186
+ // Track the file in the language service
1187
+ tsClient.addFile(filePath, fileContent || "");
1188
+
1189
+ // Get diagnostics (syntactic + semantic)
1190
+ const diagnostics = tsClient.getDiagnostics(filePath);
1191
+
1192
+ if (diagnostics.length > 0) {
1193
+ // Filter to most important diagnostics
1194
+ const importantDiags = diagnostics.filter((d) => {
1195
+ // Focus on errors and important warnings
1196
+ // Skip noisy ones like "cannot find name" from missing imports
1197
+ const code = (d as any).code;
1198
+ if (code === 2304) return false; // "Cannot find name"
1199
+ if (code === 2307) return false; // "Cannot find module"
1200
+ // DiagnosticSeverity.Error = 1, Warning = 2
1201
+ return d.severity === 1 || (d.severity === 2 && !code);
1202
+ });
1203
+
1204
+ if (importantDiags.length > 0) {
1205
+ const tsOutput = importantDiags
1206
+ .slice(0, 5)
1207
+ .map((d) => {
1208
+ // DiagnosticSeverity.Error = 1
1209
+ const severity = d.severity === 1 ? "🔴" : "🟡";
1210
+ return ` ${severity} [TS${(d as any).code}] ${d.message.split("\n")[0]}`;
1211
+ })
1212
+ .join("\n");
1213
+ lspOutput += `\n\n📐 TypeScript Diagnostics:\n${tsOutput}`;
1214
+ }
1215
+ }
1216
+ } catch (err) {
1217
+ dbg(`typescript-client error: ${err}`);
1218
+ }
1219
+ }
1220
+
1329
1221
  // Agent behavior warnings (blind writes, thrashing)
1330
1222
  if (behaviorWarnings.length > 0) {
1331
1223
  lspOutput += `\n\n${agentBehaviorClient.formatWarnings(behaviorWarnings)}`;
@@ -1354,6 +1246,15 @@ export default function (pi: ExtensionAPI) {
1354
1246
  const turnState = cacheManager.readTurnState(cwd);
1355
1247
  const files = Object.keys(turnState.files);
1356
1248
 
1249
+ // Publish turn ended event to bus (Phase 1)
1250
+ if (pi.getFlag("lens-bus") && files.length > 0) {
1251
+ TurnEnded.publish({
1252
+ cwd,
1253
+ modifiedFiles: files,
1254
+ timestamp: Date.now(),
1255
+ });
1256
+ }
1257
+
1357
1258
  if (files.length === 0) return;
1358
1259
 
1359
1260
  dbg(
@@ -1456,5 +1357,17 @@ export default function (pi: ExtensionAPI) {
1456
1357
  cwd,
1457
1358
  );
1458
1359
  }
1360
+
1361
+ // Clear fixed tracking so files can be fixed again on next turn
1362
+ fixedThisTurn.clear();
1363
+
1364
+ // --- LSP cleanup on turn end (Phase 3) ---
1365
+ // Only shutdown if no files are being actively edited
1366
+ if (pi.getFlag("lens-lsp") && files.length === 0) {
1367
+ resetLSPService();
1368
+ }
1369
+
1370
+ // --- Format service cleanup ---
1371
+ resetFormatService();
1459
1372
  });
1460
1373
  }