@herbertgao/pi-extensions 2026.8.5 → 2026.8.6

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 (155) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
  8. package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
  9. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
  10. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  11. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  12. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  13. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  14. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  15. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  16. package/node_modules/@narumitw/pi-btw/README.md +24 -17
  17. package/node_modules/@narumitw/pi-btw/package.json +5 -5
  18. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  19. package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
  20. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  21. package/node_modules/pi-lens/CHANGELOG.md +95 -0
  22. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  23. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  24. package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
  25. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  26. package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
  27. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  28. package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
  29. package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
  30. package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
  31. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
  32. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  33. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
  34. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  40. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  41. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  42. package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
  43. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  44. package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
  45. package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
  46. package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
  47. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
  48. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  49. package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
  50. package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
  51. package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
  52. package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
  53. package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
  54. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  55. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
  56. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  57. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
  58. package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
  59. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
  60. package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
  61. package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
  62. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  63. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  64. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  65. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
  66. package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
  67. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  68. package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
  69. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  70. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  71. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  72. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  73. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
  74. package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
  75. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
  76. package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
  77. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
  78. package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
  79. package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
  80. package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
  81. package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
  82. package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
  83. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  84. package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
  85. package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  87. package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
  88. package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
  89. package/node_modules/pi-lens/dist/index.js +4827 -1633
  90. package/node_modules/pi-lens/dist/mcp/server.js +8 -2
  91. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  92. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  93. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  94. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
  95. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
  96. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  97. package/node_modules/pi-lens/docs/agent-guide.md +38 -13
  98. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
  99. package/node_modules/pi-lens/docs/features.md +14 -1
  100. package/node_modules/pi-lens/docs/globalconfig.md +11 -0
  101. package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
  102. package/node_modules/pi-lens/docs/settings.md +6 -0
  103. package/node_modules/pi-lens/docs/usage.md +23 -5
  104. package/node_modules/pi-lens/docs/word-index.md +35 -0
  105. package/node_modules/pi-lens/package.json +1 -1
  106. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  107. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  108. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  109. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  110. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  111. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  112. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  113. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  114. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  115. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  116. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  117. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  118. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  119. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  120. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  121. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  122. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
  123. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  124. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  125. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  126. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  127. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  128. package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
  129. package/node_modules/pi-mcp-adapter/README.md +4 -1
  130. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  131. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  132. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  133. package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
  134. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  135. package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
  136. package/node_modules/pi-mcp-adapter/package.json +2 -1
  137. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  138. package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
  139. package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
  140. package/node_modules/pi-mcp-adapter/types.ts +93 -10
  141. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  142. package/node_modules/pi-web-access/README.md +18 -12
  143. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  144. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  145. package/node_modules/pi-web-access/curator-page.ts +5 -3
  146. package/node_modules/pi-web-access/curator-server.ts +2 -1
  147. package/node_modules/pi-web-access/extract.ts +106 -34
  148. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  149. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  150. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  151. package/node_modules/pi-web-access/index.ts +120 -48
  152. package/node_modules/pi-web-access/package.json +2 -2
  153. package/node_modules/pi-web-access/summary-review.ts +11 -5
  154. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  155. package/package.json +9 -9
