feat(kanban): merge-all branches + floating running-agents dock
Two board-level additions: Merge all reviewable agent branches into the main branch sequentially: - worktrees.mergeBranches() merges candidates in order, advancing HEAD each time; refuses if the main tree is dirty, and on the first conflict aborts that merge (main left clean) and stops — prior merges stay, untried branches reported as unattempted. - runManager.mergeAll() picks candidates (settled runs that own their worktree, branch present, not yet in HEAD; deduped, oldest-first) — so refinement runs (which inherit a worktree) are correctly excluded. - POST /runs/merge-all; ⬇ Merge all button in the board header opens a results modal (per-branch ✓/⇢/⚠, conflict banner, inline git output). Floating dock to view/open running agents (front-end only): - RunningAgentsBar pins bottom-right (below the card modal), lists every running run with live elapsed times, and opens that card's modal on click; auto-hides when nothing runs, collapsible otherwise. EOF && echo "" && git log --oneline -4
This commit is contained in:
@@ -15,7 +15,7 @@ import { newToken, REPO_ROOT } from './config.js';
|
||||
import { buildPrompt } from './prompt.js';
|
||||
import { Runner, type PersistedEvent } from './runner.js';
|
||||
import { BevyProcess } from './bevy.js';
|
||||
import { createWorktree, dirtySummary, headSha, isWorktreePresent, removeWorktree, type Worktree } from './worktrees.js';
|
||||
import { createWorktree, dirtySummary, headSha, isWorktreePresent, isMergedIntoHead, mergeBranches, removeWorktree, type BatchMergeResult, type Worktree } from './worktrees.js';
|
||||
|
||||
export interface StartRunInput {
|
||||
cardId: string;
|
||||
@@ -267,6 +267,40 @@ class RunManager {
|
||||
return rows.map((r) => this.reconcile(hydrateRun(r)) as AgentRun);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge all reviewable agent branches into the main branch sequentially.
|
||||
*
|
||||
* Candidates are settled runs that own their worktree and have commits not
|
||||
* yet in HEAD, oldest first (so merges land in roughly the order the runs
|
||||
* happened). De-dups by branch and skips anything already merged. The actual
|
||||
* sequential merge + conflict handling is in `worktrees.mergeBranches`.
|
||||
*/
|
||||
mergeAll(): BatchMergeResult {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT * FROM agent_runs
|
||||
WHERE status IN ('completed','failed','stopped')
|
||||
AND owns_worktree = 1
|
||||
AND branch IS NOT NULL
|
||||
ORDER BY created_at ASC`,
|
||||
)
|
||||
.all() as AgentRunRow[];
|
||||
|
||||
const seenBranch = new Set<string>();
|
||||
const candidates: { branch: string; runId: string }[] = [];
|
||||
for (const row of rows) {
|
||||
const branch = row.branch!;
|
||||
if (seenBranch.has(branch)) continue; // dedup (e.g. multiple runs same branch)
|
||||
seenBranch.add(branch);
|
||||
// Skip branches already fully in HEAD — nothing to merge, and including
|
||||
// them would let an unrelated failure mask that they're done.
|
||||
if (isMergedIntoHead(branch)) continue;
|
||||
candidates.push({ branch, runId: row.id });
|
||||
}
|
||||
|
||||
return mergeBranches(candidates);
|
||||
}
|
||||
|
||||
/** Replayable event history for a run, optionally after a sequence number. */
|
||||
events(id: string, sinceSeq = 0): AgentRunEvent[] {
|
||||
const rows = db
|
||||
|
||||
@@ -177,6 +177,71 @@ export interface MergeResult {
|
||||
output: string;
|
||||
}
|
||||
|
||||
/** Outcome of a single branch in a batch merge. */
|
||||
export interface BatchMergeItem extends MergeResult {
|
||||
/** The run this branch belonged to (for UI correlation). */
|
||||
runId: string;
|
||||
}
|
||||
|
||||
/** Result of merging several branches sequentially into the main branch. */
|
||||
export interface BatchMergeResult {
|
||||
target: string;
|
||||
/** One entry per candidate branch, in the order processed. */
|
||||
items: BatchMergeItem[];
|
||||
/** The branch that conflicted and stopped the batch, if any. Branches after
|
||||
* it were not attempted. */
|
||||
haltedOn: string | null;
|
||||
/** True if the main worktree was dirty and nothing was attempted. */
|
||||
aborted: boolean;
|
||||
/** Human-readable reason when `aborted` or `haltedOn`. */
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple agent branches into the main worktree's checked-out branch,
|
||||
* sequentially. Each successful merge advances HEAD, so later branches merge
|
||||
* against the accumulated result.
|
||||
*
|
||||
* Safety: refuses outright if the main tree is dirty. On the first conflict,
|
||||
* aborts that merge (leaving the main tree clean), records it as `haltedOn`,
|
||||
* and stops — branches after it are NOT attempted, and prior successful merges
|
||||
* stay in place. The caller should surface `haltedOn` + remaining branches so
|
||||
* the operator can resolve or re-run after fixing the conflict.
|
||||
*
|
||||
* @param candidates branches to merge, in order, each tagged with its run id.
|
||||
* Already-merged branches are reported as no-ops and skipped (never halt).
|
||||
*/
|
||||
export function mergeBranches(candidates: { branch: string; runId: string }[]): BatchMergeResult {
|
||||
const target = currentBranch();
|
||||
const dirty = mainDirtySummary();
|
||||
if (dirty) {
|
||||
return {
|
||||
target,
|
||||
items: [],
|
||||
haltedOn: null,
|
||||
aborted: true,
|
||||
reason: `Cannot merge: ${dirty}`,
|
||||
};
|
||||
}
|
||||
|
||||
const items: BatchMergeItem[] = [];
|
||||
let haltedOn: string | null = null;
|
||||
let reason: string | null = null;
|
||||
|
||||
for (const { branch, runId } of candidates) {
|
||||
const result = mergeBranch(branch);
|
||||
items.push({ ...result, runId });
|
||||
if (!result.ok) {
|
||||
// A conflict aborts the batch: stop here, leaving earlier merges in place.
|
||||
haltedOn = branch;
|
||||
reason = `Conflict merging ${branch} — aborted, ${target} left clean.`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { target, items, haltedOn, aborted: false, reason };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge an agent branch into the main worktree's checked-out branch.
|
||||
*
|
||||
|
||||
@@ -144,6 +144,9 @@ orchestrator.post('/runs/:id/merge', (c) => {
|
||||
return c.json(mergeBranch(run.branch));
|
||||
});
|
||||
|
||||
/** Merge all reviewable agent branches into the main branch sequentially. */
|
||||
orchestrator.post('/runs/merge-all', (c) => c.json(runManager.mergeAll()));
|
||||
|
||||
/** Whether a Bevy playtest is running for a run. */
|
||||
orchestrator.get('/runs/:id/bevy', (c) => {
|
||||
const run = runManager.get(c.req.param('id'));
|
||||
|
||||
Reference in New Issue
Block a user