@herbertgao/pi-extensions 2026.9.8 → 2026.9.9

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 (94) hide show
  1. package/README.md +8 -4
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  7. package/node_modules/@herbertgao/resume-from/package.json +2 -2
  8. package/node_modules/@narumitw/pi-btw/README.md +6 -1
  9. package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  11. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  12. package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
  13. package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
  14. package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
  15. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
  16. package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
  17. package/node_modules/pi-jev-auto-mode/README.md +285 -0
  18. package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
  19. package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
  20. package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
  21. package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
  22. package/node_modules/pi-jev-auto-mode/index.ts +1 -0
  23. package/node_modules/pi-jev-auto-mode/package.json +72 -0
  24. package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
  25. package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
  26. package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
  27. package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
  28. package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
  29. package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
  30. package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
  31. package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
  32. package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
  33. package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
  34. package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
  35. package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
  36. package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
  37. package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
  38. package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
  39. package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
  40. package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
  41. package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
  42. package/node_modules/pi-lens/CHANGELOG.md +45 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
  44. package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
  45. package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
  46. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
  47. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
  48. package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
  49. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
  50. package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
  51. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
  52. package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
  53. package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
  54. package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
  55. package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
  56. package/node_modules/pi-lens/dist/index.js +729 -344
  57. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
  58. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
  59. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
  60. package/node_modules/pi-lens/package.json +1 -1
  61. package/node_modules/pi-typesafe/LICENSE +21 -0
  62. package/node_modules/pi-typesafe/README.md +153 -0
  63. package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
  64. package/node_modules/pi-typesafe/dist/ask.js +25 -0
  65. package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
  66. package/node_modules/pi-typesafe/dist/auth.js +114 -0
  67. package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
  68. package/node_modules/pi-typesafe/dist/batch.js +116 -0
  69. package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
  70. package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
  71. package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
  72. package/node_modules/pi-typesafe/dist/client.js +188 -0
  73. package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
  74. package/node_modules/pi-typesafe/dist/credentials.js +112 -0
  75. package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
  76. package/node_modules/pi-typesafe/dist/errors.js +31 -0
  77. package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
  78. package/node_modules/pi-typesafe/dist/extension.js +222 -0
  79. package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
  80. package/node_modules/pi-typesafe/dist/index.js +9 -0
  81. package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
  82. package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
  83. package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
  84. package/node_modules/pi-typesafe/dist/login.js +38 -0
  85. package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
  86. package/node_modules/pi-typesafe/dist/schema.js +152 -0
  87. package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
  88. package/node_modules/pi-typesafe/dist/ui.js +3 -0
  89. package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
  90. package/node_modules/pi-typesafe/dist/usage.js +182 -0
  91. package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
  92. package/node_modules/pi-typesafe/extensions/index.js +2 -0
  93. package/node_modules/pi-typesafe/package.json +89 -0
  94. package/package.json +15 -8
@@ -9,6 +9,7 @@ import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
9
9
  import { fitLine } from "./tui-fit.js";
10
10
  import { WriteOrderingGuard } from "./write-ordering-guard.js";
11
11
  import { collectForwardImportMtimes } from "./blocker-freshness.js";
12
+ import { normalizeMessage } from "./finding-identity.js";
12
13
  import { freshnessFromMtime } from "./freshness.js";
13
14
  import { PAST_EOF_STALE_MARKER } from "./diagnostic-line-freshness.js";
14
15
  import { STALE_LINE_MARKER } from "./stale-marker.js";
