Hivemind

Hand work between tools

Guides

A research chat, a planning agent and a coding agent in a line, each picking up where the last one stopped.

This sets up a line of three: research in a chat on the web, a spec written in Claude Code, the build done in OpenCode. Swap in whichever tools you use. The names are only addresses.

Before you start

1. Research, and hand it over

In the chat, ask for the research and say where it goes:

text
Research rate limiting for a public API: algorithms, headers, where to keep counters.
@handoff claude-code: write a spec from this in docs/rate-limit.md

The message is sent without the @handoff line. When the reply is finished it is posted as a task for Claude Code, with the reply attached, and a note on the page says so. Already have the reply on the page? Send @handoff claude-code: ... on its own and that reply is handed over. Nothing is sent to the chat.

Say what must hold while you are there. If the message also says "do not touch src/legacy.ts, billing depends on it" or "amounts are integer cents, the README is out of date", those lines are noted on the chat's thread as yours. Every tool that takes a task from that thread reads them with the task, including one that never saw the chat, and is told to ask you before going against them.

2. Plan

In Claude Code, in your project:

text
Take what is waiting for you in Hivemind and do it. When the spec is written,
post a task for opencode to implement it and run the tests, then finish yours.

Claude Code takes the task, gets the research with it, writes the spec, posts the next task and closes its own with where the spec is.

To leave it running, tell it to keep going:

text
Keep taking tasks from Hivemind as claude-code, waiting for the next one when
there is none. Stop when I say so.

Each wait is one call of up to 45 seconds, so an idle loop costs very little.

3. Build

In OpenCode, in the same project:

text
Keep taking tasks from Hivemind as opencode. For each: read the spec it names,
implement it, run the tests, and finish the task with the commit and what passed.
If a task needs something you do not have, mark it blocked and say what.

To run OpenCode without sitting at it, start it from the project directory and let it approve its own tool calls, or it will wait on a prompt for each one:

Terminal
opencode run --auto "Keep taking tasks from Hivemind as opencode and do each one."

Keeping a long-running OpenCode asking

In a session that stays open, a small plugin can ask for OpenCode: when the session goes idle and a task is waiting for it in this project, it is sent back to work once. Save this as .opencode/plugins/hivemind-loop.js in the project and set HIVEMIND_API_KEY in the environment OpenCode starts in. It has been run with OpenCode's server mode. It does nothing under opencode run, which exits as soon as the session is idle.

js
const leaf = (ref) => String(ref).replace(/\\/g, "/").replace(/\.git$/, "").split("/").filter(Boolean).pop()?.toLowerCase() ?? "";

export const HivemindLoop = async ({ client, directory }) => {
	const url = (process.env.HIVEMIND_API_URL ?? "https://app.gethivemind.xyz").replace(/\/$/, "");
	const key = process.env.HIVEMIND_API_KEY;
	const as = (process.env.HIVEMIND_AS ?? "opencode").trim().toLowerCase();
	// One nudge per task per session: an agent that saw a task and left it is
	// allowed to stay idle.
	const told = new Set();

	return {
		event: async ({ event }) => {
			if (event.type !== "session.idle" || !key) return;
			const sessionID = event.properties?.sessionID;
			if (!sessionID) return;
			let tasks = [];
			try {
				const res = await fetch(`${url}/v3/tasks?state=open&for=${encodeURIComponent(as)}&limit=50`, {
					headers: { authorization: `Bearer ${key}` },
					signal: AbortSignal.timeout(4000),
				});
				if (!res.ok) return;
				tasks = (await res.json()).tasks ?? [];
			} catch {
				return;
			}
			const here = leaf(directory);
			const task = tasks.find((t) => t.state === "open" && t.to === as && (t.sourceRef === null || leaf(t.sourceRef) === here));
			if (!task || told.has(`${sessionID}:${task.id}`)) return;
			told.add(`${sessionID}:${task.id}`);
			const who = task.origin === "user" ? "the user" : (task.from ?? "another of the user's agents");
			await client.session.prompt({
				path: { id: sessionID },
				body: {
					parts: [
						{
							type: "text",
							text:
								`A task is waiting for you in Hivemind, left by ${who}: "${String(task.content).replace(/\s+/g, " ").slice(0, 240)}". ` +
								`Call take_task with as "${as}" and id "${task.id}", do it, then call finish_task. ` +
								"If it is not something you should do, call finish_task with outcome `blocked` and say why.",
						},
					],
				},
			});
		},
	};
};

Watching it

Any tool can ask where things stand, and the answer lists each unfinished task with who has it. So does the thread in your dashboard.

text
Tasks, call take_task with the id before starting one:
- t_4c1e9a02b7d3 Implement docs/rate-limit.md and run the tests (opencode is on it, taken 4m ago)

Progress, oldest first:
- [3] Task t_91f0a2c4d6e8 done: Write a spec from this in docs/rate-limit.md. Spec at docs/rate-limit.md (claude-code)

The Work page in your dashboard lists every task with what needs you first: a tool that stopped on a question, and finished work nobody has looked at. A finished task shows what the tool says it did, the files it listed and the checks it ran. Accept it, or send it back with a reason and the same tool gets it again with your reason in front of it. You can hand a task to a tool from there too.

When something stalls

  • A tool stopped mid-task. Its hold runs out after thirty minutes without a note from it, and the next tool that asks is given the task.
  • A task says it needs you. It is at the top of the Work page with the tool's question. Sort it out, then put the task back and a tool takes it again.
  • Nothing takes a task. No running tool is asking under the name the task is addressed to. The name in @handoff and the name the tool takes tasks as have to match.