Skip to content

feat(opencode): install a native /graphify command - #4776

Open
nihal697 wants to merge 2 commits into
Graphify-Labs:v8from
nihal697:feat/opencode-graphify-command
Open

nihal697 wants to merge 2 commits into
Graphify-Labs:v8from
nihal697:feat/opencode-graphify-command

Conversation

@nihal697

@nihal697 nihal697 commented Oct 26, 2026 •

Copy link
Copy Markdown

What this changes

graphify install --platform opencode writes the OpenCode skill and the .opencode/plugins/graphify.js hook, but no command. OpenCode does not expose skills as slash commands — they are loaded on demand through its skill tool — so typing /graphify in a session (as the README's platform table implies) is answered with an unknown command, and the only way to reach the skill is an explicit mention or the agent deciding to load it.

This adds the missing piece the same way the Kilo install already does it: a packaged command template, written to the directory OpenCode reads commands from.

Implementation

  • New graphify/command-opencode.md: front-matter description, then an instruction to invoke the graphify skill through the skill tool and pass $ARGUMENTS through unchanged (. as the target when no arguments were supplied). Shipped in package-data next to command-kilo.md.
  • graphify/install.py
    • _OPENCODE_COMMAND_PATH (.opencode/commands/graphify.md) and _OPENCODE_GLOBAL_COMMAND_PATH (~/.config/opencode/commands/graphify.md).
    • _install_opencode_command / _uninstall_opencode_command / _uninstall_opencode_global_command, writing through _write_text_atomic like the plugin so a running OpenCode never sees a half-written command.
    • Scope follows the skill it hands off to: global install → user config, --project → the project's .opencode/commands/.
    • Own-file check: the template ends with an <!-- graphify:managed --> marker and _graphify_wrote_opencode_command reads it. An existing commands/graphify.md without the marker was written by the user, so install reports it and leaves it alone rather than replacing it, and uninstall reports it and leaves it in place rather than deleting it. The marker survives re-installs and version bumps, so a file graphify wrote earlier is still recognized as its own.
    • Wired into both entry points — install(platform="opencode", ...) and _agents_install(..., "opencode") (the graphify opencode install route).
    • Uninstall symmetry: the project file is removed by _uninstall_opencode_plugin, the user-scope file by uninstall_all and graphify opencode uninstall. An emptied commands/ directory is pruned, and a user's own command files are untouched.
  • graphify/detect.py: .opencode/commands/graphify.md joins _GRAPHIFY_INSTALLED_FILES, so a project-scoped install does not ingest the command it just wrote.
  • graphify/__main__.py: re-exports the new helpers/constants alongside the existing OpenCode ones.

No behavioural change for other platforms; command-kilo.md and its install path are untouched.

Tests

tests/test_install.py

  • opencode added to the PLATFORMS expected-file map.
  • test_install_opencode_writes_the_graphify_command — global install writes the command with front matter and $ARGUMENTS.
  • test_install_project_opencode_writes_the_command_in_the_project — --project writes it inside the project and leaves the global config alone.
  • test_opencode_command_file_exists_in_package.
  • test_opencode_agents_install_writes_the_global_command — the graphify opencode install route, including the ownership marker.
  • test_opencode_agents_uninstall_removes_the_project_command.
  • test_opencode_uninstall_keeps_a_command_graphify_did_not_write — a hand-written commands/graphify.md survives install and uninstall byte-for-byte.
  • test_install_opencode_leaves_a_user_command_in_place — install reports the conflict instead of overwriting, and the rest of the install still lands.
  • test_opencode_uninstall_removes_the_global_command — graphify opencode uninstall, against a patched Path.home rather than the ambient one.

tests/test_install_references.py

  • The built-wheel payload assertion now also requires command-kilo.md and command-opencode.md, so a missing command template fails the build test instead of aborting a user's install with "not found in package".

Verification

Commands run in this checkout (Python 3.13.12, Windows):

python -m pytest tests/test_install.py tests/test_detect.py -q
# 441 passed, 17 skipped

python -m pytest tests/test_install_references.py -q
# 15 passed, 3 skipped

python -m ruff check graphify/install.py graphify/__main__.py graphify/detect.py tests/test_install.py tests/test_install_references.py
# All checks passed!

npx --yes pyright graphify/install.py graphify/detect.py graphify/__main__.py
# 10 errors, all on lines this PR does not touch (__main__.py:831, detect.py:728-734,
# detect.py:1220, detect.py:2662-2666), i.e. the pre-existing baseline

The full suite was not run locally; the three modules above are the ones that exercise install routing, the installed-file list, and the wheel contents.

I also confirmed end-to-end on my machine: graphify install --platform opencode now leaves ~/.config/opencode/commands/graphify.md, and a restarted OpenCode session resolves /graphify src --update to the skill.

Disclosure

Implemented with an AI coding assistant; disclosed in the commit trailer.

`graphify install --platform opencode` wrote the skill and the
tool.execute.before plugin, but no command. OpenCode does not expose skills
as slash commands -- they are loaded on demand through the `skill` tool -- so
`/graphify` in a session was answered with an unknown command even though the
README documents that command for the OpenCode install.

Write the command the way OpenCode reads it: a markdown file with front matter
under commands/. A global install lands in ~/.config/opencode/commands/graphify.md,
a --project install in <project>/.opencode/commands/graphify.md, matching the
scope of the skill it hands off to. `$ARGUMENTS` carries the user's text through
to the skill unchanged.

Both install entry points write it (`install(platform="opencode")` and
`graphify opencode install`), and both uninstall paths remove it: the project
copy with the plugin, the user-scope copy with `graphify opencode uninstall`
and `graphify uninstall`. The new template is added to package-data so the
wheel ships it, and to detect.py's installed-file list so a project-scope
install does not ingest its own command file.

Tests cover the global install, the project-scope install, both uninstall
paths, and the wheel payload.

Co-Authored-By: Big Pickle (OpenCode) <noreply@opencode.ai>
@nihal697
nihal697 requested a review from safishamsi as a code owner October 26, 2026 12:31
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @nihal697. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 5 changed function(s) (2 sampled, not proven, 3 not verified).

Formal verification. PR-changed functions: 2/5 verified (0 proven, 2 may-equivalent, 0 distinguished) · 3 not verified (2 vacuous, 1 unsupported).

Not verified on this run: \_agents\_install (vacuous: never exercised), install (vacuous: never exercised), uninstall\_all (unsupported).


Graphify review — findings

Adds a /graphify custom command for OpenCode, because a skill alone isn't a slash command there and the README's /graphify instruction came back as an unknown command. Install now writes command-opencode.md, which hands off to the graphify skill with $ARGUMENTS, to ~/.config/opencode/commands/ by default or .opencode/commands/ under --project, and _install_opencode_command exits with an error if the packaged file is missing. Uninstall removes the project and global command files and prunes the empty directories, but _uninstall_opencode_command unlinks whatever graphify.md sits at that path without checking graphify wrote it. The .opencode/commands/graphify.md path is also added to the detect skip list, and the new file is added to the package data.

Worth a look

  • Test installs into and deletes from the real user home config — tests/test_install.py:937 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • Uninstall deletes any graphify.md command file without verifying graphify wrote it — graphify/install.py:1859 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 4190 functions depend on the 613 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: main() — 112 callers, 3 callees
  • worse: dispatch_install_cli() — 6 callers, 32 callees
  • worse: install() — 15 callers, 12 callees
  • worse: uninstall_all() — 6 callers, 15 callees
  • worse: _agents_install() — 7 callers, 9 callees
  • new: _install_opencode_command() — 3 callers, 4 callees

Verification — 4190 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: 1843 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

453 of 453 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/pi.test.ts — full-run-safety
  • tests/test_affected_cli.py — impact, full-run-safety
  • tests/test_affected_defines.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_affected_overrides.py — full-run-safety
  • tests/test_affected_path_file_node.py — full-run-safety
  • tests/test_agents_platform.py — impact, full-run-safety
  • tests/test_alias_folding.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — impact, full-run-safety
  • tests/test_atomic_writes.py — impact, full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_backend_opt_in.py — impact, full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_benchmark_real_query.py — full-run-safety
  • tests/test_binary_media_words.py — impact, full-run-safety
  • tests/test_blade_extractor.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_located_semantic_identity.py — full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_c_include_resolution.py — full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_cache_long_paths.py — full-run-safety
  • tests/test_cache_stale_import_target.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — impact, full-run-safety
  • tests/test_cargo_missing_manifest.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — impact, full-run-safety
  • tests/test_chunking.py — impact, full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — impact, full-run-safety
  • tests/test_claude_md.py — impact, full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cloud_cta.py — full-run-safety
  • … and 403 more

non-code file(s) changed (graphify/command-opencode.md, pyproject.toml) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (graphify/command-opencode.md, pyproject.toml) — a coverage gap or a missing link — running the full suite rather than only the selected tests

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: no capturable inputs from the test suite; property tier: not verifiable: all 220 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_uninstall\_opencode\_plugin (not a proof).

The verifier ran both versions of \_uninstall\_opencode\_plugin 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.

No difference found (not proven): No behavior difference found in dispatch\_install\_cli (not a proof).

The verifier ran both versions of dispatch\_install\_cli 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: no capturable inputs from the test suite; property tier: not verifiable: all 41 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 uninstall\_all.

The verifier did not have enough to check uninstall\_all, 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: no capturable inputs from the test suite; property tier: not verifiable: the input domain has 20 values but only 10 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

· 1 grounded finding(s) anchored inline below; 5 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/install.py
if project:
return (project_dir or Path(".")) / _OPENCODE_COMMAND_PATH
return Path.home() / _OPENCODE_GLOBAL_COMMAND_PATH
def _install_opencode_command(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression — _install_opencode_command()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

The review flagged that `_uninstall_opencode_command` unlinked whatever sat at
`.opencode/commands/graphify.md` (or its user-scope twin) without checking who
wrote it, so uninstalling graphify could delete a user's own command with that
name. The mirror case was true of install, which overwrote it.

The template now ends with an `<!-- graphify:managed -->` marker, and a shared
predicate reads it. Install reports an existing unmarked file and leaves it
alone instead of replacing it; uninstall reports it and leaves it in place
instead of deleting it. The marker survives re-installs and version bumps, so
upgrading does not turn graphify's own command into a stranger's file. The rest
of the install (skill, plugin, AGENTS.md section) is unaffected either way.

Also rewrites the global-uninstall test to patch `Path.home` to a temp dir
instead of reading the ambient one, which is what the review's second finding
pointed at.
@nihal697

Copy link
Copy Markdown
Author

Addressed the two advisory findings in a42c2c8.

Uninstall could delete a command file graphify did not write (graphify/install.py:1859) — correct. The command template now ends with an <!-- graphify:managed --> marker, and _graphify_wrote_opencode_command gates both destructive paths:

  • _uninstall_opencode_command removes the file only when the marker is present; otherwise it prints not written by graphify; left in place and returns False without touching the file. An emptied commands/ dir is still pruned only after a real removal.
  • _install_opencode_command had the mirror problem — it overwrote whatever sat at that path. It now reports an unmarked file and skips it, leaving the skill, plugin, and AGENTS.md section install intact.

The marker is stable across versions, so a command graphify wrote in an earlier release is still recognized as its own after an upgrade; content-comparing against the current template would have failed that case.

Test touched the ambient home (tests/test_install.py:937) — also fair as a robustness point. The global uninstall test now patches Path.home to a temp dir and asserts on that path, matching the sibling tests. (The suite-wide conftest.py sandbox already covered it, but the test no longer depends on that.)

On the inline coupling finding for _install_opencode_command (Ca·Ce = 12): it is a new leaf called from three install routes, so I don't see a smaller shape for it without hiding the scope logic behind another indirection. Happy to take a different structure if you'd prefer one.

New tests: test_opencode_uninstall_keeps_a_command_graphify_did_not_write and test_install_opencode_leaves_a_user_command_in_place, both byte-for-byte assertions on the user's file, plus a marker assertion in the existing agents-install test.

Re-run on a42c2c8:

python -m pytest tests/test_install.py tests/test_detect.py -q   # 441 passed, 17 skipped
python -m pytest tests/test_install_references.py -q             # 15 passed, 3 skipped
python -m ruff check graphify/install.py graphify/__main__.py graphify/detect.py tests/test_install.py tests/test_install_references.py
# All checks passed!

PR description updated with the ownership rule and the new counts.

This branch has not been deployed

No deployments
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.

1 participant