taskplane 0.25.6 → 0.25.7
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 +84 -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 +91 -6
- 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,46 @@ 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
|
+
// Only inject when a JSONL-backed entry is absent or stale — the snapshot
|
|
1125
|
+
// provides real-time tool-call / cost / context data even before the
|
|
1126
|
+
// JSONL accumulator has accumulated enough events.
|
|
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 telemetry entry, or if the snapshot
|
|
1133
|
+
// is more recent than the latest accumulator update.
|
|
1134
|
+
const existing = telemetry[key];
|
|
1135
|
+
if (!existing || (snap.updatedAt && snap.updatedAt > (existing._updatedAt || 0))) {
|
|
1136
|
+
telemetry[key] = {
|
|
1137
|
+
inputTokens: agent.inputTokens || 0,
|
|
1138
|
+
outputTokens: agent.outputTokens || 0,
|
|
1139
|
+
cacheReadTokens: agent.cacheReadTokens || 0,
|
|
1140
|
+
cacheWriteTokens: agent.cacheWriteTokens || 0,
|
|
1141
|
+
cost: agent.costUsd || 0,
|
|
1142
|
+
toolCalls: agent.toolCalls || 0,
|
|
1143
|
+
lastTool: agent.lastTool || "",
|
|
1144
|
+
currentTool: snap.status === "running" ? (agent.lastTool || "") : "",
|
|
1145
|
+
contextPct: agent.contextPct || 0,
|
|
1146
|
+
// startedAt is not in the snapshot; compute from elapsed if possible.
|
|
1147
|
+
startedAt: agent.elapsedMs > 0 ? snap.updatedAt - agent.elapsedMs : snap.updatedAt,
|
|
1148
|
+
retries: 0,
|
|
1149
|
+
retryActive: false,
|
|
1150
|
+
lastRetryError: "",
|
|
1151
|
+
compactions: 0,
|
|
1152
|
+
latestTotalTokens: (agent.inputTokens || 0) + (agent.outputTokens || 0),
|
|
1153
|
+
_updatedAt: snap.updatedAt,
|
|
1154
|
+
_source: "merge-snapshot",
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1080
1159
|
// TP-115: Synthesize laneStates from V2 snapshots so the dashboard
|
|
1081
1160
|
// pipeline works without legacy lane-state-*.json sidecar files.
|
|
1082
1161
|
// V2 snapshots are authoritative when present.
|
|
@@ -1099,6 +1178,8 @@ function buildDashboardState() {
|
|
|
1099
1178
|
// Runtime V2 data (null/empty for legacy batches)
|
|
1100
1179
|
runtimeRegistry,
|
|
1101
1180
|
runtimeLaneSnapshots,
|
|
1181
|
+
// TP-164: Merge agent snapshots for live dashboard telemetry.
|
|
1182
|
+
runtimeMergeSnapshots,
|
|
1102
1183
|
mailbox: mailboxData,
|
|
1103
1184
|
batch: {
|
|
1104
1185
|
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
|
|
|
@@ -10,9 +10,9 @@ import { join, dirname, resolve, relative } from "path";
|
|
|
10
10
|
import { execLog, isV2AgentAlive, setV2LivenessRegistryCache } from "./execution.ts";
|
|
11
11
|
import { resolveOperatorId } from "./naming.ts";
|
|
12
12
|
import { MERGE_POLL_INTERVAL_MS, MERGE_RESULT_GRACE_MS, MERGE_RESULT_READ_RETRIES, MERGE_RESULT_READ_RETRY_DELAY_MS, MERGE_SPAWN_RETRY_MAX, MERGE_TIMEOUT_MAX_RETRIES, MERGE_TIMEOUT_MS, MERGE_HEALTH_POLL_INTERVAL_MS, MERGE_HEALTH_WARNING_THRESHOLD_MS, MERGE_HEALTH_STUCK_THRESHOLD_MS, MergeError, VALID_MERGE_STATUSES, buildEngineEventBase } from "./types.ts";
|
|
13
|
-
import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig, MergeHealthStatus, MergeHealthEventType, MergeSessionSnapshot, MergeSessionHealthState, EngineEvent, OrchBatchPhase } from "./types.ts";
|
|
13
|
+
import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig, MergeHealthStatus, MergeHealthEventType, MergeSessionSnapshot, MergeSessionHealthState, EngineEvent, OrchBatchPhase, RuntimeMergeSnapshot, RuntimeAgentTelemetrySnapshot } from "./types.ts";
|
|
14
14
|
import { resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
|
|
15
|
-
import { readManifest, writeManifest, buildRegistrySnapshot, writeRegistrySnapshot, readRegistrySnapshot } from "./process-registry.ts";
|
|
15
|
+
import { readManifest, writeManifest, buildRegistrySnapshot, writeRegistrySnapshot, readRegistrySnapshot, writeMergeSnapshot } from "./process-registry.ts";
|
|
16
16
|
import { generateMergeWorktreePath, sleepAsync, sleepSync } from "./worktree.ts";
|
|
17
17
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
18
18
|
import { ORCH_MESSAGES } from "./messages.ts";
|
|
@@ -20,7 +20,7 @@ import { emitEngineEvent } from "./persistence.ts";
|
|
|
20
20
|
import { loadOrchestratorConfig } from "./config.ts";
|
|
21
21
|
import { captureBaseline, diffFingerprints, runVerificationCommands, parseTestOutput, deduplicateFingerprints } from "./verification.ts";
|
|
22
22
|
import { spawnAgent } from "./agent-host.ts";
|
|
23
|
-
import type { AgentHostOptions, AgentHostResult } from "./agent-host.ts";
|
|
23
|
+
import type { AgentHostOptions, AgentHostResult, AgentTelemetryCallback } from "./agent-host.ts";
|
|
24
24
|
import type { RuntimeBackend } from "./execution.ts";
|
|
25
25
|
import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./verification.ts";
|
|
26
26
|
|
|
@@ -614,14 +614,69 @@ export async function spawnMergeAgentV2(
|
|
|
614
614
|
},
|
|
615
615
|
};
|
|
616
616
|
|
|
617
|
-
|
|
617
|
+
// Derive the 1-indexed merge number from the session name
|
|
618
|
+
// (e.g. "orch-henry-merge-1" → 1, "orch-henry-merge-2" → 2).
|
|
619
|
+
const mergeNumberMatch = sessionName.match(/-merge-(\d+)$/);
|
|
620
|
+
const mergeNumber = mergeNumberMatch ? parseInt(mergeNumberMatch[1], 10) : 1;
|
|
621
|
+
const mergeStartedAt = Date.now();
|
|
622
|
+
const mergeStateRoot = stateRoot ?? repoRoot;
|
|
623
|
+
|
|
624
|
+
// Helper: build a RuntimeAgentTelemetrySnapshot from a partial AgentHostResult.
|
|
625
|
+
const buildAgentSnap = (tel: Partial<AgentHostResult>, status: RuntimeAgentTelemetrySnapshot["status"]): RuntimeAgentTelemetrySnapshot => ({
|
|
626
|
+
agentId: sessionName,
|
|
627
|
+
status,
|
|
628
|
+
elapsedMs: tel.durationMs ?? (Date.now() - mergeStartedAt),
|
|
629
|
+
toolCalls: tel.toolCalls ?? 0,
|
|
630
|
+
contextPct: tel.contextUsage?.percent ?? 0,
|
|
631
|
+
costUsd: tel.costUsd ?? 0,
|
|
632
|
+
lastTool: tel.lastTool ?? "",
|
|
633
|
+
inputTokens: tel.inputTokens ?? 0,
|
|
634
|
+
outputTokens: tel.outputTokens ?? 0,
|
|
635
|
+
cacheReadTokens: tel.cacheReadTokens ?? 0,
|
|
636
|
+
cacheWriteTokens: tel.cacheWriteTokens ?? 0,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Telemetry callback: write a merge snapshot on every telemetry update.
|
|
640
|
+
// Non-fatal — snapshot writes must never interfere with merge execution.
|
|
641
|
+
const onMergeTelemetry: AgentTelemetryCallback = (tel) => {
|
|
642
|
+
try {
|
|
643
|
+
const snap: RuntimeMergeSnapshot = {
|
|
644
|
+
batchId: bid,
|
|
645
|
+
mergeNumber,
|
|
646
|
+
sessionName,
|
|
647
|
+
waveIndex: 0,
|
|
648
|
+
status: "running",
|
|
649
|
+
agent: buildAgentSnap(tel, "running"),
|
|
650
|
+
updatedAt: Date.now(),
|
|
651
|
+
};
|
|
652
|
+
writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
|
|
653
|
+
} catch { /* non-fatal */ }
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const { promise, kill } = spawnAgent(opts, undefined, onMergeTelemetry);
|
|
657
|
+
|
|
658
|
+
// Write an initial "running" snapshot immediately so the dashboard row
|
|
659
|
+
// appears even when the first telemetry event is delayed.
|
|
660
|
+
try {
|
|
661
|
+
const initialSnap: RuntimeMergeSnapshot = {
|
|
662
|
+
batchId: bid,
|
|
663
|
+
mergeNumber,
|
|
664
|
+
sessionName,
|
|
665
|
+
waveIndex: 0,
|
|
666
|
+
status: "running",
|
|
667
|
+
agent: buildAgentSnap({}, "running"),
|
|
668
|
+
updatedAt: Date.now(),
|
|
669
|
+
};
|
|
670
|
+
writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, initialSnap);
|
|
671
|
+
} catch { /* non-fatal */ }
|
|
618
672
|
|
|
619
673
|
// Store the kill handle for external cleanup (pause/abort).
|
|
620
674
|
// The promise runs in background — caller uses waitForMergeResult()
|
|
621
675
|
// to poll for the result file, same contract as the legacy session path.
|
|
622
|
-
activeMergeAgents.set(sessionName, { promise, kill, stateRoot:
|
|
676
|
+
activeMergeAgents.set(sessionName, { promise, kill, stateRoot: mergeStateRoot, batchId: bid });
|
|
623
677
|
|
|
624
|
-
// Fire-and-forget: the background promise handles exit logging
|
|
678
|
+
// Fire-and-forget: the background promise handles exit logging and
|
|
679
|
+
// writes a terminal snapshot ("complete" or "failed") when the agent exits.
|
|
625
680
|
promise.then(result => {
|
|
626
681
|
activeMergeAgents.delete(sessionName);
|
|
627
682
|
execLog("merge", sessionName, "merge agent exited (V2)", {
|
|
@@ -630,9 +685,39 @@ export async function spawnMergeAgentV2(
|
|
|
630
685
|
costUsd: result.costUsd,
|
|
631
686
|
killed: result.killed,
|
|
632
687
|
});
|
|
688
|
+
// Write terminal snapshot. Promise resolves for both successful and
|
|
689
|
+
// failed exits, so derive status from result fields rather than
|
|
690
|
+
// relying on .catch to handle failures.
|
|
691
|
+
const terminalStatus: RuntimeMergeSnapshot["status"] =
|
|
692
|
+
(result.killed || result.exitCode !== 0 || !result.agentEnded) ? "failed" : "complete";
|
|
693
|
+
try {
|
|
694
|
+
const snap: RuntimeMergeSnapshot = {
|
|
695
|
+
batchId: bid,
|
|
696
|
+
mergeNumber,
|
|
697
|
+
sessionName,
|
|
698
|
+
waveIndex: 0,
|
|
699
|
+
status: terminalStatus,
|
|
700
|
+
agent: buildAgentSnap(result, terminalStatus === "complete" ? "exited" : "crashed"),
|
|
701
|
+
updatedAt: Date.now(),
|
|
702
|
+
};
|
|
703
|
+
writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
|
|
704
|
+
} catch { /* non-fatal */ }
|
|
633
705
|
}).catch(err => {
|
|
634
706
|
activeMergeAgents.delete(sessionName);
|
|
635
707
|
execLog("merge", sessionName, `merge agent error (V2): ${err instanceof Error ? err.message : String(err)}`);
|
|
708
|
+
// Write a failed terminal snapshot on unexpected rejection.
|
|
709
|
+
try {
|
|
710
|
+
const snap: RuntimeMergeSnapshot = {
|
|
711
|
+
batchId: bid,
|
|
712
|
+
mergeNumber,
|
|
713
|
+
sessionName,
|
|
714
|
+
waveIndex: 0,
|
|
715
|
+
status: "failed",
|
|
716
|
+
agent: buildAgentSnap({}, "crashed"),
|
|
717
|
+
updatedAt: Date.now(),
|
|
718
|
+
};
|
|
719
|
+
writeMergeSnapshot(mergeStateRoot, bid, mergeNumber, snap);
|
|
720
|
+
} catch { /* non-fatal */ }
|
|
636
721
|
});
|
|
637
722
|
}
|
|
638
723
|
|
|
@@ -30,12 +30,14 @@ import {
|
|
|
30
30
|
runtimeRegistryPath,
|
|
31
31
|
runtimeAgentEventsPath,
|
|
32
32
|
runtimeLaneSnapshotPath,
|
|
33
|
+
runtimeMergeSnapshotPath,
|
|
33
34
|
validateAgentManifest,
|
|
34
35
|
type RuntimeAgentId,
|
|
35
36
|
type RuntimeAgentManifest,
|
|
36
37
|
type RuntimeAgentRole,
|
|
37
38
|
type RuntimeAgentStatus,
|
|
38
39
|
type RuntimeRegistry,
|
|
40
|
+
type RuntimeMergeSnapshot,
|
|
39
41
|
type PacketPaths,
|
|
40
42
|
} from "./types.ts";
|
|
41
43
|
|
|
@@ -362,3 +364,53 @@ export function readLaneSnapshot(
|
|
|
362
364
|
return null;
|
|
363
365
|
}
|
|
364
366
|
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Write a V2 merge agent snapshot to disk (atomic rename).
|
|
370
|
+
*
|
|
371
|
+
* Stored in the `lanes/` directory alongside lane snapshots so the dashboard
|
|
372
|
+
* server picks it up with the same scan that reads lane-N.json files.
|
|
373
|
+
*
|
|
374
|
+
* @param stateRoot - Repository root (where `.pi/` lives)
|
|
375
|
+
* @param batchId - Current batch identifier
|
|
376
|
+
* @param mergeNumber - 1-indexed merge agent number
|
|
377
|
+
* @param snapshot - Snapshot data to persist
|
|
378
|
+
*
|
|
379
|
+
* @since TP-164
|
|
380
|
+
*/
|
|
381
|
+
export function writeMergeSnapshot(
|
|
382
|
+
stateRoot: string,
|
|
383
|
+
batchId: string,
|
|
384
|
+
mergeNumber: number,
|
|
385
|
+
snapshot: RuntimeMergeSnapshot,
|
|
386
|
+
): void {
|
|
387
|
+
const path = runtimeMergeSnapshotPath(stateRoot, batchId, mergeNumber);
|
|
388
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
389
|
+
const tmpPath = path + ".tmp";
|
|
390
|
+
writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
391
|
+
renameSync(tmpPath, path);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Read a V2 merge agent snapshot from disk.
|
|
396
|
+
* Returns null if the file does not exist or is unreadable.
|
|
397
|
+
*
|
|
398
|
+
* @param stateRoot - Repository root (where `.pi/` lives)
|
|
399
|
+
* @param batchId - Current batch identifier
|
|
400
|
+
* @param mergeNumber - 1-indexed merge agent number
|
|
401
|
+
*
|
|
402
|
+
* @since TP-164
|
|
403
|
+
*/
|
|
404
|
+
export function readMergeSnapshot(
|
|
405
|
+
stateRoot: string,
|
|
406
|
+
batchId: string,
|
|
407
|
+
mergeNumber: number,
|
|
408
|
+
): RuntimeMergeSnapshot | null {
|
|
409
|
+
try {
|
|
410
|
+
const p = runtimeMergeSnapshotPath(stateRoot, batchId, mergeNumber);
|
|
411
|
+
if (!existsSync(p)) return null;
|
|
412
|
+
return JSON.parse(readFileSync(p, "utf-8")) as RuntimeMergeSnapshot;
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -8,7 +8,7 @@ import { join } from "path";
|
|
|
8
8
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
9
9
|
import { runDiscovery } from "./discovery.ts";
|
|
10
10
|
import { executeOrchBatch } from "./engine.ts";
|
|
11
|
-
import { computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
11
|
+
import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
13
|
import { selectRuntimeBackend } from "./engine.ts";
|
|
14
14
|
import { readRegistrySnapshot, isTerminalStatus, isProcessAlive } from "./process-registry.ts";
|
|
@@ -1461,7 +1461,7 @@ export async function resumeOrchBatch(
|
|
|
1461
1461
|
const laneResult = await executeLaneV2(
|
|
1462
1462
|
lane, orchConfig, laneRepoRoot, batchState.pauseSignal,
|
|
1463
1463
|
workspaceRoot, !!workspaceConfig,
|
|
1464
|
-
{ ORCH_BATCH_ID: batchState.batchId },
|
|
1464
|
+
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer) },
|
|
1465
1465
|
emitAlert,
|
|
1466
1466
|
);
|
|
1467
1467
|
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
@@ -1543,7 +1543,7 @@ export async function resumeOrchBatch(
|
|
|
1543
1543
|
const laneResult = await executeLaneV2(
|
|
1544
1544
|
lane, orchConfig, reExecRepoRoot, batchState.pauseSignal,
|
|
1545
1545
|
workspaceRoot, !!workspaceConfig,
|
|
1546
|
-
{ ORCH_BATCH_ID: batchState.batchId },
|
|
1546
|
+
{ ORCH_BATCH_ID: batchState.batchId, ...buildReviewerEnv(runnerConfig.reviewer) },
|
|
1547
1547
|
emitAlert,
|
|
1548
1548
|
);
|
|
1549
1549
|
const taskResult = laneResult.tasks.find(t => t.taskId === task.taskId);
|
|
@@ -1994,6 +1994,7 @@ export async function resumeOrchBatch(
|
|
|
1994
1994
|
resumeBackend,
|
|
1995
1995
|
emitAlert,
|
|
1996
1996
|
supervisorAutonomy,
|
|
1997
|
+
runnerConfig.reviewer,
|
|
1997
1998
|
);
|
|
1998
1999
|
|
|
1999
2000
|
batchState.waveResults.push(waveResult);
|
|
@@ -291,6 +291,19 @@ export interface TaskRunnerConfig {
|
|
|
291
291
|
* @since TP-055
|
|
292
292
|
*/
|
|
293
293
|
model_fallback?: "inherit" | "fail";
|
|
294
|
+
/**
|
|
295
|
+
* Reviewer agent model/thinking/tools configuration.
|
|
296
|
+
* Threaded through to `spawnReviewer()` via env vars.
|
|
297
|
+
* @since TP-160
|
|
298
|
+
*/
|
|
299
|
+
reviewer?: {
|
|
300
|
+
/** Model string (empty = inherit session default) */
|
|
301
|
+
model: string;
|
|
302
|
+
/** Thinking mode ("on" | "off" | budget string, empty = inherit) */
|
|
303
|
+
thinking: string;
|
|
304
|
+
/** Comma-separated tool allowlist */
|
|
305
|
+
tools: string;
|
|
306
|
+
};
|
|
294
307
|
}
|
|
295
308
|
|
|
296
309
|
/** Result of a preflight check */
|
|
@@ -4062,6 +4075,50 @@ export function runtimeLaneSnapshotPath(stateRoot: string, batchId: string, lane
|
|
|
4062
4075
|
return `${stateRoot}/.pi/runtime/${batchId}/lanes/lane-${laneNumber}.json`;
|
|
4063
4076
|
}
|
|
4064
4077
|
|
|
4078
|
+
/**
|
|
4079
|
+
* Telemetry snapshot for a merge agent.
|
|
4080
|
+
*
|
|
4081
|
+
* Written to `.pi/runtime/{batchId}/lanes/merge-{mergeNumber}.json` alongside
|
|
4082
|
+
* lane snapshots so the dashboard can display live merge-phase telemetry.
|
|
4083
|
+
* Follows the same file-backed pattern as {@link RuntimeLaneSnapshot} but is
|
|
4084
|
+
* simpler — merge agents have no reviewer, progress tracking, or repoId.
|
|
4085
|
+
*
|
|
4086
|
+
* @since TP-164
|
|
4087
|
+
*/
|
|
4088
|
+
export interface RuntimeMergeSnapshot {
|
|
4089
|
+
/** Batch this merge agent belongs to */
|
|
4090
|
+
batchId: string;
|
|
4091
|
+
/** 1-indexed merge agent number (e.g. 1 for "orch-henry-merge-1") */
|
|
4092
|
+
mergeNumber: number;
|
|
4093
|
+
/** Stable agent session name (e.g. "orch-henry-merge-1") */
|
|
4094
|
+
sessionName: string;
|
|
4095
|
+
/** Wave index this merge agent is processing (0-indexed, 0 when unknown) */
|
|
4096
|
+
waveIndex: number;
|
|
4097
|
+
/** Merge agent lifecycle status */
|
|
4098
|
+
status: "running" | "complete" | "failed";
|
|
4099
|
+
/** Live telemetry snapshot for the merge agent (null when not yet started) */
|
|
4100
|
+
agent: RuntimeAgentTelemetrySnapshot | null;
|
|
4101
|
+
/** Epoch ms when this snapshot was last updated */
|
|
4102
|
+
updatedAt: number;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
/**
|
|
4106
|
+
* Resolve the path for a merge agent snapshot file.
|
|
4107
|
+
*
|
|
4108
|
+
* Snapshots are stored alongside lane snapshots in the `lanes/` directory so
|
|
4109
|
+
* the dashboard server's directory scan picks them up automatically.
|
|
4110
|
+
*
|
|
4111
|
+
* @param stateRoot - Repository root (where `.pi/` lives)
|
|
4112
|
+
* @param batchId - Current batch identifier
|
|
4113
|
+
* @param mergeNumber - 1-indexed merge agent number
|
|
4114
|
+
* @returns Absolute path to the merge snapshot JSON file
|
|
4115
|
+
*
|
|
4116
|
+
* @since TP-164
|
|
4117
|
+
*/
|
|
4118
|
+
export function runtimeMergeSnapshotPath(stateRoot: string, batchId: string, mergeNumber: number): string {
|
|
4119
|
+
return `${stateRoot}/.pi/runtime/${batchId}/lanes/merge-${mergeNumber}.json`;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4065
4122
|
/**
|
|
4066
4123
|
* Resolve the path for the batch runtime registry.
|
|
4067
4124
|
*
|