things-api 0.19.3 → 0.20.0

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 (139) hide show
  1. package/README.md +1 -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 +42 -10
  6. package/dist/audit/schema.js +21 -0
  7. package/dist/audit/schema.js.map +1 -1
  8. package/dist/cli/commands/doctor.js +3 -1
  9. package/dist/cli/commands/doctor.js.map +1 -1
  10. package/dist/cli/commands/op-result.js +25 -7
  11. package/dist/cli/commands/op-result.js.map +1 -1
  12. package/dist/cli/commands/repeat-flags.d.ts +19 -1
  13. package/dist/cli/commands/repeat-flags.js +71 -0
  14. package/dist/cli/commands/repeat-flags.js.map +1 -1
  15. package/dist/cli/commands/rescue.d.ts +12 -0
  16. package/dist/cli/commands/rescue.js +83 -0
  17. package/dist/cli/commands/rescue.js.map +1 -0
  18. package/dist/cli/commands/writes.js +86 -32
  19. package/dist/cli/commands/writes.js.map +1 -1
  20. package/dist/cli/help.js +25 -25
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/main.js +12 -2
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/cli/resolve-invocation.d.ts +9 -1
  25. package/dist/cli/resolve-invocation.js +71 -0
  26. package/dist/cli/resolve-invocation.js.map +1 -1
  27. package/dist/cli/unknown-command.d.ts +26 -0
  28. package/dist/cli/unknown-command.js +135 -0
  29. package/dist/cli/unknown-command.js.map +1 -0
  30. package/dist/contracts.d.ts +4 -3
  31. package/dist/contracts.js +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/server.js +62 -10
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/model/entities.d.ts +11 -0
  38. package/dist/model/entities.js.map +1 -1
  39. package/dist/model/mappers.js +15 -0
  40. package/dist/model/mappers.js.map +1 -1
  41. package/dist/op-result.d.ts +24 -2
  42. package/dist/op-result.js +77 -20
  43. package/dist/op-result.js.map +1 -1
  44. package/dist/process-instance.d.ts +41 -0
  45. package/dist/process-instance.js +82 -0
  46. package/dist/process-instance.js.map +1 -0
  47. package/dist/rescue.d.ts +247 -0
  48. package/dist/rescue.js +811 -0
  49. package/dist/rescue.js.map +1 -0
  50. package/dist/session-grant.js +4 -15
  51. package/dist/session-grant.js.map +1 -1
  52. package/dist/ui-state.d.ts +8 -4
  53. package/dist/ui-state.js +8 -4
  54. package/dist/ui-state.js.map +1 -1
  55. package/dist/write/batch.js +4 -1
  56. package/dist/write/batch.js.map +1 -1
  57. package/dist/write/clone.js +12 -6
  58. package/dist/write/clone.js.map +1 -1
  59. package/dist/write/commands.d.ts +9 -0
  60. package/dist/write/commands.js +135 -50
  61. package/dist/write/commands.js.map +1 -1
  62. package/dist/write/disclosures.d.ts +275 -0
  63. package/dist/write/disclosures.js +277 -0
  64. package/dist/write/disclosures.js.map +1 -0
  65. package/dist/write/experimental.d.ts +16 -0
  66. package/dist/write/experimental.js +19 -0
  67. package/dist/write/experimental.js.map +1 -1
  68. package/dist/write/failure-hints.js +3 -2
  69. package/dist/write/failure-hints.js.map +1 -1
  70. package/dist/write/guards.d.ts +1 -1
  71. package/dist/write/guards.js +102 -3
  72. package/dist/write/guards.js.map +1 -1
  73. package/dist/write/heading.js +4 -7
  74. package/dist/write/heading.js.map +1 -1
  75. package/dist/write/lock.d.ts +85 -1
  76. package/dist/write/lock.js +121 -6
  77. package/dist/write/lock.js.map +1 -1
  78. package/dist/write/make-repeating-project.js +2 -1
  79. package/dist/write/make-repeating-project.js.map +1 -1
  80. package/dist/write/operations.d.ts +52 -6
  81. package/dist/write/operations.js +22 -0
  82. package/dist/write/operations.js.map +1 -1
  83. package/dist/write/opid.d.ts +69 -8
  84. package/dist/write/opid.js +116 -20
  85. package/dist/write/opid.js.map +1 -1
  86. package/dist/write/param-schema.js +14 -2
  87. package/dist/write/param-schema.js.map +1 -1
  88. package/dist/write/pipeline.d.ts +82 -3
  89. package/dist/write/pipeline.js +204 -60
  90. package/dist/write/pipeline.js.map +1 -1
  91. package/dist/write/planner.d.ts +7 -0
  92. package/dist/write/planner.js +12 -0
  93. package/dist/write/planner.js.map +1 -1
  94. package/dist/write/preserve-modified.d.ts +17 -8
  95. package/dist/write/preserve-modified.js.map +1 -1
  96. package/dist/write/promote-clone.js +106 -53
  97. package/dist/write/promote-clone.js.map +1 -1
  98. package/dist/write/reorder.js +45 -39
  99. package/dist/write/reorder.js.map +1 -1
  100. package/dist/write/repeat-rule.d.ts +25 -1
  101. package/dist/write/repeat-rule.js +69 -0
  102. package/dist/write/repeat-rule.js.map +1 -1
  103. package/dist/write/resolution-timestamps.js +104 -78
  104. package/dist/write/resolution-timestamps.js.map +1 -1
  105. package/dist/write/reversibility.js +2 -2
  106. package/dist/write/reversibility.js.map +1 -1
  107. package/dist/write/spawn-expectation.d.ts +135 -0
  108. package/dist/write/spawn-expectation.js +237 -0
  109. package/dist/write/spawn-expectation.js.map +1 -0
  110. package/dist/write/template-mutation.js +34 -35
  111. package/dist/write/template-mutation.js.map +1 -1
  112. package/dist/write/undo.js +45 -5
  113. package/dist/write/undo.js.map +1 -1
  114. package/dist/write/vectors/simulator.js +48 -2
  115. package/dist/write/vectors/simulator.js.map +1 -1
  116. package/dist/write/vectors/types.d.ts +12 -0
  117. package/dist/write/vectors/ui-recipes.js +11 -1
  118. package/dist/write/vectors/ui-recipes.js.map +1 -1
  119. package/dist/write/vectors/ui-state.d.ts +64 -6
  120. package/dist/write/vectors/ui-state.js +91 -4
  121. package/dist/write/vectors/ui-state.js.map +1 -1
  122. package/dist/write/vectors/ui.d.ts +69 -3
  123. package/dist/write/vectors/ui.js +293 -146
  124. package/dist/write/vectors/ui.js.map +1 -1
  125. package/dist/write/vectors/url-scheme.js +26 -0
  126. package/dist/write/vectors/url-scheme.js.map +1 -1
  127. package/dist/write/verify/delta.d.ts +17 -2
  128. package/dist/write/verify/delta.js +46 -16
  129. package/dist/write/verify/delta.js.map +1 -1
  130. package/dist/write/verify/poller.d.ts +3 -2
  131. package/dist/write/verify/poller.js +3 -1
  132. package/dist/write/verify/poller.js.map +1 -1
  133. package/package.json +1 -1
  134. package/schema/envelope.schema.json +1 -2
  135. package/skills/things-cli/SKILL.md +4 -2
  136. package/skills/things-cli/references/repeating.md +1 -1
  137. package/dist/cli/commands/ui-state.d.ts +0 -11
  138. package/dist/cli/commands/ui-state.js +0 -27
  139. package/dist/cli/commands/ui-state.js.map +0 -1
