things-api 0.16.0 → 0.18.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 (208) hide show
  1. package/README.md +1 -1
  2. package/deputy/VERSION +1 -0
  3. package/deputy/helpers-Info.plist +23 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +20 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +115 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +23 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  10. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +125 -0
  11. package/deputy/reader/Info.plist +20 -0
  12. package/deputy/reader/entitlements.plist +15 -0
  13. package/deputy/reader/main.swift +491 -0
  14. package/deputy/src/main.swift +133 -0
  15. package/deputy/src/osascript.swift +102 -0
  16. package/deputy/src/server.swift +350 -0
  17. package/deputy/src/sqlite.swift +121 -0
  18. package/dist/cli/commands/doctor.js +55 -2
  19. package/dist/cli/commands/doctor.js.map +1 -1
  20. package/dist/cli/commands/helpers.d.ts +17 -0
  21. package/dist/cli/commands/helpers.js +148 -0
  22. package/dist/cli/commands/helpers.js.map +1 -0
  23. package/dist/cli/commands/op-result.d.ts +15 -0
  24. package/dist/cli/commands/op-result.js +39 -0
  25. package/dist/cli/commands/op-result.js.map +1 -0
  26. package/dist/cli/commands/repeat-flags.d.ts +3 -3
  27. package/dist/cli/commands/repeat-flags.js +90 -33
  28. package/dist/cli/commands/repeat-flags.js.map +1 -1
  29. package/dist/cli/commands/writes.js +79 -6
  30. package/dist/cli/commands/writes.js.map +1 -1
  31. package/dist/cli/help.js +13 -2
  32. package/dist/cli/help.js.map +1 -1
  33. package/dist/cli/helpers-check.d.ts +10 -0
  34. package/dist/cli/helpers-check.js +59 -0
  35. package/dist/cli/helpers-check.js.map +1 -0
  36. package/dist/cli/interrupt.d.ts +42 -0
  37. package/dist/cli/interrupt.js +105 -0
  38. package/dist/cli/interrupt.js.map +1 -0
  39. package/dist/cli/main.js +27 -1
  40. package/dist/cli/main.js.map +1 -1
  41. package/dist/client.js +19 -2
  42. package/dist/client.js.map +1 -1
  43. package/dist/config.d.ts +70 -0
  44. package/dist/config.js +25 -1
  45. package/dist/config.js.map +1 -1
  46. package/dist/contracts.d.ts +9 -2
  47. package/dist/contracts.js +1 -1
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db/baselines/db-v26.js +8 -4
  50. package/dist/db/baselines/db-v26.js.map +1 -1
  51. package/dist/db/baselines/db-v27.d.ts +42 -0
  52. package/dist/db/baselines/db-v27.js +47 -0
  53. package/dist/db/baselines/db-v27.js.map +1 -0
  54. package/dist/db/baselines/index.js +2 -1
  55. package/dist/db/baselines/index.js.map +1 -1
  56. package/dist/db/locate.d.ts +4 -1
  57. package/dist/db/locate.js +3 -1
  58. package/dist/db/locate.js.map +1 -1
  59. package/dist/db/schema.d.ts +1 -1
  60. package/dist/db/schema.js +9 -0
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/deputy/bridge-worker.d.ts +1 -0
  63. package/dist/deputy/bridge-worker.js +79 -0
  64. package/dist/deputy/bridge-worker.js.map +1 -0
  65. package/dist/deputy/bridge.d.ts +14 -0
  66. package/dist/deputy/bridge.js +70 -0
  67. package/dist/deputy/bridge.js.map +1 -0
  68. package/dist/deputy/client.d.ts +19 -0
  69. package/dist/deputy/client.js +104 -0
  70. package/dist/deputy/client.js.map +1 -0
  71. package/dist/deputy/db-facade.d.ts +18 -0
  72. package/dist/deputy/db-facade.js +71 -0
  73. package/dist/deputy/db-facade.js.map +1 -0
  74. package/dist/deputy/files.d.ts +1 -0
  75. package/dist/deputy/files.js +16 -0
  76. package/dist/deputy/files.js.map +1 -0
  77. package/dist/deputy/install.d.ts +105 -0
  78. package/dist/deputy/install.js +467 -0
  79. package/dist/deputy/install.js.map +1 -0
  80. package/dist/deputy/notice.d.ts +6 -0
  81. package/dist/deputy/notice.js +28 -0
  82. package/dist/deputy/notice.js.map +1 -0
  83. package/dist/deputy/notices.d.ts +31 -0
  84. package/dist/deputy/notices.js +94 -0
  85. package/dist/deputy/notices.js.map +1 -0
  86. package/dist/deputy/osa.d.ts +21 -0
  87. package/dist/deputy/osa.js +72 -0
  88. package/dist/deputy/osa.js.map +1 -0
  89. package/dist/deputy/protocol.d.ts +86 -0
  90. package/dist/deputy/protocol.js +106 -0
  91. package/dist/deputy/protocol.js.map +1 -0
  92. package/dist/deputy/routing.d.ts +63 -0
  93. package/dist/deputy/routing.js +338 -0
  94. package/dist/deputy/routing.js.map +1 -0
  95. package/dist/deputy/shortcuts-exec.d.ts +10 -0
  96. package/dist/deputy/shortcuts-exec.js +50 -0
  97. package/dist/deputy/shortcuts-exec.js.map +1 -0
  98. package/dist/diagnose.d.ts +43 -1
  99. package/dist/diagnose.js +64 -2
  100. package/dist/diagnose.js.map +1 -1
  101. package/dist/index.d.ts +16 -2
  102. package/dist/index.js +17 -1
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp/server.js +25 -7
  105. package/dist/mcp/server.js.map +1 -1
  106. package/dist/model/mappers.d.ts +12 -3
  107. package/dist/model/mappers.js +6 -1
  108. package/dist/model/mappers.js.map +1 -1
  109. package/dist/model/occurrences.d.ts +14 -1
  110. package/dist/model/occurrences.js +20 -4
  111. package/dist/model/occurrences.js.map +1 -1
  112. package/dist/model/recurrence.d.ts +64 -9
  113. package/dist/model/recurrence.js +94 -19
  114. package/dist/model/recurrence.js.map +1 -1
  115. package/dist/model/template-projection.d.ts +26 -0
  116. package/dist/model/template-projection.js +113 -0
  117. package/dist/model/template-projection.js.map +1 -0
  118. package/dist/op-result.d.ts +46 -0
  119. package/dist/op-result.js +156 -0
  120. package/dist/op-result.js.map +1 -0
  121. package/dist/paths.d.ts +8 -0
  122. package/dist/paths.js +10 -0
  123. package/dist/paths.js.map +1 -1
  124. package/dist/read/detail.js +7 -1
  125. package/dist/read/detail.js.map +1 -1
  126. package/dist/read/queries.js +8 -1
  127. package/dist/read/queries.js.map +1 -1
  128. package/dist/read/views.d.ts +6 -4
  129. package/dist/read/views.js +60 -18
  130. package/dist/read/views.js.map +1 -1
  131. package/dist/sync-health.js +8 -3
  132. package/dist/sync-health.js.map +1 -1
  133. package/dist/trace/tracer.d.ts +87 -0
  134. package/dist/trace/tracer.js +167 -0
  135. package/dist/trace/tracer.js.map +1 -0
  136. package/dist/write/accessibility-probe.js +4 -2
  137. package/dist/write/accessibility-probe.js.map +1 -1
  138. package/dist/write/automation-probe.js +5 -1
  139. package/dist/write/automation-probe.js.map +1 -1
  140. package/dist/write/availability.js +7 -3
  141. package/dist/write/availability.js.map +1 -1
  142. package/dist/write/clear-reminder.js +24 -21
  143. package/dist/write/clear-reminder.js.map +1 -1
  144. package/dist/write/commands.js +95 -45
  145. package/dist/write/commands.js.map +1 -1
  146. package/dist/write/completion-context.d.ts +57 -0
  147. package/dist/write/completion-context.js +49 -0
  148. package/dist/write/completion-context.js.map +1 -0
  149. package/dist/write/experimental.d.ts +19 -0
  150. package/dist/write/experimental.js +50 -0
  151. package/dist/write/experimental.js.map +1 -1
  152. package/dist/write/failure-hints.d.ts +10 -1
  153. package/dist/write/failure-hints.js +8 -0
  154. package/dist/write/failure-hints.js.map +1 -1
  155. package/dist/write/guards.d.ts +1 -1
  156. package/dist/write/guards.js +10 -0
  157. package/dist/write/guards.js.map +1 -1
  158. package/dist/write/move.js +14 -6
  159. package/dist/write/move.js.map +1 -1
  160. package/dist/write/operations.d.ts +40 -7
  161. package/dist/write/pipeline.d.ts +41 -0
  162. package/dist/write/pipeline.js +289 -19
  163. package/dist/write/pipeline.js.map +1 -1
  164. package/dist/write/pre-state.d.ts +1 -1
  165. package/dist/write/pre-state.js +35 -17
  166. package/dist/write/pre-state.js.map +1 -1
  167. package/dist/write/promote-clone.js +434 -19
  168. package/dist/write/promote-clone.js.map +1 -1
  169. package/dist/write/reorder.js +76 -27
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/repeat-anchor.d.ts +140 -0
  172. package/dist/write/repeat-anchor.js +403 -0
  173. package/dist/write/repeat-anchor.js.map +1 -0
  174. package/dist/write/repeat-asserts.d.ts +28 -0
  175. package/dist/write/repeat-asserts.js +169 -0
  176. package/dist/write/repeat-asserts.js.map +1 -0
  177. package/dist/write/repeat-rule.js +37 -12
  178. package/dist/write/repeat-rule.js.map +1 -1
  179. package/dist/write/vectors/applescript.js +7 -16
  180. package/dist/write/vectors/applescript.js.map +1 -1
  181. package/dist/write/vectors/session-reachability.d.ts +133 -0
  182. package/dist/write/vectors/session-reachability.js +151 -0
  183. package/dist/write/vectors/session-reachability.js.map +1 -0
  184. package/dist/write/vectors/shortcuts.js +5 -14
  185. package/dist/write/vectors/shortcuts.js.map +1 -1
  186. package/dist/write/vectors/simulator.d.ts +16 -1
  187. package/dist/write/vectors/simulator.js +122 -32
  188. package/dist/write/vectors/simulator.js.map +1 -1
  189. package/dist/write/vectors/types.d.ts +183 -10
  190. package/dist/write/vectors/ui-recipes.d.ts +8 -0
  191. package/dist/write/vectors/ui-recipes.js +246 -58
  192. package/dist/write/vectors/ui-recipes.js.map +1 -1
  193. package/dist/write/vectors/ui.d.ts +156 -14
  194. package/dist/write/vectors/ui.js +846 -84
  195. package/dist/write/vectors/ui.js.map +1 -1
  196. package/dist/write/verify/delta.d.ts +12 -14
  197. package/dist/write/verify/delta.js +35 -34
  198. package/dist/write/verify/delta.js.map +1 -1
  199. package/dist/write/verify/poller.d.ts +13 -0
  200. package/dist/write/verify/poller.js +38 -0
  201. package/dist/write/verify/poller.js.map +1 -1
  202. package/package.json +8 -1
  203. package/schema/envelope.schema.json +22 -0
  204. package/scripts/build-helpers.sh +104 -0
  205. package/scripts/deputy-cert-setup.sh +60 -0
  206. package/skills/things-cli/SKILL.md +11 -3
  207. package/skills/things-cli/references/errors.md +4 -0
  208. package/skills/things-cli/references/repeating.md +69 -0
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # One-time ceremony: mint the persistent self-signed code-signing certificate
3
+ # ("things-deputy-signing") that scripts/build-helpers.sh signs the deputy with.
4
+ #
5
+ # WHY A PERSISTENT CERT: macOS attaches Automation/Accessibility/file grants to
6
+ # the responsible process's code signature. A stable certificate means every
7
+ # rebuild of the deputy keeps the same identity — grants survive. Ad-hoc
8
+ # signing mints a fresh identity per build, which silently re-introduces the
9
+ # grant churn the deputy exists to end, so it is deliberately not supported.
10
+ #
11
+ # INTERACTIVE: importing trust settings prompts for your login-keychain
12
+ # password / an authorization dialog. Run this yourself at the machine; it is
13
+ # not for unattended execution. Everything stays in your login keychain —
14
+ # nothing is written to the repository.
15
+ #
16
+ # Re-running is safe: if the identity already exists, the script leaves it be.
17
+ set -euo pipefail
18
+
19
+ IDENTITY="things-deputy-signing"
20
+
21
+ if security find-identity -v -p codesigning 2>/dev/null | grep -q "$IDENTITY"; then
22
+ echo "signing identity '$IDENTITY' already exists — nothing to do."
23
+ exit 0
24
+ fi
25
+
26
+ WORK=$(mktemp -d)
27
+ trap 'rm -rf "$WORK"' EXIT
28
+
29
+ cat >"$WORK/ext.cnf" <<'EOF'
30
+ [ req ]
31
+ distinguished_name = dn
32
+ x509_extensions = codesign_ext
33
+ prompt = no
34
+ [ dn ]
35
+ CN = things-deputy-signing
36
+ [ codesign_ext ]
37
+ keyUsage = critical,digitalSignature
38
+ extendedKeyUsage = critical,codeSigning
39
+ basicConstraints = critical,CA:false
40
+ EOF
41
+
42
+ # 10-year self-signed code-signing cert + key, bundled as a throwaway p12.
43
+ openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
44
+ -keyout "$WORK/key.pem" -out "$WORK/cert.pem" -config "$WORK/ext.cnf" >/dev/null 2>&1
45
+ openssl pkcs12 -export -inkey "$WORK/key.pem" -in "$WORK/cert.pem" \
46
+ -name "$IDENTITY" -passout pass:transient -out "$WORK/bundle.p12" >/dev/null 2>&1
47
+
48
+ echo "importing '$IDENTITY' into the login keychain (codesign is granted access)..."
49
+ security import "$WORK/bundle.p12" -k "$HOME/Library/Keychains/login.keychain-db" \
50
+ -P transient -T /usr/bin/codesign
51
+
52
+ echo "marking the certificate trusted for code signing (this may prompt)..."
53
+ security add-trusted-cert -p codeSign \
54
+ -k "$HOME/Library/Keychains/login.keychain-db" "$WORK/cert.pem"
55
+
56
+ echo
57
+ echo "done. Next:"
58
+ echo " bash scripts/build-helpers.sh # now signs with $IDENTITY"
59
+ echo " things deputy install # (re)install the signed helper"
60
+ echo " things deputy status # verify signing: signed ($IDENTITY)"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: things-cli
3
3
  description: Read and manage a user's tasks in the Things 3 app (macOS) through the `things` CLI — list views like Today/Inbox/Upcoming, search, and create, edit, schedule, complete, move, or organize to-dos, projects, areas, headings, and tags. Use whenever the user asks about their tasks, to-dos, projects, or anything in Things.
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  ---
6
6
 
