things-api 0.12.0 → 0.14.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 (141) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/canonical-ref.d.ts +21 -0
  3. package/dist/cli/canonical-ref.js +23 -0
  4. package/dist/cli/canonical-ref.js.map +1 -0
  5. package/dist/cli/commands/area.d.ts +5 -5
  6. package/dist/cli/commands/area.js +39 -18
  7. package/dist/cli/commands/area.js.map +1 -1
  8. package/dist/cli/commands/install-skill.d.ts +2 -2
  9. package/dist/cli/commands/install-skill.js +29 -0
  10. package/dist/cli/commands/install-skill.js.map +1 -1
  11. package/dist/cli/commands/mcp.js +12 -1
  12. package/dist/cli/commands/mcp.js.map +1 -1
  13. package/dist/cli/commands/project.js +74 -22
  14. package/dist/cli/commands/project.js.map +1 -1
  15. package/dist/cli/commands/reads.d.ts +5 -1
  16. package/dist/cli/commands/reads.js +31 -15
  17. package/dist/cli/commands/reads.js.map +1 -1
  18. package/dist/cli/commands/setup.js +11 -4
  19. package/dist/cli/commands/setup.js.map +1 -1
  20. package/dist/cli/commands/show.js +51 -32
  21. package/dist/cli/commands/show.js.map +1 -1
  22. package/dist/cli/commands/todo.js +2 -2
  23. package/dist/cli/commands/todo.js.map +1 -1
  24. package/dist/cli/commands/writes.js +138 -67
  25. package/dist/cli/commands/writes.js.map +1 -1
  26. package/dist/cli/did-you-mean.js +19 -5
  27. package/dist/cli/did-you-mean.js.map +1 -1
  28. package/dist/cli/dry-run.d.ts +55 -0
  29. package/dist/cli/dry-run.js +92 -0
  30. package/dist/cli/dry-run.js.map +1 -0
  31. package/dist/cli/glyphs.d.ts +9 -2
  32. package/dist/cli/glyphs.js +11 -5
  33. package/dist/cli/glyphs.js.map +1 -1
  34. package/dist/cli/help.js +7 -6
  35. package/dist/cli/help.js.map +1 -1
  36. package/dist/cli/main.js +7 -0
  37. package/dist/cli/main.js.map +1 -1
  38. package/dist/cli/read-driver.d.ts +47 -7
  39. package/dist/cli/read-driver.js +43 -21
  40. package/dist/cli/read-driver.js.map +1 -1
  41. package/dist/cli/ref-render.d.ts +14 -0
  42. package/dist/cli/ref-render.js +9 -0
  43. package/dist/cli/ref-render.js.map +1 -0
  44. package/dist/cli/render.d.ts +39 -25
  45. package/dist/cli/render.js +102 -62
  46. package/dist/cli/render.js.map +1 -1
  47. package/dist/cli/resolve-invocation.d.ts +5 -3
  48. package/dist/cli/resolve-invocation.js +13 -4
  49. package/dist/cli/resolve-invocation.js.map +1 -1
  50. package/dist/client.d.ts +100 -32
  51. package/dist/client.js +63 -19
  52. package/dist/client.js.map +1 -1
  53. package/dist/contracts.d.ts +34 -33
  54. package/dist/contracts.js +1 -1
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db/baselines/db-v26.js +4 -1
  57. package/dist/db/baselines/db-v26.js.map +1 -1
  58. package/dist/index.d.ts +8 -5
  59. package/dist/index.js +9 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp/server.js +192 -139
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/model/dates.d.ts +26 -0
  64. package/dist/model/dates.js +46 -4
  65. package/dist/model/dates.js.map +1 -1
  66. package/dist/model/entities.d.ts +11 -1
  67. package/dist/model/entities.js.map +1 -1
  68. package/dist/model/mappers.js +3 -0
  69. package/dist/model/mappers.js.map +1 -1
  70. package/dist/model/serialize.js +15 -3
  71. package/dist/model/serialize.js.map +1 -1
  72. package/dist/read/area-filter.d.ts +2 -2
  73. package/dist/read/area-filter.js +3 -3
  74. package/dist/read/area-filter.js.map +1 -1
  75. package/dist/read/project-view.d.ts +76 -1
  76. package/dist/read/project-view.js +80 -9
  77. package/dist/read/project-view.js.map +1 -1
  78. package/dist/read/queries.d.ts +71 -2
  79. package/dist/read/queries.js +199 -10
  80. package/dist/read/queries.js.map +1 -1
  81. package/dist/read/scope.d.ts +2 -2
  82. package/dist/read/scope.js +3 -3
  83. package/dist/read/scope.js.map +1 -1
  84. package/dist/read/shape.d.ts +87 -9
  85. package/dist/read/shape.js +483 -124
  86. package/dist/read/shape.js.map +1 -1
  87. package/dist/read/show-target.js +52 -33
  88. package/dist/read/show-target.js.map +1 -1
  89. package/dist/read/truncation.d.ts +73 -7
  90. package/dist/read/truncation.js +62 -29
  91. package/dist/read/truncation.js.map +1 -1
  92. package/dist/read/views.d.ts +20 -7
  93. package/dist/read/views.js +7 -7
  94. package/dist/read/views.js.map +1 -1
  95. package/dist/surface-copy.d.ts +13 -0
  96. package/dist/surface-copy.js +13 -0
  97. package/dist/surface-copy.js.map +1 -1
  98. package/dist/write/batch.js +0 -1
  99. package/dist/write/batch.js.map +1 -1
  100. package/dist/write/commands.d.ts +25 -1
  101. package/dist/write/commands.js +259 -122
  102. package/dist/write/commands.js.map +1 -1
  103. package/dist/write/guards.js +27 -11
  104. package/dist/write/guards.js.map +1 -1
  105. package/dist/write/move.d.ts +34 -9
  106. package/dist/write/move.js +730 -78
  107. package/dist/write/move.js.map +1 -1
  108. package/dist/write/operations.d.ts +50 -20
  109. package/dist/write/operations.js +2 -2
  110. package/dist/write/operations.js.map +1 -1
  111. package/dist/write/pipeline.js +25 -4
  112. package/dist/write/pipeline.js.map +1 -1
  113. package/dist/write/pre-state.d.ts +58 -28
  114. package/dist/write/pre-state.js +247 -49
  115. package/dist/write/pre-state.js.map +1 -1
  116. package/dist/write/reorder.js +266 -32
  117. package/dist/write/reorder.js.map +1 -1
  118. package/dist/write/resolution-timestamps.d.ts +34 -0
  119. package/dist/write/resolution-timestamps.js +286 -0
  120. package/dist/write/resolution-timestamps.js.map +1 -0
  121. package/dist/write/reversibility.js +5 -5
  122. package/dist/write/reversibility.js.map +1 -1
  123. package/dist/write/scope-guard.js +0 -5
  124. package/dist/write/scope-guard.js.map +1 -1
  125. package/dist/write/undo.d.ts +0 -6
  126. package/dist/write/undo.js +44 -4
  127. package/dist/write/undo.js.map +1 -1
  128. package/dist/write/vectors/applescript.js +14 -4
  129. package/dist/write/vectors/applescript.js.map +1 -1
  130. package/dist/write/vectors/url-scheme.js +0 -8
  131. package/dist/write/vectors/url-scheme.js.map +1 -1
  132. package/dist/write/verify/delta.d.ts +16 -2
  133. package/dist/write/verify/delta.js +21 -2
  134. package/dist/write/verify/delta.js.map +1 -1
  135. package/package.json +1 -1
  136. package/schema/envelope.schema.json +16 -78
  137. package/skills/things-cli/SKILL.md +10 -6
  138. package/skills/things-cli/references/contracts.md +3 -2
  139. package/skills/things-cli/references/errors.md +9 -1
  140. package/skills/things-cli/references/model.md +9 -7
  141. package/skills/things-cli/references/ordering.md +36 -10