@@ -29,6 +29,7 @@ import { acknowledgeTurnEnd, analyzeFile, analyzeFileFresh, canRebuildPiLens, cr
29
29
  import { createAstGrepReplaceTool } from "../tools/ast-grep-replace.js";
30
30
  import { createAstGrepSearchTool } from "../tools/ast-grep-search.js";
31
31
  import { createLensDiagnosticsTool } from "../tools/lens-diagnostics.js";
32
+ import { peekMcpSessionRuntime } from "../clients/mcp/session.js";
32
33
  import { createLspDiagnosticsTool } from "../tools/lsp-diagnostics.js";
33
34
  import { createLspNavigationTool } from "../tools/lsp-navigation.js";
34
35
  import { computeBuildStamp, STALE_SERVED_BY_FRESH, STALE_WARN_ONLY, StalenessGate, stalenessCheckEnabled, } from "./build-staleness.js";
@@ -411,7 +412,12 @@ const cacheManager = new CacheManager();
411
412
  // nothing — a no-op dressed as a fix, not a real parity gap. The 4th arg is left
412
413
  // at its default (`async () => {}`, already a no-op) rather than importing and
413
414
  // wiring a flush with nothing to flush.
414
- const lensDiagnosticsTool = createLensDiagnosticsTool(cacheManager, () => DEFAULT_CWD);
415
+ const lensDiagnosticsTool = createLensDiagnosticsTool(cacheManager, () => DEFAULT_CWD, undefined, undefined, undefined, undefined,
416
+ // #1413 surface parity: validate cached test-runner findings against the
417
+ // same session identity the in-process path uses. Resolved lazily — until
418
+ // an MCP session context exists there is no session to compare against and
419
+ // validation skips the check, which is the honest classification.
420
+ () => peekMcpSessionRuntime());
415
421
  const astGrepClient = new AstGrepClient();
416
422
  const astGrepSearchTool = createAstGrepSearchTool(astGrepClient);
417
423
  const astGrepReplaceTool = createAstGrepReplaceTool(astGrepClient);
@@ -544,7 +550,7 @@ const ALL_TOOLS = [
544
550
  properties: {
545
551
  query: {
546
552
  type: "string",
547
- description: "Identifier-ish query, e.g. 'authenticate user'.",
553
+ description: "Identifier-ish query, e.g. 'authenticate user'. Mix in composable prefix filters: lang:<kind> (e.g. lang:jsts, lang:python — kinds from file-kinds.ts), file:<substr> (path substring), ext:<ext> (e.g. ext:ts or ext:.ts), each optionally negated with a leading '-' (-file:test). Filters apply before ranking; e.g. 'lang:jsts file:clients/ -file:test rank'. Unknown prefixes/kinds error with the supported list.",
548
554
  },
549
555
  cwd: { type: "string" },
550
556
  limit: {
@@ -18,7 +18,7 @@
18
18
  * instead, so calling this tool is harmless, just unnecessary.
19
19
  */
20
20
  import { Type } from "../clients/deps/typebox.js";
21
- export function createActivateToolsTool(pi, lazyTools) {
21
+ export function createActivateToolsTool(pi, lazyTools, options = {}) {
22
22
  const lazyNames = lazyTools.map((t) => t.name);
23
23
  const lazyNameSet = new Set(lazyNames);
24
24
  const catalog = lazyTools.map((t) => `${t.name} — ${t.summary}`).join("\n");
@@ -35,7 +35,7 @@ export function createActivateToolsTool(pi, lazyTools) {
35
35
  description: "Names of situational tools to activate (see this tool's description for the catalog).",
36
36
  }),
37
37
  }),
38
- async execute(_toolCallId, params, _signal, _onUpdate) {
38
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
39
39
  const requested = Array.isArray(params.tools)
40
40
  ? params.tools.filter((t) => typeof t === "string" && lazyNameSet.has(t))
41
41
  : [];
@@ -53,10 +53,22 @@ export function createActivateToolsTool(pi, lazyTools) {
53
53
  }
54
54
  // Additive only, per the docs' contract: never drop currently active
55
55
  // tools in the same call.
56
+ // Remember every requested tool, not just the newly-added ones: a
57
+ // tool that is already active still has to survive the next
58
+ // fork/reload/resume restore.
59
+ options.onActivated?.(requested);
56
60
  const active = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
57
- const merged = [...new Set([...active, ...requested])];
58
- if (typeof pi.setActiveTools === "function") {
61
+ const activeSet = new Set(active);
62
+ const added = requested.filter((name) => !activeSet.has(name));
63
+ const merged = [...new Set([...active, ...added])];
64
+ if (added.length > 0 && typeof pi.setActiveTools === "function") {
59
65
  pi.setActiveTools(merged);
66
+ options.onMutation?.({
67
+ addedCount: added.length,
68
+ removedCount: 0,
69
+ reason: "lazy_activation",
70
+ deferralApplies: options.deferredToolSupport?.(ctx) ?? false,
71
+ });
60
72
  }
61
73
  return {
62
74
  content: [
@@ -65,7 +77,7 @@ export function createActivateToolsTool(pi, lazyTools) {
65
77
  text: `Activated: ${requested.join(", ")}. Available starting next turn.`,
66
78
  },
67
79
  ],
68
- details: { matches: requested, added: requested },
80
+ details: { matches: requested, added },
69
81
  };
70
82
  },
71
83
  };
@@ -67,7 +67,12 @@ export function createAstGrepReplaceTool(astGrepClient) {
67
67
  }),
68
68
  paths: Type.Optional(Type.Array(Type.String(), { description: "Specific files/folders" })),
69
69
  insideKind: Type.Optional(Type.String({ description: "Restrict matches to nodes inside an ancestor of this AST node kind. Synthesizes a YAML rule." })),
70
- hasKind: Type.Optional(Type.String({ description: "Restrict matches to nodes that contain a descendant of this AST node kind." })),
70
+ hasKind: Type.Optional(Type.String({
71
+ description: 'Restrict matches to nodes whose immediate child has this AST node kind (ast-grep default stopBy: neighbor). Example: `hasKind: "await_expression"`.',
72
+ })),
73
+ hasDescendantKind: Type.Optional(Type.String({
74
+ description: "Restrict matches to nodes containing this AST node kind anywhere in their descendants. Explicit recursive form (`stopBy: end`); use this instead of `hasKind` when nesting is not immediate.",
75
+ })),
71
76
  follows: Type.Optional(Type.String({ description: "Restrict matches to nodes that immediately follow a sibling matching this pattern." })),
72
77
  precedes: Type.Optional(Type.String({ description: "Restrict matches to nodes that immediately precede a sibling matching this pattern." })),
73
78
  apply: Type.Optional(Type.Boolean({ description: "Apply changes (default: false)" })),
@@ -78,7 +83,7 @@ export function createAstGrepReplaceTool(astGrepClient) {
78
83
  }),
79
84
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
80
85
  const startedAt = Date.now();
81
- const { pattern, rewrite, paths, apply, strictness, insideKind, hasKind, follows, precedes } = params;
86
+ const { pattern, rewrite, paths, apply, strictness, insideKind, hasKind, hasDescendantKind, follows, precedes } = params;
82
87
  const lang = (params.lang ?? "").replace(/^"|"$/g, "");
83
88
  const pathsCount = paths?.length ?? 1;
84
89
  const applyFlag = apply ?? false;
@@ -122,10 +127,10 @@ export function createAstGrepReplaceTool(astGrepClient) {
122
127
  }
123
128
  const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
124
129
  // Phase 3: structural-intent params → synthesize YAML with fix: field
125
- if (hasStructuralIntent({ insideKind, hasKind, follows, precedes })) {
130
+ if (hasStructuralIntent({ insideKind, hasKind, hasDescendantKind, follows, precedes })) {
126
131
  let ruleYaml;
127
132
  try {
128
- ruleYaml = synthesizeReplaceRule({ pattern, lang, rewrite, insideKind, hasKind, follows, precedes });
133
+ ruleYaml = synthesizeReplaceRule({ pattern, lang, rewrite, insideKind, hasKind, hasDescendantKind, follows, precedes });
129
134
  }
130
135
  catch (err) {
131
136
  logOutcome("error", { errorRaw: String(err) });
@@ -162,7 +162,10 @@ function verifyLine(content, absPath, tool, rule, message, callerLine) {
162
162
  };
163
163
  }
164
164
  }
165
- export function createLensDiagnosticMarkTool(getCwd) {
165
+ export function createLensDiagnosticMarkTool(getCwd,
166
+ /** Runtime telemetry identity, when known (#1448 class sweep) — attributed
167
+ * onto the disposition log alongside the mark. */
168
+ getIdentity) {
166
169
  return {
167
170
  name: "lens_diagnostic_mark",
168
171
  label: "Mark Diagnostic",
@@ -326,7 +329,7 @@ export function createLensDiagnosticMarkTool(getCwd) {
326
329
  message,
327
330
  line: verifiedLine,
328
331
  content,
329
- }, disposition, reason);
332
+ }, disposition, reason, getIdentity?.());
330
333
  const verb = disposition === "suppress"
331
334
  ? `suppressed (inline pi-lens-ignore comment written above line ${verifiedLine})`
332
335
  : disposition === "defer"
@@ -118,7 +118,7 @@ nextWriteIndex,
118
118
  // #798/#338: capture host UI methods synchronously from the active tool event.
119
119
  // The returned closure is safe for the later async sweep to invoke because it
120
120
  // never dereferences the session-guarded ctx.ui getter.
121
- captureLspStatusRepaint) {
121
+ captureLspStatusRepaint, getRuntime) {
122
122
  return {
123
123
  name: "lens_diagnostics",
124
124
  label: "Project Diagnostics",
@@ -293,6 +293,7 @@ captureLspStatusRepaint) {
293
293
  onServerReady: repaintLspStatus,
294
294
  pathsScope,
295
295
  nextWriteIndex,
296
+ runtime: getRuntime?.(),
296
297
  });
297
298
  }
