@tianhai/pi-workflow-kit 0.4.1

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.
Files changed (54) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +509 -0
  3. package/ROADMAP.md +16 -0
  4. package/agents/code-reviewer.md +18 -0
  5. package/agents/config.ts +5 -0
  6. package/agents/implementer.md +26 -0
  7. package/agents/spec-reviewer.md +13 -0
  8. package/agents/worker.md +17 -0
  9. package/banner.jpg +0 -0
  10. package/docs/developer-usage-guide.md +463 -0
  11. package/docs/oversight-model.md +49 -0
  12. package/docs/workflow-phases.md +71 -0
  13. package/extensions/constants.ts +9 -0
  14. package/extensions/lib/logging.ts +138 -0
  15. package/extensions/plan-tracker.ts +496 -0
  16. package/extensions/subagent/agents.ts +144 -0
  17. package/extensions/subagent/concurrency.ts +52 -0
  18. package/extensions/subagent/env.ts +47 -0
  19. package/extensions/subagent/index.ts +1116 -0
  20. package/extensions/subagent/lifecycle.ts +25 -0
  21. package/extensions/subagent/timeout.ts +13 -0
  22. package/extensions/workflow-monitor/debug-monitor.ts +98 -0
  23. package/extensions/workflow-monitor/git.ts +31 -0
  24. package/extensions/workflow-monitor/heuristics.ts +58 -0
  25. package/extensions/workflow-monitor/investigation.ts +52 -0
  26. package/extensions/workflow-monitor/reference-tool.ts +42 -0
  27. package/extensions/workflow-monitor/skip-confirmation.ts +19 -0
  28. package/extensions/workflow-monitor/tdd-monitor.ts +137 -0
  29. package/extensions/workflow-monitor/test-runner.ts +37 -0
  30. package/extensions/workflow-monitor/verification-monitor.ts +61 -0
  31. package/extensions/workflow-monitor/warnings.ts +81 -0
  32. package/extensions/workflow-monitor/workflow-handler.ts +358 -0
  33. package/extensions/workflow-monitor/workflow-tracker.ts +231 -0
  34. package/extensions/workflow-monitor/workflow-transitions.ts +55 -0
  35. package/extensions/workflow-monitor.ts +885 -0
  36. package/package.json +49 -0
  37. package/skills/brainstorming/SKILL.md +70 -0
  38. package/skills/dispatching-parallel-agents/SKILL.md +194 -0
  39. package/skills/executing-tasks/SKILL.md +247 -0
  40. package/skills/receiving-code-review/SKILL.md +196 -0
  41. package/skills/systematic-debugging/SKILL.md +170 -0
  42. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  43. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  44. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  45. package/skills/systematic-debugging/find-polluter.sh +63 -0
  46. package/skills/systematic-debugging/reference/rationalizations.md +61 -0
  47. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  48. package/skills/test-driven-development/SKILL.md +266 -0
  49. package/skills/test-driven-development/reference/examples.md +101 -0
  50. package/skills/test-driven-development/reference/rationalizations.md +67 -0
  51. package/skills/test-driven-development/reference/when-stuck.md +33 -0
  52. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  53. package/skills/using-git-worktrees/SKILL.md +231 -0
  54. package/skills/writing-plans/SKILL.md +149 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * File-based logger for pi-workflow-kit.
