@getpipher/armory-todo 0.1.0 → 0.2.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.
@@ -0,0 +1,323 @@
1
+ # DESIGN — armory-todo: Lifecycle Boxes + Prune + Health (Workstream A)
2
+
3
+ **Repo:** `getpipher/armory-todo` (`~/local-dev/getpipher/armory-todo`)
4
+ **Status:** Approved 2026-07-20 (brainstormed with RECTOR via the superpowers `brainstorming` skill).
5
+ **Supersedes (for the areas it covers):** the prune/growth notes in `docs/todo-SPEC.md` §8 ("Unbounded growth").
6
+ **Related issue:** [getpipher/armory-todo#1](https://github.com/getpipher/armory-todo/issues/1) — the reactive (health/suggestions) half of issue #1 is satisfied by SPEC-2 of this workstream. The preventive half (caps-on-add hard-block + project registry) remains out of scope, deferred to a future Workstream C.
7
+
8
+ ---
9
+
10
+ ## 1. Problem
11
+
12
+ `armory-todo` v0.1.0 is a flat "open/done" list backed by a single JSON file. After ~5 weeks of real use across multiple projects, the store reached **728 lines / 52KB / 47 todos**:
13
+
14
+ - **35 done/cancelled** todos (~23KB, 67% of the file) sit forever in `todo.json`. `clearTodos` hard-deletes them but is all-or-nothing and lossy — no archive, no age-based policy, no recovery — so it's never run.
15
+ - **12 open/in_progress** todos (~12KB) are auto-injected into the system prompt every turn. Several are long-running work logs (one is 3.8KB in a single `text` string) the user isn't actively working on, but there's no "deferred/someday" state — the only options are `open` (context noise) or `cancel` (feels like throwing it away).
16
+
17
+ Two root gaps:
18
+ 1. **No lifecycle beyond open→done.** No home for "I don't know when I'll work on this" work, and no sealed history for finished work.
19
+ 2. **No self-awareness.** Nothing detects bloat, nothing suggests cleanup, the auto-injected block grows without bound.
20
+
21
+ ## 2. Goal & non-goals
22
+
23
+ **Goal:** turn `armory-todo` from a flat list into a **lifecycle-box system** where:
24
+ - the agent context sees only the **active** working set (open + in_progress),
25
+ - nothing is ever deleted by default — every state is reversible,
26
+ - there's a **parked** state for deferred/someday work (preserved, not injected, one flip from active),
27
+ - finished work moves to a sealed **archive** (recoverable, not injected, queryable on demand),
28
+ - the agent **proactively surfaces bloat** and proposes **user-confirmed** hard-prune.
29
+
30
+ **Non-goals (separate workstreams):**
31
+ - **Workstream B** — `title` + `notes`/`log` schema split. The per-todo text bloat (a 3.8KB `text` string) is a separate problem from the *count* of todos; parking reduces count, not per-todo size. B is its own spec.
32
+ - **Workstream C** — preventive caps-on-add + project registry (issue #1's hard-block half + `projects` action). This workstream delivers the *reactive* self-awareness half of issue #1; the *preventive* caps half stays for C.
33
+
34
+ ## 3. Architecture — lifecycle boxes
35
+
36
+ Three logical boxes, realized as two files + one status value:
37
+
38
+ ```
39
+ ┌─────────────────────────────────────────┐
40
+ │ todo.json (live) │
41
+ │ open ──ip──▶ in_progress │
42
+ │ ▲ │ │
43
+ │ │ │ complete │
44
+ │ park done │
45
+ │ │ │ │
46
+ │ parked ◀──────────┘ (cancel from any) │
47
+ └───┬───────────────┬──────────────────────┘
48
+ │ prune (age) │ restore
49
+ ▼ │
50
+ ┌─────────────────────────────────────────┐
51
+ │ todo-archive.json (sealed) │
52
+ │ done, cancelled │
53
+ │ │ │
54
+ │ │ prune --hard --confirm │
55
+ │ ▼ │
56
+ │ (deleted — the only irreversible path) │
57
+ └─────────────────────────────────────────┘
58
+ ```
59
+
60
+ **Auto-injection boundary (the core anti-bloat guarantee):**
61
+
62
+ | File | Contents | Auto-injected into prompt? |
63
+ |---|---|---|
64
+ | `todo.json` | `open` + `in_progress` | ✅ Yes — via `renderOpenBlock`, unchanged logic (open+in_progress filter, capped 15, sorted priority→createdAt) |
65
+ | `todo.json` | `parked` | ❌ No — excluded by the existing open+in_progress filter |
66
+ | `todo-archive.json` | `done` + `cancelled` | ❌ No — separate file, never read by the injector |
67
+
68
+ `parked` is automatically excluded from injection because it's neither `open` nor `in_progress` — no injection logic change needed. The archive is a separate file the injector never touches.
69
+
70
+ ## 4. Storage layout
71
+
72
+ ```
73
+ ~/.pi/agent/todo/
74
+ todo.json # live store: open, in_progress, parked
75
+ todo-archive.json # sealed history: done, cancelled (moved here by prune)
76
+ todo.config.json # prune ages, bloat thresholds (editable)
77
+ ```
78
+
79
+ Env var for tests: `TODO_DIR` (default `~/.pi/agent/todo/`). The legacy `TODO_STORE_PATH` (file path) is retired; tests are updated to set `TODO_DIR` to a temp dir.
80
+
81
+ **Migration (one-time, on first load after upgrade):** if `~/.pi/agent/todo.json` (old single-file) exists and `~/.pi/agent/todo/` does not → create the folder, move the old file to `todo/todo.json`, write a default `todo.config.json`, leave `todo-archive.json` empty (done/cancelled stay in `todo.json` until the first `prune` relocates them). The move is atomic with a `.bak-<ts>` backup on failure; never leave the store orphaned.
82
+
83
+ ## 5. Data model
84
+
85
+ ### `Todo` (one field changed: status enum widened)
86
+
87
+ ```ts
88
+ type Status = "open" | "in_progress" | "parked" | "done" | "cancelled";
89
+ ```
90
+
91
+ All other fields unchanged from v0.1.0: `id`, `text`, `project`, `tags`, `priority`, `source`, `createdAt`, `updatedAt`, `closedAt`. `closedAt` is set on transition to `done`/`cancelled` and cleared on any transition back to a non-terminal state (including `restore` from archive → `open`).
92
+
93
+ ### `todo.json` (version bumped to 2)
94
+
95
+ ```jsonc
96
+ { "version": 2, "updatedAt": "...", "todos": [ /* open + in_progress + parked only */ ] }
97
+ ```
98
+
99
+ After a `prune`, `done`/`cancelled` are not allowed to persist here — they live in the archive. (Pre-prune, a freshly-completed todo still sits here with `closedAt` set until the next prune relocates it — this is the "recently closed" tail.)
100
+
101
+ ### `todo-archive.json` (new)
102
+
103
+ ```jsonc
104
+ { "version": 2, "updatedAt": "...", "todos": [ /* done + cancelled only */ ] }
105
+ ```
106
+
107
+ Same `Todo` schema. Append-only on prune; entries are removed only by `restore` (→ back to `todo.json` as `open`) or `prune --hard` (→ deleted).
108
+
109
+ ### `todo.config.json` (new)
110
+
111
+ ```jsonc
112
+ {
113
+ "version": 1,
114
+ "prune": {
115
+ "defaultAgeDays": 7,
116
+ "hardAgeDays": 180,
117
+ "statuses": ["done", "cancelled"]
118
+ },
119
+ "health": {
120
+ "activeMaxOpen": 15,
121
+ "activeStaleDays": 30,
122
+ "parkedMax": 10,
123
+ "parkedStaleDays": 60,
124
+ "archiveMax": 200,
125
+ "archiveOldDays": 180
126
+ }
127
+ }
128
+ ```
129
+
130
+ All values editable (via the SPEC-3 Config panel or by hand). Missing file → defaults written on first load. Corrupt file → back up to `todo.config.json.bad-<ts>` and rewrite defaults (same pattern as store corruption handling).
131
+
132
+ ## 6. Lifecycle transitions
133
+
134
+ | From | To | Action | Effect |
135
+ |---|---|---|---|
136
+ | `open`/`in_progress` | `parked` | `park <id>` (or `update --status parked`) | status flip, same file, `closedAt` cleared |
137
+ | `parked` | `open` | `update --status open` | status flip, same file |
138
+ | `open`/`in_progress`/`parked` | `done` | `complete <id>` | sets `closedAt`, stays in `todo.json` until pruned |
139
+ | any non-terminal | `cancelled` | `delete <id>` | sets `closedAt`, stays in `todo.json` until pruned |
140
+ | `done`/`cancelled` (in `todo.json`) | archive | `prune` (age-based or `--all`) | **moves** the todo from `todo.json` → `todo-archive.json` |
141
+ | archived | `open` (in `todo.json`) | `restore <id>` | **moves** back to `todo.json`, `status: open`, `closedAt: null` |
142
+ | archived | deleted | `prune --hard --confirm` | **the only irreversible action** — removes from archive permanently |
143
+
144
+ Everything is reversible except `prune --hard`. Prune (archive move) is reversible via `restore`. Park is reversible via a status flip. Complete/delete are reversible via `restore` after prune, or via status flip before prune.
145
+
146
+ ## 7. Tool API (`todo`) — new + changed actions
147
+
148
+ | action | params | behavior |
149
+ |---|---|---|
150
+ | `list` (extended) | `status?`, `project?`, `tag?`, **`archived?: boolean`**, **`since?`**, **`before?`**, **`text?: string`** (substring search), **`limit?: number`** (default 20), **`page?: number`** | `archived:false` (default) queries `todo.json`; `archived:true` queries `todo-archive.json` with filters + pagination. Bare `archived:true` with no other filter → returns a **summary** (counts by project + by month), not raw entries (Q2 — cheap overview). Any filter → filtered, paginated slice (Q1 + Q3). |
151
+ | `add` (unchanged) | `text`, `project?`, `tags?`, `priority?`, `source?` | |
152
+ | `update` (extended) | + `status: "parked"` accepted | |
153
+ | `complete` / `delete` (unchanged) | `id` | |
154
+ | **`park`** (new) | `id` | shorthand for `update --status parked` |
155
+ | **`prune`** (new) | `age?: number` (default from `config.prune.defaultAgeDays`), `all?: boolean`, `statuses?: Status[]` | moves qualifying `done`/`cancelled` from `todo.json` → `todo-archive.json`. Qualifying = `closedAt` older than `age` days, OR `all:true`. Returns count moved + ids. **Reversible** via `restore`. |
156
+ | **`restore`** (new) | `id` | moves an archived todo back to `todo.json` as `open` (`closedAt: null`). Errors if the id is not in the archive. |
157
+ | **`prune --hard`** (new) | `box?: "archive"\|"active"\|"parked"`, `olderThan?: number` (days), `project?`, `tag?`, **`confirm: boolean`** (required) | **The only deletion path.** Refuses to execute unless `confirm: true` is passed in the tool call. Returns count + ids deleted. |
158
+ | **`health`** (new) | (none) | returns the bloat report (§8). Pure read, no side effects. |
159
+ | `clear` (existing) | `status?` | **deprecated** — its two use cases are now better served separately: reversible bulk-close → `prune --all` (archives); irreversible bulk-delete → `prune --hard --confirm` (deletes). Kept for back-compat one release, then removed. |
160
+
161
+ ## 8. `health` bloat report
162
+
163
+ ```
164
+ todo health →
165
+ {
166
+ active: { open: 12, in_progress: 3, stale_30d: 6 },
167
+ parked: { count: 9, stale_60d: 4 },
168
+ archive: { count: 247, older_180d: 41 },
169
+ flags: ["ACTIVE_STALE", "PARKED_STALE", "ARCHIVE_LARGE"],
170
+ suggestions: [
171
+ "archive: 41 items older than 180d → consider `prune --hard --box archive --older-than 180 --confirm`",
172
+ "active: 6 open TODOs untouched for 30d → park or close them",
173
+ "parked: 4 parked > 60d → restore or hard-prune"
174
+ ]
175
+ }
176
+ ```
177
+
178
+ **Bloat heuristics (defaults from `todo.config.json` → `health` block):**
179
+
180
+ | Box | Signal | Threshold | Flag |
181
+ |---|---|---|---|
182
+ | active | too many open | `open + in_progress > activeMaxOpen` (15) | `ACTIVE_LARGE` |
183
+ | active | stale | `open` with `updatedAt > activeStaleDays` (30d) | `ACTIVE_STALE` |
184
+ | parked | too many parked | `parked > parkedMax` (10) | `PARKED_LARGE` |
185
+ | parked | stale parked | `parked` with `updatedAt > parkedStaleDays` (60d) | `PARKED_STALE` |
186
+ | archive | large | `count > archiveMax` (200) | `ARCHIVE_LARGE` |
187
+ | archive | very old | `closedAt > archiveOldDays` (180d) | `ARCHIVE_OLD` |
188
+
189
+ The agent is instructed (prompt guidelines on the `todo` tool) to run `health` when the user asks about hygiene/bloat, and to **surface suggestions + wait for explicit user confirmation** before any `prune --hard`.
190
+
191
+ ## 9. Hard-prune gate (the only irreversible action)
192
+
193
+ Two layers of protection:
194
+
195
+ 1. **Tool-level (structural):** `prune --hard` refuses to execute unless `confirm: true` is passed in the tool call. Even if the agent hallucinates intent, the tool demands the flag. Without it, returns: `"Refused: pass confirm:true to execute hard-prune (this permanently deletes N todos)."`
196
+ 2. **Prompt-level (behavioral):** the `todo` tool description + prompt guidelines instruct the agent to *always* surface the `health` report + the exact proposed `prune --hard` command, and wait for an explicit user "yes" before passing `confirm: true`.
197
+
198
+ The slash-command path uses `ctx.ui.confirm` (interactive yes/no prompt) as its gate — the human confirms in-TUI before the command executes.
199
+
200
+ ## 10. Slash command (`/todo`) — typed subcommands + interactive panel
201
+
202
+ ### Typed subcommands (power-user, all SPEC-1/2; the interactive panel is SPEC-3)
203
+
204
+ - `/todo` — open + in_progress (existing default).
205
+ - `/todo all` — existing, now also shows `parked`.
206
+ - **`/todo park <id>`** — park a todo.
207
+ - **`/todo restore <id>`** — restore from archive.
208
+ - **`/todo prune`** — age-based prune (default 7d). Confirms count before moving.
209
+ - **`/todo prune --all`** — prune all done/cancelled regardless of age.
210
+ - **`/todo prune --hard <opts>`** — hard-prune; slash path always prompts `ctx.ui.confirm` first.
211
+ - **`/todo archive`** — archive summary (counts by project + month).
212
+ - **`/todo archive <filter>`** — filtered archive slice (e.g. `/todo archive project:nuntius`).
213
+ - **`/todo health`** — print the bloat report.
214
+ - `/todo add` / `/todo done` / `/todo rm` / `/todo path` — existing, unchanged.
215
+
216
+ ### Interactive panel (SPEC-3) — adopts the `/cursor` + `/vision` pattern
217
+
218
+ Built on **pi-tui** (`Container` + `DynamicBorder` + `Spacer` + `Text` + `SelectList` + `Input` + `SettingsList`) inside `ctx.ui.custom()`, matching the conventions of `@getpipher/cursor` and `@getpipher/vision`:
219
+
220
+ - **TUI-only panel**; non-TUI (`ctx.mode !== "tui"`) falls back to `ctx.ui.notify` text status.
221
+ - **`Container` + `DynamicBorder` (accent) + `Spacer` + `Text`** framing (accent border top + bottom — matches `/settings`).
222
+ - **`SelectList` + `Input` filter** for the todo list (the `VisionModelPicker` search pattern), **`SettingsList`** for the Config view.
223
+ - **Live apply + persist** on each change (writes `todo.json` / `todo-archive.json` / `todo.config.json` immediately, like `/vision` writes `vision.json`).
224
+ - **Escape exits**; arrow keys navigate; Enter selects/edits/opens sub-picker.
225
+ - **Power-user typed subcommands retained** alongside the panel.
226
+
227
+ Panel layout:
228
+
229
+ ```
230
+ ┌ ──────────────────────────────────────────────────── ┐ ← DynamicBorder (accent)
231
+
232
+ TODO — Active (12) [tab: Active | Parked | Archive | Config]
233
+ ⚠ ARCHIVE_LARGE (247) · ACTIVE_STALE (6) — run /todo health
234
+
235
+ 🔍 filter:_ ← Input (type to search text/project/tag)
236
+ ❯ [td-mrin…] (critical) ⏵ Nuntius — pivoted to web-app… ← SelectList
237
+ [td-mrr0…] (critical) ZeroClaw×Solana bounty…
238
+ [td-mrau…] (critical) Anamnesis demo: SHIPPED…
239
+ ...
240
+ ↑↓ navigate • enter select • tab box • p park • c done • x rm • h health • esc done
241
+ ┌ ──────────────────────────────────────────────────── ┐ ← DynamicBorder (accent)
242
+ ```
243
+
244
+ **Box tabs (Tab / Shift+Tab cycles):**
245
+ - **Active** — `SelectList` of open + in_progress, filter `Input`, row = `[id] (prio) status⏵ text (project)`.
246
+ - **Parked** — same layout, parked todos.
247
+ - **Archive** — summary-first (counts by project + month) as `SelectList` items; Enter drills into a bucket → filtered slice; Enter on a todo opens restore.
248
+ - **Config** — `SettingsList` with `prune.defaultAgeDays`, `prune.hardAgeDays`, `prune.statuses`, and each `health.*` threshold (editable, live-persist to `todo.config.json`).
249
+
250
+ **On Enter (select a todo):** action submenu (small `SelectList`): Complete / Park / Edit text / Delete / Restore (if archived) / Cancel.
251
+
252
+ **Keybindings:** `p` park, `c` complete, `x` delete, `r` restore, `h` health, `tab`/`shift+tab` switch box, `esc` done.
253
+
254
+ ## 11. Auto-injection behavior (unchanged logic)
255
+
256
+ `renderOpenBlock` stays as-is: injects `open` + `in_progress` only, capped at 15, sorted priority → createdAt, overflow → "… +N more". `parked` is automatically excluded (neither open nor in_progress). The archive file is never read by the injector. No injection code change required — the `parked` status is excluded by the existing filter.
257
+
258
+ **`session_start` notify upgrade (SPEC-2):** the existing open-count notify is extended — if `health` flags any bloat, the notify appends `⚠ N bloat signals — run /todo health`. Cheap nudge, zero prompt cost (it's a UI notify, not a system-prompt injection).
259
+
260
+ ## 12. Edge cases & failure modes
261
+
262
+ - **Migration failure** — if the folder move fails mid-way, restore the old `~/.pi/agent/todo.json` from the `.bak-<ts>` backup; never leave the store orphaned.
263
+ - **Archive file missing** — `prune` creates it on first run; `restore` / `list --archived` on a missing archive returns empty, no error.
264
+ - **Config missing/corrupt** — rewrite defaults, back up the bad file (same pattern as store corruption).
265
+ - **Concurrent sessions** — same last-write-wins as v0.1.0 (atomic tmp+rename per file). Archive writes are append-only under the same atomic write. No `flock` in this workstream.
266
+ - **`restore` of a non-archived id** — error: `"not in archive"` (the store looks in the archive file only).
267
+ - **`prune --hard` without `confirm`** — returns: `"Refused: pass confirm:true to execute hard-prune (this permanently deletes)."` (no-op).
268
+ - **Segmented archive (future S2)** — the `list --archived` query API (filters + pagination + summary) is the abstraction. Swapping single-file `todo-archive.json` → monthly segments (`archive/2026-07.json`) later doesn't change the tool surface. Out of scope for this workstream.
269
+ - **`parked` todos in a fresh v1→v2 migration** — there are none (v1 has no `parked` status); no migration concern.
270
+
271
+ ## 13. Testing
272
+
273
+ - **Store unit tests (extend the existing `test/todo-store.test.mts`):**
274
+ - `parked` status round-trips (open → parked → open).
275
+ - `prune` moves done/cancelled to `todo-archive.json` by age; `--all` moves all regardless of age; non-terminal statuses are never pruned.
276
+ - `restore` moves an archived todo back to `todo.json` as `open` with `closedAt: null`; errors on a non-archived id.
277
+ - `prune --hard` refuses without `confirm:true` (no-op + refusal message); deletes the matching set with `confirm:true`.
278
+ - `health` returns correct flags + counts for constructed scenarios (active stale, parked stale, archive large/old).
279
+ - `list --archived` with filters returns only matches; bare `--archived` returns a summary; pagination respects `limit`/`page`.
280
+ - `todo.config.json` defaults written when missing; corrupt config → backup + defaults.
281
+ - Migration from v1 single-file (`~/.pi/agent/todo.json`) to v2 folder layout (`~/.pi/agent/todo/todo.json` + default config + empty archive); migration failure → backup restored.
282
+ - **Hard-prune gate test:** `prune --hard` without `confirm` is a no-op that returns the refusal message; with `confirm:true` deletes the specified set.
283
+ - **Manual gate (real pi session):** park a todo → confirm it drops from the injected `## Open TODOs` block; `prune` → confirm done todos appear in `/todo archive`; `restore` one → confirm it's back in `todo.json` as `open`; `health` → confirm bloat flags render; `prune --hard` without confirm → confirm refusal; with confirm → confirm deletion.
284
+
285
+ ## 14. Out of scope (separate workstreams)
286
+
287
+ - **Workstream B — `title` + `notes`/`log` schema split.** The per-todo text bloat (a 3.8KB `text` string injected verbatim). Parking reduces the *count* of injected todos, not the per-todo *size*. B is its own spec.
288
+ - **Workstream C — preventive caps-on-add + project registry.** Issue #1's *preventive* half (hard-block on `add` that exceeds a cap, `projects` action, optional project registry). This workstream delivers the *reactive* self-awareness half of issue #1 (health + suggestions); the preventive caps half stays for C.
289
+
290
+ ## 15. SPEC-N decomposition (implementation roadmap)
291
+
292
+ This design is implemented as three SPECs, each independently shippable + reviewable, each with its own plan:
293
+
294
+ | SPEC | Scope | Builds on | Shippable outcome |
295
+ |---|---|---|---|
296
+ | **SPEC-1 — Store layer: lifecycle boxes + prune + archive + restore** | Folder layout + v1→v2 migration; `parked` status; `todo-archive.json`; age-based `prune` (+ `--all`); `restore`; `todo.config.json` + defaults; tool actions (`park`, `prune`, `restore`, extended `list` with `--archived`/filters/pagination/summary); typed slash subcommands. Auto-injection unchanged (parked auto-excluded by the existing open+in_progress filter). | — | The bloat fix works day one: `/todo prune` shrinks the 52KB file; `park` drops deferred todos from the prompt. All reversible. |
297
+ | **SPEC-2 — Self-awareness: health + hard-prune** | `health` action (bloat report from config heuristics); `prune --hard --confirm` (the only irreversible path, tool-level `confirm` gate + prompt-level "always ask first"); `session_start` bloat nudge; prompt guidelines (agent surfaces health, waits for user yes). | SPEC-1 (health reads the archive prune populates) | Agent proactively surfaces bloat + suggests user-confirmed cleanup. |
298
+ | **SPEC-3 — Interactive `/todo` TUI panel** | pi-tui panel (Container + DynamicBorder + SelectList + Input filter); box tabs (Active / Parked / Archive / Config); action submenu on Enter; Config view via `SettingsList` (prune ages + thresholds, live-persist); keybindings; non-TUI fallback; power-user typed subcommands retained. | SPEC-1 + SPEC-2 (panel operates on all primitives) | `/todo` becomes a real triage surface, not a series of typed commands. |
299
+
300
+ **Order rationale:** SPEC-1 is the load-bearing store work (it's what actually shrinks the file + bounds the prompt) and is fully testable in isolation. SPEC-2 adds the intelligence layer on a proven store. SPEC-3 is the UX layer on top of both — building it last means it wraps a stable primitive surface, so a UI bug is never conflated with a store bug. This mirrors how `@getpipher/cursor` and `@getpipher/vision` each evolved their interactive panels over multiple specs rather than shipping the full panel in v1.
301
+
302
+ The next step after this design is approved is `writing-plans` for **SPEC-1**.
303
+
304
+ ---
305
+
306
+ ## Appendix A — Decisions log (from the brainstorming session)
307
+
308
+ | # | Decision | Chosen | Rationale |
309
+ |---|---|---|---|
310
+ | D1 | Workstream scope | A (pruning/GC + archive) first; B (schema split) + C (caps) separate | Pruning is the actual prerequisite for a lean store; caps are shaky without it. |
311
+ | D2 | Pruned todos | A1 — move to archive file (`todo-archive.json`) | Done todos are a real work record; archive gives recovery + audit; hard-delete loses history. |
312
+ | D3 | Prune policy | P3 — age-based default (7d) + `--all` / `--age N` override flags | Keeps a short "recently closed" tail; escape hatch for full sweeps. |
313
+ | D4 | Default prune age | 7 days | Aggressive leanness (israf stance); archive handles all older lookback. |
314
+ | D5 | Prune statuses | both `done` and `cancelled` | Both are dead weight; `cancelled` is not used as a soft "parked" (we now have a real `parked` status). |
315
+ | D6 | Box model | Approach 2 — single live file + `parked` status + physical archive file | Minimal schema change; parked → active is one status flip; archive is genuinely out of the working file. |
316
+ | D7 | Archive injection | never | Archive is read-on-demand only; the agent context sees only open + in_progress. |
317
+ | D8 | Archive read scalability | S1 (single JSON) now + Q1/Q2/Q3 query API (filter-first, summary-first, paginated); S2/S3 (segments/SQLite) deferred | Realistic volume is low-thousands; JSON parse is sub-100ms. The query API is storage-agnostic for future swaps. |
318
+ | D9 | Self-awareness | reactive (health + suggestions + user-confirmed hard-prune), not preventive caps | Non-blocking + helpful; preventive caps-on-add deferred to Workstream C. |
319
+ | D10 | Hard-prune gate | tool-level `confirm: true` flag (structural) + prompt-level "always ask first" (behavioral) + `ctx.ui.confirm` on the slash path | The only irreversible action gets the strongest gate; the tool refuses even if the agent hallucinates. |
320
+ | D11 | Config location | C2 — `todo.config.json` in a dedicated `~/.pi/agent/todo/` folder | Co-located with the data; the folder also gives the archive a natural home + room for future segmentation. |
321
+ | D12 | Heuristic defaults | active 15 / 30d stale · parked 10 / 60d stale · archive 200 / 180d old | Sensible starting points; all editable in Config. |
322
+ | D13 | `/todo` command UX | S2 — interactive TUI panel (adopting the `/cursor` + `/vision` pi-tui pattern) + retained typed subcommands | A real triage surface, not a series of typed commands. |
323
+ | D14 | Spec decomposition | 3 SPECs (store / self-awareness / panel), each its own plan | Each independently shippable + testable; panel built on a stable primitive surface. |