@getpipher/armory-todo
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.
Package details
Install @getpipher/armory-todo from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@getpipher/armory-todo- Package
@getpipher/armory-todo- Version
0.5.2- Published
- Jul 21, 2026
- Downloads
- 98/mo · 8/wk
- Author
- rz1989
- License
- MIT
- Types
- extension
- Size
- 614.6 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
The problem
pi sessions are ephemeral conversation branches. A TODO you tell to session A is invisible to session B unless you manually write it to a notes file and remember to read it next time. Every existing pi todo extension (@juicesharp/rpiv-todo, @xynogen/pix-todo, @gonrocca/zero-pi-todo, …) is conversation-branch-scoped — they persist via pi's appendEntry() and survive compaction + /reload within a single session. None bridge across separate sessions, and none make a fresh session aware of pending work on its own.
armory-todo is the other shape: a single disk file that every session reads, plus an auto-injected ## Open TODOs block in the system prompt so a fresh session starts already aware.
| survives compaction/reload within a session | survives across separate sessions | auto-surfaced in every new session | |
|---|---|---|---|
| branch-scoped todo extensions | ✅ | ❌ | ❌ |
| armory-todo | ✅ | ✅ | ✅ |
Install
pi install git:github.com/getpipher/armory-todo # from git
pi install npm:@getpipher/armory-todo # from npm (scoped)
Then restart pi (or /reload). Or add to ~/.pi/agent/settings.json:
{ "packages": ["npm:@getpipher/armory-todo"] }
Lifecycle boxes (v0.2.0)
TODOs live in one of three states, only one of which hits the agent context:
| Box | Status(es) | Auto-injected? | Recoverable? |
|---|---|---|---|
| Active | open, in_progress |
✅ Yes (capped 15) | n/a |
| Parked | parked |
❌ No | ✅ update --status open |
| Archive | done, cancelled |
❌ No | ✅ restore <id> |
Pruning: prune (default: done/cancelled older than 7 days) moves finished
todos from the live file to todo-archive.json — nothing is deleted. prune --all
ignores age. restore <id> brings an archived todo back as open.
Auto-prune (v0.3.1): on every session_start, done/cancelled todos older
than config.prune.defaultAgeDays (default 7d) archive themselves — no need
to run prune manually. Fresh done (<7d) stays in live. The startup notify
reports what moved (auto-pruned N stale done (>7d): … + a restore hint);
it's a transient message, not a prompt injection. Reversible via restore <id>.
prune --all (move fresh done too) and prune --hard (irreversible) stay manual.
The only irreversible action is prune --hard (hard-prune) — it requires an
explicit confirm: true and is always user-confirmed. See Self-awareness
below.
Storage layout:
~/.pi/agent/todo/
todo.json # active + parked
todo-archive.json # done + cancelled (sealed history)
todo.config.json # prune ages + health thresholds
A v1 single-file store at ~/.pi/agent/todo.json is migrated automatically on
first load after upgrade.
Self-awareness: health + hard-prune
health reports bloat across all three boxes — counts, stale items, and
actionable suggestions (e.g. "archive: 41 items older than 180d → consider
prune --hard --box archive --older-than 180 --confirm"). On session_start,
if any bloat flags are detected, the startup notify appends a ⚠ N bloat signals nudge.
prune --hard is the only irreversible action — it permanently deletes
todos. It's gated three ways:
- Tool-level:
confirm: trueis required in the tool call; without it the action refuses with a clear message. - Prompt-level: the agent is instructed to always run
healthfirst, surface the report + the exact proposed command, and wait for an explicit user "yes" before passingconfirm: true. - Slash-level:
/todo prune --hardprompts an interactivectx.ui.confirmyes/no dialog before executing.
Everything else in armory-todo is reversible. prune --hard is the one
irreversible escape hatch, always user-confirmed.
Project-scope management (v0.4.0)
A project registry (~/.pi/agent/todo/projects.json, lazy-synced on read) tracks known projects + an advisory per-project maxOpen cap slot. The todo tool gains two actions: projects (per-project scope overview — open/in_progress/parked/done counts + maxOpen + OVER/?typo markers + last-updated) and project_rename (rename or merge a project; rewrites live + archive + registry — the typo-cleanup path, e.g. getpither → getpipher).
health gains four per-project flags: PROJECT_OVER (open > a project's maxOpen slot), PROJECT_LARGE (open > health.perProjectDefaultMax, default 8 — fires out-of-the-box, no per-project config needed), PROJECT_STALE (project untouched > activeStaleDays), PROJECT_TYPO (1-todo project with a near-named sibling, Levenshtein ≤ 2). The /todo health report + the todo health action both show a projects: section.
The interactive /todo panel gains a 6th tab — Projects — listing the overview rows + a (no project) summary, with an action submenu per project: Rename / merge (inline input), Set maxOpen (number or clear), Filter active to project (jump to the Active tab scoped). A thin /todo projects slash mirrors the overview as text.
Advisory in v0.4.0 → enforced in v0.5.0 — maxOpen now blocks add (and project-move) when a project is at its cap. See the Caps enforcement (v0.5.0) section below.
Caps enforcement (v0.5.0)
Three caps keep the store (and its auto-injected prompt block) from bloating silently — the forcing-function half of issue #1:
Count cap (per-project
maxOpen, enforced). A project'smaxOpenslot (set via the Projects tab → Set maxOpen, orsetProjectMaxOpen) blocksaddwhen the project is at its cap, and blocks a project-move of anopen/in_progresstodo into a capped project. The cap is on theopencount (matches thePROJECT_OVERhealth flag);in_progressdoesn't count. Un-park (parked→open) is intentionally not blocked — reactivating deferred work isn't adding new work. The block message tells you how to raise/clear the cap.maxOpen: null(default) = uncapped.Notes cap (
health.maxNotesBytes, default 8192 bytes, enforced). Oversize notes are rejected atadd/update(only whennotesis being written — a title edit on a grandfathered oversize note isn't trapped). Byte-length, not char-length (notes can hold Unicode). Existing oversize notes are grandfathered;healthsurfaces the worst offender via theNOTES_OVERflag + an actionabletodo update <id> notes:…suggestion.Over-cap injection truncation. When actionable >
health.activeMaxOpen(default 15), the auto-injected## Open TODOs (N)block collapses to a ~4-line summary (total + project span + over-budget projects + atodo listpointer) instead of the row list. Under the cap → the familiar row list.activeMaxOpenitself stays advisory (it drives theACTIVE_LARGEflag and the truncation trigger; it is not a hard global block).
Backwards-compat: zero migration (store v3, config v1, registry v1 unchanged in shape). Oversize notes grandfathered. The maxOpen advisory→enforced graduation is a documented behavior change for any v0.4.0 user who set a slot (the block message tells them how to raise/clear).
Interactive panel (SPEC-3)
Run /todo (no arg) in a TUI session to open the interactive triage panel:
- Box tabs (Tab / Shift+Tab): Active · Parked · Done · Archive · Projects · Config
- Filter input: type to search by text (live filter)
- SelectList: arrow keys navigate, Enter selects
- Action submenu (on Enter): View detail / Complete / Park / Re-activate / Restore / Edit title / Delete
- Done tab (v0.3.1): all finished work (
status: done) unified across live + archive, location-tagged ([live Nd]/[archived YYYY-MM-DD]), filterable; Enter → View detail, or Restore-from-archive. Excludescancelled(that's in the Archive tab). - Detail view (View detail, or Enter on a row): renders the title + full
notesread-only, with a footer hint on editing notes via thetodotool - Archive box: summary-first (counts by project + month) → Enter on a bucket to drill down
- Projects tab (v0.4.0): per-project scope overview (open/in_progress/parked/done counts +
maxOpen+OVER/?typomarkers) + a(no project)summary row; Enter on a project → action submenu: Rename / merge · Set maxOpen · Filter active to project. - Config box: SettingsList with prune ages + health thresholds — edit live, persists to
todo.config.json - Escape: exit the panel
Typed subcommands (/todo park <id>, /todo prune, etc.) all still work alongside the panel. Non-TUI sessions (pi -p, RPC) fall back to the text list.
Usage
Say it naturally — the model calls the todo tool:
“put this in our TODO: decouple global rules into AGENTS.md” “show me the TODO” → “mark td-… done” “park td-… for now” → later: “restore td-…” “prune the done todos”
Slash command for quick human triage:
/todo list open + in-progress TODOs
/todo all include parked/done/cancelled
/todo add <title> quick add (priority: med; notes via the todo tool)
/todo finished list all done todos (live + archived, recent first)
/todo done <id> mark done
/todo rm <id> cancel (tombstone)
/todo park <id> defer (parked — not injected, recoverable)
/todo restore <id> bring an archived todo back as open
/todo prune [--all] move done/cancelled to archive (reversible)
/todo prune --hard permanent deletion (interactive confirm prompt)
/todo archive [filter] archive summary, or filtered slice (project:X / text:Y)
/todo health bloat report across all boxes + flags + suggestions
/todo clean clear all done (deprecated — use prune)
/todo path show the store file path
The todo tool (model-callable):
| action | params | effect |
|---|---|---|
list |
statusFilter?, projectFilter?, tagFilter?, text?, since?, before?, limit?, page?, archived? |
matching TODOs (default: open + in_progress). archived:true queries the archive — bare call returns a summary, filters return paginated slices |
add |
title, notes?, project?, tags?, priority?, source? |
create a TODO (title ≤120 chars; long detail goes in notes) |
get |
id |
read a TODO's full record incl. notes |
update |
id, title?, notes?, priority?, status?, project?, tags? |
edit a TODO (set status: parked to defer; notes="" clears) |
complete |
id |
mark done |
delete |
id |
cancel (tombstone) |
park |
id |
defer (parked — not injected) |
prune |
ageDays?, all? |
move done/cancelled to archive (reversible via restore) |
restore |
id |
bring an archived TODO back as open |
health |
(none) | bloat report across active/parked/archive + flags + suggestions |
prune (hard) |
hard:true, confirm:true, box?, olderThan?, project?, tag? |
PERMANENT deletion — the only irreversible action |
clear |
status? (default done) |
bulk-clear a status (deprecated — use prune) |
Each TODO carries id, title (≤120 chars), notes (any length), project, tags, priority (low|med|high|critical), status (open|in_progress|parked|done|cancelled), source, createdAt, updatedAt, closedAt. The auto-injected block + list/panel show title only; notes is read via get and never injected.
How it works
- Disk store —
~/.pi/agent/todo/folder:todo.json(live: active + parked),todo-archive.json(sealed: done + cancelled),todo.config.json(prune ages + health thresholds),projects.json(project registry: canonical names + advisorymaxOpenslots, v0.4.0). Atomic0600writes, corrupt-file auto-recovery,version: 3store schema (title≤120 chars +notesany length; v2text-only stores migrate to v3 on first load). Not pi session entries, so it outlives any conversation. todotool — model CRUD + lifecycle (above)./todocommand — human triage (above).- Auto-inject — on every
before_agent_start, a compact## Open TODOs (N)block (titles + ids, sorted by priority) is appended to the system prompt, so the agent starts every turn already aware of pending work. The block is cap-aware (v0.5.0): underhealth.activeMaxOpen(default 15) it lists the rows; over the cap it collapses to a lean summary (counts + over-budget projects + atodo listpointer) so the prompt stays bounded when the store bloats. Onlyopen+in_progressare injected —parkedand archived todos are excluded (the lifecycle-box boundary). Mutations refresh it on the next turn. - Archive query —
listwitharchived:trueis summary-first (counts by project + month) then filtered/paginated on demand, so a large archive never bloats a single query.
Full design + decisions:
- v0.4.0 (project-scope management):
docs/superpowers/specs/2026-07-21-project-scope-management-design.md - v0.3.1 (auto-prune + unified Done view):
docs/superpowers/specs/2026-07-21-auto-prune-done-view-design.md - v0.3.0 (title + notes split):
docs/superpowers/specs/2026-07-21-title-notes-split-design.md - v0.2.0 (lifecycle boxes + prune + health):
docs/superpowers/specs/2026-07-20-lifecycle-boxes-prune-design.md - Original v0.1.0 spec:
docs/todo-SPEC.md
Configuration
| env var | default | purpose |
|---|---|---|
TODO_DIR |
~/.pi/agent/todo/ |
override the store folder (tests / multiple profiles) |
Run the store tests: npm test (315/315 across 11 suites).
Known issues
- No in-panel multi-line
notesediting. The panel's inline Edit is single-line (Input) fortitleonly;ctx.ui.editor()from insidectx.ui.custom()triggers a nested-UI bug (/todowon't reopen).notesis model-managed via thetodotool (action: update, id, notes). When a safectx.ui.editor()-from-custom()pattern lands in pi-tui, in-panel notes editing is a clean follow-up. - Caps enforcement shipped in v0.5.0. Per-project
maxOpenblocksadd/move;health.maxNotesBytes(default 8KB) rejects oversize notes at write; the auto-injected block collapses to a lean summary overactiveMaxOpen. See Caps enforcement (v0.5.0) above.
Security
- Store file is
0600. Atomic write (temp + rename); a corrupt file is backed up totodo.json.bad-<ts>and a fresh store starts — the extension never crashes your session. - Never put secrets in a TODO. TODO text is injected into the system prompt and therefore reaches your model provider — same rule as
AGENTS.md/ context files.
License
MIT.