3
+ *
4
+ * Default singleton writes to ~/.pi/logs/workflow-kit.log.
5
+ * Info/warn/error always write. Debug writes when PI_WORKFLOW_KIT_DEBUG=1.
6
+ * The legacy PI_SUPERPOWERS_DEBUG env var is also honored for compatibility.
7
+ * One-deep rotation when file exceeds 5 MB.
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+
14
+ export interface LoggerOptions {
15
+ verbose?: boolean;
16
+ maxSizeBytes?: number;
17
+ rotationCheckInterval?: number;
18
+ }
19
+
20
+ export interface Logger {
21
+ info(message: string): void;
22
+ warn(message: string): void;
23
+ error(message: string, err?: unknown): void;
24
+ debug(message: string): void;
25
+ }
26
+
27
+ const DEFAULT_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
28
+ const DEFAULT_ROTATION_CHECK_INTERVAL = 60 * 60 * 1000; // 1 hour
29
+ export const MAX_MESSAGE_LENGTH = 10 * 1024; // 10 KB
30
+ const TRUNCATED_MARKER = "...(truncated)";
31
+
32
+ function formatError(err: unknown): string {
33
+ if (err instanceof Error) {
34
+ return err.stack ?? `${err.name}: ${err.message}`;
35
+ }
36
+ return String(err);
37
+ }
38
+
39
+ function timestamp(): string {
40
+ // Strip milliseconds + trailing Z so log lines stay compact and second-precision.
41
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "");
42
+ }
43
+
44
+ function truncateMessage(message: string): string {
45
+ if (message.length <= MAX_MESSAGE_LENGTH) return message;
46
+ return message.slice(0, MAX_MESSAGE_LENGTH - TRUNCATED_MARKER.length) + TRUNCATED_MARKER;
47
+ }
48
+
49
+ export function createLogger(logPath: string, options?: LoggerOptions): Logger {
50
+ const verbose = options?.verbose ?? false;
51
+ const maxSizeBytes = options?.maxSizeBytes ?? DEFAULT_MAX_SIZE;
52
+ const rotationCheckInterval = options?.rotationCheckInterval ?? DEFAULT_ROTATION_CHECK_INTERVAL;
53
+ /** Timestamp (ms) of the last rotation size check. Re-checks after rotationCheckInterval. */
54
+ let lastRotationCheck = -Infinity;
55
+ /** Set after the first error is reported to stderr, to avoid spamming. */
56
+ let stderrFallbackFired = false;
57
+
58
+ function ensureDir(): void {
59
+ const dir = path.dirname(logPath);
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Emit a one-time warning to stderr so the user knows logging is broken.
67
+ * Only fires once per logger instance to avoid spamming.
68
+ */
69
+ function stderrFallback(context: string, err: unknown): void {
70
+ if (stderrFallbackFired) return;
71
+ stderrFallbackFired = true;
72
+ const detail = err instanceof Error ? err.message : String(err);
73
+ process.stderr.write(
74
+ `[pi-workflow-kit] Logger ${context} failed: ${detail}. Further log errors will be silenced.\n`,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Rotate the log file if it exceeds maxSizeBytes.
80
+ * Re-checks at most once per rotationCheckInterval (default 1 hour)
81
+ * so long-running processes can still rotate without checking on every write.
82
+ */
83
+ function rotateIfNeeded(): void {
84
+ const now = Date.now();
85
+ if (now - lastRotationCheck < rotationCheckInterval) return;
86
+ lastRotationCheck = now;
87
+ try {
88
+ const stat = fs.statSync(logPath);
89
+ if (stat.size > maxSizeBytes) {
90
+ fs.renameSync(logPath, `${logPath}.1`);
91
+ }
92
+ } catch (err) {
93
+ // File doesn't exist yet — nothing to rotate. But surface unexpected errors once.
94
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
95
+ stderrFallback("rotation", err);
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Append a log line to the file. Uses synchronous I/O for simplicity and ordering guarantees — acceptable for low-volume diagnostic logging.
102
+ */
103
+ function write(level: string, message: string): void {
104
+ try {
105
+ ensureDir();
106
+ rotateIfNeeded();
107
+ const line = `${timestamp()} [${level}] ${truncateMessage(message)}\n`;
108
+ fs.appendFileSync(logPath, line, "utf-8");
109
+ } catch (err) {
110
+ // Logger must never crash the application, but surface the first failure.
111
+ stderrFallback("write", err);
112
+ }
113
+ }
114
+
115
+ return {
116
+ info(message: string): void {
117
+ write("INFO", message);
118
+ },
119
+ warn(message: string): void {
120
+ write("WARN", message);
121
+ },
122
+ error(message: string, err?: unknown): void {
123
+ const suffix = err ? ` — ${formatError(err)}` : "";
124
+ write("ERROR", message + suffix);
125
+ },
126
+ debug(message: string): void {
127
+ if (!verbose) return;
128
+ write("DEBUG", message);
129
+ },
130
+ };
131
+ }
132
+
133
+ /** Default singleton logger used across all extensions. */
134
+ const LOG_PATH = path.join(os.homedir(), ".pi", "logs", "workflow-kit.log");
135
+
136
+ export const log: Logger = createLogger(LOG_PATH, {
137
+ verbose: process.env.PI_WORKFLOW_KIT_DEBUG === "1" || process.env.PI_SUPERPOWERS_DEBUG === "1",
138
+ });
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Task Tracker Extension
3
+ *
4
+ * A native pi tool for tracking plan progress with per-task phase and attempt tracking.
5
+ * State is stored in tool result details for proper branching support.
6
+ * Shows a persistent TUI widget above the editor.
7
+ */
8
+
9
+ import { StringEnum } from "@mariozechner/pi-ai";
10
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
11
+ import { Text } from "@mariozechner/pi-tui";
12
+ import { type Static, Type } from "@sinclair/typebox";
13
+ import { PLAN_TRACKER_TOOL_NAME } from "./constants.js";
14
+
15
+ export type TaskStatus = "pending" | "in_progress" | "complete" | "blocked";
16
+ export type TaskPhase =
17
+ | "pending"
18
+ | "define"
19
+ | "approve"
20
+ | "execute"
21
+ | "verify"
22
+ | "review"
23
+ | "fix"
24
+ | "complete"
25
+ | "blocked";
26
+ export type TaskType = "code" | "non-code";
27
+
28
+ export interface PlanTrackerTask {
29
+ name: string;
30
+ status: TaskStatus;
31
+ phase: TaskPhase;
32
+ type: TaskType;
33
+ executeAttempts: number;
34
+ fixAttempts: number;
35
+ }
36
+
37
+ export interface PlanTrackerTaskInit {
38
+ name: string;
39
+ type?: TaskType;
40
+ }
41
+
42
+ export interface PlanTrackerDetails {
43
+ action: "init" | "update" | "status" | "clear";
44
+ tasks: PlanTrackerTask[];
45
+ error?: string;
46
+ }
47
+
48
+ const TASK_PHASES: readonly string[] = [
49
+ "pending",
50
+ "define",
51
+ "approve",
52
+ "execute",
53
+ "verify",
54
+ "review",
55
+ "fix",
56
+ "complete",
57
+ "blocked",
58
+ ] as const;
59
+
60
+ const TASK_STATUSES: readonly string[] = ["pending", "in_progress", "complete", "blocked"] as const;
61
+
62
+ const PlanTrackerParams = Type.Object({
63
+ action: StringEnum(["init", "update", "status", "clear"] as const, {
64
+ description: "Action to perform",
65
+ }),
66
+ tasks: Type.Optional(
67
+ Type.Array(
68
+ Type.Union([
69
+ Type.String(),
70
+ Type.Object({
71
+ name: Type.String({ description: "Task name" }),
72
+ type: Type.Optional(
73
+ StringEnum(["code", "non-code"] as const, {
74
+ description: "Task type",
75
+ }),
76
+ ),
77
+ }),
78
+ ]),
79
+ {
80
+ description: "Task names or typed task objects (for init)",
81
+ },
82
+ ),
83
+ ),
84
+ index: Type.Optional(
85
+ Type.Integer({
86
+ minimum: 0,
87
+ description: "Task index, 0-based (for update)",
88
+ }),
89
+ ),
90
+ status: Type.Optional(
91
+ StringEnum(TASK_STATUSES as unknown as readonly [string, ...string[]], {
92
+ description: "New status (for update)",
93
+ }),
94
+ ),
95
+ phase: Type.Optional(
96
+ StringEnum(TASK_PHASES as unknown as readonly [string, ...string[]], {
97
+ description: "New phase (for update)",
98
+ }),
99
+ ),
100
+ type: Type.Optional(
101
+ StringEnum(["code", "non-code"] as const, {
102
+ description: "Task type (for update)",
103
+ }),
104
+ ),
105
+ attempts: Type.Optional(
106
+ Type.Integer({
107
+ minimum: 0,
108
+ description: "Set attempt count for executeAttempts or fixAttempts depending on current phase (for update)",
109
+ }),
110
+ ),
111
+ });
112
+
113
+ export type PlanTrackerInput = Static<typeof PlanTrackerParams>;
114
+
115
+ function createDefaultTask(input: string | PlanTrackerTaskInit): PlanTrackerTask {
116
+ const task = typeof input === "string" ? { name: input } : input;
117
+ return {
118
+ name: task.name,
119
+ status: "pending",
120
+ phase: "pending",
121
+ type: task.type ?? "code",
122
+ executeAttempts: 0,
123
+ fixAttempts: 0,
124
+ };
125
+ }
126
+
127
+ function phaseIcon(status: TaskStatus, phase: TaskPhase): string {
128
+ if (status === "complete" || phase === "complete") return "✓";
129
+ if (status === "blocked" || phase === "blocked") return "⛔";
130
+ if (phase === "define") return "📝";
131
+ if (phase === "approve") return "👀";
132
+ if (phase === "execute") return "⚙";
133
+ if (phase === "verify") return "🔎";
134
+ if (phase === "review") return "🔍";
135
+ if (phase === "fix") return "🔧";
136
+ if (status === "in_progress") return "→";
137
+ return "○";
138
+ }
139
+
140
+ function formatWidget(tasks: PlanTrackerTask[], theme: Theme): string {
141
+ if (tasks.length === 0) return "";
142
+
143
+ const complete = tasks.filter((t) => t.status === "complete").length;
144
+ const blocked = tasks.filter((t) => t.status === "blocked").length;
145
+ const icons = tasks
146
+ .map((t) => {
147
+ switch (t.status) {
148
+ case "complete":
149
+ return theme.fg("success", "✓");
150
+ case "blocked":
151
+ return theme.fg("error", "⛔");
152
+ case "in_progress":
153
+ return theme.fg("warning", "→");
154
+ default:
155
+ return theme.fg("dim", "○");
156
+ }
157
+ })
158
+ .join("");
159
+
160
+ const summary = theme.fg("muted", `(${complete}/${tasks.length})`);
161
+ const blockedNote = blocked > 0 ? ` ${theme.fg("error", `${blocked} blocked`)}` : "";
162
+
163
+ // Show current task with phase
164
+ const current = tasks.find((t) => t.status === "in_progress") ?? tasks.find((t) => t.status === "pending");
165
+ const currentType = current?.type === "non-code" ? ` ${theme.fg("warning", "📋")}` : "";
166
+ const currentInfo =
167
+ current && current.status === "in_progress"
168
+ ? ` ${theme.fg("muted", current.name)}${currentType} — ${theme.fg("dim", current.phase)}${current.phase === "fix" || current.phase === "execute" ? ` (${current.phase === "fix" ? current.fixAttempts : current.executeAttempts}/3)` : ""}`
169
+ : current
170
+ ? ` ${theme.fg("muted", current.name)}${currentType}`
171
+ : "";
172
+
173
+ return `${theme.fg("muted", "Tasks:")} ${icons} ${summary}${blockedNote}${currentInfo}`;
174
+ }
175
+
176
+ function formatStatus(tasks: PlanTrackerTask[]): string {
177
+ if (tasks.length === 0) return "No plan active.";
178
+
179
+ const complete = tasks.filter((t) => t.status === "complete").length;
180
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
181
+ const pending = tasks.filter((t) => t.status === "pending").length;
182
+ const blocked = tasks.filter((t) => t.status === "blocked").length;
183
+
184
+ const lines: string[] = [];
185
+ lines.push(
186
+ `Plan: ${complete}/${tasks.length} complete (${inProgress} in progress, ${pending} pending${blocked > 0 ? `, ${blocked} blocked` : ""})`,
187
+ );
188
+ lines.push("");
189
+ for (let i = 0; i < tasks.length; i++) {
190
+ const t = tasks[i];
191
+ const icon = phaseIcon(t.status, t.phase);
192
+ const phaseStr = t.status === "in_progress" ? ` [${t.phase}]` : "";
193
+ const attemptsStr =
194
+ t.status === "in_progress" && (t.phase === "execute" || t.phase === "fix")
195
+ ? ` (${t.phase === "fix" ? t.fixAttempts : t.executeAttempts}/3)`
196
+ : "";
197
+ const typeStr = t.type === "non-code" ? " 📋" : "";
198
+ lines.push(` ${icon} [${i}] ${t.name}${typeStr}${phaseStr}${attemptsStr}`);
199
+ }
200
+ return lines.join("\n");
201
+ }
202
+
203
+ export default function (pi: ExtensionAPI) {
204
+ let tasks: PlanTrackerTask[] = [];
205
+
206
+ const reconstructState = (ctx: ExtensionContext) => {
207
+ tasks = [];
208
+ const entries = ctx.sessionManager.getBranch();
209
+ for (let i = entries.length - 1; i >= 0; i--) {
210
+ const entry = entries[i];
211
+ if (entry.type !== "message") continue;
212
+ const msg = entry.message;
213
+ if (msg.role !== "toolResult" || msg.toolName !== PLAN_TRACKER_TOOL_NAME) continue;
214
+ const details = msg.details as PlanTrackerDetails | undefined;
215
+ if (details && !details.error) {
216
+ tasks = details.tasks;
217
+ break;
218
+ }
219
+ }
220
+ };
221
+
222
+ const updateWidget = (ctx: ExtensionContext) => {
223
+ if (!ctx.hasUI) return;
224
+ if (tasks.length === 0) {
225
+ ctx.ui.setWidget(PLAN_TRACKER_TOOL_NAME, undefined);
226
+ } else {
227
+ ctx.ui.setWidget(PLAN_TRACKER_TOOL_NAME, (_tui, theme) => {
228
+ return new Text(formatWidget(tasks, theme), 0, 0);
229
+ });
230
+ }
231
+ };
232
+
233
+ // Reconstruct state + widget on session events
234
+ // session_start covers startup, reload, new, resume, fork (pi v0.65.0+)
235
+ pi.on("session_start", async (_event, ctx) => {
236
+ reconstructState(ctx);
237
+ updateWidget(ctx);
238
+ });
239
+
240
+ // session_tree for /tree navigation where a different session branch is loaded
241
+ pi.on("session_tree", async (_event, ctx) => {
242
+ reconstructState(ctx);
243
+ updateWidget(ctx);
244
+ });
245
+
246
+ pi.registerTool({
247
+ name: PLAN_TRACKER_TOOL_NAME,
248
+ label: "Task Tracker",
249
+ description:
250
+ "Track implementation plan progress with per-task phase and attempt tracking. Actions: init (set task list), update (change task status/phase/type/attempts), status (show current state), clear (remove plan).",
251
+ parameters: PlanTrackerParams,
252
+
253
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
254
+ switch (params.action) {
255
+ case "init": {
256
+ if (!params.tasks || params.tasks.length === 0) {
257
+ return {
258
+ content: [{ type: "text", text: "Error: tasks array required for init" }],
259
+ details: {
260
+ action: "init",
261
+ tasks: [...tasks],
262
+ error: "tasks required",
263
+ } as PlanTrackerDetails,
264
+ };
265
+ }
266
+ tasks = params.tasks.map((task) => createDefaultTask(task));
267
+ updateWidget(ctx);
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `Plan initialized with ${tasks.length} tasks.\n${formatStatus(tasks)}`,
273
+ },
274
+ ],
275
+ details: { action: "init", tasks: [...tasks] } as PlanTrackerDetails,
276
+ };
277
+ }
278
+
279
+ case "update": {
280
+ if (params.index === undefined) {
281
+ return {
282
+ content: [{ type: "text", text: "Error: index required for update" }],
283
+ details: {
284
+ action: "update",
285
+ tasks: [...tasks],
286
+ error: "index required",
287
+ } as PlanTrackerDetails,
288
+ };
289
+ }
290
+ if (tasks.length === 0) {
291
+ return {
292
+ content: [{ type: "text", text: "Error: no plan active. Use init first." }],
293
+ details: {
294
+ action: "update",
295
+ tasks: [],
296
+ error: "no plan active",
297
+ } as PlanTrackerDetails,
298
+ };
299
+ }
300
+ if (params.index < 0 || params.index >= tasks.length) {
301
+ return {
302
+ content: [
303
+ {
304
+ type: "text",
305
+ text: `Error: index ${params.index} out of range (0-${tasks.length - 1})`,
306
+ },
307
+ ],
308
+ details: {
309
+ action: "update",
310
+ tasks: [...tasks],
311
+ error: `index ${params.index} out of range`,
312
+ } as PlanTrackerDetails,
313
+ };
314
+ }
315
+
316
+ const task = tasks[params.index];
317
+ const updates: string[] = [];
318
+
319
+ // Compute target status and phase from explicit params first,
320
+ // then auto-sync only the fields that weren't explicitly set.
321
+ // This prevents one param from silently overriding the other.
322
+ const explicitStatus: TaskStatus | undefined = params.status;
323
+ const explicitPhase: TaskPhase | undefined = params.phase;
324
+
325
+ // Apply explicit status
326
+ if (explicitStatus) {
327
+ task.status = explicitStatus;
328
+ updates.push(`status → ${explicitStatus}`);
329
+ }
330
+
331
+ // Apply explicit phase
332
+ if (explicitPhase) {
333
+ task.phase = explicitPhase;
334
+ updates.push(`phase → ${explicitPhase}`);
335
+ }
336
+
337
+ // Auto-sync: derive phase from status (only if phase wasn't explicitly set)
338
+ if (explicitStatus && !explicitPhase) {
339
+ if (explicitStatus === "complete" || explicitStatus === "blocked") {
340
+ task.phase = explicitStatus;
341
+ } else if (explicitStatus === "in_progress" && task.phase === "pending") {
342
+ task.phase = "define";
343
+ }
344
+ }
345
+
346
+ // Auto-sync: derive status from phase (only if status wasn't explicitly set)
347
+ if (explicitPhase && !explicitStatus) {
348
+ if (explicitPhase === "complete" || explicitPhase === "blocked") {
349
+ task.status = explicitPhase;
350
+ } else {
351
+ task.status = "in_progress";
352
+ }
353
+ }
354
+
355
+ // Update type
356
+ if (params.type) {
357
+ task.type = params.type;
358
+ updates.push(`type → ${params.type}`);
359
+ }
360
+
361
+ // Update attempts
362
+ if (params.attempts !== undefined) {
363
+ if (task.phase === "fix") {
364
+ task.fixAttempts = params.attempts;
365
+ updates.push(`fixAttempts → ${params.attempts}`);
366
+ } else if (task.phase === "execute") {
367
+ task.executeAttempts = params.attempts;
368
+ updates.push(`executeAttempts → ${params.attempts}`);
369
+ } else {
370
+ // Default to execute attempts if phase is ambiguous
371
+ task.executeAttempts = params.attempts;
372
+ updates.push(`executeAttempts → ${params.attempts}`);
373
+ }
374
+ }
375
+
376
+ updateWidget(ctx);
377
+
378
+ const updateSummary = updates.length > 0 ? ` (${updates.join(", ")})` : "";
379
+ return {
380
+ content: [
381
+ {
382
+ type: "text",
383
+ text: `Task ${params.index} "${task.name}"${updateSummary}\n${formatStatus(tasks)}`,
384
+ },
385
+ ],
386
+ details: { action: "update", tasks: [...tasks] } as PlanTrackerDetails,
387
+ };
388
+ }
389
+
390
+ case "status": {
391
+ return {
392
+ content: [{ type: "text", text: formatStatus(tasks) }],
393
+ details: { action: "status", tasks: [...tasks] } as PlanTrackerDetails,
394
+ };
395
+ }
396
+
397
+ case "clear": {
398
+ const count = tasks.length;
399
+ tasks = [];
400
+ updateWidget(ctx);
401
+ return {
402
+ content: [
403
+ {
404
+ type: "text",
405
+ text: count > 0 ? `Plan cleared (${count} tasks removed).` : "No plan was active.",
406
+ },
407
+ ],
408
+ details: { action: "clear", tasks: [] } as PlanTrackerDetails,
409
+ };
410
+ }
411
+
412
+ default:
413
+ return {
414
+ content: [{ type: "text", text: `Unknown action: ${params.action}` }],
415
+ details: {
416
+ action: "status",
417
+ tasks: [...tasks],
418
+ error: `unknown action`,
419
+ } as PlanTrackerDetails,
420
+ };
421
+ }
422
+ },
423
+
424
+ renderCall(args, theme) {
425
+ let text = theme.fg("toolTitle", theme.bold("plan_tracker "));
426
+ text += theme.fg("muted", args.action);
427
+ if (args.action === "update" && args.index !== undefined) {
428
+ text += ` ${theme.fg("accent", `[${args.index}]`)}`;
429
+ const parts: string[] = [];
430
+ if (args.status) parts.push(args.status);
431
+ if (args.phase) parts.push(args.phase);
432
+ if (args.type) parts.push(args.type);
433
+ if (args.attempts !== undefined) parts.push(`attempt ${args.attempts}`);
434
+ if (parts.length > 0) text += ` → ${theme.fg("dim", parts.join(", "))}`;
435
+ }
436
+ if (args.action === "init" && args.tasks) {
437
+ text += ` ${theme.fg("dim", `(${args.tasks.length} tasks)`)}`;
438
+ }
439
+ return new Text(text, 0, 0);
440
+ },
441
+
442
+ renderResult(result, _options, theme) {
443
+ const details = result.details as PlanTrackerDetails | undefined;
444
+ if (!details) {
445
+ const text = result.content[0];
446
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
447
+ }
448
+
449
+ if (details.error) {
450
+ return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
451
+ }
452
+
453
+ const taskList = details.tasks;
454
+ switch (details.action) {
455
+ case "init":
456
+ return new Text(
457
+ theme.fg("success", "✓ ") + theme.fg("muted", `Plan initialized with ${taskList.length} tasks`),
458
+ 0,
459
+ 0,
460
+ );
461
+ case "update": {
462
+ const complete = taskList.filter((t) => t.status === "complete").length;
463
+ return new Text(
464
+ theme.fg("success", "✓ ") + theme.fg("muted", `Updated (${complete}/${taskList.length} complete)`),
465
+ 0,
466
+ 0,
467
+ );
468
+ }
469
+ case "status": {
470
+ if (taskList.length === 0) {
471
+ return new Text(theme.fg("dim", "No plan active"), 0, 0);
472
+ }
473
+ const complete = taskList.filter((t) => t.status === "complete").length;
474
+ let text = theme.fg("muted", `${complete}/${taskList.length} complete`);
475
+ for (const t of taskList) {
476
+ const icon =
477
+ t.status === "complete"
478
+ ? theme.fg("success", "✓")
479
+ : t.status === "blocked"
480
+ ? theme.fg("error", "⛔")
481
+ : t.status === "in_progress"
482
+ ? theme.fg("warning", "→")
483
+ : theme.fg("dim", "○");
484
+ const phaseStr = t.status === "in_progress" ? ` [${t.phase}]` : "";
485
+ text += `\n${icon} ${theme.fg("muted", t.name)}${phaseStr}`;
486
+ }
487
+ return new Text(text, 0, 0);
488
+ }
489
+ case "clear":
490
+ return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Plan cleared"), 0, 0);
491
+ default:
492
+ return new Text(theme.fg("dim", "Done"), 0, 0);
493
+ }
494
+ },
495
+ });
496
+ }