@@ -96,7 +96,7 @@
96
96
  },
97
97
  "truncation": {
98
98
  "$ref": "#/definitions/Truncation",
99
- "description": "The read's completeness metadata (the single truncation shape). Present on any read that could drop rows — flat views (row `limit`), the Today split (`sections`), and grouped views (anytime/someday/`area show`, `blocks`). `meta.truncation.truncated` is the universal \"did I see everything\" check."
99
+ "description": "The read's completeness metadata (the single truncation shape) — the WHOLE-VIEW rollup. Present on any read that could drop rows — flat views (row `limit`), the `today` view, and grouped views (anytime/someday/`area show`). Per-bucket completeness rides INLINE on the data records' own `total` (R1), never on this envelope. `meta.truncation.truncated` is the universal \"did I see everything\" check."
100
100
  },
101
101
  "resolvedCommand": {
102
102
  "type": "string",
@@ -144,6 +144,20 @@
144
144
  "additionalProperties": false,
145
145
  "description": "The active content filter this response was scoped to (ADDITIVE). Present ONLY when a scope was applied — currently `area` (the `--area` view filter), carrying the resolved area's uuid + title; ABSENT otherwise, so the wire shape is unchanged for unscoped reads (the `meta.clock` precedent)."
146
146
  },
147
+ "counts": {
148
+ "type": "object",
149
+ "properties": {
150
+ "dueOrOverdue": {
151
+ "type": "number"
152
+ },
153
+ "other": {
154
+ "type": "number"
155
+ }
156
+ },
157
+ "required": ["dueOrOverdue", "other"],
158
+ "additionalProperties": false,
159
+ "description": "Whole-view aggregate counts (ADDITIVE). Present ONLY on the `today` view: the app's sidebar count split — `dueOrOverdue` (open members whose deadline is due or overdue) vs. `other` (the rest). A convenience aggregate an agent would otherwise recompute over the rows; it lives here so `data` stays pure domain rows. Both counts are OPEN members only, and a `0` is meaningful."
160
+ },
147
161
  "scope": {
148
162
  "type": "object",
149
163
  "properties": {
@@ -184,87 +198,11 @@
184
198
  },
185
199
  "truncated": {
186
200
  "type": "boolean"
187
- },
188
- "sections": {
189
- "type": "array",
190
- "items": {
191
- "$ref": "#/definitions/SectionCount"
192
- },
193
- "description": "Per-section shown/total breakdown for a split flat view (Today); absent otherwise."
194
- },
195
- "blocks": {
196
- "type": "array",
197
- "items": {
198
- "$ref": "#/definitions/GroupBlock"
199
- },
200
- "description": "Per-block nesting for a grouped view (anytime/someday/area card); absent otherwise."
201
201
  }
202
202
  },
