Skip to content

feat: OpenCode as a managed non-routable host adapter (ADR-0017) - #67

Merged
pacphi merged 11 commits into
pacphi:mainfrom
robertelee78:feat/opencode-host
Jul 29, 2026
Merged

feat: OpenCode as a managed non-routable host adapter (ADR-0017)#67
pacphi merged 11 commits into
pacphi:mainfrom
robertelee78:feat/opencode-host

Conversation

@robertelee78

@robertelee78 robertelee78 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds opencode (opencode.ai) as a third host adapter alongside claude and codex, wired through opencode's own native surfaces — the same "write the host's own config, never a parallel config layer" rule the codex adapter established. After this, ak sync converges opencode with the same guarantees claude/codex already have: ruflo upgrades re-key its agents/skills/plugin, brain updates hot-swap through the stable-spine shim, and ak status tells the truth about all of it.

Design record: docs/adr/0017-opencode-host.md (context / decision / consequences / alternatives).

What opencode gets (all opt-in, hosts.opencode: false default)

Surface What ak manages
~/.config/opencode/opencode.json claude-flow MCP (via claude-flow-mcp, ruflo mcp start fallback) + ruvnet-brain MCP (stable-spine shim), skills.paths, permission allow-patterns
~/.config/opencode/plugins/ruflo-hooks.js Lifecycle bridge — opencode has no settings-hooks; its plugin events map to ruflo's local hook verbs (session restore/end, pre-bash safety, edit/task learning, route context)
~/.config/opencode/agents/ ruflo's agent set converted (not copied) — frontmatter shape, YAML-safe descriptions, all three mcp__* spellings rewritten to claude-flow_*, collision-prefixed, type: documentation skipped
~/.config/opencode/AGENTS.md New agents-opencode guidance target + opencode-flavored block templates (same never-mkdir gate as ~/.codex)
~/.config/opencode/skills/ruflo/ Platform SKILL.md, stamped to the catalog source

The catalog source resolves lazily: opencodeCatalogDir override → $RUFLO_REPO → claude marketplace clone (full catalog, auto-updated) → published @claude-flow/cli (substrate) → nested ruflo/node_modules layout. Drift is detected by stamp comparison (source id + deployed file list), so a ruflo upgrade or marketplace pull triggers re-conversion on the next sync.

