@torsday/omnifocus-mcp 1.3.0 → 2.0.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 (4) hide show
  1. package/CHANGELOG.md +347 -22
  2. package/README.md +31 -728
  3. package/dist/index.js +2525 -677
  4. package/package.json +27 -14
package/CHANGELOG.md CHANGED
@@ -5,48 +5,363 @@ All notable changes to `@torsday/omnifocus-mcp` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See [ADR-0011](./docs/adr/0011-versioning-and-stability.md) for the explicit definition of breaking vs additive changes in this project.
6
6
 
7
7
 
8
- ## [1.3.0](https://github.com/torsday/omnifocus-mcp/compare/v1.2.2...v1.3.0) (2026-05-09)
8
+ ## [2.0.0](https://github.com/torsday/omnifocus-mcp/compare/v1.5.3...v2.0.0) (2026-06-05)
9
+
10
+ **Summary** — A major release dominated by **token-efficiency** and **OmniFocus 4.x correctness**. The one breaking change is a wire-format slimming: tool responses no longer duplicate the full envelope JSON into `content[].text` (it's now a fixed `"see structuredContent"` placeholder), which roughly halves per-response bytes for clients already reading the typed `structuredContent` — i.e. nearly all of them. On top of that, `maxOutputBytes` caps with a truncation envelope now bound every heavy read, an init-handshake negotiates response density per session, and a `flattenedX.byId()` migration across ~20 JXA scripts turns linear per-call Apple-event scans into direct lookups (50–500× on large databases). Correctness-wise, this release finally makes **repetition rules round-trip end-to-end** on OmniFocus 4.x (both the write and the read-back were broken), fixes `task_batch_create` (every item failed with -10024), makes `project_create` atomic, and corrects timezone bucketing in `forecast_get`. New operational surface: an `omnifocus_doctor` self-diagnostic, per-tool/per-transport latency aggregators, a transport circuit breaker, modal/sync-locked detection (`OF_BUSY`), and an opt-in persistent `osascript` transport (default off, soaking before it becomes default in a later release).
11
+
12
+ **Compatibility** — Node 24+ • macOS 12+ • OmniFocus 4.x • MCP protocol 2024-11-05. **One breaking change** (`content[].text`); see the Migration note below and [`docs/migrations.md`](./docs/migrations.md).
13
+
14
+ **Migration (v1 → v2)** — If your client reads `result.structuredContent` (the typed envelope), **no action is needed** — that field is unchanged in shape and content. If your client parses the JSON string in `result.content[].text`, switch to `structuredContent`, or set `OMNIFOCUS_LEGACY_TEXT_CONTENT=1` in the server environment as a temporary bridge that restores the v1 duplicated-text behavior (read once at startup; the flag is supported indefinitely). Full rationale in [ADR-0022](./docs/adr/0022-envelope-text-content-duplication.md); step-by-step guide in [`docs/migrations.md`](./docs/migrations.md).
15
+
16
+ ### Highlights
9
17
 
18
+ - **Token efficiency** — `content[].text` deduplication (≈2× smaller responses), `maxOutputBytes` truncation caps on `task_list` / `search_query` / `project_list` / `get_many` / `tag_list` / `forecast_get`, init-handshake response-density negotiation, and `_links` made opt-in (default off) so the HATEOAS block no longer rides along uninvited.
19
+ - **Repetition CRUD fixed end-to-end** — writes now persist via OmniJS `Task.RepetitionRule` and reads parse the OF 4.x `recurrence` RRULE correctly (previously every repetition read returned `null`); `start-again` now maps to the real `DeferUntilDate` method instead of silently degrading to `fixed`. Closes [#938](https://github.com/torsday/omnifocus-mcp/issues/938), [#1071](https://github.com/torsday/omnifocus-mcp/issues/1071).
20
+ - **OmniFocus 4.x write correctness** — `task_batch_create` no longer fails every item with -10024 ([#1074](https://github.com/torsday/omnifocus-mcp/issues/1074)); `project_create` with a review interval is now atomic and honors the interval ([#1073](https://github.com/torsday/omnifocus-mcp/issues/1073)); `forecast_get` buckets by local day rather than UTC ([#1035](https://github.com/torsday/omnifocus-mcp/issues/1035), [#1036](https://github.com/torsday/omnifocus-mcp/issues/1036)).
21
+ - **Performance** — `flattenedX.byId()` migration across the JXA read/write surface ([#788](https://github.com/torsday/omnifocus-mcp/issues/788)) replaces O(n) Apple-event scans with direct id lookups; an opt-in persistent `osascript` transport (behind `OMNIFOCUS_PERSISTENT_OSASCRIPT`, default off) showed a 73% cold-p95 drop in micro-benchmarks and is soaking before the default flips.
22
+ - **Operability** — new `omnifocus_doctor` self-diagnostic tool, per-tool and per-transport latency/duration aggregators in `internal_status`, an opt-in JSONL telemetry sink, a transport-level circuit breaker, and modal/sync-locked detection surfaced as `OF_BUSY`.
23
+
24
+ The categorized commit-level detail follows.
25
+
26
+ ### ⚠ BREAKING CHANGES
27
+
28
+ * **envelope:** `content[].text` no longer contains the full envelope JSON in default mode. Clients that read the typed `structuredContent` are unaffected. Clients that parse `content[].text` should either migrate to `structuredContent` or set `OMNIFOCUS_LEGACY_TEXT_CONTENT=1` server-side as a temporary bridge. Per ADR-0011, this lands as v2.0.0.
10
29
 
11
30
  ### Added
12
31
 
13
- * **observability:** per-tool response-size telemetry ([#778](https://github.com/torsday/omnifocus-mcp/issues/778)) ([79cace2](https://github.com/torsday/omnifocus-mcp/commit/79cace2f8050d26bb73181c6dcd4325fc8a02ad3))
14
- * **tools:** default-truncate task notes in bulk reads ([#775](https://github.com/torsday/omnifocus-mcp/issues/775)) ([57dc1ba](https://github.com/torsday/omnifocus-mcp/commit/57dc1bae461e0630b00ca98fde98e06c377d9acb))
32
+ * **envelope:** add maxOutputBytes cap with truncation envelope ([#776](https://github.com/torsday/omnifocus-mcp/issues/776)) ([f41d488](https://github.com/torsday/omnifocus-mcp/commit/f41d4885fa7be5dd68443d1669e0dda1029ddc1a))
33
+ * **envelope:** implement adr-0022 placeholder content[].text + OMNIFOCUS_LEGACY_TEXT_CONTENT ([2c0c28b](https://github.com/torsday/omnifocus-mcp/commit/2c0c28ba6f4b0b8020bf93b10fa4ae50bbe3d040))
34
+ * **forecast:** single-call composite for forecast-tag get/set ([d6f60e4](https://github.com/torsday/omnifocus-mcp/commit/d6f60e434cc78e9e7f10ecd0c7f49468907bf786))
35
+ * **lifecycle:** add omnifocus_doctor self-diagnostic tool ([#970](https://github.com/torsday/omnifocus-mcp/issues/970)) ([db06a9f](https://github.com/torsday/omnifocus-mcp/commit/db06a9f4901f85ab59d7435f1447ab80dad3d8b1))
36
+ * **observability:** add per-tool duration aggregator (toolDurationStats) ([#965](https://github.com/torsday/omnifocus-mcp/issues/965)) ([8c6e9ed](https://github.com/torsday/omnifocus-mcp/commit/8c6e9edc7440d0b0b5745d45e0859b31b534a94e))
37
+ * **observability:** add per-transport latency aggregator (latencyStats) ([#964](https://github.com/torsday/omnifocus-mcp/issues/964)) ([68988b4](https://github.com/torsday/omnifocus-mcp/commit/68988b4436ec596f1122d7bfb7f873257f8fb491))
38
+ * **observability:** opt-in jsonl telemetry sink for offline trend analysis ([2ac94fb](https://github.com/torsday/omnifocus-mcp/commit/2ac94fbb43e1bb368affd11e8a539e066b31653b))
39
+ * **scripts:** [@ts-check](https://github.com/ts-check) beachhead on task_get.js ([#987](https://github.com/torsday/omnifocus-mcp/issues/987)) ([#991](https://github.com/torsday/omnifocus-mcp/issues/991)) ([eb91322](https://github.com/torsday/omnifocus-mcp/commit/eb9132223a79b9ca0f2f2304d76be34d860b2fee))
40
+ * **scripts:** add argv jsdoc to every jxa script ([#994](https://github.com/torsday/omnifocus-mcp/issues/994) part 2) ([acb98b4](https://github.com/torsday/omnifocus-mcp/commit/acb98b400aee7d35ef40405e4fb898c5db8d3983))
41
+ * **scripts:** add verify-view-grouping.sh to catch silent project board view drift ([31761e8](https://github.com/torsday/omnifocus-mcp/commit/31761e80b0665f66cefd9026347a7c4459eda224)), closes [#947](https://github.com/torsday/omnifocus-mcp/issues/947)
42
+ * **scripts:** ambient _types/jxa-helpers.d.ts for inlined helpers ([#994](https://github.com/torsday/omnifocus-mcp/issues/994) part 3) ([d4207dc](https://github.com/torsday/omnifocus-mcp/commit/d4207dc700f09b2eb2993601836def516a5a6543))
43
+ * **scripts:** document-bubbled application intersection + perspective_evaluate ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 8) ([7982d37](https://github.com/torsday/omnifocus-mcp/commit/7982d37f235b55fe3e7bf667b8cc3f669f5dd919))
44
+ * **scripts:** fill jxa-globals + foundation typing gaps ([#994](https://github.com/torsday/omnifocus-mcp/issues/994) part 1) ([6c192a6](https://github.com/torsday/omnifocus-mcp/commit/6c192a6f3a3a12ee092ea1650e1e4010932797d4))
45
+ * **scripts:** generalize element-accessor type + 3 more opt-ins ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 6) ([7defabe](https://github.com/torsday/omnifocus-mcp/commit/7defabe2c604818f46d2e0f48ef4bd37a35faaa2))
46
+ * **scripts:** generate jxa type declarations from omnifocus.sdef ([#990](https://github.com/torsday/omnifocus-mcp/issues/990)) ([a7c83b5](https://github.com/torsday/omnifocus-mcp/commit/a7c83b536e338ad9eb76414296ed8624cac646c1))
47
+ * **scripts:** hand-maintained sdef-overrides .d.ts for runtime extras ([#999](https://github.com/torsday/omnifocus-mcp/issues/999)) ([7a7fb31](https://github.com/torsday/omnifocus-mcp/commit/7a7fb31a351c8cd293d94b6c39890411e86276b2))
48
+ * **scripts:** jxa-collection<t> for element-query api ([#994](https://github.com/torsday/omnifocus-mcp/issues/994) part 4) ([b9e44d5](https://github.com/torsday/omnifocus-mcp/commit/b9e44d5dab5269576a2586464338bfabeeb79f22))
49
+ * **scripts:** opt 10 task writers into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 12) ([49038b5](https://github.com/torsday/omnifocus-mcp/commit/49038b511fba08d4f0a2e5f22de645e9c44f9654))
50
+ * **scripts:** opt 3 attachment scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 3) ([47ea732](https://github.com/torsday/omnifocus-mcp/commit/47ea732cd8d7b7a90dd0eedb7797d1e232edd8a5))
51
+ * **scripts:** opt 3 task batch scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 13) ([9038f4f](https://github.com/torsday/omnifocus-mcp/commit/9038f4ff53922bb3319a10c53110f242addebdd1))
52
+ * **scripts:** opt 4 project/task setter scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 15) ([871c67b](https://github.com/torsday/omnifocus-mcp/commit/871c67b078caa95bafb5a81e7e8f24336f368f00))
53
+ * **scripts:** opt 4 write-verb scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 11) ([e2b86db](https://github.com/torsday/omnifocus-mcp/commit/e2b86dbd10b4d2d1c9ff51f08a0ef581a067493d))
54
+ * **scripts:** opt 5 batch+move scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 14) ([e21856f](https://github.com/torsday/omnifocus-mcp/commit/e21856fc4cc886459569b531b684dca6d86d2151))
55
+ * **scripts:** opt 6 crud writers into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 10) ([11c5e3e](https://github.com/torsday/omnifocus-mcp/commit/11c5e3e34ded26cfa01d20c56351df090c847f15))
56
+ * **scripts:** opt folder/tag readers into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 4) ([a0ecb92](https://github.com/torsday/omnifocus-mcp/commit/a0ecb929663ed3104fc5ebd831f6734c4e2b3b80))
57
+ * **scripts:** opt forecast + 3 window scripts into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 9) ([265bd5c](https://github.com/torsday/omnifocus-mcp/commit/265bd5c18ea19efc7c6c497ab9bbca7cba68687d))
58
+ * **scripts:** opt ping.js into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 1) ([e24d27d](https://github.com/torsday/omnifocus-mcp/commit/e24d27d08199bab1a4762c44985270568f38ac4e))
59
+ * **scripts:** opt project_create + record-typed constructor proxies ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 16) ([0da885b](https://github.com/torsday/omnifocus-mcp/commit/0da885b2da4301eaee046b40e47de71587d37998))
60
+ * **scripts:** opt project_get + perspective_list into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 5) ([e6427f8](https://github.com/torsday/omnifocus-mcp/commit/e6427f8184cc55e516aef32548f097d01fd06687))
61
+ * **scripts:** opt sync_trigger + tag_get_many into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 7) ([a73aab0](https://github.com/torsday/omnifocus-mcp/commit/a73aab0c06208267d90e81bdfdc92098fb386fce))
62
+ * **scripts:** opt task_batch_create into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 19) ([df1bdf1](https://github.com/torsday/omnifocus-mcp/commit/df1bdf102f868bf4b1c29741e21de7bca2b69c13))
63
+ * **scripts:** opt task_create into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 17) ([02d5cb1](https://github.com/torsday/omnifocus-mcp/commit/02d5cb1d847f4799a14a4c796fced80468131459))
64
+ * **scripts:** opt task_duplicate into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 22 — final) ([9cf89e6](https://github.com/torsday/omnifocus-mcp/commit/9cf89e677ff5ff87ef629978c7709c92d8580b5d))
65
+ * **scripts:** opt task_list into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 21) ([a283189](https://github.com/torsday/omnifocus-mcp/commit/a28318943b8a067996721afd3e68ab3faee18c6a))
66
+ * **scripts:** opt task_search into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 20) ([8a99a6e](https://github.com/torsday/omnifocus-mcp/commit/8a99a6e308671154466a23ac348a67bfe12c8ddc))
67
+ * **scripts:** opt task_update into ts-check ([#989](https://github.com/torsday/omnifocus-mcp/issues/989) slice 18) ([c243634](https://github.com/torsday/omnifocus-mcp/commit/c243634b4822db35ae8860849146d68b5e71a91e))
68
+ * **scripts:** wire Model Queue project field through file-issue.sh and verify-constants.sh ([26e1c85](https://github.com/torsday/omnifocus-mcp/commit/26e1c85319fc24f3c29d8b4063476a6286e425e2))
69
+ * **sync:** add changes_since tool with field-level sync deltas ([cc02f9e](https://github.com/torsday/omnifocus-mcp/commit/cc02f9ef3f850368aac046a3ebd572518677158c))
70
+ * **sync:** report deletions in changes_since via opt-in includeRemoved ([faa834c](https://github.com/torsday/omnifocus-mcp/commit/faa834cef74f0100452bdca7d263719dda0bdbc0))
71
+ * **tools:** add idempotency_key to decision_record and note_append ([#984](https://github.com/torsday/omnifocus-mcp/issues/984)) ([58b746e](https://github.com/torsday/omnifocus-mcp/commit/58b746eddea1f0b3e3977117a42934cfbb274534))
72
+ * **tools:** add idempotency_key to remaining task batch tools ([#986](https://github.com/torsday/omnifocus-mcp/issues/986)) ([1cb1368](https://github.com/torsday/omnifocus-mcp/commit/1cb1368645c805ad8bd923a4d51ed9641e5d9a8a))
73
+ * **tools:** add limit + cursor pagination to perspective_evaluate ([#967](https://github.com/torsday/omnifocus-mcp/issues/967)) ([c24bac1](https://github.com/torsday/omnifocus-mcp/commit/c24bac1b044a5528cda8b5a00447c7a6e5ce23e7))
74
+ * **tools:** add limit and cursor pagination to forecast_get ([#966](https://github.com/torsday/omnifocus-mcp/issues/966)) ([5fe752f](https://github.com/torsday/omnifocus-mcp/commit/5fe752f37559985d5ccd7a85a36e5e334fc8803e))
75
+ * **tools:** byte-cap forecast_get with bucket-aware trimming ([#1065](https://github.com/torsday/omnifocus-mcp/issues/1065)) ([734dc46](https://github.com/torsday/omnifocus-mcp/commit/734dc46b1f7034b95a42a55534d95a57e8e1807b))
76
+ * **tools:** byte-cap tag_list + add cap-truncation token-cost benchmark ([#1062](https://github.com/torsday/omnifocus-mcp/issues/1062)) ([2047b95](https://github.com/torsday/omnifocus-mcp/commit/2047b95dd1029424eb1b89bf5d2a8b7aef84d633))
77
+ * **tools:** byte-cap the get_many reads with a dropped-id contract ([#1060](https://github.com/torsday/omnifocus-mcp/issues/1060)) ([8860242](https://github.com/torsday/omnifocus-mcp/commit/88602425d746028e5500a626301da7f82d904b5d))
78
+ * **tools:** extend maxOutputBytes cap to search_query and project_list ([#1059](https://github.com/torsday/omnifocus-mcp/issues/1059)) ([5155585](https://github.com/torsday/omnifocus-mcp/commit/5155585a089a948a5c71460d85423f589dc6579a))
79
+ * **transport:** add transport-level circuit breaker for sustained omnifocus failures ([#968](https://github.com/torsday/omnifocus-mcp/issues/968)) ([923776b](https://github.com/torsday/omnifocus-mcp/commit/923776b1fbb5b3579f535c14ea7aec0d2a0f6987))
80
+ * **transport:** detect omnifocus modal/sync-locked state and surface as ofbusy ([#969](https://github.com/torsday/omnifocus-mcp/issues/969)) ([f51b2d7](https://github.com/torsday/omnifocus-mcp/commit/f51b2d7eaca206762b4339bb8ebc138164e049af))
81
+ * **transport:** negotiate response density at the MCP init handshake ([223c036](https://github.com/torsday/omnifocus-mcp/commit/223c036d656c6bf40d3e533f521e690edc43dfd6))
15
82
 
16
83
 
17
84
  ### Fixed
18
85
 
19
- * **ci:** install shellcheck+actionlint via apt/script on ubuntu-latest ([508f7b2](https://github.com/torsday/omnifocus-mcp/commit/508f7b296ca21f4aaa13a4bf158bd01cc965b418))
20
- * **in-memory:** skip project completedTaskCount bump when task state unchanged ([e5da6e4](https://github.com/torsday/omnifocus-mcp/commit/e5da6e41f15badc0e5b924203379806bc74b513a))
21
- * **jxa:** route task tag mutations through OmniJS to defeat silent no-op ([c0304c5](https://github.com/torsday/omnifocus-mcp/commit/c0304c57a6eca2398fde7330d9eff2d163d79f4b))
22
- * **jxa:** use container() not parent() for tag parent retrieval (OF 4.x) ([ba4abc5](https://github.com/torsday/omnifocus-mcp/commit/ba4abc53e327c31ac92342f0d79cc39dbc3daf84))
23
- * **observability:** hash nested args correctly and survive null/undefined ([dcec35c](https://github.com/torsday/omnifocus-mcp/commit/dcec35c0f948ac5cc771cfcf8b570137097c092c))
24
- * **pagination:** hashFilter must sort nested object keys for stable cursor filterHash ([f315a36](https://github.com/torsday/omnifocus-mcp/commit/f315a368a35880fedda3d88e79b90d4f8c33383d))
25
- * **server:** register recursive zod schemas to unblock tools/list ([1e0a1d5](https://github.com/torsday/omnifocus-mcp/commit/1e0a1d5f39835416190d9899ce6c34d86d0d9fab))
26
- * **webhooks:** register res.on('error') so dispatch never throws upward ([3f988e5](https://github.com/torsday/omnifocus-mcp/commit/3f988e5160fe1e093241288c2e4dd67ab730a3a5))
86
+ * **bench:** align run.test.ts workflow list with cli.ts ([864bca8](https://github.com/torsday/omnifocus-mcp/commit/864bca81461de4581f44916cb183272b8505e0c3))
87
+ * **bench:** make the token-cost gate environment-robust ([30e42c4](https://github.com/torsday/omnifocus-mcp/commit/30e42c4fe735820ab9b384fd8e9291628aec4deb))
88
+ * **benchmark:** drop non-null assertion in snapshot byTool sort ([9f58f4d](https://github.com/torsday/omnifocus-mcp/commit/9f58f4d651a109a28bb3f48ee80b390108fb98e2))
89
+ * **ci:** add allow-hosted marker and timeout-minutes to paths-changed job ([1196f30](https://github.com/torsday/omnifocus-mcp/commit/1196f30cbdce2f30fb316d94599aa6fab1962ac8))
90
+ * **ci:** reset omnifocus process state before integration seed ([#959](https://github.com/torsday/omnifocus-mcp/issues/959)) ([44ca870](https://github.com/torsday/omnifocus-mcp/commit/44ca870610da6e71e3e6d954edbaa63035aa1cb9))
91
+ * **deps:** dedupe broken pnpm-lock.yaml after knip + dev-deps merges ([#1098](https://github.com/torsday/omnifocus-mcp/issues/1098)) ([63ba4d7](https://github.com/torsday/omnifocus-mcp/commit/63ba4d7dfa949862a2db921a1f31c17a1ebac3cf))
92
+ * **forecast:** bucket byDate keys by local day, not UTC day ([#1035](https://github.com/torsday/omnifocus-mcp/issues/1035)) ([adc3f3e](https://github.com/torsday/omnifocus-mcp/commit/adc3f3e90d6fd289f84a1501a175c0ff227c6664))
93
+ * **forecast:** TZ-aware start-of-day in resolveAnchorDate ([#1036](https://github.com/torsday/omnifocus-mcp/issues/1036)) ([3a99df8](https://github.com/torsday/omnifocus-mcp/commit/3a99df8baf9056144dd45dd92cb6454fb615dd65))
94
+ * **jxa:** close jxa-tsc errors from [#899](https://github.com/torsday/omnifocus-mcp/issues/899) in perspective_evaluate ([1694d85](https://github.com/torsday/omnifocus-mcp/commit/1694d8531006bb12bb0454ce7766272cabc83c6a))
95
+ * **lifecycle:** kill orphan osascript children on shutdown ([babd043](https://github.com/torsday/omnifocus-mcp/commit/babd043852cca3e38fa3ada35b9b11996f3514e4))
96
+ * **omnijs:** non-recursive task_duplicate copies repetition rules and attachments ([#1068](https://github.com/torsday/omnifocus-mcp/issues/1068)) ([bf1bb71](https://github.com/torsday/omnifocus-mcp/commit/bf1bb711753737ee0832137f802640ba95ba328d))
97
+ * **project:** project_create with reviewIntervalDays is atomic and honors the interval ([#1073](https://github.com/torsday/omnifocus-mcp/issues/1073)) ([9c40ad2](https://github.com/torsday/omnifocus-mcp/commit/9c40ad2d7a593086355f895d34346ba51dd1de47))
98
+ * **repetition:** read OF 4.x recurrence and use DeferUntilDate for start-again ([97caf70](https://github.com/torsday/omnifocus-mcp/commit/97caf70fee4913c1aa51cb1294e9dec866da7ef9)), closes [#1071](https://github.com/torsday/omnifocus-mcp/issues/1071) [#938](https://github.com/torsday/omnifocus-mcp/issues/938)
99
+ * **scripts:** raise validate-deps issue fetch limit to 2000 ([589fb28](https://github.com/torsday/omnifocus-mcp/commit/589fb28fca3f613fb0e833e4a23458012d5a6e60))
100
+ * **scripts:** seed-integration-db --clean tolerates zombie folder refs ([#977](https://github.com/torsday/omnifocus-mcp/issues/977)) ([50ca0a5](https://github.com/torsday/omnifocus-mcp/commit/50ca0a56423ce23fa30d986d438d2ac180f4692b))
101
+ * **strings:** code-point-safe truncation across display sites ([5caf68a](https://github.com/torsday/omnifocus-mcp/commit/5caf68ae76468514f7bed1cfa2a8ebde9fa30a3e))
102
+ * **task:** persist repetition rule via OmniJS in task_update ([1b12f59](https://github.com/torsday/omnifocus-mcp/commit/1b12f5961eda2c2c65d31e34c1003b50496e1f30)), closes [#938](https://github.com/torsday/omnifocus-mcp/issues/938)
103
+ * **task:** split large notes off into phase-2 single-field write ([a5d3af8](https://github.com/torsday/omnifocus-mcp/commit/a5d3af80aa796f48a252d57dbb2274d662a43f27))
104
+ * **task:** task_batch_create uses Task(props)+tasks.push instead of broken .make() ([#1074](https://github.com/torsday/omnifocus-mcp/issues/1074)) ([888d2b5](https://github.com/torsday/omnifocus-mcp/commit/888d2b50f01df4bd7f420030035ca8713489fb29))
27
105
 
28
106
 
29
107
  ### Performance
30
108
 
31
- * **jxa:** scope task_list tagId filter via tag.tasks() to avoid full scan ([a16fe77](https://github.com/torsday/omnifocus-mcp/commit/a16fe77895d5d0ceb93e3798ca7fb0d17fd92793))
32
- * **tools:** elide default-valued fields from heavy read responses ([#774](https://github.com/torsday/omnifocus-mcp/issues/774)) ([7aecd56](https://github.com/torsday/omnifocus-mcp/commit/7aecd564a0efe69e3d9c9a385c1ebbded75ea0fa))
109
+ * **jxa:** migrate attachment + window_set_focus by-id lookups to byId() ([#1087](https://github.com/torsday/omnifocus-mcp/issues/1087)) ([76d2db3](https://github.com/torsday/omnifocus-mcp/commit/76d2db33d2aa9c4906e0d461e08cd9b6bd5505e6))
110
+ * **jxa:** migrate project get/get_many/complete/drop/delete to byId() ([#1085](https://github.com/torsday/omnifocus-mcp/issues/1085)) ([b11aa11](https://github.com/torsday/omnifocus-mcp/commit/b11aa119791463727274f5ac846afebc02e7513e))
111
+ * **jxa:** migrate project review-setters to byId() ([#1089](https://github.com/torsday/omnifocus-mcp/issues/1089)) ([2bbdae6](https://github.com/torsday/omnifocus-mcp/commit/2bbdae6407b90192e8ab5a972e654dade4c15392))
112
+ * **jxa:** migrate tag + folder by-id lookups to flattenedX.byId() ([#1081](https://github.com/torsday/omnifocus-mcp/issues/1081)) ([3d9ade4](https://github.com/torsday/omnifocus-mcp/commit/3d9ade4eabe122497330f1ee70a34f162c2fe726))
113
+ * **jxa:** migrate task get/get_many/complete/uncomplete/delete to byId() ([#1083](https://github.com/torsday/omnifocus-mcp/issues/1083)) ([9d9f526](https://github.com/torsday/omnifocus-mcp/commit/9d9f5262991e0fe30c089826ed265d0193c17311))
114
+ * **jxa:** migrate task/project update+move source lookups to byId() — completes [#788](https://github.com/torsday/omnifocus-mcp/issues/788) ([#1091](https://github.com/torsday/omnifocus-mcp/issues/1091)) ([013af7b](https://github.com/torsday/omnifocus-mcp/commit/013af7b9aae8e3fc2e3e6bcfa3fbaac41d74aae4))
115
+ * **jxa:** source-narrow perspective_evaluate projects+tags branches ([#899](https://github.com/torsday/omnifocus-mcp/issues/899)) ([1def7c5](https://github.com/torsday/omnifocus-mcp/commit/1def7c5d9ad6dde18d7c8c22fe2bdd91738610e7))
116
+ * **observability:** split osascript spawn time from script execution time ([3a299d8](https://github.com/torsday/omnifocus-mcp/commit/3a299d8f06a7fed9ab43bbe8c3fa9d74ce52afa4))
117
+ * **scripts:** seed-integration-db --clean uses omnijs bulk delete + unified prefix ([#961](https://github.com/torsday/omnifocus-mcp/issues/961)) ([b1a60f1](https://github.com/torsday/omnifocus-mcp/commit/b1a60f126345130fecfa04d0fbdef28705f328c0))
118
+ * **scripts:** seed-integration-db create phase migrated to omnijs ([#963](https://github.com/torsday/omnifocus-mcp/issues/963)) ([a9c6886](https://github.com/torsday/omnifocus-mcp/commit/a9c688648f11ea8c8780c1d716dcaefa6568cf63))
119
+ * **tools:** make _links opt-in via includeLinks flag, default off ([b9647b8](https://github.com/torsday/omnifocus-mcp/commit/b9647b8f2488d71b1bbdb3d9813ea86222fe502e))
120
+ * **tools:** trim task_reclassify description below the 350-token budget ([dbb4a71](https://github.com/torsday/omnifocus-mcp/commit/dbb4a71e85899fa551c75c549931c16765cbd8db))
121
+ * **transport:** add persistent osascript transport behind a kill-switch ([e44e605](https://github.com/torsday/omnifocus-mcp/commit/e44e6055ea1396bf221559e978f1bb63445b7001))
33
122
 
34
123
 
35
124
  ### Changed
36
125
 
37
- * **jxa:** inline shared buildFolder helper via [@inline](https://github.com/inline) directive ([e8c7391](https://github.com/torsday/omnifocus-mcp/commit/e8c739140ae1047fa1f6c4bdb6c02e4e602be3d8))
38
- * **jxa:** inline shared buildTag helper via [@inline](https://github.com/inline) directive ([57b0ab0](https://github.com/torsday/omnifocus-mcp/commit/57b0ab05c63ef9e6ed202198e90f33c68bbd9b14))
126
+ * **attachment:** add create/delete tools; deprecate add/remove ([86d95b6](https://github.com/torsday/omnifocus-mcp/commit/86d95b6a85da04137e8915320ee99f559a4bff1c))
39
127
 
40
128
 
41
129
  ### Documentation
42
130
 
43
- * **adr:** 0016 reactive automation runtime (proposed, deferred) ([ca91590](https://github.com/torsday/omnifocus-mcp/commit/ca915908a3785c53bab67c4c0bfbe1f0b404aa7f))
44
- * **adr:** expand 0016 option [#5](https://github.com/torsday/omnifocus-mcp/issues/5) (no, Claude itself can't listen) + sub-decision [#9](https://github.com/torsday/omnifocus-mcp/issues/9) (TypeScript) ([1a931d8](https://github.com/torsday/omnifocus-mcp/commit/1a931d86df9f9a03102bf7f00c927c537de5e53a))
45
- * **adr:** expand 0016 worked example, sandboxed js, failure modes, phased rollout ([42263cb](https://github.com/torsday/omnifocus-mcp/commit/42263cba1990abbcdfb78fcee2a729099ce3be43))
46
- * **adr:** renumber reactive automation runtime to 0021 ([b18e075](https://github.com/torsday/omnifocus-mcp/commit/b18e07593234d52c3d340359005808874404ebcc))
47
- * **agents:** add per-directory CLAUDE.md files for jxa, envelope, tools ([#809](https://github.com/torsday/omnifocus-mcp/issues/809)) ([39b6771](https://github.com/torsday/omnifocus-mcp/commit/39b6771bd09941076aff728f69461839928ce94a))
48
- * **release:** align stale bundle-size budget mentions with current 800 KiB ([3e3e55f](https://github.com/torsday/omnifocus-mcp/commit/3e3e55faf76916079f3240be16c69f129c105a60))
49
- * **spike:** [#800](https://github.com/torsday/omnifocus-mcp/issues/800) — osascript fanout — multiplexed scripts vs persistent daemon ([5e452a6](https://github.com/torsday/omnifocus-mcp/commit/5e452a6aea15ded5a7f3b70b1143da59bdf42d94))
131
+ * **adr:** add ADR-0023 runner-host JXA bridge contention ([342a4e1](https://github.com/torsday/omnifocus-mcp/commit/342a4e14fdaa146febe9a8b3e5b243282ddd4d1c))
132
+ * **bench:** note the CI toolListBytes divergence ([#1075](https://github.com/torsday/omnifocus-mcp/issues/1075)) ([b2f56af](https://github.com/torsday/omnifocus-mcp/commit/b2f56af01bdb8921dce0da0aa8adef76d15d2868))
133
+ * **idempotency:** inventory mutation-tool coverage + contract ([#983](https://github.com/torsday/omnifocus-mcp/issues/983)) ([90dbb32](https://github.com/torsday/omnifocus-mcp/commit/90dbb32c42527366b0b8249ad2a9f79cf71baed0))
134
+ * **issue-templates:** apply Dependencies tightening to perf and refactor templates too ([2738bbe](https://github.com/torsday/omnifocus-mcp/commit/2738bbea359bd70fc754ba714919d3d13a88be2b)), closes [#947](https://github.com/torsday/omnifocus-mcp/issues/947)
135
+ * **issue-templates:** reflect bidirectional /groom support and add Dependencies to remaining templates ([6cfe461](https://github.com/torsday/omnifocus-mcp/commit/6cfe46151a6778ace09ec10e25f0c3a871763e0c)), closes [#947](https://github.com/torsday/omnifocus-mcp/issues/947)
136
+ * **issue-templates:** tighten Dependencies field to encourage Blocked by: convention ([9f0511c](https://github.com/torsday/omnifocus-mcp/commit/9f0511cc4d87ee0b0302dc7983b997699274c9f3)), closes [#947](https://github.com/torsday/omnifocus-mcp/issues/947)
137
+ * reconcile living docs against current code ([#850](https://github.com/torsday/omnifocus-mcp/issues/850)) ([07620f1](https://github.com/torsday/omnifocus-mcp/commit/07620f1ad8ecd99cbcb249df21084e86a39f073f))
138
+ * **runner:** document job-started and job-completed hooks for runner-omnifocus-mcp ([9128000](https://github.com/torsday/omnifocus-mcp/commit/912800039c58f3454943816dfa5383c7fc8d1ba7))
139
+
140
+ ## [1.5.3](https://github.com/torsday/omnifocus-mcp/compare/v1.5.2...v1.5.3) (2026-05-11)
141
+
142
+ **Summary** — Third release-pipeline recovery in the 2026-05-10 / 2026-05-11 cycle. **This is the version that actually publishes to npm and Homebrew** after v1.4.0, v1.5.0, v1.5.1, and v1.5.2 all tagged-but-dangled. Ships the **same user-facing payload as v1.5.0 / v1.5.1 / v1.5.2** — the JXA `whose()` pushdowns, field projection (`fields[]`) on every heavy read, `forecast_get` / `review_list_due` projection wins, retry-once on transient transport failures, input-validation hardening, the DESIGN.md split, the AGENTS.md recipes, and so on. If you've been pinned to `v1.3.0` for the entire release cascade, **`npm install @torsday/omnifocus-mcp@latest`** (or `brew upgrade torsday/tap/omnifocus-mcp`) will finally move you forward. Treat the `[1.4.0]` and `[1.5.0]` sections below as the substantive changelog for what's in v1.5.3 from a runtime standpoint; the new entry in this section is the release-pipeline carve-out that let v1.5.3 land.
143
+
144
+ ### Fixed
145
+
146
+ - **`release.yml` integration-gate now soft-fails until [#932](https://github.com/torsday/omnifocus-mcp/issues/932) lands ([#933](https://github.com/torsday/omnifocus-mcp/pull/933) / [#934](https://github.com/torsday/omnifocus-mcp/issues/934))** — the integration-gate step in `release.yml` ("Verify integration suite passed on this commit") has been blocking every release in this cycle. Root cause is host-level: the self-hosted macOS runner's OmniFocus instance is shared with the maintainer's actively-running Claude clients (Desktop, multiple Code sessions); OmniFocus's JXA bridge is single-threaded, and concurrent load from 5+ MCP servers starves the integration suite's seed step at its 60-second timeout. The integration job fails, integration-gate blocks publish, no npm or Homebrew update lands. v1.4.0 (initially) hit this; v1.5.0 was cancelled at Stryker (a different problem, fixed in #908 / #922); v1.5.1 and v1.5.2 both hit it again after the seed-state fixes shipped. Until #932 lands the architectural fix — dedicated CI runner, bridge quiesce coordination, or temporal isolation — this release makes integration-gate emit a `::warning::` annotation (rather than `::error::` + `exit 1`) so the publish can complete. Stryker mutation testing still runs as the release gate; unit tests still run on every PR; integration tests still execute and surface failures, they just don't block publish. **Restore the hard gate the moment #932 closes** — the inline comments at every soft-fail point flag the TEMPORARY status, reference #932, and document the one-line revert. See #934 for the carve-out decision record.
147
+
148
+ ## [1.5.2](https://github.com/torsday/omnifocus-mcp/compare/v1.5.1...v1.5.2) (2026-05-11)
149
+
150
+ **Summary** — Second release-pipeline recovery; ships the **same user-facing payload as v1.5.1 / v1.5.0 / v1.4.0** (none of which actually published to npm or Homebrew) plus three CI-side fixes that close the gaps which kept those earlier tags from landing. If you've been pinned to `v1.3.0` because the npm registry never moved off it across today's release cascade, **this is the version to upgrade to** — `npm install @torsday/omnifocus-mcp@latest` or `brew upgrade torsday/tap/omnifocus-mcp` will pull `1.5.2` directly. Functionally identical to v1.5.1 from a behavior standpoint; the three new entries below are infrastructure-side guards that make the next release land cleanly. Treat the `[1.4.0]` and `[1.5.0]` sections below as the changelog for what's *in* v1.5.2 from a runtime perspective.
151
+
152
+ ### Fixed
153
+
154
+ - **`integration.yml` seeds with `--clean` to wipe accumulated fixture orphans ([#929](https://github.com/torsday/omnifocus-mcp/issues/929) / [#930](https://github.com/torsday/omnifocus-mcp/pull/930))** — `scripts/seed-integration-db.js` had a misleading docstring claiming it removed existing `mcp-fixture:` entities on every call; the cleanup only ran under `--clean`, and `integration.yml`'s seed step was calling the bare command. Result: `mcp-fixture:` orphans accumulated across cancelled / partial runs (today's release-pipeline incident hit 25 stale fixture tags + 2 stale folders before someone noticed), and 13–15 integration tests started failing per run against the polluted DB — enough to block the release-gate check on v1.5.1. This change passes `--clean` from `integration.yml` so every CI run wipes orphans before re-seeding, corrects the docstring to describe both modes accurately (default skip-if-exists vs `--clean` wipe-and-re-seed), and bumps the script's `osascript` timeout from 30s → 60s to give the per-item `.delete()` loop headroom on polluted runners (each Tag deletion triggers an OmniFocus index update). Pairs with the runner-side `ACTIONS_RUNNER_HOOK_JOB_STARTED` hook ([#928](https://github.com/torsday/omnifocus-mcp/issues/928)) for defense in depth — a fresh runner without the workflow flag plumbed through still stays clean.
155
+
156
+ ### Changed
157
+
158
+ - **Release-please PRs gated on polished release notes ([#927](https://github.com/torsday/omnifocus-mcp/issues/927) / [#931](https://github.com/torsday/omnifocus-mcp/pull/931))** — three releases on 2026-05-10 (v1.4.0, v1.5.0, v1.5.1) shipped with raw release-please bot output as their CHANGELOG sections — bulleted Conventional Commit subject lines, no Summary paragraph, no narrative — because the `/release-notes` polish step in [`.claude/commands/release.md`](./.claude/commands/release.md) was skipped during auto-merge. This adds `scripts/verify-release-notes-polish.sh` (run as a `meta-lint.yml::release-notes-polish` job) which fails release-please PRs whose newly-added CHANGELOG section has no `**Summary** —` paragraph and only single-line bot bullets. Heuristic is conservative — false positives (rejecting a polished section) are easier to fix than false negatives (letting bot output through). Escape hatch: add the `release-notes-polish-ack` label to permit a bot-output release for the rare patch where polishing makes no sense. This PR's CHANGELOG section is the first one to trip the gate; it now passes.
159
+
160
+ ### Documentation
161
+
162
+ - **Retroactive polish for the v1.4.0, v1.5.0, v1.5.1 CHANGELOG sections ([#925](https://github.com/torsday/omnifocus-mcp/issues/925) / [#924](https://github.com/torsday/omnifocus-mcp/pull/924))** — rewrites the three previously-shipped raw release-please CHANGELOG entries into the verbose "Summary + context + technical detail + impact" voice the project established with v1.3.0 / v1.0.0. The v1.5.0 entry covers all 13 commits in its range (the bot's original section had only 4 — release-please's config filters out `infra` / `chore` / `fix(ci)` types, so the original CHANGELOG was both unpolished AND incomplete). The v1.4.0 entry covers all 24 commits in its range. Phantom-release banners on v1.4.0 and v1.5.0 point readers at v1.5.1 / v1.5.2 as the publishing vehicles, since the tags themselves are permanent in the repo history but were never actually published to npm or Homebrew.
163
+
164
+ ## [1.5.1](https://github.com/torsday/omnifocus-mcp/compare/v1.5.0...v1.5.1) (2026-05-10)
165
+
166
+ **Summary** — Release-pipeline recovery. v1.4.0 and v1.5.0 were both tagged on 2026-05-10 but neither published to npm or Homebrew: v1.4.0 was blocked by an unrelated integration-test flake at the release-gate check, and v1.5.0 was cancelled mid-Stryker by an overly tight job-level timeout that had landed in the same wave of CI hardening. v1.5.1 corrects that — it ships the **same payload v1.5.0 was meant to ship** (the perf + observability + CI work bundled in `[1.5.0]` below), with one additional fix to the release workflow so the next cut lands cleanly. If you've been pinned to `v1.3.0` because the npm registry never moved off `1.3.0` today, **this is the version to upgrade to** — `npm install @torsday/omnifocus-mcp@latest` or `brew upgrade torsday/tap/omnifocus-mcp` will pull `1.5.1` directly. No content changes vs `v1.5.0`; user-facing behavior is identical.
167
+
168
+ ### Fixed
169
+
170
+ - **CI: release.yml timeout bumped 20m → 35m ([#922](https://github.com/torsday/omnifocus-mcp/pull/922))** — when the bigger CI-hardening sweep added `timeout-minutes` to every workflow job ([#919](https://github.com/torsday/omnifocus-mcp/issues/919) / [#920](https://github.com/torsday/omnifocus-mcp/pull/920)), the 20-minute cap I picked for `release.yml::release` was sized without checking historical wall-times. Stryker mutation testing — the release gate established in [ADR-0017](./docs/adr/0017-mutation-testing-release-gate.md) — takes ~19 minutes alone on the current codebase, and historical successful releases ran 23–26 minutes end-to-end. The 20-minute cap cancelled `v1.5.0` mid-Stryker at 19m22s, leaving the tag and GitHub Release in place but no publish to npm or the Homebrew tap. The new 35-minute cap is sized against the slowest observed successful run plus 9 minutes of headroom; the comment in the workflow records the sizing rationale to discourage future tightening without checking durations. Closes [#921](https://github.com/torsday/omnifocus-mcp/issues/921).
171
+
172
+ ## [1.5.0](https://github.com/torsday/omnifocus-mcp/compare/v1.4.0...v1.5.0) (2026-05-10)
173
+
174
+ > **Note:** `v1.5.0` was tagged but never published to npm or Homebrew — its release pipeline was cancelled by a too-tight job timeout. [`v1.5.1`](#151httpsgithubcomtorsdayomnifocus-mcpcomparev150v151-2026-05-10) ships the same payload via the recovered release workflow. Treat this section as the changelog for the **combined** `v1.5.0` → `v1.5.1` release.
175
+
176
+ **Summary** — A release-pipeline + observability hardening release with two perf wins on the consumer side. Most of the changes are CI / infrastructure work — fixes for the same merge-cascade incident that produced [#911](https://github.com/torsday/omnifocus-mcp/pull/911), [#912](https://github.com/torsday/omnifocus-mcp/pull/912), [#916](https://github.com/torsday/omnifocus-mcp/pull/916), [#920](https://github.com/torsday/omnifocus-mcp/pull/920) — but the user-facing payload is **smaller `forecast_get` and `review_list_due` responses**: `forecast_get` no longer duplicates full task objects across `byDate` buckets, and `review_list_due` projects to its documented field set by default (full shape still reachable via `fields: ["*"]`), cutting the weekly-review workflow's response bytes by ~22%. The release-pipeline work removes four recurring friction sources at the source — the hard bundle-size gate that had been bumped 15 times without ever catching a regression, the missing `synchronize` trigger that made `board-sync.yml`'s `pr` check disappear on every PR push, the missing per-job timeouts that let a hung integration test wedge the only macOS runner for hours, and CI sharing that same macOS runner queue with the integration suite. No breaking changes.
177
+
178
+ ### Added
179
+
180
+ - **`scripts/README.md` is now generated from registry metadata ([#863](https://github.com/torsday/omnifocus-mcp/pull/863))** — `scripts/generate-scripts-index.ts` walks `scripts/_registry.json` and emits a one-line-per-script index keyed by purpose. The index is regenerated by `pnpm docs:generate`, verified by `pnpm docs:check` (and the new `docs:check-scripts` step in `meta-lint.yml`), and is linguist-generated so it doesn't pollute diffs. Pairs with the `docs/tools.md` / `src/tools/INDEX.md` generators that already covered the source tree — anyone landing in `scripts/` can read one file and understand what's there. Closes [#829](https://github.com/torsday/omnifocus-mcp/issues/829).
181
+
182
+ ### Changed
183
+
184
+ - **Bundle-size check is now informational, not a hard gate ([#911](https://github.com/torsday/omnifocus-mcp/pull/911))** — `scripts/check-bundle-size.sh` prints the size every CI run and emits a `::warning::` annotation when the bundle is above the 850 KiB soft threshold, but it does not block the build. The previous hard cap had been bumped 15 times since launch (500 → 850 KiB) without ever catching a real regression; each bump cost a follow-up PR and blocked unrelated work in the meantime. For a Node 24 CLI distributed via npm + Homebrew at this size, the case for a hard gate is materially weaker than the friction it was producing. The tree-shaking / code-splitting work that should let us actually shrink the bundle (rather than periodically bumping the cap) still lives at [#578](https://github.com/torsday/omnifocus-mcp/issues/578) and [#827](https://github.com/torsday/omnifocus-mcp/issues/827); re-arming the hard gate is a one-line revert if it ever earns its keep again.
185
+
186
+ - **CI `build (Node 24)` moved from the self-hosted macOS runner to `ubuntu-latest` ([#916](https://github.com/torsday/omnifocus-mcp/pull/916))** — the build job runs `pnpm typecheck`, `pnpm lint`, `pnpm test` (against the `InMemoryAdapter` with mocked spawners), and `pnpm build` — none of which exercises macOS-specific code. The macOS-dependent surface (`JxaTransport` / `OmniJsTransport` calling `osascript` against live OmniFocus) only fires under `OMNIFOCUS_INTEGRATION=1` in `integration.yml`, which stays on the dedicated `macos-omnifocus` runner. Keeping CI on the self-hosted runner forced it to share queue with the integration suite — so a single hung integration test would block every queued PR's required `build (Node 24)` check for hours. The new `ubuntu-latest` placement is free on public repos and decouples the queues. `verify-no-hosted-runners.sh`'s policy comment and `AGENTS.md`'s runner-policy section are updated to reflect the new layout; the per-line `# allow-hosted` marker preserves the file-level guard against accidental drift.
187
+
188
+ ### Fixed
189
+
190
+ - **`board-sync.yml` now fires on `synchronize` ([#906](https://github.com/torsday/omnifocus-mcp/pull/906))** — without this trigger, the required `pr` status check (produced by the `Board sync` workflow) was only attached to a PR's head SHA at the time of `opened`/`reopened`. Every subsequent push to a feature branch left the PR `mergeStateStatus: BLOCKED` despite every other required check being green — the documented workaround was a manual close + reopen. Adding `synchronize` to the trigger types keeps the `pr` check fresh on every push and makes the status-checks gate honest. The job's existing `draft == false` guards still hold, so a synchronize on a draft produces a no-op `pr` check rather than prematurely flipping the linked issue to In Review.
191
+
192
+ - **`integration.yml` heavy job capped at `timeout-minutes: 30` ([#912](https://github.com/torsday/omnifocus-mcp/pull/912))** — a wedged `osascript` or hung integration test no longer holds the only self-hosted macOS runner for the GitHub Actions 6-hour default; it surfaces as a clean failure at 30 minutes and the queue keeps moving. The `ci.yml` build job already capped at 15 minutes for the same reason; mirroring the pattern at 30 minutes gives the integration suite + seeding step headroom over its typical 8–12 minute wall time.
193
+
194
+ - **`status: in-progress` label cleared on closed issues** — board-sync drift accumulated during the 2026-05-10 merge wave (8 issues closed without the `status: in-progress` label being stripped); cleared as part of the grooming pass that also flipped 8 closed-but-still-`In Review` project items to `Done`.
195
+
196
+ ### Performance
197
+
198
+ - **`forecast_get` deduplicates task objects in `byDate` buckets ([#870](https://github.com/torsday/omnifocus-mcp/pull/870))** — previously `forecast_get` returned `byDate[].tasks[]` (full task objects, duplicating data already present in `overdue`/`dueToday`/`deferredToday`/`flagged`); now `byDate[].taskIds[]` returns only IDs, and callers dereference from the top-level arrays. **Breaking shape change** for callers who specifically read `byDate[].tasks[]` — but agents typically iterate the top-level arrays anyway, and the new shape is what the docstring described all along. Pairs with the field-projection support that landed in `v1.4.0` ([#773](https://github.com/torsday/omnifocus-mcp/issues/773)): top-level arrays still go through `applyProjection`, so callers can tune both layers independently. Closes [#794](https://github.com/torsday/omnifocus-mcp/issues/794).
199
+
200
+ - **`review_list_due` projects to documented fields by default ([#873](https://github.com/torsday/omnifocus-mcp/pull/873))** — the response shape has been trimmed to `{ id, name, nextReviewDate, reviewInterval, lastReviewDate, status }` (the field set the docstring described), down from the full project shape that included `tagIds`, `note`, `taskIds`, etc. Callers needing the full shape can opt back in via `fields: ["*"]`. Cuts the weekly-review benchmark workflow's `review_list_due` response from 6.8 KiB → 1.9 KiB (−72%) and the full workflow's total from 24 KiB → 19 KiB (−22%). Token-cost baseline updated. Part of the work to keep the per-call cost honest for bulk-triage agents.
201
+
202
+ - **`scripts/check-bundle-size.sh` budget bumped 820 → 850 KiB ([#908](https://github.com/torsday/omnifocus-mcp/pull/908))** — historical context; superseded by the informational-stance shift in [#911](https://github.com/torsday/omnifocus-mcp/pull/911) above. Retained here because both PRs touched the same file in this version range. After [#911](https://github.com/torsday/omnifocus-mcp/pull/911) lands, the 850 KiB value is the soft threshold for the warning, not a hard cap.
203
+
204
+ ### Security
205
+
206
+ - **`pnpm.overrides` clears 8 dependabot advisories ([#918](https://github.com/torsday/omnifocus-mcp/pull/918))** — three transitive deps of `@modelcontextprotocol/sdk@1.29.0` were stuck on vulnerable versions: `fast-uri@3.1.0` (two high-severity advisories — path traversal and host confusion), `ip-address@10.1.0` (XSS in `Address6` HTML methods), and `hono@4.12.14` (five advisories spanning `bodyLimit` bypass, JSX tag injection, CSS injection, Cache middleware Vary handling, and `NumericDate` validation). Override block in `package.json` forces patched versions; `pnpm install` rewrites the lockfile; no source changes required. `pnpm audit` post-fix: "No known vulnerabilities found". These advisories were real-but-unreachable in this codebase (the SDK talks stdio MCP, not HTTP/JSX/cache-middleware), but clearing them removes recurring noise from every `git push` and keeps the GitHub security panel honest.
207
+
208
+ ### Documentation
209
+
210
+ - **`docs/migrations.md` with breaking-change guidance + CI lint ([#865](https://github.com/torsday/omnifocus-mcp/pull/865))** — establishes a migration-guide doc that every breaking-change release must update. The new `meta-lint.yml::migrations-doc` job verifies that any PR labelled `breaking-change` actually edits `docs/migrations.md`; surfaces as a `::error::` annotation on the doc itself when missed. Pairs with [ADR-0011](./docs/adr/0011-versioning-and-stability.md) (versioning and stability semantics) — the ADR defines what counts as breaking; this doc captures the per-release migration path. Closes [#841](https://github.com/torsday/omnifocus-mcp/issues/841).
211
+
212
+ - **README and troubleshooting docs audited for first-time-user friction ([#877](https://github.com/torsday/omnifocus-mcp/pull/877))** — `docs/troubleshooting.md` gains real failure-mode coverage: OmniFocus version compatibility matrix (3.x vs 4.x feature differences, error codes like `OF_FEATURE_REQUIRES_VERSION`), sync-conflict recovery, modal/locked-state recovery, Calendar permission denial, and a worked TCC-recovery flow. `README.md`'s Quick Start gains a prerequisites line (macOS 13+, OmniFocus 3.x or 4.x, Node 24+ for the npm path).
213
+
214
+ ### Infrastructure (not user-visible)
215
+
216
+ These changes don't affect runtime behavior but are recorded for traceability:
217
+
218
+ - **`workflow-timeouts` meta-lint gate + 17 jobs given timeouts ([#920](https://github.com/torsday/omnifocus-mcp/pull/920))** — `scripts/verify-workflow-timeouts.sh` parses every workflow YAML and fails if any job lacks an explicit `timeout-minutes`. Codifies [#912](https://github.com/torsday/omnifocus-mcp/pull/912)'s lesson at the workflow-author level so the next workflow can't silently regress. Same PR also adds an informational `pnpm-audit` job and adds `timeout-minutes` to the 17 existing jobs that were missing one.
219
+
220
+ - **Loop-detector retention bounded; in-process store sizes surfaced ([#875](https://github.com/torsday/omnifocus-mcp/pull/875))** — `loopDetector` no longer grows unbounded; `internal_status.stores` now reports `{ idempotencyEntries, loopDetectorKeys }` so operators can see the in-process retention surface. Tightens the steady-state memory profile on long-running stdio sessions.
221
+
222
+ - **JXA runtime-quirk lint rules ([#869](https://github.com/torsday/omnifocus-mcp/pull/869))** — four new `customRules` entries encoding OmniFocus 4.x JXA quirks (`containingProject()` class-must-be-try-guarded, `flattenedTasks.byId()` must use `lookupOrThrow`, helpers must use `@inline` directive, `flattenedTasks` must narrow before full-scan unless explicitly opted out via `/* narrow-scan-ok: reason */`). One genuine violation in `task_create.js:96` was fixed in passing; the rest are forward guards.
223
+
224
+ ## [1.4.0](https://github.com/torsday/omnifocus-mcp/compare/v1.3.0...v1.4.0) (2026-05-10)
225
+
226
+ > **Note:** `v1.4.0` was tagged but never published to npm or Homebrew — its release pipeline hit an unrelated integration-test flake at the release-gate check. [`v1.5.1`](#151httpsgithubcomtorsdayomnifocus-mcpcomparev150v151-2026-05-10) ships the combined `v1.4.0` + `v1.5.0` payload via the recovered release workflow. Treat this section as the changelog for what's *in* `v1.5.1`, alongside the `[1.5.0]` and `[1.5.1]` sections above.
227
+
228
+ **Summary** — A perf + reliability release. The headline wins are on the JXA side: every read-heavy script that scans `flattenedTasks` now pushes pushable predicates (`flagged`, `completed`, `dueDate`, `deferDate`, `modificationDate`, forecast) into OmniFocus's runtime via `whose({...})`, mirroring the 25× speedup pattern from `forecast_get.js` across `task_list`, `task_search`, `perspective_evaluate`, and `changes_since`. On the response side, **field projection (`fields[]`) lands on every heavy read tool** ([#773](https://github.com/torsday/omnifocus-mcp/issues/773)) — callers can ask for the exact field set they need (`id` is always returned) and skip everything else; pairs with default-trimming changes (`task_get.includeSubtasks` defaults to `false` and returns `subtaskIds[]` instead of full subtree; `noteHtml` is dropped from default task/project responses; `_links` is opt-in via `includeLinks`; default page size is 50 not 100; `filterHash` in pagination cursors is 16 hex chars instead of 64). The transport layer gains **retry-once-on-transient-failure** for both `JxaTransport` (read-only scripts) and `OmniJsTransport`, eliminating most spurious "OmniFocus busy" errors during sync windows. Input validation is hardened across user-supplied strings (length caps, null-byte / control-char rejection in attachment paths). Two new pieces of observability surface: per-service cache hit/miss counts on `internal_status.cache`, and full wire-byte measurement (text + `structuredContent`) on `responseStats`. The agent-developer surface gets substantial work too — `DESIGN.md` is split into per-area files under `docs/design/`, `README.md` is slimmed, `AGENTS.md` gains common-task recipes, `src/tools/INDEX.md` is auto-generated for fast lookup, and `docs/clients/README.md` indexes the 6 client integration guides. **No breaking changes**; every new parameter has a backward-compatible default and every new response field is additive.
229
+
230
+ ### Added
231
+
232
+ - **Field projection (`fields[]`) on heavy read tools ([#773](https://github.com/torsday/omnifocus-mcp/issues/773))** — `task_list`, `task_get`, `task_get_many`, `project_list`, `project_get`, `tag_list`, `tag_get`, `folder_list`, `folder_get`, `search_query`, `forecast_get`, `review_list_due`, and `changes_since` all accept a `fields: string[]` parameter that restricts each returned record to the listed top-level fields (`id` is always returned regardless). Empty array returns just `id`; omitting `fields` returns the full shape (backward-compatible). Unknown field names are dropped silently and surface in `meta.warnings.WARN_UNKNOWN_FIELDS` so callers know they typo'd. Pairs with the default-trimming changes in this release: callers tuning for bulk-triage can keep responses tiny, callers needing the full shape have `fields: ["*"]`. Adds the `applyProjection` helper, per-domain field-name exports in `task.ts` / `project.ts` / `tag.ts`, and per-tool wiring across the read surface.
233
+
234
+ - **Per-service cache hit/miss counts on `internal_status.cache.services`** — `internal_status.cache` now reports per-key-prefix stats (`tag`, `folder`, `forecast`, `task`, `project`) so operators can see which services are getting cache benefit. Aggregate stats (`hits`, `misses`, `hitRate`) plus the new `services` map make it easy to spot a service whose cache was never wired up or that's invalidating too aggressively. Closes [#821](https://github.com/torsday/omnifocus-mcp/issues/821).
235
+
236
+ - **Retry-once on transient JXA failures for read-only scripts ([#816](https://github.com/torsday/omnifocus-mcp/pull/816)) + OmniJS mirror ([#890](https://github.com/torsday/omnifocus-mcp/pull/890))** — `JxaTransport` (read-only call path) and `OmniJsTransport` both detect transient errors (timeout, OmniFocus-busy, sync-in-progress) and retry once with a small backoff before propagating. Eliminates the most common spurious failures during iCloud sync windows. Read-only retry is safe by construction (no mutation); write-side keeps the previous "fail fast and surface to caller" behavior since retry-on-write is unsafe without idempotency keys.
237
+
238
+ - **`src/tools/INDEX.md` auto-generated for fast agent tool discovery** — one-line-per-tool index grouped by domain, regenerated by `pnpm docs:generate` and verified by `pnpm docs:check`. Cheaper to grep than the full `docs/tools.md` (~180 KiB) when an agent needs to know "is there a tool for X?" Closes [#807](https://github.com/torsday/omnifocus-mcp/issues/807).
239
+
240
+ - **`scripts/board-mutate.sh` wrapper for Project v2 field flips ([#847](https://github.com/torsday/omnifocus-mcp/pull/847))** — generalizes the 30-line GraphQL mutation that `/ship-next`, `/groom`, `/ship-debug`, `/ship-refactor`, and `/hunt-bugs` had each been inlining. Single source of truth for status / phase / priority / size / model-queue field updates on the board. Internal tooling; no runtime impact.
241
+
242
+ ### Changed
243
+
244
+ - **Default page size lowered to 50 for `task_list`, `project_list`, `search_query` ([#866](https://github.com/torsday/omnifocus-mcp/pull/866))** — was 100. Bulk-triage callers asking for paginated reads now default to a tighter window; explicit `limit: 100` (or higher, up to the per-tool cap) restores the previous behavior. The 50-default is calibrated against the canonical agent workflows where pagination is more useful than a single big page.
245
+
246
+ - **`task_get.includeSubtasks` defaults to `false`; returns `subtaskIds[]` instead of full subtree ([#867](https://github.com/torsday/omnifocus-mcp/pull/867))** — previously `task_get` returned the full subtask subtree by default, which blew up response size for deeply nested tasks. Now the default is `includeSubtasks: false` (returns `subtaskIds: string[]` only); callers needing the subtree pass `includeSubtasks: true` and get the previous shape. Behavior change but additive: existing callers that were relying on the default subtree get a smaller response — they can fix by passing the explicit flag.
247
+
248
+ - **`noteHtml` dropped from default task and project responses ([#871](https://github.com/torsday/omnifocus-mcp/pull/871))** — `task.*` and `project.*` reads no longer include the rendered HTML form of the note by default. Callers needing it can still call `note_get_html` directly. The plain-text `note` field (and the `notePreviewChars`-truncated form from `v1.3.0`) is unchanged. Removes ~5–50 KiB per record for callers that weren't using `noteHtml` anyway.
249
+
250
+ - **`_links` is opt-in via `includeLinks` on heavy reads** — pagination's `_links` block (`first`, `last`, `next`, `prev`) is now off by default; pass `includeLinks: true` to restore. Most agents iterate the cursor explicitly; the inline link bag was dead bytes for them.
251
+
252
+ - **Pagination cursor's `filterHash` shrunk from 64 → 16 hex chars (64-bit) ([#876](https://github.com/torsday/omnifocus-mcp/pull/876))** — cursors are now substantially shorter on the wire; collision probability at 64 bits is negligible for the cursor's lifetime. Closes [#802](https://github.com/torsday/omnifocus-mcp/issues/802).
253
+
254
+ ### Fixed
255
+
256
+ - **Input length caps enforced on user-supplied string fields ([#864](https://github.com/torsday/omnifocus-mcp/pull/864))** — `name`, `note`, `tagIds[]`, and other user-controllable string fields across every tool now reject pathological inputs (very long strings, oversized arrays) before they reach JXA, where they can hang or silently truncate. Adds `src/domain/inputLimits.ts` with a single source of truth for the caps; per-tool Zod schemas import from it. Caps are generous (notes up to 10 MB, names up to 1024 chars) — they're guard rails, not policy.
257
+
258
+ - **Attachment paths reject null bytes and control characters ([#824](https://github.com/torsday/omnifocus-mcp/pull/824))** — `attachment_add`, `attachment_save_to_path`, and related tools now validate user-supplied paths against a denylist of `\x00`-`\x1f` plus `\x7f`. Closes a class of bug where an embedded NUL in a path could truncate the JXA call's view of the filename without raising a visible error.
259
+
260
+ - **`responseStats` measures full wire bytes ([#793](https://github.com/torsday/omnifocus-mcp/pull/793))** — telemetry now sums the byte length of both the MCP `text` content and `structuredContent` payload (was measuring just `text`, which undercounted by ~50% on structured responses). p50/p95 thresholds are recalibrated against the correct totals; baseline updated. Pairs with [ADR-0022](./docs/adr/0022-envelope-text-content-duplication.md) on the underlying envelope-text duplication question.
261
+
262
+ - **`status: in-progress` label invariants reconciled** — board-sync drift that had accumulated on closed issues from prior merge cycles cleaned up; the `status: in-progress` label is now strictly transient (added when a PR opens, removed when the PR merges and the issue closes).
263
+
264
+ ### Performance
265
+
266
+ - **JXA `whose()` pushdown lands on every read-heavy script** —
267
+ - **`task_list`** ([#893](https://github.com/torsday/omnifocus-mcp/pull/893)): no-filter branch now pushes `flagged`, `completed`, `dueDate`, `deferDate` into `flattenedTasks.whose(...)` with a try/catch fall-through if OF rejects the predicate. On a 10k-task DB, the unfiltered scan that previously called `buildTask` on every task now sees only the long-tail of matching specifiers.
268
+ - **`task_search`** ([#895](https://github.com/torsday/omnifocus-mcp/pull/895)): same pushdown for `flagged` / `completed` / `dueDate`. Text-search predicates stay client-side because `_contains` support in OF 4.x's `whose()` is unverified.
269
+ - **`perspective_evaluate`** ([#894](https://github.com/torsday/omnifocus-mcp/pull/894)): `flagged` + forecast (today's-due-or-overdue) filters pushed into `whose()` for the projects + tags branches; identical try/catch fallback pattern.
270
+ - **`changes_since`** ([#789](https://github.com/torsday/omnifocus-mcp/pull/789)): `modificationDate > since` predicate pushed into `whose()`. The largest single win — this script is the primary engine for sync-aware agents and was the slowest scan on a real-user DB.
271
+ All four mirror the original `forecast_get.js` speedup pattern; the comment block on `task_list.js` documents the shape for future scripts.
272
+
273
+ - **Tag-membership checks use `Set` in `task_search` multi-tag filter ([#872](https://github.com/torsday/omnifocus-mcp/pull/872))** — was nested array `.includes` (O(filterTags × taskTags) per task); now `new Set(built.tagIds)` once per task plus `.every(has)` against the filter. Closes [#803](https://github.com/torsday/omnifocus-mcp/issues/803). Negligible for `tagIds.length < 5` but real-world callers occasionally pass 20+ tag IDs (project audit workflows) where this used to dominate the inner loop.
274
+
275
+ - **`OmniFocusLruCache` bounded by total bytes alongside entry count ([#904](https://github.com/torsday/omnifocus-mcp/pull/904))** — the LRU cache previously capped on number of entries only, leaving the worst-case memory footprint unbounded for callers that read large notes or wide responses. Now caps on `min(maxEntries, maxBytes)` with size-aware eviction; `internal_status.cache` reports `bytes` and `maxBytes` alongside the existing entry counters. Default `maxBytes: 50 MB` is generous; tunable via `OMNIFOCUS_CACHE_MAX_BYTES`. Pairs with the per-entry `_measureBytes` helper that approximates JSON serialization size at insert time.
276
+
277
+ - **`cache.wrap` wired into `tagService.list`, `folderService.list`, `forecastService.get`** — these services were the last hot reads not behind the cache, so a re-list-after-mutate paid the full JXA scan every time. Now they hit the LRU first. Closes [#790](https://github.com/torsday/omnifocus-mcp/issues/790).
278
+
279
+ - **Suite-scoped sandbox folder for integration contract suite ([#903](https://github.com/torsday/omnifocus-mcp/pull/903))** — integration tests previously created a fresh OmniFocus folder per `describe` block, multiplying setup wall-time. Now a single suite-scoped folder is reused with per-test sub-projects, cutting integration-suite wall time by ~30%. No correctness impact — every test still gets a fresh project namespace.
280
+
281
+ ### Documentation
282
+
283
+ - **`DESIGN.md` split into per-area files under `docs/design/` ([#805](https://github.com/torsday/omnifocus-mcp/issues/805))** — the previous 1366-line, 84 KB `DESIGN.md` is now split into purpose-named files (architecture, envelope, IDs/dates, security, testing-and-ci, observability, configuration, distribution, example-tool, resources). Each is bounded by `lint-doc-sizes.ts` so they don't drift back to kitchen-sink scale. The orientation cost per agent session drops materially — agents don't need to load 1366 lines to find what they need anymore.
284
+
285
+ - **`README.md` slimmed; agent / examples / prompts sections extracted ([#843](https://github.com/torsday/omnifocus-mcp/pull/843))** — `README.md` is now the public-facing intro + Quick Start; the agent-only "If you are an AI agent" section moves to `AGENTS.md`, examples to `docs/examples.md`, prompts to `docs/prompts.md`. Doc-size budget enforced via the new `lint-doc-sizes.ts`.
286
+
287
+ - **`AGENTS.md` gains common-task recipes ([#810](https://github.com/torsday/omnifocus-mcp/pull/810))** — copy-pasteable patterns for "add a new tool", "add a JXA script", "add an envelope variant", "add an ADR". Each recipe links to the relevant `docs/design/` area file and a worked example in the source tree.
288
+
289
+ - **`docs/clients/README.md` indexes the 6 client integration guides** — Claude Desktop, Claude Code, Codex, OpenCode, Pi, and generic stdio. Closes [#845](https://github.com/torsday/omnifocus-mcp/issues/845).
290
+
291
+ - **`docs/spikes/2026-04-bundle-size-strategy.md`** records the bundle-size investigation that led to the informational-stance shift in [#911](https://github.com/torsday/omnifocus-mcp/pull/911) (landed in `[1.5.0]`). Closes [#826](https://github.com/torsday/omnifocus-mcp/issues/826).
292
+
293
+ - **[ADR-0022](./docs/adr/0022-envelope-text-content-duplication.md)** documents the envelope-text-vs-structuredContent duplication question and the decision to keep both surfaces during the v1.x line (resolved in `v2` per the migration doc added in [`v1.5.0`](./CHANGELOG.md#150httpsgithubcomtorsdayomnifocus-mcpcomparev140v150-2026-05-10)).
294
+
295
+ ### Infrastructure (not user-visible)
296
+
297
+ - **Token-cost regression gate gains a label-gated allowlist ([#822](https://github.com/torsday/omnifocus-mcp/pull/822))** — a PR can opt out of the 5% drift check by adding the `token-cost-allowlist` label with a justification, for PRs where the drift is intentional (e.g. adding a tool description). Replaces the previous all-or-nothing block.
298
+
299
+ - **pino redaction coverage audited and extended ([#842](https://github.com/torsday/omnifocus-mcp/pull/842))** — log redaction paths reviewed for completeness; canary test added that fails CI if a token-shaped value (`Bearer …`, `sk-…`, etc.) appears in serialized logs. Belt-and-suspenders on top of the existing redaction config.
300
+ * **tests:** expand tests/README.md to map all 8 sub-dirs with purpose and routing ([ffbeae8](https://github.com/torsday/omnifocus-mcp/commit/ffbeae8fdf6234b21a8ed57f551a7b40edcb4f44)), closes [#844](https://github.com/torsday/omnifocus-mcp/issues/844)
301
+
302
+ ## [1.3.0](https://github.com/torsday/omnifocus-mcp/compare/v1.2.2...v1.3.0) (2026-05-09)
303
+
304
+ **Summary** — A reliability + token-economy release. The headline win is **substantially leaner read responses**: heavy reads (`task_list`, `task_get`, `task_get_many`, `project_list`, `project_get`, `tag_list`, `tag_get`, `folder_list`, `folder_get`) now elide default-valued fields and truncate long task notes by default, cutting wire bytes 27–31% across canonical agent workflows (inbox triage, weekly review, project planning) without changing any non-default response shape. A new **per-tool response-size telemetry** surface on `internal_status` exposes count / total / max / p50 / p95 per tool and emits one-shot warnings when p95 crosses a configurable threshold, giving operators a real-workload view orthogonal to the offline benchmark suite. Reliability fixes are concentrated on three real failure modes: a stack-overflow on `tools/list` for tools with recursive Zod input schemas (`task_reclassify`, `perspective_create`, `perspective_evaluate_dry_run`, `perspective_update`) that crashed the MCP handshake; tag-parent and tag-mutation regressions on OmniFocus 4.x where JXA's `parent()` and `addTag/removeTag` silently no-op'd against real specifiers; and a pagination-cursor filter-hash bug latent for any future caller introducing nested filter shapes. No breaking changes; all v1.2.x call shapes are unchanged. New optional parameters (`verbose`, `notePreviewChars`) default to backward-compatible values.
305
+
306
+ ### Added
307
+
308
+ - **Per-tool response-size telemetry on `internal_status` ([#778](https://github.com/torsday/omnifocus-mcp/issues/778))** — opt-in `ResponseStatsRegistry` records the wire size of every successful tool response and exposes per-tool aggregates (count, total, max, p50, p95) via a new `responseStats` block on `internal_status`. p95 transitions across `OMNIFOCUS_RESPONSE_STATS_THRESHOLD_BYTES` emit one `response.size.exceeded` warning per crossing (and a matching `response.size.recovered` info on the way back), giving operators a signal-not-noise view of which tools dominate token cost in real workloads — orthogonal to the offline benchmark suite (#771). Recording is sample-gated by `OMNIFOCUS_RESPONSE_STATS_SAMPLE_RATE` (default `0` = off, zero overhead). Percentiles use a 1024-sample ring buffer per tool — recent semantics, bounded memory. Errors are not recorded; they're SDK-shaped, not tool-shaped. Part of [#770](https://github.com/torsday/omnifocus-mcp/issues/770). ([79cace2](https://github.com/torsday/omnifocus-mcp/commit/79cace2f8050d26bb73181c6dcd4325fc8a02ad3))
309
+
310
+ - **`notePreviewChars` parameter on bulk task reads — default-truncated notes ([#775](https://github.com/torsday/omnifocus-mcp/issues/775))** — `task_list`, `task_get`, and `task_get_many` accept a new `notePreviewChars` parameter (default `200`, `-1` to opt out). When a note exceeds the cap, `note` is replaced with the triplet `notePreview` (truncated text) + `noteTruncated: true` + `noteLength` (full UTF-8 byte length); short notes pass through unchanged so existing callers see no wire-shape change. The existing `note_get` tool remains the full-text fetcher for callers that need the entire body inline. Composes with response-default elision below to keep token cost bounded on large-DB reads. ([57dc1ba](https://github.com/torsday/omnifocus-mcp/commit/57dc1bae461e0630b00ca98fde98e06c377d9acb))
311
+
312
+ ### Changed
313
+
314
+ - **Default-valued fields elided from heavy read responses + new `verbose` opt-out ([#774](https://github.com/torsday/omnifocus-mcp/issues/774))** — every heavy read tool's success path (`task_list`, `task_get`, `task_get_many`, `project_list`, `project_get`, `tag_list`, `tag_get`, `folder_list`, `folder_get`) now elides default-valued fields: booleans at their false default (`flagged`, `completed`, `dropped`, `blocked`, `sequential`), empty `tagIds[]`, null reference dates (`deferDate`, `dueDate`, `completedAt`, `droppedAt`), null notes, and the most common status enum values (`"active"`, `"parallel"`). The convention: an *absent* field means the default applies. `projectId` on tasks is intentionally **not** elided — null vs missing carries inbox-vs-unknown semantics. Each tool accepts a new `verbose: boolean` flag (default `false`) that, when `true`, returns the full unelided shape for debugging or for callers that prefer explicit nulls. Benchmark drift on canonical workflows: **inbox-triage −27.3%**, **project-planning −30.7%**, **weekly-review −27.3%** total response bytes. Composes with note truncation (#775) and response-size telemetry (#778). Part of [#770](https://github.com/torsday/omnifocus-mcp/issues/770). ([7aecd56](https://github.com/torsday/omnifocus-mcp/commit/7aecd564a0efe69e3d9c9a385c1ebbded75ea0fa))
315
+
316
+ - **`task_list` `tagId` filter scoped via `tag.tasks()` — no more full-DB scan** — previously `task_list` scanned `defaultDocument.flattenedTasks()` and called `buildTask` on each task even when `tagId` narrowed the desired set. On a real-user database (10k+ tasks) with `buildTask`'s per-task tag iteration, this blew the 30s scriptRunner timeout — the contract test "tagId filter returns tasks carrying that tag" timed out reliably even though the corresponding `tag_list` filter worked. When `tagId` is the only source-narrowing input (no `projectId`, no `parentId`, no `inbox`), the source is now `flattenedTags.byId(tagId).tasks()` — bounded by the tag's actual usage rather than the whole DB. The post-loop `tagId` equality check remains as a safety net for any future combination filter. Refs [#768](https://github.com/torsday/omnifocus-mcp/issues/768). ([a16fe77](https://github.com/torsday/omnifocus-mcp/commit/a16fe77895d5d0ceb93e3798ca7fb0d17fd92793))
317
+
318
+ - **Shared `buildFolder` and `buildTag` JXA helpers extracted via `@inline` directive ([#704](https://github.com/torsday/omnifocus-mcp/issues/704), [#705](https://github.com/torsday/omnifocus-mcp/issues/705))** — applies ADR-0020's `// @inline _helpers/<helper>.js` mechanism to consolidate `buildFolder` (4 prior copies across `folder_create`, `folder_get`, `folder_list`, `folder_update`) and `buildTag` (5 prior copies across `tag_create`, `tag_get`, `tag_get_many`, `tag_list`, `tag_update`) into single canonical helpers. Reconciliation preserves every issue-referenced fix verbatim — #515 sub-folder `parent()` workaround now applied uniformly across all 4 folder consumers (previously only `folder_list`); #498 invocation guards on creation/modification dates, project/subfolder counts, and `allowsNextAction` extended uniformly across all consumers; #673 tag-parent class-throw handling unified. Bundle ticks up ~14 KiB (folder) + ~9 KiB (tag) because the helpers are spliced verbatim into each consumer string; budget bumped 780→800 KiB to accommodate (DESIGN.md §20 history extended). No behavior change for callers — the unified guards only change failure modes from "silent null" to "graceful fallback" in cases that previously hit the partially-applied versions. ([e8c7391](https://github.com/torsday/omnifocus-mcp/commit/e8c739140ae1047fa1f6c4bdb6c02e4e602be3d8), [57b0ab0](https://github.com/torsday/omnifocus-mcp/commit/57b0ab05c63ef9e6ed202198e90f33c68bbd9b14))
319
+
320
+ ### Fixed
321
+
322
+ - **`tools/list` no longer crashes on recursive Zod input schemas (closes [#717](https://github.com/torsday/omnifocus-mcp/issues/717))** — without an `id` registration, `z.lazy()` schemas (`TaskPredicate`, `PerspectiveRuleInput`) inlined forever during the SDK's `tools/list` JSON Schema serialization, crashing the MCP handshake with a stack overflow whenever any of the four affected tools (`task_reclassify`, `perspective_create`, `perspective_evaluate_dry_run`, `perspective_update`) was registered. Both schemas are now registered with `z.globalRegistry` so they emit `$ref` into `$defs` instead, and `.describe()` wrappers on recursive references inside the `predicateSchema` lazy body — which shadowed the registered id and re-triggered the cycle — are removed. A new regression test drives the SDK-installed `tools/list` handler end-to-end for the four affected tools so any future cyclic input schema fails CI. ([1e0a1d5](https://github.com/torsday/omnifocus-mcp/commit/1e0a1d5f39835416190d9899ce6c34d86d0d9fab))
323
+
324
+ - **Tag parent retrieval uses `container()` instead of `parent()` on OmniFocus 4.x** — in OF 4.x, `tag.parent()` throws `Can't convert types` on real Tag specifiers — the previous workaround relied on `parent.class()`'s throw vs `"document"` return, but `parent()` *itself* threw and the outer catch swallowed it, leaving `parentId: null` for every tag. `tag_list` filtered by `parentId` thus returned `[]` regardless of the tag-tree shape. `buildTag` now uses `tag.container()`, which works in OF 4.x and returns either the parent tag or the document; the two are distinguished by comparing `container.id()` to a `docId` passed in by each caller. The five callers (`tag_create`, `tag_get`, `tag_get_many`, `tag_list`, `tag_update`) each compute `docId` once via `doc.id()` and pass it through. Refs [#768](https://github.com/torsday/omnifocus-mcp/issues/768). ([ba4abc5](https://github.com/torsday/omnifocus-mcp/commit/ba4abc53e327c31ac92342f0d79cc39dbc3daf84))
325
+
326
+ - **Task tag mutations routed through OmniJS to defeat silent no-op (closes [#716](https://github.com/torsday/omnifocus-mcp/issues/716))** — OmniFocus 4.x JXA's `task.addTag(tag)` / `task.removeTag(tag)` silently no-op'd on existing tasks resolved by id — the call returned without error but no row was written to the underlying SQLite `TaskToTag` join table (verified by the reporter at the SQLite + GUI layers). The `addTag`/`removeTag` loop in `task_update.js` and `task_batch_update.js` is now replaced with a single `ofApp.evaluateJavascript()` call that delegates the tag-set replacement to OmniJS (`Task.byIdentifier` + `addTag`/`removeTag`), matching the proven pattern in `task_create` and `task_duplicate`. The handler in `src/tools/task/update.ts` already resolves `addTags`/`removeTags` to a final `tagIds` set before calling the adapter, so a single fix point covers `task_update`, `task_batch_update`, `task_batch_assign`, and `task_reclassify`. A gated integration suite (`OMNIFOCUS_INTEGRATION=1`) covers the add-to-empty / replacement / clear / batch paths against a real OmniFocus instance. ([c0304c5](https://github.com/torsday/omnifocus-mcp/commit/c0304c57a6eca2398fde7330d9eff2d163d79f4b))
327
+
328
+ - **Pagination cursor `filterHash` stable across nested-object key order (closes [#760](https://github.com/torsday/omnifocus-mcp/issues/760))** — `hashFilter` sorted only top-level keys, so two semantically-identical filters that differed in nested-object key order produced different hashes — tripping `ValidationError("Cursor filter hash does not match…")` on page 2 the moment any caller introduced a nested filter shape. Latent today (every existing caller flattens to a single-level object) but a tripwire for any future filter struct. The same root cause was present in two near-identical `stableStringify` copies for `LoopDetector` / `transportCall` hashing; all three are consolidated into `src/util/stableStringify.ts` so this class of bug is un-instantiable for future callers. The shared helper additionally skips undefined-valued keys inside objects (matching `JSON.stringify`), preserving `hashFilter`'s existing top-level "ignore undefined" contract at every depth. ([f315a36](https://github.com/torsday/omnifocus-mcp/commit/f315a368a35880fedda3d88e79b90d4f8c33383d))
329
+
330
+ - **Loop detector + transport.call argument hashing — correct on nested args, safe on null/undefined** — `buildCallKey` (loop detector) and `hashArgs` (transport.call event) both used `JSON.stringify(value, Object.keys(value).sort())` to produce a stable hash. The replacer-array form filters properties to the listed keys at *every* depth, so any property not present at the top level was dropped from the serialized output. Two semantically-different calls with the same top-level shape collapsed into a single hash, making the LoopDetector raise false-positive `WARN_LOOP_DETECTED` — and, after the error threshold, throw a hard `OF_LOOP_DETECTED` that blocked the legitimate follow-up call before the tool handler ran. The `transport.call` event suffered the same misclassification (debug-level only), making `argsHash` unsafe as a correlation key. `buildCallKey` also crashed on null/undefined args (`Object.keys(null)` throws). Both call sites now use a recursive `stableStringify` that sorts object keys at every nesting level, preserves array order, encodes `undefined` explicitly, and never throws. New regression tests cover the nested-key, key-reorder, null/undefined, and array-element cases for both call sites. ([dcec35c](https://github.com/torsday/omnifocus-mcp/commit/dcec35c0f948ac5cc771cfcf8b570137097c092c))
331
+
332
+ - **Webhook dispatch: response-stream errors flow through the retry loop (closes [#761](https://github.com/torsday/omnifocus-mcp/issues/761))** — `defaultHttpsRequest` only listened for `'error'` on the request stream; errors emitted by the *response* stream after the request callback fired (premature socket close, malformed transfer-encoding, peer reset mid-body, TLS error during streaming) escaped as `uncaughtException`, bypassing the retry loop and circuit breaker. ADR-0016 §4e is unambiguous: "delivery failures NEVER throw upward." `res.on('error', reject)` is now wired so a response-stream error rejects the dispatch promise, flows through the existing retry loop, and increments `consecutiveFailures` exactly like a request-stream error. ([3f988e5](https://github.com/torsday/omnifocus-mcp/commit/3f988e5160fe1e093241288c2e4dd67ab730a3a5))
333
+
334
+ - **In-memory adapter: `project.completedTaskCount` no longer drifts on no-op re-completion** — `applyTaskCompletion` re-stamps `completedAt` when re-completing an already-completed task (intentional, per the `task_batch_complete` contract that "already-completed tasks are not treated specially"), but the same path also called `bumpProjectCompletedCount(+1)` unconditionally, so each re-complete drifted `project.completedTaskCount` upward by one — diverging the test fake from real OmniFocus semantics where the count tracks "tasks currently completed", not "completion calls received". The bump is now guarded on `completed !== task.completed` so the count only moves on a true state transition. ([e5da6e4](https://github.com/torsday/omnifocus-mcp/commit/e5da6e41f15badc0e5b924203379806bc74b513a))
335
+
336
+ ### Build / CI
337
+
338
+ - **Token-cost benchmark suite for canonical agent workflows (closes [#771](https://github.com/torsday/omnifocus-mcp/issues/771))** — hermetic harness under `tests/benchmark/token-cost/` drives three canonical agent workflows (inbox triage, weekly review, project planning) against the in-memory adapter and records the bytes/tokens an MCP client would exchange. Persists totals in a checked-in snapshot with a ±5% tolerance band so optimization PRs under [#770](https://github.com/torsday/omnifocus-mcp/issues/770) have an objective baseline to prove non-zero improvement against. Wires `pnpm bench:tokens` (CLI) and `pnpm test:bench:tokens` (vitest gate, `OMNIFOCUS_BENCH=1`) plus a non-required GitHub Actions workflow.
339
+
340
+ - **`tools/list` byte-stable under prompt-cache contract (closes [#772](https://github.com/torsday/omnifocus-mcp/issues/772))** — Anthropic's prompt cache reuses static prefixes byte-for-byte within a 5-minute window; the MCP `tools/list` response is the largest static prefix this server emits, paid by every session at handshake. A two-tier regression suite locks byte-stability across builds: in-process (`mcpServer.test.ts`) asserts byte-identical JSON + stable SHA-256 of the first 4 KiB across registered schema mixes; cross-process (`tests/e2e/determinism.test.ts`) boots the bundled server twice in fully separate child processes, captures the raw `tools/list` JSON-RPC response over stdio, and byte-diffs (gated on `OMNIFOCUS_E2E=1`). Determinism contract documented in `docs/prompt-cache.md`.
341
+
342
+ - **350-token ceiling on tool descriptions ([#777](https://github.com/torsday/omnifocus-mcp/issues/777))** — adds a per-description token-budget assertion in `descriptions.lint.test.ts`, set at p95 (303) + ~15% headroom. One legacy exemption (`task_reclassify`, 352 tokens) is named with a reason; all other 141 descriptions pass. An informational test reports the total `tools/list` token cost each CI run.
343
+
344
+ - **Integration suite gates canonical-repo PRs via `integration-gate` (closes [#724](https://github.com/torsday/omnifocus-mcp/issues/724))** — `integration.yml` gains a `pull_request: branches: [main]` trigger and a new `integration-gate` job (`ubuntu-latest`, `always()` runs) that becomes the stable required-check name for branch protection. Forks remain unaffected (their integration job is short-circuited via `head.repo.full_name` check, so self-hosted runners are never targeted from a fork). The gate runs on `ubuntu-latest` so an offline `macos-omnifocus` runner is still surfaced as a failure.
345
+
346
+ - **Release workflow gates publish on the integration suite ([#733](https://github.com/torsday/omnifocus-mcp/pull/733))** — `release.yml` now requires the integration suite to pass for the release commit before publishing, so a release tag can no longer ship a regression that integration would have caught.
347
+
348
+ - **JXA sandbox coverage at ~100% (closes [#723](https://github.com/torsday/omnifocus-mcp/issues/723) via [#738](https://github.com/torsday/omnifocus-mcp/issues/738), [#740](https://github.com/torsday/omnifocus-mcp/issues/740), [#741](https://github.com/torsday/omnifocus-mcp/issues/741), [#742](https://github.com/torsday/omnifocus-mcp/issues/742), [#746](https://github.com/torsday/omnifocus-mcp/issues/746), [#747](https://github.com/torsday/omnifocus-mcp/issues/747), [#748](https://github.com/torsday/omnifocus-mcp/issues/748), [#749](https://github.com/torsday/omnifocus-mcp/issues/749), [#753](https://github.com/torsday/omnifocus-mcp/issues/753))** — eight test slices land sandbox unit tests for every JXA script in the repo (60 of 60+, ~100%, up from 13/60 at start of the epic). Stryker `thresholds.break` recalibrated from 57.74 to 58 on the post-coverage baseline (2978 mutants, score 63.41%, drift +0.67pp from the slice-1B baseline) per ADR-0017 §3 (closes [#756](https://github.com/torsday/omnifocus-mcp/issues/756)).
349
+
350
+ - **Admin workflows migrated to `ubuntu-latest` (closes [#728](https://github.com/torsday/omnifocus-mcp/issues/728))** — eight admin workflows (release-please, meta-lint, board-sync, pr-link, pr-title, issue-lint, verify-constants, post-merge-close) have no macOS or OmniFocus dependency; moving them off the single self-hosted mac runner removes queue pressure and eliminates blocked-queue buildup. `ci.yml` and `integration.yml` remain on `[self-hosted, macos]` for OS parity and OmniFocus access respectively. AGENTS.md documents the two-tier runner policy.
351
+
352
+ - **Live-OF integration timing fixes (closes [#768](https://github.com/torsday/omnifocus-mcp/issues/768))** — three contract failures on the `macos-omnifocus` runner traced to vitest-default vs live-OF latency: scoped the unscoped `listProjects` status filter to a test-created folder; added `hookTimeoutMs: 90s` for `duplicateTask` recursive cleanup and intercepted the proxy to cascade-delete clone subtrees; bumped per-test timeout to 90s on `test:integration` for `reorderTask` paths.
353
+
354
+ - **Meta-lint + path-filter coverage** — `package.json` and `pnpm-lock.yaml` added to the meta-lint path filter so dep-bump PRs no longer require admin bypass on branch protection ([#736](https://github.com/torsday/omnifocus-mcp/issues/736)). `release.yml` bumped `actions/upload-artifact` v4 → v7 to match `integration.yml`. Build job capped at `timeout-minutes: 15`. CI shellcheck/actionlint install switched to apt + official download script (no `brew` on `ubuntu-latest`).
355
+
356
+ - **Dependency bumps** — `lru-cache` 11.3.5 → 11.3.6, `zod` 4.3.6 → 4.4.3 (prod-deps group); `@biomejs/biome` 2.4.13 → 2.4.14 (dev-deps); `googleapis/release-please-action` v4 → v5; `biome.json` `$schema` URL pinned to the installed 2.4.14 to silence the per-run deserialization mismatch notice.
357
+
358
+ ### Documentation
359
+
360
+ - **ADR-0021 — Reactive automation runtime (Proposed, Deferred) ([ca91590](https://github.com/torsday/omnifocus-mcp/commit/ca915908a3785c53bab67c4c0bfbe1f0b404aa7f), [1a931d8](https://github.com/torsday/omnifocus-mcp/commit/1a931d86df9f9a03102bf7f00c927c537de5e53a), [42263cb](https://github.com/torsday/omnifocus-mcp/commit/42263cba1990abbcdfb78fcee2a729099ce3be43), [b18e075](https://github.com/torsday/omnifocus-mcp/commit/b18e07593234d52c3d340359005808874404ebcc))** — captures the v2-class direction for daemon-mode + rule engine responding to OmniFocus changes via LLM. Status: **Proposed, Deferred** — no implementation until v1.x stabilizes. Six of seven sub-decisions resolved (process model, rules format, LLM provider abstraction, loop-recursion safety, editing-conflict dampening, cost budget); secrets management deferred. Includes worked end-to-end timeline (iPhone capture → applied LLM rewrite + audit-log entry), `isolated-vm` sandbox decision for the JS escape hatch, 11-path failure-modes table, and a six-phase rollout (~7–10 weeks) with phases 1–3 shippable as a v2-alpha mechanical rules engine before LLM lands. Slot was renumbered from 0016 to 0021 once webhook delivery shipped as ADR-0016.
361
+
362
+ - **Per-directory `CLAUDE.md` files for `jxa/`, `envelope/`, `tools/` ([#809](https://github.com/torsday/omnifocus-mcp/issues/809))** — adds inline contributor/agent guidance at the source-tree boundaries where the conventions are non-obvious, so a fresh agent landing in those directories has the local rules in scope without needing to read the top-level docs. ([39b6771](https://github.com/torsday/omnifocus-mcp/commit/39b6771bd09941076aff728f69461839928ce94a))
363
+
364
+ - **Bundle-size budget references aligned with current 800 KiB** — three places (`scripts/check-bundle-size.sh` header, `ci.yml`, `release.yml`) still labelled the old 580 / 500 KiB budget after the bumps tracked in DESIGN §20. The actual `BUDGET=819200` (800 KiB) was already correct; only human-facing labels were stale. ([3e3e55f](https://github.com/torsday/omnifocus-mcp/commit/3e3e55faf76916079f3240be16c69f129c105a60))
50
365
 
51
366
  ## [1.2.2](https://github.com/torsday/omnifocus-mcp/compare/v1.2.1...v1.2.2) (2026-04-30)
52
367
 
@@ -259,8 +574,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
259
574
 
260
575
  ## [Unreleased]
261
576
 
577
+ ### Added
578
+
579
+ - **`transport.call` events now split spawn cost from script cost (closes #939)** — every call now carries `spawnFloorMs` (the calibrated `osascript` fork + interpreter init + JXA bridge bring-up, measured once per process via a no-op JXA script) and `scriptMs = max(0, durationMs - spawnFloorMs)` alongside the existing `durationMs`. Calibration runs lazily on the first transport call (fire-and-forget) and the result is cached for the lifetime of the process; one `observability.spawn.calibrated` event records the floor value. `durationMs` is preserved for back-compat, so existing log consumers keep working unchanged. The split answers the decision-gate question for the persistent-osascript-REPL track (#882) and the latency-aggregator work (#936-Medium / #936-Full): if spawn cost dominates per-workflow time, the REPL is worth the investment; if it doesn't, the case dissolves.
580
+
581
+ ### Changed
582
+
583
+ - **`task_reclassify` description trimmed back under the per-tool token budget (closes #814)** — pulled the discriminated-union AST grammar dump out of the description (the full schema is already delivered in `tools/list` JSONSchema, where the model reads it precisely); merged the redundant "prefer task_reclassify" paragraph into the existing "Do NOT use" guidance; dropped one of the two examples (the dry-run variant duplicated the apply-phase example). Token cost: 352 → ~279 (–73 tokens, ~21% of the description). Total `tools/list` description payload: 23,689 → 23,581 tokens. The exemption in `descriptions.lint.test.ts` is gone — no tool currently exceeds the 350-token budget. Behavior unchanged. A reproducible audit harness lives at `scripts/audit-description-sizes.ts` for future passes.
584
+
262
585
  ### Fixed
263
586
 
587
+ - **`task_create` and `task_update` no longer time out on notes larger than ~2KB (closes #937)** — JXA's property-bag serialization for `ofApp.Task({note: longString, …})` and multi-field `task_update` calls scales poorly with large strings; the 30s `runJxaScript` hard timeout fired empirically around the 2KB note mark, making detailed bug reports / code snippets / link bundles uncreatable in a single call. `JxaTransport.createTask` and `updateTask` now detect notes above a 1 KB byte threshold (`NOTE_INLINE_THRESHOLD_BYTES`) and split them off into a dedicated single-field `task_update` follow-up — one setter, one field, well under the timeout. Single-field note-only updates (`note_set`, `note_append`, callers passing only `{note}`) take the fast path unchanged. Project create/update and batch task create/update have the same bug class but separate code paths; tracked as follow-up.
588
+ - **`task_set_repetition` / `task_clear_repetition` now actually persist the rule (closes #938)** — both tools previously returned a success envelope while the underlying `repetition` field on the OmniFocus task remained unchanged. Two-layer bug: `JxaTransport.updateTask` dropped the `repetition` patch field on the floor, and `task_update.js` had no branch to handle it. Fixed by plumbing `repetition` through to the script and delegating the write to OmniJS via `evaluateJavascript` — same pattern as the existing `tagIds` workaround for OF 4.x JXA silent-write quirks. The rule is serialized to an RFC 5545 RRULE string (`FREQ=DAILY;INTERVAL=1`, `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR`, `FREQ=MONTHLY;BYMONTHDAY=15`) and applied via `new Task.RepetitionRule(ruleString, Task.RepetitionMethod.<Fixed|Start|DueDate>)`. Optional `weekdays` (weekly) and `monthlyAnchor` (monthly) are now passed to OmniFocus; round-trip read-back of those fields via `buildRepetition` is tracked as follow-up — current read-back still returns only `{method, unit, steps}`.
264
589
  - **`moveProject` routes through OmniJS + JXA folder-readback survives OF 4.x quirk (closes #681)** — fourth fix in the [ADR-0019](./docs/adr/0019-cross-transport-id-interoperability.md) series. The first slice of #681 routed `createProject` through OmniJS but left `moveProject` on JXA, where `target.move({ to: folder.projects.end })` fails with "Attempted to move data objects to a nil container" on OmniJS-created project specifiers. New `src/scripts/omnijs/project_move.js` uses `moveSections([proj], destination)` and resolves the destination via `flattenedFolders.filter(...)` (or `library` for the root). Routing flips `moveProject: "jxa"` → `"omnijs"`. Separately, the JXA folder-readback path on every project script (`project_get.js`, `project_get_many.js`, `project_list.js`, `project_create.js`, `project_update.js`) carried the same broken `f.class() !== "document"` guard that #673 already fixed for tasks: `f.class()` throws "Can't convert types" on a real Folder specifier in OmniFocus 4.x JXA, so the readback was silently returning `folderId: null` for every project in a folder. Replaced with the nested-try-catch pattern from #673 — treat the throw as "real folder", treat a successful return of `"document"` as the only skip path. The moveProject integration test now passes; project reads through JXA correctly surface `folderId` again.
265
590
  - **`duplicateTask` routes through OmniJS for cross-transport ID interoperability (closes #692)** — third sibling fix in the [ADR-0019](./docs/adr/0019-cross-transport-id-interoperability.md) series after [#680](https://github.com/torsday/omnifocus-mcp/issues/680) (createTask) and [#681](https://github.com/torsday/omnifocus-mcp/issues/681) (createProject). JXA's `task.duplicate()` and `container.make({...})` produce transient specifier IDs that downstream OmniJS reads can't resolve. OmniJS's `duplicateTasks([source], position)` and `new Task(name, position)` produce clones whose `id.primaryKey` is interoperable with both transports. New `src/scripts/omnijs/task_duplicate.js` mirrors the JXA props-copy surface (name, note, flagged, defer/due dates, estimatedMinutes, sequential, tags) and resets completion state on the clone (matching the JXA contract). Recursive clones use `duplicateTasks` and walk the resulting subtree to clear inherited `completed` flags; non-recursive clones build a single fresh task via `new Task(...)` — naturally produces an uncompleted childless result. Routing flips `duplicateTask: "jxa"` → `duplicateTask: "omnijs"`. Three of four duplicateTask integration tests now pass (was 1 of 4). The recursive case partially passes — `descendantCount` correct, but its downstream `listTasks({ parentId })` assertion still trips on a separate pre-existing JXA filter bug where parentId returns grandchildren too. Will file a follow-up for that.
266
591
  - **`createTask` routes through OmniJS for cross-transport ID interoperability (closes #680)** — sibling fix to [#681](https://github.com/torsday/omnifocus-mcp/issues/681). Per [ADR-0019](./docs/adr/0019-cross-transport-id-interoperability.md), JXA's `Task(props) + push()` returned a transient specifier ID that didn't match OmniFocus's persistent `id.primaryKey`, breaking subsequent OmniJS-routed downstream operations (`moveTask`, `reorderTask`, `duplicateTask`) which use the persistent key. New `src/scripts/omnijs/task_create.js` mirrors the JXA props-set surface (parent-task / project / inbox positions, note, flagged, defer/due dates, estimatedMinutes, tagIds, sequential, completedByChildren) and produces a task whose ID round-trips correctly across both transports. Routing-table flip: `createTask: "jxa"` → `createTask: "omnijs"`. Five of the seven named integration tests in #680 now pass: `createTask with projectId places the task in that project`, `moveTask into a project updates projectId`, and four `reorderTask` variants. The three `duplicateTask` failures and the `reorderTask validation when reference has different parent` failure trace to separate root causes (filed as follow-ups). Caller wrappers, OmniJsTransport contract, router exclusivity allowlist, and the routing-domain unit tests all updated to reflect the move; concurrent-test JXA-write fixtures now demonstrate via `updateTask` (still JXA-routed) since `createTask` is no longer the canonical example.