engineering-conventions

star 351

Guidelines and non-negotiable engineering invariants for modifying opencode-swarm. Load before architecture, plugin initialization, subprocess, tool registration, plan durability, .swarm storage, runtime portability, session/global state, guardrails/retry, chat/system message hooks, or release/cache changes. Authoritative source: AGENTS.md at the repo root and docs/engineering-invariants.md.

zaxbysauce By zaxbysauce schedule Updated 6/16/2026

name: engineering-conventions description: > Guidelines and non-negotiable engineering invariants for modifying opencode-swarm. Load before architecture, plugin initialization, subprocess, tool registration, plan durability, .swarm storage, runtime portability, session/global state, guardrails/retry, chat/system message hooks, or release/cache changes. Authoritative source: AGENTS.md at the repo root and docs/engineering-invariants.md. effort: medium

Engineering Conventions for opencode-swarm (Claude Code)

Authoritative source: AGENTS.md at the repo root and docs/engineering-invariants.md. This skill is a pointer + summary so Claude Code loads the right invariants before touching dangerous areas. Read AGENTS.md first. When this skill conflicts with AGENTS.md, AGENTS.md wins.

When to load this skill

Load this skill before beginning implementation work that touches any of:

  • src/index.ts (plugin entry / initializeOpenCodeSwarm)
  • src/hooks/* (any hook that may run during init or QA review)
  • src/tools/* (tool registration, working-directory anchoring, test_runner)
  • src/utils/bun-compat.ts (subprocess shim — every spawn in the repo eventually flows through here)
  • src/utils/timeout.ts (the withTimeout primitive used by every bounded init step)
  • src/utils/gitignore-warning.ts (Git hygiene; runs on plugin init path)
  • package.json, build configuration, dist/, plugin export shape
  • Plan ledger / projection / checkpoint code (src/plan/*, .swarm/plan-*)
  • Session / guardrails / runtime state (src/state.ts, src/hooks/guardrails.ts)
  • Tests involving subprocesses, plugin startup, mock.module, or temp directories

If you are not sure whether you are touching one of these, you are touching one of these.

Highest-risk invariants (the ones that have already shipped regressions)

The full list of 12 invariants is in AGENTS.md. The four that have caused the most recent production regressions:

  1. Plugin initialization is bounded and fail-open. Every awaited operation on the plugin-init path must be wrapped in withTimeout(...) and degrade non-fatally on timeout. Issue #704 (v7.0.3) and the v7.3.3 git-hygiene regression both stem from violating this. The OpenCode plugin host silently drops a plugin whose entry never resolves; users see "no agents in TUI / GUI" with no error. Bounded ≠ free: withTimeout only stops an unbounded hang — awaited work's real latency still counts toward the 400 ms repro-704 init deadline. If init work does non-trivial I/O and nothing downstream needs it before server() resolves, defer it with queueMicrotask (the repoGraphHook precedent; PR #1356's bundled-skill sync is the exemplar), don't await it; await only fast (<50 ms) work a later init step depends on. Linux/macOS repro-704 green does not prove Windows — the smoke matrix enforces the 400 ms T1 deadline on the Windows runner, where cold-FS latency is several× higher (an inline-await revision of that sync was caught there and deferred before #1356 merged).
  2. Subprocesses are bounded, non-interactive, and killable. Every bunSpawn(['<bin>', ...]) call must pass cwd, stdin: 'ignore' (unless intentionally interactive), timeout: <ms>, bounded stdio, and call proc.kill() in a finally. An outer withTimeout is not enough — it lets the awaiter proceed but does not abort the child.
  3. Runtime portability — Node-ESM-loadable + v1 plugin shape. No top-level bun: imports in dist/index.js. Default export is { id, server }. All Bun.* calls go through src/utils/bun-compat.ts. v6.86.8 / v6.86.9 are the cautionary tales.
  4. Test mock isolation. mock.module(...) leaks across files in Bun's shared test-runner process. Use a file-scoped _internals dependency-injection seam (see src/utils/gitignore-warning.ts:_internals and src/hooks/diff-scope.ts:_internals) instead. Restore in afterEach. The writing-tests skill covers this in detail; load it before modifying tests.

Cross-link: writing tests

For test changes, also load .claude/skills/writing-tests/SKILL.md. It covers bun:test API, mock isolation rules, CI per-file isolation, and cross-platform anti-patterns.

Hard warning: do NOT use broad test_runner for repo validation

The OpenCode test_runner tool is for targeted agent validation with explicit files: [...] or small targeted scopes. It is not the way to validate the full repo from inside a Claude Code session that orchestrates OpenCode. In this repo:

  • MAX_SAFE_TEST_FILES = 50 (src/tools/test-runner.ts). Resolutions exceeding this return outcome: 'scope_exceeded' with a SKIP. Do not lean on this — broad scopes can stall or kill OpenCode before that guard fires.
  • For repo validation, run the shell commands in contributing.md / TESTING.md directly (per-file isolation loops + tier orchestration).
  • scope: 'all' requires allow_full_suite: true and is intended for opt-in CI mirrors only. Default to files: [...] instead.

Agent prompt strings — escaping pitfalls

Agent prompts in src/agents/*.ts are large TypeScript template literals. They frequently contain characters that have special meaning inside template literals and cause silent parse errors if unescaped:

Character Inside template literal Correct escape
Backtick ` Terminates the literal \` (single backslash — renders as ` in output)
${ Starts an interpolation \${ (single backslash)
Literal backslash \ Consumed by escape processing \\ (double backslash renders as \ in output)

The most common failure pattern: A coder adds an inline code example containing backticks to an agent prompt string. The unescaped backtick silently terminates the template literal, producing a SyntaxError: Unexpected identifier or Unexpected token at the character after the backtick — which appears unrelated to the actual cause.

// WRONG — unescaped backtick terminates the template literal
const PROMPT = `
Use `bun:test` for all tests.   // ← bare backtick before "bun" closes the literal
`;

// CORRECT — single backslash before each backtick; renders as Use `bun:test` in output
const PROMPT = `
Use \`bun:test\` for all tests.
`;

// OVER-ESCAPED (also wrong) — triple backslash produces literal \` in the rendered prompt
const PROMPT = `
Use \\\`bun:test\\\` for all tests.  // renders as: Use \`bun:test\` (backslashes visible)
`;

Detection: If bun run build or bun --smol test reports a parse error at a line number that seems far from any recent change, search the surrounding lines for an unescaped backtick inside a template literal.

Prevention: After adding any inline code example to an agent prompt, run bun run build immediately — the TypeScript compiler catches unescaped backticks as a syntax error before any tests run.

The invariant-audit gate (PR-time)

Every PR that touches a relevant area must include an ## Invariant audit section in its description. The format is in AGENTS.md ("Invariant audit required in PRs"). The commit-pr skill enforces this gate before push/PR — load it before committing.

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

Evidence file flow (.swarm/evidence/{taskId}.json)

Agents NEVER write these files directly. The delegation-gate hook writes them automatically after each reviewer/test_engineer Task delegation returns. The schema is defined in src/gate-evidence.ts:

export interface GateEvidence {
  sessionId: string;  // actual session ID from the Task delegation
  timestamp: string;  // ISO 8601
  agent: string;     // 'reviewer' | 'test_engineer' | 'sme' | etc.
}

export interface TaskEvidence {
  taskId: string;
  required_gates: string[];
  gates: Record<string, GateEvidence>;
  turbo?: boolean;
}

How to verify the flow is working:

  1. After dispatching a reviewer/test_engineer Task, the delegation-gate toolAfter hook should automatically write/update .swarm/evidence/{taskId}.json.
  2. When you call update_task_status(completed), the tool reads the evidence file and verifies the required_gates are all present.
  3. If update_task_status fails with "required QA gates not yet satisfied" or "Evidence file is corrupt or unreadable," inspect the evidence file with cat .swarm/evidence/{taskId}.json to diagnose.

Do NOT manually write or fabricate evidence files. This bypasses the gate enforcement and can cause downstream tool failures when the real session IDs are looked up.

When to suspect the flow is broken:

  • The evidence file doesn't exist after a reviewer/test_engineer Task delegation returns
  • The evidence file exists but has wrong agent or sessionId values
  • The plan has newly-added task IDs that the hook may not recognize

Workaround for broken flow: If the hook consistently fails to write the evidence file, escalate to the user — do NOT silently fabricate evidence with placeholder session IDs. The gate check exists to enforce that a real review/test run happened.

See .claude/skills/writing-tests/SKILL.md § Cross-Platform Requirements → "macOS rename-visibility race" for the ENONENT retry pattern that this gate flow triggers on macOS CI.

Install via CLI
npx skills add https://github.com/zaxbysauce/opencode-swarm --skill engineering-conventions
Repository Details
star Stars 351
call_split Forks 35
navigation Branch main
article Path SKILL.md
More from Creator