taskplane 0.11.0 → 0.13.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.
@@ -437,6 +437,10 @@ function loadTelemetryData(batchState) {
437
437
  for (const event of events) {
438
438
  switch (event.type) {
439
439
  case "message_end": {
440
+ // A successful message_end means any prior retry resolved.
441
+ // Clear retryActive to prevent stale retry badges from persisting
442
+ // across batches or after transient API errors recover.
443
+ acc.retryActive = false;
440
444
  const usage = event.message?.usage;
441
445
  if (usage) {
442
446
  acc.inputTokens += usage.input || 0;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Persistent Reviewer Extension — TP-057
3
+ *
4
+ * Provides the `wait_for_review` tool that enables a reviewer agent to stay
5
+ * alive across multiple review requests within a single task. The tool blocks
6
+ * (via filesystem polling) until the task-runner signals a new review request
7
+ * or shutdown.
8
+ *
9
+ * Signal protocol:
10
+ * - `.reviews/.review-signal-{NNN}` — new review request available
11
+ * - `.reviews/.review-shutdown` — reviewer should exit cleanly
12
+ * - `.reviews/request-R{NNN}.md` — review request content
13
+ *
14
+ * Environment:
15
+ * - REVIEWER_SIGNAL_DIR — path to .reviews/ directory (required)
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
+ import { Type } from "@mariozechner/pi-ai";
20
+ import { existsSync, readFileSync } from "fs";
21
+ import { join } from "path";
22
+ import {
23
+ REVIEWER_POLL_INTERVAL_MS,
24
+ REVIEWER_WAIT_TIMEOUT_MS,
25
+ REVIEWER_SHUTDOWN_SIGNAL,
26
+ REVIEWER_SIGNAL_PREFIX,
27
+ } from "./taskplane/types.ts";
28
+
29
+ // ── Extension ────────────────────────────────────────────────────────
30
+
31
+ export default function reviewerExtension(pi: ExtensionAPI) {
32
+ const signalDir = process.env.REVIEWER_SIGNAL_DIR;
33
+
34
+ if (!signalDir) {
35
+ // Not running in persistent reviewer mode — skip tool registration.
36
+ // This allows the extension to be loaded in non-persistent contexts
37
+ // without error (fallback fresh-spawn mode).
38
+ return;
39
+ }
40
+
41
+ /** Counter tracking which signal number to watch for next. */
42
+ let nextSignalNum = 1;
43
+
44
+ pi.registerTool({
45
+ name: "wait_for_review",
46
+ label: "Wait for Review",
47
+ description:
48
+ "Block until the next review request is available, then return its content. " +
49
+ "Call this after completing each review to wait for the next one. " +
50
+ "Returns 'SHUTDOWN' when the task is complete and you should exit.",
51
+ promptSnippet: "wait_for_review() — block until the next review request arrives (persistent reviewer mode)",
52
+ promptGuidelines: [
53
+ "Call wait_for_review() to receive each review request.",
54
+ "After writing your review to the specified output file, call wait_for_review() again.",
55
+ "When it returns 'SHUTDOWN', exit cleanly — the task is complete.",
56
+ "Reference your previous reviews when relevant (e.g., 'I flagged X in Step 1 — checking if addressed').",
57
+ ],
58
+ parameters: Type.Object({}),
59
+ async execute() {
60
+ const startTime = Date.now();
61
+ const signalNum = String(nextSignalNum).padStart(3, "0");
62
+ const signalPath = join(signalDir, `${REVIEWER_SIGNAL_PREFIX}${signalNum}`);
63
+ const shutdownPath = join(signalDir, REVIEWER_SHUTDOWN_SIGNAL);
64
+
65
+ // Poll for signal file or shutdown
66
+ while (true) {
67
+ // Check for shutdown signal first
68
+ if (existsSync(shutdownPath)) {
69
+ return {
70
+ content: [{ type: "text" as const, text: "SHUTDOWN — The task is complete. Exit cleanly." }],
71
+ details: undefined,
72
+ };
73
+ }
74
+
75
+ // Check for review signal
76
+ if (existsSync(signalPath)) {
77
+ // Signal found — read the request file path from signal content.
78
+ // Signal file content is the request filename (e.g., "request-R003.md").
79
+ const signalContent = readFileSync(signalPath, "utf-8").trim();
80
+ const requestPath = join(signalDir, signalContent);
81
+
82
+ if (!existsSync(requestPath)) {
83
+ // Signal fired but request file doesn't exist (race condition or error)
84
+ return {
85
+ content: [{
86
+ type: "text" as const,
87
+ text: `ERROR — Signal file ${REVIEWER_SIGNAL_PREFIX}${signalNum} found but ` +
88
+ `${signalContent} does not exist. Waiting for next signal.`,
89
+ }],
90
+ details: undefined,
91
+ };
92
+ }
93
+
94
+ const requestContent = readFileSync(requestPath, "utf-8");
95
+ nextSignalNum++;
96
+
97
+ return {
98
+ content: [{ type: "text" as const, text: requestContent }],
99
+ details: undefined,
100
+ };
101
+ }
102
+
103
+ // Check timeout
104
+ if (Date.now() - startTime > REVIEWER_WAIT_TIMEOUT_MS) {
105
+ return {
106
+ content: [{
107
+ type: "text" as const,
108
+ text: "TIMEOUT — No review request received within the timeout period. Exit cleanly.",
109
+ }],
110
+ details: undefined,
111
+ };
112
+ }
113
+
114
+ // Wait before next poll
115
+ await new Promise(resolve => setTimeout(resolve, REVIEWER_POLL_INTERVAL_MS));
116
+ }
117
+ },
118
+ });
119
+ }