gdharness 0.13.4 → 0.13.6

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/build/index.js CHANGED
@@ -28755,7 +28755,7 @@ var TOOL_SPECS = [
28755
28755
  },
28756
28756
  {
28757
28757
  name: "editor_run",
28758
- description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for.",
28758
+ description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
28759
28759
  parameters: {
28760
28760
  projectPath: PROJECT_PATH,
28761
28761
  scene: {
@@ -28831,7 +28831,7 @@ var TOOL_SPECS = [
28831
28831
  },
28832
28832
  {
28833
28833
  name: "editor_rescan",
28834
- description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is both halves in order: a change to the declaring script, saved, and then a rescan. Measured downstream, neither works alone, and a change on its own reads as the cure having failed. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve can go out of that file with it, which one project saw and two could not reproduce; where it happens the answer names what the file lost under cacheLost, before a fresh engine, a CI run or a clone inherits it. Needs the editor connected.",
28834
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
28835
28835
  parameters: {
28836
28836
  projectPath: PROJECT_PATH,
28837
28837
  timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
@@ -29123,7 +29123,7 @@ var TOOL_SPECS = [
29123
29123
  says: {
29124
29124
  type: "string",
29125
29125
  ops: ["until"],
29126
- description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included."
29126
+ description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
29127
29127
  },
29128
29128
  timeoutMs: {
29129
29129
  type: "number",
@@ -30851,6 +30851,7 @@ class GodotServer {
30851
30851
  through: "gdharness",
30852
30852
  pid: started.process.pid ?? null,
30853
30853
  arguments: cmdArgs,
30854
+ transcript: started.transcript,
30854
30855
  refreshedClasses: refreshed.value,
30855
30856
  endedPreviousRun: ended ?? undefined,
30856
30857
  runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs),
@@ -31252,7 +31253,7 @@ class GodotServer {
31252
31253
  const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
31253
31254
  const before = projectPath === "" ? null : cachedClasses(projectPath);
31254
31255
  const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
31255
- const blind = projectPath === "" ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
31256
+ const blind = await this.classesTheEditorCannotSee(args);
31256
31257
  const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
31257
31258
  const first = await this.handleViaBridge("rescan_filesystem", args);
31258
31259
  if (first.isError) {
@@ -31265,7 +31266,7 @@ class GodotServer {
31265
31266
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
31266
31267
  busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
31267
31268
  }
31268
- if (!busy && atRisk.length > 0) {
31269
+ if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
31269
31270
  const until = Date.now() + CACHE_WRITE_MS;
31270
31271
  while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
31271
31272
  await new Promise((settle) => setTimeout(settle, 100));
@@ -31273,18 +31274,33 @@ class GodotServer {
31273
31274
  }
31274
31275
  const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
31275
31276
  const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
31277
+ const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
31276
31278
  const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
31277
31279
  const unseen = checked.unseen;
31280
+ const stillGone = lost.filter((name) => !restored.includes(name));
31278
31281
  return this.jsonTextResponse({
31279
- ok: !busy && unseen.length === 0 && lost.length === 0 && checked.unchecked === undefined,
31282
+ ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
31280
31283
  stillWorking: busy,
31281
31284
  waitedMs: Date.now() - started,
31282
31285
  unseenByEditor: unseen.length > 0 ? unseen : undefined,
31283
31286
  cacheLost: lost.length > 0 ? lost : undefined,
31287
+ cacheRestored: restored.length > 0 ? restored : undefined,
31284
31288
  classesUnchecked: checked.unchecked,
31285
- note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, and that list is shorter than the file was: these classes were in it before the scan and are not now. Every engine that reads the cache next, including a test run, will report them as unknown identifiers in files nobody touched. Run project_import refresh_classes to put them back, and do not rescan again until the editor has been restarted with editor_launch restart, because it will write the same short list over it." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
31289
+ note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : stillGone.length > 0 ? "The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time." : unseen.length > 0 ? "The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
31286
31290
  });
31287
31291
  }
31292
+ async rebuildCacheAfterLoss(projectPath, lost) {
31293
+ if (projectPath === "") {
31294
+ return [];
31295
+ }
31296
+ const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
31297
+ if (!rebuilt.ok) {
31298
+ this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
31299
+ return [];
31300
+ }
31301
+ const now = cachedClasses(projectPath);
31302
+ return now === null ? [] : lost.filter((name) => now.has(name));
31303
+ }
31288
31304
  async alsoSayWhatTheEditorCannotSee(answered, args) {
31289
31305
  const first = answered.content[0];
31290
31306
  if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",