fix(install): correct OpenCode global install path (#3398) - #3459
fix(install): correct OpenCode global install path (#3398)#3459nikhilsaxena04 wants to merge 1 commit into
Conversation
The bug:
A non-project (global) OpenCode install wrote the plugin and registered
it in opencode.json at the CWD's `.opencode/` instead of the documented
`~/.config/opencode/`. The two helper functions shared a single code
path that joined `_OPENCODE_PLUGIN_PATH` (which starts with `.opencode/`)
onto whatever `project_dir` they received — `Path(".")\ for the global
case — so the global install was effectively the same as a project install
at CWD.
The fix:
- Replaced the two static constants `_OPENCODE_PLUGIN_PATH` /
`_OPENCODE_CONFIG_PATH` with a helper `_opencode_base(project, project_dir)`
that returns the correct `(base_dir, plugin_entry_string)` for the scope.
- Updated `_install_opencode_plugin` and `_uninstall_opencode_plugin` to
accept `project: bool` and `project_dir` instead of a single path, so
global vs. project routing is explicit at every call site.
- All call sites updated: global calls now pass `project=False`;
project-scoped calls pass `project=True, project_dir=...`.
Tests:
tests/test_opencode_install.py — three cases:
- global install lands in `~/.config/opencode/` with no `.opencode/` nesting
- project install lands in `<project_dir>/.opencode/` as before
- global uninstall cleans up `~/.config/opencode/` correctly
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) tested, no difference found (not proven).
Graphify review — findings
Fixes global opencode installs to write the plugin and register it under ~/.config/opencode/ instead of erroneously nesting it in ./.opencode/, while project installs keep writing to <project_dir>/.opencode/ with a root-relative plugin entry (#3398). Reworks _install_opencode_plugin and _uninstall_opencode_plugin to take project/project_dir and resolve their base path and entry string through the new _opencode_base helper, threading the correct scope through the opencode install/uninstall and uninstall_all paths. Adds tests covering the XDG global target, the project target, and global uninstall cleanup.
Worth a look
- _agents_uninstall opencode branch changed to project=True but skill is user-scope —
graphify/install.py:1725· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Opencode plugin install/uninstall scope mismatch in _agents_uninstall —
graphify/install.py:1725· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Opencode install/uninstall scopes are inconsistent through agents* —
graphify/install.py:1748· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _install_opencode_plugin no longer honors legacy positional project_dir —
graphify/install.py:1410· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Global opencode install now writes plugin under a different key/entry than before, breaking existing project registrations —
graphify/install.py:1571· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 414 functions depend on the 148 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 124 callees - new:
codebuddy_install()— 20 callers, 5 callees - new:
claude_install()— 19 callers, 4 callees - new:
gemini_install()— 10 callers, 7 callees - new:
claude_uninstall()— 17 callers, 4 callees - new:
_project_uninstall()— 5 callers, 13 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - new:
_copy_skill_file()— 12 callers, 5 callees - …and 13 more — each is listed as a finding
Verification — 414 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 270 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
31 of 263 test file(s) selected (12%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_atomic_version_stamp.py— impacttests/test_claude_md.py— impacttests/test_codebuddy.py— impacttests/test_devin.py— impacttests/test_explain_cli.py— impacttests/test_extract_cli.py— impacttests/test_gemini_hook.py— impacttests/test_god_nodes_cli.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_home_sandbox.py— impacttests/test_hook_strict.py— impacttests/test_incomplete_build_guard.py— impacttests/test_install.py— impacttests/test_install_references.py— impacttests/test_install_strings.py— impacttests/test_install_version_warning.py— impacttests/test_merge_chunks_validation.py— impacttests/test_multigraph_diagnostics.py— impacttests/test_no_dedup_flag.py— impacttests/test_opencode_install.py— impact, changed-testtests/test_path_cli.py— impacttests/test_query_cli.py— impacttests/test_query_induced_edges.py— impacttests/test_read_hook.py— impacttests/test_replace_or_append_section.py— impacttests/test_search_hook.py— impacttests/test_skill_version_warning.py— impacttests/test_uninstall_scope.py— impacttests/test_unverified_semantic_shrink.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_agents\_install.
The verifier did not have enough to check \_agents\_install, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_agents\_uninstall (not a proof).
The verifier ran both versions of \_agents\_uninstall on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify install.
The verifier did not have enough to check install, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_install\_opencode\_plugin.
The verifier did not have enough to check \_install\_opencode\_plugin, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in uninstall\_all (not a proof).
The verifier ran both versions of uninstall\_all on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_uninstall\_opencode\_plugin.
The verifier did not have enough to check \_uninstall\_opencode\_plugin, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set
· 21 more finding(s) on lines outside this diff (see the check run).
The bug:
A non-project (global) OpenCode install wrote the plugin and registered it in opencode.json at the CWD's
.opencode/instead of the documented~/.config/opencode/. The two helper functions shared a single code path that joined_OPENCODE_PLUGIN_PATH(which starts with.opencode/) onto whateverproject_dirthey received — `Path(".")\ for the global case — so the global install was effectively the same as a project install at CWD.The fix:
_OPENCODE_PLUGIN_PATH/_OPENCODE_CONFIG_PATHwith a helper_opencode_base(project, project_dir)that returns the correct(base_dir, plugin_entry_string)for the scope._install_opencode_pluginand_uninstall_opencode_pluginto acceptproject: boolandproject_dirinstead of a single path, so global vs. project routing is explicit at every call site.project=False; project-scoped calls passproject=True, project_dir=....Tests:
tests/test_opencode_install.py — three cases:
~/.config/opencode/with no.opencode/nesting<project_dir>/.opencode/as before~/.config/opencode/correctly