things-api 0.17.0 → 0.19.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 (277) hide show
  1. package/README.md +16 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -0
  4. package/deputy/helpers-Info.plist +27 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +22 -0
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +128 -0
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +27 -0
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +137 -0
  14. package/deputy/reader/Info.plist +22 -0
  15. package/deputy/reader/entitlements.plist +45 -0
  16. package/deputy/reader/main.swift +580 -0
  17. package/deputy/src/main.swift +133 -0
  18. package/deputy/src/osascript.swift +102 -0
  19. package/deputy/src/server.swift +373 -0
  20. package/deputy/src/sqlite.swift +121 -0
  21. package/deputy/src/tcc.swift +58 -0
  22. package/dist/audit/schema.d.ts +13 -1
  23. package/dist/audit/schema.js.map +1 -1
  24. package/dist/capability.d.ts +160 -0
  25. package/dist/capability.js +416 -0
  26. package/dist/capability.js.map +1 -0
  27. package/dist/cli/commands/doctor.js +181 -2
  28. package/dist/cli/commands/doctor.js.map +1 -1
  29. package/dist/cli/commands/helpers.d.ts +17 -0
  30. package/dist/cli/commands/helpers.js +275 -0
  31. package/dist/cli/commands/helpers.js.map +1 -0
  32. package/dist/cli/commands/op-result.d.ts +15 -0
  33. package/dist/cli/commands/op-result.js +45 -0
  34. package/dist/cli/commands/op-result.js.map +1 -0
  35. package/dist/cli/commands/reads.js +47 -1
  36. package/dist/cli/commands/reads.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +63 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +262 -109
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +53 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/helpers-check.d.ts +10 -0
  52. package/dist/cli/helpers-check.js +59 -0
  53. package/dist/cli/helpers-check.js.map +1 -0
  54. package/dist/cli/main.js +22 -1
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +22 -2
  65. package/dist/client.js +52 -15
  66. package/dist/client.js.map +1 -1
  67. package/dist/config.d.ts +31 -0
  68. package/dist/config.js +9 -0
  69. package/dist/config.js.map +1 -1
  70. package/dist/contracts.d.ts +6 -2
  71. package/dist/contracts.js +1 -1
  72. package/dist/contracts.js.map +1 -1
  73. package/dist/db/baselines/db-v26.js +8 -4
  74. package/dist/db/baselines/db-v26.js.map +1 -1
  75. package/dist/db/baselines/db-v27.d.ts +42 -0
  76. package/dist/db/baselines/db-v27.js +47 -0
  77. package/dist/db/baselines/db-v27.js.map +1 -0
  78. package/dist/db/baselines/index.js +2 -1
  79. package/dist/db/baselines/index.js.map +1 -1
  80. package/dist/db/locate.d.ts +4 -1
  81. package/dist/db/locate.js +3 -1
  82. package/dist/db/locate.js.map +1 -1
  83. package/dist/db/schema.d.ts +1 -1
  84. package/dist/db/schema.js +9 -0
  85. package/dist/db/schema.js.map +1 -1
  86. package/dist/deputy/bridge-worker.d.ts +1 -0
  87. package/dist/deputy/bridge-worker.js +79 -0
  88. package/dist/deputy/bridge-worker.js.map +1 -0
  89. package/dist/deputy/bridge.d.ts +14 -0
  90. package/dist/deputy/bridge.js +70 -0
  91. package/dist/deputy/bridge.js.map +1 -0
  92. package/dist/deputy/client.d.ts +19 -0
  93. package/dist/deputy/client.js +104 -0
  94. package/dist/deputy/client.js.map +1 -0
  95. package/dist/deputy/db-facade.d.ts +18 -0
  96. package/dist/deputy/db-facade.js +71 -0
  97. package/dist/deputy/db-facade.js.map +1 -0
  98. package/dist/deputy/files.d.ts +1 -0
  99. package/dist/deputy/files.js +16 -0
  100. package/dist/deputy/files.js.map +1 -0
  101. package/dist/deputy/install.d.ts +319 -0
  102. package/dist/deputy/install.js +1239 -0
  103. package/dist/deputy/install.js.map +1 -0
  104. package/dist/deputy/notice.d.ts +6 -0
  105. package/dist/deputy/notice.js +28 -0
  106. package/dist/deputy/notice.js.map +1 -0
  107. package/dist/deputy/notices.d.ts +31 -0
  108. package/dist/deputy/notices.js +94 -0
  109. package/dist/deputy/notices.js.map +1 -0
  110. package/dist/deputy/osa.d.ts +21 -0
  111. package/dist/deputy/osa.js +72 -0
  112. package/dist/deputy/osa.js.map +1 -0
  113. package/dist/deputy/protocol.d.ts +140 -0
  114. package/dist/deputy/protocol.js +143 -0
  115. package/dist/deputy/protocol.js.map +1 -0
  116. package/dist/deputy/routing.d.ts +84 -0
  117. package/dist/deputy/routing.js +422 -0
  118. package/dist/deputy/routing.js.map +1 -0
  119. package/dist/deputy/shortcuts-exec.d.ts +10 -0
  120. package/dist/deputy/shortcuts-exec.js +50 -0
  121. package/dist/deputy/shortcuts-exec.js.map +1 -0
  122. package/dist/diagnose.d.ts +57 -2
  123. package/dist/diagnose.js +105 -3
  124. package/dist/diagnose.js.map +1 -1
  125. package/dist/direct-setup.d.ts +78 -0
  126. package/dist/direct-setup.js +484 -0
  127. package/dist/direct-setup.js.map +1 -0
  128. package/dist/host-access.d.ts +63 -0
  129. package/dist/host-access.js +165 -0
  130. package/dist/host-access.js.map +1 -0
  131. package/dist/index.d.ts +31 -5
  132. package/dist/index.js +23 -1
  133. package/dist/index.js.map +1 -1
  134. package/dist/mcp/server.d.ts +27 -1
  135. package/dist/mcp/server.js +300 -97
  136. package/dist/mcp/server.js.map +1 -1
  137. package/dist/model/mappers.d.ts +12 -3
  138. package/dist/model/mappers.js +6 -1
  139. package/dist/model/mappers.js.map +1 -1
  140. package/dist/model/recurrence.d.ts +5 -1
  141. package/dist/model/recurrence.js +19 -1
  142. package/dist/model/recurrence.js.map +1 -1
  143. package/dist/model/template-projection.d.ts +26 -0
  144. package/dist/model/template-projection.js +113 -0
  145. package/dist/model/template-projection.js.map +1 -0
  146. package/dist/op-result.d.ts +55 -0
  147. package/dist/op-result.js +161 -0
  148. package/dist/op-result.js.map +1 -0
  149. package/dist/read/detail.js +7 -1
  150. package/dist/read/detail.js.map +1 -1
  151. package/dist/read/filter-contract.d.ts +1 -1
  152. package/dist/read/filter-contract.js +4 -0
  153. package/dist/read/filter-contract.js.map +1 -1
  154. package/dist/read/queries.js +8 -1
  155. package/dist/read/queries.js.map +1 -1
  156. package/dist/read/shape.js +4 -0
  157. package/dist/read/shape.js.map +1 -1
  158. package/dist/read/views.d.ts +35 -4
  159. package/dist/read/views.js +120 -18
  160. package/dist/read/views.js.map +1 -1
  161. package/dist/session-grant.d.ts +48 -0
  162. package/dist/session-grant.js +170 -0
  163. package/dist/session-grant.js.map +1 -0
  164. package/dist/surface-copy.d.ts +8 -0
  165. package/dist/surface-copy.js +8 -0
  166. package/dist/surface-copy.js.map +1 -1
  167. package/dist/sync-health.js +6 -2
  168. package/dist/sync-health.js.map +1 -1
  169. package/dist/wizard.d.ts +58 -0
  170. package/dist/wizard.js +167 -0
  171. package/dist/wizard.js.map +1 -0
  172. package/dist/write/accessibility-probe.js +4 -2
  173. package/dist/write/accessibility-probe.js.map +1 -1
  174. package/dist/write/automation-probe.js +5 -1
  175. package/dist/write/automation-probe.js.map +1 -1
  176. package/dist/write/availability.d.ts +1 -1
  177. package/dist/write/availability.js +10 -6
  178. package/dist/write/availability.js.map +1 -1
  179. package/dist/write/batch.d.ts +20 -0
  180. package/dist/write/batch.js +94 -15
  181. package/dist/write/batch.js.map +1 -1
  182. package/dist/write/capabilities.d.ts +9 -0
  183. package/dist/write/capabilities.js +2 -0
  184. package/dist/write/capabilities.js.map +1 -1
  185. package/dist/write/clear-reminder.js +1 -1
  186. package/dist/write/clear-reminder.js.map +1 -1
  187. package/dist/write/commands.d.ts +12 -1
  188. package/dist/write/commands.js +170 -237
  189. package/dist/write/commands.js.map +1 -1
  190. package/dist/write/experimental.d.ts +19 -0
  191. package/dist/write/experimental.js +50 -0
  192. package/dist/write/experimental.js.map +1 -1
  193. package/dist/write/failure-hints.d.ts +3 -1
  194. package/dist/write/failure-hints.js.map +1 -1
  195. package/dist/write/guards.js +19 -2
  196. package/dist/write/guards.js.map +1 -1
  197. package/dist/write/lock.d.ts +16 -13
  198. package/dist/write/lock.js +55 -0
  199. package/dist/write/lock.js.map +1 -1
  200. package/dist/write/make-repeating-project.js +9 -13
  201. package/dist/write/make-repeating-project.js.map +1 -1
  202. package/dist/write/move.js +14 -6
  203. package/dist/write/move.js.map +1 -1
  204. package/dist/write/operations.d.ts +64 -25
  205. package/dist/write/operations.js +7 -0
  206. package/dist/write/operations.js.map +1 -1
  207. package/dist/write/opid.d.ts +14 -0
  208. package/dist/write/opid.js +22 -0
  209. package/dist/write/opid.js.map +1 -1
  210. package/dist/write/param-schema.d.ts +124 -0
  211. package/dist/write/param-schema.js +747 -0
  212. package/dist/write/param-schema.js.map +1 -0
  213. package/dist/write/pipeline.d.ts +78 -2
  214. package/dist/write/pipeline.js +249 -4
  215. package/dist/write/pipeline.js.map +1 -1
  216. package/dist/write/pre-state.d.ts +8 -0
  217. package/dist/write/pre-state.js +62 -17
  218. package/dist/write/pre-state.js.map +1 -1
  219. package/dist/write/promote-clone.js +635 -367
  220. package/dist/write/promote-clone.js.map +1 -1
  221. package/dist/write/reorder.js +95 -29
  222. package/dist/write/reorder.js.map +1 -1
  223. package/dist/write/repeat-anchor.d.ts +30 -0
  224. package/dist/write/repeat-anchor.js +166 -1
  225. package/dist/write/repeat-anchor.js.map +1 -1
  226. package/dist/write/repeat-rule.d.ts +28 -1
  227. package/dist/write/repeat-rule.js +64 -4
  228. package/dist/write/repeat-rule.js.map +1 -1
  229. package/dist/write/resolution-timestamps.d.ts +11 -0
  230. package/dist/write/resolution-timestamps.js +83 -38
  231. package/dist/write/resolution-timestamps.js.map +1 -1
  232. package/dist/write/reversibility.js +4 -0
  233. package/dist/write/reversibility.js.map +1 -1
  234. package/dist/write/template-mutation.d.ts +137 -0
  235. package/dist/write/template-mutation.js +451 -0
  236. package/dist/write/template-mutation.js.map +1 -0
  237. package/dist/write/undo.js +10 -20
  238. package/dist/write/undo.js.map +1 -1
  239. package/dist/write/update-fields.d.ts +151 -0
  240. package/dist/write/update-fields.js +427 -0
  241. package/dist/write/update-fields.js.map +1 -0
  242. package/dist/write/vectors/applescript.js +10 -16
  243. package/dist/write/vectors/applescript.js.map +1 -1
  244. package/dist/write/vectors/registry.d.ts +1 -1
  245. package/dist/write/vectors/registry.js +10 -2
  246. package/dist/write/vectors/registry.js.map +1 -1
  247. package/dist/write/vectors/session-reachability.d.ts +55 -0
  248. package/dist/write/vectors/session-reachability.js +56 -8
  249. package/dist/write/vectors/session-reachability.js.map +1 -1
  250. package/dist/write/vectors/shortcuts.js +7 -16
  251. package/dist/write/vectors/shortcuts.js.map +1 -1
  252. package/dist/write/vectors/simulator.d.ts +16 -1
  253. package/dist/write/vectors/simulator.js +68 -12
  254. package/dist/write/vectors/simulator.js.map +1 -1
  255. package/dist/write/vectors/types.d.ts +124 -1
  256. package/dist/write/vectors/ui-recipes.d.ts +19 -0
  257. package/dist/write/vectors/ui-recipes.js +158 -38
  258. package/dist/write/vectors/ui-recipes.js.map +1 -1
  259. package/dist/write/vectors/ui.d.ts +89 -0
  260. package/dist/write/vectors/ui.js +504 -49
  261. package/dist/write/vectors/ui.js.map +1 -1
  262. package/dist/write/verify/delta.d.ts +21 -0
  263. package/dist/write/verify/delta.js.map +1 -1
  264. package/dist/write/verify/poller.d.ts +13 -0
  265. package/dist/write/verify/poller.js +38 -0
  266. package/dist/write/verify/poller.js.map +1 -1
  267. package/package.json +9 -1
  268. package/schema/envelope.schema.json +5 -3
  269. package/scripts/build-helpers.sh +109 -0
  270. package/scripts/deputy-cert-setup.sh +60 -0
  271. package/skills/things-cli/SKILL.md +12 -5
  272. package/skills/things-cli/references/contracts.md +1 -0
  273. package/skills/things-cli/references/data-model.md +3 -0
  274. package/skills/things-cli/references/errors.md +5 -1
  275. package/skills/things-cli/references/gui.md +1 -0
  276. package/skills/things-cli/references/repeating.md +75 -0
  277. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -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.17.0
