@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
@@ -42,6 +42,41 @@ HTTPServer → httpserver, http, server
42
42
  each through `splitIdentifier`; queries are tokenized the same way so
43
43
  `"authenticate user"` matches `authenticateUser`, `auth_user`, etc.
44
44
 
45
+ ## Query prefix filters (#1450)
46
+
47
+ The query string can mix plain terms with composable `key:value` filters,
48
+ parsed by `parseWordIndexQuery` (`clients/word-index.ts`) before tokenization:
49
+
50
+ - `lang:<kind>` — restrict to a file kind's extensions, drawn from
51
+ `KIND_EXTENSIONS` (`clients/file-kinds.ts`) — the single source of truth
52
+ (#894 invariant), so there is no second, hand-maintained language list here.
53
+ - `file:<substr>` — path substring match against the index's own stored path,
54
+ normalized through `wordIndexKey` (case-folded on win32 only, matching how
55
+ the index's own path-keyed maps already fold win32 paths, #1025).
56
+ - `ext:<ext>` — literal extension match (`ext:ts` and `ext:.ts` are
57
+ equivalent).
58
+ - Any filter may be negated with a leading `-` (`-file:test`).
59
+
60
+ Example: `lang:jsts file:clients/ -file:test rank`. Multiple positive filters
61
+ of the SAME key OR together; different keys AND; negations always subtract.
62
+ Filters are applied as a `fileFilter` predicate BEFORE BM25/priors/centrality
63
+ scoring (same seam as the pre-existing `paths`/`lang` structured options,
64
+ #771), so a surviving file's score is unaffected by filtering. An unrecognized
65
+ `key:` token passes through as an ordinary search term (colon-bearing terms
66
+ like `std::vector`, URLs, and Windows paths search normally). Only a
67
+ recognized key with a bad value — an unrecognized `lang:` kind — throws
68
+ `WordIndexQueryError`
69
+ naming the supported list — never silently falls through as a literal search
70
+ term. Both the pi `symbol_search` tool and the MCP `pilens_symbol_search`
71
+ mirror inherit the syntax for free since both pass their `query` argument
72
+ straight through to `searchWordIndex` (the entry point where parsing happens).
73
+
74
+ DF-normalization: BM25's per-token `idf` is computed from the FULL,
75
+ unfiltered posting for that token before `fileFilter` is applied per
76
+ candidate file — a filtered query reuses the global, corpus-wide document
77
+ frequency rather than recomputing it over the filtered subset. Documented as
78
+ an acceptable approximation (see the doc comment on `searchWordIndex`).
79
+
45
80
  ## Lifecycle
46
81
 
47
82
  The index is built/refreshed in **every** startup mode: load the persisted
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -0,0 +1,8 @@
1
+ id: no-chained-type-assertions
2
+ valid:
3
+ - 'const v = x as A'
4
+ - 'const w = x as A as const'
5
+ - 'const u = x as unknown as Foo'
6
+ invalid:
7
+ - 'const v = x as A as B'
8
+ - 'const v = foo.bar() as A as B;'
@@ -0,0 +1,9 @@
1
+ id: no-conditional-empty-object-spread-js
2
+ valid:
3
+ - 'const o = {...(cond ? {a: 1} : {b: 2})}'
4
+ - 'const o = {...base}'
5
+ - 'const o = {...(cond ? extra : other)}'
6
+ invalid:
7
+ - 'const o = {...(cond ? {} : {x})}'
8
+ - 'const o = {...(cond ? {y} : {})}'
9
+ - 'const o = {...cond ? {} : {x}}'
@@ -0,0 +1,9 @@
1
+ id: no-conditional-empty-object-spread
2
+ valid:
3
+ - 'const o = {...(cond ? {a: 1} : {b: 2})}'
4
+ - 'const o = {...base}'
5
+ - 'const o = {...(cond ? extra : other)}'
6
+ invalid:
7
+ - 'const o = {...(cond ? {} : {x})}'
8
+ - 'const o = {...(cond ? {y} : {})}'
9
+ - 'const o = {...cond ? {} : {x}}'
@@ -0,0 +1,7 @@
1
+ id: no-reflect-apply-js
2
+ valid:
3
+ - 'fn.apply(obj, args)'
4
+ - 'fn(...args)'
5
+ invalid:
6
+ - 'Reflect.apply(fn, obj, args)'
7
+ - 'const r = Reflect.apply(target[method], target, argumentsList);'
@@ -0,0 +1,7 @@
1
+ id: no-reflect-apply
2
+ valid:
3
+ - 'fn.apply(obj, args)'
4
+ - 'fn(...args)'
5
+ invalid:
6
+ - 'Reflect.apply(fn, obj, args)'
7
+ - 'const r = Reflect.apply(target[method], target, argumentsList);'
@@ -0,0 +1,8 @@
1
+ id: no-reflect-get-js
2
+ valid:
3
+ - 'obj.prop'
4
+ - 'obj[key]'
5
+ - 'Reflect.get(target, prop, receiver)'
6
+ invalid:
7
+ - 'Reflect.get(obj, "key")'
8
+ - 'const v = Reflect.get(target, prop);'
@@ -0,0 +1,8 @@
1
+ id: no-reflect-get
2
+ valid:
3
+ - 'obj.prop'
4
+ - 'obj[key]'
5
+ - 'Reflect.get(target, prop, receiver)'
6
+ invalid:
7
+ - 'Reflect.get(obj, "key")'
8
+ - 'const v = Reflect.get(target, prop);'
@@ -0,0 +1,11 @@
1
+ id: no-unknown-laundering
2
+ valid:
3
+ - 'const x: unknown = foo()'
4
+ - 'function f(x: unknown) {}'
5
+ - 'function isValidPayload(data: unknown): data is Foo { return true; }'
6
+ - 'function f(cause: unknown) {}'
7
+ - 'type X = { a: unknown }'
8
+ - 'type Y = Record<string, unknown>'
9
+ invalid:
10
+ - 'type X = unknown'
11
+ - 'type LooseHandler = unknown'
@@ -0,0 +1,21 @@
1
+ id: no-chained-type-assertions
2
+ language: TypeScript
3
+ severity: warning
4
+ message: "Chained type assertion (x as A as B) — stacking assertions with no runtime check between them"
5
+ note: |
6
+ `x as A as B` stacks two assertions with no runtime check between
7
+ them, so the compiler can't verify either hop. Narrow with a type
8
+ guard, or assert once to the type you actually need. `as const`
9
+ chains are allowed — they don't launder the value's type, they just
10
+ freeze a literal. `as unknown as B` chains are also allowed: routing
11
+ a cast through `unknown` (rather than through an unrelated concrete
12
+ type) is the standard, TypeScript-endorsed way to perform a cast that
13
+ the compiler can't otherwise verify as safe — see no-as-any.yml's own
14
+ note, which recommends this exact form as the safe replacement for
15
+ `as any`. An FP-scan of the pi-lens codebase found 22 chained-assertion
16
+ hits and all 22 were `as unknown as`; none were a genuine two
17
+ concrete-type stack.
18
+ rule:
19
+ pattern: $X as $A as $B
20
+ not:
21
+ pattern: "$X as unknown as $B"
@@ -0,0 +1,21 @@
1
+ id: no-conditional-empty-object-spread-js
2
+ language: JavaScript
3
+ severity: hint
4
+ message: "Conditional empty-object spread — use an explicit if/else to omit fields"
5
+ note: |
6
+ `{...(cond ? {} : {x})}` hides field omission behind a ternary — an
7
+ explicit `if` reads better. Matched structurally (spread of a ternary
8
+ whose consequence or alternative is an empty object literal), not by
9
+ exact source text, so it also catches the un-parenthesized form.
10
+
11
+ Kept at `hint` to match no-conditional-empty-object-spread.yml — see
12
+ that rule's note for the FP-scan finding (147 hits, all a deliberate
13
+ existing idiom).
14
+ rule:
15
+ kind: spread_element
16
+ has:
17
+ stopBy: end
18
+ kind: ternary_expression
19
+ any:
20
+ - has: { field: consequence, stopBy: neighbor, kind: object, regex: '^\{\s*\}$' }
21
+ - has: { field: alternative, stopBy: neighbor, kind: object, regex: '^\{\s*\}$' }
@@ -0,0 +1,29 @@
1
+ id: no-conditional-empty-object-spread
2
+ language: TypeScript
3
+ severity: hint
4
+ message: "Conditional empty-object spread — use an explicit if/else to omit fields"
5
+ note: |
6
+ `{...(cond ? {} : {x})}` hides field omission behind a ternary — an
7
+ explicit `if` reads better and lets the object's shape stay typed.
8
+ Matched structurally (spread of a ternary whose consequence or
9
+ alternative is an empty object literal), not by exact source text, so
10
+ it also catches the un-parenthesized and multi-property-spread forms.
11
+
12
+ FP-scan finding (pi-lens codebase, clients/ tools/ mcp/ index.ts): 147
13
+ hits, and every sampled hit is this exact shape used deliberately to
14
+ build an optional-field object (`...(hit.annotations ? {
15
+ annotations: hit.annotations } : {})`). That is a real, established
16
+ idiom here, not a shape mismatch — so the rule stays at `hint` rather
17
+ than `warning`: still discoverable in a scan, but it does not read as
18
+ a call to fix 147 existing sites.
19
+ rule:
20
+ kind: spread_element
21
+ has:
22
+ # the ternary sits under a parenthesized_expression when the source
23
+ # parenthesizes it, so this needs stopBy: end — spread_element's
24
+ # DIRECT child is not the ternary in that (common) form.
25
+ stopBy: end
26
+ kind: ternary_expression
27
+ any:
28
+ - has: { field: consequence, stopBy: neighbor, kind: object, regex: '^\{\s*\}$' }
29
+ - has: { field: alternative, stopBy: neighbor, kind: object, regex: '^\{\s*\}$' }
@@ -0,0 +1,9 @@
1
+ id: no-reflect-apply-js
2
+ language: JavaScript
3
+ severity: warning
4
+ message: "Reflect.apply() — use a typed call (fn.apply(...) or fn(...args)) instead"
5
+ note: |
6
+ Reflect.apply reaches for reflection where a typed call already works.
7
+ Prefer Function#apply or a direct call with spread arguments.
8
+ rule:
9
+ pattern: Reflect.apply($$$ARGS)
@@ -0,0 +1,9 @@
1
+ id: no-reflect-apply
2
+ language: TypeScript
3
+ severity: warning
4
+ message: "Reflect.apply() — use a typed call (fn.apply(...) or fn(...args)) instead"
5
+ note: |
6
+ Reflect.apply reaches for reflection where a typed call already works.
7
+ Prefer Function#apply or a direct call with spread arguments.
8
+ rule:
9
+ pattern: Reflect.apply($$$ARGS)
@@ -0,0 +1,13 @@
1
+ id: no-reflect-get-js
2
+ language: JavaScript
3
+ severity: warning
4
+ message: "Reflect.get(obj, key) — use a typed property access (obj.prop or obj[key]) instead"
5
+ note: |
6
+ Reflect.get reaches for reflection where a typed property access
7
+ already works. Prefer dot or bracket access.
8
+
9
+ Scoped to the 2-argument form only — see no-reflect-get.yml's note:
10
+ the 3-argument receiver-forwarding form is the standard Proxy `get`
11
+ trap idiom.
12
+ rule:
13
+ pattern: Reflect.get($OBJ, $KEY)
@@ -0,0 +1,16 @@
1
+ id: no-reflect-get
2
+ language: TypeScript
3
+ severity: warning
4
+ message: "Reflect.get(obj, key) — use a typed property access (obj.prop or obj[key]) instead"
5
+ note: |
6
+ Reflect.get reaches for reflection where a typed property access
7
+ already works. Prefer dot or bracket access.
8
+
9
+ Scoped to the 2-argument form only. The 3-argument form
10
+ (`Reflect.get(target, key, receiver)`) is the standard, correct way to
11
+ forward a `receiver` inside a Proxy `get` trap — an FP-scan of the
12
+ pi-lens codebase found 2 hits, both inside a Proxy trap using the
13
+ 3-argument receiver-forwarding form, so that arity is excluded rather
14
+ than allowlisted by call site.
15
+ rule:
16
+ pattern: Reflect.get($OBJ, $KEY)
@@ -0,0 +1,27 @@
1
+ id: no-unknown-laundering
2
+ language: TypeScript
3
+ severity: warning
4
+ message: "type alias to bare 'unknown' — give the alias real shape or inline 'unknown' at the one call site that needs it"
5
+ note: |
6
+ `type X = unknown` adds a name but zero type information — every
7
+ consumer of X still has to narrow from scratch, so the alias only
8
+ launders the fact that nothing was typed. Give it real shape, or drop
9
+ the alias and let the (rare) genuine unknown-input site say so
10
+ directly.
11
+
12
+ FP-scan finding (pi-lens codebase, clients/ tools/ mcp/ index.ts):
13
+ the originally proposed wider fan-out — bare `unknown` return types,
14
+ `unknown` parameters, and `Record<string, unknown>` / index-signature
15
+ dictionaries — was dropped from this rule. Each of those arms fired
16
+ overwhelmingly on legitimate, idiomatic TypeScript already in this
17
+ codebase: `unknown` parameters on type-guard/validator functions
18
+ (`errorMessage(err: unknown)`, `asRecord(value: unknown)`), `unknown`
19
+ return types on parsers and a `ProxyHandler.get` trap (whose signature
20
+ TypeScript itself requires), and `Record<string, unknown>` on
21
+ telemetry/metadata bags (`metadata?: Record<string, unknown>`). A
22
+ declared `unknown` at those positions is the CORRECT contract — it
23
+ forces the caller to narrow — so it is not syntactically distinguishable
24
+ from a laundered one; only `type X = unknown` stayed a reliable
25
+ precision signal (zero hits in the same scan).
26
+ rule:
27
+ pattern: "type $T = unknown"
@@ -12,6 +12,7 @@
12
12
  * ~/.pi-lens/actionable-warnings*.log JSONL advisory pipeline (inject/suppress)
13
13
  * ~/.pi-lens/ast-grep-tools*.log JSONL MCP ast-grep search/replace telemetry
14
14
  * ~/.pi-lens/logs/*.jsonl JSONL diagnostic findings
15
+ * ~/.pi-lens/projects/<slug>/worklog.jsonl JSONL fix worklog (rule/tool/model/provider, #1448)
15
16
  */
16
17
 
17
18
  import fs from "node:fs";
@@ -69,6 +70,7 @@ async function main() {
69
70
  analyzeSessionStart(files.sessionStart, state),
70
71
  analyzeActionableWarnings(files.actionableWarnings, state),
71
72
  analyzeAstGrepTools(files.astGrepTools, state),
73
+ analyzeWorklog(files.worklog, state),
72
74
  ]);
73
75
 
74
76
  const report = buildReport(state);
@@ -139,6 +141,13 @@ function discoverLogFiles(logRoot, archived) {
139
141
  .filter((name) => name.endsWith(".jsonl"))
140
142
  .map((name) => path.join(logsDir, name));
141
143
 
144
+ // worklog.jsonl lives per-project under projects/<slug>/ (getProjectDataDir),
145
+ // not at the log root — walk one level to find every project's file (#1448).
146
+ const projectsDir = path.join(logRoot, "projects");
147
+ const worklogs = safeReaddir(projectsDir)
148
+ .map((slug) => path.join(projectsDir, slug, "worklog.jsonl"))
149
+ .filter((file) => fs.existsSync(file));
150
+
142
151
  const byPrefix = (prefix) =>
143
152
  allRootFiles.filter((file) => {
144
153
  const base = path.basename(file);
@@ -156,6 +165,7 @@ function discoverLogFiles(logRoot, archived) {
156
165
  actionableWarnings: byPrefix("actionable-warnings"),
157
166
  astGrepTools: byPrefix("ast-grep-tools"),
158
167
  diagnostics: dailyLogs,
168
+ worklog: worklogs,
159
169
  };
160
170
  }
161
171
 
@@ -263,6 +273,14 @@ function createState(files) {
263
273
  errors: [],
264
274
  slow: [],
265
275
  },
276
+ worklog: {
277
+ // key: "<rule>||<model>" (model "" when the entry predates #1448 or the
278
+ // runtime didn't know it) -> { total, autoFixed }
279
+ byRuleModel: new Map(),
280
+ byModel: counter(),
281
+ byModelAutoFixed: counter(),
282
+ byProvider: counter(),
283
+ },
266
284
  };
267
285
  }
268
286
 
@@ -737,6 +755,39 @@ async function analyzeAstGrepTools(files, state) {
737
755
  }
738
756
  }
