taskplane 0.28.0 → 0.28.2

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.
@@ -241,6 +241,7 @@ let viewingHistoryId = null; // batchId if viewing history, null if live
241
241
 
242
242
  let viewerMode = null; // "conversation" | "status-md" | null
243
243
  let viewerTarget = null; // session name (conversation) or taskId (status-md)
244
+ let lastBatchId = null; // TP-178: track batchId for stale viewer detection (#487)
244
245
 
245
246
  // ─── Repo Helpers ───────────────────────────────────────────────────────────
246
247
 
@@ -515,14 +516,20 @@ function renderSummary(batch) {
515
516
  const fillPct = ws.total > 0 ? (ws.checked / ws.total) * 100 : 0;
516
517
  const checkboxDone = ws.checked === ws.total && ws.total > 0;
517
518
  const pastWave = ws.waveIdx < currentWaveIdx;
518
- const batchDone = batch.phase === "completed" || batch.phase === "merging";
519
- const isDone = checkboxDone || pastWave || batchDone || ws.allSucceeded;
520
- const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || batch.phase === "merging");
521
- const isFuture = ws.waveIdx > currentWaveIdx && batch.phase === "executing";
519
+ const batchDone = batch.phase === "completed";
520
+ // TP-178: During merging, only past waves are truly done. The current wave's
521
+ // checkboxDone/allSucceeded can be true (tasks finished) but the wave itself
522
+ // isn't done until the merge completes. (#493)
523
+ const isMerging = batch.phase === "merging";
524
+ const isDone = batchDone || pastWave || (!isMerging && (checkboxDone || ws.allSucceeded));
525
+ const isMergingWave = isMerging && ws.waveIdx === currentWaveIdx;
526
+ const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || isMerging);
527
+ const isFuture = ws.waveIdx > currentWaveIdx && (batch.phase === "executing" || isMerging);
522
528
 
523
529
  const fillClass = isDone ? "pct-hi" : fillPct > 50 ? "pct-mid" : fillPct > 0 ? "pct-low" : "pct-0";
524
530
  const fillWidth = isDone ? 100 : fillPct;
525
- const segClass = isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
531
+ // TP-178: Add merging visual state for the wave currently being merged (#493)
532
+ const segClass = isMergingWave ? "wave-seg-current wave-seg-merging" : isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
526
533
 
527
534
  // TP-148: Use segment-aware labels in tooltip when available
528
535
  const segLabels = waveSegmentLabels[ws.waveIdx] || new Map();
