taskplane 0.29.2 → 0.30.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/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/dashboard/public/app.js +124 -15
- package/dashboard/public/style.css +83 -2
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +78 -63
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +469 -207
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +652 -319
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +832 -280
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +209 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- package/package.json +14 -2
package/dashboard/public/app.js
CHANGED
|
@@ -382,6 +382,53 @@ function taskSegmentProgress(task, segmentStatusMap, forcedActiveSegmentId) {
|
|
|
382
382
|
};
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
// TP-197 (#464): Render a horizontal pill row of per-segment status badges for a
|
|
386
|
+
// multi-segment task. Each pill shows an icon + repoId for one segment. The icon
|
|
387
|
+
// reflects the segment's status (succeeded / running / pending / failed / stalled /
|
|
388
|
+
// skipped). The current segment (the one actively executing on its lane) gets an
|
|
389
|
+
// emphasis class. Returns "" for single-segment tasks so the rendered DOM is
|
|
390
|
+
// byte-identical to today for the non-segmented common case (no regression).
|
|
391
|
+
//
|
|
392
|
+
// Consumes:
|
|
393
|
+
// - task.segmentIds: string[] (ordered, from PersistedTaskRecord)
|
|
394
|
+
// - segmentStatusMap: Map<segmentId, PersistedSegmentStatus> built by
|
|
395
|
+
// buildSegmentStatusMap() from batch.segments[]
|
|
396
|
+
// - activeSegmentId: string|null — current executing segment (from V2 lane
|
|
397
|
+
// snapshot's segmentId, or the task's activeSegmentId field)
|
|
398
|
+
function taskSegmentPillRow(task, segmentStatusMap, activeSegmentId) {
|
|
399
|
+
const segmentIds = Array.isArray(task?.segmentIds)
|
|
400
|
+
? task.segmentIds.filter(id => typeof id === "string")
|
|
401
|
+
: [];
|
|
402
|
+
if (segmentIds.length <= 1) return "";
|
|
403
|
+
|
|
404
|
+
// Status -> { icon, className } table. Keep emoji simple/monospace-friendly.
|
|
405
|
+
// ✅ succeeded, ⏳ running, ⬚ pending, ❌ failed, ⏸ stalled, ↷ skipped.
|
|
406
|
+
const styles = {
|
|
407
|
+
succeeded: { icon: "\u2705", cls: "seg-succeeded" },
|
|
408
|
+
running: { icon: "\u23F3", cls: "seg-running" },
|
|
409
|
+
pending: { icon: "\u2B1A", cls: "seg-pending" },
|
|
410
|
+
failed: { icon: "\u274C", cls: "seg-failed" },
|
|
411
|
+
stalled: { icon: "\u23F8", cls: "seg-stalled" },
|
|
412
|
+
skipped: { icon: "\u21B7", cls: "seg-skipped" },
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const pills = segmentIds.map((segId) => {
|
|
416
|
+
const status = segmentStatusMap.get(segId) || "pending";
|
|
417
|
+
const style = styles[status] || styles.pending;
|
|
418
|
+
const parsed = parseSegmentId(segId);
|
|
419
|
+
const repoLabel = parsed?.repoId || segId;
|
|
420
|
+
const isCurrent = activeSegmentId && segId === activeSegmentId;
|
|
421
|
+
const currentCls = isCurrent ? " seg-pill-current" : "";
|
|
422
|
+
const title = `${segId} \u00b7 ${status}`;
|
|
423
|
+
return `<span class="seg-pill ${style.cls}${currentCls}" title="${escapeHtml(title)}">`
|
|
424
|
+
+ `<span class="seg-pill-icon">${style.icon}</span>`
|
|
425
|
+
+ `<span class="seg-pill-label">${escapeHtml(repoLabel)}</span>`
|
|
426
|
+
+ `</span>`;
|
|
427
|
+
}).join("");
|
|
428
|
+
|
|
429
|
+
return `<div class="task-segment-row">${pills}</div>`;
|
|
430
|
+
}
|
|
431
|
+
|
|
385
432
|
function laneActiveSegmentInfo(v2snap, laneTasks, segmentStatusMap) {
|
|
386
433
|
if (!v2snap || !v2snap.segmentId) return null;
|
|
387
434
|
const parsed = parseSegmentId(v2snap.segmentId);
|
|
@@ -620,7 +667,13 @@ function renderSummary(batch) {
|
|
|
620
667
|
// their assigned lane: tasks on the same lane render with `→` (serial),
|
|
621
668
|
// tasks on different lanes render with ` | ` (parallel). Tooltip shows
|
|
622
669
|
// the expanded lane breakdown.
|
|
623
|
-
|
|
670
|
+
// TP-197 post-merge fold: pass `batch.tasks` as the task→lane source.
|
|
671
|
+
// The previous arg `batch.lanes` only carries live Runtime V2 lane
|
|
672
|
+
// state for the *currently active* wave — past/future wave chips
|
|
673
|
+
// would fall back to comma-separated. `batch.tasks[].laneNumber` is
|
|
674
|
+
// persisted for the entire batch lifecycle, so all waves render with
|
|
675
|
+
// the correct parallelization separator regardless of active state.
|
|
676
|
+
const { compact, tooltip } = formatWaveLaneBreakdown(taskIds, batch.lanes || [], batch.tasks || [], i + 1);
|
|
624
677
|
const titleAttr = tooltip ? ` title="${escapeHtml(tooltip)}"` : "";
|
|
625
678
|
wavesHtml += `<span class="wave-chip ${cls}"${titleAttr}>W${i + 1} [${compact}]</span>`;
|
|
626
679
|
});
|
|
@@ -648,16 +701,46 @@ function renderSummary(batch) {
|
|
|
648
701
|
* are shown with the previous flat formatting and no tooltip is generated
|
|
649
702
|
* — this preserves backward compatibility with future-wave display.
|
|
650
703
|
*/
|
|
651
|
-
function formatWaveLaneBreakdown(taskIds, lanes, waveNumber) {
|
|
704
|
+
function formatWaveLaneBreakdown(taskIds, lanes, tasks, waveNumber) {
|
|
652
705
|
if (!Array.isArray(taskIds) || taskIds.length === 0) {
|
|
653
706
|
return { compact: "", tooltip: "" };
|
|
654
707
|
}
|
|
655
|
-
// Build taskId → laneNumber map
|
|
708
|
+
// Build taskId → laneNumber map. Prefer the persisted-per-task
|
|
709
|
+
// `tasks[i].laneNumber` (covers all waves, lifecycle-stable). Fall back
|
|
710
|
+
// to live `lanes[]` only when tasks data is missing or doesn't carry
|
|
711
|
+
// laneNumber for a given task.
|
|
712
|
+
//
|
|
713
|
+
// TP-197 post-merge fold: the previous implementation read ONLY from
|
|
714
|
+
// `lanes`, which is Runtime V2 live state and only populated for the
|
|
715
|
+
// currently active wave. That caused inactive waves' chips to fall back
|
|
716
|
+
// to comma-separated display (no parallelization indicator), giving the
|
|
717
|
+
// impression that the separator changed as the batch progressed. Using
|
|
718
|
+
// the persisted `tasks[].laneNumber` makes the indicator stable across
|
|
719
|
+
// all waves regardless of active state.
|
|
656
720
|
const taskToLane = new Map();
|
|
721
|
+
if (Array.isArray(tasks)) {
|
|
722
|
+
for (const t of tasks) {
|
|
723
|
+
// Persistence assigns `laneNumber: 0` as a sentinel meaning
|
|
724
|
+
// "unallocated" (see persistence.ts:1378 — `lane?.laneNumber ??
|
|
725
|
+
// outcome?.laneNumber ?? 0`). Real lane numbers start at 1. We must
|
|
726
|
+
// skip 0 here so future-wave tasks (which all have the 0 sentinel
|
|
727
|
+
// until their wave starts) don't get falsely grouped under a fake
|
|
728
|
+
// "lane 0" and rendered as serial.
|
|
729
|
+
if (
|
|
730
|
+
t &&
|
|
731
|
+
t.taskId &&
|
|
732
|
+
typeof t.laneNumber === "number" &&
|
|
733
|
+
t.laneNumber >= 1 &&
|
|
734
|
+
!taskToLane.has(t.taskId)
|
|
735
|
+
) {
|
|
736
|
+
taskToLane.set(t.taskId, t.laneNumber);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// Fallback: anything `tasks` didn't cover, try `lanes` (live state).
|
|
657
741
|
for (const lane of lanes) {
|
|
658
742
|
if (!lane || !Array.isArray(lane.taskIds)) continue;
|
|
659
743
|
for (const tid of lane.taskIds) {
|
|
660
|
-
// First lane to claim a task wins (lanes shouldn't overlap, but be defensive).
|
|
661
744
|
if (!taskToLane.has(tid)) taskToLane.set(tid, lane.laneNumber);
|
|
662
745
|
}
|
|
663
746
|
}
|
|
@@ -859,8 +942,24 @@ function renderLanesTasks(batch, sessions) {
|
|
|
859
942
|
stepHtml = `<span style="color:var(--text-faint)">${escapeHtml(task.exitReason || "—")}</span>`;
|
|
860
943
|
}
|
|
861
944
|
|
|
945
|
+
// TP-197 (#464): Compute the per-segment pill row for multi-segment tasks.
|
|
946
|
+
// Returns "" for single-segment tasks (no DOM regression for the common case).
|
|
947
|
+
// For multi-segment tasks we render the pill row in the task-row's grid row 3
|
|
948
|
+
// (via .task-segment-row CSS) and suppress the inline "Segment N/T: repo" text
|
|
949
|
+
// in detailBits to avoid duplicating signal — the pill row already shows the
|
|
950
|
+
// current segment (via seg-pill-current) and total count (via pill count).
|
|
951
|
+
const segmentPillRowHtml = taskSegmentPillRow(
|
|
952
|
+
task,
|
|
953
|
+
segmentStatusMap,
|
|
954
|
+
v2snap && v2snap.taskId === task.taskId ? v2snap.segmentId : (segmentInfo?.segmentId || null),
|
|
955
|
+
);
|
|
956
|
+
const hasSegmentPillRow = segmentPillRowHtml !== "";
|
|
957
|
+
|
|
862
958
|
const detailBits = [];
|
|
863
|
-
if (segmentInfo) {
|
|
959
|
+
if (segmentInfo && !hasSegmentPillRow) {
|
|
960
|
+
// Single-segment + non-segmented tasks: existing inline text (unchanged).
|
|
961
|
+
// Multi-segment tasks: suppressed because the new pill row carries the same
|
|
962
|
+
// information more legibly.
|
|
864
963
|
detailBits.push(`<span class="task-segment-progress" title="${escapeHtml(segmentInfo.segmentId || segmentProgressText(segmentInfo))}">${escapeHtml(segmentProgressText(segmentInfo))}</span>`);
|
|
865
964
|
}
|
|
866
965
|
if (showPacketHome) {
|
|
@@ -950,8 +1049,17 @@ function renderLanesTasks(batch, sessions) {
|
|
|
950
1049
|
const titleHtml = task.taskTitle
|
|
951
1050
|
? `<div class="task-title-subtitle">${escapeHtml(task.taskTitle)}</div>`
|
|
952
1051
|
: "";
|
|
1052
|
+
// TP-197 (#464): segmentPillRowHtml is empty for single-segment tasks so
|
|
1053
|
+
// the rendered DOM is byte-identical to today for non-segmented tasks.
|
|
1054
|
+
// For multi-segment tasks it renders as grid-row 3 of .task-row.
|
|
1055
|
+
// Sage post-merge fold: the .has-segments class opts the .task-row
|
|
1056
|
+
// grid into a 3-row template only when we actually have a pill row;
|
|
1057
|
+
// otherwise the default 2-row template preserves single-segment task
|
|
1058
|
+
// spacing exactly (an unconditional 3-row template would add an 8px
|
|
1059
|
+
// row-gap even when row 3 is empty, breaking the no-regression contract).
|
|
1060
|
+
const taskRowClass = hasSegmentPillRow ? "task-row has-segments" : "task-row";
|
|
953
1061
|
html += `
|
|
954
|
-
<div class="
|
|
1062
|
+
<div class="${taskRowClass}">
|
|
955
1063
|
<span class="task-icon"><span class="status-dot ${task.status}"></span></span>
|
|
956
1064
|
<span class="task-actions">${eyeHtml}</span>
|
|
957
1065
|
<span class="task-id status-${task.status}">${escapeHtml(task.taskId)}${showRepos ? repoBadgeHtml(tRepo, "repo-badge-task") : ""}</span>
|
|
@@ -960,6 +1068,7 @@ function renderLanesTasks(batch, sessions) {
|
|
|
960
1068
|
<span>${progressHtml}</span>
|
|
961
1069
|
<span class="task-step">${stepHtml}${workerHtml}</span>
|
|
962
1070
|
${titleHtml}
|
|
1071
|
+
${segmentPillRowHtml}
|
|
963
1072
|
</div>`;
|
|
964
1073
|
html += reviewerRowHtml;
|
|
965
1074
|
}
|
|
@@ -1054,7 +1163,15 @@ function renderMergeAgents(batch, sessions) {
|
|
|
1054
1163
|
}
|
|
1055
1164
|
|
|
1056
1165
|
let html = '<table class="merge-table"><thead><tr>';
|
|
1057
|
-
|
|
1166
|
+
// TP-197 post-merge fold: removed 'Session ID' and 'Details' columns.
|
|
1167
|
+
// SESSION ID was hardcoded to '—' in every row — dead weight.
|
|
1168
|
+
// DETAILS only populated for `mr.failureReason` (rare failure cases);
|
|
1169
|
+
// for the common all-merges-succeeded case it's always '—' too.
|
|
1170
|
+
// When a real failure happens, the operator sees status='failed' in
|
|
1171
|
+
// the Status column and can dig into engine logs for the reason —
|
|
1172
|
+
// we'll re-add a focused DETAILS column if/when we have meaningful
|
|
1173
|
+
// structured failure-reason data to surface in the dashboard table.
|
|
1174
|
+
html += '<th>Wave</th><th>Status</th><th>Session</th><th>Telemetry</th>';
|
|
1058
1175
|
html += '</tr></thead><tbody>';
|
|
1059
1176
|
|
|
1060
1177
|
// Track sessions shown in wave result rows so we don't duplicate them below
|
|
@@ -1135,10 +1252,6 @@ function renderMergeAgents(batch, sessions) {
|
|
|
1135
1252
|
html += `<td class="merge-session-cell">${effectiveAlive ? escapeHtml(effectiveSession) : "—"}</td>`;
|
|
1136
1253
|
// Full telemetry cell
|
|
1137
1254
|
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(mergeTel, effectiveAlive)}</td>`;
|
|
1138
|
-
html += `<td>`;
|
|
1139
|
-
html += '<span class="merge-no-data">—</span>';
|
|
1140
|
-
html += `</td>`;
|
|
1141
|
-
html += `<td class="merge-detail-cell">${mr.failureReason ? escapeHtml(mr.failureReason) : "—"}</td>`;
|
|
1142
1255
|
html += `</tr>`;
|
|
1143
1256
|
|
|
1144
1257
|
// Per-repo sub-rows: show when workspace mode has repo results
|
|
@@ -1159,8 +1272,6 @@ function renderMergeAgents(batch, sessions) {
|
|
|
1159
1272
|
html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
|
|
1160
1273
|
html += `<td class="merge-session-cell">${rrLanes}</td>`;
|
|
1161
1274
|
html += `<td></td>`; /* telemetry placeholder */
|
|
1162
|
-
html += `<td></td>`; /* attach placeholder */
|
|
1163
|
-
html += `<td class="merge-detail-cell">${rrDetail}</td>`;
|
|
1164
1275
|
html += `</tr>`;
|
|
1165
1276
|
}
|
|
1166
1277
|
}
|
|
@@ -1177,8 +1288,6 @@ function renderMergeAgents(batch, sessions) {
|
|
|
1177
1288
|
html += `<td class="merge-session-cell">${escapeHtml(sess)}</td>`;
|
|
1178
1289
|
// Full telemetry cell for active merge session
|
|
1179
1290
|
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(sessTel, true)}</td>`;
|
|
1180
|
-
html += `<td>—</td>`;
|
|
1181
|
-
html += `<td>—</td>`;
|
|
1182
1291
|
html += `</tr>`;
|
|
1183
1292
|
}
|
|
1184
1293
|
|
|
@@ -608,7 +608,13 @@ body {
|
|
|
608
608
|
display: grid;
|
|
609
609
|
grid-template-columns: 36px 24px 100px 90px 80px 200px 1fr;
|
|
610
610
|
/* #485 (revised): row 1 holds the primary cells; row 2 (auto, collapses to
|
|
611
|
-
* 0 when empty) holds the optional task-title-subtitle spanning cols 3–6.
|
|
611
|
+
* 0 when empty) holds the optional task-title-subtitle spanning cols 3–6.
|
|
612
|
+
* Default: 2 rows. TP-197 (#464) adds the optional per-segment pill row in
|
|
613
|
+
* grid-row 3 ONLY when the .has-segments class is set (multi-segment tasks).
|
|
614
|
+
* Non-segmented tasks keep the pre-TP-197 2-row layout exactly — 'auto'
|
|
615
|
+
* row tracks don't fully collapse with row-gap declared, so adding a third
|
|
616
|
+
* row track unconditionally would introduce an 8px visible gap for every
|
|
617
|
+
* single-segment task (sage post-merge fold). */
|
|
612
618
|
grid-template-rows: auto auto;
|
|
613
619
|
align-items: center;
|
|
614
620
|
gap: 8px 8px;
|
|
@@ -617,6 +623,13 @@ body {
|
|
|
617
623
|
transition: background 0.15s;
|
|
618
624
|
}
|
|
619
625
|
|
|
626
|
+
/* TP-197 (#464): multi-segment tasks opt-in to a 3-row grid for the segment-
|
|
627
|
+
* pill row. JS adds .has-segments to .task-row only when the pill row is
|
|
628
|
+
* non-empty (see app.js taskSegmentPillRow + task-row template). */
|
|
629
|
+
.task-row.has-segments {
|
|
630
|
+
grid-template-rows: auto auto auto;
|
|
631
|
+
}
|
|
632
|
+
|
|
620
633
|
.task-row:last-child { border-bottom: none; }
|
|
621
634
|
.task-row:hover { background: var(--bg-surface-hover); }
|
|
622
635
|
|
|
@@ -652,6 +665,71 @@ body {
|
|
|
652
665
|
margin-top: -2px;
|
|
653
666
|
}
|
|
654
667
|
|
|
668
|
+
/* ─── TP-197 (#464): Per-segment pill row for multi-segment tasks ─────── */
|
|
669
|
+
|
|
670
|
+
.task-segment-row {
|
|
671
|
+
/* Sub-row beneath the primary task row, mirroring the title-subtitle pattern
|
|
672
|
+
* (TP-485). Spans cols 3 → 7 so it shares the title area's horizontal space.
|
|
673
|
+
* Placed at grid row 3 so it sits *below* the optional title-subtitle.
|
|
674
|
+
* Container collapses to 0 height when not rendered (single-segment tasks),
|
|
675
|
+
* so non-segmented tasks render with identical row height to today.
|
|
676
|
+
* Lives OUTSIDE .task-step intentionally so the @media (max-width: 900px)
|
|
677
|
+
* rule that hides .task-step does NOT hide the pill row — keeps segment
|
|
678
|
+
* context visible at narrow viewports. */
|
|
679
|
+
grid-column: 3 / 7;
|
|
680
|
+
grid-row: 3;
|
|
681
|
+
display: flex;
|
|
682
|
+
flex-wrap: wrap;
|
|
683
|
+
align-items: center;
|
|
684
|
+
gap: 4px;
|
|
685
|
+
margin-top: 2px;
|
|
686
|
+
min-width: 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.seg-pill {
|
|
690
|
+
display: inline-flex;
|
|
691
|
+
align-items: center;
|
|
692
|
+
gap: 3px;
|
|
693
|
+
font-family: var(--font-mono);
|
|
694
|
+
font-size: 0.68rem;
|
|
695
|
+
font-weight: 500;
|
|
696
|
+
line-height: 1.4;
|
|
697
|
+
padding: 1px 7px;
|
|
698
|
+
border-radius: 8px;
|
|
699
|
+
border: 1px solid transparent;
|
|
700
|
+
max-width: 140px;
|
|
701
|
+
white-space: nowrap;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.seg-pill .seg-pill-icon {
|
|
705
|
+
font-size: 0.72rem;
|
|
706
|
+
line-height: 1;
|
|
707
|
+
flex-shrink: 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.seg-pill .seg-pill-label {
|
|
711
|
+
overflow: hidden;
|
|
712
|
+
text-overflow: ellipsis;
|
|
713
|
+
min-width: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* Status variants — reuse the existing status-badge color tokens for
|
|
717
|
+
* consistency with .status-badge.status-{succeeded,running,failed,…}. */
|
|
718
|
+
.seg-pill.seg-succeeded { background: var(--badge-succeeded-bg); color: var(--green); }
|
|
719
|
+
.seg-pill.seg-running { background: var(--badge-running-bg); color: var(--accent); }
|
|
720
|
+
.seg-pill.seg-pending { background: var(--bg-surface); color: var(--text-muted); border-color: var(--border-default, var(--border-subtle)); }
|
|
721
|
+
.seg-pill.seg-failed { background: var(--badge-failed-bg); color: var(--red); }
|
|
722
|
+
.seg-pill.seg-stalled { background: var(--badge-failed-bg); color: var(--yellow, var(--red)); opacity: 0.85; }
|
|
723
|
+
.seg-pill.seg-skipped { background: var(--bg-surface); color: var(--text-muted); opacity: 0.7; }
|
|
724
|
+
|
|
725
|
+
/* Current-segment emphasis: brighter border + slight weight bump so operator
|
|
726
|
+
* can spot "we're here right now" at a glance independent of icon. */
|
|
727
|
+
.seg-pill.seg-pill-current {
|
|
728
|
+
border-color: var(--accent);
|
|
729
|
+
font-weight: 600;
|
|
730
|
+
box-shadow: 0 0 0 1px var(--accent-dim, transparent);
|
|
731
|
+
}
|
|
732
|
+
|
|
655
733
|
.task-duration {
|
|
656
734
|
font-family: var(--font-mono);
|
|
657
735
|
font-size: 0.8rem;
|
|
@@ -783,7 +861,10 @@ body {
|
|
|
783
861
|
font-weight: 600;
|
|
784
862
|
text-transform: uppercase;
|
|
785
863
|
letter-spacing: 0.05em;
|
|
786
|
-
|
|
864
|
+
/* TP-197 post-merge fold: bumped --text-faint → --text-muted for
|
|
865
|
+
* readability. Matches other dashboard section headers (lines 224, 248,
|
|
866
|
+
* 458 of this file already use --text-muted for the same role). */
|
|
867
|
+
color: var(--text-muted);
|
|
787
868
|
border-bottom: 1px solid var(--border);
|
|
788
869
|
background: var(--bg-surface);
|
|
789
870
|
}
|
|
@@ -48,7 +48,8 @@ export default function reviewerExtension(pi: ExtensionAPI) {
|
|
|
48
48
|
"Block until the next review request is available, then return its content. " +
|
|
49
49
|
"Call this after completing each review to wait for the next one. " +
|
|
50
50
|
"Returns 'SHUTDOWN' when the task is complete and you should exit.",
|
|
51
|
-
promptSnippet:
|
|
51
|
+
promptSnippet:
|
|
52
|
+
"wait_for_review() — block until the next review request arrives (persistent reviewer mode)",
|
|
52
53
|
promptGuidelines: [
|
|
53
54
|
"Call wait_for_review() to receive each review request.",
|
|
54
55
|
"After writing your review to the specified output file, call wait_for_review() again.",
|
|
@@ -82,11 +83,14 @@ export default function reviewerExtension(pi: ExtensionAPI) {
|
|
|
82
83
|
if (!existsSync(requestPath)) {
|
|
83
84
|
// Signal fired but request file doesn't exist (race condition or error)
|
|
84
85
|
return {
|
|
85
|
-
content: [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text" as const,
|
|
89
|
+
text:
|
|
90
|
+
`ERROR — Signal file ${REVIEWER_SIGNAL_PREFIX}${signalNum} found but ` +
|
|
91
|
+
`${signalContent} does not exist. Waiting for next signal.`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
90
94
|
details: undefined,
|
|
91
95
|
};
|
|
92
96
|
}
|
|
@@ -103,16 +107,18 @@ export default function reviewerExtension(pi: ExtensionAPI) {
|
|
|
103
107
|
// Check timeout
|
|
104
108
|
if (Date.now() - startTime > REVIEWER_WAIT_TIMEOUT_MS) {
|
|
105
109
|
return {
|
|
106
|
-
content: [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text" as const,
|
|
113
|
+
text: "TIMEOUT — No review request received within the timeout period. Exit cleanly.",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
110
116
|
details: undefined,
|
|
111
117
|
};
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
// Wait before next poll
|
|
115
|
-
await new Promise(resolve => setTimeout(resolve, REVIEWER_POLL_INTERVAL_MS));
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, REVIEWER_POLL_INTERVAL_MS));
|
|
116
122
|
}
|
|
117
123
|
},
|
|
118
124
|
});
|
|
@@ -8,7 +8,18 @@ import { join } from "path";
|
|
|
8
8
|
import { execLog, killV2LaneAgents, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
9
9
|
import { killMergeAgentV2, killAllMergeAgentsV2 } from "./merge.ts";
|
|
10
10
|
import { deleteBatchState, persistRuntimeState } from "./persistence.ts";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
AbortActionStep,
|
|
13
|
+
AbortErrorCode,
|
|
14
|
+
AbortLaneResult,
|
|
15
|
+
AbortMode,
|
|
16
|
+
AbortResult,
|
|
17
|
+
AbortTargetSession,
|
|
18
|
+
AllocatedLane,
|
|
19
|
+
OrchBatchRuntimeState,
|
|
20
|
+
PersistedBatchState,
|
|
21
|
+
PersistedLaneRecord,
|
|
22
|
+
} from "./types.ts";
|
|
12
23
|
|
|
13
24
|
// ── Abort Pure Functions ─────────────────────────────────────────────
|
|
14
25
|
|
|
@@ -37,7 +48,7 @@ export function selectAbortTargetSessions(
|
|
|
37
48
|
// Filter to only lane and merge sessions for the exact orchestrator prefix.
|
|
38
49
|
// Handles both repo-mode (`<prefix>-lane-<N>`) and workspace-mode
|
|
39
50
|
// (`<prefix>-<repoId>-lane-<N>`) session name formats.
|
|
40
|
-
const targetNames = allSessionNames.filter(name => {
|
|
51
|
+
const targetNames = allSessionNames.filter((name) => {
|
|
41
52
|
const prefixWithDash = `${prefix}-`;
|
|
42
53
|
if (!name.startsWith(prefixWithDash)) return false;
|
|
43
54
|
const suffix = name.slice(prefixWithDash.length);
|
|
@@ -78,7 +89,10 @@ export function selectAbortTargetSessions(
|
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
// Build lookup from runtime lanes
|
|
81
|
-
const runtimeLookup = new Map<
|
|
92
|
+
const runtimeLookup = new Map<
|
|
93
|
+
string,
|
|
94
|
+
{ laneId: string; taskId: string | null; worktreePath: string; taskFolder: string | null }
|
|
95
|
+
>();
|
|
82
96
|
for (const lane of runtimeLanes) {
|
|
83
97
|
const currentTask = lane.tasks.length > 0 ? lane.tasks[0] : null;
|
|
84
98
|
runtimeLookup.set(lane.laneSessionId, {
|
|
@@ -90,7 +104,7 @@ export function selectAbortTargetSessions(
|
|
|
90
104
|
});
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
return targetNames.map(sessionName => {
|
|
107
|
+
return targetNames.map((sessionName) => {
|
|
94
108
|
const runtime = runtimeLookup.get(sessionName);
|
|
95
109
|
const persisted = persistedLookup.get(sessionName);
|
|
96
110
|
|
|
@@ -184,7 +198,6 @@ export function discoverAbortSessionNames(
|
|
|
184
198
|
return [...names];
|
|
185
199
|
}
|
|
186
200
|
|
|
187
|
-
|
|
188
201
|
// ── Abort Orchestration Functions ────────────────────────────────────
|
|
189
202
|
|
|
190
203
|
/**
|
|
@@ -207,10 +220,18 @@ export function writeWrapUpFiles(
|
|
|
207
220
|
if (!target.taskFolderInWorktree) {
|
|
208
221
|
// Skip child sessions (workers, reviewers) — only main lane sessions have task folders
|
|
209
222
|
// Also skip merge sessions (no task folder)
|
|
210
|
-
if (
|
|
223
|
+
if (
|
|
224
|
+
target.sessionName.endsWith("-worker") ||
|
|
225
|
+
target.sessionName.endsWith("-reviewer") ||
|
|
226
|
+
target.sessionName.includes("merge")
|
|
227
|
+
) {
|
|
211
228
|
results.push({ sessionName: target.sessionName, written: false, error: null });
|
|
212
229
|
} else {
|
|
213
|
-
results.push({
|
|
230
|
+
results.push({
|
|
231
|
+
sessionName: target.sessionName,
|
|
232
|
+
written: false,
|
|
233
|
+
error: "No task folder resolved",
|
|
234
|
+
});
|
|
214
235
|
}
|
|
215
236
|
continue;
|
|
216
237
|
}
|
|
@@ -220,7 +241,11 @@ export function writeWrapUpFiles(
|
|
|
220
241
|
|
|
221
242
|
// Ensure directory exists
|
|
222
243
|
if (!existsSync(target.taskFolderInWorktree)) {
|
|
223
|
-
results.push({
|
|
244
|
+
results.push({
|
|
245
|
+
sessionName: target.sessionName,
|
|
246
|
+
written: false,
|
|
247
|
+
error: `Task folder does not exist: ${target.taskFolderInWorktree}`,
|
|
248
|
+
});
|
|
224
249
|
continue;
|
|
225
250
|
}
|
|
226
251
|
|
|
@@ -262,7 +287,7 @@ export async function waitForSessionExit(
|
|
|
262
287
|
const deadline = Date.now() + gracePeriodMs;
|
|
263
288
|
while (Date.now() < deadline) {
|
|
264
289
|
const sleepMs = Math.max(1, Math.min(pollIntervalMs, deadline - Date.now()));
|
|
265
|
-
await new Promise(r => setTimeout(r, sleepMs));
|
|
290
|
+
await new Promise((r) => setTimeout(r, sleepMs));
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
return { exited: [], remaining: [...sessionNames] };
|
|
@@ -357,7 +382,11 @@ export async function executeAbort(
|
|
|
357
382
|
repoRoot,
|
|
358
383
|
);
|
|
359
384
|
} catch (err) {
|
|
360
|
-
execLog(
|
|
385
|
+
execLog(
|
|
386
|
+
"abort",
|
|
387
|
+
batchState.batchId,
|
|
388
|
+
`Failed to persist state during abort: ${err instanceof Error ? err.message : String(err)}`,
|
|
389
|
+
);
|
|
361
390
|
}
|
|
362
391
|
|
|
363
392
|
// TP-108: Kill all V2 merge agents (process-owned, not TMUX)
|
|
@@ -370,7 +399,11 @@ export async function executeAbort(
|
|
|
370
399
|
// Step 3: Discover target sessions from Runtime V2 state sources.
|
|
371
400
|
const allSessionNames = discoverAbortSessionNames(prefix, persistedState, batchState.currentLanes);
|
|
372
401
|
if (allSessionNames.length === 0) {
|
|
373
|
-
execLog(
|
|
402
|
+
execLog(
|
|
403
|
+
"abort",
|
|
404
|
+
batchState.batchId,
|
|
405
|
+
`No abort targets discovered for prefix "${prefix}" from runtime/persisted state.`,
|
|
406
|
+
);
|
|
374
407
|
}
|
|
375
408
|
|
|
376
409
|
// Step 4: Select and enrich target sessions
|
|
@@ -400,7 +433,7 @@ export async function executeAbort(
|
|
|
400
433
|
}
|
|
401
434
|
|
|
402
435
|
// Step 5b: Wait for sessions to exit
|
|
403
|
-
const allTargetNames = targets.map(t => t.sessionName);
|
|
436
|
+
const allTargetNames = targets.map((t) => t.sessionName);
|
|
404
437
|
const waitResult = await waitForSessionExit(allTargetNames, gracePeriodMs, pollIntervalMs);
|
|
405
438
|
gracefulExits = waitResult.exited.length;
|
|
406
439
|
|
|
@@ -414,7 +447,7 @@ export async function executeAbort(
|
|
|
414
447
|
for (const kr of killResults) {
|
|
415
448
|
killResultBySession.set(kr.sessionName, { killed: kr.killed, error: kr.error });
|
|
416
449
|
}
|
|
417
|
-
const killFailures = killResults.filter(kr => !kr.killed);
|
|
450
|
+
const killFailures = killResults.filter((kr) => !kr.killed);
|
|
418
451
|
if (killFailures.length > 0) {
|
|
419
452
|
errors.push({
|
|
420
453
|
code: "ABORT_KILL_FAILED",
|
|
@@ -426,7 +459,7 @@ export async function executeAbort(
|
|
|
426
459
|
// Build lane results
|
|
427
460
|
const exitedSet = new Set(waitResult.exited);
|
|
428
461
|
for (const target of targets) {
|
|
429
|
-
const wrapUp = wrapUpResults.find(wr => wr.sessionName === target.sessionName);
|
|
462
|
+
const wrapUp = wrapUpResults.find((wr) => wr.sessionName === target.sessionName);
|
|
430
463
|
const wasGraceful = exitedSet.has(target.sessionName);
|
|
431
464
|
const killResult = killResultBySession.get(target.sessionName);
|
|
432
465
|
const sessionKilled = wasGraceful || killResult?.killed === true;
|
|
@@ -443,7 +476,7 @@ export async function executeAbort(
|
|
|
443
476
|
}
|
|
444
477
|
} else {
|
|
445
478
|
// Hard mode: kill all immediately
|
|
446
|
-
const allTargetNames = targets.map(t => t.sessionName);
|
|
479
|
+
const allTargetNames = targets.map((t) => t.sessionName);
|
|
447
480
|
const killResults = killOrchSessions(allTargetNames, {
|
|
448
481
|
stateRoot: repoRoot,
|
|
449
482
|
batchId: batchState.batchId,
|
|
@@ -452,7 +485,7 @@ export async function executeAbort(
|
|
|
452
485
|
for (const kr of killResults) {
|
|
453
486
|
killResultBySession.set(kr.sessionName, { killed: kr.killed, error: kr.error });
|
|
454
487
|
}
|
|
455
|
-
const killFailures = killResults.filter(kr => !kr.killed);
|
|
488
|
+
const killFailures = killResults.filter((kr) => !kr.killed);
|
|
456
489
|
if (killFailures.length > 0) {
|
|
457
490
|
errors.push({
|
|
458
491
|
code: "ABORT_KILL_FAILED",
|
|
@@ -490,7 +523,7 @@ export async function executeAbort(
|
|
|
490
523
|
return {
|
|
491
524
|
mode,
|
|
492
525
|
sessionsFound: targets.length,
|
|
493
|
-
sessionsKilled: laneResults.filter(lr => lr.sessionKilled).length,
|
|
526
|
+
sessionsKilled: laneResults.filter((lr) => lr.sessionKilled).length,
|
|
494
527
|
gracefulExits,
|
|
495
528
|
laneResults,
|
|
496
529
|
wrapUpFailures,
|
|
@@ -499,4 +532,3 @@ export async function executeAbort(
|
|
|
499
532
|
durationMs: Date.now() - startTime,
|
|
500
533
|
};
|
|
501
534
|
}
|
|
502
|
-
|