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
package/dashboard/public/app.js
CHANGED
|
@@ -319,6 +319,89 @@ function repoBadgeHtml(repoId, extraClass) {
|
|
|
319
319
|
return `<span class="repo-badge ${extraClass || ""}" title="Repo: ${escapeHtml(repoId)}">${escapeHtml(repoId)}</span>`;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
function parseSegmentId(segmentId) {
|
|
323
|
+
if (!segmentId || typeof segmentId !== "string") return null;
|
|
324
|
+
const sep = segmentId.indexOf("::");
|
|
325
|
+
if (sep <= 0 || sep >= segmentId.length - 2) return null;
|
|
326
|
+
return {
|
|
327
|
+
taskId: segmentId.slice(0, sep),
|
|
328
|
+
repoId: segmentId.slice(sep + 2),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function segmentProgressText(segmentInfo) {
|
|
333
|
+
if (!segmentInfo) return "";
|
|
334
|
+
const repo = segmentInfo.repoId || "unknown";
|
|
335
|
+
if (segmentInfo.index && segmentInfo.total) {
|
|
336
|
+
return `Segment ${segmentInfo.index}/${segmentInfo.total}: ${repo}`;
|
|
337
|
+
}
|
|
338
|
+
return `Segment: ${repo}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildSegmentStatusMap(batch) {
|
|
342
|
+
const map = new Map();
|
|
343
|
+
for (const seg of (batch?.segments || [])) {
|
|
344
|
+
if (seg && typeof seg.segmentId === "string") {
|
|
345
|
+
map.set(seg.segmentId, seg.status || "pending");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return map;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function taskSegmentProgress(task, segmentStatusMap, forcedActiveSegmentId) {
|
|
352
|
+
const segmentIds = Array.isArray(task?.segmentIds)
|
|
353
|
+
? task.segmentIds.filter(id => typeof id === "string")
|
|
354
|
+
: [];
|
|
355
|
+
// Repo-singleton (or repo-mode) tasks should stay visually clean.
|
|
356
|
+
if (segmentIds.length <= 1) return null;
|
|
357
|
+
|
|
358
|
+
const activeSegmentId = forcedActiveSegmentId || task.activeSegmentId;
|
|
359
|
+
let currentSegmentId = activeSegmentId && segmentIds.includes(activeSegmentId)
|
|
360
|
+
? activeSegmentId
|
|
361
|
+
: null;
|
|
362
|
+
|
|
363
|
+
if (!currentSegmentId) {
|
|
364
|
+
if (task.status === "pending" || task.status === "running") {
|
|
365
|
+
currentSegmentId = segmentIds.find((id) => {
|
|
366
|
+
const status = segmentStatusMap.get(id);
|
|
367
|
+
return !["succeeded", "failed", "stalled", "skipped"].includes(status);
|
|
368
|
+
}) || segmentIds[segmentIds.length - 1];
|
|
369
|
+
} else {
|
|
370
|
+
currentSegmentId = segmentIds[segmentIds.length - 1];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const idx = Math.max(0, segmentIds.indexOf(currentSegmentId));
|
|
375
|
+
const parsed = parseSegmentId(currentSegmentId);
|
|
376
|
+
return {
|
|
377
|
+
index: idx + 1,
|
|
378
|
+
total: segmentIds.length,
|
|
379
|
+
repoId: parsed?.repoId || taskRepoId(task) || undefined,
|
|
380
|
+
segmentId: currentSegmentId,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function laneActiveSegmentInfo(v2snap, laneTasks, segmentStatusMap) {
|
|
385
|
+
if (!v2snap || !v2snap.segmentId) return null;
|
|
386
|
+
const parsed = parseSegmentId(v2snap.segmentId);
|
|
387
|
+
if (!parsed) return null;
|
|
388
|
+
|
|
389
|
+
const ownerTaskId = v2snap.taskId || parsed.taskId;
|
|
390
|
+
const ownerTask = (laneTasks || []).find(t => t.taskId === ownerTaskId) || null;
|
|
391
|
+
if (ownerTask) {
|
|
392
|
+
const byTask = taskSegmentProgress(ownerTask, segmentStatusMap, v2snap.segmentId);
|
|
393
|
+
if (byTask) return byTask;
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
index: null,
|
|
399
|
+
total: null,
|
|
400
|
+
repoId: parsed.repoId,
|
|
401
|
+
segmentId: v2snap.segmentId,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
322
405
|
// Repo filter change handler
|
|
323
406
|
$repoFilter.addEventListener("change", (e) => {
|
|
324
407
|
selectedRepo = e.target.value;
|
|
@@ -523,12 +606,16 @@ function renderLanesTasks(batch, sessions) {
|
|
|
523
606
|
// TP-107: V2 lane snapshots take precedence over legacy lane states when present
|
|
524
607
|
const v2Snapshots = currentData?.runtimeLaneSnapshots || {};
|
|
525
608
|
const showRepos = knownRepos.length >= 2;
|
|
609
|
+
const segmentStatusMap = buildSegmentStatusMap(batch);
|
|
526
610
|
let html = "";
|
|
527
611
|
|
|
528
612
|
for (const lane of batch.lanes) {
|
|
613
|
+
const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
|
|
614
|
+
const v2snap = v2Snapshots[lane.laneNumber] || null;
|
|
615
|
+
const laneActiveSegment = laneActiveSegmentInfo(v2snap, laneTasks, segmentStatusMap);
|
|
616
|
+
|
|
529
617
|
// Repo filtering: if a repo is selected, skip lanes that don't match
|
|
530
618
|
if (selectedRepo && showRepos) {
|
|
531
|
-
const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
|
|
532
619
|
const laneMatchesRepo = (lane.repoId === selectedRepo) ||
|
|
533
620
|
laneTasks.some(t => (taskRepoId(t) || lane.repoId) === selectedRepo);
|
|
534
621
|
if (!laneMatchesRepo) continue;
|
|
@@ -550,6 +637,9 @@ function renderLanesTasks(batch, sessions) {
|
|
|
550
637
|
if (showRepos && lane.repoId) {
|
|
551
638
|
html += ` ${repoBadgeHtml(lane.repoId, "repo-badge-lane")}`;
|
|
552
639
|
}
|
|
640
|
+
if (laneActiveSegment) {
|
|
641
|
+
html += ` <span class="lane-segment" title="${escapeHtml(laneActiveSegment.segmentId || segmentProgressText(laneActiveSegment))}">${escapeHtml(segmentProgressText(laneActiveSegment))}</span>`;
|
|
642
|
+
}
|
|
553
643
|
html += ` </div>`;
|
|
554
644
|
html += ` <div class="lane-right">`;
|
|
555
645
|
html += ` <span class="session-dot ${alive ? "alive" : "dead"}" title="${alive ? "session alive" : "session not active"}"></span>`;
|
|
@@ -565,15 +655,12 @@ function renderLanesTasks(batch, sessions) {
|
|
|
565
655
|
html += `</div>`;
|
|
566
656
|
|
|
567
657
|
// Task rows for this lane
|
|
568
|
-
const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
|
|
569
|
-
|
|
570
658
|
if (laneTasks.length === 0) {
|
|
571
659
|
html += `<div class="task-row"><span class="task-icon"></span><span style="color:var(--text-faint);grid-column:2/-1;">No tasks assigned</span></div>`;
|
|
572
660
|
}
|
|
573
661
|
|
|
574
662
|
// Get lane state and telemetry for worker stats
|
|
575
663
|
// TP-107: V2 lane snapshots take precedence when present
|
|
576
|
-
const v2snap = v2Snapshots[lane.laneNumber] || null;
|
|
577
664
|
const legacyLs = laneStates[laneSessionId] || null;
|
|
578
665
|
const ls = v2snap ? mergeV2LaneSnapshot(legacyLs, v2snap) : legacyLs;
|
|
579
666
|
const tel = telemetry[laneSessionId] || null;
|
|
@@ -587,6 +674,9 @@ function renderLanesTasks(batch, sessions) {
|
|
|
587
674
|
const dur = task.startedAt
|
|
588
675
|
? formatDuration((task.endedAt || Date.now()) - task.startedAt)
|
|
589
676
|
: "—";
|
|
677
|
+
const segmentInfo = taskSegmentProgress(task, segmentStatusMap, null);
|
|
678
|
+
const packetHomeRepo = typeof task.packetRepoId === "string" ? task.packetRepoId : "";
|
|
679
|
+
const showPacketHome = !!packetHomeRepo && packetHomeRepo !== (tRepo || lane.repoId || "");
|
|
590
680
|
|
|
591
681
|
// Progress cell
|
|
592
682
|
let progressHtml = "";
|
|
@@ -629,6 +719,17 @@ function renderLanesTasks(batch, sessions) {
|
|
|
629
719
|
stepHtml = `<span style="color:var(--text-faint)">${escapeHtml(task.exitReason || "—")}</span>`;
|
|
630
720
|
}
|
|
631
721
|
|
|
722
|
+
const detailBits = [];
|
|
723
|
+
if (segmentInfo) {
|
|
724
|
+
detailBits.push(`<span class="task-segment-progress" title="${escapeHtml(segmentInfo.segmentId || segmentProgressText(segmentInfo))}">${escapeHtml(segmentProgressText(segmentInfo))}</span>`);
|
|
725
|
+
}
|
|
726
|
+
if (showPacketHome) {
|
|
727
|
+
detailBits.push(`<span class="task-packet-home" title="Task packet home repo">packet: ${escapeHtml(packetHomeRepo)}</span>`);
|
|
728
|
+
}
|
|
729
|
+
if (detailBits.length > 0) {
|
|
730
|
+
stepHtml = `${detailBits.join('<span class="task-detail-sep"> · </span>')}<span class="task-detail-sep"> · </span><span class="task-step-main">${stepHtml}</span>`;
|
|
731
|
+
}
|
|
732
|
+
|
|
632
733
|
// Worker stats from lane state sidecar + telemetry badges
|
|
633
734
|
let workerHtml = "";
|
|
634
735
|
// Reviewer sub-row should only appear under the active running task in this lane.
|
|
@@ -507,6 +507,23 @@ body {
|
|
|
507
507
|
text-overflow: ellipsis;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
.lane-segment {
|
|
511
|
+
display: inline-flex;
|
|
512
|
+
align-items: center;
|
|
513
|
+
gap: 4px;
|
|
514
|
+
font-family: var(--font-mono);
|
|
515
|
+
font-size: 0.68rem;
|
|
516
|
+
font-weight: 600;
|
|
517
|
+
padding: 2px 7px;
|
|
518
|
+
border-radius: 8px;
|
|
519
|
+
background: var(--badge-running-bg);
|
|
520
|
+
color: var(--accent);
|
|
521
|
+
max-width: 220px;
|
|
522
|
+
overflow: hidden;
|
|
523
|
+
text-overflow: ellipsis;
|
|
524
|
+
white-space: nowrap;
|
|
525
|
+
}
|
|
526
|
+
|
|
510
527
|
.lane-right {
|
|
511
528
|
display: flex;
|
|
512
529
|
align-items: center;
|
|
@@ -610,6 +627,25 @@ body {
|
|
|
610
627
|
text-overflow: ellipsis;
|
|
611
628
|
}
|
|
612
629
|
|
|
630
|
+
.task-segment-progress {
|
|
631
|
+
font-family: var(--font-mono);
|
|
632
|
+
color: var(--accent);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.task-packet-home {
|
|
636
|
+
font-family: var(--font-mono);
|
|
637
|
+
color: var(--magenta);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.task-detail-sep {
|
|
641
|
+
color: var(--text-faint);
|
|
642
|
+
margin: 0 2px;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.task-step-main {
|
|
646
|
+
color: var(--text-muted);
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
.task-iter {
|
|
614
650
|
font-family: var(--font-mono);
|
|
615
651
|
font-size: 0.7rem;
|
|
@@ -217,7 +217,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function reviewerStatePath(taskFolder: string): string {
|
|
220
|
-
return join(taskFolder, ".reviewer-state.json");
|
|
220
|
+
return process.env.TASKPLANE_REVIEWER_STATE_PATH || join(taskFolder, ".reviewer-state.json");
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function writeReviewerState(taskFolder: string, state: {
|
|
@@ -421,8 +421,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
421
|
|
|
422
422
|
// Find task folder and paths
|
|
423
423
|
const taskFolder = process.env.TASKPLANE_TASK_FOLDER || cwd;
|
|
424
|
-
const statusPath = join(taskFolder, "STATUS.md");
|
|
425
|
-
const
|
|
424
|
+
const statusPath = process.env.TASKPLANE_STATUS_PATH || join(taskFolder, "STATUS.md");
|
|
425
|
+
const promptPath = process.env.TASKPLANE_PROMPT_PATH || join(taskFolder, "PROMPT.md");
|
|
426
|
+
const reviewsDir = process.env.TASKPLANE_REVIEWS_DIR || join(taskFolder, ".reviews");
|
|
426
427
|
if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
|
|
427
428
|
|
|
428
429
|
// Read review counter from STATUS.md
|
|
@@ -450,7 +451,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
451
|
} catch { /* use default */ }
|
|
451
452
|
|
|
452
453
|
// Generate review request prompt
|
|
453
|
-
const promptPath = join(taskFolder, "PROMPT.md");
|
|
454
454
|
const projectName = process.env.TASKPLANE_PROJECT_NAME || "project";
|
|
455
455
|
const diffCmd = baseline ? `git diff ${baseline}..HEAD` : `git diff`;
|
|
456
456
|
const diffNamesCmd = baseline ? `git diff ${baseline}..HEAD --name-only` : `git diff --name-only`;
|