pi-lens 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/README.md +45 -2
  3. package/dist/clients/actionable-warnings.js +44 -16
  4. package/dist/clients/advisory-provenance.js +187 -51
  5. package/dist/clients/cache-manager.js +64 -5
  6. package/dist/clients/cascade-format.js +24 -0
  7. package/dist/clients/child-unref.js +17 -8
  8. package/dist/clients/config-diagnostic-codes.js +2 -0
  9. package/dist/clients/config-locations.js +237 -9
  10. package/dist/clients/config-resolve.js +7 -1
  11. package/dist/clients/degradation-ledger.js +33 -3
  12. package/dist/clients/dependency-checker.js +87 -64
  13. package/dist/clients/diagnostic-dispositions.js +39 -3
  14. package/dist/clients/dispatch/finding-policy.js +118 -1
  15. package/dist/clients/dispatch/indent-detect.js +8 -2
  16. package/dist/clients/dispatch/inline-suppressions.js +23 -4
  17. package/dist/clients/dispatch/rule-ignores.js +14 -7
  18. package/dist/clients/dispatch/runners/actionlint.js +26 -28
  19. package/dist/clients/dispatch/runners/biome-check.js +38 -25
  20. package/dist/clients/dispatch/runners/cpp-check.js +32 -49
  21. package/dist/clients/dispatch/runners/credo.js +16 -14
  22. package/dist/clients/dispatch/runners/cue-vet.js +25 -7
  23. package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  24. package/dist/clients/dispatch/runners/detekt.js +11 -4
  25. package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  26. package/dist/clients/dispatch/runners/elixir-check.js +31 -46
  27. package/dist/clients/dispatch/runners/eslint.js +29 -46
  28. package/dist/clients/dispatch/runners/gleam-check.js +91 -11
  29. package/dist/clients/dispatch/runners/go-vet.js +16 -2
  30. package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  31. package/dist/clients/dispatch/runners/hadolint.js +27 -15
  32. package/dist/clients/dispatch/runners/htmlhint.js +17 -8
  33. package/dist/clients/dispatch/runners/javac.js +31 -21
  34. package/dist/clients/dispatch/runners/ktlint.js +34 -60
  35. package/dist/clients/dispatch/runners/lsp.js +6 -5
  36. package/dist/clients/dispatch/runners/markdownlint.js +12 -5
  37. package/dist/clients/dispatch/runners/mypy.js +2 -1
  38. package/dist/clients/dispatch/runners/oxlint.js +22 -6
  39. package/dist/clients/dispatch/runners/php-lint.js +39 -16
  40. package/dist/clients/dispatch/runners/phpstan.js +1 -1
  41. package/dist/clients/dispatch/runners/rubocop.js +27 -18
  42. package/dist/clients/dispatch/runners/ruff.js +18 -19
  43. package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  44. package/dist/clients/dispatch/runners/shellcheck.js +6 -2
  45. package/dist/clients/dispatch/runners/spellcheck.js +10 -3
  46. package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  47. package/dist/clients/dispatch/runners/stylelint.js +10 -2
  48. package/dist/clients/dispatch/runners/swiftlint.js +11 -2
  49. package/dist/clients/dispatch/runners/taplo.js +12 -6
  50. package/dist/clients/dispatch/runners/tflint.js +30 -15
  51. package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  52. package/dist/clients/dispatch/runners/trivy-config.js +8 -2
  53. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  54. package/dist/clients/dispatch/runners/vale.js +15 -3
  55. package/dist/clients/dispatch/runners/yamllint.js +12 -2
  56. package/dist/clients/dispatch/runners/zig-check.js +27 -39
  57. package/dist/clients/dispatch/suppress-writer.js +13 -3
  58. package/dist/clients/effective-config.js +3 -1
  59. package/dist/clients/extension-log.js +48 -1
  60. package/dist/clients/finding-delivery-gate.js +53 -23
  61. package/dist/clients/formatters.js +49 -11
  62. package/dist/clients/git-guard.js +247 -34
  63. package/dist/clients/gradle-ktfmt-style.js +121 -25
  64. package/dist/clients/inline-blocker-dispositions.js +186 -0
  65. package/dist/clients/install-diagnostics.js +19 -6
  66. package/dist/clients/installer/index.js +281 -32
  67. package/dist/clients/jscpd-client.js +8 -0
  68. package/dist/clients/latency-logger.js +8 -0
  69. package/dist/clients/lens-config.js +66 -8
  70. package/dist/clients/lsp/cascade-tier.js +3 -0
  71. package/dist/clients/lsp/client.js +139 -11
  72. package/dist/clients/lsp/config.js +9 -7
  73. package/dist/clients/lsp/content-limits.js +41 -0
  74. package/dist/clients/lsp/index.js +42 -8
  75. package/dist/clients/lsp/launch.js +23 -2
  76. package/dist/clients/lsp/server.js +106 -25
  77. package/dist/clients/lsp/sync-kind.js +14 -0
  78. package/dist/clients/lsp/wait-policy/strategies.js +105 -16
  79. package/dist/clients/mcp/analyze.js +9 -2
  80. package/dist/clients/mcp/ipc.js +232 -44
  81. package/dist/clients/mcp/review.js +53 -7
  82. package/dist/clients/opengrep-client.js +11 -6
  83. package/dist/clients/path-utils.js +51 -9
  84. package/dist/clients/persistent-reverify.js +271 -0
  85. package/dist/clients/pipeline.js +136 -67
  86. package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  87. package/dist/clients/project-lens-config.js +11 -3
  88. package/dist/clients/review-graph/builder.js +34 -26
  89. package/dist/clients/review-graph/git-identity.js +16 -10
  90. package/dist/clients/ruff-client.js +31 -4
  91. package/dist/clients/runtime-coordinator.js +38 -4
  92. package/dist/clients/runtime-tool-call.js +16 -5
  93. package/dist/clients/runtime-tool-result.js +27 -12
  94. package/dist/clients/runtime-turn.js +571 -287
  95. package/dist/clients/safe-spawn.js +256 -65
  96. package/dist/clients/skills-resolver.js +38 -2
  97. package/dist/clients/spawn-output-cap.js +84 -1
  98. package/dist/clients/tree-sitter-client.js +119 -58
  99. package/dist/clients/turn-end/lane.js +51 -0
  100. package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  101. package/dist/clients/turn-end/lanes/secrets.js +204 -0
  102. package/dist/clients/warm-attach.js +59 -0
  103. package/dist/clients/widget-state.js +294 -17
  104. package/dist/clients/workspace-topology.js +57 -3
  105. package/dist/clients/zizmor-config.js +26 -13
  106. package/dist/index.js +6342 -3995
  107. package/dist/mcp/analyze-cli.js +4 -0
  108. package/dist/mcp/server.js +33 -21
  109. package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  110. package/dist/tools/lens-diagnostic-mark.js +14 -2
  111. package/dist/tools/lens-diagnostics.js +127 -33
  112. package/dist/tools/lsp-diagnostics.js +150 -12
  113. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  114. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  115. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  116. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  117. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  118. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  119. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  120. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  121. package/docs/adr/0009-reported-path-attribution.md +114 -0
  122. package/docs/adr/README.md +26 -0
  123. package/docs/agent-tools.md +18 -1
  124. package/docs/ast-grep_rules_catalog.md +4 -3
  125. package/docs/configuration.md +55 -1
  126. package/docs/engineering-principles.md +202 -0
  127. package/docs/environment-variables.md +61 -4
  128. package/docs/features.md +2 -2
  129. package/docs/globalconfig.md +6 -1
  130. package/docs/language-coverage.md +17 -0
  131. package/docs/lsp-capability-matrix.md +98 -31
  132. package/docs/pi-lens-fixer.md +57 -3
  133. package/docs/pi-lens-investigator.md +9 -0
  134. package/docs/pi-lens-reviewer.md +16 -0
  135. package/docs/pi-lens-subagent.md +9 -0
  136. package/docs/public-api-stability.md +1 -0
  137. package/docs/release-qa-baseline.md +2 -0
  138. package/docs/servercapabilities.md +21 -11
  139. package/docs/settings.md +12 -3
  140. package/docs/subagent-compat.md +1 -1
  141. package/package.json +5 -3
  142. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  143. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  144. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  145. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  146. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  147. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  148. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  149. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  150. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  151. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  152. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  153. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  154. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  155. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  156. package/scripts/install-selftest.mjs +21 -17
  157. package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
