@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.
- package/README.md +8 -4
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
- package/node_modules/@herbertgao/resume-from/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +6 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +209 -23
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +1 -12
- package/node_modules/@narumitw/pi-btw/src/menu.ts +244 -15
- package/node_modules/@narumitw/pi-btw/src/settings.ts +5 -0
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
- package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
- package/node_modules/pi-jev-auto-mode/README.md +285 -0
- package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
- package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
- package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
- package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
- package/node_modules/pi-jev-auto-mode/index.ts +1 -0
- package/node_modules/pi-jev-auto-mode/package.json +72 -0
- package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
- package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
- package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
- package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
- package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
- package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
- package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
- package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
- package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
- package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
- package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
- package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
- package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
- package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
- package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
- package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
- package/node_modules/pi-lens/CHANGELOG.md +45 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
- package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
- package/node_modules/pi-lens/dist/index.js +729 -344
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-typesafe/LICENSE +21 -0
- package/node_modules/pi-typesafe/README.md +153 -0
- package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
- package/node_modules/pi-typesafe/dist/ask.js +25 -0
- package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
- package/node_modules/pi-typesafe/dist/auth.js +114 -0
- package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
- package/node_modules/pi-typesafe/dist/batch.js +116 -0
- package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
- package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
- package/node_modules/pi-typesafe/dist/client.js +188 -0
- package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
- package/node_modules/pi-typesafe/dist/credentials.js +112 -0
- package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
- package/node_modules/pi-typesafe/dist/errors.js +31 -0
- package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/extension.js +222 -0
- package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
- package/node_modules/pi-typesafe/dist/index.js +9 -0
- package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
- package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
- package/node_modules/pi-typesafe/dist/login.js +38 -0
- package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
- package/node_modules/pi-typesafe/dist/schema.js +152 -0
- package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
- package/node_modules/pi-typesafe/dist/ui.js +3 -0
- package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
- package/node_modules/pi-typesafe/dist/usage.js +182 -0
- package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
- package/node_modules/pi-typesafe/extensions/index.js +2 -0
- package/node_modules/pi-typesafe/package.json +89 -0
- 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.
|
|
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
|
|
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,
|
|
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`
|