@hueyexe/opencode-ensemble 0.6.4 → 0.6.5
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 +54 -8
- package/package.json +1 -1
- package/src/client.ts +6 -0
- package/src/index.ts +5 -2
- package/src/schema.ts +2 -0
- package/src/tools/team-cleanup.ts +10 -3
- package/src/tools/team-spawn.ts +52 -8
- package/src/types.ts +5 -0
package/dist/index.js
CHANGED
|
@@ -15366,7 +15366,8 @@ var MIGRATIONS = [
|
|
|
15366
15366
|
ALTER TABLE team_member ADD COLUMN worktree_branch TEXT;`,
|
|
15367
15367
|
`ALTER TABLE team_member ADD COLUMN plan_approval TEXT NOT NULL DEFAULT 'none'
|
|
15368
15368
|
CHECK(plan_approval IN ('none', 'pending', 'approved', 'rejected'));`,
|
|
15369
|
-
`ALTER TABLE team ADD COLUMN lead_agent TEXT
|
|
15369
|
+
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
15370
|
+
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`
|
|
15370
15371
|
];
|
|
15371
15372
|
function applyMigrations(db) {
|
|
15372
15373
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
@@ -15426,6 +15427,11 @@ function wrapThrowingClient(raw) {
|
|
|
15426
15427
|
remove: throwing(r.worktree.remove.bind(r.worktree)),
|
|
15427
15428
|
list: throwing(r.worktree.list.bind(r.worktree)),
|
|
15428
15429
|
reset: throwing(r.worktree.reset.bind(r.worktree))
|
|
15430
|
+
},
|
|
15431
|
+
workspace: {
|
|
15432
|
+
create: throwing(r.experimental.workspace.create.bind(r.experimental.workspace)),
|
|
15433
|
+
remove: throwing(r.experimental.workspace.remove.bind(r.experimental.workspace)),
|
|
15434
|
+
list: throwing(r.experimental.workspace.list.bind(r.experimental.workspace))
|
|
15429
15435
|
}
|
|
15430
15436
|
};
|
|
15431
15437
|
}
|
|
@@ -15906,6 +15912,19 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15906
15912
|
} catch {}
|
|
15907
15913
|
}
|
|
15908
15914
|
}
|
|
15915
|
+
let workspaceId = null;
|
|
15916
|
+
if (worktreeDir && worktreeBranch) {
|
|
15917
|
+
try {
|
|
15918
|
+
log(`spawn:workspace:start name=${args.name}`);
|
|
15919
|
+
const wsResult = await withTimeout(deps.client.workspace.create({ branch: worktreeBranch }), getSpawnTimeout(), `workspace.create for "${args.name}"`);
|
|
15920
|
+
if (wsResult.data) {
|
|
15921
|
+
workspaceId = wsResult.data.id;
|
|
15922
|
+
}
|
|
15923
|
+
log(`spawn:workspace:done name=${args.name} id=${workspaceId}`);
|
|
15924
|
+
} catch (err) {
|
|
15925
|
+
log(`spawn:workspace:failed name=${args.name} err=${err instanceof Error ? err.message : String(err)}`);
|
|
15926
|
+
}
|
|
15927
|
+
}
|
|
15909
15928
|
const isReadOnly = args.agent === "plan" || args.agent === "explore";
|
|
15910
15929
|
const TEAM_TOOLS = ["team_message", "team_broadcast", "team_tasks_list", "team_tasks_add", "team_tasks_complete", "team_claim"];
|
|
15911
15930
|
const permission = isReadOnly ? [
|
|
@@ -15920,12 +15939,17 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15920
15939
|
parentID: sessionId,
|
|
15921
15940
|
title: `${args.name} (@${args.agent} teammate)`,
|
|
15922
15941
|
...permission ? { permission } : {},
|
|
15923
|
-
...
|
|
15942
|
+
...workspaceId ? { workspaceID: workspaceId } : {}
|
|
15924
15943
|
}), getSpawnTimeout(), `session.create for "${args.name}"`);
|
|
15925
15944
|
childSessionId = createResult.data?.id;
|
|
15926
15945
|
log(`spawn:session:done name=${args.name} sessionId=${childSessionId}`);
|
|
15927
15946
|
} catch (err) {
|
|
15928
15947
|
log(`spawn:session:failed name=${args.name} err=${err instanceof Error ? err.message : String(err)}`);
|
|
15948
|
+
if (workspaceId) {
|
|
15949
|
+
try {
|
|
15950
|
+
await deps.client.workspace.remove({ id: workspaceId });
|
|
15951
|
+
} catch {}
|
|
15952
|
+
}
|
|
15929
15953
|
if (worktreeDir) {
|
|
15930
15954
|
try {
|
|
15931
15955
|
await deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } });
|
|
@@ -15934,6 +15958,11 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15934
15958
|
throw new Error(`Failed to create session for teammate "${args.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
15935
15959
|
}
|
|
15936
15960
|
if (!childSessionId) {
|
|
15961
|
+
if (workspaceId) {
|
|
15962
|
+
try {
|
|
15963
|
+
await deps.client.workspace.remove({ id: workspaceId });
|
|
15964
|
+
} catch {}
|
|
15965
|
+
}
|
|
15937
15966
|
if (worktreeDir) {
|
|
15938
15967
|
try {
|
|
15939
15968
|
await deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } });
|
|
@@ -15943,15 +15972,19 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15943
15972
|
}
|
|
15944
15973
|
const planApproval = usePlanApproval ? "pending" : "none";
|
|
15945
15974
|
const now = Date.now();
|
|
15946
|
-
deps.db.run(`INSERT INTO team_member (team_id, name, session_id, agent, status, execution_status, model, prompt, worktree_dir, worktree_branch, plan_approval, time_created, time_updated)
|
|
15947
|
-
VALUES (?, ?, ?, ?, 'busy', 'starting', ?, ?, ?, ?, ?, ?, ?)`, [teamInfo.teamId, args.name, childSessionId, args.agent, args.model ?? null, args.prompt, worktreeDir, worktreeBranch, planApproval, now, now]);
|
|
15975
|
+
deps.db.run(`INSERT INTO team_member (team_id, name, session_id, agent, status, execution_status, model, prompt, worktree_dir, worktree_branch, workspace_id, plan_approval, time_created, time_updated)
|
|
15976
|
+
VALUES (?, ?, ?, ?, 'busy', 'starting', ?, ?, ?, ?, ?, ?, ?, ?)`, [teamInfo.teamId, args.name, childSessionId, args.agent, args.model ?? null, args.prompt, worktreeDir, worktreeBranch, workspaceId, planApproval, now, now]);
|
|
15948
15977
|
deps.registry.register(teamInfo.teamId, args.name, childSessionId);
|
|
15949
15978
|
const context = [
|
|
15950
15979
|
`You are "${args.name}", a teammate in team "${teamInfo.teamName}".`,
|
|
15951
15980
|
`Your agent type is "${args.agent}".`
|
|
15952
15981
|
];
|
|
15953
|
-
if (worktreeBranch) {
|
|
15954
|
-
context.push(`You are working on branch "${worktreeBranch}" in your own worktree
|
|
15982
|
+
if (worktreeBranch && worktreeDir && !workspaceId) {
|
|
15983
|
+
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}`);
|
|
15984
|
+
} else if (worktreeBranch && worktreeDir) {
|
|
15985
|
+
context.push(`You are working on branch "${worktreeBranch}" in your own isolated worktree.`, `Your changes are isolated from other teammates.`);
|
|
15986
|
+
} else if (worktreeBranch) {
|
|
15987
|
+
context.push(`You are working on branch "${worktreeBranch}". Your changes are isolated from other teammates.`);
|
|
15955
15988
|
}
|
|
15956
15989
|
if (usePlanApproval) {
|
|
15957
15990
|
context.push("", "IMPORTANT: You are in PLAN MODE.", "Read and explore the codebase, then send your implementation plan to the lead via team_message.", "Do NOT write or modify any files until the lead approves your plan.", "Wait for the lead's approval message before proceeding with implementation.");
|
|
@@ -15974,6 +16007,9 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15974
16007
|
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND session_id = ?", [teamInfo.teamId, childSessionId]);
|
|
15975
16008
|
deps.registry.unregister(childSessionId);
|
|
15976
16009
|
deps.client.session.abort({ sessionID: childSessionId }).catch(() => {});
|
|
16010
|
+
if (workspaceId) {
|
|
16011
|
+
deps.client.workspace.remove({ id: workspaceId }).catch(() => {});
|
|
16012
|
+
}
|
|
15977
16013
|
if (worktreeDir) {
|
|
15978
16014
|
deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }).catch(() => {});
|
|
15979
16015
|
}
|
|
@@ -16149,7 +16185,7 @@ async function executeTeamCleanup(deps, args, sessionId) {
|
|
|
16149
16185
|
throw new Error("This session is not in a team.");
|
|
16150
16186
|
if (teamInfo.role !== "lead")
|
|
16151
16187
|
throw new Error("Only the team lead can clean up the team.");
|
|
16152
|
-
const members = deps.db.query("SELECT name, session_id, status, worktree_dir, worktree_branch FROM team_member WHERE team_id = ?").all(teamInfo.teamId);
|
|
16188
|
+
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);
|
|
16153
16189
|
const active = members.filter((m) => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error");
|
|
16154
16190
|
if (active.length > 0 && !args.force) {
|
|
16155
16191
|
const names = active.map((m) => m.name).join(", ");
|
|
@@ -16164,9 +16200,16 @@ async function executeTeamCleanup(deps, args, sessionId) {
|
|
|
16164
16200
|
}
|
|
16165
16201
|
const branches = [];
|
|
16166
16202
|
for (const member of members) {
|
|
16203
|
+
if (member.workspace_id) {
|
|
16204
|
+
try {
|
|
16205
|
+
await deps.client.workspace.remove({ id: member.workspace_id });
|
|
16206
|
+
deps.db.run("UPDATE team_member SET workspace_id = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name]);
|
|
16207
|
+
} catch {}
|
|
16208
|
+
}
|
|
16167
16209
|
if (member.worktree_dir) {
|
|
16168
16210
|
try {
|
|
16169
16211
|
await deps.client.worktree.remove({ worktreeRemoveInput: { directory: member.worktree_dir } });
|
|
16212
|
+
deps.db.run("UPDATE team_member SET worktree_dir = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name]);
|
|
16170
16213
|
} catch {}
|
|
16171
16214
|
}
|
|
16172
16215
|
if (member.worktree_branch) {
|
|
@@ -16638,10 +16681,13 @@ var plugin = async (input) => {
|
|
|
16638
16681
|
output.env.ENSEMBLE_ROLE = teamInfo.role;
|
|
16639
16682
|
if (teamInfo.memberName) {
|
|
16640
16683
|
output.env.ENSEMBLE_MEMBER = teamInfo.memberName;
|
|
16641
|
-
const member = db.query("SELECT worktree_branch FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, teamInfo.memberName);
|
|
16684
|
+
const member = db.query("SELECT worktree_branch, worktree_dir FROM team_member WHERE team_id = ? AND name = ?").get(teamInfo.teamId, teamInfo.memberName);
|
|
16642
16685
|
if (member?.worktree_branch) {
|
|
16643
16686
|
output.env.ENSEMBLE_BRANCH = member.worktree_branch;
|
|
16644
16687
|
}
|
|
16688
|
+
if (member?.worktree_dir) {
|
|
16689
|
+
output.env.ENSEMBLE_WORKTREE_DIR = member.worktree_dir;
|
|
16690
|
+
}
|
|
16645
16691
|
}
|
|
16646
16692
|
},
|
|
16647
16693
|
tool: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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/client.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface RawClient {
|
|
|
25
25
|
session: { create: SdkMethod; promptAsync: SdkMethod; abort: SdkMethod; status: SdkMethod }
|
|
26
26
|
tui: { showToast: SdkMethod; selectSession: SdkMethod }
|
|
27
27
|
worktree: { create: SdkMethod; remove: SdkMethod; list: SdkMethod; reset: SdkMethod }
|
|
28
|
+
experimental: { workspace: { create: SdkMethod; remove: SdkMethod; list: SdkMethod } }
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -50,5 +51,10 @@ export function wrapThrowingClient(raw: unknown): PluginClient {
|
|
|
50
51
|
list: throwing(r.worktree.list.bind(r.worktree)),
|
|
51
52
|
reset: throwing(r.worktree.reset.bind(r.worktree)),
|
|
52
53
|
},
|
|
54
|
+
workspace: {
|
|
55
|
+
create: throwing(r.experimental.workspace.create.bind(r.experimental.workspace)),
|
|
56
|
+
remove: throwing(r.experimental.workspace.remove.bind(r.experimental.workspace)),
|
|
57
|
+
list: throwing(r.experimental.workspace.list.bind(r.experimental.workspace)),
|
|
58
|
+
},
|
|
53
59
|
} as PluginClient
|
|
54
60
|
}
|
package/src/index.ts
CHANGED
|
@@ -217,11 +217,14 @@ const plugin: Plugin = async (input) => {
|
|
|
217
217
|
output.env.ENSEMBLE_ROLE = teamInfo.role
|
|
218
218
|
if (teamInfo.memberName) {
|
|
219
219
|
output.env.ENSEMBLE_MEMBER = teamInfo.memberName
|
|
220
|
-
const member = db.query("SELECT worktree_branch FROM team_member WHERE team_id = ? AND name = ?")
|
|
221
|
-
.get(teamInfo.teamId, teamInfo.memberName) as { worktree_branch: string | null } | null
|
|
220
|
+
const member = db.query("SELECT worktree_branch, worktree_dir FROM team_member WHERE team_id = ? AND name = ?")
|
|
221
|
+
.get(teamInfo.teamId, teamInfo.memberName) as { worktree_branch: string | null; worktree_dir: string | null } | null
|
|
222
222
|
if (member?.worktree_branch) {
|
|
223
223
|
output.env.ENSEMBLE_BRANCH = member.worktree_branch
|
|
224
224
|
}
|
|
225
|
+
if (member?.worktree_dir) {
|
|
226
|
+
output.env.ENSEMBLE_WORKTREE_DIR = member.worktree_dir
|
|
227
|
+
}
|
|
225
228
|
}
|
|
226
229
|
},
|
|
227
230
|
|
package/src/schema.ts
CHANGED
|
@@ -78,6 +78,8 @@ export const MIGRATIONS: string[] = [
|
|
|
78
78
|
CHECK(plan_approval IN ('none', 'pending', 'approved', 'rejected'));`,
|
|
79
79
|
// Migration 5: Track lead's agent mode so message delivery preserves it
|
|
80
80
|
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
81
|
+
// Migration 6: Track workspace ID for worktree-session binding
|
|
82
|
+
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`,
|
|
81
83
|
]
|
|
82
84
|
|
|
83
85
|
/**
|
|
@@ -15,8 +15,8 @@ export async function executeTeamCleanup(
|
|
|
15
15
|
if (!teamInfo) throw new Error("This session is not in a team.")
|
|
16
16
|
if (teamInfo.role !== "lead") throw new Error("Only the team lead can clean up the team.")
|
|
17
17
|
|
|
18
|
-
const members = deps.db.query("SELECT name, session_id, status, worktree_dir, worktree_branch FROM team_member WHERE team_id = ?")
|
|
19
|
-
.all(teamInfo.teamId) as Array<{ name: string; session_id: string; status: string; worktree_dir: string | null; worktree_branch: string | null }>
|
|
18
|
+
const members = deps.db.query("SELECT name, session_id, status, worktree_dir, worktree_branch, workspace_id FROM team_member WHERE team_id = ?")
|
|
19
|
+
.all(teamInfo.teamId) as Array<{ name: string; session_id: string; status: string; worktree_dir: string | null; worktree_branch: string | null; workspace_id: string | null }>
|
|
20
20
|
|
|
21
21
|
const active = members.filter(m => m.status !== "shutdown" && m.status !== "shutdown_requested" && m.status !== "error")
|
|
22
22
|
|
|
@@ -34,12 +34,19 @@ export async function executeTeamCleanup(
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Remove worktrees and collect branches for merging
|
|
37
|
+
// Remove workspaces, worktrees, and collect branches for merging
|
|
38
38
|
const branches: string[] = []
|
|
39
39
|
for (const member of members) {
|
|
40
|
+
if (member.workspace_id) {
|
|
41
|
+
try {
|
|
42
|
+
await deps.client.workspace.remove({ id: member.workspace_id })
|
|
43
|
+
deps.db.run("UPDATE team_member SET workspace_id = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name])
|
|
44
|
+
} catch { /* best effort — workspace may already be gone */ }
|
|
45
|
+
}
|
|
40
46
|
if (member.worktree_dir) {
|
|
41
47
|
try {
|
|
42
48
|
await deps.client.worktree.remove({ worktreeRemoveInput: { directory: member.worktree_dir } })
|
|
49
|
+
deps.db.run("UPDATE team_member SET worktree_dir = NULL WHERE team_id = ? AND name = ?", [teamInfo.teamId, member.name])
|
|
43
50
|
} catch { /* best effort — worktree may already be gone */ }
|
|
44
51
|
}
|
|
45
52
|
if (member.worktree_branch) {
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -78,6 +78,26 @@ export async function executeTeamSpawn(
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Create workspace from worktree branch — links session to worktree directory.
|
|
82
|
+
// OQ-workspace: assumes workspace.create({ branch }) auto-links to the worktree at that branch.
|
|
83
|
+
let workspaceId: string | null = null
|
|
84
|
+
if (worktreeDir && worktreeBranch) {
|
|
85
|
+
try {
|
|
86
|
+
log(`spawn:workspace:start name=${args.name}`)
|
|
87
|
+
const wsResult = await withTimeout(
|
|
88
|
+
deps.client.workspace.create({ branch: worktreeBranch }),
|
|
89
|
+
getSpawnTimeout(), `workspace.create for "${args.name}"`
|
|
90
|
+
)
|
|
91
|
+
if (wsResult.data) {
|
|
92
|
+
workspaceId = wsResult.data.id
|
|
93
|
+
}
|
|
94
|
+
log(`spawn:workspace:done name=${args.name} id=${workspaceId}`)
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log(`spawn:workspace:failed name=${args.name} err=${err instanceof Error ? err.message : String(err)}`)
|
|
97
|
+
// Non-fatal — prompt-based CWD instruction is the fallback
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
81
101
|
// Permission rules on session.create are the hard gate (server-enforced).
|
|
82
102
|
// For read-only agents, deny write tools and explicitly allow team tools
|
|
83
103
|
// so the agent's built-in restrictive permissions don't block them.
|
|
@@ -91,7 +111,8 @@ export async function executeTeamSpawn(
|
|
|
91
111
|
]
|
|
92
112
|
: undefined
|
|
93
113
|
|
|
94
|
-
// Create child session —
|
|
114
|
+
// Create child session — bind to workspace if available (server-enforced CWD isolation).
|
|
115
|
+
// Falls back to no workspace binding if workspace.create failed.
|
|
95
116
|
let childSessionId: string | undefined
|
|
96
117
|
try {
|
|
97
118
|
log(`spawn:session:start name=${args.name}`)
|
|
@@ -100,7 +121,7 @@ export async function executeTeamSpawn(
|
|
|
100
121
|
parentID: sessionId,
|
|
101
122
|
title: `${args.name} (@${args.agent} teammate)`,
|
|
102
123
|
...(permission ? { permission } : {}),
|
|
103
|
-
...(
|
|
124
|
+
...(workspaceId ? { workspaceID: workspaceId } : {}),
|
|
104
125
|
}),
|
|
105
126
|
getSpawnTimeout(), `session.create for "${args.name}"`
|
|
106
127
|
)
|
|
@@ -108,7 +129,10 @@ export async function executeTeamSpawn(
|
|
|
108
129
|
log(`spawn:session:done name=${args.name} sessionId=${childSessionId}`)
|
|
109
130
|
} catch (err) {
|
|
110
131
|
log(`spawn:session:failed name=${args.name} err=${err instanceof Error ? err.message : String(err)}`)
|
|
111
|
-
// Rollback worktree if session creation failed
|
|
132
|
+
// Rollback workspace and worktree if session creation failed
|
|
133
|
+
if (workspaceId) {
|
|
134
|
+
try { await deps.client.workspace.remove({ id: workspaceId }) } catch { /* best effort */ }
|
|
135
|
+
}
|
|
112
136
|
if (worktreeDir) {
|
|
113
137
|
try { await deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }) } catch { /* best effort */ }
|
|
114
138
|
}
|
|
@@ -116,6 +140,9 @@ export async function executeTeamSpawn(
|
|
|
116
140
|
}
|
|
117
141
|
|
|
118
142
|
if (!childSessionId) {
|
|
143
|
+
if (workspaceId) {
|
|
144
|
+
try { await deps.client.workspace.remove({ id: workspaceId }) } catch { /* best effort */ }
|
|
145
|
+
}
|
|
119
146
|
if (worktreeDir) {
|
|
120
147
|
try { await deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }) } catch { /* best effort */ }
|
|
121
148
|
}
|
|
@@ -126,9 +153,9 @@ export async function executeTeamSpawn(
|
|
|
126
153
|
const planApproval = usePlanApproval ? "pending" : "none"
|
|
127
154
|
const now = Date.now()
|
|
128
155
|
deps.db.run(
|
|
129
|
-
`INSERT INTO team_member (team_id, name, session_id, agent, status, execution_status, model, prompt, worktree_dir, worktree_branch, plan_approval, time_created, time_updated)
|
|
130
|
-
VALUES (?, ?, ?, ?, 'busy', 'starting', ?, ?, ?, ?, ?, ?, ?)`,
|
|
131
|
-
[teamInfo.teamId, args.name, childSessionId, args.agent, args.model ?? null, args.prompt, worktreeDir, worktreeBranch, planApproval, now, now]
|
|
156
|
+
`INSERT INTO team_member (team_id, name, session_id, agent, status, execution_status, model, prompt, worktree_dir, worktree_branch, workspace_id, plan_approval, time_created, time_updated)
|
|
157
|
+
VALUES (?, ?, ?, ?, 'busy', 'starting', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
158
|
+
[teamInfo.teamId, args.name, childSessionId, args.agent, args.model ?? null, args.prompt, worktreeDir, worktreeBranch, workspaceId, planApproval, now, now]
|
|
132
159
|
)
|
|
133
160
|
|
|
134
161
|
// Register in memory
|
|
@@ -140,8 +167,22 @@ export async function executeTeamSpawn(
|
|
|
140
167
|
`Your agent type is "${args.agent}".`,
|
|
141
168
|
]
|
|
142
169
|
|
|
143
|
-
if (worktreeBranch) {
|
|
144
|
-
|
|
170
|
+
if (worktreeBranch && worktreeDir && !workspaceId) {
|
|
171
|
+
// Workspace binding failed — fallback to prompt-based CWD instruction
|
|
172
|
+
context.push(
|
|
173
|
+
`You are working on branch "${worktreeBranch}" in your own worktree at: ${worktreeDir}`,
|
|
174
|
+
`Your changes are isolated from other teammates.`,
|
|
175
|
+
`IMPORTANT: All file operations and shell commands MUST target your worktree directory.`,
|
|
176
|
+
`Before running shell commands, cd to: ${worktreeDir}`,
|
|
177
|
+
)
|
|
178
|
+
} else if (worktreeBranch && worktreeDir) {
|
|
179
|
+
// Workspace binding active — server handles CWD
|
|
180
|
+
context.push(
|
|
181
|
+
`You are working on branch "${worktreeBranch}" in your own isolated worktree.`,
|
|
182
|
+
`Your changes are isolated from other teammates.`,
|
|
183
|
+
)
|
|
184
|
+
} else if (worktreeBranch) {
|
|
185
|
+
context.push(`You are working on branch "${worktreeBranch}". Your changes are isolated from other teammates.`)
|
|
145
186
|
}
|
|
146
187
|
|
|
147
188
|
// Plan approval mode — teammate must send plan before writing
|
|
@@ -200,6 +241,9 @@ export async function executeTeamSpawn(
|
|
|
200
241
|
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND session_id = ?", [teamInfo.teamId, childSessionId])
|
|
201
242
|
deps.registry.unregister(childSessionId)
|
|
202
243
|
deps.client.session.abort({ sessionID: childSessionId }).catch(() => { /* best effort */ })
|
|
244
|
+
if (workspaceId) {
|
|
245
|
+
deps.client.workspace.remove({ id: workspaceId }).catch(() => { /* best effort */ })
|
|
246
|
+
}
|
|
203
247
|
if (worktreeDir) {
|
|
204
248
|
deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }).catch(() => { /* best effort */ })
|
|
205
249
|
}
|
package/src/types.ts
CHANGED
|
@@ -62,6 +62,11 @@ export interface PluginClient {
|
|
|
62
62
|
list(): Promise<{ data?: Array<{ name: string; branch: string; directory: string }> }>
|
|
63
63
|
reset(options: { worktreeResetInput?: { directory: string } }): Promise<unknown>
|
|
64
64
|
}
|
|
65
|
+
workspace: {
|
|
66
|
+
create(options: { id?: string; type?: string; branch?: string | null; extra?: unknown | null }): Promise<{ data?: { id: string; type: string; branch: string | null; directory: string | null; projectID: string } }>
|
|
67
|
+
remove(options: { id: string }): Promise<unknown>
|
|
68
|
+
list(): Promise<{ data?: Array<{ id: string; type: string; branch: string | null; directory: string | null; projectID: string }> }>
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|