@@ -0,0 +1,202 @@
1
+ # Engineering principles
2
+
3
+ Source of truth: https://github.com/apmantza/engineering-principles (private), file PRINCIPLES.md. Verbatim copy; the repo wins on drift. Project-level AGENTS.md / CLAUDE.md add the project catalog and win on conflict.
4
+
5
+ Project-neutral rules distilled from the pi-lens contracts, playbooks and merge
6
+ train (2026-08 to 2026-09). Each rule is imperative and carries the reason it
7
+ exists. Projects keep their own catalog of defect shapes, registries, and
8
+ commands; this file is what stays the same across them.
9
+
10
+ Read order for any agent starting work: this file, then the project's own
11
+ AGENTS.md or CLAUDE.md, then the role contract for the task (fixer, reviewer,
12
+ investigator). Project instructions win on conflict.
13
+
14
+ ## 1. Building
15
+
16
+ **Premise first.** Reproduce a reported defect through the production call
17
+ path before writing a fix. Drive the real dispatcher, loader, or builder; never
18
+ a hand-shaped input that exists only to hit the bug. If it does not reproduce,
19
+ the deliverable is the recorded refutation (which invariant covers it, with the
20
+ probe) and, where cheap, an assertion that pins it, not machinery for a
21
+ collision that cannot occur.
22
+
23
+ **Climb the minimalism ladder before writing code.** Be lazy about the
24
+ solution, never about reading. Trace the real flow, then ask in order: does this
25
+ need to exist at all; does the codebase already do it (reuse the seam); does the
26
+ platform or stdlib do it; does an installed dependency do it; is it one line.
27
+ Only then write the minimum that works. Validation, error handling, security and
28
+ bounded observability are never skipped in the name of minimalism.
29
+
30
+ **Net-count rule for shared helpers.** A change that introduces a shared helper
31
+ while same-shape siblings exist deletes at least one sibling in the same change.
32
+ The helper count never goes up. Deferring the fold is allowed only when the
33
+ brief names every sibling, states why folding them together is unsafe, and
34
+ links the follow-up. This binds whoever writes the brief as much as whoever
35
+ implements it.
36
+
37
+ **A fix's pattern matters more than its instance.** When a root cause is
38
+ understood, name the defect shape and sweep the whole tree for other members
39
+ before closing. Fix the contained siblings in the same change; file the rest
40
+ with the member list; record what was grepped and what was found clean. Seeing
41
+ without recording is indistinguishable from not looking. Every sweep ends with
42
+ a consolidation verdict: fold onto one seam (name it) or stay distributed (say
43
+ why). The deciding test is deletion: would removing the shared module
44
+ concentrate complexity back into callers (fold) or merely relocate the same
45
+ lines (stay)?
46
+
47
+ **Design shared state once, at design time.** Before adding a second writer to
48
+ any shared record (a cache entry, a latch, a ledger, a mirror, a counter) decide
49
+ whether every actor needs one canonical object or each can own its record and
50
+ merge at read. Refresh mirrors inside the guard that published the durable
51
+ state. A resource bounded along one axis and unbounded along another is a leak.
52
+
53
+ **Every new failure path gets bounded observability.** One record per distinct
54
+ event per session, never one per occurrence in a loop; repeated degradations go
55
+ through a once-only or counted record. A discriminator nothing can observe is a
56
+ vacuous guard and does not ship.
57
+
58
+ **Blast radius as a call-tree diff.** For any change to a shared seam, list the
59
+ changed symbol, its callers above, its callees below, with plus and minus on the
60
+ lines that moved. Prose keeps missing callers; a tree the reviewer can grep does
61
+ not. Name every strict consumer of a durable record whose shape changes, and
62
+ prove old records still parse.
63
+
64
+ **Deletion sweeps dependents first.** Before removing a guard, a defensive
65
+ branch, or a field, grep every caller and every test double. Correct-looking
66
+ deletion asks have redded CI because dozens of doubles depended on the removed
67
+ line.
68
+
69
+ **Identity and lifecycle rules are stated before the first edit.** When a change
70
+ touches session, ownership, or resume semantics, write the invariants and the
71
+ writers-by-axis table (which actor, which identity, which timing) first, and
72
+ choose the simplest identity rule that satisfies every cell. Adding a second
73
+ identity clause "to be safe" produced two consecutive regressions on one seam.
74
+
75
+ ## 2. Testing
76
+
77
+ **Red first, and keep the transcript.** Every regression test is shown failing
78
+ on the pre-fix code with the failure that matches the defect, and the output is
79
+ pasted into the change description. A test that passes pre-fix is a finding.
80
+
81
+ **Tests ship with the code, in the same commit.** A change and the tests that
82
+ prove it land together; a test-only follow-up round means the first commit
83
+ shipped unproven. The failure list is written before the first edit, and the
84
+ mutation table is that list with transcripts, never a list invented after the
85
+ code.
86
+
87
+ **Keep fixture corpora from every supported version.** Every durable record,
88
+ config file, cache entry or wire message the project still reads has a fixture
89
+ from each version that produced it, and the parser is run over all of them. An
90
+ old-record parse proof is a fixture in the corpus, not a sentence in a PR body.
91
+
92
+ **Mutation output is quoted, not ticked.** For every new guard, branch, filter,
93
+ cap or fallback: neuter it in the built output, run the suite that should catch
94
+ it, and paste the red. A checked box with no transcript is treated as false. A
95
+ mutation that fails to compile proves nothing about the test; redo it as a
96
+ compile-valid change. A guard that cannot be made to red does not need to exist.
97
+
98
+ **Tautological tests are harmful.** A test observes behaviour through the real
99
+ seam and asserts an independent effect. It never restates the implementation,
100
+ never asserts a value its own setup supplied, and never swaps a real in-process
101
+ store, sink, coordinator or registry for a fake. Mocks belong only at true
102
+ process or host boundaries.
103
+
104
+ **Every guard test names the recurrence it prevents** in its own comment: the
105
+ shipped defect or the concrete drift it would catch. A guard with no named
106
+ recurrence is speculative and does not ship.
107
+
108
+ **Detectors match code, not prose.** Any test-side scan that regexes source
109
+ must run over comment-and-string-blanked text, with a per-needle policy for the
110
+ rare needle whose evidence is a string literal. A comment quoting the needle
111
+ must never satisfy a requirement or an exemption. The self-excuse direction
112
+ (prose makes a guard pass) is the dangerous one; the false-positive direction
113
+ reds loudly and is lower priority.
114
+
115
+ **Ratchets and baselines are admissions, not loosening.** A new real spawn,
116
+ timer, or wall-clock assertion is admitted with a header stating why the real
117
+ thing is required, a registry entry, a baseline row and lane membership. A
118
+ mocked seam is not a real spawn and gets an exemption with its reason, never an
119
+ admission.
120
+
121
+ **Run targeted files while iterating; the full suite runs once.** Build before
122
+ testing when tests execute compiled output, and rebuild between mutations. A red
123
+ is environmental only when the same test is red on the base branch in the same
124
+ environment; say so with the evidence.
125
+
126
+ **Probe hygiene.** Every probe, test run and child process pins its home,
127
+ data and log directories under the working tree. Nothing an agent runs writes
128
+ to the maintainer's real home.
129
+
130
+ ## 3. Reviewing and delegating
131
+
132
+ **Every change is reviewed adversarially, including small and self-authored
133
+ ones.** Depth follows what the diff touches, never the priority label: runtime,
134
+ tooling, CI and manifests get full adversarial review; test-only diffs that are
135
+ not ratchets get one scoped pass; docs and renames get an orchestrator read plus
136
+ CI on the exact head.
137
+
138
+ **A finding is real when a probe proves it; a fix is real when the same probe
139
+ passes.** Reviewers attack with throwaway probes and quote the output. They
140
+ reproduce the red run themselves (keep the tests, revert the source), mutate
141
+ every new guard, check merge state first (a conflicted change silently skips
142
+ its gates), and read CI on the exact head. Reviewer prescriptions are
143
+ hypotheses: the verify round attacks them as a rival's.
144
+
145
+ **The same reviewer verifies each fix round.** Continuity is the reviewer's
146
+ judgment and probe set; the checkout under it is rebuilt fresh at the new head
147
+ every round. When a review or verify names a new defect on the same seam, the
148
+ next fix opens with the state-space table written before any edit, on the
149
+ strongest available model. Never send a third patch-only round on one seam.
150
+
151
+ **Round routing.** A round that only applies a prescribed remedy with quoted
152
+ reds merges on green. A round that adds mechanism, touches lifecycle or session
153
+ semantics, or rewrites a guard gets a fresh verify. Classify by the worst
154
+ finding, not the count. Intent-free findings (a body sentence, a comment, a
155
+ literal, a changelog line) are applied by the orchestrator as trailing commits
156
+ and never re-verified; anything that changes what code means goes through a
157
+ round, however small it looks.
158
+
159
+ **Merge gate.** Merge only when the verdict is merge-ready, every gating check
160
+ genuinely executed and passed on the exact head, and every failing check was
161
+ read and judged. Absent is not green. Read exit codes directly, never after a
162
+ pipe. After a merge, check the other open changes for conflicts and prune only
163
+ the merged lane's worktrees.
164
+
165
+ **Delegation contract.** Every delegation names the role, the absolute worktree,
166
+ branch, base, acceptance criteria, non-goals, sibling files to avoid, and Git
167
+ authority. A role never grants Git authority by itself. A worker without it
168
+ leaves changes uncommitted and hands off through a PR body and a commit message
169
+ at the worktree root; the orchestrator commits. Implementation and review are
170
+ never mixed in one delegation. Scope changes are mirrored on the tracking issue
171
+ before they are sent, so the worker can verify them.
172
+
173
+ **No manufactured work.** An idle worker is cheaper than fake work. Delegate
174
+ when a real producer, consumer, probe or diagnosis is unresolved, never to keep
175
+ a pool busy. The same applies to status artifacts: they exist for coordination
176
+ someone consumes.
177
+
178
+ ## 4. Honesty and records
179
+
180
+ **Closes versus refs follows delivery, not optimism.** Close only when every
181
+ acceptance criterion is met; otherwise reference the issue and post the named
182
+ remainder before anything merges. After merging a referencing change, verify the
183
+ issue carries that comment or close it crediting the change.
184
+
185
+ **Report what ran, what was skipped, and what CI must still confirm.** A
186
+ skipped suite, an environment-blocked file, a mutation that could not be
187
+ executed: each is named, never implied green.
188
+
189
+ **Contracts move in the same session.** When a review or an incident reveals a
190
+ defect class rather than an instance, the rule enters the project's catalog with
191
+ the reference and a one-line screen before the next dispatch. Two rounds on the
192
+ same shape in one session is the trigger.
193
+
194
+ **Detection retrospective on every merged bug fix.** Record which verification
195
+ layer caught it and which layer should have caught it earlier and at what cost.
196
+ If that layer does not exist, file it with the bug as its named recurrence.
197
+
198
+ **Keep a lane ledger.** One row per lane with a fixed state vocabulary (not
199
+ started, fixing rN, waiting on CI, waiting on review, waiting on verify,
200
+ blocked, ready to merge, merged, needs user decision, held). It is what survives
201
+ a context reset, and it lets a different orchestrator build the status table
202
+ without reading transcripts.
@@ -30,13 +30,36 @@ config JSON) and how they interact, see [Settings](settings.md).
30
30
 
