@herbertgao/pi-extensions 2026.9.14 → 2026.9.16

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 (206) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +28 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  7. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  8. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  9. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
  10. package/node_modules/pi-jev-auto-mode/README.md +7 -0
  11. package/node_modules/pi-jev-auto-mode/package.json +1 -1
  12. package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
  13. package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
  14. package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
  15. package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
  16. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  17. package/node_modules/pi-lens/README.md +45 -2
  18. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  19. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  20. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  21. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  22. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  23. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  24. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  25. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  26. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  27. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  28. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  29. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  30. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  31. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  71. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  72. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  73. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  74. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  75. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  76. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  77. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  78. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  79. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  80. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  81. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  82. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  83. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  84. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  85. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  86. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  87. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  88. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  89. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  90. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  91. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  92. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  93. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  94. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  95. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  96. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  97. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  98. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  99. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  100. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  101. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  102. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  103. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  104. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  105. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  106. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  107. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  108. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  109. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  110. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  111. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  112. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  113. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  114. package/node_modules/pi-lens/dist/index.js +5695 -3733
  115. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  116. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  117. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  118. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  119. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  120. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  121. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  122. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  123. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  124. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  125. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  126. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  127. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  128. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  129. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  130. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  131. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  132. package/node_modules/pi-lens/docs/configuration.md +55 -1
  133. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  134. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  135. package/node_modules/pi-lens/docs/features.md +2 -2
  136. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  137. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  138. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  139. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  140. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  141. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  142. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  143. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  144. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  145. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  146. package/node_modules/pi-lens/docs/settings.md +12 -3
  147. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  148. package/node_modules/pi-lens/package.json +5 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  157. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  158. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  159. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  160. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  161. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  162. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  163. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  164. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  165. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  166. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  167. package/node_modules/pi-mcp-adapter/README.md +12 -5
  168. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  169. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  170. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  171. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  173. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  175. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  176. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  177. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  178. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  179. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  180. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  181. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  182. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  183. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  184. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  185. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  186. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  187. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  188. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  189. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  190. package/node_modules/pi-mcp-adapter/package.json +3 -2
  191. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  192. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  193. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  194. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  195. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  196. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  197. package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
  198. package/node_modules/pi-multi-account/README.md +15 -1
  199. package/node_modules/pi-multi-account/index.ts +295 -41
  200. package/node_modules/pi-multi-account/package.json +1 -1
  201. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  202. package/node_modules/pi-next-cue/LICENSE +21 -0
  203. package/node_modules/pi-next-cue/README.md +81 -0
  204. package/node_modules/pi-next-cue/index.ts +358 -0
  205. package/node_modules/pi-next-cue/package.json +28 -0
  206. package/package.json +10 -7
@@ -7,6 +7,7 @@ import { logLatency } from "./latency-logger.js";
7
7
  import { touchCoverageGap } from "./lsp/diagnostic-binding.js";
8
8
  import { loadLspService } from "./lsp-lazy.js";
9
9
  import { contentHash, createWarmIpcLineReader, diagnosticsIpcPathForCwd, requestWarmCodeActions, requestWarmDiagnostics, WARM_CODE_ACTION_LOOKUP_LIMIT, WARM_DIAGNOSTICS_SCHEMA_VERSION, } from "./mcp/ipc.js";
10
+ import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
10
11
  import { normalizeFilePath } from "./path-utils.js";
11
12
  const state = { local: true, servedDiagnosticHashes: new Map() };
