Skip to content

Releases: zaxbysauce/opencode-swarm

v7.3.5

03 May 00:48
be7a7ac

Choose a tag to compare

v7.3.5 — Restore multi-swarm primary architects (no agents in TUI/GUI after v7.3.x)

What changed

Primary fix — multi-swarm *_architect agents are visible again

After upgrading to v7.3.x, users with multi-swarm configs (swarms: { local: …, mega: …, paid: …, modelrelay: … }) reported that OpenCode showed the plugin as loaded but no swarm architect agents appeared in the GUI/TUI — only the native build and plan agents were selectable.

The plugin was importing successfully, server() was returning 90 agents and 58 tools, and the config hook was injecting all 90 agents into opencodeConfig.agent. The injected agents were all prefixed (mega_architect, paid_architect, lowtier_architect, modelrelay_architect, local_architect, …). None of them were marked mode: "primary", so OpenCode could not surface any of them as a selectable session default.

Root cause:

  • v7.0.0 marked an agent primary if agent.name === "architect" or agent.name.endsWith("_architect").
  • v7.3.x added default_agent with a .default("architect") schema default and switched to exact-string matching (agent.name === defaultAgent). In multi-swarm configs there is no agent literally named architect — they are all prefixed — so every architect was demoted to subagent.
  • Previous tests for default_agent only used the legacy unprefixed agent set, so the regression went undetected.