4
+ version: 0.19.0
5
5
  ---
6
6
 
7
7
  # Things CLI
@@ -10,7 +10,7 @@ version: 0.17.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
 
@@ -20,6 +20,7 @@ version: 0.17.0
20
20
  - **Area** — a top-level bucket (e.g. a sphere of responsibility) holding projects and loose to-dos. Areas have tags but no dates, and never go to the Trash — deleting one is permanent.
21
21
  - **Tags** — form a hierarchy, and are **inherited downward**: a to-do effectively carries its own tags plus those of its project and area. Headings carry no tags, but inheritance flows through them from project to to-do.
22
22
  - **Views** are queries over this model, matching the app's sidebar: `inbox` (unsorted), `today` (scheduled for today, incl. This Evening), `upcoming` (future-dated), `anytime` (all active), `someday` (kept without a date), `logbook` (completed/canceled), `trash`.
23
+ - **Notes** — a *markdown field with multi-line support*, not a plain text blob. Newlines are kept exactly as written, and `**bold**`, `_italic_`, `` `code` ``, fenced code blocks, `#` headings, `-` bullets, and numbered lists all render (their syntax characters stay visible alongside the formatting). A bare URL becomes a link by itself, so never wrap one in `[label](url)` — that spelling stays literal text. Also literal, not rendered: `> blockquotes`, `---` rules, `*` bullets, and `- [ ]`/`- [x]` checkboxes (a real checklist is a separate feature — `things todo checklist`). To pass several lines from a shell, `--notes $'first line\nsecond line'`; to pipe a whole body in (heredoc, `< notes.md`), `--notes -`.
23
24
  - **Scheduling vocabulary**: an item's *when* is `today | evening | anytime | someday | YYYY-MM-DD`. A **deadline** is a separate due date; a **reminder** is a separate time-of-day alert — never write `date@time` into *when*.
