@hueyexe/opencode-ensemble 0.2.0 → 0.3.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 +2 -2
- package/dist/index.js +50 -41
- package/package.json +1 -1
- package/src/index.ts +6 -8
- package/src/schema.ts +3 -0
- package/src/tools/team-create.ts +1 -1
- package/src/tools/team-message.ts +12 -2
- package/src/tools/team-results.ts +45 -0
- package/src/tools/team-spawn.ts +1 -1
- package/src/tools/team-status.ts +12 -0
- package/src/tools/team-view.ts +6 -3
- package/src/tools/team-approve-plan.ts +0 -42
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCode Ensemble
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run a team of parallel AI agents in OpenCode. Each agent gets its own session, context window, and task. They coordinate through messaging and a shared task board.
|
|
4
4
|
|
|
5
5
|
Built as a plugin on the public OpenCode SDK. Zero internal dependencies.
|
|
6
6
|
|
|
@@ -125,7 +125,6 @@ export { default } from "/path/to/opencode-ensemble/src/index.ts"
|
|
|
125
125
|
| `team_spawn` | Start a new teammate with a task. |
|
|
126
126
|
| `team_shutdown` | Ask a teammate to stop. |
|
|
127
127
|
| `team_cleanup` | Remove the team when done. |
|
|
128
|
-
| `team_approve_plan` | Approve or reject a teammate's plan before they write code. |
|
|
129
128
|
| `team_status` | See all members, their status, and a task summary. |
|
|
130
129
|
| `team_view` | Switch the TUI to a teammate's session. |
|
|
131
130
|
|
|
@@ -135,6 +134,7 @@ export { default } from "/path/to/opencode-ensemble/src/index.ts"
|
|
|
135
134
|
|------|-------------|
|
|
136
135
|
| `team_message` | Send a direct message to a teammate or the lead. |
|
|
137
136
|
| `team_broadcast` | Message everyone on the team. |
|
|
137
|
+
| `team_results` | Retrieve full message content (messages to lead are truncated on delivery). |
|
|
138
138
|
|
|
139
139
|
**Task board** (everyone)
|
|
140
140
|
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
3
|
var __export = (target, all) => {
|
|
8
4
|
for (var name in all)
|
|
9
5
|
__defProp(target, name, {
|
|
10
6
|
get: all[name],
|
|
11
7
|
enumerable: true,
|
|
12
8
|
configurable: true,
|
|
13
|
-
set:
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
14
10
|
});
|
|
15
11
|
};
|
|
16
12
|
|
|
@@ -12407,7 +12403,9 @@ var MIGRATIONS = [
|
|
|
12407
12403
|
CREATE INDEX IF NOT EXISTS team_message_to_idx ON team_message(to_name);
|
|
12408
12404
|
CREATE INDEX IF NOT EXISTS team_message_undelivered_idx ON team_message(team_id, delivered)
|
|
12409
12405
|
WHERE delivered = 0;
|
|
12410
|
-
|
|
12406
|
+
`,
|
|
12407
|
+
`ALTER TABLE team_message ADD COLUMN read INTEGER NOT NULL DEFAULT 0;
|
|
12408
|
+
CREATE INDEX IF NOT EXISTS team_message_unread_idx ON team_message(team_id, read) WHERE read = 0;`
|
|
12411
12409
|
];
|
|
12412
12410
|
function applyMigrations(db) {
|
|
12413
12411
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
@@ -12741,7 +12739,7 @@ async function executeTeamCreate(deps, args, sessionId) {
|
|
|
12741
12739
|
const id = generateId("team");
|
|
12742
12740
|
const now = Date.now();
|
|
12743
12741
|
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]);
|
|
12744
|
-
return `Team "${args.name}" created
|
|
12742
|
+
return `Team "${args.name}" created. You are the lead. Use team_spawn to add teammates.`;
|
|
12745
12743
|
}
|
|
12746
12744
|
|
|
12747
12745
|
// src/tools/team-spawn.ts
|
|
@@ -12809,7 +12807,7 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
12809
12807
|
} catch {}
|
|
12810
12808
|
throw new Error(`Failed to send initial prompt to teammate "${args.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
12811
12809
|
}
|
|
12812
|
-
return `Teammate "${args.name}" spawned (
|
|
12810
|
+
return `Teammate "${args.name}" spawned (agent: ${args.agent}). They are working on: ${args.prompt.slice(0, 120)}${args.prompt.length > 120 ? "..." : ""}`;
|
|
12813
12811
|
}
|
|
12814
12812
|
|
|
12815
12813
|
// src/tools/team-message.ts
|
|
@@ -12827,9 +12825,18 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
12827
12825
|
to: args.to,
|
|
12828
12826
|
content: args.text
|
|
12829
12827
|
});
|
|
12828
|
+
const isToLead = args.to === "lead";
|
|
12829
|
+
const MAX_LEAD_MSG = 500;
|
|
12830
|
+
let deliveryText;
|
|
12831
|
+
if (isToLead && args.text.length > MAX_LEAD_MSG) {
|
|
12832
|
+
const truncated = args.text.slice(0, MAX_LEAD_MSG);
|
|
12833
|
+
deliveryText = `[Team message from ${senderName}]: ${truncated}... (use team_results to read full message)`;
|
|
12834
|
+
} else {
|
|
12835
|
+
deliveryText = `[Team message from ${senderName}]: ${args.text}`;
|
|
12836
|
+
}
|
|
12830
12837
|
await deps.client.session.promptAsync({
|
|
12831
12838
|
path: { id: recipientSessionId },
|
|
12832
|
-
body: { parts: [{ type: "text", text:
|
|
12839
|
+
body: { parts: [{ type: "text", text: deliveryText }] }
|
|
12833
12840
|
});
|
|
12834
12841
|
markDelivered(deps.db, msgId);
|
|
12835
12842
|
return `Message sent to ${args.to}.`;
|
|
@@ -13027,36 +13034,36 @@ async function executeTeamClaim(deps, args, sessionId) {
|
|
|
13027
13034
|
return `Claimed task: ${task.content}`;
|
|
13028
13035
|
}
|
|
13029
13036
|
|
|
13030
|
-
// src/tools/team-
|
|
13031
|
-
async function
|
|
13032
|
-
const
|
|
13033
|
-
if (!
|
|
13034
|
-
throw new Error("
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
content: text
|
|
13046
|
-
});
|
|
13047
|
-
await deps.client.session.promptAsync({
|
|
13048
|
-
path: { id: member.session_id },
|
|
13049
|
-
body: { parts: [{ type: "text", text: `[Plan ${args.approved ? "approved" : "rejected"} by lead]: ${text}` }] }
|
|
13050
|
-
});
|
|
13051
|
-
markDelivered(deps.db, msgId);
|
|
13052
|
-
return `Plan ${args.approved ? "approved" : "rejected"} for ${args.member}.${args.feedback ? ` Feedback: ${args.feedback}` : ""}`;
|
|
13037
|
+
// src/tools/team-results.ts
|
|
13038
|
+
async function executeTeamResults(deps, args, sessionId) {
|
|
13039
|
+
const team = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
13040
|
+
if (!team)
|
|
13041
|
+
throw new Error("not in a team");
|
|
13042
|
+
const rows = args.from ? deps.db.query("SELECT id, from_name, content, time_created FROM team_message WHERE team_id = ? AND read = 0 AND from_name = ? ORDER BY time_created ASC").all(team.teamId, args.from) : deps.db.query("SELECT id, from_name, content, time_created FROM team_message WHERE team_id = ? AND read = 0 ORDER BY time_created ASC").all(team.teamId);
|
|
13043
|
+
if (rows.length === 0)
|
|
13044
|
+
return "No unread messages.";
|
|
13045
|
+
const ids = rows.map((r) => r.id);
|
|
13046
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
13047
|
+
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids);
|
|
13048
|
+
return rows.map((r) => `[Message from ${r.from_name}]:
|
|
13049
|
+
${r.content}
|
|
13050
|
+
`).join(`
|
|
13051
|
+
`);
|
|
13053
13052
|
}
|
|
13054
13053
|
|
|
13055
13054
|
// src/tools/team-status.ts
|
|
13055
|
+
var lastCallTime = new Map;
|
|
13056
|
+
var RATE_LIMIT_MS = 30000;
|
|
13056
13057
|
async function executeTeamStatus(deps, sessionId) {
|
|
13057
13058
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
13058
13059
|
if (!teamInfo)
|
|
13059
13060
|
throw new Error("This session is not in a team.");
|
|
13061
|
+
const now = Date.now();
|
|
13062
|
+
const last = lastCallTime.get(teamInfo.teamId);
|
|
13063
|
+
if (last !== undefined && now - last < RATE_LIMIT_MS) {
|
|
13064
|
+
return "No changes since last check.";
|
|
13065
|
+
}
|
|
13066
|
+
lastCallTime.set(teamInfo.teamId, now);
|
|
13060
13067
|
const members = deps.db.query("SELECT name, session_id, agent, status, execution_status FROM team_member WHERE team_id = ? ORDER BY time_created ASC").all(teamInfo.teamId);
|
|
13061
13068
|
const tasks = deps.db.query("SELECT status FROM team_task WHERE team_id = ?").all(teamInfo.teamId);
|
|
13062
13069
|
const lines = [];
|
|
@@ -13111,8 +13118,12 @@ async function executeTeamView(deps, args, sessionId) {
|
|
|
13111
13118
|
const member = deps.db.query("SELECT session_id, status, agent FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, args.member);
|
|
13112
13119
|
if (!member)
|
|
13113
13120
|
throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`);
|
|
13114
|
-
|
|
13115
|
-
|
|
13121
|
+
try {
|
|
13122
|
+
await deps.client.tui.selectSession({ sessionID: member.session_id });
|
|
13123
|
+
return `Switched to ${args.member}'s session.`;
|
|
13124
|
+
} catch {
|
|
13125
|
+
return `Could not switch to ${args.member}'s session (${member.session_id}). The session may not be accessible from the TUI.`;
|
|
13126
|
+
}
|
|
13116
13127
|
}
|
|
13117
13128
|
|
|
13118
13129
|
// src/rate-limit.ts
|
|
@@ -13421,16 +13432,14 @@ var plugin = async (input) => {
|
|
|
13421
13432
|
return result;
|
|
13422
13433
|
}
|
|
13423
13434
|
}),
|
|
13424
|
-
|
|
13425
|
-
description: "
|
|
13435
|
+
team_results: tool({
|
|
13436
|
+
description: "Retrieve full message content from teammates. Returns unread messages and marks them as read. Use this after receiving a truncated message notification.",
|
|
13426
13437
|
args: {
|
|
13427
|
-
|
|
13428
|
-
approved: tool.schema.boolean().describe("true to approve, false to reject"),
|
|
13429
|
-
feedback: tool.schema.string().optional().describe("Feedback message (optional)")
|
|
13438
|
+
from: tool.schema.string().optional().describe("Filter messages by sender name (optional, returns all if omitted)")
|
|
13430
13439
|
},
|
|
13431
13440
|
async execute(args, ctx) {
|
|
13432
|
-
const result = await
|
|
13433
|
-
ctx.metadata({ title: `
|
|
13441
|
+
const result = await executeTeamResults(deps, args, ctx.sessionID);
|
|
13442
|
+
ctx.metadata({ title: `Results${args.from ? ` from ${args.from}` : ""}` });
|
|
13434
13443
|
return result;
|
|
13435
13444
|
}
|
|
13436
13445
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
@@ -17,7 +17,7 @@ import { executeTeamTasksList } from "./tools/team-tasks-list"
|
|
|
17
17
|
import { executeTeamTasksAdd } from "./tools/team-tasks-add"
|
|
18
18
|
import { executeTeamTasksComplete } from "./tools/team-tasks-complete"
|
|
19
19
|
import { executeTeamClaim } from "./tools/team-claim"
|
|
20
|
-
import {
|
|
20
|
+
import { executeTeamResults } from "./tools/team-results"
|
|
21
21
|
import { executeTeamStatus } from "./tools/team-status"
|
|
22
22
|
import { executeTeamView } from "./tools/team-view"
|
|
23
23
|
import type { ToolDeps, PluginClient } from "./types"
|
|
@@ -254,16 +254,14 @@ const plugin: Plugin = async (input) => {
|
|
|
254
254
|
},
|
|
255
255
|
}),
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
description: "
|
|
257
|
+
team_results: tool({
|
|
258
|
+
description: "Retrieve full message content from teammates. Returns unread messages and marks them as read. Use this after receiving a truncated message notification.",
|
|
259
259
|
args: {
|
|
260
|
-
|
|
261
|
-
approved: tool.schema.boolean().describe("true to approve, false to reject"),
|
|
262
|
-
feedback: tool.schema.string().optional().describe("Feedback message (optional)"),
|
|
260
|
+
from: tool.schema.string().optional().describe("Filter messages by sender name (optional, returns all if omitted)"),
|
|
263
261
|
},
|
|
264
262
|
async execute(args, ctx) {
|
|
265
|
-
const result = await
|
|
266
|
-
ctx.metadata({ title: `
|
|
263
|
+
const result = await executeTeamResults(deps, args, ctx.sessionID)
|
|
264
|
+
ctx.metadata({ title: `Results${args.from ? ` from ${args.from}` : ""}` })
|
|
267
265
|
return result
|
|
268
266
|
},
|
|
269
267
|
}),
|
package/src/schema.ts
CHANGED
|
@@ -67,6 +67,9 @@ export const MIGRATIONS: string[] = [
|
|
|
67
67
|
CREATE INDEX IF NOT EXISTS team_message_undelivered_idx ON team_message(team_id, delivered)
|
|
68
68
|
WHERE delivered = 0;
|
|
69
69
|
`,
|
|
70
|
+
// Migration 2: Add read column to team_message for team_results tracking
|
|
71
|
+
`ALTER TABLE team_message ADD COLUMN read INTEGER NOT NULL DEFAULT 0;
|
|
72
|
+
CREATE INDEX IF NOT EXISTS team_message_unread_idx ON team_message(team_id, read) WHERE read = 0;`,
|
|
70
73
|
]
|
|
71
74
|
|
|
72
75
|
/**
|
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
|
|
31
|
+
return `Team "${args.name}" created. You are the lead. Use team_spawn to add teammates.`
|
|
32
32
|
}
|
|
@@ -25,10 +25,20 @@ export async function executeTeamMessage(
|
|
|
25
25
|
content: args.text,
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
// Deliver via promptAsync
|
|
28
|
+
// Deliver via promptAsync — truncate lead-bound messages over 500 chars
|
|
29
|
+
const isToLead = args.to === "lead"
|
|
30
|
+
const MAX_LEAD_MSG = 500
|
|
31
|
+
let deliveryText: string
|
|
32
|
+
if (isToLead && args.text.length > MAX_LEAD_MSG) {
|
|
33
|
+
const truncated = args.text.slice(0, MAX_LEAD_MSG)
|
|
34
|
+
deliveryText = `[Team message from ${senderName}]: ${truncated}... (use team_results to read full message)`
|
|
35
|
+
} else {
|
|
36
|
+
deliveryText = `[Team message from ${senderName}]: ${args.text}`
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
await deps.client.session.promptAsync({
|
|
30
40
|
path: { id: recipientSessionId },
|
|
31
|
-
body: { parts: [{ type: "text", text:
|
|
41
|
+
body: { parts: [{ type: "text", text: deliveryText }] },
|
|
32
42
|
})
|
|
33
43
|
|
|
34
44
|
markDelivered(deps.db, msgId)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToolDeps } from "../types"
|
|
2
|
+
import { findTeamBySession } from "../types"
|
|
3
|
+
|
|
4
|
+
/** Row shape for unread messages query. */
|
|
5
|
+
interface UnreadMessageRow {
|
|
6
|
+
id: string
|
|
7
|
+
from_name: string
|
|
8
|
+
content: string
|
|
9
|
+
time_created: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Retrieve unread messages from the team message store.
|
|
14
|
+
* Optionally filter by sender name. Marks returned messages as read.
|
|
15
|
+
*/
|
|
16
|
+
export async function executeTeamResults(
|
|
17
|
+
deps: ToolDeps,
|
|
18
|
+
args: { from?: string },
|
|
19
|
+
sessionId: string,
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const team = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
22
|
+
if (!team) throw new Error("not in a team")
|
|
23
|
+
|
|
24
|
+
const rows = args.from
|
|
25
|
+
? (deps.db
|
|
26
|
+
.query(
|
|
27
|
+
"SELECT id, from_name, content, time_created FROM team_message WHERE team_id = ? AND read = 0 AND from_name = ? ORDER BY time_created ASC",
|
|
28
|
+
)
|
|
29
|
+
.all(team.teamId, args.from) as UnreadMessageRow[])
|
|
30
|
+
: (deps.db
|
|
31
|
+
.query(
|
|
32
|
+
"SELECT id, from_name, content, time_created FROM team_message WHERE team_id = ? AND read = 0 ORDER BY time_created ASC",
|
|
33
|
+
)
|
|
34
|
+
.all(team.teamId) as UnreadMessageRow[])
|
|
35
|
+
|
|
36
|
+
if (rows.length === 0) return "No unread messages."
|
|
37
|
+
|
|
38
|
+
// Mark all returned messages as read
|
|
39
|
+
const ids = rows.map((r) => r.id)
|
|
40
|
+
const placeholders = ids.map(() => "?").join(", ")
|
|
41
|
+
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids)
|
|
42
|
+
|
|
43
|
+
// Format output
|
|
44
|
+
return rows.map((r) => `[Message from ${r.from_name}]:\n${r.content}\n`).join("\n")
|
|
45
|
+
}
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -88,5 +88,5 @@ export async function executeTeamSpawn(
|
|
|
88
88
|
throw new Error(`Failed to send initial prompt to teammate "${args.name}": ${err instanceof Error ? err.message : String(err)}`)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
return `Teammate "${args.name}" spawned (
|
|
91
|
+
return `Teammate "${args.name}" spawned (agent: ${args.agent}). They are working on: ${args.prompt.slice(0, 120)}${args.prompt.length > 120 ? "..." : ""}`
|
|
92
92
|
}
|
package/src/tools/team-status.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { findTeamBySession } from "../types"
|
|
3
3
|
|
|
4
|
+
/** Tracks the last time team_status was called per team ID (epoch ms). */
|
|
5
|
+
export const lastCallTime = new Map<string, number>()
|
|
6
|
+
|
|
7
|
+
const RATE_LIMIT_MS = 30_000
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Execute the team_status tool. Shows team overview with member statuses and task summary.
|
|
6
11
|
*/
|
|
@@ -11,6 +16,13 @@ export async function executeTeamStatus(
|
|
|
11
16
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
12
17
|
if (!teamInfo) throw new Error("This session is not in a team.")
|
|
13
18
|
|
|
19
|
+
const now = Date.now()
|
|
20
|
+
const last = lastCallTime.get(teamInfo.teamId)
|
|
21
|
+
if (last !== undefined && (now - last) < RATE_LIMIT_MS) {
|
|
22
|
+
return "No changes since last check."
|
|
23
|
+
}
|
|
24
|
+
lastCallTime.set(teamInfo.teamId, now)
|
|
25
|
+
|
|
14
26
|
const members = deps.db.query(
|
|
15
27
|
"SELECT name, session_id, agent, status, execution_status FROM team_member WHERE team_id = ? ORDER BY time_created ASC"
|
|
16
28
|
).all(teamInfo.teamId) as Array<{
|
package/src/tools/team-view.ts
CHANGED
|
@@ -17,7 +17,10 @@ export async function executeTeamView(
|
|
|
17
17
|
.get(teamInfo.teamId, args.member) as { session_id: string; status: string; agent: string } | null
|
|
18
18
|
if (!member) throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
try {
|
|
21
|
+
await deps.client.tui.selectSession({ sessionID: member.session_id })
|
|
22
|
+
return `Switched to ${args.member}'s session.`
|
|
23
|
+
} catch {
|
|
24
|
+
return `Could not switch to ${args.member}'s session (${member.session_id}). The session may not be accessible from the TUI.`
|
|
25
|
+
}
|
|
23
26
|
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { ToolDeps } from "../types"
|
|
2
|
-
import { findTeamBySession, resolveRecipientSession } from "../types"
|
|
3
|
-
import { sendMessage, markDelivered } from "../messaging"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Execute the team_approve_plan tool. Approves or rejects a teammate's plan
|
|
7
|
-
* and notifies them via message.
|
|
8
|
-
*/
|
|
9
|
-
export async function executeTeamApprovePlan(
|
|
10
|
-
deps: ToolDeps,
|
|
11
|
-
args: { member: string; approved: boolean; feedback?: string },
|
|
12
|
-
sessionId: string,
|
|
13
|
-
): Promise<string> {
|
|
14
|
-
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
15
|
-
if (!teamInfo) throw new Error("This session is not in a team.")
|
|
16
|
-
if (teamInfo.role !== "lead") throw new Error("Only the team lead can approve plans.")
|
|
17
|
-
|
|
18
|
-
const member = deps.db.query("SELECT session_id, status FROM team_member WHERE team_id = ? AND name = ?")
|
|
19
|
-
.get(teamInfo.teamId, args.member) as { session_id: string; status: string } | null
|
|
20
|
-
if (!member) throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`)
|
|
21
|
-
|
|
22
|
-
const text = args.approved
|
|
23
|
-
? `Your plan has been APPROVED.${args.feedback ? ` Feedback: ${args.feedback}` : " Proceed with implementation."}`
|
|
24
|
-
: `Your plan has been REJECTED. Please revise and resubmit.${args.feedback ? ` Feedback: ${args.feedback}` : ""}`
|
|
25
|
-
|
|
26
|
-
// Send approval/rejection message to teammate
|
|
27
|
-
const msgId = sendMessage(deps.db, {
|
|
28
|
-
teamId: teamInfo.teamId,
|
|
29
|
-
from: "lead",
|
|
30
|
-
to: args.member,
|
|
31
|
-
content: text,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
await deps.client.session.promptAsync({
|
|
35
|
-
path: { id: member.session_id },
|
|
36
|
-
body: { parts: [{ type: "text", text: `[Plan ${args.approved ? "approved" : "rejected"} by lead]: ${text}` }] },
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
markDelivered(deps.db, msgId)
|
|
40
|
-
|
|
41
|
-
return `Plan ${args.approved ? "approved" : "rejected"} for ${args.member}.${args.feedback ? ` Feedback: ${args.feedback}` : ""}`
|
|
42
|
-
}
|