gdharness 1.0.32 → 1.1.1

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/cli.js CHANGED
@@ -16440,7 +16440,7 @@ var init_tool_definitions = __esm(() => {
16440
16440
  },
16441
16441
  {
16442
16442
  name: "editor_rescan",
16443
- 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
16443
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
16444
16444
  parameters: {
16445
16445
  projectPath: PROJECT_PATH,
16446
16446
  timeoutMs: {
@@ -16546,7 +16546,7 @@ var init_tool_definitions = __esm(() => {
16546
16546
  says: {
16547
16547
  type: "string",
16548
16548
  ops: ["find"],
16549
- description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
16549
+ description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
16550
16550
  },
16551
16551
  limit: {
16552
16552
  type: "integer",
@@ -16596,7 +16596,7 @@ var init_tool_definitions = __esm(() => {
16596
16596
  },
16597
16597
  {
16598
16598
  name: "runtime_invoke",
16599
- description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
16599
+ description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
16600
16600
  parameters: {
16601
16601
  projectPath: RUNNING_PROJECT_PATH,
16602
16602
  pid: RUNNING_PID,
@@ -41802,6 +41802,17 @@ class GodotServer {
41802
41802
  const nearest = descendantsIn(tree, root).filter((pid) => isEngine(pid) && !ancestorsIn(tree, pid, root).some((one) => one !== root && isEngine(one)));
41803
41803
  return nearest.length === 1 ? nearest[0] : undefined;
41804
41804
  }
41805
+ async editorIsScanningOrImporting() {
41806
+ if (!this.godotBridge.isConnected()) {
41807
+ return false;
41808
+ }
41809
+ try {
41810
+ const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
41811
+ return status["scanning"] === true || status["importing"] === true;
41812
+ } catch {
41813
+ return false;
41814
+ }
41815
+ }
41805
41816
  async handleRescanFilesystem(args) {
41806
41817
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
41807
41818
  const started = Date.now();
@@ -41811,9 +41822,26 @@ class GodotServer {
41811
41822
  const blind = await this.classesTheEditorCannotSee(args);
41812
41823
  const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
41813
41824
  const ghosts = blind.stillHeld ?? [];
41814
- const first = await this.handleViaBridge("rescan_filesystem", args);
41815
- if (first.isError) {
41816
- return first;
41825
+ let waitedForEditorMs = 0;
41826
+ let first;
41827
+ for (;; ) {
41828
+ const busySince = Date.now();
41829
+ let wasBusy = false;
41830
+ while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
41831
+ wasBusy = true;
41832
+ await new Promise((settle) => setTimeout(settle, 100));
41833
+ }
41834
+ if (wasBusy) {
41835
+ waitedForEditorMs += Date.now() - busySince;
41836
+ }
41837
+ first = await this.handleViaBridge("rescan_filesystem", args);
41838
+ if (first.isError) {
41839
+ return first;
41840
+ }
41841
+ const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
41842
+ if (!declined || Date.now() - started >= timeoutMs) {
41843
+ break;
41844
+ }
41817
41845
  }
41818
41846
  let busy = true;
41819
41847
  while (busy && Date.now() - started < timeoutMs) {
@@ -41872,6 +41900,7 @@ class GodotServer {
41872
41900
  ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
41873
41901
  stillWorking: busy,
41874
41902
  waitedMs: Date.now() - started,
41903
+ waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
41875
41904
  heldBeforeReload: reloaded.heldBefore,
41876
41905
  reloadedMethods: reloaded.methods,
41877
41906
  reloadProblem: reloaded.problem,
@@ -656,10 +656,16 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
656
656
  return {"ok": false, "error": "Editor plugin unavailable"}
657
657
 
658
658
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
659
- if not Read.as_bool(args.get("statusOnly", false)):
659
+ # Not over a scan or an import the editor is already running: a scan asked for then starts a
660
+ # second reimport over the first, and the editor logs "Task 'reimport' already exists" and two
661
+ # conditions from its progress dialog. Declined and said, so the caller waits and asks again.
662
+ var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
663
+ var started: bool = false
664
+ if not Read.as_bool(args.get("statusOnly", false)) and not busy:
660
665
  _scans_finished_at_request = _scans_finished
661
666
  _scan_pending = true
662
667
  filesystem.scan()
668
+ started = true
663
669
 
664
670
  # Importing is reported separately from scanning, and a class is not registered until
665
671
  # both are done, so a caller watching only one of them can look too early.
@@ -677,6 +683,7 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
677
683
  _scan_pending = false
678
684
  return {
679
685
  "ok": true,
686
+ "started": started,
680
687
  "scanning": scanning,
681
688
  "importing": importing,
682
689
  "pending": _scan_pending,
@@ -104,6 +104,15 @@ func find_nodes(params: Dictionary) -> Dictionary:
104
104
  # find would have matched if the name had been read the way it was probably meant.
105
105
  var literal: bool = _is_literal(wanted["name"])
106
106
  var nearly: int = 0
107
+ # The same for the words: a glob is matched against the whole text, and one written as a prefix
108
+ # by habit, "Taken together*", missed the label whose sentence those words are in the middle of.
109
+ # Nothing found then read as the words not being on screen, which is the wrong conclusion when
110
+ # checking that a change to the text landed.
111
+ var widened_says: String = _widened(wanted["says"])
112
+ var widened: Dictionary[String, String] = {}
113
+ for filter: String in wanted:
114
+ widened[filter] = widened_says if filter == "says" else wanted[filter]
115
+ var nearly_said: int = 0
107
116
  var hidden: int = 0
108
117
  while not pending.is_empty():
109
118
  var node: Node = pending.pop_front()
@@ -118,6 +127,12 @@ func find_nodes(params: Dictionary) -> Dictionary:
118
127
  found.append(_found(node, wanted_property))
119
128
  elif rest and literal and str(node.name).containsn(wanted["name"]):
120
129
  nearly += 1
130
+ elif (
131
+ not widened_says.is_empty()
132
+ and _named(node, wanted["name"])
133
+ and _matches_apart_from_name(node, widened)
134
+ ):
135
+ nearly_said += 1
121
136
  # Internal children included, which they were not. A ConfirmationDialog builds its Yes and
122
137
  # its No as internal nodes, and a ScrollContainer its bars, so a find over a screen for
123
138
  # every Button came back without the two buttons the player is being asked to press:
@@ -141,6 +156,16 @@ func find_nodes(params: Dictionary) -> Dictionary:
141
156
  % [nearly, holding, wanted["name"], wanted["name"]]
142
157
  )
143
158
  )
159
+ if found.is_empty() and nearly_said > 0:
160
+ (
161
+ notes
162
+ . append(
163
+ (
164
+ 'says with a wildcard is matched as a glob against a node\'s whole text; "%s" would find %d node%s'
165
+ % [widened_says, nearly_said, "" if nearly_said == 1 else "s"]
166
+ )
167
+ )
168
+ )
144
169
  # How many the filter took out, so a short answer is not read as a small screen. Left out
145
170
  # entirely when nothing was hidden, which keeps a plain answer plain.
146
171
  if hidden > 0:
@@ -234,6 +259,15 @@ static func _says(said: String, wanted: String) -> bool:
234
259
  return said.containsn(words) if _is_literal(words) else said.matchn(words)
235
260
 
236
261
 
262
+ ## [param wanted], a glob, open at both ends so it matches its words anywhere in a text; "" when
263
+ ## it is not a glob, or already open at both ends, and so has nothing to suggest.
264
+ static func _widened(wanted: String) -> String:
265
+ if wanted.is_empty() or _is_literal(wanted):
266
+ return ""
267
+ var open: String = "*" + wanted.lstrip("*").rstrip("*") + "*"
268
+ return "" if open == wanted else open
269
+
270
+
237
271
  ## [param wanted] as words on a screen: a backslash followed by n is a line break.
238
272
  ##
239
273
  ## A button with two lines on it was asked for with the break written as the two characters, the
@@ -624,7 +658,10 @@ func set_property(params: Dictionary) -> Dictionary:
624
658
  "%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
625
659
  }
626
660
  given = typed["value"]
661
+ # The write alone, setter included, which is where a property's cost is.
662
+ var began: int = Time.get_ticks_usec()
627
663
  Paths.write(holder, named, given)
664
+ var elapsed: int = Time.get_ticks_usec() - began
628
665
  var now: Variant = Paths.read_under(holder, named)
629
666
  # Read back rather than trusted. The engine drops a write it will not take without a word, and
630
667
  # the answer then showed the old value as the new one, shaped as a success: a typed container
@@ -658,7 +695,8 @@ func set_property(params: Dictionary) -> Dictionary:
658
695
  "path": node_path,
659
696
  "property": property,
660
697
  "old_value": _values.serialize(old_value),
661
- "new_value": _values.serialize(now)
698
+ "new_value": _values.serialize(now),
699
+ "elapsed_usec": elapsed
662
700
  }
663
701
 
664
702
 
@@ -695,12 +733,15 @@ func call_method(params: Dictionary) -> Dictionary:
695
733
  if Paths.method_of(spelled).is_empty():
696
734
  spelled += "()"
697
735
  if args.is_empty() and Paths.can_read(reached["holder"], spelled):
736
+ var read_from: int = Time.get_ticks_usec()
698
737
  var answered: Variant = Paths.read_under(reached["holder"], spelled)
738
+ var read_for: int = Time.get_ticks_usec() - read_from
699
739
  return {
700
740
  "type": "method_result",
701
741
  "path": node_path,
702
742
  "method": method,
703
743
  "result": _values.serialize(answered),
744
+ "elapsed_usec": read_for,
704
745
  }
705
746
  var why: String = (
706
747
  "%s:%s takes no arguments" % [reached["called"], spelled]
@@ -772,9 +813,19 @@ func call_method(params: Dictionary) -> Dictionary:
772
813
  given = typed["value"]
773
814
  deserialized_args.append(given)
774
815
 
816
+ # Timed around the call alone, since the bridge's round trip is a second or more and swamps what
817
+ # one click's work costs; the frame metrics read whichever frame is last, which is not this one.
818
+ var began: int = Time.get_ticks_usec()
775
819
  var result: Variant = holder.callv(named, deserialized_args)
820
+ var elapsed: int = Time.get_ticks_usec() - began
776
821
 
777
- return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
822
+ return {
823
+ "type": "method_result",
824
+ "path": node_path,
825
+ "method": method,
826
+ "result": _values.serialize(result),
827
+ "elapsed_usec": elapsed
828
+ }
778
829
 
779
830
 
780
831
  ## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
package/build/index.js CHANGED
@@ -30566,7 +30566,7 @@ var TOOL_SPECS = [
30566
30566
  },
30567
30567
  {
30568
30568
  name: "editor_rescan",
30569
- 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
30569
+ description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
30570
30570
  parameters: {
30571
30571
  projectPath: PROJECT_PATH,
30572
30572
  timeoutMs: {
@@ -30672,7 +30672,7 @@ var TOOL_SPECS = [
30672
30672
  says: {
30673
30673
  type: "string",
30674
30674
  ops: ["find"],
30675
- description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
30675
+ description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
30676
30676
  },
30677
30677
  limit: {
30678
30678
  type: "integer",
@@ -30722,7 +30722,7 @@ var TOOL_SPECS = [
30722
30722
  },
30723
30723
  {
30724
30724
  name: "runtime_invoke",
30725
- description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
30725
+ description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
30726
30726
  parameters: {
30727
30727
  projectPath: RUNNING_PROJECT_PATH,
30728
30728
  pid: RUNNING_PID,
@@ -34539,6 +34539,17 @@ class GodotServer {
34539
34539
  const nearest = descendantsIn(tree, root).filter((pid) => isEngine(pid) && !ancestorsIn(tree, pid, root).some((one) => one !== root && isEngine(one)));
34540
34540
  return nearest.length === 1 ? nearest[0] : undefined;
34541
34541
  }
34542
+ async editorIsScanningOrImporting() {
34543
+ if (!this.godotBridge.isConnected()) {
34544
+ return false;
34545
+ }
34546
+ try {
34547
+ const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
34548
+ return status["scanning"] === true || status["importing"] === true;
34549
+ } catch {
34550
+ return false;
34551
+ }
34552
+ }
34542
34553
  async handleRescanFilesystem(args) {
34543
34554
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
34544
34555
  const started = Date.now();
@@ -34548,9 +34559,26 @@ class GodotServer {
34548
34559
  const blind = await this.classesTheEditorCannotSee(args);
34549
34560
  const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
34550
34561
  const ghosts = blind.stillHeld ?? [];
34551
- const first = await this.handleViaBridge("rescan_filesystem", args);
34552
- if (first.isError) {
34553
- return first;
34562
+ let waitedForEditorMs = 0;
34563
+ let first;
34564
+ for (;; ) {
34565
+ const busySince = Date.now();
34566
+ let wasBusy = false;
34567
+ while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
34568
+ wasBusy = true;
34569
+ await new Promise((settle) => setTimeout(settle, 100));
34570
+ }
34571
+ if (wasBusy) {
34572
+ waitedForEditorMs += Date.now() - busySince;
34573
+ }
34574
+ first = await this.handleViaBridge("rescan_filesystem", args);
34575
+ if (first.isError) {
34576
+ return first;
34577
+ }
34578
+ const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
34579
+ if (!declined || Date.now() - started >= timeoutMs) {
34580
+ break;
34581
+ }
34554
34582
  }
34555
34583
  let busy = true;
34556
34584
  while (busy && Date.now() - started < timeoutMs) {
@@ -34609,6 +34637,7 @@ class GodotServer {
34609
34637
  ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
34610
34638
  stillWorking: busy,
34611
34639
  waitedMs: Date.now() - started,
34640
+ waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
34612
34641
  heldBeforeReload: reloaded.heldBefore,
34613
34642
  reloadedMethods: reloaded.methods,
34614
34643
  reloadProblem: reloaded.problem,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.32",
3
+ "version": "1.1.1",
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",