things-api 0.19.0 → 0.19.2

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 (154) hide show
  1. package/README.md +3 -1
  2. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  3. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  5. package/dist/audit/schema.d.ts +19 -2
  6. package/dist/audit/schema.js.map +1 -1
  7. package/dist/capability.d.ts +166 -4
  8. package/dist/capability.js +303 -13
  9. package/dist/capability.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +13 -1
  11. package/dist/cli/commands/doctor.js +52 -22
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/helpers.js +14 -3
  14. package/dist/cli/commands/helpers.js.map +1 -1
  15. package/dist/cli/commands/mcp.js +8 -0
  16. package/dist/cli/commands/mcp.js.map +1 -1
  17. package/dist/cli/commands/repeat-flags.js +34 -2
  18. package/dist/cli/commands/repeat-flags.js.map +1 -1
  19. package/dist/cli/commands/setup.js +16 -3
  20. package/dist/cli/commands/setup.js.map +1 -1
  21. package/dist/cli/commands/ui-state.d.ts +11 -0
  22. package/dist/cli/commands/ui-state.js +27 -0
  23. package/dist/cli/commands/ui-state.js.map +1 -0
  24. package/dist/cli/commands/writes.js +102 -67
  25. package/dist/cli/commands/writes.js.map +1 -1
  26. package/dist/cli/help.js +2 -0
  27. package/dist/cli/help.js.map +1 -1
  28. package/dist/cli/interrupt.d.ts +54 -6
  29. package/dist/cli/interrupt.js +78 -12
  30. package/dist/cli/interrupt.js.map +1 -1
  31. package/dist/cli/main.js +10 -5
  32. package/dist/cli/main.js.map +1 -1
  33. package/dist/client.d.ts +1 -1
  34. package/dist/contracts.d.ts +4 -4
  35. package/dist/contracts.js +1 -1
  36. package/dist/contracts.js.map +1 -1
  37. package/dist/deputy/install.d.ts +15 -1
  38. package/dist/deputy/install.js +46 -6
  39. package/dist/deputy/install.js.map +1 -1
  40. package/dist/deputy/osa.d.ts +42 -2
  41. package/dist/deputy/osa.js +79 -5
  42. package/dist/deputy/osa.js.map +1 -1
  43. package/dist/deputy/routing.d.ts +34 -0
  44. package/dist/deputy/routing.js +95 -0
  45. package/dist/deputy/routing.js.map +1 -1
  46. package/dist/deputy/wake.d.ts +61 -0
  47. package/dist/deputy/wake.js +149 -0
  48. package/dist/deputy/wake.js.map +1 -0
  49. package/dist/diagnose.d.ts +6 -4
  50. package/dist/diagnose.js +20 -4
  51. package/dist/diagnose.js.map +1 -1
  52. package/dist/direct-setup.d.ts +31 -5
  53. package/dist/direct-setup.js +200 -26
  54. package/dist/direct-setup.js.map +1 -1
  55. package/dist/host-access.d.ts +12 -0
  56. package/dist/host-access.js +12 -0
  57. package/dist/host-access.js.map +1 -1
  58. package/dist/index.d.ts +5 -3
  59. package/dist/index.js +3 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp/server.js +23 -9
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/op-result.js +8 -1
  64. package/dist/op-result.js.map +1 -1
  65. package/dist/session-grant.js +16 -1
  66. package/dist/session-grant.js.map +1 -1
  67. package/dist/surface-copy.d.ts +11 -3
  68. package/dist/surface-copy.js +11 -3
  69. package/dist/surface-copy.js.map +1 -1
  70. package/dist/ui-state.d.ts +46 -0
  71. package/dist/ui-state.js +82 -0
  72. package/dist/ui-state.js.map +1 -0
  73. package/dist/wizard.js +8 -1
  74. package/dist/wizard.js.map +1 -1
  75. package/dist/write/accessibility-probe.js +4 -1
  76. package/dist/write/accessibility-probe.js.map +1 -1
  77. package/dist/write/automation-probe.js +7 -1
  78. package/dist/write/automation-probe.js.map +1 -1
  79. package/dist/write/availability.d.ts +0 -15
  80. package/dist/write/availability.js +7 -62
  81. package/dist/write/availability.js.map +1 -1
  82. package/dist/write/batch.js +17 -6
  83. package/dist/write/batch.js.map +1 -1
  84. package/dist/write/commands.d.ts +10 -6
  85. package/dist/write/commands.js +120 -31
  86. package/dist/write/commands.js.map +1 -1
  87. package/dist/write/failure-hints.d.ts +36 -7
  88. package/dist/write/failure-hints.js +33 -14
  89. package/dist/write/failure-hints.js.map +1 -1
  90. package/dist/write/field-limits.d.ts +100 -0
  91. package/dist/write/field-limits.js +144 -0
  92. package/dist/write/field-limits.js.map +1 -0
  93. package/dist/write/guards.js +3 -2
  94. package/dist/write/guards.js.map +1 -1
  95. package/dist/write/move.js +9 -12
  96. package/dist/write/move.js.map +1 -1
  97. package/dist/write/operations.d.ts +15 -8
  98. package/dist/write/operations.js +9 -0
  99. package/dist/write/operations.js.map +1 -1
  100. package/dist/write/opid.d.ts +67 -23
  101. package/dist/write/opid.js +141 -19
  102. package/dist/write/opid.js.map +1 -1
  103. package/dist/write/param-schema.d.ts +17 -44
  104. package/dist/write/param-schema.js +74 -7
  105. package/dist/write/param-schema.js.map +1 -1
  106. package/dist/write/pipeline.d.ts +38 -7
  107. package/dist/write/pipeline.js +134 -18
  108. package/dist/write/pipeline.js.map +1 -1
  109. package/dist/write/pre-state.d.ts +30 -16
  110. package/dist/write/pre-state.js +88 -20
  111. package/dist/write/pre-state.js.map +1 -1
  112. package/dist/write/promote-clone.d.ts +26 -0
  113. package/dist/write/promote-clone.js +333 -41
  114. package/dist/write/promote-clone.js.map +1 -1
  115. package/dist/write/repeat-collateral.d.ts +31 -0
  116. package/dist/write/repeat-collateral.js +153 -0
  117. package/dist/write/repeat-collateral.js.map +1 -0
  118. package/dist/write/resolution-timestamps.js +25 -2
  119. package/dist/write/resolution-timestamps.js.map +1 -1
  120. package/dist/write/template-mutation.js +163 -16
  121. package/dist/write/template-mutation.js.map +1 -1
  122. package/dist/write/vectors/applescript.js +3 -10
  123. package/dist/write/vectors/applescript.js.map +1 -1
  124. package/dist/write/vectors/simulator.js +8 -2
  125. package/dist/write/vectors/simulator.js.map +1 -1
  126. package/dist/write/vectors/types.d.ts +236 -4
  127. package/dist/write/vectors/ui-certification.js +63 -13
  128. package/dist/write/vectors/ui-certification.js.map +1 -1
  129. package/dist/write/vectors/ui-chord.d.ts +144 -0
  130. package/dist/write/vectors/ui-chord.js +428 -0
  131. package/dist/write/vectors/ui-chord.js.map +1 -0
  132. package/dist/write/vectors/ui-drag.d.ts +8 -4
  133. package/dist/write/vectors/ui-drag.js +2 -0
  134. package/dist/write/vectors/ui-drag.js.map +1 -1
  135. package/dist/write/vectors/ui-recipes.d.ts +51 -3
  136. package/dist/write/vectors/ui-recipes.js +452 -44
  137. package/dist/write/vectors/ui-recipes.js.map +1 -1
  138. package/dist/write/vectors/ui-state.d.ts +157 -0
  139. package/dist/write/vectors/ui-state.js +237 -0
  140. package/dist/write/vectors/ui-state.js.map +1 -0
  141. package/dist/write/vectors/ui.d.ts +386 -13
  142. package/dist/write/vectors/ui.js +1543 -124
  143. package/dist/write/vectors/ui.js.map +1 -1
  144. package/dist/write/vectors/url-scheme.js +3 -0
  145. package/dist/write/vectors/url-scheme.js.map +1 -1
  146. package/dist/write/verify/delta.d.ts +41 -0
  147. package/dist/write/verify/delta.js +36 -2
  148. package/dist/write/verify/delta.js.map +1 -1
  149. package/dist/write/verify/poller.d.ts +4 -1
  150. package/dist/write/verify/poller.js +14 -0
  151. package/dist/write/verify/poller.js.map +1 -1
  152. package/package.json +1 -1
  153. package/schema/envelope.schema.json +2 -1
  154. package/skills/things-cli/SKILL.md +2 -2
