@hueyexe/opencode-ensemble 0.1.1 → 0.1.2
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/dist/index.js +56 -7
- package/package.json +1 -1
- package/src/index.ts +4 -1
- package/src/notify.ts +41 -0
- package/src/tools/team-cleanup.ts +1 -1
- package/src/tools/team-create.ts +1 -1
- package/src/tools/team-spawn.ts +12 -5
- package/src/tools/team-status.ts +23 -0
package/dist/index.js
CHANGED
|
@@ -12633,6 +12633,30 @@ async function notifyTeamEvent(client, type, data) {
|
|
|
12633
12633
|
});
|
|
12634
12634
|
} catch {}
|
|
12635
12635
|
}
|
|
12636
|
+
async function notifyWorkingProgress(client, db, teamId) {
|
|
12637
|
+
const members = db.query("SELECT name, status FROM team_member WHERE team_id = ? ORDER BY time_created ASC").all(teamId);
|
|
12638
|
+
if (members.length === 0)
|
|
12639
|
+
return;
|
|
12640
|
+
const busy = members.filter((m) => m.status === "busy");
|
|
12641
|
+
try {
|
|
12642
|
+
if (busy.length === 0) {
|
|
12643
|
+
await client.tui.showToast({
|
|
12644
|
+
title: "Team",
|
|
12645
|
+
message: "All teammates finished",
|
|
12646
|
+
variant: "success",
|
|
12647
|
+
duration: 4000
|
|
12648
|
+
});
|
|
12649
|
+
} else {
|
|
12650
|
+
const names = busy.map((m) => m.name).join(", ");
|
|
12651
|
+
await client.tui.showToast({
|
|
12652
|
+
title: "Team",
|
|
12653
|
+
message: `Working: ${names} (${busy.length}/${members.length})`,
|
|
12654
|
+
variant: "info",
|
|
12655
|
+
duration: 5000
|
|
12656
|
+
});
|
|
12657
|
+
}
|
|
12658
|
+
} catch {}
|
|
12659
|
+
}
|
|
12636
12660
|
|
|
12637
12661
|
// src/types.ts
|
|
12638
12662
|
function findTeamBySession(db, registry2, sessionId) {
|
|
@@ -12670,7 +12694,7 @@ async function executeTeamCreate(deps, args, sessionId) {
|
|
|
12670
12694
|
const id = generateId("team");
|
|
12671
12695
|
const now = Date.now();
|
|
12672
12696
|
deps.db.run("INSERT INTO team (id, name, lead_session_id, status, delegate, time_created, time_updated) VALUES (?, ?, ?, 'active', 0, ?, ?)", [id, args.name, sessionId, now, now]);
|
|
12673
|
-
return `Team "${args.name}" created (ID: ${id}). You are the lead. Use team_spawn to add teammates.`;
|
|
12697
|
+
return `Team "${args.name}" created (ID: ${id}). You are the lead. Use team_spawn to add teammates. Teammates will message you when done \u2014 do not poll team_status repeatedly.`;
|
|
12674
12698
|
}
|
|
12675
12699
|
|
|
12676
12700
|
// src/tools/team-spawn.ts
|
|
@@ -12709,8 +12733,9 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
12709
12733
|
"- team_claim: claim a pending task from the shared board",
|
|
12710
12734
|
"",
|
|
12711
12735
|
"When you finish your task:",
|
|
12712
|
-
"1.
|
|
12713
|
-
"2.
|
|
12736
|
+
"1. If you claimed a task, mark it complete using team_tasks_complete.",
|
|
12737
|
+
"2. Send ONE message to the lead with your findings using team_message.",
|
|
12738
|
+
"3. STOP. Do not send follow-up confirmations, status updates, or 'standing by' messages.",
|
|
12714
12739
|
"",
|
|
12715
12740
|
"If you are blocked, send ONE message to the lead describing the specific blocker.",
|
|
12716
12741
|
"",
|
|
@@ -12718,13 +12743,17 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
12718
12743
|
"",
|
|
12719
12744
|
"Your task:",
|
|
12720
12745
|
args.prompt
|
|
12721
|
-
]
|
|
12746
|
+
];
|
|
12747
|
+
if (args.claim_task) {
|
|
12748
|
+
context.push("", `You have been assigned task ${args.claim_task}. Mark it complete when done.`);
|
|
12749
|
+
}
|
|
12750
|
+
const contextStr = context.join(`
|
|
12722
12751
|
`);
|
|
12723
12752
|
await deps.client.session.promptAsync({
|
|
12724
12753
|
path: { id: childSessionId },
|
|
12725
|
-
body: { parts: [{ type: "text", text:
|
|
12754
|
+
body: { parts: [{ type: "text", text: contextStr }] }
|
|
12726
12755
|
});
|
|
12727
|
-
return `Teammate "${args.name}" spawned (session: ${childSessionId}, agent: ${args.agent}). Working on: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""}
|
|
12756
|
+
return `Teammate "${args.name}" spawned (session: ${childSessionId}, agent: ${args.agent}). Working on: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""} Teammates will message you when done. Do not poll team_status to check.`;
|
|
12728
12757
|
}
|
|
12729
12758
|
|
|
12730
12759
|
// src/tools/team-message.ts
|
|
@@ -12820,7 +12849,7 @@ async function executeTeamCleanup(deps, args, sessionId) {
|
|
|
12820
12849
|
if (teamInfo.role !== "lead")
|
|
12821
12850
|
throw new Error("Only the team lead can clean up the team.");
|
|
12822
12851
|
const members = deps.db.query("SELECT name, session_id, status FROM team_member WHERE team_id = ?").all(teamInfo.teamId);
|
|
12823
|
-
const active = members.filter((m) => m.status !== "shutdown");
|
|
12852
|
+
const active = members.filter((m) => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error");
|
|
12824
12853
|
if (active.length > 0 && !args.force) {
|
|
12825
12854
|
const names = active.map((m) => m.name).join(", ");
|
|
12826
12855
|
throw new Error(`Cannot clean up team "${teamInfo.teamName}": ${active.length} member(s) still active: ${names}. Use team_shutdown on each member first, or call team_cleanup with force: true to abort them immediately.`);
|
|
@@ -12982,6 +13011,25 @@ async function executeTeamStatus(deps, sessionId) {
|
|
|
12982
13011
|
lines.push("");
|
|
12983
13012
|
lines.push(`Tasks: ${tasks.length} total (${parts.join(", ")})`);
|
|
12984
13013
|
}
|
|
13014
|
+
if (members.length > 0) {
|
|
13015
|
+
const memberParts = members.map((m) => {
|
|
13016
|
+
const label = m.status === "busy" ? "working" : m.status === "ready" ? "idle" : m.status;
|
|
13017
|
+
return `${m.name} [${label}]`;
|
|
13018
|
+
});
|
|
13019
|
+
let toastMsg = memberParts.join(", ");
|
|
13020
|
+
if (tasks.length > 0) {
|
|
13021
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
13022
|
+
toastMsg += ` | Tasks: ${completed}/${tasks.length} done`;
|
|
13023
|
+
}
|
|
13024
|
+
try {
|
|
13025
|
+
await deps.client.tui.showToast({
|
|
13026
|
+
title: "Team",
|
|
13027
|
+
message: toastMsg,
|
|
13028
|
+
variant: "info",
|
|
13029
|
+
duration: 4000
|
|
13030
|
+
});
|
|
13031
|
+
} catch {}
|
|
13032
|
+
}
|
|
12985
13033
|
return lines.join(`
|
|
12986
13034
|
`);
|
|
12987
13035
|
}
|
|
@@ -13101,6 +13149,7 @@ var plugin = async (input) => {
|
|
|
13101
13149
|
} else if (transition.to === "error") {
|
|
13102
13150
|
notifyTeamEvent(client, "error", { memberName: transition.memberName });
|
|
13103
13151
|
}
|
|
13152
|
+
await notifyWorkingProgress(client, db, transition.teamId);
|
|
13104
13153
|
}
|
|
13105
13154
|
}
|
|
13106
13155
|
if (event.type === "session.created") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Agent teams for OpenCode — parallel agents with peer-to-peer communication, shared tasks, and coordinated execution",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createDb, getDbPath } from "./db"
|
|
|
6
6
|
import { recoverStaleMembers } from "./recovery"
|
|
7
7
|
import { MemberRegistry, DescendantTracker } from "./state"
|
|
8
8
|
import { handleSessionStatusEvent, handleSessionCreatedEvent, checkToolIsolation } from "./hooks"
|
|
9
|
-
import { notifyTeamEvent } from "./notify"
|
|
9
|
+
import { notifyTeamEvent, notifyWorkingProgress } from "./notify"
|
|
10
10
|
import { executeTeamCreate } from "./tools/team-create"
|
|
11
11
|
import { executeTeamSpawn } from "./tools/team-spawn"
|
|
12
12
|
import { executeTeamMessage } from "./tools/team-message"
|
|
@@ -85,6 +85,9 @@ const plugin: Plugin = async (input) => {
|
|
|
85
85
|
} else if (transition.to === "error") {
|
|
86
86
|
notifyTeamEvent(client, "error", { memberName: transition.memberName })
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
// Show working progress after every transition so the user sees who's still active
|
|
90
|
+
await notifyWorkingProgress(client, db, transition.teamId)
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
package/src/notify.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
1
2
|
import type { PluginClient } from "./types"
|
|
2
3
|
|
|
3
4
|
type TeamEventType = "spawn" | "message" | "completed" | "error" | "shutdown"
|
|
@@ -52,3 +53,43 @@ export async function notifyTeamEvent(
|
|
|
52
53
|
// TUI may not be available — silently ignore
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fire a toast showing which teammates are still working.
|
|
59
|
+
* Called on every session.status transition so the user sees live progress.
|
|
60
|
+
* Does nothing if the team has no members.
|
|
61
|
+
*/
|
|
62
|
+
export async function notifyWorkingProgress(
|
|
63
|
+
client: PluginClient,
|
|
64
|
+
db: Database,
|
|
65
|
+
teamId: string,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const members = db.query(
|
|
68
|
+
"SELECT name, status FROM team_member WHERE team_id = ? ORDER BY time_created ASC"
|
|
69
|
+
).all(teamId) as Array<{ name: string; status: string }>
|
|
70
|
+
|
|
71
|
+
if (members.length === 0) return
|
|
72
|
+
|
|
73
|
+
const busy = members.filter(m => m.status === "busy")
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (busy.length === 0) {
|
|
77
|
+
await client.tui.showToast({
|
|
78
|
+
title: "Team",
|
|
79
|
+
message: "All teammates finished",
|
|
80
|
+
variant: "success",
|
|
81
|
+
duration: 4000,
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
const names = busy.map(m => m.name).join(", ")
|
|
85
|
+
await client.tui.showToast({
|
|
86
|
+
title: "Team",
|
|
87
|
+
message: `Working: ${names} (${busy.length}/${members.length})`,
|
|
88
|
+
variant: "info",
|
|
89
|
+
duration: 5000,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// TUI may not be available — silently ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -17,7 +17,7 @@ export async function executeTeamCleanup(
|
|
|
17
17
|
const members = deps.db.query("SELECT name, session_id, status FROM team_member WHERE team_id = ?")
|
|
18
18
|
.all(teamInfo.teamId) as Array<{ name: string; session_id: string; status: string }>
|
|
19
19
|
|
|
20
|
-
const active = members.filter(m => m.status !== "shutdown")
|
|
20
|
+
const active = members.filter(m => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error")
|
|
21
21
|
|
|
22
22
|
if (active.length > 0 && !args.force) {
|
|
23
23
|
const names = active.map(m => m.name).join(", ")
|
package/src/tools/team-create.ts
CHANGED
|
@@ -28,5 +28,5 @@ export async function executeTeamCreate(
|
|
|
28
28
|
[id, args.name, sessionId, now, now]
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
return `Team "${args.name}" created (ID: ${id}). You are the lead. Use team_spawn to add teammates.`
|
|
31
|
+
return `Team "${args.name}" created (ID: ${id}). You are the lead. Use team_spawn to add teammates. Teammates will message you when done — do not poll team_status repeatedly.`
|
|
32
32
|
}
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -54,8 +54,9 @@ export async function executeTeamSpawn(
|
|
|
54
54
|
"- team_claim: claim a pending task from the shared board",
|
|
55
55
|
"",
|
|
56
56
|
"When you finish your task:",
|
|
57
|
-
"1.
|
|
58
|
-
"2.
|
|
57
|
+
"1. If you claimed a task, mark it complete using team_tasks_complete.",
|
|
58
|
+
"2. Send ONE message to the lead with your findings using team_message.",
|
|
59
|
+
"3. STOP. Do not send follow-up confirmations, status updates, or 'standing by' messages.",
|
|
59
60
|
"",
|
|
60
61
|
"If you are blocked, send ONE message to the lead describing the specific blocker.",
|
|
61
62
|
"",
|
|
@@ -63,15 +64,21 @@ export async function executeTeamSpawn(
|
|
|
63
64
|
"",
|
|
64
65
|
"Your task:",
|
|
65
66
|
args.prompt,
|
|
66
|
-
]
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if (args.claim_task) {
|
|
70
|
+
context.push("", `You have been assigned task ${args.claim_task}. Mark it complete when done.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const contextStr = context.join("\n")
|
|
67
74
|
|
|
68
75
|
// Fire-and-forget: send prompt to teammate session
|
|
69
76
|
// OQ-1: assuming promptAsync queues when session is busy
|
|
70
77
|
// OQ-10: assuming fresh session accepts promptAsync without session.init()
|
|
71
78
|
await deps.client.session.promptAsync({
|
|
72
79
|
path: { id: childSessionId },
|
|
73
|
-
body: { parts: [{ type: "text", text:
|
|
80
|
+
body: { parts: [{ type: "text", text: contextStr }] },
|
|
74
81
|
})
|
|
75
82
|
|
|
76
|
-
return `Teammate "${args.name}" spawned (session: ${childSessionId}, agent: ${args.agent}). Working on: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""}
|
|
83
|
+
return `Teammate "${args.name}" spawned (session: ${childSessionId}, agent: ${args.agent}). Working on: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""} Teammates will message you when done. Do not poll team_status to check.`
|
|
77
84
|
}
|
package/src/tools/team-status.ts
CHANGED
|
@@ -45,5 +45,28 @@ export async function executeTeamStatus(
|
|
|
45
45
|
lines.push(`Tasks: ${tasks.length} total (${parts.join(", ")})`)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Fire a toast so the user (not just the model) sees the status summary
|
|
49
|
+
if (members.length > 0) {
|
|
50
|
+
const memberParts = members.map(m => {
|
|
51
|
+
const label = m.status === "busy" ? "working" : m.status === "ready" ? "idle" : m.status
|
|
52
|
+
return `${m.name} [${label}]`
|
|
53
|
+
})
|
|
54
|
+
let toastMsg = memberParts.join(", ")
|
|
55
|
+
if (tasks.length > 0) {
|
|
56
|
+
const completed = tasks.filter(t => t.status === "completed").length
|
|
57
|
+
toastMsg += ` | Tasks: ${completed}/${tasks.length} done`
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await deps.client.tui.showToast({
|
|
61
|
+
title: "Team",
|
|
62
|
+
message: toastMsg,
|
|
63
|
+
variant: "info",
|
|
64
|
+
duration: 4000,
|
|
65
|
+
})
|
|
66
|
+
} catch {
|
|
67
|
+
// TUI may not be available — silently ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
return lines.join("\n")
|
|
49
72
|
}
|