@skillcap/gdh 0.25.4 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/INSTALL-BUNDLE.json +1 -1
  2. package/RELEASE-SPAN-UPDATE-CONTRACTS.json +147 -0
  3. package/node_modules/@gdh/adapters/dist/claude-settings-patch.d.ts.map +1 -1
  4. package/node_modules/@gdh/adapters/dist/claude-settings-patch.js +38 -15
  5. package/node_modules/@gdh/adapters/dist/claude-settings-patch.js.map +1 -1
  6. package/node_modules/@gdh/adapters/dist/index.d.ts +12 -0
  7. package/node_modules/@gdh/adapters/dist/index.d.ts.map +1 -1
  8. package/node_modules/@gdh/adapters/dist/index.js +21 -0
  9. package/node_modules/@gdh/adapters/dist/index.js.map +1 -1
  10. package/node_modules/@gdh/adapters/dist/self-update-mechanics.d.ts.map +1 -1
  11. package/node_modules/@gdh/adapters/dist/self-update-mechanics.js +49 -16
  12. package/node_modules/@gdh/adapters/dist/self-update-mechanics.js.map +1 -1
  13. package/node_modules/@gdh/adapters/dist/skill-rendering.d.ts +5 -2
  14. package/node_modules/@gdh/adapters/dist/skill-rendering.d.ts.map +1 -1
  15. package/node_modules/@gdh/adapters/dist/skill-rendering.js +39 -0
  16. package/node_modules/@gdh/adapters/dist/skill-rendering.js.map +1 -1
  17. package/node_modules/@gdh/adapters/dist/templates/authoring-hook.js.tpl +200 -15
  18. package/node_modules/@gdh/adapters/package.json +8 -8
  19. package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.d.ts +1 -0
  20. package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.d.ts.map +1 -1
  21. package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.js +1 -0
  22. package/node_modules/@gdh/authoring/dist/diagnostics-broker-contract.js.map +1 -1
  23. package/node_modules/@gdh/authoring/dist/diagnostics-broker.d.ts +2 -1
  24. package/node_modules/@gdh/authoring/dist/diagnostics-broker.d.ts.map +1 -1
  25. package/node_modules/@gdh/authoring/dist/diagnostics-broker.js +91 -13
  26. package/node_modules/@gdh/authoring/dist/diagnostics-broker.js.map +1 -1
  27. package/node_modules/@gdh/authoring/dist/index.d.ts +1 -1
  28. package/node_modules/@gdh/authoring/dist/index.d.ts.map +1 -1
  29. package/node_modules/@gdh/authoring/dist/index.js +2 -1
  30. package/node_modules/@gdh/authoring/dist/index.js.map +1 -1
  31. package/node_modules/@gdh/authoring/dist/lsp-warmup.d.ts +30 -0
  32. package/node_modules/@gdh/authoring/dist/lsp-warmup.d.ts.map +1 -0
  33. package/node_modules/@gdh/authoring/dist/lsp-warmup.js +213 -0
  34. package/node_modules/@gdh/authoring/dist/lsp-warmup.js.map +1 -0
  35. package/node_modules/@gdh/authoring/dist/lsp.d.ts +8 -1
  36. package/node_modules/@gdh/authoring/dist/lsp.d.ts.map +1 -1
  37. package/node_modules/@gdh/authoring/dist/lsp.js +256 -104
  38. package/node_modules/@gdh/authoring/dist/lsp.js.map +1 -1
  39. package/node_modules/@gdh/authoring/dist/scene-resource.d.ts.map +1 -1
  40. package/node_modules/@gdh/authoring/dist/scene-resource.js +140 -0
  41. package/node_modules/@gdh/authoring/dist/scene-resource.js.map +1 -1
  42. package/node_modules/@gdh/authoring/package.json +2 -2
  43. package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
  44. package/node_modules/@gdh/cli/dist/index.js +75 -10
  45. package/node_modules/@gdh/cli/dist/index.js.map +1 -1
  46. package/node_modules/@gdh/cli/dist/self-update.d.ts.map +1 -1
  47. package/node_modules/@gdh/cli/dist/self-update.js +66 -10
  48. package/node_modules/@gdh/cli/dist/self-update.js.map +1 -1
  49. package/node_modules/@gdh/cli/package.json +10 -10
  50. package/node_modules/@gdh/core/dist/index.d.ts +178 -6
  51. package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
  52. package/node_modules/@gdh/core/dist/index.js +44 -4
  53. package/node_modules/@gdh/core/dist/index.js.map +1 -1
  54. package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.d.ts +15 -0
  55. package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.d.ts.map +1 -1
  56. package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.js +18 -0
  57. package/node_modules/@gdh/core/dist/migrations/managed-surface-classes.js.map +1 -1
  58. package/node_modules/@gdh/core/dist/migrations/managed-target-surface-inventory.d.ts.map +1 -1
  59. package/node_modules/@gdh/core/dist/migrations/managed-target-surface-inventory.js +2 -0
  60. package/node_modules/@gdh/core/dist/migrations/managed-target-surface-inventory.js.map +1 -1
  61. package/node_modules/@gdh/core/package.json +1 -1
  62. package/node_modules/@gdh/docs/dist/templates/guidance/authoring-and-validation.md.tpl +4 -5
  63. package/node_modules/@gdh/docs/package.json +2 -2
  64. package/node_modules/@gdh/mcp/dist/index.d.ts.map +1 -1
  65. package/node_modules/@gdh/mcp/dist/index.js +30 -1
  66. package/node_modules/@gdh/mcp/dist/index.js.map +1 -1
  67. package/node_modules/@gdh/mcp/package.json +8 -8
  68. package/node_modules/@gdh/observability/package.json +2 -2
  69. package/node_modules/@gdh/runtime/package.json +2 -2
  70. package/node_modules/@gdh/scan/package.json +3 -3
  71. package/node_modules/@gdh/verify/package.json +7 -7
  72. package/package.json +11 -11
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
5
5
  import net from "node:net";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { GDH_MANAGED_LSP_SURFACE_VERSION, resolveConfiguredGodotEditorBin } from "@gdh/core";
