Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
2f7c7cf
fix(tests): update source file paths and vitest wrappers for refactor…
HenryLach Mar 15, 2026
670d04e
feat(TP-004): repo-scoped lane allocation model
HenryLach Mar 15, 2026
a5ef8c3
checkpoint: TP-004 Step 0 complete
HenryLach Mar 15, 2026
4a17990
checkpoint: TP-004 Step 0 verified and STATUS.md updated
HenryLach Mar 15, 2026
33f4e94
feat(TP-004): make worktree operations repo-scoped
HenryLach Mar 15, 2026
ae2ce63
checkpoint: TP-004 Step 1 complete — repo-scoped worktree operations
HenryLach Mar 15, 2026
307f218
checkpoint: TP-004 Step 1 verification — all tests passing, implement…
HenryLach Mar 15, 2026
933ac4a
feat(TP-004): thread workspaceConfig through execution contracts and …
HenryLach Mar 15, 2026
b35710e
docs(TP-004): document repo-scoped lane allocation and worktree lifec…
HenryLach Mar 15, 2026
a6a1989
checkpoint: TP-004 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
8434382
feat(TP-011): add strict-routing policy controls
HenryLach Mar 15, 2026
7081da3
test(TP-011): add strict routing policy tests (19.x-24.x)
HenryLach Mar 15, 2026
706ddd6
checkpoint: TP-011 Step 1 complete — verify strict routing enforcemen…
HenryLach Mar 15, 2026
72f6d8e
checkpoint: TP-011 Step 1 status update — update test counts and exec…
HenryLach Mar 15, 2026
15dbbb8
test(TP-011): add governance scenario tests (27.1-27.5) for strict vs…
HenryLach Mar 15, 2026
306c361
checkpoint: TP-011 Step 2 complete — governance scenarios verified, t…
HenryLach Mar 15, 2026
b8a11c9
checkpoint: TP-011 Step 3 complete — all tests verified, CLI smoke ch…
HenryLach Mar 15, 2026
c1ffa19
checkpoint: TP-011 Step 3 re-verified — all tests passing, CLI smoke OK
HenryLach Mar 15, 2026
47a8efc
checkpoint: TP-011 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
1e528a6
feat(TP-005): repo-scoped merge orchestration — partition by repo
HenryLach Mar 15, 2026
fcd6563
checkpoint: TP-005 Step 0 — hydrate plan per R001, mark complete
HenryLach Mar 15, 2026
d6688d8
fix(TP-005): address R002 findings — repoId propagation, status rollu…
HenryLach Mar 15, 2026
57758c3
checkpoint: TP-005 Step 0 — R002 findings addressed, re-verified
HenryLach Mar 15, 2026
7ee40f9
fix(TP-005): detect repo-level setup failures in mergeWaveByRepo aggr…
HenryLach Mar 15, 2026
f36bc78
checkpoint: TP-005 Step 0 R002 iter2 fixes applied, tests passing
HenryLach Mar 15, 2026
1135969
feat(TP-005): add repo-divergence partial-success summaries for merge…
HenryLach Mar 15, 2026
447ef03
checkpoint: TP-005 Step 1 complete — repo-divergence partial-success …
HenryLach Mar 15, 2026
4d9fa7e
feat(TP-005): harden failure behavior with shared merge-failure polic…
HenryLach Mar 15, 2026
a481f1c
checkpoint: TP-005 Step 2 verified — repo-level fallback + 15 policy …
HenryLach Mar 15, 2026
f8ac2c0
checkpoint: TP-005 step 3 testing & verification complete — 207/207 t…
HenryLach Mar 15, 2026
05c21f8
fix(TP-005): R008 revise — dedupe review table, fix exact CLI command…
HenryLach Mar 15, 2026
af98081
checkpoint: TP-005 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
6f8d398
feat(TP-006): define schema v2 — repo-aware fields on PersistedBatchS…
HenryLach Mar 15, 2026
24f4d51
feat(TP-006): implement v1→v2 upconversion, validation, fixtures, and…
HenryLach Mar 15, 2026
c9a0584
fix(TP-006): strict v2 mode validation and mode propagation in engine…
HenryLach Mar 15, 2026
555c9e2
feat(TP-006): step 1 — serialization and validation for repo-aware v2…
HenryLach Mar 15, 2026
54551c6
checkpoint: TP-006 step 1 complete — serialization and validation
HenryLach Mar 15, 2026
ede9ca1
checkpoint: TP-006 step 1 verified — all 207 tests passing, 357 asser…
HenryLach Mar 15, 2026
b3980d8
fix(TP-006): align test reimplementations with source — R004 fixes
HenryLach Mar 15, 2026
111a959
fix(TP-006): align test reimplementations with source for R004 review
HenryLach Mar 15, 2026
1f70443
checkpoint: TP-006 step 1 R004 fixes verified — 207 tests passing
HenryLach Mar 15, 2026
3af8836
test(TP-006): add v1→v2 schema compatibility regression tests
HenryLach Mar 15, 2026
51e063e
checkpoint: TP-006 step 2 complete — v1 schema compatibility verified…
HenryLach Mar 15, 2026
d35227a
checkpoint: TP-006 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
0e03ecb
checkpoint: TP-010 Step 0 complete — naming contract defined with ope…
HenryLach Mar 15, 2026
fad880e
feat(TP-010): apply naming contract — operator-scoped sessions, workt…
HenryLach Mar 15, 2026
7781455
checkpoint: TP-010 Step 1 complete — naming contract applied consiste…
HenryLach Mar 15, 2026
7e2c293
test(TP-010): add collision resistance validation tests (Step 2)
HenryLach Mar 15, 2026
817a5ce
test(TP-010): add naming collision resistance tests (83 tests)
HenryLach Mar 15, 2026
6b0d77a
checkpoint: TP-010 Step 3 complete — all 290 tests passing, CLI smoke OK
HenryLach Mar 15, 2026
8e84404
docs(TP-010): add operator_id config reference and naming contract docs
HenryLach Mar 15, 2026
328b57d
feat(TP-007): repo-aware reconciliation — verify existing impl + add …
HenryLach Mar 15, 2026
94aca38
feat(TP-007): repo-aware reconciliation in resumeOrchBatch
HenryLach Mar 15, 2026
335a62e
feat(TP-007): repo-aware resume point computation with pending/blocke…
HenryLach Mar 15, 2026
dd19826
checkpoint: TP-007 Step 1 complete — resume point computation with re…
HenryLach Mar 15, 2026
4acdaf2
feat(TP-007): execute resumed waves safely — fix re-exec merge indexi…
HenryLach Mar 15, 2026
913eb06
checkpoint: TP-007 Step 2 verified — encounteredRepoRoots tracking, c…
HenryLach Mar 15, 2026
b243aee
test(TP-007): Step 3 complete — 290/290 tests pass, CLI smoke pass
HenryLach Mar 15, 2026
4ca3ed0
checkpoint: TP-007 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
cdeb045
feat(TP-009): add mode field to dashboard payload, verify repo field …
HenryLach Mar 15, 2026
e2dc8da
feat(TP-009): add repoResults to persisted merge records for dashboar…
HenryLach Mar 15, 2026
39e31d7
feat(TP-009): implement repo-aware dashboard UI with filter, badges, …
HenryLach Mar 15, 2026
2d2e426
checkpoint: TP-009 Step 2 complete — verified monorepo UX and viewer …
HenryLach Mar 15, 2026
a7b2a26
checkpoint: TP-009 task artifacts (.DONE, STATUS.md)
HenryLach Mar 15, 2026
5a6fafd
feat(TP-012): build polyrepo fixture workspace — Step 0
HenryLach Mar 16, 2026
136da0e
test(TP-012): add polyrepo end-to-end regression tests
HenryLach Mar 16, 2026
3856c38
checkpoint: TP-012 Step 1 complete — 47/47 polyrepo regression tests …
HenryLach Mar 16, 2026
28cee6f
test(TP-012): add monorepo compatibility regression tests and fixture…
HenryLach Mar 16, 2026
098ed1e
docs(TP-012): add fixture files table, test file organization, and li…
HenryLach Mar 16, 2026
e5b55e7
checkpoint: TP-012 step 3 testing & verification complete — 398/398 t…
HenryLach Mar 16, 2026
e7954b0
checkpoint: TP-012 task artifacts (.DONE, STATUS.md)
HenryLach Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 155 additions & 1 deletion dashboard/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ const $historySelect = $("history-select");
const $historyPanel = $("history-panel");
const $historyBody = $("history-body");