739
757
 
758
+ /**
759
+ * Per-model rollup (#1448): rule × model counts and auto-fixed vs
760
+ * agent-required rates, from worklog.jsonl's optional `model`/`provider`
761
+ * fields. Entries predating #1448 (or written outside a live agent turn)
762
+ * have neither field — bucketed under the empty-string model/provider key so
763
+ * "unattributed" volume stays visible rather than silently dropped.
764
+ */
765
+ async function analyzeWorklog(files, state) {
766
+ for (const file of files) {
767
+ await forEachJsonLine(file, "worklog", state, (entry) => {
768
+ const ts = dateOf(entry.timestamp);
769
+ if (!inWindow(ts)) return;
770
+ state.seen.inc("worklog");
771
+ const rule = entry.rule ?? "unknown";
772
+ const model = entry.model ?? "";
773
+ const provider = entry.provider ?? "";
774
+ const key = `${rule}||${model}`;
775
+ const row = state.worklog.byRuleModel.get(key) ?? {
776
+ rule,
777
+ model,
778
+ total: 0,
779
+ autoFixed: 0,
780
+ };
781
+ row.total += 1;
782
+ if (entry.autoFixed) row.autoFixed += 1;
783
+ state.worklog.byRuleModel.set(key, row);
784
+ state.worklog.byModel.inc(model || "(unknown)");
785
+ if (entry.autoFixed) state.worklog.byModelAutoFixed.inc(model || "(unknown)");
786
+ state.worklog.byProvider.inc(provider || "(unknown)");
787
+ });
788
+ }
789
+ }
790
+
740
791
  async function forEachJsonLine(file, bucket, state, visitor) {
741
792
  await forEachLine(file, async (line) => {
742
793
  if (!line.trim()) return;
@@ -1259,6 +1310,17 @@ function buildReport(state) {
1259
1310
  errors: state.astGrep.errors.slice(0, limit),
1260
1311
  slow: state.astGrep.slow.slice(0, limit),
1261
1312
  },
1313
+ worklog: {
1314
+ byModel: state.worklog.byModel.top(limit * 2),
1315
+ byProvider: state.worklog.byProvider.top(limit * 2),
1316
+ byRuleModel: [...state.worklog.byRuleModel.values()]
1317
+ .map((row) => ({
1318
+ ...row,
1319
+ autoFixedRate: row.total ? row.autoFixed / row.total : 0,
1320
+ }))
1321
+ .sort((a, b) => b.total - a.total)
1322
+ .slice(0, limit * 3),
1323
+ },
1262
1324
  };
1263
1325
  }
1264
1326
 
@@ -1397,6 +1459,23 @@ function printReport(report) {
1397
1459
  ` started=${ws.started} completed=${ws.completed} incomplete=${ws.incomplete} aborted=${ws.aborted} fileTimeouts=${ws.timedOutFilesTotal} (in ${ws.timedOutSweeps} sweeps)`,
1398
1460
  );
1399
1461
  }
