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.
- package/dashboard/public/app.js +10 -4
- package/dashboard/public/style.css +6 -5
- package/dashboard/server.cjs +85 -3
- package/extensions/taskplane/agent-bridge-extension.ts +11 -1
- package/extensions/taskplane/agent-host.ts +2 -2
- package/extensions/taskplane/config-loader.ts +5 -0
- package/extensions/taskplane/engine.ts +15 -3
- package/extensions/taskplane/execution.ts +148 -4
- package/extensions/taskplane/lane-runner.ts +19 -0
- package/extensions/taskplane/merge.ts +2917 -2815
- package/extensions/taskplane/process-registry.ts +52 -0
- package/extensions/taskplane/resume.ts +4 -3
- package/extensions/taskplane/types.ts +57 -0
- package/package.json +1 -1
package/dashboard/public/app.js
CHANGED
|
@@ -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: '
|
|
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 =
|
|
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(
|
|
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:
|
|
821
|
-
|
|
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-
|
|
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 {
|
package/dashboard/server.cjs
CHANGED
|
@@ -194,9 +194,21 @@ function parseStatusMd(taskFolder) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
function getActiveSessions() {
|
|
197
|
-
// Runtime V2
|
|
198
|
-
//
|
|
199
|
-
|
|
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",
|
|
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,
|
|
565
|
-
(event.args && typeof Object.values(event.args)[0] === "string" ? String(Object.values(event.args)[0]).slice(0,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|