@hueyexe/opencode-ensemble 0.9.0 → 0.10.0
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/README.md +36 -8
- package/dist/index.js +879 -92
- package/package.json +1 -1
- package/src/config.ts +4 -0
- package/src/dashboard-html.ts +91 -0
- package/src/dashboard-js-part1.ts +97 -0
- package/src/dashboard-js-part2.ts +232 -0
- package/src/dashboard-js-part3.ts +50 -0
- package/src/dashboard.ts +166 -0
- package/src/index.ts +51 -8
- package/src/recovery.ts +69 -6
- package/src/system-prompt.ts +6 -0
- package/src/tools/merge-helper.ts +126 -0
- package/src/tools/team-cleanup.ts +44 -87
- package/src/tools/team-merge.ts +49 -0
- package/src/tools/team-shutdown.ts +57 -17
- package/src/watchdog.ts +19 -2
package/dist/index.js
CHANGED
|
@@ -13,6 +13,109 @@ var __export = (target, all) => {
|
|
|
13
13
|
set: __exportSetter.bind(all, name)
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
|
|
18
|
+
// src/log.ts
|
|
19
|
+
function initLog(client3) {
|
|
20
|
+
_client = client3;
|
|
21
|
+
}
|
|
22
|
+
function log(msg) {
|
|
23
|
+
if (!_client)
|
|
24
|
+
return;
|
|
25
|
+
_client.app.log({ service: "ensemble", level: "info", message: msg }).catch(() => {});
|
|
26
|
+
}
|
|
27
|
+
var _client = null;
|
|
28
|
+
|
|
29
|
+
// src/tools/merge-helper.ts
|
|
30
|
+
var exports_merge_helper = {};
|
|
31
|
+
__export(exports_merge_helper, {
|
|
32
|
+
preservedBranchName: () => preservedBranchName,
|
|
33
|
+
preserveBranch: () => preserveBranch,
|
|
34
|
+
mergeBranchRaw: () => mergeBranchRaw,
|
|
35
|
+
mergeBranch: () => mergeBranch,
|
|
36
|
+
gitStashPop: () => gitStashPop,
|
|
37
|
+
gitStash: () => gitStash,
|
|
38
|
+
gitReset: () => gitReset,
|
|
39
|
+
deleteBranch: () => deleteBranch
|
|
40
|
+
});
|
|
41
|
+
async function preserveBranch(sourceBranch, targetBranch, cwd) {
|
|
42
|
+
const proc = Bun.spawn(["git", "branch", targetBranch, sourceBranch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
43
|
+
const exit = await proc.exited;
|
|
44
|
+
if (exit !== 0) {
|
|
45
|
+
const stderr = await new Response(proc.stderr).text();
|
|
46
|
+
log(`merge-helper:preserve:failed src=${sourceBranch} target=${targetBranch} err=${stderr.trim()}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
async function deleteBranch(branch, cwd) {
|
|
52
|
+
const proc = Bun.spawn(["git", "branch", "-D", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
53
|
+
const exit = await proc.exited;
|
|
54
|
+
if (exit !== 0) {
|
|
55
|
+
log(`merge-helper:delete:failed branch=${branch}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
async function mergeBranchRaw(branch, cwd) {
|
|
61
|
+
const merge2 = Bun.spawn(["git", "merge", "--squash", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
62
|
+
const stderrPromise = new Response(merge2.stderr).text();
|
|
63
|
+
const mergeExit = await merge2.exited;
|
|
64
|
+
if (mergeExit !== 0) {
|
|
65
|
+
const stderr = await stderrPromise;
|
|
66
|
+
log(`merge-helper:merge:conflict branch=${branch} err=${stderr.trim()}`);
|
|
67
|
+
const abort = Bun.spawn(["git", "merge", "--abort"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
68
|
+
await abort.exited;
|
|
69
|
+
return { ok: false, error: stderr.trim() || `merge exited with code ${mergeExit}` };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
async function gitStash(cwd) {
|
|
74
|
+
const stash = Bun.spawn(["git", "stash", "--include-untracked"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
75
|
+
const stashOut = await new Response(stash.stdout).text();
|
|
76
|
+
const stashExit = await stash.exited;
|
|
77
|
+
return stashExit === 0 && !stashOut.includes("No local changes");
|
|
78
|
+
}
|
|
79
|
+
async function gitReset(cwd) {
|
|
80
|
+
const reset = Bun.spawn(["git", "reset", "HEAD"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
81
|
+
await reset.exited;
|
|
82
|
+
}
|
|
83
|
+
async function gitStashPop(cwd) {
|
|
84
|
+
const pop = Bun.spawn(["git", "stash", "pop", "--index"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
85
|
+
const exit = await pop.exited;
|
|
86
|
+
if (exit !== 0) {
|
|
87
|
+
const fallback = Bun.spawn(["git", "stash", "pop"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
88
|
+
const fallbackExit = await fallback.exited;
|
|
89
|
+
if (fallbackExit !== 0) {
|
|
90
|
+
log("merge-helper:stash-pop:failed \u2014 stashed work remains in git stash list");
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
async function mergeBranch(branch, cwd) {
|
|
97
|
+
const didStash = await gitStash(cwd);
|
|
98
|
+
const result = await mergeBranchRaw(branch, cwd);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
if (didStash) {
|
|
101
|
+
const restored = await gitStashPop(cwd);
|
|
102
|
+
if (!restored)
|
|
103
|
+
return { ok: false, error: `${result.error}. WARNING: stashed work could not be restored \u2014 check git stash list` };
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
await gitReset(cwd);
|
|
108
|
+
if (didStash) {
|
|
109
|
+
const restored = await gitStashPop(cwd);
|
|
110
|
+
if (!restored)
|
|
111
|
+
return { ok: true, error: "Merge succeeded but stashed work could not be restored \u2014 check git stash list" };
|
|
112
|
+
}
|
|
113
|
+
return { ok: true };
|
|
114
|
+
}
|
|
115
|
+
function preservedBranchName(teamName, memberName) {
|
|
116
|
+
return `ensemble/preserved/${teamName}/${memberName}`;
|
|
117
|
+
}
|
|
118
|
+
var init_merge_helper = () => {};
|
|
16
119
|
|
|
17
120
|
// node_modules/zod/v4/classic/external.js
|
|
18
121
|
var exports_external = {};
|
|
@@ -15491,8 +15594,10 @@ function markDelivered(db, messageId) {
|
|
|
15491
15594
|
}
|
|
15492
15595
|
|
|
15493
15596
|
// src/recovery.ts
|
|
15494
|
-
|
|
15495
|
-
|
|
15597
|
+
init_merge_helper();
|
|
15598
|
+
async function recoverStaleMembers(db, client3, cwd) {
|
|
15599
|
+
const stale = db.query(`SELECT tm.session_id, tm.worktree_branch, tm.name, tm.team_id, t.name as team_name
|
|
15600
|
+
FROM team_member tm
|
|
15496
15601
|
JOIN team t ON tm.team_id = t.id
|
|
15497
15602
|
WHERE tm.status = 'busy' AND t.status = 'active'`).all();
|
|
15498
15603
|
const result = db.run(`UPDATE team_member SET status = 'error', execution_status = 'idle', time_updated = ?
|
|
@@ -15500,6 +15605,14 @@ async function recoverStaleMembers(db, client3) {
|
|
|
15500
15605
|
AND team_id IN (SELECT id FROM team WHERE status = 'active')`, [Date.now()]);
|
|
15501
15606
|
if (client3) {
|
|
15502
15607
|
for (const member of stale) {
|
|
15608
|
+
if (cwd && member.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
15609
|
+
const safeBranch = preservedBranchName(member.team_name, member.name);
|
|
15610
|
+
const ok = await preserveBranch(member.worktree_branch, safeBranch, cwd);
|
|
15611
|
+
if (ok) {
|
|
15612
|
+
db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, member.team_id, member.name]);
|
|
15613
|
+
log(`recovery:branch:preserved src=${member.worktree_branch} target=${safeBranch}`);
|
|
15614
|
+
}
|
|
15615
|
+
}
|
|
15503
15616
|
try {
|
|
15504
15617
|
await client3.session.abort({ sessionID: member.session_id });
|
|
15505
15618
|
} catch {}
|
|
@@ -15558,6 +15671,41 @@ async function recoverUndeliveredMessages(db, client3, registry2) {
|
|
|
15558
15671
|
}
|
|
15559
15672
|
return { redelivered };
|
|
15560
15673
|
}
|
|
15674
|
+
async function recoverOrphanedBranches(db, cwd) {
|
|
15675
|
+
let removed = 0;
|
|
15676
|
+
const archivedTeams = db.query(`SELECT t.name FROM team t
|
|
15677
|
+
WHERE t.status = 'archived'
|
|
15678
|
+
AND NOT EXISTS (
|
|
15679
|
+
SELECT 1 FROM team_member tm
|
|
15680
|
+
WHERE tm.team_id = t.id AND tm.status NOT IN ('shutdown', 'error')
|
|
15681
|
+
)`).all();
|
|
15682
|
+
if (archivedTeams.length === 0)
|
|
15683
|
+
return { removed: 0 };
|
|
15684
|
+
const archivedNames = new Set(archivedTeams.map((t) => t.name));
|
|
15685
|
+
const proc = Bun.spawn(["git", "branch", "--list", "ensemble/preserved/*"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
15686
|
+
const stdoutPromise = new Response(proc.stdout).text();
|
|
15687
|
+
await proc.exited;
|
|
15688
|
+
const stdout = await stdoutPromise;
|
|
15689
|
+
const branches = stdout.split(`
|
|
15690
|
+
`).map((b) => b.trim().replace(/^\* /, "")).filter(Boolean);
|
|
15691
|
+
for (const branch of branches) {
|
|
15692
|
+
const parts = branch.split("/");
|
|
15693
|
+
if (parts.length < 4)
|
|
15694
|
+
continue;
|
|
15695
|
+
const teamName = parts[2];
|
|
15696
|
+
if (!teamName || !archivedNames.has(teamName))
|
|
15697
|
+
continue;
|
|
15698
|
+
try {
|
|
15699
|
+
const del = Bun.spawn(["git", "branch", "-D", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
15700
|
+
const exitCode = await del.exited;
|
|
15701
|
+
if (exitCode === 0) {
|
|
15702
|
+
removed++;
|
|
15703
|
+
log(`recovery:branch:deleted branch=${branch}`);
|
|
15704
|
+
}
|
|
15705
|
+
} catch {}
|
|
15706
|
+
}
|
|
15707
|
+
return { removed };
|
|
15708
|
+
}
|
|
15561
15709
|
|
|
15562
15710
|
// src/state.ts
|
|
15563
15711
|
class MemberRegistry {
|
|
@@ -15831,7 +15979,7 @@ function buildLeadSystemPrompt(db, teamId) {
|
|
|
15831
15979
|
}
|
|
15832
15980
|
lines.push("--- End Messages ---");
|
|
15833
15981
|
}
|
|
15834
|
-
lines.push("", "CRITICAL: Spawn teammates ONE AT A TIME. Send only ONE team_spawn call per response.", "Wait for the tool result before spawning the next teammate.", "Multiple team_spawn calls in a single response will cause timeouts.", "", "Teammates work asynchronously and message you when done.", "Do NOT poll team_status or team_tasks_list repeatedly \u2014 wait for messages.", "After spawning all teammates, tell the user what you've set up and wait.", "When all teammates finish, summarize results and suggest next steps.", "", "Before calling team_cleanup, verify teammates have committed their work.", "team_shutdown will warn you if a teammate has uncommitted changes.", "team_cleanup will block if any worktree has uncommitted changes \u2014 merge or commit first.");
|
|
15982
|
+
lines.push("", "CRITICAL: Spawn teammates ONE AT A TIME. Send only ONE team_spawn call per response.", "Wait for the tool result before spawning the next teammate.", "Multiple team_spawn calls in a single response will cause timeouts.", "", "Teammates work asynchronously and message you when done.", "Do NOT poll team_status or team_tasks_list repeatedly \u2014 wait for messages.", "After spawning all teammates, tell the user what you've set up and wait.", "When all teammates finish, summarize results and suggest next steps.", "", "MERGE WORKFLOW:", "After a teammate finishes and you shut them down, use team_merge to merge their branch.", "Do NOT tell teammates to commit \u2014 they handle that themselves.", "Do NOT run git merge manually \u2014 use team_merge which handles stashing and unstaging.", "team_cleanup will safety-net merge any branches you forgot, but prefer explicit team_merge.", "", "Before calling team_cleanup, verify teammates have committed their work.", "team_shutdown will warn you if a teammate has uncommitted changes.", "team_cleanup will block if any worktree has uncommitted changes \u2014 merge or commit first.");
|
|
15835
15983
|
return lines.join(`
|
|
15836
15984
|
`);
|
|
15837
15985
|
}
|
|
@@ -15890,18 +16038,6 @@ function buildTeamCompactionContext(db, teamId, role, memberName) {
|
|
|
15890
16038
|
return lines.join(`
|
|
15891
16039
|
`);
|
|
15892
16040
|
}
|
|
15893
|
-
|
|
15894
|
-
// src/log.ts
|
|
15895
|
-
var _client = null;
|
|
15896
|
-
function initLog(client3) {
|
|
15897
|
-
_client = client3;
|
|
15898
|
-
}
|
|
15899
|
-
function log(msg) {
|
|
15900
|
-
if (!_client)
|
|
15901
|
-
return;
|
|
15902
|
-
_client.app.log({ service: "ensemble", level: "info", message: msg }).catch(() => {});
|
|
15903
|
-
}
|
|
15904
|
-
|
|
15905
16041
|
// src/types.ts
|
|
15906
16042
|
function findTeamBySession(db, registry2, sessionId) {
|
|
15907
16043
|
const entry = registry2.getBySession(sessionId);
|
|
@@ -15933,7 +16069,8 @@ var DEFAULT_CONFIG = {
|
|
|
15933
16069
|
stallMinSteps: 3,
|
|
15934
16070
|
stallTokenThreshold: 500,
|
|
15935
16071
|
timeoutMs: 30 * 60 * 1000,
|
|
15936
|
-
rateLimitCapacity: 10
|
|
16072
|
+
rateLimitCapacity: 10,
|
|
16073
|
+
dashboardPort: 4747
|
|
15937
16074
|
};
|
|
15938
16075
|
function readConfigFile(filePath) {
|
|
15939
16076
|
try {
|
|
@@ -15952,6 +16089,8 @@ function readConfigFile(filePath) {
|
|
|
15952
16089
|
result.timeoutMs = raw.timeoutMs;
|
|
15953
16090
|
if (typeof raw.rateLimitCapacity === "number")
|
|
15954
16091
|
result.rateLimitCapacity = raw.rateLimitCapacity;
|
|
16092
|
+
if (typeof raw.dashboardPort === "number")
|
|
16093
|
+
result.dashboardPort = raw.dashboardPort;
|
|
15955
16094
|
return result;
|
|
15956
16095
|
} catch (err) {
|
|
15957
16096
|
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
@@ -16038,6 +16177,586 @@ class ProgressTracker {
|
|
|
16038
16177
|
}
|
|
16039
16178
|
}
|
|
16040
16179
|
|
|
16180
|
+
// src/dashboard-html.ts
|
|
16181
|
+
var DASHBOARD_HEAD = `<!DOCTYPE html>
|
|
16182
|
+
<html lang="en">
|
|
16183
|
+
<head>
|
|
16184
|
+
<meta charset="UTF-8">
|
|
16185
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
16186
|
+
<title>Ensemble</title>
|
|
16187
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><circle cx='8' cy='8' r='6' fill='%2322c55e'/></svg>">
|
|
16188
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
16189
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16190
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
16191
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
16192
|
+
<script>tailwind.config={theme:{extend:{colors:{base:{950:'#0c0e14',900:'#141822',850:'#1a1f2e',800:'#1e2433',700:'#2a3144',600:'#3a4358'},txt:{100:'#e2e8f0',200:'#c1c9d9',300:'#8892a8',400:'#5e6a82',500:'#4a5568'}},fontFamily:{sans:['Inter','system-ui','sans-serif'],mono:['JetBrains Mono','monospace']}}}}</script>
|
|
16193
|
+
<style>
|
|
16194
|
+
@media(prefers-reduced-motion:no-preference){
|
|
16195
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
16196
|
+
@keyframes fadein{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
|
|
16197
|
+
@keyframes hl{from{background:rgba(59,130,246,.1)}to{background:transparent}}
|
|
16198
|
+
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
|
16199
|
+
.pulse{animation:pulse 2s ease-in-out infinite}
|
|
16200
|
+
.fadein{animation:fadein .25s ease-out}
|
|
16201
|
+
.hl{animation:hl 1.5s ease-out}
|
|
16202
|
+
.shimmer{background:linear-gradient(90deg,#22c55e 0%,#4ade80 50%,#22c55e 100%)!important;background-size:200% 100%;animation:shimmer 1.5s ease-in-out}
|
|
16203
|
+
}
|
|
16204
|
+
.scroll::-webkit-scrollbar{width:5px}.scroll::-webkit-scrollbar-track{background:transparent}.scroll::-webkit-scrollbar-thumb{background:#2a3144;border-radius:3px}
|
|
16205
|
+
details summary::-webkit-details-marker{display:none}details summary{list-style:none}
|
|
16206
|
+
select{-webkit-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%235e6a82' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:22px}
|
|
16207
|
+
.xp{max-height:0;overflow:hidden;transition:max-height .3s ease-out}.xp-open{max-height:3000px;transition:max-height .5s ease-in}
|
|
16208
|
+
.card-sel{outline:2px solid rgba(59,130,246,.4);outline-offset:1px}
|
|
16209
|
+
.md pre{background:#1a1f2e;border:1px solid #1e2433;border-radius:6px;padding:8px 12px;overflow-x:auto;margin:6px 0;font-size:12px;line-height:1.5}
|
|
16210
|
+
.md code{background:#1a1f2e;padding:1px 5px;border-radius:3px;font-size:12px}.md pre code{background:none;padding:0}
|
|
16211
|
+
.md strong{color:#e2e8f0;font-weight:600}.md em{color:#c1c9d9;font-style:italic}
|
|
16212
|
+
.md ul,.md ol{padding-left:18px;margin:4px 0}.md li{margin:2px 0}
|
|
16213
|
+
.md h1,.md h2,.md h3{color:#e2e8f0;font-weight:600;margin:8px 0 4px}.md h1{font-size:15px}.md h2{font-size:14px}.md h3{font-size:13px}
|
|
16214
|
+
.md a{color:#60a5fa;text-decoration:underline}.md p{margin:4px 0}
|
|
16215
|
+
#sco{display:none;position:fixed;inset:0;background:rgba(12,14,20,.85);backdrop-filter:blur(8px);z-index:100;align-items:center;justify-content:center}
|
|
16216
|
+
#sco.show{display:flex}
|
|
16217
|
+
#drawer{position:fixed;top:0;right:0;bottom:0;width:min(480px,85vw);background:#141822;border-left:1px solid #1e2433;z-index:90;transform:translateX(100%);transition:transform .25s ease-out;overflow-y:auto}
|
|
16218
|
+
#drawer.open{transform:translateX(0)}
|
|
16219
|
+
#drawer-bg{position:fixed;inset:0;background:rgba(12,14,20,.5);z-index:89;display:none}
|
|
16220
|
+
#drawer-bg.open{display:block}
|
|
16221
|
+
</style>
|
|
16222
|
+
</head>
|
|
16223
|
+
<body class="bg-base-950 text-txt-100 min-h-screen antialiased font-sans">
|
|
16224
|
+
<header class="fixed top-0 inset-x-0 h-11 bg-base-950/95 backdrop-blur border-b border-base-800 flex items-center justify-between px-4 z-50">
|
|
16225
|
+
<div class="flex items-center gap-3">
|
|
16226
|
+
<span class="font-mono font-semibold text-[13px] tracking-[.08em] text-txt-200">ensemble</span>
|
|
16227
|
+
<span class="text-base-700">|</span>
|
|
16228
|
+
<select id="sel" class="bg-base-900 border border-base-700 rounded-md px-2 py-[3px] text-[11px] text-txt-200 font-mono outline-none cursor-pointer hover:border-base-600 transition-colors"></select>
|
|
16229
|
+
</div>
|
|
16230
|
+
<div class="flex items-center gap-4">
|
|
16231
|
+
<div id="hring" class="w-6 h-6 rounded-full" title="Team health"></div>
|
|
16232
|
+
<div class="flex items-center gap-2">
|
|
16233
|
+
<span id="clk" class="text-[11px] text-txt-400 font-mono"></span>
|
|
16234
|
+
<span class="text-base-700">\xB7</span>
|
|
16235
|
+
<span id="cd" class="w-[7px] h-[7px] rounded-full bg-emerald-500 pulse"></span>
|
|
16236
|
+
<span id="ct" class="text-[11px] text-txt-400 font-mono">...</span>
|
|
16237
|
+
</div>
|
|
16238
|
+
</div>
|
|
16239
|
+
</header>
|
|
16240
|
+
<div id="sum" class="fixed top-11 inset-x-0 h-8 bg-base-900/80 backdrop-blur border-b border-base-800/50 flex items-center px-4 gap-4 text-[11px] text-txt-300 z-40"></div>
|
|
16241
|
+
<main class="pt-[76px] px-4 pb-16">
|
|
16242
|
+
<div id="empty" class="hidden flex-col items-center justify-center h-[70vh] gap-3">
|
|
16243
|
+
<div class="w-12 h-12 rounded-full border-2 border-base-700 flex items-center justify-center mb-2"><svg class="w-5 h-5 text-txt-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 4.5v15m7.5-7.5h-15"/></svg></div>
|
|
16244
|
+
<div class="text-txt-400 text-sm">Waiting for a team</div>
|
|
16245
|
+
<div class="text-txt-500 text-[11px]">Run <code class="px-1.5 py-0.5 bg-base-900 rounded text-txt-300 font-mono text-[11px]">team_create</code> in OpenCode to get started</div>
|
|
16246
|
+
</div>
|
|
16247
|
+
<div id="content" class="hidden">
|
|
16248
|
+
<div id="agents" class="grid gap-2 mb-4" style="grid-template-columns:repeat(auto-fill,minmax(300px,1fr))"></div>
|
|
16249
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
16250
|
+
<div id="tasks" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div>
|
|
16251
|
+
<div id="activity" class="bg-base-900 rounded-lg p-3 border border-base-800/50"></div>
|
|
16252
|
+
</div>
|
|
16253
|
+
</div>
|
|
16254
|
+
</main>
|
|
16255
|
+
<div id="tl" class="fixed bottom-0 inset-x-0 h-10 bg-base-900/90 backdrop-blur border-t border-base-800 px-4 flex items-center z-40 overflow-x-auto scroll hidden"></div>
|
|
16256
|
+
<div id="sco" onclick="this.classList.remove('show')">
|
|
16257
|
+
<div class="bg-base-900 border border-base-800 rounded-lg p-6 max-w-sm" onclick="event.stopPropagation()">
|
|
16258
|
+
<div class="text-txt-200 font-semibold text-sm mb-4">Keyboard Shortcuts</div>
|
|
16259
|
+
<div class="grid grid-cols-2 gap-y-2 gap-x-6 text-[12px]">
|
|
16260
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">j</kbd> <span class="text-txt-400">Next agent</span></div>
|
|
16261
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">k</kbd> <span class="text-txt-400">Prev agent</span></div>
|
|
16262
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">Enter</kbd> <span class="text-txt-400">Expand agent</span></div>
|
|
16263
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">Esc</kbd> <span class="text-txt-400">Collapse all</span></div>
|
|
16264
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">1-9</kbd> <span class="text-txt-400">Switch team</span></div>
|
|
16265
|
+
<div><kbd class="px-1.5 py-0.5 bg-base-800 rounded font-mono text-txt-300 text-[11px]">?</kbd> <span class="text-txt-400">This help</span></div>
|
|
16266
|
+
</div>
|
|
16267
|
+
</div>
|
|
16268
|
+
</div>
|
|
16269
|
+
<div id="drawer-bg" onclick="closeDrawer()"></div>
|
|
16270
|
+
<div id="drawer" class="scroll p-4"></div>`;
|
|
16271
|
+
|
|
16272
|
+
// src/dashboard-js-part1.ts
|
|
16273
|
+
var DASHBOARD_JS_PART1 = `
|
|
16274
|
+
let S=null,selId=null,fails=0,pollT=Date.now(),prevMC=0,selCard=-1;
|
|
16275
|
+
const expCards=new Set(),expMsgs=new Set();
|
|
16276
|
+
const E=s=>s?String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'):'';
|
|
16277
|
+
const D=ms=>{const s=Math.floor(Math.abs(ms)/1000);return s<60?s+'s':s<3600?Math.floor(s/60)+'m':Math.floor(s/3600)+'h'};
|
|
16278
|
+
const T=e=>new Date(e).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
16279
|
+
function relT(ep){const ms=Date.now()-ep;if(ms<60000)return Math.floor(ms/1000)+'s ago';if(ms<3600000)return Math.floor(ms/60000)+'m ago';if(ms<86400000)return Math.floor(ms/3600000)+'h ago';return T(ep)}
|
|
16280
|
+
|
|
16281
|
+
// Chip: small readable badge with background tint
|
|
16282
|
+
function chip(text,color){
|
|
16283
|
+
var colors={
|
|
16284
|
+
blue:'bg-blue-500/15 text-blue-400 border-blue-500/20',
|
|
16285
|
+
green:'bg-emerald-500/15 text-emerald-400 border-emerald-500/20',
|
|
16286
|
+
amber:'bg-amber-500/15 text-amber-400 border-amber-500/20',
|
|
16287
|
+
red:'bg-red-500/15 text-red-400 border-red-500/20',
|
|
16288
|
+
gray:'bg-base-700/40 text-txt-300 border-base-700/30',
|
|
16289
|
+
muted:'bg-base-800/60 text-txt-400 border-base-700/20',
|
|
16290
|
+
};
|
|
16291
|
+
var c=colors[color]||colors.muted;
|
|
16292
|
+
return '<span class="inline-flex items-center px-1.5 py-[1px] rounded text-[10px] font-medium border '+c+'">'+text+'</span>';
|
|
16293
|
+
}
|
|
16294
|
+
|
|
16295
|
+
function md(s){
|
|
16296
|
+
let h=E(s);
|
|
16297
|
+
h=h.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,function(_,l,c){return '<pre><code>'+c.trim()+'</code></pre>'});
|
|
16298
|
+
h=h.replace(/\`([^\`]+)\`/g,'<code>$1</code>');
|
|
16299
|
+
h=h.replace(/^### (.+)$/gm,'<h3>$1</h3>');
|
|
16300
|
+
h=h.replace(/^## (.+)$/gm,'<h2>$1</h2>');
|
|
16301
|
+
h=h.replace(/^# (.+)$/gm,'<h1>$1</h1>');
|
|
16302
|
+
h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
|
|
16303
|
+
h=h.replace(/\\*(.+?)\\*/g,'<em>$1</em>');
|
|
16304
|
+
h=h.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
16305
|
+
h=h.replace(/^- (.+)$/gm,'<li>$1</li>');
|
|
16306
|
+
h=h.replace(/(<li>.*<\\/li>\\n?)+/g,'<ul>$&</ul>');
|
|
16307
|
+
h=h.replace(/^\\d+\\. (.+)$/gm,'<li>$1</li>');
|
|
16308
|
+
h=h.replace(/\\n\\n/g,'</p><p>');
|
|
16309
|
+
h=h.replace(/\\n/g,'<br>');
|
|
16310
|
+
return '<p>'+h+'</p>';
|
|
16311
|
+
}
|
|
16312
|
+
|
|
16313
|
+
function saveD(el){const s={};el.querySelectorAll('details').forEach((d,i)=>{s[i]=d.open});return s}
|
|
16314
|
+
function restD(el,s){if(!s)return;el.querySelectorAll('details').forEach((d,i)=>{if(i in s)d.open=s[i]})}
|
|
16315
|
+
function saveSc(el){const s=el.querySelector('.scroll');return s?s.scrollTop:0}
|
|
16316
|
+
function restSc(el,p){const s=el.querySelector('.scroll');if(s)s.scrollTop=p}
|
|
16317
|
+
function patch(el,h){if(el.innerHTML!==h)el.innerHTML=h}
|
|
16318
|
+
|
|
16319
|
+
const ST={
|
|
16320
|
+
busy:{c:'border-blue-500 bg-blue-500/[0.04]',d:'bg-blue-500',t:'text-blue-400',l:'working',dim:false},
|
|
16321
|
+
ready:{c:'border-base-700 bg-base-950/50',d:'bg-txt-400',t:'text-txt-400',l:'idle',dim:true},
|
|
16322
|
+
shutdown_requested:{c:'border-amber-500/50 bg-amber-500/[0.04]',d:'bg-amber-500',t:'text-amber-400',l:'stopping',dim:false},
|
|
16323
|
+
shutdown:{c:'border-base-800 bg-base-950/30',d:'bg-base-700',t:'text-txt-500',l:'done',dim:true},
|
|
16324
|
+
error:{c:'border-red-500/50 bg-red-500/[0.04]',d:'bg-red-500',t:'text-red-400',l:'error',dim:false},
|
|
16325
|
+
};
|
|
16326
|
+
const si=s=>ST[s]||ST.ready;
|
|
16327
|
+
const PR={high:'text-red-400 bg-red-500/10 border border-red-500/20',medium:'text-amber-400 bg-amber-500/10 border border-amber-500/20',low:'text-txt-400 bg-base-800 border border-base-700'};
|
|
16328
|
+
|
|
16329
|
+
function parseR(c){const m=c.match(/<task-result>([\\s\\S]*?)<\\/task-result>/);if(!m)return null;const i=m[1],s=(i.match(/<status>([\\s\\S]*?)<\\/status>/)||[])[1]?.trim(),u=(i.match(/<summary>([\\s\\S]*?)<\\/summary>/)||[])[1]?.trim(),d=(i.match(/<details>([\\s\\S]*?)<\\/details>/)||[])[1]?.trim();return s&&u?{status:s,summary:u,details:d||''}:null}
|
|
16330
|
+
|
|
16331
|
+
function allTeams(){
|
|
16332
|
+
if(!S?.teams)return{active:[],archived:[]};
|
|
16333
|
+
const active=[...S.teams.filter(t=>t.status==='active')].sort((a,b)=>b.timeUpdated-a.timeUpdated);
|
|
16334
|
+
const archived=[...S.teams.filter(t=>t.status!=='active')].sort((a,b)=>b.timeUpdated-a.timeUpdated);
|
|
16335
|
+
return{active,archived};
|
|
16336
|
+
}
|
|
16337
|
+
function cur(){const{active,archived}=allTeams(),all=[...active,...archived];if(!all.length)return null;if(selId){const t=all.find(t=>t.id===selId);if(t)return t}return active[0]||all[0]}
|
|
16338
|
+
|
|
16339
|
+
function deriveHealth(t){
|
|
16340
|
+
const mm=t.members||[];if(!mm.length)return{w:0,i:0,e:0,d:0,total:0};
|
|
16341
|
+
return{w:mm.filter(m=>m.status==='busy').length,i:mm.filter(m=>m.status==='ready').length,e:mm.filter(m=>m.status==='error').length,d:mm.filter(m=>m.status==='shutdown'||m.status==='shutdown_requested').length,total:mm.length};
|
|
16342
|
+
}
|
|
16343
|
+
|
|
16344
|
+
function deriveSparkline(name,msgs){
|
|
16345
|
+
const mine=msgs.filter(m=>m.fromName===name).map(m=>m.timeCreated).sort();
|
|
16346
|
+
if(mine.length<2)return '';
|
|
16347
|
+
const min=mine[0],max=mine[mine.length-1],range=max-min||1;
|
|
16348
|
+
const buckets=new Array(12).fill(0);
|
|
16349
|
+
mine.forEach(t=>{const idx=Math.min(11,Math.floor((t-min)/range*12));buckets[idx]++});
|
|
16350
|
+
const mx=Math.max(...buckets)||1;
|
|
16351
|
+
const bars=buckets.map((v,i)=>{const h=Math.max(1,Math.round(v/mx*14));return '<rect x="'+(i*5)+'" y="'+(14-h)+'" width="3.5" height="'+h+'" rx="0.5" fill="currentColor" opacity="'+(0.3+v/mx*0.7)+'"/>'}).join('');
|
|
16352
|
+
return '<svg class="inline-block text-blue-500/60" width="60" height="14" viewBox="0 0 60 14">'+bars+'</svg>';
|
|
16353
|
+
}
|
|
16354
|
+
|
|
16355
|
+
function deriveTimeline(t){
|
|
16356
|
+
const ev=[];
|
|
16357
|
+
(t.members||[]).forEach(m=>{ev.push({t:m.timeCreated,type:'spawn',label:E(m.name)+' spawned',c:'bg-blue-400'});if(m.status==='shutdown')ev.push({t:m.timeUpdated,type:'off',label:E(m.name)+' shut down',c:'bg-txt-500'});if(m.status==='error')ev.push({t:m.timeUpdated,type:'err',label:E(m.name)+' error',c:'bg-red-500'})});
|
|
16358
|
+
(t.messages||[]).forEach(m=>{const p=parseR(m.content);ev.push({t:m.timeCreated,type:'msg',label:E(m.fromName)+' \\u2192 '+(E(m.toName)||'all'),c:p?'bg-emerald-500':'bg-blue-400'})});
|
|
16359
|
+
(t.tasks||[]).filter(x=>x.status==='completed').forEach(x=>{ev.push({t:x.timeUpdated,type:'done',label:'Task done',c:'bg-emerald-500'})});
|
|
16360
|
+
return ev.sort((a,b)=>a.t-b.t).slice(-50);
|
|
16361
|
+
}
|
|
16362
|
+
|
|
16363
|
+
function deriveThreads(msgs){
|
|
16364
|
+
const threads={};
|
|
16365
|
+
msgs.forEach(m=>{const k=m.fromName;if(!threads[k])threads[k]={from:m.fromName,msgs:[]};threads[k].msgs.push(m)});
|
|
16366
|
+
return Object.values(threads).sort((a,b)=>b.msgs[0].timeCreated-a.msgs[0].timeCreated);
|
|
16367
|
+
}
|
|
16368
|
+
`;
|
|
16369
|
+
|
|
16370
|
+
// src/dashboard-js-part2.ts
|
|
16371
|
+
var DASHBOARD_JS_PART2 = `
|
|
16372
|
+
function rSel(){
|
|
16373
|
+
const el=document.getElementById('sel'),{active,archived}=allTeams(),c=cur();
|
|
16374
|
+
if(!active.length&&!archived.length){el.innerHTML='<option>No teams</option>';return}
|
|
16375
|
+
let h='';
|
|
16376
|
+
if(active.length){h+='<optgroup label="Active">';h+=active.map(t=>'<option value="'+t.id+'">'+E(t.name)+' ('+relT(t.timeUpdated)+')</option>').join('');h+='</optgroup>'}
|
|
16377
|
+
if(archived.length){h+='<optgroup label="Archived">';h+=archived.slice(0,5).map(t=>'<option value="'+t.id+'">'+E(t.name)+'</option>').join('');if(archived.length>5)h+='<option disabled>+ '+(archived.length-5)+' more</option>';h+='</optgroup>'}
|
|
16378
|
+
if(el._lh!==h){el.innerHTML=h;el._lh=h}
|
|
16379
|
+
if(c)el.value=c.id;
|
|
16380
|
+
}
|
|
16381
|
+
|
|
16382
|
+
function rHealth(t){
|
|
16383
|
+
const el=document.getElementById('hring'),h=deriveHealth(t);
|
|
16384
|
+
if(!h.total){el.style.background='conic-gradient(#2a3144 0deg,#2a3144 360deg)';return}
|
|
16385
|
+
const total=h.total,segs=[];let deg=0;
|
|
16386
|
+
if(h.w){const d=h.w/total*360;segs.push('#3b82f6 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16387
|
+
if(h.i){const d=h.i/total*360;segs.push('#5e6a82 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16388
|
+
if(h.e){const d=h.e/total*360;segs.push('#ef4444 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16389
|
+
if(h.d){const d=h.d/total*360;segs.push('#2a3144 '+deg+'deg '+(deg+d)+'deg');deg+=d}
|
|
16390
|
+
el.style.background='conic-gradient('+segs.join(',')+')';
|
|
16391
|
+
el.style.mask='radial-gradient(circle at center,transparent 7px,black 8px)';
|
|
16392
|
+
el.style.webkitMask=el.style.mask;
|
|
16393
|
+
}
|
|
16394
|
+
|
|
16395
|
+
function rClock(t){
|
|
16396
|
+
const el=document.getElementById('clk');
|
|
16397
|
+
const now=new Date();
|
|
16398
|
+
const hh=String(now.getHours()).padStart(2,'0'),mm=String(now.getMinutes()).padStart(2,'0'),ss=String(now.getSeconds()).padStart(2,'0');
|
|
16399
|
+
const uptime=t?D(Date.now()-t.timeCreated):'';
|
|
16400
|
+
el.textContent=hh+':'+mm+':'+ss+(uptime?' \\u00B7 '+uptime:'');
|
|
16401
|
+
}
|
|
16402
|
+
|
|
16403
|
+
function rSum(t){
|
|
16404
|
+
const el=document.getElementById('sum'),mm=t.members||[],tk=t.tasks||[],msgs=t.messages||[];
|
|
16405
|
+
const h=deriveHealth(t),tc=tk.filter(x=>x.status==='completed').length;
|
|
16406
|
+
patch(el,
|
|
16407
|
+
'<span class="w-[6px] h-[6px] rounded-full '+(t.status==='active'?'bg-emerald-500':'bg-txt-500')+'"></span>'+
|
|
16408
|
+
'<span class="font-mono font-semibold text-txt-200">'+E(t.name)+'</span>'+
|
|
16409
|
+
(mm.length?chip(mm.length+' agent'+(mm.length!==1?'s':''),'gray'):'')+
|
|
16410
|
+
(h.w?chip(h.w+' working','blue'):'')+
|
|
16411
|
+
(h.i?chip(h.i+' idle','muted'):'')+
|
|
16412
|
+
(h.d?chip(h.d+' done','muted'):'')+
|
|
16413
|
+
(h.e?chip(h.e+' error','red'):'')+
|
|
16414
|
+
(tk.length?chip(tc+'/'+tk.length+' tasks','green'):'')+
|
|
16415
|
+
(msgs.length?chip(msgs.length+' msgs','muted'):'')
|
|
16416
|
+
);
|
|
16417
|
+
}
|
|
16418
|
+
|
|
16419
|
+
function rAgents(t){
|
|
16420
|
+
const el=document.getElementById('agents'),mm=t.members||[];
|
|
16421
|
+
if(!mm.length){patch(el,'<div class="col-span-full text-center py-12"><div class="text-txt-400 text-sm mb-1">No agents yet</div><div class="text-txt-500 text-[11px]">Spawn teammates with <code class="px-1 py-0.5 bg-base-900 rounded font-mono text-[10px]">team_spawn</code></div></div>');return}
|
|
16422
|
+
const n=Date.now(),msgs=t.messages||[],sorted=[...mm].sort((a,b)=>{const o={busy:0,ready:1,shutdown_requested:2,error:3,shutdown:4};return(o[a.status]??5)-(o[b.status]??5)});
|
|
16423
|
+
const html=sorted.map((m,idx)=>{
|
|
16424
|
+
const s=si(m.status),task=(t.tasks||[]).find(x=>x.assignee===m.name&&x.status==='in_progress'),msg=msgs.find(x=>x.fromName===m.name);
|
|
16425
|
+
const d=D(n-m.timeUpdated),mi=msg?relT(msg.timeCreated):'\\u2014';
|
|
16426
|
+
const tt=task?.content,tr=tt&&tt.length>90?tt.slice(0,90)+'\\u2026':tt;
|
|
16427
|
+
const spark=deriveSparkline(m.name,msgs);
|
|
16428
|
+
const isSel=selCard===idx;
|
|
16429
|
+
return '<div class="rounded-lg border '+s.c+' p-3 transition-all duration-300 cursor-pointer hover:border-base-600'+(s.dim?' opacity-50 hover:opacity-70':'')+(isSel?' card-sel':'')+'" data-card="'+E(m.name)+'" onclick="openDrawer(\\''+E(m.name)+'\\')">'+
|
|
16430
|
+
'<div class="flex items-center gap-2">'+
|
|
16431
|
+
'<span class="w-[8px] h-[8px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+' shrink-0"></span>'+
|
|
16432
|
+
'<span class="font-mono font-semibold text-[14px] truncate">'+E(m.name)+'</span>'+
|
|
16433
|
+
'<span class="text-[10px] px-1.5 py-[1px] rounded '+s.t+' bg-base-800/80 shrink-0">'+s.l+'</span>'+
|
|
16434
|
+
spark+
|
|
16435
|
+
'<span class="text-[10px] text-txt-500 ml-auto shrink-0">'+E(m.agent)+'</span>'+
|
|
16436
|
+
'</div>'+
|
|
16437
|
+
(tr?'<div class="mt-2 text-[13px] text-txt-200 leading-snug truncate">'+E(tr)+'</div>':'')+
|
|
16438
|
+
'<div class="mt-2 flex items-center gap-1.5 flex-wrap">'+
|
|
16439
|
+
chip(d,'muted')+chip('msg '+mi,'muted')+
|
|
16440
|
+
(m.worktreeBranch?chip(E(m.worktreeBranch),'muted'):'')+
|
|
16441
|
+
'</div></div>';
|
|
16442
|
+
}).join('');
|
|
16443
|
+
patch(el,html);
|
|
16444
|
+
}
|
|
16445
|
+
|
|
16446
|
+
function openDrawer(name){
|
|
16447
|
+
var t=cur();if(!t)return;
|
|
16448
|
+
var m=(t.members||[]).find(x=>x.name===name);if(!m)return;
|
|
16449
|
+
var s=si(m.status),msgs=(t.messages||[]).filter(x=>x.fromName===name);
|
|
16450
|
+
var h='';
|
|
16451
|
+
// Header
|
|
16452
|
+
h+='<div class="flex items-center justify-between mb-4">';
|
|
16453
|
+
h+='<div class="flex items-center gap-2"><span class="w-[10px] h-[10px] rounded-full '+s.d+(m.status==='busy'?' pulse':'')+'"></span><span class="font-mono font-semibold text-[16px]">'+E(m.name)+'</span><span class="text-[11px] px-2 py-[2px] rounded '+s.t+' bg-base-800/80">'+s.l+'</span></div>';
|
|
16454
|
+
h+='<button onclick="closeDrawer()" class="text-txt-500 hover:text-txt-200 transition-colors p-1"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg></button>';
|
|
16455
|
+
h+='</div>';
|
|
16456
|
+
// Meta chips
|
|
16457
|
+
var meta=[];
|
|
16458
|
+
meta.push(chip(E(m.agent),'gray'));
|
|
16459
|
+
if(m.model)meta.push(chip(E(m.model),'gray'));
|
|
16460
|
+
meta.push(chip(E(m.executionStatus||m.status),m.status==='busy'?'blue':m.status==='error'?'red':'muted'));
|
|
16461
|
+
if(m.planApproval&&m.planApproval!=='none')meta.push(chip(E(m.planApproval),m.planApproval==='approved'?'green':m.planApproval==='rejected'?'red':'amber'));
|
|
16462
|
+
meta.push(chip('spawned '+relT(m.timeCreated),'muted'));
|
|
16463
|
+
if(m.worktreeBranch)meta.push(chip(E(m.worktreeBranch),'muted'));
|
|
16464
|
+
h+='<div class="flex flex-wrap gap-1.5 mb-4 pb-4 border-b border-base-800/50">'+meta.join('')+'</div>';
|
|
16465
|
+
// Prompt
|
|
16466
|
+
if(m.prompt){
|
|
16467
|
+
h+='<div class="mb-4"><div class="text-txt-400 text-[10px] uppercase tracking-wider mb-2">Original Prompt</div>';
|
|
16468
|
+
h+='<div class="text-[13px] text-txt-200 md bg-base-800/20 rounded-lg p-3 border border-base-800/30">'+md(m.prompt)+'</div></div>';
|
|
16469
|
+
}
|
|
16470
|
+
// Chat log
|
|
16471
|
+
h+='<div class="text-txt-400 text-[10px] uppercase tracking-wider mb-3">Conversation</div>';
|
|
16472
|
+
if(!msgs.length){h+='<div class="text-txt-500 text-[12px]">No messages yet</div>'}
|
|
16473
|
+
else{
|
|
16474
|
+
// Get all messages involving this agent (sent or received)
|
|
16475
|
+
var allMsgs=(t.messages||[]).filter(function(x){return x.fromName===name||x.toName===name});
|
|
16476
|
+
allMsgs.sort(function(a,b){return a.timeCreated-b.timeCreated});
|
|
16477
|
+
allMsgs.forEach(function(am){
|
|
16478
|
+
var isAgent=am.fromName===name;
|
|
16479
|
+
var p=parseR(am.content);
|
|
16480
|
+
var deliv=am.delivered?(am.read?'\\u2713\\u2713':'\\u2713'):'';
|
|
16481
|
+
var align=isAgent?'mr-8':'ml-8';
|
|
16482
|
+
var bubbleBg=isAgent?'bg-blue-500/[0.07] border-blue-500/20':'bg-base-800/40 border-base-700/30';
|
|
16483
|
+
var sender=isAgent?E(am.fromName):'lead';
|
|
16484
|
+
h+='<div class="mb-2 '+align+'">';
|
|
16485
|
+
h+='<div class="rounded-xl border '+bubbleBg+' p-3">';
|
|
16486
|
+
h+='<div class="flex items-center gap-2 mb-1.5">';
|
|
16487
|
+
h+='<span class="text-[10px] font-medium '+(isAgent?'text-blue-400':'text-txt-300')+'">'+sender+'</span>';
|
|
16488
|
+
h+=chip(relT(am.timeCreated),'muted');
|
|
16489
|
+
if(am.toName)h+=chip('\\u2192 '+E(am.toName),'muted');
|
|
16490
|
+
if(deliv)h+='<span class="text-txt-500 text-[10px] ml-auto">'+deliv+'</span>';
|
|
16491
|
+
h+='</div>';
|
|
16492
|
+
if(p){
|
|
16493
|
+
h+='<div class="mb-1">'+chip(E(p.status),p.status==='completed'?'green':'red')+' <span class="text-[13px] text-txt-200 font-medium">'+E(p.summary)+'</span></div>';
|
|
16494
|
+
if(p.details)h+='<div class="text-[12px] text-txt-300 md mt-2">'+md(p.details)+'</div>';
|
|
16495
|
+
}else{
|
|
16496
|
+
h+='<div class="text-[12px] text-txt-300 md">'+md(am.content)+'</div>';
|
|
16497
|
+
}
|
|
16498
|
+
h+='</div></div>';
|
|
16499
|
+
});
|
|
16500
|
+
}
|
|
16501
|
+
document.getElementById('drawer').innerHTML=h;
|
|
16502
|
+
document.getElementById('drawer').classList.add('open');
|
|
16503
|
+
document.getElementById('drawer-bg').classList.add('open');
|
|
16504
|
+
}
|
|
16505
|
+
|
|
16506
|
+
function closeDrawer(){
|
|
16507
|
+
document.getElementById('drawer').classList.remove('open');
|
|
16508
|
+
document.getElementById('drawer-bg').classList.remove('open');
|
|
16509
|
+
}
|
|
16510
|
+
|
|
16511
|
+
function rTasks(t){
|
|
16512
|
+
const el=document.getElementById('tasks'),tk=t.tasks||[];
|
|
16513
|
+
if(!tk.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">No tasks yet</div>');return}
|
|
16514
|
+
const c=tk.filter(x=>x.status==='completed').length,pct=Math.max(Math.round(c/tk.length*100),tk.length>0?2:0);
|
|
16515
|
+
const allDone=c===tk.length&&tk.length>0;
|
|
16516
|
+
const g={in_progress:[],pending:[],completed:[],blocked:[],cancelled:[]};for(const x of tk)(g[x.status]||(g[x.status]=[])).push(x);
|
|
16517
|
+
const ds=saveD(el);
|
|
16518
|
+
const taskMap={};tk.forEach(x=>{taskMap[x.id]=x});
|
|
16519
|
+
function grp(label,items,dc,defOpen){
|
|
16520
|
+
if(!items.length)return'';
|
|
16521
|
+
const rows=items.map(x=>{
|
|
16522
|
+
const ic=x.status==='completed'?'\\u2713':x.status==='in_progress'?'\\u25CF':'\\u25CB';
|
|
16523
|
+
const icl=x.status==='completed'?'text-emerald-500':x.status==='in_progress'?'text-blue-400':'text-txt-500';
|
|
16524
|
+
const p=PR[x.priority]||PR.low,cn=x.content.length>55?x.content.slice(0,55)+'\\u2026':x.content;
|
|
16525
|
+
let dep='';
|
|
16526
|
+
if(x.dependsOn){const parent=taskMap[x.dependsOn];dep='<div class="text-[10px] text-txt-500 mt-0.5">\\u2514\\u2192 '+(parent?E(parent.content.slice(0,30)):E(x.dependsOn))+'</div>'}
|
|
16527
|
+
return '<div class="flex items-start gap-2 py-1.5 px-1 rounded hover:bg-base-800/40 transition-colors">'+
|
|
16528
|
+
'<span class="'+icl+' text-[11px] mt-[2px] shrink-0">'+ic+'</span>'+
|
|
16529
|
+
'<div class="flex-1 min-w-0"><div class="text-[13px] text-txt-200 truncate">'+E(cn)+'</div>'+
|
|
16530
|
+
'<div class="flex gap-2 mt-1 text-[10px]"><span class="rounded px-1.5 py-[0px] '+p+'">'+E(x.priority)+'</span>'+
|
|
16531
|
+
'<span class="text-txt-500">'+(E(x.assignee)||'unassigned')+'</span></div>'+dep+'</div></div>';
|
|
16532
|
+
}).join('');
|
|
16533
|
+
return '<details '+(defOpen?'open':'')+'><summary class="flex items-center gap-2 text-[11px] text-txt-300 cursor-pointer py-1.5 hover:text-txt-200 transition-colors select-none">'+
|
|
16534
|
+
'<svg class="w-3 h-3 transition-transform [details[open]>summary>&]:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>'+
|
|
16535
|
+
'<span class="w-[6px] h-[6px] rounded-full '+dc+'"></span>'+label+' <span class="text-txt-500">('+items.length+')</span></summary>'+
|
|
16536
|
+
'<div class="ml-5 mt-1">'+rows+'</div></details>';
|
|
16537
|
+
}
|
|
16538
|
+
el.innerHTML='<div class="flex items-center gap-3 mb-3">'+
|
|
16539
|
+
'<span class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider">Tasks</span>'+
|
|
16540
|
+
'<div class="flex-1 bg-base-800 rounded-full h-[5px]"><div class="'+(allDone?'shimmer':'bg-emerald-500')+' h-[5px] rounded-full transition-all duration-500" style="width:'+pct+'%"></div></div>'+
|
|
16541
|
+
'<span class="text-[11px] text-txt-400 font-mono">'+c+'/'+tk.length+'</span>'+
|
|
16542
|
+
(allDone?'<span class="text-emerald-400 text-[10px] font-medium">All complete</span>':'')+
|
|
16543
|
+
'</div>'+
|
|
16544
|
+
grp('In Progress',g.in_progress,'bg-blue-500',true)+grp('Pending',g.pending,'bg-txt-500',true)+grp('Blocked',g.blocked||[],'bg-amber-500',true)+grp('Completed',g.completed,'bg-emerald-500',false);
|
|
16545
|
+
restD(el,ds);
|
|
16546
|
+
}
|
|
16547
|
+
|
|
16548
|
+
function rActivity(t){
|
|
16549
|
+
const el=document.getElementById('activity'),msgs=t.messages||[];
|
|
16550
|
+
if(!msgs.length){patch(el,'<div class="text-txt-500 text-sm py-6 text-center">Waiting for agent messages...</div>');return}
|
|
16551
|
+
const sp=saveSc(el);
|
|
16552
|
+
const newCount=msgs.length,hasNew=newCount>prevMC&&prevMC>0;
|
|
16553
|
+
let html='<div class="text-[11px] text-txt-300 font-semibold uppercase tracking-wider mb-3">Activity '+chip(msgs.length+' msgs','muted')+'</div>';
|
|
16554
|
+
html+='<div class="max-h-[50vh] overflow-y-auto scroll space-y-2">';
|
|
16555
|
+
msgs.slice(0,30).forEach(function(m,mi){
|
|
16556
|
+
const isNew=hasNew&&mi<(newCount-prevMC);
|
|
16557
|
+
const isExp=expMsgs.has(m.id);
|
|
16558
|
+
const p=parseR(m.content);
|
|
16559
|
+
const deliv=m.delivered?(m.read?'\\u2713\\u2713':'\\u2713'):'';
|
|
16560
|
+
const isFromAgent=m.fromName!=='lead'&&m.fromName!=='system';
|
|
16561
|
+
const align=isFromAgent?'mr-6':'ml-6';
|
|
16562
|
+
const bubbleBg=isFromAgent?'bg-blue-500/[0.06] border-blue-500/15':'bg-base-800/40 border-base-700/30';
|
|
16563
|
+
const initial=m.fromName.charAt(0).toUpperCase();
|
|
16564
|
+
const avatarColor=m.fromName==='system'?'bg-amber-500/20 text-amber-400':isFromAgent?'bg-blue-500/20 text-blue-400':'bg-emerald-500/20 text-emerald-400';
|
|
16565
|
+
html+='<div class="'+align+(isNew?' hl':'')+' cursor-pointer" data-msg="'+E(m.id)+'" onclick="toggleMsg(\\''+E(m.id)+'\\')">';
|
|
16566
|
+
html+='<div class="flex items-start gap-2">';
|
|
16567
|
+
html+='<span class="w-5 h-5 rounded-full '+avatarColor+' flex items-center justify-center text-[9px] font-semibold shrink-0 mt-0.5">'+E(initial)+'</span>';
|
|
16568
|
+
html+='<div class="flex-1 min-w-0 rounded-xl border '+bubbleBg+' px-3 py-2">';
|
|
16569
|
+
html+='<div class="flex items-center gap-1.5 mb-1">';
|
|
16570
|
+
html+='<span class="text-[11px] font-medium '+(isFromAgent?'text-blue-400':'text-txt-300')+'">'+E(m.fromName)+'</span>';
|
|
16571
|
+
if(m.toName)html+=chip('\\u2192 '+E(m.toName),'muted');
|
|
16572
|
+
html+=chip(relT(m.timeCreated),'muted');
|
|
16573
|
+
if(deliv)html+='<span class="text-txt-500 text-[10px] ml-auto">'+deliv+'</span>';
|
|
16574
|
+
html+='</div>';
|
|
16575
|
+
if(p){
|
|
16576
|
+
html+='<div>'+chip(E(p.status),p.status==='completed'?'green':'red')+' <span class="text-[13px] text-txt-200">'+E(p.summary)+'</span></div>';
|
|
16577
|
+
if(isExp&&p.details)html+='<div class="mt-2 text-[12px] text-txt-300 md">'+md(p.details)+'</div>';
|
|
16578
|
+
if(!isExp&&p.details)html+='<div class="mt-1 text-[10px] text-txt-500">Click to expand details</div>';
|
|
16579
|
+
}else{
|
|
16580
|
+
if(isExp){html+='<div class="text-[12px] text-txt-300 md">'+md(m.content)+'</div>'}
|
|
16581
|
+
else{const preview=m.content.length>120?m.content.slice(0,120)+'\\u2026':m.content;html+='<div class="text-[13px] text-txt-300 truncate">'+E(preview)+'</div>'}
|
|
16582
|
+
}
|
|
16583
|
+
html+='</div></div></div>';
|
|
16584
|
+
});
|
|
16585
|
+
if(msgs.length>30)html+='<div class="text-center text-[10px] text-txt-500 py-2">+'+(msgs.length-30)+' older messages</div>';
|
|
16586
|
+
html+='</div>';
|
|
16587
|
+
prevMC=newCount;
|
|
16588
|
+
el.innerHTML=html;
|
|
16589
|
+
restSc(el,sp);
|
|
16590
|
+
}
|
|
16591
|
+
|
|
16592
|
+
function rTimeline(t){
|
|
16593
|
+
const el=document.getElementById('tl'),evs=deriveTimeline(t);
|
|
16594
|
+
if(!evs.length){el.classList.add('hidden');return}
|
|
16595
|
+
el.classList.remove('hidden');
|
|
16596
|
+
const html=evs.map(ev=>{
|
|
16597
|
+
return '<div class="flex flex-col items-center mx-1 shrink-0 group" title="'+E(ev.label)+' \\u00B7 '+relT(ev.t)+'"><div class="w-[6px] h-[6px] rounded-full '+ev.c+' group-hover:scale-150 transition-transform"></div><div class="text-[8px] text-txt-500 mt-0.5 hidden group-hover:block whitespace-nowrap">'+E(ev.label)+'</div></div>';
|
|
16598
|
+
}).join('<div class="w-3 h-px bg-base-700 shrink-0 self-center"></div>');
|
|
16599
|
+
patch(el,'<span class="text-[9px] text-txt-500 uppercase tracking-wider mr-3 shrink-0">Timeline</span>'+html);
|
|
16600
|
+
}
|
|
16601
|
+
`;
|
|
16602
|
+
|
|
16603
|
+
// src/dashboard-js-part3.ts
|
|
16604
|
+
var DASHBOARD_JS_PART3 = `
|
|
16605
|
+
function toggleMsg(id){if(expMsgs.has(id))expMsgs.delete(id);else expMsgs.add(id);render()}
|
|
16606
|
+
|
|
16607
|
+
function render(){
|
|
16608
|
+
rSel();const t=cur();
|
|
16609
|
+
const empty=document.getElementById('empty'),content=document.getElementById('content');
|
|
16610
|
+
if(!t){empty.classList.remove('hidden');empty.classList.add('flex');content.classList.add('hidden');document.getElementById('tl').classList.add('hidden');return}
|
|
16611
|
+
empty.classList.add('hidden');empty.classList.remove('flex');content.classList.remove('hidden');
|
|
16612
|
+
rHealth(t);rSum(t);rAgents(t);rTasks(t);rActivity(t);rTimeline(t);
|
|
16613
|
+
}
|
|
16614
|
+
|
|
16615
|
+
function conn(ok){
|
|
16616
|
+
document.getElementById('cd').className='w-[7px] h-[7px] rounded-full '+(ok?'bg-emerald-500 pulse':'bg-red-500');
|
|
16617
|
+
document.getElementById('ct').textContent=ok?D(Date.now()-pollT)+' ago':'disconnected';
|
|
16618
|
+
}
|
|
16619
|
+
|
|
16620
|
+
async function poll(){try{S=await(await fetch('/api/state')).json();fails=0;pollT=Date.now();conn(true);render()}catch{if(++fails>=3)conn(false)}}
|
|
16621
|
+
|
|
16622
|
+
// Clock update every second
|
|
16623
|
+
setInterval(function(){var t=cur();if(t)rClock(t);if(fails<3)conn(true)},1000);
|
|
16624
|
+
|
|
16625
|
+
// Poll every 2.5s
|
|
16626
|
+
setInterval(poll,2500);
|
|
16627
|
+
|
|
16628
|
+
// Keyboard shortcuts
|
|
16629
|
+
document.addEventListener('keydown',function(e){
|
|
16630
|
+
if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA')return;
|
|
16631
|
+
var t=cur();if(!t)return;
|
|
16632
|
+
var mm=t.members||[];
|
|
16633
|
+
if(e.key==='?'){e.preventDefault();document.getElementById('sco').classList.toggle('show');return}
|
|
16634
|
+
if(e.key==='Escape'){closeDrawer();expMsgs.clear();selCard=-1;document.getElementById('sco').classList.remove('show');render();return}
|
|
16635
|
+
if(e.key==='j'&&mm.length){e.preventDefault();selCard=Math.min(selCard+1,mm.length-1);render();return}
|
|
16636
|
+
if(e.key==='k'&&mm.length){e.preventDefault();selCard=Math.max(selCard-1,0);render();return}
|
|
16637
|
+
if(e.key==='Enter'&&selCard>=0&&selCard<mm.length){e.preventDefault();openDrawer(mm[selCard].name);return}
|
|
16638
|
+
if(e.key>='1'&&e.key<='9'){
|
|
16639
|
+
var teams=allTeams(),all=[...teams.active,...teams.archived];
|
|
16640
|
+
var idx=parseInt(e.key)-1;
|
|
16641
|
+
if(idx<all.length){selId=all[idx].id;render()}
|
|
16642
|
+
}
|
|
16643
|
+
});
|
|
16644
|
+
|
|
16645
|
+
// Select handler
|
|
16646
|
+
document.getElementById('sel').addEventListener('change',function(){selId=this.value;render()});
|
|
16647
|
+
|
|
16648
|
+
// Initial poll
|
|
16649
|
+
poll();
|
|
16650
|
+
|
|
16651
|
+
console.log('%c Ensemble Mission Control','font-size:14px;font-weight:bold;color:#22c55e');
|
|
16652
|
+
`;
|
|
16653
|
+
|
|
16654
|
+
// src/dashboard.ts
|
|
16655
|
+
var DASHBOARD_HTML = DASHBOARD_HEAD + `
|
|
16656
|
+
<script>` + DASHBOARD_JS_PART1 + DASHBOARD_JS_PART2 + DASHBOARD_JS_PART3 + `</script>
|
|
16657
|
+
</body></html>`;
|
|
16658
|
+
function jsonResponse(data, status = 200) {
|
|
16659
|
+
return new Response(JSON.stringify(data), {
|
|
16660
|
+
status,
|
|
16661
|
+
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
|
|
16662
|
+
});
|
|
16663
|
+
}
|
|
16664
|
+
function buildState(db) {
|
|
16665
|
+
const teams = db.query("SELECT id, name, status, lead_agent, time_created, time_updated FROM team ORDER BY time_created DESC").all();
|
|
16666
|
+
const memberStmt = db.query("SELECT name, agent, status, execution_status, worktree_branch, prompt, model, plan_approval, time_created, time_updated FROM team_member WHERE team_id = ?");
|
|
16667
|
+
const taskStmt = db.query("SELECT id, content, status, priority, assignee, depends_on, time_created, time_updated FROM team_task WHERE team_id = ?");
|
|
16668
|
+
const msgStmt = db.query("SELECT id, from_name, to_name, content, delivered, read, time_created FROM team_message WHERE team_id = ? ORDER BY time_created DESC LIMIT 50");
|
|
16669
|
+
return {
|
|
16670
|
+
teams: teams.map((t) => ({
|
|
16671
|
+
id: t.id,
|
|
16672
|
+
name: t.name,
|
|
16673
|
+
status: t.status,
|
|
16674
|
+
leadAgent: t.lead_agent,
|
|
16675
|
+
timeCreated: t.time_created,
|
|
16676
|
+
timeUpdated: t.time_updated,
|
|
16677
|
+
members: memberStmt.all(t.id).map((m) => ({
|
|
16678
|
+
name: m.name,
|
|
16679
|
+
agent: m.agent,
|
|
16680
|
+
status: m.status,
|
|
16681
|
+
executionStatus: m.execution_status,
|
|
16682
|
+
worktreeBranch: m.worktree_branch,
|
|
16683
|
+
prompt: m.prompt,
|
|
16684
|
+
model: m.model,
|
|
16685
|
+
planApproval: m.plan_approval,
|
|
16686
|
+
timeCreated: m.time_created,
|
|
16687
|
+
timeUpdated: m.time_updated
|
|
16688
|
+
})),
|
|
16689
|
+
tasks: taskStmt.all(t.id).map((tk) => ({
|
|
16690
|
+
id: tk.id,
|
|
16691
|
+
content: tk.content,
|
|
16692
|
+
status: tk.status,
|
|
16693
|
+
priority: tk.priority,
|
|
16694
|
+
assignee: tk.assignee,
|
|
16695
|
+
dependsOn: tk.depends_on,
|
|
16696
|
+
timeCreated: tk.time_created,
|
|
16697
|
+
timeUpdated: tk.time_updated
|
|
16698
|
+
})),
|
|
16699
|
+
messages: msgStmt.all(t.id).map((msg) => ({
|
|
16700
|
+
id: msg.id,
|
|
16701
|
+
fromName: msg.from_name,
|
|
16702
|
+
toName: msg.to_name,
|
|
16703
|
+
content: msg.content,
|
|
16704
|
+
delivered: msg.delivered === 1,
|
|
16705
|
+
read: msg.read === 1,
|
|
16706
|
+
timeCreated: msg.time_created
|
|
16707
|
+
}))
|
|
16708
|
+
}))
|
|
16709
|
+
};
|
|
16710
|
+
}
|
|
16711
|
+
async function startDashboard(db, port) {
|
|
16712
|
+
try {
|
|
16713
|
+
const server2 = Bun.serve({
|
|
16714
|
+
port,
|
|
16715
|
+
fetch(req) {
|
|
16716
|
+
const url2 = new URL(req.url);
|
|
16717
|
+
if (url2.pathname === "/api/health") {
|
|
16718
|
+
return jsonResponse({ ensemble: true, pid: process.pid });
|
|
16719
|
+
}
|
|
16720
|
+
if (url2.pathname === "/api/state") {
|
|
16721
|
+
return jsonResponse(buildState(db));
|
|
16722
|
+
}
|
|
16723
|
+
if (url2.pathname === "/") {
|
|
16724
|
+
return new Response(DASHBOARD_HTML, {
|
|
16725
|
+
headers: { "Content-Type": "text/html" }
|
|
16726
|
+
});
|
|
16727
|
+
}
|
|
16728
|
+
return new Response("Not Found", { status: 404 });
|
|
16729
|
+
}
|
|
16730
|
+
});
|
|
16731
|
+
log(`dashboard:started port=${port} url=http://localhost:${port}`);
|
|
16732
|
+
return server2;
|
|
16733
|
+
} catch (err) {
|
|
16734
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE") {
|
|
16735
|
+
try {
|
|
16736
|
+
const res = await fetch(`http://localhost:${port}/api/health`);
|
|
16737
|
+
const data = await res.json();
|
|
16738
|
+
if (data.ensemble && data.pid) {
|
|
16739
|
+
let alive = false;
|
|
16740
|
+
try {
|
|
16741
|
+
process.kill(data.pid, 0);
|
|
16742
|
+
alive = true;
|
|
16743
|
+
} catch {}
|
|
16744
|
+
if (alive && data.pid !== process.pid) {
|
|
16745
|
+
log(`dashboard:already-running port=${port} pid=${data.pid}`);
|
|
16746
|
+
return null;
|
|
16747
|
+
}
|
|
16748
|
+
log(`dashboard:stale-server port=${port} stale-pid=${data.pid} \u2014 run: kill -9 ${data.pid} || lsof -ti:${port} | xargs kill -9`);
|
|
16749
|
+
return null;
|
|
16750
|
+
}
|
|
16751
|
+
} catch {}
|
|
16752
|
+
log(`dashboard:port-in-use port=${port} (not an ensemble instance)`);
|
|
16753
|
+
return null;
|
|
16754
|
+
}
|
|
16755
|
+
log(`dashboard:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
16756
|
+
return null;
|
|
16757
|
+
}
|
|
16758
|
+
}
|
|
16759
|
+
|
|
16041
16760
|
// src/tools/team-create.ts
|
|
16042
16761
|
async function executeTeamCreate(deps, args, sessionId) {
|
|
16043
16762
|
const nameError = validateTeamName(args.name);
|
|
@@ -16390,7 +17109,8 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
16390
17109
|
}
|
|
16391
17110
|
|
|
16392
17111
|
// src/tools/team-shutdown.ts
|
|
16393
|
-
|
|
17112
|
+
init_merge_helper();
|
|
17113
|
+
async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktreeDirty, preserve = preserveBranch) {
|
|
16394
17114
|
const teamInfo = requireLead(deps, sessionId);
|
|
16395
17115
|
const member = deps.db.query("SELECT session_id, status, worktree_branch, worktree_dir FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, args.member);
|
|
16396
17116
|
if (!member)
|
|
@@ -16399,8 +17119,8 @@ async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktre
|
|
|
16399
17119
|
throw new Error(`Teammate "${args.member}" is already shut down`);
|
|
16400
17120
|
const force = args.force ?? false;
|
|
16401
17121
|
if (member.status === "shutdown_requested") {
|
|
16402
|
-
await
|
|
16403
|
-
const branchInfo =
|
|
17122
|
+
await preserveAndAbort(deps, teamInfo.teamId, teamInfo.teamName, args.member, member.session_id, member.worktree_branch, preserve);
|
|
17123
|
+
const branchInfo = getBranchInfo(deps, teamInfo.teamId, args.member);
|
|
16404
17124
|
const dirtyWarning = await getDirtyWarning(member.worktree_dir, args.member, isDirty);
|
|
16405
17125
|
return `Force shut down "${args.member}".${branchInfo}${dirtyWarning}`;
|
|
16406
17126
|
}
|
|
@@ -16411,29 +17131,51 @@ async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktre
|
|
|
16411
17131
|
isIdle = !sessionStatus || sessionStatus.type === "idle";
|
|
16412
17132
|
} catch {}
|
|
16413
17133
|
if (isIdle || force) {
|
|
16414
|
-
await
|
|
16415
|
-
const branchInfo =
|
|
17134
|
+
await preserveAndAbort(deps, teamInfo.teamId, teamInfo.teamName, args.member, member.session_id, member.worktree_branch, preserve);
|
|
17135
|
+
const branchInfo = getBranchInfo(deps, teamInfo.teamId, args.member);
|
|
16416
17136
|
const dirtyWarning = await getDirtyWarning(member.worktree_dir, args.member, isDirty);
|
|
16417
17137
|
return `Teammate "${args.member}" has been shut down.${branchInfo}${dirtyWarning}`;
|
|
16418
17138
|
}
|
|
17139
|
+
if (member.worktree_branch) {
|
|
17140
|
+
const safeBranch = preservedBranchName(teamInfo.teamName, args.member);
|
|
17141
|
+
const ok = await preserve(member.worktree_branch, safeBranch, deps.directory);
|
|
17142
|
+
if (ok) {
|
|
17143
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamInfo.teamId, args.member]);
|
|
17144
|
+
log(`shutdown:branch:preserved-graceful src=${member.worktree_branch} target=${safeBranch}`);
|
|
17145
|
+
}
|
|
17146
|
+
}
|
|
16419
17147
|
try {
|
|
16420
|
-
|
|
17148
|
+
deps.client.session.promptAsync({
|
|
16421
17149
|
sessionID: member.session_id,
|
|
16422
17150
|
parts: [{
|
|
16423
17151
|
type: "text",
|
|
16424
17152
|
text: `[Shutdown requested]: The lead has requested you shut down. Finish your current task, send your final findings to the lead via team_message, then stop.`
|
|
16425
17153
|
}]
|
|
16426
|
-
});
|
|
17154
|
+
}).catch(() => {});
|
|
16427
17155
|
} catch {}
|
|
16428
17156
|
deps.db.run("UPDATE team_member SET status = 'shutdown_requested', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), teamInfo.teamId, args.member]);
|
|
16429
17157
|
return `Shutdown requested for ${args.member}. They will finish current work and shut down. Call team_shutdown with force: true to abort immediately.`;
|
|
16430
17158
|
}
|
|
16431
|
-
async function
|
|
17159
|
+
async function preserveAndAbort(deps, teamId, teamName, memberName, sessionId, worktreeBranch, preserve) {
|
|
17160
|
+
if (worktreeBranch && !worktreeBranch.startsWith("ensemble/preserved/")) {
|
|
17161
|
+
const safeBranch = preservedBranchName(teamName, memberName);
|
|
17162
|
+
const ok = await preserve(worktreeBranch, safeBranch, deps.directory);
|
|
17163
|
+
if (ok) {
|
|
17164
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamId, memberName]);
|
|
17165
|
+
log(`shutdown:branch:preserved src=${worktreeBranch} target=${safeBranch}`);
|
|
17166
|
+
} else {
|
|
17167
|
+
log(`shutdown:branch:preserve-failed src=${worktreeBranch} target=${safeBranch}`);
|
|
17168
|
+
}
|
|
17169
|
+
}
|
|
16432
17170
|
try {
|
|
16433
17171
|
await deps.client.session.abort({ sessionID: sessionId });
|
|
16434
17172
|
} catch {}
|
|
16435
17173
|
deps.db.run("UPDATE team_member SET status = 'shutdown', execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), teamId, memberName]);
|
|
16436
17174
|
}
|
|
17175
|
+
function getBranchInfo(deps, teamId, memberName) {
|
|
17176
|
+
const row = deps.db.query("SELECT worktree_branch FROM team_member WHERE team_id = ? AND name = ?").get(teamId, memberName);
|
|
17177
|
+
return row?.worktree_branch ? ` Changes preserved on branch: ${row.worktree_branch}. Use team_merge to merge.` : "";
|
|
17178
|
+
}
|
|
16437
17179
|
async function getDirtyWarning(worktreeDir, memberName, isDirty) {
|
|
16438
17180
|
if (!worktreeDir)
|
|
16439
17181
|
return "";
|
|
@@ -16448,40 +17190,8 @@ Warning: ${memberName} has uncommitted changes in their worktree. Commit or merg
|
|
|
16448
17190
|
}
|
|
16449
17191
|
|
|
16450
17192
|
// src/tools/team-cleanup.ts
|
|
16451
|
-
|
|
16452
|
-
|
|
16453
|
-
const conflicted = [];
|
|
16454
|
-
const stash = Bun.spawn(["git", "stash", "--include-untracked"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16455
|
-
const stashOut = await new Response(stash.stdout).text();
|
|
16456
|
-
const stashExit = await stash.exited;
|
|
16457
|
-
const didStash = stashExit === 0 && !stashOut.includes("No local changes");
|
|
16458
|
-
for (const branch of branches) {
|
|
16459
|
-
const merge2 = Bun.spawn(["git", "merge", "--squash", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16460
|
-
const stderrPromise = new Response(merge2.stderr).text();
|
|
16461
|
-
const exit = await merge2.exited;
|
|
16462
|
-
if (exit !== 0) {
|
|
16463
|
-
const stderr = await stderrPromise;
|
|
16464
|
-
log(`cleanup:merge:conflict branch=${branch} err=${stderr.trim()}`);
|
|
16465
|
-
const abort = Bun.spawn(["git", "merge", "--abort"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16466
|
-
await abort.exited;
|
|
16467
|
-
conflicted.push(branch);
|
|
16468
|
-
} else {
|
|
16469
|
-
const del = Bun.spawn(["git", "branch", "-D", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16470
|
-
await del.exited;
|
|
16471
|
-
merged.push(branch);
|
|
16472
|
-
}
|
|
16473
|
-
}
|
|
16474
|
-
if (merged.length > 0) {
|
|
16475
|
-
const reset = Bun.spawn(["git", "reset", "HEAD"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16476
|
-
await reset.exited;
|
|
16477
|
-
}
|
|
16478
|
-
if (didStash) {
|
|
16479
|
-
const pop = Bun.spawn(["git", "stash", "pop", "--index"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16480
|
-
await pop.exited;
|
|
16481
|
-
}
|
|
16482
|
-
return { merged, conflicted };
|
|
16483
|
-
}
|
|
16484
|
-
async function executeTeamCleanup(deps, args, sessionId, isDirty = checkWorktreeDirty, mergeAll = mergeAllBranches, mergeOnCleanup = true) {
|
|
17193
|
+
init_merge_helper();
|
|
17194
|
+
async function executeTeamCleanup(deps, args, sessionId, isDirty = checkWorktreeDirty, merge2 = mergeBranch, delBranch = deleteBranch, mergeOnCleanup = true) {
|
|
16485
17195
|
const teamInfo = requireLead(deps, sessionId);
|
|
16486
17196
|
const members = deps.db.query("SELECT name, session_id, status, worktree_dir, worktree_branch, workspace_id FROM team_member WHERE team_id = ?").all(teamInfo.teamId);
|
|
16487
17197
|
const active = members.filter((m) => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error");
|
|
@@ -16513,24 +17223,35 @@ Commit or merge their work first, then call team_cleanup with acknowledge_uncomm
|
|
|
16513
17223
|
}
|
|
16514
17224
|
if (args.force) {
|
|
16515
17225
|
for (const member of active) {
|
|
17226
|
+
if (member.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
17227
|
+
const safeBranch = preservedBranchName(teamInfo.teamName, member.name);
|
|
17228
|
+
const ok = await preserveBranch(member.worktree_branch, safeBranch, deps.directory);
|
|
17229
|
+
if (ok) {
|
|
17230
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamInfo.teamId, member.name]);
|
|
17231
|
+
member.worktree_branch = safeBranch;
|
|
17232
|
+
}
|
|
17233
|
+
}
|
|
16516
17234
|
try {
|
|
16517
17235
|
await deps.client.session.abort({ sessionID: member.session_id });
|
|
16518
17236
|
} catch {}
|
|
16519
17237
|
}
|
|
16520
17238
|
}
|
|
16521
|
-
const
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
-
const
|
|
16526
|
-
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16530
|
-
|
|
17239
|
+
const unmerged = members.filter((m) => m.worktree_branch !== null);
|
|
17240
|
+
const merged = [];
|
|
17241
|
+
const conflicted = [];
|
|
17242
|
+
if (unmerged.length > 0 && mergeOnCleanup) {
|
|
17243
|
+
for (const member of unmerged) {
|
|
17244
|
+
const branch = member.worktree_branch;
|
|
17245
|
+
const result = await merge2(branch, deps.directory);
|
|
17246
|
+
if (result.ok) {
|
|
17247
|
+
await delBranch(branch, deps.directory);
|
|
17248
|
+
deps.db.run("UPDATE team_member SET worktree_branch = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name]);
|
|
17249
|
+
merged.push(`${member.name} (${branch})`);
|
|
17250
|
+
} else {
|
|
17251
|
+
log(`cleanup:merge:conflict member=${member.name} branch=${branch} err=${result.error}`);
|
|
17252
|
+
conflicted.push(`${member.name} (${branch})`);
|
|
17253
|
+
}
|
|
16531
17254
|
}
|
|
16532
|
-
mergeResult = parts.join(`
|
|
16533
|
-
`);
|
|
16534
17255
|
}
|
|
16535
17256
|
for (const member of members) {
|
|
16536
17257
|
if (member.workspace_id) {
|
|
@@ -16549,19 +17270,43 @@ Commit or merge their work first, then call team_cleanup with acknowledge_uncomm
|
|
|
16549
17270
|
deps.db.run("UPDATE team SET status = 'archived', time_updated = ? WHERE id = ?", [Date.now(), teamInfo.teamId]);
|
|
16550
17271
|
deps.registry.unregisterTeam(teamInfo.teamId);
|
|
16551
17272
|
spawnFailures.delete(teamInfo.teamId);
|
|
16552
|
-
|
|
16553
|
-
|
|
16554
|
-
${
|
|
17273
|
+
const parts = [`Team "${teamInfo.teamName}" cleaned up.`];
|
|
17274
|
+
if (merged.length > 0) {
|
|
17275
|
+
parts.push(`Safety-net merged ${merged.length} unmerged branch(es): ${merged.join(", ")}. Review with: git diff`);
|
|
16555
17276
|
}
|
|
16556
|
-
if (
|
|
16557
|
-
|
|
17277
|
+
if (conflicted.length > 0) {
|
|
17278
|
+
parts.push(`Could not auto-merge: ${conflicted.join(", ")}. Merge manually.`);
|
|
17279
|
+
}
|
|
17280
|
+
return parts.join(`
|
|
16558
17281
|
`);
|
|
16559
|
-
|
|
17282
|
+
}
|
|
17283
|
+
|
|
17284
|
+
// src/tools/team-merge.ts
|
|
17285
|
+
init_merge_helper();
|
|
17286
|
+
async function executeTeamMerge(deps, args, sessionId, merge2 = mergeBranch, delBranch = deleteBranch) {
|
|
17287
|
+
const teamInfo = requireLead(deps, sessionId);
|
|
17288
|
+
const member = deps.db.query("SELECT status, worktree_branch FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, args.member);
|
|
17289
|
+
if (!member)
|
|
17290
|
+
throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`);
|
|
17291
|
+
if (member.status !== "shutdown" && member.status !== "error") {
|
|
17292
|
+
throw new Error(`Teammate "${args.member}" is still active (status: ${member.status}). Shut them down first with team_shutdown.`);
|
|
17293
|
+
}
|
|
17294
|
+
if (!member.worktree_branch) {
|
|
17295
|
+
throw new Error(`No branch to merge for "${args.member}". They may not have a worktree, or their work was already merged.`);
|
|
17296
|
+
}
|
|
17297
|
+
const branch = member.worktree_branch;
|
|
17298
|
+
log(`merge:start member=${args.member} branch=${branch}`);
|
|
17299
|
+
const result = await merge2(branch, deps.directory);
|
|
17300
|
+
if (!result.ok) {
|
|
17301
|
+
return `Merge conflict with ${args.member}'s branch (${branch}). Resolve manually:
|
|
17302
|
+
git merge --squash ${branch}
|
|
16560
17303
|
|
|
16561
|
-
|
|
16562
|
-
${mergeCommands}`;
|
|
17304
|
+
Error: ${result.error}`;
|
|
16563
17305
|
}
|
|
16564
|
-
|
|
17306
|
+
await delBranch(branch, deps.directory);
|
|
17307
|
+
deps.db.run("UPDATE team_member SET worktree_branch = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, args.member]);
|
|
17308
|
+
log(`merge:done member=${args.member} branch=${branch}`);
|
|
17309
|
+
return `Merged ${args.member}'s changes into working directory (unstaged). Review with: git diff`;
|
|
16565
17310
|
}
|
|
16566
17311
|
|
|
16567
17312
|
// src/tools/team-tasks-list.ts
|
|
@@ -16823,6 +17568,7 @@ class TokenBucket {
|
|
|
16823
17568
|
}
|
|
16824
17569
|
|
|
16825
17570
|
// src/watchdog.ts
|
|
17571
|
+
init_merge_helper();
|
|
16826
17572
|
class Watchdog {
|
|
16827
17573
|
db;
|
|
16828
17574
|
client;
|
|
@@ -16833,6 +17579,7 @@ class Watchdog {
|
|
|
16833
17579
|
stallThresholdMs;
|
|
16834
17580
|
stallMinSteps;
|
|
16835
17581
|
stallTokenThreshold;
|
|
17582
|
+
cwd;
|
|
16836
17583
|
timer;
|
|
16837
17584
|
constructor(opts) {
|
|
16838
17585
|
this.db = opts.db;
|
|
@@ -16844,6 +17591,7 @@ class Watchdog {
|
|
|
16844
17591
|
this.stallThresholdMs = opts.stallThresholdMs ?? 0;
|
|
16845
17592
|
this.stallMinSteps = opts.stallMinSteps ?? 3;
|
|
16846
17593
|
this.stallTokenThreshold = opts.stallTokenThreshold ?? 500;
|
|
17594
|
+
this.cwd = opts.cwd;
|
|
16847
17595
|
}
|
|
16848
17596
|
static STALE_THRESHOLD_MS = Number(process.env.STALE_WORKTREE_THRESHOLD_MS) || 300000;
|
|
16849
17597
|
async cleanupStaleWorktrees() {
|
|
@@ -16907,13 +17655,21 @@ class Watchdog {
|
|
|
16907
17655
|
if (this.ttlMs === 0)
|
|
16908
17656
|
return;
|
|
16909
17657
|
const cutoff = Date.now() - this.ttlMs;
|
|
16910
|
-
const stale = this.db.query(`SELECT tm.team_id, tm.name, tm.session_id
|
|
17658
|
+
const stale = this.db.query(`SELECT tm.team_id, tm.name, tm.session_id, tm.worktree_branch, t.name as team_name
|
|
16911
17659
|
FROM team_member tm
|
|
16912
17660
|
JOIN team t ON tm.team_id = t.id
|
|
16913
17661
|
WHERE t.status = 'active'
|
|
16914
17662
|
AND tm.status = 'busy'
|
|
16915
17663
|
AND tm.time_updated < ?`).all(cutoff);
|
|
16916
17664
|
for (const member of stale) {
|
|
17665
|
+
if (this.cwd && member.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
17666
|
+
const safeBranch = preservedBranchName(member.team_name, member.name);
|
|
17667
|
+
const ok = await preserveBranch(member.worktree_branch, safeBranch, this.cwd);
|
|
17668
|
+
if (ok) {
|
|
17669
|
+
this.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, member.team_id, member.name]);
|
|
17670
|
+
log(`watchdog:branch:preserved src=${member.worktree_branch} target=${safeBranch}`);
|
|
17671
|
+
}
|
|
17672
|
+
}
|
|
16917
17673
|
this.db.run("UPDATE team_member SET status = 'error', execution_status = 'timed_out', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), member.team_id, member.name]);
|
|
16918
17674
|
try {
|
|
16919
17675
|
await this.client.session.abort({ sessionID: member.session_id });
|
|
@@ -16964,7 +17720,7 @@ var plugin = async (input) => {
|
|
|
16964
17720
|
const deps = { db, registry: registry2, tracker, client: client3, directory: input.directory };
|
|
16965
17721
|
if (!isWorktreeInstance(input.directory)) {
|
|
16966
17722
|
log("init:recovery:start (main instance)");
|
|
16967
|
-
const recovery = await recoverStaleMembers(db, client3);
|
|
17723
|
+
const recovery = await recoverStaleMembers(db, client3, input.directory);
|
|
16968
17724
|
if (recovery.interrupted > 0) {
|
|
16969
17725
|
log(`init:recovery:interrupted=${recovery.interrupted}`);
|
|
16970
17726
|
const members = db.query("SELECT tm.team_id, tm.name, tm.session_id FROM team_member tm JOIN team t ON tm.team_id = t.id WHERE t.status = 'active'").all();
|
|
@@ -16978,7 +17734,15 @@ var plugin = async (input) => {
|
|
|
16978
17734
|
recoverOrphanedWorktrees(db, client3).catch((err) => {
|
|
16979
17735
|
log(`init:recover-worktrees:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
16980
17736
|
});
|
|
17737
|
+
recoverOrphanedBranches(db, input.directory).catch((err) => {
|
|
17738
|
+
log(`init:recover-branches:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17739
|
+
});
|
|
16981
17740
|
log("init:recovery:done");
|
|
17741
|
+
if (config2.dashboardPort !== 0) {
|
|
17742
|
+
startDashboard(db, config2.dashboardPort).catch((err) => {
|
|
17743
|
+
log(`init:dashboard:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17744
|
+
});
|
|
17745
|
+
}
|
|
16982
17746
|
} else {
|
|
16983
17747
|
log(`init:skip-recovery (worktree instance: ${input.directory})`);
|
|
16984
17748
|
}
|
|
@@ -16996,7 +17760,8 @@ var plugin = async (input) => {
|
|
|
16996
17760
|
progressTracker,
|
|
16997
17761
|
stallThresholdMs: config2.stallThresholdMs,
|
|
16998
17762
|
stallMinSteps: config2.stallMinSteps,
|
|
16999
|
-
stallTokenThreshold: config2.stallTokenThreshold
|
|
17763
|
+
stallTokenThreshold: config2.stallTokenThreshold,
|
|
17764
|
+
cwd: input.directory
|
|
17000
17765
|
});
|
|
17001
17766
|
watchdog.start();
|
|
17002
17767
|
return {
|
|
@@ -17031,12 +17796,22 @@ var plugin = async (input) => {
|
|
|
17031
17796
|
});
|
|
17032
17797
|
} catch {}
|
|
17033
17798
|
} else if (transition.to === "busy_while_shutdown") {
|
|
17034
|
-
const
|
|
17035
|
-
if (
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17799
|
+
const member = deps.db.query("SELECT worktree_branch, name, team_id FROM team_member WHERE session_id = ?").get(sessionID);
|
|
17800
|
+
if (member?.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
17801
|
+
const teamRow = deps.db.query("SELECT name FROM team WHERE id = ?").get(member.team_id);
|
|
17802
|
+
if (teamRow) {
|
|
17803
|
+
const { preserveBranch: preserve, preservedBranchName: branchName } = await Promise.resolve().then(() => (init_merge_helper(), exports_merge_helper));
|
|
17804
|
+
const safeBranch = branchName(teamRow.name, member.name);
|
|
17805
|
+
const ok = await preserve(member.worktree_branch, safeBranch, deps.directory);
|
|
17806
|
+
if (ok) {
|
|
17807
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, member.team_id, member.name]);
|
|
17808
|
+
log(`busy_while_shutdown:branch:preserved src=${member.worktree_branch} target=${safeBranch}`);
|
|
17809
|
+
}
|
|
17810
|
+
}
|
|
17039
17811
|
}
|
|
17812
|
+
try {
|
|
17813
|
+
await client3.session.abort({ sessionID });
|
|
17814
|
+
} catch {}
|
|
17040
17815
|
}
|
|
17041
17816
|
await notifyWorkingProgress(client3, db, transition.teamId);
|
|
17042
17817
|
}
|
|
@@ -17129,7 +17904,7 @@ var plugin = async (input) => {
|
|
|
17129
17904
|
}
|
|
17130
17905
|
}),
|
|
17131
17906
|
team_spawn: tool({
|
|
17132
|
-
description: "Spawn a new teammate that works in parallel. The teammate starts immediately with the given prompt.
|
|
17907
|
+
description: "Spawn a new teammate that works in parallel. The teammate starts immediately with the given prompt. Each teammate gets their own git worktree for file isolation. Teammates work asynchronously and will message you when done. Do not poll for their status.",
|
|
17133
17908
|
args: {
|
|
17134
17909
|
name: tool.schema.string().describe("Teammate name (lowercase alphanumeric with hyphens)"),
|
|
17135
17910
|
agent: tool.schema.string().default("build").describe("Agent type (e.g. 'build', 'plan', 'explore')"),
|
|
@@ -17233,7 +18008,7 @@ var plugin = async (input) => {
|
|
|
17233
18008
|
}
|
|
17234
18009
|
}),
|
|
17235
18010
|
team_shutdown: tool({
|
|
17236
|
-
description: "Request a teammate to shut down. The teammate finishes current work then stops.
|
|
18011
|
+
description: "Request a teammate to shut down. The teammate finishes current work then stops. Pass force: true to abort immediately without waiting.",
|
|
17237
18012
|
args: {
|
|
17238
18013
|
member: tool.schema.string().describe("Teammate name to shut down"),
|
|
17239
18014
|
force: tool.schema.boolean().default(false).describe("Force immediate abort without waiting for current work to finish")
|
|
@@ -17255,14 +18030,26 @@ var plugin = async (input) => {
|
|
|
17255
18030
|
acknowledge_uncommitted: tool.schema.boolean().default(false)
|
|
17256
18031
|
},
|
|
17257
18032
|
async execute(args, ctx) {
|
|
17258
|
-
const result = await executeTeamCleanup(deps, args, ctx.sessionID, undefined, undefined, config2.mergeOnCleanup);
|
|
18033
|
+
const result = await executeTeamCleanup(deps, args, ctx.sessionID, undefined, undefined, undefined, config2.mergeOnCleanup);
|
|
17259
18034
|
const blocked = result.includes("uncommitted");
|
|
17260
18035
|
ctx.metadata({ title: blocked ? "Cleanup blocked \u2014 uncommitted changes" : "Team cleaned up" });
|
|
17261
18036
|
return result;
|
|
17262
18037
|
}
|
|
17263
18038
|
}),
|
|
18039
|
+
team_merge: tool({
|
|
18040
|
+
description: "Merge a shutdown teammate's branch into the working directory as unstaged changes. Use this after team_shutdown to review and integrate a teammate's work. The teammate must be shut down first.",
|
|
18041
|
+
args: {
|
|
18042
|
+
member: tool.schema.string().describe("Teammate name whose branch to merge")
|
|
18043
|
+
},
|
|
18044
|
+
async execute(args, ctx) {
|
|
18045
|
+
const result = await executeTeamMerge(deps, args, ctx.sessionID);
|
|
18046
|
+
const conflict = result.includes("conflict");
|
|
18047
|
+
ctx.metadata({ title: conflict ? `Merge conflict: ${args.member}` : `Merged ${args.member}` });
|
|
18048
|
+
return result;
|
|
18049
|
+
}
|
|
18050
|
+
}),
|
|
17264
18051
|
team_status: tool({
|
|
17265
|
-
description: "View team members with their current status, agent type, and session IDs.
|
|
18052
|
+
description: "View team members with their current status, agent type, and session IDs. Use this to check who is working, idle, or shut down. Includes a task summary.",
|
|
17266
18053
|
args: {},
|
|
17267
18054
|
async execute(_args, ctx) {
|
|
17268
18055
|
const result = await executeTeamStatus(deps, ctx.sessionID);
|
|
@@ -17274,7 +18061,7 @@ var plugin = async (input) => {
|
|
|
17274
18061
|
}
|
|
17275
18062
|
}),
|
|
17276
18063
|
team_view: tool({
|
|
17277
|
-
description: "Navigate the TUI to a teammate's session so you can see what they are doing.
|
|
18064
|
+
description: "Navigate the TUI to a teammate's session so you can see what they are doing. Use the session picker (ctrl+p) to return to the lead session.",
|
|
17278
18065
|
args: {
|
|
17279
18066
|
member: tool.schema.string().describe("Teammate name to view")
|
|
17280
18067
|
},
|