Skip to content

feat(platform): add opencode support#7

Merged
Anekoique merged 1 commit into
mainfrom
feat/opencode-support
Apr 28, 2026
Merged

feat(platform): add opencode support#7
Anekoique merged 1 commit into
mainfrom
feat/opencode-support

Conversation

@Anekoique
Copy link
Copy Markdown
Owner

@Anekoique Anekoique commented Apr 28, 2026

This pull request updates the .ark/.installed.json and .ark/specs/features/INDEX.md files to add support for the OpenCode platform. The main changes involve registering new OpenCode-related command files and updating feature documentation to reflect this support.

Platform support updates:

  • Added .opencode/commands/ark/archive.md, .opencode/commands/ark/design.md, and .opencode/commands/ark/quick.md to the list of installed files in .ark/.installed.json, replacing the previous .claude/commands/ark/* entries. [1] [2]
  • Updated file hashes in .ark/.installed.json to include the new OpenCode command files and reflect changes to .ark/specs/features/INDEX.md and .ark/workflow.md.

Documentation updates:

  • Added a new feature row for opencode-support in .ark/specs/features/INDEX.md to document the addition of OpenCode platform support.

Summary by CodeRabbit

  • New Features

    • OpenCode added as a third platform option during ark init
    • New task commands: /ark:quick, /ark:design, /ark:archive
    • Automatic injection of Ark session context into OpenCode chat sessions via a shipped plugin
  • Documentation

    • Updated workflow/tier docs and new PRD/PLAN/REVIEW/VERIFY guidance for OpenCode workflows
    • Archive process now records feature entries into the managed feature index

@Anekoique Anekoique requested a review from Copilot April 28, 2026 06:22
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds OpenCode as a third Ark platform: platform registry, init CLI flags, layout/templates, embedded OpenCode templates and TS plugin, tests for deduplicated managed blocks and round-trip load/unload, command specs/docs for OpenCode commands, and numerous planning/review/verification docs.

Changes

Cohort / File(s) Summary
Metadata & Feature Index
.ark/.installed.json, .ark/specs/features/INDEX.md
Updated install metadata and advanced timestamp; registered opencode-support in the managed FEATURES block.
Feature Spec & Planning
.ark/specs/features/opencode-support/SPEC.md, .ark/tasks/archive/2026-04/opencode-support/*, .ark/tasks/.../task.toml
New comprehensive SPEC and multi-iteration planning, review, PRD, VERIFY, and task metadata documents for the OpenCode integration.
Workflow Templates (repo & templates dir)
.ark/workflow.md, templates/ark/workflow.md, templates/opencode/commands/ark/*, .opencode/commands/ark/*
Updated tier/workflow tables and added OpenCode command specs (quick, design, archive) in both templates and extracted .opencode/ copies.
OpenCode Plugin (templates & extracted)
templates/opencode/plugins/ark-context.ts, .opencode/plugins/ark-context.ts
New TypeScript plugin that runs ark context --scope session on first chat.message, stores pending context, and prepends it to the first user text during experimental.chat.messages.transform; logs and continues on failures.
Core Platform Integration (layout/platforms/owned_dirs)
crates/ark-core/src/platforms.rs, crates/ark-core/src/layout.rs, crates/ark-core/src/lib.rs
Added OPENCODE_PLATFORM, included it in PLATFORMS, introduced layout accessors/constants for .opencode/, and changed owned_dirs() size from 3→4 (adds OpenCode dir).
Templates & Embedded Constants
crates/ark-core/src/templates.rs
Embedded OpenCode templates and OPENCODE_ARK_CONTEXT_TS constant; added template invariants/tests (frontmatter, sibling presence, structural constraints).
CLI Flags & Init Behavior
crates/ark-cli/src/main.rs
Added --opencode / --no-opencode flags, updated non-TTY error messaging, and extended unit tests for three-platform resolution and interactive selection.
Integration & Unit Tests
crates/ark-core/src/commands/{init,load,remove,unload}.rs, crates/ark-core/src/commands/tests_common.rs, crates/ark-core/src/commands/load.rs, crates/ark-core/src/commands/unload.rs
Updated/added tests to validate OpenCode scaffolding, plugin round-trip integrity, shared AGENTS.md managed-block deduplication, capture/removal counting keyed by target file, canonical .opencode/ path checks.
Skill Guidance
.codex/skills/ark-quick/SKILL.md
Adjusted wording to recommend ark-design (tier selection via --tier deep).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client (LLM runtime)
  participant Plugin as OpenCode Plugin
  participant Ark as `ark` CLI
  Client->>Plugin: send first chat.message (sessionID)
  Plugin->>Ark: exec `ark context --scope session --format json` (5s)
  Ark-->>Plugin: JSON (hookSpecificOutput.additionalContext)
  Plugin->>Plugin: store pendingContext[sessionID]
  Note over Client,Plugin: subsequent turn arrives
  Client->>Plugin: experimental.chat.messages.transform (messages)
  Plugin->>Plugin: find latest user text part
  Plugin->>Plugin: prepend <ark-context>... from pendingContext
  Plugin-->>Client: transformed messages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat(platform): add codex support #6: Adds a new platform integration with matching changes to platform registry, layout, templates, init/load/unload/remove flows, and tests—structurally similar to this OpenCode integration.

Poem

🐇 I dug a burrow, found new code light,

Three platforms now hop into the night.
OpenCode brings context, neat and quick,
Shared AGENTS.md — dedupe does the trick.
Hooray for tunnels, plugins, tests that spring!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(platform): add opencode support' directly and clearly summarizes the main change: integrating OpenCode as a new platform, which is the primary purpose reflected throughout the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/opencode-support

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 28, 2026

Claude finished @Anekoique's task in 3m 53s —— View job


PR Review: feat(platform): add opencode support

  • Read AGENTS.md and CLAUDE.md for conventions
  • Review Rust changes in crates/ark-core/src/platforms.rs
  • Review Rust changes in crates/ark-core/src/templates.rs
  • Review Rust changes in crates/ark-core/src/layout.rs
  • Review Rust changes in crates/ark-core/src/commands/
  • Review CLI changes in crates/ark-cli/src/main.rs
  • Review OpenCode templates and plugin
  • Post final review findings

Review

The implementation is clean and well-structured. The registry-driven design (PLATFORMS slice) correctly absorbs the new platform without any per-platform branching in command bodies, which is exactly the right abstraction. Test coverage is comprehensive; the round-trip test (opencode_three_platform_roundtrip) and source-scan invariants give good confidence. Three findings below, none blocking.


Finding 1 — remove.rs: no-manifest fallback doesn't dedupe shared block targets (benign inconsistency)

remove_recorded_blocks (remove.rs:114-128) takes the manifest-absent fallback path through a plain .collect():

None => PLATFORMS
    .iter()
    .filter_map(|p| {
        p.managed_block_target
            .map(|f| (PathBuf::from(f), layout.managed_marker().to_string()))
    })
    .collect(),

Codex and OpenCode both target AGENTS.md, so this produces [("CLAUDE.md","ARK"), ("AGENTS.md","ARK"), ("AGENTS.md","ARK")]. remove_managed_block is called twice for AGENTS.md; the second call is a no-op (Ok(false)), so blocks_removed stays correct. No behavioural bug — but it's inconsistent with unload.rs:131-138, which uses a BTreeSet to deduplicate the same list before collecting. If a future remove_managed_block ever returns Err for "already removed" the discrepancy becomes live. Fix this →


Finding 2 — main.rs:100-116: flags() match has a live wildcard arm that's a future extensibility trap

let flag = match p.cli_flag {
    "claude"   => PlatformFlag { on: self.claude,   off: self.no_claude   },
    "codex"    => PlatformFlag { on: self.codex,    off: self.no_codex    },
    "opencode" => PlatformFlag { on: self.opencode, off: self.no_opencode },
    _          => PlatformFlag::default(),
};

OpenCode is now in the match, so the _ arm is dead for the current three platforms. But PlatformFlag::default() silently gives a future platform {on:false, off:false}, which causes a confusing error on non-TTY instead of a compile-time reminder to wire the new flag. The cleanest fix once you're sure no unknown platforms should reach here is to unreachable!() (or to assert the arm is never hit in tests), which turns a runtime surprise into a build-time hint. Not a blocker given the internal-only flag surface. Fix this →


Finding 3 — load.rs:441-444: misleading test comment; deeper design note on extra_files on load

The test comment on line 441:

// Plugin file byte-identical (re-applied via extra_files canonical write).

The plugin file is not re-applied from the embedded constant in the restore() path — it's restored from the snapshot byte-for-byte, which happens to match in a same-version round-trip. The comment is accurate for init/upgrade but not for restore. The underlying design choice (hook files get canonical re-apply on load; extra_files do not) is consistent with how CODEX_CONFIG_TOML behaves, so this isn't wrong — but a user who unloads on v1, upgrades ark, then loads will get the old plugin body and need ark upgrade to refresh it. Worth a note in the comment (or in the test) so the distinction is deliberate-looking rather than accidental. Fix this →


Summary

No correctness bugs. Error handling uses crate::error::Result throughout; filesystem access is routed via PathExt / Layout (and the source-scan invariant tests enforce this). The Platform struct's removal_root / dest_dir distinction and the AGENTS.md block-dedup logic (manifest (file, marker) key) both handle the Codex+OpenCode shared-target case correctly. The TypeScript plugin's guard against exporting parameterized helpers (and the corresponding opencode_plugin_keeps_helpers_internal test) is a nice defensive touch.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9df44e39d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (!sessionID) return
const additionalContext = pendingContext.get(sessionID)
if (!additionalContext) return
pendingContext.delete(sessionID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep pending context until injection actually succeeds

Deleting pendingContext before confirming a text part causes permanent context loss for sessions whose first user turn is non-text (for example, image-only). In that case the plugin marks the session as processed in chat.message, then this branch clears the pending payload without injecting it, so later text turns never receive Ark context for the rest of the session.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a third “OpenCode” integration target to Ark alongside Claude and Codex, including embedded templates, CLI init flags, and a Bun-loaded OpenCode plugin to inject ark context into the first user message of each session.

Changes:

  • Add OpenCode platform registration in ark-core (templates, layout paths, platform registry entry) and expose it via ark-core’s public exports.
  • Extend ark CLI init to support --opencode / --no-opencode, and update integration tests for 3-platform init/unload/load/remove behavior.
  • Add OpenCode command + plugin templates (and checked-in installed artifacts), plus workflow/spec docs and the archived task artifacts for opencode-support.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
templates/opencode/plugins/ark-context.ts Adds the OpenCode session context-injection plugin template.
templates/opencode/commands/ark/quick.md Adds OpenCode /ark:quick command template.
templates/opencode/commands/ark/design.md Adds OpenCode /ark:design command template.
templates/opencode/commands/ark/archive.md Adds OpenCode /ark:archive command template.
templates/ark/workflow.md Updates workflow tier table to include OpenCode commands.
crates/ark-core/src/templates.rs Embeds OpenCode templates + plugin source; adds parity/invariant tests.
crates/ark-core/src/platforms.rs Registers OPENCODE_PLATFORM, adds it to PLATFORMS, and adds platform tests.
crates/ark-core/src/lib.rs Re-exports OPENCODE_PLATFORM.
crates/ark-core/src/layout.rs Adds .opencode paths and includes .opencode/ in owned_dirs().
crates/ark-core/src/commands/unload.rs Dedupes managed-block capture targets when manifest is missing.
crates/ark-core/src/commands/tests_common.rs Extends source-scan path prefix list to include .opencode/.
crates/ark-core/src/commands/remove.rs Updates tests to expect managed-block dedupe behavior with shared targets.
crates/ark-core/src/commands/load.rs Adds 3-platform init→unload→load round-trip test including plugin file.
crates/ark-core/src/commands/init.rs Updates init tests to expect .opencode commands + plugin file.
crates/ark-cli/src/main.rs Adds --opencode/--no-opencode flags and updates platform-resolution tests.
.opencode/plugins/ark-context.ts Checks in the generated OpenCode plugin file for this repo’s installed state.
.opencode/commands/ark/quick.md Checks in the generated OpenCode quick command for installed state.
.opencode/commands/ark/design.md Checks in the generated OpenCode design command for installed state.
.opencode/commands/ark/archive.md Checks in the generated OpenCode archive command for installed state.
.codex/skills/ark-quick/SKILL.md Updates quick-skill guidance text for escalation to design tier.
.ark/workflow.md Updates installed workflow doc tier table to include OpenCode commands.
.ark/tasks/archive/2026-04/opencode-support/task.toml Adds archived task metadata for opencode-support.
.ark/tasks/archive/2026-04/opencode-support/VERIFY.md Adds archived verification record for opencode-support.
.ark/tasks/archive/2026-04/opencode-support/PRD.md Adds archived PRD for opencode-support.
.ark/tasks/archive/2026-04/opencode-support/02_REVIEW.md Adds archived review record (iteration 02).
.ark/tasks/archive/2026-04/opencode-support/02_PLAN.md Adds archived plan (iteration 02).
.ark/tasks/archive/2026-04/opencode-support/01_REVIEW.md Adds archived review record (iteration 01).
.ark/tasks/archive/2026-04/opencode-support/00_REVIEW.md Adds archived review record (iteration 00).
.ark/tasks/archive/2026-04/opencode-support/00_PLAN.md Adds archived plan (iteration 00).
.ark/specs/features/opencode-support/SPEC.md Adds the promoted feature spec documenting OpenCode support.
.ark/specs/features/INDEX.md Registers opencode-support in the managed features index.
.ark/.installed.json Updates installed-file list and hashes to include OpenCode commands and updated docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- `.ark/` is initialized.
- The change is reversible in one commit and introduces no new abstractions.
If not, stop and suggest `ark-design` (standard) or `ark-design --deep` instead.
If not, stop and suggest `ark-design` instead — it covers both standard and deep tiers (deep is selected via `--tier deep` when running `ark agent task new`).
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

This quick-skill precondition text says deep tier is selected via --tier deep when running ark agent task new, but the ark-design skill’s user-facing switch for deep tier is --deep in the skill arguments. Consider rewording to avoid implying users should manually pass --tier deep, and instead suggest ark-design (add --deep for deep tier).

Suggested change
If not, stop and suggest `ark-design` instead — it covers both standard and deep tiers (deep is selected via `--tier deep` when running `ark agent task new`).
If not, stop and suggest `ark-design` instead — it covers both standard and deep tiers (add `--deep` when the work needs deep tier).

Copilot uses AI. Check for mistakes.
Comment thread .ark/.installed.json
Comment on lines 4 to +10
"files": [
".claude/commands/ark/archive.md",
".claude/commands/ark/design.md",
".claude/commands/ark/quick.md",
".codex/skills/ark-quick/SKILL.md",
".codex/skills/ark-design/SKILL.md",
".codex/skills/ark-archive/SKILL.md",
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

PR description says the new OpenCode command entries replace the existing .claude/commands/ark/* entries in .ark/.installed.json, but this manifest still lists the Claude command files as installed (and adds OpenCode entries in addition). Please update the PR description (or the manifest, if the intent really was replacement) so they agree.

Copilot uses AI. Check for mistakes.
7. `ark remove` on a Codex+OpenCode project removes both `.codex/` and `.opencode/` trees, removes the AGENTS.md `ARK` managed block once, and surgically removes the Codex `SessionStart` hook entry from `.codex/hooks.json` while preserving sibling user entries.
8. `ark upgrade` on an existing Claude+Codex project (no OpenCode) leaves the OpenCode flag-set untouched: no `.opencode/` directory is created. Adding OpenCode is an explicit `ark init --opencode` (idempotent on already-installed Claude/Codex artifacts).
9. Two parity tests pin the OpenCode command surface in lockstep with Claude: `every_claude_command_has_an_opencode_command_sibling` (existence) and `opencode_command_bodies_have_opencode_frontmatter` (frontmatter shape — `description:` only, no Claude-specific `argument-hint:` field).
10. The shipped `.opencode/plugins/ark-context.ts` is pure TypeScript (no `.js` file in the embedded tree), uses no `experimental.*` hook, and depends only on Node/Bun built-ins (`node:child_process`, `node:fs`) — no `package.json`, no npm dependencies bundled.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Archived PRD Outcome #10 claims the shipped OpenCode plugin “uses no experimental.* hook”, but the actual plugin implementation depends on experimental.chat.messages.transform for message mutation. If these archived task artifacts are meant to be accurate records of what shipped, update the PRD outcome wording to match the implementation (or add a short note explaining the change in direction).

Suggested change
10. The shipped `.opencode/plugins/ark-context.ts` is pure TypeScript (no `.js` file in the embedded tree), uses no `experimental.*` hook, and depends only on Node/Bun built-ins (`node:child_process`, `node:fs`) — no `package.json`, no npm dependencies bundled.
10. The shipped `.opencode/plugins/ark-context.ts` is pure TypeScript (no `.js` file in the embedded tree), uses OpenCode's `experimental.chat.messages.transform` hook for first-message mutation, and depends only on Node/Bun built-ins (`node:child_process`, `node:fs`) — no `package.json`, no npm dependencies bundled.

Copilot uses AI. Check for mistakes.
- **Pure helpers** (named for clarity, NOT exported): `buildEnvelopePrefix(additionalContext: string): string` returns `<ark-context>\n${additionalContext}\n</ark-context>\n\n---\n\n`. `shouldInject(sessionID: string, processed: Set<string>): boolean` returns `!processed.has(sessionID)`. **Critical**: these are plain `function` declarations, NOT `export function`. OpenCode's plugin runtime invokes every named export at load time with no arguments — exporting a parameterized helper crashes plugin loading (verified empirically: `error=undefined is not an object (evaluating 'processed.has')`). Test harnesses can validate the helpers exist via the string-level `opencode_plugin_keeps_helpers_internal` test in `crates/ark-core/src/templates.rs::tests`, which asserts the helpers are defined, NOT exported, and have live consumers in the default-exported factory.
- **Failure handling:** Best-effort. On `execFileSync` throw (e.g. `ENOENT` for missing `ark` binary), `JSON.parse` throw, or non-zero exit code: the plugin catches, calls `client.app.log({ body: { service: "ark", level: "warn", message: <reason> } })` and returns without modifying state. On the first swallowed failure per session (per TR-2), the plugin additionally writes a single-line stderr note: `ark-context: skipped context injection (see opencode logs)`. Subsequent failures in the same session are silent in stderr (still logged).
- **Per-session dedupe:** module-local `Set<string>` and `Map<string, string>`. JS event loop is single-threaded; no concurrency primitive needed.
- **Size:** ≤80 lines including license/header comment.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The OpenCode plugin file is currently 84 lines long, but the opencode-support SPEC states a hard cap of “≤80 lines”. Either tighten the plugin to meet the cap or relax/update the SPEC constraint so the documentation matches the shipped artifact.

Suggested change
- **Size:**80 lines including license/header comment.
- **Size:**84 lines including license/header comment.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ark/.installed.json:
- Around line 20-22: The install manifest (.ark/.installed.json) currently lists
the three command markdown files but omits the OpenCode plugin file that
OPENCODE_PLATFORM.extra_files installs; update the manifest to include
".opencode/plugins/ark-context.ts" (and likewise add any other missing plugin
files referenced by OPENCODE_PLATFORM.extra_files) so drift detection sees the
plugin; search for OPENCODE_PLATFORM.extra_files in
crates/ark-core/src/platforms.rs to confirm the exact filenames and add them to
the array in .ark/.installed.json (also apply the same addition for the other
referenced entries around lines 49-52).

In @.ark/specs/features/opencode-support/SPEC.md:
- Around line 119-128: The OPENCODE platform spec sets dest_dir incorrectly to
OPENCODE_DIR; update the OPENCODE_PLATFORM constant so dest_dir uses
OPENCODE_COMMANDS_DIR (the directory that holds .opencode/commands) while
leaving removal_root as needed; locate the OPENCODE_PLATFORM definition and
replace dest_dir: OPENCODE_DIR with dest_dir: OPENCODE_COMMANDS_DIR so
Platform::templates extracts the command tree correctly.

In @.ark/tasks/archive/2026-04/opencode-support/00_PLAN.md:
- Around line 126-135: The platform definition OPENCODE_PLATFORM sets dest_dir
to OPENCODE_DIR but should use OPENCODE_COMMANDS_DIR to match where OpenCode
commands live and to align with Platform::templates extraction root; update
OPENCODE_PLATFORM (the Platform struct fields dest_dir and removal_root if
applicable) to reference OPENCODE_COMMANDS_DIR instead of OPENCODE_DIR so
templates and extraction targets are consistent with the implementation.

In @.ark/tasks/archive/2026-04/opencode-support/01_PLAN.md:
- Line 77: The helpers are inconsistently named (build_envelope_prefix /
should_inject vs buildEnvelopePrefix / shouldInject); choose one convention
(prefer camelCase to match the G-9 `export default async ({ directory, client })
=> ({...})` shape) and rename all occurrences to that convention—e.g., rename
build_envelope_prefix to buildEnvelopePrefix and should_inject to
shouldInject—and update every reference in the spec and the exported object
shape (including any mentions in the export default) so all usages
(buildEnvelopePrefix, shouldInject) are uniform.
- Around line 154-193: The fenced architecture block starting with ``` followed
by the tree listing (the block that begins with "crates/") is missing a language
tag; update the opening fence to include a language identifier (e.g., change ```
to ```text) so the markdownlint MD040 warning is resolved and the block is
treated as plain text.

In @.ark/tasks/archive/2026-04/opencode-support/02_PLAN.md:
- Around line 252-253: Update the archived plan wording to make the CLI
flag-conflict contract explicitly "negative-wins" throughout: change any
occurrences that state "positive-wins" to "negative-wins" and align the
descriptive text to match the implementation in resolve_platforms_pure (use f.on
&& !f.off semantics), and ensure the CLI surface note about bare #[arg(long)]
flags and the paired on/off flags consistently documents that the negative (off)
flag takes precedence; also update the same wording at the other occurrence
referenced (the second mention around line 293) so both places match.
- Around line 83-92: The spec text G-9 currently requires helper functions to be
exported (mentions "export function" for buildEnvelopePrefix and shouldInject)
which conflicts with the archived plugin's intent and breaks the loader
contract; remove the "export function" requirement and instead state that
buildEnvelopePrefix(additionalContext: string) and shouldInject(sessionID:
string, processed: Set<string>) are pure internal helpers (kept module-local)
used for testability but not required to be exported, and update the failure
handling / helper description to refer to these helpers by name
(buildEnvelopePrefix, shouldInject) without specifying they must be exported so
the plugin factory remains the only named export.

In @.ark/tasks/archive/2026-04/opencode-support/02_REVIEW.md:
- Line 28: Trim the extra spaces inside the inline code spans that trigger
MD038: locate the backtick-quoted substring currently written as " #
`/ark:<name> $ARGUMENTS` " in the changed entries (the occurrences referenced in
V-IT-2 (c), G-12 (b), and C-6 and the matching text verified against
templates/claude/commands/ark/quick.md:6) and remove the leading/trailing
padding so the code span contains the exact literal without surrounding spaces
(i.e., no space directly after the opening backtick or before the closing
backtick); apply the same trimming to the Response/Changed list text where that
span appears.

In @.ark/tasks/archive/2026-04/opencode-support/PRD.md:
- Around line 25-26: Item 10 in the PRD incorrectly states the shipped
.opencode/plugins/ark-context.ts uses no experimental.* hook; update the PRD to
reflect that the shipped plugin intentionally relies on the
experimental.chat.messages.transform hook to mutate user messages (keep the rest
of the constraints about pure TypeScript and builtin-only deps), and ensure the
two-hook contract language (and the parity tests names
every_claude_command_has_an_opencode_command_sibling and
opencode_command_bodies_have_opencode_frontmatter) explicitly acknowledge that
experimental.chat.messages.transform is part of the allowed/expected
implementation surface.

In @.ark/tasks/archive/2026-04/opencode-support/VERIFY.md:
- Around line 57-67: Recount the actual shipped plugin file and update the V-002
entry to reflect the true line count: open
templates/opencode/plugins/ark-context.ts, run a line count (e.g., wc -l) and
replace the reported "89 lines" with the accurate number (≈83/84) in the
VERIFY.md V-002 block; if you also changed code while addressing the cap, adjust
the referenced line ranges (the multi-line type annotations around the former
line ~32 and ~67 and the idx lookup previously at ~84-87) so the finding matches
the current artifact.
- Line 28: Update the VERIFY.md entries so they match the shipped plugin: mark
V-001 and V-003 as resolved/removed and update all affected paragraphs (the
blocks covering lines referenced in the review: 34-53, 71-81, 87-89) to reflect
that ark-context.ts uses stderrNoticed: Set<string> keyed by sessionID (so the
TR-2 “first failure per session” text is satisfied) and that the helpers
buildEnvelopePrefix and shouldInject are intentionally internal (not exported) —
reference the ark-context.ts symbol names and the crate test
crates::ark-core::templates::tests::opencode_plugin_keeps_helpers_internal to
justify the change and remove the stale “named for unit-testability” complaint
from the verification summary.

In @.opencode/commands/ark/archive.md:
- Line 16: Change the phrasing in the archive command guidance so that when
VERIFY was *Rejected* the message uses “ask the user to address findings first”
instead of “tell the user to address findings first”; update the sentence
containing the VERIFY guidance (replace the phrase “tell the user” with “ask the
user”) so the rejection guidance reads more polite and consistent.

In @.opencode/plugins/ark-context.ts:
- Around line 62-70: The handler for "chat.message" currently calls
processedSessions.add(sessionID) before attempting to fetch context, which marks
sessions as processed even when fetchContext(directory) fails; move the
processedSessions.add(sessionID) call inside the try block after
pendingContext.set(sessionID, fetchContext(directory)) so the session is only
marked processed on successful initiation of context fetch, and leave the catch
to call warn(client, sessionID, ...) without adding to processedSessions; keep
the existing use of shouldInject(sessionID, processedSessions) and
pendingContext.set as-is.

In `@templates/opencode/plugins/ark-context.ts`:
- Around line 51-59: fetchContext currently uses execFileSync which blocks the
chat.message hook; replace it with an asynchronous child-process invocation
(e.g., child_process.execFile or spawn wrapped in a Promise) and enforce a hard
timeout/cancellation so a slow or hung "ark context --scope session --format
json" cannot block delivery. Locate the fetchContext function and change
execFileSync to an async call that returns a Promise<string>, parse the stdout
(not stderr) as JSON, and reject/kill the child and throw a clear error if the
process exceeds a configurable timeout; keep the same payload validation
(hookSpecificOutput?.additionalContext string) and surface errors consistently
from fetchContext.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac5c1985-196f-4748-a97f-5d5b23d6fd74

📥 Commits

Reviewing files that changed from the base of the PR and between f19c8e9 and 9df44e3.

📒 Files selected for processing (33)
  • .ark/.installed.json
  • .ark/specs/features/INDEX.md
  • .ark/specs/features/opencode-support/SPEC.md
  • .ark/tasks/archive/2026-04/opencode-support/00_PLAN.md
  • .ark/tasks/archive/2026-04/opencode-support/00_REVIEW.md
  • .ark/tasks/archive/2026-04/opencode-support/01_PLAN.md
  • .ark/tasks/archive/2026-04/opencode-support/01_REVIEW.md
  • .ark/tasks/archive/2026-04/opencode-support/02_PLAN.md
  • .ark/tasks/archive/2026-04/opencode-support/02_REVIEW.md
  • .ark/tasks/archive/2026-04/opencode-support/PRD.md
  • .ark/tasks/archive/2026-04/opencode-support/VERIFY.md
  • .ark/tasks/archive/2026-04/opencode-support/task.toml
  • .ark/workflow.md
  • .codex/skills/ark-quick/SKILL.md
  • .opencode/commands/ark/archive.md
  • .opencode/commands/ark/design.md
  • .opencode/commands/ark/quick.md
  • .opencode/plugins/ark-context.ts
  • crates/ark-cli/src/main.rs
  • crates/ark-core/src/commands/init.rs
  • crates/ark-core/src/commands/load.rs
  • crates/ark-core/src/commands/remove.rs
  • crates/ark-core/src/commands/tests_common.rs
  • crates/ark-core/src/commands/unload.rs
  • crates/ark-core/src/layout.rs
  • crates/ark-core/src/lib.rs
  • crates/ark-core/src/platforms.rs
  • crates/ark-core/src/templates.rs
  • templates/ark/workflow.md
  • templates/opencode/commands/ark/archive.md
  • templates/opencode/commands/ark/design.md
  • templates/opencode/commands/ark/quick.md
  • templates/opencode/plugins/ark-context.ts

Comment thread .ark/.installed.json
Comment thread .ark/specs/features/opencode-support/SPEC.md
Comment on lines +126 to +135
pub const OPENCODE_PLATFORM: Platform = Platform {
id: "opencode",
templates: &crate::templates::OPENCODE_TEMPLATES,
dest_dir: OPENCODE_DIR,
removal_root: OPENCODE_DIR,
cli_flag: "opencode",
managed_block_target: Some(AGENTS_MD),
hook_file: None,
extra_files: &[(OPENCODE_PLUGIN_FILE, OPENCODE_ARK_CONTEXT_TS)],
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use OPENCODE_COMMANDS_DIR in the platform snippet.

The data-structure block still sets dest_dir to OPENCODE_DIR, but G-2 and the implementation both place OpenCode commands under .opencode/commands. As written, this snippet conflicts with the extraction root that Platform::templates actually needs.

Suggested fix
-    dest_dir: OPENCODE_DIR,
+    dest_dir: OPENCODE_COMMANDS_DIR,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub const OPENCODE_PLATFORM: Platform = Platform {
id: "opencode",
templates: &crate::templates::OPENCODE_TEMPLATES,
dest_dir: OPENCODE_DIR,
removal_root: OPENCODE_DIR,
cli_flag: "opencode",
managed_block_target: Some(AGENTS_MD),
hook_file: None,
extra_files: &[(OPENCODE_PLUGIN_FILE, OPENCODE_ARK_CONTEXT_TS)],
};
pub const OPENCODE_PLATFORM: Platform = Platform {
id: "opencode",
templates: &crate::templates::OPENCODE_TEMPLATES,
dest_dir: OPENCODE_COMMANDS_DIR,
removal_root: OPENCODE_DIR,
cli_flag: "opencode",
managed_block_target: Some(AGENTS_MD),
hook_file: None,
extra_files: &[(OPENCODE_PLUGIN_FILE, OPENCODE_ARK_CONTEXT_TS)],
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/opencode-support/00_PLAN.md around lines 126 -
135, The platform definition OPENCODE_PLATFORM sets dest_dir to OPENCODE_DIR but
should use OPENCODE_COMMANDS_DIR to match where OpenCode commands live and to
align with Platform::templates extraction root; update OPENCODE_PLATFORM (the
Platform struct fields dest_dir and removal_root if applicable) to reference
OPENCODE_COMMANDS_DIR instead of OPENCODE_DIR so templates and extraction
targets are consistent with the implementation.

Comment thread .ark/tasks/archive/2026-04/opencode-support/01_PLAN.md
Comment on lines +154 to +193
```
crates/
├── ark-cli/src/main.rs — InitArgs gains 2 OpenCode flags
│ (bare #[arg(long)], no
│ conflicts_with — matches existing
│ --claude/--no-claude shape).
│ interactive prompt + non-TTY
│ error path extend to 3 platforms
└── ark-core/src/
├── lib.rs — re-exports OPENCODE_PLATFORM
├── platforms.rs — adds OPENCODE_PLATFORM const;
│ PLATFORMS slice grows to 3 entries;
│ test asserting registry shape updated
├── layout.rs — adds OPENCODE_DIR,
│ OPENCODE_PLUGIN_FILE consts;
│ Layout::opencode_dir(),
│ Layout::opencode_plugin_file();
│ owned_dirs grows to [PathBuf; 4]
├── io/fs.rs — UNCHANGED (no new hook plumbing)
├── state/ — UNCHANGED
├── commands/ — UNCHANGED command bodies; iteration
│ over PLATFORMS picks up the new
│ entry for free
└── templates.rs — adds OPENCODE_TEMPLATES static +
OPENCODE_ARK_CONTEXT_TS const
templates/
├── ark/ — unchanged
├── claude/ — unchanged
├── codex/ — unchanged
└── opencode/ — NEW
├── commands/
│ └── ark/
│ ├── quick.md — frontmatter + body of claude/quick.md
│ ├── design.md — ditto
│ └── archive.md — ditto
└── plugins/
└── ark-context.ts — Bun-loaded TS plugin (≤80 lines),
two-hook (chat.message +
experimental.chat.messages.transform)
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced architecture block (markdownlint MD040).

Line 154 opens a code fence without a language identifier; this will keep docs lint noisy.

Suggested fix
-```
+```text
 crates/
 ├── ark-cli/src/main.rs                    — InitArgs gains 2 OpenCode flags
 │                                            (bare #[arg(long)], no
 │                                            conflicts_with — matches existing
 │                                            --claude/--no-claude shape).
 ...
     └── plugins/
         └── ark-context.ts                  — Bun-loaded TS plugin (≤80 lines),
                                                two-hook (chat.message +
                                                experimental.chat.messages.transform)
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 154-154: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/opencode-support/01_PLAN.md around lines 154 -
193, The fenced architecture block starting with followed by the tree listing (the block that begins with "crates/") is missing a language tag; update the opening fence to include a language identifier (e.g., change to ```text)
so the markdownlint MD040 warning is resolved and the block is treated as plain
text.


</details>

<!-- fingerprinting:phantom:triton:puma:e7bc2245-ef95-4827-b64a-0067c2b256fa -->

<!-- d98c2f50 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread .ark/tasks/archive/2026-04/opencode-support/VERIFY.md
Comment on lines +57 to +67
### V-002 `Plugin file exceeds G-9 size cap of 80 lines`

- Severity: LOW
- Scope: SPEC Drift
- Location: `templates/opencode/plugins/ark-context.ts:1-89`
- Problem:
G-9 names "Size: ≤80 lines including license/header comment." `wc -l` reports 89 lines on the shipped file. The overage is small (9 lines) and the file is still tight, but the SPEC is a hard cap and the verifier-of-reviewers role flags it because the same Spec section is the future feature SPEC.
- Why it matters:
Same archive-promotion concern as V-001. The cap is a guardrail against the plugin growing into a meaningful TS module that would need its own tests / package.json / build step (NG-6 / G-15 invariants). 89 lines is well within the spirit of the cap, but the literal number is over.
- Expected:
Follow-up task. Either (a) tighten the plugin (collapse the type annotations on lines 32 and 67 — they expand to multi-line wrap-arounds; the `idx` lookup on lines 84-87 can be one expression), or (b) revise 02_PLAN G-9 to "≤90 lines including license/header comment" before archive. (b) is honest about what landed; (a) preserves the original constraint.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Recount the plugin before filing the size-cap follow-up.

The cap violation may still stand, but the evidence here is stale: the shipped plugin in this PR is ~83/84 lines, not 89. Please update the finding so the record matches the artifact under review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/opencode-support/VERIFY.md around lines 57 - 67,
Recount the actual shipped plugin file and update the V-002 entry to reflect the
true line count: open templates/opencode/plugins/ark-context.ts, run a line
count (e.g., wc -l) and replace the reported "89 lines" with the accurate number
(≈83/84) in the VERIFY.md V-002 block; if you also changed code while addressing
the cap, adjust the referenced line ranges (the multi-line type annotations
around the former line ~32 and ~67 and the idx lookup previously at ~84-87) so
the finding matches the current artifact.

- Standard/Deep: `phase = "verify"` with a VERIFY verdict of *Approved* or *Approved with Follow-ups*.
- The user has confirmed they want to archive now (not implicit; they typed `/ark:archive`).

If VERIFY was *Rejected*, refuse and tell the user to address findings first.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prefer “ask” over “tell” in the rejection guidance.

“Ask the user to address findings first” reads a bit better here than “tell the user.”

🧰 Tools
🪛 LanguageTool

[style] ~16-~16: Consider using the more polite verb “ask” (“tell” implies ordering/instructing someone).
Context: .... If VERIFY was Rejected, refuse and tell the user to address findings first. ##...

(TELL_ASK)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/commands/ark/archive.md at line 16, Change the phrasing in the
archive command guidance so that when VERIFY was *Rejected* the message uses
“ask the user to address findings first” instead of “tell the user to address
findings first”; update the sentence containing the VERIFY guidance (replace the
phrase “tell the user” with “ask the user”) so the rejection guidance reads more
polite and consistent.

Comment on lines +62 to +70
"chat.message": async (input: { sessionID?: string }) => {
const sessionID = input?.sessionID
if (!sessionID || !shouldInject(sessionID, processedSessions)) return
processedSessions.add(sessionID)
try {
pendingContext.set(sessionID, fetchContext(directory))
} catch (err) {
warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't mark the session as processed until context fetch succeeds.

A failed first ark context call currently burns the session forever because processedSessions is updated before the try. That makes transient failures unrecoverable and prevents later messages in the same session from ever retrying context injection.

🔧 Minimal fix
 export default async ({ directory, client }: { directory: string; client: Client }) => ({
   "chat.message": async (input: { sessionID?: string }) => {
     const sessionID = input?.sessionID
     if (!sessionID || !shouldInject(sessionID, processedSessions)) return
-    processedSessions.add(sessionID)
     try {
       pendingContext.set(sessionID, fetchContext(directory))
+      processedSessions.add(sessionID)
     } catch (err) {
       warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
     }
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"chat.message": async (input: { sessionID?: string }) => {
const sessionID = input?.sessionID
if (!sessionID || !shouldInject(sessionID, processedSessions)) return
processedSessions.add(sessionID)
try {
pendingContext.set(sessionID, fetchContext(directory))
} catch (err) {
warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
}
"chat.message": async (input: { sessionID?: string }) => {
const sessionID = input?.sessionID
if (!sessionID || !shouldInject(sessionID, processedSessions)) return
try {
pendingContext.set(sessionID, fetchContext(directory))
processedSessions.add(sessionID)
} catch (err) {
warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/ark-context.ts around lines 62 - 70, The handler for
"chat.message" currently calls processedSessions.add(sessionID) before
attempting to fetch context, which marks sessions as processed even when
fetchContext(directory) fails; move the processedSessions.add(sessionID) call
inside the try block after pendingContext.set(sessionID,
fetchContext(directory)) so the session is only marked processed on successful
initiation of context fetch, and leave the catch to call warn(client, sessionID,
...) without adding to processedSessions; keep the existing use of
shouldInject(sessionID, processedSessions) and pendingContext.set as-is.

Comment on lines +51 to +59
function fetchContext(directory: string): string {
const raw = execFileSync("ark", ["context", "--scope", "session", "--format", "json"], {
cwd: directory, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"],
})
const parsed = JSON.parse(raw) as { hookSpecificOutput?: { additionalContext?: unknown } }
const value = parsed.hookSpecificOutput?.additionalContext
if (typeof value !== "string" || !value) throw new Error("unexpected payload shape")
return value
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the ark-context.ts file and examine its full contents
find . -name "ark-context.ts" -type f

Repository: Anekoique/Ark

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Search for where fetchContext is called
rg "fetchContext" -A 3 -B 3

Repository: Anekoique/Ark

Length of output: 1262


🏁 Script executed:

#!/bin/bash
# Look at the integration tests in commands/init.rs
find . -path "*/commands/init.rs" -type f

