taskplane 0.25.6 → 0.25.8

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.
@@ -79,7 +79,7 @@ function mergeV2LaneSnapshot(legacyLs, v2snap) {
79
79
  if (w) {
80
80
  // Map V2 agent status to legacy dashboard status strings
81
81
  if (w.status) {
82
- const statusMap = { running: 'running', spawning: 'running', exited: 'done', crashed: 'error', killed: 'error', timed_out: 'error', wrapping_up: 'running' };
82
+ const statusMap = { running: 'running', spawning: 'running', exited: 'done', crashed: 'error', killed: 'done', timed_out: 'error', wrapping_up: 'running' };
83
83
  base.workerStatus = statusMap[w.status] || w.status;
84
84
  }
85
85
  if (w.elapsedMs != null) base.workerElapsed = w.elapsedMs;
@@ -1062,9 +1062,15 @@ function renderAgentsPanel(registry) {
1062
1062
  let html = '<div class="agents-grid">';
1063
1063
 
1064
1064
  for (const agent of agents) {
1065
+ const isCrash = ['crashed', 'timed_out'].includes(agent.status);
1065
1066
  const isTerminal = ['exited', 'crashed', 'timed_out', 'killed'].includes(agent.status);
1066
- const statusClass = isTerminal ? 'agent-terminal' : 'agent-live';
1067
- const icon = isTerminal ? '\u{1F534}' : '\u{1F7E2}';
1067
+ const statusClass = isTerminal ? (isCrash ? 'agent-terminal agent-crashed' : 'agent-terminal') : 'agent-live';
1068
+ const icon = isCrash ? '\u{1F534}' : (isTerminal ? '\u26AA' : '\u{1F7E2}');
1069
+ // Display label: exited and killed both show as 'shutdown' — the mechanism is an
1070
+ // implementation detail. Only crashed/timed_out warrant a different label.
1071
+ const displayStatus = (agent.status === 'exited' || agent.status === 'killed') ? 'shutdown'
1072
+ : agent.status === 'timed_out' ? 'timed out'
1073
+ : agent.status;
1068
1074
  const elapsed = agent.startedAt ? Math.round((Date.now() - agent.startedAt) / 1000) : 0;
1069
1075
  const elapsedStr = elapsed > 0 ? formatDuration(elapsed * 1000) : '';
1070
1076
 
@@ -1074,7 +1080,7 @@ function renderAgentsPanel(registry) {
1074
1080
  html += `<span class="agent-badge">${escapeHtml(agent.role)}</span>`;
1075
1081
  if (agent.laneNumber != null) html += `<span class="agent-badge">lane ${agent.laneNumber}</span>`;
1076
1082
  if (agent.taskId) html += `<span class="agent-badge">${escapeHtml(agent.taskId)}</span>`;
1077
- html += `<span class="agent-badge agent-status-${agent.status}">${escapeHtml(agent.status)}</span>`;
1083
+ html += `<span class="agent-badge agent-status-${agent.status}">${escapeHtml(displayStatus)}</span>`;
1078
1084
  if (elapsedStr && !isTerminal) html += `<span class="agent-badge">${elapsedStr}</span>`;
1079
1085
  html += `</div>`;
1080
1086
  html += `</div>`;
@@ -817,8 +817,9 @@ body {
817
817
  .worker-last-tool {
818
818
  color: var(--text-muted);
819
819
  max-width: 600px;
820
- overflow: hidden;
821
- text-overflow: ellipsis;
820
+ overflow-wrap: break-word;
821
+ word-break: break-all;
822
+ white-space: normal;
822
823
  }
823
824
 
824
825
  /* ─── Reviewer Sub-Row ─────────────────────────────────────────────── */
@@ -1774,10 +1775,10 @@ body {
1774
1775
  }
1775
1776
  .agent-status-running { color: var(--green); }
1776
1777
  .agent-status-spawning { color: var(--yellow); }
1777
- .agent-status-exited { color: var(--text-faint); }
1778
+ .agent-status-exited { color: var(--text-faint); } /* displayed as 'shutdown' */
1779
+ .agent-status-killed { color: var(--text-faint); } /* displayed as 'shutdown' */
1778
1780
  .agent-status-crashed { color: var(--red); }
1779
- .agent-status-killed { color: var(--red); }
1780
- .agent-status-timed_out { color: var(--yellow); }
1781
+ .agent-status-timed_out { color: var(--red); }
1781
1782
 
1782
1783
  /* TP-107: Messages Panel */
1783
1784
  .messages-list {
@@ -194,9 +194,21 @@ function parseStatusMd(taskFolder) {
194
194
  }
195
195
 
196
196
  function getActiveSessions() {
197
- // Runtime V2 no longer relies on external terminal multiplexers for liveness.
198
- // Dashboard session data is driven by runtime registry + persisted state.
199
- return [];
197
+ // Runtime V2: return active merger session names from the runtime registry
198
+ // so the dashboard merge pane can display live telemetry for running agents.
199
+ // Terminal statuses indicate the agent is no longer alive.
200
+ const TERMINAL_STATUSES = new Set(["exited", "killed", "crashed", "timed_out"]);
201
+ try {
202
+ const state = loadBatchState();
203
+ if (!state || !state.batchId) return [];
204
+ const registry = loadRuntimeRegistry(state.batchId);
205
+ if (!registry || !registry.agents) return [];
206
+ return Object.values(registry.agents)
207
+ .filter(a => a.role === "merger" && !TERMINAL_STATUSES.has(a.status))
208
+ .map(a => a.agentId);
209
+ } catch {
210
+ return [];
211
+ }
200
212
  }
201
213
 
202
214
  function checkDoneFile(taskFolder) {
@@ -406,6 +418,33 @@ function loadRuntimeLaneSnapshots(batchId) {
406
418
  return snapshots;
407
419
  }
408
420
 
421
+ /**
422
+ * Load Runtime V2 merge agent snapshots for the current batch.
423
+ *
424
+ * Reads all `merge-N.json` files from `.pi/runtime/{batchId}/lanes/`.
425
+ * Returns a map of mergeNumber (string) → snapshot data.
426
+ *
427
+ * Follows the same pattern as {@link loadRuntimeLaneSnapshots}.
428
+ *
429
+ * @since TP-164
430
+ */
431
+ function loadRuntimeMergeSnapshots(batchId) {
432
+ if (!batchId) return {};
433
+ const lanesDir = path.join(REPO_ROOT, ".pi", "runtime", batchId, "lanes");
434
+ const snapshots = {};
435
+ try {
436
+ if (!fs.existsSync(lanesDir)) return snapshots;
437
+ const files = fs.readdirSync(lanesDir).filter(f => f.startsWith("merge-") && f.endsWith(".json"));
438
+ for (const file of files) {
439
+ try {
440
+ const data = JSON.parse(fs.readFileSync(path.join(lanesDir, file), "utf-8"));
441
+ if (data.mergeNumber != null) snapshots[data.mergeNumber] = data;
442
+ } catch { continue; }
443
+ }
444
+ } catch { /* dir missing */ }
445
+ return snapshots;
446
+ }
447
+
409
448
  /**
410
449
  * Load Runtime V2 agent events for a specific agent.
411
450
  * Returns the last N events from the agent's events.jsonl.
@@ -1077,6 +1116,47 @@ function buildDashboardState() {
1077
1116
  const runtimeLaneSnapshots = loadRuntimeLaneSnapshots(state.batchId);
1078
1117
  const mailboxData = loadMailboxData(state.batchId);
1079
1118
 
1119
+ // TP-164: Load merge agent snapshots for live dashboard telemetry.
1120
+ const runtimeMergeSnapshots = loadRuntimeMergeSnapshots(state.batchId);
1121
+
1122
+ // TP-164: Inject merge snapshot telemetry into the telemetry map so
1123
+ // `telemetry[sessionName]` resolves for the merge pane.
1124
+ // Snapshots are the PRIMARY source for merge telemetry — merge agents don't
1125
+ // write to .pi/telemetry/*.jsonl (the JSONL path is for lane workers only).
1126
+ // Inject snapshot data when no JSONL-backed entry exists for this session.
1127
+ for (const snap of Object.values(runtimeMergeSnapshots)) {
1128
+ const key = snap.sessionName;
1129
+ if (!key) continue;
1130
+ const agent = snap.agent;
1131
+ if (!agent) continue;
1132
+ // Only inject if there is no existing JSONL-backed telemetry entry.
1133
+ // Merge agents don't emit to .pi/telemetry/, so existing will always be
1134
+ // absent unless something else wrote to it — in that case defer to it.
1135
+ const existing = telemetry[key];
1136
+ if (!existing) {
1137
+ telemetry[key] = {
1138
+ inputTokens: agent.inputTokens || 0,
1139
+ outputTokens: agent.outputTokens || 0,
1140
+ cacheReadTokens: agent.cacheReadTokens || 0,
1141
+ cacheWriteTokens: agent.cacheWriteTokens || 0,
1142
+ cost: agent.costUsd || 0,
1143
+ toolCalls: agent.toolCalls || 0,
1144
+ lastTool: agent.lastTool || "",
1145
+ currentTool: snap.status === "running" ? (agent.lastTool || "") : "",
1146
+ contextPct: agent.contextPct || 0,
1147
+ // startedAt is not in the snapshot; compute from elapsed if possible.
1148
+ startedAt: agent.elapsedMs > 0 ? snap.updatedAt - agent.elapsedMs : snap.updatedAt,
1149
+ retries: 0,
1150
+ retryActive: false,
1151
+ lastRetryError: "",
1152
+ compactions: 0,
1153
+ latestTotalTokens: (agent.inputTokens || 0) + (agent.outputTokens || 0),
1154
+ _updatedAt: snap.updatedAt,
1155
+ _source: "merge-snapshot",
1156
+ };
1157
+ }
1158
+ }
1159
+
1080
1160
  // TP-115: Synthesize laneStates from V2 snapshots so the dashboard
1081
1161
  // pipeline works without legacy lane-state-*.json sidecar files.
1082
1162
  // V2 snapshots are authoritative when present.
@@ -1099,6 +1179,8 @@ function buildDashboardState() {
1099
1179
  // Runtime V2 data (null/empty for legacy batches)
1100
1180
  runtimeRegistry,
1101
1181
  runtimeLaneSnapshots,
1182
+ // TP-164: Merge agent snapshots for live dashboard telemetry.
1183
+ runtimeMergeSnapshots,
1102
1184
  mailbox: mailboxData,
1103
1185
  batch: {
1104
1186
  batchId: state.batchId,
@@ -429,12 +429,22 @@ export default function (pi: ExtensionAPI) {
429
429
  // Pre-clean stale reviewer state from prior interrupted review
430
430
  removeReviewerState(taskFolder);
431
431
  return new Promise((resolve) => {
432
+ // Read reviewer config from env vars set by lane-runner from runnerConfig.reviewer.
433
+ // Empty string means inherit from session default (no flag passed to pi CLI).
434
+ const reviewerModel = process.env.TASKPLANE_REVIEWER_MODEL || "";
435
+ const reviewerThinking = process.env.TASKPLANE_REVIEWER_THINKING || "";
436
+ // Fall back to the schema default reviewer tool list (read-only + bash/grep).
437
+ // Must match config-schema.ts reviewer.tools default to avoid capability expansion.
438
+ const reviewerTools = process.env.TASKPLANE_REVIEWER_TOOLS || "read,bash,grep,find,ls";
439
+
432
440
  const cliPath = resolvePiCliPath();
433
441
  const args = [
434
442
  cliPath, "--mode", "rpc", "--no-session", "--no-extensions", "--no-skills",
435
- "--tools", "read,write,edit,bash,grep,find,ls",
443
+ "--tools", reviewerTools,
436
444
  "--system-prompt", systemPrompt,
437
445
  ];
446
+ if (reviewerModel) args.push("--model", reviewerModel);
447
+ if (reviewerThinking) args.push("--thinking", reviewerThinking);
438
448
  const proc = nodeSpawn(process.execPath, args, {
439
449
  shell: false,
440
450
  cwd,
@@ -561,8 +561,8 @@ export function spawnAgent(
561
561
  case "tool_execution_start": {
562
562
  toolCalls++;
563
563
  const toolName = event.toolName || "tool";
564
- const argPreview = typeof event.args === "string" ? event.args.slice(0, 80) :
565
- (event.args && typeof Object.values(event.args)[0] === "string" ? String(Object.values(event.args)[0]).slice(0, 80) : "");
564
+ const argPreview = typeof event.args === "string" ? event.args.slice(0, 300) :
565
+ (event.args && typeof Object.values(event.args)[0] === "string" ? String(Object.values(event.args)[0]).slice(0, 300) : "");
566
566
  lastTool = argPreview ? `${toolName}: ${argPreview}` : toolName;
567
567
  // TP-111: Bounded payload only — no raw args in durable event log
568
568
  const toolPath = event.args?.path ? String(event.args.path).slice(0, 200) : "";
@@ -1177,6 +1177,11 @@ export function toTaskRunnerConfig(config: TaskplaneConfig): import("./types.ts"
1177
1177
  reference_docs: { ...config.taskRunner.referenceDocs },
1178
1178
  ...(hasTestingCommands ? { testing_commands: { ...testingCommands } } : {}),
1179
1179
  model_fallback: config.taskRunner.modelFallback ?? "inherit",
1180
+ reviewer: {
1181
+ model: config.taskRunner.reviewer.model,
1182
+ thinking: config.taskRunner.reviewer.thinking,
1183
+ tools: config.taskRunner.reviewer.tools,
1184
+ },
1180
1185
  };
1181
1186
  }
1182
1187
 
@@ -6,7 +6,7 @@ import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync } from "f
6
6
  import { join, resolve } from "path";
7
7
 
8
8
  import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
9
- import { computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents } from "./execution.ts";
9
+ import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents } from "./execution.ts";
10
10
  import type { RuntimeBackend } from "./execution.ts";
11
11
  import type { MonitorUpdateCallback } from "./execution.ts";
12
12
  // classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
@@ -1307,7 +1307,7 @@ async function attemptWorkerCrashRetry(
1307
1307
  retryPauseSignal,
1308
1308
  wsRoot,
1309
1309
  isWsMode,
1310
- { ORCH_BATCH_ID: batchState.batchId }, // TP-089: ensure mailbox works for retries
1310
+ { ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) }, // TP-089: ensure mailbox works for retries
1311
1311
  );
1312
1312
 
1313
1313
  const retryOutcome = retryResult.tasks[0];
@@ -1566,7 +1566,7 @@ async function attemptModelFallbackRetry(
1566
1566
  // Pass TASKPLANE_MODEL_FALLBACK=1 as extra env var to signal
1567
1567
  // the task-runner to use the session model instead of configured model.
1568
1568
  // TP-089: Also include ORCH_BATCH_ID so mailbox steering works for retries.
1569
- const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId };
1569
+ const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig?.reviewer) };
1570
1570
  const retryResult = await executeLaneV2(
1571
1571
  retryLane,
1572
1572
  orchConfig,
@@ -1706,6 +1706,7 @@ async function attemptStaleWorktreeRecovery(
1706
1706
  runtimeBackend?: RuntimeBackend,
1707
1707
  onSupervisorAlert?: SupervisorAlertCallback,
1708
1708
  supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
1709
+ runnerConfig?: TaskRunnerConfig,
1709
1710
  ): Promise<WaveExecutionResult | null> {
1710
1711
  // Only attempt recovery for ALLOC_WORKTREE_FAILED
1711
1712
  if (!waveResult.allocationError || waveResult.allocationError.code !== "ALLOC_WORKTREE_FAILED") {
@@ -1808,6 +1809,11 @@ async function attemptStaleWorktreeRecovery(
1808
1809
  runtimeBackend,
1809
1810
  onSupervisorAlert,
1810
1811
  supervisorAutonomy,
1812
+ {
1813
+ model: runnerConfig?.reviewer?.model || "",
1814
+ thinking: runnerConfig?.reviewer?.thinking || "",
1815
+ tools: runnerConfig?.reviewer?.tools || "",
1816
+ },
1811
1817
  );
1812
1818
 
1813
1819
  return retryResult;
@@ -2376,6 +2382,11 @@ export async function executeOrchBatch(
2376
2382
  selectedBackend,
2377
2383
  emitAlert,
2378
2384
  supervisorAutonomy,
2385
+ {
2386
+ model: runnerConfig?.reviewer?.model || "",
2387
+ thinking: runnerConfig?.reviewer?.thinking || "",
2388
+ tools: runnerConfig?.reviewer?.tools || "",
2389
+ },
2379
2390
  );
2380
2391
 
2381
2392
  // ── TP-039: Tier 0 — Stale worktree recovery ────────────
@@ -2398,6 +2409,7 @@ export async function executeOrchBatch(
2398
2409
  selectedBackend,
2399
2410
  emitAlert,
2400
2411
  supervisorAutonomy,
2412
+ runnerConfig,
2401
2413
  );
2402
2414
  if (retryResult) {
2403
2415
  const staleRecovered = !retryResult.allocationError;
@@ -1411,6 +1411,7 @@ export function ensureTaskFilesCommitted(
1411
1411
  pending: Map<string, ParsedTask>,
1412
1412
  repoRoot: string,
1413
1413
  waveIndex: number,
1414
+ orchBranch?: string,
1414
1415
  ): void {
1415
1416
  // Collect task folder paths for this wave
1416
1417
  const foldersToCheck: { taskId: string; relPath: string }[] = [];
@@ -1477,6 +1478,121 @@ export function ensureTaskFilesCommitted(
1477
1478
  folders: foldersToStage,
1478
1479
  commit: commitResult.stdout.trim().split("\n")[0],
1479
1480
  });
1481
+
1482
+ // Fast-forward (or merge) the orch branch to include the staging commit so
1483
+ // that worktrees—which branch from orchBranch—see the new task files and
1484
+ // workers can find their PROMPT.md / STATUS.md without an ENOENT crash.
1485
+ //
1486
+ // The orch branch was created from baseBranch before executeWave runs, so in
1487
+ // wave 1 it is always an ancestor of HEAD and a plain fast-forward applies.
1488
+ // In wave 2+ the orch branch may have advanced due to prior wave merges
1489
+ // (commits from worker worktrees merged back). In that case we create a merge
1490
+ // commit so the task files become visible without rewinding any wave history.
1491
+ //
1492
+ // Failure here is non-fatal: the commit already succeeded; if the ref update
1493
+ // fails the subsequent worktree allocation will produce a clear error.
1494
+ if (orchBranch) {
1495
+ try {
1496
+ const headRes = runGit(["rev-parse", "HEAD"], repoRoot);
1497
+ const orchTipRes = runGit(["rev-parse", `refs/heads/${orchBranch}`], repoRoot);
1498
+
1499
+ if (headRes.ok && orchTipRes.ok) {
1500
+ const newHead = headRes.stdout.trim();
1501
+ const orchTip = orchTipRes.stdout.trim();
1502
+
1503
+ // Check whether the orch branch tip is an ancestor of the new HEAD
1504
+ // (i.e., a fast-forward is safe and sufficient).
1505
+ const ancestorCheck = runGit(
1506
+ ["merge-base", "--is-ancestor", orchTip, newHead],
1507
+ repoRoot,
1508
+ );
1509
+
1510
+ if (ancestorCheck.ok) {
1511
+ // FF case: orch branch is behind HEAD — move it forward.
1512
+ // Expected-old-sha semantics guard against concurrent ref moves.
1513
+ const ffResult = runGit(
1514
+ ["update-ref", `refs/heads/${orchBranch}`, newHead, orchTip],
1515
+ repoRoot,
1516
+ );
1517
+ if (ffResult.ok) {
1518
+ execLog("wave", `W${waveIndex}`, `fast-forwarded orch branch to include staging commit`, {
1519
+ orchBranch,
1520
+ from: orchTip.slice(0, 8),
1521
+ to: newHead.slice(0, 8),
1522
+ });
1523
+ } else {
1524
+ execLog("wave", `W${waveIndex}`, `warning: failed to fast-forward orch branch (non-fatal)`, {
1525
+ orchBranch,
1526
+ error: ffResult.stderr,
1527
+ });
1528
+ }
1529
+ } else {
1530
+ // Non-FF case: orch branch has advanced due to prior wave merges.
1531
+ // Create a merge commit so the new task files become visible in
1532
+ // worktrees without discarding any accumulated wave history.
1533
+ // Requires git ≥ 2.38 for `merge-tree --write-tree`.
1534
+ const mergeTreeRes = runGit(
1535
+ ["merge-tree", "--write-tree", orchTip, newHead],
1536
+ repoRoot,
1537
+ );
1538
+ if (mergeTreeRes.ok) {
1539
+ // First line of stdout is the merged tree SHA.
1540
+ // git merge-tree --write-tree exits 0 on clean merge, non-zero on conflicts.
1541
+ // Since it exited 0, the tree should be conflict-free, but validate
1542
+ // the SHA looks like a valid 40-hex OID before using it.
1543
+ const mergedTree = mergeTreeRes.stdout.trim().split("\n")[0];
1544
+ if (!/^[0-9a-f]{40}$/i.test(mergedTree)) {
1545
+ execLog("wave", `W${waveIndex}`, `warning: merge-tree returned unexpected output (non-fatal)`, {
1546
+ orchBranch,
1547
+ output: mergedTree.slice(0, 60),
1548
+ });
1549
+ } else {
1550
+ const mergeCommitMsg = `merge: include staged task files for wave ${waveIndex} into orch branch`;
1551
+ const commitTreeRes = runGit(
1552
+ ["commit-tree", mergedTree, "-p", orchTip, "-p", newHead, "-m", mergeCommitMsg],
1553
+ repoRoot,
1554
+ );
1555
+ if (commitTreeRes.ok) {
1556
+ const mergeCommitSha = commitTreeRes.stdout.trim();
1557
+ const refUpdateRes = runGit(
1558
+ ["update-ref", `refs/heads/${orchBranch}`, mergeCommitSha, orchTip],
1559
+ repoRoot,
1560
+ );
1561
+ if (refUpdateRes.ok) {
1562
+ execLog("wave", `W${waveIndex}`, `merged staging commit into orch branch (non-FF wave)`, {
1563
+ orchBranch,
1564
+ orchTip: orchTip.slice(0, 8),
1565
+ newHead: newHead.slice(0, 8),
1566
+ mergeCommit: mergeCommitSha.slice(0, 8),
1567
+ });
1568
+ } else {
1569
+ execLog("wave", `W${waveIndex}`, `warning: failed to update orch branch ref after merge-tree (non-fatal)`, {
1570
+ orchBranch,
1571
+ error: refUpdateRes.stderr,
1572
+ });
1573
+ }
1574
+ } else {
1575
+ execLog("wave", `W${waveIndex}`, `warning: failed to create merge commit for orch branch (non-fatal)`, {
1576
+ orchBranch,
1577
+ error: commitTreeRes.stderr,
1578
+ });
1579
+ }
1580
+ } // end valid tree SHA
1581
+ } else {
1582
+ execLog("wave", `W${waveIndex}`, `warning: failed to compute merge-tree for orch branch (non-fatal; requires git ≥ 2.38)`, {
1583
+ orchBranch,
1584
+ error: mergeTreeRes.stderr,
1585
+ });
1586
+ }
1587
+ }
1588
+ }
1589
+ } catch (refErr: unknown) {
1590
+ execLog("wave", `W${waveIndex}`, `warning: orch branch ref update threw unexpectedly (non-fatal)`, {
1591
+ orchBranch,
1592
+ error: refErr instanceof Error ? refErr.message : String(refErr),
1593
+ });
1594
+ }
1595
+ }
1480
1596
  }
1481
1597
 
1482
1598
  // ── Wave Execution Core ──────────────────────────────────────────────
@@ -1515,7 +1631,7 @@ export function ensureTaskFilesCommitted(
1515
1631
  * @param batchId - Batch ID for naming
1516
1632
  * @param pauseSignal - Shared pause signal (mutated by stop-wave policy)
1517
1633
  * @param dependencyGraph - Dependency graph for computing transitive dependents
1518
- * @param baseBranch - Branch to base worktrees on (captured at batch start)
1634
+ * @param orchBranch - Orch branch to base worktrees on (and to update after staging commits)
1519
1635
  * @param onMonitorUpdate - Optional callback for dashboard updates during monitoring
1520
1636
  * @param onLanesAllocated - Optional callback fired after lane allocation succeeds
1521
1637
  * @param workspaceConfig - Workspace configuration for repo routing (null/undefined = repo mode)
@@ -1540,13 +1656,14 @@ export async function executeWave(
1540
1656
  batchId: string,
1541
1657
  pauseSignal: { paused: boolean },
1542
1658
  dependencyGraph: DependencyGraph,
1543
- baseBranch: string,
1659
+ orchBranch: string,
1544
1660
  onMonitorUpdate?: MonitorUpdateCallback,
1545
1661
  onLanesAllocated?: (lanes: AllocatedLane[]) => void,
1546
1662
  workspaceConfig?: WorkspaceConfig | null,
1547
1663
  runtimeBackend?: RuntimeBackend,
1548
1664
  onSupervisorAlert?: SupervisorAlertCallback,
1549
1665
  supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
1666
+ reviewerConfig?: { model?: string; thinking?: string; tools?: string },
1550
1667
  ): Promise<WaveExecutionResult> {
1551
1668
  const startedAt = Date.now();
1552
1669
  const policy = config.failure.on_task_failure;
@@ -1561,8 +1678,10 @@ export async function executeWave(
1561
1678
  // Task folders may contain untracked files (PROMPT.md, STATUS.md) that
1562
1679
  // won't appear in worktrees unless committed. Stage and commit them now,
1563
1680
  // before worktree creation, so workers can find their TASK_AUTOSTART paths.
1681
+ // Pass orchBranch so the staging commit is reflected in the orch branch
1682
+ // before worktrees are allocated from it.
1564
1683
  try {
1565
- ensureTaskFilesCommitted(waveTasks, pending, repoRoot, waveIndex);
1684
+ ensureTaskFilesCommitted(waveTasks, pending, repoRoot, waveIndex, orchBranch);
1566
1685
  } catch (err: unknown) {
1567
1686
  const errMsg = err instanceof Error ? err.message : String(err);
1568
1687
  execLog("wave", `W${waveIndex}`, `task file commit failed: ${errMsg}`);
@@ -1586,7 +1705,7 @@ export async function executeWave(
1586
1705
  }
1587
1706
 
1588
1707
  // ── Stage 1: Allocate lanes ──────────────────────────────────
1589
- const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId, baseBranch, workspaceConfig);
1708
+ const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId, orchBranch, workspaceConfig);
1590
1709
 
1591
1710
  if (!allocResult.success) {
1592
1711
  const errMsg = allocResult.error?.message || "Unknown allocation failure";
@@ -1651,6 +1770,7 @@ export async function executeWave(
1651
1770
  executeLaneV2(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, {
1652
1771
  ORCH_BATCH_ID: batchId,
1653
1772
  TASKPLANE_SUPERVISOR_AUTONOMY: supervisorAutonomy,
1773
+ ...buildReviewerEnv(reviewerConfig),
1654
1774
  }, onSupervisorAlert),
1655
1775
  );
1656
1776
 
@@ -2162,6 +2282,27 @@ import { executeTaskV2, type LaneRunnerConfig, type LaneRunnerTaskResult } from
2162
2282
  *
2163
2283
  * @since TP-105
2164
2284
  */
2285
+
2286
+ /**
2287
+ * Build reviewer env vars from a TaskRunnerConfig or reviewer config object.
2288
+ * Used to ensure reviewer config is consistently passed to executeLaneV2
2289
+ * across all call sites (initial waves, resume, retries).
2290
+ *
2291
+ * Returns only the keys that have non-empty values, so that empty/inherit
2292
+ * config does not override inherited env vars from the parent process.
2293
+ *
2294
+ * @since TP-160
2295
+ */
2296
+ export function buildReviewerEnv(
2297
+ reviewerConfig?: { model?: string; thinking?: string; tools?: string } | null,
2298
+ ): Record<string, string> {
2299
+ const env: Record<string, string> = {};
2300
+ if (reviewerConfig?.model) env.TASKPLANE_REVIEWER_MODEL = reviewerConfig.model;
2301
+ if (reviewerConfig?.thinking) env.TASKPLANE_REVIEWER_THINKING = reviewerConfig.thinking;
2302
+ if (reviewerConfig?.tools) env.TASKPLANE_REVIEWER_TOOLS = reviewerConfig.tools;
2303
+ return env;
2304
+ }
2305
+
2165
2306
  export async function executeLaneV2(
2166
2307
  lane: AllocatedLane,
2167
2308
  config: OrchestratorConfig,
@@ -2247,6 +2388,9 @@ export async function executeLaneV2(
2247
2388
  workerTools: "read,write,edit,bash,grep,find,ls",
2248
2389
  workerThinking: "",
2249
2390
  workerSystemPrompt,
2391
+ reviewerModel: extraEnvVars?.TASKPLANE_REVIEWER_MODEL || "",
2392
+ reviewerThinking: extraEnvVars?.TASKPLANE_REVIEWER_THINKING || "",
2393
+ reviewerTools: extraEnvVars?.TASKPLANE_REVIEWER_TOOLS || "",
2250
2394
  supervisorAutonomy,
2251
2395
  projectName: config.project?.name || "project",
2252
2396
  maxIterations: 20,
@@ -92,6 +92,22 @@ export interface LaneRunnerConfig {
92
92
  workerThinking: string;
93
93
  /** Worker system prompt */
94
94
  workerSystemPrompt: string;
95
+ /**
96
+ * Reviewer model (empty string = inherit session default).
97
+ * Set from TASKPLANE_REVIEWER_MODEL env var, sourced from runnerConfig.reviewer.model.
98
+ * @since TP-160
99
+ */
100
+ reviewerModel: string;
101
+ /**
102
+ * Reviewer thinking mode (empty string = inherit).
103
+ * @since TP-160
104
+ */
105
+ reviewerThinking: string;
106
+ /**
107
+ * Reviewer tool allowlist (comma-separated).
108
+ * @since TP-160
109
+ */
110
+ reviewerTools: string;
95
111
  /** Supervisor autonomy level for bridge-tool guards. */
96
112
  supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
97
113
  /** Project name (for review request context) */
@@ -331,6 +347,9 @@ export async function executeTaskV2(
331
347
  TASKPLANE_ACTIVE_SEGMENT_ID: segmentId ?? "",
332
348
  TASKPLANE_SUPERVISOR_AUTONOMY: config.supervisorAutonomy || "autonomous",
333
349
  ORCH_BATCH_ID: config.batchId,
350
+ ...(config.reviewerModel ? { TASKPLANE_REVIEWER_MODEL: config.reviewerModel } : {}),
351
+ ...(config.reviewerThinking ? { TASKPLANE_REVIEWER_THINKING: config.reviewerThinking } : {}),
352
+ ...(config.reviewerTools ? { TASKPLANE_REVIEWER_TOOLS: config.reviewerTools } : {}),
334
353
  },
335
354
  };
336
355