taskplane 0.26.1 → 0.28.0

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.
@@ -367,22 +367,71 @@ export function buildDashboardViewModel(
367
367
  // Build lane cards from monitor state (if available) or current lanes
368
368
  const laneCards: OrchLaneCardData[] = [];
369
369
 
370
- if (monitorState && monitorState.lanes.length > 0) {
370
+ // TP-170: Detect stale monitor data from prior waves.
371
+ // When wave N+1 starts, batchState.currentLanes is updated to wave N+1's
372
+ // lanes, but monitorState may still hold wave N's data until the first
373
+ // poll of wave N+1's monitor. Detect this mismatch by checking whether
374
+ // the monitor's lane numbers match the current allocation.
375
+ const monitorIsFresh = monitorState && monitorState.lanes.length > 0 && (
376
+ // If no current allocation, monitor data is the best we have
377
+ // (covers terminal phases like completed/failed/stopped)
378
+ batchState.currentLanes.length === 0 ||
379
+ // If allocated lanes exist, verify monitor lanes match them
380
+ monitorState.lanes.some(ml =>
381
+ batchState.currentLanes.some(cl => cl.laneNumber === ml.laneNumber),
382
+ )
383
+ );
384
+
385
+ // TP-170: Build a laneNumber → AllocatedLane index for identity reconciliation.
386
+ // In workspace mode, the monitor’s sessionName (e.g., "orch-henry-api-lane-1")
387
+ // may differ from the V2 registry agentId ("orch-henry-lane-3-worker").
388
+ // Cross-referencing with the current allocation ensures the displayed session
389
+ // name matches the authoritative laneSessionId for the current wave.
390
+ const allocatedByLaneNumber = new Map<number, { laneSessionId: string; laneId: string }>();
391
+ for (const cl of batchState.currentLanes) {
392
+ allocatedByLaneNumber.set(cl.laneNumber, { laneSessionId: cl.laneSessionId, laneId: cl.laneId });
393
+ }
394
+
395
+ if (monitorIsFresh && monitorState) {
371
396
  // Sort lanes by laneNumber (deterministic)
372
397
  const sortedLanes = [...monitorState.lanes].sort((a, b) => a.laneNumber - b.laneNumber);
373
398
 
374
399
  for (const lane of sortedLanes) {
375
400
  const snap = lane.currentTaskSnapshot;
401
+ const alloc = allocatedByLaneNumber.get(lane.laneNumber);
402
+
403
+ // TP-170: Reconcile task-level vs lane-level sessionAlive.
404
+ // resolveTaskMonitorState may derive sessionAlive from the lane
405
+ // snapshot file (snap.status === "running") while the lane-level
406
+ // sessionAlive comes from isV2AgentAlive (PID check). When the
407
+ // task snapshot says "running" but the lane session is confirmed
408
+ // dead, the task is effectively failed — not still running.
376
409
  let status: OrchLaneCardData["status"] = "idle";
377
- if (lane.failedTasks.length > 0) status = "failed";
378
- else if (snap?.status === "stalled") status = "stalled";
379
- else if (snap?.status === "running") status = "running";
380
- else if (lane.completedTasks.length > 0 && lane.remainingTasks.length === 0 && !lane.currentTaskId) status = "succeeded";
410
+ if (lane.failedTasks.length > 0) {
411
+ status = "failed";
412
+ } else if (snap?.status === "stalled") {
413
+ status = "stalled";
414
+ } else if (snap?.status === "running") {
415
+ // TP-170: TOCTOU guard — if lane session is dead but task snapshot
416
+ // still says "running", treat as failed instead of showing
417
+ // "session dead" in the card. This prevents the false positive
418
+ // where the lane snapshot file lags behind the PID liveness check.
419
+ status = lane.sessionAlive ? "running" : "failed";
420
+ } else if (
421
+ lane.completedTasks.length > 0 &&
422
+ lane.remainingTasks.length === 0 &&
423
+ !lane.currentTaskId
424
+ ) {
425
+ status = "succeeded";
426
+ }
381
427
 
382
428
  laneCards.push({
383
429
  laneNumber: lane.laneNumber,
384
- laneId: lane.laneId,
385
- sessionName: lane.sessionName,
430
+ laneId: alloc?.laneId || lane.laneId,
431
+ // TP-170: Prefer the allocation’s laneSessionId (current-wave authority)
432
+ // over the monitor’s sessionName which may use a stale or workspace-local
433
+ // name that doesn’t match the V2 registry.
434
+ sessionName: alloc?.laneSessionId || lane.sessionName,
386
435
  sessionAlive: lane.sessionAlive,
387
436
  currentTaskId: lane.currentTaskId,
388
437
  currentStepName: snap?.currentStepName || null,
@@ -395,7 +444,9 @@ export function buildDashboardViewModel(
395
444
  });
396
445
  }
397
446
  } else if (batchState.currentLanes.length > 0) {
398
- // No monitor data yet — show lanes from allocation
447
+ // No fresh monitor data — show lanes from allocation.
448
+ // This covers both initial startup (monitor hasn't polled yet)
449
+ // and wave transitions (monitor data is stale from prior wave).
399
450
  const sortedLanes = [...batchState.currentLanes].sort((a, b) => a.laneNumber - b.laneNumber);
400
451
  for (const lane of sortedLanes) {
401
452
  laneCards.push({
@@ -495,7 +546,11 @@ export function renderLaneCard(card: OrchLaneCardData, colWidth: number, theme:
495
546
  if (card.currentStepName) {
496
547
  stepInfo = trunc(card.currentStepName, w - 2);
497
548
  } else if (card.currentTaskId && card.totalItems === 0) {
498
- stepInfo = "waiting for data...";
549
+ // TP-170: Distinguish startup-grace (no STATUS.md yet) from
550
+ // genuine stale data. During startup, the lane is alive but
551
+ // hasn’t written STATUS.md yet — show "starting..." instead of
552
+ // the misleading "waiting for data..." which implies a problem.
553
+ stepInfo = card.sessionAlive ? "starting..." : "no status data";
499
554
  } else if (!card.currentTaskId && card.status !== "idle") {
500
555
  stepInfo = `${card.completedTasks}/${card.totalLaneTasks} tasks`;
501
556
  }
@@ -512,8 +567,12 @@ export function renderLaneCard(card: OrchLaneCardData, colWidth: number, theme:
512
567
  extraInfo = `${card.totalChecked}/${card.totalItems} ✓`;
513
568
  extraColor = card.totalChecked === card.totalItems ? "success" : "muted";
514
569
  } else if (!card.sessionAlive && card.status === "running") {
515
- extraInfo = "session dead";
516
- extraColor = "error";
570
+ // TP-170: With the TOCTOU guard in buildDashboardViewModel, a lane
571
+ // with a dead session and task snapshot "running" now gets status
572
+ // "failed" instead. This branch guards any remaining edge cases
573
+ // (e.g., allocation-fallback lane assumed alive but actually dead).
574
+ extraInfo = "session ended";
575
+ extraColor = "warning";
517
576
  }
518
577
  const extraStr = theme.fg(extraColor, trunc(extraInfo, w));
519
578
  const extraVis = Math.min(extraInfo.length, w);
@@ -54,3 +54,37 @@ export function runGit(
54
54
  }
55
55
  }
56
56
 
57
+ /**
58
+ * Run a git command with custom environment variables.
59
+ *
60
+ * Used by TP-169 to create commits on the orch branch without
61
+ * modifying HEAD, via GIT_INDEX_FILE for alternate index manipulation.
62
+ *
63
+ * @param args - Git command arguments
64
+ * @param cwd - Working directory
65
+ * @param env - Additional environment variables to set
66
+ */
67
+ export function runGitWithEnv(
68
+ args: string[],
69
+ cwd: string,
70
+ env: Record<string, string>,
71
+ ): { ok: boolean; stdout: string; stderr: string } {
72
+ try {
73
+ const stdout = execFileSync("git", args, {
74
+ encoding: "utf-8",
75
+ timeout: 30_000,
76
+ cwd,
77
+ stdio: ["pipe", "pipe", "pipe"],
78
+ env: { ...process.env, ...env },
79
+ }).trim();
80
+ return { ok: true, stdout, stderr: "" };
81
+ } catch (err: unknown) {
82
+ const e = err as { stdout?: string; stderr?: string; message?: string };
83
+ return {
84
+ ok: false,
85
+ stdout: (e.stdout ?? "").toString().trim(),
86
+ stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
87
+ };
88
+ }
89
+ }
90
+