things-api 0.11.0 → 0.13.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 +16 -5
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/canonical-ref.d.ts +21 -0
  5. package/dist/cli/canonical-ref.js +23 -0
  6. package/dist/cli/canonical-ref.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +81 -77
  9. package/dist/cli/commands/area.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +1 -2
  11. package/dist/cli/commands/doctor.js +11 -1
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/install-skill.d.ts +19 -4
  14. package/dist/cli/commands/install-skill.js +130 -52
  15. package/dist/cli/commands/install-skill.js.map +1 -1
  16. package/dist/cli/commands/mcp.js +21 -1
  17. package/dist/cli/commands/mcp.js.map +1 -1
  18. package/dist/cli/commands/project.d.ts +1 -0
  19. package/dist/cli/commands/project.js +92 -48
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +5 -1
  22. package/dist/cli/commands/reads.js +110 -31
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +11 -4
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +75 -36
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +2 -2
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +795 -144
  31. package/dist/cli/commands/writes.js.map +1 -1
  32. package/dist/cli/did-you-mean.d.ts +14 -3
  33. package/dist/cli/did-you-mean.js +44 -7
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/dry-run.d.ts +55 -0
  36. package/dist/cli/dry-run.js +92 -0
  37. package/dist/cli/dry-run.js.map +1 -0
  38. package/dist/cli/glyphs.d.ts +16 -2
  39. package/dist/cli/glyphs.js +60 -12
  40. package/dist/cli/glyphs.js.map +1 -1
  41. package/dist/cli/help.js +69 -13
  42. package/dist/cli/help.js.map +1 -1
  43. package/dist/cli/main.js +8 -1
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/move-hint.d.ts +3 -2
  46. package/dist/cli/move-hint.js +2 -2
  47. package/dist/cli/move-hint.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +53 -5
  49. package/dist/cli/read-driver.js +82 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/ref-render.d.ts +14 -0
  52. package/dist/cli/ref-render.js +9 -0
  53. package/dist/cli/ref-render.js.map +1 -0
  54. package/dist/cli/render.d.ts +40 -14
  55. package/dist/cli/render.js +128 -55
  56. package/dist/cli/render.js.map +1 -1
  57. package/dist/cli/resolve-invocation.d.ts +7 -5
  58. package/dist/cli/resolve-invocation.js +16 -7
  59. package/dist/cli/resolve-invocation.js.map +1 -1
  60. package/dist/cli/skill-check.d.ts +12 -6
  61. package/dist/cli/skill-check.js +31 -14
  62. package/dist/cli/skill-check.js.map +1 -1
  63. package/dist/cli/skill.d.ts +8 -0
  64. package/dist/cli/skill.js +10 -0
  65. package/dist/cli/skill.js.map +1 -1
  66. package/dist/cli/verb-hint.js +1 -1
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/client.d.ts +158 -26
  69. package/dist/client.js +254 -38
  70. package/dist/client.js.map +1 -1
  71. package/dist/config.d.ts +66 -2
  72. package/dist/config.js +120 -12
  73. package/dist/config.js.map +1 -1
  74. package/dist/contracts.d.ts +187 -28
  75. package/dist/contracts.js +22 -1
  76. package/dist/contracts.js.map +1 -1
  77. package/dist/db/baselines/db-v26.js +4 -1
  78. package/dist/db/baselines/db-v26.js.map +1 -1
  79. package/dist/diagnose.d.ts +34 -2
  80. package/dist/diagnose.js +36 -1
  81. package/dist/diagnose.js.map +1 -1
  82. package/dist/index.d.ts +21 -8
  83. package/dist/index.js +26 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp/server.d.ts +9 -0
  86. package/dist/mcp/server.js +790 -322
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/model/entities.d.ts +57 -4
  89. package/dist/model/entities.js.map +1 -1
  90. package/dist/model/mappers.d.ts +2 -0
  91. package/dist/model/mappers.js +44 -2
  92. package/dist/model/mappers.js.map +1 -1
  93. package/dist/model/recurrence.d.ts +8 -1
  94. package/dist/model/recurrence.js.map +1 -1
  95. package/dist/model/serialize.js +15 -3
  96. package/dist/model/serialize.js.map +1 -1
  97. package/dist/read/area-filter.d.ts +76 -0
  98. package/dist/read/area-filter.js +58 -0
  99. package/dist/read/area-filter.js.map +1 -0
  100. package/dist/read/area-view.d.ts +22 -11
  101. package/dist/read/area-view.js +54 -28
  102. package/dist/read/area-view.js.map +1 -1
  103. package/dist/read/detail.js +15 -8
  104. package/dist/read/detail.js.map +1 -1
  105. package/dist/read/predicates.d.ts +18 -0
  106. package/dist/read/predicates.js +19 -0
  107. package/dist/read/predicates.js.map +1 -1
  108. package/dist/read/project-view.d.ts +91 -15
  109. package/dist/read/project-view.js +135 -37
  110. package/dist/read/project-view.js.map +1 -1
  111. package/dist/read/pseudo-area.d.ts +29 -0
  112. package/dist/read/pseudo-area.js +27 -0
  113. package/dist/read/pseudo-area.js.map +1 -0
  114. package/dist/read/queries.d.ts +175 -19
  115. package/dist/read/queries.js +376 -32
  116. package/dist/read/queries.js.map +1 -1
  117. package/dist/read/scope.d.ts +126 -0
  118. package/dist/read/scope.js +161 -0
  119. package/dist/read/scope.js.map +1 -0
  120. package/dist/read/search-rank.d.ts +2 -5
  121. package/dist/read/shape.d.ts +181 -0
  122. package/dist/read/shape.js +737 -0
  123. package/dist/read/shape.js.map +1 -0
  124. package/dist/read/show-target.d.ts +9 -1
  125. package/dist/read/show-target.js +77 -17
  126. package/dist/read/show-target.js.map +1 -1
  127. package/dist/read/stage.d.ts +199 -0
  128. package/dist/read/stage.js +125 -0
  129. package/dist/read/stage.js.map +1 -0
  130. package/dist/read/truncation.d.ts +7 -12
  131. package/dist/read/truncation.js +56 -21
  132. package/dist/read/truncation.js.map +1 -1
  133. package/dist/read/views.d.ts +68 -16
  134. package/dist/read/views.js +206 -52
  135. package/dist/read/views.js.map +1 -1
  136. package/dist/surface-copy.d.ts +16 -0
  137. package/dist/surface-copy.js +16 -0
  138. package/dist/surface-copy.js.map +1 -1
  139. package/dist/write/batch.d.ts +49 -10
  140. package/dist/write/batch.js +423 -71
  141. package/dist/write/batch.js.map +1 -1
  142. package/dist/write/commands.js +268 -55
  143. package/dist/write/commands.js.map +1 -1
  144. package/dist/write/guards.d.ts +1 -1
  145. package/dist/write/guards.js +71 -10
  146. package/dist/write/guards.js.map +1 -1
  147. package/dist/write/heading.d.ts +10 -1
  148. package/dist/write/heading.js +35 -5
  149. package/dist/write/heading.js.map +1 -1
  150. package/dist/write/make-repeating-project.d.ts +2 -2
  151. package/dist/write/make-repeating-project.js +9 -9
  152. package/dist/write/make-repeating-project.js.map +1 -1
  153. package/dist/write/move.d.ts +134 -0
  154. package/dist/write/move.js +1874 -0
  155. package/dist/write/move.js.map +1 -0
  156. package/dist/write/operations.d.ts +105 -19
  157. package/dist/write/operations.js +56 -7
  158. package/dist/write/operations.js.map +1 -1
  159. package/dist/write/opid.d.ts +31 -0
  160. package/dist/write/opid.js +30 -0
  161. package/dist/write/opid.js.map +1 -0
  162. package/dist/write/pipeline.d.ts +70 -3
  163. package/dist/write/pipeline.js +198 -33
  164. package/dist/write/pipeline.js.map +1 -1
  165. package/dist/write/pre-state.d.ts +172 -8
  166. package/dist/write/pre-state.js +467 -25
  167. package/dist/write/pre-state.js.map +1 -1
  168. package/dist/write/reorder.d.ts +23 -3
  169. package/dist/write/reorder.js +2106 -86
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/reversibility.js +19 -7
  172. package/dist/write/reversibility.js.map +1 -1
  173. package/dist/write/scope-guard.d.ts +31 -0
  174. package/dist/write/scope-guard.js +162 -0
  175. package/dist/write/scope-guard.js.map +1 -0
  176. package/dist/write/undo.js +120 -13
  177. package/dist/write/undo.js.map +1 -1
  178. package/dist/write/vectors/applescript.js +16 -7
  179. package/dist/write/vectors/applescript.js.map +1 -1
  180. package/dist/write/vectors/shortcuts.js +1 -1
  181. package/dist/write/vectors/shortcuts.js.map +1 -1
  182. package/dist/write/vectors/simulator.js +83 -27
  183. package/dist/write/vectors/simulator.js.map +1 -1
  184. package/dist/write/vectors/types.d.ts +11 -0
  185. package/dist/write/vectors/ui-certification.d.ts +3 -3
  186. package/dist/write/vectors/ui-certification.js +41 -5
  187. package/dist/write/vectors/ui-certification.js.map +1 -1
  188. package/dist/write/vectors/ui-drag.d.ts +21 -1
  189. package/dist/write/vectors/ui-drag.js +138 -27
  190. package/dist/write/vectors/ui-drag.js.map +1 -1
  191. package/dist/write/vectors/ui-recipes.d.ts +21 -1
  192. package/dist/write/vectors/ui-recipes.js +166 -7
  193. package/dist/write/vectors/ui-recipes.js.map +1 -1
  194. package/dist/write/vectors/ui.d.ts +25 -1
  195. package/dist/write/vectors/ui.js +142 -33
  196. package/dist/write/vectors/ui.js.map +1 -1
  197. package/dist/write/verify/delta.d.ts +17 -1
  198. package/dist/write/verify/delta.js +106 -29
  199. package/dist/write/verify/delta.js.map +1 -1
  200. package/package.json +7 -1
  201. package/schema/envelope.schema.json +397 -0
  202. package/skills/things-cli/SKILL.md +36 -17
  203. package/skills/things-cli/references/banner.md +35 -0
  204. package/skills/things-cli/references/contracts.md +32 -9
  205. package/skills/things-cli/references/errors.md +57 -0
  206. package/skills/things-cli/references/gui.md +1 -1
  207. package/skills/things-cli/references/model.md +18 -7
  208. package/skills/things-cli/references/ordering.md +88 -0
