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.
@@ -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 donePath = resolveTaskDonePath(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
1186
- const statusResult = await parseWorktreeStatusMdAsync(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
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: config.worktreePath,
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
- statusPath: string,
616
+ reviewerStatePathOrStatusPath: string,
581
617
  ): (RuntimeAgentTelemetrySnapshot & { reviewType?: string; reviewStep?: number }) | null {
582
- const reviewerPath = join(dirname(statusPath), ".reviewer-state.json");
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, statusPath);
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: null,
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);