# @gamaraan/todos-tool — OMP-style todos for pi

A [pi](https://github.com/earendil-works/pi-coding-agent) coding-agent
extension that ports the **Oh My Pi (OMP) todo tool, tracker, and `/todo`
command** to pi as a self-contained extension package: a phased todo list the
agent plans and updates itself, with completion reminders, an eager first-turn
prelude, a `/todo` slash command, a persistent HUD widget, and optional
best-effort desktop notification requests.

```text
Todo  2/5 done
  I. Foundation 1/2
    ✓ Scaffold crate
    ○ Wire workspace
  II. Auth 1/3
    ○ Port credential store
```

## Features

**The `todo` tool** (for the agent): `init | start | done | rm | drop | block |
unblock | append | view`.

- Phased lists (`init` with `list: [{phase, items}]` or a flattened
  `items: [...]`), tasks referenced **by exact content, never IDs**.
- Auto-promote only fires when nothing is in progress: an existing
  `in_progress` task keeps its place, and completing a task with none in
  progress promotes the earliest still-open task. Out-of-order completion
  may move the pointer back to an earlier phase (completed tasks never
  revert).
- `block`/`unblock` for work waiting on external input — blocked tasks are
  excluded from stop-time reminders and carry an optional `reason` that
  survives the Markdown round-trip.
- Strict batch semantics: a failing batch is discarded wholesale, so a model
  retry never hits "already exists" for ops that partially landed.
- Missing `op` is inferred for unambiguous shapes (`{list:...}` → `init`,
  `{phase, items}` → `append`, bare `items` on an empty list → `init`) via
  the tool's `prepareArguments` shim.
- Custom TUI rendering: roman-numeral phase headers, progress counters,
  status-colored checkboxes, strikethrough completions, and a walking
  collapsed viewport that keeps the just-completed task visible while
  collapsing untouched phases.

**The tracker** (session behavior):

- **Eager prelude** — with `todo.eager: "preferred" | "always"`, the first
  turn of a session injects a hidden reminder asking the model to lay out a
  phased plan with a single `init` call before working. Guards: skipped for
  questions/exclamations, when todos already exist, or when the tool is not
  active. pi's extension API cannot force a `tool_choice`, so `"always"`
  injects a MUST-call reminder instead (see Differences from OMP).
- **Mid-run nudge** — after 12 successful mutating tool results (bash/edit/
  write/eval) with incomplete todos, a hidden steer message asks the agent to
  mark finished tasks done (≤2 per prompt cycle).
- **Bundled `todo-discipline` skill** — while the tool is enabled, the
  extension contributes a load-on-demand skill (via pi's
  `resources_discover`) whose description sits in every model's system
  prompt: it mandates a phased `init` before any 3+ step request and marking
  each individual task `done` the moment that task finishes — never
  retro-batched at the end of a phase or session. Load it explicitly with
  `/skill:todo-discipline`.
- **Completion reminder** — when the agent settles with incomplete todos and
  isn't waiting for user input, a reminder listing the remaining items is
  injected and a fresh turn is triggered (`todo.reminders`,
  `todo.remindersMax`, default 3). Reminders pause until the agent makes
  progress (any tool result) and never fire while the assistant is awaiting
  your answer.

**The `/todo` command** (for you):

```text
/todo                              Show current todos
/todo edit                         Edit todos in the built-in editor
/todo copy                         Copy todos as Markdown (OSC 52 clipboard)
/todo export [<path>]              Write todos to a file (default: TODO.md)
/todo import [<path>]              Replace todos from a file
/todo append [<phase>] <task...>   Append a task (phase fuzzy-matched/created)
/todo start  <task>                Mark a task in_progress (fuzzy match)
/todo done/drop [<task|phase>]     Mark completed / abandoned
/todo rm     [<task|phase>]        Remove task/phase/all
```

Manual edits persist as `user_todo_edit` custom entries (they win over tool
results when newer) and inject a hidden reminder telling the model what
changed — including explicit "do NOT recreate" directives after removals.
Outside the TUI, `/todo edit` falls back to `$VISUAL`/`$EDITOR` on a temp
file.

**The HUD widget** — a compact per-phase checklist with progress
(`2/5 done`), rendered above the editor and kept in sync with every tool
result, manual edit, and session reload.

## Install

The published package is `@gamaraan/todos-tool`:

```bash
pi install npm:@gamaraan/todos-tool
```

Pin a release with `pi install npm:@gamaraan/todos-tool@0.2.1`. From GitHub:
`pi install git:github.com/gamaraan/todos-tool-pi-extension`. Manual: copy
`src/index.ts` (plus the `src/` modules it imports) into
`~/.pi/agent/extensions/`, or add the path to the `extensions` array in
`~/.pi/agent/settings.json`. Restart pi (or start a new session) after
installing.

## Configure

The extension cannot extend pi's built-in settings schema, so it reads a
small JSON file from the host agent dir, optionally overridden per project:

| File | Scope |
| --- | --- |
| `~/.pi/agent/todo.json` | Global |
| `<cwd>/.pi/todo.json` | Project (only when the project is trusted) |

```json
{
  "enabled": true,
  "reminders": true,
  "remindersMax": 3,
  "eager": "default"
}
```

| Key | Default | Notes |
| --- | --- | --- |
| `enabled` | `true` | Gates the tool and all tracker behaviors. When `false` the `todo` tool is removed from the active tool set at session start. A global `false` is a floor — project config cannot re-enable it. |
| `reminders` | `true` | Stop-time incomplete-todo reminders. |
| `remindersMax` | `3` | Max reminder attempts per prompt cycle. |
| `eager` | `"default"` | `"default"` = no prelude, `"preferred"` = soft reminder, `"always"` = MUST-call reminder on the first turn. |

Invalid values and unknown keys are ignored with a warning. Configure these
settings interactively with `/todos-configure`; it saves the global JSON file and
reloads the extension when finished. At startup, precedence is CLI flag >
environment variable > project JSON > global JSON > built-in default:

| Setting | CLI flag | Environment variable |
| --- | --- | --- |
| Enabled | `--todo-enabled on/off` | `PI_TODO_ENABLED` |
| Reminders | `--todo-reminders on/off` | `PI_TODO_REMINDERS` |
| Reminder limit | `--todo-reminders-max N` | `PI_TODO_REMINDERS_MAX` |
| Eager mode | `--todo-eager default/preferred/always` | `PI_TODO_EAGER` |

## Optional desktop notifications

In TUI mode, a successful transition from active work to `completed` or
`blocked` emits a named `desktop-notify:request` EventBus request that includes
the transitioned task names:

```ts
{
  title: "Todo completed",
  body: "Completed: Ship it",
  type: "todo-completed",
  urgency: "normal",
  sound: "info"
}
```

Blocked transitions use a `"Todo blocked"` title, a body such as
`"Blocked: Need input"`, type `"todo-blocked"`, and sound `"warning"`.
Multiple transitions of the same kind are listed in one body. Blocker reasons
are not included. Requests are emitted only on terminals advertising OSC
9/99-capable focus handling (Kitty, Ghostty, WezTerm, iTerm2, or Warp), so the
terminal can suppress the toast while the pi tab is focused. Print, JSON, RPC,
replay, read-only, failed, repeated, and unsupported-terminal paths remain
silent. `@gamaraan/desktop-notify` is optional: no dependency or installation
is required, and without a listener the todo extension continues normally.
The desktop-notify extension's own settings control delivery when it is loaded.

Use `/todos-configure` for the successive interactive settings dialogues. The
wizard writes `todo.json` and reloads the extension after saving; startup flags
and environment variables still override the saved values for that process.

## How it works

- **Persistence is the tool result itself.** Every successful `todo` result
  carries `details.phases`; on session start, rewind (`session_tree`), and
  compaction, the extension replays the branch (`getLatestTodoPhasesFromEntries`)
  and takes the newest snapshot — the latest `user_todo_edit` custom entry,
  else the latest successful `todo` toolResult. Branching and rewinding
  always show the todo state correct for that point in history.
- **Errors are thrown.** pi signals tool failures by throwing; the model
  receives the omp-style summary text (errors + full current list) and the
  previous state stays intact.
- **Reminders re-enter the loop** via `pi.sendMessage(..., { triggerTurn: true })`
  from the `agent_settled` handler; mid-run nudges use `deliverAs: "steer"`.

## Compatibility

**Pi:** supported range **0.84.x** (the API surface this extension uses —
`ToolDefinition.prepareArguments`, `before_agent_start` message injection,
`agent_settled`, `sendMessage` with `triggerTurn`/`deliverAs` — is current
for 0.84.0+). CI runs the unit suite and typecheck against the pinned
published packages.

**OMP:** not supported, by design — OMP ships its own native `todo` tool, so
loading this extension there would register a duplicate tool name. The port
target is pi only.

## Differences from OMP

Faithful port, with these deliberate adaptations:

| OMP | This extension |
| --- | --- |
| `todo.eager: "always"` forces a `tool_choice` | pi extensions cannot force tool choices; `"always"` injects a MUST-call reminder (models virtually always comply) |
| Sticky HUD header at the top of the chat | HUD widget above the editor (`ctx.ui.setWidget`) |
| Strikethrough reveal animation driven by the spinner frame | Completed tasks strike through immediately (pi render options carry no frame counter) |
| Settings via OMP's settings schema (`todo.*`) | `todo.json` config files (global + trusted project), `/todos-configure`, CLI flags, and environment overrides |
| Desktop notification integration | Optional named EventBus `desktop-notify:request` payloads with transitioned task names, gated to OSC 9/99-capable TUI terminals |
| `$EDITOR` for `/todo edit` | Built-in pi editor dialog in the TUI; `$EDITOR` fallback outside it |
| Plan-mode pause, subagent reconciliation, eager task prelude | Out of scope (pi has no core plan mode / subagents); the guarded hooks are omitted |
| Branch replay casts persisted `phases` blindly | Structurally validated (`isTodoPhase`, including the optional `blocker` type) first; corrupt snapshots are skipped, never crash session sync |
| Task content / phase names stored verbatim | Whitespace runs (incl. newlines) collapsed at `init`/`append` input, same one-line guarantee as blocker `reason`; blank entries rejected |
| Blocker-comment parse binds the first `<!-- blocker:` (lazy) | Binds the trailing comment the writer emits (greedy) AND delimiters inside the blocker reason are percent-escaped, so blockers containing `-->` or `<!-- blocker:` round-trip intact; legacy unescaped exports still parse |
| `/todo edit` external-editor temp file uses a predictable pid/timestamp name in tmp | `mkdtemp` (0700) + 0600 file: no pre-created symlink hijack on multi-user machines; saves are aborted when the list changed while the editor was open (no silent overwrite of concurrent progress) |
| `/todo export` writes unconditionally | Refuses symlink targets and refuses clobbering an existing file without an explicit confirm |
| Env/flag overrides fall back silently | Invalid values warn (no silent `enabled` from a typo); whitespace-only values are ignored |
| Manual `/todo append/import/edit` accept blank/duplicate identities | Shared identity validator rejects them (duplicates would be permanently unaddressable) |
| Todo snapshots merge "no snapshot" with "explicitly cleared" | Replay returns `undefined` vs a valid `[]` — `/todo` never resurrects an explicitly cleared list |
| `<system-reminder>` as a `developer` message | Same text as a hidden `custom` message (pi converts these to user-role in context — the only injection mechanism extensions have) |
| System prompts (`prompts/system/*.md`) shipped in-core | A bundled `todo-discipline` skill contributed via `resources_discover` when the tool is enabled (extensions cannot edit the system prompt builder; skills are pi's extension-facing equivalent) |

## Develop

```bash
bun install
bun run typecheck        # tsc --noEmit against the published pi 0.84.1 types
bun test                 # 149 unit tests across state/markdown/persistence/format/render/tracker/command/config/smoke
bun run verify:package   # npm pack --dry-run
```

The extension imports `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`,
and `@earendil-works/pi-ai` (peer dependencies, provided by your pi
installation). The `src/` modules are loaded by pi's extension loader (jiti)
directly as TypeScript — no build step. Desktop notifications use only pi's
EventBus and do not add a `desktop-notify` dependency.

See [`AGENTS.md`](./AGENTS.md) for the development guide — architecture,
conventions, and the pre-release manual smoke checklist.

## License

MIT — see [LICENSE](./LICENSE). This project ports code from
[Oh My Pi](https://github.com/oh-my-pi) (MIT, © Can Bölük) which is itself a
fork of [pi](https://github.com/earendil-works/pi-coding-agent) (MIT,
© Mario Zechner); both copyright notices are retained.