@@ -49,6 +50,19 @@ function fileMapKey(filePath) {
49
50
  * from what the footer counts.
50
51
  */
51
52
  export function isBlocking(d) {
53
+ // #3158 round 2 F1: a finding the agent marked `false-positive`/`suppress` is
54
+ // not a hard stop — it is an explicitly dismissed one. Before this line only
55
+ // `countDiagnostics` knew that (it skips tagged entries), so the footer header
56
+ // counted zero errors while every consumer that asks THIS predicate still
57
+ // treated the row as blocking: `renderWidget` drew a red ● row for it,
58
+ // `capStoredDiagnostics` hoisted it ahead of live findings (evicting one at the
59
+ // display cap), and `getWidgetBlockingFilesForSweep`,
60
+ // `markWidgetFileBlockersStale` and `reconcileStaleWidgetDependencyBlockers`
61
+ // admitted it to the turn-end blocker sweep. The mismatch was latent while a
62
+ // tagged row lived only between a mark and the next scan; retention makes it
63
+ // durable, so the predicate has to answer for the tag.
64
+ if (d.disposition)
65
+ return false;
52
66
  // #1631: a dependency-drift-demoted finding is no longer a hard stop. The gate
53
67
  // sets `stale` rather than dropping the entry (#1419 demote-not-drop), so every
54
68
  // tally and render that asks "is this blocking?" must answer no once demoted.
@@ -82,6 +96,20 @@ const diagnosticsWriteGuard = new WriteOrderingGuard();
82
96
  */
83
97
  const runnerWriteGuard = new WriteOrderingGuard();
84
98
  const MAX_STORED_DIAGNOSTICS_PER_FILE = 12;
99
+ /**
100
+ * #3158 / AGENTS.md shape 9: the per-file bound on
101
+ * {@link WidgetDiagnostic.suppressedRetained} rows. Retention is the only rule
102
+ * that lets a record hold findings the latest scan did not report, so it is the
103
+ * only axis of `allDiagnostics` that can grow while the live set shrinks — every
104
+ * other row is replaced wholesale by the next write. Its own constant rather
105
+ * than `MAX_STORED_DIAGNOSTICS_PER_FILE`: that one is the TUI's display cap
106
+ * (`capStoredDiagnostics`) and the two would drift the moment either policy
107
+ * moved. Twelve because the chip is a COUNT, not a list — past a dozen
108
+ * suppressed findings on one unchanged file the exact total stops informing the
109
+ * footer, and the truncation is stated once per file through the ledger rather
110
+ * than dropped silently.
111
+ */
112
+ const MAX_RETAINED_SUPPRESSED_PER_FILE = 12;
85
113
  const MAX_INACTIVE_FILE_RECORDS = 1024;
86
114
  const ACTIVE_FILE_IDLE_MS = 30 * 60_000;
87
115
  export const MAX_LSP_SERVER_RECORDS = 128;
@@ -140,24 +168,34 @@ export function reconcileWidgetDisposition(cwd, target, _disposition, content) {
140
168
  }
141
169
  }
142
170
  const active = new Set(applyDispositions(current, cwd, target.filePath, source));
143
- const normalized = current.map((diagnostic) => {
171
+ const normalized = current.flatMap((diagnostic) => {
144
172
  const { strict, weak } = anchorsForDiagnostic(cwd, target.filePath, diagnostic, source);
145
173
  const entry = getDisposition(cwd, strict) ?? getDisposition(cwd, weak);
146
174
  if (!active.has(diagnostic) &&
147
175
  (entry?.disposition === "false-positive" ||
148
176
  entry?.disposition === "suppress")) {
149
- return { ...diagnostic, disposition: entry.disposition, flagged: false };
177
+ return [
178
+ { ...diagnostic, disposition: entry.disposition, flagged: false },
179
+ ];
150
180
  }
181
+ // #3158 retirement rule 3: a row the store holds ONLY because it was
182
+ // suppressed (`suppressedRetained`) has no live scan behind it — the last
183
+ // scan of this file did not report it. Once its mark stops suppressing it
184
+ // (a strict `false-positive` anchor whose marked line changed), retire it
185
+ // rather than re-arming it below as a finding the agent would read as
186
+ // currently present.
187
+ if (diagnostic.suppressedRetained)
188
+ return [];
151
189
  const { disposition: _disposition, flagged: _flagged, ...baseDiagnostic } = diagnostic;
152
190
  return entry?.disposition === "flagged"
153
- ? { ...baseDiagnostic, flagged: true }
154
- : baseDiagnostic;
191
+ ? [{ ...baseDiagnostic, flagged: true }]
192
+ : [baseDiagnostic];
155
193
  });
156
194
  const rec = getOrCreate(target.filePath);
157
195
  const writeIndex = admitWidgetDiagnosticsWrite(target.filePath);
158
196
  if (!diagnosticsWriteGuard.shouldWrite(fileMapKey(target.filePath), writeIndex))
159
197
  return;
160
- commitDiagnostics(rec, target.filePath, normalized, Date.now());
198
+ commitDiagnostics(rec, target.filePath, normalized, Date.now(), fileMapKey(target.filePath), false);
161
199
  }
162
200
  /** Reserve a widget write for a producer that has no runtime write index. */
163
201
  export function admitWidgetDiagnosticsWrite(filePath, writeIndex) {
@@ -473,11 +511,94 @@ function normalizeDiagnostics(filePath, diagnostics, observedTs) {
473
511
  };
474
512
  });
475
513
  }