@@ -0,0 +1,57 @@
1
+ # The error contract (deep reference)
2
+
3
+ A failed `--json` call carries `ok: false`, `kind: "error"`, and an `error` object with a machine-readable `code`, a human `message`, an optional `likelyCause` and `remediation`, and at most one structured `detail`. The exit code mirrors the family (see [contracts.md](contracts.md)). A nonzero exit is informative, not a dead end — the change did not silently half-apply, and the message usually names the fix.
4
+
5
+ ## Candidates — self-correcting from an ambiguous or missing ref
6
+
7
+ When a name or partial-uuid resolves to more than one row, or to none, `error.detail.candidates` offers a disambiguation list so you self-correct without another round-trip. Each entry is ONE fixed, slim shape:
8
+
9
+ - `uuid` and `title` — always. `type` (`"project" | "heading" | "area" | "tag"`) — present for those kinds only; **absent `type` = to-do** (the same convention the item wire uses).
10
+ - A container hint `area` and/or `project` as a TITLE string — only when set.
11
+ - `stage` / `when` (the same derivations as the item wire) — only for a to-do/project candidate whose row carries them. A trashed/logged candidate needs no boolean: `stage` already reads `"trash"` / `"logbook"`.
12
+ - NOTHING else — no notes, counts, dates, or raw fields.
13
+
14
+ The shape is **flag-invariant**: `--full` / `--all` never widen it (an error payload is the most determinism-critical surface). The list is **capped at 8**; when more matched, the `message` states the total (e.g. "matches 12 projects … 4 more"). `ambiguous` carries the candidate list; `not-found` carries `candidates: []`.
15
+
16
+ **Fused refs and decorated input.** On a TTY the human candidate lines render each entry in the fused form **`Title [8charPrefix]`** (a duplicate-titled project/to-do candidate also gets a `· started YYYY-MM-DD` tail so recurring twins are distinguishable). That fused string is itself a valid **decorated ref**: `<ref>` slots everywhere (CLI flags, `--to-project` / `--to-heading` / `--area`, MCP args, library refs) accept **`Title [ref]`** — the bracketed segment (a uuid or ≥6-char partial-uuid) is resolved and the title half is an ignored comment, so a stale copy keeps working after a rename. Copy a fused candidate straight back as the ref. A *literal* title that really looks like `Name [abcd1234]` still wins (exact-title resolution outranks the bracket parse); the empty-title form `[prefix]` (a titleless heading) is legal.
17
+
18
+ **Live-scoped pool + dead-row hints.** By default candidates are LIVE rows only — a trashed or logged row never appears as a "did you mean". A trash/logbook-domain op (e.g. `project restore`) widens its own pool to that domain. When a name matches ZERO live rows but a DEAD row exists, the `message` gains an honest tail ("1 trashed item matches this name — see `things trash`", or the logbook equivalent) and `candidates` stays `[]` — no dangling-ref invitation. A write whose only name-match is a completed/logged project resolves to nothing by name (target it by uuid if intended); this prevents stranding an open child inside an invisible done project.
19
+
20
+ **Read-side liveness (project name resolution).** The SAME live-only law governs names on the read side, so `things "X"` (the bare-ref shorthand) and `things project show X` always agree, and an ambiguity's count always matches its candidate list. A NAME resolves against live (untrashed) rows only; an explicit uuid / partial-uuid still reaches a trashed project (viewing one by id is unchanged). Three read-only consequences:
21
+
22
+ - **Unique-dead fallback.** A name matching zero live rows but exactly ONE trashed project resolves to it and the view discloses it (the card's `(trashed)` marker / the JSON node's `stage: "trash"`). Several trashed-only twins → not-found with the dead-row hint above.
23
+ - **Trash disclosure on ambiguity.** `"X" matches N projects` counts only live rows (equal to the candidate list); extra trashed twins are disclosed — `… also matched: N in the trash — \`things trash\` lists them, a uuid reaches one directly` — never folded into the count. The error `code` is `ambiguous`, with the live candidates under `detail.candidates`.
24
+ - **Cross-kind merge.** The shorthand's namespace spans to-dos, areas, and projects. A unique winner follows the chain uuid → area → project; but an AMBIGUOUS subject with live matches at more than one kind refuses with the candidates MERGED across kinds (each carries its `type`) — `"X" matches 2 areas and 3 projects — use \`things area show\` / \`things project show\`, or a ref below` — so the shorthand never hides an option the narrower command would show.
25
+
26
+ Never guess between candidates for a destructive action — inspect details or ask.
27
+
28
+ ## Guarded writes and their acknowledgments
29
+
30
+ Some writes have a cascading or permanent effect. Each is refused BEFORE touching the app until you pass the flag that names the consequence; a `--dry-run` shows the same refusal. The CLI describes each by its consequence, but the JSON error's `blocked:<hazard>` code is the machine-readable handle:
31
+
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
+ - **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.)
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
+ - Other guards name their own consequence the same way (heading cascades, backdating an open item, checklist replacement, repeat scheduling, tag-subtree delete, …).
37
+
38
+ `things capabilities` lists every operation's support and preconditions; `--dry-run` previews any plan. If a request needs a capability the tool reports as unsupported, say so plainly rather than improvising through unrelated commands.
39
+
40
+ ## The error-code families
41
+
42
+ Every `error.code` is drawn from a frozen registry. Route on the code, and for the two template families route on the prefix:
43
+
44
+ | Family | Meaning | Exit |
45
+ |---|---|---|
46
+ | `usage` | Bad flags/arguments, mutually-exclusive flags, an unparseable date, or a move that meant to schedule. | 2 |
47
+ | `not-found` | A ref or subject resolved to nothing (`candidates: []`). | 2 |
48
+ | `ambiguous` | A name/partial-uuid matched several rows (carries `candidates`). | 2 |
49
+ | `unsupported` | No available write vector supports the operation (`detail.considered`). | 6 |
50
+ | `environment` | Database not found, Things not installed, or a permission problem. | 7 |
51
+ | `unexpected` | An internal error (a bug) — stop and report. | 1 |
52
+ | `verify-failed:<reason>` | A single mutation executed but the read-back check failed. `<reason>` ∈ `timeout | mismatch | silent-noop`. | 3 |
53
+ | `blocked:<suffix>` | Refused before dispatch — a hazard id (above) or a reason (`disruption-tier`, `lock`, `scope`, `clock`, `environment`, `drift`). `blocked:drift` is exit 5; all other `blocked:` are exit 4. | 4 / 5 |
54
+ | `bounce-aborted` | A reorder bounce aborted part-way (`detail.placed`/`remaining`/`cause`). | 3 |
55
+ | `verify-failed` | A multi-leg move/reorder failed mid-way (`detail.failed`/`completed`). | 3 |
56
+
57
+ A consumer that does not recognize a specific suffix routes on the prefix (`blocked:` / `verify-failed:`) and the exit code.
@@ -5,7 +5,7 @@ Curated rendering facts — the bench loop may compress or relocate these but mu
5
5
  <!-- STATUS: v0 seeded from Mike's description 2026-07-17; awaiting his review pass. -->