// ─── Repo Filter State ──────────────────────────────────────────────────────

const $repoFilter = $("repo-filter");
let selectedRepo = ""; // "" means "All repos"
let knownRepos = []; // sorted list of known repo IDs
let repoFilterVisible = false;

// ─── History State ──────────────────────────────────────────────────────────

let historyList = []; // compact batch summaries
Expand All @@ -147,6 +154,97 @@ let viewingHistoryId = null; // batchId if viewing history, null if live
let viewerMode = null; // "conversation" | "status-md" | null
let viewerTarget = null; // session name (conversation) or taskId (status-md)

// ─── Repo Helpers ───────────────────────────────────────────────────────────

/**
* Build a sorted, deduplicated list of repo IDs from the batch payload.
* Returns empty array when mode !== "workspace" or when fewer than 2 repos.
*/
function buildRepoSet(batch) {
if (!batch || batch.mode !== "workspace") return [];

const repos = new Set();
for (const lane of (batch.lanes || [])) {
if (lane.repoId) repos.add(lane.repoId);
}
for (const task of (batch.tasks || [])) {
const rid = task.resolvedRepoId || task.repoId;
if (rid) repos.add(rid);
}
for (const mr of (batch.mergeResults || [])) {
for (const rr of (mr.repoResults || [])) {
if (rr.repoId) repos.add(rr.repoId);
}
}
const sorted = Array.from(repos).sort();
return sorted.length >= 2 ? sorted : [];
}

