@herbertgao/pi-extensions 2026.9.7 → 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/grok-mermaid/CHANGELOG.md +46 -0
- package/node_modules/grok-mermaid/LICENSE +205 -0
- package/node_modules/grok-mermaid/README.md +191 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
- package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/ansi.js +23 -0
- package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/canvas.js +366 -0
- package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
- package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/graph.js +91 -0
- package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
- package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
- package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/index.js +100 -0
- package/node_modules/grok-mermaid/dist/index.js.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
- package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/labels.js +324 -0
- package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
- package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
- package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
- package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/layout.js +881 -0
- package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
- package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/parse.js +1151 -0
- package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/source-box.js +78 -0
- package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
- package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
- package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/types.js +1 -0
- package/node_modules/grok-mermaid/dist/types.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
- package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width-data.js +994 -0
- package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
- package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
- package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
- package/node_modules/grok-mermaid/dist/width.js +76 -0
- package/node_modules/grok-mermaid/dist/width.js.map +1 -0
- package/node_modules/grok-mermaid/package.json +49 -0
- package/node_modules/grok-mermaid/src/ansi.ts +34 -0
- package/node_modules/grok-mermaid/src/canvas.ts +373 -0
- package/node_modules/grok-mermaid/src/graph.ts +142 -0
- package/node_modules/grok-mermaid/src/index.ts +104 -0
- package/node_modules/grok-mermaid/src/labels.ts +326 -0
- package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
- package/node_modules/grok-mermaid/src/layout.ts +1015 -0
- package/node_modules/grok-mermaid/src/parse.ts +1189 -0
- package/node_modules/grok-mermaid/src/source-box.ts +89 -0
- package/node_modules/grok-mermaid/src/types.ts +43 -0
- package/node_modules/grok-mermaid/src/width-data.ts +993 -0
- package/node_modules/grok-mermaid/src/width.ts +74 -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 +16 -8
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3170 — the bounded persistent-reverify pass.
|
|
3
|
+
*
|
|
4
|
+
* A finding whose own file is unchanged is re-delivered turn after turn from
|
|
5
|
+
* the persisted actionable-warnings report without ever being re-observed
|
|
6
|
+
* against the live server: the in-band publish carries forward DEFERRED-origin
|
|
7
|
+
* file entries while their file has not moved
|
|
8
|
+
* (`mergeActionableWarningsReports`'s scope guard), and the delta freshness
|
|
9
|
+
* gate passes them because the file's mtime has not moved either. When the
|
|
10
|
+
* root cause was fixed in a DIFFERENT file, the carried finding is stale in
|
|
11
|
+
* fact but fresh by every on-disk axis.
|
|
12
|
+
*
|
|
13
|
+
* This module re-observes those files at turn_end, BEFORE the advisory
|
|
14
|
+
* assembles, and the replacement entries fold into the turn's SINGLE in-band
|
|
15
|
+
* publish (#3176 F1: a separate replacement publish spends the carry marker
|
|
16
|
+
* and the main publish then drops the entries at the scope guard — the
|
|
17
|
+
* blocker the review caught).
|
|
18
|
+
*
|
|
19
|
+
* Review-round contracts (#3176):
|
|
20
|
+
* - **F2**: the fresh observation converts through the PRODUCER's own
|
|
21
|
+
* pipeline (`enrichFileFromLsp` — severity-2, code-action enrichment,
|
|
22
|
+
* fixable-only, the per-file caps), fed the touch's collected diagnostics
|
|
23
|
+
* as the cached arm so no second pull happens. No re-implementation.
|
|
24
|
+
* - **F3**: an empty result is clean ONLY when the touch itself answers
|
|
25
|
+
* `confirmation: "confirmed"` — the silent-on-clean rule (#240/#533/#1253).
|
|
26
|
+
* A bare `{diags: []}` (the house double's silent empty, a tier-3 server
|
|
27
|
+
* still analyzing, a `skipReason`) is UNCONFIRMED: the carried warnings are
|
|
28
|
+
* kept verbatim and the render labels the gap. `touched.inconclusive` is
|
|
29
|
+
* `resolveTouchVerdict`'s output computed service-side and is honored
|
|
30
|
+
* directly.
|
|
31
|
+
* - **F4**: every await is bound-wrapped (`bounded()`); `maxClientWaitMs` is
|
|
32
|
+
* only the spawn/ready budget, not a request ceiling.
|
|
33
|
+
* - **F6**: no skip guard — every deferred-origin entry re-verifies each
|
|
34
|
+
* turn (re-arm), per the issue's failure table.
|
|
35
|
+
*
|
|
36
|
+
* Bounds: at most {@link MAX_REVERIFY_FILES} files per turn_end, a wall
|
|
37
|
+
* budget of {@link REVERIFY_BUDGET_MS}, the turn-end abort signal, and no
|
|
38
|
+
* new durable state — candidates come from the persisted report itself,
|
|
39
|
+
* which is what was delivered last turn.
|
|
40
|
+
*/
|
|
41
|
+
import * as fs from "node:fs";
|
|
42
|
+
import { enrichFileFromLsp } from "./actionable-warnings.js";
|
|
43
|
+
import { bounded } from "./deadline-utils.js";
|
|
44
|
+
import { logLatency } from "./latency-logger.js";
|
|
45
|
+
import { touchCompletedConfirmationPolicy, } from "./lsp/diagnostic-binding.js";
|
|
46
|
+
import { findAuxiliaryProfileForSource } from "./dispatch/auxiliary-lsp.js";
|
|
47
|
+
/** At most this many files are re-observed per turn_end (the drift backstop's
|
|
48
|
+
* own 4-resyncs-per-pass precedent). */
|
|
49
|
+
export const MAX_REVERIFY_FILES = 4;
|
|
50
|
+
/** Wall-clock budget for the whole pass; a touch that cannot answer inside
|
|
51
|
+
* its remaining slice is recorded unconfirmed, never waited out. */
|
|
52
|
+
export const REVERIFY_BUDGET_MS = 3000;
|
|
53
|
+
/** The touch options the `source=lsp` probe uses, with this pass's source. */
|
|
54
|
+
const TOUCH_SOURCE = "persistent_reverify";
|
|
55
|
+
/**
|
|
56
|
+
* Select the report's re-verify candidates: DEFERRED-origin file entries (the
|
|
57
|
+
* only population the in-band publish carries forward, so the only population
|
|
58
|
+
* that re-serves across turns — F6: with no skip guard, so every carried
|
|
59
|
+
* entry re-verifies each turn) whose file stat is UNCHANGED since the entry's
|
|
60
|
+
* own observation stamp. A file that changed is skipped and counted — the
|
|
61
|
+
* edit path already re-observed it. Missing files are skipped too: the delta
|
|
62
|
+
* freshness gate drops them outright.
|
|
63
|
+
*/
|
|
64
|
+
export function selectPersistentReverifyFiles(report, nowMs = Date.now()) {
|
|
65
|
+
const candidates = [];
|
|
66
|
+
let skippedChanged = 0;
|
|
67
|
+
for (const entry of report.files) {
|
|
68
|
+
if (entry.origin !== "deferred")
|
|
69
|
+
continue;
|
|
70
|
+
// F3 (population half): the pass re-observes through a PRIMARY-scope
|
|
71
|
+
// touch, so an auxiliary-lane finding can never be re-confirmed — an
|
|
72
|
+
// entry carrying aux-sourced warnings is skipped (kept verbatim),
|
|
73
|
+
// never scored dropped. The aux lookup is the one shared seam.
|
|
74
|
+
if (entry.warnings.some((w) => findAuxiliaryProfileForSource(w.source ?? "") !== undefined)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const stamp = entry.generatedAt ? Date.parse(entry.generatedAt) : NaN;
|
|
78
|
+
if (!Number.isFinite(stamp) || stamp > nowMs)
|
|
79
|
+
continue;
|
|
80
|
+
let mtimeMs;
|
|
81
|
+
try {
|
|
82
|
+
mtimeMs = fs.statSync(entry.filePath).mtimeMs;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (mtimeMs > stamp) {
|
|
88
|
+
skippedChanged += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (candidates.length < MAX_REVERIFY_FILES)
|
|
92
|
+
candidates.push(entry);
|
|
93
|
+
}
|
|
94
|
+
return { candidates, skippedChanged };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run the bounded re-verify pass over the report's candidates and build the
|
|
98
|
+
* replacement entries for the caller's single in-band publish. Never throws:
|
|
99
|
+
* an unexpected per-file failure degrades that file to `unconfirmed` (kept
|
|
100
|
+
* verbatim, labeled), which is the shape-48 direction — the harm reaching the
|
|
101
|
+
* user is a hidden real finding, worse than re-delivering a stale one.
|
|
102
|
+
*/
|
|
103
|
+
export async function runPersistentReverify(args) {
|
|
104
|
+
const started = Date.now();
|
|
105
|
+
const deadlineAt = started + (args.budgetMs ?? REVERIFY_BUDGET_MS);
|
|
106
|
+
const nowMs = args.nowMs ?? started;
|
|
107
|
+
const { candidates, skippedChanged } = selectPersistentReverifyFiles(args.report, nowMs);
|
|
108
|
+
const outcomes = [];
|
|
109
|
+
const replacementFiles = [];
|
|
110
|
+
let touched = 0;
|
|
111
|
+
// F2: the producer's own pipeline (enrichFileFromLsp) — constructed once,
|
|
112
|
+
// reused per candidate. deltaOnly: false (a re-observation is not gated on
|
|
113
|
+
// this turn's modified ranges — the file was not edited).
|
|
114
|
+
const buildArgs = {
|
|
115
|
+
cwd: args.cwd,
|
|
116
|
+
sessionId: args.report.sessionId,
|
|
117
|
+
turnIndex: 0,
|
|
118
|
+
files: [],
|
|
119
|
+
modifiedRangesByFile: new Map(),
|
|
120
|
+
dispatchWarnings: [],
|
|
121
|
+
includeLspCodeActions: true,
|
|
122
|
+
deltaOnly: false,
|
|
123
|
+
...(args.signal !== undefined ? { signal: args.signal } : {}),
|
|
124
|
+
lspBudgetMs: REVERIFY_BUDGET_MS,
|
|
125
|
+
dbg: () => { },
|
|
126
|
+
};
|
|
127
|
+
// R3-4: every candidate the budget or the abort cut, so the phase row
|
|
128
|
+
// distinguishes "the pass ran out of time" from "the server answered
|
|
129
|
+
// nothing". Counted from what the loop actually reached, never predicted.
|
|
130
|
+
let processed = 0;
|
|
131
|
+
for (const entry of candidates) {
|
|
132
|
+
if (args.signal?.aborted || Date.now() >= deadlineAt)
|
|
133
|
+
break;
|
|
134
|
+
processed += 1;
|
|
135
|
+
let content;
|
|
136
|
+
try {
|
|
137
|
+
content = fs.readFileSync(entry.filePath, "utf-8");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
outcomes.push({
|
|
141
|
+
filePath: entry.filePath,
|
|
142
|
+
displayPath: entry.displayPath,
|
|
143
|
+
outcome: "unconfirmed",
|
|
144
|
+
dropped: 0,
|
|
145
|
+
kept: entry.warnings.length,
|
|
146
|
+
});
|
|
147
|
+
replacementFiles.push({ ...entry, reVerifyIncomplete: true });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// F4: the touch is bound-wrapped — `maxClientWaitMs` bounds the
|
|
151
|
+
// spawn/ready window, `bounded()` is the request ceiling.
|
|
152
|
+
let touchedResult;
|
|
153
|
+
try {
|
|
154
|
+
touchedResult = await bounded(args.lspService.touchFile(entry.filePath, content, {
|
|
155
|
+
diagnostics: "document",
|
|
156
|
+
collectDiagnostics: true,
|
|
157
|
+
source: TOUCH_SOURCE,
|
|
158
|
+
clientScope: "primary",
|
|
159
|
+
}), {
|
|
160
|
+
ms: Math.max(250, deadlineAt - Date.now()),
|
|
161
|
+
signal: args.signal,
|
|
162
|
+
hook: "turn_end",
|
|
163
|
+
label: "persistent_reverify_touch",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// The never-throws contract: a wedged touch degrades to unconfirmed
|
|
168
|
+
// (kept verbatim, labeled), never an escaped error.
|
|
169
|
+
touchedResult = undefined;
|
|
170
|
+
}
|
|
171
|
+
// F3 (#3176): an empty result is clean ONLY when the touch COMPLETED
|
|
172
|
+
// its confirmation policy — the silent-on-clean rule (#240/#533/#1253).
|
|
173
|
+
// A bare `{diags: []}` (the house double's silent empty, a tier-3
|
|
174
|
+
// server still analyzing) or a `skipReason` is UNCONFIRMED: the carried
|
|
175
|
+
// warnings are kept verbatim and the render labels the gap.
|
|
176
|
+
// `touched.inconclusive` is resolveTouchVerdict's output, computed
|
|
177
|
+
// service-side, and is honored directly.
|
|
178
|
+
// R3-3: read the policy through `touchCompletedConfirmationPolicy`, the
|
|
179
|
+
// #1470 seam, NOT the literal `confirmation !== "confirmed"` that
|
|
180
|
+
// `diagnostic-binding.ts:274-288` exists to forbid: `partial` means
|
|
181
|
+
// every server except the named cut-off auxiliaries answered, so the
|
|
182
|
+
// primary's silent-clean gates ran to completion and its observation is
|
|
183
|
+
// as trustworthy as a full confirmation. The literal labels a file the
|
|
184
|
+
// primary fully answered and keeps a finding the touch just re-derived.
|
|
185
|
+
if (touchedResult === undefined ||
|
|
186
|
+
touchedResult.inconclusive === true ||
|
|
187
|
+
touchedResult.skipReason !== undefined ||
|
|
188
|
+
(touchedResult.diagnosticsUnsupportedServerIds ?? []).length > 0 ||
|
|
189
|
+
!touchCompletedConfirmationPolicy(touchedResult)) {
|
|
190
|
+
outcomes.push({
|
|
191
|
+
filePath: entry.filePath,
|
|
192
|
+
displayPath: entry.displayPath,
|
|
193
|
+
outcome: "unconfirmed",
|
|
194
|
+
dropped: 0,
|
|
195
|
+
kept: entry.warnings.length,
|
|
196
|
+
});
|
|
197
|
+
replacementFiles.push({ ...entry, reVerifyIncomplete: true });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
touched += 1;
|
|
201
|
+
// F2: the touch's collected diagnostics feed the producer's pipeline
|
|
202
|
+
// as the cached arm — no second pull, the producer's filters and
|
|
203
|
+
// caps are reused verbatim.
|
|
204
|
+
// F4: the outer await is bound-wrapped too — the module carries ZERO
|
|
205
|
+
// syntactic unbounded awaits (#2523 slice-3 worklist), and the hard
|
|
206
|
+
// ceiling composes with enrichFileFromLsp's own internal deadline.
|
|
207
|
+
const freshRecords = (await bounded(enrichFileFromLsp(args.cwd, buildArgs, { filePath: entry.filePath, cached: touchedResult.diags }, {
|
|
208
|
+
lspService: args.lspService,
|
|
209
|
+
pullTimeoutMs: 2000,
|
|
210
|
+
deadlineAt,
|
|
211
|
+
...(args.signal !== undefined ? { signal: args.signal } : {}),
|
|
212
|
+
site: { hook: "turn_end", label: "persistent_reverify" },
|
|
213
|
+
}), {
|
|
214
|
+
ms: Math.max(250, deadlineAt - Date.now()),
|
|
215
|
+
signal: args.signal,
|
|
216
|
+
hook: "turn_end",
|
|
217
|
+
label: "persistent_reverify_enrich",
|
|
218
|
+
})) ?? [];
|
|
219
|
+
let dropped = 0;
|
|
220
|
+
let kept = 0;
|
|
221
|
+
for (const warning of entry.warnings) {
|
|
222
|
+
const stillThere = freshRecords.some((record) => record.rule === warning.rule && record.message === warning.message);
|
|
223
|
+
if (stillThere)
|
|
224
|
+
kept += 1;
|
|
225
|
+
else
|
|
226
|
+
dropped += 1;
|
|
227
|
+
}
|
|
228
|
+
const outcome = freshRecords.length === 0
|
|
229
|
+
? "clean"
|
|
230
|
+
: dropped === 0
|
|
231
|
+
? "reconfirmed"
|
|
232
|
+
: "mixed";
|
|
233
|
+
outcomes.push({
|
|
234
|
+
filePath: entry.filePath,
|
|
235
|
+
displayPath: entry.displayPath,
|
|
236
|
+
outcome,
|
|
237
|
+
dropped,
|
|
238
|
+
kept,
|
|
239
|
+
});
|
|
240
|
+
replacementFiles.push({
|
|
241
|
+
...entry,
|
|
242
|
+
warnings: freshRecords,
|
|
243
|
+
reVerified: true,
|
|
244
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
logLatency({
|
|
248
|
+
type: "phase",
|
|
249
|
+
toolName: "turn_end",
|
|
250
|
+
filePath: args.cwd,
|
|
251
|
+
phase: "persistent_reverify",
|
|
252
|
+
durationMs: Date.now() - started,
|
|
253
|
+
metadata: {
|
|
254
|
+
candidates: candidates.length,
|
|
255
|
+
touched,
|
|
256
|
+
clean: outcomes.filter((o) => o.outcome === "clean").length,
|
|
257
|
+
reconfirmed: outcomes.filter((o) => o.outcome === "reconfirmed").length,
|
|
258
|
+
mixed: outcomes.filter((o) => o.outcome === "mixed").length,
|
|
259
|
+
unconfirmed: outcomes.filter((o) => o.outcome === "unconfirmed").length,
|
|
260
|
+
skippedChanged,
|
|
261
|
+
skippedBudget: candidates.length - processed,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
outcomes,
|
|
266
|
+
replacementFiles,
|
|
267
|
+
candidates: candidates.length,
|
|
268
|
+
touched,
|
|
269
|
+
skippedChanged,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -923,6 +923,15 @@ export async function runFormatPhase(filePath, getFormatService, dbg, signal, bu
|
|
|
923
923
|
* → const randomInt = Math.floor(result);
|
|
924
924
|
*/
|
|
925
925
|
function buildEnrichedBlockerOutput(blockers, fileContent = "") {
|
|
926
|
+
// #3188: no blockers, no banner. The delivery gate above can retract every
|
|
927
|
+
// blocker (all cited files deleted), and the header interpolates the count,
|
|
928
|
+
// so without this the agent reads "🔴 STOP — 0 issue(s) must be fixed:" with
|
|
929
|
+
// nothing under it. Same guard, same reason, as `formatDiagnostics` in
|
|
930
|
+
// dispatch/utils/format-utils.ts — it belongs to the renderer, not to each
|
|
931
|
+
// call site, because the #2028 branch below cannot gate on the count without
|
|
932
|
+
// falling through to the UNGATED raw output.
|
|
933
|
+
if (blockers.length === 0)
|
|
934
|
+
return "";
|
|
926
935
|
// Empty fileContent (readback failed, e.g. deleted-file race) still
|
|
927
936
|
// renders the gated blocker list - just without per-line snippets.
|
|
928
937
|
const fileLines = fileContent ? fileContent.split("\n") : [];
|
|
@@ -1228,7 +1237,12 @@ export async function runPipeline(ctx, deps) {
|
|
|
1228
1237
|
const rest = dispatchResult.output.slice(dispatchResult.blockerOutput.length);
|
|
1229
1238
|
if (rest)
|
|
1230
1239
|
gatedOut += rest;
|
|
1231
|
-
|
|
1240
|
+
// #3188: every blocker retracted and nothing after the blocker
|
|
1241
|
+
// section leaves nothing to say — appending the separator alone would
|
|
1242
|
+
// still deliver a (whitespace-only) tool-result block, because
|
|
1243
|
+
// `handleToolResult` decides delivery on `output` truthiness.
|
|
1244
|
+
if (gatedOut)
|
|
1245
|
+
output += `\n\n${gatedOut}`;
|
|
1232
1246
|
}
|
|
1233
1247
|
else {
|
|
1234
1248
|
output += `\n\n${dispatchResult.output}`;
|
|
@@ -40,6 +40,8 @@ import { getLSPService } from "./lsp/index.js";
|
|
|
40
40
|
import { drainPendingAuxCapEvictedCount, drainPendingAuxiliaryCoverage, isPendingAuxiliaryPastRearmTtl, rearmPendingAuxiliaryCoverage, MAX_LATE_AUX_REARMS, pendingAuxiliaryCoverageSize, } from "./lsp/pending-aux-coverage.js";
|
|
41
41
|
import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
42
42
|
import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
|
|
43
|
+
import { runPersistentReverify, } from "./persistent-reverify.js";
|
|
44
|
+
import { cascadeCarrySuffix } from "./cascade-format.js";
|
|
43
45
|
import { applyFindingPolicy, loadProjectRulePolicyMap, renderedRuleIdentities, } from "./dispatch/finding-policy.js";
|
|
44
46
|
import { detectFileRole } from "./file-role.js";
|
|
45
47
|
import { drainPendingRunnerFindings, dropStaleRunnerFindings, pendingRunnerFindingsSize, } from "./dispatch/pending-runner-findings.js";
|
|
@@ -894,6 +896,24 @@ export async function handleTurnEnd(deps) {
|
|
|
894
896
|
return true;
|
|
895
897
|
});
|
|
896
898
|
const cascadeResults = cascadeRuns.flatMap((r) => r.result ? [r.result] : []);
|
|
899
|
+
// Fix B (#3167): which results belong to a CARRIED run — the label is
|
|
900
|
+
// per-result at render time, so the run→result pairing must survive the
|
|
901
|
+
// flatMap above (which discards the wrapper).
|
|
902
|
+
// Fix B (#3168 F6): the success-path record — latency.log could not show
|
|
903
|
+
// the feature ever rendered without it (the only phases were the drop/
|
|
904
|
+
// settle records). One bounded record per turn, emitted after both render
|
|
905
|
+
// sections below.
|
|
906
|
+
let carriedRunsRendered = 0;
|
|
907
|
+
let labeledAdvisories = 0;
|
|
908
|
+
const carriedMetaByResult = new Map();
|
|
909
|
+
for (const r of cascadeRuns) {
|
|
910
|
+
if (r.result && (r.carriedTurns ?? 0) > 0 && r.carriedTurns !== undefined) {
|
|
911
|
+
carriedMetaByResult.set(r.result, {
|
|
912
|
+
carriedTurns: r.carriedTurns,
|
|
913
|
+
observedAt: r.observedAt,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
897
917
|
// #1550 class sweep: every cascade record below summarises `cascadeResults`
|
|
898
918
|
// — runs, which carry their own paths and can be carried across turns
|
|
899
919
|
// (#1443) — so labelling them with the turn's first EDITED file is the same
|
|
@@ -931,7 +951,16 @@ export async function handleTurnEnd(deps) {
|
|
|
931
951
|
const pk = normalizeMapKey(result.filePath);
|
|
932
952
|
const ownsAny = result.neighbors.some((n) => neighborOwner.get(normalizeMapKey(n.filePath)) === pk);
|
|
933
953
|
if (ownsAny && result.formatted) {
|
|
934
|
-
|
|
954
|
+
// Fix B (#3167/#3168): a carried run's re-rendered blocker is labeled
|
|
955
|
+
// so the agent can tell it from a fresh observation — with the
|
|
956
|
+
// run's own observation age (#3168 F3).
|
|
957
|
+
const carryMeta = carriedMetaByResult.get(result);
|
|
958
|
+
const carrySuffix = cascadeCarrySuffix(carryMeta?.carriedTurns, carryMeta?.observedAt);
|
|
959
|
+
parts.push(carrySuffix
|
|
960
|
+
? `${result.formatted}\n${carrySuffix}`
|
|
961
|
+
: result.formatted);
|
|
962
|
+
if (carrySuffix)
|
|
963
|
+
carriedRunsRendered += 1;
|
|
935
964
|
injectedNeighborCount += result.neighbors.length;
|
|
936
965
|
injectedDiagnosticCount += result.neighbors.reduce((s, n) => s + n.diagnostics.length, 0);
|
|
937
966
|
}
|
|
@@ -1081,27 +1110,59 @@ export async function handleTurnEnd(deps) {
|
|
|
1081
1110
|
// "ℹ️ Advisory — no action required this turn:" label, so an imperative
|
|
1082
1111
|
// ("review dependents manually") would contradict it. The #533 substance
|
|
1083
1112
|
// stays: a clean cascade result does NOT cover these files' dependents.
|
|
1084
|
-
|
|
1113
|
+
// Fix B (#3167/#3168 F4): a coverage advisory computed from a CARRIED
|
|
1114
|
+
// indeterminate run describes the previous turn's evidence — label it so
|
|
1115
|
+
// the absence-of-coverage statement is not read as current. A MIXED
|
|
1116
|
+
// bucket (some carried, some fresh) is left UNLABELED: a Math.max
|
|
1117
|
+
// suffix on the finished multi-line advisory would attach the carry to
|
|
1118
|
+
// a file that was not carried.
|
|
1119
|
+
const withCarryLabel = (advisory, runs) => {
|
|
1120
|
+
if (advisory === undefined)
|
|
1121
|
+
return undefined;
|
|
1122
|
+
const carried = runs.filter((r) => (r.carriedTurns ?? 0) > 0);
|
|
1123
|
+
if (carried.length === 0 || carried.length !== runs.length) {
|
|
1124
|
+
return advisory;
|
|
1125
|
+
}
|
|
1126
|
+
// #3168 F13: the bucket's age is the OLDEST carried observation, and
|
|
1127
|
+
// only when EVERY carried run carries one. A `Math.min` sentinel
|
|
1128
|
+
// (`?? Number.MAX_SAFE_INTEGER`) silently ignored the unstamped runs
|
|
1129
|
+
// and stated a confident age for a bucket that contains an unaged
|
|
1130
|
+
// one; one missing stamp collapses the age half to the helper's
|
|
1131
|
+
// neutral "scan age unknown" wording instead.
|
|
1132
|
+
const stamps = carried.map((r) => r.observedAt);
|
|
1133
|
+
const observedAt = stamps.every((s) => s !== undefined)
|
|
1134
|
+
? Math.min(...stamps)
|
|
1135
|
+
: undefined;
|
|
1136
|
+
const suffix = cascadeCarrySuffix(carried[0]?.carriedTurns, observedAt);
|
|
1137
|
+
if (!suffix)
|
|
1138
|
+
return advisory;
|
|
1139
|
+
labeledAdvisories += 1;
|
|
1140
|
+
// #3168 F11: newline, as the blocker path does above. A space join
|
|
1141
|
+
// welded the suffix onto the LAST bullet of a multi-bullet advisory,
|
|
1142
|
+
// so the carry label read as a property of that one file.
|
|
1143
|
+
return `${advisory}\n${suffix}`;
|
|
1144
|
+
};
|
|
1145
|
+
const graphAdvisory = withCarryLabel(buildAdvisory(graphRuns, {
|
|
1085
1146
|
lead: (fileCount, reasons) => `Cascade could not compute downstream impact for ${fileCount} edited file(s) this turn — ` +
|
|
1086
1147
|
`the review graph was unavailable (${reasons}), so their dependents were not ` +
|
|
1087
1148
|
`cascade-checked and a clean cascade result does not cover them.`,
|
|
1088
1149
|
fallbackDetail: (r) => r.indeterminate?.reason === "missing_node"
|
|
1089
1150
|
? "changed file not in the review graph"
|
|
1090
1151
|
: "review graph unavailable",
|
|
1091
|
-
});
|
|
1152
|
+
}), graphRuns);
|
|
1092
1153
|
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1093
1154
|
if (graphAdvisory)
|
|
1094
1155
|
advisoryParts.push(graphAdvisory);
|
|
1095
|
-
const bindingAdvisory = buildAdvisory(bindingRuns, {
|
|
1156
|
+
const bindingAdvisory = withCarryLabel(buildAdvisory(bindingRuns, {
|
|
1096
1157
|
lead: (fileCount, reasons) => `Cascade identified dependents for ${fileCount} edited file(s) this turn, but their ` +
|
|
1097
1158
|
`diagnostics could not be freshly confirmed (${reasons}) and were withheld — a clean ` +
|
|
1098
1159
|
`cascade result does not cover them.`,
|
|
1099
1160
|
fallbackDetail: () => "cascade diagnostics withheld (binding rejected)",
|
|
1100
|
-
});
|
|
1161
|
+
}), bindingRuns);
|
|
1101
1162
|
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1102
1163
|
if (bindingAdvisory)
|
|
1103
1164
|
advisoryParts.push(bindingAdvisory);
|
|
1104
|
-
const budgetAdvisory = buildAdvisory(budgetRuns, {
|
|
1165
|
+
const budgetAdvisory = withCarryLabel(buildAdvisory(budgetRuns, {
|
|
1105
1166
|
lead: (fileCount, reasons) => `Cascade checked the selected neighbors for ${fileCount} edited file(s) this turn, ` +
|
|
1106
1167
|
`but some eligible dependents were not checked because the cascade budget ` +
|
|
1107
1168
|
`was exhausted (${reasons}); a clean cascade result does not cover them.`,
|
|
@@ -1114,7 +1175,7 @@ export async function handleTurnEnd(deps) {
|
|
|
1114
1175
|
? `${detail}; transitive expansion was capped before all eligible dependents were enumerated`
|
|
1115
1176
|
: detail;
|
|
1116
1177
|
},
|
|
1117
|
-
});
|
|
1178
|
+
}), budgetRuns);
|
|
1118
1179
|
// @delivery-surface: runtime-turn:cascade-coverage-advisory
|
|
1119
1180
|
if (budgetAdvisory)
|
|
1120
1181
|
advisoryParts.push(budgetAdvisory);
|
|
@@ -1152,6 +1213,14 @@ export async function handleTurnEnd(deps) {
|
|
|
1152
1213
|
},
|
|
1153
1214
|
});
|
|
1154
1215
|
}
|
|
1216
|
+
// Fix B (#3168 F6): the success-path record — see the counters above.
|
|
1217
|
+
if (carriedRunsRendered > 0 || labeledAdvisories > 0) {
|
|
1218
|
+
logCascade({
|
|
1219
|
+
phase: "cascade_carry_rendered",
|
|
1220
|
+
filePath: cascadeLogFilePath,
|
|
1221
|
+
metadata: { carriedRunsRendered, labeledAdvisories },
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1155
1224
|
const cascadeSkipped = {
|
|
1156
1225
|
blockers: 0,
|
|
1157
1226
|
non_code: 0,
|
|
@@ -2672,7 +2741,37 @@ export async function handleTurnEnd(deps) {
|
|
|
2672
2741
|
fileSeqByPath.set(filePath, getFileSeq.call(runtime, filePath));
|
|
2673
2742
|
}
|
|
2674
2743
|
}
|
|
2744
|
+
let reverify;
|
|
2675
2745
|
if (getFlag("lens-actionable-warnings")) {
|
|
2746
|
+
// #3170: re-verify carried deferred findings before the advisory
|
|
2747
|
+
// assembles — a finding whose file is unchanged re-serves from the
|
|
2748
|
+
// persisted report without ever being re-observed; root-cause-fixed-
|
|
2749
|
+
// elsewhere findings converge here instead of repeating. Bounded in the
|
|
2750
|
+
// module (≤4 files, wall budget, abort signal). #3176 F1: the
|
|
2751
|
+
// replacement entries fold into THIS turn's single in-band publish
|
|
2752
|
+
// below — a separate replacement publish spends the carry marker and
|
|
2753
|
+
// this publish then drops the entries at the scope guard (the blocker
|
|
2754
|
+
// the review caught).
|
|
2755
|
+
const persistedReport = cacheManager.readCache("actionable-warnings", cwd, Number.MAX_SAFE_INTEGER)?.data;
|
|
2756
|
+
if (persistedReport?.files?.some((entry) => entry.origin === "deferred")) {
|
|
2757
|
+
const reverifyLspService = getLSPService();
|
|
2758
|
+
if (reverifyLspService) {
|
|
2759
|
+
// #2523: the hook-path await is bound-wrapped with the hook's own
|
|
2760
|
+
// budget and signal; the pass's internal deadline (3s) is the
|
|
2761
|
+
// tighter of the two.
|
|
2762
|
+
reverify = await bounded(runPersistentReverify({
|
|
2763
|
+
report: persistedReport,
|
|
2764
|
+
cwd,
|
|
2765
|
+
lspService: reverifyLspService,
|
|
2766
|
+
signal: getAmbientAbortSignal(),
|
|
2767
|
+
}), {
|
|
2768
|
+
ms: HOOK_WALL_BUDGET_MS.turn_end,
|
|
2769
|
+
signal: getAmbientAbortSignal(),
|
|
2770
|
+
hook: "turn_end",
|
|
2771
|
+
label: "persistent_reverify",
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2676
2775
|
try {
|
|
2677
2776
|
const report = await buildActionableWarningsReport({
|
|
2678
2777
|
cwd,
|
|
@@ -2751,6 +2850,24 @@ export async function handleTurnEnd(deps) {
|
|
|
2751
2850
|
// forward only entries a DEFERRAL produced, and only while their
|
|
2752
2851
|
// file has not moved, so a `turn_delta` report does not accumulate
|
|
2753
2852
|
// every prior turn's findings.
|
|
2853
|
+
// #3176 F1: the re-verify replacements fold into THIS report — one
|
|
2854
|
+
// in-band publish total, so the carry marker is spent exactly once and
|
|
2855
|
+
// the merged report cannot drop the entries behind the publish (the
|
|
2856
|
+
// blocker the review caught).
|
|
2857
|
+
if (reverify?.replacementFiles.length) {
|
|
2858
|
+
const replacementByPath = new Map(reverify.replacementFiles.map((file) => [
|
|
2859
|
+
normalizeMapKey(file.filePath),
|
|
2860
|
+
file,
|
|
2861
|
+
]));
|
|
2862
|
+
const files = (report.files ?? []).map((file) => replacementByPath.get(normalizeMapKey(file.filePath)) ?? file);
|
|
2863
|
+
for (const replacement of reverify.replacementFiles) {
|
|
2864
|
+
if (!files.some((file) => normalizeMapKey(file.filePath) ===
|
|
2865
|
+
normalizeMapKey(replacement.filePath))) {
|
|
2866
|
+
files.push(replacement);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
report.files = files;
|
|
2870
|
+
}
|
|
2754
2871
|
const publishResult = publishActionableWarningsReport(cacheManager, cwd, report, {
|
|
2755
2872
|
origin: "in-band",
|
|
2756
2873
|
getFileSeq: getFileSeq
|
|
@@ -82,7 +82,43 @@ export function resolveSkillPaths(importMetaUrl) {
|
|
|
82
82
|
});
|
|
83
83
|
if (health.status !== "healthy") {
|
|
84
84
|
const entryDir = path.dirname(fileURLToPath(importMetaUrl));
|
|
85
|
-
const
|
|
85
|
+
const healthDescription = describeBundledResourceHealth(health, skillsDir, `no SKILL.md (or loadable .md) found under ${skillsDir}`);
|
|
86
|
+
// #3175: two DIFFERENT strings, not one `reason` reused for both sinks
|
|
87
|
+
// as this used to be.
|
|
88
|
+
//
|
|
89
|
+
// `notifyReason` (uncapped — `notifyUserDegradation` never truncates)
|
|
90
|
+
// keeps the full, natural description: classification first, then
|
|
91
|
+
// where the entry file that triggered this lookup was loaded from.
|
|
92
|
+
const notifyReason = `${healthDescription} (entry loaded from ${entryDir})`;
|
|
93
|
+
// `ledgerReason` is built from a SHORT, path-free classification tag
|
|
94
|
+
// instead of `healthDescription`, because the LEDGER's `reason` field
|
|
95
|
+
// IS capped (`truncateForLedger`, `LEDGER_FIELD_MAX` = 200 chars,
|
|
96
|
+
// head-preserving — see `ledger-bounds.ts`) and `healthDescription`
|
|
97
|
+
// repeats `skillsDir` in full — already recorded, untruncated, in
|
|
98
|
+
// `subject` below whenever it's under the cap on its own. A
|
|
99
|
+
// managed-cache `skillsDir` plus this entry's `entryDir` sharing one
|
|
100
|
+
// long root routinely sums past 200 chars (#3175: TMPDIR ~60+ chars is
|
|
101
|
+
// a real dry-roll and CI-lane shape, not a pathological one); a
|
|
102
|
+
// head-preserving cap over the OLD concatenation (dir-bearing text
|
|
103
|
+
// first, `entryDir` last) then kept the redundant path and dropped the
|
|
104
|
+
// one fact nothing else records — which entry file's directory
|
|
105
|
+
// triggered this lookup (#2587's "entry file copied out of the
|
|
106
|
+
// package tree" case is exactly this). Simply swapping that order is
|
|
107
|
+
// NOT enough: `healthDescription`'s own `unreadable` branch reads
|
|
108
|
+
// `cannot read ${dir} (${fsErrorCode})`, so `dir` sitting in the
|
|
109
|
+
// MIDDLE of the classification text can still push `fsErrorCode`
|
|
110
|
+
// itself off the cap once `entryDir` leads. Dropping the redundant
|
|
111
|
+
// path from the ledger's copy entirely — keeping only the
|
|
112
|
+
// classification word/code — removes that squeeze instead of moving
|
|
113
|
+
// it, and leaves comfortable room (entry directories under roughly
|
|
114
|
+
// 150 chars fit whole) for every managed-cache/npm-install layout
|
|
115
|
+
// observed so far.
|
|
116
|
+
const classification = health.status === "unreadable"
|
|
117
|
+
? `cannot read (${health.fsErrorCode})`
|
|
118
|
+
: health.status === "empty"
|
|
119
|
+
? "no SKILL.md found"
|
|
120
|
+
: "no such directory";
|
|
121
|
+
const ledgerReason = `${classification} (entry loaded from ${entryDir})`;
|
|
86
122
|
// #2626 review F3: the notify-once gate previously re-derived "have we
|
|
87
123
|
// already recorded this" by scanning `getDegradationSummary()` and
|
|
88
124
|
// comparing the RAW `skillsDir` against subjects the ledger stores
|
|
@@ -99,7 +135,7 @@ export function resolveSkillPaths(importMetaUrl) {
|
|
|
99
135
|
// never silently replace it. Passed explicitly rather than inferred
|
|
100
136
|
// from `label` so this stays a per-caller decision, not a convention
|
|
101
137
|
// the next caller has to remember.
|
|
102
|
-
reportBundledResourceDirHealth(SKILLS_DIR_MISSING_KIND, skillsDir, health, "skills",
|
|
138
|
+
reportBundledResourceDirHealth(SKILLS_DIR_MISSING_KIND, skillsDir, health, "skills", ledgerReason, `pi-lens: registers zero skills — ${notifyReason}.`);
|
|
103
139
|
}
|
|
104
140
|
return [skillsDir];
|
|
105
141
|
}
|