fix(skillkit): install OpenCode skills via native symlinks - #947
Conversation
santoshkumarradha
left a comment
There was a problem hiding this comment.
Thanks for pushing this. I reproduced the local go test ./internal/skillkit -count=1 failure, and there are two concrete regressions to fix before this is ready: the OpenCode uninstall path now hides missing-home errors, and the status path reports current instead of the installed skill version.
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
santoshkumarradha
left a comment
There was a problem hiding this comment.
Thanks for addressing the two regressions I called out earlier. I revalidated the OpenCode install/status paths locally and the updated symlink version handling looks right now.
Moving the OpenCode target to a native ~/.config/opencode/skills/<name> symlink leaves the marker block older af binaries appended to ~/.config/opencode/AGENTS.md behind forever: uninstallMarkerBlock is no longer reachable for this target, so nothing can remove it. Upgrading users end up with the native skill *and* the stale instructions — the AGENTS.md bloat Agent-Field#813 was actually about. Codex made the same migration in Agent-Field#910 and shipped removeLegacyMarkerBlock for exactly this reason. Install (once the symlink is in place) and Uninstall (per catalog skill) now strip that block. The rules are deliberately stricter than the Codex helper, because the two files are not alike: Codex's AGENTS.override.md was created by af for itself, while ~/.config/opencode/AGENTS.md is written by the user and read by OpenCode. So a file holding no block of ours is never opened for writing — bytes and mtime stay exactly as the user left them — and the file is deleted only when removing our block is what emptied it. Reusing uninstallMarkerBlock verbatim would instead rewrite any AGENTS.md it can read (measured: a user file with no agentfield block goes 23 -> 21 bytes) and delete a deliberately empty one on every install. Other tools' marker blocks and user prose on both sides of ours survive; a missing file is a no-op; read/write failures propagate, matching the target's existing Uninstall error contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…new root TestOpenCodeTargetUninstallRemovesCatalogEntries was the one OpenCode test that did not call withTempHome, so it built and tore down catalog entries in the home shared by the whole package instead of its own. realHomeSnapshot also still only fingerprinted the old ~/.config/opencode/AGENTS.md. Now that OpenCode installs a directory of symlinks, add ~/.config/opencode/skills so the real-home pollution guard covers the path this target actually writes to. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kage seams removeLegacyMarkerBlock called os.Remove/os.WriteFile/os.Rename directly while every other write path in the package goes through the reconcile* seams (reconcile.go), which exist precisely so a test can force a failure. The consequence was that its "remove", "write" and "rename into" branches could not be exercised at all: six lines that never ran once, and error strings that could ship wrongly wrapped without anything noticing. Switch the four filesystem calls to reconcileReadFile/reconcileRemove/ reconcileWriteFile/reconcileRename and cover each failure through Uninstall, modelled on the reconciler's own rewrite-failure subtests. Also drop legacyRulesPath's error return. It could only fail when TargetPath() fails, and both call sites have already proven TargetPath() succeeds before reaching it — so the branch was unreachable and told a reader about a failure mode that does not exist. It now takes the resolved skills root, which lets Uninstall use the TargetPath() result it was already computing and discarding instead of re-resolving it per skill. No behaviour change: same files read, same files written, same errors returned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…block cannot be cleaned Install created the symlink first and then failed the whole target if the legacy ~/.config/opencode/AGENTS.md could not be cleaned up — a file mode 000 from a botched dotfiles restore, or a directory sitting at that path. The caller records nothing for a failed target, so the outcome was the worst of both: the integration live on disk, `af skill list` reporting OpenCode as not installed, and every later `af skill install` exiting non-zero, all over a stale block that has no bearing on whether OpenCode can load the skill. Downgrade the cleanup to a warning on the install path so the install that already succeeded is reported and recorded. Uninstall keeps propagating the error: there, stripping the block is the entire point of the call. The ordering is deliberate and unchanged — the native skill goes in before the old block comes out, so an interrupted migration leaves the user with a working integration rather than neither. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks — the symlink move is the right call and the target itself looks good. I've pushed four commits on top rather than block on a review round. You wrote that the legacy
Switching
The part worth reviewing closely is that I deliberately did not copy the Codex helper, for the reason you gave.
My first pass called Same commit drops
This is a behaviour change to my own first commit, and the one I'd most like a second opinion on. The cleanup is now advisory on the install path (one line to stderr) and still fatal on Validation contract → test
Both directions verified. With Gates (from Patch coverage is the one worth calling out, since Two suite-level failures on my machine, both pre-existing and both reproduced in a clean checkout of Only the Go control plane is touched, so the SDK gates don't apply. Refs #813. |
AbirAbbas
left a comment
There was a problem hiding this comment.
Re-approving after my four commits on top of cf40b60 (Santosh's approval was auto-dismissed by the push). Contributor's commits are untouched; the additions are the legacy AGENTS.md cleanup + its tests and the seam refactor described in my comment above. Gates and patch coverage (87%) in the comment.
Problem
OpenCode users currently receive AgentField skills by appending a marker block to
~/.config/opencode/AGENTS.md. This injects the full skill guidance into every OpenCode session and does not use OpenCode's native skill discovery.Fix
Fixes #813
~/.config/opencode/skills/<name>symlinked to the canonical~/.agentfield/skills/<name>/currentdirectory;af skill liststatus;This fixes the installation path for new installs and updates. It does not attempt to
migrate or remove marker blocks written by older versions from
AGENTS.md; that is keptout of this focused change to avoid deleting user-authored OpenCode instructions.
Validation
go test ./internal/skillkit -run 'TestOpenCodeTargetInstallsSkillSymlink|TestTargetSpecificEdgeCases/marker_targets_report_empty_status_after_plain_file_uninstall' -count=1— passedgo vet ./internal/skillkit— passedgit diff --check— passedThe focused tests verify the actual skillkit install/uninstall path and filesystem
symlink target, without requiring a real OpenCode process or credentials. A real
OpenCode CLI end-to-end check was not run. The full
go test ./internal/skillkitrunis currently affected by pre-existing Windows test-environment assumptions around
HOME/USERPROFILEand embedded path separators; the changed OpenCode tests passindependently.
Known limitations
The replacement currently removes an existing target entry before creating the new
symlink, so installation is not crash-atomic. The target is expected to be an
AgentField-managed skill path; protecting arbitrary user content at that exact path
and adding failure-injection coverage are follow-up hardening work.
AI disclosure
This PR was prepared with GPT-5 Codex. The implementation was reviewed against the existing Claude Code symlink lifecycle and the OpenCode target tests.