@agnishc/edb-todo 0.8.2 → 0.10.3
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 +27 -0
- package/README.md +145 -33
- package/package.json +1 -1
- package/src/auto-clear.ts +87 -0
- package/src/component.ts +194 -57
- package/src/config.ts +25 -0
- package/src/file-store.ts +408 -0
- package/src/index.ts +554 -108
- package/src/process-tracker.ts +146 -0
- package/src/prompt.ts +15 -11
- package/src/schemas.ts +52 -27
- package/src/state.ts +224 -97
- package/src/types.ts +14 -1
package/src/index.ts
CHANGED
|
@@ -1,111 +1,249 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* edb-todo
|
|
3
3
|
*
|
|
4
|
-
* Task management extension
|
|
5
|
-
* agents to lose track of the original plan as context grows.
|
|
4
|
+
* Task management extension with pi-tasks tool names, descriptions, and behavior.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* branch, so /tree navigation and forking work correctly
|
|
6
|
+
* Tools:
|
|
7
|
+
* TaskCreate — Create a structured task
|
|
8
|
+
* TaskList — List all tasks with status and blocked-by info
|
|
9
|
+
* TaskGet — Get full task details, description, dependencies
|
|
10
|
+
* TaskUpdate — Update status, fields, dependencies; status:"deleted" removes
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
* todo_read — read the current task list
|
|
16
|
-
* todo_remove — remove tasks by ID (permanent deletion)
|
|
17
|
-
* Command: /todos — open interactive full-screen task viewer
|
|
12
|
+
* Command: /todos — interactive task manager with settings panel
|
|
18
13
|
*/
|
|
19
14
|
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
20
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
22
18
|
import { Type } from "typebox";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
19
|
+
import { AutoClearManager } from "./auto-clear.js";
|
|
20
|
+
import { openTodosMenu, TodoViewComponent } from "./component.js";
|
|
21
|
+
import { loadTodoConfig } from "./config.js";
|
|
22
|
+
import { FileTaskStore } from "./file-store.js";
|
|
23
|
+
import { ProcessTracker } from "./process-tracker.js";
|
|
24
|
+
import { buildSystemPromptBlock, formatListForLLM } from "./prompt.js";
|
|
25
|
+
import { TodoCreateParams, TodoGetParams, TodoUpdateParams } from "./schemas.js";
|
|
26
|
+
import { priorityColor, priorityLabel, renderTaskListResult, TodoWidget } from "./state.js";
|
|
27
|
+
import type { TaskDetails, TaskPriority } from "./types.js";
|
|
28
|
+
|
|
29
|
+
// ── Constants ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const TASK_TOOL_NAMES = new Set(["TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "TaskOutput", "TaskStop"]);
|
|
32
|
+
const REMINDER_INTERVAL = 4;
|
|
33
|
+
const AUTO_CLEAR_DELAY = 4;
|
|
34
|
+
|
|
35
|
+
const SYSTEM_REMINDER = `<system-reminder>
|
|
36
|
+
The task tools haven't been used recently. If you're working on tasks that would benefit from tracking progress, consider using TaskCreate to add new tasks and TaskUpdate to update task status (set to in_progress when starting, completed when done). Also consider cleaning up the task list if it has become stale. Only use these if relevant to the current work. This is just a gentle reminder - ignore if not applicable. Make sure that you NEVER mention this reminder to the user
|
|
37
|
+
</system-reminder>`;
|
|
28
38
|
|
|
29
39
|
// ── Extension ──────────────────────────────────────────────────────────────────
|
|
30
40
|
|
|
31
41
|
export default function todoExtension(pi: ExtensionAPI): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
let cwd = process.cwd();
|
|
43
|
+
let cfg = loadTodoConfig(cwd);
|
|
44
|
+
const taskScope = cfg.taskScope ?? "session";
|
|
45
|
+
|
|
46
|
+
function resolveStorePath(sessionId?: string): string | undefined {
|
|
47
|
+
const envVal = process.env.PI_TODO;
|
|
48
|
+
if (envVal === "off") return undefined;
|
|
49
|
+
if (envVal?.startsWith("/")) return envVal;
|
|
50
|
+
if (envVal?.startsWith(".")) return resolve(envVal);
|
|
51
|
+
if (envVal) return join(process.env.HOME ?? "~", ".pi", "tasks", `${envVal}.json`);
|
|
52
|
+
|
|
53
|
+
if (taskScope === "memory") return undefined;
|
|
54
|
+
if (taskScope === "session" && sessionId) {
|
|
55
|
+
return join(cwd, ".pi", "tasks", `tasks-${sessionId}.json`);
|
|
56
|
+
}
|
|
57
|
+
if (taskScope === "session") return undefined;
|
|
58
|
+
return join(cwd, ".pi", "tasks", "tasks.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let store = new FileTaskStore(resolveStorePath());
|
|
62
|
+
const tracker = new ProcessTracker();
|
|
63
|
+
const widget = new TodoWidget(store);
|
|
64
|
+
const autoClear = new AutoClearManager(
|
|
65
|
+
() => store,
|
|
66
|
+
() => cfg.autoClearCompleted ?? "on_list_complete",
|
|
67
|
+
AUTO_CLEAR_DELAY,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let storeUpgraded = false;
|
|
71
|
+
let persistedTasksShown = false;
|
|
72
|
+
|
|
73
|
+
function upgradeStoreIfNeeded(sessionId?: string) {
|
|
74
|
+
if (storeUpgraded) return;
|
|
75
|
+
if (taskScope === "session" && !process.env.PI_TODO) {
|
|
76
|
+
const path = resolveStorePath(sessionId);
|
|
77
|
+
if (path) {
|
|
78
|
+
store = new FileTaskStore(path);
|
|
79
|
+
widget.setStore(store);
|
|
80
|
+
autoClear.getStore = () => store;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
storeUpgraded = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showPersistedTasks(isResume = false) {
|
|
87
|
+
if (persistedTasksShown) return;
|
|
88
|
+
persistedTasksShown = true;
|
|
89
|
+
const tasks = store.list();
|
|
90
|
+
if (tasks.length > 0) {
|
|
91
|
+
if (!isResume && tasks.every((t) => t.status === "completed")) {
|
|
92
|
+
store.clearCompleted();
|
|
93
|
+
if (taskScope === "session") store.deleteFileIfEmpty();
|
|
94
|
+
} else {
|
|
95
|
+
widget.update();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Turn tracking ──────────────────────────────────────────────────────────
|
|
101
|
+
let currentTurn = 0;
|
|
102
|
+
let lastTaskToolUseTurn = 0;
|
|
103
|
+
let reminderInjectedThisCycle = false;
|
|
104
|
+
|
|
105
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
106
|
+
currentTurn++;
|
|
107
|
+
cwd = ctx.cwd;
|
|
108
|
+
cfg = loadTodoConfig(cwd);
|
|
109
|
+
widget.setUICtx(ctx.ui);
|
|
110
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
111
|
+
if (autoClear.onTurnStart(currentTurn)) widget.update();
|
|
36
112
|
});
|
|
37
113
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
114
|
+
// ── System-reminder injection ──────────────────────────────────────────────
|
|
115
|
+
pi.on("tool_result", async (event) => {
|
|
116
|
+
if (TASK_TOOL_NAMES.has(event.toolName)) {
|
|
117
|
+
lastTaskToolUseTurn = currentTurn;
|
|
118
|
+
reminderInjectedThisCycle = false;
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
if (currentTurn - lastTaskToolUseTurn < REMINDER_INTERVAL) return {};
|
|
122
|
+
if (reminderInjectedThisCycle) return {};
|
|
123
|
+
if (store.list().length === 0) return {};
|
|
124
|
+
reminderInjectedThisCycle = true;
|
|
125
|
+
lastTaskToolUseTurn = currentTurn;
|
|
126
|
+
return {
|
|
127
|
+
content: [...event.content, { type: "text" as const, text: SYSTEM_REMINDER }],
|
|
128
|
+
};
|
|
41
129
|
});
|
|
42
130
|
|
|
43
|
-
// ── System-prompt injection
|
|
44
|
-
pi.on("before_agent_start", async (event,
|
|
45
|
-
|
|
131
|
+
// ── System-prompt injection ────────────────────────────────────────────────
|
|
132
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
133
|
+
cwd = ctx.cwd;
|
|
134
|
+
cfg = loadTodoConfig(cwd);
|
|
135
|
+
widget.setUICtx(ctx.ui);
|
|
136
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
137
|
+
showPersistedTasks();
|
|
138
|
+
const block = buildSystemPromptBlock(store);
|
|
46
139
|
if (!block) return;
|
|
47
140
|
return { systemPrompt: `${event.systemPrompt}\n\n${block}` };
|
|
48
141
|
});
|
|
49
142
|
|
|
50
|
-
// ── Post-turn widget refresh ───────────────────────────────────────────
|
|
51
143
|
pi.on("agent_end", async (_event, ctx) => {
|
|
52
|
-
|
|
144
|
+
widget.setUICtx(ctx.ui);
|
|
145
|
+
widget.update();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── Session lifecycle ──────────────────────────────────────────────────────
|
|
149
|
+
pi.on("session_start", async (event, ctx) => {
|
|
150
|
+
const isResume = event.reason === "resume";
|
|
151
|
+
cwd = ctx.cwd;
|
|
152
|
+
cfg = loadTodoConfig(cwd);
|
|
153
|
+
storeUpgraded = false;
|
|
154
|
+
persistedTasksShown = false;
|
|
155
|
+
currentTurn = 0;
|
|
156
|
+
lastTaskToolUseTurn = 0;
|
|
157
|
+
reminderInjectedThisCycle = false;
|
|
158
|
+
autoClear.reset();
|
|
159
|
+
if (!isResume && taskScope === "memory") store.clearAll();
|
|
160
|
+
upgradeStoreIfNeeded(ctx.sessionManager.getSessionId());
|
|
161
|
+
widget.setUICtx(ctx.ui);
|
|
162
|
+
showPersistedTasks(isResume);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
166
|
+
widget.setUICtx(ctx.ui);
|
|
167
|
+
widget.update();
|
|
53
168
|
});
|
|
54
169
|
|
|
55
|
-
// ── Tool:
|
|
170
|
+
// ── Tool: TaskCreate ───────────────────────────────────────────────────────
|
|
56
171
|
pi.registerTool({
|
|
57
|
-
name: "
|
|
58
|
-
label: "
|
|
59
|
-
description:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
172
|
+
name: "TaskCreate",
|
|
173
|
+
label: "TaskCreate",
|
|
174
|
+
description: `Use this tool to create a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
175
|
+
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
176
|
+
|
|
177
|
+
## When to Use This Tool
|
|
178
|
+
|
|
179
|
+
Use this tool proactively in these scenarios:
|
|
180
|
+
|
|
181
|
+
- Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
|
182
|
+
- Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
|
183
|
+
- User explicitly requests todo list - When the user directly asks you to use the todo list
|
|
184
|
+
- User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
|
185
|
+
- After receiving new instructions - Immediately capture user requirements as tasks
|
|
186
|
+
- When you start working on a task - Mark it as in_progress BEFORE beginning work
|
|
187
|
+
- After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
|
|
188
|
+
|
|
189
|
+
## When NOT to Use This Tool
|
|
190
|
+
|
|
191
|
+
Skip using this tool when:
|
|
192
|
+
- There is only a single, straightforward task
|
|
193
|
+
- The task is trivial and tracking it provides no organizational benefit
|
|
194
|
+
- The task can be completed in less than 3 trivial steps
|
|
195
|
+
- The task is purely conversational or informational
|
|
196
|
+
|
|
197
|
+
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
|
198
|
+
|
|
199
|
+
## Task Fields
|
|
200
|
+
|
|
201
|
+
- **content**: A brief, actionable title in imperative form (e.g., "Fix authentication bug in login flow")
|
|
202
|
+
- **description**: Detailed description of what needs to be done, including context and acceptance criteria
|
|
203
|
+
- **activeForm** (optional): Present continuous form shown in the spinner when in_progress (e.g., "Fixing authentication bug"). If omitted, the spinner shows the content instead.
|
|
204
|
+
- **priority**: high / medium / low
|
|
205
|
+
|
|
206
|
+
All tasks are created with status \`pending\`.
|
|
207
|
+
|
|
208
|
+
## Tips
|
|
209
|
+
|
|
210
|
+
- Create tasks with clear, specific content that describes the outcome
|
|
211
|
+
- Include enough detail in the description for another agent to understand and complete the task
|
|
212
|
+
- After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed
|
|
213
|
+
- Check TaskList first to avoid creating duplicate tasks`,
|
|
64
214
|
promptGuidelines: [
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
"Only one task should be 'in_progress' at a time unless tasks are genuinely parallel.",
|
|
69
|
-
"Immediately after completing a task, call todo_write to mark it 'completed'. " +
|
|
70
|
-
"Do not batch completions — mark each task done as soon as it is finished. " +
|
|
71
|
-
"Completed tasks remain visible in the list; they are never automatically deleted.",
|
|
72
|
-
"todo_write REPLACES the entire list. Always include ALL tasks (both changed and unchanged) in every call.",
|
|
73
|
-
"To permanently remove tasks, use todo_remove instead of omitting them from todo_write.",
|
|
215
|
+
"When working on complex multi-step tasks, use TaskCreate to track progress and TaskUpdate to update status.",
|
|
216
|
+
"Mark tasks as in_progress before starting work and completed when done.",
|
|
217
|
+
"Use TaskList to check for available work after completing a task.",
|
|
74
218
|
],
|
|
75
|
-
parameters:
|
|
219
|
+
parameters: TodoCreateParams,
|
|
76
220
|
|
|
77
221
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
store.applyStatusTransitions(updated);
|
|
88
|
-
store.setTasks(updated);
|
|
89
|
-
store.syncIdCounter();
|
|
90
|
-
updateWidget(ctx);
|
|
91
|
-
|
|
222
|
+
autoClear.resetBatchCountdown();
|
|
223
|
+
const task = store.create(params.content, {
|
|
224
|
+
description: params.description,
|
|
225
|
+
priority: params.priority as TaskPriority | undefined,
|
|
226
|
+
activeForm: params.activeForm,
|
|
227
|
+
metadata: params.metadata,
|
|
228
|
+
});
|
|
229
|
+
widget.setUICtx(ctx.ui);
|
|
230
|
+
widget.update();
|
|
92
231
|
return {
|
|
93
|
-
content: [{ type: "text", text:
|
|
94
|
-
details: { tasks: [...store.
|
|
232
|
+
content: [{ type: "text", text: `Task #${task.id} created successfully: ${task.content}` }],
|
|
233
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
95
234
|
};
|
|
96
235
|
},
|
|
97
236
|
|
|
98
237
|
renderCall(args, theme) {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return new Text(text, 0, 0);
|
|
238
|
+
const content = (args.content as string) ?? "";
|
|
239
|
+
const priority = (args.priority as string) ?? "medium";
|
|
240
|
+
const pColor = priorityColor(priority as TaskPriority);
|
|
241
|
+
const pLabel = theme.fg(pColor, priorityLabel(priority as TaskPriority));
|
|
242
|
+
return new Text(
|
|
243
|
+
`${theme.fg("toolTitle", theme.bold("TaskCreate ")) + pLabel} ${theme.fg("muted", content)}`,
|
|
244
|
+
0,
|
|
245
|
+
0,
|
|
246
|
+
);
|
|
109
247
|
},
|
|
110
248
|
|
|
111
249
|
renderResult(result, { expanded }, theme) {
|
|
@@ -113,23 +251,65 @@ export default function todoExtension(pi: ExtensionAPI): void {
|
|
|
113
251
|
},
|
|
114
252
|
});
|
|
115
253
|
|
|
116
|
-
// ── Tool:
|
|
254
|
+
// ── Tool: TaskList ─────────────────────────────────────────────────────────
|
|
117
255
|
pi.registerTool({
|
|
118
|
-
name: "
|
|
119
|
-
label: "
|
|
120
|
-
description:
|
|
121
|
-
|
|
256
|
+
name: "TaskList",
|
|
257
|
+
label: "TaskList",
|
|
258
|
+
description: `Use this tool to list all tasks in the task list.
|
|
259
|
+
|
|
260
|
+
## When to Use This Tool
|
|
261
|
+
|
|
262
|
+
- To see what tasks are available to work on (status: pending, not blocked)
|
|
263
|
+
- To check overall progress on the project
|
|
264
|
+
- To find tasks that are blocked and need dependencies resolved
|
|
265
|
+
- After completing a task, to check for newly unblocked work
|
|
266
|
+
- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available
|
|
267
|
+
|
|
268
|
+
## Output
|
|
269
|
+
|
|
270
|
+
Returns a summary of each task:
|
|
271
|
+
- **id**: Task identifier (use with TaskGet, TaskUpdate)
|
|
272
|
+
- **content**: Brief description of the task
|
|
273
|
+
- **status**: pending, in_progress, or completed
|
|
274
|
+
- **priority**: high, medium, or low
|
|
275
|
+
- **blockedBy**: Open task IDs that must be resolved first
|
|
276
|
+
|
|
277
|
+
Use TaskGet with a specific task ID to view full details including description.`,
|
|
122
278
|
parameters: Type.Object({}),
|
|
123
279
|
|
|
124
280
|
async execute() {
|
|
281
|
+
const tasks = store.list();
|
|
282
|
+
if (tasks.length === 0)
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: "No tasks found" }],
|
|
285
|
+
details: { tasks: [] } satisfies TaskDetails,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const statusOrder: Record<string, number> = { pending: 0, in_progress: 1, completed: 2 };
|
|
289
|
+
const sorted = [...tasks].sort((a, b) => {
|
|
290
|
+
const so = (statusOrder[a.status] ?? 0) - (statusOrder[b.status] ?? 0);
|
|
291
|
+
if (so !== 0) return so;
|
|
292
|
+
return a.id.localeCompare(b.id);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const lines = sorted.map((task) => {
|
|
296
|
+
let line = `[${task.status}] [${task.priority}] #${task.id} ${task.content}`;
|
|
297
|
+
const openBlockers = task.blockedBy.filter((bid) => {
|
|
298
|
+
const b = store.get(bid);
|
|
299
|
+
return b && b.status !== "completed";
|
|
300
|
+
});
|
|
301
|
+
if (openBlockers.length > 0) line += ` [blocked by ${openBlockers.map((id) => `#${id}`).join(", ")}]`;
|
|
302
|
+
return line;
|
|
303
|
+
});
|
|
304
|
+
|
|
125
305
|
return {
|
|
126
|
-
content: [{ type: "text", text:
|
|
127
|
-
details: { tasks:
|
|
306
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
307
|
+
details: { tasks: sorted } satisfies TaskDetails,
|
|
128
308
|
};
|
|
129
309
|
},
|
|
130
310
|
|
|
131
311
|
renderCall(_args, theme) {
|
|
132
|
-
return new Text(theme.fg("toolTitle", theme.bold("
|
|
312
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskList")), 0, 0);
|
|
133
313
|
},
|
|
134
314
|
|
|
135
315
|
renderResult(result, { expanded }, theme) {
|
|
@@ -137,67 +317,333 @@ export default function todoExtension(pi: ExtensionAPI): void {
|
|
|
137
317
|
},
|
|
138
318
|
});
|
|
139
319
|
|
|
140
|
-
// ── Tool:
|
|
320
|
+
// ── Tool: TaskGet ──────────────────────────────────────────────────────────
|
|
141
321
|
pi.registerTool({
|
|
142
|
-
name: "
|
|
143
|
-
label: "
|
|
144
|
-
description:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
322
|
+
name: "TaskGet",
|
|
323
|
+
label: "TaskGet",
|
|
324
|
+
description: `Use this tool to retrieve a task by its ID from the task list.
|
|
325
|
+
|
|
326
|
+
## When to Use This Tool
|
|
327
|
+
|
|
328
|
+
- When you need the full description and context before starting work on a task
|
|
329
|
+
- To understand task dependencies (what it blocks, what blocks it)
|
|
330
|
+
- After being assigned a task, to get complete requirements
|
|
331
|
+
|
|
332
|
+
## Output
|
|
333
|
+
|
|
334
|
+
Returns full task details:
|
|
335
|
+
- **content**: Task title
|
|
336
|
+
- **description**: Detailed requirements and context
|
|
337
|
+
- **status**: pending, in_progress, or completed
|
|
338
|
+
- **priority**: high, medium, or low
|
|
339
|
+
- **blocks**: Tasks waiting on this one to complete
|
|
340
|
+
- **blockedBy**: Tasks that must complete before this one can start
|
|
341
|
+
|
|
342
|
+
## Tips
|
|
343
|
+
|
|
344
|
+
- After fetching a task, verify its blockedBy list is empty before beginning work.
|
|
345
|
+
- Use TaskList to see all tasks in summary form.`,
|
|
346
|
+
parameters: TodoGetParams,
|
|
347
|
+
|
|
348
|
+
async execute(_id, params) {
|
|
349
|
+
const task = store.get(params.id);
|
|
350
|
+
if (!task) {
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: "text", text: `Task not found` }],
|
|
353
|
+
details: undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const desc = task.description?.replace(/\\n/g, "\n") ?? "(no description)";
|
|
358
|
+
const lines: string[] = [
|
|
359
|
+
`Task #${task.id}: ${task.content}`,
|
|
360
|
+
`Status: ${task.status}`,
|
|
361
|
+
`Priority: ${task.priority}`,
|
|
362
|
+
];
|
|
363
|
+
if (task.owner) lines.push(`Owner: ${task.owner}`);
|
|
364
|
+
lines.push(`Description: ${desc}`);
|
|
365
|
+
|
|
366
|
+
const openBlockers = task.blockedBy.filter((bid) => {
|
|
367
|
+
const b = store.get(bid);
|
|
368
|
+
return b && b.status !== "completed";
|
|
369
|
+
});
|
|
370
|
+
if (openBlockers.length > 0) lines.push(`Blocked by: ${openBlockers.map((id) => `#${id}`).join(", ")}`);
|
|
371
|
+
if (task.blocks.length > 0) lines.push(`Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
|
|
372
|
+
const metaKeys = Object.keys(task.metadata);
|
|
373
|
+
if (metaKeys.length > 0) lines.push(`Metadata: ${JSON.stringify(task.metadata)}`);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
377
|
+
details: { tasks: [task] } satisfies TaskDetails,
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
renderCall(args, theme) {
|
|
382
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskGet ")) + theme.fg("muted", `#${args.id}`), 0, 0);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
renderResult(result, { expanded }, theme) {
|
|
386
|
+
return renderTaskListResult((result.details as TaskDetails | undefined)?.tasks ?? [], expanded, theme);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ── Tool: TaskUpdate ───────────────────────────────────────────────────────
|
|
391
|
+
pi.registerTool({
|
|
392
|
+
name: "TaskUpdate",
|
|
393
|
+
label: "TaskUpdate",
|
|
394
|
+
description: `Use this tool to update a task in the task list.
|
|
395
|
+
|
|
396
|
+
## When to Use This Tool
|
|
397
|
+
|
|
398
|
+
**Before starting work on a task:**
|
|
399
|
+
- Mark it in_progress BEFORE beginning — do not start work without updating status first
|
|
400
|
+
|
|
401
|
+
**Mark tasks as completed:**
|
|
402
|
+
- When you have completed the work described in a task
|
|
403
|
+
- IMPORTANT: Always mark your tasks as completed when you finish them
|
|
404
|
+
- After completing, call TaskList to find your next task
|
|
405
|
+
|
|
406
|
+
- ONLY mark a task as completed when you have FULLY accomplished it
|
|
407
|
+
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
|
|
408
|
+
- When blocked, create a new task describing what needs to be resolved
|
|
409
|
+
- Never mark a task as completed if:
|
|
410
|
+
- Tests are failing
|
|
411
|
+
- Implementation is partial
|
|
412
|
+
- You encountered unresolved errors
|
|
413
|
+
- You couldn't find necessary files or dependencies
|
|
414
|
+
|
|
415
|
+
**Delete tasks:**
|
|
416
|
+
- When a task is no longer relevant or was created in error
|
|
417
|
+
- Setting status to \`deleted\` permanently removes the task
|
|
418
|
+
|
|
419
|
+
**Update task details:**
|
|
420
|
+
- When requirements change or become clearer
|
|
421
|
+
- When establishing dependencies between tasks
|
|
422
|
+
|
|
423
|
+
## Fields You Can Update
|
|
424
|
+
|
|
425
|
+
- **status**: pending → in_progress → completed (or deleted to remove)
|
|
426
|
+
- **content**: Change the task title
|
|
427
|
+
- **description**: Change the task description
|
|
428
|
+
- **activeForm**: Spinner text when in_progress (e.g., "Running tests")
|
|
429
|
+
- **priority**: high, medium, or low
|
|
430
|
+
- **owner**: Agent name or owner
|
|
431
|
+
- **metadata**: Merge metadata keys (set a key to null to delete it)
|
|
432
|
+
- **addBlocks**: Task IDs that cannot start until this one completes
|
|
433
|
+
- **addBlockedBy**: Task IDs that must complete before this one can start
|
|
434
|
+
|
|
435
|
+
## Status Workflow
|
|
436
|
+
|
|
437
|
+
\`pending\` → \`in_progress\` → \`completed\`
|
|
438
|
+
|
|
439
|
+
Use \`deleted\` to permanently remove a task.
|
|
440
|
+
|
|
441
|
+
## Examples
|
|
442
|
+
|
|
443
|
+
Mark as in progress:
|
|
444
|
+
\`{ "id": "t1", "status": "in_progress" }\`
|
|
445
|
+
|
|
446
|
+
Mark as completed:
|
|
447
|
+
\`{ "id": "t1", "status": "completed" }\`
|
|
448
|
+
|
|
449
|
+
Delete a task:
|
|
450
|
+
\`{ "id": "t1", "status": "deleted" }\`
|
|
451
|
+
|
|
452
|
+
Set dependencies:
|
|
453
|
+
\`{ "id": "t2", "addBlockedBy": ["t1"] }\``,
|
|
148
454
|
promptGuidelines: [
|
|
149
|
-
"
|
|
150
|
-
|
|
455
|
+
"Mark tasks in_progress BEFORE starting work, completed immediately after finishing. Never batch completions.",
|
|
456
|
+
"ONLY mark completed when fully done — not when tests are failing or implementation is partial.",
|
|
151
457
|
],
|
|
152
|
-
parameters:
|
|
458
|
+
parameters: TodoUpdateParams,
|
|
153
459
|
|
|
154
460
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
155
|
-
const
|
|
156
|
-
|
|
461
|
+
const { id, ...fields } = params;
|
|
462
|
+
const { task, changedFields, warnings } = store.update(id, fields as any);
|
|
463
|
+
|
|
464
|
+
if (changedFields.length === 0 && !task) {
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: `Task #${id} not found` }],
|
|
467
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (fields.status === "in_progress") {
|
|
472
|
+
widget.setActiveTask(id);
|
|
473
|
+
autoClear.resetBatchCountdown();
|
|
474
|
+
} else if (fields.status === "pending") {
|
|
475
|
+
autoClear.resetBatchCountdown();
|
|
476
|
+
} else if (fields.status === "completed") {
|
|
477
|
+
widget.setActiveTask(id, false);
|
|
478
|
+
autoClear.trackCompletion(id, currentTurn);
|
|
479
|
+
} else if (fields.status === "deleted") {
|
|
480
|
+
widget.setActiveTask(id, false);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
widget.setUICtx(ctx.ui);
|
|
484
|
+
widget.update();
|
|
485
|
+
|
|
486
|
+
let msg: string;
|
|
487
|
+
if (changedFields.includes("deleted")) {
|
|
488
|
+
msg = `→ Updated task #${id} deleted`;
|
|
489
|
+
} else {
|
|
490
|
+
msg = `→ Updated task #${id} ${changedFields.join(", ")}`;
|
|
491
|
+
}
|
|
492
|
+
if (warnings.length > 0) msg += ` (warning: ${warnings.join("; ")})`;
|
|
157
493
|
|
|
158
|
-
|
|
494
|
+
return {
|
|
495
|
+
content: [{ type: "text", text: msg }],
|
|
496
|
+
details: { tasks: [...store.list()] } satisfies TaskDetails,
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
renderCall(args, theme) {
|
|
501
|
+
const id = args.id as string;
|
|
502
|
+
const status = args.status as string | undefined;
|
|
503
|
+
let extra = "";
|
|
504
|
+
if (status) extra = ` ${theme.fg("muted", `→ ${status}`)}`;
|
|
505
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskUpdate ")) + theme.fg("accent", `#${id}`) + extra, 0, 0);
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
renderResult(result, { expanded }, theme) {
|
|
509
|
+
return TodoViewComponent.renderTaskResult(result.details as TaskDetails | undefined, expanded, theme);
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ── Tool: TaskOutput ────────────────────────────────────────────
|
|
514
|
+
pi.registerTool({
|
|
515
|
+
name: "TaskOutput",
|
|
516
|
+
label: "TaskOutput",
|
|
517
|
+
description:
|
|
518
|
+
"Retrieves output from a running or completed background task process.\n" +
|
|
519
|
+
"- Takes a task_id parameter identifying the task\n" +
|
|
520
|
+
"- Returns the task output along with status information\n" +
|
|
521
|
+
"- Use block=true (default) to wait for task completion\n" +
|
|
522
|
+
"- Use block=false for a non-blocking check of current status\n" +
|
|
523
|
+
"- Task IDs can be found using TaskList",
|
|
524
|
+
parameters: Type.Object({
|
|
525
|
+
task_id: Type.String({ description: "The task ID to get output from" }),
|
|
526
|
+
block: Type.Optional(Type.Boolean({ description: "Whether to wait for completion (default: true)" })),
|
|
527
|
+
timeout: Type.Optional(
|
|
528
|
+
Type.Number({
|
|
529
|
+
description: "Max wait time in ms (default: 30000, max: 600000)",
|
|
530
|
+
minimum: 0,
|
|
531
|
+
maximum: 600000,
|
|
532
|
+
}),
|
|
533
|
+
),
|
|
534
|
+
}),
|
|
535
|
+
|
|
536
|
+
async execute(_id, params, signal) {
|
|
537
|
+
const { task_id, block = true, timeout = 30000 } = params;
|
|
538
|
+
const processOutput = tracker.getOutput(task_id);
|
|
539
|
+
|
|
540
|
+
if (!processOutput) {
|
|
541
|
+
const task = store.get(task_id);
|
|
542
|
+
if (!task) {
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text", text: `No task or process found with ID ${task_id}` }],
|
|
545
|
+
details: undefined,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
159
548
|
return {
|
|
160
|
-
content: [{ type: "text", text:
|
|
161
|
-
details:
|
|
549
|
+
content: [{ type: "text", text: `Task #${task_id} [${task.status}] — no background process attached` }],
|
|
550
|
+
details: undefined,
|
|
162
551
|
};
|
|
163
552
|
}
|
|
164
553
|
|
|
554
|
+
if (block && processOutput.status === "running") {
|
|
555
|
+
const result = await tracker.waitForCompletion(task_id, timeout, signal ?? undefined);
|
|
556
|
+
if (result) {
|
|
557
|
+
return {
|
|
558
|
+
content: [
|
|
559
|
+
{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: `Task #${task_id} (${result.status})${result.exitCode !== undefined ? ` exit ${result.exitCode}` : ""}\n\n${result.output}`,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
details: undefined,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
165
569
|
return {
|
|
166
570
|
content: [
|
|
167
571
|
{
|
|
168
572
|
type: "text",
|
|
169
|
-
text: `
|
|
573
|
+
text: `Task #${task_id} (${processOutput.status})${processOutput.exitCode !== undefined ? ` exit ${processOutput.exitCode}` : ""}\n\n${processOutput.output}`,
|
|
170
574
|
},
|
|
171
575
|
],
|
|
172
|
-
details:
|
|
576
|
+
details: undefined,
|
|
173
577
|
};
|
|
174
578
|
},
|
|
175
579
|
|
|
176
580
|
renderCall(args, theme) {
|
|
177
|
-
const ids = (args.ids as string[]) ?? [];
|
|
178
|
-
const idStr = ids.map((id: string) => theme.fg("accent", id)).join(", ");
|
|
179
581
|
return new Text(
|
|
180
|
-
theme.fg("toolTitle", theme.bold("
|
|
582
|
+
theme.fg("toolTitle", theme.bold("TaskOutput ")) + theme.fg("muted", `#${args.task_id}`),
|
|
181
583
|
0,
|
|
182
584
|
0,
|
|
183
585
|
);
|
|
184
586
|
},
|
|
587
|
+
});
|
|
185
588
|
|
|
186
|
-
|
|
187
|
-
|
|
589
|
+
// ── Tool: TaskStop ──────────────────────────────────────────────
|
|
590
|
+
pi.registerTool({
|
|
591
|
+
name: "TaskStop",
|
|
592
|
+
label: "TaskStop",
|
|
593
|
+
description:
|
|
594
|
+
"Stops a running background task process.\n" +
|
|
595
|
+
"- Sends SIGTERM, waits 5 seconds, then SIGKILL if still running\n" +
|
|
596
|
+
"- Marks the task as completed after stopping\n" +
|
|
597
|
+
"- Use this tool when you need to terminate a long-running task",
|
|
598
|
+
parameters: Type.Object({
|
|
599
|
+
task_id: Type.String({ description: "The task ID of the background process to stop" }),
|
|
600
|
+
}),
|
|
601
|
+
|
|
602
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
603
|
+
const { task_id } = params;
|
|
604
|
+
const stopped = await tracker.stop(task_id);
|
|
605
|
+
|
|
606
|
+
if (!stopped) {
|
|
607
|
+
const task = store.get(task_id);
|
|
608
|
+
if (!task)
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: `No running background process for task ${task_id}` }],
|
|
611
|
+
details: undefined,
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
content: [
|
|
615
|
+
{ type: "text", text: `Task #${task_id} has no running background process (status: ${task.status})` },
|
|
616
|
+
],
|
|
617
|
+
details: undefined,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
store.update(task_id, { status: "completed" });
|
|
622
|
+
autoClear.trackCompletion(task_id, currentTurn);
|
|
623
|
+
widget.setActiveTask(task_id, false);
|
|
624
|
+
widget.setUICtx(ctx.ui);
|
|
625
|
+
widget.update();
|
|
626
|
+
|
|
627
|
+
return { content: [{ type: "text", text: `Task #${task_id} stopped successfully` }], details: undefined };
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
renderCall(args, theme) {
|
|
631
|
+
return new Text(theme.fg("toolTitle", theme.bold("TaskStop ")) + theme.fg("accent", `#${args.task_id}`), 0, 0);
|
|
188
632
|
},
|
|
189
633
|
});
|
|
190
634
|
|
|
191
|
-
// ── Command: /todos
|
|
635
|
+
// ── Command: /todos ────────────────────────────────────────────────────────
|
|
192
636
|
pi.registerCommand("todos", {
|
|
193
|
-
description: "Open the interactive task viewer",
|
|
637
|
+
description: "Open the interactive task viewer and manager",
|
|
194
638
|
handler: async (_args, ctx) => {
|
|
195
639
|
if (!ctx.hasUI) {
|
|
196
|
-
ctx.ui.notify(store.
|
|
640
|
+
ctx.ui.notify(store.list().length === 0 ? "No tasks yet." : formatListForLLM(store), "info");
|
|
197
641
|
return;
|
|
198
642
|
}
|
|
199
|
-
await ctx.ui
|
|
200
|
-
|
|
643
|
+
await openTodosMenu(ctx.ui, store, cfg, cwd, (taskId, status) => {
|
|
644
|
+
if (status === "in_progress") widget.setActiveTask(taskId);
|
|
645
|
+
else if (status) widget.setActiveTask(taskId, false);
|
|
646
|
+
widget.update();
|
|
201
647
|
});
|
|
202
648
|
},
|
|
203
649
|
});
|