Fix (no OpenCode-side changes; PR #735's Windows / plugin-loading hardening is preserved):

  • src/config/schema.tsdefault_agent is now an optional string with no schema default, so omitted vs explicit "architect" are distinguishable. Empty / whitespace-only values normalize to undefined. Arbitrary strings parse without invalidating the entire plugin config; semantic validation now happens at agent-generation time.
  • src/agents/index.ts — new exported helper resolvePrimaryAgentNames(agentNames, defaultAgent) returns the set of generated agents that should be primary plus a reason tag and an optional warning. Resolution rules:
    • Omitted ⇒ every architect-role agent (canonical base role === "architect") is primary. Restores v7.0.0 multi-swarm behavior.
    • Exact generated-name match (e.g. "local_architect") ⇒ only that agent.
    • Base role in ALL_AGENT_NAMES (e.g. "coder") ⇒ every generated agent whose canonical base role matches.
    • Unknown / unmatched ⇒ warns once, falls back to architect-role primaries; if no architect role exists, falls back to the first generated agent. Never returns zero primaries when at least one agent exists.
    • "not_an_architect" is not treated as a base-role request even though stripKnownSwarmPrefix() returns "architect" for it — base-role matching only fires when the literal user value is itself one of ALL_AGENT_NAMES.
  • src/agents/index.ts:getAgentConfigs — calls the resolver once after createAgents(config), marks mode: "primary" for agents in the resolver's set (with permission.task: "allow" and model deleted as before), and mode: "subagent" otherwise. Tool-filter behavior, tool snapshots, council validation, prompt/model/variant logic, and existing swarm-prefix behavior are unchanged.
  • Diagnostic invariantgetAgentConfigs now emits a deferred warning if a non-empty generated agent set produces zero primaries (defense in depth against a future regression in the resolver). The check is non-blocking.

Tests

  • tests/unit/config/default-agent-config.test.ts — fully replaced. Removes the now-incorrect assertions that the schema rejects arbitrary strings and defaults default_agent to "architect". Adds direct unit tests for resolvePrimaryAgentNames (every resolution branch, plus the not_an_architect matching guard) and end-to-end getAgentConfigs tests against real multi-swarm configs, not legacy unprefixed agents. Covers:
    • omitted default_agent with prefixed-only swarms ⇒ every *_architect primary
    • omitted default_agent with a default swarm + extras ⇒ architect AND every *_architect primary
    • exact default_agent: "local_architect" ⇒ only local_architect primary
    • base default_agent: "architect" ⇒ all generated architect-role agents primary
    • exact default_agent: "local_coder" ⇒ only local_coder primary
    • base default_agent: "coder" ⇒ every generated coder-role agent primary
    • invalid default_agent ⇒ falls back to architect-role primaries
    • all architects disabled + invalid default_agent ⇒ falls back to one primary, never zero
    • schema accepts omitted/base-role/prefixed/arbitrary strings, normalizes whitespace
  • tests/unit/config/schema.test.ts — the "empty object applies defaults" test no longer expects default_agent: "architect" in the parsed output (the schema default was the proximate cause of the bug).
  • tests/integration/config-hook-multi-swarm.test.ts (new) — boots the plugin via default.server(ctx) against a temp project with a multi-swarm config, calls hooks.config({}), and asserts that opencodeConfig.agent contains prefixed primary architects. This is the exact path OpenCode exercises in production.

Why

AGENTS.md invariant 11 (tool registration + agent-map coherence) now requires that any change to primary/subagent selection or default_agent test both legacy unprefixed and multi-swarm prefixed agent names. The v7.3.x test suite passed because it only used legacy single-swarm fixtures; the regression slipped past CI on every push between v7.3.0 and v7.3.4.

Migration steps

No user action required. Existing configs without default_agent now resolve every *_architect to primary, restoring the v7.0.0-compatible multi-swarm experience. Configs that explicitly set default_agent to a base role or exact generated name continue to work and now also accept arbitrary strings without invalidating the entire config (semantic validation issues a warning and falls back to architect-role primaries instead).

If you previously upgraded to v7.3.0–v7.3.4 and the plugin loaded but showed no swarm architect agents in the TUI/GUI: upgrade to v7.3.5 and run bunx opencode-swarm update to refresh OpenCode's plugin cache, then restart OpenCode.

Breaking changes

None. default_agent accepts a strictly larger set of values than before; legacy explicit values ("architect", "coder", "reviewer", etc.) keep working unchanged.

Known caveats

  • An invalid default_agent no longer causes the merged plugin config to fail Zod validation and trigger the loadPluginConfig safe-defaults fallback. Instead the resolver warns once and falls back to architect-role primaries. This is intentional — losing an entire user config because of a typo in default_agent was the wrong failure mode.

Invariant audit

  • 1 (plugin init): not touched
  • 2 (runtime portability): not touched
  • 3 (subprocesses): not touched
  • 4 (.swarm containment): not touched
  • 5 (plan durability): not touched
  • 6 (test_runner safety): not touched
  • 7 (test writing): touched — new tests use bun:test, real swarms fixtures (not legacy unprefixed agents), temp dirs via os.tmpdir() + realpathSync. tests/unit/config/default-agent-config.test.ts calls mock.module('node:fs/promises', ...) to stub the agent-tool-snapshot writer — this matches the existing convention in tests/unit/tools/co-change-analyzer.adversarial.test.ts and is covered by the per-file CI isolation loop. Evidence: bun --smol test tests/unit/config/default-agent-config.test.ts tests/unit/config/schema.test.ts tests/integration/config-hook-multi-swarm.test.ts all green; full tests/unit/config group: 1051 pass / 30 fail (all 30 pre-existing on be21cf0 baseline; my changes net −1 failure).
  • 8 (session state): not touched
  • 9 (guardrails/retry): not touched
  • 10 (chat/system msg): not touched
  • 11 (tool registration): touched — primary-vs-subagent selection rewritten via resolvePrimaryAgentNames. Multi-swarm coverage added (see test list above) and AGENTS.md invariant 11 amended to require both legacy unprefixed and multi-swarm prefixed test fixtures.
  • 12 (release/cache): not touched — package.json#version, CHANGELOG.md, and .release-please-manifest.json were NOT manually edited. release-please owns those.

v7.3.4

02 May 17:52
be21cf0

Choose a tag to compare

v7.3.4 — Fix Windows / cross-platform plugin-load hang + repository engineering contract

What changed

Primary runtime fix — ensureSwarmGitExcluded no longer hangs plugin init (cross-platform)

The v7.3.3 commit 17fc49f added ensureSwarmGitExcluded to auto-protect .swarm/ from Git pollution before any runtime write. The function correctly handled worktrees, submodules, and tracked-file detection, but it shipped the call on the plugin-init critical path with three latent defects:

  1. The call site in src/index.ts awaited ensureSwarmGitExcluded(...) with no outer withTimeout (compare the adjacent loadSnapshot call, which is wrapped in withTimeout(5_000)).
  2. Each of the four sequential bunSpawn(['git', ...]) invocations inside ensureSwarmGitExcluded was issued without a per-call timeout.
  3. None of those spawns set stdin: 'ignore'. Bun on Windows can leave the child waiting for stdin EOF that never arrives.

On any host where one of the four git children fails to exit promptly — Windows 11 antivirus interception, credential helper prompts, NFS-stalled .git, sandboxed Desktop / GUI exec contexts, code-signing handshakes — the awaited Promise.all never resolves. OpenCode's plugin host silently drops a plugin whose entry never resolves (the same failure mode as issue #704), so users see "no agents in TUI / GUI" with no error message at all. The user-reported reproduction was Windows 11; the underlying defect is platform-agnostic.

Fix (defense in depth, no process.platform branches):

  • src/index.ts — wrap the call in withTimeout(ensureSwarmGitExcluded(...), 3_000, ...) and treat timeout as non-fatal via .catch(...) + the existing log helper. Mirrors the loadSnapshot pattern.
  • src/utils/gitignore-warning.ts — every bunSpawn(['git', ...]) invocation now passes { timeout: 1_500, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }, with the spawn-and-await wrapped in try { … } finally { proc.kill() } so the child is guaranteed to be killed even on a runtime that ignores timeout.
  • src/hooks/diff-scope.ts — same hardening at both bunSpawn(['git', 'diff', ...]) sites in getChangedFiles. This is the same defect class but not on the plugin-init path; it can hang QA-review hooks under the same host conditions.
  • New exports: ENSURE_SWARM_GIT_EXCLUDED_OUTER_TIMEOUT_MS = 3_000 and ENSURE_SWARM_GIT_EXCLUDED_PER_CALL_TIMEOUT_MS = 1_500. The constants are reused by diff-scope.ts so the per-call budget is consistent.
  • New regression test files using a file-scoped _internals dependency-injection seam (not mock.module, which leaks across files in Bun's shared test-runner process):
    • tests/gitignore-warning-bounded.test.ts (3 tests: constants exported, every spawn site receives the new options, every spawn site invokes proc.kill() in finally).
    • tests/diff-scope-bounded.test.ts (1 test: same contract for getChangedFiles).

Repository engineering contract — AGENTS.md, engineering invariants, conventions skill, tightened commit-pr

The v7.3.3 regression belongs to a broader pattern already visible in repo history: #704 (v7.0.3, repo-graph Desktop hang), #675 (v6.86.8 / v6.86.9, plugin export shape and Node-ESM compatibility), and now #732 (v7.3.3, Git hygiene on the init path). The common defect is plugin registration awaiting unbounded environmental work. This release adds first-class repository documentation and skill hardening so future Claude Code and OpenCode agents are forced to catch this class of bug.

  • AGENTS.md (new, root) — concise operational engineering contract. 12 non-negotiable invariants (plugin init, runtime portability, subprocesses, .swarm/ containment, plan durability, test_runner safety, test writing, session/global state, guardrails/retry, chat/system-message hooks, tool registration, release/cache hygiene). Required reading order, prime directive, invariant-audit-required-in-PRs section.
  • docs/engineering-invariants.md (new) — long-form rationale and historical failure map (v6.48.0, v6.80.2, v6.82.2, v6.85.1, v6.86.8, v6.86.9, v6.86.14, v7.0.1, v7.0.3, v7.3.3). Anti-pattern → required pattern → verification examples for the highest-risk invariants. Pasteable PR-description template.
  • .opencode/skills/engineering-conventions/SKILL.md (new) — OpenCode-side skill that points at AGENTS.md and lists the highest-risk invariants. Loaded automatically before architecture / plugin-init / subprocess / tool-registration / plan-durability / .swarm / runtime-portability changes.
  • .claude/skills/engineering-conventions/SKILL.md (new) — Claude-Code-side equivalent.
  • CLAUDE.md (updated) — directs Claude to read AGENTS.md first; clarifies that swarm-mode adds workflow structure, not exceptions to the engineering invariants.
  • .claude/skills/commit-pr/SKILL.md (updated) — new mandatory Step −1 Engineering Invariant Audit gate: read AGENTS.md, identify touched invariant categories, add ## Invariant audit to PR body with concrete evidence per touched category. Hard-stop language: "If any touched invariant cannot be proven from source and test output, do not push." Step adds invariant-specific commands (build + repro-704 + dist import for plugin-init / runtime-portability / subprocess changes; subprocess grep; config + tools tests for tool registration). Pre-merge checklist now includes the invariant audit, the build/repro-704/dist-import line, the subprocess hardening line, and the explicit "do not use broad test_runner for repo validation" line.
  • .opencode/skills/writing-tests/SKILL.md and .claude/skills/writing-tests/SKILL.md (updated) — high-visibility note that the OpenCode test_runner is for targeted agent validation only; MAX_SAFE_TEST_FILES = 50; broad scopes can stall or kill OpenCode; allow_full_suite is for opt-in CI mirrors. For repo validation, use the shell commands in the skill.
  • TESTING.md (updated) — points at AGENTS.md; repeats the test_runner warning.
  • contributing.md (updated)AGENTS.md added to the authoritative reading flow; PR checklist gains the invariant-audit, test_runner-broad-scope, and startup-validation lines.

Why

  • The Windows / plugin-load symptom was reported by a user who had updated to v7.3.3 and was greeted by an OpenCode session with no agents available in either TUI or GUI. The branch name (claude/fix-plugin-loading-windows-ubPU2) and prior release history (#704) made the failure-mode hypothesis straightforward; the fix had to be platform-agnostic because the same defect class is reachable on macOS Desktop sandboxes and Linux Snap / Flatpak / NFS hosts.
  • The historical failure map shows the same engineering mistake recurring across multiple unrelated subsystems. Documenting the invariants and forcing an audit gate in commit-pr is the lowest-cost intervention that catches future instances at PR time, before they ship.

Migration steps

No user action required. The fix is transparent. On a healthy host, every git call still completes in <50 ms; the timeouts only fire on pathological hosts where the previous code would have hung indefinitely.

If you previously upgraded to v7.3.3 and the plugin failed to load on Windows 11 (or any host with an antivirus / sandbox / network home that intercepts subprocess execution): upgrade to v7.3.4 and run bunx opencode-swarm update to clear OpenCode's plugin cache (covers all three known cache layouts on Windows / macOS / Linux), then restart OpenCode.

Breaking changes

None.

Known caveats

  • If the outer 3 s budget fires on a host with extremely slow git, that session does not get the .swarm/ exclude write or the tracked-file remediation warning. The plugin still loads and works. To re-run the git-hygiene check, restart OpenCode (the once-per-process flag resets on process startup).
  • The _internals DI seam exported from src/utils/gitignore-warning.ts and src/hooks/diff-scope.ts is test-only. Production code must continue to call through it, but external callers should not import _internals — the underscore prefix marks it as a private surface.

v7.3.3

01 May 20:54
c8ce977

Choose a tag to compare

v7.3.3 — Git Hygiene: Auto-protect .swarm/ before writes

What changed

Fixed: .swarm/ runtime-artifact Git pollution

Users reported "weird uncommitted changes" with paths like git:<sha>:.swarm/dark-matter.md, indicating runtime files were tracked in Git. Every plugin startup writes to .swarm/, and tracked files bypass .gitignore, causing permanent diffs.

Changes

  • ensureSwarmGitExcluded(): New async function that auto-protects .swarm/ from Git pollution before any write:

    • Uses git rev-parse --show-toplevel and --git-path (handles worktrees and submodules where .git is a file, not a directory)
    • Checks if .swarm/ is already ignored via git check-ignore
    • Appends .swarm/ to .git/info/exclude (local-only rules) if not already covered
    • Detects tracked .swarm/ files via git ls-files and emits unsuppressed remediation warning
    • Idempotent — safe to call multiple times; .swarm/ appears only once in exclude
  • Protection timing: ensureSwarmGitExcluded() now runs before initTelemetry(), writeSwarmConfigExampleIfNew(), and writeProjectConfigIfNew() to prevent any write from creating tracked files

  • validateDiffScope() filter: Runtime .swarm/ paths are now filtered during diff scope validation to prevent tracked runtime files from triggering false scope-violation warnings in QA review

  • Unsuppressed warnings: Hygiene warnings (tracked-file remediation) are never suppressed by quiet mode; they must be visible to users

  • Backward compat: Original warnIfSwarmNotGitignored() remains exported for compatibility

Why

Tracked .swarm/ files bypass .gitignore rules entirely — Git will include them in every status and diff. The root cause was:

  1. No protection before the first .swarm/ write in a non-.gitignore'd repo
  2. Lack of detection for already-tracked .swarm/ files
  3. Advisory-only warnings that ran after damage was done

Now the protection is proactive (before any write) and uses git CLI to safely handle all repository layouts (monorepos, worktrees, submodules).

Migration steps

No migration required. The protection runs automatically on every plugin startup. If a repo has already-tracked .swarm/ files:

  • A remediation warning will appear with instructions to run:
    git rm -r --cached .swarm
    echo ".swarm/" >> .gitignore
    git commit -m "Stop tracking opencode-swarm runtime state"

Breaking changes

None.

Known caveats

  • .swarm/ protection relies on .git/info/exclude (local-only rules managed per-repo) and git CLI availability. Repos with disabled git or missing .git/ directory will be skipped without error.
  • If a repo has very old tracked .swarm/ files, users will see the remediation warning on every startup until they run git rm -r --cached .swarm.

v7.3.2

01 May 18:30
8d89c9d

Choose a tag to compare

7.3.2 (2026-05-01)

Bug Fixes

  • council: make council additive at phase-level, never suppress per-task Stage B gates (#728) (aa96c74)

v7.3.1

01 May 14:51
a3b881e

Choose a tag to compare

7.3.1 (2026-05-01)

Bug Fixes

  • reduce false telemetry failures and mirrored state spam (#727) (079d8cc)

v7.3.0

01 May 11:44
c79d6b1

Choose a tag to compare

7.3.0 (2026-05-01)

Features

  • add first-run UX, categorized help, and deprecation aliases (#723) (0e2ed93)

v7.2.0

01 May 02:03
73b7d14

Choose a tag to compare

v7.2.0 Release Notes

What Changed

Auto-create .opencode/opencode-swarm.json on plugin init (#657)

When opencode-swarm loads in a project directory that has no local
.opencode/opencode-swarm.json, the plugin now creates one automatically.
The file is an empty JSON object ({}) that deep-merges as a no-op against
any global config. A console.warn message points users to the global config
and .swarm/config.example.json for customization guidance.

Why: First-time users had no discoverable per-project customization file
and no clear indication that one was expected. This change makes the project
config surface on first use without requiring bunx opencode-swarm install.

Design:

  • Written with { flag: 'wx' } (exclusive create) — atomic and safe under
    concurrent plugin loads; EEXIST is silently swallowed so an existing file
    is never overwritten.
  • An empty {} JSON object deep-merges as a mathematical no-op against any
    global config (~/.config/opencode/opencode-swarm.json on Linux/macOS,
    %APPDATA%\opencode\opencode-swarm.json on Windows). Users who rely
    entirely on their global config will see zero change in resolved settings.
  • All filesystem errors (permissions, disk full, read-only mount) are caught
    and swallowed — the plugin continues with defaults. Creation is non-fatal.
  • The one-line console.warn that announces creation is gated by
    config.quiet, consistent with all other startup messages.

Also fixed: writeSwarmConfigExampleIfNew silently failed on brand-new
projects because .swarm/ did not yet exist when writeFileSync ran. Added
the missing mkdirSync guard so .swarm/config.example.json is now reliably
written on first use.

Files Changed

  • src/config/project-init.ts — new module exporting writeProjectConfigIfNew
  • src/index.ts — import and call writeProjectConfigIfNew; fix .swarm/ mkdirSync in writeSwarmConfigExampleIfNew
  • tests/unit/config/project-init.test.ts — 9 bun:test cases covering creation, idempotency, no-overwrite, JSON validity, quiet flag, and non-fatal error paths

Migration Steps

None. The auto-created file is an empty JSON object and has no effect on existing
global-config-only setups. Delete or ignore the file in any project where
you do not want per-project overrides.

Breaking Changes

None.

Known Caveats

  • On read-only filesystems or containers without write access to the project
    root, the file will not be created (non-fatal). The plugin continues normally.
  • The console.warn announcement is suppressed when quiet: true is set in
    your global config.

v7.1.1

30 Apr 19:26
7c3cbb2

Choose a tag to compare

7.1.1 (2026-04-30)

Bug Fixes

  • suppress knowledge-injector headroom warning from chat UI (#715) (db63336)

v7.1.0

30 Apr 17:36
5728d74

Choose a tag to compare

v7.1.0 Release Notes

What Changed

Mandatory Reuse Scan & Duplicate Prevention Gate

Added a hard-gated reuse scan protocol that runs before any new function, utility type, or class is written. The Reviewer independently re-verifies the scan, and the Architect enforces the verdict field.

Four-layer defense-in-depth:

  1. Coder REUSE SCAN PROTOCOL — The Coder must semantically search src/utils/, src/hooks/, src/tools/, src/services/ for existing implementations before writing new code. Reports REUSE_SCAN in DONE output with self-audit checkbox.

  2. Reviewer REUSE RE-VERIFICATION — The Reviewer independently runs 3+ semantic search queries per new export. DUPLICATION_DETECTED causes immediate Tier 1 CORRECTNESS rejection. Cross-checks the Coder's scan report. Outputs REUSE_RE_VERIFICATION in verdict format.

  3. Slop Detector Passive Telemetry — New checkDuplicateUtility heuristic detects name collisions in utility directories at write/edit time. Advisory only — provides observability without blocking the pipeline.

  4. Architect Enforcement — The Architect's Stage B gate mandates REUSE_RE_VERIFICATION field presence in reviewer verdicts. Completion checklist includes the field with semantic tuple validation (e.g., EXPORTS_ADDED non-empty → REUSE_RE_VERIFICATION must be VERIFIED or DUPLICATION_DETECTED).

Files Changed

  • src/agents/coder.ts — REUSE SCAN PROTOCOL block, REUSE_SCAN field in DONE template, SELF-AUDIT checkbox
  • src/agents/reviewer.ts — REUSE RE-VERIFICATION block, REUSE_RE_VERIFICATION in verdict/output format
  • src/hooks/slop-detector.ts — DUPLICATE_UTILITY heuristic, type union extension
  • src/agents/architect.ts — Stage B enforcement, ANTI-EXEMPTION pair, completion checklist
  • src/agents/coder.test.ts — 15 tests for REUSE SCAN PROTOCOL
  • src/agents/reviewer.test.ts — 14 tests for REUSE RE-VERIFICATION
  • src/hooks/slop-detector.test.ts — 3 new tests for DUPLICATE_UTILITY
  • src/agents/architect.commands-list.test.ts — 3 new tests for architect enforcement

Migration Steps

None. All changes are additive — no existing behavior is modified. The new gates activate automatically.

Known Caveats

  • The slop detector's checkDuplicateUtility uses name-only collision detection (no signature analysis). False positives are possible but the heuristic is advisory-only.
  • The reviewer's "implements the same behavior" criterion (step 3) requires human-level judgment — semantic similarity is assessed by the LLM, not a deterministic check.

v7.0.3

30 Apr 14:21
f8e32e1

Choose a tag to compare

v7.0.3

What changed

OpenCode Desktop loading-screen hang — fixed (issue #704)

Plugin init blocked the event loop, freezing Desktop's loading screen indefinitely.

The root cause: repoGraphHook.init() was called directly in the plugin's
initializeOpenCodeSwarm function. JavaScript executes async function bodies
synchronously up to the first await, so the recursive readdir/statSync
walk held the Node/Bun event loop. OpenCode's plugin loader await server(...)
never resolved, and Desktop displayed a frozen loading screen forever. The TUI
and CLI tolerated the same blocking init because they don't depend on the same
async plugin-loader contract.

Three aggravating factors were also fixed:

  1. Symlink cycles caused unbounded recursion. findSourceFiles had no
    visited-path set and used statSync (follows symlinks), so any cycle
    (macOS iCloud/FileVault, Windows junctions, Linux FUSE) caused the walk to
    loop forever. A seenRealPaths set using realpathSync/realpath now
    detects cycles and bails.

  2. maxFiles cap was applied after the walk, not during. A large repo
    would still complete a full scan before truncating results. The cap is now
    enforced inside the traversal loop.

  3. Direct Bun.* calls threw ReferenceError under Node. The compiled
    bundle targets --target node. OpenCode's own plugin-loader source
    ($: typeof Bun === "undefined" ? undefined : Bun.$) confirms plugins run
    under Node in some configurations. All 26 Bun.file, Bun.write,
    Bun.spawn, Bun.spawnSync, and Bun.hash call sites now go through a
    portability shim that delegates to native Bun when available and falls back
    to node:fs/promises + node:child_process otherwise.

Why

Issue #704 was reported on macOS with OpenCode Desktop. The fix also covers
Linux (where FUSE symlink cycles are common) and Windows (where directory
junctions exhibit the same cycle behavior). The ReferenceError fix matters for
any OpenCode configuration where plugins run under Node rather than Bun.

Migration steps

No configuration changes required. The fix is transparent.

If you previously worked around the hang by restarting OpenCode or clearing
plugin state, those workarounds are no longer needed.

Breaking changes

None. The buildWorkspaceGraph / buildWorkspaceGraphAsync API is unchanged.
The new isRefusedWorkspaceRoot() guard rejects os.homedir(), /, /Users,
/home, /root, os.tmpdir(), and Windows drive roots (C:\, C:\Users)
as workspace roots — this is a safety guard, not a behavioral regression for
any real project.

Known caveats

  • The bun-compat.ts shim's bunSpawnSync uses child_process.spawnSync
    under Node, which is blocking. This is only called from paths that were
    already synchronous; no new blocking is introduced.
  • The async walker yields every 200 directory entries. On extremely slow
    network-mounted filesystems the yield interval may need tuning; a
    walkBudgetMs option (default: 30 000 ms) provides a hard time cap.