package/dist/op-result.js CHANGED
@@ -14,17 +14,26 @@
14
14
  * caller runs `things op-result <op-id>` in a FRESH short-lived process and reads
15
15
  * the final outcome the killed process already durably wrote (fsync per record).
16
16
  *
17
- * Three outcomes:
17
+ * Four outcomes:
18
18
  * - FOUND — a final (non-intent) record carries the id: report its result code,
19
19
  * target uuid, and post-verify observation (the standard fields the killed
20
20
  * process would have printed).
21
- * - INTENT-ONLYonly an `intent` marker exists: the op is still running, or the
22
- * process died between touching the app and writing its final record the
23
- * outcome is UNCERTAIN (the app-side change may have landed). The newest
21
+ * - IN-FLIGHT — an unsuperseded `intent` marker whose HOLDER IS ALIVE: the op is
22
+ * still running right now. The caller's move is to poll again, not to retry.
23
+ * - ORPHANED an unsuperseded `intent` marker whose holder is GONE: the process
24
+ * died between touching the app and writing its final record, so the outcome is
25
+ * genuinely UNCERTAIN (the app-side change may have landed). The newest
24
26
  * correlated trace file is surfaced when determinable.
25
27
  * - UNKNOWN — no record carries the id: it never ran, the id is mistyped, or the
