@getpipher/armory-todo 0.2.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 +22 -8
- package/docs/superpowers/plans/2026-07-21-title-notes-split.md +1586 -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 +58 -28
- package/package.json +2 -2
- package/src/archive.ts +13 -3
- package/src/health.ts +17 -1
- package/src/migrate.ts +110 -1
- package/src/panel-data.ts +7 -14
- package/src/panel.ts +48 -4
- package/src/todo-store.ts +43 -21
|
@@ -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).
|
package/extensions/todo.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
completeTodo,
|
|
28
28
|
deleteTodo,
|
|
29
29
|
clearTodos,
|
|
30
|
+
getTodo,
|
|
30
31
|
listTodos,
|
|
31
32
|
renderOpenBlock,
|
|
32
33
|
updateTodo,
|
|
@@ -38,12 +39,28 @@ import { healthReport } from "../src/health";
|
|
|
38
39
|
import { hardPrune } from "../src/hard-prune";
|
|
39
40
|
import { TodoPanel } from "../src/panel";
|
|
40
41
|
|
|
41
|
-
const ACTIONS = ["list", "add", "update", "complete", "delete", "clear", "park", "prune", "restore", "health"] as const;
|
|
42
|
+
const ACTIONS = ["list", "add", "update", "get", "complete", "delete", "clear", "park", "prune", "restore", "health"] as const;
|
|
42
43
|
|
|
43
44
|
function fmt(t: ReturnType<typeof listTodos>[number]): string {
|
|
44
45
|
const tag = t.project ? ` (${t.project})` : "";
|
|
45
46
|
const pins = t.tags.length ? ` #${t.tags.join(" #")}` : "";
|
|
46
|
-
|
|
47
|
+
const dot = t.notes.trim() ? " •" : "";
|
|
48
|
+
return `- [${t.id}] (${t.priority}/${t.status})${dot} ${t.title}${tag}${pins}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmtFull(t: ReturnType<typeof getTodo>): string {
|
|
52
|
+
const tag = t.project ? ` (${t.project})` : "";
|
|
53
|
+
const tags = t.tags.length ? ` #${t.tags.join(" #")}` : "";
|
|
54
|
+
return [
|
|
55
|
+
`${t.id} [${t.priority}/${t.status}] ${t.title}${tag}${tags}`,
|
|
56
|
+
`created: ${t.createdAt}`,
|
|
57
|
+
`updated: ${t.updatedAt}`,
|
|
58
|
+
`closed: ${t.closedAt ?? "(open)"}`,
|
|
59
|
+
`source: ${t.source || "(none)"}`,
|
|
60
|
+
"",
|
|
61
|
+
"notes:",
|
|
62
|
+
t.notes || "(empty)",
|
|
63
|
+
].join("\n");
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
export default function (pi: ExtensionAPI) {
|
|
@@ -92,20 +109,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
92
109
|
"Never put secrets in a TODO — the text reaches the model provider.",
|
|
93
110
|
promptSnippet: "Read/update the global cross-session TODO list (active / parked / archive) + bloat health",
|
|
94
111
|
promptGuidelines: [
|
|
95
|
-
"Use todo (action:'
|
|
96
|
-
"Use todo (action:'
|
|
112
|
+
"Use todo (action:'add', title, notes?, project?, tags?, priority?, source?) when the user says 'put this in our TODO'. title max 120 chars (one-line summary); put long detail in notes.",
|
|
113
|
+
"Use todo (action:'get', id) to read a todo's full notes before acting on it (the bullet marker in lists means notes exist).",
|
|
114
|
+
"Use todo (action:'update', id, title?, notes?, project?, tags?, priority?, status?) to edit; notes empty string clears.",
|
|
115
|
+
"Use todo (action:'list') when the user asks 'show me the TODO' / 'what's pending' (text filter searches title+notes).",
|
|
97
116
|
"Use todo (action:'complete', id) to mark a TODO done; (action:'delete', id) to cancel it.",
|
|
98
117
|
"Use todo (action:'park', id) to defer a TODO (not injected, recoverable); (action:'update', id, status:'open') to un-park.",
|
|
99
118
|
"Use todo (action:'prune') to move done/cancelled todos to the archive (reversible); (action:'prune', all:true) to prune all regardless of age.",
|
|
100
119
|
"Use todo (action:'restore', id) to bring an archived TODO back as open.",
|
|
101
|
-
"Use todo (action:'list', archived:true) to query the archive
|
|
102
|
-
"Use todo (action:'health') to check bloat across all boxes
|
|
103
|
-
"Use todo (action:'prune', hard:true, confirm:true, box?, olderThan?) for PERMANENT deletion
|
|
120
|
+
"Use todo (action:'list', archived:true) to query the archive; bare call returns a summary, add a filter (project/text/since) for specific items.",
|
|
121
|
+
"Use todo (action:'health') to check bloat across all boxes (counts + flags + suggestions). Run when the user asks about hygiene/bloat or before any hard-prune.",
|
|
122
|
+
"Use todo (action:'prune', hard:true, confirm:true, box?, olderThan?) for PERMANENT deletion (the only irreversible action). ALWAYS run health first, show the user the report + the exact proposed command, and wait for an explicit yes before passing confirm:true. Never hard-prune without explicit user confirmation.",
|
|
104
123
|
],
|
|
105
124
|
parameters: Type.Object({
|
|
106
125
|
action: StringEnum(ACTIONS),
|
|
107
|
-
id: Type.Optional(Type.String({ description: "Todo id (for update/complete/delete/park/restore)" })),
|
|
108
|
-
|
|
126
|
+
id: Type.Optional(Type.String({ description: "Todo id (for update/complete/delete/park/restore/get)" })),
|
|
127
|
+
title: Type.Optional(Type.String({ description: "Todo title (add required; update optional). Max 120 chars; put detail in notes." })),
|
|
128
|
+
notes: Type.Optional(Type.String({ description: "Todo notes/body (add/update optional; long-form, not injected). Pass empty string on update to clear." })),
|
|
129
|
+
text: Type.Optional(Type.String({ description: "Search query (list only). Substring match on title OR notes. Not used by add/update." })),
|
|
109
130
|
project: Type.Optional(Type.String({ description: "Project tag, e.g. 'pi', 'sip', or '' for global" })),
|
|
110
131
|
tags: Type.Optional(Type.Array(Type.String())),
|
|
111
132
|
priority: Type.Optional(StringEnum(["low", "med", "high", "critical"] as const)),
|
|
@@ -173,43 +194,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
194
|
return { content: [{ type: "text" as const, text: todos.map(fmt).join("\n") }] };
|
|
174
195
|
}
|
|
175
196
|
case "add": {
|
|
176
|
-
if (!params.
|
|
177
|
-
return { content: [{ type: "text" as const, text: "Error: `
|
|
197
|
+
if (!params.title) {
|
|
198
|
+
return { content: [{ type: "text" as const, text: "Error: `title` is required for add." }] };
|
|
178
199
|
}
|
|
179
200
|
const t = addTodo({
|
|
180
|
-
|
|
201
|
+
title: params.title,
|
|
202
|
+
notes: params.notes,
|
|
181
203
|
project: params.project,
|
|
182
204
|
tags: params.tags,
|
|
183
205
|
priority: params.priority as any,
|
|
184
206
|
source: params.source as any,
|
|
185
207
|
});
|
|
186
|
-
return { content: [{ type: "text" as const, text: `Added ${t.id}: ${t.
|
|
208
|
+
return { content: [{ type: "text" as const, text: `Added ${t.id}: ${t.title}` }] };
|
|
187
209
|
}
|
|
188
210
|
case "update": {
|
|
189
211
|
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for update." }] };
|
|
190
212
|
const t = updateTodo(params.id, {
|
|
191
|
-
|
|
213
|
+
title: params.title,
|
|
214
|
+
notes: params.notes,
|
|
192
215
|
project: params.project,
|
|
193
216
|
tags: params.tags,
|
|
194
217
|
priority: params.priority as any,
|
|
195
218
|
status: params.status as any,
|
|
196
219
|
});
|
|
197
|
-
return { content: [{ type: "text" as const, text: `Updated ${t.id}: ${t.
|
|
220
|
+
return { content: [{ type: "text" as const, text: `Updated ${t.id}: ${t.title} [${t.status}]` }] };
|
|
221
|
+
}
|
|
222
|
+
case "get": {
|
|
223
|
+
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for get." }] };
|
|
224
|
+
const t = getTodo(params.id);
|
|
225
|
+
return { content: [{ type: "text" as const, text: fmtFull(t) }] };
|
|
198
226
|
}
|
|
199
227
|
case "complete": {
|
|
200
228
|
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for complete." }] };
|
|
201
229
|
const t = completeTodo(params.id);
|
|
202
|
-
return { content: [{ type: "text" as const, text: `Completed ${t.id}: ${t.
|
|
230
|
+
return { content: [{ type: "text" as const, text: `Completed ${t.id}: ${t.title}` }] };
|
|
203
231
|
}
|
|
204
232
|
case "delete": {
|
|
205
233
|
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for delete." }] };
|
|
206
234
|
const t = deleteTodo(params.id);
|
|
207
|
-
return { content: [{ type: "text" as const, text: `Cancelled ${t.id}: ${t.
|
|
235
|
+
return { content: [{ type: "text" as const, text: `Cancelled ${t.id}: ${t.title}` }] };
|
|
208
236
|
}
|
|
209
237
|
case "park": {
|
|
210
238
|
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for park." }] };
|
|
211
239
|
const t = parkTodo(params.id);
|
|
212
|
-
return { content: [{ type: "text" as const, text: `Parked ${t.id}: ${t.
|
|
240
|
+
return { content: [{ type: "text" as const, text: `Parked ${t.id}: ${t.title}` }] };
|
|
213
241
|
}
|
|
214
242
|
case "prune": {
|
|
215
243
|
if (params.hard) {
|
|
@@ -232,6 +260,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
232
260
|
`active: ${report.active.open} open + ${report.active.in_progress} in_progress (${report.active.stale_30d} stale)`,
|
|
233
261
|
`parked: ${report.parked.count} (${report.parked.stale_60d} stale)`,
|
|
234
262
|
`archive: ${report.archive.count} (${report.archive.older_180d} old)`,
|
|
263
|
+
`notes: ${report.notesBytes.total}B total · max ${report.notesBytes.max}B · avg ${report.notesBytes.avg}B`,
|
|
235
264
|
report.flags.length ? `flags: ${report.flags.join(", ")}` : "flags: (none — healthy)",
|
|
236
265
|
...report.suggestions.map((s) => ` → ${s}`),
|
|
237
266
|
];
|
|
@@ -240,7 +269,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
269
|
case "restore": {
|
|
241
270
|
if (!params.id) return { content: [{ type: "text" as const, text: "Error: `id` is required for restore." }] };
|
|
242
271
|
const t = restoreTodo(params.id);
|
|
243
|
-
return { content: [{ type: "text" as const, text: `Restored ${t.id}: ${t.
|
|
272
|
+
return { content: [{ type: "text" as const, text: `Restored ${t.id}: ${t.title} [open]` }] };
|
|
244
273
|
}
|
|
245
274
|
case "clear": {
|
|
246
275
|
const n = clearTodos((params.status as any) ?? "done");
|
|
@@ -259,9 +288,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
288
|
pi.registerCommand("todo", {
|
|
260
289
|
description:
|
|
261
290
|
"Global cross-session TODO list. " +
|
|
262
|
-
"/todo
|
|
263
|
-
"/todo park <id>
|
|
264
|
-
"/todo archive [project:X|text:Y]
|
|
291
|
+
"/todo / /todo all / /todo add <title> / /todo done <id> / /todo rm <id> / " +
|
|
292
|
+
"/todo park <id> / /todo restore <id> / /todo prune [--all|--hard --box <b> --older-than <d>] / " +
|
|
293
|
+
"/todo archive [project:X|text:Y] / /todo health / /todo clean / /todo path",
|
|
265
294
|
handler: async (args, ctx) => {
|
|
266
295
|
const a = (args ?? "").trim();
|
|
267
296
|
const [sub, ...rest] = a.split(/\s+/);
|
|
@@ -273,10 +302,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
302
|
return;
|
|
274
303
|
}
|
|
275
304
|
if (sub === "add") {
|
|
276
|
-
const
|
|
277
|
-
if (!
|
|
278
|
-
const t = addTodo({
|
|
279
|
-
if (ctx.hasUI) ctx.ui.notify(`Added ${t.id}: ${t.
|
|
305
|
+
const title = rest.join(" ").trim();
|
|
306
|
+
if (!title) { if (ctx.hasUI) ctx.ui.notify("usage: /todo add <title> (notes via the todo tool)", "warning"); return; }
|
|
307
|
+
const t = addTodo({ title, source: "slash" });
|
|
308
|
+
if (ctx.hasUI) ctx.ui.notify(`Added ${t.id}: ${t.title}`, "info");
|
|
280
309
|
return;
|
|
281
310
|
}
|
|
282
311
|
if (sub === "done") {
|
|
@@ -297,14 +326,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
297
326
|
const id = rest[0];
|
|
298
327
|
if (!id) { if (ctx.hasUI) ctx.ui.notify("usage: /todo park <id>", "warning"); return; }
|
|
299
328
|
const t = parkTodo(id);
|
|
300
|
-
if (ctx.hasUI) ctx.ui.notify(`Parked ${t.id}: ${t.
|
|
329
|
+
if (ctx.hasUI) ctx.ui.notify(`Parked ${t.id}: ${t.title}`, "info");
|
|
301
330
|
return;
|
|
302
331
|
}
|
|
303
332
|
if (sub === "restore") {
|
|
304
333
|
const id = rest[0];
|
|
305
334
|
if (!id) { if (ctx.hasUI) ctx.ui.notify("usage: /todo restore <id>", "warning"); return; }
|
|
306
335
|
const t = restoreTodo(id);
|
|
307
|
-
if (ctx.hasUI) ctx.ui.notify(`Restored ${t.id}: ${t.
|
|
336
|
+
if (ctx.hasUI) ctx.ui.notify(`Restored ${t.id}: ${t.title}`, "info");
|
|
308
337
|
return;
|
|
309
338
|
}
|
|
310
339
|
if (sub === "prune") {
|
|
@@ -340,6 +369,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
340
369
|
` active: ${report.active.open} open + ${report.active.in_progress} in_progress (${report.active.stale_30d} stale)`,
|
|
341
370
|
` parked: ${report.parked.count} (${report.parked.stale_60d} stale)`,
|
|
342
371
|
` archive: ${report.archive.count} (${report.archive.older_180d} old)`,
|
|
372
|
+
` notes: ${report.notesBytes.total}B total · max ${report.notesBytes.max}B · avg ${report.notesBytes.avg}B`,
|
|
343
373
|
report.flags.length ? ` ⚠ ${report.flags.join(", ")}` : " ✅ healthy",
|
|
344
374
|
...report.suggestions.map((s) => ` → ${s}`),
|
|
345
375
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-todo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Global, cross-session TODO for pi — persists across all sessions and is auto-injected into every prompt. The disk-backed counterpart to branch-scoped pi todo extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "for t in todo-store todo-archive todo-config todo-migrate todo-health todo-hard-prune panel-data; do node test/$t.test.mts || exit 1; done"
|
|
38
|
+
"test": "for t in todo-store todo-title-notes todo-archive todo-config todo-migrate todo-health todo-hard-prune panel-data; do node test/$t.test.mts || exit 1; done"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-ai": "*",
|