things-api 0.19.0 → 0.19.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.
Files changed (131) 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 +24 -14
  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/writes.js +102 -67
  22. package/dist/cli/commands/writes.js.map +1 -1
  23. package/dist/cli/interrupt.d.ts +54 -6
  24. package/dist/cli/interrupt.js +78 -12
  25. package/dist/cli/interrupt.js.map +1 -1
  26. package/dist/cli/main.js +8 -5
  27. package/dist/cli/main.js.map +1 -1
  28. package/dist/client.d.ts +1 -1
  29. package/dist/contracts.d.ts +3 -3
  30. package/dist/contracts.js +1 -1
  31. package/dist/deputy/install.d.ts +15 -1
  32. package/dist/deputy/install.js +46 -6
  33. package/dist/deputy/install.js.map +1 -1
  34. package/dist/deputy/routing.d.ts +16 -0
  35. package/dist/deputy/routing.js +70 -0
  36. package/dist/deputy/routing.js.map +1 -1
  37. package/dist/deputy/wake.d.ts +61 -0
  38. package/dist/deputy/wake.js +149 -0
  39. package/dist/deputy/wake.js.map +1 -0
  40. package/dist/diagnose.d.ts +6 -4
  41. package/dist/diagnose.js +20 -4
  42. package/dist/diagnose.js.map +1 -1
  43. package/dist/direct-setup.d.ts +31 -5
  44. package/dist/direct-setup.js +200 -26
  45. package/dist/direct-setup.js.map +1 -1
  46. package/dist/host-access.d.ts +12 -0
  47. package/dist/host-access.js +12 -0
  48. package/dist/host-access.js.map +1 -1
  49. package/dist/index.d.ts +3 -3
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/mcp/server.js +23 -9
  53. package/dist/mcp/server.js.map +1 -1
  54. package/dist/op-result.js +8 -1
  55. package/dist/op-result.js.map +1 -1
  56. package/dist/session-grant.js +16 -1
  57. package/dist/session-grant.js.map +1 -1
  58. package/dist/surface-copy.d.ts +11 -3
  59. package/dist/surface-copy.js +11 -3
  60. package/dist/surface-copy.js.map +1 -1
  61. package/dist/wizard.js +8 -1
  62. package/dist/wizard.js.map +1 -1
  63. package/dist/write/availability.d.ts +0 -15
  64. package/dist/write/availability.js +7 -62
  65. package/dist/write/availability.js.map +1 -1
  66. package/dist/write/batch.js +17 -6
  67. package/dist/write/batch.js.map +1 -1
  68. package/dist/write/commands.d.ts +10 -6
  69. package/dist/write/commands.js +120 -31
  70. package/dist/write/commands.js.map +1 -1
  71. package/dist/write/failure-hints.d.ts +29 -6
  72. package/dist/write/failure-hints.js +9 -14
  73. package/dist/write/failure-hints.js.map +1 -1
  74. package/dist/write/guards.js +3 -2
  75. package/dist/write/guards.js.map +1 -1
  76. package/dist/write/move.js +9 -12
  77. package/dist/write/move.js.map +1 -1
  78. package/dist/write/operations.d.ts +15 -8
  79. package/dist/write/operations.js +9 -0
  80. package/dist/write/operations.js.map +1 -1
  81. package/dist/write/opid.d.ts +67 -23
  82. package/dist/write/opid.js +141 -19
  83. package/dist/write/opid.js.map +1 -1
  84. package/dist/write/param-schema.d.ts +0 -44
  85. package/dist/write/param-schema.js +10 -3
  86. package/dist/write/param-schema.js.map +1 -1
  87. package/dist/write/pipeline.d.ts +38 -7
  88. package/dist/write/pipeline.js +104 -18
  89. package/dist/write/pipeline.js.map +1 -1
  90. package/dist/write/pre-state.d.ts +30 -16
  91. package/dist/write/pre-state.js +88 -20
  92. package/dist/write/pre-state.js.map +1 -1
  93. package/dist/write/promote-clone.js +132 -18
  94. package/dist/write/promote-clone.js.map +1 -1
  95. package/dist/write/repeat-collateral.d.ts +31 -0
  96. package/dist/write/repeat-collateral.js +153 -0
  97. package/dist/write/repeat-collateral.js.map +1 -0
  98. package/dist/write/resolution-timestamps.js +25 -2
  99. package/dist/write/resolution-timestamps.js.map +1 -1
  100. package/dist/write/template-mutation.js +163 -16
  101. package/dist/write/template-mutation.js.map +1 -1
  102. package/dist/write/vectors/applescript.js +3 -10
  103. package/dist/write/vectors/applescript.js.map +1 -1
  104. package/dist/write/vectors/simulator.js +8 -2
  105. package/dist/write/vectors/simulator.js.map +1 -1
  106. package/dist/write/vectors/types.d.ts +211 -1
  107. package/dist/write/vectors/ui-certification.js +63 -13
  108. package/dist/write/vectors/ui-certification.js.map +1 -1
  109. package/dist/write/vectors/ui-chord.d.ts +144 -0
  110. package/dist/write/vectors/ui-chord.js +428 -0
  111. package/dist/write/vectors/ui-chord.js.map +1 -0
  112. package/dist/write/vectors/ui-drag.d.ts +8 -4
  113. package/dist/write/vectors/ui-drag.js +2 -0
  114. package/dist/write/vectors/ui-drag.js.map +1 -1
  115. package/dist/write/vectors/ui-recipes.d.ts +51 -3
  116. package/dist/write/vectors/ui-recipes.js +452 -44
  117. package/dist/write/vectors/ui-recipes.js.map +1 -1
  118. package/dist/write/vectors/ui.d.ts +267 -10
  119. package/dist/write/vectors/ui.js +1006 -64
  120. package/dist/write/vectors/ui.js.map +1 -1
  121. package/dist/write/vectors/url-scheme.js +3 -0
  122. package/dist/write/vectors/url-scheme.js.map +1 -1
  123. package/dist/write/verify/delta.d.ts +41 -0
  124. package/dist/write/verify/delta.js +36 -2
  125. package/dist/write/verify/delta.js.map +1 -1
  126. package/dist/write/verify/poller.d.ts +4 -1
  127. package/dist/write/verify/poller.js +14 -0
  128. package/dist/write/verify/poller.js.map +1 -1
  129. package/package.json +1 -1
  130. package/schema/envelope.schema.json +1 -1
  131. package/skills/things-cli/SKILL.md +1 -1
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
+ <img src="assets/things-api-icon-160x160.png" alt="" width="80" height="80">
2
+
1
3
  # things-api