/**
* Update the repo filter dropdown options and visibility.
* Resets selection to "All repos" if the previously selected repo disappeared.
*/
function updateRepoFilter(repos) {
knownRepos = repos;
const shouldShow = repos.length >= 2;

if (shouldShow !== repoFilterVisible) {
$repoFilter.style.display = shouldShow ? "" : "none";
repoFilterVisible = shouldShow;
}

if (!shouldShow) {
selectedRepo = "";
return;
}

// If selected repo disappeared, reset to "All"
if (selectedRepo && !repos.includes(selectedRepo)) {
selectedRepo = "";
}

// Rebuild options only if repo set changed
const currentOpts = Array.from($repoFilter.options).slice(1).map(o => o.value);
const changed = currentOpts.length !== repos.length || currentOpts.some((v, i) => v !== repos[i]);
if (changed) {
// Preserve selection
const prev = selectedRepo;
$repoFilter.innerHTML = '<option value="">All repos</option>';
for (const r of repos) {
const opt = document.createElement("option");
opt.value = r;
opt.textContent = r;
$repoFilter.appendChild(opt);
}
$repoFilter.value = prev;
}
}

/** Get the effective repo ID for a task (prefer resolvedRepoId, fallback repoId). */
function taskRepoId(task) {
return task.resolvedRepoId || task.repoId || undefined;
}

/** Render a repo badge span. Returns "" if repoId is falsy or repos not active. */
function repoBadgeHtml(repoId, extraClass) {
if (!repoId || knownRepos.length < 2) return "";
return `<span class="repo-badge ${extraClass || ""}" title="Repo: ${escapeHtml(repoId)}">${escapeHtml(repoId)}</span>`;
}

// Repo filter change handler
$repoFilter.addEventListener("change", (e) => {
selectedRepo = e.target.value;
// Re-render with current data
if (currentData) {
const batch = currentData.batch;
const tmux = currentData.tmuxSessions || [];
if (batch) {
renderLanesTasks(batch, tmux);
renderMergeAgents(batch, tmux);
}
}
});

// ─── Render: Header ─────────────────────────────────────────────────────────