@@ -1,6 +1,7 @@
1
1
  import { type ThingsApiConfig } from "../../config.ts";
2
2
  import { type UiDriveAux } from "./ui-drag.ts";
3
- import type { UiPrimitive, UiStep, WriteVector } from "./types.ts";
3
+ import { type UiSheetKind, type UiState } from "./ui-state.ts";
4
+ import type { UiClearOutcome, UiPrimitive, UiStep, WriteVector } from "./types.ts";
4
5
  /**
5
6
  * Command-level primitives. Extends the recipe `UiPrimitive` set with the
6
7
  * INTERNAL sub-steps composite recipe steps decompose into: a `click-element`
@@ -10,7 +11,16 @@ import type { UiPrimitive, UiStep, WriteVector } from "./types.ts";
10
11
  * every subprocess call behind the injectable `run` seam makes the
11
12
  * orchestration unit-testable without a GUI.
12
13
  */
13
- export type UiCommandPrimitive = UiPrimitive | "resolve-frame" | "click-point" | "sidebar-snapshot" | "sidebar-scroll" | "sidebar-drag" | "sidebar-held-drag";
14
+ export type UiCommandPrimitive = UiPrimitive | "resolve-frame" | "click-point" | "sidebar-snapshot" | "sidebar-scroll" | "sidebar-drag" | "sidebar-held-drag"
15
+ /** One modifier-bearing key event pair posted straight at the Things process (ui-chord.ts). */
16
+ | "chord-post"
17
+ /**
18
+ * The cleanup ladder's first rung: press the open dialog's own Cancel button
19
+ * (issue #620). Its own primitive rather than a `press` so a recipe's
20
+ * actuations and the cleanup's are never confused — in a trace, in a test, or
21
+ * in the completed-steps trail.
22
+ */
23
+ | "dismiss-dialog";
14
24
  /** A single primitive dispatch — one stable shape per primitive. */