1462
+ const wl = report.worklog;
1463
+ if (wl && (wl.byModel.length || wl.byRuleModel.length)) {
1464
+ console.log("\nWorklog per-model rollup (#1448)");
1465
+ console.log(
1466
+ ` by model: ${wl.byModel.map((x) => `${x.key}=${x.count}`).join(", ")}`,
1467
+ );
1468
+ if (wl.byProvider.length)
1469
+ console.log(
1470
+ ` by provider: ${wl.byProvider.map((x) => `${x.key}=${x.count}`).join(", ")}`,
1471
+ );
1472
+ console.log(" rule × model (auto-fixed vs agent-required):");
1473
+ for (const row of wl.byRuleModel.slice(0, limit))
1474
+ console.log(
1475
+ ` ${String(row.total).padStart(5)} ${row.rule} [${row.model || "(unknown)"}] autoFixed=${row.autoFixed} (${(row.autoFixedRate * 100).toFixed(0)}%)`,
1476
+ );
1477
+ }
1478
+
1400
1479
  const timeouts = Object.entries(report.latency.phaseTimeouts ?? {});
1401
1480
  if (timeouts.length) {
1402
1481
  console.log("\nPhase timeouts");
@@ -52,12 +52,15 @@ Use `$$$` when you don't need the captured value; `$$$NAME` when you do.
52
52
 
53
53
  Use these instead of writing raw YAML:
54
54
 
55
- | Parameter | What it does |
56
- |---|---|
57
- | `insideKind` | Only match inside an ancestor of this node kind |
58
- | `hasKind` | Only match nodes that contain a descendant of this kind |
59
- | `follows` | Only match nodes preceded by a sibling matching this pattern |
60
- | `precedes` | Only match nodes followed by a sibling matching this pattern |
55
+ | Parameter | Tool | What it does |
56
+ |---|---|---|
57
+ | `insideKind` | both | Only match inside an ancestor of this node kind (searches ALL ancestors, `stopBy: end`) |
58
+ | `hasKind` | both | Only match nodes whose **immediate child** has this kind (`stopBy: neighbor` — NOT recursive) |
59
+ | `hasDescendantKind` | both | Only match nodes containing this kind **anywhere in their descendants** (`stopBy: end`) — use this instead of `hasKind` when the target isn't a direct child |
60
+ | `follows` | both | Only match nodes preceded by a sibling matching this pattern |
61
+ | `precedes` | both | Only match nodes followed by a sibling matching this pattern |
62
+
63
+ `hasKind` and `hasDescendantKind` are mutually exclusive on both tools — combining them errors.
61
64
 
62
65
  ```
63
66
  # console.log only inside functions
@@ -152,13 +155,9 @@ Use these as starting points, then scope `paths` tightly.
152
155
 
153
156
  | Task | Pattern / params |
154
157
  |---|---|
155
- | Find pi lifecycle handlers | `pattern: pi.on($EVENT, $HANDLER)` |
156
- | Find timers | `pattern: setTimeout($CALLBACK, $$$REST)` or `setInterval($CALLBACK, $$$REST)` |
157
- | Find immediate/deferred work | `pattern: setImmediate($CALLBACK)` |
158
- | Find promise callbacks | `pattern: $PROMISE.then($CALLBACK)` / `.catch($CALLBACK)` / `.finally($CALLBACK)` |
159
158
  | Find object-literal function dependency by name | `pattern: { resetLSPService: $FN, $$$REST }` |
160
- | Find fire-and-forget async calls | `pattern: void $CALL` |
161
159
  | Find empty catches | `pattern: try { $$$BODY } catch ($ERR) { }` |
160
+ | Find fire-and-forget async calls | `pattern: void $CALL` |
162
161
 
163
162
  For lifecycle bugs, search first, then use the returned `details.matchLocations[].readSlice` handle for bounded context.
164
163
 
@@ -13,39 +13,39 @@ Use `lsp_diagnostics` before builds/tests or after touching several files:
13
13
 
14
14
  | Need | Tool call |
15
15
  |---|---|
16
- | Check one file | `lsp_diagnostics({ filePath: "src/file.ts" })` |
17
- | Check a folder | `lsp_diagnostics({ filePath: "src/", severity: "error" })` |
18
- | Check exact touched files | `lsp_diagnostics({ filePaths: ["src/a.ts", "src/b.ts"], concurrency: 8 })` |
19
- | Slow server (Rust, Java) | `lsp_diagnostics({ filePaths: files, waitMs: 2000 })` |
20
- | Include warnings | `lsp_diagnostics({ filePaths: files, severity: "all" })` |
16
+ | Check one file | `lsp_diagnostics({ path: "src/file.ts" })` |
17
+ | Check a folder | `lsp_diagnostics({ path: "src/", severity: "error" })` |
18
+ | Check exact touched files | `lsp_diagnostics({ paths: ["src/a.ts", "src/b.ts"], concurrency: 8 })` |
19
+ | Slow server (Rust, Java) | `lsp_diagnostics({ paths: files, waitMs: 2000 })` |
20
+ | Include warnings | `lsp_diagnostics({ paths: files, severity: "all" })` |
21
21
 
22
- Prefer explicit `filePaths` batches after multi-file edits — bounded concurrency, no unrelated directory noise.
22
+ Prefer explicit `paths` batches after multi-file edits — bounded concurrency, no unrelated directory noise.
23
23
 
24
24
  ## Navigation (Code Intelligence)
25
25
 
26
26
  | Question | Operation | Parameters |
27
27
  |---|---|---|
28
- | Where is this defined? | `definition` | filePath, line, character |
29
- | Where is this symbol's *type* defined? | `typeDefinition` | filePath, line, character |
30
- | Where is this declared (vs defined)? | `declaration` | filePath, line, character |
31
- | Find all usages | `references` | filePath, line, character |
32
- | What type is this? | `hover` | filePath, line, character |
33
- | Call signature | `signatureHelp` | filePath, line, character (at arg position) |
34
- | Symbols in this file | `documentSymbol` | filePath |
35
- | Find symbol across project | `workspaceSymbol` | query + filePath (strongly recommended) |
36
- | Quick fixes available | `codeAction` | filePath, line, character, endLine, endCharacter |
37
- | Rename symbol safely | `rename` | filePath, line, character, newName |
38
- | Who implements this? | `implementation` | filePath, line, character |
39
- | Who calls this function? | `prepareCallHierarchy` → `incomingCalls` | filePath, line, character |
40
- | What does this call? | `prepareCallHierarchy` → `outgoingCalls` | filePath, line, character |
41
- | What commands does the server offer? | `capabilities` | (optional filePath) — lists advertised commands |
28
+ | Where is this defined? | `definition` | path, line, character |
29
+ | Where is this symbol's *type* defined? | `typeDefinition` | path, line, character |
30
+ | Where is this declared (vs defined)? | `declaration` | path, line, character |
31
+ | Find all usages | `references` | path, line, character |
32
+ | What type is this? | `hover` | path, line, character |
33
+ | Call signature | `signatureHelp` | path, line, character (at arg position) |
34
+ | Symbols in this file | `documentSymbol` | path |
35
+ | Find symbol across project | `workspaceSymbol` | query + path (strongly recommended) |
36
+ | Quick fixes available | `codeAction` | path, line, character, endLine, endCharacter |
37
+ | Rename symbol safely | `rename` | path, line, character, newName |
38
+ | Who implements this? | `implementation` | path, line, character |
39
+ | Who calls this function? | `prepareCallHierarchy` → `incomingCalls` | path, line, character |
40
+ | What does this call? | `prepareCallHierarchy` → `outgoingCalls` | path, line, character |
41
+ | What commands does the server offer? | `capabilities` | (optional path) — lists advertised commands |
42
42
  | Run a server command (e.g. organize imports) | `executeCommand` | command (+ commandArguments); dry-run unless `apply:true` |
43
43
 
44
44
  ## Call Hierarchy Pattern
45
45
 
46
46
  ```
47
47
  // Step 1
48
- lsp_navigation(operation="prepareCallHierarchy", filePath="src/api.ts", line=42, character=10)
48
+ lsp_navigation(operation="prepareCallHierarchy", path="src/api.ts", line=42, character=10)
49
49
  // → returns callHierarchyItem
50
50
 
51
51
  // Step 2
@@ -56,7 +56,7 @@ lsp_navigation(operation="outgoingCalls", callHierarchyItem=<item from step 1>)
56
56
  ## Operational Notes
57
57
 
58
58
  - **`definition` returns nothing?** The file may not be open/indexed yet. Read it first, then retry.
59
- - **`workspaceSymbol` empty?** Always pass `filePath`. Unscoped queries are best-effort and frequently return nothing. If TypeScript returns "No Project", open the scoped file first.
59
+ - **`workspaceSymbol` empty?** Always pass `path`. Unscoped queries are best-effort and frequently return nothing. If TypeScript returns "No Project", open the scoped file first.
60
60
  - **`references`** — query from the *definition site* for full cross-file coverage; usage-site queries can be partial.
61
61
  - **`signatureHelp`** — only valid at call-site argument positions; declaration positions return empty.
62
62
  - **`workspaceDiagnostics`** — tracked push snapshot only, not an active check. Use `lsp_diagnostics` when you need fresh results.