203
203
  "required": ["shown", "total", "limit", "truncated"],
204
204
  "additionalProperties": false,
205
- "description": "The single truncation-metadata shape for every read (the `meta.truncation` field). `shown` items were returned of `total` that matched after all filters; `limit` is the effective cap (null when the caller asked for all rows, and always null on a grouped view whose caps are per-block); `truncated` is the UNIVERSAL completeness check — true exactly when anything was dropped (`shown < total`, or any block hid rows). The dropped remainder is `total - shown`.\n\nTwo optional per-shape breakdowns hang off it: `sections` for a split flat view (the Today split per render-section shown/total), and `blocks` for a grouped view (anytime/someday/`area show` — the identity-carrying per-block nesting). Exactly one of them is present on the shapes that have it; both are absent on a plain flat view."
206
- },
207
- "SectionCount": {
208
- "type": "object",
209
- "properties": {
210
- "key": {
211
- "type": "string",
212
- "enum": ["today", "evening"]
213
- },
214
- "shown": {
215
- "type": "number"
216
- },
217
- "total": {
218
- "type": "number"
219
- }
220
- },
221
- "required": ["key", "shown", "total"],
222
- "additionalProperties": false,
223
- "description": "Per-section counts for a SPLIT flat view — currently only `today`'s Today / This-Evening split. One entry per section IN RENDER ORDER, each reporting the rows `shown` of the `total` that matched after all filters. Lets a renderer keep a downstream section (This Evening) honest under a single global cap without a pre-truncation copy of the view."
224
- },
225
- "GroupBlock": {
226
- "type": "object",
227
- "properties": {
228
- "kind": {
229
- "type": "string",
230
- "enum": ["loose", "area", "project", "projects"]
231
- },
232
- "ref": {
233
- "type": ["string", "null"],
234
- "description": "Container reference (area or project uuid); null for the loose block."
235
- },
236
- "title": {
237
- "type": ["string", "null"],
238
- "description": "Container title; null for the loose block."
239
- },
240
- "shown": {
241
- "type": "number"
242
- },
243
- "total": {
244
- "type": "number"
245
- },
246
- "limit": {
247
- "type": ["number", "null"],
248
- "description": "The cap that applied to THIS block (null = uncapped)."
249
- },
250
- "totalProjects": {
251
- "type": "number",
252
- "description": "Type split for blocks that mix project rows and to-dos (someday's loose/area blocks; projects always list first, so the hidden split is `totalProjects - min(shown, totalProjects)` projects, remainder to-dos). Absent on single-type blocks."
253
- },
254
- "totalTodos": {
255
- "type": "number"
256
- },
257
- "children": {
258
- "type": "array",
259
- "items": {
260
- "$ref": "#/definitions/GroupBlock"
261
- },
262
- "description": "Nested project blocks (anytime item-lists / someday active-project groups). Absent when none."
263
- }
264
- },
265
- "required": ["kind", "ref", "title", "shown", "total", "limit"],
266
- "additionalProperties": false,
267
- "description": "One identity-carrying block of a grouped catalogue (anytime/someday) or a sectioned detail view (`area show`). Every header/section is always rendered; only the innermost item lists are capped. Emitted for every block that has rows to cap (`total > 0`) — including a block whose rows were ALL dropped (`shown: 0`), so no truncated header is untraceable. A block with no cappable rows of its own (`total: 0`) is omitted UNLESS it wraps truncated `children` — an area whose only capped content is its project item-lists still appears as their container.\n\nBlocks are NESTED: an area/loose block carries its project blocks in `children` — in anytime the project item-lists inside the area, in someday the active-project child groups found in that section. The `area show` `projects`/`area` blocks are siblings of one area and stay top-level."
205
+ "description": "The single truncation-metadata shape for every read (the `meta.truncation` field). `shown` items were returned of `total` that matched after all filters; `limit` is the effective cap (null when the caller asked for all rows, and always null on a grouped view whose caps are per-block); `truncated` is the UNIVERSAL completeness check — true exactly when anything was dropped (`shown < total`, or any block hid rows). The dropped remainder is `total - shown`.\n\nThis is the WHOLE-VIEW rollup only. Per-bucket completeness rides INLINE on the data records (read-shape v2 R1): each `children`/`sections`/`projects` bucket carries its own `total`, present iff it was capped. The pre-v2 `blocks[]` descriptor-join sidecar is RETIRED from the wire entirely (doctrine v2 PR 5) — the per-block detail the TTY renderers need is internal render plumbing ( {@link GroupBlock } ), never serialized."
268
206
  },