function renderHeader(batch) {
Expand Down Expand Up @@ -288,9 +386,18 @@ function renderLanesTasks(batch, tmuxSessions) {
const tasks = batch.tasks || [];
const tmuxSet = new Set(tmuxSessions || []);
const laneStates = currentData?.laneStates || {};
const showRepos = knownRepos.length >= 2;
let html = "";

for (const lane of batch.lanes) {
// Repo filtering: if a repo is selected, skip lanes that don't match
if (selectedRepo && showRepos) {
const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
const laneMatchesRepo = (lane.repoId === selectedRepo) ||
laneTasks.some(t => (taskRepoId(t) || lane.repoId) === selectedRepo);
if (!laneMatchesRepo) continue;
}

const alive = tmuxSet.has(lane.tmuxSessionName);
const tmuxCmd = `tmux attach -t ${lane.tmuxSessionName}`;

Expand All @@ -301,6 +408,9 @@ function renderLanesTasks(batch, tmuxSessions) {
html += ` <div class="lane-meta">`;
html += ` <span class="lane-session">${escapeHtml(lane.tmuxSessionName || "—")}</span>`;
html += ` <span class="lane-branch">${escapeHtml(lane.branch || "—")}</span>`;
if (showRepos && lane.repoId) {
html += ` ${repoBadgeHtml(lane.repoId, "repo-badge-lane")}`;
}
html += ` </div>`;
html += ` <div class="lane-right">`;
html += ` <span class="tmux-dot ${alive ? "alive" : "dead"}" title="${alive ? "tmux alive" : "tmux dead"}"></span>`;
Expand All @@ -326,6 +436,10 @@ function renderLanesTasks(batch, tmuxSessions) {
const ls = laneStates[lane.tmuxSessionName] || null;

for (const task of laneTasks) {
// Repo filtering at task level
const tRepo = taskRepoId(task) || lane.repoId;
if (selectedRepo && showRepos && tRepo !== selectedRepo) continue;

const sd = task.statusData;
const dur = task.startedAt
? formatDuration((task.endedAt || Date.now()) - task.startedAt)
Expand Down Expand Up @@ -402,7 +516,7 @@ function renderLanesTasks(batch, tmuxSessions) {
<div class="task-row">
<span class="task-icon"><span class="status-dot ${task.status}"></span></span>
<span class="task-actions">${eyeHtml}</span>
<span class="task-id status-${task.status}">${escapeHtml(task.taskId)}</span>
<span class="task-id status-${task.status}">${escapeHtml(task.taskId)}${showRepos ? repoBadgeHtml(tRepo, "repo-badge-task") : ""}</span>
<span><span class="status-badge status-${task.status}"><span class="status-dot ${task.status}"></span> ${task.status}</span></span>
<span class="task-duration">${dur}</span>
<span>${progressHtml}</span>
Expand All @@ -421,6 +535,7 @@ function renderLanesTasks(batch, tmuxSessions) {
function renderMergeAgents(batch, tmuxSessions) {
const mergeResults = batch?.mergeResults || [];
const tmuxSet = new Set(tmuxSessions || []);
const showRepos = knownRepos.length >= 2;

// Check for active merge sessions (convention: orch-merge-*)
const mergeSessions = (tmuxSessions || []).filter(s => s.startsWith("orch-merge"));
Expand All @@ -436,6 +551,14 @@ function renderMergeAgents(batch, tmuxSessions) {

// Show merge results
for (const mr of mergeResults) {
// Repo filtering: if a repo is selected and this merge has repoResults,
// check if the selected repo is among them
const repoResults = mr.repoResults || [];
if (selectedRepo && showRepos && repoResults.length >= 2) {
const hasSelectedRepo = repoResults.some(rr => rr.repoId === selectedRepo);
if (!hasSelectedRepo) continue;
}

const statusCls = mr.status === "succeeded" ? "status-succeeded"
: mr.status === "partial" ? "status-stalled"
: "status-failed";
Expand All @@ -458,6 +581,29 @@ function renderMergeAgents(batch, tmuxSessions) {
html += `</td>`;
html += `<td style="font-size:0.8rem;color:var(--text-muted);">${mr.failureReason ? escapeHtml(mr.failureReason) : "—"}</td>`;
html += `</tr>`;

// Per-repo sub-rows: only when repoResults has 2+ entries (workspace mode)
if (showRepos && repoResults.length >= 2) {
const displayRepos = selectedRepo
? repoResults.filter(rr => rr.repoId === selectedRepo)
: repoResults;

for (const rr of displayRepos) {
const rrStatusCls = rr.status === "succeeded" ? "status-succeeded"
: rr.status === "partial" ? "status-stalled"
: "status-failed";
const rrLanes = (rr.laneNumbers || []).map(n => `L${n}`).join(", ") || "—";
const rrDetail = rr.failureReason ? escapeHtml(rr.failureReason) : "—";

html += `<tr class="merge-repo-row">`;
html += `<td>${repoBadgeHtml(rr.repoId)}</td>`;
html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
html += `<td style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-faint);">${rrLanes}</td>`;
html += `<td></td>`;
html += `<td style="font-size:0.75rem;color:var(--text-faint);">${rrDetail}</td>`;
html += `</tr>`;
}
}
}

// Show active merge sessions not yet in results
Expand Down Expand Up @@ -504,6 +650,9 @@ function renderNoBatch() {
if (noBatchRendered) return;
noBatchRendered = true;

// Hide repo filter when no batch
updateRepoFilter([]);

// Hide live panels, show history panel
const $lanesPanel = document.getElementById("lanes-tasks-panel");
const $mergePanel = document.getElementById("merge-panel");
Expand Down Expand Up @@ -570,6 +719,11 @@ function render(data) {

renderHeader(batch);
renderSummary(batch);

// Update repo filter based on current batch data
const repos = buildRepoSet(batch);
updateRepoFilter(repos);

renderLanesTasks(batch, tmux);
renderMergeAgents(batch, tmux);
renderErrors(batch);
Expand Down
3 changes: 3 additions & 0 deletions dashboard/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<select class="history-select" id="history-select" title="View past batch runs">
<option value="">History ▾</option>
</select>
<select class="repo-filter-select" id="repo-filter" title="Filter by repository" style="display:none;">
<option value="">All repos</option>
</select>
<div class="header-meta">
<span id="last-update">—</span>
<span class="connection-dot disconnected" id="conn-dot" title="SSE connection"></span>
Expand Down
63 changes: 63 additions & 0 deletions dashboard/public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,69 @@ body {
.progress-bar-bg { width: 160px; }
}

/* ─── Repo Filter & Badges ─────────────────────────────────────────────── */

.repo-filter-select {
background: var(--bg-surface);
border: 1px solid var(--border);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 8px;
cursor: pointer;
max-width: 220px;
}
.repo-filter-select:hover {
border-color: var(--accent);
color: var(--text);
}
.repo-filter-select:focus {
outline: none;
border-color: var(--accent);
}

.repo-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-family: var(--font-mono);
font-size: 0.68rem;
font-weight: 500;
padding: 1px 7px;
border-radius: 8px;
background: rgba(188,140,255,0.12);
color: var(--magenta);
white-space: nowrap;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}

.repo-badge-lane {
margin-left: 8px;
}

.repo-badge-task {
margin-left: 4px;
}

/* Merge repo sub-rows */
.merge-repo-row td {
padding: 4px 12px 4px 28px !important;
font-size: 0.78rem !important;
color: var(--text-muted);
border-bottom: 1px solid var(--border-subtle);
}

.merge-repo-row td:first-child {
padding-left: 28px !important;
}

.merge-repo-row:last-child td {
border-bottom: none;
}

/* ─── Scrollbar ────────────────────────────────────────────────────────── */

::-webkit-scrollbar { width: 6px; }
Expand Down
5 changes: 5 additions & 0 deletions dashboard/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,16 @@ function buildDashboardState() {
currentWaveIndex: state.currentWaveIndex || 0,
totalWaves: state.totalWaves || (state.wavePlan ? state.wavePlan.length : 0),
wavePlan: state.wavePlan || [],
// Lanes already include repoId (string|undefined) from PersistedLaneRecord (v2).
lanes: state.lanes || [],
// Tasks already include repoId, resolvedRepoId (string|undefined) from PersistedTaskRecord (v2).
tasks,
mergeResults: state.mergeResults || [],
errors: state.errors || [],
lastError: state.lastError || null,
// Workspace mode: "repo" (default/v1) or "workspace" (v2 multi-repo).
// Additive field — absent in v1 state files, frontend must default to "repo".
mode: state.mode || "repo",
},
tmuxSessions,
timestamp: Date.now(),
Expand Down
31 changes: 24 additions & 7 deletions docs/explanation/persistence-and-resume.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Canonical persisted orchestration state.
Contains (high level):

- schema version (`schemaVersion`)
- batch metadata (`batchId`, phase, timestamps)
- batch metadata (`batchId`, phase, timestamps, mode)
- wave plan and current wave index
- per-lane records (session/worktree/branch/task IDs)
- per-task records (status, folder, session, timings, done marker)
- merge summaries
- per-lane records (session/worktree/branch/task IDs, repo ID)
- per-task records (status, folder, session, timings, done marker, repo attribution)
- merge summaries (grouped by repo in workspace mode)
- aggregate counters and error history

### Lane sidecars (`.pi/lane-state-*.json`)
Expand Down Expand Up @@ -90,9 +90,26 @@ Non-resumable phases (for example `failed`, `stopped`, `completed`) require clea
- re-execute
- mark failed
- skip terminal
6. Compute first incomplete wave
7. Reconstruct runtime counters/state
8. Continue execution from resume wave
- pending (never started)
6. Compute first incomplete wave (skipping waves where all tasks are terminal)
7. Reconstruct runtime counters/state (including blocked-task accounting)
8. Seed blocked task set from reconciled failures and their transitive dependents
9. Continue execution from resume wave

### Workspace mode (polyrepo)

In workspace mode, resume reconciliation is repo-aware:

- Each persisted lane and task record carries a `repoId` identifying its target repo
- Repo roots are resolved at resume time from workspace config (not stored as absolute paths, keeping state portable)
- Reconciliation actions (reconnect, re-execute, worktree reset, cleanup) use per-lane repo roots
- Cross-repo dependency propagation: a failure in one repo correctly blocks dependents in another repo
- Lane metadata and task repo attribution are preserved across resume checkpoints

Backward compatibility:

- State files from before workspace mode (schema v1, no repo fields) resume identically to existing behavior
- Missing repo fields default to `undefined` and fall through to single-repo semantics

---

Expand Down
Loading