@getpipher/armory-todo 0.2.0 → 0.3.1
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/README.md +32 -9
- package/docs/superpowers/plans/2026-07-21-auto-prune-done-view.md +893 -0
- package/docs/superpowers/plans/2026-07-21-title-notes-split.md +1586 -0
- package/docs/superpowers/specs/2026-07-21-auto-prune-done-view-design.md +217 -0
- package/docs/superpowers/specs/2026-07-21-title-notes-split-design.md +411 -0
- package/docs/todo-SPEC.md +5 -0
- package/extensions/todo.ts +116 -34
- package/package.json +2 -2
- package/src/archive.ts +68 -6
- package/src/auto-prune.ts +15 -0
- package/src/health.ts +17 -1
- package/src/migrate.ts +110 -1
- package/src/panel-data.ts +26 -15
- package/src/panel.ts +67 -13
- package/src/todo-store.ts +43 -21
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Workstream v0.3.1 — auto-prune on session_start + unified `Done` view
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-21
|
|
4
|
+
**Status:** Design approved → pending implementation plan
|
|
5
|
+
**Branch:** `feat/auto-prune-done-view` off `main`
|
|
6
|
+
**Predecessor:** v0.3.0 (title + notes split), shipped 2026-07-21 (PR #4)
|
|
7
|
+
**Target ship:** v0.3.1, auto-published via `release.yml` on `v0.3.1` tag (now also creates a GitHub Release)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Problem
|
|
12
|
+
|
|
13
|
+
v0.3.0 split title/notes and fixed per-todo injection bloat, but two friction points remain in the *lifecycle* of finished work:
|
|
14
|
+
|
|
15
|
+
1. **Prune is 100% manual.** Done/cancelled todos sit in the live `todo.json` forever until someone runs `/todo prune`. On a busy stretch they pile up; the `session_start` bloat nudge only *suggests* pruning — it never does it. The user is the prune trigger.
|
|
16
|
+
2. **"Done" is split across two stores after pruning.** Recent done (<7d) lives in the live store; older done lives in `todo-archive.json`. There's no single "show me my finished work" view — you query live and archive separately. The `/todo` panel's Archive tab shows only the archive file (sealed history: done + cancelled), not recent live done.
|
|
17
|
+
|
|
18
|
+
## 2. Goals
|
|
19
|
+
|
|
20
|
+
- **Auto-prune:** done/cancelled todos older than `config.prune.defaultAgeDays` (default 7d) archive *themselves* on `session_start`, without the user asking. Deterministic (the extension does it, not the model remembering to) — but **silent when there's nothing stale**.
|
|
21
|
+
- **Transparency:** every prune (auto or manual) reports a **rich result** (ids + titles + age + `restore` hint), so the user is never blind to what moved — especially when the agent auto-pruned without an explicit trigger.
|
|
22
|
+
- **Unified `Done` view:** one command + one panel tab to see all finished work (`status: done`), spanning live (recent) + archive (old), annotated by location, filterable.
|
|
23
|
+
- **Preserve the injection contract unchanged:** only `open` + `in_progress` are auto-injected into the prompt. Parked / done / archive are not injected. v0.3.1 does not alter this boundary.
|
|
24
|
+
|
|
25
|
+
## 3. Non-goals (deferred)
|
|
26
|
+
|
|
27
|
+
- **`cancelled`-in-live visibility** — a pre-existing gap (cancelled todos <7d are in live but shown in no panel tab). Cancelled shows in the Archive tab once pruned. Separate concern; not addressed here.
|
|
28
|
+
- **`prune --all` / `prune --hard`** — unchanged (manual only; `--hard` stays the only irreversible, gated action).
|
|
29
|
+
- **Auto-prune of `parked`** — parked is deferred-not-finished, not "done"; auto-prune targets `done`/`cancelled` only (same as manual `prune`).
|
|
30
|
+
- **Time-based/scheduled prune outside session_start** — no cron; auto-prune runs only on the `session_start` hook (cold start / resume / fork / reload).
|
|
31
|
+
|
|
32
|
+
## 4. Decisions log (from brainstorm Q&A)
|
|
33
|
+
|
|
34
|
+
| # | Question | Decision |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Q1 | Where does "done" live in the `/todo` panel? | **A** — new 5th box tab `Done` (status `done` only, unified live+archive). `done` (status) ≠ `archive` (location) — different axes; `Done` = workflow view, `Archive` = storage vault. |
|
|
37
|
+
| Q2 | Auto-prune trigger mechanism | Deterministic on `session_start` (extension code), age-gated (>`defaultAgeDays`), **never `--all`**. Not prompt-driven (the model doesn't have to remember). |
|
|
38
|
+
| Q3 | Confirm gate for auto-prune? | No gate — reversible + age-gated, not destructive. The `--hard` gate stays untouched. |
|
|
39
|
+
| Q4 | Visibility when prune runs | Rich result (ids + titles + age + `restore` hint) for BOTH auto and manual prune. |
|
|
40
|
+
| Q5 | Unified `done` listing | `todo list status:done` spans live+archive in one call; `/todo done` slash shortcut. |
|
|
41
|
+
| Q6 | Injection contract | **Unchanged** — only `open`+`in_progress` injected. Auto-prune moves done→archive (both non-injected); zero injection impact. The Done tab is a panel view only. |
|
|
42
|
+
|
|
43
|
+
## 5. Auto-prune on `session_start`
|
|
44
|
+
|
|
45
|
+
### 5.1 Trigger
|
|
46
|
+
|
|
47
|
+
The extension's existing `session_start` handler (in `extensions/todo.ts`) currently: loads open todos, computes `healthReport()`, and notifies `armory-todo: N open TODOs` (+ a `⚠ N bloat signals` suffix if flags). v0.3.1 inserts an **auto-prune step** before the notify:
|
|
48
|
+
|
|
49
|
+
1. Load the live store.
|
|
50
|
+
2. Compute the set of done/cancelled todos with `closedAt` older than `config.prune.defaultAgeDays` (default 7d) — i.e. exactly what `pruneTodos({})` (age-gated, no `--all`) would move.
|
|
51
|
+
3. **If that set is non-empty → call `pruneTodos({})`** (age-gated; **never `--all`**) and capture the moved ids + todos.
|
|
52
|
+
4. Notify once, with the rich format (§6).
|
|
53
|
+
5. **If empty → skip** (just the normal `N open TODOs` notify; no prune noise).
|
|
54
|
+
|
|
55
|
+
This runs on every `session_start` (cold start / resume / fork / `/reload`). Because it's age-gated, it's a no-op on a clean store — no churn, no prune spam. Idempotent: once pruned, the todos are in the archive; the next start sees nothing stale.
|
|
56
|
+
|
|
57
|
+
### 5.2 Why deterministic (not prompt-driven)
|
|
58
|
+
|
|
59
|
+
"Agent thinks" = the agent's *awareness* drives it, but relying on the model to remember to call `prune` every session is fragile. Wiring it into the `session_start` hook makes it **just happen** reliably — the extension acts on the user's behalf at session start, deterministically. The model still *sees* the result (via the notify) and can act on it (e.g. mention it, or `restore` if the user asks). This matches the user's intent ("automatically triggered") without depending on model compliance.
|
|
60
|
+
|
|
61
|
+
### 5.3 Config
|
|
62
|
+
|
|
63
|
+
Reuses `config.prune.defaultAgeDays` (7) — already tunable via the Config tab / `todo.config.json`. **No new config key.** A future v0.4.x could add `config.prune.autoOnSessionStart: boolean` to disable auto-prune, but v0.3.1 ships it always-on (age-gated).
|
|
64
|
+
|
|
65
|
+
### 5.4 Notify format
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
armory-todo: 2 open TODOs · auto-pruned 3 stale done (>7d):
|
|
69
|
+
[td-…] done armory-todo v0.2.0 — Workstream A shipped…
|
|
70
|
+
[td-…] done Task 1: Store schema break…
|
|
71
|
+
[td-…] cancelled QA smoke v0.3.0
|
|
72
|
+
Undo any with: todo restore <id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(When nothing was pruned, just `armory-todo: N open TODOs` — unchanged from v0.3.0, optionally + the existing `⚠ bloat` suffix if health flags fire.)
|
|
76
|
+
|
|
77
|
+
The notify is a **transient `ctx.ui.notify` message**, NOT a system-prompt injection. The `## Open TODOs` prompt block is untouched.
|
|
78
|
+
|
|
79
|
+
## 6. Rich prune result (auto + manual)
|
|
80
|
+
|
|
81
|
+
Both the auto-prune (§5) and manual `prune` (tool + `/todo prune` slash) produce the same structured output. New helper in `src/archive.ts`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
export interface PruneDetail {
|
|
85
|
+
moved: number;
|
|
86
|
+
items: { id: string; status: "done" | "cancelled"; title: string; ageDays: number }[];
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`pruneTodos` gains an optional `detail: true` flag (default false for back-compat) that returns `items` + `ageDays` (computed from `closedAt`). When `detail` is true, the caller (extension) formats:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Pruned 3 todos to archive:
|
|
94
|
+
[td-…] done <title> (was 9d old)
|
|
95
|
+
[td-…] cancelled <title> (was 12d old)
|
|
96
|
+
Undo any with: todo restore <id>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **Manual `prune`** (tool + slash): always uses `detail: true` → rich output.
|
|
100
|
+
- **Auto-prune** (session_start): uses `detail: true` → rich notify.
|
|
101
|
+
- The `--all` path: same rich format (age shown as `(--all)` or the real age; minor).
|
|
102
|
+
|
|
103
|
+
The existing `PruneResult { moved, ids }` shape stays (back-compat for any caller); `PruneDetail` is additive.
|
|
104
|
+
|
|
105
|
+
## 7. Unified `Done` listing
|
|
106
|
+
|
|
107
|
+
### 7.1 Store layer (`src/archive.ts` + `src/todo-store.ts`)
|
|
108
|
+
|
|
109
|
+
New `listDoneUnified(filter?): DoneItem[]` (in `archive.ts` — it already coordinates live + archive via `loadStore`/`loadArchive`):
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export interface DoneItem extends Todo {
|
|
113
|
+
location: "live" | "archive";
|
|
114
|
+
archivedAt: string | null; // closedAt for live; closedAt for archive (the prune time ≈ closedAt)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- Pulls done todos from the live store (`loadStore().todos.filter(status === "done")`) tagged `location: "live"`, plus done todos from the archive (`loadArchive().todos.filter(status === "done")`) tagged `location: "archive"`.
|
|
119
|
+
- **Excludes `cancelled`** (Done = finished work; cancelled = abandoned → lives in the Archive tab only).
|
|
120
|
+
- Sorts newest-`closedAt` first.
|
|
121
|
+
- Filter: `text` (title|notes substring), `project`, `since`, `before`, `limit`, `page` (same shape as `listTodos`/`listArchived` filters).
|
|
122
|
+
|
|
123
|
+
### 7.2 Tool (`extensions/todo.ts`)
|
|
124
|
+
|
|
125
|
+
- **`todo list status:done`** now returns the **unified** set (live + archive) via `listDoneUnified`, formatted:
|
|
126
|
+
```
|
|
127
|
+
- [td-…] done <title> [live 3d]
|
|
128
|
+
- [td-…] done <title> [archived 2026-07-15]
|
|
129
|
+
```
|
|
130
|
+
(When `status:done` is set, the `archived` param is ignored — `done` is always unified. Other status values keep current behavior: `archived:true` queries the archive, default queries live.)
|
|
131
|
+
|
|
132
|
+
### 7.3 Slash (`/todo done`)
|
|
133
|
+
|
|
134
|
+
New slash subcommand: `/todo done` → prints the unified done list (recent first), same row format as §7.2. `/todo done project:bug-bounty` → filtered.
|
|
135
|
+
|
|
136
|
+
## 8. `/todo` panel — new `Done` box tab
|
|
137
|
+
|
|
138
|
+
### 8.1 Tabs
|
|
139
|
+
|
|
140
|
+
`BOXES` becomes `["active", "parked", "done", "archive", "config"]` (5 tabs).
|
|
141
|
+
|
|
142
|
+
### 8.2 Done tab content
|
|
143
|
+
|
|
144
|
+
- Uses `listDoneUnified({ text: filter })` (the panel's filter input searches title|notes across the unified set).
|
|
145
|
+
- `todoToItem` rows: `[id] (done) (project) • <title>` + a location tag suffix `[live N]` or `[archived <YYYY-MM-DD>]`. (Add a `todoDoneItem` helper in `panel-data.ts` that extends `todoToItem` with the location tag, or add an optional `location` param to `todoToItem`.)
|
|
146
|
+
- Sorted newest-closed first.
|
|
147
|
+
- Action submenu on Enter: **View detail** + (if location is archive) **Restore** (brings it back as open). Live done rows offer **View detail** only (no Restore — they are already in the live store; use `todo update <id> status:open` to reopen, or leave as done). No **Delete** for done rows (already finished; deletion/cancel is for open todos).
|
|
148
|
+
|
|
149
|
+
### 8.3 Archive tab — unchanged
|
|
150
|
+
|
|
151
|
+
Still the sealed-vault browse view: summary-first (counts by project + month across done + cancelled), drill-down with a filter. Unchanged from v0.3.0.
|
|
152
|
+
|
|
153
|
+
### 8.4 Footer hint
|
|
154
|
+
|
|
155
|
+
Update the panel footer to reflect 5 tabs: `↑↓ navigate • enter select/action • tab switch box • esc done`.
|
|
156
|
+
|
|
157
|
+
## 9. Injection contract (unchanged — explicit)
|
|
158
|
+
|
|
159
|
+
`renderOpenBlock()` still calls `listTodos()` with the default filter (`status === "open" || status === "in_progress"`). v0.3.1 does NOT touch `renderOpenBlock` or `listTodos`'s default. Therefore:
|
|
160
|
+
|
|
161
|
+
- Only `open` + `in_progress` are auto-injected (title + `•`, capped 15).
|
|
162
|
+
- Parked / done / archive are NOT injected — available via `list`/`get`/panel only.
|
|
163
|
+
- Auto-prune moves done (not injected) → archive (not injected): **zero injection impact**.
|
|
164
|
+
- The Done tab is a panel view; it does not inject.
|
|
165
|
+
|
|
166
|
+
The only new thing the *agent* sees at session start is the transient `ctx.ui.notify` (§5.4), not a system-prompt change.
|
|
167
|
+
|
|
168
|
+
## 10. Tests
|
|
169
|
+
|
|
170
|
+
Baseline: 220 across 8 suites. Target: ~240+.
|
|
171
|
+
|
|
172
|
+
### 10.1 New suite `test/todo-auto-prune.test.mts`
|
|
173
|
+
- `session_start` auto-prune: stale done (>7d) → moved to archive; fresh done (<7d) → stays in live; open/in_progress/parked untouched.
|
|
174
|
+
- Auto-prune never uses `--all` (fresh done <7d stays even if `--all` would move it).
|
|
175
|
+
- Auto-prune is a no-op when nothing is stale (live store unchanged, no prune notify payload).
|
|
176
|
+
- Idempotent: running twice (two session_starts) → second is a no-op.
|
|
177
|
+
- `cancelled` also auto-pruned when stale.
|
|
178
|
+
- Rich result: `PruneDetail.items` has id + status + title + ageDays.
|
|
179
|
+
|
|
180
|
+
### 10.2 Extend `test/todo-archive.test.mts`
|
|
181
|
+
- `pruneTodos({ detail: true })` returns `items` with `ageDays` computed from `closedAt`.
|
|
182
|
+
- `listDoneUnified`: live done + archived done merged, `cancelled` excluded, sorted newest-closed first, `location` tag correct, filters (text/project/since/before/limit/page) work.
|
|
183
|
+
- `listDoneUnified` empty when no done todos.
|
|
184
|
+
|
|
185
|
+
### 10.3 Extend `test/panel-data.test.mts`
|
|
186
|
+
- `todoDoneItem` (or `todoToItem` with location): label includes `[live N]` / `[archived <date>]`.
|
|
187
|
+
|
|
188
|
+
### 10.4 Extend `test/todo-store.test.mts` (if `list status:done` routing changes)
|
|
189
|
+
- `listTodos({ status: "done" })` still returns live done (back-compat) — the unification happens at the extension layer for the `todo list status:done` tool call, OR move unification into the store. **Decision:** keep `listTodos({status:"done"})` as live-only (back-compat); the unification lives in `listDoneUnified` (archive.ts), called by the extension for `todo list status:done`. So no `todo-store` change for routing — test that `listTodos({status:"done"})` is unchanged.
|
|
190
|
+
|
|
191
|
+
### 10.5 Manual-gate (no unit test)
|
|
192
|
+
- Extension `session_start` auto-prune + notify format.
|
|
193
|
+
- `/todo` panel Done tab (rendering, filter, action submenu, restore-from-archive).
|
|
194
|
+
- `/todo done` slash.
|
|
195
|
+
- `todo list status:done` unified output.
|
|
196
|
+
|
|
197
|
+
Verified by RECTOR in a real pi session before merge (same gate as v0.3.0).
|
|
198
|
+
|
|
199
|
+
## 11. Branch + ship
|
|
200
|
+
|
|
201
|
+
- Branch `feat/auto-prune-done-view` off `main`.
|
|
202
|
+
- Commits: `feat(auto-prune): ...`, `feat(prune): rich result ...`, `feat(done): unified listing ...`, `feat(panel): Done tab ...`, `test: ...`, `docs: ...` — one logical change per commit.
|
|
203
|
+
- PR to `main`, `--merge --delete-branch`. No GitLab mirror (getpipher).
|
|
204
|
+
- **QA gate:** RECTOR tests in a real pi session: stale done auto-prunes on start (fresh done stays); the notify shows the rich result; `/todo done` + the Done tab show unified done (live + archive, tagged); `todo restore <id>` undoes an auto-prune; injection unchanged (only active injected).
|
|
205
|
+
- Tag `v0.3.1` → CI auto-publish (npm) + auto-create GitHub Release (the v0.3.0-post workflow change).
|
|
206
|
+
- Post-ship: bump README/AGENTS test count + add the Done tab / auto-prune to the docs; mark this spec shipped.
|
|
207
|
+
|
|
208
|
+
## 12. Open question (resolve at implementation)
|
|
209
|
+
|
|
210
|
+
- **`pruneTodos` `detail` flag vs always-rich:** simplest is to make the rich `items` always returned (no flag) and have callers that only need `{moved, ids}` ignore `items`. Decide during implementation — leaning toward always-return for simplicity, since the cost is computing `ageDays` (trivial) per moved todo.
|
|
211
|
+
|
|
212
|
+
## 13. Out of scope (future)
|
|
213
|
+
|
|
214
|
+
- **`cancelled`-in-live panel visibility** — a pre-existing gap; could add a `Cancelled` filter or fold into the Archive tab's live portion in a future workstream.
|
|
215
|
+
- **`config.prune.autoOnSessionStart` toggle** — disable auto-prune (v0.4.x).
|
|
216
|
+
- **Auto-prune notify dedup** — if `/reload` fires multiple times in a session, the notify repeats; acceptable for v0.3.1 (it's a transient inform).
|
|
217
|
+
- **Workstream C (v0.4.0):** preventive caps-on-add (notes length cap + project registry) — still the next major milestone.
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Workstream B — `title` + `notes` schema split (v0.3.0)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-21
|
|
4
|
+
**Status:** Shipped (v0.3.0, PR #4, 2026-07-21)
|
|
5
|
+
**Branch:** `feat/title-notes-split` off `main`
|
|
6
|
+
**Predecessor:** v0.2.0 (Workstream A — lifecycle boxes + prune + health + TUI panel), PR #3, shipped 2026-07-21
|
|
7
|
+
**Supersedes (for this scope):** `2026-07-20-lifecycle-boxes-prune-design.md` §14 (which deferred this exact split to Workstream B)
|
|
8
|
+
**Target ship:** v0.3.0, auto-published via `release.yml` on `v0.3.0` tag
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Problem
|
|
13
|
+
|
|
14
|
+
v0.2.0 shipped the lifecycle boxes (active / parked / archive) — that fixed the *count* of injected todos. It did not fix the *per-todo size*. The `Todo` schema has a single `text` field that is a junk-drawer: a todo's title AND its running log crammed into one string. The live ZeroClaw todo (`td-mrt3zp9fcnug3p`) is ~1.8KB and is injected verbatim into every fresh session's system prompt via `renderOpenBlock`. RECTOR flagged "why is there no title field?" during v0.2.0 QA; the v0.2.0 panel's ~80-char truncation is a band-aid treating the symptom.
|
|
15
|
+
|
|
16
|
+
This workstream is the real fix: **split `text` into `title` (short, injected) + `notes` (long, not injected).**
|
|
17
|
+
|
|
18
|
+
## 2. Goals
|
|
19
|
+
|
|
20
|
+
- A todo's **title** is a ≤120-char one-line summary — the only thing injected into the system prompt and shown in compact lists.
|
|
21
|
+
- A todo's **notes** is an arbitrary-length editable body — the running detail, progress log, pointers. Never auto-injected.
|
|
22
|
+
- The model can **read notes on demand** via a new `get` action (so `list` can become compact without losing retrievability).
|
|
23
|
+
- Backwards-compatible with v0.2.0 stores: v2→v3 migration runs on load, curated for the 2 known todos + a deterministic fallback for any others.
|
|
24
|
+
- No re-bloat: a hard cap on `title` at the write boundary prevents the junk-drawer pattern from re-forming inside `title`.
|
|
25
|
+
|
|
26
|
+
## 3. Non-goals (deferred)
|
|
27
|
+
|
|
28
|
+
- **Append-only `log` array** (timestamped entries). Considered (option B/C in brainstorm) and rejected as YAGNI — the bloat fix only needs a title/body split, not structured history. Clean v0.4.0 addition on top of `notes` if it earns its place.
|
|
29
|
+
- **Notes caps-on-add + project registry** — Workstream C (issue #1's hard-block half). `notes` is uncapped in B; health reports notes-bytes as a diagnostic only.
|
|
30
|
+
- **In-panel multi-line notes editing** — blocked by the v0.2.0 constraint that `ctx.ui.editor()` from inside `ctx.ui.custom()` causes a nested-UI bug (`/todo` won't reopen). Tracked as a known deferred issue; `notes` is model-managed via the `todo` tool until a safe pattern exists.
|
|
31
|
+
|
|
32
|
+
## 4. Decisions log (from brainstorm Q&A)
|
|
33
|
+
|
|
34
|
+
| # | Question | Decision |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| Q1 | Shape of the notes side | **A** — `notes` single editable string. No append-only log. |
|
|
37
|
+
| Q2 | Migration of existing `text`-only todos | Hand-curated for the 2 known ids; first-line fallback for any others. No heuristic sentence-split. |
|
|
38
|
+
| Q3 | Panel handling of notes | **C** — detail view shows title+notes read-only; inline Edit edits title only; notes via `todo` tool; limitation tracked as known deferred issue. |
|
|
39
|
+
| Q4 | `add`/`update` tool surface | **A** — clean break. `add` requires `title` (+optional `notes`); `text` param removed. `update` gains `title`+`notes`, drops `text`. `list.text` searches title+notes. |
|
|
40
|
+
| Q5 | Reading notes | **A** — new `get` action returns one todo's full record. `list` shows title + `•` indicator when notes non-empty. |
|
|
41
|
+
| Q6 | Title cap | **A** — hard reject >120 chars at `add`/`update`. `notes` uncapped in B. |
|
|
42
|
+
|
|
43
|
+
## 5. Schema (`src/todo-store.ts`)
|
|
44
|
+
|
|
45
|
+
### 5.1 `Todo` interface
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export interface Todo {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string; // NEW — ≤120 chars, non-empty, trimmed
|
|
51
|
+
notes: string; // NEW — any length, may be ""
|
|
52
|
+
project: string;
|
|
53
|
+
tags: string[];
|
|
54
|
+
priority: Priority;
|
|
55
|
+
status: Status;
|
|
56
|
+
source: string;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
closedAt: string | null;
|
|
60
|
+
// `text` REMOVED
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 5.2 `Store`
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export interface Store {
|
|
68
|
+
version: 3; // was 2
|
|
69
|
+
updatedAt: string;
|
|
70
|
+
todos: Todo[];
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`emptyStore()` returns `version: 3`.
|
|
75
|
+
|
|
76
|
+
### 5.3 Inputs
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export interface AddInput {
|
|
80
|
+
title: string; // required (was: text)
|
|
81
|
+
notes?: string; // optional, default ""
|
|
82
|
+
project?: string;
|
|
83
|
+
tags?: string[];
|
|
84
|
+
priority?: Priority;
|
|
85
|
+
source?: string;
|
|
86
|
+
// `text` REMOVED
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UpdateInput {
|
|
90
|
+
title?: string; // non-empty + ≤120 if present
|
|
91
|
+
notes?: string; // "" clears
|
|
92
|
+
project?: string;
|
|
93
|
+
tags?: string[];
|
|
94
|
+
priority?: Priority;
|
|
95
|
+
status?: Status;
|
|
96
|
+
// `text` REMOVED
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`ListFilter.text` stays — its semantic is "search query", not "the text field" (see §7.3).
|
|
101
|
+
|
|
102
|
+
### 5.4 Title cap constant
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const TITLE_MAX = 120;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Enforced in `addTodo` and `updateTodo`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
function normalizeTitle(raw: string): string {
|
|
112
|
+
const t = raw.trim();
|
|
113
|
+
if (!t) throw new TodoError("title is required");
|
|
114
|
+
if (t.length > TITLE_MAX) {
|
|
115
|
+
throw new TodoError(`title must be ≤${TITLE_MAX} chars (got ${t.length}); move detail into notes`);
|
|
116
|
+
}
|
|
117
|
+
return t;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`notes` is trimmed but **not** length-checked. `addTodo` with no `notes` → `notes: ""`. `updateTodo` with `notes: ""` → clears.
|
|
122
|
+
|
|
123
|
+
## 6. Migration v2→v3 (`src/migrate.ts`)
|
|
124
|
+
|
|
125
|
+
### 6.1 When it runs
|
|
126
|
+
|
|
127
|
+
In `loadStore`, after the existing v1→v2 file-move migration (which is guarded to default `TODO_DIR`), if `parsed.version === 2`:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
if (parsed.version === 2) {
|
|
131
|
+
parsed = migrateV2ToV3(parsed); // in-memory transform
|
|
132
|
+
saveStore(parsed); // persist once → v3 on disk
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Subsequent loads see `version: 3` and skip migration.
|
|
137
|
+
|
|
138
|
+
### 6.2 No env guard (unlike v1→v2)
|
|
139
|
+
|
|
140
|
+
The v1→v2 migration moves the *real* legacy file (`~/.pi/agent/todo.json`) — that's why it's guarded to default `TODO_DIR` (the v0.2.0 incident lesson). The v2→v3 migration only touches the *live* path (`<TODO_DIR>/todo.json`) — which in tests is a temp dir. It never reaches the real legacy file, so it does **not** need the env guard. The v1→v2 guard stays as-is.
|
|
141
|
+
|
|
142
|
+
### 6.3 Curated map (the 2 known todos)
|
|
143
|
+
|
|
144
|
+
Hardcoded by id. The titles + notes were hand-curated during the 2026-07-21 brainstorm session (reformatted for clarity, not a mechanical split):
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const CURATED_V2_TO_V3: Record<string, { title: string; notes: string }> = {
|
|
148
|
+
"td-mrt3zp9fcnug3p": {
|
|
149
|
+
title: "ZeroClaw×Solana bounty — Phase 4-5: demo video (score bottleneck, unstarted)",
|
|
150
|
+
notes: `superteam.fun/earn/listing/zeroclaw · Superteam Brasil · 5,000 USDG pool / 1st=1,800 · winner Aug 21 2026 · TARGET #1.
|
|
151
|
+
|
|
152
|
+
PHASE 0-2 DONE ✅. PHASE 3 (RESEARCH+SPEC+PLAN + impl alerts+custody+docs) DONE ✅ — slices A-F+H, 45 tests, committed 8fd7483→80614c8, PUSHED, PR #76 retitled "Palinurus — depin-attest + depin-rewards", 17 commits.
|
|
153
|
+
|
|
154
|
+
claim_tx (G) DEFERRED — Helium hotspots are cNFTs → claim needs distribute_compression_rewards_v0 + DAS get_asset_proof (merkle proof), multi-session; PDAs verified, design in README.
|
|
155
|
+
|
|
156
|
+
Decision (score-max): ship alerts core complete, pivot to DEMO track.
|
|
157
|
+
|
|
158
|
+
NEXT (★ Phase 4-5, the score bottleneck — submission REQUIRES a demo video, currently unstarted):
|
|
159
|
+
(1) ASYNC: RECTOR's free Relay Community key → real Helium fixtures + live smoke test;
|
|
160
|
+
(2) Phase 4: wiring SVG (docs/wiring-diagram.svg, dark-mode, NOT ASCII) + marketing site (palinurus.rectorspace.com, Next.js+Tailwind+shadcn) + demo recording guide;
|
|
161
|
+
(3) Phase 5: record demo ≤3min (real ZeroClaw+Telegram, terminal+phone) → ElevenLabs voiceover → ffmpeg → submit on Superteam Earn + engage #solana-bounty Discord.
|
|
162
|
+
|
|
163
|
+
Test totals: 184 (71 palinurus-core + 68 depin-attest + 45 depin-rewards), all clippy+wasm clean.
|
|
164
|
+
HANDOFF: ~/Documents/secret/strategy/zeroclaw-solana/session-handoff-2026-07-21.md
|
|
165
|
+
Docs: {RESEARCH-3,SPEC-3,PLAN-3}-depin-rewards.md (SPEC-3 §4 + PLAN-3 G corrected for cNFT)
|
|
166
|
+
Cwd: ~/local-dev/RECTOR-LABS/zeroclaw-plugins/plugins/depin-rewards
|
|
167
|
+
PR: https://github.com/zeroclaw-labs/zeroclaw-plugins/pull/76`,
|
|
168
|
+
},
|
|
169
|
+
"td-mrt4e1qi9td6jz": {
|
|
170
|
+
title: "armory-todo v0.2.0 — Workstream A shipped (lifecycle boxes + prune + health + TUI)",
|
|
171
|
+
notes: `ALL 3 SPECS DONE ✅. SPEC-1 (store: parked+prune+archive+restore, 12 tasks), SPEC-2 (health+hard-prune, 6 tasks), SPEC-3 (interactive /todo TUI panel, 4 tasks). 147/147 tests across 7 suites. 24 commits on feat/spec-1-lifecycle-boxes, PR #3 retitled to full v0.2.0 scope. Auto-publish CI (release.yml, org NPM_TOKEN).
|
|
172
|
+
|
|
173
|
+
INCIDENT (SPEC-1 Task 9): migration bug destroyed real 52KB/47-todo store (35 done + ~10 open lost, no backup). FIXED (c034509): migration guarded to only run when TODO_DIR is default. RECOVERED: 2 todos.
|
|
174
|
+
|
|
175
|
+
Shipped: merge PR #3 → tag v0.2.0 → CI auto-publish → npm:@getpipher/armory-todo@0.2.0.
|
|
176
|
+
Out of scope: B (title+notes split), C (preventive caps+project registry).`,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 6.4 Fallback (any other v2 todo)
|
|
182
|
+
|
|
183
|
+
For a v2 todo whose `id` is not in `CURATED_V2_TO_V3`:
|
|
184
|
+
|
|
185
|
+
1. Split `text` on the first `\n` → `firstLine`, `rest`.
|
|
186
|
+
2. If `firstLine.length ≤ TITLE_MAX`: `title = firstLine.trim()`, `notes = rest.trim()`.
|
|
187
|
+
3. If `firstLine.length > TITLE_MAX`: truncate at the last word boundary ≤ `TITLE_MAX` (fall back to hard cut at `TITLE_MAX` if no boundary). `title` = the truncated form (no `…` suffix — the cap is a hard rule, not a display truncation). `notes` = the **full original `firstLine`** + `\n` + `rest` (nothing lost).
|
|
188
|
+
4. If `text` is a single line (no `\n`): `title = text.trim()` (capped as in step 2/3), `notes = ""` — unless step 3 truncation applied, in which case `notes` = the full original `text`.
|
|
189
|
+
5. If `text` is empty/whitespace (shouldn't happen — v2 `addTodo` rejected empty text, but defensively): `title = "(untitled)"`, `notes = ""`.
|
|
190
|
+
|
|
191
|
+
The fallback is deterministic and idempotent — running it twice on the same v2 todo yields the same v3 todo.
|
|
192
|
+
|
|
193
|
+
### 6.5 `migrateV2ToV3` signature
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
export function migrateV2ToV3(store: { version: 2; updatedAt: string; todos: V2Todo[] }): Store {
|
|
197
|
+
const todos = store.todos.map((t) => {
|
|
198
|
+
const curated = CURATED_V2_TO_V3[t.id];
|
|
199
|
+
if (curated) return { ...t, title: curated.title, notes: curated.notes, text: undefined };
|
|
200
|
+
const { title, notes } = splitTextFallback(t.text);
|
|
201
|
+
return { ...t, title, notes, text: undefined };
|
|
202
|
+
});
|
|
203
|
+
return { version: 3, updatedAt: store.updatedAt, todos } as Store;
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`V2Todo` is the v2 shape (with `text`, without `title`/`notes`). The `text: undefined` is stripped by JSON.stringify on save (so v3 files on disk have no `text` key).
|
|
208
|
+
|
|
209
|
+
## 7. Tool surface (`extensions/todo.ts`)
|
|
210
|
+
|
|
211
|
+
### 7.1 `ACTIONS`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const ACTIONS = ["list", "add", "update", "get", "complete", "delete", "clear", "park", "prune", "restore", "health"] as const;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`get` added.
|
|
218
|
+
|
|
219
|
+
### 7.2 Input schema (typebox)
|
|
220
|
+
|
|
221
|
+
- `title: Type.Optional(Type.String({ description: "Todo title (add required; update optional). ≤120 chars." }))`
|
|
222
|
+
- `notes: Type.Optional(Type.String({ description: "Todo notes/body (add/update optional; long-form, not injected). Pass \"\" on update to clear." }))`
|
|
223
|
+
- `text`: kept, but description rewritten to `Type.Optional(Type.String({ description: "Search query (list only) — substring match on title OR notes. Not used by add/update." }))`
|
|
224
|
+
- `id`, `project`, `tags`, `priority`, `status`, `archived`, `since`, `before`, `limit`, `page`, `ageDays`, `all`, `hard`, `confirm`, `box`, `olderThan` — unchanged.
|
|
225
|
+
|
|
226
|
+
### 7.3 Action behaviors
|
|
227
|
+
|
|
228
|
+
**`add`**: require `title` (error if missing/empty). `notes` optional. Calls `addTodo({ title, notes, project, tags, priority, source })`. Output: `Added ${id}: ${title}`.
|
|
229
|
+
|
|
230
|
+
**`update`**: require `id`. Patch `{ title, notes, project, tags, priority, status }` (only fields present). `title`/`notes` flow through `updateTodo`. Output: `Updated ${id}: ${title} [${status}]`.
|
|
231
|
+
|
|
232
|
+
**`get`** (NEW): require `id`. Calls a new exported `getTodo(id)` store function (added to `src/todo-store.ts` — loads the store, finds by id, throws `TodoError` if missing, returns the `Todo`). The extension formats it as a full record:
|
|
233
|
+
```
|
|
234
|
+
${id} [${priority}/${status}] ${title}
|
|
235
|
+
project: ${project || "(none)"}
|
|
236
|
+
tags: ${tags.join(", ") || "(none)"}
|
|
237
|
+
created: ${createdAt}
|
|
238
|
+
updated: ${updatedAt}
|
|
239
|
+
closed: ${closedAt || "(open)"}
|
|
240
|
+
source: ${source || "(none)"}
|
|
241
|
+
|
|
242
|
+
notes:
|
|
243
|
+
${notes || "(empty)"}
|
|
244
|
+
```
|
|
245
|
+
`TodoError` if id not found → extension returns the error message (existing catch).
|
|
246
|
+
|
|
247
|
+
**`list`**: `text` filter now matches `title` OR `notes` (case-insensitive substring). Output fmt:
|
|
248
|
+
```
|
|
249
|
+
- [${id}] (${priority}/${status})${notesDot} ${title}${tag}${pins}
|
|
250
|
+
```
|
|
251
|
+
where `notesDot = notes.trim() ? " •" : ""`, placed immediately after the priority/status parenthetical so the title stays clean and the indicator is discoverable. `tag = project ? " (${project})" : ""`, `pins` unchanged (⏵ for in_progress, etc.).
|
|
252
|
+
|
|
253
|
+
**`complete`/`delete`/`park`/`clear`/`prune`/`restore`**: output lines show `title` instead of `text`. e.g. `Completed ${id}: ${title}`, `Parked ${id}: ${title}`, `Restored ${id}: ${title} [open]`.
|
|
254
|
+
|
|
255
|
+
### 7.4 Prompt guidelines (rewritten)
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
Use todo (action:'add', title, notes?, project?, tags?, priority?, source?) when the user says 'put this in our TODO'. title is ≤120 chars (one-line summary); put long detail in notes.
|
|
259
|
+
Use todo (action:'get', id) to read a todo's full notes before acting on it (the • marker in lists means notes exist).
|
|
260
|
+
Use todo (action:'update', id, title?, notes?, …) to edit; notes="" clears.
|
|
261
|
+
Use todo (action:'list', text?) to scan titles (text searches title+notes); add archived:true to query the archive.
|
|
262
|
+
Use todo (action:'park', id) to defer (not injected); (action:'update', id, status:'open') to un-park.
|
|
263
|
+
Use todo (action:'prune') to move done/cancelled to archive (reversible via restore); prune --hard (confirm:true) is the only irreversible action.
|
|
264
|
+
Never put secrets in a TODO — the text reaches the model provider.
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 7.5 Slash command (`/todo`)
|
|
268
|
+
|
|
269
|
+
`/todo add <title>` — was `/todo add <text>`. The slash parser passes the remainder as `title`. `/todo add <title> | <notes>`? **No** — keep slash add simple (title only); notes added via the tool or panel. (The slash command is for quick human adds; notes are model-managed.) Document this.
|
|
270
|
+
|
|
271
|
+
Other slash subcommands unchanged (just show `title` in output).
|
|
272
|
+
|
|
273
|
+
## 8. Auto-injection (`renderOpenBlock`)
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const lines = shown.map((t) => {
|
|
277
|
+
const tag = t.project ? ` (${t.project})` : "";
|
|
278
|
+
const pin = t.status === "in_progress" ? " ⏵" : "";
|
|
279
|
+
const dot = t.notes.trim() ? " •" : "";
|
|
280
|
+
return `- [${t.id}] (${t.priority})${pin}${dot} ${t.title}${tag}`;
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**`title` only — never `notes`.** The `•` marker tells the model "this todo has notes; `get` it before acting." The 1.8KB ZeroClaw blob → one line. This is the bloat fix.
|
|
285
|
+
|
|
286
|
+
## 9. Panel (`src/panel.ts` + `src/panel-data.ts`)
|
|
287
|
+
|
|
288
|
+
### 9.1 List rows
|
|
289
|
+
|
|
290
|
+
`todoToItem` returns a `SelectListItem` whose `label` is `title` + `•` (when notes non-empty) + project tag + status pin. The v0.2.0 ~80-char truncation hack (`truncateForList`) is **deleted** — `title` is already ≤120 chars by the cap.
|
|
291
|
+
|
|
292
|
+
### 9.2 Detail view
|
|
293
|
+
|
|
294
|
+
Selecting a todo (Enter on a list row) opens a detail view inside the panel:
|
|
295
|
+
- Bordered, scrollable region rendering:
|
|
296
|
+
```
|
|
297
|
+
${title}
|
|
298
|
+
(${priority}/${status}) · ${project || "no project"} · ${tags.join(" ")}
|
|
299
|
+
|
|
300
|
+
notes:
|
|
301
|
+
${notes}
|
|
302
|
+
```
|
|
303
|
+
- Footer hint: `notes: read-only · todo update <id> notes=… to edit`
|
|
304
|
+
- Back action returns to the list.
|
|
305
|
+
|
|
306
|
+
### 9.3 Inline Edit
|
|
307
|
+
|
|
308
|
+
The Edit action (from the action submenu or a detail-view key) edits **`title` only** via a single-line `Input` (same constraint as v0.2.0 — `ctx.ui.editor()` from inside `ctx.ui.custom()` is the known nested-UI bug). Saving writes via `updateTodo(id, { title })`.
|
|
309
|
+
|
|
310
|
+
### 9.4 Known deferred issue (carried from v0.2.0)
|
|
311
|
+
|
|
312
|
+
> No in-panel multi-line `notes` editing. `notes` is model-managed via the `todo` tool (`action:'update', id, notes`). When a safe `ctx.ui.editor()`-from-`custom()` pattern lands in pi-tui, notes panel-editing is a clean follow-up.
|
|
313
|
+
|
|
314
|
+
Tracked in README "Known issues" + AGENTS.md.
|
|
315
|
+
|
|
316
|
+
## 10. Health (`src/health.ts`)
|
|
317
|
+
|
|
318
|
+
The bloat report gains a **notes-bytes diagnostic** (read-only — no caps enforced in B):
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
notes bytes: total=N max=M avg=A (active+parked)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Computed across active + parked todos. Lets you see notes bloat growing (the latent bloat that *isn't* injected). Existing count-based heuristics unchanged. Workstream C will add caps-on-add.
|
|
325
|
+
|
|
326
|
+
## 11. Archive (`src/archive.ts`)
|
|
327
|
+
|
|
328
|
+
Archived todos carry `title` + `notes` (same `Todo` shape). `archiveSummary` (counts by project/month) is unaffected. `listArchived` fmt shows `title` + `•` (same as `list`). `restoreTodo` returns the todo — output shows `title`. No archive-specific migration: the v2→v3 migration in §6 runs on the *live* store on load; the archive file is v3-only going forward. **Edge case:** if a v2 `todo-archive.json` exists (it doesn't today — the incident wiped the store and only 2 todos were recovered, none archived), `loadArchive` must also migrate v2→v3 on first load using the same `migrateV2ToV3` (curated map + fallback). Add this guard symmetrically.
|
|
329
|
+
|
|
330
|
+
## 12. Tests
|
|
331
|
+
|
|
332
|
+
Baseline: 151 across 7 suites. Target: ~175+.
|
|
333
|
+
|
|
334
|
+
### 12.1 New suite `test/todo-title-notes.test.mts`
|
|
335
|
+
|
|
336
|
+
- `addTodo` with `title` only (notes defaults to `""`)
|
|
337
|
+
- `addTodo` with `title` + `notes`
|
|
338
|
+
- `addTodo` rejects empty `title`
|
|
339
|
+
- `addTodo` rejects `title` >120 chars (exact boundary: 120 ok, 121 reject)
|
|
340
|
+
- `addTodo` trims `title` before length check (trailing whitespace doesn't count)
|
|
341
|
+
- `updateTodo` with `title` (non-empty, ≤120)
|
|
342
|
+
- `updateTodo` with `notes` (set, clear with `""`)
|
|
343
|
+
- `updateTodo` rejects `title` >120
|
|
344
|
+
- `getTodo` returns full record (found)
|
|
345
|
+
- `getTodo` missing id → `TodoError`
|
|
346
|
+
- `listTodos` `text` filter matches `title` OR `notes`
|
|
347
|
+
- `listTodos` output includes `•` when notes non-empty, omits when empty
|
|
348
|
+
- `renderOpenBlock` renders `title` only (never `notes`), includes `•`
|
|
349
|
+
|
|
350
|
+
### 12.2 Extend `test/todo-migrate.test.mts`
|
|
351
|
+
|
|
352
|
+
- v2→v3 curated: the 2 known ids get the hand-written title+notes (exact match)
|
|
353
|
+
- v2→v3 fallback: single-line text → title=whole, notes=""
|
|
354
|
+
- v2→v3 fallback: multi-line text → title=first line, notes=rest
|
|
355
|
+
- v2→v3 fallback: first line >120 → title=truncated at word boundary ≤120, notes=full original first line + rest
|
|
356
|
+
- v2→v3 fallback: first line >120 with no word boundary → title=hard cut at 120, notes=full original
|
|
357
|
+
- v2→v3 idempotent (running on a v3 store is a no-op / `loadStore` skips)
|
|
358
|
+
- v2→v3 persists to disk on first load (second load sees version 3)
|
|
359
|
+
- v2→v3 does **not** require the default-`TODO_DIR` guard (runs in temp-dir test fixtures) — explicit test that it works with `TODO_DIR` set
|
|
360
|
+
|
|
361
|
+
### 12.3 Extend `test/todo-store.test.mts`
|
|
362
|
+
|
|
363
|
+
- Update existing add/update/list tests that referenced `text` → use `title`/`notes`
|
|
364
|
+
- Add title-cap error cases (mirror 12.1 where they overlap — keep the boundary tests in the dedicated suite, reference cases in store tests)
|
|
365
|
+
|
|
366
|
+
### 12.4 Extend `test/panel-data.test.mts`
|
|
367
|
+
|
|
368
|
+
- `todoToItem` label uses `title` (no truncation)
|
|
369
|
+
- `todoToItem` label includes `•` when notes non-empty
|
|
370
|
+
- `todoToItem` label omits `•` when notes empty
|
|
371
|
+
|
|
372
|
+
### 12.5 Extend `test/todo-archive.test.mts`
|
|
373
|
+
|
|
374
|
+
- `listArchived` fmt shows `title` + `•`
|
|
375
|
+
- v2 archive file → v3 migration on load (curated + fallback) — symmetric with live store
|
|
376
|
+
- `restoreTodo` output shows `title`
|
|
377
|
+
|
|
378
|
+
### 12.6 Extend `test/todo-health.test.mts`
|
|
379
|
+
|
|
380
|
+
- Report includes `notes bytes: total/max/avg` line
|
|
381
|
+
- Values computed across active+parked only (archived excluded)
|
|
382
|
+
|
|
383
|
+
### 12.7 Existing suites
|
|
384
|
+
|
|
385
|
+
`todo-config.test.mts`, `todo-hard-prune.test.mts` — no title/notes-specific changes, but any test fixtures using v2 `text`-only todos must be updated to v3 shape (`title`+`notes`). Audit + fix.
|
|
386
|
+
|
|
387
|
+
## 13. Branch + ship
|
|
388
|
+
|
|
389
|
+
- Branch `feat/title-notes-split` off `main`.
|
|
390
|
+
- Commits: `feat(store): ...`, `feat(migrate): ...`, `feat(ext): ...`, `feat(panel): ...`, `feat(health): ...`, `test: ...`, `docs: ...` — one logical change per commit.
|
|
391
|
+
- PR to `main`, `--merge --delete-branch`. No GitLab mirror (getpipher).
|
|
392
|
+
- **QA gate:** RECTOR tests in a real pi session before merge — local install (`pi install npm:@getpipher/armory-todo` or local path) → restart pi → verify:
|
|
393
|
+
1. v2→v3 migration runs on first load (the 2 known todos get curated titles/notes).
|
|
394
|
+
2. `## Open TODOs` injection shows title only (+ `•`), not the 1.8KB blob.
|
|
395
|
+
3. `/todo` panel list shows titles + `•`; detail view shows notes; Edit edits title.
|
|
396
|
+
4. `todo add` with `title`+`notes` works; `todo get <id>` returns notes; `todo update <id> notes=…` edits notes.
|
|
397
|
+
5. Title >120 rejects with actionable error.
|
|
398
|
+
- Tag `v0.3.0` → CI auto-publish (`release.yml`, org `NPM_TOKEN`, no OTP).
|
|
399
|
+
- Post-ship: update README (test count, schema section, known issues), AGENTS.md (modules list + known deferred issues), this spec → status "shipped".
|
|
400
|
+
|
|
401
|
+
## 14. Incident lesson (carried forward from v0.2.0)
|
|
402
|
+
|
|
403
|
+
The v0.2.0 SPEC-1 Task 9 incident: `migrateIfNeeded` ran in test mode (`TODO_DIR` = temp dir), moved the real `~/.pi/agent/todo.json` (52KB, 47 todos) into the temp dir, test cleanup deleted it. Fix: v1→v2 file-move migration is guarded to only run when `TODO_DIR` is the default.
|
|
404
|
+
|
|
405
|
+
**For Workstream B:** the v2→v3 schema migration (§6) does **not** need this guard — it only touches the live path (`<TODO_DIR>/todo.json`), never the real legacy file. But the lesson stands: any future migration that *moves real user data between paths* must be guarded by environment, not test conventions. The v2→v3 transform is in-memory + persist-to-same-path — safe under `TODO_DIR` override.
|
|
406
|
+
|
|
407
|
+
## 15. Out of scope (future workstreams)
|
|
408
|
+
|
|
409
|
+
- **Workstream C (v0.4.0):** preventive caps-on-add (`notes` length cap, project registry, self-awareness caps) — issue #1's hard-block half.
|
|
410
|
+
- **Append-only `log` (v0.4.0+):** structured timestamped entries on top of `notes`, if it earns its place.
|
|
411
|
+
- **In-panel multi-line notes editor:** blocked on a safe `ctx.ui.editor()`-from-`custom()` pattern in pi-tui.
|
package/docs/todo-SPEC.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# SPEC — `armory-todo` (global, cross-session TODO for pi)
|
|
2
2
|
|
|
3
|
+
> **⚠ SUPERSEDED (v0.2.0):** This is the original v0.1.0 design spec. For the v0.2.0 lifecycle-boxes
|
|
4
|
+
> design (active/parked/archive, prune, health, hard-prune, interactive TUI panel), see
|
|
5
|
+
> [`docs/superpowers/specs/2026-07-20-lifecycle-boxes-prune-design.md`](superpowers/specs/2026-07-20-lifecycle-boxes-prune-design.md).
|
|
6
|
+
> This doc is kept for historical context (the v0.1.0 architecture + decisions).
|
|
7
|
+
|
|
3
8
|
**Repo:** `getpipher/armory` (`~/local-dev/armory`) · **Extension file:** `extensions/todo.ts`
|
|
4
9
|
**Status:** Draft (2026-06-23) — pending RECTOR sign-off on the flagged Decisions.
|
|
5
10
|
**Related:** closes the "cross-session TODO" pain recorded in `~/Documents/secret/claude-strategy/pi/session-handoff-2026-06-23.md` (Next steps #7 is the canonical TODO this spec exists to never lose again).
|