@hueyexe/opencode-ensemble 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -10
- package/dist/index.js +335 -96
- package/package.json +1 -1
- package/src/index.ts +43 -8
- package/src/recovery.ts +69 -6
- package/src/result-parser.ts +40 -0
- package/src/system-prompt.ts +13 -1
- 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-results.ts +7 -2
- package/src/tools/team-shutdown.ts +57 -17
- package/src/tools/team-spawn.ts +11 -1
- 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 {
|
|
@@ -15741,6 +15889,33 @@ async function notifyWorkingProgress(client3, db, teamId) {
|
|
|
15741
15889
|
} catch {}
|
|
15742
15890
|
}
|
|
15743
15891
|
|
|
15892
|
+
// src/result-parser.ts
|
|
15893
|
+
function parseTaskResult(content) {
|
|
15894
|
+
const match = content.match(/<task-result>([\s\S]*?)<\/task-result>/);
|
|
15895
|
+
if (!match)
|
|
15896
|
+
return null;
|
|
15897
|
+
const inner = match[1];
|
|
15898
|
+
const status = inner.match(/<status>([\s\S]*?)<\/status>/)?.[1]?.trim();
|
|
15899
|
+
const summary = inner.match(/<summary>([\s\S]*?)<\/summary>/)?.[1]?.trim();
|
|
15900
|
+
const details = inner.match(/<details>([\s\S]*?)<\/details>/)?.[1]?.trim();
|
|
15901
|
+
const branch = inner.match(/<branch>([\s\S]*?)<\/branch>/)?.[1]?.trim();
|
|
15902
|
+
if (!status || !summary || !details)
|
|
15903
|
+
return null;
|
|
15904
|
+
return { status, summary, details, branch: branch || undefined };
|
|
15905
|
+
}
|
|
15906
|
+
function formatTaskResult(from, result) {
|
|
15907
|
+
const lines = [
|
|
15908
|
+
`[Result from ${from}]:`,
|
|
15909
|
+
` Status: ${result.status}`,
|
|
15910
|
+
` Summary: ${result.summary}`,
|
|
15911
|
+
` Details: ${result.details}`
|
|
15912
|
+
];
|
|
15913
|
+
if (result.branch)
|
|
15914
|
+
lines.push(` Branch: ${result.branch}`);
|
|
15915
|
+
return lines.join(`
|
|
15916
|
+
`);
|
|
15917
|
+
}
|
|
15918
|
+
|
|
15744
15919
|
// src/system-prompt.ts
|
|
15745
15920
|
function truncate(s, maxLen) {
|
|
15746
15921
|
return s.length > maxLen ? `${s.slice(0, maxLen)}...` : s;
|
|
@@ -15791,7 +15966,11 @@ function buildLeadSystemPrompt(db, teamId) {
|
|
|
15791
15966
|
lines.push("", "--- Team Messages ---");
|
|
15792
15967
|
const MAX_MSG = 500;
|
|
15793
15968
|
for (const msg of pendingMessages) {
|
|
15794
|
-
|
|
15969
|
+
const parsed = parseTaskResult(msg.content);
|
|
15970
|
+
if (parsed) {
|
|
15971
|
+
const truncatedResult = { ...parsed, details: truncate(parsed.details, 500) };
|
|
15972
|
+
lines.push(formatTaskResult(msg.from_name, truncatedResult));
|
|
15973
|
+
} else if (msg.content.length > MAX_MSG) {
|
|
15795
15974
|
lines.push(`[From ${msg.from_name}]: ${msg.content.slice(0, MAX_MSG)}... (use team_results to read full message)`);
|
|
15796
15975
|
} else {
|
|
15797
15976
|
lines.push(`[From ${msg.from_name}]: ${msg.content}`);
|
|
@@ -15800,7 +15979,7 @@ function buildLeadSystemPrompt(db, teamId) {
|
|
|
15800
15979
|
}
|
|
15801
15980
|
lines.push("--- End Messages ---");
|
|
15802
15981
|
}
|
|
15803
|
-
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.");
|
|
15804
15983
|
return lines.join(`
|
|
15805
15984
|
`);
|
|
15806
15985
|
}
|
|
@@ -15859,18 +16038,6 @@ function buildTeamCompactionContext(db, teamId, role, memberName) {
|
|
|
15859
16038
|
return lines.join(`
|
|
15860
16039
|
`);
|
|
15861
16040
|
}
|
|
15862
|
-
|
|
15863
|
-
// src/log.ts
|
|
15864
|
-
var _client = null;
|
|
15865
|
-
function initLog(client3) {
|
|
15866
|
-
_client = client3;
|
|
15867
|
-
}
|
|
15868
|
-
function log(msg) {
|
|
15869
|
-
if (!_client)
|
|
15870
|
-
return;
|
|
15871
|
-
_client.app.log({ service: "ensemble", level: "info", message: msg }).catch(() => {});
|
|
15872
|
-
}
|
|
15873
|
-
|
|
15874
16041
|
// src/types.ts
|
|
15875
16042
|
function findTeamBySession(db, registry2, sessionId) {
|
|
15876
16043
|
const entry = registry2.getBySession(sessionId);
|
|
@@ -16184,6 +16351,10 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
16184
16351
|
`You are "${args.name}", a teammate in team "${teamInfo.teamName}".`,
|
|
16185
16352
|
`Your agent type is "${args.agent}".`
|
|
16186
16353
|
];
|
|
16354
|
+
const otherMembers = deps.db.query("SELECT name FROM team_member WHERE team_id = ? AND name != ? AND status NOT IN ('shutdown', 'error')").all(teamInfo.teamId, args.name);
|
|
16355
|
+
if (otherMembers.length > 0) {
|
|
16356
|
+
context.push(`Other teammates: ${otherMembers.map((m) => m.name).join(", ")}`);
|
|
16357
|
+
}
|
|
16187
16358
|
if (worktreeBranch && worktreeDir && !workspaceId) {
|
|
16188
16359
|
context.push(`You are working on branch "${worktreeBranch}" in your own worktree at: ${worktreeDir}`, `Your changes are isolated from other teammates.`, `IMPORTANT: All file operations and shell commands MUST target your worktree directory.`, `Before running shell commands, cd to: ${worktreeDir}`);
|
|
16189
16360
|
} else if (worktreeBranch && worktreeDir) {
|
|
@@ -16216,7 +16387,7 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
16216
16387
|
}
|
|
16217
16388
|
context.push("</task-result>");
|
|
16218
16389
|
const lastStep = !isReadOnly && worktreeBranch ? "4" : !isReadOnly ? "3" : "2";
|
|
16219
|
-
context.push(`${lastStep}. STOP. Do not send follow-up confirmations, status updates, or 'standing by' messages.`, "", "If you are blocked,
|
|
16390
|
+
context.push(`${lastStep}. STOP. Do not send follow-up confirmations, status updates, or 'standing by' messages.`, "", "If you are blocked:", "- Send ONE message to the lead via team_message describing the specific blocker.", "- Do NOT attempt workarounds or make assumptions. Wait for the lead's response.", "", "Your plain text output is NOT visible to the team. You MUST use team_message to communicate.");
|
|
16220
16391
|
context.push("", "Your task:", args.prompt);
|
|
16221
16392
|
if (args.claim_task) {
|
|
16222
16393
|
context.push("", `You have been assigned task ${args.claim_task}. Mark it complete when done.`);
|
|
@@ -16355,7 +16526,8 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
16355
16526
|
}
|
|
16356
16527
|
|
|
16357
16528
|
// src/tools/team-shutdown.ts
|
|
16358
|
-
|
|
16529
|
+
init_merge_helper();
|
|
16530
|
+
async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktreeDirty, preserve = preserveBranch) {
|
|
16359
16531
|
const teamInfo = requireLead(deps, sessionId);
|
|
16360
16532
|
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);
|
|
16361
16533
|
if (!member)
|
|
@@ -16364,8 +16536,8 @@ async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktre
|
|
|
16364
16536
|
throw new Error(`Teammate "${args.member}" is already shut down`);
|
|
16365
16537
|
const force = args.force ?? false;
|
|
16366
16538
|
if (member.status === "shutdown_requested") {
|
|
16367
|
-
await
|
|
16368
|
-
const branchInfo =
|
|
16539
|
+
await preserveAndAbort(deps, teamInfo.teamId, teamInfo.teamName, args.member, member.session_id, member.worktree_branch, preserve);
|
|
16540
|
+
const branchInfo = getBranchInfo(deps, teamInfo.teamId, args.member);
|
|
16369
16541
|
const dirtyWarning = await getDirtyWarning(member.worktree_dir, args.member, isDirty);
|
|
16370
16542
|
return `Force shut down "${args.member}".${branchInfo}${dirtyWarning}`;
|
|
16371
16543
|
}
|
|
@@ -16376,29 +16548,51 @@ async function executeTeamShutdown(deps, args, sessionId, isDirty = checkWorktre
|
|
|
16376
16548
|
isIdle = !sessionStatus || sessionStatus.type === "idle";
|
|
16377
16549
|
} catch {}
|
|
16378
16550
|
if (isIdle || force) {
|
|
16379
|
-
await
|
|
16380
|
-
const branchInfo =
|
|
16551
|
+
await preserveAndAbort(deps, teamInfo.teamId, teamInfo.teamName, args.member, member.session_id, member.worktree_branch, preserve);
|
|
16552
|
+
const branchInfo = getBranchInfo(deps, teamInfo.teamId, args.member);
|
|
16381
16553
|
const dirtyWarning = await getDirtyWarning(member.worktree_dir, args.member, isDirty);
|
|
16382
16554
|
return `Teammate "${args.member}" has been shut down.${branchInfo}${dirtyWarning}`;
|
|
16383
16555
|
}
|
|
16556
|
+
if (member.worktree_branch) {
|
|
16557
|
+
const safeBranch = preservedBranchName(teamInfo.teamName, args.member);
|
|
16558
|
+
const ok = await preserve(member.worktree_branch, safeBranch, deps.directory);
|
|
16559
|
+
if (ok) {
|
|
16560
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamInfo.teamId, args.member]);
|
|
16561
|
+
log(`shutdown:branch:preserved-graceful src=${member.worktree_branch} target=${safeBranch}`);
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16384
16564
|
try {
|
|
16385
|
-
|
|
16565
|
+
deps.client.session.promptAsync({
|
|
16386
16566
|
sessionID: member.session_id,
|
|
16387
16567
|
parts: [{
|
|
16388
16568
|
type: "text",
|
|
16389
16569
|
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.`
|
|
16390
16570
|
}]
|
|
16391
|
-
});
|
|
16571
|
+
}).catch(() => {});
|
|
16392
16572
|
} catch {}
|
|
16393
16573
|
deps.db.run("UPDATE team_member SET status = 'shutdown_requested', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), teamInfo.teamId, args.member]);
|
|
16394
16574
|
return `Shutdown requested for ${args.member}. They will finish current work and shut down. Call team_shutdown with force: true to abort immediately.`;
|
|
16395
16575
|
}
|
|
16396
|
-
async function
|
|
16576
|
+
async function preserveAndAbort(deps, teamId, teamName, memberName, sessionId, worktreeBranch, preserve) {
|
|
16577
|
+
if (worktreeBranch && !worktreeBranch.startsWith("ensemble/preserved/")) {
|
|
16578
|
+
const safeBranch = preservedBranchName(teamName, memberName);
|
|
16579
|
+
const ok = await preserve(worktreeBranch, safeBranch, deps.directory);
|
|
16580
|
+
if (ok) {
|
|
16581
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamId, memberName]);
|
|
16582
|
+
log(`shutdown:branch:preserved src=${worktreeBranch} target=${safeBranch}`);
|
|
16583
|
+
} else {
|
|
16584
|
+
log(`shutdown:branch:preserve-failed src=${worktreeBranch} target=${safeBranch}`);
|
|
16585
|
+
}
|
|
16586
|
+
}
|
|
16397
16587
|
try {
|
|
16398
16588
|
await deps.client.session.abort({ sessionID: sessionId });
|
|
16399
16589
|
} catch {}
|
|
16400
16590
|
deps.db.run("UPDATE team_member SET status = 'shutdown', execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), teamId, memberName]);
|
|
16401
16591
|
}
|
|
16592
|
+
function getBranchInfo(deps, teamId, memberName) {
|
|
16593
|
+
const row = deps.db.query("SELECT worktree_branch FROM team_member WHERE team_id = ? AND name = ?").get(teamId, memberName);
|
|
16594
|
+
return row?.worktree_branch ? ` Changes preserved on branch: ${row.worktree_branch}. Use team_merge to merge.` : "";
|
|
16595
|
+
}
|
|
16402
16596
|
async function getDirtyWarning(worktreeDir, memberName, isDirty) {
|
|
16403
16597
|
if (!worktreeDir)
|
|
16404
16598
|
return "";
|
|
@@ -16413,40 +16607,8 @@ Warning: ${memberName} has uncommitted changes in their worktree. Commit or merg
|
|
|
16413
16607
|
}
|
|
16414
16608
|
|
|
16415
16609
|
// src/tools/team-cleanup.ts
|
|
16416
|
-
|
|
16417
|
-
|
|
16418
|
-
const conflicted = [];
|
|
16419
|
-
const stash = Bun.spawn(["git", "stash", "--include-untracked"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16420
|
-
const stashOut = await new Response(stash.stdout).text();
|
|
16421
|
-
const stashExit = await stash.exited;
|
|
16422
|
-
const didStash = stashExit === 0 && !stashOut.includes("No local changes");
|
|
16423
|
-
for (const branch of branches) {
|
|
16424
|
-
const merge2 = Bun.spawn(["git", "merge", "--squash", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16425
|
-
const stderrPromise = new Response(merge2.stderr).text();
|
|
16426
|
-
const exit = await merge2.exited;
|
|
16427
|
-
if (exit !== 0) {
|
|
16428
|
-
const stderr = await stderrPromise;
|
|
16429
|
-
log(`cleanup:merge:conflict branch=${branch} err=${stderr.trim()}`);
|
|
16430
|
-
const abort = Bun.spawn(["git", "merge", "--abort"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16431
|
-
await abort.exited;
|
|
16432
|
-
conflicted.push(branch);
|
|
16433
|
-
} else {
|
|
16434
|
-
const del = Bun.spawn(["git", "branch", "-D", branch], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16435
|
-
await del.exited;
|
|
16436
|
-
merged.push(branch);
|
|
16437
|
-
}
|
|
16438
|
-
}
|
|
16439
|
-
if (merged.length > 0) {
|
|
16440
|
-
const reset = Bun.spawn(["git", "reset", "HEAD"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16441
|
-
await reset.exited;
|
|
16442
|
-
}
|
|
16443
|
-
if (didStash) {
|
|
16444
|
-
const pop = Bun.spawn(["git", "stash", "pop", "--index"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
16445
|
-
await pop.exited;
|
|
16446
|
-
}
|
|
16447
|
-
return { merged, conflicted };
|
|
16448
|
-
}
|
|
16449
|
-
async function executeTeamCleanup(deps, args, sessionId, isDirty = checkWorktreeDirty, mergeAll = mergeAllBranches, mergeOnCleanup = true) {
|
|
16610
|
+
init_merge_helper();
|
|
16611
|
+
async function executeTeamCleanup(deps, args, sessionId, isDirty = checkWorktreeDirty, merge2 = mergeBranch, delBranch = deleteBranch, mergeOnCleanup = true) {
|
|
16450
16612
|
const teamInfo = requireLead(deps, sessionId);
|
|
16451
16613
|
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);
|
|
16452
16614
|
const active = members.filter((m) => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error");
|
|
@@ -16478,24 +16640,35 @@ Commit or merge their work first, then call team_cleanup with acknowledge_uncomm
|
|
|
16478
16640
|
}
|
|
16479
16641
|
if (args.force) {
|
|
16480
16642
|
for (const member of active) {
|
|
16643
|
+
if (member.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
16644
|
+
const safeBranch = preservedBranchName(teamInfo.teamName, member.name);
|
|
16645
|
+
const ok = await preserveBranch(member.worktree_branch, safeBranch, deps.directory);
|
|
16646
|
+
if (ok) {
|
|
16647
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, teamInfo.teamId, member.name]);
|
|
16648
|
+
member.worktree_branch = safeBranch;
|
|
16649
|
+
}
|
|
16650
|
+
}
|
|
16481
16651
|
try {
|
|
16482
16652
|
await deps.client.session.abort({ sessionID: member.session_id });
|
|
16483
16653
|
} catch {}
|
|
16484
16654
|
}
|
|
16485
16655
|
}
|
|
16486
|
-
const
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
const
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16656
|
+
const unmerged = members.filter((m) => m.worktree_branch !== null);
|
|
16657
|
+
const merged = [];
|
|
16658
|
+
const conflicted = [];
|
|
16659
|
+
if (unmerged.length > 0 && mergeOnCleanup) {
|
|
16660
|
+
for (const member of unmerged) {
|
|
16661
|
+
const branch = member.worktree_branch;
|
|
16662
|
+
const result = await merge2(branch, deps.directory);
|
|
16663
|
+
if (result.ok) {
|
|
16664
|
+
await delBranch(branch, deps.directory);
|
|
16665
|
+
deps.db.run("UPDATE team_member SET worktree_branch = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name]);
|
|
16666
|
+
merged.push(`${member.name} (${branch})`);
|
|
16667
|
+
} else {
|
|
16668
|
+
log(`cleanup:merge:conflict member=${member.name} branch=${branch} err=${result.error}`);
|
|
16669
|
+
conflicted.push(`${member.name} (${branch})`);
|
|
16670
|
+
}
|
|
16496
16671
|
}
|
|
16497
|
-
mergeResult = parts.join(`
|
|
16498
|
-
`);
|
|
16499
16672
|
}
|
|
16500
16673
|
for (const member of members) {
|
|
16501
16674
|
if (member.workspace_id) {
|
|
@@ -16514,19 +16687,43 @@ Commit or merge their work first, then call team_cleanup with acknowledge_uncomm
|
|
|
16514
16687
|
deps.db.run("UPDATE team SET status = 'archived', time_updated = ? WHERE id = ?", [Date.now(), teamInfo.teamId]);
|
|
16515
16688
|
deps.registry.unregisterTeam(teamInfo.teamId);
|
|
16516
16689
|
spawnFailures.delete(teamInfo.teamId);
|
|
16517
|
-
|
|
16518
|
-
|
|
16519
|
-
${
|
|
16690
|
+
const parts = [`Team "${teamInfo.teamName}" cleaned up.`];
|
|
16691
|
+
if (merged.length > 0) {
|
|
16692
|
+
parts.push(`Safety-net merged ${merged.length} unmerged branch(es): ${merged.join(", ")}. Review with: git diff`);
|
|
16520
16693
|
}
|
|
16521
|
-
if (
|
|
16522
|
-
|
|
16694
|
+
if (conflicted.length > 0) {
|
|
16695
|
+
parts.push(`Could not auto-merge: ${conflicted.join(", ")}. Merge manually.`);
|
|
16696
|
+
}
|
|
16697
|
+
return parts.join(`
|
|
16523
16698
|
`);
|
|
16524
|
-
|
|
16699
|
+
}
|
|
16700
|
+
|
|
16701
|
+
// src/tools/team-merge.ts
|
|
16702
|
+
init_merge_helper();
|
|
16703
|
+
async function executeTeamMerge(deps, args, sessionId, merge2 = mergeBranch, delBranch = deleteBranch) {
|
|
16704
|
+
const teamInfo = requireLead(deps, sessionId);
|
|
16705
|
+
const member = deps.db.query("SELECT status, worktree_branch FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, args.member);
|
|
16706
|
+
if (!member)
|
|
16707
|
+
throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`);
|
|
16708
|
+
if (member.status !== "shutdown" && member.status !== "error") {
|
|
16709
|
+
throw new Error(`Teammate "${args.member}" is still active (status: ${member.status}). Shut them down first with team_shutdown.`);
|
|
16710
|
+
}
|
|
16711
|
+
if (!member.worktree_branch) {
|
|
16712
|
+
throw new Error(`No branch to merge for "${args.member}". They may not have a worktree, or their work was already merged.`);
|
|
16713
|
+
}
|
|
16714
|
+
const branch = member.worktree_branch;
|
|
16715
|
+
log(`merge:start member=${args.member} branch=${branch}`);
|
|
16716
|
+
const result = await merge2(branch, deps.directory);
|
|
16717
|
+
if (!result.ok) {
|
|
16718
|
+
return `Merge conflict with ${args.member}'s branch (${branch}). Resolve manually:
|
|
16719
|
+
git merge --squash ${branch}
|
|
16525
16720
|
|
|
16526
|
-
|
|
16527
|
-
${mergeCommands}`;
|
|
16721
|
+
Error: ${result.error}`;
|
|
16528
16722
|
}
|
|
16529
|
-
|
|
16723
|
+
await delBranch(branch, deps.directory);
|
|
16724
|
+
deps.db.run("UPDATE team_member SET worktree_branch = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, args.member]);
|
|
16725
|
+
log(`merge:done member=${args.member} branch=${branch}`);
|
|
16726
|
+
return `Merged ${args.member}'s changes into working directory (unstaged). Review with: git diff`;
|
|
16530
16727
|
}
|
|
16531
16728
|
|
|
16532
16729
|
// src/tools/team-tasks-list.ts
|
|
@@ -16622,9 +16819,14 @@ async function executeTeamResults(deps, args, sessionId) {
|
|
|
16622
16819
|
const ids = rows.map((r) => r.id);
|
|
16623
16820
|
const placeholders = ids.map(() => "?").join(", ");
|
|
16624
16821
|
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids);
|
|
16625
|
-
return rows.map((r) =>
|
|
16626
|
-
|
|
16627
|
-
|
|
16822
|
+
return rows.map((r) => {
|
|
16823
|
+
const parsed = parseTaskResult(r.content);
|
|
16824
|
+
if (parsed)
|
|
16825
|
+
return formatTaskResult(r.from_name, parsed);
|
|
16826
|
+
return `[Message from ${r.from_name}]:
|
|
16827
|
+
${r.content}`;
|
|
16828
|
+
}).join(`
|
|
16829
|
+
|
|
16628
16830
|
`);
|
|
16629
16831
|
}
|
|
16630
16832
|
|
|
@@ -16783,6 +16985,7 @@ class TokenBucket {
|
|
|
16783
16985
|
}
|
|
16784
16986
|
|
|
16785
16987
|
// src/watchdog.ts
|
|
16988
|
+
init_merge_helper();
|
|
16786
16989
|
class Watchdog {
|
|
16787
16990
|
db;
|
|
16788
16991
|
client;
|
|
@@ -16793,6 +16996,7 @@ class Watchdog {
|
|
|
16793
16996
|
stallThresholdMs;
|
|
16794
16997
|
stallMinSteps;
|
|
16795
16998
|
stallTokenThreshold;
|
|
16999
|
+
cwd;
|
|
16796
17000
|
timer;
|
|
16797
17001
|
constructor(opts) {
|
|
16798
17002
|
this.db = opts.db;
|
|
@@ -16804,6 +17008,7 @@ class Watchdog {
|
|
|
16804
17008
|
this.stallThresholdMs = opts.stallThresholdMs ?? 0;
|
|
16805
17009
|
this.stallMinSteps = opts.stallMinSteps ?? 3;
|
|
16806
17010
|
this.stallTokenThreshold = opts.stallTokenThreshold ?? 500;
|
|
17011
|
+
this.cwd = opts.cwd;
|
|
16807
17012
|
}
|
|
16808
17013
|
static STALE_THRESHOLD_MS = Number(process.env.STALE_WORKTREE_THRESHOLD_MS) || 300000;
|
|
16809
17014
|
async cleanupStaleWorktrees() {
|
|
@@ -16867,13 +17072,21 @@ class Watchdog {
|
|
|
16867
17072
|
if (this.ttlMs === 0)
|
|
16868
17073
|
return;
|
|
16869
17074
|
const cutoff = Date.now() - this.ttlMs;
|
|
16870
|
-
const stale = this.db.query(`SELECT tm.team_id, tm.name, tm.session_id
|
|
17075
|
+
const stale = this.db.query(`SELECT tm.team_id, tm.name, tm.session_id, tm.worktree_branch, t.name as team_name
|
|
16871
17076
|
FROM team_member tm
|
|
16872
17077
|
JOIN team t ON tm.team_id = t.id
|
|
16873
17078
|
WHERE t.status = 'active'
|
|
16874
17079
|
AND tm.status = 'busy'
|
|
16875
17080
|
AND tm.time_updated < ?`).all(cutoff);
|
|
16876
17081
|
for (const member of stale) {
|
|
17082
|
+
if (this.cwd && member.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
17083
|
+
const safeBranch = preservedBranchName(member.team_name, member.name);
|
|
17084
|
+
const ok = await preserveBranch(member.worktree_branch, safeBranch, this.cwd);
|
|
17085
|
+
if (ok) {
|
|
17086
|
+
this.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, member.team_id, member.name]);
|
|
17087
|
+
log(`watchdog:branch:preserved src=${member.worktree_branch} target=${safeBranch}`);
|
|
17088
|
+
}
|
|
17089
|
+
}
|
|
16877
17090
|
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]);
|
|
16878
17091
|
try {
|
|
16879
17092
|
await this.client.session.abort({ sessionID: member.session_id });
|
|
@@ -16924,7 +17137,7 @@ var plugin = async (input) => {
|
|
|
16924
17137
|
const deps = { db, registry: registry2, tracker, client: client3, directory: input.directory };
|
|
16925
17138
|
if (!isWorktreeInstance(input.directory)) {
|
|
16926
17139
|
log("init:recovery:start (main instance)");
|
|
16927
|
-
const recovery = await recoverStaleMembers(db, client3);
|
|
17140
|
+
const recovery = await recoverStaleMembers(db, client3, input.directory);
|
|
16928
17141
|
if (recovery.interrupted > 0) {
|
|
16929
17142
|
log(`init:recovery:interrupted=${recovery.interrupted}`);
|
|
16930
17143
|
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();
|
|
@@ -16938,6 +17151,9 @@ var plugin = async (input) => {
|
|
|
16938
17151
|
recoverOrphanedWorktrees(db, client3).catch((err) => {
|
|
16939
17152
|
log(`init:recover-worktrees:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
16940
17153
|
});
|
|
17154
|
+
recoverOrphanedBranches(db, input.directory).catch((err) => {
|
|
17155
|
+
log(`init:recover-branches:failed err=${err instanceof Error ? err.message : String(err)}`);
|
|
17156
|
+
});
|
|
16941
17157
|
log("init:recovery:done");
|
|
16942
17158
|
} else {
|
|
16943
17159
|
log(`init:skip-recovery (worktree instance: ${input.directory})`);
|
|
@@ -16956,7 +17172,8 @@ var plugin = async (input) => {
|
|
|
16956
17172
|
progressTracker,
|
|
16957
17173
|
stallThresholdMs: config2.stallThresholdMs,
|
|
16958
17174
|
stallMinSteps: config2.stallMinSteps,
|
|
16959
|
-
stallTokenThreshold: config2.stallTokenThreshold
|
|
17175
|
+
stallTokenThreshold: config2.stallTokenThreshold,
|
|
17176
|
+
cwd: input.directory
|
|
16960
17177
|
});
|
|
16961
17178
|
watchdog.start();
|
|
16962
17179
|
return {
|
|
@@ -16991,12 +17208,22 @@ var plugin = async (input) => {
|
|
|
16991
17208
|
});
|
|
16992
17209
|
} catch {}
|
|
16993
17210
|
} else if (transition.to === "busy_while_shutdown") {
|
|
16994
|
-
const
|
|
16995
|
-
if (
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
17211
|
+
const member = deps.db.query("SELECT worktree_branch, name, team_id FROM team_member WHERE session_id = ?").get(sessionID);
|
|
17212
|
+
if (member?.worktree_branch && !member.worktree_branch.startsWith("ensemble/preserved/")) {
|
|
17213
|
+
const teamRow = deps.db.query("SELECT name FROM team WHERE id = ?").get(member.team_id);
|
|
17214
|
+
if (teamRow) {
|
|
17215
|
+
const { preserveBranch: preserve, preservedBranchName: branchName } = await Promise.resolve().then(() => (init_merge_helper(), exports_merge_helper));
|
|
17216
|
+
const safeBranch = branchName(teamRow.name, member.name);
|
|
17217
|
+
const ok = await preserve(member.worktree_branch, safeBranch, deps.directory);
|
|
17218
|
+
if (ok) {
|
|
17219
|
+
deps.db.run("UPDATE team_member SET worktree_branch = ? WHERE team_id = ? AND name = ?", [safeBranch, member.team_id, member.name]);
|
|
17220
|
+
log(`busy_while_shutdown:branch:preserved src=${member.worktree_branch} target=${safeBranch}`);
|
|
17221
|
+
}
|
|
17222
|
+
}
|
|
16999
17223
|
}
|
|
17224
|
+
try {
|
|
17225
|
+
await client3.session.abort({ sessionID });
|
|
17226
|
+
} catch {}
|
|
17000
17227
|
}
|
|
17001
17228
|
await notifyWorkingProgress(client3, db, transition.teamId);
|
|
17002
17229
|
}
|
|
@@ -17089,7 +17316,7 @@ var plugin = async (input) => {
|
|
|
17089
17316
|
}
|
|
17090
17317
|
}),
|
|
17091
17318
|
team_spawn: tool({
|
|
17092
|
-
description: "Spawn a new teammate that works in parallel. The teammate starts immediately with the given prompt.
|
|
17319
|
+
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.",
|
|
17093
17320
|
args: {
|
|
17094
17321
|
name: tool.schema.string().describe("Teammate name (lowercase alphanumeric with hyphens)"),
|
|
17095
17322
|
agent: tool.schema.string().default("build").describe("Agent type (e.g. 'build', 'plan', 'explore')"),
|
|
@@ -17193,7 +17420,7 @@ var plugin = async (input) => {
|
|
|
17193
17420
|
}
|
|
17194
17421
|
}),
|
|
17195
17422
|
team_shutdown: tool({
|
|
17196
|
-
description: "Request a teammate to shut down. The teammate finishes current work then stops.
|
|
17423
|
+
description: "Request a teammate to shut down. The teammate finishes current work then stops. Pass force: true to abort immediately without waiting.",
|
|
17197
17424
|
args: {
|
|
17198
17425
|
member: tool.schema.string().describe("Teammate name to shut down"),
|
|
17199
17426
|
force: tool.schema.boolean().default(false).describe("Force immediate abort without waiting for current work to finish")
|
|
@@ -17215,14 +17442,26 @@ var plugin = async (input) => {
|
|
|
17215
17442
|
acknowledge_uncommitted: tool.schema.boolean().default(false)
|
|
17216
17443
|
},
|
|
17217
17444
|
async execute(args, ctx) {
|
|
17218
|
-
const result = await executeTeamCleanup(deps, args, ctx.sessionID, undefined, undefined, config2.mergeOnCleanup);
|
|
17445
|
+
const result = await executeTeamCleanup(deps, args, ctx.sessionID, undefined, undefined, undefined, config2.mergeOnCleanup);
|
|
17219
17446
|
const blocked = result.includes("uncommitted");
|
|
17220
17447
|
ctx.metadata({ title: blocked ? "Cleanup blocked \u2014 uncommitted changes" : "Team cleaned up" });
|
|
17221
17448
|
return result;
|
|
17222
17449
|
}
|
|
17223
17450
|
}),
|
|
17451
|
+
team_merge: tool({
|
|
17452
|
+
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.",
|
|
17453
|
+
args: {
|
|
17454
|
+
member: tool.schema.string().describe("Teammate name whose branch to merge")
|
|
17455
|
+
},
|
|
17456
|
+
async execute(args, ctx) {
|
|
17457
|
+
const result = await executeTeamMerge(deps, args, ctx.sessionID);
|
|
17458
|
+
const conflict = result.includes("conflict");
|
|
17459
|
+
ctx.metadata({ title: conflict ? `Merge conflict: ${args.member}` : `Merged ${args.member}` });
|
|
17460
|
+
return result;
|
|
17461
|
+
}
|
|
17462
|
+
}),
|
|
17224
17463
|
team_status: tool({
|
|
17225
|
-
description: "View team members with their current status, agent type, and session IDs.
|
|
17464
|
+
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.",
|
|
17226
17465
|
args: {},
|
|
17227
17466
|
async execute(_args, ctx) {
|
|
17228
17467
|
const result = await executeTeamStatus(deps, ctx.sessionID);
|
|
@@ -17234,7 +17473,7 @@ var plugin = async (input) => {
|
|
|
17234
17473
|
}
|
|
17235
17474
|
}),
|
|
17236
17475
|
team_view: tool({
|
|
17237
|
-
description: "Navigate the TUI to a teammate's session so you can see what they are doing.
|
|
17476
|
+
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.",
|
|
17238
17477
|
args: {
|
|
17239
17478
|
member: tool.schema.string().describe("Teammate name to view")
|
|
17240
17479
|
},
|