Safety contracts (the part I'd want reviewed hardest)

  • Receipt-precise artifact ownership: plugin, agent, stamp, and skill files are refreshed or removed only when their SHA-256 content matches ak's exact last-written receipt. Marker-bearing edits and exact pre-existing copies without receipts remain user-owned. Directories are pruned only when empty.
  • CLI-gated writes: setup/sync/status all no-op gracefully when the opencode CLI is absent (enabled-but-absent never fabricates ~/.config/opencode); sync's opencode step runs after the hosts install branch.
  • Purge ordering: uninstall reads ownership markers before any kit.json removal.

Adjacent fixes folded in (each justified in the ADR)

  • nudge.mjs now uses the shared guidanceTargets + retiredForTarget composition (its contract is "never disagrees with ak status"; the hardcoded subset also missed codex's agents-user — pre-existing gap, now closed).
  • x provider pick preserves hosts.opencode + ownership markers instead of rebuilding providers without them; opencode is excluded from the pickable routing hosts with a warning.
  • detectHosts reads the config-file host's wired state from its own config (was env[null] → permanently "not wired").
  • Dashboard categorizes the opencode subsystem into the Hosts tab (its designed fallback is Runtime, so nothing was ever dropped — this is categorization, plus tests pinning it).
  • hostAuthState gains a home test seam; fixes a pre-existing env-dependent test that failed on any machine with file-based claude credentials (it failed on main here).

Deliberately out of scope (matches codex's own asymmetry)

Routing-table integration (routing.mjs untouched), aqe provider wiring (no opencode provider type exists), statusline (no upstream surface), drivingHost session detection (opencode sets no session env marker), and usage/cost attribution (usage-index.mjs reads claude/codex transcripts only — the pricing surface has no opencode input and shows nothing for it).

Verification

Final head: f1e538b

@pacphi pacphi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes after an independent documentation, command-surface, and dashboard review.

Summary:

  • Documentation precedent is not met yet: the new opencode section is useful, but canonical command/provider/upgrade docs still describe a Claude/Codex-only lifecycle and contradict the actual opencode opt-in path.
  • The implementation touches the relevant lifecycle paths (setup, status, sync, uninstall, provider status/pick/off, guidance/nudge), but command-level integration coverage is incomplete.
  • The dashboard implementation appears to preserve the core read-only/status-row contract and places opencode under Hosts, but its tests do not yet prove rendered behavior or managed-version drift/update-banner behavior.
  • There is a current merge blocker: pnpm run check fails in ESLint.

Verification performed on head 53f41d6:

  • pnpm test: PASS (including 58 dashboard tests)
  • pnpm run test:surface: PASS (22 tests)
  • pnpm run check: FAIL (two no-unused-vars errors at src/commands/x/provider.mjs:20)
  • Dashboard visual harness: timed out waiting for networkidle; I am not treating that timeout as a product finding because it was not isolated from the harness/environment.

Please address the inline blockers and documentation/test gaps. In particular, update docs/PROVIDERS.md and docs/UPGRADING.md even though they are not currently in this diff: both are canonical adoption docs and presently direct users to provider pick, which deliberately refuses opencode.

Comment thread src/commands/x/provider.mjs Outdated
import { loadKitConfig, saveKitConfig } from '../../lib/config.mjs';
import { undoOpencode, removeArtifacts } from '../../lib/opencode.mjs';
import { ok, warn, fail, info, dim, bold, yellow } from '../../lib/output.mjs';
import { installedVersion, cmpVersions } from '../../lib/versions.mjs';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: pnpm run check fails here because both installedVersion and cmpVersions are unused. Please remove this import (or restore the intended use) so the required check gate passes.

Comment thread README.md
### opencode host (opt-in)

[opencode](https://opencode.ai) is a third host alongside claude/codex — wired through its own
native surfaces rather than env flags. `ak setup --opencode` (or `providers.hosts.opencode: true`

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation precedent is not met by adding only the later opencode section. These canonical verb rows still say setup enables Codex only, status covers Claude/Codex only, and sync installs Claude/Codex only (lines 71-73); the Frontier hosts section below also says “one or both” and enumerates only those two. Please make the command table and host model internally consistent with the third host. Also update docs/PROVIDERS.md and docs/UPGRADING.md: they currently teach ak x provider pick as the host opt-in switch, while this PR deliberately rejects opencode in that command. Existing users otherwise have no accurate canonical adoption path.

assert.deepEqual(mcpCommandFor({ binPresent: false, nestedPath: nested }), ['node', nested], 'nested absolute path when no bin (fresh ruflo-only machine)');
assert.deepEqual(mcpCommandFor({ binPresent: false, nestedPath: path.join(d, 'absent.js') }), ['ruflo', 'mcp', 'start'], 'last resort matches the claude/codex registration');
rm(d);
});

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper/owner-module cases are strong, but this suite never executes the new command orchestration. This PR adds machine-mutating branches to setup, sync, uninstall, and provider off/pick plus a new status collector path. Repository precedent explicitly treats those as high-blast-radius command paths. Please add sandboxed command-level tests for enable → setup/sync → status → off/uninstall, including --dry-run, absent opencode CLI, persistence ordering, and teardown. Helper tests alone will not catch a command forgetting a gate, save, or dry-run boundary.

Comment thread tests/dashboard.test.cjs
const r = await get(url);
contains(r.body, 'opencode:"hosts"');
// and it must sort with the host MCP subsystems, not at the unknown end
contains(r.body, '"codex-mcp","opencode"');

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proves payload pass-through and that two mapping strings exist in served JavaScript, but not the dashboard behavior required by docs/MANAGED-TOOLS.md. Please add behavior-level coverage that an opencode row renders in the Hosts group with its level/detail/fix, plus drift/update-banner coverage for the managed opencode-ai version (including external-install/unknown-version behavior as applicable). The implementation looks directionally correct; the gap is that the tests do not lock the user-visible contract.

Comment thread docs/adr/0015-opencode-host.md Outdated
home seam), `src/lib/blocks.mjs` (`agents-opencode` target, new registry rows),
`src/commands/{sync,status,setup,uninstall}.mjs`, `src/commands/x/provider.mjs`
(`off` teardown + `pick` preservation), `src/lib/nudge.mjs` (shared targets),
`src/lib/dashboard-server.mjs` (Hosts-tab categorization),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reference is inaccurate: Hosts-tab categorization is implemented in src/lib/dashboard/client.mjs (GROUP/PREF), not src/lib/dashboard-server.mjs. Please point the ADR to the actual decision-bearing code so future audits can follow the precedent.

@pacphi

pacphi commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Thanks for the substantial work here. The owner module and ownership model are thoughtful, and I do not want the follow-up to become a redesign of the adapter. The remaining work is about making this a first-class ak capability: one truthful adoption path, command-level proof around the lifecycle, and dashboard/version tests that lock the user-visible contract.

Below is the maintainer remediation plan I recommend. Please treat items 1–5 as required for another review. The final section explicitly identifies what should remain out of scope.

1. Restore the required quality gate first

pnpm run check currently stops in ESLint because installedVersion and cmpVersions are imported but unused in src/commands/x/provider.mjs.

Required:

  • Remove the unused import, unless those functions are genuinely needed by the final provider-picker implementation.
  • Run the complete gate after all changes:
pnpm run check
pnpm run test:surface

Acceptance:

  • Both commands exit 0 on the final PR head.
  • Update the PR verification section with the actual final test counts and head SHA. Please do not retain older counts from earlier revisions.

2. Make the host model and adoption path coherent

The current PR calls opencode a third host, but the ordinary host-management command deliberately refuses it. That creates two competing models:

  • setup, status, sync, uninstall, and HOSTS treat opencode as a host;
  • ak x provider pick treats only Claude/Codex as hosts;
  • the upgrade guide says provider pick is how an existing installation adopts a new host capability;
  • the PR instead requires an existing user to rerun ak setup --opencode or hand-edit kit.json.

My preferred resolution is to make ak x provider pick manage all enabled host integrations, while keeping the routing subset explicit:

  • Managed host integrations: claude, codex, opencode.
  • Primary/routing hosts: claude, codex only.
  • Dual routing: still seeds only when Claude and Codex are both enabled.
  • opencode: participates in install/config/guidance/status/sync/teardown, but is never offered as primaryHost, never written into dualRouting, and never described as an AQE provider.

Concretely, in src/commands/x/provider.mjs:

  • Let interactive selection display installed opencode alongside Claude/Codex, with a short qualifier such as “integration host; not a routing target.”

  • Let non-interactive --host accept opencode, for example:

    ak x provider pick --host claude,opencode --yes
    ak x provider pick --host claude,codex,opencode --yes
  • Treat --host as the desired complete enabled-host set, as the option already implies.

  • Continue validating --primary-host against Claude/Codex only.

  • Derive the routing host set separately before calling the dual-routing seed/projector.

  • Apply opencode wiring/artifacts through the same owner-module operation used by setup/sync. Do not duplicate merge or ownership logic in the command.

  • Preserve all unrelated provider fields and ownership markers on every picker rewrite.

  • Keep provider off as the complete reversible reset to the Claude-only default.

  • Update provider help/status output so the distinction between “managed host integration” and “routing host” is visible rather than implicit.

If there is a concrete upstream limitation that makes this unsafe, the acceptable fallback is to add a dedicated post-install command (not a manual kit.json edit) and update all documentation around that command. Merely documenting “rerun setup” is not my preferred outcome because it breaks the established upgrading precedent.

Acceptance:

  • An existing user can enable opencode through a documented, non-bootstrap ak command.
  • The same command can disable it without disturbing user-owned opencode config.
  • Adding opencode never changes primaryHost or seeds opencode routes.
  • Selecting/retuning Claude/Codex does not silently discard opencode ownership state.

3. Add command-level lifecycle tests

tests/kit/opencode.test.mjs provides good owner-module coverage. It does not execute the command orchestration where ordering, gating, persistence, and dry-run mistakes occur.

Please extend the existing command suites rather than putting every scenario into the owner-module test:

  • tests/kit/setup-command.test.mjs
  • tests/kit/setup-host-flags.test.mjs
  • tests/kit/status-command.test.mjs
  • tests/kit/sync-command.test.mjs
  • tests/kit/uninstall-command.test.mjs
  • tests/kit/provider-cli.test.mjs and/or tests/kit/providers.test.mjs

Use the existing home sandbox/fake-bin patterns. Tests must not touch the contributor's real home, global npm tree, or opencode configuration.

Required command scenarios:

Setup

  • ak setup --opencode --yes persists the enabled host and invokes the shared opencode apply path when the CLI/config-home prerequisites are present.
  • ak setup --opencode --dry-run writes no kit.json, opencode.json, plugin, agent, skill, or guidance file.
  • Enabled but absent CLI does not fabricate ~/.config/opencode before installation succeeds.
  • Setup emits the restart guidance after successful wiring.

Provider selection

  • Interactive/non-interactive enablement follows the host-model decision in item 2.
  • --primary-host opencode is rejected and leaves the prior valid primary unchanged.
  • opencode does not enter dualRouting.
  • A provider retune preserves opencodeMcp, opencodeManaged, opencodeCatalogDir, and the existing Codex MCP ownership fields. The PR body already notes the related Codex-marker hazard; do not introduce or retain that data-loss class here.
  • provider off reads ownership before resetting config, restores prior values, removes only marked artifacts, and is idempotent.

Sync

  • Enabled + present + drifted runs opencode convergence after host installation and before final verification.
  • Enabled + absent does not create the config home if installation fails or remains absent.
  • Disabled + installed makes no opencode writes and removes/strips enablement-gated guidance according to the intended lifecycle.
  • --dry-run reports the planned repair without mutating any opencode surface.
  • A second sync is a no-op after convergence.

Status

  • Enabled + converged produces an opencode OK row.
  • Enabled + drifted/foreign/JSONC-refused states produce honest non-OK detail and a specific fix.
  • Disabled or unavailable states follow one documented policy consistently; they must not claim active wiring.
  • ak status --json carries the same subsystem, level, message/detail, and fix consumed by the dashboard.
  • Status remains strictly read-only, including catalog/config-home discovery.

Uninstall

  • Default uninstall restores owned config values and removes only ak-marked plugin/agent/skill/guidance artifacts.
  • User-edited and marker-less files survive.
  • Repeated uninstall is harmless.
  • uninstall --purge reads ownership before removing kit.json and does not recreate kit.json.
  • --dry-run performs no writes or removals.

The goal is not to duplicate every helper test. One or two end-to-end sandbox scenarios per command, plus the critical negative/dry-run cases, are enough to prove orchestration.

4. Bring the canonical documentation up to precedent

The new README section is useful, but a later feature section cannot contradict the command table and the canonical provider/upgrading guides.

Required documentation updates:

README.md

  • Update the setup/status/sync/uninstall verb rows to include opencode behavior.
  • Replace “one or both” where the text now means three managed host integrations.
  • Explicitly distinguish all managed hosts from the Claude/Codex routing pair.
  • Document the adopted post-install enablement command from item 2.
  • Keep the honest exclusions: no opencode statusline, driving-session detection, routing, usage/cost attribution, or AQE provider type.

docs/PROVIDERS.md

  • Expand the host model beyond Claude/Codex.
  • Explain that opencode is a managed integration host but not a per-activity routing target or AQE provider.
  • Add enable/status/disable examples.
  • Update the “ak way ↔ raw tool way” ownership/config table with:
    • opencode.json MCP/skills/permission ownership;
    • plugin, converted agents, platform skill, and AGENTS.md;
    • teardown behavior and collision/no-clobber rules.
  • Make clear which values are user-owned, ak-owned, restored, or preserved.

docs/UPGRADING.md

  • Add a worked example for adopting opencode on an existing installation.
  • Update the sync vs setup vs provider pick table to match the final CLI behavior.
  • State that sync converges recorded intent but never opts a host in.

ADR and help surfaces

  • In docs/adr/0015-opencode-host.md, replace the incorrect dashboard reference with src/lib/dashboard/client.mjs.
  • Update ak --help --all, ak setup --help, and ak x provider --help wherever the host/adoption descriptions are incomplete.
  • Ensure ADR numbering is consistent everywhere: the PR title/body currently mention ADR-0011 while the file and commit use ADR-0015.

Acceptance:

  • A new user and an upgrading user can each find one accurate path without reading source code or editing kit.json.
  • No document calls opencode a routing host, primary host, AQE provider, transcript source, or statusline-capable host.
  • README, command help, provider guide, upgrade guide, ADR, and troubleshooting use the same terms.

5. Lock the dashboard and managed-version contract

The dashboard code appears directionally correct: status rows flow through the common payload and opencode is mapped to Hosts. The current tests prove payload pass-through and the presence of mapping literals, not the rendered or drift behavior.

Required:

  • Add a fixture opencode status row containing a distinctive message/detail/fix.
  • Prove it is grouped under Hosts & Routing, ordered as intended, and rendered with the original level/message/fix—not merely that opencode:"hosts" exists in the served JavaScript.
  • Add managed-version coverage for npm-managed opencode-ai:
    • installed + current;
    • installed + outdated, appearing in the update/drift banner;
    • external/non-npm install does not fabricate an npm version or claim that ak owns its update.
  • Prefer a small pure seam for managed-package selection/group classification if that makes deterministic unit tests possible. Avoid a large dashboard refactor solely for testing.
  • If the deterministic Playwright corpus is extended, add an opencode row/card assertion there too, but keep a non-visual unit test in the required pnpm test path.

The dashboard does not need opencode transcript, cost, quota, Live, or statusline support in this PR. Honest absence is the correct behavior until those upstream surfaces exist.

Acceptance:

  • ak status, /api/status, the Hosts group/card, and the update banner cannot disagree about opencode.
  • External installs remain visible as host state without being represented as npm-managed drift.
  • The dashboard remains read-only, offline-first, token-gated, and free of new browser dependencies.

6. Suggested implementation order

  1. Fix lint.
  2. Resolve the host/adoption model in provider CLI and help.
  3. Add command-level tests around that final model.
  4. Update README/provider/upgrading/ADR documentation.
  5. Add version-drift and rendered dashboard tests.
  6. Run the full gate and update the PR description with final evidence.

Keeping this order avoids writing docs and tests twice around an unsettled provider-picker contract.

Merge checklist

  • pnpm run check passes.
  • pnpm run test:surface passes.
  • opencode has a first-class post-install enable/disable path.
  • Primary-host and activity routing remain Claude/Codex-only.
  • Setup/sync/status/provider/uninstall have sandboxed command-level coverage.
  • Every mutating opencode path has a dry-run/no-write assertion.
  • README, provider guide, upgrade guide, help, troubleshooting, and ADR agree.
  • ADR number and source references are corrected.
  • Dashboard renders opencode under Hosts and preserves status truth.
  • npm-managed opencode drift appears in the update banner; external installs are not falsely owned.
  • No real home/global configuration is touched by tests.

Explicitly out of scope

Please do not expand this PR into opencode activity routing, primary-host/session-driver detection, AQE provider support, statusline support, transcript/usage/cost attribution, or a dashboard control plane. The right standard here is complete support for the surfaces opencode actually provides, plus honest absence everywhere else.

Once these items are addressed, please reply with a short mapping from each numbered section above to the commit/tests that satisfy it, and request re-review.

@robertelee78 robertelee78 changed the title feat: opencode as a third host adapter — ak-managed ruflo/ruvnet-brain wiring (ADR-0011) feat: opencode as a third host adapter — ak-managed ruflo/ruvnet-brain wiring (ADR-0015) Jul 29, 2026
@robertelee78

Copy link
Copy Markdown
Contributor Author

Remediation complete on head 4f1ddae. Mapping per your numbered sections, then the merge checklist.

1. Quality gate

  • Unused imports removed (208b591). pnpm run check exit 0 (typecheck + eslint + markdownlint + build + 929 kit + 220 cjs tests) and pnpm run test:surface exit 0 (22) — both run against the exact pushed HEAD. The PR Verification section now carries these final counts and the head SHA; older counts removed.

2. Host model + adoption path (208b591)

Your preferred resolution implemented — three managed host integrations, claude/codex as the routing pair:

  • pick manages all three: interactive display shows opencode with the "integration host — wired + guided, never a routing target" qualifier; --host claude,opencode / --host claude,codex,opencode accepted as the complete desired enabled-host set; --primary-host still validated claude/codex-only (opencode rejected, prior primary kept — pinned by test).
  • Routing set is derived from a new HOSTS[].routing capability flag (not a hardcoded id list) — the embryonic seam for Architecture: capability-driven host, provider, binding, and observability adapters #71's capability registry. seedDualRouting still only ever sees claude/codex (pinned: no opencode anywhere in the seeded policy even with all three enabled).
  • Enable applies the same owner-module composition as setup/sync (opencodeStack), plus guidance reconciliation (reconcileOpencodeGuidance) — no merge/ownership logic in the command. Disable-by-exclusion strips ak wiring/artifacts marker-gated, restores priors, preserves user-owned config, and strips enablement-gated guidance.
  • Every pick rewrite preserves all five ownership markers — codexMcp/rufloCodexMcp included (the data-loss class you told me not to retain).
  • provider off remains the complete reversible reset; help/status output now names the two tiers explicitly.

Round-3 cross-vendor review (Codex) on this rework returned 4 findings, all fixed: stale markers now re-persist on converged runs (markersChanged), unknown --host is a hard error before mutation, interactive defaults never drop an enabled-but-absent host nor opt in a disabled one, JSONC/absent-config teardown is honest (markers retained + active wiring reported / stale markers cleared — never "disabled" over live wiring). One finding deliberately not taken: codex-side bridge/route teardown on routing-host exclusion — pre-existing behavior, outside your items 1–5; provider off is the documented full reset. Happy to file it as a follow-up.

3. Command-level lifecycle tests (5264b2c, existing suites extended)

  • setup-command: --opencode --yes persists + wires via the shared stack (CLI present) with restart guidance; --dry-run writes nothing (kit.json/opencode.json/plugin/agents/skill/guidance); enabled+absent CLI never fabricates ~/.config/opencode.
  • setup-host-flags: --opencode opt-in/idempotence; --primary-host opencode ignored with warning.
  • provider-cli (real-CLI spawns): enable/disable/retune/primary-host-rejection/unknown-host/JSONC-retire/absent-config-retire/interactive-defaults — kit.json + on-disk artifacts asserted per run.
  • sync-command (a real sync is now exercised hermetically): enabled+drifted converges after hosts, before final verification; converged-claude-guidance + fresh enable lands opencode guidance on the SAME sync; second sync is a true no-op; enabled+absent installs via hosts and skips wiring without creating the config home; disabled makes no wiring writes and strips only enablement-gated guidance; --dry-run mutates no opencode surface.
  • status-command: converged (all-ok, no fix), drifted (warn + sync fix), JSONC-refused (honest non-OK + manual-merge fix), foreign plugin (info, no overwrite plan), CLI-absent (hosts story), disabled (silence + pick hint on providers), --json shape, strict read-only.
  • uninstall-command: restores owned values, removes only marked artifacts, user-edited/marker-less survive, repeated runs harmless, --purge reads ownership before removing kit.json (not recreated), --dry-run writes nothing.
  • providers.test: capability flag pinned; dualRouting seeds claude/codex only with opencode co-enabled.

4. Documentation (4f1ddae)

  • README: setup/status/sync/uninstall rows include opencode; "one or both" replaced by the three-integration model with the routing pair distinguished; adoption command documented as x provider pick; honest exclusions kept (no statusline/session-detection/routing/primary/aqe-type/usage-cost).
  • PROVIDERS.md: host model beyond Claude/Codex; enable/status/disable examples; the ownership/config table now documents opencode.json MCP/skills/permission, plugin, converted agents, platform skill, and AGENTS.md — ak-owned vs user-owned vs restored vs preserved, incl. collision, JSONC-refusal, and absent-config behavior.
  • UPGRADING.md: worked opencode adoption example; sync-vs-setup-vs-pick table matches final behavior; "sync never opts a host in" stated.
  • ADR-0015: pick section rewritten for the two-tier model; dashboard reference corrected to src/lib/dashboard/client.mjs; guidance detectors described as enablement-gated (as shipped); tests reference updated. TROUBLESHOOTING's ADR link corrected to 0015. PR title/body now say 0015 everywhere. Help surfaces (ak --help --all, setup --help, x provider --help) name the tiers + adoption path.

5. Dashboard + managed-version contract (e378d5b)

  • New pure seam src/lib/dashboard/groups.mjs (classification/grouping/card/notice logic) — one source of truth: node tests import it; the served <script> interpolates the exact same function sources + byte-stable tables. A bundle-parse test guards the interpolation.
  • Rendered (non-visual, in the pnpm test path): a fixture opencode row with distinctive message/fix is proven grouped under Hosts & Routing, ordered worst-first then preferred, rendered with original level/message/fix — not merely the opencode:"hosts" literal (that literal test also still passes).
  • Managed-version coverage: npm-managed opencode-ai installed+current (not drift), installed+outdated (appears in the update banner via noticeHtml), external/non-npm (no row — ak never claims to own its update), in tests/kit/opencode-version-drift.test.mjs + the banner cases in tests/dashboard.test.cjs.
  • No transcript/cost/quota/Live/statusline support added — honest absence as specified.

Merge checklist

  • pnpm run check passes (exit 0 on 4f1ddae)
  • pnpm run test:surface passes (22/22)
  • opencode first-class post-install enable/disable (x provider pick)
  • Primary-host + activity routing remain Claude/Codex-only (capability flag + tests)
  • Sandboxed command-level coverage on setup/sync/status/provider/uninstall
  • Every mutating opencode path has a dry-run/no-write assertion
  • README, PROVIDERS, UPGRADING, help, TROUBLESHOOTING, ADR agree; ADR number corrected to 0015 everywhere
  • Dashboard renders opencode under Hosts with status truth preserved
  • npm-managed opencode-ai drift in the update banner; external installs unclaimed
  • No real home/global configuration touched by tests (round-4 review caught the one escape; fixed with destination seams + a guard comment)

Explicitly out of scope per your instruction: opencode activity routing, primary-host/session-driver detection, AQE provider type, statusline, transcript/usage/cost attribution, dashboard control plane — all untouched.

Requesting re-review. 🙏

@robertelee78

Copy link
Copy Markdown
Contributor Author

Follow-up filed: the one out-of-scope finding from the round-3 cross-vendor review (routing-host exclusion leaves codex routes/bridges live — pre-existing, predates this PR) is now tracked as #73 with a fix sketch and acceptance criteria. Keeping it out of this PR per your scope instruction; can pick it up next.

@robertelee78

Copy link
Copy Markdown
Contributor Author

@pacphi ready for re-review — remediation mapping in the comment above, final gates green on 4f1ddae.

@pacphi

pacphi commented Jul 29, 2026

Copy link
Copy Markdown
Owner

With #74 now merged, this PR should be rebased onto current main and adapted to the capability-driven integration architecture before re-review. This is architectural integration work, not only conflict resolution.

Recommended order:

  1. Rebase feat/opencode-host onto current main.

  2. Replace this PR's provisional HOSTS[].routing/parallel host-capability model with the merged registry capabilities:

    • canDriveSession: true
    • canBePrimary: false
    • canRouteActivities: false
  3. Keep the substantive OpenCode implementation (opencode.json ownership-aware merging, MCP wiring, converted agents, plugin/skill/guidance deployment, collision handling, teardown, and convergence checks), but expose or wrap it through the merged detect → plan → apply → verify → undo lifecycle contract.

  4. Align OpenCode ownership records and config migration with the merged integration ownership/migration model. Add an additive migration test for any pre-registry OpenCode marker state that must remain supported.

  5. Make ak host the canonical adoption surface:

    ak host pick --host claude,opencode
    ak host pick --host claude,codex,opencode

    Compatibility aliases may remain tested, but new docs/help should use ak host, not the deprecated ak provider / ak x provider terminology.

  6. Preserve the intended routing invariants:

    • --host may include OpenCode as a managed integration.
    • --primary-host opencode remains invalid.
    • OpenCode never enters dualRouting.
    • Claude/Codex route seeding and escalation remain unchanged.
    • Excluding OpenCode invokes ownership-safe teardown.
    • Provider retuning preserves OpenCode and Codex ownership state.
  7. Reconcile ADR-0015 with merged ADR-0016: ADR-0016 is the generic integration architecture; ADR-0015 should describe OpenCode as its concrete managed, non-routable implementation.

  8. Reconcile the dashboard work with the merged normalized host/provider identity model. Retain the useful rendered-row and npm-managed-version tests, but derive OpenCode classification from normalized integration facts/registry identity and do not reintroduce host→provider inference.

  9. Port and de-duplicate the command tests around the final architecture:

    • lifecycle-adapter conformance;
    • canonical ak host plus deprecated aliases;
    • registry-derived managed/routable sets;
    • normalized status facts;
    • migration and ownership-safe dry-run/teardown;
    • proof that OpenCode never receives activity routes.
  10. Run pnpm run check and pnpm run test:surface, update the PR body with the new final head/test evidence, and request re-review.

The earlier requested-change items appear addressed at the pre-rebase head, but the review state should remain open until the combined post-#74 implementation is verified. Please avoid resolving conflicts by retaining both capability systems; the registry/lifecycle model now on main should be the single source of truth.

robertelee78 and others added 8 commits July 28, 2026 22:22
…n wiring (ADR-0015)

opencode (opencode.ai) joins claude and codex behind the host-adapter
abstraction, wired through its own native surfaces (never env flags, which
don't exist for it):

- ~/.config/opencode/opencode.json: claude-flow + ruvnet-brain MCP servers,
  skills.paths, permission patterns — merged backup-first with VALUE-precise
  ownership (collisions preserved, priors restored on teardown, stale ak
  entries pruned only while == written; scalar permission lifted to {"*": v};
  JSONC refused, never clobbered)
- plugins/ruflo-hooks.js: opencode's plugin events mapped to ruflo's local
  hook verbs (session lifecycle, pre-bash safety, edit/task learning, route
  context) — the only hook surface opencode has
- agents/: ruflo's agent set converted (frontmatter shape, YAML-safe
  descriptions, all three mcp__ spellings rewritten, collision-prefixed,
  documentation skipped), drift-stamped against a lazily-resolved catalog
  source (override → RUFLO_REPO → marketplace clone → npm direct/nested)
- AGENTS.md guidance: new agents-opencode target + opencode-flavored block
  templates, same never-mkdir gate as ~/.codex
- lifecycle: status subsystem (deep convergence probe), sync apply (after
  hosts install, CLI-gated), setup --opencode, x provider off / uninstall
  teardown, dashboard Hosts-tab categorization, nudge consistency via the
  shared guidanceTargets composition, pick preserves the opencode flag,
  detectHosts reads config-file wired state

25 new kit tests + dashboard/opencode cases; suites green: 605 + 189 tests,
eslint, tsc, markdownlint. Cross-vendor reviewed (21 findings addressed).
…eview (round 2)

Address all seven round-2 findings:

- ownership: a collision the user later aligns to the desired value stays
  unmanaged (never adopted with a stale prior); desired-set shrink RESTORES
  priors instead of deleting entries ak didn't author; permission scalar
  origin is recorded explicitly (a pre-existing {"*": v} object survives
  undo as an object)
- guidance: the opencode blocks are gated on enablement (opencodeEnabled
  flag, same mechanism as dualMode) — an installed-but-disabled host no
  longer receives guidance asserting active wiring, and off → sync strips it
- lifecycle: uninstall --purge no longer recreates kit.json via the opencode
  teardown save; isDefault counts hosts.opencode (claude+opencode is not
  'claude-only'); injected routing parts use schema-valid PartIDs (prt prefix)
- tests: 6 round-2 cases (collision-align, shrink-restore, wildcard-object
  preservation, flag-gated blocks, isDefault, PartID) — 31 in the suite,
  611+189 across the repo, tsc/eslint/markdownlint clean
- mcpCommandFor: claude-flow MCP command resolves bin-on-PATH → nested
  ruflo/node_modules/@claude-flow/cli/bin/mcp-server.js (absolute node path,
  no PATH/cwd dependence — the plain 'npm i -g ruflo' layout, verified) →
  ruflo mcp start. A fresh ruflo-only machine gets the purpose-built stdio
  server instead of the last-resort path.
- setup prints the restart hint after wiring: opencode loads
  config/plugins/MCP/agents once at startup (observed live: sessions miss
  hooks/tools until restarted; the guidance template now says the same).
- TROUBLESHOOTING.md: four opencode rows (startup loading, JSONC refusal,
  no-clobber 'foreign', missing catalog source).
- test: three-way mcpCommandFor selection — 32 in the suite, 612+189 repo-wide.
…code as integration host

The maintainer's core review point: the PR called opencode a third host but
the ordinary host-management command refused it, leaving existing users no
first-class adoption path. Model is now coherent:

- Managed host integrations (install/config/guidance/status/sync/teardown):
  claude, codex, opencode — derived from the HOSTS[].routing capability flag,
  not hardcoded id lists (the seam issue pacphi#71's registry lands on).
- Routing hosts (primaryHost + per-activity seeds): claude, codex only.

pick: --host accepts opencode as the complete desired enabled-host set;
--primary-host stays claude/codex; opencode never enters dualRouting; an
unknown --host token is a hard error before any mutation; interactive
defaults are enabled hosts ∪ newly-detected routing hosts (an enabled-but-
absent host is never dropped by a bare enter, an installed-but-disabled
integration host is never opted in by one). Excluding opencode disables it
via the same marker-gated teardown as off/uninstall. Every pick rewrite
preserves ALL ownership markers (codexMcp/rufloCodexMcp included — the
data-loss class the review called out).

setup/sync/pick/off/uninstall now share ONE owner-module composition
(opencodeStack / retireOpencode / reconcileOpencodeGuidance) — no merge or
ownership logic in commands. opencodeStack reports markersChanged so a
converged file with stale markers still re-persists the teardown proof;
retireOpencode clears markers on absent config and fails honestly on JSONC
(markers retained, wiring reported active, manual remediation named). sync's
blocks branch runs after the opencode branch with an opencode guard, so a
fresh enable converges guidance in one sync (second sync is a true no-op).

Also removes the unused versions.mjs imports that broke pnpm run check.

Cross-vendor review (Codex, round 3): 4 findings, all fixed here or in the
accompanying tests; codex-side bridge teardown on routing-host exclusion
deliberately left as-is (pre-existing behavior; 'provider off' is the
documented full reset).
…regressions

The owner-module suite alone could not catch orchestration mistakes, so each
mutating command now has sandboxed end-to-end scenarios (home sandbox +
fake-bin patterns; no real home/global npm/PATH is ever touched):

- setup: --opencode persists the host and wires it when the CLI is present,
  restart guidance emitted; --dry-run writes nothing anywhere; an absent CLI
  never fabricates ~/.config/opencode.
- provider pick (real CLI spawns): enable wires config/plugin/agents/skill +
  guidance and preserves user config; exclusion disables surgically;
  --primary-host opencode rejected; retune preserves every ownership marker;
  unknown --host fails before mutation; JSONC/absent-config disables report
  honestly; interactive defaults never drop an enabled-but-absent host nor
  opt in a disabled one; stale markers re-persist on a converged re-pick.
- sync: enabled+drifted converges after hosts, before final verification;
  converged claude guidance + fresh enable lands opencode guidance on the
  SAME sync; second sync is a true no-op; absent CLI skips without creating
  the config home; disabled makes no wiring writes and strips only
  enablement-gated guidance; --dry-run mutates no opencode surface.
- status: converged/drifted/JSONC-refused/foreign-plugin/CLI-absent rows are
  honest non-OK with a specific fix; disabled is silent with the pick hint;
  --json carries the dashboard-consumed shape; strictly read-only.
- uninstall: restores owned config values, removes only marked artifacts,
  user-edited/marker-less files survive; repeated runs harmless; --purge
  reads ownership before removing kit.json; --dry-run writes nothing.
- providers lib: routing capability flag pinned; dualRouting seeds
  claude/codex routes ONLY with opencode co-enabled.

Plus codex-review regressions: marker persistence on converged files
(opencodeStack markersChanged, with destination seams so the test can never
escape the sandbox — r4) and npm-managed vs external opencode-ai update
ownership (new opencode-version-drift suite).
…te banner under test

The previous tests proved payload pass-through and the presence of mapping
literals, not rendered behavior. The classification/grouping/card/notice
logic now lives in src/lib/dashboard/groups.mjs (pure, no DOM) — one source
of truth imported by node tests and serialized verbatim into the served
<script> (byte-stable tables, so the shipped JS and the tested JS cannot
drift). New tests prove, without a browser: a fixture opencode row is
grouped under Hosts & Routing, ordered worst-first then preferred, rendered
with its original level/message/fix; an outdated npm-managed opencode-ai
names itself in the update banner while a current install fabricates none;
the served bundle parses with every interpolated function present. External
(non-npm) installs remain unclaimed (versions.mjs coverage in the previous
commit). Dashboard stays read-only, offline-first, token-gated, dependency-free.
…, UPGRADING, ADR-0015, help

Canonical docs no longer contradict the command surface:

- README: setup/status/sync/uninstall verb rows include opencode behavior;
  the host model names three managed integrations with the claude/codex
  routing pair distinguished; the adoption command is provider pick (setup
  --opencode stays the bootstrap path); the honest-absence list is explicit
  (no statusline, session detection, routing/primary, aqe type, or
  usage/cost attribution).
- PROVIDERS.md: the host model expands beyond Claude/Codex with enable /
  status / disable examples; the ak-way↔raw-tool-way table gains the
  opencode row; a value-precise ownership table documents what is ak-owned,
  user-owned, restored, or preserved across every opencode surface incl.
  collision, JSONC-refusal, and absent-config behavior.
- UPGRADING.md: worked example for adopting opencode on an existing
  install via ak x provider pick; the sync vs setup vs pick table matches
  final CLI behavior and states sync never opts a host in.
- ADR-0015: the pick section is rewritten for the two-tier model; the
  dashboard reference is corrected to src/lib/dashboard/client.mjs; the
  enablement-gated guidance detectors are described as shipped; the
  shared stack composition, marker persistence, retire honesty, and
  capability-flag derivation are recorded.
- TROUBLESHOOTING: the ADR link is corrected to 0015 (was 0011).
- bin/agentic-kit.mjs + setup/provider --help: the two-tier distinction is
  visible in help output, and the post-install adoption path is named.
@pacphi
pacphi force-pushed the feat/opencode-host branch from 4f1ddae to b7519b3 Compare July 29, 2026 05:46
@pacphi pacphi changed the title feat: opencode as a third host adapter — ak-managed ruflo/ruvnet-brain wiring (ADR-0015) feat: OpenCode as a managed non-routable host adapter (ADR-0017) Jul 29, 2026
@pacphi

pacphi commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Rebase/remediation complete on b7519b3 (force-with-lease pushed).

What changed to align this PR with current main / ADR-0016:

  • OpenCode is registry-derived as managed (canDriveSession:true) but non-primary/non-routable (canBePrimary:false, canRouteActivities:false); legacy host adapter exports remain compatible.
  • OpenCode lifecycle now uses the ADR-0016 detect/plan/apply/verify/undo contract across setup, sync, host pick/off, and teardown; dry-run guards both apply and undo.
  • ADR renumbered to ADR-0017 to avoid the merged ADR-0015/0016 collision; canonical user guidance is ak host.
  • Legacy OpenCode ownership migrates additively even beside existing ownership receipts, without inventing provider provenance.
  • Config ownership remains value-precise; artifact ownership is now exact SHA-256 last-written receipts for plugin, agents, stamp, and skill. Marker-bearing user edits and exact pre-existing unreceipted copies are preserved.
  • JSONC refusal is fatal and atomic for setup/apply (no partial executable artifacts or guidance); incomplete teardown retains recovery receipts and exits nonzero. Valid user-value collisions remain nonfatal and independent surfaces still converge.
  • Stale permission keys restore recorded user priors; corrupted config cannot promote OpenCode to primary.
  • Dashboard host grouping and test-plan bookkeeping are aligned with the rebased suite.
  • Wildcard MCP authorization and fail-open bash screening are explicitly disclosed as opt-in/defense-in-depth behavior.

Verification:

  • pnpm run check passes on the exact pushed head (typecheck, eslint, markdownlint, build/package checks, complete Node/CJS test suite).
  • QE-Court was convened with architecture, security/ownership, and mutation/test-adequacy prosecutors plus an independent Claude cross-vendor review. Initial REMAND/BLOCK charges were reproduced and fixed; the configured overturn depth completed with unanimous SHIP.

PR is now GitHub-mergeable; CI is running.

@pacphi

pacphi commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Windows CI exposed a portability issue in the OpenCode CLI test fixtures: they created only POSIX executables, so where opencode could not discover them on Windows. Commit e07e3ea adds matching .cmd no-op shims for the OpenCode/Claude fixtures. The 65 directly affected tests and the complete pnpm run check gate pass locally. Replacement CI is now running.

@pacphi

pacphi commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Follow-up 9cf4b13 fixes the underlying Windows behavior found by the replacement run: executable detection now uses the same direct PATHEXT-aware resolver as invocation, and opencode is included among Windows command shims. This avoids the unreliable nested where probe under a controlled PATH. The focused 71-test set and the complete pnpm run check gate pass locally.

@pacphi

pacphi commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Final verification: CI is green on Ubuntu, macOS, and Windows for Node 22/24/26, plus quality and internal-link checks. GitHub reports this PR as MERGEABLE with CLEAN merge state at f1e538b. The Windows fixture/resolution corrections are now covered by the full matrix.

@pacphi
pacphi merged commit 26e052e into pacphi:main Jul 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants