@fyeeme/pi-todo 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [1.0.0] - 2026-09-09
6
+
7
+ ### Added
8
+
9
+ - Phased todo tool for pi: nine operations (`init`/`start`/`done`/`rm`/`drop`/`block`/`unblock`/`append`/`view`) over phase/task state with oh-my-pi-verbatim semantics (batch-atomic duplicate rejection, single `in_progress` invariant with earliest-pending auto-promotion, drop = abandoned, block skips finished work).
10
+ - Session persistence: full snapshot entry per mutation; branch-aware restore on session start/fork/tree navigation (`getBranch()`, so snapshots from abandoned branches no longer win the backward scan after `/fork` or `/tree`).
11
+ - `/todo` command with oh-my-pi's full verb set — `edit`, `copy`, `export`, `import`, `append`, `start`, `done`, `drop`, `rm`, `help` — with quote-aware tokenizing, fuzzy task/phase matching, and the Markdown round-trip (`phasesToMarkdown`/`markdownToPhases` with blocker HTML comments and escaped-bracket tolerance). Every mutation injects omp's `<system-reminder>` developer message ("The user manually modified the todo list (...)"), with explicit do-not-recreate notes after removals.
12
+ - Stop reminders matching omp `TodoTracker.checkCompletion`: a hidden `<system-reminder>` ("You stopped with N incomplete todo item(s)... (Reminder X/3)") queues a continuation turn, up to 3 attempts per user prompt, silent when the assistant's last line asks the user something (omp `isAwaitingUserAnswer`). The transcript note uses omp's `TodoReminderComponent` text: `⚠ N incomplete todos - reminder X/3` plus the italic unchecked list.
13
+ - Transcript rendering ports omp's renderer: call line `⏳ Todo · <op> <task> · N items`, result header `☑ Todo · N tasks`, roman-numeral phase headers with `closed/total` progress, touched-phase collapsing, tree glyphs (`├─`/`└─`), omp checkbox glyph set with strikethrough closed rows, and the collapsed walking viewport (`#5873`: last closed row leads, active work first, `… N more todos` summary).
14
+ - Tool prompt guidelines use omp's eager-todo wording.
15
+ - `todo_updated` event-bus broadcast after every successful mutation (pi-goal todo-context integration point).
16
+ - `test/omp-alignment.test.ts`: alignment suite pinning the markdown round-trip, roman numerals, collapsed-viewport selection, subagent-match helper, reminder guards, and summary text to omp sources verbatim.
17
+
18
+ ### Fixed
19
+
20
+ - The `op` parameter uses `StringEnum` instead of a `Type.Union` of literals (Google API compatibility).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fyeeme
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # pi-todo
2
+
3
+ Phased todo lists for [pi](https://github.com/earendil-works/pi-coding-agent) — the oh-my-pi todo tool migrated to a pi extension.
4
+
5
+ The agent gets a `todo` tool: a phased task list (phases → tasks with lifecycle statuses) persisted with the session. You get a `/todo` command, a transcript reminder when the agent stops with unfinished work, and a `todo_updated` event other extensions can consume (pi-goal uses it to attach live progress state to goal continuations).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:@fyeeme/pi-todo
11
+ ```
12
+
13
+ No configuration, no settings, no flags — installed means active. Works in TUI, RPC, and headless (`-p`) modes.
14
+
15
+ ## The tool
16
+
17
+ Nine operations over `phases: [{ phase, items: string[] }]` state:
18
+
19
+ | `op` | Fields | Effect |
20
+ | ---------- | ------------------------- | --------------------------------------------------------- |
21
+ | `init` | `list` (or flat `items`) | Replace the whole list; all tasks start `pending` |
22
+ | `start` | `task` | Mark `in_progress`; demotes every other `in_progress` task |
23
+ | `done` | `task` or `phase` | Mark `completed` |
24
+ | `drop` | `task` or `phase` | Mark `abandoned` (never deleted) |
25
+ | `block` | `task` or `phase`, `reason?` | Park open work awaiting external input; one-line reason |
26
+ | `unblock` | `task` or `phase` | `blocked` → `pending`, clears the note |
27
+ | `rm` | `task?` or `phase?` | Remove a task / clear a phase / clear everything |
28
+ | `append` | `phase`, `items` | Add `pending` tasks; lazily creates the phase |
29
+ | `view` | — | Read-only snapshot |
30
+
31
+ Semantics kept verbatim from oh-my-pi: batch-atomic duplicate rejection (a failing op applies nothing), a single `in_progress` invariant with the earliest pending task auto-promoted on every mutation, `block` never reopens finished work, `view` never writes.
32
+
33
+ ## Command
34
+
35
+ The full oh-my-pi `/todo` verb set:
36
+
37
+ ```
38
+ /todo Show current todos (Markdown checklist)
39
+ /todo edit Round-trip todos through the editor
40
+ /todo copy Print todos as Markdown
41
+ /todo export [<path>] Write todos to file (default: TODO.md)
42
+ /todo import [<path>] Replace todos from file (default: TODO.md)
43
+ /todo append [<phase>] <task...> Append a task; phase fuzzy-matched or auto-created
44
+ /todo start <task> Mark task in_progress (fuzzy match)
45
+ /todo done [<task|phase>] Mark task/phase/all completed
46
+ /todo drop [<task|phase>] Mark task/phase/all abandoned
47
+ /todo rm [<task|phase>] Remove task/phase/all
48
+ ```
49
+
50
+ Every mutation commits a `<system-reminder>` hidden message telling the agent the user manually modified the list (with explicit intent notes after removals, so it never rebuilds cleared items).
51
+
52
+ ## Stop reminders
53
+
54
+ When an agent run ends while tasks are still `pending`/`in_progress`, oh-my-pi's reminder loop kicks in: a hidden `<system-reminder>` message ("You stopped with N incomplete todo item(s)... (Reminder X/3)") is queued as a follow-up turn so the agent continues or marks work done, and a `⚠ N incomplete todos - reminder X/3` note is anchored in the transcript. The cycle allows 3 reminders, restarts on each new user prompt, and stays silent when the assistant's last line is a question to the user (the ball is in your court) or when only `blocked` tasks remain — those are parked awaiting external input.
55
+
56
+ ## Event contract
57
+
58
+ After every successful mutation:
59
+
60
+ ```ts
61
+ pi.events.emit("todo_updated", { phases }); // full snapshot; never on view/failure
62
+ ```
63
+
64
+ ### pi-goal integration
65
+
66
+ [pi-goal](../pi-goal) embeds a `<todo_context>` block into its per-turn goal context when the `todo` tool is active and the list is non-empty — live progress state for autonomous goal continuations. pi-todo is fully usable without pi-goal; pi-goal degrades silently without pi-todo.
67
+
68
+ ## Persistence
69
+
70
+ Every mutation appends a full `todo-phases` snapshot entry to the session. On session start (resume/fork/tree navigation/reload), the latest valid snapshot on the **current branch** wins, so navigating the session tree restores the todo state of that point in history. Malformed entries are skipped, never fatal.
71
+
72
+ ## Ported from oh-my-pi — what was dropped
73
+
74
+ Source: `oh-my-pi/packages/coding-agent/src/tools/todo.ts` (plus reminder, slash-command helpers, prompts). Behavior semantics are ported verbatim; host-internal surfaces with no pi counterpart are not:
75
+
76
+ | omp surface | Status in pi-todo |
77
+ | ------------------------------------------ | -------------------------------------------------------- |
78
+ | mid-run todo nudge (tool-choice queue) | Dropped — omp host-internal |
79
+ | eager-todo / prewalk system-prompt arming | Replaced by `promptGuidelines` on the tool definition |
80
+ | `/todo edit` + markdown round-trip | Dropped — no host editor surface |
81
+ | Sticky HUD / collapsed viewport / animations| Dropped — omp TUI-internal |
82
+ | Subagent todo-match lighting | Dropped — no subagent HUD contract |
83
+ | `todo.enabled` settings gate | Dropped — no settings API by design: installed means active |
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ npm run typecheck # from packages/extensions/pi-todo
89
+ npm run test # vitest
90
+ ```
91
+
92
+ License: MIT
package/index.ts ADDED
@@ -0,0 +1,222 @@
1
+ /**
2
+ * pi-todo — the oh-my-pi todo tool migrated to a pi extension.
3
+ *
4
+ * Source: oh-my-pi (github.com/can1357/oh-my-pi, a fork of badlogic/pi-mono)
5
+ * - packages/coding-agent/src/tools/todo.ts (state + ops) → src/state.ts
6
+ * - packages/coding-agent/src/tools/todo.ts (TodoTool) → src/tool.ts
7
+ * - packages/coding-agent/src/tools/todo.ts (renderer) → src/render.ts
8
+ * - packages/coding-agent/src/prompts/tools/todo.md → src/prompts/todo.md
9
+ * - packages/coding-agent/src/session/todo-tracker.ts (checkCompletion)
10
+ * → agent_end reminder loop
11
+ * - packages/coding-agent/src/modes/components/todo-reminder.ts → reminder entry renderer
12
+ * - packages/coding-agent/src/modes/controllers/todo-command-controller.ts
13
+ * → src/commands.ts
14
+ *
15
+ * Adaptations for pi's extension API (each maps an omp-internal surface to
16
+ * the public extension boundary):
17
+ *
18
+ * omp surface → pi adaptation
19
+ * ─────────────────────────────────────────────────────────────────────────
20
+ * session getTodoPhases/setTodoPhases → closure state + pi.appendEntry
21
+ * full-snapshot entries ("todo-phases")
22
+ * restore via tool-result details → session_start backward scan of the
23
+ * current branch (appendEntry chains
24
+ * off the leaf, so tree navigation
25
+ * and forks restore branch-local state)
26
+ * ArkType schema + lenientArgValidation
27
+ * → TypeBox schema + prepareArguments
28
+ * (missing-op inference before validation)
29
+ * concurrency: "exclusive" → executionMode: "sequential"
30
+ * mergeCallAndResult renderer → split renderCall/renderResult slots
31
+ * eager-todo system-prompt prewalk → promptGuidelines on the tool definition
32
+ * developer-role injected messages → pi.sendMessage display:false (custom
33
+ * role; both /todo edits and stop
34
+ * reminders)
35
+ * TodoTracker.checkCompletion → agent_end handler: same reminder text,
36
+ * 3-attempt cycle, question guard, and
37
+ * followUp-triggered continuation
38
+ * TodoReminderComponent (TUI box) → "todo-reminder" entry renderer
39
+ * (transcript-anchored, same text)
40
+ * $EDITOR round-trip → ctx.ui.editor (Markdown round-trip)
41
+ * mid-run nudge (tool-choice queue) → dropped (omp host-internal)
42
+ * subagent-match lighting → dropped (omp host-internal provider)
43
+ *
44
+ * omp operation semantics are kept verbatim: batch-atomic duplicate
45
+ * rejection, single in_progress invariant with earliest-pending auto-promotion,
46
+ * drop = abandoned (never delete), block skips finished work, view is a read.
47
+ *
48
+ * Integration contract for other extensions:
49
+ * - `pi.events.emit("todo_updated", { phases })` after every successful
50
+ * mutation (never on view or failure)
51
+ * - stop reminders count pending + in_progress only (blocked is parked)
52
+ */
53
+
54
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
55
+ import { Text, type Component } from "@earendil-works/pi-tui";
56
+ import { createTodoCommand } from "./src/commands.ts";
57
+ import { isAwaitingUserAnswer, type TodoItem, type TodoPhase } from "./src/state.ts";
58
+ import { createTodoTool } from "./src/tool.ts";
59
+ import {
60
+ TODO_PHASES_ENTRY_TYPE,
61
+ TODO_REMINDER_ENTRY_TYPE,
62
+ restorePhasesFromEntries,
63
+ } from "./src/restore.ts";
64
+
65
+ /** omp settings todo.remindersMax default (3) — extensions have no settings. */
66
+ const REMINDERS_MAX = 3;
67
+
68
+ interface AssistantTextLike {
69
+ role?: string;
70
+ content?: unknown;
71
+ }
72
+
73
+ function assistantText(message: AssistantTextLike): string {
74
+ if (!Array.isArray(message.content)) return "";
75
+ return message.content
76
+ .filter((block): block is { type: string; text?: string } =>
77
+ !!block && typeof block === "object" && (block as { type?: unknown }).type === "text",
78
+ )
79
+ .map(block => block.text ?? "")
80
+ .join("\n")
81
+ .trim();
82
+ }
83
+
84
+ export default function piTodoExtension(pi: ExtensionAPI): void {
85
+ let phases: TodoPhase[] = [];
86
+ /** omp TodoTracker #reminderCount: reminders sent in the current cycle. */
87
+ let reminderCount = 0;
88
+ /** Last assistant reply text, for omp's awaiting-user-answer guard. */
89
+ let lastAssistantText = "";
90
+
91
+ const deps = {
92
+ getPhases: () => phases,
93
+ setPhases: (next: TodoPhase[]) => {
94
+ phases = next;
95
+ },
96
+ persist: (next: TodoPhase[]) => {
97
+ pi.appendEntry(TODO_PHASES_ENTRY_TYPE, { phases: clonePhasesForEntry(next) });
98
+ },
99
+ broadcast: (next: TodoPhase[]) => {
100
+ pi.events.emit("todo_updated", { phases: clonePhasesForEntry(next) });
101
+ },
102
+ // omp #commit injects a developer system-reminder after user edits.
103
+ sendHiddenMessage: (content: string) => {
104
+ pi.sendMessage({ customType: "todo-user-edit", content, display: false });
105
+ },
106
+ };
107
+
108
+ pi.registerTool(createTodoTool(deps));
109
+
110
+ pi.registerCommand("todo", {
111
+ description: "View or modify the agent's todo list",
112
+ handler: createTodoCommand(deps),
113
+ });
114
+
115
+ pi.on("session_start", (_event, ctx) => {
116
+ // Branch-aware restore: latest valid snapshot on the current branch wins.
117
+ // getBranch() excludes entries on abandoned branches created after a
118
+ // /fork or /tree navigation; getEntries() would let a later snapshot
119
+ // from an abandoned branch win the backward scan.
120
+ phases = restorePhasesFromEntries(ctx.sessionManager.getBranch());
121
+ reminderCount = 0;
122
+ });
123
+
124
+ pi.on("message_start", (event) => {
125
+ // omp resetCycle: a fresh user prompt starts a new reminder cycle.
126
+ if ((event.message as { role?: string }).role === "user") {
127
+ reminderCount = 0;
128
+ }
129
+ });
130
+
131
+ pi.on("agent_end", (event) => {
132
+ // Capture the last assistant text for the question guard BEFORE the
133
+ // reminder decision (omp checkCompletion sees the terminal message).
134
+ const messages = (event.messages ?? []) as AssistantTextLike[];
135
+ for (let i = messages.length - 1; i >= 0; i--) {
136
+ const message = messages[i];
137
+ if (message?.role === "assistant") {
138
+ lastAssistantText = assistantText(message);
139
+ break;
140
+ }
141
+ }
142
+ checkCompletion();
143
+ });
144
+
145
+ /** omp TodoTracker.checkCompletion: nag + auto-continue on open todos. */
146
+ function checkCompletion(): void {
147
+ if (reminderCount >= REMINDERS_MAX) return;
148
+ if (phases.length === 0) {
149
+ reminderCount = 0;
150
+ return;
151
+ }
152
+ const incompleteByPhase = phases
153
+ .map(phase => ({
154
+ name: phase.name,
155
+ tasks: phase.tasks.filter(task => task.status === "pending" || task.status === "in_progress"),
156
+ }))
157
+ .filter(phase => phase.tasks.length > 0);
158
+ const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
159
+ if (incomplete.length === 0) {
160
+ reminderCount = 0;
161
+ return;
162
+ }
163
+ // omp isAwaitingUserAnswer: the assistant ended by asking the user
164
+ // something — skip the reminder, the ball is in the user's court.
165
+ if (isAwaitingUserAnswer(lastAssistantText)) return;
166
+
167
+ reminderCount++;
168
+ const todoList = incompleteByPhase
169
+ .map(phase => `- ${phase.name}\n${phase.tasks.map(task => ` - ${task.content}`).join("\n")}`)
170
+ .join("\n");
171
+ const reminder =
172
+ `<system-reminder>\n` +
173
+ `You stopped with ${incomplete.length} incomplete todo item(s):\n${todoList}\n\n` +
174
+ `Please continue working on these tasks or mark them complete if finished.\n` +
175
+ `(Reminder ${reminderCount}/${REMINDERS_MAX})\n` +
176
+ `</system-reminder>`;
177
+ // omp: append developer message + scheduleAgentContinue.
178
+ pi.sendMessage(
179
+ { customType: "todo-reminder", content: reminder, display: false },
180
+ { triggerTurn: true, deliverAs: "followUp" },
181
+ );
182
+ pi.appendEntry(TODO_REMINDER_ENTRY_TYPE, {
183
+ count: incomplete.length,
184
+ attempt: reminderCount,
185
+ maxAttempts: REMINDERS_MAX,
186
+ todos: incomplete.map(task => ({ content: task.content, status: task.status })),
187
+ });
188
+ }
189
+
190
+ // omp TodoReminderComponent: warning box committed into the transcript —
191
+ // `⚠ N incomplete todos - reminder X/Y` + italic unchecked list.
192
+ pi.registerEntryRenderer<{
193
+ count?: number;
194
+ openCount?: number;
195
+ attempt?: number;
196
+ maxAttempts?: number;
197
+ todos?: TodoItem[];
198
+ }>(TODO_REMINDER_ENTRY_TYPE, (entry, _options, theme) => {
199
+ const data = entry.data;
200
+ const count = typeof data?.count === "number" ? data.count : typeof data?.openCount === "number" ? data.openCount : 0;
201
+ const attempt = typeof data?.attempt === "number" ? data.attempt : 1;
202
+ const maxAttempts = typeof data?.maxAttempts === "number" ? data.maxAttempts : REMINDERS_MAX;
203
+ const label = count === 1 ? "todo" : "todos";
204
+ const header = `⚠ ${count} incomplete ${label} - reminder ${attempt}/${maxAttempts}`;
205
+ const todos = Array.isArray(data?.todos) ? data.todos : [];
206
+ const list = todos.length > 0 ? `\n\n${theme.italic(todos.map(todo => ` ☐ ${todo.content}`).join("\n"))}` : "";
207
+ const component: Component = new Text(theme.fg("warning", header) + list, 0, 0);
208
+ return component;
209
+ });
210
+ }
211
+
212
+ /** Defensive clone for entries/events (omp TodoTracker.#clonePhases). */
213
+ function clonePhasesForEntry(phases: TodoPhase[]): TodoPhase[] {
214
+ return phases.map(phase => ({
215
+ name: phase.name,
216
+ tasks: phase.tasks.map(task =>
217
+ task.blocker !== undefined
218
+ ? { content: task.content, status: task.status, blocker: task.blocker }
219
+ : { content: task.content, status: task.status },
220
+ ),
221
+ }));
222
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fyeeme/pi-todo",
3
+ "version": "1.0.0",
4
+ "description": "Phased todo list tool for pi — nine operations (init/start/done/rm/drop/block/unblock/append/view) over phase/task state persisted with the session, a /todo command, stop-time reminders for unfinished work, and a todo_updated event bus broadcast for other extensions.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "fyeeme",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fyeeme/pi-packages.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/fyeeme/pi-packages/issues"
17
+ },
18
+ "homepage": "https://github.com/fyeeme/pi-packages#readme",
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi",
25
+ "extension",
26
+ "todo",
27
+ "tasks",
28
+ "phases",
29
+ "checklist",
30
+ "progress"
31
+ ],
32
+ "files": [
33
+ "index.ts",
34
+ "src",
35
+ "test",
36
+ "README.md",
37
+ "LICENSE",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "pi": {
41
+ "extensions": [
42
+ "./index.ts"
43
+ ]
44
+ },
45
+ "scripts": {
46
+ "test": "vitest --run",
47
+ "typecheck": "tsc"
48
+ },
49
+ "peerDependencies": {
50
+ "@earendil-works/pi-coding-agent": ">=0.84.1",
51
+ "@earendil-works/pi-tui": ">=0.84.1",
52
+ "typebox": ">=1.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@earendil-works/pi-coding-agent": "0.84.1",
56
+ "@earendil-works/pi-tui": "0.84.1",
57
+ "@types/node": "22.19.19",
58
+ "typescript": "5.9.3",
59
+ "vitest": "3.2.7"
60
+ }
61
+ }