gdharness 1.2.4 → 1.2.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/cli.js CHANGED
@@ -8129,7 +8129,7 @@ function alive(pid) {
8129
8129
  var init_alive = () => {};
8130
8130
 
8131
8131
  // src/scratch.ts
8132
- import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
8132
+ import { existsSync as existsSync6, mkdtempSync, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
8133
8133
  import { tmpdir } from "node:os";
8134
8134
  import { join as join6 } from "node:path";
8135
8135
  import process6 from "node:process";
@@ -8139,7 +8139,7 @@ function scratchDirectory(kind) {
8139
8139
  function sweepAbandonedScratch(directory = tmpdir()) {
8140
8140
  let entries;
8141
8141
  try {
8142
- entries = readdirSync2(directory);
8142
+ entries = readdirSync3(directory);
8143
8143
  } catch {
8144
8144
  return;
8145
8145
  }
@@ -9461,7 +9461,7 @@ var init_gdscript_source = __esm(() => {
9461
9461
  });
9462
9462
 
9463
9463
  // src/class-cache.ts
9464
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
9464
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
9465
9465
  import { join as join9, posix } from "node:path";
9466
9466
  function isToolScript(source) {
9467
9467
  return TOOL.test(codeOf(source));
@@ -9481,7 +9481,7 @@ function eachScript(projectPath, each) {
9481
9481
  if (existsSync9(join9(directory, ".gdignore"))) {
9482
9482
  return;
9483
9483
  }
9484
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
9484
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
9485
9485
  if (entry.name.startsWith(".")) {
9486
9486
  continue;
9487
9487
  }
@@ -17266,7 +17266,7 @@ import {
17266
17266
  cpSync,
17267
17267
  existsSync as existsSync10,
17268
17268
  mkdirSync as mkdirSync4,
17269
- readdirSync as readdirSync4,
17269
+ readdirSync as readdirSync5,
17270
17270
  readFileSync as readFileSync9,
17271
17271
  rmdirSync,
17272
17272
  rmSync as rmSync5,
@@ -17284,7 +17284,7 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
17284
17284
  function addonsDigest(names, from) {
17285
17285
  const hash = createHash3("sha256");
17286
17286
  const walk = (directory, relative) => {
17287
- for (const name of readdirSync4(directory).sort()) {
17287
+ for (const name of readdirSync5(directory).sort()) {
17288
17288
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
17289
17289
  continue;
17290
17290
  }
@@ -17386,7 +17386,7 @@ function removeAddons(projectPath) {
17386
17386
  }
17387
17387
  }
17388
17388
  const addons = join10(projectPath, "addons");
17389
- if (removed.length > 0 && existsSync10(addons) && readdirSync4(addons).length === 0) {
17389
+ if (removed.length > 0 && existsSync10(addons) && readdirSync5(addons).length === 0) {
17390
17390
  rmdirSync(addons);
17391
17391
  removed.push(addons);
17392
17392
  }
@@ -18898,6 +18898,12 @@ var init_tool_definitions = __esm(() => {
18898
18898
  ops: ["until"],
18899
18899
  description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
18900
18900
  },
18901
+ compare: {
18902
+ type: "string",
18903
+ enum: ["==", "!=", ">=", "<=", ">", "<"],
18904
+ ops: ["until"],
18905
+ description: "until, with a property: how what the property reads stands to value before the wait is met. Default ==. != waits for it to read as anything else. >=, <=, > and < put a number in order, met on the first look past the value or level with it at the precision the property keeps, which is how a counter that steps rather than lands is waited on: a day counter on a fast clock went from 31 to 34 between two looks, and a wait for 32 ran out. For these the value is the number it is rather than fitted to the property, so a fraction on a whole-number property is a threshold rather than refused, and a property holding anything but a number is refused."
18906
+ },
18901
18907
  says: {
18902
18908
  type: "string",
18903
18909
  ops: ["until"],
@@ -33891,7 +33897,7 @@ var init_stdio2 = __esm(() => {
33891
33897
  });
33892
33898
 
33893
33899
  // src/runtime-client.ts
33894
- import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync6, unlinkSync } from "node:fs";
33900
+ import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync6, unlinkSync } from "node:fs";
33895
33901
  import { createConnection } from "node:net";
33896
33902
  import { tmpdir as tmpdir3 } from "node:os";
33897
33903
  import { dirname as dirname8, join as join13, resolve as resolve4 } from "node:path";
@@ -34061,7 +34067,7 @@ function announcedIn(directory) {
34061
34067
  const found = [];
34062
34068
  const candidates = [];
34063
34069
  const entries = [];
34064
- for (const entry of readdirSync6(directory)) {
34070
+ for (const entry of readdirSync7(directory)) {
34065
34071
  const report = ERROR_REPORT_PATTERN.exec(entry);
34066
34072
  if (report) {
34067
34073
  sweepErrorReport(join13(directory, entry), Number.parseInt(report[1] ?? "", 10));
@@ -40548,7 +40554,7 @@ var init_process_time = __esm(() => {
40548
40554
  });
40549
40555
 
40550
40556
  // src/project-scan.ts
40551
- import { existsSync as existsSync17, readdirSync as readdirSync7, readFileSync as readFileSync18 } from "node:fs";
40557
+ import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "node:fs";
40552
40558
  import { join as join21 } from "node:path";
40553
40559
  function skipped(name) {
40554
40560
  return name.startsWith(".") || SKIPPED.has(name);
@@ -40562,7 +40568,7 @@ function projectStructure(projectPath) {
40562
40568
  if (steppedOver(directory)) {
40563
40569
  return;
40564
40570
  }
40565
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
40571
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
40566
40572
  if (skipped(entry.name)) {
40567
40573
  continue;
40568
40574
  }
@@ -40591,7 +40597,7 @@ function scriptsWithoutUid(projectPath) {
40591
40597
  if (steppedOver(directory)) {
40592
40598
  return;
40593
40599
  }
40594
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
40600
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
40595
40601
  if (skipped(entry.name)) {
40596
40602
  continue;
40597
40603
  }
@@ -40621,7 +40627,7 @@ function projectTexts(projectPath) {
40621
40627
  if (steppedOver(directory)) {
40622
40628
  return;
40623
40629
  }
40624
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
40630
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
40625
40631
  if (skipped(entry.name)) {
40626
40632
  continue;
40627
40633
  }
@@ -40648,7 +40654,7 @@ function ignoredTexts(projectPath) {
40648
40654
  const found = [];
40649
40655
  const visit = (directory, prefix, ignored) => {
40650
40656
  const under = ignored || steppedOver(directory);
40651
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
40657
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
40652
40658
  if (skipped(entry.name)) {
40653
40659
  continue;
40654
40660
  }
@@ -40685,7 +40691,7 @@ function searchProject(projectPath, options) {
40685
40691
  if (steppedOver(directory)) {
40686
40692
  return;
40687
40693
  }
40688
- for (const entry of readdirSync7(directory, { withFileTypes: true })) {
40694
+ for (const entry of readdirSync8(directory, { withFileTypes: true })) {
40689
40695
  if (full()) {
40690
40696
  return;
40691
40697
  }
@@ -41544,7 +41550,7 @@ var init_rename = __esm(() => {
41544
41550
  });
41545
41551
 
41546
41552
  // src/rename-symbol.ts
41547
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync8, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
41553
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync9, realpathSync as realpathSync5, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
41548
41554
  import { basename as basename5, dirname as dirname16, join as join22, relative as relative4, resolve as resolve6 } from "node:path";
41549
41555
  function resourcePathOf(projectPath, absolute) {
41550
41556
  const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
@@ -41992,7 +41998,7 @@ function writeLeftAloneDetail(projectPath, symbol, detail) {
41992
41998
  const path = join22(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
41993
41999
  writeFileSync14(path, `${JSON.stringify(detail, null, 2)}
41994
42000
  `, "utf8");
41995
- const kept = readdirSync8(directory).filter((name) => name.endsWith(".json")).sort().reverse();
42001
+ const kept = readdirSync9(directory).filter((name) => name.endsWith(".json")).sort().reverse();
41996
42002
  for (const old of kept.slice(10)) {
41997
42003
  rmSync10(join22(directory, old), { force: true });
41998
42004
  }
@@ -42081,7 +42087,7 @@ import { createHash as createHash4 } from "node:crypto";
42081
42087
  import {
42082
42088
  mkdirSync as mkdirSync14,
42083
42089
  openSync as openSync2,
42084
- readdirSync as readdirSync9,
42090
+ readdirSync as readdirSync10,
42085
42091
  readFileSync as readFileSync21,
42086
42092
  realpathSync as realpathSync6,
42087
42093
  rmSync as rmSync12,
@@ -42207,7 +42213,7 @@ function everyRunRecord() {
42207
42213
  for (const directory of runtimeDirectories()) {
42208
42214
  let entries;
42209
42215
  try {
42210
- entries = readdirSync9(join24(directory, "runs"));
42216
+ entries = readdirSync10(join24(directory, "runs"));
42211
42217
  } catch {
42212
42218
  continue;
42213
42219
  }
@@ -42448,7 +42454,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
42448
42454
  ]));
42449
42455
  let entries;
42450
42456
  try {
42451
- entries = readdirSync9(directory);
42457
+ entries = readdirSync10(directory);
42452
42458
  } catch {
42453
42459
  return;
42454
42460
  }
@@ -42734,7 +42740,7 @@ import {
42734
42740
  fstatSync,
42735
42741
  mkdirSync as mkdirSync15,
42736
42742
  openSync as openSync3,
42737
- readdirSync as readdirSync10,
42743
+ readdirSync as readdirSync11,
42738
42744
  readFileSync as readFileSync23,
42739
42745
  readSync as readSync2,
42740
42746
  rmdirSync as rmdirSync2,
@@ -43618,7 +43624,7 @@ function elsewhereIn(projectPath, asked) {
43618
43624
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
43619
43625
  let near = [];
43620
43626
  try {
43621
- near = readdirSync10(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
43627
+ near = readdirSync11(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
43622
43628
  } catch {
43623
43629
  return "";
43624
43630
  }
@@ -43712,7 +43718,7 @@ function scriptsUnder(directory, prefix) {
43712
43718
  return [];
43713
43719
  }
43714
43720
  const found = [];
43715
- for (const entry of readdirSync10(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
43721
+ for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
43716
43722
  if (entry.name.startsWith(".")) {
43717
43723
  continue;
43718
43724
  }
@@ -45387,7 +45393,7 @@ class GodotServer {
45387
45393
  let report = null;
45388
45394
  let reportProblem = null;
45389
45395
  try {
45390
- const written = existsSync20(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
45396
+ const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
45391
45397
  const newest = written.at(-1);
45392
45398
  if (newest !== undefined) {
45393
45399
  report = parseJUnit(readFileSync23(join26(reportsDir, newest, "results.xml"), "utf8"));
@@ -48698,6 +48704,7 @@ ${staleNote}`}`
48698
48704
  path: nodePath,
48699
48705
  property: readString(args, "property") ?? "",
48700
48706
  value: args["value"],
48707
+ ...readString(args, "compare") === undefined ? {} : { compare: readString(args, "compare") },
48701
48708
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
48702
48709
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
48703
48710
  timeout_ms: timeoutMs
@@ -48815,7 +48822,7 @@ import { existsSync as existsSync21 } from "node:fs";
48815
48822
  import { join as join27, resolve as resolve8 } from "node:path";
48816
48823
 
48817
48824
  // src/harnesses.ts
48818
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
48825
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
48819
48826
  import { homedir as homedir2 } from "node:os";
48820
48827
  import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve2 } from "node:path";
48821
48828
  import process4 from "node:process";
@@ -49562,6 +49569,30 @@ function mainCheckoutOf(projectPath) {
49562
49569
  const main = join4(dirname3(common), relative2(top, resolve2(projectPath)));
49563
49570
  return isSameDirectory(main, projectPath) || !existsSync4(join4(main, "project.godot")) ? null : main;
49564
49571
  }
49572
+ function worktreeCheckoutsOf(projectPath) {
49573
+ let top = resolve2(projectPath);
49574
+ while (!existsSync4(join4(top, ".git"))) {
49575
+ const up = dirname3(top);
49576
+ if (up === top) {
49577
+ return [];
49578
+ }
49579
+ top = up;
49580
+ }
49581
+ const listed = join4(top, ".git", "worktrees");
49582
+ if (!statSync2(join4(top, ".git")).isDirectory() || !existsSync4(listed)) {
49583
+ return [];
49584
+ }
49585
+ const below = relative2(top, resolve2(projectPath));
49586
+ return readdirSync2(listed).flatMap((name) => {
49587
+ const pointer = join4(listed, name, "gitdir");
49588
+ if (!existsSync4(pointer)) {
49589
+ return [];
49590
+ }
49591
+ const checkout = dirname3(resolve2(listed, name, readFileSync2(pointer, "utf8").trim()));
49592
+ const project = join4(checkout, below);
49593
+ return existsSync4(join4(project, "project.godot")) ? [project] : [];
49594
+ });
49595
+ }
49565
49596
  function entryEnvironment(harness, projectPath) {
49566
49597
  if (harness.toml || harness.yaml || harness.snippet) {
49567
49598
  return;
@@ -49753,7 +49784,7 @@ init_setup();
49753
49784
  init_setup();
49754
49785
  init_tool_definitions();
49755
49786
  init_tool_reference();
49756
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
49787
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
49757
49788
  import { basename as basename4, dirname as dirname6, join as join11 } from "node:path";
49758
49789
  var SKILL_NAME = "gdharness";
49759
49790
  var SHARED_SKILLS = join11(".agents", "skills");
@@ -50000,7 +50031,7 @@ function pruneEmpty(directory, projectPath) {
50000
50031
  const shared = join11(projectPath, ".agents");
50001
50032
  let path = directory;
50002
50033
  while (path !== projectPath && (basename4(path) === "skills" || path === shared)) {
50003
- if (readdirSync5(path).length > 0) {
50034
+ if (readdirSync6(path).length > 0) {
50004
50035
  return;
50005
50036
  }
50006
50037
  rmSync6(path, { recursive: true, force: true });
@@ -50353,6 +50384,17 @@ On ${version}. What is left for you:`);
50353
50384
  ` + " editor_launch restart tool, which closes and reopens the window.");
50354
50385
  const serving = await serverServing(projectPath);
50355
50386
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
50387
+ const trailing = worktreeCheckoutsOf(projectPath).flatMap((path) => {
50388
+ const addon = inspectProject(path).addons.find((one) => one.installed);
50389
+ return addon === undefined || addon.version === version ? [] : [{ path, version: addon.version }];
50390
+ });
50391
+ if (trailing.length > 0) {
50392
+ console.log(`
50393
+ Git worktrees of this project keep untracked addons of their own, which this upgrade does not reach:`);
50394
+ for (const worktree of trailing) {
50395
+ console.log(` ${worktree.path}: ${worktree.version ?? "an unmarked copy"}. Run gdharness upgrade ${worktree.path}`);
50396
+ }
50397
+ }
50356
50398
  }
50357
50399
  function doctorReport(projectPath) {
50358
50400
  const report = inspectProject(projectPath);
@@ -21,6 +21,11 @@ const MOST_FRAMES: int = 600
21
21
  ## gives looking about a tenth of the time.
22
22
  const LOOKS_APART: int = 9
23
23
 
24
+ ## How a property wait compares what it reads with the value: equality by default, and order for a
25
+ ## number that steps rather than lands, as a day counter on a fast clock goes from 31 to 34 between
26
+ ## two looks and a wait for 32 never sees it.
27
+ const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
28
+
24
29
  var _host: Node
25
30
  var _values: Values
26
31
 
@@ -107,8 +112,10 @@ func wait_until(params: Dictionary) -> Dictionary:
107
112
  # path, a property compares one value on one node. Taking says and dropping the other left a
108
113
  # caller watching a screen while believing they were watching a property, and the answer says
109
114
  # which of them it is about only if you already know that says wins.
110
- if not says.is_empty() and (not property.is_empty() or params.has("value")):
111
- var other: String = property if not property.is_empty() else "value"
115
+ if not says.is_empty() and (not property.is_empty() or params.has("value") or params.has("compare")):
116
+ var other: String = (
117
+ property if not property.is_empty() else ("value" if params.has("value") else "compare")
118
+ )
112
119
  return {
113
120
  "type": "error",
114
121
  "message":
@@ -123,10 +130,10 @@ func wait_until(params: Dictionary) -> Dictionary:
123
130
  if not says.is_empty():
124
131
  var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
125
132
  return await _wait_until_said(node_path, says, timeout_ms, include_hidden)
126
- if property.is_empty():
127
- return {"type": "error", "message": "A property and a value, or says, are required"}
128
- if not params.has("value"):
129
- return {"type": "error", "message": "A value to wait for is required"}
133
+ var unasked: String = _property_wait_unasked(property, params)
134
+ if not unasked.is_empty():
135
+ return {"type": "error", "message": unasked}
136
+ var compare: String = str(params.get("compare", "=="))
130
137
 
131
138
  var node: Node = standing["node"]
132
139
 
@@ -139,6 +146,8 @@ func wait_until(params: Dictionary) -> Dictionary:
139
146
  return watched
140
147
 
141
148
  var current: Variant = watched["value"]
149
+ if compare in [">=", "<=", ">", "<"]:
150
+ return await _wait_in_order(node, params, current, timeout_ms)
142
151
  var wanted: Variant = _values.fitted(params["value"], typeof(current))
143
152
  # Read against what the property is declared as, not only against what it holds this frame: an
144
153
  # object property holding null holds nothing typed, so any value was taken as it came and compared
@@ -173,9 +182,10 @@ func wait_until(params: Dictionary) -> Dictionary:
173
182
  # At the precision the property keeps, as set compares: a float arrives as 64 bits and most
174
183
  # engine properties keep 32, so h_offset waited on as -1.1 read -1.10000002384186 every frame and
175
184
  # the wait ran out on a value the game had reached.
185
+ var differs: bool = compare == "!="
176
186
  while (
177
187
  Values.comparable(current, wanted)
178
- and not Changes.held_already(wanted, current)
188
+ and Changes.held_already(wanted, current) == differs
179
189
  and Time.get_ticks_msec() - started < timeout_ms
180
190
  ):
181
191
  await _host.get_tree().process_frame
@@ -191,13 +201,111 @@ func wait_until(params: Dictionary) -> Dictionary:
191
201
  "type": "condition",
192
202
  "path": node_path,
193
203
  "property": property,
194
- "met": Changes.held_already(wanted, current),
204
+ "compare": compare,
205
+ "met": Values.comparable(current, wanted) and Changes.held_already(wanted, current) != differs,
195
206
  "value": _values.serialize(current),
196
207
  "elapsed_ms": Time.get_ticks_msec() - started,
197
208
  "frames": frames,
198
209
  }
199
210
 
200
211
 
212
+ ## What a property wait is missing or was given wrongly, or "" when it can go ahead.
213
+ static func _property_wait_unasked(property: String, params: Dictionary) -> String:
214
+ if property.is_empty():
215
+ return "A property and a value, or says, are required"
216
+ if not params.has("value"):
217
+ return "A value to wait for is required"
218
+ var compare: String = str(params.get("compare", "=="))
219
+ if not COMPARES.has(compare):
220
+ return 'compare is one of %s, not "%s"' % [", ".join(COMPARES), compare]
221
+ return ""
222
+
223
+
224
+ ## [method wait_until] for a number compared by order: met on the first look at which the property
225
+ ## is past the value, or level with it at the precision the property keeps, as `==` compares.
226
+ ##
227
+ ## The value is read as the number it is rather than fitted to the property's type, since fitting
228
+ ## truncates: 31.5 on a whole-number property became 31, and a wait for "at least 31.5" was met at 31.
229
+ func _wait_in_order(node: Node, params: Dictionary, current: Variant, timeout_ms: int) -> Dictionary:
230
+ var node_path: String = str(params.get("path", ""))
231
+ var property: String = str(params.get("property", ""))
232
+ var compare: String = str(params.get("compare", ""))
233
+ var given: Variant = params["value"]
234
+ # The property first: a string waited on in order is refused for being a string, whatever value
235
+ # came with it, since no number would make the wait answerable.
236
+ var unordered: String = _no_order(current, node_path, property, compare)
237
+ if not unordered.is_empty():
238
+ return {"type": "error", "message": unordered}
239
+ var number: Variant = given
240
+ if given is String:
241
+ var text: String = given
242
+ number = Read.json_or_null(text)
243
+ if typeof(number) != TYPE_INT and typeof(number) != TYPE_FLOAT:
244
+ return {
245
+ "type": "error",
246
+ "message":
247
+ (
248
+ "compare %s puts a number in order, and the value to wait for is %s: give a number."
249
+ % [compare, str(given)]
250
+ )
251
+ }
252
+ var limit: float = Read.as_float(number)
253
+
254
+ var started: int = Time.get_ticks_msec()
255
+ var frames: int = 0
256
+ while not _in_order(compare, limit, current) and Time.get_ticks_msec() - started < timeout_ms:
257
+ await _host.get_tree().process_frame
258
+ frames += 1
259
+ if not is_instance_valid(node):
260
+ return {"type": "error", "message": "%s was freed while waiting" % node_path}
261
+ var watched: Dictionary = _watched(node, node_path, property)
262
+ if watched.has("message"):
263
+ return watched
264
+ current = watched["value"]
265
+ unordered = _no_order(current, node_path, property, compare)
266
+ if not unordered.is_empty():
267
+ return {"type": "error", "message": unordered}
268
+
269
+ return {
270
+ "type": "condition",
271
+ "path": node_path,
272
+ "property": property,
273
+ "compare": compare,
274
+ "met": _in_order(compare, limit, current),
275
+ "value": _values.serialize(current),
276
+ "elapsed_ms": Time.get_ticks_msec() - started,
277
+ "frames": frames,
278
+ }
279
+
280
+
281
+ ## Why [param current] cannot be put in order, or "" when it is a number.
282
+ static func _no_order(current: Variant, node_path: String, property: String, compare: String) -> String:
283
+ if typeof(current) == TYPE_INT or typeof(current) == TYPE_FLOAT:
284
+ return ""
285
+ return (
286
+ "%s.%s holds %s, which has no order, so compare %s cannot be asked of it: use == or !=."
287
+ % [node_path, property, type_string(typeof(current)), compare]
288
+ )
289
+
290
+
291
+ ## Whether [param current], a number, stands to [param limit] as [param compare] says. Level counts
292
+ ## at the precision the property keeps, so a 32-bit float holding 0.30000001 is at least 0.3.
293
+ static func _in_order(compare: String, limit: float, current: Variant) -> bool:
294
+ var reading: float = Read.as_float(current)
295
+ var level: bool = (
296
+ reading == limit or (typeof(current) == TYPE_FLOAT and Changes.held_already(limit, current))
297
+ )
298
+ match compare:
299
+ ">=":
300
+ return level or reading > limit
301
+ "<=":
302
+ return level or reading < limit
303
+ ">":
304
+ return not level and reading > limit
305
+ _:
306
+ return not level and reading < limit
307
+
308
+
201
309
  ## Why [param given] would have to be cut to be waited on a whole number or a truth, or "" when not.
202
310
  ##
203
311
  ## A number arrives from JSON as a float, so one is converted to what the property holds, and the
package/build/index.js CHANGED
@@ -35025,6 +35025,12 @@ var TOOL_SPECS = [
35025
35025
  ops: ["until"],
35026
35026
  description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
35027
35027
  },
35028
+ compare: {
35029
+ type: "string",
35030
+ enum: ["==", "!=", ">=", "<=", ">", "<"],
35031
+ ops: ["until"],
35032
+ description: "until, with a property: how what the property reads stands to value before the wait is met. Default ==. != waits for it to read as anything else. >=, <=, > and < put a number in order, met on the first look past the value or level with it at the precision the property keeps, which is how a counter that steps rather than lands is waited on: a day counter on a fast clock went from 31 to 34 between two looks, and a wait for 32 ran out. For these the value is the number it is rather than fitted to the property, so a fraction on a whole-number property is a threshold rather than refused, and a property holding anything but a number is refused."
35033
+ },
35028
35034
  says: {
35029
35035
  type: "string",
35030
35036
  ops: ["until"],
@@ -41195,6 +41201,7 @@ ${staleNote}`}`
41195
41201
  path: nodePath,
41196
41202
  property: readString(args, "property") ?? "",
41197
41203
  value: args["value"],
41204
+ ...readString(args, "compare") === undefined ? {} : { compare: readString(args, "compare") },
41198
41205
  ...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
41199
41206
  ...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
41200
41207
  timeout_ms: timeoutMs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.4",
3
+ "version": "1.2.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",