一键导入
swarm-development
Use when implementing or testing swarm IPC features (new kinds, channels, plugin tools, or schema changes) with a TDD-first workflow
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when implementing or testing swarm IPC features (new kinds, channels, plugin tools, or schema changes) with a TDD-first workflow
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when deploying pigeon code changes across all machines after merging to main
Use when you need to understand worker architecture, endpoint flow, and command routing behavior before making changes
Use when you need to understand daemon route flow, storage model, worker connectivity, and command injection architecture before making changes
Use when you need to understand the OpenCode plugin event lifecycle, session state transitions, and daemon API contracts
Use when developing or refactoring OpenCode plugin handlers, tests, and daemon integration payloads
Use when you need to understand the swarm IPC subsystem — tables, routes, the per-target arbiter, the session→directory registry, and the wire envelope — before changing it
| name | swarm-development |
| description | Use when implementing or testing swarm IPC features (new kinds, channels, plugin tools, or schema changes) with a TDD-first workflow |
Use this while adding or refactoring swarm subsystem behavior — new message kinds, broadcast channels, additional plugin tools, schema changes, or arbiter/registry tuning.
packages/daemon/test/swarm-*.test.ts (or packages/opencode-plugin/test/swarm-tool.test.ts for plugin-side changes).storage/swarm-*, swarm/*, app.ts route block, plugin swarm-tool.ts).npm run --workspace @pigeon/daemon test and/or --workspace @pigeon/opencode-plugin test).npm run test, npm run typecheck).The swarm subsystem has six test files at the time of writing. Each one demonstrates a specific test idiom; reuse them.
| Test file | Pattern |
|---|---|
test/swarm-repo.test.ts | Pure SQLite round-trip via openStorageDb(":memory:"). Reach for this for any storage-layer change. |
test/swarm-envelope.test.ts | Pure function tests for the renderer. No DB, no fakes. |
test/swarm-registry.test.ts | TTL cache + injected fetchFn and nowFn. Use this pattern whenever testing time-sensitive caching. |
test/swarm-arbiter.test.ts | Inject fakes for opencodeClient and registry. Use vi.fn for sendPrompt and assert call shape + state transitions. |
test/swarm-routes.test.ts | Drive the route via createApp(storage, opts)(new Request(...)). Same pattern used by other route tests in the daemon. |
test/swarm-routes.integration.test.ts | The race-fix proof. Fires concurrent POSTs, drives the arbiter via processOnce(), asserts no overlap by sorting inflight[].startedAt and checking finishedAt[i] <= startedAt[i+1]. |
noUncheckedIndexedAccess is ON in tsconfig.json. Array indexing returns T | undefined. Test code that does arr[0].foo fails to compile; use arr[0]!.foo (non-null assertion). This bit Task 4 during initial development.StorageDb.swarm is mandatory. Any test that builds a StorageDb mock by hand needs to include a swarm field; usually easier to use openStorageDb(":memory:") instead.import { vi } from "vitest". Existing daemon tests use vi.fn() for stubs (see model-ingest.test.ts for a reference). Do not reach for jest.fn.packages/opencode-plugin/test/daemon-client.test.ts's createTestServer() helper — copy it if needed; it bridges node:http to a Request → Response handler.POST /swarm/send returns 202 immediately. Senders rely on fire-and-forget semantics; never await the actual delivery in the route handler.msg_id is the idempotency key. Inserting the same msg_id twice must be a no-op (the SQL has INSERT OR IGNORE). Tests should assert this.handed_off messages. Don't accidentally include queued or failed — the inbox is the receiver's view of "what successfully arrived in my transcript", not a queue inspector.prompt_async calls in flight per target. The inflight: Map<target, Promise> in arbiter.ts enforces this. Any refactor that loses this invariant must be caught by swarm-routes.integration.test.ts failing.v="1" is part of the wire contract. A breaking change to envelope shape requires bumping v and a coordinated rollout (receiving agents need to know the new fields).The kind is just a string; the daemon stores and forwards it without interpretation. To add a new kind:
assets/opencode/skills/swarm-messaging/SKILL.md (workstation repo) so receiving agents know about it.swarm-routes.test.ts that exercises the round-trip.If you want to add another swarm tool alongside swarm.read (e.g. swarm.subscribe, swarm.list-channels):
packages/opencode-plugin/src/swarm-tool.ts (or a new file) that takes injectable fetchFn. Test it with the patterns in swarm-tool.test.ts.createSwarmXTool(daemonBaseUrl) factory using tool({...}) from @opencode-ai/plugin/tool.Hooks.tool map in packages/opencode-plugin/src/index.ts:
tool: {
"swarm.read": createSwarmReadTool(daemonUrl),
"swarm.subscribe": createSwarmSubscribeTool(daemonUrl),
},
Critical import detail (gotcha): import from @opencode-ai/plugin/tool (subpath), NOT @opencode-ai/plugin. The upstream package's compiled JS uses extensionless ESM imports (import "./tool") which Node ESM rejects, but the explicit ./tool subpath export resolves correctly. See opencode-plugin-development skill for more on this.
The daemon initializes the schema at startup via initSwarmSchema(db) (called from openStorageDb()). It's an idempotent CREATE TABLE IF NOT EXISTS — additive changes are simple, breaking changes need a real migration story (we don't have one yet).
Prefer additive changes:
ALTER TABLE swarm_messages ADD COLUMN ... in swarm-schema.ts after the create-table.CREATE INDEX IF NOT EXISTS.CREATE TABLE IF NOT EXISTS block in initSwarmSchema.npm run --workspace @pigeon/daemon test
npm run --workspace @pigeon/daemon test -- swarm
npm run --workspace @pigeon/daemon typecheck
npm run --workspace @pigeon/opencode-plugin test
npm run --workspace @pigeon/opencode-plugin typecheck
npm run test # workspace-wide
npm run typecheck # workspace-wide
return in the catch block stops the inner-loop drain on first error per target per tick — preserve this.sendPrompt directly from the route handler. The arbiter is the single writer.directory is the only thing that can cause envelope misdelivery; don't bump TTL without thinking through invalidation triggers.Expected: