taskplane 0.12.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.
- package/extensions/reviewer-extension.ts +119 -0
- package/extensions/task-runner.ts +388 -43
- package/extensions/taskplane/supervisor-primer.md +5 -3
- package/extensions/taskplane/types.ts +35 -0
- package/package.json +2 -1
- package/templates/agents/local/task-reviewer.md +1 -0
- package/templates/agents/task-reviewer.md +26 -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
|
+
}
|
|
@@ -24,13 +24,18 @@ import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
|
|
24
24
|
import { spawn, spawnSync } from "child_process";
|
|
25
25
|
import {
|
|
26
26
|
readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, unlinkSync,
|
|
27
|
-
statSync, openSync, readSync, closeSync,
|
|
27
|
+
readdirSync, statSync, openSync, readSync, closeSync,
|
|
28
28
|
} from "fs";
|
|
29
29
|
import { tmpdir, userInfo } from "os";
|
|
30
30
|
import { join, dirname, basename, resolve } from "path";
|
|
31
31
|
import { loadProjectConfig, toTaskConfig } from "./taskplane/config-loader.ts";
|
|
32
32
|
import { loadWorkspaceConfig, resolvePointer } from "./taskplane/workspace.ts";
|
|
33
33
|
import type { PointerResolution } from "./taskplane/types.ts";
|
|
34
|
+
import {
|
|
35
|
+
REVIEWER_SHUTDOWN_GRACE_MS,
|
|
36
|
+
REVIEWER_SIGNAL_PREFIX,
|
|
37
|
+
REVIEWER_SHUTDOWN_SIGNAL,
|
|
38
|
+
} from "./taskplane/types.ts";
|
|
34
39
|
import { classifyExit } from "./taskplane/diagnostics.ts";
|
|
35
40
|
import type { TaskExitDiagnostic, ExitSummary } from "./taskplane/diagnostics.ts";
|
|
36
41
|
import {
|
|
@@ -143,6 +148,12 @@ interface TaskState {
|
|
|
143
148
|
reviewerProc: any;
|
|
144
149
|
reviewerTimer: any;
|
|
145
150
|
reviewCounter: number;
|
|
151
|
+
/** TP-057: Persistent reviewer session — tracks the long-lived reviewer tmux session. */
|
|
152
|
+
persistentReviewerSession: string | null;
|
|
153
|
+
/** TP-057: Kill function for the persistent reviewer (to stop sidecar polling). */
|
|
154
|
+
persistentReviewerKill: (() => void) | null;
|
|
155
|
+
/** TP-057: Signal counter for the persistent reviewer (monotonically increasing). */
|
|
156
|
+
persistentReviewerSignalNum: number;
|
|
146
157
|
totalIterations: number;
|
|
147
158
|
stepStatuses: Map<number, StepInfo>;
|
|
148
159
|
}
|
|
@@ -160,7 +171,9 @@ function freshState(): TaskState {
|
|
|
160
171
|
reviewerElapsed: 0, reviewerLastTool: "", reviewerToolCount: 0,
|
|
161
172
|
reviewerInputTokens: 0, reviewerOutputTokens: 0, reviewerCacheReadTokens: 0, reviewerCacheWriteTokens: 0,
|
|
162
173
|
reviewerCostUsd: 0, reviewerContextPct: 0, reviewerProc: null, reviewerTimer: null,
|
|
163
|
-
reviewCounter: 0,
|
|
174
|
+
reviewCounter: 0,
|
|
175
|
+
persistentReviewerSession: null, persistentReviewerKill: null, persistentReviewerSignalNum: 0,
|
|
176
|
+
totalIterations: 0, stepStatuses: new Map(),
|
|
164
177
|
};
|
|
165
178
|
}
|
|
166
179
|
|
|
@@ -633,6 +646,43 @@ function resolveRpcWrapperPath(): string {
|
|
|
633
646
|
);
|
|
634
647
|
}
|
|
635
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Resolve the path to reviewer-extension.ts from the installed taskplane package.
|
|
651
|
+
* Mirrors resolveRpcWrapperPath() resolution strategy.
|
|
652
|
+
*
|
|
653
|
+
* @returns Absolute path to reviewer-extension.ts, or null if not found
|
|
654
|
+
* @since TP-057
|
|
655
|
+
*/
|
|
656
|
+
function resolveReviewerExtensionPath(): string | null {
|
|
657
|
+
const extRelPath = join("extensions", "reviewer-extension.ts");
|
|
658
|
+
|
|
659
|
+
// 1. Package root
|
|
660
|
+
const root = findPackageRoot();
|
|
661
|
+
if (root) {
|
|
662
|
+
const p = join(root, extRelPath);
|
|
663
|
+
if (existsSync(p)) return p;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 2. Extension-file-relative (dev scenario: task-runner.ts is sibling)
|
|
667
|
+
try {
|
|
668
|
+
const args = process.argv;
|
|
669
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
670
|
+
if (args[i] === "-e" && args[i + 1]?.includes("task-runner")) {
|
|
671
|
+
const extPath = resolve(args[i + 1]);
|
|
672
|
+
const derivedRoot = resolve(extPath, "..", "..");
|
|
673
|
+
const p = join(derivedRoot, extRelPath);
|
|
674
|
+
if (existsSync(p)) return p;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch { /* ignore */ }
|
|
678
|
+
|
|
679
|
+
// 3. Development fallback: cwd
|
|
680
|
+
const cwdDev = join(process.cwd(), extRelPath);
|
|
681
|
+
if (existsSync(cwdDev)) return cwdDev;
|
|
682
|
+
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
636
686
|
/**
|
|
637
687
|
* Load an agent definition with prompt inheritance.
|
|
638
688
|
*
|
|
@@ -1567,6 +1617,9 @@ function spawnAgentTmux(opts: {
|
|
|
1567
1617
|
/** Optional extension paths to load in the spawned pi session (via rpc-wrapper --extensions).
|
|
1568
1618
|
* When provided, --no-extensions is NOT passed to pi (would conflict). */
|
|
1569
1619
|
extensions?: string[];
|
|
1620
|
+
/** Optional extra environment variables to set in the spawned tmux session.
|
|
1621
|
+
* Injected as `KEY=VALUE` prefixes in the shell command. @since TP-057 */
|
|
1622
|
+
env?: Record<string, string>;
|
|
1570
1623
|
/** Called on each poll tick with accumulated telemetry from the sidecar JSONL.
|
|
1571
1624
|
* Enables the tmux poll loop to update TaskState (tokens, cost, context%, tools, retries)
|
|
1572
1625
|
* with the same signals that subprocess mode gets from onTokenUpdate/onContextPct/onToolCall. */
|
|
@@ -1710,7 +1763,11 @@ function spawnAgentTmux(opts: {
|
|
|
1710
1763
|
// Pi's ink/react TUI hangs with TERM=tmux-256color (tmux default), so we
|
|
1711
1764
|
// force xterm-256color.
|
|
1712
1765
|
const tmuxCwd = opts.cwd.replace(/^([A-Za-z]):\\/, (_, d: string) => `/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
1713
|
-
|
|
1766
|
+
// Build extra env var prefix (TP-057: e.g., REVIEWER_SIGNAL_DIR for persistent reviewer)
|
|
1767
|
+
const extraEnv = opts.env
|
|
1768
|
+
? Object.entries(opts.env).map(([k, v]) => `${k}=${quoteArg(v)}`).join(" ") + " "
|
|
1769
|
+
: "";
|
|
1770
|
+
const wrappedCommand = `cd ${quoteArg(tmuxCwd)} && ${extraEnv}TERM=xterm-256color ${wrapperCommand}`;
|
|
1714
1771
|
const createResult = spawnSync("tmux", [
|
|
1715
1772
|
"new-session", "-d",
|
|
1716
1773
|
"-s", opts.sessionName,
|
|
@@ -2017,6 +2074,80 @@ export default function (pi: ExtensionAPI) {
|
|
|
2017
2074
|
state.reviewerTimer = null;
|
|
2018
2075
|
}
|
|
2019
2076
|
|
|
2077
|
+
/**
|
|
2078
|
+
* TP-057: Remove stale signal and shutdown files from .reviews/ directory.
|
|
2079
|
+
* Called before spawning a new persistent reviewer to prevent the reviewer
|
|
2080
|
+
* from consuming old signals or immediately seeing a stale shutdown marker.
|
|
2081
|
+
*/
|
|
2082
|
+
function cleanStaleReviewerSignals(reviewsDir: string): void {
|
|
2083
|
+
try {
|
|
2084
|
+
const files = readdirSync(reviewsDir);
|
|
2085
|
+
for (const f of files) {
|
|
2086
|
+
if (f.startsWith(REVIEWER_SIGNAL_PREFIX) || f === REVIEWER_SHUTDOWN_SIGNAL) {
|
|
2087
|
+
try { unlinkSync(join(reviewsDir, f)); } catch {}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
} catch {
|
|
2091
|
+
// Directory may not exist yet — not an error
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/**
|
|
2096
|
+
* TP-057: Shut down the persistent reviewer session cleanly.
|
|
2097
|
+
* Writes shutdown signal, waits for clean exit within grace period,
|
|
2098
|
+
* then force-kills the session if still alive.
|
|
2099
|
+
*
|
|
2100
|
+
* Called from all executeTask exit paths (success, pause, error, stall)
|
|
2101
|
+
* to prevent orphan tmux sessions.
|
|
2102
|
+
*
|
|
2103
|
+
* @param reason - Why the reviewer is being shut down (for logging)
|
|
2104
|
+
*/
|
|
2105
|
+
async function shutdownPersistentReviewer(reason: string): Promise<void> {
|
|
2106
|
+
if (!state.persistentReviewerSession) return;
|
|
2107
|
+
|
|
2108
|
+
const sessionName = state.persistentReviewerSession;
|
|
2109
|
+
console.error(`[task-runner] persistent reviewer: shutting down (${reason})`);
|
|
2110
|
+
|
|
2111
|
+
// Write shutdown signal so the reviewer exits cleanly
|
|
2112
|
+
if (state.task) {
|
|
2113
|
+
const reviewsDir = join(state.task.taskFolder, ".reviews");
|
|
2114
|
+
const shutdownPath = join(reviewsDir, REVIEWER_SHUTDOWN_SIGNAL);
|
|
2115
|
+
try {
|
|
2116
|
+
if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
|
|
2117
|
+
writeFileSync(shutdownPath, "shutdown");
|
|
2118
|
+
} catch (err: any) {
|
|
2119
|
+
console.error(`[task-runner] persistent reviewer: failed to write shutdown signal: ${err?.message}`);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Poll for session death within grace period
|
|
2124
|
+
const graceStart = Date.now();
|
|
2125
|
+
while (Date.now() - graceStart < REVIEWER_SHUTDOWN_GRACE_MS) {
|
|
2126
|
+
const alive = spawnSync("tmux", ["has-session", "-t", sessionName]);
|
|
2127
|
+
if (alive.status !== 0) break;
|
|
2128
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// Force kill if still alive after grace period
|
|
2132
|
+
const finalCheck = spawnSync("tmux", ["has-session", "-t", sessionName]);
|
|
2133
|
+
if (finalCheck.status === 0) {
|
|
2134
|
+
console.error(`[task-runner] persistent reviewer: killing session after grace period`);
|
|
2135
|
+
spawnSync("tmux", ["kill-session", "-t", sessionName]);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// Reset state
|
|
2139
|
+
state.persistentReviewerSession = null;
|
|
2140
|
+
state.persistentReviewerKill = null;
|
|
2141
|
+
state.persistentReviewerSignalNum = 0;
|
|
2142
|
+
clearReviewerState();
|
|
2143
|
+
writeLaneState(state);
|
|
2144
|
+
|
|
2145
|
+
if (state.task) {
|
|
2146
|
+
const statusPath = join(state.task.taskFolder, "STATUS.md");
|
|
2147
|
+
logExecution(statusPath, "Persistent reviewer", `Shutdown complete (${reason})`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2020
2151
|
if (isOrchestratedMode()) {
|
|
2021
2152
|
pi.registerTool({
|
|
2022
2153
|
name: "review_step",
|
|
@@ -2080,9 +2211,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2080
2211
|
const outputPath = join(reviewsDir, `R${num}-${reviewType}-step${stepNum}.md`);
|
|
2081
2212
|
|
|
2082
2213
|
// Resolve step baseline commit for code reviews.
|
|
2083
|
-
// The worker should pass the pre-step HEAD SHA as `baseline` so the
|
|
2084
|
-
// reviewer sees only this step's changes (not cumulative diff).
|
|
2085
|
-
// Falls back to undefined (full diff) if baseline is not provided.
|
|
2086
2214
|
const stepBaselineCommit: string | undefined =
|
|
2087
2215
|
reviewType === "code" ? (baseline || undefined) : undefined;
|
|
2088
2216
|
|
|
@@ -2118,12 +2246,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
2118
2246
|
state.reviewerElapsed = 0;
|
|
2119
2247
|
state.reviewerLastTool = "";
|
|
2120
2248
|
state.reviewerToolCount = 0;
|
|
2121
|
-
|
|
2122
|
-
state.
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2249
|
+
// Don't reset cumulative token counts for persistent reviewer — they accumulate
|
|
2250
|
+
if (!state.persistentReviewerSession) {
|
|
2251
|
+
state.reviewerInputTokens = 0;
|
|
2252
|
+
state.reviewerOutputTokens = 0;
|
|
2253
|
+
state.reviewerCacheReadTokens = 0;
|
|
2254
|
+
state.reviewerCacheWriteTokens = 0;
|
|
2255
|
+
state.reviewerCostUsd = 0;
|
|
2256
|
+
state.reviewerContextPct = 0;
|
|
2257
|
+
}
|
|
2127
2258
|
updateWidgets();
|
|
2128
2259
|
|
|
2129
2260
|
const startTime = Date.now();
|
|
@@ -2132,23 +2263,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
2132
2263
|
updateWidgets();
|
|
2133
2264
|
}, 1000);
|
|
2134
2265
|
|
|
2135
|
-
// Read the request file content as the prompt
|
|
2136
|
-
const promptContent = readFileSync(requestPath, "utf-8");
|
|
2137
|
-
|
|
2138
2266
|
// Resolve context window for reviewer context% calculation
|
|
2139
2267
|
const { contextWindow } = resolveContextWindow(config, ctx);
|
|
2140
2268
|
|
|
2141
|
-
|
|
2142
|
-
|
|
2269
|
+
// ── TP-057: Persistent Reviewer Session ─────────────────
|
|
2270
|
+
// On the first review_step call, spawn a persistent reviewer
|
|
2271
|
+
// that stays alive via the wait_for_review tool. On subsequent
|
|
2272
|
+
// calls, reuse the existing session by writing signal files.
|
|
2273
|
+
// Fall back to fresh-spawn if the persistent session dies.
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* Check if the persistent reviewer tmux session is still alive.
|
|
2277
|
+
*/
|
|
2278
|
+
function isPersistentReviewerAlive(): boolean {
|
|
2279
|
+
if (!state.persistentReviewerSession) return false;
|
|
2280
|
+
const result = spawnSync("tmux", ["has-session", "-t", state.persistentReviewerSession]);
|
|
2281
|
+
return result.status === 0;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* Spawn a persistent reviewer session with the reviewer-extension
|
|
2286
|
+
* loaded, so the reviewer can use wait_for_review to receive requests.
|
|
2287
|
+
*/
|
|
2288
|
+
function spawnPersistentReviewer(): void {
|
|
2289
|
+
const reviewerExtPath = resolveReviewerExtensionPath();
|
|
2290
|
+
if (!reviewerExtPath) {
|
|
2291
|
+
throw new Error("Cannot find reviewer-extension.ts. Ensure taskplane is installed correctly.");
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Clean stale signal/shutdown files before spawning
|
|
2295
|
+
cleanStaleReviewerSignals(reviewsDir);
|
|
2296
|
+
|
|
2297
|
+
// Initial prompt tells the reviewer to call wait_for_review
|
|
2298
|
+
const initialPrompt =
|
|
2299
|
+
"You are a persistent reviewer for this task. " +
|
|
2300
|
+
"Call the `wait_for_review` tool now to receive your first review request. " +
|
|
2301
|
+
"After writing each review, call `wait_for_review` again for the next one.";
|
|
2302
|
+
|
|
2143
2303
|
const spawned = spawnAgentTmux({
|
|
2144
2304
|
sessionName,
|
|
2145
2305
|
cwd: ctx.cwd,
|
|
2146
2306
|
systemPrompt,
|
|
2147
|
-
prompt:
|
|
2307
|
+
prompt: initialPrompt,
|
|
2148
2308
|
model: reviewerModel,
|
|
2149
2309
|
tools: config.reviewer.tools || reviewerDef?.tools || "read,write,bash,grep,find,ls",
|
|
2150
2310
|
thinking: config.reviewer.thinking || "on",
|
|
2151
2311
|
taskId: task.taskId,
|
|
2312
|
+
extensions: [reviewerExtPath],
|
|
2313
|
+
env: { REVIEWER_SIGNAL_DIR: reviewsDir },
|
|
2152
2314
|
onTelemetry: (delta) => {
|
|
2153
2315
|
// Accumulate tokens and cost
|
|
2154
2316
|
state.reviewerInputTokens += delta.inputTokens;
|
|
@@ -2173,27 +2335,96 @@ export default function (pi: ExtensionAPI) {
|
|
|
2173
2335
|
},
|
|
2174
2336
|
});
|
|
2175
2337
|
|
|
2338
|
+
// Store persistent session state
|
|
2339
|
+
state.persistentReviewerSession = sessionName;
|
|
2340
|
+
state.persistentReviewerKill = spawned.kill;
|
|
2341
|
+
state.persistentReviewerSignalNum = 0;
|
|
2176
2342
|
state.reviewerProc = { kill: spawned.kill };
|
|
2177
2343
|
|
|
2178
|
-
//
|
|
2179
|
-
|
|
2344
|
+
// Don't await spawned.promise — the session stays alive across reviews.
|
|
2345
|
+
// Handle session death via isPersistentReviewerAlive() checks.
|
|
2346
|
+
spawned.promise.then(() => {
|
|
2347
|
+
// Session ended (reviewer exited or was killed)
|
|
2348
|
+
console.error(`[task-runner] persistent reviewer session '${sessionName}' ended`);
|
|
2349
|
+
}).catch((err: any) => {
|
|
2350
|
+
console.error(`[task-runner] persistent reviewer session error: ${err?.message || err}`);
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Write signal file to notify the persistent reviewer of a new request.
|
|
2356
|
+
* Returns the signal number used.
|
|
2357
|
+
*/
|
|
2358
|
+
function signalPersistentReviewer(): number {
|
|
2359
|
+
state.persistentReviewerSignalNum++;
|
|
2360
|
+
const sigNum = String(state.persistentReviewerSignalNum).padStart(3, "0");
|
|
2361
|
+
const signalPath = join(reviewsDir, `${REVIEWER_SIGNAL_PREFIX}${sigNum}`);
|
|
2362
|
+
// Write the request filename so the reviewer can find it
|
|
2363
|
+
// (signal num and review counter may diverge after respawns)
|
|
2364
|
+
writeFileSync(signalPath, `request-R${num}.md`);
|
|
2365
|
+
return state.persistentReviewerSignalNum;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
/**
|
|
2369
|
+
* Poll for the verdict file to appear (written by the reviewer).
|
|
2370
|
+
* Same pattern as the original review_step handler.
|
|
2371
|
+
*/
|
|
2372
|
+
async function pollForVerdict(): Promise<string> {
|
|
2373
|
+
const verdictTimeout = 30 * 60 * 1000; // 30 minutes
|
|
2374
|
+
const pollStart = Date.now();
|
|
2375
|
+
while (Date.now() - pollStart < verdictTimeout) {
|
|
2376
|
+
if (existsSync(outputPath)) {
|
|
2377
|
+
return readFileSync(outputPath, "utf-8");
|
|
2378
|
+
}
|
|
2379
|
+
// Also check if persistent reviewer died while we're waiting
|
|
2380
|
+
if (state.persistentReviewerSession && !isPersistentReviewerAlive()) {
|
|
2381
|
+
throw new Error("Persistent reviewer session died while waiting for verdict");
|
|
2382
|
+
}
|
|
2383
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
2384
|
+
}
|
|
2385
|
+
throw new Error("Reviewer verdict timeout — no output file after 30 minutes");
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
try {
|
|
2389
|
+
// ── Persistent reviewer: spawn or reuse ─────────────
|
|
2390
|
+
const needsSpawn = !state.persistentReviewerSession || !isPersistentReviewerAlive();
|
|
2391
|
+
|
|
2392
|
+
if (needsSpawn && state.persistentReviewerSession) {
|
|
2393
|
+
// Session was previously active but died — log fallback
|
|
2394
|
+
console.error(`[task-runner] persistent reviewer session dead — respawning`);
|
|
2395
|
+
logExecution(statusPath, `Reviewer R${num}`,
|
|
2396
|
+
`persistent reviewer dead — respawning for ${reviewType} review`);
|
|
2397
|
+
state.persistentReviewerSession = null;
|
|
2398
|
+
state.persistentReviewerKill = null;
|
|
2399
|
+
state.persistentReviewerSignalNum = 0;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
if (needsSpawn) {
|
|
2403
|
+
spawnPersistentReviewer();
|
|
2404
|
+
// Give the reviewer a moment to start and call wait_for_review
|
|
2405
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
2406
|
+
}
|
|
2180
2407
|
|
|
2181
|
-
|
|
2408
|
+
// Signal the reviewer with the new request
|
|
2409
|
+
signalPersistentReviewer();
|
|
2410
|
+
|
|
2411
|
+
// Poll for the verdict file
|
|
2412
|
+
const reviewContent = await pollForVerdict();
|
|
2413
|
+
|
|
2414
|
+
// Stop the per-review timer
|
|
2415
|
+
if (state.reviewerTimer) clearInterval(state.reviewerTimer);
|
|
2182
2416
|
state.reviewerElapsed = Date.now() - startTime;
|
|
2183
|
-
state.reviewerStatus =
|
|
2184
|
-
state.reviewerProc = null;
|
|
2417
|
+
state.reviewerStatus = "done";
|
|
2185
2418
|
writeLaneState(state);
|
|
2186
2419
|
updateWidgets();
|
|
2187
2420
|
|
|
2188
2421
|
// Extract verdict from review output
|
|
2189
2422
|
let verdict = "UNKNOWN";
|
|
2190
2423
|
let reviseDetails = "";
|
|
2191
|
-
if (
|
|
2192
|
-
|
|
2193
|
-
verdict = extractVerdict(review);
|
|
2424
|
+
if (reviewContent) {
|
|
2425
|
+
verdict = extractVerdict(reviewContent);
|
|
2194
2426
|
if (verdict === "REVISE") {
|
|
2195
|
-
|
|
2196
|
-
const summaryMatch = review.match(/###?\s*Summary[:\s]*([\s\S]*?)(?=###|$)/i);
|
|
2427
|
+
const summaryMatch = reviewContent.match(/###?\s*Summary[:\s]*([\s\S]*?)(?=###|$)/i);
|
|
2197
2428
|
reviseDetails = summaryMatch
|
|
2198
2429
|
? summaryMatch[1].trim().slice(0, 500)
|
|
2199
2430
|
: "See review file for details.";
|
|
@@ -2211,8 +2442,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
2211
2442
|
`${reviewType} Step ${stepNum}: ${verdict}`);
|
|
2212
2443
|
updateStatusField(statusPath, "Review Counter", `${state.reviewCounter}`);
|
|
2213
2444
|
|
|
2214
|
-
//
|
|
2215
|
-
|
|
2445
|
+
// Set reviewer to idle (NOT clear — persistent session stays alive)
|
|
2446
|
+
state.reviewerStatus = "idle";
|
|
2447
|
+
state.reviewerType = "";
|
|
2448
|
+
state.reviewerStep = 0;
|
|
2449
|
+
if (state.reviewerTimer) clearInterval(state.reviewerTimer);
|
|
2450
|
+
state.reviewerTimer = null;
|
|
2216
2451
|
writeLaneState(state);
|
|
2217
2452
|
updateWidgets();
|
|
2218
2453
|
|
|
@@ -2233,20 +2468,116 @@ export default function (pi: ExtensionAPI) {
|
|
|
2233
2468
|
details: undefined,
|
|
2234
2469
|
};
|
|
2235
2470
|
} catch (err: any) {
|
|
2236
|
-
//
|
|
2237
|
-
|
|
2238
|
-
clearReviewerState();
|
|
2239
|
-
state.reviewerStatus = "error";
|
|
2240
|
-
writeLaneState(state);
|
|
2241
|
-
updateWidgets();
|
|
2242
|
-
|
|
2471
|
+
// ── Fallback: kill persistent session, try fresh spawn ──
|
|
2472
|
+
console.error(`[task-runner] persistent reviewer error: ${err?.message || err}`);
|
|
2243
2473
|
logExecution(statusPath, `Reviewer R${num}`,
|
|
2244
|
-
|
|
2474
|
+
`persistent reviewer failed — falling back to fresh spawn: ${err?.message || err}`);
|
|
2245
2475
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
}
|
|
2476
|
+
// Kill the dead/broken persistent session
|
|
2477
|
+
if (state.persistentReviewerKill) {
|
|
2478
|
+
try { state.persistentReviewerKill(); } catch {}
|
|
2479
|
+
}
|
|
2480
|
+
state.persistentReviewerSession = null;
|
|
2481
|
+
state.persistentReviewerKill = null;
|
|
2482
|
+
state.persistentReviewerSignalNum = 0;
|
|
2483
|
+
|
|
2484
|
+
// ── Fresh spawn fallback (original behavior) ────────
|
|
2485
|
+
try {
|
|
2486
|
+
const promptContent = readFileSync(requestPath, "utf-8");
|
|
2487
|
+
const spawned = spawnAgentTmux({
|
|
2488
|
+
sessionName,
|
|
2489
|
+
cwd: ctx.cwd,
|
|
2490
|
+
systemPrompt,
|
|
2491
|
+
prompt: promptContent,
|
|
2492
|
+
model: reviewerModel,
|
|
2493
|
+
tools: config.reviewer.tools || reviewerDef?.tools || "read,write,bash,grep,find,ls",
|
|
2494
|
+
thinking: config.reviewer.thinking || "on",
|
|
2495
|
+
taskId: task.taskId,
|
|
2496
|
+
onTelemetry: (delta) => {
|
|
2497
|
+
state.reviewerInputTokens += delta.inputTokens;
|
|
2498
|
+
state.reviewerOutputTokens += delta.outputTokens;
|
|
2499
|
+
state.reviewerCacheReadTokens += delta.cacheReadTokens;
|
|
2500
|
+
state.reviewerCacheWriteTokens += delta.cacheWriteTokens;
|
|
2501
|
+
state.reviewerCostUsd += delta.cost;
|
|
2502
|
+
state.reviewerToolCount += delta.toolCalls;
|
|
2503
|
+
if (delta.lastTool) state.reviewerLastTool = delta.lastTool;
|
|
2504
|
+
if (delta.latestTotalTokens > 0 && contextWindow > 0) {
|
|
2505
|
+
state.reviewerContextPct = (delta.latestTotalTokens / contextWindow) * 100;
|
|
2506
|
+
}
|
|
2507
|
+
writeLaneState(state);
|
|
2508
|
+
updateWidgets();
|
|
2509
|
+
},
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2512
|
+
state.reviewerProc = { kill: spawned.kill };
|
|
2513
|
+
const result = await spawned.promise;
|
|
2514
|
+
|
|
2515
|
+
clearInterval(state.reviewerTimer);
|
|
2516
|
+
state.reviewerElapsed = Date.now() - startTime;
|
|
2517
|
+
state.reviewerStatus = result.exitCode === 0 ? "done" : "error";
|
|
2518
|
+
state.reviewerProc = null;
|
|
2519
|
+
writeLaneState(state);
|
|
2520
|
+
updateWidgets();
|
|
2521
|
+
|
|
2522
|
+
// Extract verdict from fallback review
|
|
2523
|
+
let verdict = "UNKNOWN";
|
|
2524
|
+
let reviseDetails = "";
|
|
2525
|
+
if (existsSync(outputPath)) {
|
|
2526
|
+
const review = readFileSync(outputPath, "utf-8");
|
|
2527
|
+
verdict = extractVerdict(review);
|
|
2528
|
+
if (verdict === "REVISE") {
|
|
2529
|
+
const summaryMatch = review.match(/###?\s*Summary[:\s]*([\s\S]*?)(?=###|$)/i);
|
|
2530
|
+
reviseDetails = summaryMatch
|
|
2531
|
+
? summaryMatch[1].trim().slice(0, 500)
|
|
2532
|
+
: "See review file for details.";
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
verdict = "UNAVAILABLE";
|
|
2536
|
+
logExecution(statusPath, `Reviewer R${num}`,
|
|
2537
|
+
`${reviewType} review — fallback reviewer did not produce output`);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
logReview(statusPath, `R${num}`, reviewType, stepNum, verdict,
|
|
2541
|
+
`.reviews/R${num}-${reviewType}-step${stepNum}.md`);
|
|
2542
|
+
logExecution(statusPath, `Review R${num}`,
|
|
2543
|
+
`${reviewType} Step ${stepNum}: ${verdict} (fallback)`);
|
|
2544
|
+
updateStatusField(statusPath, "Review Counter", `${state.reviewCounter}`);
|
|
2545
|
+
|
|
2546
|
+
clearReviewerState();
|
|
2547
|
+
writeLaneState(state);
|
|
2548
|
+
updateWidgets();
|
|
2549
|
+
|
|
2550
|
+
let resultText: string;
|
|
2551
|
+
if (verdict === "APPROVE") {
|
|
2552
|
+
resultText = "APPROVE";
|
|
2553
|
+
} else if (verdict === "REVISE") {
|
|
2554
|
+
resultText = `REVISE: ${reviseDetails}\n\nFull review: .reviews/R${num}-${reviewType}-step${stepNum}.md`;
|
|
2555
|
+
} else if (verdict === "RETHINK") {
|
|
2556
|
+
resultText = `RETHINK — reconsider your approach. See .reviews/R${num}-${reviewType}-step${stepNum}.md`;
|
|
2557
|
+
} else {
|
|
2558
|
+
resultText = `UNAVAILABLE — reviewer did not produce a usable verdict.`;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
return {
|
|
2562
|
+
content: [{ type: "text" as const, text: resultText }],
|
|
2563
|
+
details: undefined,
|
|
2564
|
+
};
|
|
2565
|
+
} catch (fallbackErr: any) {
|
|
2566
|
+
// Both persistent and fallback failed
|
|
2567
|
+
clearInterval(state.reviewerTimer);
|
|
2568
|
+
clearReviewerState();
|
|
2569
|
+
state.reviewerStatus = "error";
|
|
2570
|
+
writeLaneState(state);
|
|
2571
|
+
updateWidgets();
|
|
2572
|
+
|
|
2573
|
+
logExecution(statusPath, `Reviewer R${num}`,
|
|
2574
|
+
`${reviewType} review — both persistent and fallback failed: ${fallbackErr?.message || fallbackErr}`);
|
|
2575
|
+
|
|
2576
|
+
return {
|
|
2577
|
+
content: [{ type: "text" as const, text: `UNAVAILABLE — reviewer error: ${fallbackErr?.message || fallbackErr}` }],
|
|
2578
|
+
details: undefined,
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2250
2581
|
}
|
|
2251
2582
|
},
|
|
2252
2583
|
});
|
|
@@ -2298,6 +2629,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2298
2629
|
if (state.phase === "paused") {
|
|
2299
2630
|
logExecution(statusPath, "Paused", `User paused at iteration ${iter + 1}`);
|
|
2300
2631
|
ctx.ui.notify(`Task paused at iteration ${iter + 1}`, "info");
|
|
2632
|
+
await shutdownPersistentReviewer("task paused");
|
|
2301
2633
|
return;
|
|
2302
2634
|
}
|
|
2303
2635
|
|
|
@@ -2329,7 +2661,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2329
2661
|
|
|
2330
2662
|
await runWorker(remainingSteps, ctx);
|
|
2331
2663
|
|
|
2332
|
-
if (state.phase === "error")
|
|
2664
|
+
if (state.phase === "error") {
|
|
2665
|
+
await shutdownPersistentReviewer("worker error");
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2333
2668
|
|
|
2334
2669
|
// ── Post-worker: determine which steps were newly completed ──
|
|
2335
2670
|
const afterStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
|
|
@@ -2345,6 +2680,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2345
2680
|
logExecution(statusPath, "Task blocked", `No progress after ${noProgressCount} iterations`);
|
|
2346
2681
|
ctx.ui.notify(`⚠️ Task blocked — no progress after ${noProgressCount} iterations`, "error");
|
|
2347
2682
|
state.phase = "error";
|
|
2683
|
+
await shutdownPersistentReviewer("task stalled");
|
|
2348
2684
|
return;
|
|
2349
2685
|
}
|
|
2350
2686
|
} else {
|
|
@@ -2409,10 +2745,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2409
2745
|
logExecution(statusPath, "Task incomplete", `Max iterations (${config.context.max_worker_iterations}) reached with incomplete steps: ${incomplete}`);
|
|
2410
2746
|
ctx.ui.notify(`⚠️ Task incomplete — max iterations reached. Incomplete: ${incomplete}`, "error");
|
|
2411
2747
|
state.phase = "error";
|
|
2748
|
+
await shutdownPersistentReviewer("max iterations reached");
|
|
2412
2749
|
return;
|
|
2413
2750
|
}
|
|
2414
2751
|
}
|
|
2415
2752
|
|
|
2753
|
+
// ── TP-057: Shutdown persistent reviewer ────────────────────────
|
|
2754
|
+
await shutdownPersistentReviewer("task complete");
|
|
2755
|
+
|
|
2416
2756
|
// All steps done — run quality gate if enabled, then create .DONE
|
|
2417
2757
|
if (config.quality_gate.enabled) {
|
|
2418
2758
|
// ── Quality Gate Enabled ─────────────────────────────────
|
|
@@ -3545,6 +3885,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
3545
3885
|
// Kill any running subprocesses
|
|
3546
3886
|
if (state.workerProc) try { state.workerProc.kill(); } catch {}
|
|
3547
3887
|
if (state.reviewerProc) try { state.reviewerProc.kill(); } catch {}
|
|
3888
|
+
// TP-057: Kill persistent reviewer session if alive
|
|
3889
|
+
if (state.persistentReviewerKill) try { state.persistentReviewerKill(); } catch {}
|
|
3890
|
+
state.persistentReviewerSession = null;
|
|
3891
|
+
state.persistentReviewerKill = null;
|
|
3892
|
+
state.persistentReviewerSignalNum = 0;
|
|
3548
3893
|
if (state.workerTimer) clearInterval(state.workerTimer);
|
|
3549
3894
|
if (state.reviewerTimer) clearInterval(state.reviewerTimer);
|
|
3550
3895
|
|
|
@@ -50,10 +50,12 @@ You (supervisor) ← operator talks to you
|
|
|
50
50
|
│ └── Create .DONE file when all steps complete
|
|
51
51
|
│
|
|
52
52
|
├── Reviewer Agents (LLM, cross-model)
|
|
53
|
-
│ ├──
|
|
54
|
-
│ ├──
|
|
53
|
+
│ ├── Persistent: one reviewer per task, stays alive across all reviews
|
|
54
|
+
│ ├── Receives review requests via wait_for_review tool (signal files)
|
|
55
|
+
│ ├── Reviews plans (before implementation) and code (after)
|
|
55
56
|
│ ├── Write structured verdict to .reviews/ directory
|
|
56
|
-
│
|
|
57
|
+
│ ├── APPROVE or REVISE (worker addresses feedback inline)
|
|
58
|
+
│ └── Falls back to fresh spawn if persistent session dies
|
|
57
59
|
│
|
|
58
60
|
└── Merge Agents (LLM)
|
|
59
61
|
├── Run in temporary merge worktrees
|
|
@@ -1306,6 +1306,41 @@ export const MERGE_HEALTH_STUCK_THRESHOLD_MS = 20 * 60 * 1000; // 20 minutes
|
|
|
1306
1306
|
*/
|
|
1307
1307
|
export const MERGE_HEALTH_CAPTURE_LINES = 10;
|
|
1308
1308
|
|
|
1309
|
+
// ── Persistent Reviewer Constants (TP-057) ───────────────────────────
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Polling interval (ms) for the `wait_for_review` tool to check for signal files.
|
|
1313
|
+
* Reviews take minutes; 3s latency is invisible to the user.
|
|
1314
|
+
* @since TP-057
|
|
1315
|
+
*/
|
|
1316
|
+
export const REVIEWER_POLL_INTERVAL_MS = 3_000;
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Maximum time (ms) for the `wait_for_review` tool to wait for a review signal.
|
|
1320
|
+
* 30 minutes — generous for long-running code reviews.
|
|
1321
|
+
* @since TP-057
|
|
1322
|
+
*/
|
|
1323
|
+
export const REVIEWER_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Grace period (ms) after writing shutdown signal before killing the reviewer session.
|
|
1327
|
+
* Allows the reviewer to exit cleanly after receiving the shutdown signal.
|
|
1328
|
+
* @since TP-057
|
|
1329
|
+
*/
|
|
1330
|
+
export const REVIEWER_SHUTDOWN_GRACE_MS = 10_000;
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Signal file prefix for review requests. Full name: `.review-signal-{NNN}`
|
|
1334
|
+
* @since TP-057
|
|
1335
|
+
*/
|
|
1336
|
+
export const REVIEWER_SIGNAL_PREFIX = ".review-signal-";
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Shutdown signal filename written to .reviews/ when the task is complete.
|
|
1340
|
+
* @since TP-057
|
|
1341
|
+
*/
|
|
1342
|
+
export const REVIEWER_SHUTDOWN_SIGNAL = ".review-shutdown";
|
|
1343
|
+
|
|
1309
1344
|
// ── Merge Health Event Types (TP-056) ────────────────────────────────
|
|
1310
1345
|
|
|
1311
1346
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskplane",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"dashboard/",
|
|
32
32
|
"extensions/task-runner.ts",
|
|
33
33
|
"extensions/task-orchestrator.ts",
|
|
34
|
+
"extensions/reviewer-extension.ts",
|
|
34
35
|
"extensions/taskplane/",
|
|
35
36
|
"skills/",
|
|
36
37
|
"templates/"
|
|
@@ -16,6 +16,7 @@ name: task-reviewer
|
|
|
16
16
|
- Verdict format (APPROVE / REVISE)
|
|
17
17
|
- Review file output conventions
|
|
18
18
|
- Plan granularity guidance
|
|
19
|
+
- Persistent reviewer mode (wait_for_review tool workflow)
|
|
19
20
|
|
|
20
21
|
Add project-specific review criteria below. Common examples:
|
|
21
22
|
- Required test coverage thresholds
|
|
@@ -9,12 +9,38 @@ task implementations. You have full read access to the codebase and can run comm
|
|
|
9
9
|
|
|
10
10
|
## How You Work
|
|
11
11
|
|
|
12
|
+
You operate in one of two modes depending on available tools:
|
|
13
|
+
|
|
14
|
+
### Persistent Mode (when `wait_for_review` tool is available)
|
|
15
|
+
|
|
16
|
+
You are a **persistent reviewer** that stays alive across all review requests for
|
|
17
|
+
a task. This preserves your context — you remember what you reviewed in earlier
|
|
18
|
+
steps and can reference previous findings.
|
|
19
|
+
|
|
20
|
+
1. Call `wait_for_review()` to receive your first review request
|
|
21
|
+
2. The request specifies an **output file path** — you MUST write your review there
|
|
22
|
+
3. Use your tools to explore the codebase — read files, run `git diff`, check patterns
|
|
23
|
+
4. **Use the `write` tool to create the output file with your review**
|
|
24
|
+
5. Use the appropriate verdict: APPROVE, REVISE, or RETHINK
|
|
25
|
+
6. Call `wait_for_review()` again to receive the next request
|
|
26
|
+
7. Repeat until you receive a `SHUTDOWN` signal, then exit cleanly
|
|
27
|
+
|
|
28
|
+
**Cross-step awareness:** When reviewing later steps, reference your earlier
|
|
29
|
+
reviews when relevant. For example: "I flagged X in Step 2's plan review —
|
|
30
|
+
checking if it was addressed in this code review."
|
|
31
|
+
|
|
32
|
+
### Fresh Spawn Mode (when `wait_for_review` is NOT available)
|
|
33
|
+
|
|
34
|
+
You handle a single review request and then exit.
|
|
35
|
+
|
|
12
36
|
1. Read the review request provided to you carefully
|
|
13
37
|
2. The request specifies an **output file path** — you MUST write your review there
|
|
14
38
|
3. Use your tools to explore the codebase — read files, run `git diff`, check patterns
|
|
15
39
|
4. **Use the `write` tool to create the output file with your review**
|
|
16
40
|
5. Use the appropriate verdict: APPROVE, REVISE, or RETHINK
|
|
17
41
|
|
|
42
|
+
### Critical Rule (Both Modes)
|
|
43
|
+
|
|
18
44
|
**CRITICAL:** Your review MUST be written to disk using the `write` tool.
|
|
19
45
|
Do NOT just respond with text — the orchestrator reads the OUTPUT FILE to get
|
|
20
46
|
your verdict. If you don't write the file, your review is lost.
|