@harms-haus/pi-tasks 0.1.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/src/tools.ts ADDED
@@ -0,0 +1,611 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import type {
3
+ ToolDefinition,
4
+ ExtensionAPI,
5
+ AgentToolResult,
6
+ Theme,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ WriteTasksParams,
10
+ EditTasksParams,
11
+ CompileTasksParams,
12
+ ClearTasksParams,
13
+ GetReadyTasksParams,
14
+ AdvanceTasksParams,
15
+ } from "./schemas";
16
+ import {
17
+ CUSTOM_EVENT_TYPE,
18
+ CUSTOM_SNAPSHOT_TYPE,
19
+ TERMINAL_STATUSES,
20
+ ACTIVE_STATUSES,
21
+ } from "./types";
22
+ import type { TaskBoardSnapshot, TaskEdit, TaskWorkflowEvent } from "./types";
23
+ import { writeTasks, applyEdits, compileBoard, createEmptyBoard, claimReadyTasks } from "./engine";
24
+ import { getStatusCounts } from "./validation";
25
+ import {
26
+ getBoardRef,
27
+ setBoard,
28
+ persistEntries,
29
+ updateUI,
30
+ getLastToolWasAdvance,
31
+ setLastToolWasAdvance,
32
+ } from "./state";
33
+ import { loadConfig, resolvePhasePrompt } from "./config";
34
+ import {
35
+ formatBoardText,
36
+ formatSummaryLine,
37
+ formatAllDoneMessage,
38
+ formatClaimedTaskDetails,
39
+ } from "./formatting";
40
+
41
+ // ── Details Type ──
42
+
43
+ interface TaskToolDetails {
44
+ snapshot: TaskBoardSnapshot;
45
+ error?: string;
46
+ }
47
+
48
+ // ── Result Helpers ──
49
+
50
+ function makeSuccessResult(
51
+ text: string,
52
+ snapshot: TaskBoardSnapshot,
53
+ ): AgentToolResult<TaskToolDetails> {
54
+ return {
55
+ content: [{ type: "text", text }],
56
+ details: { snapshot },
57
+ };
58
+ }
59
+
60
+ function makeErrorResult(
61
+ errorText: string,
62
+ snapshot: TaskBoardSnapshot,
63
+ ): AgentToolResult<TaskToolDetails> {
64
+ return {
65
+ content: [{ type: "text", text: errorText }],
66
+ details: { snapshot, error: errorText },
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Check for phases that just completed (before vs after) and set pending phase prompt.
72
+ * MUTATES `afterBoard` — sets `afterBoard.pendingPhasePrompt` directly.
73
+ * Returns void; callers must read from the mutated board.
74
+ */
75
+ async function checkAndSetPhaseCompletion(
76
+ beforePhases: TaskBoardSnapshot["phases"],
77
+ afterBoard: TaskBoardSnapshot,
78
+ ): Promise<void> {
79
+ // Find phases that were NOT completed before but ARE completed after
80
+ for (const afterPhase of afterBoard.phases) {
81
+ if (afterPhase.status !== "completed") continue;
82
+ const beforePhase = beforePhases.find((p) => p.phase === afterPhase.phase);
83
+ if (!beforePhase || beforePhase.status !== "completed") {
84
+ // This phase just completed
85
+ const config = await loadConfig();
86
+ const template = config.phaseCompletionPromptTemplate
87
+ ? resolvePhasePrompt(config.phaseCompletionPromptTemplate, afterPhase.phase)
88
+ : undefined;
89
+ if (template) {
90
+ afterBoard.pendingPhasePrompt = {
91
+ phase: afterPhase.phase,
92
+ message: template,
93
+ };
94
+ }
95
+ return; // Only handle the first newly completed phase
96
+ }
97
+ }
98
+ }
99
+
100
+ // ── Shared Result Renderer ──
101
+
102
+ function renderColoredBoardResult(text: string, _snapshot: TaskBoardSnapshot, theme: Theme): Text {
103
+ const lines = text.split("\n");
104
+ const colored = lines
105
+ .map((line) => {
106
+ if (/^─── Phase \d+/.test(line)) {
107
+ return theme.fg("accent", theme.bold(line));
108
+ }
109
+ const taskLineMatch = line.match(/^(\S+\s+)(t-\d+\.\d+:\s)(.*)/);
110
+ if (taskLineMatch) {
111
+ const [, prefix, id, rest] = taskLineMatch;
112
+ if (prefix != null && id != null && rest != null) {
113
+ return prefix + theme.fg("muted", id) + rest;
114
+ }
115
+ }
116
+ if (line.includes("→ depends on")) {
117
+ return line.replace(/(t-\d+\.\d+)/g, (m) => theme.fg("muted", m));
118
+ }
119
+ if (line.startsWith("Summary:")) {
120
+ return theme.fg("muted", line);
121
+ }
122
+ return line;
123
+ })
124
+ .join("\n");
125
+ return new Text(colored, 0, 0);
126
+ }
127
+
128
+ // ── Tool 1: write_tasks ──
129
+
130
+ export function createWriteTasksTool(
131
+ pi: ExtensionAPI,
132
+ ): ToolDefinition<typeof WriteTasksParams, TaskToolDetails> {
133
+ return {
134
+ name: "write_tasks",
135
+ label: "Write Tasks",
136
+ description:
137
+ "Add tasks to the phased task board. Provide phases (each with a title and tasks) and a mode ('replace' to clear the board first, 'append' to add to existing tasks). Phases are numbered automatically from array position. Tasks are created in 'draft' status. After writing, use compile_tasks to validate dependencies and activate phases.",
138
+ parameters: WriteTasksParams,
139
+ promptSnippet:
140
+ "Phased task board: write, edit (data/blockers/advance/abandon), compile, claim ready tasks",
141
+ promptGuidelines: [
142
+ "Use write_tasks to add tasks grouped by phase, then compile_tasks to validate and activate them.",
143
+ "write_tasks accepts a mode ('replace' or 'append') and an array of phases, each with a title and tasks.",
144
+ "Each task needs a title, prompt, and profile. Phase numbers are assigned automatically from array position.",
145
+ "Use 'replace' mode to start fresh (clears the board). Use 'append' mode to add phases to an existing board.",
146
+ "Use edit_tasks to set dependencies between tasks after writing.",
147
+ "Tasks are written in 'draft' status. Use compile_tasks to transition them to 'configured' or 'ready'.",
148
+ "Phases gate execution: tasks in later phases only become ready after earlier phases are complete.",
149
+ "Maximum 100 tasks allowed on the board.",
150
+ ],
151
+
152
+ // eslint-disable-next-line @typescript-eslint/require-await
153
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
154
+ const board = getBoardRef();
155
+ const now = new Date().toISOString();
156
+
157
+ try {
158
+ const newBoard = writeTasks(
159
+ board,
160
+ {
161
+ mode: params.mode as "replace" | "append",
162
+ phases: params.phases.map((p) => ({
163
+ title: p.title,
164
+ tasks: p.tasks.map((t) => ({
165
+ title: t.title,
166
+ prompt: t.prompt,
167
+ profile: t.profile,
168
+ })),
169
+ })),
170
+ },
171
+ now,
172
+ );
173
+ setBoard(newBoard);
174
+
175
+ const totalNewTasks = params.phases.reduce(
176
+ (sum: number, p: { tasks: unknown[] }) => sum + p.tasks.length,
177
+ 0,
178
+ );
179
+ const newPhaseNumbers = newBoard.phases.slice(-params.phases.length).map((p) => p.phase);
180
+ const event: TaskWorkflowEvent = {
181
+ type: "write_tasks",
182
+ mode: params.mode as "replace" | "append",
183
+ phases: params.phases.map((p: (typeof params.phases)[number], i: number) => ({
184
+ phase: newPhaseNumbers[i] ?? i + 1,
185
+ title: p.title.trim(),
186
+ tasks: newBoard.tasks
187
+ .filter((t) => t.phase === (newPhaseNumbers[i] ?? i + 1))
188
+ .map((t) => ({
189
+ id: t.id,
190
+ title: t.title,
191
+ prompt: t.prompt,
192
+ profile: t.profile,
193
+ phase: t.phase,
194
+ })),
195
+ })),
196
+ };
197
+ persistEntries(pi, event, newBoard);
198
+ updateUI(ctx, newBoard);
199
+
200
+ return makeSuccessResult(
201
+ `Added ${totalNewTasks} task(s) to the board.\n\n${formatBoardText(newBoard)}`,
202
+ newBoard,
203
+ );
204
+ } catch (err) {
205
+ return makeErrorResult(err instanceof Error ? err.message : String(err), board);
206
+ }
207
+ },
208
+
209
+ renderCall(args, theme) {
210
+ const phases = args.phases as Array<{ tasks: Array<unknown> }> | undefined;
211
+ const totalTasks = phases ? phases.reduce((sum: number, p) => sum + p.tasks.length, 0) : 0;
212
+ return new Text(
213
+ theme.fg("toolTitle", theme.bold("write_tasks ")) +
214
+ theme.fg("muted", `(${totalTasks} items)`),
215
+ 0,
216
+ 0,
217
+ );
218
+ },
219
+
220
+ renderResult(result, _options, theme) {
221
+ const details = result.details as TaskToolDetails | undefined;
222
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
223
+ if (!details) return new Text(text, 0, 0);
224
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
225
+ return renderColoredBoardResult(text, details.snapshot, theme);
226
+ },
227
+ };
228
+ }
229
+
230
+ // ── Tool 2: edit_tasks ──
231
+
232
+ export function createEditTasksTool(
233
+ pi: ExtensionAPI,
234
+ ): ToolDefinition<typeof EditTasksParams, TaskToolDetails> {
235
+ return {
236
+ name: "edit_tasks",
237
+ label: "Edit Tasks",
238
+ description:
239
+ "Batch-edit tasks on the board. Supports three edit types: 'data' (modify title/prompt/profile/phase), 'blockers' (set dependencies), and 'abandon' (mark as abandoned). Edits are atomic — if any fails, none are applied.",
240
+ parameters: EditTasksParams,
241
+ promptSnippet: undefined,
242
+ promptGuidelines: undefined,
243
+
244
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
245
+ const board = getBoardRef();
246
+ const now = new Date().toISOString();
247
+
248
+ // Snapshot before-phases for phase completion detection
249
+ const beforePhases = board.phases.map((p) => ({ ...p }));
250
+
251
+ const edits: TaskEdit[] = params.tasks.map((t: (typeof params.tasks)[number]): TaskEdit => {
252
+ if (t.type === "data") {
253
+ return {
254
+ id: t.id,
255
+ type: "data",
256
+ data: (
257
+ t as { data: { title?: string; prompt?: string; profile?: string; phase?: number } }
258
+ ).data,
259
+ };
260
+ }
261
+ if (t.type === "blockers") {
262
+ return {
263
+ id: t.id,
264
+ type: "blockers",
265
+ data: (t as { data: { dependencies: string[] } }).data,
266
+ };
267
+ }
268
+ return { id: t.id, type: "abandon" };
269
+ });
270
+
271
+ try {
272
+ const newBoard = applyEdits(board, edits, now);
273
+
274
+ // Detect phase completion and set pending prompt
275
+ await checkAndSetPhaseCompletion(beforePhases, newBoard);
276
+
277
+ setBoard(newBoard);
278
+
279
+ // Emit one event per edit with the correct type
280
+ for (const edit of edits) {
281
+ let event: TaskWorkflowEvent;
282
+ if (edit.type === "data") {
283
+ event = { type: "edit_task_data", id: edit.id, data: edit.data };
284
+ } else if (edit.type === "blockers") {
285
+ event = {
286
+ type: "edit_task_blockers",
287
+ id: edit.id,
288
+ dependencies: edit.data.dependencies,
289
+ };
290
+ } else {
291
+ event = { type: "abandon_task", id: edit.id };
292
+ }
293
+ pi.appendEntry(CUSTOM_EVENT_TYPE, event);
294
+ }
295
+ pi.appendEntry(CUSTOM_SNAPSHOT_TYPE, newBoard);
296
+ updateUI(ctx, newBoard);
297
+
298
+ const counts = getStatusCounts(newBoard);
299
+ const summary = formatSummaryLine(newBoard, counts);
300
+ const hasStructuralEdits = edits.some((e) => e.type === "data" || e.type === "blockers");
301
+ const allTerminal = newBoard.tasks.every((t) => TERMINAL_STATUSES.has(t.status));
302
+ const boardText =
303
+ hasStructuralEdits || allTerminal
304
+ ? formatBoardText(newBoard, { counts })
305
+ : formatBoardText(newBoard, { activePhaseOnly: true, counts });
306
+ return makeSuccessResult(
307
+ `Applied ${edits.length} edit(s). ${summary}\n\n${boardText}`,
308
+ newBoard,
309
+ );
310
+ } catch (err) {
311
+ return makeErrorResult(err instanceof Error ? err.message : String(err), board);
312
+ }
313
+ },
314
+
315
+ renderCall(args, theme) {
316
+ return new Text(
317
+ theme.fg("toolTitle", theme.bold("edit_tasks ")) +
318
+ theme.fg("warning", `(${args.tasks.length} edits)`),
319
+ 0,
320
+ 0,
321
+ );
322
+ },
323
+
324
+ renderResult(result, _options, theme) {
325
+ const details = result.details as TaskToolDetails | undefined;
326
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
327
+ if (!details) return new Text(text, 0, 0);
328
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
329
+ return renderColoredBoardResult(text, details.snapshot, theme);
330
+ },
331
+ };
332
+ }
333
+
334
+ // ── Tool 3: compile_tasks ──
335
+
336
+ export function createCompileTasksTool(
337
+ pi: ExtensionAPI,
338
+ ): ToolDefinition<typeof CompileTasksParams, TaskToolDetails> {
339
+ return {
340
+ name: "compile_tasks",
341
+ label: "Compile Tasks",
342
+ description:
343
+ "Validate and compile the task board. Checks for cycles, invalid dependencies, and duplicate ids. Moves draft tasks to 'configured' and computes which tasks are 'ready' based on phase gating and dependency resolution.",
344
+ parameters: CompileTasksParams,
345
+ promptSnippet: undefined,
346
+ promptGuidelines: undefined,
347
+
348
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
349
+ const board = getBoardRef();
350
+ const now = new Date().toISOString();
351
+
352
+ // Snapshot before-phases for phase completion detection
353
+ const beforePhases = board.phases.map((p) => ({ ...p }));
354
+
355
+ try {
356
+ const newBoard = compileBoard(board, now);
357
+
358
+ // Detect phase completion and set pending prompt
359
+ await checkAndSetPhaseCompletion(beforePhases, newBoard);
360
+
361
+ setBoard(newBoard);
362
+
363
+ const event: TaskWorkflowEvent = { type: "compile_tasks" };
364
+ persistEntries(pi, event, newBoard);
365
+ updateUI(ctx, newBoard);
366
+
367
+ const counts = getStatusCounts(newBoard);
368
+ const readyCount = counts.ready;
369
+ const summary = formatSummaryLine(newBoard, counts);
370
+ return makeSuccessResult(
371
+ `Board compiled. ${summary}. ${readyCount} task(s) ready to claim.\n\n${formatBoardText(newBoard, { counts })}`,
372
+ newBoard,
373
+ );
374
+ } catch (err) {
375
+ return makeErrorResult(err instanceof Error ? err.message : String(err), board);
376
+ }
377
+ },
378
+
379
+ renderCall(_args, theme) {
380
+ return new Text(theme.fg("toolTitle", theme.bold("compile_tasks")), 0, 0);
381
+ },
382
+
383
+ renderResult(result, _options, theme) {
384
+ const details = result.details as TaskToolDetails | undefined;
385
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
386
+ if (!details) return new Text(text, 0, 0);
387
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
388
+ return renderColoredBoardResult(text, details.snapshot, theme);
389
+ },
390
+ };
391
+ }
392
+
393
+ // ── Tool 4: clear_tasks ──
394
+
395
+ export function createClearTasksTool(
396
+ pi: ExtensionAPI,
397
+ ): ToolDefinition<typeof ClearTasksParams, TaskToolDetails> {
398
+ return {
399
+ name: "clear_tasks",
400
+ label: "Clear Tasks",
401
+ description: "Clear the entire task board, removing all tasks and resetting state.",
402
+ parameters: ClearTasksParams,
403
+ promptSnippet: undefined,
404
+ promptGuidelines: undefined,
405
+
406
+ // eslint-disable-next-line @typescript-eslint/require-await
407
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
408
+ const board = getBoardRef();
409
+
410
+ if (board.tasks.some((t) => ACTIVE_STATUSES.has(t.status))) {
411
+ return makeErrorResult(
412
+ "Cannot clear board while tasks are implementing or reviewing. Complete or advance active tasks first.",
413
+ board,
414
+ );
415
+ }
416
+
417
+ const emptyBoard = createEmptyBoard();
418
+ setBoard(emptyBoard);
419
+
420
+ const event: TaskWorkflowEvent = { type: "clear_tasks" };
421
+ persistEntries(pi, event, emptyBoard);
422
+ updateUI(ctx, emptyBoard);
423
+
424
+ return makeSuccessResult("Board cleared.", emptyBoard);
425
+ },
426
+
427
+ renderCall(_args, theme) {
428
+ return new Text(theme.fg("toolTitle", theme.bold("clear_tasks")), 0, 0);
429
+ },
430
+
431
+ renderResult(result, _options, theme) {
432
+ const details = result.details as TaskToolDetails | undefined;
433
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
434
+ if (!details) return new Text(text, 0, 0);
435
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
436
+ return new Text(theme.fg("text", text), 0, 0);
437
+ },
438
+ };
439
+ }
440
+
441
+ // ── Tool 5: get_ready_tasks ──
442
+
443
+ export function createGetReadyTasksTool(
444
+ pi: ExtensionAPI,
445
+ ): ToolDefinition<typeof GetReadyTasksParams, TaskToolDetails> {
446
+ return {
447
+ name: "get_ready_tasks",
448
+ label: "Get Ready Tasks",
449
+ description:
450
+ "Claim ready tasks from the board. Moves claimed tasks to 'implementing' status. Ordered by phase ascending, then creation order. After claiming, work through implementing → reviewing → done using advance_tasks.",
451
+ parameters: GetReadyTasksParams,
452
+ promptSnippet: undefined,
453
+ promptGuidelines: undefined,
454
+
455
+ // eslint-disable-next-line @typescript-eslint/require-await
456
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
457
+ const board = getBoardRef();
458
+ const now = new Date().toISOString();
459
+
460
+ try {
461
+ const result = claimReadyTasks(board, params.count, now);
462
+
463
+ if (result.claimed.length === 0) {
464
+ // Determine why no tasks were claimed
465
+ const allTerminal = board.tasks.every((t) => TERMINAL_STATUSES.has(t.status));
466
+ if (allTerminal && board.tasks.length > 0) {
467
+ return makeErrorResult(formatAllDoneMessage(board), board);
468
+ }
469
+
470
+ // Deadlock: non-terminal tasks but none actionable
471
+ const nonTerminal = board.tasks.filter((t) => !TERMINAL_STATUSES.has(t.status));
472
+ if (nonTerminal.length > 0) {
473
+ const blockedList = nonTerminal
474
+ .map((t) => `[${t.id}] ${t.title} (${t.status}, Phase ${t.phase})`)
475
+ .join("\n");
476
+ return makeErrorResult(
477
+ `No ready tasks available. Tasks remain but none are actionable. Check dependencies and phase gating.\n\nBlocked tasks:\n${blockedList}\n\nUse edit_tasks to resolve blockers, then compile_tasks.`,
478
+ board,
479
+ );
480
+ }
481
+
482
+ return makeErrorResult("No tasks on the board.", board);
483
+ }
484
+
485
+ // Success: claimed tasks
486
+ setBoard(result.board);
487
+
488
+ const event: TaskWorkflowEvent = {
489
+ type: "claim_ready_tasks",
490
+ ids: result.claimed.map((t) => t.id),
491
+ };
492
+ persistEntries(pi, event, result.board);
493
+ updateUI(ctx, result.board);
494
+
495
+ const claimedDetails = formatClaimedTaskDetails(result.claimed);
496
+
497
+ return makeSuccessResult(
498
+ `Claimed ${result.claimed.length} task(s).\n\n${claimedDetails}\n\nReview each claimed task and advance through implementing → reviewing → done using advance_tasks.\n\n${formatBoardText(result.board, { activePhaseOnly: true })}`,
499
+ result.board,
500
+ );
501
+ } catch (err) {
502
+ return makeErrorResult(err instanceof Error ? err.message : String(err), board);
503
+ }
504
+ },
505
+
506
+ renderCall(args, theme) {
507
+ return new Text(
508
+ theme.fg("toolTitle", theme.bold("get_ready_tasks ")) +
509
+ theme.fg("muted", `(count: ${args.count})`),
510
+ 0,
511
+ 0,
512
+ );
513
+ },
514
+
515
+ renderResult(result, _options, theme) {
516
+ const details = result.details as TaskToolDetails | undefined;
517
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
518
+ if (!details) return new Text(text, 0, 0);
519
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
520
+ return renderColoredBoardResult(text, details.snapshot, theme);
521
+ },
522
+ };
523
+ }
524
+
525
+ // ── Tool 6: advance_tasks ──
526
+
527
+ export function createAdvanceTasksTool(
528
+ pi: ExtensionAPI,
529
+ ): ToolDefinition<typeof AdvanceTasksParams, TaskToolDetails> {
530
+ return {
531
+ name: "advance_tasks",
532
+ label: "Advance Tasks",
533
+ description:
534
+ "Advance tasks through their lifecycle: implementing → reviewing → done. Each call advances each task by one step. Tasks must be in 'implementing' or 'reviewing' status.",
535
+ parameters: AdvanceTasksParams,
536
+ promptSnippet: "advance_tasks: move tasks implementing → reviewing → done",
537
+ promptGuidelines: [
538
+ "Use advance_tasks to advance claimed tasks through implementing → reviewing → done.",
539
+ "Each call advances by one step: implementing→reviewing, then reviewing→done.",
540
+ "IMPORTANT: Do NOT skip the review step. After advancing to reviewing, review the work before advancing to done.",
541
+ ],
542
+
543
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
544
+ const wasConsecutive = getLastToolWasAdvance();
545
+ setLastToolWasAdvance(true);
546
+
547
+ const board = getBoardRef();
548
+ const now = new Date().toISOString();
549
+ const beforePhases = board.phases.map((p) => ({ ...p }));
550
+
551
+ const uniqueIds = [...new Set(params.ids)];
552
+ const edits: TaskEdit[] = uniqueIds.map((id) => ({ id, type: "advance" as const }));
553
+
554
+ try {
555
+ const newBoard = applyEdits(board, edits, now);
556
+ await checkAndSetPhaseCompletion(beforePhases, newBoard);
557
+
558
+ setBoard(newBoard);
559
+
560
+ for (const edit of edits) {
561
+ const original = board.tasks.find((t) => t.id === edit.id);
562
+ const updated = newBoard.tasks.find((t) => t.id === edit.id);
563
+ const event: TaskWorkflowEvent =
564
+ original && updated
565
+ ? {
566
+ type: "advance_task",
567
+ id: edit.id,
568
+ from: original.status as "implementing" | "reviewing",
569
+ to: updated.status as "reviewing" | "done",
570
+ }
571
+ : { type: "advance_task", id: edit.id, from: "implementing", to: "reviewing" };
572
+ pi.appendEntry(CUSTOM_EVENT_TYPE, event);
573
+ }
574
+ pi.appendEntry(CUSTOM_SNAPSHOT_TYPE, newBoard);
575
+ updateUI(ctx, newBoard);
576
+
577
+ const counts = getStatusCounts(newBoard);
578
+ const allTerminal = newBoard.tasks.every((t) => TERMINAL_STATUSES.has(t.status));
579
+ const boardText = allTerminal
580
+ ? formatBoardText(newBoard, { counts })
581
+ : formatBoardText(newBoard, { activePhaseOnly: true, counts });
582
+
583
+ const summary = formatSummaryLine(newBoard, counts);
584
+ let text = `Advanced ${edits.length} task(s). ${summary}\n\n${boardText}`;
585
+ if (wasConsecutive) {
586
+ text = `⚠️ Review should not be skipped. Please actually review the work before advancing to done.\n\n${text}`;
587
+ }
588
+ return makeSuccessResult(text, newBoard);
589
+ } catch (err) {
590
+ return makeErrorResult(err instanceof Error ? err.message : String(err), board);
591
+ }
592
+ },
593
+
594
+ renderCall(args, theme) {
595
+ return new Text(
596
+ theme.fg("toolTitle", theme.bold("advance_tasks ")) +
597
+ theme.fg("muted", `(${args.ids.length} tasks)`),
598
+ 0,
599
+ 0,
600
+ );
601
+ },
602
+
603
+ renderResult(result, _options, theme) {
604
+ const details = result.details as TaskToolDetails | undefined;
605
+ const text = result.content[0] && "text" in result.content[0] ? result.content[0].text : "";
606
+ if (!details) return new Text(text, 0, 0);
607
+ if (details.error) return new Text(theme.fg("error", details.error), 0, 0);
608
+ return renderColoredBoardResult(text, details.snapshot, theme);
609
+ },
610
+ };
611
+ }