12
13
  function record(event, cwd, reason, pid, source) {
@@ -138,6 +139,33 @@ function startServer(cwd) {
138
139
  }
139
140
  const server = net.createServer((socket) => {
140
141
  socket.setEncoding("utf8");
142
+ // #3389: an ACCEPTED socket with no `error` listener rethrows, and
143
+ // `server.on("error")` below covers only the listener. A client that walks
144
+ // away while this incumbent is answering — every `requestWarmDiagnostics`
145
+ // timeout, schema refusal and validation refusal destroys its socket
146
+ // (`clients/mcp/ipc.ts:312`) — therefore turned a routine
147
+ // `read ECONNRESET` into an uncaught exception in THIS host, the same
148
+ // ending #3375 closed for an unbounded `data` handler. The handler does NOT
149
+ // tear the connection down: a real system failure has already destroyed the
150
+ // stream before this runs (measured: `destroyed` is true at entry), and an
151
+ // `error` emitted with no errno leaves a socket that is still answering
152
+ // (review round 1) — destroying that one would drop a live request. So the
153
+ // handler exists to keep the event catchable and to leave the peer's
154
+ // behavior visible in the ledger.
155
+ socket.on("error", (failure) => {
156
+ incrementDegradationCount({
157
+ // Subject is the errno, so one peer's repeated resets stay one row.
158
+ // No `?? "unknown"` fallback: `normalizeForLedger`
159
+ // (`clients/ledger-bounds.ts:29`) already maps a missing value to
160
+ // `"unknown"`, and a second copy of that rule here was a branch no
161
+ // mutation could red (#3389 review round 1, F3395-01) — an error
162
+ // emitted with no `code` is pinned end-to-end in
163
+ // `tests/clients/warm-attach-socket-error.test.ts`.
164
+ kind: "warm-attach-socket-error",
165
+ subject: failure.code,
166
+ reason: String(failure),
167
+ });
168
+ });
141
169
  // One-shot per connection (#1219 family): the same defect shape as the
142
170
  // MCP warm socket — clients write exactly one request and read one
143
171
  // reply, so a handler that kept re-reading the same buffered line
@@ -153,6 +181,11 @@ function startServer(cwd) {
153
181
  socket.end(`${JSON.stringify({ error: String(error) })}\n`);
154
182
  }
155
183
  })();
184
+ }, {
185
+ label: "warm-attach-server",
186
+ // #3383: an unterminated request line is answered like any other
187
+ // unusable request rather than buffered without a ceiling.
188
+ onOverflow: () => socket.end(`${JSON.stringify({ error: "warm request line exceeded the framing limit" })}\n`),
156
189
  }));
157
190
  });