298
299
  return formatDeltaMode(cacheManager, cwd, severity, pathsScope);
@@ -935,7 +936,7 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
935
936
  // scan, makes it genuinely concurrent — all three phases now race the SAME
936
937
  // signal from the same starting point instead of stacking.
937
938
  const analyzersPromise = shouldIncludeProjectRunners(options.refreshRunners)
938
- ? loadBootstrapClients().then((clients) => fetchFreshProjectDiagnostics(cacheManager, cwd, clients, signal))
939
+ ? loadBootstrapClients().then((clients) => fetchFreshProjectDiagnostics(cacheManager, cwd, clients, signal, { runtime: options.runtime }))
939
940
  : Promise.resolve({
940
941
  diagnostics: [],
941
942
  runners: [],
@@ -14,7 +14,7 @@ import { primaryServerId } from "../clients/lsp/config.js";
14
14
  import { combineAbortSignals, withDeadline } from "../clients/deadline-utils.js";
15
15
  import { applyAuxiliarySuppressions, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
16
16
  import { detectFileRole } from "../clients/file-role.js";
17
- import { hashDiagnosticContent, } from "../clients/lsp/diagnostic-binding.js";
17
+ import { hashDiagnosticContent, touchCompletedConfirmationPolicy, touchCoverageGap, } from "../clients/lsp/diagnostic-binding.js";
18
18
  import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
19
19
  import { attemptTsserverSyncDiagnostics, } from "../clients/lsp/tsserver-sync.js";
20
20
  import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
@@ -471,8 +471,15 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
471
471
  // confirmation: it carries the same caveat all-scope local
472
472
  // touches do, and classic TypeScript still needs the primary-only
473
473
  // tsserver sync check below rather than this verdict.
474
- confirmedByTouch: attached.response.confirmation === "confirmed" &&
474
+ // #1470: `"partial"` counts, for the same reason the local touch
475
+ // branch below accepts it — the incumbent's primary confirmed; only
476
+ // a named auxiliary did not.
477
+ confirmedByTouch: attached.response.confirmation !== undefined &&
475
478
  primaryServerId(absPath) !== "typescript",
479
+ // #1470: the incumbent's narrowed verdict, carried as an explicit DTO
480
+ // field across the socket. An older incumbent omits it → empty → the
481
+ // pre-#1470 handling, unchanged.
482
+ unconfirmedServerIds: attached.response.unconfirmedServerIds ?? [],
476
483
  content,
477
484
  };
478
485
  }
@@ -526,11 +533,20 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
526
533
  // one; the openFile-only / getDiagnostics fallback leaves it undefined →
527
534
  // "unknown", no demotion).
528
535
  const binding = usedTouch ? touched?.binding : undefined;
529
- const confirmedByTouch = usedTouch && touched?.confirmation === "confirmed";
536
+ // #1470: `"partial"` counts here. `confirmedByTouch` feeds
537
+ // `canTrustTouchConfirmation`, which asks about the PRIMARY's own verdict —
538
+ // and a partial touch is one whose primary confirmed while an auxiliary was
539
+ // cut off. Excluding it would render "Primary LSP: unconfirmed" for a primary
540
+ // that did confirm. The coverage gap is carried separately, below.
541
+ const confirmedByTouch = usedTouch && touchCompletedConfirmationPolicy(touched);
530
542
  return {
531
543
  diagnostics: filtered,
532
544
  timedOut,
533
545
  confirmedByTouch,
546
+ // #1470: only a touch actually contributes a coverage gap; the
547
+ // openFile+getDiagnostics fallback never reports one, which is honest —
548
+ // that path claims no confirmation at all.
549
+ unconfirmedServerIds: usedTouch ? touchCoverageGap(touched) : [],
534
550
  content,
535
551
  binding,
536
552
  };
@@ -733,7 +749,7 @@ cwd = process.cwd()) {
733
749
  };
734
750
  }
735
751
  }
736
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, content: collectedContent, binding, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
752
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
737
753
  const health = lspService.getDiagnosticsHealth?.(file);
738
754
  // #570: a timed-out priming check is never a confirmed "clean" — treat it
739
755
  // as unconfirmed without consulting the (unrelated) silent-tier