26
28
  * record has aged out of the local history.
27
29
  *
30
+ * Splitting the last two used to be impossible: an intent recorded no holder, so
31
+ * this could only hedge ("still running, OR died mid-flight") and the caller had
32
+ * to guess which. A keyed intent now carries the writing process's instance
33
+ * identity (#639), which makes it a question with an answer. The rare third
34
+ * shape — an intent with no holder recorded — keeps the old hedged status,
35
+ * because for that record the honest answer really is "cannot tell".
36
+ *
28
37
  * This is a pure history read — it opens no database and drives nothing. Reused by
29
38
  * the CLI subcommand and (read-only) the MCP surface through the single library
30
39
  * entry point (src/index.ts), like {@link diagnose}.
@@ -32,6 +41,7 @@
32
41
  import { readdirSync, statSync } from "node:fs";
33
42
  import { join } from "node:path";
34
43
  import { auditDir as defaultAuditDir, traceDir as defaultTraceDir } from "./paths.js";
44
+ import { instanceAlive } from "./process-instance.js";
35
45
  import { readAuditRecords } from "./write/undo.js";
36
46
  /** The trace file whose mtime is closest to `targetTs` (within a window), else null. */
37
47
  function correlatedTrace(traceDir, targetTs) {
@@ -116,18 +126,26 @@ export function opResult(opId, options = {}) {
116
126
  uuid: null,
117
127
  observed: null,
118
128
  verify: null,
129
+ steps: null,
119
130
  ts: null,
120
131
  durationMs: null,
121
132
  tracePath: null,
133
+ holder: null,
122
134
  note: `no change-history record carries op-id "${opId}" — it never ran, the id is mistyped, or ` +
123
135
  "the record has aged out of the local history (`things doctor` shows the history health)",
124
136
  };
125
137
  }
126
- // A final (non-intent) record supersedes the intent marker. Take the NEWEST one
127
- // (an ambiguously-failed op can be re-dispatched under the same id, yielding
128
- // several finals the last is the current truth).
129
- const finals = matched.filter((r) => r.result !== "intent");
130
- if (finals.length > 0) {
138
+ // An intent is superseded by the final of its OWN attempt, paired by `ts` —
139
+ // both records of one attempt derive from the same `startedAt`, which is the
140
+ // schema's documented sibling invariant. Deliberately NOT "the last record
141
+ // wins": `readAuditRecords` RE-SORTS the trail by `ts`, so file order does not
142
+ // survive the read. `findPendingIntent` (write/opid.ts) applies the identical
143
+ // rule, so the reporting surface and the dispatch gate can never disagree
144
+ // about whether a key is in flight.
145
+ const settled = new Set(matched.filter((r) => r.result !== "intent").map((r) => r.ts));
146
+ const pending = matched.findLast((r) => r.result === "intent" && !settled.has(r.ts));
147
+ if (pending === undefined) {
148
+ const finals = matched.filter((r) => r.result !== "intent");
131
149
  const rec = finals[finals.length - 1];
132
150
  return {
133
151
  opId,
@@ -138,29 +156,68 @@ export function opResult(opId, options = {}) {
138
156
  ...(rec.occurrence !== undefined && { occurrence: rec.occurrence }),
139
157
  observed: rec.observed,
140
158
  verify: rec.verify,
159
+ steps: rec.steps ?? null,
141
160
  ts: rec.ts,
142
161
  durationMs: rec.durationMs,
143
162
  tracePath: null,
163
+ holder: null,
144
164
  note: foundNote(rec.result, rec.uuid),
145
165
  };
146
166
  }
147
- // Only intent marker(s): still running, or the process died mid-flight.
148
- const rec = matched[matched.length - 1];
149
- const tracePath = correlatedTrace(traceDir, rec.ts);
150
- return {
167
+ // An unsuperseded intent. Its holder decides which of the two very different
168
+ // situations this is and the caller's next move differs completely between
169
+ // them, which is why the old single hedged answer was worth splitting.
170
+ const last = pending;
171
+ const tracePath = correlatedTrace(traceDir, last.ts);
172
+ const recorded = last.holder;
173
+ const holder = recorded === undefined ? null : { ...recorded, alive: instanceAlive(recorded) };
174
+ const target = last.uuid ?? "<uuid>";
175
+ const base = {
151
176
  opId,
152
- status: "intent-only",
153
- op: rec.op,
177
+ op: last.op,
154
178
  result: null,
155
- uuid: rec.uuid,
179
+ uuid: last.uuid,
156
180
  observed: null,
157
181
  verify: null,
158
- ts: rec.ts,
182
+ // An intent marker is written BEFORE the app is touched, so it never carries
183
+ // steps — the drive had not run yet. The trace file is the recovery here.
184
+ steps: null,
185
+ ts: last.ts,
159
186
  durationMs: null,
160
187
  tracePath,
161
- note: "the operation was recorded as STARTED but no final outcome was written — it is still " +
162
- "running, or the process died mid-flight (its GUI-side change may have landed). The outcome " +
163
- "is UNCERTAIN: re-read the target and inspect the trace" +
188
+ holder,
189
+ };
190
+ if (holder !== null && holder.alive) {
191
+ return {
192
+ ...base,
193
+ status: "in-flight",
194
+ note: `the operation is STILL RUNNING — it started ${last.ts} and the process that owns it ` +
195
+ `(pid ${holder.pid}) is alive. Nothing to recover yet: run this command again in a few ` +
196
+ "seconds until it reports a final outcome. Do NOT resubmit the write — a GUI-driven " +
197
+ "change takes seconds, and longer on a large or syncing database. (Resubmitting the same " +
198
+ "op-id while it runs is refused rather than executed, so a retry costs you the wait " +
199
+ "either way.)",
200
+ };
201
+ }
202
+ if (holder !== null) {
203
+ return {
204
+ ...base,
205
+ status: "orphaned",
206
+ note: `the operation started ${last.ts} and the process that owned it (pid ${holder.pid}) is ` +
207
+ "GONE without recording an outcome — its app-side change may or may not have landed. " +
208
+ `Re-read the target (\`things show ${target}\`) and inspect the trace` +
209
+ (tracePath !== null ? ` (${tracePath})` : "") +
210
+ " rather than blind-retrying. Resubmitting the SAME command with the SAME op-id is safe: " +
211
+ "it re-reads state to decide whether that change is there, replaying it rather than " +
212
+ "repeating it (and refusing outright when the record cannot settle the question)",
213
+ };
214
+ }
215
+ return {
216
+ ...base,
217
+ status: "intent-only",
218
+ note: "the operation was recorded as STARTED but no final outcome was written, and the record " +
219
+ "names no process, so whether it is still running cannot be determined from here. The " +
220
+ `outcome is UNCERTAIN: re-read the target (\`things show ${target}\`) and inspect the trace` +
164
221
  (tracePath !== null ? ` (${tracePath})` : "") +
165
222
  " rather than blind-retrying",
166
223
  };
@@ -1 +1 @@
1
- {"version":3,"file":"op-result.js","sourceRoot":"","sources":["../src/op-result.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"op-result.js","sourceRoot":"","sources":["../src/op-result.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /** The process that owns an in-flight piece of work, as an identity (not just a pid). */
2
+ export interface ProcessInstance {
3
+ pid: number;
4
+ /**
5
+ * The pid's start time (`ps -o lstart=`), stable for that process's whole
6
+ * life. Null when it could not be read — the pid alone then carries the
7
+ * answer, which is weaker but never fabricated.
8
+ */
9
+ start: string | null;
10
+ }
11
+ /**
12
+ * Does a process with this pid exist right now? (Signal 0 tests existence only.)
13
+ *
14
+ * Non-positive pids are rejected BEFORE the syscall, and that guard is
15
+ * load-bearing rather than defensive tidiness: in `kill(2)`, pid 0 means "every
16
+ * process in the caller's group" and a negative pid means "the group" — both
17
+ * succeed, so a zero or negative holder would report itself alive forever and
18
+ * wedge its idempotency key. A holder is always a real process or it is nothing.
19
+ */
20
+ export declare function pidAlive(pid: number): boolean;
21
+ /** A pid's start time, or null when the pid is gone / `ps` could not answer. */
22
+ export declare function processStart(pid: number): string | null;
23
+ /** This process, as the identity to record on a piece of work it is starting. */
24
+ export declare function currentInstance(): ProcessInstance;
25
+ /** Seams so tests never shell out. */
26
+ export interface InstanceLivenessDeps {
27
+ pidAlive?: (pid: number) => boolean;
28
+ processStart?: (pid: number) => string | null;
29
+ }
30
+ /**
31
+ * Is the RECORDED instance still running?
32
+ *
33
+ * Deliberately CONSERVATIVE: this answers a question whose wrong answer is
34
+ * expensive in one direction only. Reporting a dead writer as alive costs a
35
+ * caller one more poll; reporting a LIVE writer as dead invites a second
36
+ * execution of a mutation that is still in flight. So every ambiguity — a start
37
+ * time that was never recorded, a `ps` that would not answer now — resolves to
38
+ * "alive". Only a pid that is gone, or one whose start time PROVES it is a
39
+ * different process wearing the same number, is reported dead.
40
+ */
41
+ export declare function instanceAlive(instance: ProcessInstance, deps?: InstanceLivenessDeps): boolean;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * PROCESS INSTANCE IDENTITY — a pid paired with its start time, which is what
3
+ * makes "is that process still alive?" answerable across a pid reuse.
4
+ *
5
+ * A bare pid is not an identity: the kernel recycles pids, so a dead writer's
6
+ * number can belong to an unrelated process minutes later and `kill(pid, 0)`
7
+ * would report the writer as alive. Pairing the pid with `ps -o lstart=` — a
8
+ * value the reusing process cannot reproduce, because it is the moment the
9
+ * kernel started IT — turns the pair into an identity that only the original
10
+ * process satisfies.
11
+ *
12
+ * This is the same instance-key shape `session-grant.ts` uses to decide whether
13
+ * the app instance a grant was minted for is still the one running; both callers
14
+ * share this module so the liveness rule is stated once.
15
+ */
16
+ import { execFileSync } from "node:child_process";
17
+ /**
18
+ * Does a process with this pid exist right now? (Signal 0 tests existence only.)
19
+ *
20
+ * Non-positive pids are rejected BEFORE the syscall, and that guard is
21
+ * load-bearing rather than defensive tidiness: in `kill(2)`, pid 0 means "every
22
+ * process in the caller's group" and a negative pid means "the group" — both
23
+ * succeed, so a zero or negative holder would report itself alive forever and
24
+ * wedge its idempotency key. A holder is always a real process or it is nothing.
25
+ */
26
+ export function pidAlive(pid) {
27
+ if (!Number.isInteger(pid) || pid <= 0)
28
+ return false;
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /** A pid's start time, or null when the pid is gone / `ps` could not answer. */
38
+ export function processStart(pid) {
39
+ try {
40
+ const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
41
+ encoding: "utf8",
42
+ timeout: 3000,
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ });
45
+ const trimmed = out.trim();
46
+ return trimmed === "" ? null : trimmed;
47
+ }
48
+ catch {
49
+ // A dead pid exits nonzero — the instance is gone, which is an answer.
50
+ return null;
51
+ }
52
+ }
53
+ /** This process, as the identity to record on a piece of work it is starting. */
54
+ export function currentInstance() {
55
+ return { pid: process.pid, start: processStart(process.pid) };
56
+ }
57
+ /**
58
+ * Is the RECORDED instance still running?
59
+ *
60
+ * Deliberately CONSERVATIVE: this answers a question whose wrong answer is
61
+ * expensive in one direction only. Reporting a dead writer as alive costs a
62
+ * caller one more poll; reporting a LIVE writer as dead invites a second
63
+ * execution of a mutation that is still in flight. So every ambiguity — a start
64
+ * time that was never recorded, a `ps` that would not answer now — resolves to
65
+ * "alive". Only a pid that is gone, or one whose start time PROVES it is a
66
+ * different process wearing the same number, is reported dead.
67
+ */
68
+ export function instanceAlive(instance, deps = {}) {
69
+ const alive = deps.pidAlive ?? pidAlive;
70
+ const start = deps.processStart ?? processStart;
71
+ if (!alive(instance.pid))
72
+ return false;
73
+ // The pid exists. Without both start times we cannot rule out reuse, so we
74
+ // do not: the conservative answer is that this is still our process.
75
+ if (instance.start === null)
76
+ return true;
77
+ const current = start(instance.pid);
78
+ if (current === null)
79
+ return true;
80
+ return current === instance.start;
81
+ }
82
+ //# sourceMappingURL=process-instance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-instance.js","sourceRoot":"","sources":["../src/process-instance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAalD;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE;YACnE,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;AAChE,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,QAAyB,EAAE,OAA6B,EAAE;IACtF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,OAAO,KAAK,QAAQ,CAAC,KAAK,CAAC;AACpC,CAAC"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * `things rescue` — the headless emergency surface for un-sticking Things
3
+ * (issue #640).
4
+ *
5
+ * WHY THIS EXISTS. Things can reach a state where the app is up, answering, and
6
+ * unusable to automation, with no scripted way back. Three measured behaviours
7
+ * converge on it:
8
+ *
9
+ * - a standing modal sheet EMPTIES the app's top-level AppleScript collections
10
+ * (oddities §25 / MODALX1), so `delete` reports `-1728 Can't get to do id` on
11
+ * a row the database holds open — issue #620's "ghost clone", which has no
12
+ * second cause;
13
+ * - a standing sheet GATES Things Cloud sync entirely (oddities §24), so writes
14
+ * land locally and silently never leave the Mac;
15
+ * - a Repeat dialog opened while Things is BACKGROUNDED becomes a detached
16
+ * window that nothing dismisses (oddities §26 / DRVLAT1 §5) — not its own
17
+ * Cancel, not Escape, not ⌘W, not a real HID click at its Cancel frame.
18
+ *
19
+ * The shipped driver never creates the third state (every dialog recipe
20
+ * activates Things first, which is exactly why that step is permanent), but a
21
+ * killed client, a harness tool-timeout or a person walking away mid-dialog can
22
+ * all leave the first two. {@link rescueStatus} reports them; the other two
23
+ * verbs act on them. This is the single home for both halves — the read-only
24
+ * census that was once the top-level `ui-state` command lives here now.
25
+ *
26
+ * THE THREE VERBS, separated by what they can cost you:
27
+ *
28
+ * - {@link rescueStatus} free, ungated, read-only — the census + the lock
29
+ * table + a "we must not touch that" verdict;
30
+ * - {@link rescueDismiss} gated two ways — presses ONE dialog's own Cancel;
31
+ * - {@link rescueRelaunch} gated two ways — ends the process and starts it
32
+ * again, the only cure measured for oddities §26.
33
+ *
34
+ * PERMISSIONS DOCTRINE. Nothing here raises a macOS consent dialog. The screen
35
+ * is read behind the prompt-free capability verdict, exactly as `doctor
36
+ * --ui-state` reads it, and a machine that has not granted the access is TOLD so
37
+ * rather than
38
+ * prompted. {@link rescueRelaunch} is the deliberate exception to needing that
39
+ * grant at all — see its own note.
40
+ */
41
+ import { type AuditWriter } from "./audit/log.ts";
42
+ import { type UiCapability } from "./capability.ts";
43
+ import { type UiState } from "./write/vectors/ui-state.ts";
44
+ import { type UiRunner } from "./write/vectors/ui.ts";
45
+ /** What a relaunch could establish about the database it left behind. */
46
+ export interface RescueSchemaVerdict {
47
+ /** Did the database open and read as a shape this build understands? */
48
+ ok: boolean;
49
+ /** One sentence, for the report. */
50
+ detail: string;
51
+ }
52
+ /**
53
+ * Every side-effecting thing this module does, injectable. Production defaults
54
+ * are the real ones; the unit suite substitutes all of them, which is what makes
55
+ * the gating, the refusals and the kill ladder testable without an app.
56
+ */
57
+ export interface RescueDeps {
58
+ /** The prompt-free GUI capability verdict. */
59
+ uiCapability?: () => UiCapability;
60
+ /** The osascript / reveal dispatch seam — the same transport the drive uses. */
61
+ run?: UiRunner;
62
+ /** Process environment, for config, the lock path and the change-history directory. */
63
+ env?: NodeJS.ProcessEnv;
64
+ /** Where a rescue ACTION records what it did. */
65
+ audit?: AuditWriter;
66
+ now?: () => number;
67
+ pidAlive?: (pid: number) => boolean;
68
+ /** Which pids Things is running as, newest first; empty when it is not running. */
69
+ thingsPids?: () => Promise<number[]>;
70
+ /** Signal one pid. Never throws for a pid that has already gone. */
71
+ signal?: (pid: number, sig: NodeJS.Signals) => void;
72
+ /** Start Things in the BACKGROUND — we do not steal the screen to fix the screen. */
73
+ launch?: () => Promise<{
74
+ ok: boolean;
75
+ detail: string;
76
+ }>;
77
+ /** Read the database's own verdict, for the post-relaunch check. */
78
+ schemaStatus?: () => RescueSchemaVerdict;
79
+ sleep?: (ms: number) => Promise<void>;
80
+ }
81
+ /**
82
+ * A modal that is NOT ours. `inspectable: false` is the signature of a secure
83
+ * system modal — a macOS privacy/consent dialog, which belongs to no
84
+ * application's Accessibility tree and which no rescue verb may ever press a
85
+ * button on. It is reported so the operator knows to look at the screen, and so
86
+ * they know the refusal that follows is deliberate rather than a failure.
87
+ */
88
+ export interface RescueForeignModal {
89
+ /** The application that owns the screen, or null when even that could not be read. */
90
+ owner: string | null;
91
+ detail: string;
92
+ }
93
+ /** The mutation lock, as `rescue status` reports it. Read-only, always. */
94
+ export interface RescueLockView {
95
+ path: string;
96
+ held: boolean;
97
+ pid: number | null;
98
+ /** ISO instant the holder took it. */
99
+ since: string | null;
100
+ /** Human age ("4m 12s"), or null when it could not be computed. */
101
+ heldFor: string | null;
102
+ /** Does a process with that pid still exist? */
103
+ alive: boolean;
104
+ /** Alive AND older than any change takes — old enough to say so. */
105
+ suspect: boolean;
106
+ detail: string;
107
+ }
108
+ export interface RescueStatusReport {
109
+ /** Could the screen be read at all on this machine? */
110
+ screenReadable: boolean;
111
+ /** One sentence: the census summary, or the reason there is none. */
112
+ detail: string;
113
+ /** The census; null when it could not be read. */
114
+ state: UiState | null;
115
+ lock: RescueLockView;
116
+ /** Set when something we must not touch owns the screen. */
117
+ foreignModal: RescueForeignModal | null;
118
+ /** Consequences worth stating — the open-dialog sync gate. */
119
+ warnings: string[];
120
+ /** What to do next, including which rescue verb applies. */
121
+ remediation: string[];
122
+ }
123
+ /**
124
+ * Read everything a stuck machine can be asked about, without touching any of
125
+ * it. Never throws and never refuses: the cases this exists for are exactly the
126
+ * ones where something is already wrong, so every failure is a REPORTED state.
127
+ */
128
+ export declare function rescueStatus(deps?: RescueDeps): Promise<RescueStatusReport>;
129
+ export type RescueDismissOutcome =
130
+ /** Nothing was stranded — no dialog was open. */
131
+ "no-dialog"
132
+ /** Cancel was pressed and the dialog is confirmed gone. */
133
+ | "dismissed"
134
+ /** Cancel was pressed and the dialog is demonstrably still there (oddities §26). */
135
+ | "still-open"
136
+ /** Cancel was pressed and the screen could not be re-read to confirm either way. */
137
+ | "unverified"
138
+ /** Nothing was pressed. */
139
+ | "refused";
140
+ export interface RescueDismissResult {
141
+ outcome: RescueDismissOutcome;
142
+ /** Which press worked, when one did. */
143
+ how: "cancel-button" | "cancel-click" | null;
144
+ before: UiState | null;
145
+ after: UiState | null;
146
+ /** Dialogs still stacked after this one; null when it could not be re-read. */
147
+ levelsRemaining: number | null;
148
+ detail: string;
149
+ remediation: string[];
150
+ notes: string[];
151
+ warnings: string[];
152
+ }
153
+ /** The two keys `rescue dismiss` needs, spelled the way each surface spells them. */
154
+ export interface RescueDismissAck {
155
+ /** `--dangerously-dismiss-dialog`. */
156
+ dangerouslyDismissDialog?: boolean;
157
+ }
158
+ /**
159
+ * Close the dialog IN FRONT by pressing its own Cancel button — FGRD2's proven
160
+ * Cancel, generalized off the drive's abort path and onto a dialog no drive
161
+ * opened.
162
+ *
163
+ * EXACTLY ONE LIFO LEVEL PER INVOCATION. Sheets nest as `AXSheet` children of
164
+ * the sheet below and dismiss strictly last-in-first-out (MODALX1 §6). A loop
165
+ * that cleared a whole stack in one command would be a loop pressing an unknown
166
+ * number of buttons on an unknown number of dialogs; instead this closes the top
167
+ * one, re-reads, and reports how many remain so the caller decides.
168
+ *
169
+ * PROVEN, never assumed. The press reporting success is NOT closure: the
170
+ * detached editor of oddities §26 accepts an AXPress on its Cancel and stays
171
+ * exactly where it is. Closure is decided by a fresh census showing the stack one
172
+ * level shorter, and an unconfirmable press is reported as unconfirmed.
173
+ *
174
+ * WHAT IT CAN COST YOU: the dialog's own pending edits, and nothing else. The
175
+ * database is untouched, and a URL command parked behind a consent sheet is
176
+ * discarded with it (URLEN1).
177
+ */
178
+ export declare function rescueDismiss(ack?: RescueDismissAck, deps?: RescueDeps): Promise<RescueDismissResult>;
179
+ export type RescueRelaunchOutcome = "relaunched" | "refused" | "failed";
180
+ export interface RescueRelaunchResult {
181
+ outcome: RescueRelaunchOutcome;
182
+ /** Which rung actually ended the process. */
183
+ endedBy: "quit" | "sigterm" | "sigkill" | "not-running" | null;
184
+ before: UiState | null;
185
+ after: UiState | null;
186
+ /** The step-by-step account of the ladder, always present. */
187
+ ladder: string[];
188
+ detail: string;
189
+ remediation: string[];
190
+ notes: string[];
191
+ warnings: string[];
192
+ }
193
+ /** The keys `rescue relaunch` needs. The second one is profile-dependent. */
194
+ export interface RescueRelaunchAck {
195
+ /** `--yes`. */
196
+ yes?: boolean;
197
+ /** `--dangerously-force-quit`; required only under the `workstation` profile. */
198
+ dangerouslyForceQuit?: boolean;
199
+ }
200
+ /**
201
+ * End Things and start it again — the nuclear rung, and the ONLY cure this
202
+ * project has measured for the detached editor of oddities §26. DRVLAT1 §5 tried
203
+ * every other route against the same live dialog (its own Cancel backgrounded,
204
+ * foregrounded and after `AXRaise`; Escape; ⌘W then re-activate; a real HID click
205
+ * at the AX-resolved Cancel frame) and every one was inert.
206
+ *
207
+ * THE LADDER, each rung bounded:
208
+ *
209
+ * 1. ask Things to quit through its own scripting interface. Bounded, because
210
+ * a standing sheet swallows ⌘Q and the `quit` verb can block behind one.
211
+ * 2. SIGTERM, and wait for the process to go.
212
+ * 3. SIGKILL — the lab's canonical reset for a modal that will not clear.
213
+ * 4. start it again in the BACKGROUND: we do not steal the screen to fix the
214
+ * screen.
215
+ * 5. prove it: the process is answering, the database still reads as the shape
216
+ * this version expects, and no dialog is open.
217
+ *
218
+ * WHAT IS LOST: the Things database is write-ahead-logged, so everything already
219
+ * committed survives a kill unconditionally. The ceiling is what was never
220
+ * committed — the edits sitting in the dialog being destroyed.
221
+ *
222
+ * WHAT COMES BACK: sync. While a dialog stands, Things stops sending changes to
223
+ * Things Cloud (oddities §24); killing the dialog with the process releases that,
224
+ * and everything written on this Mac in the meantime goes out.
225
+ *
226
+ * DELIBERATELY NOT GATED ON THE GUI CAPABILITY. Every other verb that touches the
227
+ * app needs the GUI grant, because it synthesizes input into someone's window.
228
+ * This one signals a process the user already owns, which needs no grant at all —
229
+ * and the machines that need it most are exactly the ones where the Accessibility
230
+ * path is what broke. The quit rung and the census still run through the granted
231
+ * seam when it is available, and are skipped without complaint when it is not.
232
+ */
233
+ export declare function rescueRelaunch(ack?: RescueRelaunchAck, deps?: RescueDeps): Promise<RescueRelaunchResult>;
234
+ /**
235
+ * The human render of `things rescue status`.
236
+ *
237
+ * A ROW WHOSE PROBE DID NOT ANSWER SAYS SO (issue #629). Printing a field's
238
+ * unset default ("none", "unknown") beside rows that WERE measured is exactly
239
+ * what made a stalled inspection read as a clean screen in the field, so an
240
+ * unproven probe renders as "not established" rather than as its default, and
241
+ * the probes are named again in full on the `unproven:` row.
242
+ */
243
+ export declare function rescueStatusLines(report: RescueStatusReport): string[];
244
+ /** The human render of `things rescue dismiss`. */
245
+ export declare function rescueDismissLines(result: RescueDismissResult): string[];
246
+ /** The human render of `things rescue relaunch`. */
247
+ export declare function rescueRelaunchLines(result: RescueRelaunchResult): string[];