7
7
  # Things CLI
@@ -10,7 +10,7 @@ version: 0.16.0
10
10
 
11
11
  `things --help` is the one-screen index; `things <group> --help` lists a group's verbs and flags (always current for the binary you invoke); `things help <topic>` opens a contract guide — topics: `agent`, `filters`, `ids`, `move`, `output`, `repeating`, `writes`.
12
12
 
13
- **This skill's reference files** are exactly `data-model`, `contracts`, `ordering`, `errors`, `banner`, `gui` (all `references/*.md`) — do not guess other names. Read *shapes* (every view's JSON, `stage`/`when`, the compact/full tiers) live in `data-model`; the envelope, exit codes, undo, batch, and recurrence in `contracts`; move/reorder axes in `ordering`; the error contract in `errors`. There is no `reads`/`writes`/`recurrence`/`safety` file. Most tasks need only this page — open a reference only when a link below sends you there.
13
+ **This skill's reference files** are exactly `data-model`, `contracts`, `repeating`, `ordering`, `errors`, `banner`, `gui` (all `references/*.md`) — do not guess other names. Read *shapes* (every view's JSON, `stage`/`when`, the compact/full tiers) live in `data-model`; the envelope, exit codes, undo, batch, and the recurrence contract in `contracts`; the repeating model, scheduling vocabulary, and deadline law in `repeating`; move/reorder axes in `ordering`; the error contract in `errors`. There is no `reads`/`writes`/`recurrence`/`safety` file. Most tasks need only this page — open a reference only when a link below sends you there.
14
14
 