Repository: Anekoique/Ark

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Read the full ark-context.ts template file
cat -n ./templates/opencode/plugins/ark-context.ts

Repository: Anekoique/Ark

Length of output: 4571


🏁 Script executed:

#!/bin/bash
# Examine the init.rs file to see integration tests
head -100 ./crates/ark-core/src/commands/init.rs

Repository: Anekoique/Ark

Length of output: 3147


🏁 Script executed:

#!/bin/bash
# Search for test functions in init.rs
rg "mod tests|#\[test\]|fn test" ./crates/ark-core/src/commands/init.rs -B 2

Repository: Anekoique/Ark

Length of output: 1443


Avoid a synchronous subprocess on the OpenCode chat path.

execFileSync runs inside the chat.message hook (the first-message injection point) and has no timeout, so a slow or hung ark context invocation can block message delivery for the entire session. Switch this to an async child-process call or add a hard timeout / cancellation boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/opencode/plugins/ark-context.ts` around lines 51 - 59, fetchContext
currently uses execFileSync which blocks the chat.message hook; replace it with
an asynchronous child-process invocation (e.g., child_process.execFile or spawn
wrapped in a Promise) and enforce a hard timeout/cancellation so a slow or hung
"ark context --scope session --format json" cannot block delivery. Locate the
fetchContext function and change execFileSync to an async call that returns a
Promise<string>, parse the stdout (not stderr) as JSON, and reject/kill the
child and throw a clear error if the process exceeds a configurable timeout;
keep the same payload validation (hookSpecificOutput?.additionalContext string)
and surface errors consistently from fetchContext.

@Anekoique Anekoique force-pushed the feat/opencode-support branch from 9df44e3 to cb54f60 Compare April 28, 2026 06:56
@claude
Copy link
Copy Markdown

claude Bot commented Apr 28, 2026

Claude finished @Anekoique's task in 4m 51s —— View job


PR Review: feat(platform): add opencode support

Reviewed by: Claude (claude-sonnet-4-6)


Summary

The OpenCode platform support follows the same registry-driven pattern as Codex well. The core architecture is sound: OPENCODE_PLATFORM entry, extra_files for the TypeScript plugin, owned_dirs() expansion, and the template parity tests. Two issues need addressing before merge, plus three minor observations.


🔴 Finding 1 — Correctness bug: session permanently poisoned on transient failure

File: templates/opencode/plugins/ark-context.ts (line 70) and .opencode/plugins/ark-context.ts (identical copy)

export default async ({ directory, client }: { directory: string; client: Client }) => ({
  "chat.message": async (input: { sessionID?: string }) => {
    const sessionID = input?.sessionID
    if (!sessionID || !shouldInject(sessionID, processedSessions)) return
    processedSessions.add(sessionID)   // ← marked BEFORE the try block
    try {
      pendingContext.set(sessionID, fetchContext(directory))
    } catch (err) {
      warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
    }
  },

processedSessions.add(sessionID) runs before fetchContext. If fetchContext throws (e.g. ark binary not yet on $PATH during the first message, or a 5-second timeout), the session is permanently recorded as processed. Every subsequent user message in that session skips injection — there is no retry. The user has to restart the OpenCode session to recover.

Fix: Move processedSessions.add(sessionID) to after the successful fetch:

    try {
      pendingContext.set(sessionID, fetchContext(directory))
      processedSessions.add(sessionID)   // only mark processed on success
    } catch (err) {
      warn(client, sessionID, `ark context failed: ${(err as Error)?.message ?? String(err)}`)
    }

Note: both the installed file (.opencode/plugins/ark-context.ts) and the template (templates/opencode/plugins/ark-context.ts) are byte-identical and both need this fix. The opencode_plugin_keeps_helpers_internal test in templates.rs reads the template constant, so it will catch structural changes there.


🔴 Finding 2 — Stray package-lock.json committed

File: .opencode/package-lock.json (376 lines, added in this PR)

This file contains an npm lock for @opencode-ai/plugin@1.14.28. It appears to be a leftover from npm install @opencode-ai/plugin run during development/exploration. Ark's plugin is a standalone TypeScript file loaded directly by OpenCode's Bun runtime — no package.json or npm dependencies are needed or intended (the opencode_templates_ships_no_package_json test in templates.rs explicitly guards against this).

The committed lock file:

  • Violates NG-6 ("No npm dependencies bundled into the plugin")
  • Will confuse contributors into thinking the plugin has a build/install step
  • May cause OpenCode to attempt npm resolution in .opencode/

Fix: Delete .opencode/package-lock.json before merge.


🟡 Finding 3 — remove.rs no-manifest branch lacks BTreeSet dedup

File: crates/ark-core/src/commands/remove.rs (None branch of remove_recorded_blocks)

None => PLATFORMS
    .iter()
    .filter_map(|p| {
        p.managed_block_target
            .map(|f| (PathBuf::from(f), layout.managed_marker().to_string()))
    })
    .collect(),

With three platforms (Claude → CLAUDE.md, Codex → AGENTS.md, OpenCode → AGENTS.md), this produces [(CLAUDE.md, ARK), (AGENTS.md, ARK), (AGENTS.md, ARK)]. The duplicate remove_managed_block call is idempotent (the block is already gone the second time), so there is no user-visible bug — but it is inconsistent with unload.rs which already wraps the same logic in a BTreeSet. Low priority, but worth aligning.


🟡 Finding 4 — main.rs wildcard arm silently swallows unknown platform IDs

File: crates/ark-cli/src/main.rs, flags() method

let flag = match p.cli_flag {
    "claude"   => PlatformFlag { on: self.claude,   off: self.no_claude   },
    "codex"    => PlatformFlag { on: self.codex,    off: self.no_codex    },
    "opencode" => PlatformFlag { on: self.opencode, off: self.no_opencode },
    _          => PlatformFlag::default(),   // silently ignored
};

A future platform added to PLATFORMS with a new cli_flag will silently get PlatformFlag::default() (both on and off false). That means the platform is always included, regardless of what the user passes on the CLI. Changing _ to unreachable!("unknown platform cli_flag: {}", p.cli_flag) turns this into a compile-time-detectable (test-time) panic rather than a silent behaviour change.


🟡 Finding 5 — Misleading comment in load.rs about plugin restore path

File: crates/ark-core/src/commands/load.rs (around line 441)

// Plugin file byte-identical (re-applied via extra_files canonical write).

The restore() path restores from snapshot bytes, not from the embedded OPENCODE_ARK_CONTEXT_TS constant. The extra_files canonical write only runs on init and upgrade. The comment implies the opposite and will mislead anyone debugging a load-from-snapshot divergence.


Checklist

  • Platform registry integration (PLATFORMS slice, is_installed, apply_managed_state)
  • extra_files lifecycle (unconditional on init/upgrade, snapshot bytes on load)
  • owned_dirs() expansion to include .opencode/
  • Template parity tests (V-IT-1, V-IT-2) pass
  • Plugin internal-helper guard test (opencode_plugin_keeps_helpers_internal)
  • OPENCODE_TEMPLATES rooted at commands/ not plugins/
  • Plugin session-poisoning bug on transient failure (Finding 1 — must fix)
  • Stray package-lock.json removal (Finding 2 — must fix)
  • remove.rs BTreeSet dedup (Finding 3 — minor)
  • main.rs unreachable! arm (Finding 4 — minor)
  • load.rs comment accuracy (Finding 5 — minor)

Triggered by @aneko · CI run

@Anekoique Anekoique merged commit 3dbf56d into main Apr 28, 2026
3 of 4 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