269
207
  "ErrorEnvelope": {
270
208
  "type": "object",
@@ -1,12 +1,12 @@
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.12.0
4
+ version: 0.14.0
5
5
  ---
6
6
 
7
7
  # Things CLI
8
8
 
9
- `things` is a command-line interface to the user's Things 3 task database. Reads are instant SQL queries; writes go through the app itself and are checked after they land. Use `things` when it is on your PATH; otherwise — or when `things --version` reports below **0.12.0** — substitute `npx -y things-api@latest` in every command (identical subcommands and flags), which always pairs the current commands with their current help.
9
+ `things` is a command-line interface to the user's Things 3 task database. Reads are instant SQL queries; writes go through the app itself and are checked after they land. Use `things` when it is on your PATH; otherwise — or when `things --version` reports below the version stamped in this skill's `version:` frontmatter above (set at publish time; the commands and read-view JSON shapes documented here including the bucketed `today` / project / area shapes — track the release this skill ships with) — substitute `npx -y things-api@latest` in every command (identical subcommands and flags), which always pairs the current commands with their current help.
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
 
@@ -32,15 +32,17 @@ Reads decompose an item's position onto two derived, presence-keyed words (they
32
32
 
33
33
  ## Referring to items
34
34
 
35
- Commands take a `<ref>`: a UUID, a unique UUID prefix (≥ 6 chars), a Things share link, or a (unique) title. Ambiguous refs fail with the candidates listed — pick one and retry ([references/errors.md](references/errors.md) has the candidate/dead-row/hazard contract). Discover UUIDs via any read command; add `--json` for stable machine output (UUIDs are in `.uuid`; emitted UUIDs are always full).
35
+ Commands take a `<ref>`: a UUID, a unique UUID prefix (≥ 6 chars), a Things share link, a (unique) title, or a **decorated ref `Title [ref]`** (the bracketed uuid/partial-uuid resolves; the title is an ignored comment, so a stale copy still works). Ambiguous refs fail with the candidates listed, each rendered in the fused `Title [8charPrefix]` form you can paste straight back — pick one and retry ([references/errors.md](references/errors.md) has the candidate/dead-row/hazard contract). Discover UUIDs via any read command; add `--json` for stable machine output (UUIDs are in `.uuid`; emitted UUIDs are always full).
36
+
37
+ **Container refs and the `type` shorthand in `--json`.** A row's container ref (`area`/`project`/`heading`) is a bare **title string**; a flat sibling `areaUuid` / `projectUuid` / `headingUuid` appears **only when that title alone would not resolve back** to the exact same item (a duplicate title, or a title that shadows a UUID). **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 always emits the `*Uuid` siblings). And **absent `type` = to-do** — a row omits `type` when it is a to-do; it is present for `project`, `heading`, `area`, and `tag`.
36
38
 
37
39
  ## Stable contracts
38
40
 
39
41
  These hold regardless of the binary version; see [references/contracts.md](references/contracts.md) for the full text.
40
42
 
41
- - **JSON envelope**: every `--json` response is `{ apiVersion, ok, kind, data, meta }`. Read results from `.data` (`.data.items`/`.data.sections`/`.data.item`/`.data.view` per `kind`), 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.
43
+ - **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`), 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.
42
44
  - **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.
43
- - **Previews, undo & idempotency**: `--dry-run` shows any write's 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`).
45
+ - **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`).
44
46
  - **Preconditions**: referenced containers and tags must already exist — create nested structures outside-in and reuse each returned UUID.
45
47
  - If the user requests a JSON reply schema, return exactly that object after the read or checked write.
46
48
 
@@ -54,7 +56,9 @@ Namespaced verb families — run `things <group> --help` for the verbs and `thin
54
56
 
55
57
  **Scheduling is an update, not a move**: `things todo update <ref> --when today|evening|anytime|someday|YYYY-MM-DD` schedules or parks an item; `move` changes its CONTAINER only.
56
58
 
