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
@@ -433,9 +433,15 @@ function emitMoveResult(result, opts, meta) {
433
433
  : result.refusal === "unsupported"
434
434
  ? ExitCode.Unsupported
435
435
  : ExitCode.Blocked;
436
+ // A hoisted placement block names its hazard — surface it with the canonical
437
+ // `blocked:<hazard>` code + `BLOCKED (<hazard>)` copy, exactly as a direct
438
+ // `things reorder` hazard block does (emitResult "blocked" case).
439
+ const jsonCode = result.hazard !== undefined
440
+ ? blockedCode({ hazard: result.hazard, reason: "hazard" })
441
+ : result.refusal;
436
442
  if (opts.json) {
437
443
  process.stdout.write(`${JSON.stringify(errorEnvelope({
438
- code: result.refusal,
444
+ code: jsonCode,
439
445
  message: result.detail,
440
446
  ...(result.remediation !== undefined && { remediation: result.remediation }),
441
447
  ...(result.candidates !== undefined && {
@@ -444,7 +450,12 @@ function emitMoveResult(result, opts, meta) {
444
450
  }, meta))}\n`);
445
451
  }
446
452
  else {
447
- process.stderr.write(`${result.refusal === "usage" ? "error" : result.refusal.toUpperCase()}: ${result.detail}\n` +
453
+ const prefix = result.refusal === "usage"
454
+ ? "error"
455
+ : result.hazard !== undefined
456
+ ? `BLOCKED (${result.hazard})`
457
+ : result.refusal.toUpperCase();
458
+ process.stderr.write(`${prefix}: ${result.detail}\n` +
448
459
  (result.remediation !== undefined ? ` remediation: ${result.remediation}\n` : ""));
449
460
  }
450
461
  process.exitCode = code;
@@ -540,6 +551,38 @@ function addPositionFlags(cmd) {
540
551
  .option("--before <ref>", "place the block immediately before this item (same bucket)")
541
552
  .option("--after <ref>", "place the block immediately after this item (same bucket)");
542
553
  }
554
+ /**
555
+ * The universal-`reorder` anchor grammar (spec §7 ratified vocabulary):
556
+ * --start / --end / --before / --after. `--start`/`--end` name the block's top/
557
+ * bottom slot in its scope; --before/--after anchor against a sibling.
558
+ */
559
+ function addReorderPositionFlags(cmd) {
560
+ return cmd
561
+ .option("--start", "place the block at the start of its scope")
562
+ .option("--end", "place the block at the end of its scope")
563
+ .option("--before <ref>", "place the block immediately before this item")
564
+ .option("--after <ref>", "place the block immediately after this item");
565
+ }
566
+ /** Build a MovePosition from the reorder --start/--end/--before/--after flags. */
567
+ function reorderPosition(opts) {
568
+ const chosen = [
569
+ opts["start"] === true,
570
+ opts["end"] === true,
571
+ opts["before"] !== undefined,
572
+ opts["after"] !== undefined,
573
+ ].filter(Boolean).length;
574
+ if (chosen > 1)
575
+ return "conflict";
576
+ if (opts["start"] === true)
577
+ return { at: "first" };
578
+ if (opts["end"] === true)
579
+ return { at: "last" };
580
+ if (opts["before"] !== undefined)
581
+ return { before: opts["before"] };
582
+ if (opts["after"] !== undefined)
583
+ return { after: opts["after"] };
584
+ return undefined;
585
+ }
543
586
  /** One TTY line for a bulk-add batch item (the human, non-JSON multi rendering). */
544
587
  function addResultLine(r) {
545
588
  const o = r.outcome;
@@ -658,6 +701,8 @@ export function registerWriteCommands(program) {
658
701
  .option("--project <ref>", "destination project (uuid or unique name)")
659
702
  .option("--area <ref>", "destination area (uuid or unique name)")
660
703
  .option("--heading <name>", "existing heading in the destination project")
704
+ .option("--created-at <iso>", "born with this creation timestamp (ISO date or datetime; a date is noon in the effective zone)")
705
+ .option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp (ISO date or datetime); drop --when/--reminder")
661
706
  .option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")
662
707
  .option("--stdin", "read newline-delimited titles from stdin (blank lines skipped); exclusive with title arguments")
663
708
  .option("--id-only", "print only the new uuid(s), one per line in creation order, and nothing else (exclusive with --json)"))).action(async (titles, opts) => {
@@ -695,6 +740,8 @@ export function registerWriteCommands(program) {
695
740
  ...(project !== undefined && { project }),
696
741
  ...(area !== undefined && { area }),
697
742
  ...(opts["heading"] !== undefined && { heading: opts["heading"] }),
743
+ ...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
744
+ ...(opts["completedAt"] !== undefined && { completedAt: opts["completedAt"] }),
698
745
  });
699
746
  const ackReopen = opts["acknowledgeProjectReopen"] !== undefined
700
747
  ? { acknowledgeProjectReopen: opts["acknowledgeProjectReopen"] }
@@ -772,7 +819,9 @@ export function registerWriteCommands(program) {
772
819
  .option("--reminder <HH:mm>", "set a reminder (24h); requires --when today|evening|date")
773
820
  .option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
774
821
  .option("--deadline <date>", "YYYY-MM-DD")
775
- .option("--clear-deadline", "remove the deadline")).action(async (uuid, opts) => {
822
+ .option("--clear-deadline", "remove the deadline")
823
+ .option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
824
+ .option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved to-do (a canceled one stays canceled); open to-dos are refused — use complete/cancel")).action(async (uuid, opts) => {
776
825
  const notesModes = ["notes", "appendNotes", "prependNotes"].filter((k) => opts[k] !== undefined);
777
826
  if (notesModes.length > 1) {
778
827
  usageError(opts, "--notes, --append-notes, --prepend-notes are exclusive");
@@ -796,19 +845,30 @@ export function registerWriteCommands(program) {
796
845
  ...(opts["clearReminder"] === true && { reminder: null }),
797
846
  ...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
798
847
  ...(opts["clearDeadline"] === true && { deadline: null }),
848
+ ...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
849
+ ...(opts["completedAt"] !== undefined && { completedAt: opts["completedAt"] }),
799
850
  }, writeOptionsFrom(opts)));
800
851
  });
801
- for (const [verb, method] of [
802
- ["complete", "completeTodo"],
803
- ["cancel", "cancelTodo"],
804
- ["reopen", "reopenTodo"],
805
- ]) {
852
+ for (const verb of ["complete", "cancel"]) {
853
+ const method = verb === "complete" ? "completeTodo" : "cancelTodo";
854
+ const stampNote = verb === "complete"
855
+ ? "resolving it first if needed, then backdating"
856
+ : "keeping it canceled via the certified flip legs";
806
857
  addWriteFlags(todo
807
858
  .command(`${verb} <uuid>`)
808
- .description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do. Not available for repeating to-dos.`)).action(async (uuid, opts) => {
809
- await runWrite(opts, (c) => c.write[method](uuid, writeOptionsFrom(opts)));
859
+ .description(`${verb[0]?.toUpperCase()}${verb.slice(1)} a to-do. Not available for repeating to-dos. ` +
860
+ '--completed-at sets the completion timestamp (also the "Completed on" stamp for a canceled ' +
861
+ `item), ${stampNote} — a multi-leg sequence, disclosed in the result and --dry-run.`)
862
+ .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
863
+ const resolution = opts["completedAt"] !== undefined ? { completedAt: opts["completedAt"] } : {};
864
+ await runWrite(opts, (c) => c.write[method](uuid, resolution, writeOptionsFrom(opts)));
810
865
  });
811
866
  }
867
+ addWriteFlags(todo
868
+ .command("reopen <uuid>")
869
+ .description("Reopen a to-do. Not available for repeating to-dos.")).action(async (uuid, opts) => {
870
+ await runWrite(opts, (c) => c.write.reopenTodo(uuid, writeOptionsFrom(opts)));
871
+ });
812
872
  addPositionFlags(addWriteFlags(todo
813
873
  .command("move <refs...>")
814
874
  .description("Move one or more to-dos as an ordered block (the argument order is the order they " +
@@ -975,35 +1035,10 @@ export function registerWriteCommands(program) {
975
1035
  return;
976
1036
  await runWrite(opts, (c) => c.write.clearReminder(uuid, writeOptionsFrom(opts)));
977
1037
  });
978
- addWriteFlags(todo
979
- .command("backdate <uuid>")
980
- .description("Rewrite a to-do's completion and/or creation timestamp to noon (local) on the " +
981
- "given date. --completed-on requires the to-do to already be completed or " +
982
- "canceled. The Logbook re-sorts to the new date.")
983
- .option("--completed-on <date>", "YYYY-MM-DD — new completion date")
984
- .option("--created-on <date>", "YYYY-MM-DD — new creation date")).action(async (uuid, opts) => {
985
- await runWrite(opts, (c) => c.write.backdateTodo(uuid, {
986
- ...(opts["completedOn"] !== undefined && {
987
- completionDate: opts["completedOn"],
988
- }),
989
- ...(opts["createdOn"] !== undefined && { creationDate: opts["createdOn"] }),
990
- }, writeOptionsFrom(opts)));
991
- });
992
- addWriteFlags(todo
993
- .command("add-logged <title>")
994
- .description("Create a to-do directly in the Logbook: completed, with the given past " +
995
- "completion date (and optionally a past creation date). For importing history " +
996
- "from another system.")
997
- .requiredOption("--completed-on <date>", "YYYY-MM-DD — completion date (required)")
998
- .option("--created-on <date>", "YYYY-MM-DD — creation date (must be <= completed-on)")
999
- .option("--notes <text>", "notes body")).action(async (title, opts) => {
1000
- await runWrite(opts, (c) => c.write.addLoggedTodo({
1001
- title,
1002
- completionDate: opts["completedOn"],
1003
- ...(opts["createdOn"] !== undefined && { creationDate: opts["createdOn"] }),
1004
- ...(opts["notes"] !== undefined && { notes: opts["notes"] }),
1005
- }, writeOptionsFrom(opts)));
1006
- });
1038
+ // NB: the bespoke `todo backdate` / `todo add-logged` commands were removed
1039
+ // (plan PR A). Backdated creation and Logbook import are now `todo add
1040
+ // --created-at/--completed-at`; rewriting an existing item's timestamps is
1041
+ // `todo update --created-at/--completed-at` (or complete/cancel --completed-at).
1007
1042
  // --- ui vector: GUI-driven transforms (two-key gated) --------------------
1008
1043
  const REPEAT_FREQ_HELP = "daily | weekly | monthly | yearly";
1009
1044
  const REPEAT_INTERVAL_HELP = "every N units (1–99)";
@@ -1288,7 +1323,9 @@ export function registerWriteCommands(program) {
1288
1323
  .option("--area <ref>", "destination area (uuid or unique name)")
1289
1324
  .option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
1290
1325
  .option("--deadline <date>", "YYYY-MM-DD")
1291
- .option("--todo <title>", "initial child to-do, repeatable (seeds the new project)", collect, [])).action(async (title, opts) => {
1326
+ .option("--todo <title>", "initial child to-do, repeatable (seeds the new project)", collect, [])
1327
+ .option("--created-at <iso>", "born with this creation timestamp (ISO date or datetime; a date is noon in the effective zone)")
1328
+ .option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp; cannot seed open child to-dos (§5b)")).action(async (title, opts) => {
1292
1329
  const todos = opts["todo"];
1293
1330
  const area = containerRef(opts["area"]);
1294
1331
  await runWrite(opts, (c) => c.write.addProject({
@@ -1298,6 +1335,8 @@ export function registerWriteCommands(program) {
1298
1335
  ...(opts["when"] !== undefined && { when: opts["when"] }),
1299
1336
  ...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
1300
1337
  ...(todos.length > 0 && { todos }),
1338
+ ...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
1339
+ ...(opts["completedAt"] !== undefined && { completedAt: opts["completedAt"] }),
1301
1340
  }, writeOptionsFrom(opts)));
1302
1341
  });
1303
1342
  addWriteFlags(project
@@ -1316,6 +1355,8 @@ export function registerWriteCommands(program) {
1316
1355
  .option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
1317
1356
  .option("--deadline <date>", "YYYY-MM-DD")
1318
1357
  .option("--clear-deadline", "remove the deadline")
1358
+ .option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
1359
+ .option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved project (a canceled one stays canceled); open projects are refused — use complete/cancel")
1319
1360
  .action(async (uuid, opts) => {
1320
1361
  const notesModes = ["notes", "appendNotes", "prependNotes"].filter((k) => opts[k] !== undefined);
1321
1362
  if (notesModes.length > 1) {
@@ -1342,6 +1383,10 @@ export function registerWriteCommands(program) {
1342
1383
  ...(opts["clearReminder"] === true && { reminder: null }),
1343
1384
  ...(opts["deadline"] !== undefined && { deadline: opts["deadline"] }),
1344
1385
  ...(opts["clearDeadline"] === true && { deadline: null }),
1386
+ ...(opts["createdAt"] !== undefined && { createdAt: opts["createdAt"] }),
1387
+ ...(opts["completedAt"] !== undefined && {
1388
+ completedAt: opts["completedAt"],
1389
+ }),
1345
1390
  }, writeOptionsFrom(opts)));
1346
1391
  });
1347
1392
  addCreateTagsFlag(addWriteFlags(project
@@ -1396,9 +1441,17 @@ export function registerWriteCommands(program) {
1396
1441
  .command("cancel <ref>")
1397
1442
  .description("Cancel a project (target by uuid or unique name). Canceling also cancels its open " +
1398
1443
  "to-dos, so an explicit --children policy is required; already-completed children " +
1399
- "are never altered.")
1400
- .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-cancel (cancel them too)")).action(async (uuid, opts) => {
1401
- await runWrite(opts, (c) => c.write.cancelProject(uuid, { children: opts.children }, writeOptionsFrom(opts)));
1444
+ "are never altered. --completed-at sets the completion timestamp and keeps the project " +
1445
+ "canceled via the certified flip legs (multi-leg, disclosed); it is refused while the " +
1446
+ "project still has open children.")
1447
+ .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-cancel (cancel them too)")
1448
+ .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
1449
+ await runWrite(opts, (c) => c.write.cancelProject(uuid, {
1450
+ children: opts.children,
1451
+ ...(opts["completedAt"] !== undefined && {
1452
+ completedAt: opts["completedAt"],
1453
+ }),
1454
+ }, writeOptionsFrom(opts)));
1402
1455
  });
1403
1456
  addWriteFlags(project
1404
1457
  .command("reopen <ref>")
@@ -1464,9 +1517,16 @@ export function registerWriteCommands(program) {
1464
1517
  addWriteFlags(project
1465
1518
  .command("complete <ref>")
1466
1519
  .description("Complete a project (target by uuid or unique name). Completing also completes its " +
1467
- "open to-dos, so an explicit --children policy is required.")
1468
- .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-complete (complete them too)")).action(async (uuid, opts) => {
1469
- await runWrite(opts, (c) => c.write.completeProject(uuid, { children: opts.children }, writeOptionsFrom(opts)));
1520
+ "open to-dos, so an explicit --children policy is required. --completed-at sets the " +
1521
+ "completion timestamp and backdates it (multi-leg, disclosed).")
1522
+ .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-complete (complete them too)")
1523
+ .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
1524
+ await runWrite(opts, (c) => c.write.completeProject(uuid, {
1525
+ children: opts.children,
1526
+ ...(opts["completedAt"] !== undefined && {
1527
+ completedAt: opts["completedAt"],
1528
+ }),
1529
+ }, writeOptionsFrom(opts)));
1470
1530
  });
1471
1531
  addWriteFlags(project
1472
1532
  .command("delete <ref>")
@@ -1806,27 +1866,30 @@ export function registerWriteCommands(program) {
1806
1866
  client?.close();
1807
1867
  }
1808
1868
  });
1809
- addPositionFlags(addWriteFlags(program
1869
+ addReorderPositionFlags(addDriveGuiFlag(addWriteFlags(program
1810
1870
  .command("reorder <refs...>")
1811
- .description("Rearrange to-dos (and the project rows the Today/Evening/day lists intermix with " +
1812
- "them) IN PLACE within the container and bucket they already share REARRANGES, " +
1813
- "never changes membership (to change what an item belongs to, use `things todo " +
1814
- "move` / `things project move`). Argument order is the resulting order; unmentioned " +
1815
- "siblings keep theirs. Bare (no position) assembles the named items as a block at " +
1816
- "the EARLIEST one's current slot (partial-selection friendly); --first/--last/" +
1817
- "--before/--after position the block. Operands that span containers or buckets fail " +
1818
- "closed. A Today/Evening member also has an index slot in its container, so a set " +
1819
- "sharing BOTH axes is ambiguous pass --in to say which (the refusal names both " +
1820
- "spellings). Ordering uses the native re-rank where available (private surface, on " +
1821
- "by default) and a verified when= bounce otherwise; bounce-max-items caps a bounce, " +
1822
- "bounce-enabled=false refuses bounce-dependent placements rather than degrading. " +
1823
- "For a project's HEADINGS use `things project move-heading`; for sidebar AREAS use " +
1824
- "`things area reorder`.")
1825
- .option("--in <target>", "disambiguate the axis of a Today/Evening set: today | evening | anytime | someday | " +
1826
- "inbox, or a project/area/heading ref (uuid or unique title)"))).action(async (refs, opts) => {
1827
- const position = movePosition(opts);
1871
+ .description("The ONE reorder verb — rearrange to-dos, projects, headings, OR sidebar areas IN " +
1872
+ "PLACE (REARRANGES, never changes membership; to change what an item belongs to use " +
1873
+ "`things todo move` / `things project move`). All operands must be ONE kind (only " +
1874
+ "to-dos and projects intermix, and only on the shared Today/Evening/day axes); a " +
1875
+ "mixed-kind set, a cross-container set, and a non-member anchor each fail closed with " +
1876
+ "one precise message. Argument order is the resulting order; unmentioned siblings " +
1877
+ "keep theirs. Bare (no position) assembles the named items as a block at the " +
1878
+ "EARLIEST one's current slot; --start/--end/--before/--after position the block. A " +
1879
+ "Today/Evening member also has an index slot in its container, so a set sharing BOTH " +
1880
+ "axes is ambiguous pass --in to say which. HEADINGS: same-project heading " +
1881
+ "re-ranking runs the native heading-block wire; an archived heading is reorderable " +
1882
+ "but repositioning it brings it back to open (disclosed). AREAS: this drives the " +
1883
+ "local Things app (sidebar drag). `things area reorder` and `things project " +
1884
+ "move-heading` remain as kind-specific spellings.")
1885
+ .option("--in <target>", "to-dos/projects only — name the axis to reorder on: today | evening | anytime | " +
1886
+ "someday | inbox, a project/area/heading ref (uuid or unique title), upcoming (the " +
1887
+ "one future day the set shares), or a YYYY-MM-DD day-block. A stage-list or container " +
1888
+ "axis sorts one KIND at a time; only today | evening | upcoming | a day-block " +
1889
+ "intermix both kinds.")))).action(async (refs, opts) => {
1890
+ const position = reorderPosition(opts);
1828
1891
  if (position === "conflict") {
1829
- usageError(opts, "pass at most one of --first/--last/--before/--after");
1892
+ usageError(opts, "pass at most one of --start / --end / --before / --after");
1830
1893
  return;
1831
1894
  }
1832
1895
  const request = {
@@ -1834,7 +1897,7 @@ export function registerWriteCommands(program) {
1834
1897
  ...(position !== undefined && { position }),
1835
1898
  ...(opts["in"] !== undefined && { in: opts["in"] }),
1836
1899
  };
1837
- await runMoveCmd(opts, (c) => c.write.reorderTodos(request, writeOptionsFrom(opts)));
1900
+ await runMoveCmd(opts, (c) => c.write.reorderAny(request, writeOptionsFrom(opts)));
1838
1901
  });
1839
1902
  program
1840
1903
  .command("capabilities")
@@ -1903,7 +1966,7 @@ export function registerWriteCommands(program) {
1903
1966
  .description("Persist a config key: profile | maxDisruption | actor | auditEnabled | " +
1904
1967
  "accepted-fingerprint | certified-app-version | allow-experimental | bounce-enabled | " +
1905
1968
  "bounce-max-items | ui-enabled | scope")
1906
- .action((key, value) => {
1969
+ .action((key, value, opts) => {
1907
1970
  const map = {
1908
1971
  profile: "profile",
1909
1972
  maxDisruption: "maxDisruption",
@@ -1931,6 +1994,14 @@ export function registerWriteCommands(program) {
1931
1994
  target === "uiEnabled"
1932
1995
  ? value === "true"
1933
1996
  : value;
1997
+ // Universal `--dry-run` (../dry-run.ts): `config set` writes local config
1998
+ // state (not the Things DB), so it honors the flag with an honest preview —
1999
+ // the key is still validated above, but nothing is persisted. Exit 0.
2000
+ if (opts.dryRun === true) {
2001
+ process.stdout.write(`DRY RUN would set ${key} = ${String(parsed)} (nothing written)\n`);
2002
+ process.exitCode = ExitCode.Ok;
2003
+ return;
2004
+ }
1934
2005
  saveConfigKey(target, parsed);
1935
2006
  process.stdout.write(`set ${key} = ${String(parsed)}\n`);
1936
2007
  // A stored scope jails EVERY process on this host — including this