24
25
 
25
26
  ## Reading position from JSON: `stage` and `when`
@@ -45,14 +46,14 @@ These hold regardless of the binary version; see [references/contracts.md](refer
45
46
  - **JSON envelope**: every `--json` response is `{ apiVersion, ok, kind, data, meta }`. Read results from `.data` (`.data.items`/`.data.sections`/`.data.item`/`.data.view`/`.data.children` per `kind`; `today` is `.data.children.{today,evening}.items` with its count on `meta.counts`; the `logbook` view carries the log-move cadence on `meta.logging = {cadence, lastLoggedAt?}` — CC's Settings words `Immediately|Daily|Manually`, `lastLoggedAt` under Daily and Manually), never `.items`; UUIDs are `.uuid`, not `.id`. Check `meta.truncation.truncated` before concluding "no match" or "that's everything". List/search rows are compact summaries whose `tags` may be incomplete — use `things show <ref> --json` for effective tags, checklist, notes, and placement.
46
47
  - **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
48
  - **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
- - **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
+ - **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` — the worked recipe is [references/tag-cleanup.md](references/tag-cleanup.md). `things batch` takes it too: at the RUN level it defaults every line, and a line's own `"options": {"preserveModified": …}` outranks that. 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
50
  - **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.
50
51
  - **Preconditions**: referenced containers and tags must already exist — create nested structures outside-in and reuse each returned UUID.
51
52
  - If the user requests a JSON reply schema, return exactly that object after the read or checked write.
52
53
 
53
54
  ## Reading
54
55
 
55
- Views and lookups — pass `--json` whenever you will act on the output: `things today | inbox | upcoming | anytime | someday | logbook | trash`, `things show <ref>` (full detail incl. notes + checklist + effective tags), `things projects [ref]`, `things areas [ref]`, `things tags`, `things search <words>`, `things changes --since <moment>`. `things deadlines` is the deadline horizon — every item (to-dos AND projects) that HAS a deadline, in deadline order, most-overdue first; it is a derived query view, NOT reorderable, and repeating items appear at their next occurrence's projected deadline. Scope it with `--today` (only what's due in Today), `--overdue` (only items already past their deadline), and `--project`/`--area`/`--tag`. Compact rows carry `hasNotes`, `checklist:{open,total}` (to-dos), and `todos:{open,total}` (projects); `--full` (or `show`) adds the full `notes`, `startDate`, and checklist `items`. A `search` hit carries `match:{field,text}` provenance when it matched something other than the title. Filters (`--tag`, `--untagged`, `--overdue`, `--limit N`, `--all`, …) compose with AND — see `things help filters`. The reserved read-only ref `loose` addresses the area-less items as a pseudo-area (`areas loose`, `area show loose`, `--area loose`) and wins over any real area named "Loose"; every write verb refuses it.
56
+ Views and lookups — pass `--json` whenever you will act on the output: `things today | inbox | upcoming | anytime | someday | logbook | trash`, `things show <ref>` (full detail incl. notes + checklist + effective tags), `things projects [ref]`, `things areas [ref]`, `things tags`, `things search <words>`, `things changes --since <moment>`. `things deadlines` is the deadline horizon — every item (to-dos AND projects) that HAS a deadline, in deadline order, most-overdue first; it is a derived query view, NOT reorderable, and repeating items appear at their next occurrence's projected deadline. Scope it with `--today` (only what's due in Today), `--overdue` (only items already past their deadline), and `--project`/`--area`/`--tag`. `things repeaters` is the repeating-series catalogue — every repeating to-do AND project in the library, each row carrying its decoded rule under `repeating.rule`, ordered by next occurrence (paused, ended, and after-completion series are included and come last, since they project no date). It is the ONLY view that lists the series themselves: every other view shows the occurrences a series spawns, and search matches those occurrences but never the series behind them — so this is where a series' uuid and rule come from before you `reschedule-repeat`, `pause-repeat`, or `show` it. Compact rows carry `hasNotes`, `checklist:{open,total}` (to-dos), and `todos:{open,total}` (projects); `--full` (or `show`) adds the full `notes`, `startDate`, and checklist `items`. A `search` hit carries `match:{field,text}` provenance when it matched something other than the title. Filters (`--tag`, `--untagged`, `--overdue`, `--limit N`, `--all`, …) compose with AND — see `things help filters`. The reserved read-only ref `loose` addresses the area-less items as a pseudo-area (`areas loose`, `area show loose`, `--area loose`) and wins over any real area named "Loose"; every write verb refuses it.
56
57
 
57
58
  ## Writing
58
59
 
@@ -66,15 +67,21 @@ Namespaced verb families — run `things <group> --help` for the verbs and `thin
66
67
 
67
68
  **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
 
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. **Do NOT fire an identical retry** — repeated blind retries after a timeout 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.
70
+ **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.
70
71
 
71
72
  **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`.
72
73
 
74
+ ## The CLI is the only interface — never route around it
75
+
76
+ 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.
77
+
73
78
  ## Going deeper
74
79
 
75
80
  - [references/data-model.md](references/data-model.md) — the full data model, `stage`/`when` derivation, the compact/full tiers, view membership, and filters.
76
81
  - [references/contracts.md](references/contracts.md) — the JSON envelope, exit codes, safety/undo/idempotency, batch chaining, and recurrence.
82
+ - [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.
77
83
  - [references/ordering.md](references/ordering.md) — move vs reorder in depth: axes, gates, caps, automatic fallbacks, placement guarantees, and the one dead class (templates).
78
84
  - [references/errors.md](references/errors.md) — the error contract: the candidate shape, dead-row hints, hazard acknowledgments, and the error-code registry.
79
85
  - [references/banner.md](references/banner.md) — the Today "new to-dos" banner, the provisional `•` pip, reminder/evening liveness, and what a watcher sees.
80
86
  - [references/gui.md](references/gui.md) — how the user sees Things in the app (where results appear, what list rows show).
87
+ - [references/tag-cleanup.md](references/tag-cleanup.md) — the mass tag-cleanup recipe: which moves are free on the modification-date timeline (rename, delete) and which need `--preserve-modified` (applying a tag), plus retiring an archived area.
@@ -57,6 +57,7 @@ A verified write is committed to the LOCAL Things database immediately — `ok`/
57
57
  - **Stop-on-failure is the DEFAULT:** a runtime per-line failure HALTS the batch — the failed line is reported and every later line is reported not-run. Pass `--continue-on-error` (MCP `continue_on_error`) to run past failures instead (the old proceed-past behavior). The summary states how many lines did not run and whether a verbatim resubmission is safe to resume.
58
58
  - **`tempId` (chaining):** a line that CREATES something (a to-do, project, area, heading — never `tag.add`, and NOT `make-repeating`/`add-repeating`) can carry `"tempId":"proj1"`; a LATER line references that new uuid as `"$proj1"` in any id/container field. This is how you "create a project, then file to-dos into it" in one submission without knowing the uuid up front. Handles are `[A-Za-z0-9_-]{1,32}` and unique per batch.
59
59
  - **`opId` (safe retry + RESUME):** carry a stable `"opId"` per line so resubmitting a batch after a failure does not double-create — a line matching an earlier success is reported `already-applied`, not re-run. **Put an `opId` on EVERY line** so a batch that stopped mid-way can be resubmitted VERBATIM to resume: already-applied lines are skipped and the run continues from the failure. Without opIds, a verbatim resubmission RE-RUNS the lines that already committed (the summary's resume guidance names those indices). The single-op analogue is `--op-id <key>` (MCP `op_id`) on ONE mutation: a resubmission with a matched key returns the original success (`alreadyApplied: true`, the original `uuid`/`undoToken`) instead of running again. The variadic `move`/`reorder` are multi-leg compounds and REFUSE `--op-id` — express their idempotency as `things batch` with a per-line `opId`.
60
+ - **`preserveModified` (timeline-silent bulk):** the universal write flag reaches a batch in two shapes — the run-level `--preserve-modified` (MCP `preserve_modified`), which applies to EVERY line, and a per-line `"options": {"preserveModified": true|false}`, whose explicit value outranks the run level (so `false` opts one line back onto the timeline). Each line's result then carries `preservedModified` (and `preserveFailures` for a restore that did not land), exactly as the per-item verb does. This is the vehicle for a mass re-tag that must not flood `changes` — see the tag-cleanup recipe.
60
61
  - **Undo the whole batch:** the trailing summary line returns `tempIdMapping` (handle → uuid) and `undoToken`; `things undo --txn <undoToken>` reverses the entire submission as one unit.
61
62
 
62
63
  ## Recurrence (contract summary)
@@ -22,6 +22,7 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
22
22
  - **Status**: open → completed or canceled (both land in the Logbook) or trashed (Trash; restorable until emptied). Reopen brings a logged item back.
23
23
  - **when** (`today | evening | anytime | someday | YYYY-MM-DD`) controls which view an item appears in; **deadline** is an independent due date shown alongside the item; **reminder** is a time-of-day alert attached to a dated when.
24
24
  - "Overdue" = open with a deadline strictly before today (a deadline of today is "due", not overdue).
25
+ - **notes is a markdown field with multi-line support**, on both to-dos and projects — never treat it as a flat text blob. Newlines survive verbatim: give the body inline with `--notes $'first line\nsecond line'`, or pass `--notes -` to read the whole body from stdin (a heredoc, a pipe, or `< notes.md`; one trailing newline is dropped, and a terminal stdin is refused rather than left hanging). Things renders the formatting while KEEPING the syntax characters visible, so the stored text and the rendered card carry the same characters: `#`/`##` headings, `_italic_`, `*italic*`, `**bold**`, `__bold__`, `` `code` ``, ```` ``` ```` fenced blocks, `-` bullets (nesting preserved), and `1.` numbered lists all render. A bare URL — with or without `<angle brackets>` — becomes a clickable link on its own, which makes `[label](url)` pure noise: the label and brackets stay literal and only the raw URL inside links. Inert, stored and shown as literal characters: `> blockquote`, the `---` rule, `*` used as a bullet, and `- [ ]`/`- [x]` checkboxes (`- [x]` lines are additionally drawn dimmed, which is cosmetic only — nothing is checked off). Real sub-steps are the checklist (`things todo checklist`), a separate structure from the notes body.
25
26
 
26
27
  ## Views (queries over the model)
27
28
 
@@ -37,6 +38,8 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
37
38
 
38
39
  Beyond the sidebar mirror, **`things deadlines`** is a derived query view (not an app sidebar list): one flat `data.items[]` of every LIVE item — to-dos AND projects — that carries a deadline, ordered `deadline` ASC (most-overdue first), tie-broken by Today order then uuid. Its `stage` is KEPT (stage-mixed, like `search`/`changes`). It is presentation-ordered and **NON-SCOPE** — deadline order IS the view, so it is not reorderable (`reorder --in deadlines` is refused). A dismissed deadline nag does NOT hide a row here (dismissal only affects the Today pull, not the deadline). Deadline-bearing repeating items are PROJECTED at their next occurrence's deadline (the same projection the `upcoming` view uses); a deadline-less or unprojectable template is left out. Filters: `--today` (only current Today members, This-Evening included), `--overdue` (only open items already past today — never projections), `--project`/`--area`/`--tag`, `--limit`/`--all`. MCP: `read_view deadlines` with `today`/`overdue`/`project`/`area`/`tag` params.
39
40
 
41
+ **`things repeaters`** is the other derived query view: one flat `data.items[]` of every LIVE repeating TEMPLATE — to-do and project alike — each row carrying its decoded rule under `repeating.rule` (a shape no other list row has; ordinarily the rule rides a detail read only). It exists because templates are invisible everywhere else — every other view lists the OCCURRENCES a series spawns, `search` matches those occurrences but never the series behind them, and the app hides templates too — so a series' uuid, the handle every repeat write needs, had nowhere to come from. Paused, ended, and after-completion series are INCLUDED (a catalogue that hid them would hide exactly what a reader is asking about); each row's chip states which. Ordered by next occurrence ASC, the ones that project nowhere last (title, then uuid). Filters: `--tag`/`--exact-tag`/`--untagged`, `--limit`/`--all`; `--overdue` does not apply (a template's own deadline column is an app sentinel, never a real due date). MCP: `read_view repeaters` with the `tag` params.
42
+
40
43
  `things projects`/`areas`/`tags` list containers; `things projects <ref>` / `things areas <ref>` / `things show <ref>` show one item's full detail — notes, checklist, effective tags — which the compact list rows do NOT display.
41
44
 
42
45
  ### Reading view membership from JSON — `stage` and `when`
@@ -31,7 +31,7 @@ Some writes have a cascading or permanent effect. Each is refused BEFORE touchin
31
31
 
32
32
  - **Delete a NON-EMPTY area** → `--allow-non-empty`. Deleting an area sends its to-dos AND its projects (with their children) to the Trash and destroys the area row permanently. A delete pre-counts live members and refuses with the counts ("the area is not empty — it contains 3 projects and 12 to-dos"), naming both remediations: empty the area first, or pass `--allow-non-empty`. An empty area is unaffected (still just `--dangerously-permanent`). Code: `blocked:H-AREA-NOT-EMPTY`.
33
33
  - **Permanent delete** (an area; emptying the Trash) → `--dangerously-permanent`. There is no inverse; `undo` names the manual one rather than pretending. Code: `blocked:H-PERMANENT-DELETE`.
34
- - **Drive the live UI** (the ops the app offers nowhere else — e.g. `area reorder`, and some heading/repeat ops) → the two-key gate: the `--dangerously-drive-gui` flag AND `things config set ui-enabled true` (plus Accessibility granted to the process). These visibly drive the Things window. Code: `blocked:H-UI-DRIVE`. (Separately, the `--allow-disruptive` / `--allow-very-disruptive` flags raise the process's disruption ceiling for any op that steals focus or drives the UI.)
34
+ - **Drive the live UI** (the ops the app offers nowhere else — e.g. `area reorder`, and some heading/repeat ops) → the two-key gate: the `--dangerously-drive-gui` flag AND `things config set ui-enabled true`, plus the GUI-driving permissions, which are granted to the helper pair alone via `things helpers setup --gui` (never to the terminal). These visibly drive the Things window. Code: `blocked:H-UI-DRIVE` for the missing flag, `blocked:environment` for the missing permissions — the latter names exactly which grant is absent. (Separately, the `--allow-disruptive` / `--allow-very-disruptive` flags raise the process's disruption ceiling for any op that steals focus or drives the UI.)
35
35
  - **Reopen or reuse a resolved project** — moving or adding an OPEN child into a completed/canceled project reopens it via the app; that is acknowledged (`blocked:H-REOPEN-RESOLVED-PROJECT`) so it is never a silent side effect.
36
36
  - Other guards name their own consequence the same way (heading cascades, backdating an open item, checklist replacement, repeat scheduling, tag-subtree delete, …).
37
37
 
@@ -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.
@@ -7,5 +7,6 @@ Curated rendering facts — the bench loop may compress or relocate these but mu
7
7
  - **Sidebar**: top-level views (Inbox, Today, Upcoming, Anytime, Someday, Logbook, Trash), then each **area** with its active **projects** nested beneath it — in the same order this CLI/API reports them. Reordering areas or projects changes what the user sees there.
8
8
  - **List rows are compact**: a to-do in any list shows its checkbox/status, title, tags, deadline, and small marker chips — the Today ★, This Evening ⏾, and (in the Today view only) the provisional `•` "new item" pip, plus has-notes / reminder / checklist glyphs (`things legend` names them all). The **notes TEXT and checklist CONTENTS are invisible until the user opens the item**. Put must-see-at-a-glance information in the title; supporting detail belongs in notes. Projects carry the same Today ★ / This Evening ⏾ pips as to-dos. Why some chips vanish once a row's date goes stale — the reminder bell, the This-Evening section, and the provisional pip — is in [banner.md](banner.md).
9
9
  - **Project notes** are likewise visible only when the project itself is opened in project view — in lists, a project is just its title (and progress ring).
10
+ - **An open card renders the notes as markdown, with the syntax characters left in place** — headings, bold/italic, code spans, fenced blocks, and `-`/numbered lists take on their formatting while the user still sees the `#`, `**`, backticks, and dashes that produced it. Write notes the user will read as source AND as formatting: no huge heading stacks, and no `[label](url)` (a bare URL links itself; the label form stays literal). See [data-model.md](data-model.md) for the full construct list.
10
11
  - **Today** shows the day's scheduled items, with **This Evening** as a separate section beneath. **Upcoming** is a forward-looking date-ordered calendar of scheduled items. **Logbook** is where completed/canceled items go — after completing something for the user, that's where they'll find it.
11
12
  - When telling the user where something landed, name the container and view ("in project P under area A"; "it'll show in Today this evening").
@@ -0,0 +1,75 @@
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
+ **Telling the two halves apart in output.** On a TTY both wear the `↻` glyph, in different slots: a TEMPLATE keeps an ordinary checkbox and carries a **blue `↻` in front of its title** (`[ ] ↻ Water plants ‹Jul 8›`, `( ) ↻ Weekly review`); an INSTANCE is an ordinary checkable row with a **muted `↻` after the title**, in the marker cluster beside `≡`/`◷`/`≔` (`[ ] Water plants ↻ ≡`). In `--json` the same split is presence-keyed: a template carries a `repeating` object, an instance carries a flat `instanceOf: <templateUuid>` (the template's uuid — pass it to the repeat verbs), and a plain row carries neither. `things legend` lists both marks.
6
+
7
+ ## Finding a series in the first place
8
+
9
+ `things repeaters` lists every repeating series in the library — to-dos and projects — with its uuid, its next occurrence, its container, and its decoded rule. Reach for it before any repeat write, because a template is invisible to every other read: the list views show the OCCURRENCES a series spawns rather than the series itself, and `search` matches those occurrences but never the template behind them. Paused and ended series are listed too (each row says which), which is usually the fastest answer to "why has this stopped appearing?". `--json` carries the rule structurally under `repeating.rule`; the uuid it gives you is what `reschedule-repeat`, `pause-repeat`, `resume-repeat`, and `show` all take.
10
+
11
+ ## The verbs
12
+
13
+ - `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).
14
+ - `things todo add-repeating "<title>" --frequency … --interval …` — create a new to-do already repeating, in one step.
15
+ - `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.
16
+ - `things todo pause-repeat <ref>` / `resume-repeat <ref>` — suspend/restart spawning without losing the rule.
17
+ - `things project …` mirrors all of the above for repeating projects.
18
+
19
+ 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.
20
+
21
+ ## The scheduling vocabulary
22
+
23
+ - **`--frequency`** `daily | weekly | monthly | yearly`, **`--interval N`** = every N of those units.
24
+ - **`--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.
25
+ - **The calendar anchor** — WHAT day the series recurs on, per frequency:
26
+ - weekly → `--weekdays monday,wednesday,friday` (one or more).
27
+ - monthly → `--on-day <1–31 | last>`, OR an nth-weekday with `--on-weekday <weekday> --on-ordinal <1–5 | last>`.
28
+ - yearly → `--yearly-month <1–12>` plus `--on-day` (or the nth-weekday pair).
29
+ - **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.
30
+
31
+ ## Off-schedule first occurrence (a first-class pattern)
32
+
33
+ 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`.
34
+
35
+ **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`.
36
+
37
+ ## The deadline law (read this before combining `--deadline` with an anchor)
38
+
39
+ `--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.
40
+
41
+ Worked example — intent "appears October 16, due October 30" (a 14-day lead):
42
+
43
+ - 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.
44
+ - Explicit anchor equivalent: name the DUE date — `--yearly-month 10 --on-day 30 --when 2028-10-16 --deadline --start-days-earlier 14`.
45
+
46
+ 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`).
47
+
48
+ ### Repeating deadlines on `add-repeating`: two spellings for one geometry
49
+
50
+ 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:
51
+
52
+ - **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.
53
+ - **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").
54
+
55
+ 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.
56
+
57
+ 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).)
58
+
59
+ ## Ends bounds, reminder, and limits worth knowing
60
+
61
+ - **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`.
62
+ - **Reminder**: `--reminder HH:mm` sets a time-of-day alert on each occurrence.
63
+ - **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.
64
+ - **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.
65
+
66
+ ## Operational rules for the GUI-driven repeat commands
67
+
68
+ 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.)
69
+
70
+ - 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.
71
+ - **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.
72
+ - **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.)
73
+ - **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.
74
+ - 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.
75
+ - A genuine success always returns a structured JSON result (with `--json`) naming the new/updated uuid.
@@ -0,0 +1,64 @@
1
+ # Mass tag cleanup — reshaping a tag vocabulary without flooding the timeline
2
+
3
+ A tag cleanup ("we have 40 tags, six of them mean the same thing") touches hundreds of items, and the naive version re-dates every one of them: `things changes --since <moment>` — and any watcher or sync-driven review keyed on the modification date — then reports the whole library as freshly changed, burying whatever really happened that day.
4
+
5
+ It does not have to. **Two of the three moves cost nothing on the timeline, and the third has a flag.** Work in that order.
6
+
7
+ ## The three moves, cheapest first
8
+
9
+ | Move | Command | Timeline cost |
10
+ | --- | --- | --- |
11
+ | **Rename a tag** (merge a synonym into the survivor's name, fix casing, re-word) | `things tag update <ref> --title "<new>"` | **none** — the assignment stores the tag's identity, not its text, so no member item is touched. Existing assignments follow the rename. |
12
+ | **Delete a tag** (retire it everywhere, including its whole nested subtree) | `things tag delete <ref> --dangerously-permanent [--acknowledge-subtree]` | **none** — the tag is removed from every item without re-dating any of them. Permanent: tags do not go to the Trash. |
13
+ | **Apply or remove a tag on items** (`--set` replaces the tag set, `--add` merges) | `things todo tags <uuid> --add "<tag>"` / `things project tags <ref> --set "<a>,<b>"` | **one re-date per item** — this is the only move that lands on the timeline, and the only one that wants `--preserve-modified`. |
14
+
15
+ So: **reshape the vocabulary first, re-tag items last.** A rename that turns `errands`, `Errand`, and `errand-run` into one surviving tag costs nothing; every item you can leave alone by renaming instead of re-tagging is an item that never reaches the timeline.
16
+
17
+ ## The recipe
18
+
19
+ 1. **Look before you write.** `things tags` lists the hierarchy; `things anytime --tag <name> --all --json` (or `things search`) enumerates what a tag actually holds. Decide which tag survives.
20
+ 2. **Rename the survivor into the name you want** (`tag update`), rather than creating a new tag and re-tagging into it. Free.
21
+ 3. **Fold the synonyms.** For each doomed tag, the items that carry it and NOT the survivor need the survivor added — this is the only pass that re-dates anything, so run it with `--preserve-modified`. One item at a time:
22
+ ```sh
23
+ things todo tags <uuid> --add "errand" --preserve-modified
24
+ ```
25
+ For hundreds of them, `things batch` is the vehicle: it takes `--preserve-modified` at the RUN level, applying it to every line, and the whole submission comes back under one `undoToken`. The batch op is `todo.set-tags`, which REPLACES the tag set (there is no per-line merge — `--add` is CLI sugar over a read), so build each line's full set from the item's current tags:
26
+ ```sh
27
+ # anytime is a SECTIONED view (data.sections[].items[]); item tags are
28
+ # {title} objects, and todo.set-tags wants a flat list of titles.
29
+ things anytime --tag "Errand" --all --json \
30
+ | jq -c '.data.sections[].items[]
31
+ | select(.type == "to-do")
32
+ | {op: "todo.set-tags",
33
+ params: {uuid, tags: ([(.tags // [])[].title] - ["errand"] + ["errand"])}}' \
34
+ | things batch --preserve-modified
35
+ ```
36
+ (Project rows carry their own tags — filter them out as above and re-tag them with `project.set-tags` in a second pass, or drop the `select` and branch the `op` on `.type`.)
37
+ Each line's result carries `preservedModified` (and `preserveFailures` for any restore that did not land), so you can see the pass stayed silent. A single line can opt back onto the timeline with its own `"options": {"preserveModified": false}` — the per-line value always outranks the run flag. Add an `"opId"` per line if you want a stopped batch to be resubmittable verbatim.
38
+ 4. **Delete the doomed tags** (`tag delete … --dangerously-permanent`, plus `--acknowledge-subtree` when the tag has children — deleting a parent deletes its whole subtree). Free, and it also removes the tag from anything you missed in step 3, so a stray assignment does not survive as a ghost.
39
+ 5. **Check the timeline you were protecting**: `things changes --since <the moment you started>` should show only what you meant to surface.
40
+
41
+ ## `--preserve-modified` in one paragraph
42
+
43
+ It captures each pre-existing edited item's modification date before the write and restores it (to the whole second) afterwards, so the edit does not surface in `changes`. It is universal — every write verb takes it — and a no-op on a pure create. The restore is best-effort: a failure is reported per item and the change itself still stands, so a cleanup never half-applies because a restore missed. It is **safe against a synced library**: the restored date propagates to your other devices and survives the round-trip, so the item stays off the timeline everywhere; the one edge is a genuinely concurrent edit to the same item on another device, which re-dates it through Things Cloud's per-attribute merge — the edit resurfaces rather than being silently hidden (SYNC2B).
44
+
45
+ Full contract: the `--preserve-modified` bullet in [../SKILL.md](../SKILL.md); per-operation detail in `things <verb> --help`.
46
+
47
+ ## The adjacent cleanup: retiring an area
48
+
49
+ The same "make the free move first" logic applies to an `(archived)`-style area you want to dissolve. **Deleting an area treats its members by status:**
50
+
51
+ - an **open** direct member (to-do or project) is moved to the **Trash** and re-dated;
52
+ - a **logged** member (completed or canceled) is merely **detached** — its area link is cleared, it stays live in the Logbook, and it is **not** re-dated.
53
+
54
+ So deleting a long-dead area full of finished work is mostly free: the history stays in the Logbook, unlinked, off the timeline. Only the open remnants move, and those are exactly the ones you should look at first. Deleting an area is **permanent** (areas do not go to the Trash) and a non-empty one is refused unless you pass `--allow-non-empty`:
55
+
56
+ ```sh
57
+ things area delete "Old Client (archived)" --dangerously-permanent --allow-non-empty --preserve-modified
58
+ ```
59
+
60
+ Preview it first — `--dry-run` reports the plan — and if you want the open remnants kept, move them out before deleting rather than restoring them from the Trash afterwards.
61
+
62
+ ## What this rests on
63
+
64
+ The laws above are measured, not assumed: the rename/delete/apply modification-date footprint (TAGMOD), the area-delete status-dependent trash-vs-detach split (TAGMOD-T4, refining AREADEL), and the sync round-trip that makes the restore safe on a real Things Cloud account (SYNC2B). The evidence lives with the project's lab documentation, indexed by those names.