@getpipher/armory-todo 0.1.0 → 0.3.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/README.md +107 -14
- package/docs/superpowers/plans/2026-07-20-spec-1-store-layer.md +1691 -0
- package/docs/superpowers/plans/2026-07-20-spec-2-health-hard-prune.md +762 -0
- package/docs/superpowers/plans/2026-07-20-spec-3-interactive-panel.md +651 -0
- package/docs/superpowers/plans/2026-07-21-title-notes-split.md +1586 -0
- package/docs/superpowers/specs/2026-07-20-lifecycle-boxes-prune-design.md +323 -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 +273 -36
- package/package.json +2 -2
- package/src/archive.ts +214 -0
- package/src/config.ts +101 -0
- package/src/hard-prune.ts +89 -0
- package/src/health.ts +97 -0
- package/src/migrate.ts +154 -0
- package/src/panel-data.ts +63 -0
- package/src/panel.ts +382 -0
- package/src/paths.ts +36 -0
- package/src/todo-store.ts +86 -37
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Workstream B — `title` + `notes` schema split (v0.3.0)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-21
|
|
4
|
+
**Status:** Design approved → pending implementation plan
|
|
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).
|