6
6
 
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
- - **List rows are compact**: a to-do in any list shows its title, tags, and deadline only. Its **notes and checklist are invisible until the user opens the item**. Put must-see-at-a-glance information in the title; supporting detail belongs in notes.
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
10
  - **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
11
  - 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").
@@ -17,7 +17,7 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
17
17
 
18
18
  - A to-do has at most ONE container, and may have none — standalone to-dos (no project, no area) are normal and appear at the top level of Anytime/Someday/Upcoming. Moving changes the container; completing or trashing does not.
19
19
  - The **Inbox is a state, not a container**: "in the Inbox" means untriaged — no container AND no schedule. Filing or scheduling an inbox to-do moves it out (filing promotes it to Anytime); moving a to-do TO the Inbox clears both its container and its schedule.
20
- - A heading is a section label inside one project — not a task. It cannot be scheduled, tagged, or completed; deleting/archiving it affects only the label, per the operation's contract.
20
+ - A heading is a section label inside one project — not a task. It cannot be scheduled or tagged, and its lifecycle is **archive/unarchive ONLY** — a heading is never completed or canceled (it has no canceled state). In a `project show` view every heading (live and archived) is an entry in the flat `headings` catalog `{uuid, title, archived?}` — it carries no `status`/`stage`, and emits a presence-keyed **`archived`** (the ISO archive timestamp) when archived, absent when open. A heading's members are not nested under it: they ride the flat `items` (live children) and `logbook` (logged children) rows as a `heading` ref, so you reconstruct a heading's contents by filtering those lists on the ref. Deleting/archiving a heading affects only the label, per the operation's contract.
21
21
  - Tag inheritance flows downward: area → project → (through heading) → to-do. A to-do's *effective* tags = own tags ∪ project tags ∪ area tags. List output distinguishes own vs inherited tags.
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.
@@ -37,14 +37,25 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
37
37
 