2
4
 
3
5
  A typed TypeScript library + CLI (`things`) for programmatic interaction with [Things 3](https://culturedcode.com/things/) by Cultured Code.
4
6
 
5
- **Status: read + write + MCP layers live and published to npm (v0.19.0 — see [CHANGELOG.md](CHANGELOG.md)).** Reads go straight to the local SQLite database (UI-exact Today ordering, sidebar-grouped Anytime/Someday with container-status cascade, decoded repeat rules, occurrence projections); writes run a verified pipeline over four write vectors — two lab-validated headless surfaces (the URL scheme + AppleScript) and two opt-in surfaces (Apple Shortcuts and an Accessibility-driven GUI vector) — with hazard guards, disruption-tier policy, a JSONL audit trail, batch mode, audit-replay undo (transactional across compound operations), full project lifecycle (complete/cancel/reopen/restore), heading rename/archive/unarchive with children policies, container detach, granular stateful checklists, tag hierarchy management incl. un-nesting, completion/creation backdating, Logbook imports, tiered fuzzy name resolution (uuid prefixes, `things:///show?id=` share links), and kind-neutral in-place reordering within any container or view (one `things reorder <refs…> [--start|--end|--before|--after] --in <target>` verb — Today/Evening, a project, an area, a heading's children, someday, inbox — plus `project move-heading` for the headings themselves and `area reorder` for the sidebar). The Apple Shortcuts surface is wired for the two headless capabilities nothing else can do: creating a heading in an existing project (`things project add-heading`) and clearing a reminder from a date-scheduled item (`things todo clear-reminder`). Both run through bundled proxy shortcuts (`things setup`) and are gated on their presence. The fourth vector — the Accessibility-driven GUI vector — is off by default and drives the local Things app to reach transforms that exist on no scriptable surface (repeat-rule editing on existing to-dos and projects, convert-to-project, sidebar area reorder); see [Accessibility GUI vector](#accessibility-gui-vector-optional-off-by-default) below. Single-item permanent delete stays interactive-only (its macOS consent has no always-allow) and is out of the headless pipeline. See [docs/design/](docs/design/) for the architecture and VM-lab design, [docs/lab/](docs/lab/harness.md) for the probe harness and campaign results the write layer is grounded in, and [docs/atlas/](docs/atlas/schema-v26.md) for the database↔UI map.
7
+ **Status: read + write + MCP layers live and published to npm (v0.19.1 — see [CHANGELOG.md](CHANGELOG.md)).** Reads go straight to the local SQLite database (UI-exact Today ordering, sidebar-grouped Anytime/Someday with container-status cascade, decoded repeat rules, occurrence projections); writes run a verified pipeline over four write vectors — two lab-validated headless surfaces (the URL scheme + AppleScript) and two opt-in surfaces (Apple Shortcuts and an Accessibility-driven GUI vector) — with hazard guards, disruption-tier policy, a JSONL audit trail, batch mode, audit-replay undo (transactional across compound operations), full project lifecycle (complete/cancel/reopen/restore), heading rename/archive/unarchive with children policies, container detach, granular stateful checklists, tag hierarchy management incl. un-nesting, completion/creation backdating, Logbook imports, tiered fuzzy name resolution (uuid prefixes, `things:///show?id=` share links), and kind-neutral in-place reordering within any container or view (one `things reorder <refs…> [--start|--end|--before|--after] --in <target>` verb — Today/Evening, a project, an area, a heading's children, someday, inbox — plus `project move-heading` for the headings themselves and `area reorder` for the sidebar). The Apple Shortcuts surface is wired for the two headless capabilities nothing else can do: creating a heading in an existing project (`things project add-heading`) and clearing a reminder from a date-scheduled item (`things todo clear-reminder`). Both run through bundled proxy shortcuts (`things setup`) and are gated on their presence. The fourth vector — the Accessibility-driven GUI vector — is off by default and drives the local Things app to reach transforms that exist on no scriptable surface (repeat-rule editing on existing to-dos and projects, convert-to-project, sidebar area reorder); see [Accessibility GUI vector](#accessibility-gui-vector-optional-off-by-default) below. Single-item permanent delete stays interactive-only (its macOS consent has no always-allow) and is out of the headless pipeline. See [docs/design/](docs/design/) for the architecture and VM-lab design, [docs/lab/](docs/lab/harness.md) for the probe harness and campaign results the write layer is grounded in, and [docs/atlas/](docs/atlas/schema-v26.md) for the database↔UI map.
6
8
 
7
9
  ```sh
8
10
  things today --json # read: your Today list, Evening split, UI order
@@ -1,4 +1,4 @@
1
- import type { OccurrenceResolution } from "../write/verify/delta.ts";
1
+ import type { DeltaSpec, OccurrenceResolution } from "../write/verify/delta.ts";
2
2
  export interface AuditRecord {
3
3
  v: 1;
4
4
  ts: string;
@@ -51,6 +51,18 @@ export interface AuditRecord {
51
51
  * call returned without re-reading the database.
52
52
  */
53
53
  occurrence?: OccurrenceResolution;
54
+ /**
55
+ * AMBIGUOUS-OUTCOME reconciliation key (ADDITIVE): the expected-state
56
+ * assertion this attempt was verifying, recorded on `verify-failed:timeout`
57
+ * records only — the one result class where the change may or may not have
58
+ * landed. A resubmission carrying the same `opId` re-evaluates THIS assertion
59
+ * against current state to decide whether the timed-out change is there
60
+ * (replay it as already-applied) or absent (execute normally), so the presence
61
+ * test is the attempt's OWN oracle rather than a per-operation guess
62
+ * (`src/write/opid.ts`). Absent on every other record — and an absent one is a
63
+ * refusal to guess, never an assumption either way.
64
+ */
65
+ expected?: DeltaSpec;
54
66
  /** Normalized requested delta (params as given, post-normalization). */
55
67
  requested: Record<string, unknown>;
56
68
  /** Asserted-field subset of the pre-state (null when target didn't exist). */
@@ -59,7 +71,12 @@ export interface AuditRecord {
59
71
  observed: Record<string, unknown> | null;
60
72
  result: "intent" | "ok" | "verify-failed:timeout" | "verify-failed:mismatch" | "verify-failed:silent-noop"
61
73
  /** A GUI drive stopped because the Things window was unreachable/unresponsive (#512). */
62
- | "verify-failed:ui-unreachable" | `blocked:${string}` | "unsupported";
74
+ | "verify-failed:ui-unreachable"
75
+ /**
76
+ * The requested change landed, but a field the caller never named moved with it
77
+ * and nothing in the operation's vocabulary attributes the movement (CGRD1).
78
+ */
79
+ | "verify-failed:collateral" | `blocked:${string}` | "unsupported";
63
80
  verify: {
64
81
  attempts: number;
65
82
  elapsedMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/audit/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA8EzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,SAAS,CACvB,MAA0E;IAE1E,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IACzD,yEAAyE;IACzE,wEAAwE;IACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,IAAI;QACX,MAAM,CAAC,IAAI,IAAI,EAAE;KAClB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChF,OAAO,KAAK,MAAM,EAAE,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/audit/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA+FzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,SAAS,CACvB,MAA0E;IAE1E,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IACzD,yEAAyE;IACzE,wEAAwE;IACxE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,IAAI;QACX,MAAM,CAAC,IAAI,IAAI,EAAE;KAClB,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChF,OAAO,KAAK,MAAM,EAAE,CAAC;AACvB,CAAC"}
@@ -1,7 +1,8 @@
1
+ import { type TargetWake } from "./deputy/wake.ts";
1
2
  import { type HostAccessDeps, type HostApp } from "./host-access.ts";
2
3
  export { fdaGranted, type FdaVerdict, type HostApp, hostApp, hostDisplayName, tccDbPath, } from "./host-access.ts";
3
4
  /** The Things application's bundle identifier — the Automation grant's target. */
4
- export declare const THINGS_BUNDLE_ID = "com.culturedcode.ThingsMac";
5
+ export { THINGS_BUNDLE_ID } from "./deputy/wake.ts";
5
6
  /** How reads may reach the live library, if at all. */
6
7
  export type ReadCapabilityMode =
7
8
  /** A caller-supplied database path — outside the doctrine entirely (Article VI). */
@@ -26,14 +27,37 @@ export type UiCapabilityMode =
26
27
  | "config-disabled"
27
28
  /** No deputy answers, so there is no identity that could hold the grants. */
28
29
  | "helpers-missing"
30
+ /** System Events is down and would not start — a liveness fault, not a grant one. */
31
+ | "target-unreachable"
29
32
  /** The deputy answers but the `--gui` tier is incomplete. */
30
33
  | "tier-incomplete";
34
+ /**
35
+ * Where Things' OWN in-app authorization for the URL scheme stands (URLEN1).
36
+ *
37
+ * This is not a macOS consent class at all — `open -g things:///…` is a
38
+ * LaunchServices dispatch that needs no grant. The gate is the app's: Settings
39
+ * ▸ General ▸ "Enable Things URLs", recorded as `uriSchemeEnabled` in the
40
+ * group-container preferences plist. Three states, all measured.
41
+ */
42
+ export type UrlSchemeCapabilityMode =
43
+ /** `uriSchemeEnabled = 1` — URL commands execute. */
44
+ "enabled"
45
+ /** `uriSchemeEnabled = 0` — the app drops URL MUTATIONS with no dialog at all. */
46
+ | "disabled"
47
+ /** No key: nobody has answered the app's first-use dialog on this machine. */
48
+ | "never-asked"
49
+ /** The plist cannot be reached prompt-free — never resolved by dispatching. */
50
+ | "unreadable";
31
51
  /** How app automation may be delivered, if at all. */
32
52
  export type WriteCapabilityMode =
33
53
  /** The deputy is onboarded; Apple Events are sent under the helper identity. */
34
54
  "deputy"
55
+ /** Things is not running, so where the deputy's grant stands cannot be read. */
56
+ | "deputy-target-dormant"
35
57
  /** macOS records an Automation grant for the host app against Things. */
36
58
  | "direct-granted"
59
+ /** The lab's documented in-guest escape (see {@link WRITE_DIRECT_ESCAPE_ENV}). */
60
+ | "direct-escape"
37
61
  /** macOS records a REFUSAL. It will not re-ask; the human must re-arm it. */
38
62
  | "direct-denied"
39
63
  /** No record either way, or none readable — never resolved by prompting. */
@@ -53,10 +77,23 @@ export interface Capability<Mode> {
53
77
  export type ReadCapability = Capability<ReadCapabilityMode>;
54
78
  export type WriteCapability = Capability<WriteCapabilityMode>;
55
79
  export type UiCapability = Capability<UiCapabilityMode>;
80
+ export type UrlSchemeCapability = Capability<UrlSchemeCapabilityMode>;
56
81
  /** True when this verdict permits opening the live container. */
57
82
  export declare function readAllowed(capability: ReadCapability): boolean;
58
83
  /** True when this verdict permits driving the Things window. */
59
84
  export declare function uiAllowed(capability: UiCapability): boolean;
85
+ /**
86
+ * True when a URL-scheme MUTATION may be dispatched.
87
+ *
88
+ * `unreadable` is permissive, and deliberately so — the asymmetry with the
89
+ * write gate is the point. There, an unknown standing is refused because
90
+ * resolving it means sending the Apple Event that IS the dialog. Here,
91
+ * dispatching costs nothing on a machine whose answer is already "enabled",
92
+ * which is every settled machine; only the two states we have positively READ
93
+ * as not-enabled are refused. What catches the unreadable case instead is the
94
+ * read-after-write verify plus its likely-cause hint (src/write/failure-hints.ts).
95
+ */
96
+ export declare function urlSchemeAllowed(capability: UrlSchemeCapability): boolean;
60
97
  /** True when app automation may be dispatched. */
61
98
  export declare function writeAllowed(capability: WriteCapability): boolean;
62
99
  /**
@@ -81,6 +118,25 @@ export interface CapabilityDeps extends HostAccessDeps {
81
118
  axTrusted: boolean | undefined;
82
119
  systemEvents: string | undefined;
83
120
  } | null;
121
+ /**
122
+ * Start System Events and re-read its Automation standing, for the one state
123
+ * where the handshake reports liveness instead of authorization
124
+ * (`not-running`). Prompt-free — see ./deputy/wake.ts for why the launch must
125
+ * come before the determination.
126
+ */
127
+ wakeSystemEvents?: () => TargetWake;
128
+ /**
129
+ * Start Things and re-read its Automation standing — the same liveness step
130
+ * for the AppleScript vector's own target (#617). Consulted ONLY on the
131
+ * dispatch path: a survey never launches the user's app.
132
+ */
133
+ wakeThings?: () => TargetWake;
134
+ /**
135
+ * Report a woken target's real standing back to the routing layer, which
136
+ * defers its `auto` onboarding gate while Things is closed
137
+ * (./deputy/routing.ts, {@link settleDeputyAutomation}).
138
+ */
139
+ settleDeputyAutomation?: (standing: string | undefined) => void;
84
140
  /** Is GUI-driving switched on in config (`ui-enabled`)? */
85
141
  uiEnabled?: () => boolean;
86
142
  /**
@@ -90,6 +146,15 @@ export interface CapabilityDeps extends HostAccessDeps {
90
146
  automationAuthValue?: (client: string, target: string) => number | null;
91
147
  /** Resolve a running app's display name from its bundle id (LaunchServices). */
92
148
  lookupAppName?: (bundleId: string) => string | null;
149
+ /**
150
+ * The read standing {@link urlSchemeCapability} consults before it touches
151
+ * the group container. Defaults to a live {@link readCapability} call.
152
+ */
153
+ readStanding?: () => ReadCapability;
154
+ /** Raw bytes of the Things group-container preferences plist. Throws when unreachable. */
155
+ readPrefsPlist?: () => Buffer;
156
+ /** Pull `uriSchemeEnabled` out of those bytes. Throws when the key is absent. */
157
+ extractUriSchemeEnabled?: (plistBytes: Buffer) => string;
93
158
  }
94
159
  /**
95
160
  * Can this process read the live Things library, and on whose authority?
@@ -107,6 +172,49 @@ export declare class ReadCapabilityError extends Error {
107
172
  readonly capability: ReadCapability;
108
173
  constructor(capability: ReadCapability);
109
174
  }
175
+ /**
176
+ * The LAB's escape hatch for the AppleScript vector — the write-side twin of
177
+ * {@link UI_DIRECT_ESCAPE_ENV}, and just as deliberately not consumer surface.
178
+ *
179
+ * A guest shell in a golden clone descends from sshd, not from an application
180
+ * bundle, so `hostApp()` finds no `__CFBundleIdentifier` and macOS has no
181
+ * identity to have recorded an Automation grant against. The verdict is
182
+ * therefore `direct-unknown` in every clone, which blocks every AppleScript-
183
+ * vector verb and every composite carrying an AppleScript leg. What the clone
184
+ * actually has is an in-guest Automation grant on the runner's own processes
185
+ * (the same AXVM1 layer the ui escape leans on), so the block is an artefact of
186
+ * UNKNOWABILITY, not of a missing grant. Setting this to `1` says so.
187
+ *
188
+ * Bounded, and the bound is the point: it is consulted ONLY on the
189
+ * bundle-id-less path. A host that has an identity is answered from its TCC row
190
+ * as it always was, so the escape can never mask a recorded refusal
191
+ * (`direct-denied`) or manufacture a grant for a real user's terminal.
192
+ * Documented in docs/lab/harness.md and exported by the lab's guest
193
+ * environment; nothing consumer-facing mentions it.
194
+ */
195
+ export declare const WRITE_DIRECT_ESCAPE_ENV = "THINGS_API_WRITE_DIRECT";
196
+ /**
197
+ * What a {@link writeCapability} verdict is FOR — the one thing that decides
198
+ * whether a dormant Things may be started while the verdict is taken (#617).
199
+ *
200
+ * The asymmetry with the GUI preflight is deliberate. System Events is a
201
+ * headless macOS component nobody sees, so {@link uiCapability} wakes it for
202
+ * every caller. Things is the user's own app: starting it is visible, so only a
203
+ * caller that is ABOUT TO DRIVE IT may do so.
204
+ */
205
+ export interface WriteCapabilityOptions {
206
+ /**
207
+ * - `survey` (the default) — `doctor`, the MCP startup bake, `--dry-run`.
208
+ * Launches nothing and reports a closed Things as the liveness state
209
+ * `deputy-target-dormant`.
210
+ * - `dispatch` — the write gate and the two setup ceremonies, which are about
211
+ * to send Things an Apple Event anyway. A dormant target is started in the
212
+ * background first and its standing re-read, which is also what keeps the
213
+ * operation at tier 0/1: an Apple Event to a CLOSED Things auto-launches it
214
+ * WITH focus steal (A40/A41), a background pre-launch does not.
215
+ */
216
+ purpose?: "survey" | "dispatch";
217
+ }
110
218
  /**
111
219
  * May this process drive Things over Apple Events, and on whose authority?
112
220
  *
@@ -116,8 +224,21 @@ export declare class ReadCapabilityError extends Error {
116
224
  * last state is deliberately NOT resolved here: resolving it means sending a
117
225
  * real Apple Event, which is what raises the dialog, and Article I reserves
118
226
  * that for `things setup`.
227
+ *
228
+ * LIVENESS BEFORE AUTHORIZATION (#617). The deputy's `not-running` is the
229
+ * ask-false determination having no answer for a CLOSED Things — a fact about
230
+ * the app's process, not about the grant. Two rules follow, and both matter:
231
+ *
232
+ * - while the deputy is standing, that value NEVER falls through to the direct
233
+ * host branch. A silent direct engagement would put consent back on the
234
+ * terminal (the routing doctrine's no-fallback rule), and on a machine with
235
+ * no host record it would refuse a fully onboarded user with "run
236
+ * `things setup`" — the #610 false-onboarding loop, one vector over;
237
+ * - a `dispatch` caller resolves it by STARTING Things (a background
238
+ * LaunchServices dispatch, never an Apple Event) and re-reading the
239
+ * standing. Only what comes back is an authorization fact.
119
240
  */
120
- export declare function writeCapability(deps?: CapabilityDeps): WriteCapability;
241
+ export declare function writeCapability(options?: WriteCapabilityOptions, deps?: CapabilityDeps): WriteCapability;
121
242
  /** Thrown when app automation is refused for want of capability (Article II). */
122
243
  export declare class WriteCapabilityError extends Error {
123
244
  readonly remediation: string[];
@@ -125,7 +246,8 @@ export declare class WriteCapabilityError extends Error {
125
246
  constructor(capability: WriteCapability);
126
247
  }
127
248
  /**
128
- * The LAB's escape hatch, and deliberately not consumer surface. The VM lab and
249
+ * The LAB's escape hatch for the ui vector, and deliberately not consumer
250
+ * surface (its write-side twin is {@link WRITE_DIRECT_ESCAPE_ENV}). The VM lab and
129
251
  * the guest e2e bundle drive the UI vector DIRECT — the in-guest Accessibility
130
252
  * grant is held by the runner's own processes (the AXVM1 layer), there is no
131
253
  * helper bundle in a disposable clone, and there is nobody to answer a dialog
@@ -147,14 +269,54 @@ export declare const UI_DIRECT_ESCAPE_ENV = "THINGS_API_UI_DIRECT";
147
269
  *
148
270
  * Every answer is prompt-free: the config key is a file read, and the deputy's
149
271
  * `hello` carries `AXIsProcessTrusted()` plus its own `AEDeterminePermission`
150
- * verdict for System Events.
272
+ * verdict for System Events. The one state that needs more than a read is a
273
+ * DORMANT System Events, which macOS reaps whenever it has been idle: the
274
+ * target is started in the background — never by sending it an event — and the
275
+ * determination re-read, so `not-running` resolves to the truth instead of
276
+ * masquerading as a missing grant (./deputy/wake.ts).
151
277
  */
152
278
  export declare function uiCapability(deps?: CapabilityDeps): UiCapability;
279
+ /**
280
+ * Has Things been authorized to act on `things:///` commands, and how do we know?
281
+ *
282
+ * Stateless per invocation, like every other verdict here: the setting is the
283
+ * user's to flip at any moment, and a cached "yes" would be a stored onboarding
284
+ * flag by another name. The common case costs one file read plus one `plutil`.
285
+ *
286
+ * WHY THIS IS GATED ON THE READ STANDING. The plist lives inside the Things
287
+ * group container, which is the same `kTCCServiceSystemPolicyAppData` class as
288
+ * the database — so the open is itself what would raise the app-data modal on a
289
+ * machine that holds no standing. This function therefore never opens it
290
+ * speculatively: it asks {@link readCapability} first and reports `unreadable`
291
+ * unless the container is already reachable (helpers, FDA, or a live session
292
+ * grant). Where the helpers are serving, the read rides the reader's own
293
+ * security-scoped bookmark over the container, so the prefs plist is inside the
294
+ * granted subtree and no host grant is involved at all.
295
+ *
296
+ * MEASURED, all three states (URLEN1, golden-v4 / Things 3.23):
297
+ *
298
+ * - `1` — URL mutations execute.
299
+ * - `0` — every mutating verb (`add`, token-bearing `update`, the `json`
300
+ * batch) is dropped in TOTAL SILENCE: zero row delta, no dialog, no window
301
+ * of any kind, and nothing to wait for. Navigation URLs (`things:///show`)
302
+ * still work, so this gate covers mutations only.
303
+ * - absent — nobody has answered the app's own first-use "Things URL Scheme"
304
+ * dialog (Cancel / Enable). The dispatched request PARKS behind that dialog
305
+ * rather than being dropped, which is what #611 saw: with nobody at the
306
+ * machine, the verify window expired and the write reported a silent no-op.
307
+ */
308
+ export declare function urlSchemeCapability(deps?: CapabilityDeps): UrlSchemeCapability;
153
309
  /** Thrown when GUI-driving is refused for want of capability (Article IV). */
154
310
  export declare class UiCapabilityError extends Error {
155
311
  readonly remediation: string[];
156
312
  readonly capability: UiCapability;
157
313
  constructor(capability: UiCapability);
158
314
  }
315
+ /** Thrown when a URL-scheme mutation is refused for want of the app's own authorization. */
316
+ export declare class UrlSchemeCapabilityError extends Error {
317
+ readonly remediation: string[];
318
+ readonly capability: UrlSchemeCapability;
319
+ constructor(capability: UrlSchemeCapability);
320
+ }
159
321
  /** Test seam: forget the one memo this module keeps (the host's display name). */
160
322
  export declare function resetCapabilityForTests(): void;