57
- **Move vs reorder — keep them apart** (`things help move`). MOVE changes WHAT an item belongs to: `things todo move <refs…> --to-project|--to-heading|--to-area <ref>`, or the detach family `--no-heading` / `--loose` / `--inbox`; `things project move <refs…> --to-area <ref>|--no-area`. REORDER changes only ARRANGEMENT in place, never membership: `things reorder <refs…> [--first|--last|--before <ref>|--after <ref>] [--in <target>]`. Both are variadic (selection order = landing order). An anchor POSITIONS but never MIGRATES — a cross-container/bucket anchor fails closed. A Today/Evening member sits on two axes (its Today slot and its container slot); a set coherent on both is REFUSED until you pass `--in` (`today|evening|anytime|someday|inbox`, or a project/area/heading ref). Every non-template item is sortable; the axes, gates, and automatic fallbacks are in [references/ordering.md](references/ordering.md). (MCP exposes these as the `reorder` and `reorder_areas` tools.)
59
+ **Move vs reorder — keep them apart** (`things help move`). MOVE changes WHAT an item belongs to: `things todo move <refs…> --to-project|--to-heading|--to-area <ref>`, or the detach family `--no-heading` / `--loose` / `--inbox`; `things project move <refs…> --to-area <ref>|--no-area`. REORDER changes only ARRANGEMENT in place, never membership. `things reorder <refs…> [--start|--end|--before <ref>|--after <ref>] [--in <target>]` is the ONE reorder verb across ALL kinds to-dos, projects, a project's headings, AND sidebar areas dispatching the protocol by kind. All refs must be one KIND (only to-dos and projects intermix, and only on the shared Today/Evening/day axes); a mixed-kind set, a cross-container set, a cross-axis anchor, and a non-member each fail closed with one precise message. Reorder is variadic (selection order = landing order); an anchor POSITIONS but never MIGRATES. A Today/Evening member sits on two axes (its Today slot and its container slot); a set coherent on both is REFUSED until you pass `--in` (`today|evening|anytime|someday|inbox`, or a project/area/heading ref — to-dos/projects only). Reordering a HEADING runs the native heading-block wire (children follow); an archived heading is reorderable but repositioning it brings it back to open (disclosed). `things project move-heading` (cross-project moves / demotion) and `things area reorder` remain as kind-specific spellings. The axes, gates, #V11 heading policy, and automatic fallbacks are in [references/ordering.md](references/ordering.md). (MCP exposes this as the single `reorder` tool.)
60
+
61
+ **Resolution timestamps** (`--created-at` / `--completed-at`, both to-dos and projects; ISO date or datetime — a bare date is NOON in the effective zone): `add --completed-at <iso> [--created-at <iso>]` creates the item already RESOLVED in the Logbook (the history-import / GTD-migration path); `add --created-at <iso>` alone is born-open with a backdated creation. On an EXISTING item, `update --created-at` rewrites creation (any status); `update --completed-at` rewrites the completion timestamp of an ALREADY-resolved item (a canceled one stays canceled) — an OPEN item is refused, pointing you at `complete --completed-at` (resolve-and-backdate completed) or `cancel --completed-at` (resolve-and-backdate canceled; the "Completed on" stamp applies to canceled items too). Backdating a resolved item can be a multi-leg, non-atomic sequence — the leg list is disclosed in the result and in `--dry-run`. A project `add --completed-at` cannot seed open `--todo` children (a completed-project import needs every child resolved).
58
62
 
59
63
  **Guarded writes** surface their consequence and require an explicit flag: deleting a NON-EMPTY area needs `--allow-non-empty`, a permanent delete needs `--dangerously-permanent`, and a UI-driving op (e.g. `area reorder`) needs `--dangerously-drive-gui` plus `things config set ui-enabled true`. The full acknowledgment contract is in [references/errors.md](references/errors.md).
60
64
 
@@ -6,8 +6,9 @@ The apiVersion-stable contracts that hold regardless of which binary version you
6
6
 
7
7
  Every `--json` response is an envelope `{ apiVersion, ok, kind, data, meta }`:
8
8
 
