@hueyexe/opencode-ensemble 0.8.0 → 0.9.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 +17 -7
- package/dist/index.js +45 -5
- package/package.json +1 -1
- package/src/result-parser.ts +40 -0
- package/src/system-prompt.ts +7 -1
- package/src/tools/team-results.ts +7 -2
- package/src/tools/team-spawn.ts +11 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
4
4
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
5
|
-
[]()
|
|
6
6
|
[]()
|
|
7
|
-
[]()
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
|
|
10
10
|
Run parallel AI agents in OpenCode. Each agent gets its own session, context window, and task. They coordinate through messaging and a shared task board.
|
|
@@ -95,13 +95,13 @@ Two steps: add the plugin, then allowlist worktree paths.
|
|
|
95
95
|
|
|
96
96
|
### 1. Add the plugin
|
|
97
97
|
|
|
98
|
-
Add to your OpenCode config. Project-level or global.
|
|
98
|
+
Add to your OpenCode config with a pinned version. Project-level or global.
|
|
99
99
|
|
|
100
100
|
**Project-level** (`opencode.json` in your project root):
|
|
101
101
|
|
|
102
102
|
```json
|
|
103
103
|
{
|
|
104
|
-
"plugin": ["@hueyexe/opencode-ensemble"]
|
|
104
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.9.0"]
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
@@ -109,11 +109,21 @@ Add to your OpenCode config. Project-level or global.
|
|
|
109
109
|
|
|
110
110
|
```json
|
|
111
111
|
{
|
|
112
|
-
"plugin": ["@hueyexe/opencode-ensemble"]
|
|
112
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.9.0"]
|
|
113
113
|
}
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
OpenCode auto-installs npm plugins at startup
|
|
116
|
+
OpenCode auto-installs npm plugins at startup. To update, bump the version number in your config and restart OpenCode.
|
|
117
|
+
|
|
118
|
+
**Why pin versions?** OpenCode has a [known bug](https://github.com/anomalyco/opencode/issues/6774) where unpinned plugins (e.g., `"@hueyexe/opencode-ensemble"`) get cached on first install and never auto-update, even after restarting. Pinning to a specific version avoids this — when you change the version string, OpenCode sees a new package spec and installs it fresh.
|
|
119
|
+
|
|
120
|
+
If you're stuck on an old version, clear the cache manually:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
rm -rf ~/.cache/opencode/packages/@hueyexe
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Then restart OpenCode.
|
|
117
127
|
|
|
118
128
|
### 2. Allow worktree directory access
|
|
119
129
|
|
|
@@ -298,7 +308,7 @@ Same coordination model (shared tasks, peer messaging, lead coordination) with s
|
|
|
298
308
|
```bash
|
|
299
309
|
bun install
|
|
300
310
|
bun run typecheck
|
|
301
|
-
bun test #
|
|
311
|
+
bun test # 432 tests
|
|
302
312
|
bun run build
|
|
303
313
|
```
|
|
304
314
|
|
package/dist/index.js
CHANGED
|
@@ -15741,6 +15741,33 @@ async function notifyWorkingProgress(client3, db, teamId) {
|
|
|
15741
15741
|
} catch {}
|
|
15742
15742
|
}
|
|
15743
15743
|
|
|
15744
|
+
// src/result-parser.ts
|
|
15745
|
+
function parseTaskResult(content) {
|
|
15746
|
+
const match = content.match(/<task-result>([\s\S]*?)<\/task-result>/);
|
|
15747
|
+
if (!match)
|
|
15748
|
+
return null;
|
|
15749
|
+
const inner = match[1];
|
|
15750
|
+
const status = inner.match(/<status>([\s\S]*?)<\/status>/)?.[1]?.trim();
|
|
15751
|
+
const summary = inner.match(/<summary>([\s\S]*?)<\/summary>/)?.[1]?.trim();
|
|
15752
|
+
const details = inner.match(/<details>([\s\S]*?)<\/details>/)?.[1]?.trim();
|
|
15753
|
+
const branch = inner.match(/<branch>([\s\S]*?)<\/branch>/)?.[1]?.trim();
|
|
15754
|
+
if (!status || !summary || !details)
|
|
15755
|
+
return null;
|
|
15756
|
+
return { status, summary, details, branch: branch || undefined };
|
|
15757
|
+
}
|
|
15758
|
+
function formatTaskResult(from, result) {
|
|
15759
|
+
const lines = [
|
|
15760
|
+
`[Result from ${from}]:`,
|
|
15761
|
+
` Status: ${result.status}`,
|
|
15762
|
+
` Summary: ${result.summary}`,
|
|
15763
|
+
` Details: ${result.details}`
|
|
15764
|
+
];
|
|
15765
|
+
if (result.branch)
|
|
15766
|
+
lines.push(` Branch: ${result.branch}`);
|
|
15767
|
+
return lines.join(`
|
|
15768
|
+
`);
|
|
15769
|
+
}
|
|
15770
|
+
|
|
15744
15771
|
// src/system-prompt.ts
|
|
15745
15772
|
function truncate(s, maxLen) {
|
|
15746
15773
|
return s.length > maxLen ? `${s.slice(0, maxLen)}...` : s;
|
|
@@ -15791,7 +15818,11 @@ function buildLeadSystemPrompt(db, teamId) {
|
|
|
15791
15818
|
lines.push("", "--- Team Messages ---");
|
|
15792
15819
|
const MAX_MSG = 500;
|
|
15793
15820
|
for (const msg of pendingMessages) {
|
|
15794
|
-
|
|
15821
|
+
const parsed = parseTaskResult(msg.content);
|
|
15822
|
+
if (parsed) {
|
|
15823
|
+
const truncatedResult = { ...parsed, details: truncate(parsed.details, 500) };
|
|
15824
|
+
lines.push(formatTaskResult(msg.from_name, truncatedResult));
|
|
15825
|
+
} else if (msg.content.length > MAX_MSG) {
|
|
15795
15826
|
lines.push(`[From ${msg.from_name}]: ${msg.content.slice(0, MAX_MSG)}... (use team_results to read full message)`);
|
|
15796
15827
|
} else {
|
|
15797
15828
|
lines.push(`[From ${msg.from_name}]: ${msg.content}`);
|
|
@@ -16184,6 +16215,10 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
16184
16215
|
`You are "${args.name}", a teammate in team "${teamInfo.teamName}".`,
|
|
16185
16216
|
`Your agent type is "${args.agent}".`
|
|
16186
16217
|
];
|
|
16218
|
+
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);
|
|
16219
|
+
if (otherMembers.length > 0) {
|
|
16220
|
+
context.push(`Other teammates: ${otherMembers.map((m) => m.name).join(", ")}`);
|
|
16221
|
+
}
|
|
16187
16222
|
if (worktreeBranch && worktreeDir && !workspaceId) {
|
|
16188
16223
|
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
16224
|
} else if (worktreeBranch && worktreeDir) {
|
|
@@ -16216,7 +16251,7 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
16216
16251
|
}
|
|
16217
16252
|
context.push("</task-result>");
|
|
16218
16253
|
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,
|
|
16254
|
+
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
16255
|
context.push("", "Your task:", args.prompt);
|
|
16221
16256
|
if (args.claim_task) {
|
|
16222
16257
|
context.push("", `You have been assigned task ${args.claim_task}. Mark it complete when done.`);
|
|
@@ -16622,9 +16657,14 @@ async function executeTeamResults(deps, args, sessionId) {
|
|
|
16622
16657
|
const ids = rows.map((r) => r.id);
|
|
16623
16658
|
const placeholders = ids.map(() => "?").join(", ");
|
|
16624
16659
|
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids);
|
|
16625
|
-
return rows.map((r) =>
|
|
16626
|
-
|
|
16627
|
-
|
|
16660
|
+
return rows.map((r) => {
|
|
16661
|
+
const parsed = parseTaskResult(r.content);
|
|
16662
|
+
if (parsed)
|
|
16663
|
+
return formatTaskResult(r.from_name, parsed);
|
|
16664
|
+
return `[Message from ${r.from_name}]:
|
|
16665
|
+
${r.content}`;
|
|
16666
|
+
}).join(`
|
|
16667
|
+
|
|
16628
16668
|
`);
|
|
16629
16669
|
}
|
|
16630
16670
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Structured result parsed from `<task-result>` XML. */
|
|
2
|
+
export interface TaskResult {
|
|
3
|
+
status: string
|
|
4
|
+
summary: string
|
|
5
|
+
details: string
|
|
6
|
+
branch?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse `<task-result>` XML from message content.
|
|
11
|
+
* Returns null if not found or missing required fields.
|
|
12
|
+
*/
|
|
13
|
+
export function parseTaskResult(content: string): TaskResult | null {
|
|
14
|
+
const match = content.match(/<task-result>([\s\S]*?)<\/task-result>/)
|
|
15
|
+
if (!match) return null
|
|
16
|
+
|
|
17
|
+
const inner = match[1]!
|
|
18
|
+
const status = inner.match(/<status>([\s\S]*?)<\/status>/)?.[1]?.trim()
|
|
19
|
+
const summary = inner.match(/<summary>([\s\S]*?)<\/summary>/)?.[1]?.trim()
|
|
20
|
+
const details = inner.match(/<details>([\s\S]*?)<\/details>/)?.[1]?.trim()
|
|
21
|
+
const branch = inner.match(/<branch>([\s\S]*?)<\/branch>/)?.[1]?.trim()
|
|
22
|
+
|
|
23
|
+
if (!status || !summary || !details) return null
|
|
24
|
+
|
|
25
|
+
return { status, summary, details, branch: branch || undefined }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format a TaskResult as clean readable text.
|
|
30
|
+
*/
|
|
31
|
+
export function formatTaskResult(from: string, result: TaskResult): string {
|
|
32
|
+
const lines = [
|
|
33
|
+
`[Result from ${from}]:`,
|
|
34
|
+
` Status: ${result.status}`,
|
|
35
|
+
` Summary: ${result.summary}`,
|
|
36
|
+
` Details: ${result.details}`,
|
|
37
|
+
]
|
|
38
|
+
if (result.branch) lines.push(` Branch: ${result.branch}`)
|
|
39
|
+
return lines.join("\n")
|
|
40
|
+
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import { markDelivered } from "./messaging"
|
|
3
|
+
import { parseTaskResult, formatTaskResult } from "./result-parser"
|
|
3
4
|
|
|
4
5
|
/** Truncate a string to maxLen chars, appending "..." if truncated. */
|
|
5
6
|
function truncate(s: string, maxLen: number): string {
|
|
@@ -82,7 +83,12 @@ export function buildLeadSystemPrompt(db: Database, teamId: string): string {
|
|
|
82
83
|
lines.push("", "--- Team Messages ---")
|
|
83
84
|
const MAX_MSG = 500
|
|
84
85
|
for (const msg of pendingMessages) {
|
|
85
|
-
|
|
86
|
+
const parsed = parseTaskResult(msg.content)
|
|
87
|
+
if (parsed) {
|
|
88
|
+
// Truncate details for system prompt — full content available via team_results
|
|
89
|
+
const truncatedResult = { ...parsed, details: truncate(parsed.details, 500) }
|
|
90
|
+
lines.push(formatTaskResult(msg.from_name, truncatedResult))
|
|
91
|
+
} else if (msg.content.length > MAX_MSG) {
|
|
86
92
|
lines.push(`[From ${msg.from_name}]: ${msg.content.slice(0, MAX_MSG)}... (use team_results to read full message)`)
|
|
87
93
|
} else {
|
|
88
94
|
lines.push(`[From ${msg.from_name}]: ${msg.content}`)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { requireTeamMember } from "./shared"
|
|
3
|
+
import { parseTaskResult, formatTaskResult } from "../result-parser"
|
|
3
4
|
|
|
4
5
|
/** Row shape for unread messages query. */
|
|
5
6
|
interface UnreadMessageRow {
|
|
@@ -39,6 +40,10 @@ export async function executeTeamResults(
|
|
|
39
40
|
const placeholders = ids.map(() => "?").join(", ")
|
|
40
41
|
deps.db.run(`UPDATE team_message SET read = 1 WHERE id IN (${placeholders})`, ids)
|
|
41
42
|
|
|
42
|
-
// Format output
|
|
43
|
-
return rows.map((r) =>
|
|
43
|
+
// Format output — parse structured task results when present
|
|
44
|
+
return rows.map((r) => {
|
|
45
|
+
const parsed = parseTaskResult(r.content)
|
|
46
|
+
if (parsed) return formatTaskResult(r.from_name, parsed)
|
|
47
|
+
return `[Message from ${r.from_name}]:\n${r.content}`
|
|
48
|
+
}).join("\n\n")
|
|
44
49
|
}
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -197,6 +197,14 @@ export async function executeTeamSpawn(
|
|
|
197
197
|
`Your agent type is "${args.agent}".`,
|
|
198
198
|
]
|
|
199
199
|
|
|
200
|
+
// Show other teammates so this agent knows who to message
|
|
201
|
+
const otherMembers = deps.db.query(
|
|
202
|
+
"SELECT name FROM team_member WHERE team_id = ? AND name != ? AND status NOT IN ('shutdown', 'error')"
|
|
203
|
+
).all(teamInfo.teamId, args.name) as Array<{ name: string }>
|
|
204
|
+
if (otherMembers.length > 0) {
|
|
205
|
+
context.push(`Other teammates: ${otherMembers.map(m => m.name).join(", ")}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
if (worktreeBranch && worktreeDir && !workspaceId) {
|
|
201
209
|
// Workspace binding failed — fallback to prompt-based CWD instruction
|
|
202
210
|
context.push(
|
|
@@ -276,7 +284,9 @@ export async function executeTeamSpawn(
|
|
|
276
284
|
context.push(
|
|
277
285
|
`${lastStep}. STOP. Do not send follow-up confirmations, status updates, or 'standing by' messages.`,
|
|
278
286
|
"",
|
|
279
|
-
"If you are blocked
|
|
287
|
+
"If you are blocked:",
|
|
288
|
+
"- Send ONE message to the lead via team_message describing the specific blocker.",
|
|
289
|
+
"- Do NOT attempt workarounds or make assumptions. Wait for the lead's response.",
|
|
280
290
|
"",
|
|
281
291
|
"Your plain text output is NOT visible to the team. You MUST use team_message to communicate.",
|
|
282
292
|
)
|