15
15
  ## Data model (read this first)
16
16
 
@@ -46,6 +46,7 @@ These hold regardless of the binary version; see [references/contracts.md](refer
46
46
  - **Exit codes**: `0` landed and checked · `2` usage · `3` verify-failed · `4` blocked · `5` drift-blocked · `6` unsupported · `7` environment (`1` is an internal bug). Nonzero means the change did NOT stick; the message names the fix.
47
47
  - **Previews, undo & idempotency**: `--dry-run` is universal — accepted by every command, it guarantees nothing changes: on a read it returns the normal output unchanged, on a write it shows the exact plan without executing; `things undo` reverses recent changes made through this tool (each reversible write returns an `undoToken`); a single mutation may carry `--op-id <key>` so an ambiguous resubmission is recognized as already applied (the variadic `move`/`reorder` refuse it — use `things batch` with a per-line `opId`).
48
48
  - **Timeline-silent mutations** (`--preserve-modified`, MCP `preserve_modified`): a universal write flag that keeps a change off the modification-date timeline. Some writes (applying/removing a tag, editing a field, resolving an item) re-stamp each edited item's modification date — the key `things changes --since <moment>` and any watch/sync process sort on. `--preserve-modified` captures each PRE-EXISTING edited item's modification date before the write and restores it (to the whole second) afterward, so the edit does not surface as a new change. It is the enabler for a mass retro-tag or bulk cleanup that must not flood `changes`. A pure create (`add`) is a no-op; the restore is best-effort (a failure is reported per item, the change itself stands) and the result carries `preservedModified: <n>`. **Safe against a synced (Things Cloud) store** — the restored modification date syncs to the other devices and survives the round-trip, so the item stays off the timeline on every device (SYNC2B); the only edge is a genuinely concurrent edit to the same item on another device, which re-dates it via Things Cloud's per-attribute merge (fails safe — the edit resurfaces, never silently hidden).
49
+ - **Completion hints**: a successful `todo complete`/`todo cancel` result carries a `context` object (`data.context` under `--json`) reporting the OPEN work remaining in the to-do's container(s) — `context.project = {uuid, title, remainingOpen}` when it was in a project (heading children included), `context.today = {remainingOpen}` when it was in Today. Use it to notice an emptied container: `remainingOpen: 0` on the project means every open task there is now done, so you might offer to complete the project itself — it only informs, it never auto-acts. Absent when the to-do was in neither.
49
50
  - **Preconditions**: referenced containers and tags must already exist — create nested structures outside-in and reuse each returned UUID.
50
51
  - If the user requests a JSON reply schema, return exactly that object after the read or checked write.
51
52
 
@@ -63,14 +64,21 @@ Namespaced verb families — run `things <group> --help` for the verbs and `thin
63
64
 
64
65
  **Resolution timestamps** (`--created-at` / `--completed-at`, both to-dos and projects; ISO date or datetime — a bare date is NOON in the effective zone): `add --completed-at <iso> [--created-at <iso>]` creates the item already RESOLVED in the Logbook (the history-import / GTD-migration path); `add --created-at <iso>` alone is born-open with a backdated creation. On an EXISTING item, `update --created-at` rewrites creation (any status); `update --completed-at` rewrites the completion timestamp of an ALREADY-resolved item (a canceled one stays canceled) — an OPEN item is refused, pointing you at `complete --completed-at` (resolve-and-backdate completed) or `cancel --completed-at` (resolve-and-backdate canceled; the "Completed on" stamp applies to canceled items too). Backdating a resolved item can be a multi-leg, non-atomic sequence — the leg list is disclosed in the result and in `--dry-run`. A project `add --completed-at` cannot seed open `--todo` children (a completed-project import needs every child resolved).
65
66
 
66
- **Guarded writes** surface their consequence and require an explicit flag: deleting a NON-EMPTY area needs `--allow-non-empty`, a permanent delete needs `--dangerously-permanent`, and a UI-driving op (e.g. `area reorder`) needs `--dangerously-drive-gui` plus `things config set ui-enabled true`. The full acknowledgment contract is in [references/errors.md](references/errors.md).
67
+ **Guarded writes** surface their consequence and require an explicit flag: deleting a NON-EMPTY area needs `--allow-non-empty`, a permanent delete needs `--dangerously-permanent`, and a UI-driving op (e.g. `area reorder`, `make-repeating`) needs `--dangerously-drive-gui` plus `things config set ui-enabled true`. The full acknowledgment contract is in [references/errors.md](references/errors.md).
68
+
69
+ **UI-driving ops are slow — allow a generous timeout, and NEVER read a timeout as success.** A `--dangerously-drive-gui` op visibly drives the Things app and can take OVER A MINUTE on a large, syncing database. Give the command a longer-than-normal wrapper timeout — **at least 180s** when your harness allows it (120s is the floor), and pass `--verify-timeout 120000` or more so the app's own budget outlasts the drive. The cardinal rule: **a timeout, EMPTY output, or a silent no-op is NOT a success** — treat it as "outcome unknown". If your own timeout kills the command first you get EMPTY output (not a result, not an error) and the change MAY STILL have landed; if the app's budget is exceeded first you get a structured timeout that says the outcome is uncertain. In BOTH cases, before doing anything else **re-read the item with `things show <uuid>`** (and, on a dev checkout, check the trace under `~/.local/state/things-api/trace/`) to learn what actually happened. **If your harness KILLS long commands** at a cap you cannot raise, pass `--op-id <key>` on the write, let it die, then run **`things op-result <key>`** in a fresh command — it reads the change history the killed process already wrote and reports whether the op landed (`found`), is still-running/died-mid-flight (`intent-only`, outcome UNCERTAIN), or never recorded (`unknown`). **Do NOT fire an identical retry** — repeated blind retries after a timeout or a kill are exactly how you get duplicate or half-configured series (the original item trashed, a wrong template created). If the state is wrong or ambiguous and a re-read does not clarify it, **STOP and file a bug** with the trace rather than retrying. A genuine success always returns a structured JSON result (with `--json`) naming the new/updated uuid; anything else is not a success. The trace can hold real task titles, so never paste it into a public issue.
67
70
 
68
71
  **Quick skeletons**: `things todo add "T1" "T2" "T3" [shared flags]` creates several to-dos in one call (every shared flag — `--project`/`--area`/`--when`/`--tags`/… — applies to each; `--id-only` prints the new uuids one per line for chaining; one `undoToken` removes the set). To stand up a new project with children, `things project add "<title>" --todo "T1" --todo "T2" …` (repeatable). For richer per-item metadata or cross-item references, use `things batch`.
69
72
 
73
+ ## The CLI is the only interface — never route around it
74
+
75
+ Never construct `things:///` URLs, AppleScript (`tell application "Things3" …`), or Shortcuts invocations yourself — for reads or writes. When this skill is installed, the `things` CLI is the sole supported way to touch Things: raw calls bypass its validation, guards, read-back verification, undo/audit records, and session checks, and some raw shapes crash the app or silently do nothing. If the CLI cannot express something you need, or a command refuses or fails in a way that seems wrong, that is a finding, not an obstacle to work around: capture the exact command and its `--json` error (plus `things op-result <op-id>` output where relevant) and report it on the public issue tracker at github.com/mikegreiling/things-api (synthetic repro data only — never the user's real task content), or surface it to your user if you cannot file. A refusal always names its sanctioned override flag when one exists — that acknowledgment system is the only supported escape hatch.
76
+
70
77
  ## Going deeper
71
78
 
72
79
  - [references/data-model.md](references/data-model.md) — the full data model, `stage`/`when` derivation, the compact/full tiers, view membership, and filters.
73
80
  - [references/contracts.md](references/contracts.md) — the JSON envelope, exit codes, safety/undo/idempotency, batch chaining, and recurrence.
81
+ - [references/repeating.md](references/repeating.md) — repeating series in depth: template vs instances, the scheduling vocabulary, off-schedule first occurrences, the deadline law (the anchor names the DUE date), ends bounds, and the GUI-drive operational rules.
74
82
  - [references/ordering.md](references/ordering.md) — move vs reorder in depth: axes, gates, caps, automatic fallbacks, placement guarantees, and the one dead class (templates).
75
83
  - [references/errors.md](references/errors.md) — the error contract: the candidate shape, dead-row hints, hazard acknowledgments, and the error-code registry.
76
84
  - [references/banner.md](references/banner.md) — the Today "new to-dos" banner, the provisional `•` pip, reminder/evening liveness, and what a watcher sees.
@@ -55,3 +55,7 @@ Every `error.code` is drawn from a frozen registry. Route on the code, and for t
55
55
  | `verify-failed` | A multi-leg move/reorder failed mid-way (`detail.failed`/`completed`). | 3 |
56
56
 
57
57
  A consumer that does not recognize a specific suffix routes on the prefix (`blocked:` / `verify-failed:`) and the exit code.
58
+
59
+ ## A failure is never a license to bypass the CLI
60
+
61
+ Whatever the error, do not fall back to hand-built `things:///` URLs, AppleScript, or Shortcuts calls — those bypass the validation, verification, and undo/audit layers this tool exists to provide, and several raw shapes crash the app or fail silently. Refusals that support an override name their acknowledgment flag in the message; that flag system is the only sanctioned escape hatch. Anything else the CLI cannot do is a reportable finding (see the main skill document), not an invitation to improvise.
@@ -0,0 +1,69 @@
1
+ # Repeating series — the model, the scheduling vocabulary, and the deadline law
2
+
3
+ A repeating item is a **template** plus the **instances** it spawns. The template holds the rule (how often, on what calendar day, with what deadline/reminder/end bound); each occurrence the app materializes is a separate to-do (or project) you complete normally. Completing an instance never touches the template; the template keeps spawning the next occurrence on schedule.
4
+
5
+ ## The verbs
6
+
7
+ - `things todo make-repeating <ref> --frequency … --interval …` — turn an existing plain to-do into a repeating series. This **REPLACES the item**: the original to-do disappears and a fresh template takes its place, so the uuid you get back is the TEMPLATE's, not the original's. Cannot be undone through the normal path (the original is moved to the Trash; `things undo` removes the new series and restores it).
8
+ - `things todo add-repeating "<title>" --frequency … --interval …` — create a new to-do already repeating, in one step.
9
+ - `things todo reschedule-repeat <ref> --frequency … --interval …` — change the rule of an EXISTING template in place (identity preserved). Only the fields you name change; an unnamed deadline/reminder/anchor is left as it was.
10
+ - `things todo pause-repeat <ref>` / `resume-repeat <ref>` — suspend/restart spawning without losing the rule.
11
+ - `things project …` mirrors all of the above for repeating projects.
12
+
13
+ All of these drive the Things app's own Repeat dialog, so they require `--dangerously-drive-gui` (and `things config set ui-enabled true`). See the operational rules at the bottom.
14
+
15
+ ## The scheduling vocabulary
16
+
17
+ - **`--frequency`** `daily | weekly | monthly | yearly`, **`--interval N`** = every N of those units.
18
+ - **`--when <YYYY-MM-DD>`** — the date the FIRST occurrence APPEARS (its start). Later occurrences follow the rule's calendar anchor. If you omit `--when`, `make-repeating` starts from the item's own scheduled date.
19
+ - **The calendar anchor** — WHAT day the series recurs on, per frequency:
20
+ - weekly → `--weekdays monday,wednesday,friday` (one or more).
21
+ - monthly → `--on-day <1–31 | last>`, OR an nth-weekday with `--on-weekday <weekday> --on-ordinal <1–5 | last>`.
22
+ - yearly → `--yearly-month <1–12>` plus `--on-day` (or the nth-weekday pair).
23
+ - **Derived anchor.** If you give `--when` but NO explicit anchor, the anchor is taken FROM `--when` (weekly → its weekday, monthly → its day-of-month, yearly → its month + day). So `make-repeating <ref> --frequency yearly --when 2028-10-16` recurs every October 16 with no anchor flags needed.
24
+
25
+ ## Off-schedule first occurrence (a first-class pattern)
26
+
27
+ The first occurrence does NOT have to sit on the recurring grid. If `--when` disagrees with an explicit anchor, the series APPEARS on `--when` the first time and follows the anchor from the second occurrence on. Example: `--frequency weekly --weekdays wednesday --when <a Thursday>` = a Thursday first occurrence, then Wednesdays every week after. Weekly and yearly rules support this; the command discloses both halves of the landed pattern (`appears <date>; thereafter <rule>`) in its result and in `--dry-run`.
28
+
29
+ **One exception — monthly.** A monthly rule cannot start off its anchor day: the app snaps the first occurrence to the anchor's day-of-month, so an off-anchor monthly first is refused up front. Put `--when` on the anchor day, or omit the anchor to take it from `--when`.
30
+
31
+ ## The deadline law (read this before combining `--deadline` with an anchor)
32
+
33
+ `--deadline` gives each occurrence a due date; `--start-days-earlier N` makes each occurrence APPEAR N days before it is DUE. The key rule: **in deadline mode the calendar anchor names the DUE date, and `--when` is still the APPEAR (start) date.** So an explicit anchor must name `when + N` (the due date), not `--when` itself.
34
+
35
+ Worked example — intent "appears October 16, due October 30" (a 14-day lead):
36
+
37
+ - Simplest: `--frequency yearly --when 2028-10-16 --deadline --start-days-earlier 14` with NO anchor flags — the due-date anchor is derived as October 30 and each occurrence appears 14 days earlier, on October 16.
38
+ - Explicit anchor equivalent: name the DUE date — `--yearly-month 10 --on-day 30 --when 2028-10-16 --deadline --start-days-earlier 14`.
39
+
40
+ If you instead write an explicit anchor on the APPEAR date (`--on-day 16` with the same `--when` and lead), you are asking for an off-schedule first occurrence relative to the due-date anchor — for weekly/yearly this is honored and disclosed (the first occurrence appears/dues on your `--when`, the ongoing series dues on the anchor); a monthly combination of this shape is refused as above. When you want the two to agree, either drop the anchor flags (derived) or set the anchor to the due date (`when + N`).
41
+
42
+ ### Repeating deadlines on `add-repeating`: two spellings for one geometry
43
+
44
+ A concrete `--deadline <date>` on `todo add-repeating` sets each occurrence's due date relative to its own start, and the RULE owns it: every occurrence is deadlined, the FIRST one due on the date you name. You state that geometry in ONE of two equivalent ways — the tool does the arithmetic:
45
+
46
+ - **Concrete** — `--deadline <date>` is the first occurrence's due date; the per-occurrence lead is derived as `deadline − --when`. Reach for this when you know both dates.
47
+ - **Relative** — `--start-days-earlier N` names the lead directly (each occurrence starts N days before its own deadline; the first deadline is derived as `--when + N`). Reach for this when the intent is genuinely relative ("due two weeks after it appears").
48
+
49
+ The steer: state the two dates you actually know, pick the spelling that matches your intent, and **give ONE form or the other, never both** — never compute one from the other yourself. Both need a concrete `--when` (and `--deadline` must be on or after it); the anchor names the DUE date as usual; neither applies to an after-completion series. The safety net: if you do pass both, they must agree (`deadline − --when == N`), and a mismatch is refused up front with a message naming both corrected spellings.
50
+
51
+ One call, no follow-up `reschedule-repeat`: `todo add-repeating "Taxes" --when 2027-03-15 --deadline 2027-05-31 --frequency yearly --yearly-month 5 --on-weekday monday --on-ordinal last` deadlines every occurrence with a 77-day lead. (A concrete `--deadline` now maps to the rule; historically it landed on the seed only and double-booked the first occurrence — see [docs/lab/dblspawn1-preserved-instance.md](../../../docs/lab/dblspawn1-preserved-instance.md).)
52
+
53
+ ## Ends bounds, reminder, and limits worth knowing
54
+
55
+ - **Ends bound** (default: never): `--ends-after N` stops after N total occurrences (the ORIGINAL count, not a remaining tally — it does not tick down), or `--ends-on <date>` stops after that date. An exhausted series reads as `ended`.
56
+ - **Reminder**: `--reminder HH:mm` sets a time-of-day alert on each occurrence.
57
+ - **After-completion cadence**: `--after-completion` repeats N units AFTER each occurrence is completed, rather than on a fixed calendar. It has NO calendar anchor and takes NO end bound — those are refused.
58
+ - **Expressibility**: a rule can carry only ONE end bound (not both a date and a count), and a monthly/yearly rule only ONE calendar anchor. Reschedule cannot restore a rule shape the Repeat dialog itself cannot produce.
59
+
60
+ ## Operational rules for the GUI-driven repeat commands
61
+
62
+ These commands visibly drive the Things app and are slow — often OVER A MINUTE on a large, syncing database. (This is the same guidance as the main skill page and `things help repeating`, condensed.)
63
+
64
+ - Allow a generous wrapper timeout — **at least 180s** where your harness permits (120s floor) — and pass `--verify-timeout 120000` or more so the app's own budget outlasts the drive.
65
+ - **A timeout, empty output, or a silent no-op is NOT success** — treat it as "outcome unknown". Before doing anything else, re-read the item with `things show <ref>` (and, on a dev checkout, the trace under `~/.local/state/things-api/trace/`) to learn what actually landed.
66
+ - **If your environment kills long commands** (a hard wall-time cap you cannot raise): pass `--op-id <key>` on the write, let the command die, then run **`things op-result <key>`** in a fresh command to read what actually happened — it reads the local change history the killed process already wrote and reports `found` (the final result + target), `intent-only` (started but no outcome recorded — still running or the process died mid-flight, outcome UNCERTAIN), or `unknown`. This is the recovery path for a capped harness; do NOT infer the outcome from the kill. (`--op-id` works on the single-op repeat verbs like `reschedule-repeat`; `make-repeating` / `add-repeating` are compounds — express them as a `batch` line with a per-line `opId` if you need this.)
67
+ - **Never fire an identical retry** after a timeout or a kill — a blind retry is how you get a duplicate or half-configured series (original trashed, wrong template created). Learn the real outcome first (`things op-result` / `things show`); if the state is wrong or ambiguous, STOP and file a bug with the trace.
68
+ - These ops need Things reachable on the current desktop (not a locked screen or a covering full-screen app); a session it cannot reach is refused cleanly with a remediation, touching nothing.
69
+ - A genuine success always returns a structured JSON result (with `--json`) naming the new/updated uuid.