@pi-archimedes/todo 2.0.1 → 2.2.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 CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Todo list management with auto-clear and live subagent visibility for the [Pi coding agent](https://github.com/earendil-works/pi).
4
4
 
5
- ## Features
5
+ Track complex multi-step tasks structured in a todo list with automatic clearing on completion and live side-by-side visibility into subagent tasks. Having an active progress display keeps long tasks on track and gives both you and the model clear visibility into completed steps and next actions.
6
+
7
+ ## What you get
6
8
 
7
9
  - **`manage_todo_list` tool** — structured todo tracking with `read` and `write` operations
8
10
  - **Auto-clear** — when all todos are completed, the list clears itself after a brief 2-second delay
@@ -25,16 +27,16 @@ Main agent todos (left) alongside a subagent's todos (right), separated by a div
25
27
 
26
28
  ![todos and subagent](../../docs/images/todos-and-subagent.png)
27
29
 
28
- ## Installation
30
+ ## Install
29
31
 
30
32
  ```bash
31
- pi install @pi-archimedes/todo
33
+ pi install npm:@pi-archimedes/todo
32
34
  ```
33
35
 
34
- Or install the full [pi-archimedes](../..) meta package for the integrated experience:
36
+ Or install full meta package:
35
37
 
36
38
  ```bash
37
- pi install pi-archimedes
39
+ pi install npm:pi-archimedes
38
40
  ```
39
41
 
40
42
  ## Usage
@@ -79,8 +81,10 @@ The `manage_todo_list` tool accepts two operations:
79
81
 
80
82
  ## Auto-clear
81
83
 
82
- When all todos in the list are marked `completed`, the widget shows the all-done state for 2 seconds, then auto-clears. No need to manually run `/todos clear`.
84
+ When all todos in the list are marked as `completed`, the widget shows the all-done state for 2 seconds, then auto-clears. No need to manually run `/todos clear`.
83
85
 
84
- ## Subagent integration
86
+ ## Integration
85
87
 
86
88
  When installed via `pi-archimedes` (the meta package), subagent todo events flow through `@pi-archimedes/core/bus` and appear as separate columns in the widget. Each subagent gets its own column labeled with its agent name. The column auto-removes when the subagent finishes.
89
+
90
+ ← Back to [pi-archimedes](../../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/todo",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "main": "./src/index.ts",
13
13
  "dependencies": {
14
- "@pi-archimedes/core": "2.0.1"
14
+ "@pi-archimedes/core": "2.2.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@earendil-works/pi-ai": ">=0.1.0",
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as fc from "fast-check";
3
+ import { TodoStateManager } from "./state-manager.js";
4
+ import type { TodoItem } from "./types.js";
5
+
6
+ // ── Helpers ─────────────────────────────────────────────────────────────────
7
+
8
+ function makeTodo(id: number, title: string, description: string, status: TodoItem["status"]): TodoItem {
9
+ return { id, title, description, status };
10
+ }
11
+
12
+ // ── TodoStateManager ────────────────────────────────────────────────────────
13
+
14
+ describe("TodoStateManager", () => {
15
+ let manager: TodoStateManager;
16
+
17
+ beforeEach(() => {
18
+ manager = new TodoStateManager();
19
+ vi.useFakeTimers();
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.useRealTimers();
24
+ });
25
+
26
+ describe("read", () => {
27
+ it("returns empty array initially", () => {
28
+ expect(manager.read()).toEqual([]);
29
+ });
30
+
31
+ it("returns copy (mutations don't affect internal state)", () => {
32
+ manager.write([makeTodo(1, "Test", "desc", "not-started")]);
33
+ const copy = manager.read();
34
+ copy.push(makeTodo(2, "Injected", "desc", "not-started"));
35
+ expect(manager.read()).toHaveLength(1);
36
+ });
37
+ });
38
+
39
+ describe("write", () => {
40
+ it("stores todos correctly", () => {
41
+ manager.write([
42
+ makeTodo(1, "Task 1", "desc 1", "not-started"),
43
+ makeTodo(2, "Task 2", "desc 2", "in-progress"),
44
+ ]);
45
+ expect(manager.read()).toHaveLength(2);
46
+ expect(manager.read()[0]?.title).toBe("Task 1");
47
+ expect(manager.read()[1]?.status).toBe("in-progress");
48
+ });
49
+
50
+ it("with all completed schedules auto-clear", () => {
51
+ manager.write([makeTodo(1, "Done", "desc", "completed")]);
52
+ expect(manager.read()).toHaveLength(1);
53
+ vi.advanceTimersByTime(2000);
54
+ expect(manager.read()).toHaveLength(0);
55
+ });
56
+
57
+ it("does not schedule auto-clear when not all completed", () => {
58
+ manager.write([
59
+ makeTodo(1, "Done", "desc", "completed"),
60
+ makeTodo(2, "Pending", "desc", "not-started"),
61
+ ]);
62
+ vi.advanceTimersByTime(2000);
63
+ expect(manager.read()).toHaveLength(2);
64
+ });
65
+ });
66
+
67
+ describe("clear", () => {
68
+ it("empties todos", () => {
69
+ manager.write([makeTodo(1, "Task", "desc", "not-started")]);
70
+ manager.clear();
71
+ expect(manager.read()).toEqual([]);
72
+ });
73
+ });
74
+
75
+ describe("getStats", () => {
76
+ it("computes correct counts", () => {
77
+ manager.write([
78
+ makeTodo(1, "A", "desc", "not-started"),
79
+ makeTodo(2, "B", "desc", "in-progress"),
80
+ makeTodo(3, "C", "desc", "completed"),
81
+ makeTodo(4, "D", "desc", "completed"),
82
+ ]);
83
+ expect(manager.getStats()).toEqual({
84
+ total: 4,
85
+ completed: 2,
86
+ inProgress: 1,
87
+ notStarted: 1,
88
+ });
89
+ });
90
+
91
+ it("returns zero stats for empty list", () => {
92
+ expect(manager.getStats()).toEqual({
93
+ total: 0,
94
+ completed: 0,
95
+ inProgress: 0,
96
+ notStarted: 0,
97
+ });
98
+ });
99
+ });
100
+
101
+ describe("validate", () => {
102
+ it("catches missing fields", () => {
103
+ const result = manager.validate([{ id: 1, title: "", description: "", status: "" } as any]);
104
+ expect(result.valid).toBe(false);
105
+ expect(result.errors.length).toBeGreaterThan(0);
106
+ });
107
+
108
+ it("catches invalid status values", () => {
109
+ const result = manager.validate([{ id: 1, title: "Test", description: "desc", status: "invalid" } as any]);
110
+ expect(result.valid).toBe(false);
111
+ expect(result.errors[0]).toContain("status");
112
+ });
113
+
114
+ it("catches non-array input", () => {
115
+ const result = manager.validate("not an array" as any);
116
+ expect(result.valid).toBe(false);
117
+ expect(result.errors[0]).toBe("todoList must be an array");
118
+ });
119
+
120
+ it("accepts valid input", () => {
121
+ const result = manager.validate([
122
+ makeTodo(1, "Task", "desc", "not-started"),
123
+ makeTodo(2, "Task 2", "desc", "in-progress"),
124
+ makeTodo(3, "Task 3", "desc", "completed"),
125
+ ]);
126
+ expect(result.valid).toBe(true);
127
+ expect(result.errors).toEqual([]);
128
+ });
129
+
130
+ it("property: validate(write(x)).valid === true after write (round-trip)", () => {
131
+ fc.assert(
132
+ fc.property(
133
+ fc.array(
134
+ fc.record({
135
+ id: fc.nat({ max: 1000 }),
136
+ title: fc.string({ minLength: 1, maxLength: 50 }),
137
+ description: fc.string({ minLength: 1, maxLength: 100 }),
138
+ status: fc.oneof(fc.constant("not-started"), fc.constant("in-progress"), fc.constant("completed")),
139
+ }),
140
+ ),
141
+ (todos) => {
142
+ const mgr = new TodoStateManager();
143
+ mgr.write(todos as TodoItem[]);
144
+ const readTodos = mgr.read();
145
+ const result = mgr.validate(readTodos);
146
+ if (!result.valid) {
147
+ throw new Error(`Round-trip validation failed: ${JSON.stringify(result.errors)}`);
148
+ }
149
+ },
150
+ ),
151
+ { verbose: false },
152
+ );
153
+ });
154
+ });
155
+ });