@@ -773,6 +789,14 @@ cwd = process.cwd()) {
773
789
  const boundMismatch = binding?.boundToCurrentDisk === false;
774
790
  if (boundMismatch)
775
791
  confirmation = "unconfirmed";
792
+ // #1470: an auxiliary cut off by the aux grace timer contributed no evidence
793
+ // about this file, so a "clean" verdict computed from the merged result would
794
+ // be claiming coverage this batch does not have. Demote it — that also keeps
795
+ // the entry out of the workspace cache below, which would otherwise replay the
796
+ // partial answer as a confirmed clean on every later sweep.
797
+ if (unconfirmedServerIds.length > 0 && confirmation === "clean") {
798
+ confirmation = "unconfirmed";
799
+ }
776
800
  const filteredDiags = applySeverityFilter(effectiveRawDiags, severity);
777
801
  reconcileWidgetFromLspResult(file, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
778
802
  // #671: only a CONFIRMED outcome ("clean", or a non-empty result — either
@@ -800,7 +824,7 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
800
824
  // Reserve the token before awaiting this file's LSP result. The direct-file
801
825
  // path performs its own confirmation/reconciliation below (#1198).
802
826
  const writeIndex = nextWriteIndex?.();
803
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, content: collectedContent, binding, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
827
+ const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, content: collectedContent, binding, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
804
828
  const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
805
829
  const unavailable = lspUnavailableMessage(absPath, lspHealth);
806
830
  // #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
@@ -842,6 +866,15 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
842
866
  const boundMismatch = binding?.boundToCurrentDisk === false;
843
867
  if (boundMismatch)
844
868
  confirmation = "unconfirmed";
869
+ // #1470: NARROWED, not collapsed. An auxiliary the aux grace timer cut off makes
870
+ // the FILE-level verdict unconfirmed — the merged result is missing whatever that
871
+ // scanner would have said, and this tool is the security lane's read surface. The
872
+ // PRIMARY's own verdict is untouched (`primaryCoverageGapOnly` below), so a
873
+ // TypeScript answer stays "confirmed clean" on its own line while an explicit
874
+ // line names the scanner whose coverage is absent.
875
+ const primaryCoverageGapOnly = unconfirmedServerIds.length > 0 && confirmation === "clean";
876
+ if (primaryCoverageGapOnly)
877
+ confirmation = "unconfirmed";
845
878
  const filtered = applySeverityFilter(effectiveRawDiags, severity);
846
879
  const total = filtered.length;
847
880
  const truncated = total > MAX_DIAGNOSTICS;
@@ -860,7 +893,11 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
860
893
  "file may still have errors that just hadn't been reported yet. " +
861
894
  "Re-check after the server settles, or increase waitMs.");
862
895
  }
