taskplane 0.7.2 → 0.8.1
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 +66 -15
- package/dashboard/server.cjs +12 -4
- package/extensions/task-runner.ts +346 -127
- package/extensions/taskplane/config-loader.ts +1 -1
- package/extensions/taskplane/config-schema.ts +7 -5
- package/extensions/taskplane/execution.ts +153 -7
- package/extensions/taskplane/extension.ts +16 -3
- package/extensions/taskplane/merge.ts +91 -21
- package/extensions/taskplane/supervisor.ts +46 -3
- package/extensions/taskplane/types.ts +2 -2
- package/package.json +1 -1
- package/templates/agents/local/task-worker.md +3 -2
- package/templates/agents/task-worker.md +17 -13
- package/templates/config/task-orchestrator.yaml +1 -1
- package/templates/config/task-runner.yaml +3 -3
package/dashboard/public/app.js
CHANGED
|
@@ -17,9 +17,11 @@ function formatDuration(ms) {
|
|
|
17
17
|
return `${m}m ${String(s).padStart(2, "0")}s`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function relativeTime(
|
|
21
|
-
if (!
|
|
22
|
-
const
|
|
20
|
+
function relativeTime(epochOrIso) {
|
|
21
|
+
if (!epochOrIso) return "";
|
|
22
|
+
const ts = typeof epochOrIso === "string" ? new Date(epochOrIso).getTime() : epochOrIso;
|
|
23
|
+
if (isNaN(ts)) return "";
|
|
24
|
+
const diff = Date.now() - ts;
|
|
23
25
|
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
|
|
24
26
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
25
27
|
return `${Math.floor(diff / 3600000)}h ago`;
|
|
@@ -587,6 +589,7 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
587
589
|
const mergeResults = batch?.mergeResults || [];
|
|
588
590
|
const tmuxSet = new Set(tmuxSessions || []);
|
|
589
591
|
const showRepos = knownRepos.length >= 2;
|
|
592
|
+
const telemetry = currentData?.telemetry || {};
|
|
590
593
|
|
|
591
594
|
// Check for active merge sessions (convention: orch-merge-*)
|
|
592
595
|
const mergeSessions = (tmuxSessions || []).filter(s => s.startsWith("orch-merge"));
|
|
@@ -597,7 +600,7 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
597
600
|
}
|
|
598
601
|
|
|
599
602
|
let html = '<table class="merge-table"><thead><tr>';
|
|
600
|
-
html += '<th>Wave</th><th>Status</th><th>Session</th><th>Attach</th><th>Details</th>';
|
|
603
|
+
html += '<th>Wave</th><th>Status</th><th>Session</th><th>Telemetry</th><th>Attach</th><th>Details</th>';
|
|
601
604
|
html += '</tr></thead><tbody>';
|
|
602
605
|
|
|
603
606
|
// Show merge results
|
|
@@ -618,10 +621,29 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
618
621
|
const sessionName = `orch-merge-w${mr.waveIndex + 1}`;
|
|
619
622
|
const alive = tmuxSet.has(sessionName);
|
|
620
623
|
|
|
624
|
+
// Look for merge telemetry data
|
|
625
|
+
const mergeTel = telemetry[sessionName] || telemetry[`orch-merge-${mr.waveIndex + 1}`] || null;
|
|
626
|
+
|
|
621
627
|
html += `<tr>`;
|
|
622
628
|
html += `<td style="font-family:var(--font-mono);">Wave ${mr.waveIndex + 1}</td>`;
|
|
623
629
|
html += `<td><span class="status-badge ${statusCls}">${mr.status}</span></td>`;
|
|
624
630
|
html += `<td style="font-family:var(--font-mono);font-size:0.8rem;">${alive ? escapeHtml(sessionName) : "—"}</td>`;
|
|
631
|
+
// Telemetry cell
|
|
632
|
+
html += `<td style="font-size:0.75rem;">`;
|
|
633
|
+
if (mergeTel) {
|
|
634
|
+
const totalTok = (mergeTel.inputTokens || 0) + (mergeTel.outputTokens || 0);
|
|
635
|
+
const cost = mergeTel.cost || 0;
|
|
636
|
+
if (totalTok > 0 || cost > 0) {
|
|
637
|
+
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
638
|
+
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
639
|
+
html += `</span>`;
|
|
640
|
+
} else {
|
|
641
|
+
html += '<span style="color:var(--text-faint);">—</span>';
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
html += '<span style="color:var(--text-faint);">—</span>';
|
|
645
|
+
}
|
|
646
|
+
html += `</td>`;
|
|
625
647
|
html += `<td>`;
|
|
626
648
|
if (alive) {
|
|
627
649
|
const cmd = `tmux attach -t ${sessionName}`;
|
|
@@ -650,7 +672,8 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
650
672
|
html += `<td>${repoBadgeHtml(rr.repoId)}</td>`;
|
|
651
673
|
html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
|
|
652
674
|
html += `<td style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-faint);">${rrLanes}</td>`;
|
|
653
|
-
html += `<td></td>`;
|
|
675
|
+
html += `<td></td>`; /* telemetry placeholder */
|
|
676
|
+
html += `<td></td>`; /* attach placeholder */
|
|
654
677
|
html += `<td style="font-size:0.75rem;color:var(--text-faint);">${rrDetail}</td>`;
|
|
655
678
|
html += `</tr>`;
|
|
656
679
|
}
|
|
@@ -662,11 +685,28 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
662
685
|
const alreadyShown = mergeResults.some((mr) => `orch-merge-w${mr.waveIndex + 1}` === sess);
|
|
663
686
|
if (alreadyShown) continue;
|
|
664
687
|
|
|
688
|
+
const sessTel = telemetry[sess] || null;
|
|
665
689
|
const cmd = `tmux attach -t ${sess}`;
|
|
666
690
|
html += `<tr>`;
|
|
667
691
|
html += `<td style="font-family:var(--font-mono);">—</td>`;
|
|
668
692
|
html += `<td><span class="status-badge status-running"><span class="status-dot running"></span> merging</span></td>`;
|
|
669
693
|
html += `<td style="font-family:var(--font-mono);font-size:0.8rem;">${escapeHtml(sess)}</td>`;
|
|
694
|
+
// Telemetry cell for active merge session
|
|
695
|
+
html += `<td style="font-size:0.75rem;">`;
|
|
696
|
+
if (sessTel) {
|
|
697
|
+
const totalTok = (sessTel.inputTokens || 0) + (sessTel.outputTokens || 0);
|
|
698
|
+
const cost = sessTel.cost || 0;
|
|
699
|
+
if (totalTok > 0 || cost > 0) {
|
|
700
|
+
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
701
|
+
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
702
|
+
html += `</span>`;
|
|
703
|
+
} else {
|
|
704
|
+
html += '<span style="color:var(--text-faint);">—</span>';
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
html += '<span style="color:var(--text-faint);">—</span>';
|
|
708
|
+
}
|
|
709
|
+
html += `</td>`;
|
|
670
710
|
html += `<td><span class="tmux-cmd" data-tmux="${escapeHtml(sess)}" onclick="copyTmuxCmd('${escapeHtml(sess)}')" title="Click to copy">${escapeHtml(cmd)}</span></td>`;
|
|
671
711
|
html += `<td>—</td>`;
|
|
672
712
|
html += `</tr>`;
|
|
@@ -711,17 +751,16 @@ function renderNoBatch() {
|
|
|
711
751
|
if ($mergePanel) $mergePanel.style.display = "none";
|
|
712
752
|
if ($errorsPanel) $errorsPanel.style.display = "none";
|
|
713
753
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// No history yet — show placeholder in the history panel
|
|
754
|
+
// Show a placeholder while history loads. loadHistoryList() (called in
|
|
755
|
+
// render() just before this) is async — the fresh list may not be
|
|
756
|
+
// available yet. The loadHistoryList callback will replace this with
|
|
757
|
+
// the actual latest entry once it resolves.
|
|
758
|
+
if (!viewingHistoryId) {
|
|
720
759
|
$historyBody.innerHTML = `
|
|
721
760
|
<div class="no-batch">
|
|
722
761
|
<div class="no-batch-icon">⏳</div>
|
|
723
|
-
<div class="no-batch-title">
|
|
724
|
-
<div class="no-batch-hint"
|
|
762
|
+
<div class="no-batch-title">Batch complete</div>
|
|
763
|
+
<div class="no-batch-hint">Loading history…</div>
|
|
725
764
|
</div>`;
|
|
726
765
|
$historyPanel.style.display = "";
|
|
727
766
|
}
|
|
@@ -1405,10 +1444,22 @@ function loadHistoryList() {
|
|
|
1405
1444
|
.then(list => {
|
|
1406
1445
|
historyList = list || [];
|
|
1407
1446
|
renderHistoryDropdown();
|
|
1408
|
-
//
|
|
1409
|
-
|
|
1447
|
+
// Auto-select the latest history entry when no live batch is running.
|
|
1448
|
+
// Always update the view here — renderNoBatch() shows a placeholder
|
|
1449
|
+
// while this async fetch completes, so we need to replace it with
|
|
1450
|
+
// the actual latest entry. This fixes #20 where the stale cached
|
|
1451
|
+
// historyList caused the previous batch to be shown.
|
|
1452
|
+
if (noBatchRendered && historyList.length > 0) {
|
|
1410
1453
|
viewHistoryEntry(historyList[0].batchId);
|
|
1411
1454
|
$historySelect.value = historyList[0].batchId;
|
|
1455
|
+
} else if (noBatchRendered && historyList.length === 0) {
|
|
1456
|
+
$historyBody.innerHTML = `
|
|
1457
|
+
<div class="no-batch">
|
|
1458
|
+
<div class="no-batch-icon">⏳</div>
|
|
1459
|
+
<div class="no-batch-title">No batch running</div>
|
|
1460
|
+
<div class="no-batch-hint">.pi/batch-state.json not found<br>Start an orchestrator batch to see the dashboard.</div>
|
|
1461
|
+
</div>`;
|
|
1462
|
+
$historyPanel.style.display = "";
|
|
1412
1463
|
}
|
|
1413
1464
|
})
|
|
1414
1465
|
.catch(() => {});
|
package/dashboard/server.cjs
CHANGED
|
@@ -239,7 +239,8 @@ const telemetryPrefixFiles = new Map();
|
|
|
239
239
|
/**
|
|
240
240
|
* Parse a telemetry JSONL filename to extract lane number and role.
|
|
241
241
|
* Pattern: {opId}-{batchId}-{repoId}[-{taskId}][-lane-{N}]-{role}.jsonl
|
|
242
|
-
*
|
|
242
|
+
* Roles: worker, reviewer, merger
|
|
243
|
+
* Returns { laneNumber: number|null, role: string, mergeNumber: number|null } or null if unparseable.
|
|
243
244
|
*/
|
|
244
245
|
function parseTelemetryFilename(filename) {
|
|
245
246
|
// Remove .jsonl extension
|
|
@@ -248,13 +249,17 @@ function parseTelemetryFilename(filename) {
|
|
|
248
249
|
const lastDash = base.lastIndexOf("-");
|
|
249
250
|
if (lastDash < 0) return null;
|
|
250
251
|
const role = base.slice(lastDash + 1);
|
|
251
|
-
if (role !== "worker" && role !== "reviewer") return null;
|
|
252
|
+
if (role !== "worker" && role !== "reviewer" && role !== "merger") return null;
|
|
252
253
|
|
|
253
254
|
// Extract lane number from -lane-{N}- pattern
|
|
254
255
|
const laneMatch = base.match(/-lane-(\d+)-/);
|
|
255
256
|
const laneNumber = laneMatch ? parseInt(laneMatch[1], 10) : null;
|
|
256
257
|
|
|
257
|
-
|
|
258
|
+
// Extract merge number from -merge-{N}- pattern (merge agents)
|
|
259
|
+
const mergeMatch = base.match(/-merge-(\d+)-/);
|
|
260
|
+
const mergeNumber = mergeMatch ? parseInt(mergeMatch[1], 10) : null;
|
|
261
|
+
|
|
262
|
+
return { laneNumber, role, mergeNumber };
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
/**
|
|
@@ -375,7 +380,10 @@ function loadTelemetryData(batchState) {
|
|
|
375
380
|
|
|
376
381
|
// Determine the key (tmux prefix)
|
|
377
382
|
let prefix;
|
|
378
|
-
if (parsed.
|
|
383
|
+
if (parsed.role === "merger") {
|
|
384
|
+
// Merge agent — key by merge session number or generic "merge"
|
|
385
|
+
prefix = parsed.mergeNumber != null ? `orch-merge-${parsed.mergeNumber}` : "orch-merge";
|
|
386
|
+
} else if (parsed.laneNumber != null && laneToPrefix[parsed.laneNumber]) {
|
|
379
387
|
prefix = laneToPrefix[parsed.laneNumber];
|
|
380
388
|
} else if (parsed.laneNumber != null) {
|
|
381
389
|
// Lane number found but no batch-state mapping — use heuristic
|