38
38
  `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.
39
39
 
40
- ### Reading view membership from JSON
40
+ ### Reading view membership from JSON — `stage` and `when`
41
41
 
42
- When reasoning about which view an item belongs to from `--json`, key on the fields together, not on a single hint:
42
+ Reads decompose an item's position onto two derived, presence-keyed words (they REPLACED the old `start`/`startDate`/`logged`/`trashed`/`todaySection` wire fields, which no longer appear):
43
43
 
44
- - `start: "inbox"` Inbox; `start: "someday"` Someday; `start: "active"` with no `startDate` Anytime.
45
- - A dated open item belongs to **Today** when dated for `meta.clock.today`, otherwise **Upcoming** when future-dated.
46
- - `todaySection` only describes placement WITHIN Today (e.g. `evening`); it is NOT evidence that an undated item is in Today.
47
- - Completed/canceled items are in the Logbook and trashed items in Trash, regardless of a stale-looking `logged` field.
44
+ - **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly. It is dropped inside a section/catalogue that provably states it (the stage-pure `inbox`/`anytime`/`someday`/`logbook`/`trash` lists and the `today` view) and kept everywhere it is stage-mixed (the mixed `upcoming` catalogue, `search`, `changes`, the projects/areas listings, the project/area card `items` and the project `logbook`, and `detail`).
45
+ - **`when`** the TIME POSITION: `today | evening | a future ISO date`, or absent (unscheduled and not in Today). `evening` implies today. Someday is a bucket, never a `when`. The `today` view is one flat `data.items` list (no `sections`); each row's `when` marks Today-proper vs This-Evening, and the whole-view count rides `meta.counts` (`{dueOrOverdue, other}`).
46
+ - The two are DIFFERENT facts. A due deadline pulls an UNDATED row into Today: it reads `when: "today"` and derives `stage: "anytime"` — the app re-files a deadline-pulled Inbox/Someday row into Anytime at pull time (R13/BANNER1b), so it drops out of the Inbox/Someday lists and joins Anytime while its `when` reads `today`. Completed/canceled → `stage: "logbook"`, trashed → `stage: "trash"`, regardless of any other hint.
47
+ - `provisional: true` marks a Today member the app has not yet materialized see [banner.md](banner.md).
48
+
49
+ ## Tiers — compact vs full (absence is meaningful)
50
+
51
+ Every row comes back at one of two densities, selected by view kind + flag, never by a caller-supplied field list:
52
+
53
+ - **Compact** (the list default) keeps identity + structural + non-default facts. A field at its default is OMITTED, so absence = the default: no `status` = open, no `checklist` = none, no `todos` = no child to-dos, no `when` = not in Today and unscheduled, no `provisional` = materialized. The full `notes` string is dropped and replaced by presence-keyed `hasNotes: true`; `startDate`, `created`, and `modified` are dropped.
54
+ - **Full** (`show`/`detail`, or a list forced with `--full`) is the whole record — the complete `notes`, the raw `startDate` substrate behind `when`, the checklist `items` array, and timestamps.
55
+ - Compact rows still carry the useful summaries: `checklist:{open,total}` on a to-do, `todos:{open,total}` on a project (app-maintained leaf-action counts — never headings, checklist items, or trashed rows), and `match:{field,text}` on a `search` hit whose match was NOT the title (`field` ∈ `heading | notes | checklist`).
56
+ - **Container absence rule:** inside a single-container node (a project/area card, an `anytime`/`someday` section) an item omits any ancestry the node already states, so absent `project`/`area` there means *inherited from the enclosing node*. A mixed list (`inbox`/`today`/`search`/`changes`) still names each row's own `project`/`area` (the `heading` ref is compact-dropped outside a project view; `--full` keeps it). **The project view is the exception where a `heading` ref is self-describing state:** a project-view row (the flat `items`, and a `logbook` row) that lives under a heading KEEPS its `heading` ref even in compact, because membership is a per-row attribute, not a bucket — so a swept child of an ARCHIVED heading is a flat `logbook` row carrying its `heading` ref (no more nested group). **Two-view sublabel asymmetry:** that in-project `logbook` row's `heading` labels the HEADING, while the same row in the GLOBAL `logbook` view is labeled by its PROJECT instead.
57
+ - **Container ref shape:** a container ref is a bare **title string** (`"area": "Family"`, `"project": "Groceries"`, `"heading": "Backlog"`). A flat sibling `areaUuid` / `projectUuid` / `headingUuid` (the full uuid) rides alongside **only when the bare title would not resolve back** to that exact item — a duplicate title in the same resolution scope, or a title that is itself a valid uuid prefix. **To act on a ref, pass `.areaUuid // .area`** (same for project/heading): the uuid when present, else the title. For unattended pipelines or stored refs, use `--full` and key on the uuids — the full tier emits every `*Uuid` sibling unconditionally. A row whose container project is a repeating template carries a flat `projectIsTemplate: true` (the JSON twin of the TTY `↻` glyph) — acting on such a row edits the blueprint, affecting future occurrences, so target the intended copy via `projectUuid` (a same-titled occurrence exists alongside the hidden template).
58
+ - **`type` is presence-keyed:** **absent `type` = to-do.** A row omits `type` when it is a to-do; `type` is present for a `project`, `heading`, `area`, or `tag` ROW (including in the error `candidates` shape). This is scoped to ROWS/candidates — a positional keyed sub-object whose kind is fixed by its slot (a `project show` heading GROUP node) drops `type` too, since its position already states it is a heading.
48
59
 