158
191
  server.on("error", (error) => {
@@ -191,6 +224,20 @@ export async function tryWarmAttachedDiagnostics(file, content, timeoutMs, sourc
191
224
  }
192
225
  const result = await requestWarmDiagnostics(state.cwd, state.incumbentPid, file, content, timeoutMs);
193
226
  if (!result.available) {
227
+ // #3255 H1: a confirmed-alive incumbent whose endpoint has no listener is
228
+ // the upgrade-stranded shape — narrowing the workspace-id case fold
229
+ // renamed the socket, so a peer that registered before the upgrade is
230
+ // still serving the previous name. `promoteToLocal` alone left that in the
231
+ // latency log only; this is the entry `/lens-perf` renders. Bounded twice
232
+ // over: once per kind/subject by the ledger, and once per session by
233
+ // `promoteToLocal`'s latch, which returns early on every later call.
234
+ if (result.reason === "no-listener") {
235
+ recordDegradationOnce({
236
+ kind: "warm-ipc-endpoint-missing",
237
+ subject: diagnosticsIpcPathForCwd(state.cwd, state.incumbentPid),
238
+ reason: "the registry confirms this incumbent is alive but nothing is listening on its derived endpoint — if pi-lens was upgraded while it was running, restart it",
239
+ });
240
+ }
194
241
  promoteToLocal(result.reason);
195
242
  }
196
243
  else {
@@ -231,6 +278,18 @@ export function _resetWarmAttachForTests() {
231
278
  state.local = true;
232
279
  state.servedDiagnosticHashes.clear();
233
280
  }
281
+ /**
282
+ * The live diagnostics server, exposed for tests (#3389 review round 1,
283
+ * F3395-01). The `error` handler on an ACCEPTED socket has exactly one input
284
+ * channel — an `error` event on that socket — and a test cannot reach the
285
+ * socket any other way: it is created by `net.createServer` inside the closure
286
+ * above, and Node's ESM namespace for `node:net` is not configurable, so the
287
+ * factory cannot be wrapped from a test either. A test observes the server's
288
+ * own `connection` event through this accessor; nothing in production reads it.
289
+ */
290
+ export function _warmAttachServerForTests() {
291
+ return state.server;
292
+ }
234
293
  export function _setWarmAttachForTests(cwd, incumbentPid) {
235
294
  state.cwd = path.resolve(cwd);
236
295
  state.incumbentPid = incumbentPid;
@@ -13,7 +13,7 @@ import { normalizeMessage } from "./finding-identity.js";
13
13
  import { freshnessFromMtime } from "./freshness.js";
14
14
  import { PAST_EOF_STALE_MARKER } from "./diagnostic-line-freshness.js";
15
15
  import { STALE_LINE_MARKER } from "./stale-marker.js";
16
- import { anchorsForDiagnostic, applyDispositions, getDisposition, registerWidgetDispositionReconciler, } from "./diagnostic-dispositions.js";
16
+ import { anchorsForDiagnostic, applyDispositions, getDisposition, registerWidgetDispositionReconciler, strictAnchorSpan, strictAnchorSpansIn, } from "./diagnostic-dispositions.js";
17
17
  import { recordDegradationOnce } from "./degradation-ledger.js";
18
18
  /**
19
19
  * Canonical key for the `files` map (and `diagnosticsWriteGuard`) — #1020.
@@ -174,8 +174,23 @@ export function reconcileWidgetDisposition(cwd, target, _disposition, content) {
174
174
  if (!active.has(diagnostic) &&
175
175
  (entry?.disposition === "false-positive" ||
176
176
  entry?.disposition === "suppress")) {
177
+ // #3183: stamp the strict anchor's own SPAN so the row's lifetime can
178
+ // later ask the mark instead of the file's mtime. `source` is the content
179
+ // the anchors above were derived from, so the span is the one the mark
180
+ // itself binds to. Only the STRICT arm has a span — a weak `suppress`
181
+ // mark hashes no line — and re-tagging always re-derives it, so a stale
182
+ // span from an earlier tag can never survive into a new disposition.
183
+ const { anchorSpan: _previousSpan, ...base } = diagnostic;
184
+ const span = entry.disposition === "false-positive"
185
+ ? strictAnchorSpan(source, diagnostic.line)
186
+ : undefined;
177
187
  return [
178
- { ...diagnostic, disposition: entry.disposition, flagged: false },
188
+ {
189
+ ...base,
190
+ disposition: entry.disposition,
191
+ flagged: false,
192
+ ...(span === undefined ? {} : { anchorSpan: span }),
193
+ },
179
194
  ];
180
195
  }
181
196
  // #3158 retirement rule 3: a row the store holds ONLY because it was
@@ -186,7 +201,11 @@ export function reconcileWidgetDisposition(cwd, target, _disposition, content) {
186
201
  // currently present.
187
202
  if (diagnostic.suppressedRetained)
188
203
  return [];
189
- const { disposition: _disposition, flagged: _flagged, ...baseDiagnostic } = diagnostic;
204
+ const { disposition: _disposition, flagged: _flagged,
205
+ // #3183: a row that is no longer disposition-tagged has no mark to
206
+ // anchor, so its span goes with the tag — leaving it would exempt a LIVE
207
+ // row from the sweep's mtime gate.
208
+ anchorSpan: _anchorSpan, ...baseDiagnostic } = diagnostic;
190
209
  return entry?.disposition === "flagged"
191
210
  ? [{ ...baseDiagnostic, flagged: true }]
192
211
  : [baseDiagnostic];
@@ -538,6 +557,16 @@ function normalizeDiagnostics(filePath, diagnostics, observedTs) {
538
557
  * file — so it would discriminate nothing. `normalizeMessage` is the shared
539
558
  * derivation from `clients/finding-identity.ts` that the anchor uses, imported
540
559
  * rather than re-spelled.
560
+ *
561
+ * #3183: this identity is OCCURRENCE-BLIND, and for a STRICT (`false-positive`)
562
+ * row that is not enough on its own. Two findings of one rule with identical
563
+ * messages on different lines collapse onto it, so a scan reporting only the
564
+ * UNMARKED one matched the marked row and replaced it — the chip lost a mark
565
+ * that was still applying. A collision is therefore resolved by the second half
566
+ * of {@link WidgetDiagnostic.anchorSpan}'s contract, in `retainSuppressedRows`:
567
+ * only a colliding incoming finding sitting ON the marked line is the same
568
+ * finding. That test, not this one, is what keeps the round-2 F2 case above
569
+ * closed — the marked line's TEXT moved, so its span still matches.
541
570
  */
542
571
  function retentionIdentity(d) {
543
572
  return JSON.stringify([
@@ -546,6 +575,39 @@ function retentionIdentity(d) {
546
575
  normalizeMessage(d.message),
547
576
  ]);
548
577
  }
578
+ /** `filePath`'s current content, or `undefined` when it cannot be read. */
579
+ function readContentOrUndefined(filePath) {
580
+ try {
581
+ return readFileSync(filePath, "utf8");
582
+ }
583
+ catch {
584
+ // Only reached once this module has established there IS a span-carrying row
585
+ // to ask about, so the record is bounded by that population — and worth
586
+ // making: with no content both callers fall back to the pre-#3183 rules (the
587
+ // per-entry mtime gate, the coarse retention identity), which is the
588
+ // conservative direction but can retire a mark that still stands.
589
+ recordDegradationOnce({
590
+ kind: "widget-mark-anchor-unreadable",
591
+ subject: filePath,
592
+ reason: "the marked file could not be read, so its suppressed rows fall back to the file-mtime retirement gate and the footer chip may under-count it",
593
+ });
594
+ return undefined;
595
+ }
596
+ }
597
+ /**
598
+ * {@link strictAnchorSpansIn} over `filePath`'s current content (#3183), or
599
+ * `undefined` when it cannot be read.
600
+ *
601
+ * Synchronous in both callers, matching `applyCachedDispositions`
602
+ * (`tools/lens-diagnostics.ts`) — the seam that asks the very same question on
603
+ * the very same read path, with `statSync`/`readFileSync`. An async read here
604
+ * would add two unbounded awaits to a module a registered hook handler imports
605
+ * directly (`tests/config/hook-await-bounds.test.ts`) for no behavioural gain.
606
+ */
607
+ function readAnchorSpans(filePath) {
608
+ const content = readContentOrUndefined(filePath);
609
+ return content === undefined ? undefined : strictAnchorSpansIn(content);
610
+ }
549
611
  /**
550
612
  * #3158: carry the record's disposition-tagged rows across a whole-replace
551
613
  * write when the incoming set no longer reports them, so a `false-positive` /
@@ -564,8 +626,46 @@ function retainSuppressedRows(previous, incoming, filePath) {
564
626
  const tagged = previous.filter((d) => d.disposition !== undefined);
565
627
  if (tagged.length === 0)
566
628
  return incoming;
567
- const reported = new Set(incoming.map(retentionIdentity));
568
- const retained = tagged.filter((d) => !reported.has(retentionIdentity(d)));
629
+ const reported = new Map();
630
+ for (const d of incoming) {
631
+ const id = retentionIdentity(d);
632
+ const bucket = reported.get(id);
633
+ if (bucket)
634
+ bucket.push(d);
635
+ else
636
+ reported.set(id, [d]);
637
+ }
638
+ // #3183: the ONE file read on this path, and only for the state the coarse
639
+ // identity genuinely cannot resolve — a strict-anchored row whose identity an
640
+ // incoming finding collides with. Every other write (no marks at all, a mark
641
+ // with no collision, a weak `suppress` mark) stays zero-I/O, which is what
642
+ // keeps this off the per-edit `recordDiagnostics` cost.
643
+ const content = tagged.some((d) => d.anchorSpan !== undefined && reported.has(retentionIdentity(d)))
644
+ ? readContentOrUndefined(filePath)
645
+ : undefined;
646
+ const spans = content === undefined ? undefined : strictAnchorSpansIn(content);
647
+ const retained = tagged.filter((d) => {
648
+ const collisions = reported.get(retentionIdentity(d));
649
+ // Nothing re-reported this finding, so retention rule 2 has nothing to say.
650
+ if (collisions === undefined)
651
+ return true;
652
+ // No span to ask (a weak `suppress` mark, or a row from an older build), or
653
+ // the file is unreadable: fall back to the pre-#3183 coarse rule. The safe
654
+ // direction is to UNDER-count, never to leave a phantom duplicate standing
655
+ // beside a live finding.
656
+ if (d.anchorSpan === undefined || spans === undefined)
657
+ return false;
658
+ // The mark stopped applying — its line is gone from the file — so the
659
+ // colliding finding IS this one, re-reported now that the filter no longer
660
+ // drops it (retirement rule 2, and the same verdict the sweep reaches).
661
+ if (!spans.has(d.anchorSpan))
662
+ return false;
663
+ // The mark still applies. Only a collision sitting ON the marked line is
664
+ // the same finding (its line number may have shifted under an edit — #3158
665
+ // round 2 F2); any other is a DIFFERENT occurrence this strict anchor never
666
+ // covered, and replacing the row with it loses a live mark (#3183).
667
+ return !collisions.some((i) => strictAnchorSpan(content, i.line) === d.anchorSpan);
668
+ });
569
669
  if (retained.length > MAX_RETAINED_SUPPRESSED_PER_FILE) {
570
670
  // Freshest observations win the cap. One record per FILE, never one per
571
671
  // dropped row (AGENTS.md "bounded observability"): the chip under-counts
@@ -886,13 +986,24 @@ export async function reconcileStaleWidgetFiles() {
886
986
  // at dispatch/integration.ts). A missing per-entry stamp (a migrated
887
987
  // pre-#1186 record) inherits the record's `touchedAt`. Tolerance matches
888
988
  // the Windows host-clock skew rationale above.
889
- const survivors = rec.allDiagnostics.filter((d) => {
890
- const v = freshnessFromMtime({
891
- mtimeMs,
892
- referenceMs: d.observedAt ?? rec.touchedAt,
893
- });
894
- return v.verdict !== "stale";
895
- });
989
+ const mtimeStale = (d) => freshnessFromMtime({
990
+ mtimeMs,
991
+ referenceMs: d.observedAt ?? rec.touchedAt,
992
+ }).verdict === "stale";
993
+ // #3183: a disposition-tagged row carrying a strict anchor span is gated
994
+ // on its MARK, not on the file's mtime (retirement rule 1 — see
995
+ // `WidgetDiagnostic.suppressedRetained`): it survives while the marked
996
+ // LINE is still somewhere in the file, and is retired when it is not.
997
+ // The content read is gated on a span-carrying row the mtime gate WOULD
998
+ // have dropped — the only state where reading can change an outcome — so
999
+ // the debounced render sweep stays off the disk both for a file with no
1000
+ // marks and for a marked file nothing has touched.
1001
+ const spans = rec.allDiagnostics.some((d) => d.anchorSpan !== undefined && mtimeStale(d))
1002
+ ? readAnchorSpans(rec.filePath)
1003
+ : undefined;
1004
+ const survivors = rec.allDiagnostics.filter((d) => d.anchorSpan !== undefined && spans !== undefined
1005
+ ? spans.has(d.anchorSpan)
1006
+ : !mtimeStale(d));
896
1007
  if (survivors.length === rec.allDiagnostics.length) {
897
1008
  return { mapKey, action: "keep" }; // nothing stale
898
1009
  }
@@ -1185,18 +1296,38 @@ export function scheduleStaleReconcile() {
1185
1296
  * Used by lens_diagnostics tool (mode: "all"). Exposes the FULL per-file
1186
1297
  * diagnostic set — decoupled from the widget's display cap — so the agent sees
1187
1298
  * everything, not just the 12 the TUI keeps for rendering.
1299
+ *
1300
+ * #3183: "everything" means every row a live scan stands behind.
1301
+ * `disposition`-tagged rows are the store's own bookkeeping for a mark — the
1302
+ * scan that produced this record did not report them — and they are excluded
1303
+ * here. That is the same predicate `countDiagnostics` and `isBlocking` have
1304
+ * always used, so this makes the projected array agree with the counts beside
1305
+ * it; before #3183 the array disagreed, and once retirement rule 1 let a row
1306
+ * outlive the file's mtime, `applyCachedDispositions` reached it in the
1307
+ * WEAK-fallback branch — which can never match a STRICT `false-positive`
1308
+ * anchor — and `mode=all` served a marked finding as a live warning (#3158 AC3).
1309
+ * The exclusion lives HERE, at the one projection every cache-only surface and
1310
+ * the mode=full merge read, rather than as a second disposition rule inside
1311
+ * `summarizeDiagnostics`' tally: that tally still skips `stale` only, and the
1312
+ * disposition filter still reaches it pre-applied through the shared
1313
+ * `applyCachedDispositions` seam.
1314
+ *
1315
+ * Deliberately NOT applied to {@link getFileDiagnostics}, the per-file read:
1316
+ * `reconcileWidgetDisposition` needs the tagged rows to enforce retirement
1317
+ * rule 3, and `lens_diagnostic_mark`'s cross-check re-marks through them.
1188
1318
  */
1189
1319
  export function getFileDiagnosticSummaries() {
1190
1320
  return [...files.values()].map((rec) => {
1191
1321
  applyPastEofGate(rec);
1322
+ const live = rec.allDiagnostics.filter((d) => d.disposition === undefined);
1192
1323
  return {
1193
1324
  filePath: rec.filePath,
1194
1325
  blocking: rec.diagnosticCounts.blocking,
1195
1326
  errors: rec.diagnosticCounts.errors,
1196
1327
  warnings: rec.diagnosticCounts.warnings,
1197
- advisories: countAdvisories(rec.allDiagnostics),
1328
+ advisories: countAdvisories(live),
1198
1329
  hasFinalSnapshot: rec.hasFinalDiagnosticsSnapshot,
1199
- diagnostics: rec.allDiagnostics.map((d) => ({ ...d })),
1330
+ diagnostics: live.map((d) => ({ ...d })),
1200
1331
  };
1201
1332
  });
1202
1333
  }
@@ -39,7 +39,7 @@
39
39
  import * as fs from "node:fs";
40
40
  import * as os from "node:os";
41
41
  import * as path from "node:path";
42
- import { PROJECT_CONFIG_BASENAMES } from "./config-locations.js";
42
+ import { PROJECT_CONFIG_BASENAMES, isResolvedGlobalConfigPath, } from "./config-locations.js";
43
43
  import { logLatency } from "./latency-logger.js";
44
44
  import { isAtOrAboveHomeDir, nameMatchesMarkerGlob, walkUpDirs, } from "./path-utils.js";
45
45
  /** Depth cap on any upward marker walk — mirrors `findNearestMarkerRoot`'s bound. */
@@ -214,9 +214,53 @@ export function getDirectoryMarkers(dir) {
214
214
  function walkCacheKey(startDir, markerKey) {
215
215
  return `${path.resolve(startDir)}\0${markerKey}`;
216
216
  }
217
- function walkStillFresh(dirMtimes) {
217
+ export function dirMtimesStillFresh(dirMtimes) {
218
218
  return dirMtimes.every(({ dir, mtimeMs }) => safeDirMtimeMs(dir) === mtimeMs);
219
219
  }
220
+ /**
221
+ * Async arm of `dirMtimesStillFresh` — same records, same comparison, stats
222
+ * issued in parallel. It exists for the async marker walk in
223
+ * `clients/lsp/server.ts`'s `NearestRoot`, whose own marker probes are
224
+ * `fs/promises` and which runs on the per-file LSP touch path: a `statSync` per
225
+ * directory there would block the event loop the walk deliberately keeps free.
226
+ * Both arms live here so "is this recorded walk still current" has one owner
227
+ * (#3412).
228
+ *
229
+ * Written with promise combinators rather than `await`, along with the two
230
+ * readers below, so the ONLY place that waits on these stats is the caller's
231
+ * `await bounded(...)` — a hook-path await that a later editor cannot leave
232
+ * unbounded, instead of an inner await bounded only by a registry admission
233
+ * (#3412 review round 1, M-3421-02).
234
+ */
235
+ export function dirMtimesStillFreshAsync(dirMtimes) {
236
+ return Promise.all(dirMtimes.map(({ dir }) => dirMtimeMsAsync(dir))).then((current) => dirMtimes.every(({ mtimeMs }, index) => current[index] === mtimeMs));
237
+ }
238
+ /**
239
+ * Current mtime of `dir`, or `-1` when it cannot be stat'ed — the async arm of
240
+ * `safeDirMtimeMs`. A recorder and `dirMtimesStillFreshAsync` MUST read through
241
+ * the same function so the absent-directory sentinel is the same value on both
242
+ * sides: `prisma/` not existing yet is a recordable state, and the record must
243
+ * compare unequal once it is created (#3412).
244
+ */
245
+ function dirMtimeMsAsync(dir) {
246
+ return fs.promises.stat(dir).then((stats) => stats.mtimeMs, () => -1);
247
+ }
248
+ /** One freshness record per directory, read in parallel (#3412). */
249
+ export function dirMtimeRecordsAsync(dirs) {
250
+ return Promise.all(dirs.map((dir) => dirMtimeMsAsync(dir).then((mtimeMs) => ({ dir, mtimeMs }))));
251
+ }
252
+ /**
253
+ * Records for directories whose mtime could NOT be read — because the caller's
254
+ * bound fired, so the answer is unknown rather than absent. `NaN` compares
255
+ * unequal to every mtime including itself, so an entry carrying one of these
256
+ * can never be served: the walk's answer is still returned, it is simply not
257
+ * memoized. Unknown must never be spelled as a number a directory could
258
+ * actually have, and dropping the directory from the signature (recording
259
+ * nothing) would hide every later change in it (#3412 review round 1, S9).
260
+ */
261
+ export function unknownDirMtimeRecords(dirs) {
262
+ return dirs.map((dir) => ({ dir, mtimeMs: Number.NaN }));
263
+ }
220
264
  /**
221
265
  * Shared core of the two nearest-marker walkers below: the cache check, the
222
266
  * home-guarded/depth-capped upward loop, per-directory mtime recording, and
@@ -229,7 +273,7 @@ function walkStillFresh(dirMtimes) {
229
273
  function walkToNearestMatch(startDir, cacheSuffix, matches, capMetadata, homeDir) {
230
274
  const key = walkCacheKey(startDir, cacheSuffix);
231
275
  const cached = walkCache.get(key);
232
- if (cached && walkStillFresh(cached.dirMtimes)) {
276
+ if (cached && dirMtimesStillFresh(cached.dirMtimes)) {
233
277
  touchWalk(key, cached);
234
278
  return cached.dir;
235
279
  }
@@ -339,11 +383,21 @@ export function findNearestDirWithAnyBasename(startDir, basenames, homeDir = os.
339
383
  * `.pi-lens.json`/`pi-lens.json` directly IN `dir` — no upward walk. The
340
384
  * shared-index equivalent of `project-lens-config.ts`'s old private
341
385
  * `findPiLensConfigInDir` probe loop.
386
+ *
387
+ * A candidate that IS the resolved global config path is refused
388
+ * (global-config-location PR, refs #2457): the global tier reads that file by
389
+ * absolute path, and discovering it here a second time made the nested ignore
390
+ * layering full-validate it and warn that its global-only keys are "not
391
+ * honored in a project config" — including the notice advising the user to
392
+ * set them in the very file being ignored. The refusal is observable through
393
+ * the marker staying absent, the same shape as a directory with no marker.
342
394
  */
343
395
  export function findPiLensConfigMarkerInDir(dir) {
344
396
  const markers = getDirectoryMarkers(dir);
345
397
  if (!markers.piLensConfigPath)
346
398
  return undefined;
399
+ if (isResolvedGlobalConfigPath(markers.piLensConfigPath))
400
+ return undefined;
347
401
  const stat = (() => {
348
402
  try {
349
403
  return fs.statSync(markers.piLensConfigPath);
@@ -157,10 +157,12 @@ function classifyGhTokenFailure(res, hostStallMs) {
157
157
  classifiedBy: "caller",
158
158
  };
159
159
  }
160
- async function deriveGhCliToken() {
160
+ async function deriveGhCliToken(consumer) {
161
161
  const sampler = startHostStallSampler();
162
162
  const startedAt = Date.now();
163
- // Best-effort: a missing/unauthenticated `gh` just leaves zizmor offline.
163
+ // Best-effort: a missing/unauthenticated `gh` leaves the requesting
164
+ // consumer without a derived token; only the zizmor wrapper turns that into
165
+ // an offline-mode degradation.
164
166
  // ignoreAmbientSignal so a mid-turn Esc can't silently drop the server into
165
167
  // offline mode; short timeout so a wedged `gh` never stalls the warm spawn.
166
168
  // safeSpawnAsync never rejects (every failure resolves into `res`), so no
@@ -176,7 +178,7 @@ async function deriveGhCliToken() {
176
178
  if (token.length > 0) {
177
179
  ghTokenLatch.noteAvailable();
178
180
  logAvailabilityDecision({
179
- tool: "zizmor-gh-token",
181
+ tool: consumer === "zizmor" ? "zizmor-gh-token" : "github-token",
180
182
  verdict: "available",
181
183
  outcome: "success",
182
184
  cause: "ok",
@@ -195,13 +197,13 @@ async function deriveGhCliToken() {
195
197
  // regardless of whether `token` was empty, so the record claimed the
196
198
  // online audits ran while zizmor was actually about to launch offline
197
199
  // — the #1535 silence moved into the telemetry instead of being fixed.
198
- return recordGhTokenUnavailable({
200
+ return recordGhTokenUnavailable(consumer, {
199
201
  outcome: "non-installable",
200
202
  cause: "empty-result",
201
203
  classifiedBy: "caller",
202
204
  }, elapsedMs, hostStallMs);
203
205
  }
204
- return recordGhTokenUnavailable(classifyGhTokenFailure(res, hostStallMs), elapsedMs, hostStallMs);
206
+ return recordGhTokenUnavailable(consumer, classifyGhTokenFailure(res, hostStallMs), elapsedMs, hostStallMs);
205
207
  }
206
208
  /**
207
209
  * Shared tail for every "no token" verdict: latches (or cools down) the
@@ -209,13 +211,13 @@ async function deriveGhCliToken() {
209
211
  * and logs the decision. Centralized so the empty-answer and failure paths
210
212
  * can't drift on which fields get set.
211
213
  */
212
- function recordGhTokenUnavailable({ outcome, cause, classifiedBy }, elapsedMs, hostStallMs) {
214
+ function recordGhTokenUnavailable(consumer, { outcome, cause, classifiedBy }, elapsedMs, hostStallMs) {
213
215
  const retryAfterMs = ghTokenLatch.noteUnavailable(outcome, cause);
214
- if (outcome === "transient") {
216
+ if (consumer === "zizmor" && outcome === "transient") {
215
217
  recordZizmorOfflineDegradation(`gh auth token probe ${cause}; running offline until the next zizmor start (retry allowed in ${Math.round(retryAfterMs / 1000)}s)`);
216
218
  }
217
219
  logAvailabilityDecision({
218
- tool: "zizmor-gh-token",
220
+ tool: consumer === "zizmor" ? "zizmor-gh-token" : "github-token",
219
221
  verdict: "unavailable",
220
222
  outcome,
221
223
  cause,
@@ -278,14 +280,25 @@ export async function resolveZizmorGitHubToken() {
278
280
  // Respect an explicit offline request — never derive a token then.
279
281
  if (process.env.ZIZMOR_OFFLINE)
280
282
  return undefined;
281
- const fromEnv = process.env.ZIZMOR_GITHUB_TOKEN ||
282
- process.env.GH_TOKEN ||
283
- process.env.GITHUB_TOKEN;
283
+ return resolveGitHubToken(["ZIZMOR_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"], { consumer: "zizmor" });
284
+ }
285
+ /**
286
+ * Resolve a GitHub token for an API client. Explicit environment variables are
287
+ * checked in the caller's order; when none is present, reuse the bounded,
288
+ * session-reset `gh auth token` resolver above rather than spawning once per
289
+ * request. The returned token is ephemeral and callers must not persist or
290
+ * send it to non-API hosts.
291
+ */
292
+ export async function resolveGitHubToken(envKeys = ["GITHUB_TOKEN", "GH_TOKEN"], options = {}) {
293
+ const consumer = options.consumer ?? "github-api";
294
+ const fromEnv = envKeys.map((key) => process.env[key]).find(Boolean);
284
295
  if (fromEnv)
285
296
  return fromEnv;
286
297
  const memo = ghTokenLatch.read();
287
298
  if (memo !== null) {
288
- if (memo === false && ghTokenLatch.getOutcome() === "transient") {
299
+ if (consumer === "zizmor" &&
300
+ memo === false &&
301
+ ghTokenLatch.getOutcome() === "transient") {
289
302
  // Served straight from the still-cooling latch: no new probe runs,
290
303
  // so `deriveGhCliToken`'s own logging never fires. Without this, once
291
304
  // the cooldown ladder crosses zizmor's own respawn cadence (bounded
@@ -322,6 +335,6 @@ export async function resolveZizmorGitHubToken() {
322
335
  }
323
336
  return memo ? cachedToken : undefined;
324
337
  }
325
- cachedToken = await deriveGhCliToken();
338
+ cachedToken = await deriveGhCliToken(consumer);
326
339
  return cachedToken;
327
340
  }