514
+ /**
515
+ * The identity a retained suppressed row is matched against a later scan's
516
+ * entries by (#3158), so a scan that reports the finding AGAIN replaces the
517
+ * retained row instead of standing beside it as a duplicate.
518
+ *
519
+ * These are the WEAK ANCHOR's identity parts — `computeWeakAnchor`
520
+ * (`clients/diagnostic-dispositions.ts`) hashes `[tool, rule,
521
+ * normalizeMessage(message)]` over the file — deliberately, because the weak
522
+ * anchor is how the disposition store itself decides which findings a mark
523
+ * covers. Matching at any finer granularity means the store can disagree with
524
+ * the mark that created the row.
525
+ *
526
+ * Round 2 F2: `line` USED to be part of it, and that was the finer granularity
527
+ * the paragraph above forbids. A writer that re-reports the marked finding one
528
+ * line down — `clients/pipeline.ts`'s per-edit `recordDiagnostics`, or
529
+ * `reconcileCascadeNeighborLspErrors`, neither of which runs
530
+ * `reconcileStaleWidgetFiles` — left the retained row standing beside the live
531
+ * one, so a single finding was counted live AND in `suppressed: N`.
532
+ *
533
+ * `computeWeakAnchor` itself is not called here: it needs `cwd` to build its
534
+ * path component, which `commitDiagnostics` does not have and could only get by
535
+ * widening `recordDiagnostics`, `reconcileScanDiagnostics` and
536
+ * `reconcileCorrelatedScanDiagnostics` up through `tools/`. That component is
537
+ * constant across one file record — both sides of this comparison are the same
538
+ * file — so it would discriminate nothing. `normalizeMessage` is the shared
539
+ * derivation from `clients/finding-identity.ts` that the anchor uses, imported
540
+ * rather than re-spelled.
541
+ */
542
+ function retentionIdentity(d) {
543
+ return JSON.stringify([
544
+ d.tool ?? "",
545
+ d.rule ?? "",
546
+ normalizeMessage(d.message),
547
+ ]);
548
+ }
549
+ /**
550
+ * #3158: carry the record's disposition-tagged rows across a whole-replace
551
+ * write when the incoming set no longer reports them, so a `false-positive` /
552
+ * `suppress` mark keeps contributing to the footer's `suppressed: N` chip. See
553
+ * {@link WidgetDiagnostic.suppressedRetained} for the full lifetime statement.
554
+ *
555
+ * Retained rows are APPENDED after the incoming ones: `capStoredDiagnostics`
556
+ * fills the TUI's display list from the front, so a retained row never evicts a
557
+ * live finding from the rendered set.
558
+ *
559
+ * The tagged-row check comes FIRST so a project with no disposition marks — the
560
+ * overwhelmingly common case, and this runs on the per-edit write path — pays
561
+ * one predicate per stored row and never builds the identity set.
562
+ */
563
+ function retainSuppressedRows(previous, incoming, filePath) {
564
+ const tagged = previous.filter((d) => d.disposition !== undefined);
565
+ if (tagged.length === 0)
566
+ return incoming;
567
+ const reported = new Set(incoming.map(retentionIdentity));
568
+ const retained = tagged.filter((d) => !reported.has(retentionIdentity(d)));
569
+ if (retained.length > MAX_RETAINED_SUPPRESSED_PER_FILE) {
570
+ // Freshest observations win the cap. One record per FILE, never one per
571
+ // dropped row (AGENTS.md "bounded observability"): the chip under-counts
572
+ // by exactly `dropped` for this file until its content changes.
573
+ retained.sort((a, b) => (b.observedAt ?? 0) - (a.observedAt ?? 0));
574
+ recordDegradationOnce({
575
+ kind: "widget-suppressed-retention-capped",
576
+ subject: filePath,
577
+ reason: `${retained.length - MAX_RETAINED_SUPPRESSED_PER_FILE} suppressed row(s) beyond the ${MAX_RETAINED_SUPPRESSED_PER_FILE}-row per-file retention cap are no longer counted by the footer chip`,
578
+ });
579
+ retained.length = MAX_RETAINED_SUPPRESSED_PER_FILE;
580
+ }
581
+ return [
582
+ ...incoming,
583
+ ...retained.map((d) => d.suppressedRetained ? d : { ...d, suppressedRetained: true }),
584
+ ];
585
+ }
476
586
  /** Store `normalized` as the record's complete diagnostic set: recompute counts,
477
587
  * cap the display list, stamp `touchedAt` (at `observedAt` when given, else now
478
588
  * — #1093), persist, and re-render. The caller decides what `normalized`
479
589
  * contains (a full replace, or a merge — see `reconcileCascadeNeighborLspErrors`). */