8
+ import { GDH_MANAGED_LSP_MIN_GODOT_VERSION, GDH_MANAGED_LSP_SURFACE_VERSION, resolveConfiguredGodotEditorBin, } from "@gdh/core";
9
9
  import { connectGodotLspClient, GdhLspClientError, } from "./lsp-client.js";
10
10
  import { summarizeBlockingDiagnostics } from "./diagnostics-summary.js";
11
11
  import { classifyDiagnosticsFreshness, DIAGNOSTICS_BROKER_DEFAULT_FRESHNESS_STALE_AFTER_MS, resolveDiagnosticsBrokerPaths, } from "./diagnostics-broker-contract.js";
@@ -24,6 +24,7 @@ const MANAGED_LSP_HEALTH_RETRY_DELAY_MS = 100;
24
24
  const MANAGED_LSP_DIAGNOSTIC_CONNECT_TIMEOUT_MS = 1_000;
25
25
  const MANAGED_LSP_DIAGNOSTIC_REQUEST_TIMEOUT_MS = 1_000;
26
26
  const MANAGED_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS = 10_000;
27
+ const MANAGED_LSP_POST_EDIT_DIAGNOSTIC_WAIT_TIMEOUT_MS = 1_500;
27
28
  class GdhManagedLspStateLockError extends Error {
28
29
  reason;
29
30
  reasons;
@@ -60,7 +61,7 @@ async function readBrokerSnapshot(targetPath) {
60
61
  *
61
62
  * Returns null otherwise (fall through to direct LSP check).
62
63
  */
63
- function extractBrokerDiagnosticsIfFresh(snapshot, currentInstanceId, requestedUris) {
64
+ function extractBrokerDiagnosticsIfFresh(snapshot, currentInstanceId, requestedUris, currentContentHashesByUri) {
64
65
  if (snapshot.lspInstanceId !== currentInstanceId) {
65
66
  return null;
66
67
  }
@@ -73,6 +74,12 @@ function extractBrokerDiagnosticsIfFresh(snapshot, currentInstanceId, requestedU
73
74
  if (file === undefined) {
74
75
  return null; // Requested URI not in broker — fall through.
75
76
  }
77
+ if (typeof file.contentHash !== "string" || file.contentHash.length === 0) {
78
+ return null; // Pre-content-hash snapshots cannot prove current file content.
79
+ }
80
+ if (currentContentHashesByUri.get(uri) !== file.contentHash) {
81
+ return null; // Snapshot was captured for different file content.
82
+ }
76
83
  const freshness = classifyDiagnosticsFreshness({
77
84
  fileSnapshot: {
78
85
  uri: file.uri,
@@ -115,122 +122,190 @@ export async function getManagedLspStatus(input) {
115
122
  return unavailable;
116
123
  }
117
124
  try {
118
- return await withManagedLspStateLock(worktree, input.owner ?? "gdh lsp status", async (stateLock) => await withLease(worktree, input.owner ?? "gdh lsp status", async (lease, activeLeaseCount) => {
119
- let cleanedStaleInstance = false;
120
- let cleanupReasons = [];
125
+ return await withManagedLspStateLock(worktree, input.owner ?? "gdh lsp status", async (stateLock) => {
121
126
  const projectPath = path.resolve(input.targetPath, input.status.primaryProjectPath ?? ".");
122
127
  const expectedIdentity = await resolveCurrentManagedLspIdentity(worktree, projectPath);
123
- const existing = await readPersistedInstance(worktree);
124
- if (existing !== null) {
125
- const validation = await validatePersistedInstance(existing.instance, expectedIdentity);
126
- if (validation.reusable) {
127
- const reusedInstance = {
128
- ...existing.instance,
129
- lastValidatedAt: new Date().toISOString(),
130
- trust: validation.trust,
131
- };
132
- await persistInstance(worktree, {
133
- ...existing,
134
- instance: reusedInstance,
135
- rawPayload: validation.rawPayload ?? existing.rawPayload,
128
+ // Phase 81 / LSP-08 / D-17: refuse-to-launch when the configured
129
+ // Godot version is below the managed-LSP floor. Short-circuits
130
+ // BEFORE lsp-leases.json acquisition (withLease) and BEFORE
131
+ // persisted-instance reads — but executes inside the lsp.lock
132
+ // (stateLock) so identity resolution is serialized with other
133
+ // lifecycle mutations. Misconfigured 4.5.0/4.5.1 setups never
134
+ // acquire a lease or block other waiters.
135
+ if (expectedIdentity.editorVersionReason === "godot_editor_version_unsupported_for_lsp") {
136
+ return createUnavailableStatus({
137
+ targetPath: input.targetPath,
138
+ readiness: input.status.readiness,
139
+ worktree,
140
+ inventoryObservedAt: input.status.inventoryObservedAt,
141
+ projectConfigPresent: input.projectConfig !== null,
142
+ availability: "unavailable",
143
+ reasons: dedupe([
144
+ ...stateLock.recoveredReasons,
145
+ "godot_editor_version_unsupported_for_lsp",
146
+ ]),
147
+ summary: "The configured Godot editor version does not meet the minimum version required for the managed authoring.lsp. Upgrade to Godot 4.5.2 or later.",
148
+ versionFloorAdvisory: {
149
+ detectedVersion: expectedIdentity.editorVersion,
150
+ minimumVersion: GDH_MANAGED_LSP_MIN_GODOT_VERSION,
151
+ recommendedVersions: ["4.5.2", "4.6.x"],
152
+ },
153
+ });
154
+ }
155
+ return await withLease(worktree, input.owner ?? "gdh lsp status", async (lease, activeLeaseCount) => {
156
+ let cleanedStaleInstance = false;
157
+ let cleanupReasons = [];
158
+ const existing = await readPersistedInstance(worktree);
159
+ if (existing !== null) {
160
+ const validation = await validatePersistedInstance(existing.instance, expectedIdentity);
161
+ if (validation.reusable) {
162
+ const reusedInstance = {
163
+ ...existing.instance,
164
+ lastValidatedAt: new Date().toISOString(),
165
+ trust: validation.trust,
166
+ };
167
+ await persistInstance(worktree, {
168
+ ...existing,
169
+ instance: reusedInstance,
170
+ rawPayload: validation.rawPayload ?? existing.rawPayload,
171
+ });
172
+ return createStatusResult({
173
+ targetPath: input.targetPath,
174
+ readiness: input.status.readiness,
175
+ availability: "available",
176
+ status: validation.trust === "trusted" ? "ready" : "degraded",
177
+ validationMode: existing.validationMode,
178
+ summary: validation.trust === "trusted"
179
+ ? "Reused the managed authoring.lsp instance for this worktree."
180
+ : "Reused the managed authoring.lsp instance, but trust is degraded and the instance should be treated cautiously.",
181
+ reasons: dedupe([
182
+ ...stateLock.recoveredReasons,
183
+ ...(validation.trust === "trusted" ? [] : ["lsp_instance_degraded"]),
184
+ ]),
185
+ worktree,
186
+ lease,
187
+ activeLeaseCount,
188
+ reusedInstance: true,
189
+ cleanedStaleInstance,
190
+ instance: reusedInstance,
191
+ inventoryObservedAt: input.status.inventoryObservedAt,
192
+ projectConfigPresent: input.projectConfig !== null,
193
+ rawPayload: validation.rawPayload ?? existing.rawPayload,
194
+ });
195
+ }
196
+ cleanedStaleInstance = true;
197
+ cleanupReasons = validation.reasons;
198
+ await cleanupPersistedInstance(worktree, existing.instance, validation.reason, {
199
+ expectedIdentity,
136
200
  });
137
- return createStatusResult({
201
+ // Phase 81 / LSP-04 / D-11 / Pitfall 7: when the LSP instance
202
+ // identity has changed, the broker snapshot referenced the
203
+ // now-defunct instance. Prune so the next read returns
204
+ // broker_metadata_missing instead of lsp_instance_identity_mismatch.
205
+ {
206
+ const { pruneAuthoringDiagnostics } = await import("./diagnostics-broker.js");
207
+ await pruneAuthoringDiagnostics({
208
+ targetPath: input.targetPath,
209
+ status: input.status,
210
+ projectConfig: input.projectConfig,
211
+ });
212
+ }
213
+ }
214
+ if (input.launchPolicy === "reuse_existing") {
215
+ return createUnavailableStatus({
138
216
  targetPath: input.targetPath,
139
217
  readiness: input.status.readiness,
140
- availability: "available",
141
- status: validation.trust === "trusted" ? "ready" : "degraded",
142
- validationMode: existing.validationMode,
143
- summary: validation.trust === "trusted"
144
- ? "Reused the managed authoring.lsp instance for this worktree."
145
- : "Reused the managed authoring.lsp instance, but trust is degraded and the instance should be treated cautiously.",
218
+ worktree,
219
+ inventoryObservedAt: input.status.inventoryObservedAt,
220
+ projectConfigPresent: input.projectConfig !== null,
221
+ availability: lspCapability?.availability ?? "available",
146
222
  reasons: dedupe([
147
223
  ...stateLock.recoveredReasons,
148
- ...(validation.trust === "trusted" ? [] : ["lsp_instance_degraded"]),
224
+ ...(cleanedStaleInstance ? ["stale_lsp_instance_cleaned"] : []),
225
+ ...cleanupReasons,
226
+ "lsp_instance_not_running",
227
+ "post_edit_fast_path_did_not_launch_lsp",
149
228
  ]),
150
- worktree,
229
+ summary: "No reusable managed authoring.lsp instance is currently running; post-edit fast check did not launch Godot.",
151
230
  lease,
152
231
  activeLeaseCount,
153
- reusedInstance: true,
154
232
  cleanedStaleInstance,
155
- instance: reusedInstance,
233
+ rawPayload: {
234
+ launchPolicy: "reuse_existing",
235
+ projectPath,
236
+ cleanedStaleInstance,
237
+ cleanupReasons,
238
+ },
239
+ });
240
+ }
241
+ const launchResult = await launchManagedInstance(worktree, projectPath);
242
+ if ("unavailable" in launchResult) {
243
+ return createUnavailableStatus({
244
+ targetPath: input.targetPath,
245
+ readiness: input.status.readiness,
246
+ worktree,
156
247
  inventoryObservedAt: input.status.inventoryObservedAt,
157
248
  projectConfigPresent: input.projectConfig !== null,
158
- rawPayload: validation.rawPayload ?? existing.rawPayload,
249
+ availability: "unavailable",
250
+ reasons: [
251
+ ...stateLock.recoveredReasons,
252
+ ...(cleanedStaleInstance ? ["stale_lsp_instance_cleaned"] : []),
253
+ ...cleanupReasons,
254
+ ...launchResult.unavailable.reasons,
255
+ ],
256
+ summary: launchResult.unavailable.summary,
257
+ lease,
258
+ activeLeaseCount,
259
+ cleanedStaleInstance,
260
+ rawPayload: launchResult.unavailable.rawPayload,
159
261
  });
160
262
  }
161
- cleanedStaleInstance = true;
162
- cleanupReasons = validation.reasons;
163
- await cleanupPersistedInstance(worktree, existing.instance, validation.reason, {
164
- expectedIdentity,
263
+ const launchedInstance = {
264
+ instanceId: launchResult.instanceId,
265
+ launcher: launchResult.launcher,
266
+ port: launchResult.port,
267
+ pid: launchResult.pid,
268
+ projectPath,
269
+ command: launchResult.command,
270
+ trust: launchResult.trust,
271
+ launchedAt: new Date().toISOString(),
272
+ lastValidatedAt: new Date().toISOString(),
273
+ identity: launchResult.identity,
274
+ };
275
+ await persistInstance(worktree, {
276
+ schemaVersion: LSP_INSTANCE_SCHEMA_VERSION,
277
+ instance: launchedInstance,
278
+ validationMode: launchResult.validationMode,
279
+ diagnostics: launchResult.diagnostics,
280
+ rawPayload: launchResult.rawPayload,
165
281
  });
166
- }
167
- const launchResult = await launchManagedInstance(worktree, projectPath);
168
- if ("unavailable" in launchResult) {
169
- return createUnavailableStatus({
282
+ return createStatusResult({
170
283
  targetPath: input.targetPath,
171
284
  readiness: input.status.readiness,
172
- worktree,
173
- inventoryObservedAt: input.status.inventoryObservedAt,
174
- projectConfigPresent: input.projectConfig !== null,
175
- availability: "unavailable",
176
- reasons: [
285
+ availability: "available",
286
+ status: launchResult.trust === "trusted" ? "ready" : "degraded",
287
+ validationMode: launchResult.validationMode,
288
+ summary: launchResult.trust === "trusted"
289
+ ? "Started a managed authoring.lsp instance for this worktree."
290
+ : "Started a managed authoring.lsp instance, but trust is degraded and validation should be treated cautiously.",
291
+ reasons: dedupe([
177
292
  ...stateLock.recoveredReasons,
178
293
  ...(cleanedStaleInstance ? ["stale_lsp_instance_cleaned"] : []),
179
294
  ...cleanupReasons,
180
- ...launchResult.unavailable.reasons,
181
- ],
182
- summary: launchResult.unavailable.summary,
295
+ ...(launchResult.trust === "trusted" ? [] : ["lsp_instance_degraded"]),
296
+ ]),
297
+ worktree,
183
298
  lease,
184
299
  activeLeaseCount,
300
+ reusedInstance: false,
185
301
  cleanedStaleInstance,
186
- rawPayload: launchResult.unavailable.rawPayload,
302
+ instance: launchedInstance,
303
+ inventoryObservedAt: input.status.inventoryObservedAt,
304
+ projectConfigPresent: input.projectConfig !== null,
305
+ rawPayload: launchResult.rawPayload,
187
306
  });
188
- }
189
- const launchedInstance = {
190
- instanceId: launchResult.instanceId,
191
- launcher: launchResult.launcher,
192
- port: launchResult.port,
193
- pid: launchResult.pid,
194
- projectPath,
195
- command: launchResult.command,
196
- trust: launchResult.trust,
197
- launchedAt: new Date().toISOString(),
198
- lastValidatedAt: new Date().toISOString(),
199
- identity: launchResult.identity,
200
- };
201
- await persistInstance(worktree, {
202
- schemaVersion: LSP_INSTANCE_SCHEMA_VERSION,
203
- instance: launchedInstance,
204
- validationMode: launchResult.validationMode,
205
- diagnostics: launchResult.diagnostics,
206
- rawPayload: launchResult.rawPayload,
207
- });
208
- return createStatusResult({
209
- targetPath: input.targetPath,
210
- readiness: input.status.readiness,
211
- availability: "available",
212
- status: launchResult.trust === "trusted" ? "ready" : "degraded",
213
- validationMode: launchResult.validationMode,
214
- summary: launchResult.trust === "trusted"
215
- ? "Started a managed authoring.lsp instance for this worktree."
216
- : "Started a managed authoring.lsp instance, but trust is degraded and validation should be treated cautiously.",
217
- reasons: dedupe([
218
- ...stateLock.recoveredReasons,
219
- ...(cleanedStaleInstance ? ["stale_lsp_instance_cleaned"] : []),
220
- ...cleanupReasons,
221
- ...(launchResult.trust === "trusted" ? [] : ["lsp_instance_degraded"]),
222
- ]),
223
- worktree,
224
- lease,
225
- activeLeaseCount,
226
- reusedInstance: false,
227
- cleanedStaleInstance,
228
- instance: launchedInstance,
229
- inventoryObservedAt: input.status.inventoryObservedAt,
230
- projectConfigPresent: input.projectConfig !== null,
231
- rawPayload: launchResult.rawPayload,
232
307
  });
233
- }));
308
+ });
234
309
  }
235
310
  catch (error) {
236
311
  if (error instanceof GdhManagedLspStateLockError) {
@@ -248,6 +323,36 @@ export async function getManagedLspStatus(input) {
248
323
  throw error;
249
324
  }
250
325
  }
326
+ // ─────────────────────────────────────────────────────────────────────────────
327
+ // Phase 82 / LSP-02 / LSP-07 — managed-LSP warmup surface.
328
+ //
329
+ // `warmupManagedLsp` is the in-process entry point that the post-edit hook
330
+ // detach-spawns (via `gdh lsp warmup`) and the MCP `authoring.warmup` tool
331
+ // invokes. It is idempotent: concurrent callers fan in via a single-attempt
332
+ // `<targetPath>/.gdh-state/lsp.lock` acquire so the underlying Godot launch
333
+ // happens at most once.
334
+ //
335
+ // The implementation lives in `./lsp-warmup.js`. That sibling module imports
336
+ // `getManagedLspStatus` from `./lsp.js`, which is the documented vitest
337
+ // workaround for the "vi.mock cannot intercept same-module internal calls"
338
+ // pitfall (https://vitest.dev/guide/mocking/modules — Internal Method Calls).
339
+ //
340
+ // IMPORTANT: this is a thin wrapper that DEFERS to the impl via dynamic
341
+ // `await import("./lsp-warmup.js")`. The deferral guarantees `lsp-warmup.ts`
342
+ // is NOT a static dependency of `lsp.ts`. Were it static (a re-export), it
343
+ // would be evaluated as part of `./lsp.js`'s module graph, with its
344
+ // `import { getManagedLspStatus } from "./lsp.js"` resolving against the
345
+ // partially-evaluated original module record — so the unit-test mock
346
+ // (`vi.mock("./lsp.js", ...)`) would never reach the warmup impl. By
347
+ // keeping it deferred, the test's `await import("./lsp-warmup.js")` is
348
+ // the FIRST point at which lsp-warmup.ts loads, and at that time the mock
349
+ // is already registered, so the `./lsp.js` import resolves to the mocked
350
+ // module. See `./lsp-warmup.ts` and `./lsp-warmup.test.ts` for context.
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+ export async function warmupManagedLsp(input) {
353
+ const { warmupManagedLsp: impl } = await import("./lsp-warmup.js");
354
+ return impl(input);
355
+ }
251
356
  export async function checkManagedLsp(input) {
252
357
  const worktree = resolveWorktreeIdentity(input.targetPath, input.status, input.projectConfig);
253
358
  return await withManagedLspLifecycleLock(worktree, "check", "gdh lsp check", async (stateLock) => {
@@ -370,6 +475,19 @@ export async function stopManagedLsp(input) {
370
475
  killProcess: true,
371
476
  expectedIdentity,
372
477
  });
478
+ // Phase 81 / LSP-04 / D-11: prune the broker snapshot inside the lifecycle
479
+ // lock so concurrent readers cannot observe a stale snapshot pointing at
480
+ // the just-stopped LSP instance. The .primed marker is intentionally
481
+ // preserved (D-10) — broker absence remains "broker_metadata_missing"
482
+ // until the next refresh, distinct from "broker_not_yet_primed".
483
+ {
484
+ const { pruneAuthoringDiagnostics } = await import("./diagnostics-broker.js");
485
+ await pruneAuthoringDiagnostics({
486
+ targetPath: input.targetPath,
487
+ status: input.status,
488
+ projectConfig: input.projectConfig,
489
+ });
490
+ }
373
491
  return createLifecycleCommandResult({
374
492
  targetPath: input.targetPath,
375
493
  command: "stop",
@@ -820,7 +938,8 @@ async function tryBrokerDiagnosticsForPostEdit(input) {
820
938
  return null;
821
939
  }
822
940
  const requestedUris = resolvedFiles.map((f) => pathToFileURL(f).href);
823
- const brokerResult = extractBrokerDiagnosticsIfFresh(snapshot, input.instance.instanceId, requestedUris);
941
+ const currentContentHashesByUri = await hashFilesByUri(resolvedFiles);
942
+ const brokerResult = extractBrokerDiagnosticsIfFresh(snapshot, input.instance.instanceId, requestedUris, currentContentHashesByUri);
824
943
  if (brokerResult === null) {
825
944
  return null;
826
945
  }
@@ -880,7 +999,7 @@ async function collectManagedGdscriptDiagnostics(input) {
880
999
  const uri = pathToFileURL(filePath).href;
881
1000
  const diagnosticsPromise = client.waitForDiagnostics({
882
1001
  uri,
883
- timeoutMs: resolveManagedLspDiagnosticWaitTimeoutMs(),
1002
+ timeoutMs: resolveManagedLspDiagnosticWaitTimeoutMs(input.mode),
884
1003
  });
885
1004
  await client.didOpen({
886
1005
  uri,
@@ -1071,14 +1190,29 @@ function canonicalExistingPath(filePath) {
1071
1190
  return path.resolve(filePath);
1072
1191
  }
1073
1192
  }
1074
- function resolveManagedLspDiagnosticWaitTimeoutMs() {
1193
+ async function hashFilesByUri(filePaths) {
1194
+ const hashes = new Map();
1195
+ for (const filePath of filePaths) {
1196
+ const text = await fs.readFile(filePath, "utf8").catch(() => "");
1197
+ hashes.set(pathToFileURL(filePath).href, hashText(text));
1198
+ }
1199
+ return hashes;
1200
+ }
1201
+ function hashText(text) {
1202
+ return createHash("sha256").update(text).digest("hex");
1203
+ }
1204
+ function resolveManagedLspDiagnosticWaitTimeoutMs(mode) {
1075
1205
  const configured = process.env["GDH_TEST_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS"];
1076
1206
  if (configured === undefined) {
1077
- return MANAGED_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS;
1207
+ return mode === "post_edit"
1208
+ ? MANAGED_LSP_POST_EDIT_DIAGNOSTIC_WAIT_TIMEOUT_MS
1209
+ : MANAGED_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS;
1078
1210
  }
1079
1211
  const parsed = Number(configured);
1080
1212
  if (!Number.isFinite(parsed) || parsed <= 0) {
1081
- return MANAGED_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS;
1213
+ return mode === "post_edit"
1214
+ ? MANAGED_LSP_POST_EDIT_DIAGNOSTIC_WAIT_TIMEOUT_MS
1215
+ : MANAGED_LSP_DIAGNOSTIC_WAIT_TIMEOUT_MS;
1082
1216
  }
1083
1217
  return parsed;
1084
1218
  }
@@ -1102,13 +1236,20 @@ function resolveWorktreeIdentity(targetPath, status, projectConfig) {
1102
1236
  async function resolveCurrentManagedLspIdentity(worktree, projectPath) {
1103
1237
  const testMode = process.env["GDH_TEST_LSP_MODE"];
1104
1238
  if (testMode) {
1239
+ const syntheticVersion = normalizeTestMode(testMode);
1240
+ // Phase 81 / LSP-08 / D-15: honor the version floor for synthetic test versions
1241
+ // so unit tests can exercise the floor-blocked path via GDH_TEST_LSP_MODE.
1242
+ const syntheticFloorViolation = typeof syntheticVersion === "string" &&
1243
+ (syntheticVersion.startsWith("4.5.0") || syntheticVersion.startsWith("4.5.1"));
1105
1244
  return await createManagedLspInstanceIdentity({
1106
1245
  worktree,
1107
1246
  projectPath,
1108
1247
  launcher: "test_fake",
1109
- editorBinPath: `test_fake:${normalizeTestMode(testMode)}`,
1110
- editorVersion: normalizeTestMode(testMode),
1111
- editorVersionReason: null,
1248
+ editorBinPath: `test_fake:${syntheticVersion}`,
1249
+ editorVersion: syntheticVersion,
1250
+ editorVersionReason: syntheticFloorViolation
1251
+ ? "godot_editor_version_unsupported_for_lsp"
1252
+ : null,
1112
1253
  });
1113
1254
  }
1114
1255
  const godotBin = await resolveConfiguredGodotEditorBin({
@@ -1116,15 +1257,25 @@ async function resolveCurrentManagedLspIdentity(worktree, projectPath) {
1116
1257
  environment: process.env,
1117
1258
  });
1118
1259
  const version = godotBin === null ? null : await probeGodotEditorVersion(godotBin);
1260
+ const versionStr = version?.version ?? null;
1261
+ // Phase 81 / LSP-08 / D-15 / Pitfall 4: Godot reports versions like
1262
+ // "4.5.0.stable" or "4.5.1.stable.official"; use a startsWith prefix match
1263
+ // on the 3-part version prefix to handle every suffix form. No semver
1264
+ // dependency is added — the floor check is intentionally narrow.
1265
+ const isFloorViolation = versionStr !== null &&
1266
+ (versionStr.startsWith("4.5.0") || versionStr.startsWith("4.5.1"));
1267
+ const editorVersionReason = godotBin === null
1268
+ ? "godot_editor_not_configured"
1269
+ : isFloorViolation
1270
+ ? "godot_editor_version_unsupported_for_lsp"
1271
+ : (version?.reason ?? null);
1119
1272
  return await createManagedLspInstanceIdentity({
1120
1273
  worktree,
1121
1274
  projectPath,
1122
1275
  launcher: "godot_editor",
1123
1276
  editorBinPath: godotBin,
1124
- editorVersion: version?.version ?? null,
1125
- editorVersionReason: godotBin === null
1126
- ? "godot_editor_not_configured"
1127
- : (version?.reason ?? "godot_editor_version_unavailable"),
1277
+ editorVersion: versionStr,
1278
+ editorVersionReason,
1128
1279
  });
1129
1280
  }
1130
1281
  async function createManagedLspInstanceIdentity(input) {
@@ -1976,6 +2127,7 @@ function createUnavailableStatus(input) {
1976
2127
  stateRootPath: input.worktree.stateRootPath,
1977
2128
  rawPayload: input.rawPayload ?? null,
1978
2129
  },
2130
+ versionFloorAdvisory: input.versionFloorAdvisory ?? null,
1979
2131
  };
1980
2132
  }
1981
2133
  function isPidAlive(pid) {