9
- - Results are in `.data`, in exactly one of four wrappers named by `kind`: `.data.item` (one entity), `.data.view` (an area/project card), `.data.items` (a flat list), or `.data.sections` (a list split into named sections). Never `.items` at the top level. Item UUIDs are in `.uuid`, not `.id`; emitted UUIDs are always full.
10
- - Check `meta.truncation.truncated` before concluding "no match" or "that's everything": it is `true` exactly when any row was hidden (`shown < total`, or any grouped block capped). Raise `--limit`/`--all` or narrow the filter rather than assuming you saw everything.
9
+ - Results are in `.data`, in exactly one of five wrappers named by `kind`: `.data.item` (one entity), `.data.view` (an area/project card), `.data.items` (a flat list), `.data.sections` (a list split into named sections), or `.data.children` (the `today` view's two keyed buckets `{ today: {items, total?}, evening: {items, total?} }`). Never `.items` at the top level. Item UUIDs are in `.uuid`, not `.id`; emitted UUIDs are always full. The `today` view's whole-view count rides `meta.counts` (`{dueOrOverdue, other}`), not `data`.
10
+ - **Container refs and `type`.** A row's `area`/`project`/`heading` is a bare **title string**; a flat sibling `areaUuid`/`projectUuid`/`headingUuid` appears **only when that title would not resolve back** to the exact same item. **To act on a ref, pass `.areaUuid // .area`** (same for project/heading). For unattended pipelines or stored refs, use `--full` and key on uuids (the full tier always emits the `*Uuid` siblings). **Absent `type` = to-do** — `type` is present only for a `project`, `heading`, `area`, or `tag` ROW/candidate (a `project show` `headings[]` entry drops it — its slot already states it is a heading, and it emits a presence-keyed **`archived`** — the archive timestamp, present iff archived — instead of any status/stage; a heading is archive/unarchive only, never completed/canceled). **A `project show` view is `data.view = { project, children, headings[] }`** (read-shape v2): `children` is the un-headed body's stage buckets `{ anytime: {items, total?}, upcoming: [{when, items, total?} …], someday: {items, total?}, logbook: {items, total?} }` (the `upcoming` day-block array ends with a `{when: null, items}` resting block when a container holds date-less recurring templates); `headings[]` is EVERY heading in order — open, archived-unswept, AND archived-swept — each `{ uuid, title, archived?, children }` with the same recursive `children`. A swept heading's logged children ride ITS `children.logbook`; there is no `logbookHeadings` and no root `logbook` (the GUI's merged logbook region is TTY-only). **An `area show` view is `data.view = { area, children, projects }`** (read-shape v2): `area` is the area node (or `null` for the `loose` pseudo-area); `children` is the area's DIRECT to-dos as three stage buckets `{ anytime: {items, total?}, upcoming: [{when, items, total?} …], someday: {items, total?} }` (same `{when: null, items}` resting-block rule; NO `logbook` key — read an area's archive with `things logbook --area <ref>`); `projects` is the child-project rows as ONE record `{items, total?}` (a mixed-stage listing keeping each row's `stage`/`when`). A capped scope carries its inline `total` — `children.anytime` for `--area-limit`, `projects` for `--project-limit`; this view emits no `meta.truncation.blocks`. **The global `things upcoming` view is `data.sections = [{when, items, total?} …]`** (read-shape v2 PR 4): chronological DAY BLOCKS keyed by `when` (an ISO date — also the `--in <when>` reorder token), each the complete global day scope, then a trailing `{when: null, items}` block of date-less recurring templates when any exist. Rows KEEP `stage` (the view is mixed — future-dated `upcoming` rows beside deadline-forecast `anytime`/`someday` rows at their deadline day) and drop `when` inside their block (a scheduled row's `when` is the block key). To flatten: `.data.sections[].items[]`. A capped day block (the `--limit` cut straddled it) carries its inline `total`; the whole-view `shown/total/limit/truncated` rollup rides `meta.truncation` (no `blocks`). **The global `things anytime` / `things someday` views are `data.sections = [{ area, items, total? } …]`** (read-shape v2 PR 5): area-grouped sections in sidebar-rank order, `area` the area node (or `null` for the loose section). Each section's `items` are its flattened rows (area-direct/loose to-dos, then each project row with its capped children); the section carries its inline `total` iff those `items` were capped (by `--area-limit` / `--project-limit`). To flatten: `.data.sections[].items[]`. **NO view emits `meta.truncation.blocks[]` any longer — it is retired from the wire entirely (PR 5); per-bucket completeness is always the inline `total`.**
11
+ - Check `meta.truncation.truncated` before concluding "no match" or "that's everything": it is `true` exactly when any row was hidden (`shown < total` on the whole-view rollup, or any bucket's inline `total` exceeds its shown `items`). Raise `--limit` (or `--area-limit`/`--project-limit` on the grouped views) / `--all`, or narrow the filter, rather than assuming you saw everything.
11
12
  - List/search rows are compact **summaries**: their `tags` field is not necessarily the complete effective set, and placement can be partial. Use `things show <ref> --json` when notes, checklist, placement, or inherited/effective tags matter.
12
13
 
13
14
  ## Exit codes (writes are verified after they land)
@@ -6,15 +6,23 @@ A failed `--json` call carries `ok: false`, `kind: "error"`, and an `error` obje
6
6
 
7
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
8
 
9
- - `uuid`, `title`, and `type` (`"to-do" | "project" | "heading" | "area" | "tag"`) — always.
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
10
  - A container hint `area` and/or `project` as a TITLE string — only when set.
11
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
12
  - NOTHING else — no notes, counts, dates, or raw fields.
13
13
 
14
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
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
+
16
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.
17
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
+
18
26
  Never guess between candidates for a destructive action — inspect details or ask.
19
27
 
20
28
  ## Guarded writes and their acknowledgments
@@ -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 (read-shape v2) a heading is an entry of `headings[]` — `{ uuid, title, archived?, children }` — carrying no `status`/`stage`; it emits a presence-keyed **`archived`** (the ISO archive timestamp) when archived, absent when open. ALL headings live in that one `headings[]` array in order regardless of lifecycle class — open, archived-unswept, AND archived-swept; sweep state is NOT on the wire. A swept heading simply carries `archived` and its logged children ride ITS `children.logbook`; the GUI's grouping of swept headings into the logbook region is TTY-derived from that, not a separate wire array. 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.
@@ -29,9 +29,9 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
29
29
 
30
30
  - **inbox** — untriaged to-dos (no container, no schedule).
31
31
  - **today** — items scheduled for today, with the **This Evening** section beneath.
32
- - **upcoming** — future-dated items, forward-ordered by date.
33
- - **anytime** — all active items kept without a specific date (standalone or filed).
34
- - **someday** — items deliberately kept without a date.
32
+ - **upcoming** — future-dated items, forward-ordered by date. JSON returns `data.sections = [{when, items, total?} …]` — chronological day blocks keyed by `when` (an ISO date), then a trailing `{when: null, items}` block of date-less recurring templates when any exist (flatten with `.data.sections[].items[]`).
33
+ - **anytime** — all active items kept without a specific date (standalone or filed). JSON returns `data.sections = [{area, items, total?} …]` — area-grouped sections (sidebar-rank order; `area` is `null` for the loose section), each carrying its inline `total` iff its rows were capped by `--area-limit`/`--project-limit` (flatten with `.data.sections[].items[]`).
34
+ - **someday** — items deliberately kept without a date. Same `data.sections = [{area, items, total?} …]` shape as anytime.
35
35
  - **logbook** — completed/canceled items.
36
36
  - **trash** — trashed items (restorable until the trash is emptied).
37
37
 
@@ -41,8 +41,8 @@ Entities, relationships, and how the sidebar views are computed over them, as ex
41
41
 
42
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
- - **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly. It is dropped inside a section/catalogue that already states it (the stage-pure `inbox`/`anytime`/`someday`/`logbook`/`trash` lists and the `today` view's sections) and kept everywhere it is not implied (the mixed `upcoming` catalogue, `search`, `changes`, the projects/areas listings, 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`.
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 already states it (the stage-pure `inbox`/`anytime`/`someday`/`logbook`/`trash` lists and the `today` view's two `children` buckets) and kept everywhere it is not implied (the mixed `upcoming` day blocks — future-dated `upcoming` rows beside deadline-forecast `anytime`/`someday` ones — plus `search`, `changes`, the projects/areas listings, 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 returns `data.children = { today: {items, total?}, evening: {items, total?} }` — two keyed buckets (the bucket key states today/evening, so `when` is dropped inside them); the whole-view count rides `meta.counts` (`{dueOrOverdue, other}`).
46
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
47
  - `provisional: true` marks a Today member the app has not yet materialized — see [banner.md](banner.md).
48
48
 
@@ -53,7 +53,9 @@ Every row comes back at one of two densities, selected by view kind + flag, neve
53
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
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
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, a heading group) an item omits any ancestry the node already states, so absent `project`/`area`/`heading` 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).
56
+ - **Container absence rule:** inside a single-container node (a project/area card, an `anytime`/`someday` section, a `project show` `headings[]` entry) an item omits any ancestry the node already states, so absent `project`/`area`/`heading` 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). In a read-shape v2 **`project show`** view membership is fully STRUCTURAL: every bucket row — un-headed body children AND rows nested under a `headings[]` entry, INCLUDING each `children.logbook` row — drops its `heading` ref, because its position states it. A swept child of an OPEN heading rides that heading's `children.logbook`; a swept child of an ARCHIVED heading rides the archived heading's `children.logbook`; the GUI's PROJECT-vs-HEADING logbook sublabel is TTY-derived from that structural placement, not from a per-row ref.
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 node whose kind is fixed by its slot (a `project show` `headings[]` entry) drops `type` too, since its position already states it is a heading.
57
59
 