480
- function commitDiagnostics(rec, filePath, normalized, observedAt, key = fileMapKey(filePath)) {
590
+ function commitDiagnostics(rec, filePath, incoming, observedAt, key = fileMapKey(filePath),
591
+ /**
592
+ * #3158: apply the suppressed-row retention rule to this write. True for
593
+ * every seam that hands in a fresh SCAN of the file (`recordDiagnostics` and
594
+ * both reconciles), false for `reconcileWidgetDisposition`, which re-maps the
595
+ * record's own current rows and is the seam that RETIRES a retained row —
596
+ * re-adding what it just dropped would make retirement rule 3 inert.
597
+ */
598
+ retainSuppressed = true) {
599
+ const normalized = retainSuppressed
600
+ ? retainSuppressedRows(rec.allDiagnostics, incoming, filePath)
601
+ : incoming;
481
602
  rec.diagnosticCounts = countDiagnostics(normalized);
482
603
  rec.diagnostics = capStoredDiagnostics(normalized);
483
604
  rec.allDiagnostics = normalized;
@@ -1079,6 +1200,31 @@ export function getFileDiagnosticSummaries() {
1079
1200
  };
1080
1201
  });
1081
1202
  }
1203
+ /**
1204
+ * How many {@link WidgetDiagnostic.suppressedRetained} rows this file's record
1205
+ * is currently carrying (#3158 round 2 F4).
1206
+ *
1207
+ * The success-path counterpart of `widget-suppressed-retention-capped`: the
1208
+ * ledger only hears about retention when the per-file cap TRUNCATES it, so
1209
+ * nothing observed the healthy case. `tools/lsp-diagnostics.ts` folds this into
1210
+ * the `lsp_probe_disposition_filter` phase it already emits once per filtered
1211
+ * file per scan, so the count is bounded by that record's existing cardinality
1212
+ * — never one row per retained finding.
1213
+ *
1214
+ * Deliberately not {@link getFileDiagnostics}: that applies the past-EOF gate,
1215
+ * which can `stat` the file. This is a map lookup plus a count over rows the
1216
+ * caller's own scan just filtered, so it adds no I/O to the probe path.
1217
+ */
1218
+ export function countRetainedSuppressedRows(filePath) {
1219
+ const rec = files.get(fileMapKey(filePath));
1220
+ if (!rec)
1221
+ return 0;
1222
+ let n = 0;
1223
+ for (const d of rec.allDiagnostics)
1224
+ if (d.suppressedRetained)
1225
+ n += 1;
1226
+ return n;
1227
+ }
1082
1228
  /**
1083
1229
  * Return the current FULL (uncapped) diagnostic set for a single file, as
1084
1230
  * last recorded by {@link recordDiagnostics} — the same `allDiagnostics`