taskplane 0.12.0 → 0.14.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/bin/taskplane.mjs CHANGED
@@ -507,6 +507,7 @@ async function cmdUninstall(args) {
507
507
  ".pi/agents/task-worker.md",
508
508
  ".pi/agents/task-reviewer.md",
509
509
  ".pi/agents/task-merger.md",
510
+ ".pi/agents/supervisor.md",
510
511
  ".pi/batch-state.json",
511
512
  ".pi/batch-history.json",
512
513
  ".pi/orch-abort-signal",
@@ -1301,7 +1302,7 @@ async function cmdInit(args) {
1301
1302
  const skipIfExists = !force && !userConfirmedOverwrite;
1302
1303
 
1303
1304
  // Agent prompts
1304
- for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md"]) {
1305
+ for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md", "supervisor.md"]) {
1305
1306
  copyTemplate(
1306
1307
  path.join(TEMPLATES_DIR, "agents", "local", agent),
1307
1308
  path.join(taskplaneDir, "agents", agent),
@@ -1524,7 +1525,7 @@ async function cmdInit(args) {
1524
1525
 
1525
1526
  // Agent prompts — copy thin local files (base prompts ship in the package
1526
1527
  // and are composed automatically by the task-runner at runtime)
1527
- for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md"]) {
1528
+ for (const agent of ["task-worker.md", "task-reviewer.md", "task-merger.md", "supervisor.md"]) {
1528
1529
  copyTemplate(
1529
1530
  path.join(TEMPLATES_DIR, "agents", "local", agent),
1530
1531
  path.join(projectRoot, ".pi", "agents", agent),
@@ -1685,6 +1686,7 @@ function printFileList(vars, noExamples, preset, exampleTemplateDirs = [], proje
1685
1686
  ".pi/agents/task-worker.md",
1686
1687
  ".pi/agents/task-reviewer.md",
1687
1688
  ".pi/agents/task-merger.md",
1689
+ ".pi/agents/supervisor.md",
1688
1690
  ".pi/task-runner.yaml",
1689
1691
  ];
1690
1692
  if (preset !== "runner-only") files.push(".pi/task-orchestrator.yaml");
@@ -1723,6 +1725,7 @@ function printWorkspaceFileList(vars, noExamples, preset, exampleTemplateDirs, c
1723
1725
  `${prefix}/agents/task-worker.md`,
1724
1726
  `${prefix}/agents/task-reviewer.md`,
1725
1727
  `${prefix}/agents/task-merger.md`,
1728
+ `${prefix}/agents/supervisor.md`,
1726
1729
  `${prefix}/task-runner.yaml`,
1727
1730
  ];
1728
1731
  if (preset !== "runner-only") files.push(`${prefix}/task-orchestrator.yaml`);
@@ -2518,6 +2521,7 @@ function cmdDoctor() {
2518
2521
  { path: "agents/task-worker.md", required: true },
2519
2522
  { path: "agents/task-reviewer.md", required: true },
2520
2523
  { path: "agents/task-merger.md", required: true },
2524
+ { path: "agents/supervisor.md", required: false },
2521
2525
  { path: "taskplane.json", required: false },
2522
2526
  ];
2523
2527
 
@@ -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
+ }