taskplane 0.8.2 β†’ 0.9.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.
@@ -530,18 +530,19 @@ function renderLanesTasks(batch, tmuxSessions) {
530
530
  // Worker stats from lane state sidecar + telemetry badges
531
531
  let workerHtml = "";
532
532
  const telemBadges = task.status !== "pending" ? telemetryBadgesHtml(tel) : "";
533
+ const reviewerActive = ls && ls.reviewerStatus === "running";
533
534
  if (ls && ls.workerStatus === "running" && task.status === "running") {
534
535
  const elapsed = ls.workerElapsed ? `${Math.round(ls.workerElapsed / 1000)}s` : "";
535
536
  const tools = ls.workerToolCount || 0;
536
537
  const ctx = ls.workerContextPct ? `${Math.round(ls.workerContextPct)}%` : "";
537
- const lastTool = ls.workerLastTool || "";
538
+ const lastTool = reviewerActive ? "[awaiting review]" : (ls.workerLastTool || "");
538
539
  const tokenStr = tokenSummaryFromLaneState(ls);
539
540
  workerHtml = `<div class="worker-stats">`;
540
541
  workerHtml += `<span class="worker-stat" title="Worker elapsed">⏱ ${elapsed}</span>`;
541
542
  workerHtml += `<span class="worker-stat" title="Tool calls">πŸ”§ ${tools}</span>`;
542
543
  if (ctx) workerHtml += `<span class="worker-stat" title="Context window used">πŸ“Š ${ctx}</span>`;
543
544
  if (tokenStr) workerHtml += `<span class="worker-stat" title="Tokens: input↑ output↓ cacheRead(R) cacheWrite(W)">πŸͺ™ ${tokenStr}</span>`;
544
- if (lastTool) workerHtml += `<span class="worker-stat worker-last-tool" title="Last tool call">${escapeHtml(lastTool)}</span>`;
545
+ if (lastTool) workerHtml += `<span class="worker-stat worker-last-tool" title="${reviewerActive ? 'Waiting for reviewer' : 'Last tool call'}">${reviewerActive ? '<span style="color:var(--yellow)">' + escapeHtml(lastTool) + '</span>' : escapeHtml(lastTool)}</span>`;
545
546
  workerHtml += telemBadges;
546
547
  workerHtml += `</div>`;
547
548
  } else if (!ls && tel && task.status === "running") {
@@ -560,6 +561,35 @@ function renderLanesTasks(batch, tmuxSessions) {
560
561
  workerHtml = `<div class="worker-stats">${telemBadges}</div>`;
561
562
  }
562
563
 
564
+ // Reviewer sub-row: shown when reviewer is actively running
565
+ let reviewerRowHtml = "";
566
+ if (reviewerActive) {
567
+ const rElapsed = ls.reviewerElapsed ? `${Math.round(ls.reviewerElapsed / 1000)}s` : "";
568
+ const rTools = ls.reviewerToolCount || 0;
569
+ const rCtx = ls.reviewerContextPct ? `${Math.round(ls.reviewerContextPct)}%` : "";
570
+ const rLastTool = ls.reviewerLastTool || "";
571
+ const rCost = ls.reviewerCostUsd ? `$${ls.reviewerCostUsd.toFixed(2)}` : "";
572
+ const rType = ls.reviewerType || "review";
573
+ const rStep = ls.reviewerStep || "?";
574
+ reviewerRowHtml = `
575
+ <div class="task-row reviewer-sub-row">
576
+ <span class="task-icon"></span>
577
+ <span class="task-actions"></span>
578
+ <span class="reviewer-label">πŸ“‹ Reviewer</span>
579
+ <span class="reviewer-type">${escapeHtml(rType)} Β· Step ${rStep}</span>
580
+ <span class="task-duration">${rElapsed}</span>
581
+ <span></span>
582
+ <span class="task-step">
583
+ <div class="worker-stats reviewer-stats">
584
+ <span class="worker-stat" title="Reviewer tool calls">πŸ”§ ${rTools}</span>
585
+ ${rCtx ? `<span class="worker-stat" title="Reviewer context used">πŸ“Š ${rCtx}</span>` : ""}
586
+ ${rCost ? `<span class="worker-stat" title="Reviewer cost">${rCost}</span>` : ""}
587
+ ${rLastTool ? `<span class="worker-stat worker-last-tool" title="Reviewer last tool">${escapeHtml(rLastTool)}</span>` : ""}
588
+ </div>
589
+ </span>
590
+ </div>`;
591
+ }
592
+
563
593
  const isViewingStatus = viewerMode === 'status-md' && viewerTarget === task.taskId;
564
594
  const eyeHtml = task.status !== 'pending'
565
595
  ? `<button class="viewer-eye-btn${isViewingStatus ? ' active' : ''}" onclick="viewStatusMd('${escapeHtml(task.taskId)}')" title="View STATUS.md">πŸ‘</button>`
@@ -575,6 +605,7 @@ function renderLanesTasks(batch, tmuxSessions) {
575
605
  <span>${progressHtml}</span>
576
606
  <span class="task-step">${stepHtml}${workerHtml}</span>
577
607
  </div>`;
608
+ html += reviewerRowHtml;
578
609
  }
579
610
 
580
611
  html += `</div>`; // close lane-group
@@ -631,6 +631,32 @@ body {
631
631
  text-overflow: ellipsis;
632
632
  }
633
633
 
634
+ /* ─── Reviewer Sub-Row ─────────────────────────────────────────────── */
635
+
636
+ .reviewer-sub-row {
637
+ background: var(--bg-surface-hover);
638
+ border-left: 3px solid var(--yellow, #e5c07b);
639
+ padding-left: 11px; /* compensate for border */
640
+ font-size: 0.85em;
641
+ }
642
+
643
+ .reviewer-sub-row .reviewer-label {
644
+ font-family: var(--font-mono);
645
+ font-size: 0.75rem;
646
+ color: var(--yellow, #e5c07b);
647
+ font-weight: 600;
648
+ }
649
+
650
+ .reviewer-sub-row .reviewer-type {
651
+ font-family: var(--font-mono);
652
+ font-size: 0.7rem;
653
+ color: var(--text-muted);
654
+ }
655
+
656
+ .reviewer-stats {
657
+ margin-top: 0;
658
+ }
659
+
634
660
  /* ─── Telemetry Badges (retry, compaction) ─────────────────────────────── */
635
661
 
636
662
  .telem-badge {
@@ -740,12 +740,15 @@ function computeBatchTotalCost(laneStates, telemetry) {
740
740
  let totalCost = 0;
741
741
  const coveredPrefixes = new Set();
742
742
 
743
- // Primary: sum cost from lane states
743
+ // Primary: sum cost from lane states (worker + reviewer)
744
744
  for (const [prefix, ls] of Object.entries(laneStates)) {
745
745
  if (ls.workerCostUsd) {
746
746
  totalCost += ls.workerCostUsd;
747
747
  coveredPrefixes.add(prefix);
748
748
  }
749
+ if (ls.reviewerCostUsd) {
750
+ totalCost += ls.reviewerCostUsd;
751
+ }
749
752
  }
750
753
 
751
754
  // Supplementary: add cost from telemetry for uncovered lanes only
@@ -19,6 +19,7 @@
19
19
 
20
20
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
21
21
  import { DynamicBorder } from "@mariozechner/pi-coding-agent";
22
+ import { Type } from "@mariozechner/pi-ai";
22
23
  import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
23
24
  import { spawn, spawnSync } from "child_process";
24
25
  import {
@@ -128,8 +129,17 @@ interface TaskState {
128
129
  workerExitDiagnostic: TaskExitDiagnostic | null;
129
130
  reviewerStatus: "idle" | "running" | "done" | "error";
130
131
  reviewerType: string;
132
+ reviewerStep: number;
133
+ reviewerSessionName: string;
131
134
  reviewerElapsed: number;
132
135
  reviewerLastTool: string;
136
+ reviewerToolCount: number;
137
+ reviewerInputTokens: number;
138
+ reviewerOutputTokens: number;
139
+ reviewerCacheReadTokens: number;
140
+ reviewerCacheWriteTokens: number;
141
+ reviewerCostUsd: number;
142
+ reviewerContextPct: number;
133
143
  reviewerProc: any;
134
144
  reviewerTimer: any;
135
145
  reviewCounter: number;
@@ -146,8 +156,10 @@ function freshState(): TaskState {
146
156
  workerProc: null, workerTimer: null,
147
157
  workerRetryActive: false, workerRetryCount: 0, workerLastRetryError: "",
148
158
  workerExitDiagnostic: null,
149
- reviewerStatus: "idle", reviewerType: "", reviewerElapsed: 0,
150
- reviewerLastTool: "", reviewerProc: null, reviewerTimer: null,
159
+ reviewerStatus: "idle", reviewerType: "", reviewerStep: 0, reviewerSessionName: "",
160
+ reviewerElapsed: 0, reviewerLastTool: "", reviewerToolCount: 0,
161
+ reviewerInputTokens: 0, reviewerOutputTokens: 0, reviewerCacheReadTokens: 0, reviewerCacheWriteTokens: 0,
162
+ reviewerCostUsd: 0, reviewerContextPct: 0, reviewerProc: null, reviewerTimer: null,
151
163
  reviewCounter: 0, totalIterations: 0, stepStatuses: new Map(),
152
164
  };
153
165
  }
@@ -162,7 +174,7 @@ const DEFAULT_CONFIG: TaskConfig = {
162
174
  standards_overrides: {},
163
175
  task_areas: {},
164
176
  worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
165
- reviewer: { model: "openai/gpt-5.3-codex", tools: "read,bash,grep,find,ls", thinking: "on" },
177
+ reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
166
178
  context: {
167
179
  worker_context_window: 0, warn_percent: 85, kill_percent: 95,
168
180
  max_worker_iterations: 20, max_review_cycles: 2, no_progress_limit: 3,
@@ -408,6 +420,18 @@ function writeLaneState(state: TaskState): void {
408
420
  workerLastRetryError: state.workerLastRetryError,
409
421
  workerExitDiagnostic: state.workerExitDiagnostic || undefined,
410
422
  reviewerStatus: state.reviewerStatus || "idle",
423
+ reviewerSessionName: state.reviewerSessionName || "",
424
+ reviewerType: state.reviewerType || "",
425
+ reviewerStep: state.reviewerStep || 0,
426
+ reviewerElapsed: state.reviewerElapsed || 0,
427
+ reviewerContextPct: state.reviewerContextPct || 0,
428
+ reviewerLastTool: state.reviewerLastTool || "",
429
+ reviewerToolCount: state.reviewerToolCount || 0,
430
+ reviewerCostUsd: state.reviewerCostUsd || 0,
431
+ reviewerInputTokens: state.reviewerInputTokens || 0,
432
+ reviewerOutputTokens: state.reviewerOutputTokens || 0,
433
+ reviewerCacheReadTokens: state.reviewerCacheReadTokens || 0,
434
+ reviewerCacheWriteTokens: state.reviewerCacheWriteTokens || 0,
411
435
  timestamp: Date.now(),
412
436
  };
413
437
  writeFileSync(filePath, JSON.stringify(data) + "\n");
@@ -1540,6 +1564,9 @@ function spawnAgentTmux(opts: {
1540
1564
  tools: string;
1541
1565
  thinking: string;
1542
1566
  taskId?: string;
1567
+ /** Optional extension paths to load in the spawned pi session (via rpc-wrapper --extensions).
1568
+ * When provided, --no-extensions is NOT passed to pi (would conflict). */
1569
+ extensions?: string[];
1543
1570
  /** Called on each poll tick with accumulated telemetry from the sidecar JSONL.
1544
1571
  * Enables the tmux poll loop to update TaskState (tokens, cost, context%, tools, retries)
1545
1572
  * with the same signals that subprocess mode gets from onTokenUpdate/onContextPct/onToolCall. */
@@ -1652,12 +1679,20 @@ function spawnAgentTmux(opts: {
1652
1679
  "--system-prompt-file", quoteArg(sysTmpFile),
1653
1680
  "--prompt-file", quoteArg(promptTmpFile),
1654
1681
  "--tools", quoteArg(opts.tools),
1655
- // Passthrough pi args: flags forwarded to the underlying pi --mode rpc process.
1656
- // Note: --no-session is NOT passed here β€” rpc-wrapper.mjs already injects it.
1657
- "--",
1658
- "--thinking", quoteArg(opts.thinking),
1659
- "--no-extensions", "--no-skills",
1660
1682
  ];
1683
+ // When extensions are provided, pass them to rpc-wrapper (which translates to `pi -e`)
1684
+ // and do NOT pass --no-extensions (would conflict).
1685
+ if (opts.extensions && opts.extensions.length > 0) {
1686
+ wrapperArgs.push("--extensions", quoteArg(opts.extensions.join(",")));
1687
+ }
1688
+ // Passthrough pi args: flags forwarded to the underlying pi --mode rpc process.
1689
+ // Note: --no-session is NOT passed here β€” rpc-wrapper.mjs already injects it.
1690
+ wrapperArgs.push("--");
1691
+ wrapperArgs.push("--thinking", quoteArg(opts.thinking));
1692
+ if (!opts.extensions || opts.extensions.length === 0) {
1693
+ wrapperArgs.push("--no-extensions");
1694
+ }
1695
+ wrapperArgs.push("--no-skills");
1661
1696
  const wrapperCommand = wrapperArgs.join(" ");
1662
1697
 
1663
1698
  // ── Handle stale session ─────────────────────────────────────────
@@ -1957,6 +1992,262 @@ export default function (pi: ExtensionAPI) {
1957
1992
  });
1958
1993
  }
1959
1994
 
1995
+ // ── review_step Tool (orchestrated mode only) ───────────────────
1996
+
1997
+ /**
1998
+ * Reset reviewer telemetry fields on state to idle/zero.
1999
+ * Called after a review completes to clear dashboard metrics.
2000
+ */
2001
+ function clearReviewerState(): void {
2002
+ state.reviewerStatus = "idle";
2003
+ state.reviewerType = "";
2004
+ state.reviewerStep = 0;
2005
+ state.reviewerSessionName = "";
2006
+ state.reviewerElapsed = 0;
2007
+ state.reviewerLastTool = "";
2008
+ state.reviewerToolCount = 0;
2009
+ state.reviewerInputTokens = 0;
2010
+ state.reviewerOutputTokens = 0;
2011
+ state.reviewerCacheReadTokens = 0;
2012
+ state.reviewerCacheWriteTokens = 0;
2013
+ state.reviewerCostUsd = 0;
2014
+ state.reviewerContextPct = 0;
2015
+ state.reviewerProc = null;
2016
+ if (state.reviewerTimer) clearInterval(state.reviewerTimer);
2017
+ state.reviewerTimer = null;
2018
+ }
2019
+
2020
+ if (isOrchestratedMode()) {
2021
+ pi.registerTool({
2022
+ name: "review_step",
2023
+ label: "Review Step",
2024
+ description:
2025
+ "Spawn a reviewer agent to evaluate your work on a step. " +
2026
+ "Returns APPROVE, REVISE, RETHINK, or UNAVAILABLE. " +
2027
+ "Use at step boundaries based on the task's review level.",
2028
+ promptSnippet: "review_step(step, type) β€” spawn reviewer for a step (plan/code review)",
2029
+ promptGuidelines: [
2030
+ "Call review_step at step boundaries based on the task's Review Level (from STATUS.md header).",
2031
+ "Review Level 0: skip all reviews. Level 1: plan review before implementing. Level 2: plan + code review. Level 3: plan + code + test review.",
2032
+ "Skip reviews for Step 0 (Preflight) and the final documentation/delivery step.",
2033
+ "For code reviews: before starting a step, capture the current HEAD commit with `git rev-parse HEAD` and pass it as the `baseline` parameter. This lets the reviewer see only that step's changes.",
2034
+ "On REVISE: read the review file in .reviews/ for detailed feedback, address the issues, commit fixes, then proceed.",
2035
+ "On RETHINK: reconsider your plan approach, adjust, then implement.",
2036
+ "On UNAVAILABLE: reviewer failed β€” proceed with caution.",
2037
+ ],
2038
+ parameters: Type.Object({
2039
+ step: Type.Number({ description: "Step number to review" }),
2040
+ type: Type.Union(
2041
+ [Type.Literal("plan"), Type.Literal("code")],
2042
+ { description: 'Review type: "plan" or "code"' },
2043
+ ),
2044
+ baseline: Type.Optional(Type.String({
2045
+ description: "Git commit SHA to use as the diff baseline for code reviews. " +
2046
+ "Capture HEAD before starting a step and pass it here so the reviewer " +
2047
+ "sees only that step's changes. If omitted, the reviewer sees the full diff against HEAD.",
2048
+ })),
2049
+ }),
2050
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2051
+ const { step: stepNum, type: reviewType, baseline } = params;
2052
+
2053
+ if (!state.task || !state.config) {
2054
+ return {
2055
+ content: [{ type: "text" as const, text: "UNAVAILABLE β€” no task loaded" }],
2056
+ details: undefined,
2057
+ };
2058
+ }
2059
+
2060
+ const task = state.task;
2061
+ const config = state.config;
2062
+ const statusPath = join(task.taskFolder, "STATUS.md");
2063
+ const reviewsDir = join(task.taskFolder, ".reviews");
2064
+ if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
2065
+
2066
+ // Low-risk step check (safety net β€” worker template also skips)
2067
+ if (isLowRiskStep(stepNum, task.steps.length)) {
2068
+ const label = stepNum === 0 ? "Preflight" : "final step";
2069
+ logExecution(statusPath, `Skip ${reviewType} review`, `Step ${stepNum} (${label}) β€” low-risk`);
2070
+ return {
2071
+ content: [{ type: "text" as const, text: `APPROVE β€” Step ${stepNum} is low-risk (${label}), review skipped` }],
2072
+ details: undefined,
2073
+ };
2074
+ }
2075
+
2076
+ // Increment review counter
2077
+ state.reviewCounter++;
2078
+ const num = String(state.reviewCounter).padStart(3, "0");
2079
+ const requestPath = join(reviewsDir, `request-R${num}.md`);
2080
+ const outputPath = join(reviewsDir, `R${num}-${reviewType}-step${stepNum}.md`);
2081
+
2082
+ // 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
+ const stepBaselineCommit: string | undefined =
2087
+ reviewType === "code" ? (baseline || undefined) : undefined;
2088
+
2089
+ // Find step info for the name
2090
+ const stepInfo = task.steps.find(s => s.number === stepNum);
2091
+ const stepName = stepInfo?.name || `Step ${stepNum}`;
2092
+
2093
+ // Generate review request
2094
+ const request = generateReviewRequest(
2095
+ reviewType, stepNum, stepName, task, config, outputPath, stepBaselineCommit,
2096
+ );
2097
+ writeFileSync(requestPath, request);
2098
+
2099
+ // Load reviewer agent definition
2100
+ const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2101
+ const reviewerModel = config.reviewer.model
2102
+ || reviewerDef?.model
2103
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2104
+ const reviewerPrompt = reviewerDef?.systemPrompt
2105
+ || "You are a code reviewer. Read the request and write your review to the specified output file.";
2106
+ const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
2107
+
2108
+ // Update state for dashboard visibility
2109
+ const sessionName = `${getTmuxPrefix()}-reviewer`;
2110
+ state.reviewerStatus = "running";
2111
+ state.reviewerType = `${reviewType} review`;
2112
+ state.reviewerStep = stepNum;
2113
+ state.reviewerSessionName = sessionName;
2114
+ state.reviewerElapsed = 0;
2115
+ state.reviewerLastTool = "";
2116
+ state.reviewerToolCount = 0;
2117
+ state.reviewerInputTokens = 0;
2118
+ state.reviewerOutputTokens = 0;
2119
+ state.reviewerCacheReadTokens = 0;
2120
+ state.reviewerCacheWriteTokens = 0;
2121
+ state.reviewerCostUsd = 0;
2122
+ state.reviewerContextPct = 0;
2123
+ updateWidgets();
2124
+
2125
+ const startTime = Date.now();
2126
+ state.reviewerTimer = setInterval(() => {
2127
+ state.reviewerElapsed = Date.now() - startTime;
2128
+ updateWidgets();
2129
+ }, 1000);
2130
+
2131
+ // Read the request file content as the prompt
2132
+ const promptContent = readFileSync(requestPath, "utf-8");
2133
+
2134
+ // Resolve context window for reviewer context% calculation
2135
+ const { contextWindow } = resolveContextWindow(config, ctx);
2136
+
2137
+ try {
2138
+ // Spawn reviewer via spawnAgentTmux with onTelemetry for live metrics
2139
+ const spawned = spawnAgentTmux({
2140
+ sessionName,
2141
+ cwd: ctx.cwd,
2142
+ systemPrompt,
2143
+ prompt: promptContent,
2144
+ model: reviewerModel,
2145
+ tools: config.reviewer.tools || reviewerDef?.tools || "read,write,bash,grep,find,ls",
2146
+ thinking: config.reviewer.thinking || "on",
2147
+ taskId: task.taskId,
2148
+ onTelemetry: (delta) => {
2149
+ // Accumulate tokens and cost
2150
+ state.reviewerInputTokens += delta.inputTokens;
2151
+ state.reviewerOutputTokens += delta.outputTokens;
2152
+ state.reviewerCacheReadTokens += delta.cacheReadTokens;
2153
+ state.reviewerCacheWriteTokens += delta.cacheWriteTokens;
2154
+ state.reviewerCostUsd += delta.cost;
2155
+
2156
+ // Tool tracking
2157
+ state.reviewerToolCount += delta.toolCalls;
2158
+ if (delta.lastTool) {
2159
+ state.reviewerLastTool = delta.lastTool;
2160
+ }
2161
+
2162
+ // Context %
2163
+ if (delta.latestTotalTokens > 0 && contextWindow > 0) {
2164
+ state.reviewerContextPct = (delta.latestTotalTokens / contextWindow) * 100;
2165
+ }
2166
+
2167
+ writeLaneState(state);
2168
+ updateWidgets();
2169
+ },
2170
+ });
2171
+
2172
+ state.reviewerProc = { kill: spawned.kill };
2173
+
2174
+ // Await reviewer completion
2175
+ const result = await spawned.promise;
2176
+
2177
+ clearInterval(state.reviewerTimer);
2178
+ state.reviewerElapsed = Date.now() - startTime;
2179
+ state.reviewerStatus = result.exitCode === 0 ? "done" : "error";
2180
+ state.reviewerProc = null;
2181
+ writeLaneState(state);
2182
+ updateWidgets();
2183
+
2184
+ // Extract verdict from review output
2185
+ let verdict = "UNKNOWN";
2186
+ let reviseDetails = "";
2187
+ if (existsSync(outputPath)) {
2188
+ const review = readFileSync(outputPath, "utf-8");
2189
+ verdict = extractVerdict(review);
2190
+ if (verdict === "REVISE") {
2191
+ // Extract a brief summary from the review for the worker
2192
+ const summaryMatch = review.match(/###?\s*Summary[:\s]*([\s\S]*?)(?=###|$)/i);
2193
+ reviseDetails = summaryMatch
2194
+ ? summaryMatch[1].trim().slice(0, 500)
2195
+ : "See review file for details.";
2196
+ }
2197
+ } else {
2198
+ verdict = "UNAVAILABLE";
2199
+ logExecution(statusPath, `Reviewer R${num}`,
2200
+ `${reviewType} review β€” reviewer did not produce output`);
2201
+ }
2202
+
2203
+ // Log the review in STATUS.md
2204
+ logReview(statusPath, `R${num}`, reviewType, stepNum, verdict,
2205
+ `.reviews/R${num}-${reviewType}-step${stepNum}.md`);
2206
+ logExecution(statusPath, `Review R${num}`,
2207
+ `${reviewType} Step ${stepNum}: ${verdict}`);
2208
+ updateStatusField(statusPath, "Review Counter", `${state.reviewCounter}`);
2209
+
2210
+ // Clear reviewer state for dashboard
2211
+ clearReviewerState();
2212
+ writeLaneState(state);
2213
+ updateWidgets();
2214
+
2215
+ // Return verdict to the worker
2216
+ let resultText: string;
2217
+ if (verdict === "APPROVE") {
2218
+ resultText = "APPROVE";
2219
+ } else if (verdict === "REVISE") {
2220
+ resultText = `REVISE: ${reviseDetails}\n\nFull review: .reviews/R${num}-${reviewType}-step${stepNum}.md`;
2221
+ } else if (verdict === "RETHINK") {
2222
+ resultText = `RETHINK β€” reconsider your approach. See .reviews/R${num}-${reviewType}-step${stepNum}.md`;
2223
+ } else {
2224
+ resultText = `UNAVAILABLE β€” reviewer did not produce a usable verdict.`;
2225
+ }
2226
+
2227
+ return {
2228
+ content: [{ type: "text" as const, text: resultText }],
2229
+ details: undefined,
2230
+ };
2231
+ } catch (err: any) {
2232
+ // Reviewer crashed
2233
+ clearInterval(state.reviewerTimer);
2234
+ clearReviewerState();
2235
+ state.reviewerStatus = "error";
2236
+ writeLaneState(state);
2237
+ updateWidgets();
2238
+
2239
+ logExecution(statusPath, `Reviewer R${num}`,
2240
+ `${reviewType} review β€” reviewer crashed: ${err?.message || err}`);
2241
+
2242
+ return {
2243
+ content: [{ type: "text" as const, text: `UNAVAILABLE β€” reviewer error: ${err?.message || err}` }],
2244
+ details: undefined,
2245
+ };
2246
+ }
2247
+ },
2248
+ });
2249
+ }
2250
+
1960
2251
  // ── Execution Engine ─────────────────────────────────────────────
1961
2252
 
1962
2253
  async function executeTask(ctx: ExtensionContext): Promise<void> {
@@ -1972,27 +2263,12 @@ export default function (pi: ExtensionAPI) {
1972
2263
 
1973
2264
  // ── Per-task worker loop ─────────────────────────────────────
1974
2265
  // Spawn one worker per iteration; each worker handles ALL remaining
1975
- // steps. Reviews run after the worker exits, per newly-completed step.
2266
+ // steps. The worker drives reviews inline via the review_step tool
2267
+ // (in orchestrated mode) β€” no deferred reviews after worker exit.
1976
2268
  // If context limit is hit mid-task, the next iteration picks up from
1977
2269
  // the first incomplete step via STATUS.md β€” same recovery mechanism.
1978
2270
 
1979
- // Collect baseline commits per step (for code review diffs).
1980
- // Baselines are updated at step boundaries so each code review
1981
- // sees only that step's changes, not cumulative diffs.
1982
- const stepBaselineCommits = new Map<number, string>();
1983
-
1984
- // Track steps that received a REVISE verdict and need rework.
1985
- // This prevents the checkbox-count heuristic from re-completing them
1986
- // before the worker has had a chance to address reviewer feedback.
1987
- const needsRework = new Set<number>();
1988
-
1989
- // Track which steps have already received a plan review so we don't
1990
- // re-run plan review on rework cycles (only code review reruns).
1991
- const planReviewedSteps = new Set<number>();
1992
-
1993
- // Mark all incomplete steps as in-progress and capture the baseline
1994
- // commit for the first one. Reviews are transition-based: they run
1995
- // when a step newly completes after the worker exits, not up-front.
2271
+ // Mark all incomplete steps as in-progress
1996
2272
  {
1997
2273
  const currentStatus = parseStatusMd(readFileSync(statusPath, "utf-8"));
1998
2274
  for (const step of task.steps) {
@@ -2002,23 +2278,12 @@ export default function (pi: ExtensionAPI) {
2002
2278
  // Mark step as in-progress and log its start
2003
2279
  updateStepStatus(statusPath, step.number, "in-progress");
2004
2280
  logExecution(statusPath, `Step ${step.number} started`, step.name);
2005
-
2006
- // Capture baseline commit for the FIRST incomplete step only.
2007
- // Later steps get their baselines when the prior step completes
2008
- // (see the newlyCompleted handler in the iteration loop).
2009
- // This prevents cross-step diff bleeding in code reviews.
2010
- if (!stepBaselineCommits.size) {
2011
- stepBaselineCommits.set(step.number, getHeadCommitSha());
2012
- }
2013
2281
  }
2014
2282
  }
2015
2283
 
2016
2284
  // Helper: determine if a parsed step is complete.
2017
- // A step in needsRework (set after REVISE) is never complete, even if
2018
- // all checkboxes are checked β€” the worker must address the feedback first.
2019
2285
  function isStepComplete(ss: StepInfo | undefined): boolean {
2020
2286
  if (!ss) return false;
2021
- if (needsRework.has(ss.number)) return false;
2022
2287
  if (ss.status === "complete") return true;
2023
2288
  // Fallback: infer from checkboxes (covers "in-progress" and "not-started")
2024
2289
  return ss.totalChecked === ss.totalItems && ss.totalItems > 0;
@@ -2058,13 +2323,6 @@ export default function (pi: ExtensionAPI) {
2058
2323
  if (isStepComplete(ss)) completedBefore.add(ss.number);
2059
2324
  }
2060
2325
 
2061
- // Ensure the first remaining step has a baseline. On subsequent
2062
- // iterations (after context-limit recovery), the first remaining step
2063
- // may not have a baseline yet if it wasn't the first step originally.
2064
- if (remainingSteps.length > 0 && !stepBaselineCommits.has(remainingSteps[0].number)) {
2065
- stepBaselineCommits.set(remainingSteps[0].number, getHeadCommitSha());
2066
- }
2067
-
2068
2326
  await runWorker(remainingSteps, ctx);
2069
2327
 
2070
2328
  if (state.phase === "error") return;
@@ -2090,49 +2348,17 @@ export default function (pi: ExtensionAPI) {
2090
2348
  }
2091
2349
 
2092
2350
  // Find newly completed steps.
2093
- // For steps in needsRework, the worker must have addressed the
2094
- // reviewer feedback β€” we detect this by checking if STATUS.md was
2095
- // updated (the worker adds/checks revision items). We temporarily
2096
- // remove the step from needsRework to let isStepComplete() evaluate
2097
- // the checkbox state, then re-add if it's still not complete.
2098
2351
  const newlyCompleted: StepInfo[] = [];
2099
2352
  for (const step of task.steps) {
2100
2353
  if (completedBefore.has(step.number)) continue;
2101
2354
  const ss = afterStatus.steps.find(s => s.number === step.number);
2102
- if (needsRework.has(step.number)) {
2103
- // For rework steps, check if the worker set status to "complete"
2104
- // or if the step now has all checkboxes checked (worker addressed feedback)
2105
- if (ss?.status === "complete" ||
2106
- (ss && ss.totalChecked === ss.totalItems && ss.totalItems > 0)) {
2107
- needsRework.delete(step.number);
2108
- updateStepStatus(statusPath, step.number, "complete");
2109
- logExecution(statusPath, `Step ${step.number} complete`, `${step.name} (rework)`);
2110
- newlyCompleted.push(step);
2111
- }
2112
- } else if (isStepComplete(ss)) {
2355
+ if (isStepComplete(ss)) {
2113
2356
  updateStepStatus(statusPath, step.number, "complete");
2114
2357
  logExecution(statusPath, `Step ${step.number} complete`, step.name);
2115
2358
  newlyCompleted.push(step);
2116
2359
  }
2117
2360
  }
2118
2361
 
2119
- // Update baselines for subsequent steps using step boundary commits.
2120
- // When the worker completes multiple steps in one iteration, each step's
2121
- // commit becomes the baseline for the next step's code review.
2122
- if (newlyCompleted.length > 1) {
2123
- for (let i = 0; i < newlyCompleted.length; i++) {
2124
- const completedStep = newlyCompleted[i];
2125
- const boundaryCommit = findStepBoundaryCommit(
2126
- completedStep.number, task.taskId,
2127
- stepBaselineCommits.get(completedStep.number)
2128
- );
2129
- if (boundaryCommit && i + 1 < newlyCompleted.length) {
2130
- // Use this step's completion commit as the next step's baseline
2131
- stepBaselineCommits.set(newlyCompleted[i + 1].number, boundaryCommit);
2132
- }
2133
- }
2134
- }
2135
-
2136
2362
  // Log iteration summary with progress delta and completed steps
2137
2363
  const completedNames = newlyCompleted.map(s => `Step ${s.number}`).join(", ");
2138
2364
  if (newlyCompleted.length > 0) {
@@ -2143,57 +2369,8 @@ export default function (pi: ExtensionAPI) {
2143
2369
  ctx.ui.notify(`Iteration ${iter + 1}: +${progressDelta} checkboxes (no steps fully completed)`, "info");
2144
2370
  }
2145
2371
 
2146
- // ── Run reviews for newly completed steps (transition-based) ──
2147
- // Plan reviews run once per step (first completion); code reviews
2148
- // run on every completion (including rework). Both respect review
2149
- // level and low-risk skip logic.
2150
- // Gate on phase !== "error" so reviews still run when paused
2151
- // (pause is honored after reviews, before next iteration).
2152
- if (state.phase !== "error") {
2153
- for (const step of newlyCompleted) {
2154
- const lowRisk = isLowRiskStep(step.number, task.steps.length);
2155
-
2156
- // ── Plan review (level β‰₯ 1, first completion only) ──
2157
- if (task.reviewLevel >= 1 && !planReviewedSteps.has(step.number)) {
2158
- if (lowRisk) {
2159
- const label = step.number === 0 ? "Preflight" : "final step";
2160
- logExecution(statusPath, `Skip plan review`, `Step ${step.number} (${label}) β€” low-risk`);
2161
- ctx.ui.notify(`⏭️ Skipping plan review for Step ${step.number} (${label})`, "info");
2162
- } else {
2163
- const verdict = await doReview("plan", step, ctx);
2164
- if (verdict === "RETHINK") {
2165
- ctx.ui.notify(`Reviewer: RETHINK on Step ${step.number} plan. Proceeding with caution.`, "warning");
2166
- }
2167
- }
2168
- planReviewedSteps.add(step.number);
2169
- }
2170
-
2171
- // ── Code review (level β‰₯ 2) ──
2172
- if (task.reviewLevel >= 2) {
2173
- if (lowRisk) {
2174
- const label = step.number === 0 ? "Preflight" : "final step";
2175
- logExecution(statusPath, `Skip code review`, `Step ${step.number} (${label}) β€” low-risk`);
2176
- ctx.ui.notify(`⏭️ Skipping code review for Step ${step.number} (${label})`, "info");
2177
- } else {
2178
- const baseline = stepBaselineCommits.get(step.number);
2179
- const verdict = await doReview("code", step, ctx, baseline);
2180
- if (verdict === "REVISE") {
2181
- ctx.ui.notify(`Reviewer: REVISE on Step ${step.number}. Will rework in next iteration.`, "warning");
2182
- // Mark step as needing rework β€” undo the "complete" status.
2183
- // needsRework ensures isStepComplete() won't re-complete this
2184
- // step based on checkbox counts alone; the worker must address
2185
- // the reviewer feedback first, which will update STATUS.md and
2186
- // clear the rework flag when the step is newly completed again.
2187
- needsRework.add(step.number);
2188
- updateStepStatus(statusPath, step.number, "in-progress");
2189
- // Update baseline so the next code review for this step
2190
- // diffs only the rework changes, not the original step work
2191
- stepBaselineCommits.set(step.number, getHeadCommitSha());
2192
- }
2193
- }
2194
- }
2195
- }
2196
- }
2372
+ // Reviews are now driven inline by the worker via the review_step
2373
+ // tool (orchestrated mode). No deferred review logic here.
2197
2374
 
2198
2375
  // Update local cache
2199
2376
  const refreshed = parseStatusMd(readFileSync(statusPath, "utf-8"));
@@ -2727,7 +2904,7 @@ export default function (pi: ExtensionAPI) {
2727
2904
  writeFileSync(requestPath, request);
2728
2905
 
2729
2906
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2730
- const reviewerModel = config.reviewer.model || reviewerDef?.model || "openai/gpt-5.3-codex";
2907
+ const reviewerModel = config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2731
2908
  const reviewerPrompt = reviewerDef?.systemPrompt || "You are a code reviewer. Read the request and write your review to the specified output file.";
2732
2909
  const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
2733
2910
 
@@ -2866,7 +3043,7 @@ export default function (pi: ExtensionAPI) {
2866
3043
  const reviewModel = config.quality_gate.review_model
2867
3044
  || config.reviewer.model
2868
3045
  || reviewerDef?.model
2869
- || "openai/gpt-5.3-codex";
3046
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2870
3047
 
2871
3048
  const reviewerPrompt = reviewerDef?.systemPrompt
2872
3049
  || "You are a quality gate reviewer. Read the review request and write your JSON verdict to the specified file.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
4
4
  "description": "AI agent orchestration for pi β€” parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -16,6 +16,7 @@ name: task-worker
16
16
  - Multi-step execution (worker handles all remaining steps per invocation)
17
17
  - Iteration recovery (context limit β†’ next invocation resumes from STATUS.md)
18
18
  - Git commit conventions (per-step commits) and .DONE file creation
19
+ - Review protocol (inline reviews via review_step tool when available)
19
20
  - Review response handling
20
21
 
21
22
  Add project-specific rules below. Common examples:
@@ -158,6 +158,45 @@ When a reviewer returns REVISE with specific feedback items:
158
158
  - Do NOT expand task scope beyond what the steps require
159
159
  - If you discover something out of scope, note it in STATUS.md Discoveries table
160
160
 
161
+ ## Review Protocol
162
+
163
+ If you have access to a `review_step` tool, use it at step boundaries to spawn
164
+ a reviewer agent. The tool takes two parameters: `step` (number) and `type`
165
+ ("plan" or "code"). It returns a verdict string.
166
+
167
+ **When to call reviews** (based on Review Level from STATUS.md header):
168
+
169
+ - **Review Level 0 (None):** Skip all reviews.
170
+ - **Review Level 1 (Plan Only):** Before implementing each step, call
171
+ `review_step(step=N, type="plan")` to get plan feedback.
172
+ - **Review Level 2 (Plan + Code):** Plan review before implementing, then code
173
+ review after implementing and committing.
174
+ - **Review Level 3 (Full):** Plan + code + test reviews.
175
+
176
+ **Always skip reviews for:** Step 0 (Preflight) and the final step (typically
177
+ documentation/delivery). These are low-risk steps where review overhead exceeds
178
+ value.
179
+
180
+ **Handling verdicts:**
181
+ - **APPROVE** β†’ proceed to next step
182
+ - **RETHINK** β†’ reconsider your plan approach, adjust, then implement
183
+ - **REVISE** β†’ read the review file in `.reviews/` for detailed feedback,
184
+ address the issues, commit fixes, then proceed
185
+ - **UNAVAILABLE** β†’ reviewer failed, proceed with caution
186
+
187
+ **Example flow for a Review Level 2 task, Step 3:**
188
+ 1. Read Step 3 requirements
189
+ 2. Call `review_step(step=3, type="plan")` β†’ get plan feedback
190
+ 3. Capture baseline: run `git rev-parse HEAD` and save the SHA
191
+ 4. Implement Step 3
192
+ 5. Commit changes
193
+ 6. Call `review_step(step=3, type="code", baseline="<saved SHA>")` β†’ get code feedback
194
+ 7. If REVISE: fix issues, commit again
195
+ 8. Move to Step 4
196
+
197
+ If the `review_step` tool is not available (e.g., non-orchestrated mode), skip
198
+ this protocol entirely β€” the task-runner handles reviews externally.
199
+
161
200
  ## Self-Documentation
162
201
 
163
202
  You have standing permission to: