@hueyexe/opencode-ensemble 0.2.0 → 0.3.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 +1 -1
- package/dist/index.js +52 -37
- 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 +13 -1
- package/src/tools/team-view.ts +9 -2
- package/src/tools/team-approve-plan.ts +0 -42
package/README.md
CHANGED
|
@@ -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
|
@@ -12407,7 +12407,9 @@ var MIGRATIONS = [
|
|
|
12407
12407
|
CREATE INDEX IF NOT EXISTS team_message_to_idx ON team_message(to_name);
|
|
12408
12408
|
CREATE INDEX IF NOT EXISTS team_message_undelivered_idx ON team_message(team_id, delivered)
|
|
12409
12409
|
WHERE delivered = 0;
|
|
12410
|
-
|
|
12410
|
+
`,
|
|
12411
|
+
`ALTER TABLE team_message ADD COLUMN read INTEGER NOT NULL DEFAULT 0;
|
|
12412
|
+
CREATE INDEX IF NOT EXISTS team_message_unread_idx ON team_message(team_id, read) WHERE read = 0;`
|
|
12411
12413
|
];
|
|
12412
12414
|
function applyMigrations(db) {
|
|
12413
12415
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
@@ -12741,7 +12743,7 @@ async function executeTeamCreate(deps, args, sessionId) {
|
|
|
12741
12743
|
const id = generateId("team");
|
|
12742
12744
|
const now = Date.now();
|
|
12743
12745
|
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 (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.`;
|
|
12746
|
+
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. IMPORTANT: After spawning all teammates, do NOT call any tools. Tell the user what you've set up and STOP. You will be woken automatically.`;
|
|
12745
12747
|
}
|
|
12746
12748
|
|
|
12747
12749
|
// src/tools/team-spawn.ts
|
|
@@ -12809,7 +12811,7 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
12809
12811
|
} catch {}
|
|
12810
12812
|
throw new Error(`Failed to send initial prompt to teammate "${args.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
12811
12813
|
}
|
|
12812
|
-
return `Teammate "${args.name}" spawned (
|
|
12814
|
+
return `Teammate "${args.name}" spawned (agent: ${args.agent}). IMPORTANT: Do NOT call any tools after this. Tell the user what you've set up and STOP. You will be woken automatically when teammates message you.`;
|
|
12813
12815
|
}
|
|
12814
12816
|
|
|
12815
12817
|
// src/tools/team-message.ts
|
|
@@ -12827,9 +12829,18 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
12827
12829
|
to: args.to,
|
|
12828
12830
|
content: args.text
|
|
12829
12831
|
});
|
|
12832
|
+
const isToLead = args.to === "lead";
|
|
12833
|
+
const MAX_LEAD_MSG = 500;
|
|
12834
|
+
let deliveryText;
|
|
12835
|
+
if (isToLead && args.text.length > MAX_LEAD_MSG) {
|
|
12836
|
+
const truncated = args.text.slice(0, MAX_LEAD_MSG);
|
|
12837
|
+
deliveryText = `[Team message from ${senderName}]: ${truncated}... (use team_results to read full message)`;
|
|
12838
|
+
} else {
|
|
12839
|
+
deliveryText = `[Team message from ${senderName}]: ${args.text}`;
|
|
12840
|
+
}
|
|
12830
12841
|
await deps.client.session.promptAsync({
|
|
12831
12842
|
path: { id: recipientSessionId },
|
|
12832
|
-
body: { parts: [{ type: "text", text:
|
|
12843
|
+
body: { parts: [{ type: "text", text: deliveryText }] }
|
|
12833
12844
|
});
|
|
12834
12845
|
markDelivered(deps.db, msgId);
|
|
12835
12846
|
return `Message sent to ${args.to}.`;
|
|
@@ -13027,36 +13038,36 @@ async function executeTeamClaim(deps, args, sessionId) {
|
|
|
13027
13038
|
return `Claimed task: ${task.content}`;
|
|
13028
13039
|
}
|
|
13029
13040
|
|
|
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}` : ""}`;
|
|
13041
|
+
// src/tools/team-results.ts
|
|
13042
|
+
async function executeTeamResults(deps, args, sessionId) {
|
|
13043
|
+
const team = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
13044
|
+
if (!team)
|
|
13045
|
+
throw new Error("not in a team");
|
|
13046
|
+
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);
|
|
13047
|
+
if (rows.length === 0)
|
|
13048
|
+
return "No unread messages.";
|
|
13049
|
+
const ids = rows.map((r) => r.id);
|
|
13050
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
13051
|
+
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids);
|
|
13052
|
+
return rows.map((r) => `[Message from ${r.from_name}]:
|
|
13053
|
+
${r.content}
|
|
13054
|
+
`).join(`
|
|
13055
|
+
`);
|
|
13053
13056
|
}
|
|
13054
13057
|
|
|
13055
13058
|
// src/tools/team-status.ts
|
|
13059
|
+
var lastCallTime = new Map;
|
|
13060
|
+
var RATE_LIMIT_MS = 30000;
|
|
13056
13061
|
async function executeTeamStatus(deps, sessionId) {
|
|
13057
13062
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
13058
13063
|
if (!teamInfo)
|
|
13059
13064
|
throw new Error("This session is not in a team.");
|
|
13065
|
+
const now = Date.now();
|
|
13066
|
+
const last = lastCallTime.get(teamInfo.teamId);
|
|
13067
|
+
if (last !== undefined && now - last < RATE_LIMIT_MS) {
|
|
13068
|
+
return "Status unchanged. STOP \u2014 do not call any more tools. Tell the user you are waiting for teammates to finish. You will be woken automatically when they message you.";
|
|
13069
|
+
}
|
|
13070
|
+
lastCallTime.set(teamInfo.teamId, now);
|
|
13060
13071
|
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
13072
|
const tasks = deps.db.query("SELECT status FROM team_task WHERE team_id = ?").all(teamInfo.teamId);
|
|
13062
13073
|
const lines = [];
|
|
@@ -13099,7 +13110,9 @@ async function executeTeamStatus(deps, sessionId) {
|
|
|
13099
13110
|
});
|
|
13100
13111
|
} catch {}
|
|
13101
13112
|
}
|
|
13102
|
-
return
|
|
13113
|
+
return `[Relay this status to the user in your next message:]
|
|
13114
|
+
|
|
13115
|
+
` + lines.join(`
|
|
13103
13116
|
`);
|
|
13104
13117
|
}
|
|
13105
13118
|
|
|
@@ -13111,8 +13124,12 @@ async function executeTeamView(deps, args, sessionId) {
|
|
|
13111
13124
|
const member = deps.db.query("SELECT session_id, status, agent FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, args.member);
|
|
13112
13125
|
if (!member)
|
|
13113
13126
|
throw new Error(`Teammate "${args.member}" not found in team "${teamInfo.teamName}"`);
|
|
13114
|
-
|
|
13115
|
-
|
|
13127
|
+
try {
|
|
13128
|
+
if (typeof deps.client.tui.selectSession === "function") {
|
|
13129
|
+
await deps.client.tui.selectSession({ sessionID: member.session_id });
|
|
13130
|
+
}
|
|
13131
|
+
} catch {}
|
|
13132
|
+
return `View ${args.member}'s session: ${member.session_id}. Use the session picker (ctrl+p) to navigate to their session.`;
|
|
13116
13133
|
}
|
|
13117
13134
|
|
|
13118
13135
|
// src/rate-limit.ts
|
|
@@ -13421,16 +13438,14 @@ var plugin = async (input) => {
|
|
|
13421
13438
|
return result;
|
|
13422
13439
|
}
|
|
13423
13440
|
}),
|
|
13424
|
-
|
|
13425
|
-
description: "
|
|
13441
|
+
team_results: tool({
|
|
13442
|
+
description: "Retrieve full message content from teammates. Returns unread messages and marks them as read. Use this after receiving a truncated message notification.",
|
|
13426
13443
|
args: {
|
|
13427
|
-
|
|
13428
|
-
approved: tool.schema.boolean().describe("true to approve, false to reject"),
|
|
13429
|
-
feedback: tool.schema.string().optional().describe("Feedback message (optional)")
|
|
13444
|
+
from: tool.schema.string().optional().describe("Filter messages by sender name (optional, returns all if omitted)")
|
|
13430
13445
|
},
|
|
13431
13446
|
async execute(args, ctx) {
|
|
13432
|
-
const result = await
|
|
13433
|
-
ctx.metadata({ title: `
|
|
13447
|
+
const result = await executeTeamResults(deps, args, ctx.sessionID);
|
|
13448
|
+
ctx.metadata({ title: `Results${args.from ? ` from ${args.from}` : ""}` });
|
|
13434
13449
|
return result;
|
|
13435
13450
|
}
|
|
13436
13451
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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 (ID: ${id}). You are the lead. Use team_spawn to add teammates. Teammates will message you when done — do not poll team_status repeatedly.`
|
|
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. IMPORTANT: After spawning all teammates, do NOT call any tools. Tell the user what you've set up and STOP. You will be woken automatically.`
|
|
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}). IMPORTANT: Do NOT call any tools after this. Tell the user what you've set up and STOP. You will be woken automatically when teammates message you.`
|
|
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 "Status unchanged. STOP — do not call any more tools. Tell the user you are waiting for teammates to finish. You will be woken automatically when they message you."
|
|
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<{
|
|
@@ -68,5 +80,5 @@ export async function executeTeamStatus(
|
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
71
|
-
return lines.join("\n")
|
|
83
|
+
return "[Relay this status to the user in your next message:]\n\n" + lines.join("\n")
|
|
72
84
|
}
|
package/src/tools/team-view.ts
CHANGED
|
@@ -17,7 +17,14 @@ 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
|
-
|
|
20
|
+
// TUI session navigation — try selectSession if available, otherwise guide the user
|
|
21
|
+
try {
|
|
22
|
+
if (typeof deps.client.tui.selectSession === "function") {
|
|
23
|
+
await deps.client.tui.selectSession({ sessionID: member.session_id })
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// selectSession may not be available in all SDK versions — fall through to manual guidance
|
|
27
|
+
}
|
|
21
28
|
|
|
22
|
-
return `
|
|
29
|
+
return `View ${args.member}'s session: ${member.session_id}. Use the session picker (ctrl+p) to navigate to their session.`
|
|
23
30
|
}
|
|
@@ -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
|
-
}
|