863
- if (unconfirmed) {
896
+ // #1470: a file demoted ONLY because an auxiliary was cut off must not render
897
+ // the silent-on-clean text — the primary did confirm, and saying otherwise is
898
+ // the same overclaim in the opposite direction. The coverage line below names
899
+ // what is actually missing.
900
+ if (unconfirmed && !primaryCoverageGapOnly) {
864
901
  return (`Primary LSP${primaryId ? ` (${primaryId})` : ""}: unconfirmed — ` +
865
902
  "cannot confirm clean (push-only, silent-on-clean, e.g. classic " +
866
903
  "typescript-language-server never publishes on a clean re-check). " +
@@ -872,9 +909,20 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
872
909
  }
873
910
  return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: ${primaryDiags.length} diagnostic${primaryDiags.length === 1 ? "" : "s"}.`;
874
911
  })();
912
+ // #1470: this is the narrowing made readable. It states exactly which servers
913
+ // this result does NOT speak for, so "no auxiliary findings" can never be read
914
+ // as "the security scanners found nothing".
915
+ const coverageLine = unconfirmedServerIds.length > 0
916
+ ? `Auxiliary coverage INCOMPLETE — ${[...unconfirmedServerIds].join(", ")} did not answer within the wait budget, so ${unconfirmedServerIds.length === 1 ? "its findings are" : "their findings are"} NOT included here. This is not a clean bill of health for ${unconfirmedServerIds.length === 1 ? "that scanner" : "those scanners"}; re-check after the next edit, or use waitMs to wait longer.`
917
+ : undefined;
875
918
  let text;
876
919
  if (total === 0) {
877
- text = [primaryLine, "", unavailable ?? "No auxiliary findings."].join("\n");
920
+ text = [
921
+ primaryLine,
922
+ "",
923
+ unavailable ?? "No auxiliary findings.",
924
+ ...(coverageLine ? [coverageLine] : []),
925
+ ].join("\n");
878
926
  }
879
927
  else {
880
928
  const lines = [primaryLine, ""];
@@ -885,6 +933,8 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
885
933
  lines.push(`Auxiliary findings (${auxiliaryDiags.length}):`);
886
934
  lines.push(...auxiliaryDiags.map(formatDiag));
887
935
  }
936
+ if (coverageLine)
937
+ lines.push("", coverageLine);
888
938
  if (unavailable)
889
939
  lines.unshift(unavailable, "");
890
940
  if (truncated) {
@@ -914,6 +964,11 @@ async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWri
914
964
  truncated,
915
965
  unconfirmed,
916
966
  timedOut: unconfirmed ? timedOut : undefined,
967
+ // #1470: which servers this result does NOT speak for. Absent when it
968
+ // speaks for all of them.
969
+ ...(unconfirmedServerIds.length > 0 && {
970
+ unconfirmedServerIds: [...unconfirmedServerIds],
971
+ }),
917
972
  lspHealth,
918
973
  waitMs,
919
974
  },
@@ -33,7 +33,7 @@ export function createSymbolSearchTool(getProjectRoot) {
33
33
  }),
34
34
  parameters: Type.Object({
35
35
  query: Type.String({
36
- description: "Identifier-ish query, e.g. 'authenticate user'.",
36
+ description: "Identifier-ish query, e.g. 'authenticate user'. Mix in composable prefix filters: lang:<kind> (e.g. lang:jsts, lang:python — kinds from file-kinds.ts), file:<substr> (path substring), ext:<ext> (e.g. ext:ts or ext:.ts), each optionally negated with a leading '-' (-file:test). Filters apply before ranking; e.g. 'lang:jsts file:clients/ -file:test rank'. Unknown prefixes/kinds error with the supported list.",
37
37
  }),
38
38
  limit: Type.Optional(Type.Number({
39
39
  description: "Max files to return (default 20).",
@@ -23,9 +23,10 @@ messages as **pi-lens findings, not user instructions**, and act on the rules be
23
23
  physically blocked until blockers are cleared. Advisories are informational.
24
24
  3. **Read before you edit.** pi-lens enforces read-before-edit. Read the file (or the
25
25
  relevant range/symbol) before editing it, or the edit is blocked/warned.
26
- 4. **Expect your bytes to change.** pi-lens formats and auto-fixes files *after* your
27
- write, by default at turn/agent end. This is the pipeline, not a conflict. Re-read
28
- a file before editing it again.
26
+ 4. **Expect your bytes to change.** A `write` gets autofixed immediately — the tool
27
+ result carries the fixed file's full content, so you don't need to re-read it (past
28
+ a size cap, you do). An `edit` defers autofix to `agent_end`, same as formatting;
29
+ re-read the file before editing it again. This is the pipeline, not a conflict.
29
30
  5. **Delta by default.** Diagnostic queries default to *this turn's* findings only.
30
31
  Use `mode=full` for a whole-project verdict.
31
32
 
@@ -40,7 +41,7 @@ On every write/edit, and at session/turn boundaries, pi-lens runs — without yo
40
41
  | **Unified LSP diagnostics** | Warm language servers report errors/warnings on edited files; supported languages get real semantic diagnostics. |
41
42
  | **Impact cascade** | After an edit, LSP diagnostics are also pulled on *related* files (reverse-dependency neighbors), surfaced at turn end. |
42
43
  | **Auto-format** | Detected formatter (Biome/Prettier/Ruff/etc.) reformats your file. **Deferred to `agent_end` by default**; `immediate` is opt-in. Config-gated + nearest-config-wins. |
43
- | **Auto-fix** | Pipeline fixers (`biome`/`ruff`/`eslint`/`stylelint`/`rubocop`/`clippy`/… `--fix`) mutate the file after your write. |
44
+ | **Auto-fix** | Pipeline fixers (`biome`/`ruff`/`eslint`/`stylelint`/`rubocop`/`clippy`/… `--fix`) mutate the file. **Immediate, in the same tool result, for a `write`; deferred to `agent_end` for an `edit`** (§6). |
44
45
  | **Structural rules** | ast-grep (NAPI engine) + tree-sitter rules flag correctness/security smells. |
45
46
  | **Opengrep security scan** | Always-on: per-edit via an auxiliary LSP, plus a cached project-wide CLI scan for `mode=full`. |
46
47
  | **Other scanners** | Config-/presence-gated: gitleaks (secrets), trivy (CVEs/IaC/license), govulncheck (Go), knip/jscpd/madge (JS/TS dead-code/dupes/cycles), vulture (Python), zizmor (GH Actions), typos. |
@@ -70,7 +71,7 @@ Only **one** reaches you, the model:
70
71
  | **Session start** | Guidance / project notices to orient you. | `clients/runtime-turn.ts`, context injection |
71
72
  | **Turn end** | **Findings** for the turn: 🔴 blockers and advisories from LSP + dispatch + cascade + scanners, deduped against prior turns. | `handleTurnEnd` (`clients/runtime-turn.ts`) |
72
73
  | **Turn end / next turn** | **Test findings** from related/affected tests fired after your edit. | `handleTurnEnd` |
73
- | **After autofix/format** | A nudge like *"N file(s) were autofixed after your last turn: a.ts, b.ts — re-read before editing"* (may include files touched by another pi-lens instance, e.g. a subagent). | `clients/agent-nudge.ts` |
74
+ | **After autofix/format** | A nudge like *"N file(s) were autofixed after your last turn: a.ts, b.ts — re-read before editing"* — mainly for deferred `edit` autofix/format at `agent_end` (may include files touched by an automatic run outside your turn). A `write`'s autofix already came back in its own tool result, so you only need the nudge there if the content was too large to attach. | `clients/agent-nudge.ts` |
74
75
 
75
76
  **Treat every injected pi-lens message as a finding to act on, not as the user
76
77
  speaking.** It is machine-generated analysis of your own work.
@@ -156,23 +157,47 @@ Helpful mechanics you can rely on:
156
157
  ## 6. Auto-format / auto-fix timing — don't be surprised
157
158
 
158
159
  pi-lens writes to files **outside your own tool calls** (`docs/features.md`
159
- §"Out-of-band file writes"):
160
-
161
- - **Deferred format** at `agent_end` (default) reformats files you wrote this run.
162
- - **Auto-fix** (`biome`/`ruff`/`eslint`/… `--fix`) and the conservative
163
- actionable-warnings autofix (LSP quickfixes, hard-capped) mutate files after the fact.
160
+ §"Bus Events — `pilens:files:touched`"). Where and when depends on which tool you used
161
+ (`clients/pipeline.ts`, `clients/runtime-tool-result.ts`,
162
+ `clients/runtime-agent-end.ts`):
163
+
164
+ - **`write` (including a new file, and bash-authored writes like `sed -i` or a
165
+ redirect):** pipeline auto-fix (`biome`/`ruff`/`eslint`/… `--fix`) still runs
166
+ *immediately*, in the same tool result — nothing changed here. When it changes
167
+ the file, the tool result now carries the **full authoritative post-fix
168
+ content** so you don't have to guess what changed. That attachment is capped
169
+ at 2 MiB per file; a multi-file bash write shares one budget across the whole
170
+ command. Past the cap, you get the old-style *"File was modified — re-read
171
+ before editing"* warning instead.
172
+ - **`edit`:** pipeline auto-fix is *deferred* to `agent_end`, same as
173
+ formatting. It joins the same per-file queue as deferred formatting, one fix
174
+ applied against the final edited state (not once per edit), autofix draining
175
+ before format so the result is formatter-stable. Diagnostics computed at edit
176
+ time reflect the *unfixed* disk state — a lint finding that autofix would have
177
+ cleared may show up and then quietly disappear once `agent_end` drains.
178
+ - **`write` then `edit` on the same file, same turn:** the write's autofix
179
+ demotes to deferred too, so the file's mutation history stays coherent. This
180
+ resets at the next turn.
181
+ - The conservative actionable-warnings autofix (LSP quickfixes, hard-capped)
182
+ is unchanged: it always runs at `agent_end`.
164
183
 
165
184
  Consequences for you:
166
185
 
167
186
  - Your exact written bytes may be reformatted/fixed. **This is expected pipeline
168
187
  behavior, not a conflict or a failed write.**
169
- - A file you wrote last turn may have changed on disk. **Re-read before editing it
170
- again** (this also keeps the read-guard happy — the autofix nudge tells you which
171
- files changed).
188
+ - **`write`:** trust the authoritative content attached to the tool result; only
189
+ re-read if you see the size-cap warning, or if a *different* file (a side
190
+ effect of the same bash command) was touched.
191
+ - **`edit`:** the file may change on disk after your turn ends. **Re-read before
192
+ editing it again** (this also keeps the read-guard happy — the autofix nudge
193
+ tells you which files changed).
172
194
  - **Delta mode:** `lens_diagnostics` shows only diagnostics *introduced this turn* by
173
195
  default (`mode=delta`). Use `mode=all` (cache-wide) or `mode=full` (fresh scan) for
174
196
  the complete picture.
175
197
 
198
+ See `AGENTS.md`'s "Per-edit autofix mutation boundary (#1414)" invariant for the
199
+ authoritative routing rules.
200
+
176
201
  ---
177
202
 
178
203
  ## 7. Tools & commands you can use
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- GENERATED by scripts/gen-rule-catalogs.mjs — do not edit by hand. Run `npm run docs:rule-catalogs` after changing rules. -->
4
4
 
5
- pi-lens bundles **438 enabled** ast-grep rules (254 pi-lens-authored + 184 vendored from CodeRabbit) across **16 languages**, plus 16 disabled.
5
+ pi-lens bundles **446 enabled** ast-grep rules (262 pi-lens-authored + 184 vendored from CodeRabbit) across **16 languages**, plus 16 disabled.
6
6
 
7
7
  See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `rules/ast-grep-rules/rules/` (pi-lens), `rules/ast-grep-rules/coderabbit/rules/` (vendored).
8
8
 
@@ -133,7 +133,7 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
133
133
  | `use-of-sha1-java` | warning | CodeRabbit | Detected SHA1 hash algorithm which is considered insecure. SHA1 is not collision resistant and is therefore not suitable as a cryptographic… |
134
134
  | `weak-ssl-context-java` | warning | CodeRabbit | 'An insecure SSL context was detected. TLS versions 1.0, 1.1, and all SSL versions are considered weak encryption and are deprecated. Use S… |
135
135
 
136
- ### JavaScript (76)
136
+ ### JavaScript (79)
137
137
 
138
138
  | Rule | Severity | Source | Description |
139
139
  |---|---|---|---|
@@ -161,6 +161,7 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
161
161
  | `no-case-declarations-js` | error | pi-lens | Unexpected lexical declaration in case block — wrap in braces to restrict scope |
162
162
  | `no-compare-neg-zero-js` | error | pi-lens | Disallow comparing against -0 |
163
163
  | `no-cond-assign-js` | error | pi-lens | Expected a conditional expression and instead saw an assignment. |
164
+ | `no-conditional-empty-object-spread-js` | hint | pi-lens | Conditional empty-object spread — use an explicit if/else to omit fields |
164
165
  | `no-console-except-error-js` | warning | pi-lens | Avoid console.log/debug/warn in production code — use a logger or console.error inside catch only |
165
166
  | `no-constant-condition-js` | error | pi-lens | Unexpected constant condition |
166
167
  | `no-discarded-error-js` | error | pi-lens | new Error() result discarded — add 'throw' or assign to a variable |
@@ -180,6 +181,8 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
180
181
  | `no-new-wrappers-js` | error | pi-lens | Do not use 'new' with wrapper objects — use literal primitives instead |
181
182
  | `no-open-redirect-js` | error | pi-lens | Potential open redirect vulnerability — validate redirect URLs |
182
183
  | `no-prototype-builtins-js` | error | pi-lens | Use Object.hasOwn() or Object.prototype.hasOwnProperty.call() instead of calling methods directly on the object |
184
+ | `no-reflect-apply-js` | warning | pi-lens | Reflect.apply() — use a typed call (fn.apply(...) or fn(...args)) instead |
185
+ | `no-reflect-get-js` | warning | pi-lens | Reflect.get(obj, key) — use a typed property access (obj.prop or obj[key]) instead |
183
186
  | `no-single-promise-in-promise-methods-js` | warning | pi-lens | Promise.all/race with a single promise is unnecessary — await it directly |
184
187
  | `no-sql-in-code-js` | error | pi-lens | Raw SQL string in code — use query builder or ORM |
185
188
  | `no-throw-string-js` | error | pi-lens | Throw Error objects, not strings |
@@ -401,7 +404,7 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
401
404
  | `redundant-usestate-type` | info | pi-lens | `useState<TYPE>(...)` where TYPE is a primitive redundant — TS can infer it from the initial value |
402
405
  | `unnecessary-react-hook` | warning | pi-lens | Function with a `use*` name does not call any other React hook — rename to a plain function |
403
406
 
404
- ### TypeScript (120)
407
+ ### TypeScript (125)
405
408
 
406
409
  | Rule | Severity | Source | Description |
407
410
  |---|---|---|---|
@@ -431,8 +434,10 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
431
434
  | `no-await-in-promise-methods` | warning | pi-lens | Do not await inside Promise.all/allSettled — pass the promises directly |
432
435
  | `no-bare-host-path-in-win32-branch` | warning | pi-lens | Use win32 path operations inside a branch committed to Windows path semantics |
433
436
  | `no-case-declarations` | error | pi-lens | Unexpected lexical declaration in case block — wrap in braces to restrict scope |
437
+ | `no-chained-type-assertions` | warning | pi-lens | Chained type assertion (x as A as B) — stacking assertions with no runtime check between them |
434
438
  | `no-compare-neg-zero` | error | pi-lens | Disallow comparing against -0 |
435
439
  | `no-cond-assign` | error | pi-lens | Expected a conditional expression and instead saw an assignment. |
440
+ | `no-conditional-empty-object-spread` | hint | pi-lens | Conditional empty-object spread — use an explicit if/else to omit fields |
436
441
  | `no-console-except-error` | warning | pi-lens | Avoid console.log/debug/warn in production code — use a logger or console.error inside catch only |
437
442
  | `no-constant-condition` | error | pi-lens | Unexpected constant condition |
438
443
  | `no-demeter-message-chain` | warning | pi-lens | Message chain (Law of Demeter) — navigating 3+ deep through accessors couples the caller to the whole path. |
@@ -458,6 +463,8 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
458
463
  | `no-prototype-builtins` | error | pi-lens | Use Object.hasOwn() or Object.prototype.hasOwnProperty.call() instead of calling methods directly on the object |
459
464
  | `no-prototype-builtins-native` | hint | pi-lens | Use Object.hasOwn() instead of hasOwnProperty() — ES2022 standard |
460
465
  | `no-raw-json-store-write` | warning | pi-lens | Use the atomic write seam for JSON stores instead of writeFileSync |
466
+ | `no-reflect-apply` | warning | pi-lens | Reflect.apply() — use a typed call (fn.apply(...) or fn(...args)) instead |
467
+ | `no-reflect-get` | warning | pi-lens | Reflect.get(obj, key) — use a typed property access (obj.prop or obj[key]) instead |
461
468
  | `no-relative-cross-package-import` | info | pi-lens | Avoid relative imports across package boundaries (../../). Use absolute imports (@app/..., @components/...) for better agent reasoning abou… |
462
469
  | `no-return-await` | warning | pi-lens | Unnecessary 'await' in return statement — use direct return instead |
463
470
  | `no-single-promise-in-promise-methods` | warning | pi-lens | Promise.all/race with a single promise is unnecessary — await it directly |
@@ -466,6 +473,7 @@ See [`docs/custom-rules.md`](custom-rules.md) to add your own. Rule sources: `ru
466
473
  | `no-throw-string` | error | pi-lens | Throw Error objects, not strings |
467
474
  | `no-typeof-undefined` | hint | pi-lens | Use === undefined instead of typeof x === 'undefined' |
468
475
  | `no-unimplemented-stub` | warning | pi-lens | Unimplemented stub — function was scaffolded but never completed |
476
+ | `no-unknown-laundering` | warning | pi-lens | type alias to bare 'unknown' — give the alias real shape or inline 'unknown' at the one call site that needs it |
469
477
  | `no-unnecessary-array-flat-depth` | hint | pi-lens | .flat(1) is the default depth — just use .flat() |
470
478
  | `no-useless-length-check` | warning | pi-lens | Use .some() instead of .filter().length check — short-circuits at first match |
471
479
  | `no-useless-promise-resolve-reject` | warning | pi-lens | Unnecessary Promise.resolve/reject wrapping — return the value directly |
@@ -40,6 +40,19 @@ Repositories can disable all immediate and deferred auto-format mutations with
40
40
  keeping formatter detection, lint dispatch, LSP synchronization, and
41
41
  diagnostics available.
42
42
 
43
+ **Auto-fix timing depends on the tool.** A `write` (new file, full overwrite,
44
+ or a bash-authored write like `sed -i`/a redirect) still gets pipeline autofix
45
+ immediately, in the same tool result; when it changes the file, the result
46
+ carries the full authoritative post-fix content (capped at 2 MiB per file, one
47
+ shared budget across a multi-file bash write — past that it degrades to a
48
+ re-read warning). An `edit` defers autofix to `agent_end`, where it joins the
49
+ same per-file deferred-mutation queue as deferred formatting — one coalesced
50
+ record per file (`kinds: {autofix, format}`), autofix draining before format
51
+ so the final state is formatter-stable. A `write` immediately followed by an
52
+ `edit` on the same file in the same turn demotes the write's autofix to
53
+ deferred too. See `clients/pipeline.ts`, `clients/runtime-tool-result.ts`, and
54
+ `clients/runtime-agent-end.ts`.
55
+
43
56
  Deferred formatting (the `agent_end` default) runs with **bounded
44
57
  concurrency**: at most three formatter subprocesses in flight at once, with
45
58
  results applied in admission order and cooperative yields between files, so a
@@ -155,7 +168,7 @@ When `actionableWarnings.autoFix.enabled` is set in global or project config (or
155
168
 
156
169
  ### Bus Events — `pilens:files:touched` (#482)
157
170
 
158
- pi-lens writes files **outside the agent's own tool calls**: dispatch autofix (biome/ruff/eslint/stylelint/sqlfluff/rubocop/ktlint/rust-clippy/dart-fix/golangci-lint/detekt/ktfmt/markdownlint/oxlint --fix) and formatter runs (immediate or deferred-at-`agent_end`) both mutate files after the fact, and the conservative actionable-warnings autofix above applies LSP quickfixes the same way. Other extensions in the same session that track file mutations are otherwise blind to those writes.
171
+ pi-lens writes files **outside the agent's own tool calls**: dispatch autofix (biome/ruff/eslint/stylelint/sqlfluff/rubocop/ktlint/ktfmt/rust-clippy/dart-fix/golangci-lint/detekt/markdownlint/oxlint --fix) mutates the file immediately for a `write` and at `agent_end` for a deferred `edit` (see "Formatters" above); formatter runs (immediate or deferred-at-`agent_end`) do the same; and the conservative actionable-warnings autofix above applies LSP quickfixes at `agent_end` the same way. Other extensions in the same session that track file mutations are otherwise blind to those writes — this event, published either way, is how they find out.
159
172
 
160
173
  pi-lens broadcasts them on pi's shared in-process event bus (`pi.events`, exposed to every extension via the `ExtensionAPI`) as a single named event:
161
174
 
@@ -25,6 +25,17 @@ Each runtime toggle is settable from the CLI *and* from `config.json`. The two a
25
25
  | `--lens-actionable-warning-autofix` | `actionableWarnings.autoFix.enabled` | `false` |
26
26
  | `--lens-actionable-warning-all` | `actionableWarnings.deltaOnly` (`false`) | `true` |
27
27
  | `--lens-compact-tool-line` | `ui.compactToolLine` | `false` |
28
+ | `--no-lazy-tools` | `tools.lazy` | `true` |
29
+
30
+ By default pi-lens registers six situational tools (the `ast_grep_*` family,
31
+ `lsp_navigation`, `lens_diagnostic_mark`) inactive and exposes a small loader,
32
+ `pi_lens_activate_tools`, that the model calls to activate the ones it needs.
33
+ `--no-lazy-tools` turns that off: every pi-lens tool is active from the first
34
+ turn and pi-lens never changes the tool list. Use it if you would rather spend
35
+ the tokens of a longer tool list than have the list change mid-session. The
36
+ loader tool is still registered and still describes itself as activating
37
+ inactive tools; under this flag those tools are already active, so calling it
38
+ does nothing.
28
39
 
29
40
  Keys are positive (`"enabled": true` means the feature runs), so a `--no-*` flag corresponds to setting its key `false`. A `no-*` flag on the command line is a one-way switch: it can disable, never re-enable, so `--no-lsp` wins over `lsp.enabled: true` but nothing on the CLI overrides `lsp.enabled: false`. Set the key back to `true` to re-enable.
30
41
 
@@ -6,7 +6,7 @@ server's `initialize` handshake. Generated by `node scripts/server-capabilities.
6
6
  absent on the generating host are listed under *Unavailable* — run in a
7
7
  provisioned environment (the nightly) to capture those rows.
8
8
 
9
- _Last generated: 2026-08-14 on linux; 29 servers captured, 14 unavailable._
9
+ _Last generated: 2026-08-15 on linux; 29 servers captured, 14 unavailable._
10
10
 
11
11
  ## Diagnostic mode + navigation/edit operations
12
12
 
@@ -70,6 +70,12 @@ column is the effective behavior when nothing is set.
70
70
  | `--lens-actionable-warning-autofix` | `actionableWarnings.autoFix.enabled` | project | **off** |
71
71
  | `--lens-actionable-warning-all` | `actionableWarnings.deltaOnly` (`false`) | global | `deltaOnly` **on** (report this turn only) |
72
72
  | `--lens-compact-tool-line` | `ui.compactToolLine` | global | **off** (two-row tool rendering) |
73
+ | `--no-lazy-tools` | `tools.lazy` | global | lazy tools **on** (six situational tools start inactive) |
74
+
75
+ `--no-lazy-tools` keeps every pi-lens tool active for the whole session, so the
76
+ advertised tool list never changes. The `pi_lens_activate_tools` loader stays
77
+ registered and keeps its usual description; under this flag the tools it names
78
+ are already active, so calling it is a no-op.
73
79
 
74
80
  `--lens-guard` is **EXPERIMENTAL and strictly opt-in**. When enabled, actual
75
81
  `git commit`/`git push` commands are blocked only for current, structured
@@ -18,17 +18,34 @@ pi-lens hooks into the pi agent lifecycle:
18
18
 
19
19
  ## On-write pipeline
20
20
 
21
- For each write/edit, pi-lens runs a language-aware pipeline:
22
-
23
- 1. Format queue / immediate formatting when configured.
24
- 2. Safe autofix from tools with deterministic fix support.
21
+ For each write/edit, pi-lens runs a language-aware pipeline. Format and safe
22
+ autofix (steps 1–2) run on different schedules depending on the tool:
23
+
24
+ - **`write`** (new file, full overwrite, bash-authored write): autofix runs
25
+ immediately, in the same tool result, carrying the fixed file's full content
26
+ back to the agent when it fits under the per-file cap.
27
+ - **`edit`**: autofix defers to `agent_end`, joining the same per-file queue as
28
+ deferred formatting; autofix drains before format so the final state is
29
+ formatter-stable. A `write` followed by an `edit` on the same file in the
30
+ same turn demotes the write's autofix to deferred too.
31
+
32
+ 1. Format queue / immediate formatting when configured (deferred to
33
+ `agent_end` for an `edit` even under `--immediate-format`, so it lands
34
+ after autofix reformats the file).
35
+ 2. Safe autofix from tools with deterministic fix support — immediate for
36
+ `write`, deferred to `agent_end` for `edit` (see above).
25
37
  3. LSP file sync and diagnostic wait.
26
38
  4. Parallel dispatch runners: LSP, ast-grep, tree-sitter, fact rules, and
27
- language-specific linters/security scanners.
39
+ language-specific linters/security scanners. For a deferred `edit`, these
40
+ run against the not-yet-autofixed disk state, so a lint finding autofix
41
+ would have cleared may appear here and resolve itself at `agent_end`.
28
42
  5. Cascade diagnostics for likely affected neighbors.
29
43
  6. Deduplication and routing to blockers, actionable warnings, or code-quality
30
44
  history.
31
45
 
46
+ See [`docs/agent-guide.md`](agent-guide.md#6-auto-format--auto-fix-timing--dont-be-surprised)
47
+ for the consumer-facing version of this routing.
48
+
32
49
  ## Agent tools
33
50
 
34
51
  pi-lens exposes these high-value tools to agents:
@@ -92,6 +109,7 @@ pi --no-opengrep # Disable the Opengrep security scanner (default-on aux
92
109
  pi --no-read-guard # Disable the read-before-edit behavior monitor
93
110
  pi --lens-turn-summary # Persist a per-turn summary of diagnostics, autofixes, and autoformats
94
111
  pi --lens-compact-tool-line # Render tool results as one compact, theme-aware line (closes #1327)
112
+ pi --no-lazy-tools # Keep every pi-lens tool active instead of activating the situational ones on demand
95
113
 
96
114
  # Actionable warnings (all default off)
97
115
  pi --lens-actionable-warnings # Report fixable warnings at turn end