@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.
Files changed (158) 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/grok-mermaid/CHANGELOG.md +46 -0
  16. package/node_modules/grok-mermaid/LICENSE +205 -0
  17. package/node_modules/grok-mermaid/README.md +191 -0
  18. package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
  19. package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
  20. package/node_modules/grok-mermaid/dist/ansi.js +23 -0
  21. package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
  22. package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
  23. package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
  24. package/node_modules/grok-mermaid/dist/canvas.js +366 -0
  25. package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
  26. package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
  27. package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
  28. package/node_modules/grok-mermaid/dist/graph.js +91 -0
  29. package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
  30. package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
  31. package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
  32. package/node_modules/grok-mermaid/dist/index.js +100 -0
  33. package/node_modules/grok-mermaid/dist/index.js.map +1 -0
  34. package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
  35. package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
  36. package/node_modules/grok-mermaid/dist/labels.js +324 -0
  37. package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
  38. package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
  39. package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
  40. package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
  41. package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
  42. package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
  43. package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
  44. package/node_modules/grok-mermaid/dist/layout.js +881 -0
  45. package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
  46. package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
  47. package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
  48. package/node_modules/grok-mermaid/dist/parse.js +1151 -0
  49. package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
  50. package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
  51. package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
  52. package/node_modules/grok-mermaid/dist/source-box.js +78 -0
  53. package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
  54. package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
  55. package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
  56. package/node_modules/grok-mermaid/dist/types.js +1 -0
  57. package/node_modules/grok-mermaid/dist/types.js.map +1 -0
  58. package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
  59. package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
  60. package/node_modules/grok-mermaid/dist/width-data.js +994 -0
  61. package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
  62. package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
  63. package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
  64. package/node_modules/grok-mermaid/dist/width.js +76 -0
  65. package/node_modules/grok-mermaid/dist/width.js.map +1 -0
  66. package/node_modules/grok-mermaid/package.json +49 -0
  67. package/node_modules/grok-mermaid/src/ansi.ts +34 -0
  68. package/node_modules/grok-mermaid/src/canvas.ts +373 -0
  69. package/node_modules/grok-mermaid/src/graph.ts +142 -0
  70. package/node_modules/grok-mermaid/src/index.ts +104 -0
  71. package/node_modules/grok-mermaid/src/labels.ts +326 -0
  72. package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
  73. package/node_modules/grok-mermaid/src/layout.ts +1015 -0
  74. package/node_modules/grok-mermaid/src/parse.ts +1189 -0
  75. package/node_modules/grok-mermaid/src/source-box.ts +89 -0
  76. package/node_modules/grok-mermaid/src/types.ts +43 -0
  77. package/node_modules/grok-mermaid/src/width-data.ts +993 -0
  78. package/node_modules/grok-mermaid/src/width.ts +74 -0
  79. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +110 -0
  80. package/node_modules/pi-jev-auto-mode/LICENSE +21 -0
  81. package/node_modules/pi-jev-auto-mode/README.md +285 -0
  82. package/node_modules/pi-jev-auto-mode/SECURITY.md +32 -0
  83. package/node_modules/pi-jev-auto-mode/docs/calibration.md +154 -0
  84. package/node_modules/pi-jev-auto-mode/docs/design.md +164 -0
  85. package/node_modules/pi-jev-auto-mode/docs/security.md +124 -0
  86. package/node_modules/pi-jev-auto-mode/index.ts +1 -0
  87. package/node_modules/pi-jev-auto-mode/package.json +72 -0
  88. package/node_modules/pi-jev-auto-mode/src/call.ts +180 -0
  89. package/node_modules/pi-jev-auto-mode/src/decide.ts +86 -0
  90. package/node_modules/pi-jev-auto-mode/src/extension.ts +862 -0
  91. package/node_modules/pi-jev-auto-mode/src/intent.ts +71 -0
  92. package/node_modules/pi-jev-auto-mode/src/jev/availability.ts +53 -0
  93. package/node_modules/pi-jev-auto-mode/src/jev/criteria.ts +19 -0
  94. package/node_modules/pi-jev-auto-mode/src/jev/decide.ts +187 -0
  95. package/node_modules/pi-jev-auto-mode/src/jev/engine.ts +165 -0
  96. package/node_modules/pi-jev-auto-mode/src/jev/index.ts +20 -0
  97. package/node_modules/pi-jev-auto-mode/src/jev/questions.ts +273 -0
  98. package/node_modules/pi-jev-auto-mode/src/jev/response.ts +64 -0
  99. package/node_modules/pi-jev-auto-mode/src/jev/state.ts +20 -0
  100. package/node_modules/pi-jev-auto-mode/src/jev/transport.ts +117 -0
  101. package/node_modules/pi-jev-auto-mode/src/jev/types.ts +46 -0
  102. package/node_modules/pi-jev-auto-mode/src/policy.ts +558 -0
  103. package/node_modules/pi-jev-auto-mode/src/records.ts +118 -0
  104. package/node_modules/pi-jev-auto-mode/src/settings.ts +327 -0
  105. package/node_modules/pi-jev-auto-mode/src/ui.ts +233 -0
  106. package/node_modules/pi-lens/CHANGELOG.md +45 -0
  107. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +21 -9
  108. package/node_modules/pi-lens/dist/clients/cascade-format.js +24 -0
  109. package/node_modules/pi-lens/dist/clients/extension-log.js +48 -1
  110. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +11 -12
  111. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +3 -0
  112. package/node_modules/pi-lens/dist/clients/lsp/index.js +41 -7
  113. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +9 -2
  114. package/node_modules/pi-lens/dist/clients/path-utils.js +51 -9
  115. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +271 -0
  116. package/node_modules/pi-lens/dist/clients/pipeline.js +15 -1
  117. package/node_modules/pi-lens/dist/clients/runtime-turn.js +124 -7
  118. package/node_modules/pi-lens/dist/clients/skills-resolver.js +38 -2
  119. package/node_modules/pi-lens/dist/clients/widget-state.js +152 -6
  120. package/node_modules/pi-lens/dist/index.js +729 -344
  121. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +14 -2
  122. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +84 -17
  123. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +32 -5
  124. package/node_modules/pi-lens/package.json +1 -1
  125. package/node_modules/pi-typesafe/LICENSE +21 -0
  126. package/node_modules/pi-typesafe/README.md +153 -0
  127. package/node_modules/pi-typesafe/dist/ask.d.ts +32 -0
  128. package/node_modules/pi-typesafe/dist/ask.js +25 -0
  129. package/node_modules/pi-typesafe/dist/auth.d.ts +56 -0
  130. package/node_modules/pi-typesafe/dist/auth.js +114 -0
  131. package/node_modules/pi-typesafe/dist/batch.d.ts +74 -0
  132. package/node_modules/pi-typesafe/dist/batch.js +116 -0
  133. package/node_modules/pi-typesafe/dist/calibrate.d.ts +106 -0
  134. package/node_modules/pi-typesafe/dist/calibrate.js +157 -0
  135. package/node_modules/pi-typesafe/dist/client.d.ts +69 -0
  136. package/node_modules/pi-typesafe/dist/client.js +188 -0
  137. package/node_modules/pi-typesafe/dist/credentials.d.ts +46 -0
  138. package/node_modules/pi-typesafe/dist/credentials.js +112 -0
  139. package/node_modules/pi-typesafe/dist/errors.d.ts +9 -0
  140. package/node_modules/pi-typesafe/dist/errors.js +31 -0
  141. package/node_modules/pi-typesafe/dist/extension.d.ts +3 -0
  142. package/node_modules/pi-typesafe/dist/extension.js +222 -0
  143. package/node_modules/pi-typesafe/dist/index.d.ts +18 -0
  144. package/node_modules/pi-typesafe/dist/index.js +9 -0
  145. package/node_modules/pi-typesafe/dist/key-prompt.d.ts +3 -0
  146. package/node_modules/pi-typesafe/dist/key-prompt.js +43 -0
  147. package/node_modules/pi-typesafe/dist/login.d.ts +27 -0
  148. package/node_modules/pi-typesafe/dist/login.js +38 -0
  149. package/node_modules/pi-typesafe/dist/schema.d.ts +43 -0
  150. package/node_modules/pi-typesafe/dist/schema.js +152 -0
  151. package/node_modules/pi-typesafe/dist/ui.d.ts +3 -0
  152. package/node_modules/pi-typesafe/dist/ui.js +3 -0
  153. package/node_modules/pi-typesafe/dist/usage.d.ts +73 -0
  154. package/node_modules/pi-typesafe/dist/usage.js +182 -0
  155. package/node_modules/pi-typesafe/examples/decision-extension.ts +41 -0
  156. package/node_modules/pi-typesafe/extensions/index.js +2 -0
  157. package/node_modules/pi-typesafe/package.json +89 -0
  158. package/package.json +16 -8