49
60
  ## Filters over views
50
61
 
@@ -0,0 +1,88 @@
1
+ # Move and reorder (deep reference)
2
+
3
+ Two verbs, kept strictly apart:
4
+
5
+ - **move** changes WHAT an item belongs to — a to-do's project/area/heading, a project's area.
6
+ - **reorder** changes only the ARRANGEMENT of items already sharing a container and bucket — never membership. Unmentioned siblings keep their order.
7
+
8
+ The SKILL.md summary is enough for most calls; this page is the full contract. Exact flags always come from `things todo move --help`, `things project move --help`, and `things reorder --help` (current for the binary you invoke).
9
+
10
+ ## Move — changing membership
11
+
12
+ **To-dos** (`things todo move <refs…> [destination] [position]`, one destination):
13
+
14
+ - `--to-project <ref>` · `--to-heading <sel>` · `--to-area <ref>` — place into a container.
15
+ - Detach family: `--no-heading` (leave the heading, stay in the project) · `--loose` (leave heading, project, AND area) · `--inbox` (back to the Inbox — this also DROPS the schedule).
16
+ - There is no `--detach` (removed) and no `--no-area` on a to-do — a to-do's area is inherited, so use `--loose`.
17
+
18
+ **Projects** (`things project move <refs…> [--to-area <ref> | --no-area] [position]`): `--no-area` is a project's detach (the to-do word `--loose` is refused on a project).
19
+
20
+ **`--to-heading` scoping.** A heading selector (exact title or uuid) resolves within the movees' shared project; when the movees are not already all in that project, name it with `--to-project`. A heading belongs to one project and cannot hold projects.
21
+
22
+ ## Reorder — changing arrangement
23
+
24
+ `things reorder <refs…> [--first | --last | --before <ref> | --after <ref>] [--in <target>]` is the ONE kind-neutral in-place verb (it replaced the old `todo reorder` subcommand and the raw `reorder --scope`). It rearranges to-dos AND the project rows the Today/Evening/day lists intermix with them.
25
+
26
+ - **Bare (no position)**: the named items assemble as ONE block at the EARLIEST one's current slot, in argument order (`--first` is NOT implied). Partial selection is fine — unmentioned siblings keep their order.
27
+ - **Positioned**: `--first`/`--last` send the block to the top/bottom of its bucket; `--before <ref>`/`--after <ref>` place it relative to an anchor.
28
+ - **Selection order = landing order** (reverse by naming the refs backwards).
29
+ - **Mixed to-do + project refs are allowed** in the Today/Evening/day buckets those kinds share; elsewhere reorder operates within one container+bucket.
30
+ - **Deadline-forecast rows sort on their Upcoming day too (projects too).** A someday/anytime to-do OR project with a future **deadline** and no start date renders in the Upcoming block on its deadline day. `things reorder` treats it as a first-class member of that day-group: it sorts on the same axis as the day's scheduled rows and interleaves with them, so you can reorder a mixed set of scheduled rows, deadline-forecast to-dos, and deadline-forecast projects in one call. This works with `allow-experimental` OFF — it rides a public-URL deadline-cycle, not the private surface (still gated by `bounce-enabled`, like every multi-leg reorder). An INBOX to-do that merely carries the same deadline is NOT on that day's axis and is refused with a message saying so (schedule it, or take it out of the Inbox, first).
31
+
32
+ Sibling verbs for the two things `reorder` does NOT touch: `things project move-heading` reorders a project's HEADINGS; `things area reorder` reorders the sidebar AREAS.
33
+
34
+ ## Anchors POSITION, never MIGRATE
35
+
36
+ A `--before`/`--after` anchor only says WHERE in a bucket — it never moves the anchor and never reschedules a movee to reach it. The anchor must already sit in the movees' container+bucket; a cross-container or cross-bucket anchor **fails closed** with a message naming where the rows and the anchor actually are (no silent rescheduling). For a global-axis bucket (today/evening/tomorrow/a future day-group) the anchor need only share the movees' day-group, not their structural container — the app permits exactly that drag.
37
+
38
+ ## The dual axis and `--in` (fail-closed)
39
+
40
+ A Today/Evening member has TWO order slots: its slot in the Today VIEW (`todayIndex`) and its slot in its own CONTAINER (`index` — a project/area/heading child, or the loose Anytime bucket). When a set (and its anchor) is coherent on BOTH axes and no `--in` is given, the reorder is REFUSED with a message naming both readings and their exact `--in` spellings — this replaces any silent always-Today guess.
41
+
42
+ **The forecast day is a second dual axis (§9o dual-citizen).** A deadline-forecast row (someday/anytime, no start date, a future deadline) is BOTH a member of its Upcoming day-block (ordered on `todayIndex`) and a member of its container's someday/anytime list (ordered on `index`). So a set where every movee is a forecast member of ONE shared day AND they all share one direct container (same project/heading/area, or all loose) is dual-axis too: a bare reorder is REFUSED, naming the real date and the container/list spelling. A forecast set that spans containers, or a mix of scheduled + forecast rows on one day, is coherent on only the day axis (the day-block) and auto-routes there — no `--in` needed.
43
+
44
+ - `--in` accepts `today | evening | anytime | someday | inbox`, a project/area/heading ref (uuid or unique title), `upcoming` (the one future day the whole set shares), or a `YYYY-MM-DD` day-block. `loose` is refused (it is a read view, not a bucket).
45
+ - `--in <YYYY-MM-DD>` names one exact Upcoming day-block: every movee must be on that day (scheduled for it, or carrying it as a deadline) and the date must be strictly future (a today/past date is refused, pointing you at `--in today`). `--in upcoming` is the proxy — it derives the single future day the set shares and refuses (listing the per-item days) if they span days. Neither needs a shared container — a day-block is ONE cross-container sortability bucket.
46
+ - `--in someday` / `--in anytime` reorder within a shared container's stage list: every movee must BE that stage AND they must share one direct container (same project, heading, area, or all loose), else it is refused naming the mismatch. `--in inbox` needs only that every movee is inbox-stage.
47
+ - **One KIND per index token.** A stage-list token (`anytime`/`someday`/`inbox`) or a container ref sorts each object kind in its own order-space, so all movees must be the SAME kind — a mixed to-do + project set is refused naming each movee's kind, *even when they share a container* (an area's someday to-dos and someday projects are different index buckets). The `today`/`evening`/`upcoming`/`<YYYY-MM-DD>` day-and-view tokens are the exception: those axes intermix to-dos and projects. A **heading** has no stage/day/view order of its own — it is never a member of any of these buckets, so a heading movee is refused on every token, pointing you at `things project move-heading`.
48
+ - The container form of `--in` is POLYMORPHIC — it tries project → area → heading — so a heading whose title is shadowed by a project or area of the same name would resolve to the wrong kind. Pin it with the heading's **decorated ref** `Heading [ref]` (or a bare uuid). Decorated refs (`Title [ref]`) work in every `<ref>` slot: the bracketed uuid/partial-uuid resolves and the title is an ignored comment. A literal title that really contains brackets still wins over the bracket parse (exact-title resolves first).
49
+ - `--in someday` / `--in anytime` / a container ref on a same-day forecast set reorders its CONTAINER index (its someday/anytime list order), never the day-block — the explicit axis is honored, not overridden by the day route.
50
+ - Forcing the container index axis on a Today/Evening member PRESERVES the Today/Evening flag — a flag-safe move protocol routes it off the de-Today path. Only the someday/inbox loose axes still refuse a flagged member (their re-entry cannot preserve the flag).
51
+
52
+ ## Mixed-stage move placement
53
+
54
+ A `move` selection spanning stage sub-buckets (anytime + scheduled + someday + templates):
55
+
56
+ - `--before`/`--after` is REFUSED unless every movee shares the anchor's sub-bucket (remediation: split the call, or drop the anchor).
57
+ - `--first`/`--last` apply PER sub-bucket — each stage-group lands at the top/bottom of ITS matching bucket in the destination, and the result note states every group's placement outcome.
58
+
59
+ ## Gates, caps, and automatic fallbacks
60
+
61
+ Three config knobs (`things config get/set …`) tune ordering; every default gives full sortability:
62
+
63
+ - **`allow-experimental`** (default `true`) — enables the private NATIVE re-rank command for the scopes only it can reach directly (`inbox`, `someday`, a project's unheaded children, an area's members, a container's same-day children, `tomorrow`). It is the off-switch, not an opt-in.
64
+ - **`bounce-enabled`** (default `true`) — permits the verified `when=`/move round-trip protocols the other scopes use. `false` REFUSES a bounce-dependent placement rather than degrading destructively.
65
+ - **`bounce-max-items`** (default `30`) — caps how many items one bounce may touch; a set larger than the cap is refused, not truncated.
66
+
67
+ **Automatic non-experimental fallbacks (SIT7).** When `allow-experimental` is off (or the native surface is unavailable), the native-only scopes DO NOT fail — each degrades to a proven, verified, flag-safe move protocol (park-and-re-home for `inbox`/`project`/`area`, a `when=` bounce for `someday` and day-groups). Collateral is preserved (Today/Evening flag, live reminder, deadline, container FKs). The result's `warnings` note discloses which fallback ran (e.g. "reordered via the non-experimental PROJROOT fallback because the native reorder is unavailable") — a native placement is never silently mistaken for a degraded one.
68
+
69
+ **Flag-aware routing (SIT6).** A reorder touching a Today/Evening-FLAGGED row never de-Todays it: the whole touched set swaps to a flag-safe MOVE protocol on the same axis (the `when=` bounce would strip the flag). This is transparent — you still call `things reorder`; the chosen strategy is disclosed in the result.
70
+
71
+ ## Placement guarantees
72
+
73
+ "Top of bucket in selection order" is GUARANTEED wherever a lab-clean protocol exists: loose inbox/today/evening/someday/anytime; a project's or area's members (anytime AND someday); a heading's anytime/someday children; any container child's evening slot; area-less someday/anytime projects; and a whole future day-group across containers (including scheduled project rows, area'd ones too, and deadline-forecast rows — to-dos and projects — on their deadline day). The result's placement class names which guarantee you got, and a bounce that co-touches unnamed siblings to honor a `--before`/`--after` anchor lists them.
74
+
75
+ ## Repeating templates in day-blocks
76
+
77
+ A repeating template's projected occurrence renders in an Upcoming day-block, and `things reorder` can now position it there — with two constraints, because a dated `when=`/`deadline=` leg CRASHES a template so the app's own private surface is the only safe writer.
78
+
79
+ - **On TOMORROW, full power.** When a template's next occurrence lands on tomorrow, BOTH to-do and project templates sort inline with the day's scheduled rows in one native `list "Tomorrow"` call — any position, exact order.
80
+ - **On a later future day, to-do templates sort; project templates hold their spot.** A TO-DO template front-inserts via a single-id `list "Upcoming"` native leg interleaved with the day's other rows, so it lands anywhere in the block. A PROJECT template has NO headless reach on a non-tomorrow day (only the Tomorrow call or a manual drag place it), so it stays byte-untouched at the bottom of the block and every other item sorts ABOVE it. A reorder that would need a project template above a movable row (or would re-order two project templates) is REFUSED, naming the arrangement it CAN reach — do that in the app by dragging, or reorder on the day it becomes tomorrow.
81
+ - **Templates need `allow-experimental` on.** Any day-group reorder that includes a template requires the private native surface (the to-do-template leg uses it). With `allow-experimental` off, such a reorder is REFUSED, naming the template(s) — it never silently skips them and never routes them onto a crash-path leg.
82
+ - A template's placement write is `userModificationDate`-SILENT (a sync/watcher keyed on that timestamp will not see the move); the result discloses it.
83
+
84
+ Everything non-template is sortable on every guaranteed surface above.
85
+
86
+ ## MCP parity
87
+
88
+ The MCP server exposes reorder as two tools: **`reorder`** (the planner form — `refs`, an optional position, an optional `in` axis) mirrors `things reorder`, and **`reorder_areas`** mirrors `things area reorder` (sidebar areas, with the same two-key GUI gate). Both call the same library entries the CLI does, so the dual-axis refusal, the flag-safe routing, and the automatic fallbacks behave identically.