agentleFS
zaxbysaucezaxbysauce/opencode-swarm/AGENTS.md

opencode-swarm

AGENTS.md517 starsChanged 3 days agoMITView on GitHub

This file is the root engineering contract for opencode-swarm. It applies to every contributor — human, Claude Code, OpenCode agent, or other automated tool. Read it fully before making any code change. It is intentionally short and operational; the long-form rationale and historical failure map live in docs/engineering-invariants.md. AGENTS.md and docs/engineering-invariants.md together are the single source of truth for repository invariants. When CLAUDE.md, contributing.md, TESTING.md, or any skill conflicts with this file, this file wins; that skill or doc is…

We split this AGENTS.md into the rules it gives an agent

17 rules. About 0 are common, 8 are distinctive, and 9 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before creating or rebuilding any repository-derived cache or index, agents must first search the current worktree and known sibling/source …

    line 21
  2. Distinctive:

    Before initializing, rebuilding, or performing a full sync of a ZaxbyGraph issue/PR database, search the current worktree and known …

    line 22
  3. Distinctive:

    Every PR that touches a relevant area must list which of these invariants it touched and how it verified them.

    line 34
  4. Distinctive:

    Array-form spawn only: No shell-string commands unless the use case is independently justified and quoted.

    line 57
  5. Very specific:

    No tool may create .swarm/ under src/, tests/, packages/*, or any arbitrary cwd.

    line 70
  6. Very specific:

    Do not hand-edit plan.md as a source of truth.

    line 80
  7. Very specific:

    Do not use the OpenCode test_runner tool with scope: 'all' or broad 'graph' / 'impact' scope for whole-repo validation.

    line 85
  8. Very specific:

    All tests use bun:test only.

    line 92
  9. Very specific:

    Use os.tmpdir() + path.join(...) for temp paths.

    line 107
  10. Distinctive:

    Add cooldowns for repeated safety/advisory behavior (e.g.

    line 114
  11. Distinctive:

    No cross-session pollution.

    line 115
  12. Very specific:

    Do not emit diagnostic noise into chat-visible streams.

    line 133
  13. Very specific:

    Run tests/unit/config/*.test.ts and /swarm doctor tools after any tool, agent-map, command, or help change.

    line 139
  14. Very specific:

    Every PR that touches a relevant area must include a ## Invariant audit section in its description (see .claude/skills/commit-pr/SKILL.md).

    line 155
  15. Distinctive:

    If you cannot prove a touched invariant from source and test output, do not push.

    line 175
  16. Very specific:

    Ask, don't guess. The patterns in this file are the result of prior outages — every one has a release note in docs/releases/.

    line 179
  17. Distinctive:

    Prefer the smallest patch that closes the issue without unwired functionality, untested branches, or hidden regressions.

    line 180

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md — Engineering contract for opencode-swarm

This file is the root engineering contract for opencode-swarm. It applies to every contributor — human, Claude Code, OpenCode agent, or other automated tool. Read it fully before making any code change. It is intentionally short and operational; the long-form rationale and historical failure map live in docs/engineering-invariants.md.

Required reading order

When you are about to do…Read these (in order)
Any code changeAGENTS.md (this file) → docs/engineering-invariants.md (skim, deep-dive on touched invariants)
Write or modify any test filethis file → .opencode/skills/writing-tests/SKILL.md (or .claude/skills/writing-tests/SKILL.md for Claude)
Commit / push / open a PRthis file → .claude/skills/commit-pr/SKILL.md
Trace, investigate, root-cause, fix, or resolve an issue or bugthis file → .opencode/skills/issue-tracer/SKILL.md (or .claude/skills/issue-tracer/SKILL.md for Claude)
Change Epic Mode (src/epic/**, an Epic seam in a shared file, or the epic config block)this file → src/epic/README.md (Epic maintainer guide: invariants, seams, tests, recipes)
Swarm-mode Claude workthis file → CLAUDE.md → .claude/session/swarm-mode.md (when present)
Architecture / plugin init / subprocess / tool-registration / plan-durability / .swarm storage / runtime-portability changethis file → docs/engineering-invariants.md → .opencode/skills/engineering-conventions/SKILL.md (or .claude/skills/engineering-conventions/SKILL.md)

AGENTS.md and docs/engineering-invariants.md together are the single source of truth for repository invariants. When CLAUDE.md, contributing.md, TESTING.md, or any skill conflicts with this file, this file wins; that skill or doc is out of date and must be reconciled.

Repository-history cache reuse

  • Before creating or rebuilding any repository-derived cache or index, agents must first search the current worktree and known sibling/source repository locations for an existing compatible artifact.
  • Before initializing, rebuilding, or performing a full sync of a ZaxbyGraph issue/PR database, search the current worktree and known sibling/source repository locations for an existing cache first.
  • Validate any discovered cache against the requested repository and inspect its sync metadata before reuse. When it is compatible, reuse it in place if writable or copy it into the current worktree and run only an incremental sync.
  • Build a new cache from scratch only when no compatible, usable cache exists. Record that discovery result before starting the rebuild so agents do not repeat avoidable GitHub API work or consume rate limits unnecessarily.

Prime directive

Preserve the runtime contracts that keep the plugin loadable, portable, bounded, recoverable, and safe across Windows, macOS, Linux, GUI, TUI, Bun, and Node-hosted plugin contexts.

A plugin that loads on macOS but hangs Desktop Windows is a regression. A change that passes locally on one platform but corrupts state on another is a regression. Default to "what could go wrong on the host I am not testing on?"

Non-negotiable invariants

Every PR that touches a relevant area must list which of these invariants it touched and how it verified them. See "Invariant audit required in PRs" below.

1. Plugin initialization is fast, bounded, fail-open, side-effect-minimal

  • Plugin registration must complete in bounded time on every supported platform. The OpenCode plugin host silently drops a plugin whose entry never resolves; users see "no agents in TUI/GUI" with no error.
  • No unbounded filesystem scans, Git commands, network calls, package-manager calls, cache repair, repo-graph construction, or large JSON repair before returning the plugin manifest.
  • Any init-path environmental work must be wrapped in withTimeout(...) (or equivalent), log non-fatally, and continue. Compare loadSnapshot(...) in src/index.ts (already wrapped) and ensureSwarmGitExcluded(...) (post-fix wrapped).
  • Bounded is not free. withTimeout only prevents an unbounded hang — the awaited work's latency still counts toward the cross-platform repro-704 init deadline (~400 ms). If init-path work does non-trivial I/O and nothing downstream depends on its completion before server() resolves, register it with the wrapper-owned post-resolution task queue (the repoGraphHook precedent in src/index.ts) instead of awaiting it or using queueMicrotask inside initializeOpenCodeSwarm. Only await init work that is genuinely fast (<~50 ms) and must complete before a later init step (e.g. ensureSwarmGitExcluded before .swarm/ writes). Linux repro-704 passing does not prove Windows; cold-FS latency is several× higher and the deadline is enforced on every platform in CI.
  • Any init-path subprocess must use explicit cwd, stdin: 'ignore' (unless intentionally interactive), timeout, bounded or ignored stdout/stderr, and a best-effort proc.kill() in finally.
  • GUI and TUI must still receive the plugin manifest even if optional startup work fails.
  • Reference prior failures: v7.0.3 (#704 repo-graph Desktop hang), v7.3.3 (#732 Git-hygiene startup regression — the proximate cause of this AGENTS.md), and the PR #1920 merge-queue smoke failure where a queueMicrotask began cold-filesystem work during later init awaits. PR #1356 remains the deferred-work exemplar, now registered through the wrapper-owned post-resolution task queue: its bundled-skill sync stays off the server()-resolution path after an inline-await revision was corrected pre-merge (see the in-code comment near the bundled-skill registration and the "Bounded is not free" bullet above).

2. Runtime portability — Node-ESM-loadable + dual v1/v2 plugin shape

  • The main plugin bundle (dist/index.js) must remain Node-ESM-loadable. No top-level bun: imports (see v6.86.8 / bundle-portability.test.ts).
  • No un-fenced bun:-scheme runtime module resolution. A lazy require('bun:sqlite') / createRequire(...)('bun:...') / dynamic import('bun:...') passes the top-level-import check yet still throws under the OpenCode Desktop Node sidecar (Cannot find module 'bun:sqlite'). All bun: runtime resolution must route through a single Node-fallback-providing loader — bun:sqlite → src/db/sqlite-loader.ts (native bun:sqlite under Bun, a node:sqlite adapter under Node; issue #1873). Type-only import type … from 'bun:…' is fine (erased). Enforced statically by tests/unit/build/bundle-portability.test.ts (source scan + bundle-pairing, runs on every PR) and functionally by scripts/repro-1873.mjs (the merge-queue-gated CI smoke job, real Node — not on plain PR pushes). Note node:sqlite is stricter than bun:sqlite: it rejects a bound parameter when the SQL has no placeholder (SQLITE_RANGE / "column index out of range"), so keep every query's parameter count exact.
  • No direct Bun.* calls outside src/utils/bun-compat.ts. CLI-only modules that intentionally --target bun are the only exception.
  • The default export must remain the dual-shape plugin object { id, server, setup } (issue #3004 / ADR-0003, docs/decisions/0003-opencode-v2-plugin-api.md): v1 hosts (OpenCode 1, @opencode-ai/plugin 1.x) call server(); OpenCode 2 hosts (@opencode/plugin 2.x) decode the default export against { id, setup | effect } (excess keys ignored) and call setup(ctx) — the v2 registration surface lives in src/host/v2/ with the full v1→v2 mapping in docs/host/v2-hook-inventory.md. server() stays byte-stable for v1; v1 readV1Plugin never consults setup (verified against anomalyco/opencode v1.18.3 and v1.18.33; see v6.86.9 / bundle-plugin-shape.test.ts for the v1 history).
  • Any change touching src/index.ts, package exports, package.json#main, bun build config, or plugin entry shape must run the bundle-portability and plugin-shape tests AND node --input-type=module -e "await import('./dist/index.js')".
  • dist/ is generated build output and is not committed (#1047). Do not stage it. CI builds it (unit, package-check, smoke run bun run build) and package-check validates the packed npm tarball; release/publish builds from source.

3. Subprocesses — bounded, non-interactive, killable, portable

  • Array-form spawn only. No shell-string commands unless the use case is independently justified and quoted.
  • Set cwd explicitly, OR for Git CLI calls use an explicit git -C <directory> argument. Do not rely on inherited process cwd.
  • stdin: 'ignore' unless the spawn is intentionally interactive. A never-closed stdin pipe under Bun on Windows can block the child from exiting (see v7.3.3 fix).
  • timeout: <ms> is required for git, package managers, test runners, language tooling, and any external binary. There is no platform on which "git is always fast" is a safe assumption.
  • Consume, bound, or ignore stdout/stderr. Never leave a piped stream unattended on a long-running child.
  • Best-effort proc.kill() in finally. An outer withTimeout alone is not enough — it lets the awaiter proceed but does not abort the child.
  • Windows is first-class. Handle .cmd extensions for npm/bun binaries, PATH differences, and the fact that child_process.spawn('bin', ...) does not behave identically to cmd.exe.

4. Working directory and .swarm/ containment

  • Runtime state lives only under the project root .swarm/ directory. Tools must use ctx.directory injected via createSwarmTool (src/tools/create-tool.ts).
  • process.cwd() is an explicitly documented direct-CLI / test fallback only. Do not introduce new process.cwd() callers in tools or hooks.
  • User-supplied working_directory must resolve to a project root, never an ordinary subdirectory. A nested directory is an independent root only when it directly contains a .git file/directory (repository, linked worktree, or submodule declaration) or a .opencode/ directory (#2127). These are explicit local declarations: direct malformed/empty .git markers still opt in, but marker symlinks/junctions and inaccessible markers do not. save_plan, the shared resolveWorkingDirectory helper, every duplicate root guard, and all low-level plan/scope/evidence/evaluation mutation sinks must apply the same policy; ambiguous ancestor .swarm or project-indicator state and ancestor-depth exhaustion fail closed. See also v6.82.2 (#577).
  • No tool may create .swarm/ under src/, tests/, packages/*, or any arbitrary cwd. New-directory checks must be explicit.
  • .swarm/ must not become Git pollution. The ensureSwarmGitExcluded flow (v7.3.3) keeps the project's .git/info/exclude honest; do not bypass it.
  • Bundled skill ownership: plugin-shipped runtime protocols materialize only under .swarm/bundled-skills/<slug>/. Native skill roots (.opencode/skills/, .claude/skills/, .agents/skills/) are project-owned and must never be a plugin sync destination. A repository skill may reuse a bundled slug without overwrite; architect MODE stubs must load the private runtime copy.
  • Exception — developer skill-sync tool: drift:fix (issue #1781 E3) is a developer-invoked, explicitly-confirmed (SWARM_SKILL_SYNC_CONFIRM=1) tool that reconciles native-skill mirror pairs before push. It is NOT a plugin-runtime sync and is never invoked at plugin init or under DRIFT_CHECK_ENFORCE. It is exempt from the runtime-sync prohibition above.
  • Exception — version-controlled deliverables: documentation the docs agent authors into the project repo is output, not runtime state. The docs agent already writes README/CHANGELOG; the docs_design agent (issue #1080, opt-in) writes design docs under design_docs.out_dir (default docs/). These are intentionally committed and are exempt from .swarm/ containment. Their runtime drift signal (.swarm/doc-drift-phase-N.json) stays under .swarm/.

5. Plan durability — ledger is authoritative

  • .swarm/plan-ledger.jsonl is the authoritative source of plan state. .swarm/plan.json and .swarm/plan.md are derived projections.
  • SWARM_PLAN.{json,md} files are checkpoint / export artifacts, scoped to .swarm/plan-export/ (issue #852). Legacy reads from flat .swarm/ and project root are supported with deprecation warnings; cleanup removes all three locations.
  • Do not hand-edit plan.md as a source of truth. Do not write to plan.json outside the ledger replay path.
  • Any plan-schema or status-shape change must update all six of: ledger replay, projection, checkpoint import/export, get_approved_plan, tests, and docs (docs/plan-durability.md).

6. Test execution — do not use broad test_runner for repo validation

  • Do not use the OpenCode test_runner tool with scope: 'all' or broad 'graph' / 'impact' scope for whole-repo validation. scope: 'all' is gated behind the SWARM_ALLOW_FULL_SUITE=1 env var (intended for opt-in CI mirrors, not interactive use; there is no allow_full_suite arg).
  • MAX_SAFE_TEST_FILES = 50 (src/tools/test-runner.ts). Resolutions exceeding this return outcome: 'scope_exceeded' with a SKIP instruction; broad scopes can stall or kill OpenCode.
  • For repo validation, prefer shell commands (the per-file isolation loops in contributing.md / TESTING.md).
  • For targeted agent validation, use test_runner with explicit files: [...] or small targeted scopes.

7. Test writing — bun:test, mock isolation, DI over mock.module

  • All tests use bun:test only. No Jest, Vitest, etc. Bun's vitest-compat layer has known isolation bugs.
  • Load the writing-tests skill (.opencode/skills/writing-tests/SKILL.md or .claude/skills/writing-tests/SKILL.md) before modifying tests.
  • mock.module(...) leaks across test files in Bun's shared test-runner process. Spread the real module when mocking, or — preferred — use dependency injection via a small _internals seam (see src/utils/gitignore-warning.ts:_internals and src/hooks/diff-scope.ts:_internals introduced by the v7.3.4 fix). Restore the seam in afterEach. Current _internals seams include: src/hooks/knowledge-migrator.ts:_internals (writeSentinel, mkdir, writeFile for evidence-spoofing tests), src/evidence/manager.ts:_internals (validateEvidence, saveEvidence for evidence integrity tests — src/tools/syntax-check.ts calls saveEvidence through this seam rather than a local snapshot, since a plain object-literal snapshot of an imported binding is captured at module-init time and will not observe a sibling test file's later mock.module() replacement — issue #1248 item 16), src/hooks/guardrails/index.ts:_internals, src/hooks/curator.ts:_internals, and others listed in docs/engineering-invariants.md.
  • _internals is intentionally absent from src/lang/backends/php.ts (removed in the #1145/#1194 hardening pass; siblings go.ts, python.ts, typescript.ts retain theirs). The PHP backend's tests use public-API testing (backend.selectFramework?.(tmpDir)) instead — selectFramework had no external consumer needing DI, so the seam was dead weight. Do not re-add it without a concrete test need.
  • FR-006/007/008/009/010/011/012 enforcement: All new test files must be under 500 lines. delegation-gate.test.ts (2835 lines) was split into 45 focused files for FR-006 SC-006.1. SME tests (FR-008) use parameterization for coverage. Lean Turbo behavioral tests (FR-009) cover acquire-locks, plan-lanes, review, runner-status, generate-mutants, set-qa-gates, and get-qa-gate-profile. FR-010/FR-011/FR-012 (Phase 3–4 structural debt) added 11 new hook test files covering previously untested hooks (conflict-resolution, curator-types, delegate-ack-collector, delegate-directive-injection, knowledge-reinforcement, normalize-tool-name, phase-complete-directive-gate, phase-directives, semantic-diff-injection) and consolidated knowledge-curator tests with shared fixtures.
  • Spread-real-exports pattern for node:* mocks: When mocking Node built-ins (node:fs, node:child_process, node:fs/promises, etc.), always spread the real module's exports into the mock return object. This prevents test pollution and ensures only the intended functions are overridden. CI enforces this via bun run check:mock-cleanup (Check 2).
    import * as realFs from 'node:fs';
    mock.module('node:fs', () => ({
      ...realFs,           // mandatory — preserves all other exports
      readFileSync: mockFn, // override only what you need
    }));
  • mock.module allowlist growth ratchet (issue #1666): scripts/mock-allowlist.txt is closed against unapproved growth by bun run check:invariants Check 4. Adding a new mock.module target requires a matching standalone marker line # APPROVED-NEW: <normalized-target> in scripts/mock-allowlist.txt (preserved across regen by scripts/generate-mock-allowlist.sh). MOCK_ALLOWLIST_ENFORCE=0 soft-warns for a deliberate growth PR. Prefer _internals DI for new code — the allowlist is a legacy-pattern debt surface, not a way to bypass the seam convention.
  • Family-migration destination-lock admission (issue #2577): bun run check:invariants Check 8 fail-closes the memory and knowledge family-migration engines — a destination-lock acquisition failure must throw (typed contention/storage), never proceed unlocked. The check errors on a swallowed admission, a missing engine file (single-engine drift), or a drifted scan anchor.
  • Use os.tmpdir() + path.join(...) for temp paths. No hardcoded /tmp or C:\ strings.
  • mkdtempSync must be wrapped in realpathSync if the result is chdir'd on macOS.

8. Session and global state — keyed and bounded

  • Anything session-scoped must be keyed by sessionID (see v6.80.2 recentToolCallsBySession and lastSpiralTimestampBySession fixes).
  • Module-level global state must have an explicit eviction strategy (MAX_TRACKED_SESSIONS, FIFO eviction).
  • Add cooldowns for repeated safety/advisory behavior (e.g. spiral-detection 60 s cooldown).
  • No cross-session pollution. Global arrays and maps that mix session data are bugs.

9. Guardrails / retry semantics

  • Distinguish transient infrastructure / provider errors (HTTP 429 / 503 / 529, timeouts, "temporarily unavailable") from real agent-logic failures (see v6.86.14).
  • Transient errors use bounded retry (max_transient_retries, default 5) before counting toward consecutiveErrors / circuit-breaker accounting.
  • transientRetryCount resets per invocation. Do not persist it across invocations.
  • Transient retry and model fallback are independent — neither subsumes the other.
  • Failure classification is source-aware and structured. Provider, shell/spawn, filesystem, Git, policy/gate, validation, cancellation, and deadline channels must retain distinct categories and retry/repair ownership; bounded display evidence is never reclassified by downstream consumers. Provider quota patterns never run over arbitrary tool output, and deterministic policy/containment failures never enter provider fallback.
  • Retry and circuit state is invocation-owned, bounded, and action-local, keyed by the exact session, invocation, semantic action digest, and failure category. Parser, structured command-unavailable, and sandbox-wrapper failures may open immediately; repeated permanent failures use their category threshold. An open circuit blocks only the matching action. Read/diagnose/rescope/repair/handoff/abort/Full-Auto-exit controls remain reachable, while a sandbox-wrapper circuit never permits unsandboxed shell execution.
  • A corrected successful execution clears only the matching action's circuits. Failures that require external repair use an explicit audited reset bound to the exact active session/invocation/action; stale or foreign resets fail closed. Late results from an older invocation/generation cannot clear current state. The false-to-true transition emits one bounded structured advisory/telemetry event without raw prompt, secret, URL, command, or output text.
  • Write authorization requires an exact active v2 scope binding correlated to the current session and Task call. Empty scope, v1 scope, stale plan/task identity, and another session's declaration fail closed with SCOPE_NOT_DECLARED; no child state is published before this preflight passes. Exception: the coder write/edit/patch gate (src/hooks/scope-guard.ts) emits the more precise SCOPE_WORKSPACE_MISMATCH instead of SCOPE_NOT_DECLARED when it can positively identify the failure as a wrong-resolved-root case — an active, otherwise-plausible binding for this exact session/task exists but is rooted somewhere other than where the gate resolved (see describeScopeWorkspaceMismatch, src/scope/scope-binding.ts, issue #2002). This is diagnostic-only — it never widens or narrows what the preflight above would otherwise grant or deny, and does not fire for an unresolvable directory, a pr_feedback binding, or a stale/uncorrelated one. The shell-write path (src/hooks/guardrails/tool-before.ts) is unaffected and always emits SCOPE_NOT_DECLARED.

10. Chat / system-message hook contracts

  • Preserve OpenCode's expected message shape end-to-end. Multiple output.system entries are materialized into multiple { role: 'system' } messages.
  • A hook output property must be mutated IN PLACE for the two chat transform chains. The host invokes each hook as M(input, output), discards the return value, and afterwards reads its own local array — so output.system = … / output.messages = … inside experimental.chat.system.transform or experimental.chat.messages.transform is a silent no-op. Use push / splice / length = 0 + refill. (chat.params / chat.headers do consume the returned object; do not over-generalize.) Two shipped fixes were dead for years because of this — see v6.85.1 below and docs/engineering-invariants.md. Enforced by tests/unit/hooks/chat-transform-rebind-guard.test.ts.
  • Model-only guidance rides USER-role guidance carriers — never role:'system' entries (issue #2526). The pinned host (@opencode-ai 1.18.3) message→request converter branches only on user and assistant with no else, and dereferences msg.parts unconditionally: a role:'system' entry inside experimental.chat.messages.transform output is silently discarded before the request is built, and a flat entry without parts throws a TypeError in the host prompt build. Every plugin injection must go through src/hooks/system-guidance-carrier.ts — a carrier is { info: { id: 'swarm-guidance:<kind>', role: 'user' }, parts: [{ type: 'text', text: fenced }] }, where fenced wraps the body in the <swarm_system_directive source="opencode-swarm" kind="…"> provenance fence; carrier detection is by info.id prefix, never text sniffing. Delivery/telemetry predicates assert the host-render contract (deliveredGuidanceDelta — non-empty delta + renderable carrier shape), not presence in the plugin's own array. The final structure-mutating handler runs materializeSystemGuidanceInPlace (the boundary that converts any remaining system entry to a carrier in place, or drops tool-result/whitespace ones), so the transformed array contains ZERO system entries — a stronger form of the old "one system message at index 0" local-model guarantee (#608/#628), since the host's own system content arrives only via the separate output.system string surface. Pinned by tests/unit/hooks/host-message-role-contract-2526.test.ts (version tripwire: the installed @opencode-ai/plugin and @opencode-ai/sdk versions must equal the fixture's PINNED_HOST_PACKAGE_VERSION; on a lockfile bump OR a host release, re-verify tests/helpers/host-contract-v1_18_3.ts against the new host source — the weekly host-contract-check.yml scheduled check (scripts/check-host-contract.ts) is the trigger and compares the converter's structural digest against the host source at npm-latest) and tests/unit/hooks/system-splice-ratchet-2526.test.ts (no role:'system' construction outside the role-filter system-string adapter and the materializer). The pre-#2526 consolidateSystemMessages merge and the v6.85.1/#628 output.system collapse are both gone.
  • Do not emit diagnostic noise into chat-visible streams. Use the debug-gated logger (src/utils/log.ts) unless the message is an operational or security warning that must always be visible.
  • OpenCode 2 (v2) chat contracts are the dual of the v1 rules above (issue #3004 / ADR-0003). The v2 session context hook (src/host/v2/guidance.ts) receives a DeepMutable SessionContext event and mutates event.system / event.messages in place (same class of contract as the v1 output rule — the host reads its own arrays post-hook). v2 renders event.system natively, so the #2526 USER-role-carrier workaround is v1-only: on v2 the adapter re-homes the materialized swarm-guidance:* carrier text into event.system as { type: 'text' } parts (the user-to-system transport delta is recorded in docs/host/v2-hook-inventory.md); the provenance fence stays in the text. Each guidance unit has exactly one destination. event.agent is an Agent.Info object on v2 and must be normalized to a name before any v1-style agent comparison.

11. Tool registration + agent-map coherence

  • A tool addition is incomplete until: (a) a TOOL_METADATA entry in src/tools/tool-metadata.ts (description + agents: [...] are required fields — a missing one is a compile error), (b) a TOOL_MANIFEST handler thunk in src/tools/manifest.ts (registered via defineHandlers<T extends Record<ToolName, () => ToolDefinition>>; the generic constraint makes the handler set exhaustive vs the metadata keys — a missing or stray handler is a compile error), (c) an export in the src/tools/index.ts barrel (still a required surface for wiring and registration tests — enforced by scripts/check-tool-registration.ts), (d) help/documentation surfaces, (e) tests covering the new entry. Everything else is derived, not hand-maintained: the plugin object is built by buildPluginToolObject(...) in src/index.ts (there is no manual tool: {} block to edit), and TOOL_NAMES / TOOL_NAME_SET / AGENT_TOOL_MAP invert automatically from TOOL_METADATA.agents (feature-gated tools instead declare an opt-in map such as MEMORY_AGENT_TOOL_MAP in src/config/constants.ts).
  • Run tests/unit/config/*.test.ts and /swarm doctor tools after any tool, agent-map, command, or help change. See v6.48.0.
  • CI drift check (issue #1497): .github/workflows/drift-check.yml runs scripts/drift-check.ts --enforce on PR, merge_group, and push: main. It detects drift across skills (.opencode↔.claude mirror contracts in src/config/skill-mirrors.ts), bundled-skill completeness (the issue #1496 class: .opencode/skills/ vs BUNDLED_PROJECT_SKILLS vs package.json#files), tool registration (reuses scripts/check-tool-registration.ts), commands, agents, public numeric docs claims, and — when SWARM_DEP_FRESHNESS_CHECK=1 (set in CI) — dependency freshness (issue #1899: locked @opencode-ai/* resolution AND the vendored @opencode/plugin pin in src/host/v2/types.ts vs npm-latest (issue #2910; fires past a five-minor-series gap), a fail-open advisory notice that never blocks even under enforce). The focused required-check-contract detector reconciles exact required contexts against freshness-bound captured GitHub evidence; its pre-promotion RULESET_DIVERGENCE notice for intended-required drift is visible but non-blocking until the live ruleset is promoted. Blocking drift findings fail the CI job while annotations and the sticky PR comment preserve the full report; local bun run drift:check remains soft-warn unless invoked with --enforce (or DRIFT_CHECK_ENFORCE=1). New cross-tree skill pairs must be classified in src/config/skill-mirrors.ts or the check warns. Run bun run drift:check --enforce locally before pushing skill/tool/command/agent/docs-claim changes.
  • Skill audience metadata: every tracked static SKILL.md in the three native trees declares an audience under the Agent-Skills-spec metadata: map (issue #2907; a legacy top-level audience: still resolves during the transition, and a metadata-vs-top-level conflict fails closed). metadata.audience: swarm-plugin identifies this repository's static plugin skills. Consumer repositories may use a domain tag and an optional runner:opencode|claude|codex constraint (list values serialize as JSON array strings — metadata is string-typed per the spec; see docs/skills.md "Frontmatter conventions"). Untagged runtime-generated skills remain backward-compatible match-all because the generator cannot invent a repository identity. Empty or malformed declarations fail closed, and explicit SKILLS: references are validated even when automatic propagation is disabled.
  • Parity assertions across sibling map entries are intentional. If a parity test fails, mirror the change to siblings (most common) or update the invariant test if the design intent has actually changed.
  • Opt-in tool maps: Some tools are intentionally gated behind feature flags and use separate opt-in maps (e.g., MEMORY_AGENT_TOOL_MAP for memory tools when memory.enabled === true; EPIC_AGENT_TOOL_MAP for the architect-only Epic tools when epic.mode.enabled === true, tested by tests/unit/agents/epic-tool-gating.test.ts). These tools must still have a TOOL_METADATA entry, a TOOL_MANIFEST handler, and a barrel export in src/tools/index.ts, but they are excluded from (or included in) the derived TOOL_METADATA.agents lists and merged into agent configs conditionally at build time. Opt-in maps must be validated by tests that verify: (a) tools do NOT appear when the feature is disabled, (b) tools DO appear when the feature is enabled, (c) the merged tool set is correct for each agent role. See the memory-tool merge path in src/agents/index.ts and tests/unit/agents/memory-tool-gating.test.ts for the current pattern.
  • Agent registration changes must test both legacy unprefixed AND multi-swarm prefixed agent names. v7.3.x regression: a schema default on default_agent ("architect") combined with strict equality demoted every *_architect to subagent in multi-swarm configs, so OpenCode showed the plugin as loaded but no swarm architect agents appeared. Tests that only used legacy unprefixed architect missed the bug entirely. Any change to primary/subagent selection, default_agent, or getAgentConfigs MUST include a multi-swarm swarms: { local: ..., mega: ... } test that asserts at least one prefixed agent is mode: 'primary'.

12. Release / cache hygiene

  • A plugin fix is incomplete if users stay pinned to a stale OpenCode plugin cache. Install / update / cache changes must cover all known cache layouts, including the macOS / Windows variants documented in v6.86.9 (Layout 1 ~/.cache/opencode/packages/opencode-swarm@latest, Layout 2 ~/.config/opencode/node_modules/opencode-swarm, Layout 3 ~/.cache/opencode/node_modules/opencode-swarm).
  • Cache-deletion code must use isSafeCachePath four-layer defense (depth, basename whitelist, recognized parent, canonical structure).
  • Never hand-edit package.json#version, CHANGELOG.md, or .release-please-manifest.json. release-please owns those.
  • Every user-visible PR ships a docs/releases/pending/<unique-slug>.md fragment (mandatory — see contributing.md). Do NOT compute a next version or create docs/releases/vX.Y.Z.md; release-please owns the version, and scripts/release-notes-fragments.mjs aggregates pending fragments into the release PR / GitHub Release body. The aggregated text is the only narrative future users will see.

Invariant audit required in PRs

Every PR that touches a relevant area must include a ## Invariant audit section in its description (see .claude/skills/commit-pr/SKILL.md). The required format is:

## Invariant audit
- 1 (plugin init):       touched / not touched — <evidence>
- 2 (runtime portability): touched / not touched — <evidence>
- 3 (subprocesses):       touched / not touched — <evidence>
- 4 (.swarm containment): touched / not touched — <evidence>
- 5 (plan durability):    touched / not touched — <evidence>
- 6 (test_runner safety): touched / not touched — <evidence>
- 7 (test writing):       touched / not touched — <evidence>
- 8 (session state):      touched / not touched — <evidence>
- 9 (guardrails/retry):   touched / not touched — <evidence>
- 10 (chat/system msg):   touched / not touched — <evidence>
- 11 (tool registration): touched / not touched — <evidence>
- 12 (release/cache):     touched / not touched — <evidence>

For every "touched" entry, the evidence must be a concrete artifact: a command run with its output, a test that proves the invariant, a grep showing no remaining anti-patterns, or a quoted spec citation. "Looks fine" is not evidence.

If you cannot prove a touched invariant from source and test output, do not push.

When in doubt

  • Ask, don't guess. The patterns in this file are the result of prior outages — every one has a release note in docs/releases/.
  • Prefer the smallest patch that closes the issue without unwired functionality, untested branches, or hidden regressions.
  • Defense in depth is cheap; an unbounded await is not.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.