@@ -604,9 +611,11 @@ function renderSummary(batch) {
604
611
  const waveIdx = batch.currentWaveIndex || 0;
605
612
  let wavesHtml = '<span style="color:var(--text-muted); font-weight:600; margin-right:4px;">Waves</span>';
606
613
  batch.wavePlan.forEach((taskIds, i) => {
607
- const isDone = i < waveIdx || batch.phase === "completed" || batch.phase === "merging";
608
- const isCurrent = i === waveIdx && batch.phase === "executing";
609
- const cls = isDone ? "done" : isCurrent ? "current" : "";
614
+ // TP-178: During merging, only past waves are done; current wave shows merging state (#493)
615
+ const isDone = i < waveIdx || batch.phase === "completed";
616
+ const isCurrent = i === waveIdx && (batch.phase === "executing" || batch.phase === "merging");
617
+ const isMergingChip = i === waveIdx && batch.phase === "merging";
618
+ const cls = isDone ? "done" : isMergingChip ? "current merging" : isCurrent ? "current" : "";
610
619
  wavesHtml += `<span class="wave-chip ${cls}">W${i + 1} [${taskIds.join(", ")}]</span>`;
611
620
  });
612
621
  $summaryWaves.innerHTML = wavesHtml;
@@ -704,7 +713,10 @@ function renderLanesTasks(batch, sessions) {
704
713
  // TP-176: Succeeded tasks always show 100% regardless of sidecar/statusData (#491).
705
714
  let progressHtml = "";
706
715
  const v2p = ls && ls._v2Progress;
707
- const useV2 = v2p && v2p.total > 0 && ls.taskId === task.taskId;
716
+ const taskMatch = v2p && ls.taskId === task.taskId;
717
+ // Split V2 usage: progress needs totals > 0, but step/iter can be used whenever present
718
+ const useV2Progress = taskMatch && v2p.total > 0;
719
+ const useV2Step = taskMatch && !!v2p.currentStep;
708
720
  if (task.status === "succeeded") {
709
721
  // #491 fix: succeeded tasks always show 100%
710
722
  progressHtml = `
@@ -712,9 +724,9 @@ function renderLanesTasks(batch, sessions) {
712
724
  <div class="task-progress-bar"><div class="task-progress-fill pct-hi" style="width:100%"></div></div>
713
725
  <span class="task-progress-text">100%</span>
714
726
  </div>`;
715
- } else if (sd || useV2) {
716
- const displayChecked = useV2 ? v2p.checked : (sd ? sd.checked : 0);
717
- const displayTotal = useV2 ? v2p.total : (sd ? sd.total : 0);
727
+ } else if (useV2Progress || (sd && sd.total > 0)) {
728
+ const displayChecked = useV2Progress ? v2p.checked : sd.checked;
729
+ const displayTotal = useV2Progress ? v2p.total : sd.total;
718
730
  const displayProgress = displayTotal > 0 ? Math.round((displayChecked / displayTotal) * 100) : 0;
719
731
  const fillClass = pctClass(displayProgress);
720
732
  progressHtml = `
@@ -724,6 +736,14 @@ function renderLanesTasks(batch, sessions) {
724
736
  </div>
725
737
  <span class="task-progress-text">${displayProgress}% ${displayChecked}/${displayTotal}</span>
726
738
  </div>`;
739
+ } else if (task.status === "running") {
740
+ // #494 fix: running tasks without meaningful totals show executing indicator
741
+ // This covers non-final segments, early execution before sidecar captures, and stale 0/0 data
742
+ progressHtml = `
743
+ <div class="task-progress">
744
+ <div class="task-progress-bar"><div class="task-progress-fill pct-low task-progress-executing" style="width:100%"></div></div>
745
+ <span class="task-progress-text">executing…</span>
746
+ </div>`;
727
747
  } else if (task.status === "pending") {
728
748
  progressHtml = `
729
749
  <div class="task-progress">
@@ -735,13 +755,20 @@ function renderLanesTasks(batch, sessions) {
735
755
  }
736
756
 
737
757
  // Step cell
758
+ // TP-178: Prefer V2 snapshot currentStep (refreshed every sidecar poll) over
759
+ // server-parsed statusData which can lag behind (#488).
738
760
  let stepHtml = "";
739
- if (sd) {
740
- stepHtml = escapeHtml(sd.currentStep);
741
- if (sd.iteration > 0) stepHtml += `<span class="task-iter">i${sd.iteration}</span>`;
742
- if (sd.reviews > 0) stepHtml += `<span class="task-iter">r${sd.reviews}</span>`;
743
- } else if (task.status === "succeeded") {
761
+ if (task.status === "succeeded") {
762
+ // TP-178: Succeeded tasks always show "Complete" regardless of sidecar data (#491)
744
763
  stepHtml = '<span style="color:var(--green)">Complete</span>';
764
+ } else if (sd || useV2Step) {
765
+ // #488 fix: prefer V2 step name whenever present (even if totals are 0)
766
+ const stepName = useV2Step ? v2p.currentStep : (sd ? sd.currentStep : "Unknown");
767
+ const iter = (useV2Step && v2p.iteration != null) ? v2p.iteration : (sd ? sd.iteration : 0);
768
+ const revs = (useV2Step && v2p.reviews != null) ? v2p.reviews : (sd ? sd.reviews : 0);
769
+ stepHtml = escapeHtml(stepName);
770
+ if (iter > 0) stepHtml += `<span class="task-iter">i${iter}</span>`;
771
+ if (revs > 0) stepHtml += `<span class="task-iter">r${revs}</span>`;
745
772
  } else if (task.status === "pending") {
746
773
  stepHtml = '<span style="color:var(--text-faint)">Waiting</span>';
747
774
  } else {
@@ -987,15 +1014,27 @@ function renderMergeAgents(batch, sessions) {
987
1014
  const effectiveAlive = !!effectiveSession;
988
1015
  if (effectiveSession) shownSessions.add(effectiveSession);
989
1016
 
990
- // Find merge telemetry: try sessions by lane number first
1017
+ // TP-178: Find merge telemetry precisely using waveIndex (#498).
1018
+ // First try matching by waveIndex from the telemetry entries (injected from merge snapshots).
1019
+ // Then fall back to session-based matching (lane numbers), but never use a
1020
+ // catch-all fallback that grabs any merge session's telemetry.
991
1021
  let mergeTel = null;
992
- for (const ln of waveLaneNums) {
993
- const candidate = getMergeSessionName(ln);
994
- if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
1022
+ // Priority 1: Match by waveIndex in telemetry entries
1023
+ for (const [telKey, tel] of Object.entries(telemetry)) {
1024
+ if (tel._source === "merge-snapshot" && tel.waveIndex === mr.waveIndex) {
1025
+ mergeTel = tel;
1026
+ break;
1027
+ }
1028
+ }
1029
+ // Priority 2: Match by lane number session
1030
+ if (!mergeTel) {
1031
+ for (const ln of waveLaneNums) {
1032
+ const candidate = getMergeSessionName(ln);
1033
+ if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
1034
+ }
995
1035
  }
996
- // Fallback: effective session telemetry or any merge session
1036
+ // Priority 3: Effective session telemetry only (no catch-all fallback)
997
1037
  if (!mergeTel && effectiveSession) mergeTel = telemetry[effectiveSession] || null;
998
- if (!mergeTel) mergeTel = mergeSessions.reduce((found, ms) => found || telemetry[ms] || null, null);
999
1038
 
1000
1039
  html += `<tr>`;
1001
1040
  html += `<td class="merge-wave-cell">Wave ${mr.waveIndex + 1}</td>`;
@@ -1443,6 +1482,8 @@ function buildRecoveryTimeline(supervisor) {
1443
1482
  target: a.target || a.lane || a.taskId || "",
1444
1483
  outcome: a.outcome || a.result || "",
1445
1484
  reason: a.reason || "",
1485
+ context: a.context || "",
1486
+ detail: a.detail || "",
1446
1487
  source: "action"
1447
1488
  }));
1448
1489
 
@@ -1456,6 +1497,8 @@ function buildRecoveryTimeline(supervisor) {
1456
1497
  target: e.target || e.lane || e.taskId || "",
1457
1498
  outcome: e.outcome || e.result || "",
1458
1499
  reason: e.reason || e.message || "",
1500
+ context: e.context || "",
1501
+ detail: e.detail || "",
1459
1502
  source: "event"
1460
1503
  }));
1461
1504
 
@@ -1488,6 +1531,7 @@ function renderSupervisorActions(supervisor) {
1488
1531
  const target = entry.target;
1489
1532
  const outcome = entry.outcome;
1490
1533
  const reason = entry.reason;
1534
+ const description = entry.context || entry.detail || "";
1491
1535
 
1492
1536
  const outcomeCls = outcome === "success" || outcome === "recovered"
1493
1537
  ? "action-success"
@@ -1507,6 +1551,11 @@ function renderSupervisorActions(supervisor) {
1507
1551
  if (target) html += `<span class="supervisor-action-target">${escapeHtml(target)}</span>`;
1508
1552
  if (outcome) html += `<span class="supervisor-action-outcome ${outcomeCls}">${escapeHtml(outcome)}</span>`;
1509
1553
  html += ` </div>`;
1554
+ if (description) {
1555
+ const fullDesc = escapeHtml(description);
1556
+ const truncated = description.length > 100 ? escapeHtml(description.slice(0, 100)) + "\u2026" : fullDesc;
1557
+ html += `<div class="supervisor-action-description" title="${fullDesc}">${truncated}</div>`;
1558
+ }
1510
1559
  if (reason) {
1511
1560
  html += `<div class="supervisor-action-reason">${escapeHtml(reason)}</div>`;
1512
1561
  }
@@ -1567,6 +1616,9 @@ function render(data) {
1567
1616
  $lastUpdate.textContent = new Date().toLocaleTimeString();
1568
1617
 
1569
1618
  if (!batch) {
1619
+ // TP-178: Clear viewer when batch disappears (#487)
1620
+ if (lastBatchId && viewerMode) closeViewer();
1621
+ lastBatchId = null;
1570
1622
  renderHeader(null);
1571
1623
  renderSummary(null);
1572
1624
  renderSupervisor(data);
@@ -1576,6 +1628,12 @@ function render(data) {
1576
1628
  return;
1577
1629
  }
1578
1630
 
1631
+ // TP-178: Detect batchId change — clear stale viewer state (#487)
1632
+ if (batch.batchId && lastBatchId && batch.batchId !== lastBatchId && viewerMode) {
1633
+ closeViewer();
1634
+ }
1635
+ lastBatchId = batch.batchId || null;
1636
+
1579
1637
  // Live batch is running — hide history panel, reset viewing state
1580
1638
  if (viewingHistoryId) {
1581
1639
  viewingHistoryId = null;
@@ -415,6 +415,20 @@ body {
415
415
  color: var(--green);
416
416
  }
417
417
 
418
+ /* TP-178: Merging wave indicator — pulsing accent to distinguish from executing (#493) */
419
+ .wave-chip.merging {
420
+ animation: merge-pulse 1.5s ease-in-out infinite;
421
+ }
422
+
423
+ .wave-seg-merging {
424
+ animation: merge-pulse 1.5s ease-in-out infinite;
425
+ }
426
+
427
+ @keyframes merge-pulse {
428
+ 0%, 100% { opacity: 1; }
429
+ 50% { opacity: 0.5; }
430
+ }
431
+
418
432
  /* ─── Content ──────────────────────────────────────────────────────────── */
419
433
 
420
434
  .content {
@@ -713,6 +727,16 @@ body {
713
727
  .task-progress-fill.pct-mid { background: var(--cyan); }
714
728
  .task-progress-fill.pct-hi { background: var(--green); }
715
729
 
730
+ /* TP-178: Pulsing indicator for executing tasks without sidecar progress (#494) */
731
+ .task-progress-executing {
732
+ opacity: 0.4;
733
+ animation: executing-pulse 2s ease-in-out infinite;
734
+ }
735
+ @keyframes executing-pulse {
736
+ 0%, 100% { opacity: 0.25; }
737
+ 50% { opacity: 0.55; }
738
+ }
739
+
716
740
  .task-progress-text {
717
741
  font-family: var(--font-mono);
718
742
  font-size: 0.75rem;
@@ -1555,6 +1579,18 @@ body {
1555
1579
  color: var(--yellow);
1556
1580
  }
1557
1581
 
1582
+ .supervisor-action-description {
1583
+ font-size: 0.8rem;
1584
+ color: var(--text-secondary, #b0b0b0);
1585
+ margin-top: 3px;
1586
+ line-height: 1.4;
1587
+ overflow: hidden;
1588
+ text-overflow: ellipsis;
1589
+ white-space: nowrap;
1590
+ max-width: 600px;
1591
+ cursor: default;
1592
+ }
1593
+
1558
1594
  .supervisor-action-reason {
1559
1595
  font-size: 0.78rem;
1560
1596
  color: var(--text-muted);
@@ -1153,6 +1153,8 @@ function buildDashboardState() {
1153
1153
  latestTotalTokens: (agent.inputTokens || 0) + (agent.outputTokens || 0),
1154
1154
  _updatedAt: snap.updatedAt,
1155
1155
  _source: "merge-snapshot",
1156
+ // TP-178: Include waveIndex for precise wave-telemetry association (#498)
1157
+ waveIndex: snap.waveIndex != null ? snap.waveIndex : undefined,
1156
1158
  };
1157
1159
  }
1158
1160
  }
@@ -659,11 +659,11 @@ export function parsePromptForOrchestrator(
659
659
 
660
660
  // Check for "None" variants
661
661
  if (!/\*?\*?None\*?\*?/i.test(depBody) && depBody.length > 0) {
662
- // Pattern 1: "**Requires:** COMP-005 ..." or "**Requires:** time-off/TO-014 ..."
663
- const requiresMatches = depBody.matchAll(
664
- /\*?\*?Requires:?\*?\*?\s*((?:[a-z0-9-]+\/)?[A-Z]+-\d+)/gi,
662
+ // Pattern 1: "**Requires:** COMP-005 ..." or "**Task:** TO-014 ..."
663
+ const labeledMatches = depBody.matchAll(
664
+ /\*?\*?(?:Requires|Task):?\*?\*?\s*((?:[a-z0-9-]+\/)?[A-Z]+-\d+)/gi,
665
665
  );
666
- for (const m of requiresMatches) {
666
+ for (const m of labeledMatches) {
667
667
  const dep = normalizeDependencyReference(m[1]);
668
668
  if (!dependencies.includes(dep)) dependencies.push(dep);
669
669
  }
@@ -15,7 +15,7 @@ import { ORCH_MESSAGES, computeIntegrateCleanupResult } from "./messages.ts";
15
15
  import type { IntegrateCleanupRepoFindings } from "./messages.ts";
16
16
  import { computeWaveAssignments } from "./waves.ts";
17
17
  import { createOrchWidget, formatDependencyGraph, formatWavePlan } from "./formatting.ts";
18
- import { deleteBatchState, loadBatchState, saveBatchState, detectOrphanSessions } from "./persistence.ts";
18
+ import { deleteBatchState, loadBatchState, saveBatchState, detectOrphanSessions, updateBatchHistoryIntegration } from "./persistence.ts";
19
19
  import { deleteStaleBranches, listWorktrees, resolveWorktreeBasePath, formatPreflightResults, runPreflight } from "./worktree.ts";
20
20
  import { computeTransitiveDependents, resolveCanonicalTaskPaths } from "./execution.ts";
21
21
  import { executeOrchBatch } from "./engine.ts";
@@ -1385,7 +1385,7 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
1385
1385
  }
1386
1386
  },
1387
1387
  deleteBatchState: () => {
1388
- try { deleteBatchState(repoRoot); } catch { /* best effort */ }
1388
+ try { deleteBatchState(stateRoot ?? repoRoot); } catch { /* best effort */ }
1389
1389
  },
1390
1390
  };
1391
1391
 
@@ -1411,6 +1411,11 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
1411
1411
  try {
1412
1412
  cleanupPostIntegrate(stateRoot ?? repoRoot, context.batchId);
1413
1413
  } catch { /* best effort — don't fail integration for cleanup errors */ }
1414
+
1415
+ // TP-179: Write integratedAt to batch history before state is gone
1416
+ try {
1417
+ updateBatchHistoryIntegration(stateRoot ?? repoRoot, context.batchId, Date.now());
1418
+ } catch { /* best effort */ }
1414
1419
  }
1415
1420
 
1416
1421
  return result;
@@ -1428,7 +1433,7 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
1428
1433
  *
1429
1434
  * @since TP-043
1430
1435
  */
1431
- export function buildCiDeps(repoRoot: string): CiDeps {
1436
+ export function buildCiDeps(repoRoot: string, stateRoot?: string): CiDeps {
1432
1437
  return {
1433
1438
  runCommand: (cmd: string, cmdArgs: string[]) => {
1434
1439
  try {
@@ -1450,7 +1455,7 @@ export function buildCiDeps(repoRoot: string): CiDeps {
1450
1455
  },
1451
1456
  runGit: (gitArgs: string[]) => runGit(gitArgs, repoRoot),
1452
1457
  deleteBatchState: () => {
1453
- try { deleteBatchState(repoRoot); } catch { /* best effort */ }
1458
+ try { deleteBatchState(stateRoot ?? repoRoot); } catch { /* best effort */ }
1454
1459
  },
1455
1460
  };
1456
1461
  }
@@ -2149,7 +2154,7 @@ export default function (pi: ExtensionAPI) {
2149
2154
  mode,
2150
2155
  repoRoot,
2151
2156
  buildIntegrationExecutor(repoRoot, opId, execCtx!.workspaceRoot),
2152
- buildCiDeps(repoRoot),
2157
+ buildCiDeps(repoRoot, execCtx!.workspaceRoot),
2153
2158
  sDeps,
2154
2159
  );
2155
2160
  return;
@@ -2486,7 +2491,7 @@ export default function (pi: ExtensionAPI) {
2486
2491
  mode,
2487
2492
  execCtx!.repoRoot,
2488
2493
  buildIntegrationExecutor(execCtx!.repoRoot, opId, execCtx!.workspaceRoot),
2489
- buildCiDeps(execCtx!.repoRoot),
2494
+ buildCiDeps(execCtx!.repoRoot, execCtx!.workspaceRoot),
2490
2495
  sDeps,
2491
2496
  );
2492
2497
  return;
@@ -3320,7 +3325,12 @@ export default function (pi: ExtensionAPI) {
3320
3325
  hasWarning = true;
3321
3326
  }
3322
3327
 
3323
- try { deleteBatchState(repoRoot); } catch { /* best effort */ }
3328
+ // TP-179: Write integratedAt to batch history before deleting state
3329
+ if (batchId) {
3330
+ try { updateBatchHistoryIntegration(stateRoot, batchId, Date.now()); } catch { /* best effort */ }
3331
+ }
3332
+
3333
+ try { deleteBatchState(stateRoot); } catch { /* best effort */ }
3324
3334
 
3325
3335
  // ── TP-065: Post-integrate artifact cleanup (Layer 1) ────
3326
3336
  // Delete batch-specific telemetry and merge result files.
@@ -1876,6 +1876,35 @@ export function saveBatchHistory(repoRoot: string, summary: BatchHistorySummary)
1876
1876
  }
1877
1877
  }
1878
1878
 
1879
+ /**
1880
+ * Update an existing batch history entry with the integration timestamp.
1881
+ *
1882
+ * Sets `integratedAt` on the matching entry (by batchId). If no entry
1883
+ * is found, this is a no-op — the batch may predate the history feature.
1884
+ *
1885
+ * @since TP-179
1886
+ */
1887
+ export function updateBatchHistoryIntegration(repoRoot: string, batchId: string, integratedAt: number): void {
1888
+ const filePath = batchHistoryPath(repoRoot);
1889
+ try {
1890
+ const history = loadBatchHistory(repoRoot);
1891
+ const entry = history.find(e => e.batchId === batchId);
1892
+ if (!entry) {
1893
+ execLog("batch", "history", `no history entry found for batchId=${batchId}, skipping integratedAt update`);
1894
+ return;
1895
+ }
1896
+ entry.integratedAt = integratedAt;
1897
+ const dir = dirname(filePath);
1898
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1899
+ const tmpPath = filePath + ".tmp";
1900
+ writeFileSync(tmpPath, JSON.stringify(history, null, 2));
1901
+ renameSync(tmpPath, filePath);
1902
+ execLog("batch", "history", `updated integratedAt for batchId=${batchId}`);
1903
+ } catch (err) {
1904
+ execLog("batch", "history", `failed to update integratedAt: ${err}`);
1905
+ }
1906
+ }
1907
+
1879
1908
 
1880
1909
  // ── Tier 0 Supervisor Event Logging (TP-039 Step 2) ─────────────────
1881
1910
 
@@ -3295,6 +3295,8 @@ export interface BatchHistorySummary {
3295
3295
  tokens: TokenCounts;
3296
3296
  tasks: BatchTaskSummary[];
3297
3297
  waves: BatchWaveSummary[];
3298
+ /** Timestamp (ms since epoch) when the batch was integrated. Set by orch-integrate. */
3299
+ integratedAt?: number;
3298
3300
  }
3299
3301
 
3300
3302
  /** Max number of batch history entries to retain. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",