@@ -251,7 +251,7 @@ function lineInModifiedRanges(line, ranges) {
251
251
  return true;
252
252
  return ranges.some((range) => line >= range.start - 2 && line <= range.end + 2);
253
253
  }
254
- function recordFromLspDiagnostic(diag, filePath, cwd) {
254
+ export function recordFromLspDiagnostic(diag, filePath, cwd) {
255
255
  const line = diag.range.start.line + 1;
256
256
  const column = diag.range.start.character + 1;
257
257
  // Keep agreement keyed to the producer that supplied the diagnostic. The
@@ -736,7 +736,7 @@ export async function buildActionableWarningsReport(args) {
736
736
  * off-hook loop run byte-identical logic — the deferral must not become a
737
737
  * second, drifting copy of the enrichment.
738
738
  */
739
- async function enrichFileFromLsp(cwd, args, target, deps) {
739
+ export async function enrichFileFromLsp(cwd, args, target, deps) {
740
740
  const { lspService } = deps;
741
741
  const { filePath } = target;
742
742
  const out = [];
@@ -1110,12 +1110,24 @@ export function mergeActionableWarningsReports(args) {
1110
1110
  }
1111
1111
  mergedFiles++;
1112
1112
  const merged = incumbent
1113
- ? {
1114
- ...incumbent,
1115
- fileSeq: incumbent.fileSeq ?? entry.fileSeq,
1116
- generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
1117
- warnings: mergeWarnings([...incumbent.warnings, ...entry.warnings]),
1118
- }
1113
+ ? incumbent.reVerified || incumbent.reVerifyIncomplete
1114
+ ? // #3170: the runtime just RE-OBSERVED this file (the bounded
1115
+ // persistent-reverify pass) — the fresh observation supersedes
1116
+ // the carried entry instead of unioning with it: a converged
1117
+ // finding would otherwise survive its own supersession via the
1118
+ // id-union in `mergeWarnings`.
1119
+ {
1120
+ ...incumbent,
1121
+ fileSeq: incumbent.fileSeq ?? entry.fileSeq,
1122
+ generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
1123
+ warnings: incumbent.warnings,
1124
+ }
1125
+ : {
1126
+ ...incumbent,
1127
+ fileSeq: incumbent.fileSeq ?? entry.fileSeq,
1128
+ generatedAt: olderStamp(incumbent.generatedAt ?? newerReport?.generatedAt, entry.generatedAt ?? olderReport?.generatedAt),
1129
+ warnings: mergeWarnings([...incumbent.warnings, ...entry.warnings]),
1130
+ }
1119
1131
  : { ...entry };
1120
1132
  if (deferredPublish) {
1121
1133
  // Mark it, so the next in-band publish carries it forward across the
@@ -1541,7 +1553,7 @@ export function formatActionableWarningsAdvisory(report, cwd, host = "pi") {
1541
1553
  const files = report.files.filter((file) => file.warnings.some((warning) => !warning.suppressed));
1542
1554
  const fileList = files
1543
1555
  .slice(0, 5)
1544
- .map((file) => ` ${file.displayPath}: ${file.warnings.filter((warning) => !warning.suppressed).length}`)
1556
+ .map((file) => ` ${file.displayPath}: ${file.warnings.filter((warning) => !warning.suppressed).length}${file.reVerifyIncomplete ? " (re-verify incomplete)" : ""}`)
1545
1557
  .join("\n");
1546
1558
  const more = files.length > 5 ? `\n ... and ${files.length - 5} more file(s)` : "";
1547
1559
  const safe = report.summary.autoFixEligible > 0
@@ -3,6 +3,7 @@ import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
3
3
  import { applyFindingPolicy, loadProjectRulePolicyMap, renderedRuleIdentities, } from "./dispatch/finding-policy.js";
4
4
  import { detectFileRole } from "./file-role.js";
5
5
  import { logLatency } from "./latency-logger.js";
6
+ import { formatCacheAgeLabel } from "./finding-delivery-gate.js";
6
7
  import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
7
8
  import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
8
9
  export function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
@@ -140,6 +141,12 @@ export function buildResolvedFoundCascadeRun(cwd, neighbor) {
140
141
  },
141
142
  neighborCount: 1,
142
143
  diagnosticCount: diagnostics.length,
144
+ // #3168 F3: the #1444 publish stamp — the run's own observation time, so
145
+ // the carried-render age label states the real age instead of claiming
146
+ // no stamp exists.
147
+ ...(neighbor.publishedAt !== undefined
148
+ ? { observedAt: neighbor.publishedAt }
149
+ : {}),
143
150
  };
144
151
  }
145
152
  /**
@@ -289,3 +296,20 @@ function readNeighborContent(filePath) {
289
296
  return undefined;
290
297
  }
291
298
  }
299
+ /**
300
+ * Fix B (#3167/#3168): the carry label for a cascade run re-rendered at a
301
+ * later turn_end. The carry is bounded to ONE turn (`RuntimeCoordinator.beginTurn`
302
+ * drops anything that would reach 2), so the honest label names the carry
303
+ * count. #3168 F3 corrected this docstring's earlier claim that no stamp
304
+ * exists: the run DOES carry one (`observedAt`, threaded from #1444's
305
+ * `publishedAt` through the resolved-found plumb), so the age half renders
306
+ * from it via `formatCacheAgeLabel`. The deferred-compute re-park arm has no
307
+ * stamp and correctly falls to the neutral wording. Returns `undefined` for
308
+ * non-carried runs: no label noise on fresh observations.
309
+ */
310
+ export function cascadeCarrySuffix(carriedTurns, observedAt) {
311
+ if (carriedTurns === undefined || carriedTurns < 1)
312
+ return undefined;
313
+ const noun = carriedTurns === 1 ? "turn" : "turns";
314
+ return `(carried ${carriedTurns} ${noun} · ${formatCacheAgeLabel(observedAt)})`;
315
+ }
@@ -211,6 +211,48 @@ function inCaptureWindow(fn) {
211
211
  function isCaptureSeam(prop) {
212
212
  return (prop === "on" || (typeof prop === "string" && prop.startsWith("register")));
213
213
  }
214
+ /**
215
+ * True for a host SCHEMA value that happens to be callable.
216
+ *
217
+ * A tool definition's `parameters` is DATA the host reads, not a pi-lens
218
+ * callback — but the schema builder pi-lens imports is host-provided
219
+ * (`clients/deps/typebox.ts`: the host resolves the bare `typebox`
220
+ * specifier), and on a host whose builders are ArkType-style the schema is a
221
+ * FUNCTION. `@oh-my-pi/pi-coding-agent` rewrites an extension's `typebox`
222
+ * import to its own `src/extensibility/legacy-typebox.ts`, which re-exports
223
+ * `@oh-my-pi/omptype`'s builders; those return a callable whose prototype
224
+ * carries `toJsonSchema`/`assert`. `inCaptureWindow` returns a plain rest
225
+ * wrapper with neither, so the host falls off its ark branch, the wire schema
226
+ * degrades to `undefined`, and every xd:// device renders `type Args =
227
+ * unknown` and rejects every call with `root: schema must be an object or
228
+ * boolean` (#3195).
229
+ *
230
+ * The predicate IS the host's own, copied from `@oh-my-pi/pi-ai@18.2.4`
231
+ * `src/utils/schema/wire.ts:21-27` (`isArkSchema`) — the same test the host's
232
+ * `applyToolProxy` (`@oh-my-pi/pi-coding-agent@18.2.4`
233
+ * `src/extensibility/tool-proxy.ts:26`) uses to leave a callable schema
234
+ * unbound for exactly this hazard ("`bind()` … drops the schema surface …
235
+ * so a bound schema later stringifies to `undefined`"). Matching it means
236
+ * pi-lens declines to wrap precisely the values the host reads as schemas,
237
+ * and still wraps everything else.
238
+ * `tests/clients/pi-host-callable-schema-contract.test.ts` asserts against
239
+ * that upstream predicate (and against a fixture measured from a real
240
+ * `@oh-my-pi/omptype@18.2.4` schema), not against this copy of it.
241
+ *
242
+ * Deliberately NOT an allow-list of handler keys (`execute`, `renderResult`,
243
+ * `handler` — the measured set today): that is `isCaptureSeam`'s #1434 S1a
244
+ * mistake one level down (defect shape 34, spelling enumerator). The host's
245
+ * own `ToolDefinition` already declares `onSession`, `renderCall` and
246
+ * `shellEnv`, and `registerCommand` takes `getArgumentCompletions`; the day
247
+ * pi-lens adopts one of those, an allow-list drops its console capture
248
+ * SILENTLY — the #1333 regression this seam exists to prevent — while this
249
+ * predicate keeps wrapping it.
250
+ */
251
+ function isCallableSchema(value) {
252
+ return (typeof value === "function" &&
253
+ typeof value.toJsonSchema === "function" &&
254
+ typeof value.assert === "function");
255
+ }
214
256
  /**
215
257
  * Wrap every function value found in `value`, in place. Handles the two
216
258
  * shapes a `register*`/`on` call takes today: a function passed directly
@@ -218,6 +260,11 @@ function isCaptureSeam(prop) {
218
260
  * function ONE level inside a plain options/definition object
219
261
  * (`options.handler`, `tool.execute`).
220
262
  *
263
+ * Only the property walk screens for a callable schema (#3195): every
264
+ * function pi-lens passes as a register argument DIRECTLY is a handler
265
+ * (`on`, `registerMessageRenderer`), measured across the whole activation,
266
+ * so a schema can only ever arrive as a property of a definition object.
267
+ *
221
268
  * Deliberately NOT recursive past that one level (#1434 perf review): a tool
222
269
  * definition's `parameters`/`schema` is a large, deeply nested TypeBox object
223
270
  * that holds no functions, but walking its full tree on every `registerTool`
@@ -249,7 +296,7 @@ function wrapFunctionsInPlace(value) {
249
296
  const obj = value;
250
297
  for (const key of Object.keys(obj)) {
251
298
  const propValue = obj[key];
252
- if (typeof propValue === "function") {
299
+ if (typeof propValue === "function" && !isCallableSchema(propValue)) {
253
300
  assignWrapped(obj, key, propValue);
254
301
  }
255
302
  }
@@ -284,20 +284,19 @@ export const DELIVERY_SURFACES = {
284
284
  "runtime-turn:late-runner-findings": gated(RUNTIME_TURN_FILE, "Turn-end CLI runner findings collected after the post-write path.", ["gateFindingsByPathFreshness"], ['store: "late-runner-findings"'], { evidenceMin: 2 }),
285
285
  "runtime-turn:cascade-blocker": labeled(RUNTIME_TURN_FILE, "Turn-end 🧪 cascade neighbor blocker.", "Cascade results settle synchronously this turn where possible " +
286
286
  "(`settleCascadeRuns`), but an unsettled compute can carry over to a " +
287
- "later turn (bounded by a carry cap) — this round does not freshness-" +
288
- "gate that carry-over window.", "live", [], {
289
- status: "partial",
290
- partialReason: "A carried-over cascade result (run.carriedTurns > 0) is rendered " +
291
- "without an age label. Follow-up: surface carriedTurns as an explicit " +
292
- "label when > 0, or route through formatCacheAgeLabel using the run's " +
293
- "own timestamp.",
294
- }),
287
+ "later turn (bounded by a carry cap) — a carried-over result renders " +
288
+ "with an explicit `(carried N turns · scanned Xm ago)` label (#3167, " +
289
+ "#3168 F3), so the agent can tell it from a fresh observation.", "live", ["cascadeCarrySuffix("], { evidenceMin: 1 }),
295
290
  "runtime-turn:cascade-coverage-advisory": labeled(RUNTIME_TURN_FILE, "Turn-end cascade-coverage-gap advisories (graph/binding/budget).", "Explains what the cascade check could NOT confirm this turn — not a " +
296
291
  "finding with a cited path, an absence-of-coverage disclosure computed " +
297
- "from this turn's own indeterminate-run list.", "live", [], {
298
- status: "partial",
299
- partialReason: "Same cascade carry-over caveat as runtime-turn:cascade-blocker.",
300
- }),
292
+ "from this turn's own indeterminate-run list. An advisory computed from " +
293
+ "a carried indeterminate run is labeled `(carried N turns · <age>)` " +
294
+ "(#3167, #3168 F4 dropped on mixed carried/fresh buckets). The age " +
295
+ "half reads `scan age unknown` on this surface today: no indeterminate-" +
296
+ "run producer stamps `observedAt` (only the resolved-found plumb does, " +
297
+ "`clients/cascade-format.ts`), and one unstamped carried run collapses " +
298
+ "the whole bucket's age rather than claiming the stamped runs' (#3168 " +
299
+ "F13). The carry COUNT is always real.", "live", ["withCarryLabel("], { evidenceMin: 3 }),
301
300
  // #3102: the cold-neighbour cascade run is BUILT here, in the quiet-window
302
301
  // reconcile (`onResolvedFound` in index.ts), a turn earlier than the
303
302
  // `runtime-turn:cascade-blocker` render that finally carries it — so it is
@@ -344,6 +344,9 @@ export function registerCascadeTierReconcileTask(getLspService, options = {}) {
344
344
  filePath: o.filePath,
345
345
  serverId: o.serverId,
346
346
  diagnostics: o.diagnostics,
347
+ ...(o.publishedAt !== undefined
348
+ ? { publishedAt: o.publishedAt }
349
+ : {}),
347
350
  });
348
351
  }
349
352
  else if (o.outcome === "resolved-clean" && o.publishedAt != null) {
@@ -3955,6 +3955,12 @@ export class LSPService {
3955
3955
  // (undefined = the race didn't produce an answer; the end-of-wait
3956
3956
  // fallback below may still fill it in on a timed-out empty result).
3957
3957
  let tsserverSyncEligible = false;
3958
+ // #1639/#3187: `ensureWarmForSweep`'s readiness probe (`source:
3959
+ // "lsp_sweep_warmup"`) is the one touch that is single-server,
3960
+ // `clientScope: "primary"` and NON-collecting at the same time. Both
3961
+ // sync-confirm gates below and the per-server wait's settle-source tag
3962
+ // read the same fact, so it is derived once, here.
3963
+ const isWarmupTouch = source === "lsp_sweep_warmup";
3958
3964
  let tsserverSyncConfirmed;
3959
3965
  let diagnosticsUnsupportedServerIds = [];
3960
3966
  if (diagnosticsMode !== "none") {
@@ -4128,8 +4134,19 @@ export class LSPService {
4128
4134
  // synchronous gate and pays ZERO extra work — not even the snapshot
4129
4135
  // read; a non-typescript `silentOnClean` server instead gets the
4130
4136
  // generic (non-racing) clean-confirm fallback further below.
4137
+ // #3187: the warm-up touch is eligible too, though it collects
4138
+ // nothing. `collectDiagnostics` is here because a touch that discards
4139
+ // its diagnostics has nothing to carry a confirm INTO — but the warm-up
4140
+ // is not after diagnostics at all, it is after a VERDICT, and the sync
4141
+ // commands are the only thing that can hand a tier3-silent server one
4142
+ // before its whole cold-start budget lapses (measured: 6,972 ms of a
4143
+ // 20,000 ms warm-up budget, then the sweep touched the same file again
4144
+ // in 113 ms). It stays non-collecting on purpose: a collecting warm-up
4145
+ // would write this file's `lastKnownDiagnostics` from primary-scope-only
4146
+ // evidence — and an empty confirm would DELETE a previously confirmed
4147
+ // record — which is the #1470/#1493 prime hazard from a new direction.
4131
4148
  if (!notifyWriteTimedOut &&
4132
- options.collectDiagnostics === true &&
4149
+ (options.collectDiagnostics === true || isWarmupTouch) &&
4133
4150
  clientScope === "primary" &&
4134
4151
  spawned.length === 1 &&
4135
4152
  spawned[0].client.serverId === "typescript" &&
@@ -4279,8 +4296,8 @@ export class LSPService {
4279
4296
  // consumer can tell them apart instead of double-counting. Omitted
4280
4297
  // (rather than passed as "pull") on the common path — the client
4281
4298
  // already defaults to "pull", and existing tests assert the exact
4282
- // argument list `waitForDiagnostics` is called with.
4283
- const isWarmupTouch = source === "lsp_sweep_warmup";
4299
+ // argument list `waitForDiagnostics` is called with. #3187: derived
4300
+ // once above, next to the sync-confirm gate that reads the same fact.
4284
4301
  // #743: per-server — a server we DID push to still gets the
4285
4302
  // version-baseline wait even when a sibling was debounced away.
4286
4303
  const waitForDiagnostics = () => !notifySkippedServerIds.has(entry.info.id) &&
@@ -5127,9 +5144,9 @@ export class LSPService {
5127
5144
  // ever double-deciding typescript's own touches — when the sync race
5128
5145
  // was attempted and failed/was unavailable, typescript's existing
5129
5146
  // "falls through to inconclusive, unchanged" contract (#707) is
5130
- // preserved exactly; typescript touches that never enter that gate
5131
- // (e.g. `collectDiagnostics: false`, like `ensureWarmForSweep`'s own
5132
- // warm-up call) are still eligible here as a genuine bonus fix. Scoped
5147
+ // preserved exactly for a COLLECTING touch; #3187 re-admits the
5148
+ // non-collecting warm-up, which has no end-of-wait retry to fall
5149
+ // through to (see the exclusion's own comment below). Scoped
5133
5150
  // to `clientScope === "primary"`/`spawned.length === 1` exactly like
5134
5151
  // the sync-eligible gate above (and like `ensureWarmForSweep`'s own
5135
5152
  // `clientScope: "primary"` warm-up touch) so a multi-server
@@ -5149,7 +5166,17 @@ export class LSPService {
5149
5166
  // below means the one server IS the primary, so this is the same condition
5150
5167
  // written in the vocabulary the rest of the merge now uses.
5151
5168
  !primaryNotifyWriteTimedOut &&
5152
- !tsserverSyncEligible &&
5169
+ // #3187: the warm-up is sync-ELIGIBLE now, and eligibility alone must
5170
+ // not cost it this fallback. For a collecting touch the exclusion is
5171
+ // #707's contract (the end-of-wait retry above is typescript's second
5172
+ // chance, and failing it stays inconclusive); the warm-up collects
5173
+ // nothing, so that retry's `collected !== undefined` precondition can
5174
+ // never hold for it and the exclusion would leave a server whose
5175
+ // `typescript.tsserverRequest` is unavailable stranded inconclusive —
5176
+ // a #744 warm-up failure, its retry, and a SKIPPED sweep group, where
5177
+ // today it is certified clean. Reaching here at all means the race
5178
+ // produced no answer, so this decides nothing the sync already decided.
5179
+ (!tsserverSyncEligible || isWarmupTouch) &&
5153
5180
  clientScope === "primary" &&
5154
5181
  spawned.length === 1 &&
5155
5182
  getStrategy(spawned[0].client.serverId, spawned[0].client.getLaunchVariant?.()).silentOnClean === true) {
@@ -6824,6 +6851,13 @@ export class LSPService {
6824
6851
  });
6825
6852
  const warmupAttempt = this.touchFile(representativeFile, content, {
6826
6853
  diagnostics: "document",
6854
+ // #3187: stays FALSE. This touch wants a verdict, not findings: its
6855
+ // `clientScope: "primary"` evidence does not speak for the auxiliaries
6856
+ // the sweep's own `clientScope: "all"` touch waits on, so collecting
6857
+ // here would prime this file's `lastKnownDiagnostics` from a narrower
6858
+ // scope than the result that follows (and an empty confirm would delete
6859
+ // a previously confirmed record). `touchFile`'s sync-confirm gates key
6860
+ // off `source` instead, so the verdict still arrives at ~grace+RTT.
6827
6861
  collectDiagnostics: false,
6828
6862
  clientScope: "primary",
6829
6863
  source: "lsp_sweep_warmup",
@@ -273,8 +273,15 @@ export async function analyzeFile(filePath, cwd, options = {}) {
273
273
  const durationMs = Date.now() - start;
274
274
  if (options.record !== false) {
275
275
  // Mirror pipeline.ts's recording so pilens_diagnostics (mode=all) and
276
- // pilens_health see what this analysis found.
277
- recordDiagnostics(absPath, result.diagnostics);
276
+ // pilens_health see what this analysis found. #3160 F1: `absPath` here
277
+ // is derived raw from the agent-supplied `file` arg (path.resolve above)
278
+ // — unlike pipeline.ts, which always records ctx.filePath, already
279
+ // canonicalized to on-disk casing by createDispatchContext (#2016/
280
+ // #3098). Normalize before recording so a mis-cased analyze call lands
281
+ // under the SAME widget-state key a canonical per-edit write already
282
+ // uses, instead of an orphaned key no reader (raw or normalized) can
283
+ // reach.
284
+ recordDiagnostics(normalizeMapKey(absPath), result.diagnostics);
278
285
  if (result.diagnostics.length > 0) {
279
286
  getDiagnosticTracker().trackShown(result.diagnostics);
280
287
  }
@@ -161,6 +161,7 @@ function adoptCanonicalCasing(held, canonical) {
161
161
  * (needed for new files where we haven't written yet)
162
162
  *
163
163
  * On POSIX:
164
+ * - Folds `.`, `..` and duplicate separators first (#3184, see below).
164
165
  * - If the file exists: adopts the on-disk CASING of the trailing segments
165
166
  * (see `adoptCanonicalCasing`) — no lowercasing, no symlink resolution.
166
167
  * - If the file doesn't exist: returns the path as-is, case-PRESERVING. On a
@@ -169,6 +170,26 @@ function adoptCanonicalCasing(held, canonical) {
169
170
  * answer for a path that EXISTS can tell the two apart, and for a path that
170
171
  * does not exist there is no such answer to ask for.
171
172
  *
173
+ * Why the POSIX arm folds dot segments (#3184): the casing arm returns the
174
+ * caller's own spelling whenever `adoptCanonicalCasing` changes nothing, and
175
+ * for `<base>/src/../src/a.ts` it always changes nothing — `realpath` answers
176
+ * a string with FEWER segments, which a casing-only rewrite cannot express, so
177
+ * it declines and the caller's un-folded spelling came back as the map key.
178
+ * Every canonical writer keys through `path.resolve` first (`ctx.filePath` =
179
+ * `normalizeMapKey(resolveAgainstAncestors(...))`, `clients/dispatch/
180
+ * runner-context.ts:49`), so a consumer that passes an ALREADY-absolute
181
+ * agent-typed path straight in (`tools/lens-diagnostic-mark.ts`,
182
+ * `clients/mcp/analyze.ts`) derived an orphan key that no reader could reach.
183
+ * Folding here is pure string algebra — no cwd, no filesystem — so a relative
184
+ * path stays relative (`src/../x` → `x`, `../x` → `../x`, never resolved
185
+ * against `process.cwd()`; refs #2490, where a cwd fold broke every monorepo)
186
+ * and a symlinked package still keys under the path the caller held. These
187
+ * are `path.resolve`'s own TEXTUAL `..` semantics, which is exactly what
188
+ * makes a folded reader key equal to the canonical writer's key: where a
189
+ * `..` sits right after a symlinked directory, textual folding and the
190
+ * kernel disagree, and both sides of every comparison take the textual
191
+ * answer because every canonical writer already resolved that way.
192
+ *
172
193
  * Why POSIX canonicalizes casing at all (#3098, the #1024 defect's live half):
173
194
  * a case-insensitive POSIX filesystem — macOS's default APFS, `nocase` vfat /
174
195
  * ntfs3 / cifs mounts — makes `SUB/a.ts` and `sub/a.ts` ONE file, so a raw
@@ -194,16 +215,37 @@ export function normalizeFilePath(filePath) {
194
215
  // Convert backslashes to forward slashes first
195
216
  const normalized = filePath.replace(/\\/g, "/");
196
217
  if (process.platform !== "win32" && !isWindowsPath(normalized)) {
218
+ // #3184. `path.posix`, not the host default: this branch is already
219
+ // committed to POSIX parsing of a slash-folded string (shape 2). Only
220
+ // the POSIX arm needs this — the win32 arm below reaches `realpath` or
221
+ // `win32.resolve`/`win32.normalize` on every path, all of which fold
222
+ // dot segments already (measured: `C:\repo\src\..\src\a.ts` →
223
+ // `c:/repo/src/a.ts` on this POSIX host, before this change) — and
224
+ // folding BEFORE the arms would also move which arm a degenerate
225
+ // drive-letter path selects (`path.posix.normalize("C:/repo/../..")`
226
+ // is `"."`, no longer Windows-shaped).
227
+ // Two inputs keep the caller's spelling instead:
228
+ // - "" is a non-path sentinel in this codebase's path-typed fields
229
+ // (see `normalizeLoggedPath`'s doc); `posix.normalize("")` invents
230
+ // ".", the process cwd.
231
+ // - a UNC root (`\\server\share`, slash-folded to `//server/share`)
232
+ // reaches THIS arm on a POSIX host, because `isWindowsPath` tests
233
+ // the already-folded string and sees no backslash; POSIX
234
+ // `normalize` collapses its leading `//` to `/`, renaming the path
235
+ // to an unrelated local one.
236
+ const folded = normalized === "" || normalized.startsWith("//")
237
+ ? normalized
238
+ : path.posix.normalize(normalized);
197
239
  try {
198
- const canonical = realpathSync.native(normalized);
199
- // Fast path, not a guard: both arms answer `normalized` when the
240
+ const canonical = realpathSync.native(folded);
241
+ // Fast path, not a guard: both arms answer `folded` when the
200
242
  // strings match, but skipping the two `split`s there is a measured
201
243
  // 1.9 vs 2.3 microseconds per call on the per-edit seam (#3098).
202
- if (canonical === normalized)
203
- return normalized;
204
- const adopted = adoptCanonicalCasing(normalized, canonical);
205
- if (adopted === normalized)
206
- return normalized;
244
+ if (canonical === folded)
245
+ return folded;
246
+ const adopted = adoptCanonicalCasing(folded, canonical);
247
+ if (adopted === folded)
248
+ return folded;
207
249
  // The rewrite is string algebra and can land on a DIFFERENT file
208
250
  // (#3159 review round 2, F1 — see `adoptCanonicalCasing`). Adopt it
209
251
  // only once the filesystem agrees it still names the file the caller
@@ -213,11 +255,11 @@ export function normalizeFilePath(filePath) {
213
255
  // never on an already-canonical path. A throw here (the rewritten
214
256
  // path does not exist, the #2490 monorepo case) lands in the catch
215
257
  // below and keeps the caller's spelling, which is the same answer.
216
- return realpathSync.native(adopted) === canonical ? adopted : normalized;
258
+ return realpathSync.native(adopted) === canonical ? adopted : folded;
217
259
  }
218
260
  catch {
219
261
  // Does not exist (or is unreadable): case-preserving, as above.
220
- return normalized;
262
+ return folded;
221
263
  }
222
264
  }
223
265
  // Windows: try realpathSync.native() for canonical casing