15
25
  export interface UiCommand {
16
26
  primitive: UiCommandPrimitive;
@@ -36,23 +46,220 @@ export interface UiRunResult {
36
46
  * (CLAUDE.md safety rails — the production app is never a valid target).
37
47
  */
38
48
  export type UiRunner = (command: UiCommand, timeoutMs: number) => Promise<UiRunResult>;
49
+ /**
50
+ * The IN-SCRIPT half of the per-step focus guard (issue #620).
51
+ *
52
+ * A synthetic keystroke is not addressed at an element — System Events hands it
53
+ * to whatever application owns the screen at that instant. So every script that
54
+ * types re-asserts, in the same osascript hop that will do the typing, that
55
+ * Things is still frontmost; the drive-level census (see {@link guardedRun})
56
+ * runs a moment earlier and cannot close the last few milliseconds. The
57
+ * assertion FAILS CLOSED and names the application that owns the screen
58
+ * instead — never the contents of its window.
59
+ *
60
+ * This is the cheapest possible check: one System Events property read, no
61
+ * sleeps, no polling (UI-automation determinism doctrine).
62
+ */
63
+ export declare const AX_FOCUS_GUARD_HANDLERS = "on fgFrontApp()\n\tset frontName to \"\"\n\ttry\n\t\ttell application \"System Events\" to set frontName to (name of first application process whose frontmost is true) as text\n\tend try\n\treturn frontName\nend fgFrontApp\n\non fgAssertFront(what)\n\tset f to my fgFrontApp()\n\tif f is \"Things3\" then return true\n\tif f is \"\" then\n\t\terror \"refused to \" & what & \": the frontmost application could not be read, so there is no proof the keystrokes would reach Things \u2014 nothing was typed\"\n\tend if\n\terror \"refused to \" & what & \": \" & f & \" is frontmost, not Things \u2014 a keystroke goes to whatever owns the screen, so nothing was typed\"\nend fgAssertFront";
39
64
  /** resolve-element: does the element exist right now? Returns "true"/"false". */
40
65
  export declare function axResolveScript(path: string): string;
41
66
  /** press: AXPress the element. */
42
67
  export declare function axPressScript(path: string): string;
43
68
  /**
44
69
  * set-field-value: enter a value into the dialog's numeric text field (interval,
45
- * ends-count, start-days-earlier). It FOCUSES the field, selects all, TYPES the
46
- * value, and Tabs to commit — because `set value of <field>` writes the field's
47
- * displayed text WITHOUT firing the edit, so the app's binding keeps the old
48
- * number (the field shows "5" but the rule stays interval 1 — a silent no-op
49
- * exactly like `set value` on a pop-up, UIC6; it went unnoticed while every base
50
- * case used the default interval 1). Real keystrokes fire the change the binding
51
- * needs; Tab (not Return, which would fire the default OK button) commits and
52
- * moves focus. Foreground-bound (keystrokes reach the frontmost app) the
53
- * reveal/activate preamble puts Things there. One stable command shape.
70
+ * ends-count, start-days-earlier) and into the Move… picker's filter field. It
71
+ * FOCUSES the field, TYPES the value, and Tabs to commit — because
72
+ * `set value of <field>` writes the field's displayed text WITHOUT firing the
73
+ * edit, so the app's binding keeps the old number (the field shows "5" but the
74
+ * rule stays interval 1 — a silent no-op exactly like `set value` on a pop-up,
75
+ * UIC6; it went unnoticed while every base case used the default interval 1).
76
+ * Real keystrokes fire the change the binding needs; Tab (not Return, which would
77
+ * fire the default OK button) commits and moves focus. Foreground-bound
78
+ * (keystrokes reach the frontmost app) — the reveal/activate preamble puts Things
79
+ * there. One stable command shape.
80
+ *
81
+ * NO SELECT-ALL KEYSTROKE (BEEP1, 2026-08-25, docs/lab/beep1-numeric-field-beep.md).
82
+ * The primitive used to send ⌘A before typing, and that ONE keystroke was the
83
+ * audible macOS alert beep every numeric-field drive fired on the live host:
84
+ * Things' `Edit ▸ Select All` menu item exists and is DISABLED while the Repeat
85
+ * sheet is up, AppKit dispatches ⌘A as a menu key equivalent FIRST, the disabled
86
+ * item swallows it, nothing handles it → NSBeep. It is a menu-dispatch fact, not
87
+ * a focus race: the beep survives a verified first responder and a 1.5 s settle,
88
+ * while Tab and the digits themselves are silent. ⌘A was also REDUNDANT —
89
+ * `set focused of tf to true` installs the field editor with the ENTIRE content
90
+ * selected (measured: `AXSelectedTextRange` length goes 0 → the full value
91
+ * length, on both a 1- and a 2-character value), so typing replaces the old value
92
+ * outright, including the shrinking case (12 → 3) that a stale caret would have
93
+ * corrupted into "123". Dropping the keystroke is therefore silent AND correct on
94
+ * all three fields.
95
+ *
96
+ * CLOSED-LOOP (determinism doctrine): type, Tab-commit, then READ THE FIELD BACK
97
+ * and retry if it did not hold — the interval field, when it is the first numeric
98
+ * field after a frequency/type switch, races the dialog's group re-layout and
99
+ * reverts to 1 (UIC7, oddities §8l). Re-focus + re-type after a settle lands it
100
+ * once the re-layout has finished, and the re-focus re-selects the whole value,
101
+ * so a retry starts from a clean field without ⌘A. Fail-closed (an `error`, i.e.
102
+ * a transport failure the pipeline re-verifies) if it never holds — the
103
+ * create/reschedule delta's rule assertion is the final DB-level authority.
104
+ *
105
+ * READ-BACK FIRST (issue #620 item 7): a field that ALREADY holds the requested
106
+ * value is left alone and the script returns {@link OK_ALREADY} — the whole
107
+ * keystroke class disappears for the defaults, which is most drives (the field
108
+ * incident died typing interval `1` into a field already showing `1`). The skip
109
+ * is proven by TWO reads a settle apart, because the one way a matching value
110
+ * can go stale is the UIC7 re-layout revert, which lands within that window; and
111
+ * whatever this decides, the pre-commit audit ({@link axAuditDialogScript})
112
+ * re-reads every control through its own address before the OK press, so a
113
+ * wrongly-skipped field cannot commit.
54
114
  */
55
115
  export declare function axSetValueScript(path: string, value: string, attempts?: number): string;
116
+ /**
117
+ * What a typing primitive returns when it typed NOTHING because the field
118
+ * already held the requested value (issue #620 item 7). Distinct from "OK" so
119
+ * the drive can disclose the skip — and so a lab cell can assert that no
120
+ * keystroke hop fired.
121
+ */
122
+ export declare const OK_ALREADY = "OK-ALREADY";
123
+ /**
124
+ * set-group-number: drive ONE of the Repeat dialog's two numeric fields —
125
+ * the cadence INTERVAL or the ENDS-AFTER COUNT — addressed by the LABEL ROW it
126
+ * sits on (HXPC1, docs/lab/hxpc1-picker-assert.md §A; hardened by CGRD1,
127
+ * docs/lab/cgrd1-precommit-audit.md §A).
128
+ *
129
+ * Both fields used to be spelled `text field 1 of group 1`, which is the same
130
+ * control at different moments. Measured on Things 3.23 (build 32300036):
131
+ *
132
+ * Ends: never → group text fields = 1 · tf1 = interval @[311,283]
133
+ * Ends: after N → group text fields = 2 · tf1 = COUNT @[402,372]
134
+ * tf2 = interval @[311,283]
135
+ *
136
+ * i.e. selecting the "after" bound INSERTS the count ahead of the interval. The
137
+ * create path got away with it by driving the interval while it was still the
138
+ * sole field, but a RESCHEDULE opens the dialog pre-populated: a rule that
139
+ * already ends after N presents both fields from the first step, so the interval
140
+ * drive wrote the requested interval into the count field, the count drive then
141
+ * overwrote it, and the interval silently never changed.
142
+ *
143
+ * The addresses are the {@link AX_CADENCE_HANDLERS} laws — POSITIVE label-row
144
+ * matches wherever the app offers a label to match, a uniqueness check where it
145
+ * does not, and a fail-closed refusal naming the whole numeric-field inventory
146
+ * otherwise. The write itself is the {@link axSetValueScript} closed loop —
147
+ * focus, type, Tab-commit, read back, bounded retries — and, like it, sends NO
148
+ * select-all keystroke: the ⌘A that used to open it is the macOS alert beep
149
+ * (BEEP1, docs/lab/beep1-numeric-field-beep.md), and focusing the field already
150
+ * selects its whole content.
151
+ *
152
+ * The read-back this loop performs is SELF-REFERENTIAL by construction — it
153
+ * re-reads the field it addressed, so it can only prove the keystrokes landed
154
+ * where they were aimed, never that they were aimed at the right control. The
155
+ * PRE-COMMIT AUDIT ({@link axAuditDialogScript}) is what closes that: it re-reads
156
+ * every control through these same handlers just before the OK press.
157
+ */
158
+ export declare function axSetGroupNumberScript(groupPath: string, target: "interval" | "ends-count", value: string, attempts?: number, rowTolerance?: number): string;
159
+ /**
160
+ * set-row-field: drive a Repeat-dialog text field addressed by the pinned English
161
+ * LABEL sharing its row — the same discrimination law as
162
+ * {@link axSetGroupNumberScript}, applied to a field that lives on the dialog
163
+ * SHELL rather than in the cadence group.
164
+ *
165
+ * Its one caller is the "and start [N] days earlier" offset the "Add deadlines"
166
+ * checkbox reveals, which shipped as `text field 1` of the shell. That address
167
+ * was the HXPC1 error class exactly: a value-bearing text field picked by index
168
+ * out of a STATE-DEPENDENT tree (the field does not exist at all until the
169
+ * checkbox is ticked), verified only by re-reading the same index it wrote. It
170
+ * happened to be right on 3.23 — measured, the shell carries 0 direct text fields
171
+ * with deadlines off and exactly 1 with them on, whether or not reminders are also
172
+ * on (CGRD1 §B census) — but nothing in the address said so, and an AX tree is an
173
+ * undocumented private surface that may add a second field in any release.
174
+ *
175
+ * The label anchor is `days earlier` (y=413 against the field's y=409, CGRD1 §B).
176
+ * A missing label, or anything other than exactly one field on its row, FAILS
177
+ * CLOSED naming the shell's whole text-field inventory. The write is the
178
+ * {@link axSetValueScript} closed loop.
179
+ */
180
+ export declare function axSetRowFieldScript(containerPath: string, rowLabel: string, value: string, attempts?: number, rowTolerance?: number): string;
181
+ /**
182
+ * ONE control the PRE-COMMIT DIALOG AUDIT re-reads, in the form the generator
183
+ * needs: a resolved element path (the shell / dialog-shape disjunctions are
184
+ * settled by the driver before the script is built) plus the value(s) the drive
185
+ * intended for it.
186
+ */
187
+ export interface AuditScriptControl {
188
+ /** Human name of the control, as the mismatch report should say it. */
189
+ label: string;
190
+ kind: "popup" | "checkbox" | "group-number" | "row-field" | "weekdays" | "occurrence-popup";
191
+ /** popup / checkbox / occurrence-popup: the resolved element path. */
192
+ path?: string;
193
+ /** group-number: which of the cadence group's numeric fields. */
194
+ numberTarget?: "interval" | "ends-count";
195
+ /** row-field: the pinned English label sharing the field's row. */
196
+ rowLabel?: string;
197
+ /** weekdays: the group pop-up index of the first weekday row (shape-selected). */
198
+ weekdayBase?: number;
199
+ /** The accepted observed values — ANY one satisfies (the singular/plural pair). */
200
+ expected: string[];
201
+ /** How the intended value should READ in the report ("checked", not "1"). */
202
+ expectedLabel?: string;
203
+ }
204
+ /** The resolved audit the {@link axAuditDialogScript} generator compiles. */
205
+ export interface AuditScriptSpec {
206
+ /** The resolved dialog shell (attached sheet or detached editor window). */
207
+ shell: string;
208
+ /** The resolved cadence group inside that shell. */
209
+ group: string;
210
+ controls: AuditScriptControl[];
211
+ }
212
+ /**
213
+ * audit-dialog: RE-READ EVERY CONTROL THIS DRIVE SET, through each control's own
214
+ * discriminated address, and refuse to commit if any one of them does not hold
215
+ * the value the drive intended.
216
+ *
217
+ * This exists because a per-step read-back is SELF-REFERENTIAL. Every setter here
218
+ * confirms its write by re-reading the element it addressed, so it proves the
219
+ * keystrokes landed where they were aimed — and nothing else. The #589 wrong-field
220
+ * write reported OK for exactly that reason: the interval drive typed into the
221
+ * ends-count field, then read the ends-count field back and found its own number
222
+ * sitting there. The address was wrong; a read-back through the same address
223
+ * cannot see that.
224
+ *
225
+ * The audit is the outside view. It is assembled from the recipe's OWN step list
226
+ * (so no control the recipe drives can be left out of the audit by omission) and
227
+ * runs as the last step before the OK press, comparing the dialog's complete
228
+ * intended state against what the dialog actually shows: frequency, the
229
+ * after-completion cadence unit, interval, ends bound and its count, the
230
+ * deadline/reminder checkboxes, the start-days-earlier offset, the weekday set,
231
+ * the monthly/yearly anchor pop-ups and the 3.23 first-occurrence pop-up. A
232
+ * mismatch is an `error` naming EVERY differing control with both values, which
233
+ * aborts the drive fail-closed BEFORE the commit and runs the standard clean-abort
234
+ * path — nothing reaches the database.
235
+ *
236
+ * Deterministic throughout: the cadence group is settled on its own shape
237
+ * signature (the BEEP1 two-agreeing-reads gate), never on a sleep, and every field
238
+ * is found by its label row rather than by index ({@link AX_CADENCE_HANDLERS}).
239
+ *
240
+ * The dialog's three `AXDateTimeArea` controls are audited separately — their
241
+ * values are NSDates no System Events read can reach, so they ride
242
+ * {@link axAuditDateAreasScript} through the same ObjC bridge that writes them.
243
+ */
244
+ export declare function axAuditDialogScript(spec: AuditScriptSpec, rowTolerance?: number): string;
245
+ /** ONE date/time area the pre-commit audit re-reads through the ObjC bridge. */
246
+ export interface AuditDateArea {
247
+ /** Human name of the control, as the mismatch report should say it. */
248
+ label: string;
249
+ target: "next" | "ends" | "reminder";
250
+ /** The spec the drive wrote: `date:YYYY-MM-DD` or `time:HH:mm`. */
251
+ spec: string;
252
+ }
253
+ /**
254
+ * The pre-commit audit's DATE-AREA leg. The dialog's first-occurrence, ends-on and
255
+ * reminder controls are `AXDateTimeArea`s whose value is an NSDate — unreachable
256
+ * from System Events — so they are re-read through the SAME ObjC bridge, the same
257
+ * shell-scoped walk and the SAME deterministic `pick` discriminator that
258
+ * {@link axSetDateTimeScript} writes them through. A control the audit cannot find,
259
+ * or one holding a different date/time, throws naming every area the dialog does
260
+ * present (y-position + time-of-day), so the drive aborts before the OK press.
261
+ */
262
+ export declare function axAuditDateAreasScript(areas: AuditDateArea[]): string;
56
263
  /**
57
264
  * ensure-checkbox: converge a dialog checkbox to a target state through a
58
265
  * DETERMINISTIC CLOSED LOOP (RRD1, determinism doctrine) — never a blind toggle.
@@ -140,6 +347,32 @@ export declare function axProbeDialogShapeScript(groupPath: string, rowTolerance
140
347
  * (the fail-closed read-back the ANCH2/YANCH1 date drives established).
141
348
  */
142
349
  export declare function axSelectNextOccurrenceScript(popupPath: string, isoDate: string, maxLevels?: number): string;
350
+ /**
351
+ * settle-occurrences: let the 3.23 `Next:` pop-up ABSORB the rule change the
352
+ * preceding steps made, before the drive touches the dialog again (NEXTPOP1).
353
+ *
354
+ * MEASURED (golden-v4 / Things 3.23, `research-nextpop1.sh` DIAG3/DIAG4): the
355
+ * dialog recomputes the first-occurrence pop-up — its displayed value AND the
356
+ * menu of occurrences behind it — ASYNCHRONOUSLY. After the yearly anchor was
357
+ * moved from Aug 6 to Aug 20 the control flipped at **t+0.4s** with nothing else
358
+ * driven; when the very next step (the "Add deadlines" checkbox) was pressed
359
+ * inside that window instead, the control NEVER caught up — it still read
360
+ * `Thu, Aug 6, 2026`, and its menu still enumerated the Aug-6 series, six
361
+ * seconds later. A cancelled recompute does not retry.
362
+ *
363
+ * That is what made every deadlined monthly/yearly promote fail closed on 3.23:
364
+ * the anchor drive is followed immediately by the deadline controls, so by the
365
+ * time `select-next-occurrence` opened the menu it was the SEED's series, and
366
+ * the requested date — the rule's own first due date — was genuinely not in it
367
+ * (VMRES1 §4.3, reproduced and explained in NEXTPOP1).
368
+ *
369
+ * The wait is closed-loop in the direction that matters: it exits the moment the
370
+ * control MOVES, which is the case that needs waiting for. When the rule change
371
+ * did not move the first occurrence there is nothing to observe, so the budget
372
+ * bounds it — deliberately over-cautious, since the cost is a fraction of a
373
+ * second and the alternative is a series that starts on the wrong date.
374
+ */
375
+ export declare function axSettleOccurrencesScript(popupPath: string, budgetMs?: number, pollMs?: number): string;
143
376
  /**
144
377
  * converge-weekdays: drive the weekly dialog's weekday ROWS onto an exact target
145
378
  * set through a deterministic closed loop (RDLG2 — the RRD1 fix).
@@ -228,10 +461,103 @@ export declare function axSelectHeadingRowScript(tablePath: string, ordinal: num
228
461
  export declare function axAssertEligibleScript(targetUuid: string, menuItemPath: string): string;
229
462
  /** activate: foreground Things (the fallback preamble step). */
230
463
  export declare function axActivateScript(): string;
231
- /** key: a space-separated keystroke spec (e.g. "down down return"). */
464
+ /**
465
+ * key: a space-separated keystroke spec (e.g. "down down return").
466
+ *
467
+ * Frontmost-guarded in-script (issue #620): `key code`/`keystroke` reach
468
+ * whatever application owns the screen, so the script refuses — naming that
469
+ * application — rather than firing keys into someone else's window.
470
+ */
232
471
  export declare function axKeyScript(keys: string): string;
233
- /** The abort keystroke sent to dismiss a half-open sheet/popover on failure. */
472
+ /**
473
+ * type-text: send literal text to whatever control holds focus (HXPC1). The
474
+ * Move… picker focuses its own filter field the instant it opens, and that field
475
+ * is NOT addressable as a direct child of the picker window — so there is no
476
+ * element to hand `set-value`, whose select-all + Tab commit would be wrong for a
477
+ * search field regardless (a popover filter has no next key view for Tab to move
478
+ * to). Unlike {@link axKeyScript}, which splits its spec on whitespace and would
479
+ * drop the spaces out of a multi-word project title, this sends the string as
480
+ * ONE keystroke. It is deliberately not self-verifying: the `click-picker-row`
481
+ * step that follows resolves the destination row by name and fails closed when
482
+ * the filter did not produce it, so a keystroke that landed elsewhere can never
483
+ * be committed. One stable command shape.
484
+ */
485
+ export declare function axTypeTextScript(text: string): string;
486
+ /**
487
+ * resolve-frame for a control nested inside a CONTENT-TABLE ROW: walk the
488
+ * table's rows → cells → cell children and return the frame of the one whose
489
+ * `AXDescription` equals `description` (HXPC1, docs/lab/hxpc1-picker-assert.md
490
+ * §B0). Same "x y w h" contract as {@link axFrameScript}.
491
+ *
492
+ * This exists because the heading row's `…` button — the only content-row
493
+ * control that carries its own title (`"More. <heading title>"`, the HEADXPROJ
494
+ * enabler) — sits at `UI element N of cell 1 of row M of the table`, and
495
+ * `first UI element of <table> whose description is …` searches the table's
496
+ * DIRECT children only. Those are the rows, which carry no description, so the
497
+ * shipped one-level spelling matched nothing and the ellipsis drives
498
+ * (`project.move-heading-to-project`, `project.dissolve-heading`) died at their
499
+ * own frame resolution before any click — measured on Things 3.23 against a
500
+ * heading whose button the raw Accessibility API resolves at the same instant.
501
+ * The row/cell indices are never guessed: every row is walked and the match is
502
+ * exact, so a heading whose title changed under us fails closed by name.
503
+ */
504
+ export declare function axRowCellFrameScript(tablePath: string, description: string): string;
505
+ /**
506
+ * resolve-frame for the Move… picker ROW carrying an exact project title — the
507
+ * step that replaced the recipe's blind Return (HXPC1,
508
+ * docs/lab/hxpc1-picker-assert.md §B).
509
+ *
510
+ * The picker exposes no `AXSelected` / `AXFocused` / `AXHighlighted` on any row
511
+ * (measured — only its filter field is focused), so there is nothing to read
512
+ * back from a keyboard commit and no way to assert what Return would take. What
513
+ * it does expose is one `AXUnknown` per row whose `AXDescription` IS the project
514
+ * title, and — whenever the filter holds text — a trailing
515
+ * `New Project "<typed text>"` row that CREATES a project when committed. That
516
+ * row is what the blind Return took whenever the destination was missing from
517
+ * the picker, which an ordinary database-resolved destination reaches: a
518
+ * COMPLETED or CANCELED project appears nowhere in the picker, so the drive
519
+ * minted a second project of the same title and moved the heading into it
520
+ * (measured 3.23: projects 14 → 15, heading re-parented to the new row).
521
+ *
522
+ * So the commit is addressed instead of guessed. The script requires:
523
+ * - the picker to be the window it claims (its `AXIdentifier` begins
524
+ * `MovePopUpDialog-`) — a positive identity check, so a different detached
525
+ * window can never be clicked into;
526
+ * - EXACTLY ONE row whose description equals the destination title (the
527
+ * New-Project row's description is the quoted form, so an exact match cannot
528
+ * hit it);
529
+ * - that row's centre to lie inside the picker's own scroll area — the CNCAC1
530
+ * off-screen hazard, where a row scrolled past the fold still resolves a
531
+ * frame and a click at it lands on the desktop.
532
+ * Any miss FAILS CLOSED naming the destination and listing every row the picker
533
+ * actually offered, so the caller learns what the app was willing to move to.
534
+ */
535
+ export declare function axPickerRowFrameScript(pickerPath: string, title: string): string;
536
+ /**
537
+ * The abort keystroke, sent ONLY from the audited cleanup ladder (issue #620)
538
+ * and only once that ladder has proven Things owns the screen. It is scoped to
539
+ * the Things process for readability, but scoping is not what makes it safe —
540
+ * a synthetic key goes to whatever is frontmost, which is why the script
541
+ * carries the same in-script frontmost assertion every other keystroke does.
542
+ */
234
543
  export declare function axAbortScript(): string;
544
+ /**
545
+ * Dismiss the open dialog by PRESSING ITS OWN CANCEL BUTTON (issue #620).
546
+ *
547
+ * Preferred over Escape wherever it works, for two independent reasons: an
548
+ * AXPress is addressed at an ELEMENT, so it cannot leak into another
549
+ * application the way a keystroke can, and it works while Things is in the
550
+ * BACKGROUND — the cleanup never has to steal the user's focus to undo its own
551
+ * half-finished dialog. The button is addressed by its pinned English title,
552
+ * exactly like every other selector in this vector, and the dialog shell is
553
+ * resolved the same two ways the census resolves it (attached sheet, or the
554
+ * detached editor window Things presents when it is not frontmost).
555
+ *
556
+ * Returns "OK" after pressing, or a diagnostic ("NO-DIALOG" / "NO-CANCEL") the
557
+ * ladder falls through on — it never claims a dismissal; the caller re-reads
558
+ * the census to decide that.
559
+ */
560
+ export declare function axCancelDialogScript(): string;
235
561
  /**
236
562
  * The PROVEN app-level clearance / relocation maneuver (SESSGATE, #480, live-host
237
563
  * recovery): close the front Things window — which takes an attached modal sheet
@@ -257,6 +583,30 @@ export declare function axCloseReopenActivateScript(): string;
257
583
  * command shape.
258
584
  */
259
585
  export declare function axSheetOpenScript(): string;
586
+ /**
587
+ * The outcome of clearing a half-open dialog after a failed drive:
588
+ * - "none" — the census found no dialog open (nothing to clear);
589
+ * - "dismissed" — it is gone, and a fresh census CONFIRMED that;
590
+ * - "cleared-blind" — the session was AX-blind (locked / off-Space), so no
591
+ * census and no keystroke can be trusted; the PROVEN
592
+ * app-level close+reopen ran to clear it (cannot be
593
+ * AX-confirmed, but the maneuver works blind — SESSGATE);
594
+ * - "foreign" — a dialog is open that this drive did not open, so it was
595
+ * LEFT ALONE (a cleanup must never dismiss the dialog the
596
+ * person at the keyboard opened after our failure);
597
+ * - "may-remain" — ours, and nothing in the ladder would close it
598
+ * (fail-closed: report it precisely, with the sync gate).
599
+ */
600
+ export type ClearOutcome = UiClearOutcome;
601
+ export interface ClearResult {
602
+ state: ClearOutcome;
603
+ /** How it was closed, for the trace and the disclosure. */
604
+ how?: "cancel-button" | "escape" | "window-close";
605
+ /** What the census identified as open at cleanup time. */
606
+ sheetKind?: UiSheetKind;
607
+ /** Who owned the screen when the cleanup started, when it was not Things. */
608
+ focusOwner?: string;
609
+ }
260
610
  /**
261
611
  * resolve-frame: read the element's on-screen frame (top-left origin, points)
262
612
  * from the live AX tree and print "x y w h". Used by `click-element` to target
@@ -320,6 +670,29 @@ export declare function parseFrameCenter(stdout: string): {
320
670
  x: number;
321
671
  y: number;
322
672
  } | null;
673
+ /**
674
+ * Read the live window/focus census through the shipped dispatch seam — the
675
+ * transport behind the `ui-state` diagnostic (src/ui-state.ts) and, in tests,
676
+ * behind any injected runner. READ-ONLY: the census clicks nothing, types
677
+ * nothing, and changes no state; see src/write/vectors/ui-state.ts.
678
+ */
679
+ export declare function readLiveUiState(run?: UiRunner): Promise<UiState | null>;
680
+ /**
681
+ * Judge one guard reading. Returns null to proceed, or the refusal sentence —
682
+ * which always names who owns the screen, because that is the one fact the
683
+ * person reading it cannot recover after the fact.
684
+ *
685
+ * Exported for the unit matrix: every branch here is a fail-closed decision
686
+ * about synthetic input, and each one is worth a test.
687
+ */
688
+ export declare function judgeFocusGuard(state: UiState | null, expectedSheet: UiSheetKind | null, label: string): string | null;
689
+ /**
690
+ * The cleanup disclosure (issue #620). Every branch states what is TRUE of the
691
+ * app right now, and — whenever a dialog may still be open — that Things Cloud
692
+ * sync is held until someone dismisses it, which is the consequence a caller
693
+ * cannot see and would otherwise discover hours later on another device.
694
+ */
695
+ export declare function describeCleanup(clear: ClearResult): string;
323
696
  /** Compile one recipe step into its primitive command (no dispatch). */
324
697
  export declare function commandForStep(step: UiStep, targetUuid: string): UiCommand;
325
698
  /**