31
31
  ### `PI_LENS_CONFIG_PATH`
32
32
 
33
- Override the path of the global config file. **Default:** `~/.pi-lens/config.json`
34
- (`%USERPROFILE%\.pi-lens\config.json` on Windows). When set, the value is
35
- resolved to an absolute path and used verbatim.
33
+ Override the path of the global config file. **Default:** the resolution order
34
+ below. When set, the value is resolved to an absolute path and used verbatim;
35
+ it wins over every other location. See the [winning-location table](configuration.md#global-config-location).
36
36
 
37
37
  **When to set it:** keeping the config under version control or a dotfiles
38
38
  manager at a non-default location, or pointing CI at a fixture config.
39
39
 
40
+ ### `PI_CODING_AGENT_DIR`
41
+
42
+ Host-relative global config location, honored by pi-lens when pi sets it.
43
+ **Resolved path:** `$PI_CODING_AGENT_DIR/extensions/pi-lens.json`. The tier is
44
+ opt-in by creation: the file is read when it EXISTS and the legacy default
45
+ does not. Absent, nothing changes.
46
+
47
+ The full resolution order for the global config file, highest first:
48
+
49
+ 1. `PI_LENS_CONFIG_PATH` — an explicit file override, used verbatim.
50
+ 2. `~/.pi-lens/config.json` — **only when it already exists**; current users
51
+ never move.
52
+ 3. `$PI_CODING_AGENT_DIR/extensions/pi-lens.json` — **only when it exists**
53
+ and step 2 missed.
54
+ 4. `~/.pi-lens/config.json` — the canonical default (unchanged).
55
+
56
+ pi-lens does not write the global config; there is no settings writer in this
57
+ repository. Create or edit the selected file by hand. `effective_config` shows
58
+ which file supplies the global tier. If an existence probe errors (a file where
59
+ a directory belongs, permission errors, or a symlink loop), the errored tier is
60
+ retained and the read failure is reported under `PILENS_CFG_0001`; it is not
61
+ treated as absent. See the [XDG-like recipe](configuration.md#global-config-location).
62
+
40
63
  ## Data directory
41
64
 
42
65
  ### `PILENS_DATA_DIR`
@@ -260,12 +283,46 @@ Days to keep rotated logs before cleanup. **Default:** `7`.
260
283
 
261
284
  Maximum log size (MB) before rotation. **Default:** `10`.
262
285
 
286
+ ## Diagnostic-only knobs
287
+
288
+ These use the `PILENS_` (no underscore after `PI`) prefix, so they are
289
+ invisible to a `grep PI_LENS_` sweep — use the wider `grep -E 'PI_?LENS_'`
290
+ below to find them and anything like them. Not part of the supported surface;
291
+ each exists for a narrow diagnostic or escape-hatch purpose, not everyday
292
+ tuning.
293
+
294
+ ### `PILENS_PROBE`
295
+
296
+ Set to `1` to force an ad-hoc probe (a bare `node -e` or throwaway script
297
+ against built `clients/*.js`, run outside a test harness and without
298
+ `PI_LENS_HOME` set) to redirect its home/log directory away from the real
299
+ `~/.pi-lens`, the same way running from an agent worktree or `os.tmpdir()`
300
+ already does automatically (`clients/probe-home-state.ts`). Prefer setting
301
+ `PI_LENS_HOME` explicitly; this is the forced opt-in for a probe run from an
302
+ ordinary project checkout, where the automatic detection would not fire.
303
+
304
+ ### `PILENS_UNSAFE_FORCE_GRAMMAR_LOAD`
305
+
306
+ Diagnostic escape hatch for the grammar-health probe only (`clients/grammar-source.ts`).
307
+ Set to `1` to force-load a tree-sitter grammar this runtime has blocklisted,
308
+ to test whether a newer build/runtime lifts the block. Disables the crash
309
+ protection the blocklist provides and can abort the process. Never set in
310
+ normal operation.
311
+
312
+ ### `PILENS_PUB_DEBUG`
313
+
314
+ Set to `1` to trace each LSP server's `publishDiagnostics` behavior (version +
315
+ diagnostic count) to diagnose the clean-file affirmative-signal question:
316
+ which servers publish an empty-with-version set on a clean scan vs. go silent
317
+ (`clients/lsp/client.ts`). Off by default.
318
+
263
319
  ## Advanced tuning knobs
264
320
 
265
321
  pi-lens also has many advanced/internal tuning variables — LSP timeouts and
266
322
  memory budgets, debounce intervals (`PI_LENS_LSP_*`, and others). These are for
267
323
  edge-case tuning, are not part of the supported surface above, and are documented
268
- in the source; enumerate them with `grep PI_LENS_ clients/`.
324
+ in the source; enumerate them with `grep -E 'PI_?LENS_' clients/` (the wider
325
+ pattern catches the `PILENS_` diagnostic-only knobs above too).
269
326
 
270
327
  ## Related
271
328
 
package/docs/features.md CHANGED
@@ -316,7 +316,7 @@ Covers JavaScript/TypeScript, Python, Go, Rust, Ruby, Shell, and CMake. A TypeSc
316
316
 
317
317
  Structural rules organized by language in `rules/tree-sitter-queries/<language>/`. Rules marked **🔴** block the agent inline at write time (only for lines in the current edit); others are advisory.
318
318
 
319
- **Suppressing a finding:** add `// pi-lens-ignore: rule-id` on the flagged line or the line above (JS/TS), or `# pi-lens-ignore: rule-id` for Python/Ruby/Shell. This suppresses that specific rule at that location only.
319
+ **Suppressing a finding:** add `// pi-lens-ignore: rule-id` on the flagged line or the line above (JS/TS), or `# pi-lens-ignore: rule-id` for Python/Ruby/Shell. Rule ids are comma-separated. A supported separator (` -- `, ` — `, or `: `) starts one trailing reason suffix for the whole payload; all remaining text, including commas, stays in the reason and is not parsed as another rule id. This suppresses each listed rule at that location only.
320
320
 
321
321
  **Bring your own rules:** drop YAML query files into `rules/tree-sitter-queries/<language>/` in your project — pi-lens merges them with the built-ins on session start. The schema, predicates (`eq`, `match`, `any-of`), and `inline_tier` (`blocking` | `warning` | `review`) are documented in [`custom-rules.md`](custom-rules.md). A `rules/tree-sitter-queries/rule-schema.json` JSON Schema is bundled for editor autocomplete via `.vscode/settings.json`.
322
322
 
@@ -401,7 +401,7 @@ pi-lens ships an MCP (Model Context Protocol) server so Claude Code — or any M
401
401
  | **Per-turn** | `pilens_turn_end` | Drives the **real** `handleTurnEnd` (knip incremental, dep-circular, cascade, tests, actionable+code-quality warnings) — not a re-implementation. Caller-supplied edited files are auto-registered into turn-state via `addModifiedRange`. |
402
402
  | **Per-session** | `pilens_session_start` | Drives the **real** `handleSessionStart` — full jscpd/knip/madge/govulncheck/gitleaks/trivy scans + complexity baselines + LSP warm. The error-debt baseline is not currently populated by the production session-start path. |
403
403
  | **Project / observability** | `pilens_project_scan`, `pilens_project_report`, `pilens_diagnostics`, `pilens_health`, `pilens_latency`, `pilens_symbol_search`, `pilens_effective_config` | Cheap project-wide scans, cached diagnostic state, latency telemetry, ranked identifier search (BM25 over the persisted word index — see [docs/word-index.md](word-index.md)). Cross-file blast radius now lives in `pilens_module_report`'s `blastRadius` option. `pilens_health` (and its pi-side `/lens-health` counterpart) also reports a bounded, process-local **degradation ledger** — trust refusals, mode suppressions, LSP breaker trips, formatter skips/failures, TypeScript/word-index/review-graph/project-snapshot idle evictions, WASM aborts, and diagnostics-timeout tallies — so silently degraded behavior stays visible instead of vanishing into a log. `pilens_effective_config` answers **“why is X running / why is X not running”** from one query — the resolved configuration with the provenance of every setting, and for a file you name, its language plus every LSP server with the reason it was selected or denied and the runners that would dispatch. It reports sources, never values. |
404
- | **Lifecycle / loop** | `pilens_rebuild` | Runs `npm run build:dist` so `pilens_analyze mode=fresh` reflects the latest commit. Makes the review loop self-contained: commit → `pilens_rebuild` → `pilens_analyze mode=fresh` → `pilens_latency`. |
404
+ | **Lifecycle / loop** | `pilens_rebuild`, `pilens_session_end` | `pilens_rebuild` runs `npm run build:dist` so `pilens_analyze mode=fresh` reflects the latest commit. Makes the review loop self-contained: commit → `pilens_rebuild` → `pilens_analyze mode=fresh` → `pilens_latency`. `pilens_session_end` ends the MCP connection's telemetry session and writes its bounded situational-tool telemetry line; terminal for the connection. |
405
405
 
406
406
  **Honest limits** (live-tested, documented in `mcp.md`):
407
407
 
@@ -1,6 +1,11 @@
1
1
  # Configuration
2
2
 
3
- pi-lens reads optional user preferences from `~/.pi-lens/config.json` (`%USERPROFILE%\\.pi-lens\\config.json` on Windows). An unrecognized top-level key is logged once (to surface a typo like `lps` for `lsp`) and then ignored; `$schema` is always allowed for editor JSON-schema association. Missing or invalid config falls back to defaults.
3
+ pi-lens reads optional user preferences from the global file selected by the
4
+ [winning-location table](configuration.md#global-config-location) (`~/.pi-lens/config.json`
5
+ by default; `%USERPROFILE%\\.pi-lens\\config.json` on Windows). An unrecognized
6
+ top-level key is logged once (to surface a typo like `lps` for `lsp`) and then
7
+ ignored; `$schema` is always allowed for editor JSON-schema association. Missing
8
+ or invalid config falls back to defaults.
4
9
 
5
10
  ## Every toggle, both ways
6
11
 
@@ -50,6 +50,8 @@ Dispatch is diagnostics-oriented: automatic formatting and safe autofix happen i
50
50
  | CMake | ✓ (cmake-language-server) | lsp | cmake-format |
51
51
  | CUE | ✓ (syntax via cue lsp, evaluation via cue vet) | lsp, cue-vet | cue fmt |
52
52
 
53
+ ### CUE tree-sitter grammar
54
+
53
55
  `cue lsp` reports load and parse errors as you type but leaves conflicting
54
56
  values and failed constraints to `cue vet` — the `cue-vet` auxiliary runner
55
57
  (#1522) covers that gap, so together they give full coverage: syntax/parse
@@ -79,6 +81,21 @@ Both are upstream grammar limitations (tracked among
79
81
  [eonpatapon/tree-sitter-cue](https://github.com/eonpatapon/tree-sitter-cue)'s
80
82
  open issues), not something a query change here can fix.
81
83
 
84
+ ## Non-core grammars
85
+
86
+ The `tree-sitter` runner and `symbol_search`/`module_report` structural
87
+ extraction ship a fixed set of core grammars; grammars for the other
88
+ languages in the table above (CUE included) are fetched lazily on first use
89
+ into web-tree-sitter's resolved package directory, verified against
90
+ `scripts/grammars.lock.json`. This resolution works on a runtime that cannot
91
+ resolve a bare package specifier — pi shipped as a `bun build --compile`
92
+ binary — via one shared subpath-based ladder
93
+ (`scripts/lib/web-tree-sitter-dir.mjs`, #3409); before that fix, non-core
94
+ languages could never be analysed on those hosts. A grammar that never loads
95
+ (fetch failure, unwritable target, an unsupported runtime) is recorded as a
96
+ `grammar_unavailable` **skip**, never a clean pass
97
+ (`clients/dispatch/runners/tree-sitter.ts`).
98
+
82
99
  ## Considered and skipped (2026-08-20 survey, closed out by #1757)
83
100
 
84
101
  Recorded so these are not re-litigated. The 2026-08-20 survey's first pass
@@ -9,7 +9,8 @@ confirmed against Neovim's LSP client, which sidesteps this entirely by being as
9
9
 
10
10
  Generate/refresh this matrix with `node scripts/characterize-lsp.mjs [--install]`
11
11
  (the `mode` column) and `node scripts/probe-clean-signal.mjs [--install]` (the
12
- `clean-behavior` column — 4-way: 2 / 2* / 3 / unknown). Both **merge** in place: a server the
12
+ `clean-behavior` column — 4-way: 2 / 2* / 3 / unknown — and the `first-publish`
13
+ column, #3310). Both **merge** in place: a server the
13
14
  running host couldn't spawn keeps its prior row, so an ubuntu-poor run can't
14
15
  regress a richer one (#390). The nightly **tool-smoke** workflow runs both (plus
15
16
  `server-capabilities.mjs`) and opens/updates a single auto-PR
@@ -27,6 +28,49 @@ anything). The native TS7 launch variant (`typescript7`/`typescript7-clean`,
27
28
  #524/#526) is deliberately excluded from comparison against classic's marker —
28
29
  they share a server id but not a verified clean-signal behavior.
29
30
 
31
+ What IS gated is the pair of COMMITTED sources:
32
+ `tests/config/lsp-clean-behavior-census.test.ts` (#3347) compares the
33
+ `clean-behavior` column below against the `silentOnClean` markers in both
34
+ directions — every measured push row against its marker, and every marker
35
+ against a push row measured `silent` — so a nightly refresh cannot merge a
36
+ re-measured server until its marker moves with it, and a marker cannot outlive
37
+ the measurement that justified it. Push rows the probe has not classified are
38
+ admitted by name in that file, and an admission reds once its row becomes
39
+ measured.
40
+
41
+ ## First publish: is the first push an answer? (#3310)
42
+
43
+ A second, ORTHOGONAL axis to the tiers below, measured from the same probe run
44
+ and written as the `first-publish` column:
45
+
46
+ | value | meaning | wait policy |
47
+ |---|---|---|
48
+ | `direct` | the first publish for a dirty file carries the findings | the first publish resolves the wait, as it always did |
49
+ | `empty-first` | the server answers `didOpen` with an EMPTY set while a one-time index builds, then publishes again once indexing ends | the client HOLDS that first empty publish (`emptyFirstPublish: "indexing"` in `clients/lsp/wait-policy/strategies.ts`); the server's own next publish releases it, and the hold is one-shot per client session |
50
+ | `empty-only` | every publish on the dirty fixture was empty | not classifiable on this axis — an undiagnosed dirty fixture looks exactly like an unfinished index, so nothing is inferred in either direction |
51
+ | `n/a (pull)` / `TBD` | pull-mode, or not measured on a host that could reach the server | no behavior change |
52
+
53
+ Why this axis had to exist at all: the wait needs a positive signal, and an
54
+ empty publish is a positive signal for a Tier 2/2\* server (a genuinely clean
55
+ file) and a MEANINGLESS one for a server that has not finished indexing. The two
56
+ are identical on the wire, so the difference is a measurement, not an inference.
57
+ intelephense is the measured member (2026-09-23, v1.18.5): `[]` at +295ms,
58
+ `indexingStarted` at +301ms — i.e. AFTER the empty publish, so no
59
+ work-in-progress signal is available at the moment the decision must be made —
60
+ `indexingEnded` at +689ms and the real 2-diagnostic set at +696ms. On a
61
+ genuinely clean file the same session publishes `[]` twice (at +304ms and again
62
+ at +663ms, right after indexing), which is what lets the held publish be
63
+ released by the server itself rather than by a timer; a WARM touch publishes the
64
+ real set first, so the hold costs nothing once the session is warm. pi-lens does
65
+ not advertise `window.workDoneProgress` (#974: advertising it crash-loops
66
+ opengrep's `--experimental` LSP mode), so `$/progress` is not an available
67
+ discriminator either.
68
+
69
+ `tests/config/lsp-first-publish-census.test.ts` compares this column against the
70
+ `emptyFirstPublish` markers and reds in BOTH directions, which is the expiry
71
+ check: when the nightly re-measures a server into a different class, the docs
72
+ refresh cannot merge until the marker is updated with it.
73
+
30
74
  ## The strategies
31
75
 
32
76
  | Tier | Signal | Affirmative clean? | Example |
@@ -48,7 +92,7 @@ the next touch probes again. Only silence through the full push budget latches
48
92
  that server id as navigation-only for the service session. A publish upgrades it
49
93
  to the ordinary push-wait policy.
50
94
 
51
- ## Matrix (dev box + CI nightly; mode last refreshed 2026-06-17 from run 27713958681, clean-behavior probed on the dev box 2026-07-08 — #460)
95
+ ## Matrix (dev box + CI nightly; mode last refreshed 2026-06-17 from run 27713958681, clean-behavior probed in run 35914033696 — #460)
52
96
 
53
97
  `mode` from cached capabilities; `clean-behavior` from the phase-aware publish-trace
54
98
  probe. The probe attributes publishes to two phases — the **dirty touch** (proves the
@@ -63,37 +107,49 @@ nightly steps, **dev** = the dev box (a row measured on both reads `dev+ci`).
63
107
  Merges never blank a prior good value, so a CI non-result leaves the dev
64
108
  classification standing.
65
109
 
66
- | lang | server | mode | clean-behavior | tier | src |
67
- |---|---|---|---|---|---|
68
- | json | vscode-json-language-server | pull | — | 1 | dev+ci |
69
- | css | vscode-css-language-server | pull | — | 1 | dev+ci |
70
- | html | vscode-html-language-server | pull | — | 1 | dev+ci |
71
- | rust | rust-analyzer | pull | — | 1 | dev |
72
- | svelte | svelte-language-server | pull | — | 1 | dev+ci |
73
- | deno | deno (alt of typescript) | pull | — | 1 | dev+ci |
74
- | ruby | ruby-lsp | pull | — | 1 | ci |
75
- | csharp | csharp-ls | pull | — | 1 | ci |
76
- | typescript | typescript-language-server | push-only | silent | 3 | dev+ci |
77
- | python | pyright | push-only | publishes-versioned | 2 | dev+ci |
78
- | jedi | jedi-language-server (alt of python) | push-only | publishes-versioned | 2 | ci |
79
- | yaml | yaml-language-server | push-only | publishes-unversioned | 2* | dev+ci |
80
- | shell | bash-language-server | push-only | publishes-versioned | 2 | dev+ci |
81
- | dockerfile | docker-langserver | push-only | publishes-unversioned | 2* | dev+ci |
82
- | toml | taplo | push-only | publishes-unversioned | 2* | dev+ci |
83
- | terraform | terraform-ls | push-only | TBD | 2/3? | dev+ci |
84
- | prisma | @prisma/language-server | push-only | publishes-unversioned | 2* | dev+ci |
85
- | php | intelephense | push-only | TBD | 2/3? | dev+ci |
86
- | zig | zls | push-only | publishes-unversioned | 2* | dev+ci |
87
- | vue | @vue/language-server | push-only | TBD | 2/3? | dev+ci |
88
- | dart | dart language-server | push-only | publishes-unversioned | 2* | ci |
89
- | gleam | gleam lsp | push-only | publishes-unversioned | 2* | ci |
90
- | clojure | clojure-lsp | push-only | publishes-unversioned | 2* | ci |
91
- | opengrep | opengrep (aux) | push-only | publishes-unversioned | 2* | dev+ci |
92
- | ast-grep | ast-grep (aux) | push-only | publishes-versioned | 2 | dev+ci |
110
+ `vue`'s `clean-behavior` was hand-reset to `unknown` (#3390): its
111
+ `publishes-unversioned` cell came from 58/45 publishes that the shared
112
+ `extension.log` window had attributed to vue but that belonged to `tinymist`.
113
+ With the sink scoped per server, nightly 36046209160 measured vue 0/0, and an
114
+ `unknown` result is never written by the merge above — so the refuted value had
115
+ to be cleared by hand. It stays `unknown` until a run observes
116
+ `@vue/language-server` publish; `tests/config/lsp-clean-behavior-census.test.ts`
117
+ carries the named admission until then.
118
+
119
+ | lang | server | mode | clean-behavior | first-publish | tier | src |
120
+ |---|---|---|---|---|---|---|
121
+ | json | vscode-json-language-server | pull | — | n/a (pull) | 1 | dev+ci |
122
+ | css | vscode-css-language-server | pull | — | n/a (pull) | 1 | dev+ci |
123
+ | html | vscode-html-language-server | pull | — | n/a (pull) | 1 | dev+ci |
124
+ | rust | rust-analyzer | pull | — | n/a (pull) | 1 | dev |
125
+ | svelte | svelte-language-server | pull | — | n/a (pull) | 1 | dev+ci |
126
+ | deno | deno (alt of typescript) | pull | — | n/a (pull) | 1 | dev+ci |
127
+ | ruby | ruby-lsp | pull | — | n/a (pull) | 1 | ci |
128
+ | csharp | csharp-ls | pull | — | n/a (pull) | 1 | ci |
129
+ | typescript | typescript-language-server | push-only | silent | direct | 3 | dev+ci |
130
+ | markdown | marksman | push-only | silent | direct | 3 | ci |
131
+ | lua | lua-language-server | push-only | silent | direct | 3 | dev+ci |
132
+ | python | pyright | push-only | publishes-versioned | direct | 2 | dev+ci |
133
+ | jedi | jedi-language-server (alt of python) | push-only | publishes-versioned | direct | 2 | ci |
134
+ | yaml | yaml-language-server | push-only | publishes-unversioned | direct | 2* | dev+ci |
135
+ | shell | bash-language-server | push-only | publishes-versioned | direct | 2 | dev+ci |
136
+ | dockerfile | docker-langserver | push-only | publishes-unversioned | direct | 2* | dev+ci |
137
+ | toml | taplo | push-only | publishes-unversioned | direct | 2* | dev+ci |
138
+ | terraform | terraform-ls | push-only | TBD | TBD | 2/3? | dev+ci |
139
+ | prisma | @prisma/language-server | push-only | publishes-unversioned | direct | 2* | dev+ci |
140
+ | php | intelephense | push-only | publishes-unversioned | empty-first | 2* | dev+ci |
141
+ | zig | zls | push-only | publishes-unversioned | direct | 2* | dev+ci |
142
+ | vue | @vue/language-server | push-only | unknown | direct | 2/3? | dev+ci |
143
+ | dart | dart language-server | push-only | publishes-unversioned | direct | 2* | ci |
144
+ | gleam | gleam lsp | push-only | publishes-unversioned | direct | 2* | ci |
145
+ | clojure | clojure-lsp | push-only | publishes-unversioned | direct | 2* | ci |
146
+ | opengrep | opengrep (aux) | push-only | publishes-unversioned | direct | 2* | dev+ci |
147
+ | ast-grep | ast-grep (aux) | push-only | publishes-versioned | direct | 2 | dev+ci |
148
+ | cue | CUE Language Server (cue lsp serve) | push-only | publishes-versioned | direct | 2 | dev+ci |
93
149
 
94
150
  **Unknown — fixture exists, mode not yet captured.** The toolchain-gated family
95
151
  (no auto-install today; tracked in #241) — `go` (gopls), `java` (jdtls),
96
- `kotlin`, `swift` (sourcekit-lsp), `lua`, `cpp` (clangd), `haskell`, `elixir`,
152
+ `kotlin`, `swift` (sourcekit-lsp), `cpp` (clangd), `haskell`, `elixir`,
97
153
  `ocaml`, `nix` (nixd), `fsharp`. Their servers don't install in the nightly, so
98
154
  characterize reports `unknown` (a non-failure ⚠). Once #241 lands they'll fill in
99
155
  the same way clojure-lsp/gleam now do (both auto-install via the github strategy
@@ -103,7 +159,8 @@ and were characterized `push-only` in the run above).
103
159
  - **Mode ≠ tier, and the split needs BOTH axes.** Push-only further splits along
104
160
  latency (does anything publish on a clean transition? — silence is the only
105
161
  budget-wait case, because pi-lens's publish handler emits and early-returns the
106
- wait on EVERY publish, versioned or not) and currency-proof (is the publish
162
+ wait on every publish, versioned or not, with the single #3310 exception of a
163
+ held empty FIRST publish from an `empty-first` server) and currency-proof (is the publish
107
164
  versioned, i.e. provably about the live edit?). The 4-way
108
165
  `probe-clean-signal.mjs` measurement drives this: ast-grep → 2, yaml/opengrep →
109
166
  2\*, typescript (clean file) → 3.
@@ -121,6 +178,16 @@ and were characterized `push-only` in the run above).
121
178
  fixtures for exactly this reason. Corollary: a 2\* measured only on a dirty
122
179
  fixture may overstate a server whose publishes stop when its set goes empty —
123
180
  langs without a clean fixture carry that caveat.
181
+ - **The probe's publish capture was dead, and the column looked alive anyway
182
+ (#3310).** `probe-clean-signal.mjs` intercepted `console.error` for the
183
+ `[lsp-pub]` trace, but #1333 moved that trace's sink to `extension.log` — so
184
+ every phase counted zero publishes, every server classified `unknown`, and the
185
+ #390 merge guard then preserved the July 2026 values verbatim. A dead
186
+ instrument and a healthy one are indistinguishable when non-results are
187
+ discarded by design. The probe now reads the sink the client actually writes;
188
+ `first-publish` measurements on the dev box, 2026-09-23: php `empty-first`,
189
+ typescript / opengrep / ast-grep / marksman `direct` (so the class does not
190
+ extend to them on the measurement, whatever their comments suggest).
124
191
  - **#458's learned-deadline target set = the tier-3 rows only.** 2\* rows resolve
125
192
  the wait at runtime and must NOT be given learned deadlines.
126
193
  - **Tier 3 is budget-bound by necessity**, not laziness: a silent server's silence is
@@ -9,8 +9,48 @@
9
9
  - Preserve contributor authorship and leave Git authority to the orchestrator
10
10
  unless the delegation grants it explicitly.
11
11
 
12
+ ## Standing procedure
13
+
14
+ ### Standard mechanics
15
+
16
+ - Commit the code as soon as the targeted suite is green, then add evidence in
17
+ a later commit. A worker can be settled mid-evidence-pass; on PR #3268 the
18
+ orchestrator had to commit the tree.
19
+ - A whole-module `vi.mock` of a production module must spread `importOriginal`.
20
+ Run `tests/config/vi-mock-export-sweep.test.ts`.
21
+ - Exact-pin sweeps and merge state: `tests/config/glossary-synonym-sweep.test.ts`
22
+ (#3279) pins the live retired-synonym identifier population per (term, file)
23
+ exactly, in both directions. If a change adds or removes one of the pinned
24
+ identifier uses, run the sweep on the head and on the merge of
25
+ `origin/master` + head before pushing, then re-pin in the same PR using the
26
+ sweep's own `UNPINNED`/`STALE` output. On 2026-09-23, two green PRs merged
27
+ red (#3279's pins predated #3283, leaving master red until #3288), and #3284
28
+ was red on its own `path` count changes (cue-vet 5→6, dart-analyze 6→4)
29
+ until an orchestrator trailing commit re-pinned.
30
+
31
+ ### Failure list before code
32
+
33
+ Before the first edit of any fix, write the list of ways the change could fail
34
+ (the directions the mutation table will later prove) in the PR body. The
35
+ mutation table is that list with transcripts, never a list invented after the
36
+ code. This week's evidence: #3252 r1 shipped an exit table whose inverse
37
+ direction (nonzero WITH findings) was never listed and was caught by the
38
+ reviewer.
39
+
40
+ Seams are named in the brief before the round; no test is written at an
41
+ unconfirmed seam — a fixer that needs a new seam stops and reports it as a
42
+ finding, not as a test.
43
+
44
+ A fix round does not deepen: no refactor, no helper extraction, no rename
45
+ beyond the fix's own lines; deepening is its own slice under the owning
46
+ umbrella. #3254 and #3256 stayed inside their briefs; #3178's four rounds show
47
+ the cost of not doing so.
48
+
12
49
  ## Evidence
13
50
 
51
+ - Witness rule (ADR 0007): #1605 owns the witness lanes, and their fixtures
52
+ live under `tests/fixtures/witness/<slice>/`.
53
+
14
54
  - Add a regression test through the production path.
15
55
  - Capture the pre-fix assertion failure.
16
56
  - Prove the fixed test passes.
@@ -47,9 +87,23 @@
47
87
 
48
88
  - Without Git authority, leave changes uncommitted.
49
89
  - Write root-level `PR_BODY.md` and `COMMIT_MSG.txt`; keep both untracked.
50
- - PR body headings: `## Summary`, `## Tests`, `## Blast radius`,
51
- `## Class sweep`, `## Observability`, and `## Test assessment` when tests
52
- changed.
90
+ - The PR body is the whole `.github/PULL_REQUEST_TEMPLATE.md`, every
91
+ heading present in order: `## Why` (one sentence), `## Notes for the
92
+ reviewer`, `## Change outline`, `## Summary`, `## Type of change`,
93
+ `## Area`, `## Checklist`, `## Tests`, `## Blast radius`,
94
+ `## Observability` (a record literal from the runtime diff, or exactly
95
+ `No new failure path; no record added.`), `## Class sweep`, and
96
+ `## Test assessment`. A brief that names only some headings does not
97
+ shorten this list.
98
+ - Run `node scripts/check-pr-body.mjs --lint-local PR_BODY.md` and
99
+ `node scripts/check-changelog-fragments.mjs` before the hand-back; both
100
+ must pass, and the hand-back quotes them. A changelog fragment is
101
+ `---` / `section: <Added|Changed|Deprecated|Removed|Fixed|Security>` /
102
+ `---` / blank / one `- ` bullet. (Four of six Luna PRs on 2026-09-23
103
+ redded the PR-body and changelog gates on the first head; the fixes were
104
+ all mechanical.)
105
+ - The hand-back carries the commit SHA; a dirty tree is an incomplete
106
+ round.
53
107
  - Include every red, mutation result, skipped check, and environment block.
54
108
  - Answer each finding id with `fixed`, `not fixed`, or `withdrawn (reason)`.
55
109
  - Report verdict, changed files, totals, and unverifiable checks.
@@ -26,6 +26,15 @@
26
26
 
27
27
  ## Evidence rules
28
28
 
29
+ - **Already-shipped check before naming a slice:** for every umbrella member the
30
+ brief cites as remaining work, grep the current tree and the closing PRs and
31
+ state shipped, partially shipped, or not shipped with `file:line`; a first
32
+ slice may name only work whose absence was verified on the current head.
33
+ The 2026-09-22 primitives brief named three scanner filter blocks as the
34
+ #1461 first slice even though #1622/#1625/#1628 had already removed them;
35
+ the fixer found the premise error in PR #3264 and the #1892 comment on
36
+ 2026-09-23.
37
+
29
38
  - A reported defect is not confirmed until the production-path probe is red on
30
39
  the current tree for the reported reason.
31
40
  - A probe must distinguish competing hypotheses, use independent observations,