58
60
  ## Filters over views
59
61
 
@@ -21,14 +21,24 @@ The SKILL.md summary is enough for most calls; this page is the full contract. E
21
21
 
22
22
  ## Reorder — changing arrangement
23
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.
24
+ `things reorder <refs…> [--start | --end | --before <ref> | --after <ref>] [--in <target>]` is the ONE reorder verb across EVERY kind — to-dos, projects, a project's headings, AND sidebar areas — dispatching the protocol by kind (it replaced the old `todo reorder` subcommand and the raw `reorder --scope`). All refs must be one KIND; only to-dos and projects intermix, and only on the shared Today/Evening/day axes.
25
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.
26
+ - **Bare (no position)**: the named items assemble as ONE block at the EARLIEST one's current slot, in argument order (`--start` is NOT implied). Partial selection is fine — unmentioned siblings keep their order.
27
+ - **Positioned**: `--start`/`--end` send the block to the start/end of its scope; `--before <ref>`/`--after <ref>` place it relative to an anchor.
28
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.
29
+ - **Mixed to-do + project refs are allowed** in the Today/Evening/day buckets those kinds share; elsewhere reorder operates within one container+bucket. A heading or area NEVER mixes with another kind — a mixed-kind set is refused with one message.
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
+ - **Resolved to-do movees (LOGSORT ORD-13):** an UNSWEPT completed/canceled to-do — one still struck-through in its live container body, before the Logbook sweep boundary — CAN be reordered in place: it re-ranks by index only, staying resolved (no reopen). This is permitted ONLY on the pure-native index reorder (a project/area/inbox/someday re-rank, which needs `allow-experimental` ON); a mixed OPEN + unswept-resolved set is fine. A SWEPT completed/canceled to-do (a Logbook resident) is refused, pointing at `things todo reopen` (reactivation) or `--completed-at` (to re-date it in the Logbook — sort order can't express Logbook re-dating). An unswept-resolved movee that would have to ride an uncertified path (a bounce/move fallback — e.g. `allow-experimental` OFF — or an `--in <date>` day-axis target) is likewise refused, telling you to reopen it first.
30
32
 
31
- 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
+ Headings and areas are now first-class `reorder` operands (see the two sections below); the kind-specific spellings `things project move-heading` (which ALSO does cross-project moves / demotion — the placement verb) and `things area reorder` (a single sidebar area) remain.
34
+
35
+ ## Reordering a project's headings (#V11)
36
+
37
+ `things reorder <heading-refs…> --start|--end|--before <h>|--after <h>` re-ranks a project's headings in place, dispatching onto the certified native heading-block wire (children follow their heading). All headings must be in ONE project (cross-project is refused). `--in` does not apply (a heading has no stage/day/view axis). Archived headings (completed — unswept OR swept) are reorderable UNGUARDED, exactly like the GUI: the engine composes the minimal wire so an archived heading that need not move stays untouched, but when the target order forces an archived heading to move it PROCEEDS and DISCLOSES that repositioning it brought it back to open (result warning + `--dry-run` leg plan) — never silent. `things project move-heading` remains for cross-project moves and demotion.
38
+
39
+ ## Reordering sidebar areas
40
+
41
+ `things reorder <area-refs…> --start|--end|--before <area>|--after <area>` re-ranks sidebar areas. This drives the local Things app (a sidebar drag), so it needs `--dangerously-drive-gui` plus `things config set ui-enabled true`; a set of areas is a sequence of drags (non-atomic, disclosed). An area reorder needs a position (there is no bucket to assemble a block in). `things area reorder <ref>` is the single-area spelling.
32
42
 
33
43
  ## Anchors POSITION, never MIGRATE
34
44
 
@@ -38,7 +48,14 @@ A `--before`/`--after` anchor only says WHERE in a bucket — it never moves the
38
48
 
39
49
  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.
40
50
 
41
- - `--in` accepts `today | evening | anytime | someday | inbox`, or a project/area/heading ref (uuid or unique title). `loose` is refused (it is a read view, not a bucket).
51
+ **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.
52
+
53
+ - `--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).
54
+ - `--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.
55
+ - `--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.
56
+ - **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, so `--in` never applies to a heading set — reorder headings without `--in` (they dispatch onto the project's heading axis; see "Reordering a project's headings" above).
57
+ - 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).
58
+ - `--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.
42
59
  - 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).
