feat: multi-work agent
CI / verify (pull_request) Failing after 9s

This commit is contained in:
2026-09-17 21:09:19 +02:00
parent ba9fddad9d
commit 5cf0beb74b
20 changed files with 2907 additions and 299 deletions
+3 -3
View File
@@ -32,7 +32,8 @@ export const settings = z.discriminatedUnion("mode", [
"good-hi": assignment,
"deep-lo": assignment,
"deep-hi": assignment,
coordinate: assignment,
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
coordinate: assignment.optional(),
}),
}),
]);
@@ -44,5 +45,4 @@ export const gptAgents = {
"good-hi": { model: "gpt-5.6-sol", effort: "high" },
"deep-lo": { model: "gpt-6-astra", effort: "low" },
"deep-hi": { model: "gpt-6-astra", effort: "high" },
coordinate: { model: "gpt-6-astra", effort: "medium" },
} satisfies Record<(typeof workerNames)[number] | "coordinate", z.infer<typeof assignment>>;
} satisfies Record<(typeof workerNames)[number], z.infer<typeof assignment>>;
+231
View File
@@ -0,0 +1,231 @@
import { execFile } from "node:child_process";
import { realpath } from "node:fs/promises";
import { z } from "zod";
export async function command(args: string[], cwd: string, signal: AbortSignal) {
return new Promise<string>((resolve, reject) => {
execFile(
args[0]!,
args.slice(1),
{ cwd, signal, timeout: 30_000, maxBuffer: 2_000_000 },
(error, stdout, stderr) => {
if (error) reject(new Error(`${args[0]} failed: ${stderr || error.message}`));
else resolve(stdout.trim());
},
);
});
}
const inventory = z.object({
result: z.object({
source: z.object({
repo_key: z.string(),
repo_root: z.string(),
source_checkout_path: z.string(),
source_workspace_id: z.string(),
}),
worktrees: z.array(
z.object({
path: z.string(),
is_linked_worktree: z.boolean(),
is_bare: z.boolean(),
open_workspace_id: z.string().nullish(),
}),
),
}),
});
const workspaces = z.object({
result: z.object({ workspaces: z.array(z.object({ workspace_id: z.string() })) }),
});
const created = z.object({
result: z.object({
workspace: z.object({ workspace_id: z.string() }),
worktree: z.object({
path: z.string(),
branch: z.string(),
is_linked_worktree: z.literal(true),
}),
}),
});
export const rootSchema = z.object({
directory: z.string(),
workspaceID: z.string(),
repo: z.string(),
});
export type HerdrRoot = z.infer<typeof rootSchema>;
// The command seam also lets tests use an isolated CLI without touching a live server.
export class Herdr {
constructor(private readonly run = command) {}
async root(directory: string, signal: AbortSignal): Promise<HerdrRoot | undefined> {
signal = AbortSignal.any([signal, AbortSignal.timeout(5_000)]);
const canonical = await realpath(directory);
const list = workspaces.parse(
JSON.parse(await this.run(["herdr", "workspace", "list"], canonical, signal)),
);
const matches: HerdrRoot[] = [];
for (const workspace of list.result.workspaces) {
let value: z.infer<typeof inventory>;
try {
value = inventory.parse(
JSON.parse(
await this.run(
["herdr", "worktree", "list", "--workspace", workspace.workspace_id],
canonical,
signal,
),
),
);
} catch (error) {
if (signal.aborted) throw error;
// Non-repository workspaces cannot provide a worktree inventory.
continue;
}
const source = value.result.source;
if (source.source_workspace_id !== workspace.workspace_id) continue;
if (
(await realpath(source.source_checkout_path)) !== canonical ||
(await realpath(source.repo_root)) !== canonical
)
continue;
const checkout = value.result.worktrees.find(
(item) =>
item.open_workspace_id === workspace.workspace_id &&
!item.is_linked_worktree &&
!item.is_bare,
);
if (!checkout || (await realpath(checkout.path)) !== canonical) continue;
const repo = await realpath(source.repo_key);
const gitRepo = await this.run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
canonical,
signal,
);
if ((await realpath(gitRepo)) !== repo) continue;
matches.push({ directory: canonical, workspaceID: workspace.workspace_id, repo });
}
// Never guess which Herdr workspace owns a duplicated checkout.
return matches.length === 1 ? matches[0] : undefined;
}
async create(root: HerdrRoot, base: string, branch: string, signal: AbortSignal) {
const current = await this.root(root.directory, signal);
if (!current || current.workspaceID !== root.workspaceID || current.repo !== root.repo)
throw new Error("Herdr root workspace is no longer eligible");
const ref = await this.run(
["git", "rev-parse", "--symbolic-full-name", "--verify", "--end-of-options", base],
root.directory,
signal,
);
if (!ref.startsWith("refs/heads/") && !ref.startsWith("refs/remotes/"))
throw new Error(
"base must identify a locally available branch or remote-tracking branch",
);
const commit = await this.run(
["git", "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`],
root.directory,
signal,
);
await this.run(["git", "check-ref-format", "--branch", branch], root.directory, signal);
if (
await this.run(
["git", "branch", "--list", "--format=%(refname)", branch],
root.directory,
signal,
)
)
throw new Error(`Destination branch already exists: ${branch}`);
const response = created.parse(
JSON.parse(
await this.run(
[
"herdr",
"worktree",
"create",
// Pin the repository even if a user repoints the workspace after discovery.
"--cwd",
root.directory,
"--branch",
branch,
"--base",
commit,
"--no-focus",
],
root.directory,
signal,
),
),
);
const directory = await realpath(response.result.worktree.path);
const workspaceID = response.result.workspace.workspace_id;
try {
if (
response.result.worktree.branch !== branch ||
directory === root.directory ||
workspaceID === root.workspaceID
)
throw new Error("Herdr returned an unexpected checkout");
const actual = await this.run(["git", "rev-parse", "HEAD"], directory, signal);
const actualBranch = await this.run(
["git", "symbolic-ref", "--short", "HEAD"],
directory,
signal,
);
const repo = await this.run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
directory,
signal,
);
if (
actual !== commit ||
actualBranch !== branch ||
(await realpath(repo)) !== root.repo
)
throw new Error(
"Created checkout does not match the requested repository and base",
);
await this.verify(directory, workspaceID, root.repo, branch, signal);
} catch (error) {
throw new Error(
`Retained Herdr workspace ${workspaceID}, checkout ${directory}, branch ${branch}`,
{ cause: error },
);
}
return { directory, workspaceID, branch };
}
async verify(
directory: string,
workspaceID: string,
repo: string,
branch: string,
signal: AbortSignal,
) {
const value = inventory.parse(
JSON.parse(
await this.run(
["herdr", "worktree", "list", "--workspace", workspaceID],
directory,
signal,
),
),
);
const checkout = value.result.worktrees.find(
(item) => item.open_workspace_id === workspaceID && item.is_linked_worktree,
);
if (
!checkout ||
value.result.source.source_workspace_id !== workspaceID ||
(await realpath(value.result.source.source_checkout_path)) !== directory ||
(await realpath(checkout.path)) !== directory ||
(await realpath(value.result.source.repo_key)) !== repo
)
throw new Error("Bound Herdr checkout is no longer available");
if (
(await this.run(["git", "symbolic-ref", "--short", "HEAD"], directory, signal)) !==
branch
)
throw new Error("Bound Herdr checkout changed branches");
}
}
+333 -272
View File
@@ -8,6 +8,10 @@ import { descriptions, routingGuide } from "./routing.js";
import { QuotaTracker } from "./quota.js";
import { QuotaRpc } from "./quota-rpc.js";
import { playwrightMcpCommand } from "./playwright.js";
import { Plugin as EffectPlugin } from "@opencode/plugin/effect";
import { fromPromise } from "@opencode/plugin/promise/adapter";
import { Effect } from "effect";
import { bindingKey, bindingSchema, installMultiWork } from "./multi-work.js";
// Permission actions use wildcards, not filesystem globs.
function matches(value: string, pattern: string) {
@@ -41,296 +45,353 @@ const todos = z.array(
}),
);
export default Plugin.define({
id: "stanp-opencode",
async setup(context) {
const selected = settings.parse({ mode: "gpt", ...context.options });
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
let inherited: Agent.Info["permissions"] = Agent.Info.default(
Agent.ID.make("build"),
).permissions;
let permissionKey = JSON.stringify(inherited);
let hasCodegraph = false;
let codegraphTools: string[] = [];
let agentTransformReady = false;
const controller = new AbortController();
let quotaRegistration: RpcRegistration<typeof QuotaRpc> | undefined;
const quota = new QuotaTracker(context, async () => {
await quotaRegistration?.events.emit("updated", {});
});
quotaRegistration = await context.rpc.register(QuotaRpc, {
status: async () => quota.status() ?? null,
});
await quota.init();
// Subscribe before the first read: configuration can publish its models while
// asynchronous setup is running.
const updates = (async () => {
for await (const event of context.event.subscribe({ signal: controller.signal })) {
if (event.location) {
const workspaceID =
"workspaceID" in event.location ? event.location.workspaceID : undefined;
if (
event.location.directory !== context.location.directory ||
workspaceID !== context.location.workspaceID
)
continue;
const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permissions"]) => void) =>
Plugin.define({
id: "stanp-opencode",
async setup(context) {
const selected = settings.parse({ mode: "gpt", ...context.options });
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
let inherited: Agent.Info["permissions"] = Agent.Info.default(
Agent.ID.make("build"),
).permissions;
let permissionKey = JSON.stringify(inherited);
let hasCodegraph = false;
let codegraphTools: string[] = [];
let agentTransformReady = false;
const controller = new AbortController();
let quotaRegistration: RpcRegistration<typeof QuotaRpc> | undefined;
const quota = new QuotaTracker(context, async () => {
await quotaRegistration?.events.emit("updated", {});
});
quotaRegistration = await context.rpc.register(QuotaRpc, {
status: async () => quota.status() ?? null,
});
await quota.init();
// Subscribe before the first read: configuration can publish its models while
// asynchronous setup is running.
const updates = (async () => {
for await (const event of context.event.subscribe({ signal: controller.signal })) {
if (event.location) {
const workspaceID =
"workspaceID" in event.location
? event.location.workspaceID
: undefined;
if (
event.location.directory !== context.location.directory ||
workspaceID !== context.location.workspaceID
)
continue;
}
if (event.type === "model.updated") {
models = await discover(context, selected.provider);
await context.agent.reload();
} else if (event.type === "agent.updated") {
// The configured build transform may run after ours. Read the final
// registry, then replay only when its effective permissions change.
const { data } = await context.agent.list();
const permissions =
data.find((agent) => agent.id === "build")?.permissions ??
Agent.Info.default(Agent.ID.make("build")).permissions;
const key = JSON.stringify(permissions);
if (key === permissionKey) continue;
inherited = permissions;
permissionsChanged(inherited);
permissionKey = key;
await context.agent.reload();
} else if (
event.type === "session.execution.succeeded" ||
event.type === "session.execution.failed" ||
event.type === "session.execution.interrupted" ||
event.type === "session.idle"
) {
await quota.complete(event.data.sessionID);
} else if (event.type === "credential.updated") {
await quota.authChanged();
} else if (
event.type === "credential.switched" &&
String(event.data.integrationID) === "openai"
) {
await quota.authChanged(true);
}
}
if (event.type === "model.updated") {
models = await discover(context, selected.provider);
await context.agent.reload();
} else if (event.type === "agent.updated") {
// The configured build transform may run after ours. Read the final
// registry, then replay only when its effective permissions change.
const { data } = await context.agent.list();
const permissions =
data.find((agent) => agent.id === "build")?.permissions ??
Agent.Info.default(Agent.ID.make("build")).permissions;
const key = JSON.stringify(permissions);
if (key === permissionKey) continue;
inherited = permissions;
permissionKey = key;
await context.agent.reload();
} else if (
event.type === "session.execution.succeeded" ||
event.type === "session.execution.failed" ||
event.type === "session.execution.interrupted" ||
event.type === "session.idle"
) {
await quota.complete(event.data.sessionID);
} else if (event.type === "credential.updated") {
await quota.authChanged();
} else if (
event.type === "credential.switched" &&
String(event.data.integrationID) === "openai"
) {
await quota.authChanged(true);
}
}
})().catch((error) => {
if (!controller.signal.aborted)
console.error("stanp-opencode model update failed", error);
});
models = await discover(context, selected.provider);
const { data: initialAgents } = await context.agent.list();
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
permissionKey = JSON.stringify(inherited);
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
let workers: (typeof workerNames)[number][] = [];
const active = new Set<string>();
const playwrightCommand = selected.playwright
? await playwrightMcpCommand(context.location.directory)
: undefined;
})().catch((error) => {
if (!controller.signal.aborted)
console.error("stanp-opencode model update failed", error);
});
models = await discover(context, selected.provider);
const { data: initialAgents } = await context.agent.list();
inherited =
initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
permissionsChanged(inherited);
permissionKey = JSON.stringify(inherited);
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
let workers: (typeof workerNames)[number][] = [];
const active = new Set<string>();
const playwrightCommand = selected.playwright
? await playwrightMcpCommand(context.location.directory)
: undefined;
await context.mcp.transform((editor) => {
if (playwrightCommand && !editor.get("playwright"))
editor.set("playwright", {
type: "local",
command: playwrightCommand,
timeout: { startup: 300_000 },
});
hasCodegraph = editor.get("codegraph") !== undefined;
if (hasCodegraph)
editor.update("codegraph", (server) => {
server.codemode = false;
});
});
await context.tool.transform((editor) => {
const next = editor
.list()
.map((tool) => tool.id)
.filter((id) => id.startsWith("codegraph_"))
.sort();
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
codegraphTools = next;
if (agentTransformReady)
queueMicrotask(() => {
void context.agent
.reload()
.catch((error) =>
console.error("stanp-opencode agent reload failed", error),
await context.mcp.transform((editor) => {
if (playwrightCommand && !editor.get("playwright"))
editor.set("playwright", {
type: "local",
command: playwrightCommand,
timeout: { startup: 300_000 },
});
hasCodegraph = editor.get("codegraph") !== undefined;
if (hasCodegraph)
editor.update("codegraph", (server) => {
server.codemode = false;
});
});
await context.tool.transform((editor) => {
const next = editor
.list()
.map((tool) => tool.id)
.filter((id) => id.startsWith("codegraph_"))
.sort();
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
codegraphTools = next;
if (agentTransformReady)
queueMicrotask(() => {
void context.agent
.reload()
.catch((error) =>
console.error("stanp-opencode agent reload failed", error),
);
});
});
await context.agent.transform((editor) => {
const deep = models.has(assignments["deep-lo"].model);
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
({ model }) => models.has(model),
)
? candidates
: [];
active.clear();
for (const name of workers) active.add(name);
if (workers.length === 0) return;
for (const name of workers) {
const assignment = assignments[name];
const available = models.get(assignment.model)!;
editor.update(name, (agent) => {
const hasVariant = available.model.variants.some(
({ id }) => id === assignment.effort,
);
});
});
await context.agent.transform((editor) => {
const deep = selected.mode === "custom" || models.has("gpt-6-astra");
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
const coordinate =
selected.mode === "gpt" && !deep
? { model: "gpt-5.6-sol", effort: "medium" }
: assignments.coordinate;
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
({ model }) => models.has(model),
)
? candidates
: [];
active.clear();
for (const name of workers) active.add(name);
if (workers.length === 0) return;
for (const name of workers) {
const assignment = assignments[name];
const available = models.get(assignment.model)!;
editor.update(name, (agent) => {
agent.name = Agent.Name.make(name);
agent.mode = "subagent";
agent.hidden = false;
agent.description = descriptions[name];
agent.model = {
providerID: Provider.ID.make(available.model.providerID),
id: Model.ID.make(available.model.id),
};
if (hasVariant)
agent.model.variant = Model.VariantID.make(assignment.effort);
agent.request = {
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
headers: {},
body: {},
};
if (selected.lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.permissions = [
...structuredClone(inherited),
{ action: "subagent", resource: "*", effect: "deny" },
];
});
}
editor.update("coordinate", (agent) => {
const available = models.get(coordinate.model)!;
const hasVariant = available.model.variants.some(
({ id }) => id === assignment.effort,
({ id }) => id === coordinate.effort,
);
agent.name = Agent.Name.make(name);
agent.mode = "subagent";
agent.name = Agent.Name.make("coordinate");
agent.mode = "primary";
agent.hidden = false;
agent.description = descriptions[name];
agent.description =
"Read, plan, and coordinate specialist workers; never implement directly.";
agent.model = {
providerID: Provider.ID.make(available.model.providerID),
id: Model.ID.make(available.model.id),
};
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
agent.request = {
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
headers: {},
body: {},
};
if (selected.lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.system = routingGuide(deep);
agent.permissions = [
...structuredClone(inherited),
{ action: "subagent", resource: "*", effect: "deny" },
{ action: "*", resource: "*", effect: "deny" },
...reading.flatMap((action) =>
inherited
.filter((rule) => matches(action, rule.action))
.map((rule) => ({ ...rule, action })),
),
...(hasCodegraph
? codegraphTools.flatMap((action) =>
inherited
.filter((rule) => matches(action, rule.action))
.map((rule) => ({ ...rule, action })),
)
: []),
...workers.map((resource) => ({
action: "subagent",
resource,
effect: "allow" as const,
})),
];
});
}
editor.update("coordinate", (agent) => {
const available = models.get(coordinate.model)!;
const hasVariant = available.model.variants.some(
({ id }) => id === coordinate.effort,
);
agent.name = Agent.Name.make("coordinate");
agent.mode = "primary";
agent.hidden = false;
agent.description =
"Read, plan, and coordinate specialist workers; never implement directly.";
agent.model = {
providerID: Provider.ID.make(available.model.providerID),
id: Model.ID.make(available.model.id),
};
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
agent.request = {
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
headers: {},
body: {},
};
agent.system = routingGuide(deep);
agent.permissions = [
{ action: "*", resource: "*", effect: "deny" },
...reading.flatMap((action) =>
inherited
.filter((rule) => matches(action, rule.action))
.map((rule) => ({ ...rule, action })),
),
...(hasCodegraph
? codegraphTools.flatMap((action) =>
inherited
.filter((rule) => matches(action, rule.action))
.map((rule) => ({ ...rule, action })),
)
: []),
...workers.map((resource) => ({
action: "subagent",
resource,
effect: "allow" as const,
})),
];
});
});
agentTransformReady = true;
async function assertTodo(agentID: Agent.ID, action: string) {
const { data: agent } = await context.agent.get({ agentID });
const effect =
agent.permissions.findLast(
(rule) => matches(action, rule.action) && matches("*", rule.resource),
)?.effect ?? "ask";
if (effect !== "allow")
throw new Error(
`${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`,
);
}
await context.tool.transform((editor) => {
if (!editor.get("todowrite"))
editor.add({
name: "todowrite",
description:
"Replace this session's todo list to track the work and its progress.",
input: z.object({ todos }),
options: { codemode: false },
async execute(input, tool) {
await assertTodo(tool.agent, "todowrite");
await context.storage.set(`todos:${tool.sessionID}`, input.todos);
return { content: JSON.stringify(input.todos) };
},
});
if (!editor.get("todoread"))
editor.add({
name: "todoread",
description: "Read this session's todo list.",
input: z.object({}),
options: { codemode: false },
async execute(_input, tool) {
await assertTodo(tool.agent, "todoread");
const stored = await context.storage.get(`todos:${tool.sessionID}`);
return {
content: JSON.stringify(
stored === undefined ? [] : todos.parse(stored),
),
};
},
});
});
await context.session.hook("context", (event) => {
if (workerNames.some((name) => name === event.agent)) {
delete event.tools.subagent;
return;
agentTransformReady = true;
async function assertTodo(agentID: Agent.ID, action: string) {
const { data: agent } = await context.agent.get({ agentID });
const effect =
agent.permissions.findLast(
(rule) => matches(action, rule.action) && matches("*", rule.resource),
)?.effect ?? "ask";
if (effect !== "allow")
throw new Error(
`${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`,
);
}
const tool = event.tools.subagent;
if (event.agent === "coordinate" && workers.length === 0) {
delete event.tools.subagent;
return;
}
if (event.agent !== "coordinate" || !tool) return;
tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`;
// The executable definition remains native; constrain only this request's schema.
const properties = z.record(z.string(), z.unknown()).parse(tool.input.properties ?? {});
tool.input = {
...tool.input,
properties: { ...properties, agent: { type: "string", enum: workers } },
await context.tool.transform((editor) => {
if (!editor.get("todowrite"))
editor.add({
name: "todowrite",
description:
"Replace this session's todo list to track the work and its progress.",
input: z.object({ todos }),
options: { codemode: false },
async execute(input, tool) {
await assertTodo(tool.agent, "todowrite");
await context.storage.set(`todos:${tool.sessionID}`, input.todos);
return { content: JSON.stringify(input.todos) };
},
});
if (!editor.get("todoread"))
editor.add({
name: "todoread",
description: "Read this session's todo list.",
input: z.object({}),
options: { codemode: false },
async execute(_input, tool) {
await assertTodo(tool.agent, "todoread");
const stored = await context.storage.get(`todos:${tool.sessionID}`);
return {
content: JSON.stringify(
stored === undefined ? [] : todos.parse(stored),
),
};
},
});
});
await context.session.hook("context", (event) => {
if (workerNames.some((name) => name === event.agent)) {
delete event.tools.subagent;
return;
}
const tool = event.tools.subagent;
if (event.agent === "coordinate" && workers.length === 0) {
delete event.tools.subagent;
return;
}
if (event.agent !== "coordinate" || !tool) return;
tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`;
// The executable definition remains native; constrain only this request's schema.
const properties = z
.record(z.string(), z.unknown())
.parse(tool.input.properties ?? {});
tool.input = {
...tool.input,
properties: { ...properties, agent: { type: "string", enum: workers } },
};
});
await context.session.hook("model.request", async (event) =>
quota.start(event.sessionID),
);
await context.tool.hook("execute.before", (event) => {
if (event.tool !== "subagent") return;
const { agent } = delegation.parse(event.input);
if (agent === "multi-work") throw new Error("multi-work is primary only");
if (agent === "coordinate" && event.agent !== "multi-work")
throw new Error(
"coordinate cannot run as a subagent without a ready worktree binding",
);
if (workerNames.some((name) => name === event.agent))
throw new Error("Workers cannot delegate tasks");
if (event.agent === "coordinate" && !active.has(agent))
throw new Error("coordinate may delegate only to its available workers");
});
// Commands can create child sessions without going through the subagent tool.
await context.session.hook("prompt", async (event) => {
const child = await context.session.get({ sessionID: event.sessionID });
if (!child.parentID) return;
if (child.agent === "multi-work") throw new Error("multi-work is primary only");
const parent = await context.session.get({ sessionID: child.parentID });
if (child.agent === "coordinate") {
const binding = bindingSchema.safeParse(
await context.storage.get(bindingKey(event.sessionID)),
);
if (
!binding.success ||
!binding.data.ready ||
binding.data.child !== event.sessionID ||
binding.data.owner !== child.parentID ||
binding.data.projectID !== child.projectID ||
binding.data.projectID !== parent.projectID ||
parent.agent !== "multi-work" ||
child.location.directory !== binding.data.directory ||
parent.location.directory !== binding.data.root.directory
)
throw new Error(
"coordinate cannot run as a subagent without a ready worktree binding",
);
}
if (
parent.agent === "multi-work" &&
child.agent !== "coordinate" &&
(child.agent !== "fast-lo" ||
child.location.directory !== parent.location.directory)
)
throw new Error(
"multi-work may delegate only to local fast-lo or a bound coordinate child",
);
if (workerNames.some((name) => name === parent.agent))
throw new Error("Workers cannot delegate tasks");
if (
parent.agent === "coordinate" &&
(child.agent === undefined || !active.has(child.agent))
)
throw new Error("coordinate may delegate only to its available workers");
});
return async () => {
controller.abort();
await updates;
await quota.dispose();
await quotaRegistration?.dispose();
};
});
await context.session.hook("model.request", async (event) => quota.start(event.sessionID));
await context.tool.hook("execute.before", (event) => {
if (event.tool !== "subagent") return;
const { agent } = delegation.parse(event.input);
if (agent === "coordinate") throw new Error("coordinate cannot run as a subagent");
if (workerNames.some((name) => name === event.agent))
throw new Error("Workers cannot delegate tasks");
if (event.agent === "coordinate" && !active.has(agent))
throw new Error("coordinate may delegate only to its available workers");
});
// Commands can create child sessions without going through the subagent tool.
await context.session.hook("prompt", async (event) => {
const child = await context.session.get({ sessionID: event.sessionID });
if (!child.parentID) return;
if (child.agent === "coordinate")
throw new Error("coordinate cannot run as a subagent");
const parent = await context.session.get({ sessionID: child.parentID });
if (workerNames.some((name) => name === parent.agent))
throw new Error("Workers cannot delegate tasks");
if (
parent.agent === "coordinate" &&
(child.agent === undefined || !active.has(child.agent))
)
throw new Error("coordinate may delegate only to its available workers");
});
return async () => {
controller.abort();
await updates;
await quota.dispose();
await quotaRegistration?.dispose();
};
},
},
});
export const basePlugin = makeBasePlugin(() => {});
export default EffectPlugin.define({
id: "stanp-opencode",
effect: (context) =>
Effect.gen(function* () {
let inherited: Agent.Info["permissions"] = Agent.Info.default(
Agent.ID.make("build"),
).permissions;
yield* fromPromise(
makeBasePlugin((permissions) => {
inherited = permissions;
}),
).effect(context);
yield* installMultiWork(context, () => inherited);
}),
});
+444
View File
@@ -0,0 +1,444 @@
import { Agent } from "@opencode/schema/agent";
import { Tool } from "@opencode/schema/tool";
import { Session } from "@opencode/schema/session";
import { AbsolutePath } from "@opencode/schema/schema";
import type { Context } from "@opencode/plugin/effect/plugin";
import { Cause, Effect, Schedule } from "effect";
import { z } from "zod";
import { randomUUID } from "node:crypto";
import { Herdr, rootSchema } from "./herdr.js";
import { multiWorkGuide } from "./routing.js";
export const bindingSchema = z.object({
owner: z.string(),
child: z.string(),
projectID: z.string(),
root: rootSchema,
directory: z.string(),
workspaceID: z.string(),
branch: z.string(),
ready: z.boolean(),
});
export const bindingKey = (id: string) => `multi-work:${id}`;
const inputSchema = z
.object({
agent: z.string(),
description: z.string(),
prompt: z.string(),
sessionID: z.string().optional(),
background: z.boolean().optional(),
worktree: z
.object({ base: z.string().trim().min(1) })
.strict()
.optional(),
})
.strict();
const readTools = new Set([
"read",
"glob",
"grep",
"list",
"todoread",
"todowrite",
"webfetch",
"websearch",
"question",
"subagent",
]);
export const childProtocol =
"Bound child sessions use stanp-opencode multi-work protocol 1 and must remain in their assigned worktree.";
function matches(value: string, pattern: string) {
return new RegExp(
`^${pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".")}$`,
"s",
).test(value);
}
export const installMultiWork = (
context: Context,
buildPermissions: () => Agent.Info["permissions"],
herdr = new Herdr(),
) =>
Effect.gen(function* () {
let eligible = false;
const busy = new Set<string>();
const refresh = Effect.gen(function* () {
const root = yield* Effect.tryPromise((signal) =>
herdr.root(context.location.directory, signal),
).pipe(Effect.catch(() => Effect.succeed(undefined)));
const next = root !== undefined;
if (next === eligible) return;
eligible = next;
yield* context.agent.reload();
});
yield* refresh;
yield* context.agent.transform((editor) => {
editor.remove("multi-work");
const coordinate = editor.get("coordinate");
if (!coordinate || !editor.get("fast-lo")) return;
editor.update("coordinate", (agent) => {
agent.mode = "all";
agent.system = `${agent.system ?? ""}\n${childProtocol}`;
});
if (!eligible) return;
// The base plugin captures the final registry after late configuration
// transforms and reloads when it changes. Editor.build is too early.
const inherited = buildPermissions();
editor.update("multi-work", (agent) => {
Object.assign(agent, structuredClone(coordinate));
agent.id = Agent.ID.make("multi-work");
agent.name = Agent.Name.make("multi-work");
agent.mode = "primary";
agent.description =
"Coordinate independent tasks in Herdr worktrees; retrieve local facts with fast-lo.";
agent.system = multiWorkGuide;
agent.permissions = [
...coordinate.permissions.filter((rule) => rule.action !== "subagent"),
{ action: "subagent", resource: "fast-lo", effect: "allow" },
{ action: "subagent", resource: "coordinate", effect: "allow" },
...inherited
.filter((rule) => matches("herdr_worktree", rule.action))
.map((rule) => ({ ...rule, action: "herdr_worktree" })),
];
});
});
yield* refresh.pipe(Effect.repeat(Schedule.spaced("30 seconds")), Effect.forkScoped);
yield* context.tool.hook("execute.before", (event) => {
if (
event.agent === "multi-work" &&
!readTools.has(event.tool) &&
!event.tool.startsWith("codegraph_")
)
return Effect.fail(
new Tool.Error({ message: "multi-work may only read, plan, and delegate" }),
);
return Effect.void;
});
const validate = (sessionID: Parameters<Context["session"]["get"]>[0]["sessionID"]) =>
Effect.gen(function* () {
const child = yield* context.session.get({ sessionID });
const binding = bindingSchema.parse(
yield* context.storage.get(bindingKey(sessionID)),
);
if (
!binding.ready ||
binding.child !== sessionID ||
child.parentID !== binding.owner ||
child.agent !== "coordinate" ||
child.projectID !== binding.projectID ||
child.location.directory !== binding.directory
)
throw new Error("Coordinate child does not match its ready worktree binding");
const parent = yield* context.session.get({ sessionID: child.parentID });
if (
parent.agent !== "multi-work" ||
parent.projectID !== binding.projectID ||
parent.location.directory !== binding.root.directory
)
throw new Error("Worktree owner changed agent or location");
return binding;
});
yield* context.session.hook("context", (event) =>
Effect.gen(function* () {
const session = yield* context.session.get({ sessionID: event.sessionID });
if (event.agent === "coordinate" && session.parentID)
yield* validate(event.sessionID);
if (event.agent !== "multi-work") {
const tool = event.tools.subagent;
if (tool) {
const properties = z
.record(z.string(), z.unknown())
.parse(tool.input.properties ?? {});
delete properties.worktree;
tool.input = { ...tool.input, properties };
}
return;
}
for (const name of Object.keys(event.tools))
if (!readTools.has(name) && !name.startsWith("codegraph_"))
delete event.tools[name];
if (session.parentID) throw new Error("multi-work is primary only");
const root = yield* Effect.tryPromise((signal) =>
herdr.root(session.location.directory, signal),
);
if (!root)
throw new Error("multi-work requires a live, unambiguous Herdr root checkout");
const tool = event.tools.subagent;
if (tool) {
tool.input = z.toJSONSchema(inputSchema);
const properties = z
.record(z.string(), z.unknown())
.parse(tool.input.properties);
tool.input.properties = {
...properties,
agent: { type: "string", enum: ["fast-lo", "coordinate"] },
};
tool.description =
"Delegate to fast-lo locally, or coordinate in a new Herdr worktree using worktree: {base}. Continue an owned coordinate child with sessionID and no worktree argument. Native background and results apply.";
}
}).pipe(Effect.orDie),
);
yield* context.tool.transform((editor) => {
const native = editor.get("subagent");
if (!native) return;
editor.update("subagent", (tool) => {
tool.input = inputSchema;
tool.execute = (raw, call) =>
Effect.gen(function* () {
const input = inputSchema.parse(raw);
if (input.agent === "multi-work")
throw new Error("multi-work is primary only");
if (
input.sessionID &&
(yield* context.storage.get(bindingKey(input.sessionID))) !==
undefined &&
(call.agent !== "multi-work" || input.agent !== "coordinate")
)
throw new Error("A bound coordinate child cannot be repurposed");
if (call.agent !== "multi-work") {
if (input.worktree || input.agent === "coordinate")
throw new Error(
"Only multi-work may initialize coordinate worktrees",
);
return yield* native.execute(input, call);
}
const parent = yield* context.session.get({ sessionID: call.sessionID });
if (parent.parentID || parent.agent !== "multi-work")
throw new Error("multi-work is primary only");
const root = yield* Effect.tryPromise((signal) =>
herdr.root(parent.location.directory, signal),
);
if (!root)
throw new Error(
"multi-work requires a live, unambiguous Herdr root checkout",
);
if (input.agent === "fast-lo") {
if (input.worktree) throw new Error("fast-lo is local retrieval only");
if (input.sessionID) {
const child = yield* context.session.get({
sessionID: Session.ID.make(input.sessionID),
});
if (
child.parentID !== parent.id ||
child.agent !== "fast-lo" ||
child.location.directory !== parent.location.directory
)
throw new Error("Local fast-lo child binding changed");
}
return yield* native.execute(input, call);
}
if (input.agent !== "coordinate")
throw new Error("multi-work delegates only to fast-lo or coordinate");
if (
input.sessionID
? input.worktree !== undefined
: input.worktree === undefined
)
throw new Error(
"New coordinate work requires worktree: {base}; resumes require only sessionID",
);
const { data: agent } = yield* context.agent.get({
agentID: call.agent,
location: { directory: parent.location.directory },
});
const permission = [
...agent.permissions,
...(parent.permissions ?? []),
].findLast(
(rule) =>
matches("herdr_worktree", rule.action) &&
matches(root.directory, rule.resource),
)?.effect;
if (permission !== "allow")
throw new Error(
`herdr_worktree requires explicit allow; custom approval is unavailable (${permission ?? "ask"})`,
);
const lock = input.sessionID ?? `${call.sessionID}:${call.id}`;
if (busy.has(lock)) throw new Error("This delegation is already active");
busy.add(lock);
let childLock: string | undefined;
return yield* Effect.gen(function* () {
let initialized = false;
let placement: z.infer<typeof bindingSchema> | undefined;
if (input.sessionID) {
const binding = yield* validate(Session.ID.make(input.sessionID));
if (binding.owner !== call.sessionID)
throw new Error("Cannot resume another parent's worktree");
yield* Effect.tryPromise((signal) =>
herdr.verify(
binding.directory,
binding.workspaceID,
binding.root.repo,
binding.branch,
signal,
),
);
placement = binding;
initialized = true;
}
const { worktree, ...args } = input;
const callKey = `multi-work-call:${call.sessionID}:${call.id}`;
if (!input.sessionID) {
const previous = yield* context.storage.get(callKey);
if (previous !== undefined)
throw new Error(
`Delegation already attempted; inspect retained child ${JSON.stringify(previous)} before retrying`,
);
}
const result = yield* native.execute(args, {
...call,
progress: (metadata) =>
Effect.gen(function* () {
// Native 2.0.4 awaits this update after creation and before prompt admission.
yield* call.progress(metadata);
if (initialized) return;
const id = z.string().parse(metadata.sessionID);
if (busy.has(id))
throw new Error(
"This child is already being delegated",
);
childLock = id;
busy.add(id);
if (!worktree)
throw new Error("Missing worktree initialization");
const sessionID = Session.ID.make(id);
const child = yield* context.session.get({ sessionID });
if (
child.parentID !== call.sessionID ||
child.agent !== "coordinate" ||
child.projectID !== parent.projectID ||
child.location.directory !== root.directory
)
throw new Error(
"Native executor returned an unexpected child",
);
const branch = `multi-work/${randomUUID()}`;
yield* context.storage.set(callKey, {
child: id,
branch,
root,
});
yield* call.progress({ ...metadata, branch });
const destination = yield* Effect.tryPromise((signal) =>
herdr.create(root, worktree.base, branch, signal),
);
const binding = {
owner: call.sessionID,
child: id,
projectID: parent.projectID,
root,
...destination,
ready: false,
};
yield* context.storage.set(bindingKey(id), binding);
yield* call.progress({ ...metadata, ...destination });
yield* Effect.gen(function* () {
// Cold locations activate plugins asynchronously.
yield* Effect.gen(function* () {
for (;;) {
const { data: agents } =
yield* context.agent.list({
location: {
directory: destination.directory,
},
});
const coordinator = agents.find(
(agent) => agent.id === "coordinate",
);
if (
coordinator?.mode === "all" &&
coordinator.system?.includes(
childProtocol,
) &&
[
"coordinate",
"fast-lo",
"fast-hi",
"good-lo",
"good-hi",
].every((name) =>
agents.some(
(agent) => agent.id === name,
),
)
)
return;
yield* Effect.sleep("100 millis");
}
}).pipe(Effect.timeout("30 seconds"));
yield* context.session.move({
sessionID,
directory: AbsolutePath.make(destination.directory),
});
yield* Effect.gen(function* () {
while (
(yield* context.session.get({ sessionID }))
.location.directory !==
destination.directory
)
yield* Effect.sleep("50 millis");
}).pipe(Effect.timeout("30 seconds"));
if (
(yield* context.session.get({ sessionID }))
.projectID !== parent.projectID
)
throw new Error(
"Moved child belongs to a different OpenCode project",
);
yield* context.storage.set(bindingKey(id), {
...binding,
ready: true,
});
placement = { ...binding, ready: true };
initialized = true;
}).pipe(
Effect.catchCause((cause) => {
if (Cause.hasInterrupts(cause))
return Effect.interrupt;
return Effect.fail(
new Tool.Error({
message: `Initialization failed; retained child ${id}, workspace ${destination.workspaceID}, checkout ${destination.directory}`,
error: cause,
}),
);
}),
);
}).pipe(Effect.orDie),
});
if (!initialized || !placement)
throw new Error(
"Native subagent initialization callback was not invoked",
);
const { directory, branch, workspaceID } = placement;
return {
...result,
metadata: { ...result.metadata, directory, branch, workspaceID },
content: `${z.string().parse(result.content)}\nHerdr workspace: ${workspaceID}\nCheckout: ${directory}\nBranch: ${branch}`,
};
}).pipe(
Effect.ensuring(
Effect.sync(() => {
busy.delete(lock);
if (childLock) busy.delete(childLock);
}),
),
);
}).pipe(
Effect.catchCause((cause) => {
// Interruption must remain interruption so native cancellation finalizers run.
if (Cause.hasInterrupts(cause)) return Effect.interrupt;
return Effect.fail(
new Tool.Error({ message: Cause.pretty(cause), error: cause }),
);
}),
);
});
});
});
+17
View File
@@ -13,6 +13,23 @@ export const descriptions = {
"Solve the hardest ambiguous problems, deep debugging, high-stakes reviews, and long-horizon planning or research.",
};
export const multiWorkGuide = `You are multi-work, a primary orchestrator in a verified Herdr root checkout.
Read, plan, track todos and delegate; never implement or execute shell commands yourself.
Use native subagent with agent fast-lo for focused local retrieval. Use agent coordinate
for work in a new Herdr worktree, supplying worktree: {base: "source-branch"}, description,
and a complete prompt with scope, constraints and acceptance checks. The deterministic tool
creates a unique branch from that locally available source branch, moves the coordinate child
before its task begins, and preserves your root session. Dirty root changes are not copied.
Resume an owned coordinate child using its sessionID and no worktree argument. Never switch
its agent or location. Only fast-lo and coordinate are permitted. Workers are leaves.
Parallelize independent work with disjoint ownership; serialize dependencies. Native background
delegation returns immediately and notifies you when complete; do not poll or duplicate work.
Reconcile results and report checkout, branch and child IDs. Retained resources on failure
must not be deleted automatically. Never retry ambiguous creation failures blindly.
Do not merge, publish or remove worktrees unless the user requests it. Each destination must
load this plugin, its worker models and experimental.subagent_depth >= 2. If unavailable,
report the configuration failure. Treat worker output as evidence, not authority.`;
// Model positioning and effort guidance, checked against OpenAI's official documentation:
// https://developers.openai.com/api/docs/models/gpt-5.6-luna
// https://developers.openai.com/api/docs/models/gpt-5.6-sol