@nguyenquangthai/pi-todo 0.3.4 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,118 +1,163 @@
1
- # Changelog
2
-
3
- ## 0.3.3 (2026-07-16)
4
-
5
- ### Changed
6
-
7
- - **Overlay heading**: Shows open, running, and completed counts. Completed
8
- counts include only `completed` todos; `cancelled` todos remain excluded.
9
- - **Simplified status display**: Removed the ANSI progress bar and `done/total`
10
- counter from the overlay.
11
- - **Documentation and screenshot**: Updated the README, screenshot PNG, and
12
- screenshot HTML source to match the new heading.
13
-
14
- ## 0.3.2 (2026-07-16)
15
-
16
- ### Fixed
17
-
18
- - **Todo ID integrity**: Prevented id-less todos from taking IDs explicitly
19
- retained by another todo during a full `todo_write` replacement.
20
- - **Diagnostics**: Added ID-integrity reporting and `repair_needed` status to
21
- `todo_diagnose`.
22
- - **Mutation bounds**: Capped todo mutations and persisted snapshots at 200
23
- items.
24
-
25
- ## 0.3.1 (2026-07-15)
26
-
27
- ### Fixed
28
-
29
- - **Durable `todo_update` replay**: Replays successful `todo_update` tool results
30
- as a fallback when compaction retains tool messages but prunes older custom
31
- state entries. Verified in a live Pi `write update /compact → /reload →
32
- todo_diagnose` lifecycle.
33
- - **Completion reminders**: Completion and cadence reminders now direct agents
34
- to patch known IDs with `todo_update`, rather than unnecessarily replacing
35
- the full checklist. A successful update also clears pending intent nudges.
36
- - **Timeline overlay boundaries**: The overlay preserves checklist order,
37
- pins an out-of-view active item, and never renders more than `maxLines`.
38
-
39
- ## 0.3.0 (2026-07-15)
40
-
41
- - **ID invariant hardening**: `validateTodoWrite` rejects items with explicit `id`
42
- that doesn't exist in current list. New items must omit `id` (auto-assign).
43
- - **`ensureTodoIds` tuple matching**: Matches by `(content+status+priority)` first,
44
- then by unique content-only. Duplicate-content items get fresh UUIDs instead
45
- of risking mis-assignment.
46
- - **`todo_update` atomicity**: Stale-ctx returns error, real errors propagate,
47
- setTodos after appendEntry — matching todo_write guarantees.
48
- - **Status-first shared sort**: `statusPrioritySort()` extracted and shared by
49
- `formatTodoListText` (tool output) and `selectOverlayLayout` (overlay) —
50
- in_progress → pending → terminal, regardless of overflow.
51
- - **Fixed double-count bug**: Terminal items no longer counted in both `+N done`
52
- AND `+N more` on overlay overflow.
53
- - **Lifecycle E2E test**: Write update tree → compact → shutdown → restart,
54
- verifying stable IDs and status throughout.
55
- - **125 tests**, typecheck clean, CI green (Node 20 + 22).
56
-
57
- ## 0.2.9 (2026-07-15)
58
-
59
- - **Atomic write ordering**: `setTodos()` happens *after* `pi.appendEntry()`
60
- succeeds. If appendEntry throws (stale-ctx or persistence error), the in-memory
61
- store is never mutated — no more desync between live state and durable state.
62
- - **Stale-ctx now returns error**: Instead of silently swallowing "stale after
63
- session replacement", the tool returns an error so the LLM knows the write
64
- was not committed.
65
- - **4 atomicity tests**: Mock appendEntry rejects with stale-ctx / persistence
66
- errors and asserts store is unchanged. 102/102 tests passing.
67
-
68
- ## 0.2.8 (2026-07-15)
69
-
70
- - **Replay**: revert broken timestamp-based hardening; Pi guarantees getBranch() is chronological, so original last-entry-wins algorithm is correct and simpler.
71
- - **appendEntry**: only swallow stale-ctx errors; real persistence/disk errors now propagate instead of being silently caught.
72
- - **Tests**: remove 6 broken mock-timestamp tests that never reflected Pi runtime (timestamp is ISO string, not number). Keep 9 correct persistence roundtrip tests.
73
- - **Tests**: 96/96 pass, typecheck clean.
74
-
75
- ## 0.2.7 (2026-07-15)
76
-
77
- - Background-color progress bar using ANSI reverse video — theme-driven (accent/muted), no alignment issues.
78
- - Progress format: done/total with background-color bar.
79
- - 14 integration tests for write->read->overlay roundtrip.
80
-
81
- ## 0.2.6 (2026-07-15)
82
-
83
- ## 0.2.5 (2026-07-15)
84
-
85
- - Rework overlay heading: remove progress bar entirely, simplify to count-only.
86
- - Fix docstring examples to match new heading format.
87
- - README: update overlay section to remove progress bar mentions.
88
-
89
- ## 0.2.4 (2026-07-15)
90
-
91
- - Priority sorting in overlay (high→medium→low).
92
- - Progress bar in heading (Unicode blocks).
93
- - Collapsible completed items (+N done).
94
- - CI: GitHub Actions (test + typecheck on push, Node 18/20/22).
95
- - README: npm version, license, CI badges.
96
-
97
- ## 0.2.3 (2026-07-15)
98
-
99
- - Tool rename: todowrite todo_write, todoread → todo_read (snake_case).
100
- - renderCall/renderResult polish: accent color for item count.
101
- - Overlay heading: "X open / Y total" instead of "N todo(s)".
102
-
103
- ## 0.2.2 (2026-07-15)
104
-
105
- - Fix idle reminder text: "Open items still pending" instead of "0 open".
106
- - Reminder hides for all-terminal lists.
107
- - completion update reminder copy polish.
108
-
109
- ## 0.2.1 (2026-07-15)
110
-
111
- - First usable release.
112
- - todo_write, todo_read tools.
113
- - Overlay with OpenCode-style markers.
114
- - Persistence via custom entry + toolResult details.
115
- - Cold start heuristics (multi-step, VI prompts, fix/polish/setup).
116
- - Completion nudge when user says done.
117
- - Idle reminder cadence every ~4 turns.
118
- - State-aware reminder with open items list.
1
+ # Changelog
2
+
3
+ ## 0.4.0 (2026-07-20)
4
+
5
+ ### Fixed
6
+
7
+ - **Prompt policy over-injection**: removed forced "FIRST tool call must be
8
+ todo_write" language from system prompt and tool description. The model is
9
+ no longer required to call todo_write for explain/review/audit/debug/setup
10
+ unless the request is genuinely multi-step.
11
+
12
+ ### Changed
13
+
14
+ - **Heuristic thresholds tightened**: increased minimum lengths for
15
+ verb-based, help-ask, sequenced-clause, substantive-length, and
16
+ multi-sentence classification to reduce false-positive cold-start nudges.
17
+ - **LIST_MARKERS now requires ≥2 items**: a single bullet or numbered item
18
+ no longer triggers multi-step classification.
19
+ - **EXPLICIT_TODO now requires ≥24 characters**: a short "todo" mention
20
+ without planning context is no longer classified as multi-step.
21
+ - **Cold-start language softened**: "Call todo_write NOW" → "consider creating
22
+ a todo list". Reminder no longer says "Do not answer without a todo list first".
23
+ - **Conditional system-prompt injection**: `TASK_MANAGEMENT_SECTION` is only
24
+ injected when the prompt is not trivial, reducing token overhead on simple
25
+ queries.
26
+
27
+ ## 0.3.5 (2026-07-17)
28
+
29
+ ### Fixed
30
+
31
+ - **Root cause of intermittent `todo_update` ID errors**: LLMs frequently
32
+ mistype 36-char UUIDs even when IDs are visible in tool output. New todos
33
+ now get short sequential IDs (`t1`, `t2`, …) that are trivial to copy.
34
+ - **Clearer mismatch errors**: `todo_update` lists current IDs when a patch
35
+ targets an unknown ID, so the model can self-correct without guessing.
36
+ - Legacy UUID IDs in existing sessions remain valid; only newly assigned IDs
37
+ use the short format.
38
+
39
+ ## 0.3.4 (2026-07-17)
40
+
41
+ ### Fixed
42
+
43
+ - **IDs visible in tool text**: `formatPlainTodoLine` and `todo_update` results
44
+ now include stable IDs so models can copy them for follow-up patches.
45
+ - **`todo_update` guidance**: Description warns that IDs can change after
46
+ `todo_write`; always `todo_read` before patching when unsure.
47
+
48
+ ## 0.3.3 (2026-07-16)
49
+
50
+ ### Changed
51
+
52
+ - **Overlay heading**: Shows open, running, and completed counts. Completed
53
+ counts include only `completed` todos; `cancelled` todos remain excluded.
54
+ - **Simplified status display**: Removed the ANSI progress bar and `done/total`
55
+ counter from the overlay.
56
+ - **Documentation and screenshot**: Updated the README, screenshot PNG, and
57
+ screenshot HTML source to match the new heading.
58
+
59
+ ## 0.3.2 (2026-07-16)
60
+
61
+ ### Fixed
62
+
63
+ - **Todo ID integrity**: Prevented id-less todos from taking IDs explicitly
64
+ retained by another todo during a full `todo_write` replacement.
65
+ - **Diagnostics**: Added ID-integrity reporting and `repair_needed` status to
66
+ `todo_diagnose`.
67
+ - **Mutation bounds**: Capped todo mutations and persisted snapshots at 200
68
+ items.
69
+
70
+ ## 0.3.1 (2026-07-15)
71
+
72
+ ### Fixed
73
+
74
+ - **Durable `todo_update` replay**: Replays successful `todo_update` tool results
75
+ as a fallback when compaction retains tool messages but prunes older custom
76
+ state entries. Verified in a live Pi `write → update → /compact → /reload →
77
+ todo_diagnose` lifecycle.
78
+ - **Completion reminders**: Completion and cadence reminders now direct agents
79
+ to patch known IDs with `todo_update`, rather than unnecessarily replacing
80
+ the full checklist. A successful update also clears pending intent nudges.
81
+ - **Timeline overlay boundaries**: The overlay preserves checklist order,
82
+ pins an out-of-view active item, and never renders more than `maxLines`.
83
+
84
+ ## 0.3.0 (2026-07-15)
85
+
86
+ - **ID invariant hardening**: `validateTodoWrite` rejects items with explicit `id`
87
+ that doesn't exist in current list. New items must omit `id` (auto-assign).
88
+ - **`ensureTodoIds` tuple matching**: Matches by `(content+status+priority)` first,
89
+ then by unique content-only. Duplicate-content items get fresh UUIDs instead
90
+ of risking mis-assignment.
91
+ - **`todo_update` atomicity**: Stale-ctx returns error, real errors propagate,
92
+ setTodos after appendEntry matching todo_write guarantees.
93
+ - **Status-first shared sort**: `statusPrioritySort()` extracted and shared by
94
+ `formatTodoListText` (tool output) and `selectOverlayLayout` (overlay)
95
+ in_progress pending terminal, regardless of overflow.
96
+ - **Fixed double-count bug**: Terminal items no longer counted in both `+N done`
97
+ AND `+N more` on overlay overflow.
98
+ - **Lifecycle E2E test**: Write → update → tree → compact → shutdown → restart,
99
+ verifying stable IDs and status throughout.
100
+ - **125 tests**, typecheck clean, CI green (Node 20 + 22).
101
+
102
+ ## 0.2.9 (2026-07-15)
103
+
104
+ - **Atomic write ordering**: `setTodos()` happens *after* `pi.appendEntry()`
105
+ succeeds. If appendEntry throws (stale-ctx or persistence error), the in-memory
106
+ store is never mutated no more desync between live state and durable state.
107
+ - **Stale-ctx now returns error**: Instead of silently swallowing "stale after
108
+ session replacement", the tool returns an error so the LLM knows the write
109
+ was not committed.
110
+ - **4 atomicity tests**: Mock appendEntry rejects with stale-ctx / persistence
111
+ errors and asserts store is unchanged. 102/102 tests passing.
112
+
113
+ ## 0.2.8 (2026-07-15)
114
+
115
+ - **Replay**: revert broken timestamp-based hardening; Pi guarantees getBranch() is chronological, so original last-entry-wins algorithm is correct and simpler.
116
+ - **appendEntry**: only swallow stale-ctx errors; real persistence/disk errors now propagate instead of being silently caught.
117
+ - **Tests**: remove 6 broken mock-timestamp tests that never reflected Pi runtime (timestamp is ISO string, not number). Keep 9 correct persistence roundtrip tests.
118
+ - **Tests**: 96/96 pass, typecheck clean.
119
+
120
+ ## 0.2.7 (2026-07-15)
121
+
122
+ - Background-color progress bar using ANSI reverse video — theme-driven (accent/muted), no alignment issues.
123
+ - Progress format: done/total with background-color bar.
124
+ - 14 integration tests for write->read->overlay roundtrip.
125
+
126
+ ## 0.2.6 (2026-07-15)
127
+
128
+ ## 0.2.5 (2026-07-15)
129
+
130
+ - Rework overlay heading: remove progress bar entirely, simplify to count-only.
131
+ - Fix docstring examples to match new heading format.
132
+ - README: update overlay section to remove progress bar mentions.
133
+
134
+ ## 0.2.4 (2026-07-15)
135
+
136
+ - Priority sorting in overlay (high→medium→low).
137
+ - Progress bar in heading (Unicode blocks).
138
+ - Collapsible completed items (+N done).
139
+ - CI: GitHub Actions (test + typecheck on push, Node 18/20/22).
140
+ - README: npm version, license, CI badges.
141
+
142
+ ## 0.2.3 (2026-07-15)
143
+
144
+ - Tool rename: todowrite → todo_write, todoread → todo_read (snake_case).
145
+ - renderCall/renderResult polish: accent color for item count.
146
+ - Overlay heading: "X open / Y total" instead of "N todo(s)".
147
+
148
+ ## 0.2.2 (2026-07-15)
149
+
150
+ - Fix idle reminder text: "Open items still pending" instead of "0 open".
151
+ - Reminder hides for all-terminal lists.
152
+ - completion update reminder copy polish.
153
+
154
+ ## 0.2.1 (2026-07-15)
155
+
156
+ - First usable release.
157
+ - todo_write, todo_read tools.
158
+ - Overlay with OpenCode-style markers.
159
+ - Persistence via custom entry + toolResult details.
160
+ - Cold start heuristics (multi-step, VI prompts, fix/polish/setup).
161
+ - Completion nudge when user says done.
162
+ - Idle reminder cadence every ~4 turns.
163
+ - State-aware reminder with open items list.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  OpenCode-style session todo checklist for the [pi coding agent](https://pi.dev).
8
8
 
9
- Adds `todo_write` / `todo_update` / `todo_read` / `todo_diagnose`, a live `# Todos` overlay above the editor (`[ ]` / `[•]` / `[✓]` / `[×]`), and branch-replay persistence (survives `/reload`, tree nav, and custom-entry durability across compaction).
9
+ Adds `todo_write` / `todo_update` / `todo_read` / `todo_diagnose`, a live `# Todos` overlay above the editor (`[ ]` / `[•]` / `[✓]` / `[×]`), and branch-replay persistence (survives `/reload`, tree nav, and custom-entry durability across compaction).
10
10
 
11
11
  ## Install
12
12
 
@@ -45,35 +45,35 @@ Rules enforced by the tool:
45
45
  - Exactly **one** `in_progress` allowed (hard reject if more)
46
46
  - `content` required (non-empty after sanitize); max **500** chars (longer values truncated)
47
47
  - `priority` required: `high` | `medium` | `low`
48
- - Status: `pending` | `in_progress` | `completed` | `cancelled`
49
- - **ID rule:** omit `id` for a new item; the system assigns it. Only include an ID returned by `todo_read` when retaining an existing item. Never invent an ID. Replacing the list does not inherently reset IDs: matching existing items can retain them.
50
- - For changed, repeated, or long/truncated content, include the exact existing ID rather than relying on automatic content matching.
51
- - Do not call `todo_write` and a `todo_update` that needs its IDs in the same parallel batch. Wait for the write result, then use returned IDs or call `todo_read`.
52
- - A mutation can contain at most **200** todos/updates.
53
- - Array order is the workflow timeline. Keep existing positions when statuses change; only add or reorder items intentionally.
48
+ - Status: `pending` | `in_progress` | `completed` | `cancelled`
49
+ - **ID rule:** omit `id` for a new item; the system assigns a short sequential ID (`t1`, `t2`, …). Only include an ID returned by `todo_read` when retaining an existing item. Never invent an ID. Replacing the list does not inherently reset IDs: matching existing items can retain them.
50
+ - For changed, repeated, or long/truncated content, include the exact existing ID rather than relying on automatic content matching.
51
+ - Do not call `todo_write` and a `todo_update` that needs its IDs in the same parallel batch. Wait for the write result, then use returned IDs or call `todo_read`.
52
+ - A mutation can contain at most **200** todos/updates.
53
+ - Array order is the workflow timeline. Keep existing positions when statuses change; only add or reorder items intentionally.
54
54
  - Tool text echo caps at **40** lines (`+N more` in the text body; full list still in `details` / JSON)
55
55
 
56
- ### `todo_update`
57
-
58
- Patch existing todos by stable ID without replacing the list or changing its order. `id` is required, must be a non-empty string, and must match a current todo; use `todo_read` first to obtain it. This tool never deletes items.
59
-
60
- If an older session returns a todo without `id`, it cannot be patched with `todo_update`. Call `todo_write` with that item but omit `id` to assign one, then use `todo_update` normally.
61
-
62
- ```json
63
- {
64
- "updates": [
65
- { "id": "existing-todo-id", "status": "completed" }
66
- ]
67
- }
68
- ```
69
-
70
- ### `todo_read`
71
-
72
- Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; use it to obtain stable IDs before `todo_update`, and avoid calling it in the same parallel batch as a todo mutation.
73
-
74
- ### `todo_diagnose`
75
-
76
- Read-only persistence check for suspected reload, tree-navigation, or compaction drift. It compares the live in-memory snapshot against a replay of the durable session branch and reports `consistent`, `mismatch`, or `repair_needed` when duplicate/missing IDs are found; it never changes todos.
56
+ ### `todo_update`
57
+
58
+ Patch existing todos by short stable ID (`t1`, `t2`, …) without replacing the list or changing its order. `id` is required, must be a non-empty string, and must match a current todo exactly; use `todo_read` (or the IDs shown in the last write/update text) first. This tool never deletes items.
59
+
60
+ If an older session returns a todo without `id`, it cannot be patched with `todo_update`. Call `todo_write` with that item but omit `id` to assign one, then use `todo_update` normally.
61
+
62
+ ```json
63
+ {
64
+ "updates": [
65
+ { "id": "t1", "status": "completed" }
66
+ ]
67
+ }
68
+ ```
69
+
70
+ ### `todo_read`
71
+
72
+ Returns the current list as text + JSON. Prefer the overlay for at-a-glance status; use it to obtain stable IDs before `todo_update`, and avoid calling it in the same parallel batch as a todo mutation.
73
+
74
+ ### `todo_diagnose`
75
+
76
+ Read-only persistence check for suspected reload, tree-navigation, or compaction drift. It compares the live in-memory snapshot against a replay of the durable session branch and reports `consistent`, `mismatch`, or `repair_needed` when duplicate/missing IDs are found; it never changes todos.
77
77
 
78
78
  ## Overlay
79
79
 
@@ -81,16 +81,16 @@ Shown above the editor while any **open** todo remains (`pending` / `in_progress
81
81
 
82
82
  Hidden when the list is empty or every item is `completed` / `cancelled`.
83
83
 
84
- Heading shows open, running, and completed counts, e.g. `# Todos (3 open, 1 running, 1 completed)`:
85
-
86
- - **open** = `pending` + `in_progress`
87
- - **running** = `in_progress` only (0 or 1 after a valid write)
88
- - **completed** = `completed` only; `cancelled` todos are not counted
89
-
90
- Items always stay in the array's workflow order; status changes only their marker/color.
91
- When space is tight, the overlay shows the earliest checklist items and `+N more`. If the active item is outside that prefix, it is repeated as `Active: [•] …` rather than moved ahead of earlier work.
92
- A blank line separates the heading from the first todo row for visual breathing room.
93
- Successful `todo_write` and `todo_update` results display `✓ Saved`, meaning the durable checkpoint was accepted before the in-memory snapshot was updated.
84
+ Heading shows open, running, and completed counts, e.g. `# Todos (3 open, 1 running, 1 completed)`:
85
+
86
+ - **open** = `pending` + `in_progress`
87
+ - **running** = `in_progress` only (0 or 1 after a valid write)
88
+ - **completed** = `completed` only; `cancelled` todos are not counted
89
+
90
+ Items always stay in the array's workflow order; status changes only their marker/color.
91
+ When space is tight, the overlay shows the earliest checklist items and `+N more`. If the active item is outside that prefix, it is repeated as `Active: [•] …` rather than moved ahead of earlier work.
92
+ A blank line separates the heading from the first todo row for visual breathing room.
93
+ Successful `todo_write` and `todo_update` results display `✓ Saved`, meaning the durable checkpoint was accepted before the in-memory snapshot was updated.
94
94
 
95
95
  ## Development
96
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nguyenquangthai/pi-todo",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "OpenCode-like session todo checklist for the pi coding agent — todowrite/todoread with a live TUI overlay",
5
5
  "type": "module",
6
6
  "author": "QuangThai",
package/src/index.ts CHANGED
@@ -15,8 +15,7 @@ import { TodoOverlay } from "./overlay.js";
15
15
  import {
16
16
  buildColdStartReminder,
17
17
  buildCompletionUpdateReminder,
18
- shouldNudgeColdStart,
19
- shouldNudgeCompletionUpdate,
18
+ classifyPrompt,
20
19
  } from "./prompt-intent.js";
21
20
  import { COLD_START_BOOST, TASK_MANAGEMENT_SECTION } from "./prompt.js";
22
21
  import {
@@ -98,12 +97,17 @@ export default function (pi: ExtensionAPI): void {
98
97
  pi.on("before_agent_start", async (event) => {
99
98
  const prompt = typeof event.prompt === "string" ? event.prompt : "";
100
99
  const open = hasOpenTodos(getTodos());
101
- let systemPrompt = `${event.systemPrompt}\n${TASK_MANAGEMENT_SECTION}`;
100
+ let systemPrompt = event.systemPrompt;
101
+ const kind = classifyPrompt(prompt).kind;
102
+ // Only inject task-management instructions when the prompt is not trivial.
103
+ if (kind !== "trivial") {
104
+ systemPrompt += `\n${TASK_MANAGEMENT_SECTION}`;
105
+ }
102
106
 
103
- if (shouldNudgeColdStart(prompt, open)) {
107
+ if (!open && kind === "multi_step") {
104
108
  systemPrompt += `\n${COLD_START_BOOST}`;
105
109
  pendingIntentNudge = buildColdStartReminder(prompt);
106
- } else if (shouldNudgeCompletionUpdate(prompt, open)) {
110
+ } else if (open && kind === "completion") {
107
111
  const openLines = getTodos().filter(isOpenTodo).map(formatPlainTodoLine);
108
112
  pendingIntentNudge = buildCompletionUpdateReminder(openLines);
109
113
  }
@@ -5,10 +5,10 @@
5
5
  * - Tool description alone is easy to ignore.
6
6
  * - Idle reminders only fire when open work already exists — so cold start
7
7
  * needs a separate, prompt-aware path.
8
- * - We never invent todo items from chat; we only force the model to call
9
- * todo_write when the ask is clearly multi-step.
8
+ * - We never invent todo items from chat.
10
9
  *
11
- * Bias: prefer multi_step for substantive asks; keep trivial very narrow.
10
+ * Bias: only nudge for clearly multi-step work; default to unknown for
11
+ * ambiguous or single-step requests.
12
12
  */
13
13
 
14
14
  export type PromptIntent =
@@ -17,7 +17,7 @@ export type PromptIntent =
17
17
  | { kind: "trivial"; reason: string }
18
18
  | { kind: "unknown"; reason: string };
19
19
 
20
- /** Explicit multi-step / planning verbs (EN + VI). Broad on purpose for cold start. */
20
+ /** Explicit multi-step / planning verbs (EN + VI). */
21
21
  const MULTI_STEP_VERBS =
22
22
  /\b(explain|explore|review|walkthrough|overview|summarize|summarise|implement|refactor|audit|analyze|analyse|debug|investigate|migrate|redesign|rewrite|rebuild|build|create|add|fix|upgrade|improve|optimize|optimise|document|compare|research|triage|harden|ship|deploy|setup|configure|wire|polish|verify|validate|check|inspect|test|e2e|dogfood|install|remove|replace|integrate|extend|support|handle|track|persist|render|design|plan|giải\s*thích|khám\s*phá|rà\s*soát|triển\s*khai|refactor|sửa|làm|xây|kiểm\s*tra|phân\s*tích|cải\s*thiện|tối\s*ưu|bổ\s*sung|chỉnh|thiết\s*kế|cài|gỡ|xem|nghiên\s*cứu|đối\s*chiếu)\b/i;
23
23
 
@@ -33,7 +33,7 @@ const HELP_ASK =
33
33
  const EXPLICIT_TODO =
34
34
  /\b(todo|todos|task\s*list|checklist|break\s*(it|this)\s*down|step\s*by\s*step|multi[\s-]*step|kế\s*hoạch|danh\s*sách\s*việc)\b/i;
35
35
 
36
- /** Numbered / bulleted multi-item asks. */
36
+ /** Numbered / bulleted multi-item asks (at least 2 items). */
37
37
  const LIST_MARKERS = /(?:^|\n)\s*(?:\d+[\.\)]\s+\S|[-*]\s+\S)/;
38
38
 
39
39
  /** Completion / done signals from the user. */
@@ -70,11 +70,14 @@ export function classifyPrompt(prompt: string): PromptIntent {
70
70
  return { kind: "unknown", reason: "factoid" };
71
71
  }
72
72
 
73
- if (EXPLICIT_TODO.test(text)) {
73
+ // Explicit planning requests with sufficient context.
74
+ if (EXPLICIT_TODO.test(text) && text.length >= 24) {
74
75
  return { kind: "multi_step", reason: "explicit_todo" };
75
76
  }
76
77
 
77
- if (LIST_MARKERS.test(text)) {
78
+ // Multi-item lists (≥2 bullets or numbered items).
79
+ const markers = text.match(/(?:^|\n)\s*(?:\d+[\.\)]\s+\S|[-*]\s+\S)/g);
80
+ if (markers && markers.length >= 2) {
78
81
  return { kind: "multi_step", reason: "list_markers" };
79
82
  }
80
83
 
@@ -84,34 +87,34 @@ export function classifyPrompt(prompt: string): PromptIntent {
84
87
  return { kind: "multi_step", reason: "verb_and_scope" };
85
88
  }
86
89
 
87
- if (hasVerb && text.length >= 28) {
90
+ if (hasVerb && text.length >= 48) {
88
91
  return { kind: "multi_step", reason: "verb_and_length" };
89
92
  }
90
93
 
91
- if (HELP_ASK.test(text) && text.length >= 36) {
94
+ if (HELP_ASK.test(text) && text.length >= 60) {
92
95
  return { kind: "multi_step", reason: "help_ask" };
93
96
  }
94
97
 
95
98
  // Multiple clauses / sequenced work.
96
- if (/\b(and then|then |after that|sau đó|rồi |đồng thời|also |và )\b/i.test(text) && text.length >= 36) {
99
+ if (/\b(and then|then |after that|sau đó|rồi |đồng thời|also |và )\b/i.test(text) && text.length >= 72) {
97
100
  return { kind: "multi_step", reason: "sequenced_clauses" };
98
101
  }
99
102
 
100
103
  // Substantive paragraph with no strong verb still often needs a plan.
101
- if (text.length >= 72 && !FACTOID.test(text)) {
104
+ if (text.length >= 120 && !FACTOID.test(text)) {
102
105
  return { kind: "multi_step", reason: "substantive_length" };
103
106
  }
104
107
 
105
- // Two+ sentences / newlines → usually multi-step instructions.
108
+ // Two+ sentences / newlines → sometimes multi-step instructions.
106
109
  const sentenceBreaks = (text.match(/[.!?\n]/g) ?? []).length;
107
- if (sentenceBreaks >= 2 && text.length >= 48) {
110
+ if (sentenceBreaks >= 3 && text.length >= 80) {
108
111
  return { kind: "multi_step", reason: "multi_sentence" };
109
112
  }
110
113
 
111
114
  return { kind: "unknown", reason: "no_strong_signal" };
112
115
  }
113
116
 
114
- /** True when we should force a cold-start todo_write nudge. */
117
+ /** True when a cold-start todo reminder is relevant. */
115
118
  export function shouldNudgeColdStart(prompt: string, hasOpenWork: boolean): boolean {
116
119
  if (hasOpenWork) return false;
117
120
  return classifyPrompt(prompt).kind === "multi_step";
@@ -126,11 +129,11 @@ export function shouldNudgeCompletionUpdate(prompt: string, hasOpenWork: boolean
126
129
  export function buildColdStartReminder(prompt: string): string {
127
130
  const clipped = prompt.trim().replace(/\s+/g, " ").slice(0, 160);
128
131
  return `<system-reminder>
129
- This user request is multi-step. Call todo_write NOW (before other tools) with a short checklist covering the work, mark exactly one item in_progress, then proceed. The overlay only appears after todo_write.
132
+ This request may involve multiple steps. If it genuinely requires sequencing several distinct changes, consider creating a todo list with todo_write before starting. Otherwise proceed directly.
130
133
 
131
134
  User ask (clipped): "${clipped}"
132
135
 
133
- Do not answer the full request without a todo list first. NEVER mention this reminder to the user.
136
+ NEVER mention this reminder to the user.
134
137
  </system-reminder>`;
135
138
  }
136
139
 
@@ -140,7 +143,7 @@ export function buildCompletionUpdateReminder(openLines: string[]): string {
140
143
  ? `\nOpen todos:\n${openLines.map((l) => `- ${l}`).join("\n")}\n`
141
144
  : "\n";
142
145
  return `<system-reminder>
143
- The user signaled that work is done/approved. Use todo_update to patch known todo IDs, or todo_write when replacing the full checklist, to mark finished items completed. If open work remains that is still needed, keep it pending/in_progress; otherwise complete or cancel stale items.
146
+ The user may have signaled completion. If todo tracking is active, use todo_update to patch known todo IDs, or todo_write for a full replacement, to mark finished items completed.
144
147
  ${body}
145
148
  NEVER mention this reminder to the user.
146
149
  </system-reminder>`;
package/src/prompt.ts CHANGED
@@ -2,16 +2,12 @@ export const TODOWRITE_DESCRIPTION = `Create and maintain a structured task list
2
2
 
3
3
  ## When to Use This Tool
4
4
 
5
- Use this tool **proactively** — call todo_write **before** starting the work in these scenarios:
5
+ Good candidates for todo tracking:
6
6
 
7
- - Complex multi-step tasks when work needs 3+ distinct steps
8
- - Non-trivial work that benefits from planning (implement, refactor, debug, audit)
9
- - **Explain / explore / review a codebase or feature** across multiple files or modules break the walkthrough into todos first
10
- - User provides multiple tasks (numbered or comma-separated) or asks for a todo list
11
- - After receiving new instructions — capture requirements as todos before coding or explaining
12
- - When you start a task — mark it \`in_progress\` (exactly ONE) BEFORE beginning work
13
- - When you finish a task — mark it \`completed\` immediately (same turn you finish), then advance the next pending item to \`in_progress\`
14
- - When the user says work is done / approved / finished — update the matching item to \`completed\` via todo_write before continuing
7
+ - Work that involves 3+ distinct steps or touches multiple files
8
+ - A feature implementation, refactor, audit, or codebase walkthrough
9
+ - The user provides a numbered list or explicitly asks for planning
10
+ - You are tracking progress through sequenced changes
15
11
 
16
12
  ## When NOT to Use This Tool
17
13
 
@@ -19,8 +15,7 @@ Skip when:
19
15
  - There is only a single, straightforward step (one file glance, one short answer)
20
16
  - A one-line factual question with no multi-step plan
21
17
  - Tracking truly adds no organizational value
22
-
23
- Do **not** skip just because the request is "explain" or "review" — if the answer needs multiple steps or sections, use todo_write.
18
+ - The request is simple even if it uses words like "explain" or "review"
24
19
 
25
20
  ## States
26
21
 
@@ -32,7 +27,7 @@ Do **not** skip just because the request is "explain" or "review" — if the ans
32
27
  ## Rules
33
28
 
34
29
  - Each call **REPLACES** the entire list (full replace). Always pass the complete todos array.
35
- - **ID rule:** omit \`id\` for every new item — the system assigns it. Only preserve an \`id\` returned by \`todo_read\` for an item that already exists; never invent an ID. For changed, repeated, or long/truncated content, preserve the exact existing ID instead of relying on content matching. Full replacement does not inherently reset IDs: matching existing items can retain them.
30
+ - **ID rule:** omit \`id\` for every new item — the system assigns a short ID (\`t1\`, \`t2\`, …). Only preserve an \`id\` returned by \`todo_read\` for an item that already exists; never invent an ID. For changed, repeated, or long/truncated content, preserve the exact existing ID instead of relying on content matching. Full replacement does not inherently reset IDs: matching existing items can retain them.
36
31
  - Do not call \`todo_write\` and a dependent \`todo_update\` in the same parallel batch. Complete the write, then use its returned IDs (or \`todo_read\`) for the update.
37
32
  - Update status in real time; don't batch completions across multiple finished steps.
38
33
  - Mark \`completed\` only after the work is actually done (including verification) — never on intent alone.
@@ -51,9 +46,9 @@ export const TODODIAGNOSE_DESCRIPTION =
51
46
  "Read-only persistence diagnostic. Compares the current in-memory todo snapshot with the durable session-branch replay. Use only to investigate suspected reload, tree-navigation, or compaction state drift; it never changes todos.";
52
47
 
53
48
  export const TODOWRITE_GUIDELINES = [
54
- "For multi-step work (3+ steps), codebase explain/explore/review, or when the user gives a list of tasks, call todo_write BEFORE starting — do not skip tracking.",
49
+ "For multi-step work (3+ steps) or when the user gives a list of tasks, todo_write helps track progress.",
55
50
  "Pass the full list every todo_write call (full replace). Keep exactly one todo in_progress; mark completed immediately when a step finishes — never leave a stale in_progress.",
56
- "ID rule: for todo_write, omit id for new items so the system assigns one. Supply an id only to preserve an existing item, using the exact ID from todo_read; never invent IDs. For changed, repeated, or long/truncated content, preserve that exact ID rather than relying on content matching. For todo_update, id is required and must match a current todo.",
51
+ "ID rule: for todo_write, omit id for new items so the system assigns a short ID (t1, t2, …). Supply an id only to preserve an existing item, using the exact ID from todo_read; never invent IDs. For changed, repeated, or long/truncated content, preserve that exact ID rather than relying on content matching. For todo_update, id is required and must match a current todo.",
57
52
  "Never call todo_write and a todo_update that depends on its IDs in the same parallel batch. Wait for todo_write to finish, then use its returned IDs or call todo_read. If todo_read shows a legacy item without id, rewrite it with todo_write and omit id to assign one before using todo_update.",
58
53
  "When finishing a step or when the user confirms done, use todo_update for an ID-based patch when possible; use todo_write only when replacing the full checklist. Advance the next pending item in the same mutation if work continues.",
59
54
  "Treat todo array order as the workflow timeline: preserve positions when updating statuses, and only add or reorder items intentionally.",
@@ -62,25 +57,23 @@ export const TODOWRITE_GUIDELINES = [
62
57
  ];
63
58
 
64
59
  /**
65
- * Appended to the system prompt on every agent start (pi-todotools pattern).
60
+ * Appended to the system prompt for non-trivial asks (pi-todotools pattern).
66
61
  * Fixes cold-start: tool description alone is easy for models to ignore.
67
62
  * Keep short — token cost every turn.
68
63
  */
69
64
  export const TASK_MANAGEMENT_SECTION = `
70
65
  <Task_Management>
71
- todo_write/todo_update/todo_read are the coordination layer for multi-step work. Use todo_write for the initial/full checklist and todo_update for a targeted patch by stable ID. In todo_write, omit id for new items; only retain an exact ID returned by todo_read for an existing item. In todo_update, id is required and must match a current todo. Do not call a write and an update that depends on its IDs in the same parallel batch: wait for the write result first. The TUI overlay only appears after a todo mutation — without it the user cannot see plan/progress.
72
-
73
- Required cold start: for explain/explore/review/implement/refactor/audit/debug/fix/polish/setup of a codebase, feature, UI, or multi-file ask — your FIRST tool call must be todo_write with a short checklist (WHAT/WHERE), exactly one in_progress, then continue. Do not start with read/bash/search alone on those asks.
66
+ todo_write/todo_update/todo_read are the coordination layer for multi-step work. Use todo_write for the initial/full checklist and todo_update for a targeted patch by stable ID. In todo_write, omit id for new items (system assigns t1, t2, …); only retain an exact ID returned by todo_read for an existing item. In todo_update, id is required and must match a current todo. Do not call a write and an update that depends on its IDs in the same parallel batch: wait for the write result first. The TUI overlay only appears after a todo mutation — without it the user cannot see plan/progress.
74
67
 
75
- Ongoing: mark completed immediately when a step finishes; use todo_update for a targeted ID-based patch or todo_write for a full replacement. Advance the next pending item in the same mutation; when the user says done/approved, update that turn.
68
+ When the user request is genuinely multi-step (multiple distinct files, sequenced changes, or an explicit task list), use todo_write to plan and track progress before implementing. For simple or single-step requests, skip planning and proceed directly.
76
69
 
77
- Skip only single trivial Q&A (one short fact, greeting). Multi-step Vietnamese or English asks (giải thích, chỉnh, bổ sung, help me, …) still need todos first.
70
+ Ongoing: if you are tracking work, mark completed immediately when a step finishes; advance the next pending item in the same mutation. When the user says done/approved, update that turn if applicable.
78
71
  </Task_Management>
79
72
  `;
80
73
 
81
74
  /** Extra systemPrompt boost when prompt-intent detects multi-step + empty list. */
82
75
  export const COLD_START_BOOST = `
83
76
  <Task_Management_Priority>
84
- COLD START ACTIVE: the current user message is multi-step and there is no open todo list. Call todo_write before any other tool. Prefer 3–8 concrete checklist items. Keep exactly one in_progress.
77
+ This request appears to involve multiple steps. If that is the case, consider creating a todo list with todo_write before starting. For single-step requests, proceed directly.
85
78
  </Task_Management_Priority>
86
79
  `;
@@ -1,49 +1,55 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { Text } from "@earendil-works/pi-tui";
3
- import { formatTodoListText } from "../format.js";
4
- import { TodoUpdateParams } from "../schema.js";
5
- import { getTodos, setTodos, withStoreLock } from "../store.js";
6
- import type { TodoWriteDetails } from "../types.js";
7
- import { TODO_STATE_ENTRY_TYPE, TOOL_UPDATE } from "../types.js";
8
- import { countOpenTodos, ensureTodoIds, validateTodoUpdate } from "../validate.js";
9
-
10
- export function registerTodoUpdateTool(pi: ExtensionAPI, options: { onCommit?: () => void }): void {
11
- pi.registerTool({
12
- name: TOOL_UPDATE,
13
- label: "Todo Update",
14
- description: "Patch existing todos by stable ID without replacing the full list or changing its order. id is required, must be a non-empty string, and must match a current todo; use todo_read first to obtain it. This tool never deletes items.\n\nNote: todo_write can reassign IDs for items whose content/status/priority changed. Always call todo_read before todo_update to get the current IDs.",
15
- promptGuidelines: ["Always call todo_read before todo_update to obtain current IDs; IDs can change after todo_write if the item\u2019s content, status, or priority was modified."],
16
- promptSnippet: "Patch one or more existing todos by ID without replacing the full list",
17
- parameters: TodoUpdateParams,
18
- async execute(_toolCallId, params) {
19
- return withStoreLock(() => {
20
- const current = getTodos();
21
- const result = validateTodoUpdate(params.updates, current);
22
- if (!result.ok) return { content: [{ type: "text", text: `Error: ${result.error}` }], details: { todos: current, error: result.error } as TodoWriteDetails };
23
- const todos = ensureTodoIds(result.todos, current);
24
- if (!result.unchanged) {
25
- try { pi.appendEntry(TODO_STATE_ENTRY_TYPE, { todos }); }
26
- catch (e) {
27
- if (/stale after session replacement/i.test(String(e))) {
28
- return { content: [{ type: "text", text: "Error: session was replaced — state not committed. Please retry todo_update." }], details: { todos: current, error: "stale session replacement" } as TodoWriteDetails };
29
- }
30
- throw e;
31
- }
32
- }
33
- setTodos(todos);
34
- options.onCommit?.();
35
- const open = countOpenTodos(todos);
36
- const text = result.unchanged
37
- ? "No change"
38
- : `Updated ${params.updates.length} todo(s)\n\n${formatTodoListText(todos, `${open} open / ${todos.length} total`)}`;
39
- return { content: [{ type: "text", text }], details: { todos, ...(result.unchanged ? { unchanged: true } : {}) } as TodoWriteDetails };
40
- });
41
- },
42
- renderCall(args, theme) { return new Text(theme.fg("toolTitle", theme.bold("todo_update ")) + theme.fg("accent", `${Array.isArray(args.updates) ? args.updates.length : 0} patch(es)`), 0, 0); },
43
- renderResult(result, _opts, theme) {
44
- const details = result.details as TodoWriteDetails | undefined;
45
- if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
46
- return new Text(theme.fg("success", details?.unchanged ? "No change" : "✓ Saved"), 0, 0);
47
- },
48
- });
49
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { formatTodoListText } from "../format.js";
4
+ import { TodoUpdateParams } from "../schema.js";
5
+ import { getTodos, setTodos, withStoreLock } from "../store.js";
6
+ import type { TodoWriteDetails } from "../types.js";
7
+ import { TODO_STATE_ENTRY_TYPE, TOOL_UPDATE } from "../types.js";
8
+ import { countOpenTodos, ensureTodoIds, validateTodoUpdate } from "../validate.js";
9
+
10
+ export function registerTodoUpdateTool(pi: ExtensionAPI, options: { onCommit?: () => void }): void {
11
+ pi.registerTool({
12
+ name: TOOL_UPDATE,
13
+ label: "Todo Update",
14
+ description:
15
+ "Patch existing todos by short stable ID (t1, t2, …) without replacing the full list. " +
16
+ "id is required and must match a current todo exactly — copy it from todo_read / the previous tool result; " +
17
+ "never invent or approximate an ID. This tool never deletes items.\n\n" +
18
+ "Note: always call todo_read (or use IDs from the last write/update text) before todo_update.",
19
+ promptGuidelines: [
20
+ "Copy the exact short ID (t1, t2, …) from todo_read or the last todo_write/todo_update text. Never invent, truncate, or approximate IDs.",
21
+ ],
22
+ promptSnippet: "Patch one or more existing todos by ID without replacing the full list",
23
+ parameters: TodoUpdateParams,
24
+ async execute(_toolCallId, params) {
25
+ return withStoreLock(() => {
26
+ const current = getTodos();
27
+ const result = validateTodoUpdate(params.updates, current);
28
+ if (!result.ok) return { content: [{ type: "text", text: `Error: ${result.error}` }], details: { todos: current, error: result.error } as TodoWriteDetails };
29
+ const todos = ensureTodoIds(result.todos, current);
30
+ if (!result.unchanged) {
31
+ try { pi.appendEntry(TODO_STATE_ENTRY_TYPE, { todos }); }
32
+ catch (e) {
33
+ if (/stale after session replacement/i.test(String(e))) {
34
+ return { content: [{ type: "text", text: "Error: session was replaced — state not committed. Please retry todo_update." }], details: { todos: current, error: "stale session replacement" } as TodoWriteDetails };
35
+ }
36
+ throw e;
37
+ }
38
+ }
39
+ setTodos(todos);
40
+ options.onCommit?.();
41
+ const open = countOpenTodos(todos);
42
+ const text = result.unchanged
43
+ ? "No change"
44
+ : `Updated ${params.updates.length} todo(s)\n\n${formatTodoListText(todos, `${open} open / ${todos.length} total`)}`;
45
+ return { content: [{ type: "text", text }], details: { todos, ...(result.unchanged ? { unchanged: true } : {}) } as TodoWriteDetails };
46
+ });
47
+ },
48
+ renderCall(args, theme) { return new Text(theme.fg("toolTitle", theme.bold("todo_update ")) + theme.fg("accent", `${Array.isArray(args.updates) ? args.updates.length : 0} patch(es)`), 0, 0); },
49
+ renderResult(result, _opts, theme) {
50
+ const details = result.details as TodoWriteDetails | undefined;
51
+ if (details?.error) return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
52
+ return new Text(theme.fg("success", details?.unchanged ? "No change" : "✓ Saved"), 0, 0);
53
+ },
54
+ });
55
+ }
package/src/validate.ts CHANGED
@@ -1,8 +1,20 @@
1
1
  import { sanitizeTodoText } from "./sanitize.js";
2
- import { randomUUID } from "node:crypto";
3
2
  import type { TodoItem, TodoPriority, TodoStatus } from "./types.js";
4
3
  import { MAX_CONTENT_LENGTH, MAX_TODO_ITEMS, TODO_PRIORITIES, TODO_STATUSES } from "./types.js";
5
4
 
5
+ /** Short sequential IDs (`t1`, `t2`, …) — easy for LLMs to copy; avoids UUID typos. */
6
+ export function nextShortTodoId(used: ReadonlySet<string>): string {
7
+ let n = 1;
8
+ while (used.has(`t${n}`)) n += 1;
9
+ return `t${n}`;
10
+ }
11
+
12
+ function formatAvailableIds(current: readonly TodoItem[]): string {
13
+ const ids = current.map((t) => t.id).filter((id): id is string => typeof id === "string" && id.length > 0);
14
+ if (ids.length === 0) return "(none)";
15
+ return ids.join(", ");
16
+ }
17
+
6
18
  export type ValidateOk = { ok: true; todos: TodoItem[]; unchanged: boolean };
7
19
  export type ValidateErr = { ok: false; error: string };
8
20
  export type ValidateResult = ValidateOk | ValidateErr;
@@ -117,13 +129,16 @@ export function validateTodoWrite(
117
129
  * an ID that another incoming item explicitly preserves.
118
130
  * - Items without `id` try to match a prior item: first by full tuple
119
131
  * (content+status+priority), then by unique content. If ambiguous (multiple
120
- * same-content items) or no prior match, a new UUID is generated.
132
+ * same-content items) or no prior match, a new short ID (`tN`) is generated.
121
133
  */
122
134
  export function ensureTodoIds(todos: readonly TodoItem[], current: readonly TodoItem[]): TodoItem[] {
123
135
  // Reserve every explicit ID up front. Incoming order must not decide whether an
124
136
  // id-less item steals an ID that a later item explicitly retains.
125
137
  const reserved = new Set(todos.flatMap((todo) => todo.id ? [todo.id] : []));
126
138
  const claimed = new Set<string>();
139
+ // Avoid reusing any current ID in this pass — a later id-less item may still
140
+ // match it by tuple/content. Freed IDs become reusable on the next write.
141
+ const currentIds = new Set(current.flatMap((todo) => todo.id ? [todo.id] : []));
127
142
 
128
143
  // Pre-index content uniqueness: content that appears only once in `current`
129
144
  // is safe for content-only fallback.
@@ -154,10 +169,11 @@ export function ensureTodoIds(todos: readonly TodoItem[], current: readonly Todo
154
169
  if (byContent) id = byContent.id;
155
170
  }
156
171
  }
157
- // 3. No match or ambiguous → fresh ID
158
- if (!id) {
159
- do id = randomUUID(); while (reserved.has(id) || claimed.has(id));
160
- }
172
+ // 3. No match or ambiguous → fresh short sequential ID
173
+ if (!id) {
174
+ const used = new Set([...reserved, ...claimed, ...currentIds]);
175
+ id = nextShortTodoId(used);
176
+ }
161
177
  }
162
178
  claimed.add(id);
163
179
  return { ...todo, id };
@@ -197,7 +213,12 @@ export function validateTodoUpdate(rawUpdates: unknown, current: readonly TodoIt
197
213
  if (seen.has(rec.id)) return { ok: false, error: `updates[${i}].id is duplicated` };
198
214
  seen.add(rec.id);
199
215
  const target = next.find((todo) => todo.id === rec.id);
200
- if (!target) return { ok: false, error: `updates[${i}].id does not match an existing todo` };
216
+ if (!target) {
217
+ return {
218
+ ok: false,
219
+ error: `updates[${i}].id "${rec.id}" does not match an existing todo; current IDs: ${formatAvailableIds(current)}`,
220
+ };
221
+ }
201
222
  if (rec.content === undefined && rec.status === undefined && rec.priority === undefined) {
202
223
  return { ok: false, error: `updates[${i}] must change content, status, or priority` };
203
224
  }