43
60
 
44
61
  ## Mixed-stage move placement
@@ -60,12 +77,21 @@ Three config knobs (`things config get/set …`) tune ordering; every default gi
60
77
 
61
78
  **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.
62
79
 
63
- ## Placement guarantees and the one dead class
80
+ ## Placement guarantees
81
+
82
+ "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.
83
+
84
+ ## Repeating templates in day-blocks
85
+
86
+ 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.
64
87
 
65
- "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). 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.
88
+ - **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.
89
+ - **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.
90
+ - **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.
91
+ - A template's placement write is `userModificationDate`-SILENT (a sync/watcher keyed on that timestamp will not see the move); the result discloses it.
66
92
 
67
- **The one class that cannot be reordered is a repeating TEMPLATE** — a dated `when=` leg crashes it, so a template movee or anchor is refused and template placement stays app-default (disclosed). Everything non-template is sortable.
93
+ Everything non-template is sortable on every guaranteed surface above.
68
94
 
69
95
  ## MCP parity
70
96
 
71
- 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.
97
+ The MCP server exposes reorder as the single **`reorder`** tool — `refs`, an optional position (`start`/`end`/`before`/`after`), an optional `in` axis, and `dangerously_drive_gui` for area drags mirroring `things reorder` across every kind. It calls the same library entry the CLI does, so the kind dispatch, the mixed-kind / cross-container / cross-axis refusals, the #V11 heading disclosure, the dual-axis refusal, the flag-safe routing, and the automatic fallbacks behave identically. (`reorder_areas` was folded into this tool and removed — plan PR D.)