taskplane 0.24.7 → 0.24.9
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 +105 -4
- package/dashboard/public/style.css +36 -0
- package/extensions/taskplane/agent-bridge-extension.ts +4 -4
- package/extensions/taskplane/engine.ts +526 -21
- package/extensions/taskplane/execution.ts +18 -9
- package/extensions/taskplane/extension.ts +158 -0
- package/extensions/taskplane/lane-runner.ts +58 -18
- package/extensions/taskplane/persistence.ts +12 -0
- package/extensions/taskplane/resume.ts +267 -24
- package/extensions/taskplane/supervisor-primer.md +10 -0
- package/extensions/taskplane/supervisor.ts +97 -0
- package/extensions/taskplane/types.ts +90 -0
- package/package.json +1 -1
|
@@ -1182,8 +1182,10 @@ export async function monitorLanes(
|
|
|
1182
1182
|
currentTaskId = task.taskId;
|
|
1183
1183
|
|
|
1184
1184
|
const tracker = getOrCreateTracker(task.taskId, now);
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1185
|
+
const unit = buildExecutionUnit(lane, task, repoRoot, isWorkspaceMode);
|
|
1186
|
+
const donePath = unit.packet.donePath;
|
|
1187
|
+
const statusPath = unit.packet.statusPath;
|
|
1188
|
+
const statusResult = await parseWorktreeStatusMdAsync(dirname(statusPath), lane.worktreePath, repoRoot, false);
|
|
1187
1189
|
|
|
1188
1190
|
const snapshot = await resolveTaskMonitorState(
|
|
1189
1191
|
task.taskId,
|
|
@@ -1966,6 +1968,16 @@ export function buildExecutionUnit(
|
|
|
1966
1968
|
const segmentId = task.task.activeSegmentId ?? null;
|
|
1967
1969
|
const id = segmentId ?? task.taskId;
|
|
1968
1970
|
|
|
1971
|
+
const packet = task.task.packetTaskPath
|
|
1972
|
+
? resolvePacketPaths(task.task.packetTaskPath)
|
|
1973
|
+
: {
|
|
1974
|
+
promptPath: resolved.taskFolderResolved + "/PROMPT.md",
|
|
1975
|
+
statusPath: resolved.statusPath,
|
|
1976
|
+
donePath: resolved.donePath,
|
|
1977
|
+
reviewsDir: resolved.taskFolderResolved + "/.reviews",
|
|
1978
|
+
taskFolder: resolved.taskFolderResolved,
|
|
1979
|
+
};
|
|
1980
|
+
|
|
1969
1981
|
return {
|
|
1970
1982
|
id,
|
|
1971
1983
|
taskId: task.taskId,
|
|
@@ -1973,13 +1985,7 @@ export function buildExecutionUnit(
|
|
|
1973
1985
|
executionRepoId,
|
|
1974
1986
|
packetHomeRepoId,
|
|
1975
1987
|
worktreePath: lane.worktreePath,
|
|
1976
|
-
packet
|
|
1977
|
-
promptPath: resolved.taskFolderResolved + "/PROMPT.md",
|
|
1978
|
-
statusPath: resolved.statusPath,
|
|
1979
|
-
donePath: resolved.donePath,
|
|
1980
|
-
reviewsDir: resolved.taskFolderResolved + "/.reviews",
|
|
1981
|
-
taskFolder: resolved.taskFolderResolved,
|
|
1982
|
-
},
|
|
1988
|
+
packet,
|
|
1983
1989
|
task: task.task,
|
|
1984
1990
|
};
|
|
1985
1991
|
}
|
|
@@ -2181,11 +2187,13 @@ export async function executeLaneV2(
|
|
|
2181
2187
|
});
|
|
2182
2188
|
|
|
2183
2189
|
for (const task of lane.tasks) {
|
|
2190
|
+
const taskSegmentId = task.task.activeSegmentId ?? null;
|
|
2184
2191
|
if (shouldSkipRemaining || pauseSignal.paused) {
|
|
2185
2192
|
const reason = pauseSignal.paused ? "Skipped due to pause signal" : "Skipped due to prior task failure in lane";
|
|
2186
2193
|
outcomes.push({
|
|
2187
2194
|
taskId: task.taskId,
|
|
2188
2195
|
status: "skipped",
|
|
2196
|
+
segmentId: taskSegmentId,
|
|
2189
2197
|
startTime: null,
|
|
2190
2198
|
endTime: null,
|
|
2191
2199
|
exitReason: reason,
|
|
@@ -2246,6 +2254,7 @@ export async function executeLaneV2(
|
|
|
2246
2254
|
outcomes.push({
|
|
2247
2255
|
taskId: task.taskId,
|
|
2248
2256
|
status: "failed",
|
|
2257
|
+
segmentId: taskSegmentId,
|
|
2249
2258
|
startTime: Date.now(),
|
|
2250
2259
|
endTime: Date.now(),
|
|
2251
2260
|
exitReason: `Runtime V2 execution error: ${errMsg}`,
|
|
@@ -1120,6 +1120,8 @@ export function startBatchInWorker(
|
|
|
1120
1120
|
|
|
1121
1121
|
case "error": {
|
|
1122
1122
|
errorReceivedViaIpc = true;
|
|
1123
|
+
rotateStderrLogToBatch(batchState.batchId || undefined);
|
|
1124
|
+
const stderrTail = readStderrTail();
|
|
1123
1125
|
const sourceLabel = msg.source ? ` (${msg.source})` : "";
|
|
1124
1126
|
const stackLine = msg.stack?.split("\n")[0]?.trim();
|
|
1125
1127
|
if (batchState.phase !== "completed" && batchState.phase !== "failed") {
|
|
@@ -1134,6 +1136,36 @@ export function startBatchInWorker(
|
|
|
1134
1136
|
` Batch ${batchState.batchId} marked as failed.`,
|
|
1135
1137
|
"error",
|
|
1136
1138
|
);
|
|
1139
|
+
// Alert supervisor — this is the PRIMARY notification path for engine
|
|
1140
|
+
// crashes caught by uncaughtException/unhandledRejection handlers.
|
|
1141
|
+
// The child.on("exit") handler is suppressed when errorReceivedViaIpc
|
|
1142
|
+
// is true, so this is the only path that reaches the supervisor.
|
|
1143
|
+
onSupervisorAlert?.({
|
|
1144
|
+
category: "task-failure",
|
|
1145
|
+
summary:
|
|
1146
|
+
`🔴 Engine crashed with unhandled error${sourceLabel}: ${msg.message}\n` +
|
|
1147
|
+
(stackLine ? ` Stack: ${stackLine}\n` : "") +
|
|
1148
|
+
` Batch ${batchState.batchId} marked as failed.\n\n` +
|
|
1149
|
+
`Engine stderr tail (${stderrLogPath}):\n${stderrTail}\n\n` +
|
|
1150
|
+
`This is a critical engine failure. The batch cannot continue.\n` +
|
|
1151
|
+
`Available actions:\n` +
|
|
1152
|
+
` - orch_status() to inspect state\n` +
|
|
1153
|
+
` - orch_resume(force=true) to retry from last checkpoint`,
|
|
1154
|
+
context: {
|
|
1155
|
+
batchProgress: batchState.totalTasks > 0 ? {
|
|
1156
|
+
succeededTasks: batchState.succeededTasks,
|
|
1157
|
+
failedTasks: batchState.failedTasks,
|
|
1158
|
+
skippedTasks: batchState.skippedTasks,
|
|
1159
|
+
blockedTasks: batchState.blockedTasks,
|
|
1160
|
+
totalTasks: batchState.totalTasks,
|
|
1161
|
+
currentWave: batchState.currentWaveIndex + 1,
|
|
1162
|
+
totalWaves: batchState.totalWaves,
|
|
1163
|
+
} : undefined,
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
// Persist failed state to disk so dashboard/resume see it.
|
|
1167
|
+
// The engine-worker is dead and can't persist — we must do it here.
|
|
1168
|
+
try { saveBatchState(JSON.stringify(batchState, null, 2), wkData.cwd); } catch { /* best effort */ }
|
|
1137
1169
|
updateWidget();
|
|
1138
1170
|
break;
|
|
1139
1171
|
}
|
|
@@ -1223,6 +1255,8 @@ export function startBatchInWorker(
|
|
|
1223
1255
|
} : undefined,
|
|
1224
1256
|
},
|
|
1225
1257
|
});
|
|
1258
|
+
// Persist failed state to disk (engine is dead, can't persist itself)
|
|
1259
|
+
try { saveBatchState(JSON.stringify(batchState, null, 2), wkData.cwd); } catch { /* best effort */ }
|
|
1226
1260
|
}
|
|
1227
1261
|
settle();
|
|
1228
1262
|
});
|
|
@@ -2085,6 +2119,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
2085
2119
|
};
|
|
2086
2120
|
}
|
|
2087
2121
|
|
|
2122
|
+
function repoIdFromSegmentId(segmentId: string): string {
|
|
2123
|
+
const idx = segmentId.indexOf("::");
|
|
2124
|
+
if (idx <= 0 || idx >= segmentId.length - 2) return "unknown";
|
|
2125
|
+
return segmentId.slice(idx + 2);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function buildTaskSegmentProgressLabel(
|
|
2129
|
+
task: { taskId: string; segmentIds?: string[]; activeSegmentId?: string | null; status?: string } | undefined,
|
|
2130
|
+
segments: Array<{ taskId: string; segmentId: string; status: string; repoId: string }> | undefined,
|
|
2131
|
+
preferredSegmentId?: string | null,
|
|
2132
|
+
): string | null {
|
|
2133
|
+
if (!task || !Array.isArray(task.segmentIds) || task.segmentIds.length <= 1) return null;
|
|
2134
|
+
|
|
2135
|
+
const segmentIds = task.segmentIds.filter(segmentId => typeof segmentId === "string" && segmentId.trim().length > 0);
|
|
2136
|
+
if (segmentIds.length <= 1) return null;
|
|
2137
|
+
|
|
2138
|
+
const bySegmentId = new Map<string, { status: string; repoId: string }>();
|
|
2139
|
+
for (const segment of segments ?? []) {
|
|
2140
|
+
if (segment.taskId === task.taskId) {
|
|
2141
|
+
bySegmentId.set(segment.segmentId, { status: segment.status, repoId: segment.repoId });
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
let activeSegmentId = task.activeSegmentId ?? preferredSegmentId ?? null;
|
|
2146
|
+
if (!activeSegmentId || !segmentIds.includes(activeSegmentId)) {
|
|
2147
|
+
activeSegmentId = segmentIds.find((segmentId) => {
|
|
2148
|
+
const status = bySegmentId.get(segmentId)?.status;
|
|
2149
|
+
return !["succeeded", "failed", "stalled", "skipped"].includes(status || "pending");
|
|
2150
|
+
}) || segmentIds[segmentIds.length - 1];
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const index = Math.max(0, segmentIds.indexOf(activeSegmentId));
|
|
2154
|
+
const repoId = bySegmentId.get(activeSegmentId)?.repoId ?? repoIdFromSegmentId(activeSegmentId);
|
|
2155
|
+
return `Segment ${index + 1}/${segmentIds.length}: ${repoId}`;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2088
2158
|
/**
|
|
2089
2159
|
* Core logic for orch-status. Returns a formatted status string.
|
|
2090
2160
|
* Reads in-memory state first, falls back to disk if idle.
|
|
@@ -2114,6 +2184,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
2114
2184
|
` Elapsed: ${elapsedSec}s`,
|
|
2115
2185
|
];
|
|
2116
2186
|
|
|
2187
|
+
const segmentRecords = diskState.segments || [];
|
|
2188
|
+
const multiSegmentTasks = (diskState.tasks || []).filter((task) => Array.isArray(task.segmentIds) && task.segmentIds.length > 1);
|
|
2189
|
+
if (multiSegmentTasks.length > 0) {
|
|
2190
|
+
const byStatus = {
|
|
2191
|
+
succeeded: segmentRecords.filter((segment) => segment.status === "succeeded").length,
|
|
2192
|
+
failed: segmentRecords.filter((segment) => segment.status === "failed").length,
|
|
2193
|
+
running: segmentRecords.filter((segment) => segment.status === "running").length,
|
|
2194
|
+
pending: segmentRecords.filter((segment) => segment.status === "pending").length,
|
|
2195
|
+
skipped: segmentRecords.filter((segment) => segment.status === "skipped").length,
|
|
2196
|
+
stalled: segmentRecords.filter((segment) => segment.status === "stalled").length,
|
|
2197
|
+
};
|
|
2198
|
+
const segParts = [`${byStatus.succeeded} succeeded`];
|
|
2199
|
+
if (byStatus.failed > 0) segParts.push(`${byStatus.failed} failed`);
|
|
2200
|
+
if (byStatus.running > 0) segParts.push(`${byStatus.running} running`);
|
|
2201
|
+
if (byStatus.pending > 0) segParts.push(`${byStatus.pending} pending`);
|
|
2202
|
+
if (byStatus.skipped > 0) segParts.push(`${byStatus.skipped} skipped`);
|
|
2203
|
+
if (byStatus.stalled > 0) segParts.push(`${byStatus.stalled} stalled`);
|
|
2204
|
+
lines.push(` Segments: ${segParts.join(", ")} (${multiSegmentTasks.length} multi-segment task(s))`);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const sortedDiskLanes = [...(diskState.lanes || [])].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
2208
|
+
if (sortedDiskLanes.length > 0) {
|
|
2209
|
+
lines.push(" Lanes:");
|
|
2210
|
+
for (const laneRec of sortedDiskLanes) {
|
|
2211
|
+
const laneTasks = (diskState.tasks || []).filter((task) => task.laneNumber === laneRec.laneNumber);
|
|
2212
|
+
const runningTask = laneTasks.find((task) => task.status === "running");
|
|
2213
|
+
const activeTask = runningTask || laneTasks[laneTasks.length - 1];
|
|
2214
|
+
const taskLabel = activeTask ? `${activeTask.taskId} (${activeTask.status})` : "idle";
|
|
2215
|
+
const segmentLabel = buildTaskSegmentProgressLabel(activeTask, segmentRecords, activeTask?.activeSegmentId ?? null);
|
|
2216
|
+
const segmentPart = segmentLabel ? ` · ${segmentLabel}` : "";
|
|
2217
|
+
const repoPart = laneRec.repoId ? ` · repo: ${laneRec.repoId}` : "";
|
|
2218
|
+
lines.push(` - Lane ${laneRec.laneNumber}: ${taskLabel}${segmentPart}${repoPart}`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2117
2222
|
if (diskState.errors.length > 0) {
|
|
2118
2223
|
lines.push(` Errors: ${diskState.errors.length}`);
|
|
2119
2224
|
}
|
|
@@ -2132,6 +2237,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
2132
2237
|
` Elapsed: ${elapsedSec}s`,
|
|
2133
2238
|
];
|
|
2134
2239
|
|
|
2240
|
+
const segmentRecords = orchBatchState.segments || [];
|
|
2241
|
+
const multiSegmentTaskCount = orchBatchState.currentLanes.reduce((count, laneRec) => {
|
|
2242
|
+
return count + laneRec.tasks.filter((task) => Array.isArray(task.task.segmentIds) && task.task.segmentIds.length > 1).length;
|
|
2243
|
+
}, 0);
|
|
2244
|
+
if (multiSegmentTaskCount > 0) {
|
|
2245
|
+
const byStatus = {
|
|
2246
|
+
succeeded: segmentRecords.filter((segment) => segment.status === "succeeded").length,
|
|
2247
|
+
failed: segmentRecords.filter((segment) => segment.status === "failed").length,
|
|
2248
|
+
running: segmentRecords.filter((segment) => segment.status === "running").length,
|
|
2249
|
+
pending: segmentRecords.filter((segment) => segment.status === "pending").length,
|
|
2250
|
+
skipped: segmentRecords.filter((segment) => segment.status === "skipped").length,
|
|
2251
|
+
stalled: segmentRecords.filter((segment) => segment.status === "stalled").length,
|
|
2252
|
+
};
|
|
2253
|
+
const segParts = [`${byStatus.succeeded} succeeded`];
|
|
2254
|
+
if (byStatus.failed > 0) segParts.push(`${byStatus.failed} failed`);
|
|
2255
|
+
if (byStatus.running > 0) segParts.push(`${byStatus.running} running`);
|
|
2256
|
+
if (byStatus.pending > 0) segParts.push(`${byStatus.pending} pending`);
|
|
2257
|
+
if (byStatus.skipped > 0) segParts.push(`${byStatus.skipped} skipped`);
|
|
2258
|
+
if (byStatus.stalled > 0) segParts.push(`${byStatus.stalled} stalled`);
|
|
2259
|
+
lines.push(` Segments: ${segParts.join(", ")} (${multiSegmentTaskCount} multi-segment task(s))`);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (orchBatchState.currentLanes.length > 0) {
|
|
2263
|
+
lines.push(" Lanes:");
|
|
2264
|
+
const sortedLanes = [...orchBatchState.currentLanes].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
2265
|
+
for (const laneRec of sortedLanes) {
|
|
2266
|
+
const monLane = latestMonitorState?.lanes.find((laneState) => laneState.laneNumber === laneRec.laneNumber);
|
|
2267
|
+
const currentTaskId = monLane?.currentTaskId || laneRec.tasks[0]?.taskId;
|
|
2268
|
+
const allocatedTask = currentTaskId
|
|
2269
|
+
? laneRec.tasks.find((task) => task.taskId === currentTaskId)
|
|
2270
|
+
: laneRec.tasks[0];
|
|
2271
|
+
const taskLabel = allocatedTask
|
|
2272
|
+
? `${allocatedTask.taskId} (${monLane?.currentTaskSnapshot?.status || "running"})`
|
|
2273
|
+
: "idle";
|
|
2274
|
+
const segmentLabel = buildTaskSegmentProgressLabel(
|
|
2275
|
+
allocatedTask?.task,
|
|
2276
|
+
segmentRecords,
|
|
2277
|
+
allocatedTask?.task.activeSegmentId ?? null,
|
|
2278
|
+
);
|
|
2279
|
+
const segmentPart = segmentLabel ? ` · ${segmentLabel}` : "";
|
|
2280
|
+
const repoPart = laneRec.repoId ? ` · repo: ${laneRec.repoId}` : "";
|
|
2281
|
+
lines.push(` - Lane ${laneRec.laneNumber}: ${taskLabel}${segmentPart}${repoPart}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2135
2285
|
if (orchBatchState.errors.length > 0) {
|
|
2136
2286
|
lines.push(` Errors: ${orchBatchState.errors.length}`);
|
|
2137
2287
|
}
|
|
@@ -4154,6 +4304,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
4154
4304
|
|
|
4155
4305
|
if (currentTask) {
|
|
4156
4306
|
lines.push(`**Task:** ${currentTask.taskId} (${currentTask.status})`);
|
|
4307
|
+
const segmentLabel = buildTaskSegmentProgressLabel(currentTask, state.segments || [], currentTask.activeSegmentId ?? null);
|
|
4308
|
+
if (segmentLabel) lines.push(`**Segment:** ${segmentLabel}`);
|
|
4309
|
+
if (currentTask.activeSegmentId) lines.push(`**Segment ID:** ${currentTask.activeSegmentId}`);
|
|
4310
|
+
const packetHomeRepo = typeof currentTask.packetRepoId === "string" ? currentTask.packetRepoId : "";
|
|
4311
|
+
const effectiveTaskRepo = currentTask.resolvedRepoId || currentTask.repoId || laneRec.repoId || "";
|
|
4312
|
+
if (packetHomeRepo && packetHomeRepo !== effectiveTaskRepo) {
|
|
4313
|
+
lines.push(`**Packet Home Repo:** ${packetHomeRepo}`);
|
|
4314
|
+
}
|
|
4157
4315
|
|
|
4158
4316
|
// Read STATUS.md from canonical task paths (workspace-safe, cross-repo-safe)
|
|
4159
4317
|
try {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
19
|
-
import { join, dirname, resolve } from "path";
|
|
19
|
+
import { join, dirname, resolve, basename } from "path";
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
|
|
22
22
|
import {
|
|
@@ -158,7 +158,9 @@ export async function executeTaskV2(
|
|
|
158
158
|
const donePath = unit.packet.donePath;
|
|
159
159
|
const promptPath = unit.packet.promptPath;
|
|
160
160
|
const taskFolder = unit.packet.taskFolder;
|
|
161
|
+
const reviewerStatePath = join(taskFolder, ".reviewer-state.json");
|
|
161
162
|
const taskId = unit.taskId;
|
|
163
|
+
const segmentId = unit.segmentId;
|
|
162
164
|
const workerAgentId = buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "worker");
|
|
163
165
|
|
|
164
166
|
// ── 1. Ensure STATUS.md exists ──────────────────────────────────
|
|
@@ -183,8 +185,8 @@ export async function executeTaskV2(
|
|
|
183
185
|
for (let iter = 0; iter < config.maxIterations; iter++) {
|
|
184
186
|
if (pauseSignal.paused) {
|
|
185
187
|
logExecution(statusPath, "Paused", `User paused at iteration ${totalIterations}`);
|
|
186
|
-
return makeResult(taskId, workerAgentId, "skipped", startTime,
|
|
187
|
-
"Paused by user", false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath);
|
|
188
|
+
return makeResult(taskId, segmentId, workerAgentId, "skipped", startTime,
|
|
189
|
+
"Paused by user", false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
// Determine remaining steps
|
|
@@ -225,11 +227,38 @@ export async function executeTaskV2(
|
|
|
225
227
|
`Iteration: ${totalIterations}`,
|
|
226
228
|
`Wrap-up signal file: ${wrapUpFile}`,
|
|
227
229
|
``,
|
|
230
|
+
`Execution repo context:`,
|
|
231
|
+
`- Execution repo ID: ${unit.executionRepoId}`,
|
|
232
|
+
`- Execution worktree (worker cwd): ${unit.worktreePath}`,
|
|
233
|
+
`- Lane repo ID: ${config.repoId}`,
|
|
234
|
+
`- Active segment ID: ${segmentId ?? "(none / whole-task execution)"}`,
|
|
235
|
+
``,
|
|
236
|
+
`Packet home context:`,
|
|
237
|
+
`- Packet home repo ID: ${unit.packetHomeRepoId}`,
|
|
238
|
+
`- Packet task folder: ${taskFolder}`,
|
|
239
|
+
`- Packet PROMPT path: ${promptPath}`,
|
|
240
|
+
`- Packet STATUS path: ${statusPath}`,
|
|
241
|
+
`- Packet .DONE path: ${donePath}`,
|
|
242
|
+
`- Packet .reviews path: ${unit.packet.reviewsDir}`,
|
|
243
|
+
``,
|
|
228
244
|
`⚠️ ORCHESTRATED RUN: Do NOT archive or move the task folder. The orchestrator handles post-merge archival.`,
|
|
229
245
|
``,
|
|
230
246
|
`⚠️ CHECKPOINT RULE: After completing EACH checkbox item, immediately edit STATUS.md to check it off (- [ ] → - [x]) BEFORE starting the next item. Do NOT batch checkbox updates at the end of a step.`,
|
|
231
247
|
];
|
|
232
248
|
|
|
249
|
+
const segmentDag = unit.task.explicitSegmentDag;
|
|
250
|
+
if (segmentDag && segmentDag.repoIds.length > 0) {
|
|
251
|
+
const edgeSummary = segmentDag.edges.length > 0
|
|
252
|
+
? segmentDag.edges.map(edge => `${edge.fromRepoId}->${edge.toRepoId}`).join(", ")
|
|
253
|
+
: "(no explicit edges)";
|
|
254
|
+
promptLines.push(
|
|
255
|
+
``,
|
|
256
|
+
`Segment DAG context (from PROMPT metadata):`,
|
|
257
|
+
`- Repos: ${segmentDag.repoIds.join(", ")}`,
|
|
258
|
+
`- Edges: ${edgeSummary}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
233
262
|
if (totalIterations > 1 && remainingSteps.length > 0) {
|
|
234
263
|
const remainingSet = new Set(remainingSteps.map(s => s.number));
|
|
235
264
|
const completedSteps = parsed.steps.filter(s => !remainingSet.has(s.number));
|
|
@@ -260,7 +289,7 @@ export async function executeTaskV2(
|
|
|
260
289
|
laneNumber: config.laneNumber,
|
|
261
290
|
taskId,
|
|
262
291
|
repoId: config.repoId,
|
|
263
|
-
cwd:
|
|
292
|
+
cwd: unit.worktreePath,
|
|
264
293
|
prompt: promptLines.join("\n"),
|
|
265
294
|
systemPrompt: config.workerSystemPrompt || undefined,
|
|
266
295
|
model: config.workerModel || undefined,
|
|
@@ -278,6 +307,10 @@ export async function executeTaskV2(
|
|
|
278
307
|
TASKPLANE_OUTBOX_DIR: outboxDir,
|
|
279
308
|
TASKPLANE_AGENT_ID: workerAgentId,
|
|
280
309
|
TASKPLANE_TASK_FOLDER: taskFolder,
|
|
310
|
+
TASKPLANE_STATUS_PATH: statusPath,
|
|
311
|
+
TASKPLANE_PROMPT_PATH: promptPath,
|
|
312
|
+
TASKPLANE_REVIEWS_DIR: unit.packet.reviewsDir,
|
|
313
|
+
TASKPLANE_REVIEWER_STATE_PATH: reviewerStatePath,
|
|
281
314
|
TASKPLANE_PROJECT_NAME: config.projectName || "project",
|
|
282
315
|
ORCH_BATCH_ID: config.batchId,
|
|
283
316
|
},
|
|
@@ -305,7 +338,7 @@ export async function executeTaskV2(
|
|
|
305
338
|
iterationTelemetry = telemetry;
|
|
306
339
|
lastTelemetry = telemetry;
|
|
307
340
|
// Emit lane snapshot
|
|
308
|
-
emitSnapshot(config, taskId, "running", telemetry, statusPath);
|
|
341
|
+
emitSnapshot(config, taskId, segmentId, "running", telemetry, statusPath, reviewerStatePath);
|
|
309
342
|
} catch { /* non-fatal: telemetry callback must never crash the engine */ }
|
|
310
343
|
});
|
|
311
344
|
|
|
@@ -315,7 +348,7 @@ export async function executeTaskV2(
|
|
|
315
348
|
let reviewerSnapshotFailures = 0;
|
|
316
349
|
const reviewerRefreshFailureThreshold = 5;
|
|
317
350
|
const reviewerRefresh = setInterval(() => {
|
|
318
|
-
const ok = emitSnapshot(config, taskId, "running", iterationTelemetry, statusPath);
|
|
351
|
+
const ok = emitSnapshot(config, taskId, segmentId, "running", iterationTelemetry, statusPath, reviewerStatePath);
|
|
319
352
|
if (ok) {
|
|
320
353
|
reviewerSnapshotFailures = 0;
|
|
321
354
|
return;
|
|
@@ -445,8 +478,8 @@ export async function executeTaskV2(
|
|
|
445
478
|
`Iteration ${totalIterations}: 0 new checkboxes (${noProgressCount}/${config.noProgressLimit} stall limit)`);
|
|
446
479
|
if (noProgressCount >= config.noProgressLimit) {
|
|
447
480
|
logExecution(statusPath, "Task blocked", `No progress after ${noProgressCount} iterations`);
|
|
448
|
-
return makeResult(taskId, workerAgentId, "failed", startTime,
|
|
449
|
-
`No progress after ${noProgressCount} iterations`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, lastTelemetry);
|
|
481
|
+
return makeResult(taskId, segmentId, workerAgentId, "failed", startTime,
|
|
482
|
+
`No progress after ${noProgressCount} iterations`, false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
450
483
|
}
|
|
451
484
|
} else {
|
|
452
485
|
noProgressCount = 0;
|
|
@@ -485,9 +518,9 @@ export async function executeTaskV2(
|
|
|
485
518
|
.map(s => `Step ${s.number}`)
|
|
486
519
|
.join(", ");
|
|
487
520
|
logExecution(statusPath, "Task incomplete", `Max iterations reached. Incomplete: ${incomplete}`);
|
|
488
|
-
return makeResult(taskId, workerAgentId, "failed", startTime,
|
|
521
|
+
return makeResult(taskId, segmentId, workerAgentId, "failed", startTime,
|
|
489
522
|
`Max iterations (${config.maxIterations}) reached with incomplete steps: ${incomplete}`,
|
|
490
|
-
false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, lastTelemetry);
|
|
523
|
+
false, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
491
524
|
}
|
|
492
525
|
|
|
493
526
|
// Create .DONE if not already present
|
|
@@ -497,8 +530,8 @@ export async function executeTaskV2(
|
|
|
497
530
|
updateStatusField(statusPath, "Status", "✅ Complete");
|
|
498
531
|
logExecution(statusPath, "Task complete", ".DONE created");
|
|
499
532
|
|
|
500
|
-
return makeResult(taskId, workerAgentId, "succeeded", startTime,
|
|
501
|
-
".DONE file created by lane-runner", true, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, lastTelemetry);
|
|
533
|
+
return makeResult(taskId, segmentId, workerAgentId, "succeeded", startTime,
|
|
534
|
+
".DONE file created by lane-runner", true, totalIterations, cumulativeCostUsd, cumulativeTokens, config, statusPath, reviewerStatePath, lastTelemetry);
|
|
502
535
|
}
|
|
503
536
|
|
|
504
537
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
@@ -522,6 +555,7 @@ export function mapLaneSnapshotStatusToWorkerStatus(
|
|
|
522
555
|
|
|
523
556
|
function makeResult(
|
|
524
557
|
taskId: string,
|
|
558
|
+
segmentId: string | null,
|
|
525
559
|
sessionName: string,
|
|
526
560
|
status: LaneTaskStatus,
|
|
527
561
|
startTime: number,
|
|
@@ -532,6 +566,7 @@ function makeResult(
|
|
|
532
566
|
totalTokens: number,
|
|
533
567
|
config?: LaneRunnerConfig,
|
|
534
568
|
statusPath?: string,
|
|
569
|
+
reviewerStatePath?: string,
|
|
535
570
|
finalTelemetry?: Partial<AgentHostResult>,
|
|
536
571
|
): LaneRunnerTaskResult {
|
|
537
572
|
const telemetry = status === "skipped"
|
|
@@ -550,6 +585,7 @@ function makeResult(
|
|
|
550
585
|
outcome: {
|
|
551
586
|
taskId,
|
|
552
587
|
status,
|
|
588
|
+
segmentId,
|
|
553
589
|
startTime,
|
|
554
590
|
endTime: Date.now(),
|
|
555
591
|
exitReason,
|
|
@@ -564,9 +600,9 @@ function makeResult(
|
|
|
564
600
|
};
|
|
565
601
|
|
|
566
602
|
// TP-115: Emit terminal snapshot with real telemetry from agent-host result
|
|
567
|
-
if (config && statusPath) {
|
|
603
|
+
if (config && statusPath && reviewerStatePath) {
|
|
568
604
|
const terminalStatus = mapLaneTaskStatusToTerminalSnapshotStatus(status);
|
|
569
|
-
emitSnapshot(config, taskId, terminalStatus, finalTelemetry ?? {}, statusPath);
|
|
605
|
+
emitSnapshot(config, taskId, segmentId, terminalStatus, finalTelemetry ?? {}, statusPath, reviewerStatePath);
|
|
570
606
|
}
|
|
571
607
|
|
|
572
608
|
return result;
|
|
@@ -577,9 +613,11 @@ const REVIEWER_STATE_STALE_MS = 120_000;
|
|
|
577
613
|
|
|
578
614
|
export function readReviewerTelemetrySnapshot(
|
|
579
615
|
config: LaneRunnerConfig,
|
|
580
|
-
|
|
616
|
+
reviewerStatePathOrStatusPath: string,
|
|
581
617
|
): (RuntimeAgentTelemetrySnapshot & { reviewType?: string; reviewStep?: number }) | null {
|
|
582
|
-
const reviewerPath =
|
|
618
|
+
const reviewerPath = basename(reviewerStatePathOrStatusPath).toLowerCase() === "status.md"
|
|
619
|
+
? join(dirname(reviewerStatePathOrStatusPath), ".reviewer-state.json")
|
|
620
|
+
: reviewerStatePathOrStatusPath;
|
|
583
621
|
if (!existsSync(reviewerPath)) return null;
|
|
584
622
|
|
|
585
623
|
try {
|
|
@@ -636,9 +674,11 @@ export function readReviewerTelemetrySnapshot(
|
|
|
636
674
|
function emitSnapshot(
|
|
637
675
|
config: LaneRunnerConfig,
|
|
638
676
|
taskId: string,
|
|
677
|
+
segmentId: string | null,
|
|
639
678
|
status: "running" | "idle" | "complete" | "failed",
|
|
640
679
|
telemetry: Partial<AgentHostResult>,
|
|
641
680
|
statusPath: string,
|
|
681
|
+
reviewerStatePath: string,
|
|
642
682
|
): boolean {
|
|
643
683
|
try {
|
|
644
684
|
// Parse progress from STATUS.md
|
|
@@ -658,7 +698,7 @@ function emitSnapshot(
|
|
|
658
698
|
};
|
|
659
699
|
} catch { /* best effort */ }
|
|
660
700
|
|
|
661
|
-
const reviewerSnapshot = readReviewerTelemetrySnapshot(config,
|
|
701
|
+
const reviewerSnapshot = readReviewerTelemetrySnapshot(config, reviewerStatePath);
|
|
662
702
|
|
|
663
703
|
const snapshot: RuntimeLaneSnapshot = {
|
|
664
704
|
batchId: config.batchId,
|
|
@@ -666,7 +706,7 @@ function emitSnapshot(
|
|
|
666
706
|
laneId: `lane-${config.laneNumber}`,
|
|
667
707
|
repoId: config.repoId,
|
|
668
708
|
taskId,
|
|
669
|
-
segmentId
|
|
709
|
+
segmentId,
|
|
670
710
|
status,
|
|
671
711
|
worker: {
|
|
672
712
|
agentId: buildRuntimeAgentId(config.agentIdPrefix, config.laneNumber, "worker"),
|
|
@@ -319,6 +319,18 @@ export function persistRuntimeState(
|
|
|
319
319
|
if (taskRecord.resolvedRepoId === undefined && parsedTask.resolvedRepoId !== undefined) {
|
|
320
320
|
taskRecord.resolvedRepoId = parsedTask.resolvedRepoId;
|
|
321
321
|
}
|
|
322
|
+
if ((taskRecord as any).packetRepoId === undefined && parsedTask.packetRepoId !== undefined) {
|
|
323
|
+
(taskRecord as any).packetRepoId = parsedTask.packetRepoId;
|
|
324
|
+
}
|
|
325
|
+
if ((taskRecord as any).packetTaskPath === undefined && parsedTask.packetTaskPath !== undefined) {
|
|
326
|
+
(taskRecord as any).packetTaskPath = parsedTask.packetTaskPath;
|
|
327
|
+
}
|
|
328
|
+
if ((taskRecord as any).segmentIds === undefined && parsedTask.segmentIds !== undefined) {
|
|
329
|
+
(taskRecord as any).segmentIds = parsedTask.segmentIds;
|
|
330
|
+
}
|
|
331
|
+
if ((taskRecord as any).activeSegmentId === undefined && parsedTask.activeSegmentId !== undefined) {
|
|
332
|
+
(taskRecord as any).activeSegmentId = parsedTask.activeSegmentId;
|
|
333
|
+
}
|
|
322
334
|
}
|
|
323
335
|
}
|
|
324
336
|
const enrichedJson = JSON.stringify(parsed, null, 2);
|