Skip to content

feat: add OpenCode as a fourth supported host (#53)#54

Merged
earino merged 5 commits into
mainfrom
feature/issue-53-opencode-support
Apr 26, 2026
Merged

feat: add OpenCode as a fourth supported host (#53)#54
earino merged 5 commits into
mainfrom
feature/issue-53-opencode-support

Conversation

@earino

@earino earino commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Closes #53.

Summary

Adds OpenCode (sst/opencode, opencode.ai) as a fourth supported host alongside Claude Code, Codex CLI, and Gemini CLI.

The spike comment on #53 confirmed both architectural risks resolved positively before committing to scope:

  1. Discovery already works. OpenCode reads ~/.claude/skills/ natively as a Claude-compat directory. Verified empirically: opencode debug skill from a /tmp/opencode-spike/ directory with resumasher cloned to .claude/skills/resumasher/ discovered the skill and parsed our YAML frontmatter cleanly.
  2. Sub-agent dispatch is shape-compatible. OpenCode's built-in task tool has identical {description, prompt, subagent_type} shape to Claude Code's Task (confirmed in packages/opencode/src/tool/task.ts). Resumasher's existing dispatch pattern translates 1:1.

Tool surface is full parity with Claude Code: taskTask, webfetchWebFetch, websearchWebSearch (gated on OPENCODE_ENABLE_EXA=1 or the OpenCode provider), questionAskUserQuestion.

Concrete edits

  • bin/resumasher-telemetry-log — sniff OPENCODE / OPENCODE_VERSION env vars; add ~/.opencode/skills/ to user-scope detection so host=opencode_cli lands in the dashboard.
  • scripts/orchestration.py — add .opencode to DEFAULT_IGNORE_DIRS so a project-scope OpenCode install doesn't get its own skill source mined as evidence.
  • SKILL.md — add ~/.opencode/skills/resumasher (and project variants) to the path resolution prologue (both copies, guarded by test_skill_md_prologue.py); document task / webfetch / websearch / question tool names alongside the existing three hosts at every cross-host enumeration; add opencode_cli to the HOST enum everywhere.
  • README.md — OpenCode install block (defaulting to Claude-compat path; native ~/.opencode/skills/ documented as the alternative) + OpenCode update block alongside the existing three.
  • PRIVACY.md — install-path enumerations and host list updated.
  • Tests — OPENCODE_VERSION env-var sniff assertion, install-scope auto-detection for ~/.opencode/skills/, .opencode added to the parametrized self-mining-risk regression.

Decisions explicitly skipped (per scoping convo)

  • install.sh stays document-only — Claude-compat path is the recommended default, no install.sh branching needed.
  • Inline-prompt sub-agent dispatch stays the model (the Codex pattern). No .opencode/agents/<name>.md files shipped — OpenCode's built-in general subagent_type accepts arbitrary prompts at runtime, identical to how Codex handles dispatch.

Test plan

  • Full deterministic test suite: 435 passed
  • Live tests skip cleanly with RESUMASHER_SKIP_LIVE=1
  • OpenCode skill discovery verified empirically in sandbox (opencode v1.14.25)
  • CI green (Linux + Windows × Python 3.10/3.11/3.12)
  • Host-side end-to-end run on macOS with opencode auth configured against Anthropic, before declaring the port done

🤖 Generated with Claude Code

earino and others added 3 commits April 26, 2026 09:07
OpenCode (sst/opencode at opencode.ai) joins Claude Code, Codex CLI,
and Gemini CLI. Spike confirmed both architectural risks resolved
positively: OpenCode reads ~/.claude/skills/ natively as a Claude-compat
directory (skill discoverable without re-install), and the built-in
`task` tool has the same shape as Claude Code's Task tool (sub-agent
dispatch translates 1:1).

- bin/resumasher-telemetry-log: sniff OPENCODE / OPENCODE_VERSION env
  vars; add ~/.opencode/skills/ to user-scope detection.
- scripts/orchestration.py: add .opencode to DEFAULT_IGNORE_DIRS so
  project-scope OpenCode installs don't get the skill source mined.
- SKILL.md: add ~/.opencode/skills/resumasher to path resolution
  prologue (both copies); document `task` / `webfetch` / `websearch` /
  `question` tool names alongside the existing three hosts; add HOST
  enum value `opencode_cli` everywhere.
- README, PRIVACY: add OpenCode install + update blocks; default to
  the Claude-compat path with native ~/.opencode/skills/ documented
  as the alternative.
- Tests: OPENCODE_VERSION env-var sniff assertion, install-scope
  auto-detection for ~/.opencode/skills/, .opencode parametrize on
  the self-mining-risk regression. All 435 deterministic tests pass.

Skipped on purpose: install.sh stays document-only (Claude-compat
path is the recommended default, no branching); inline-prompt
sub-agent dispatch stays the model (Codex pattern, no
.opencode/agents/ files shipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first real /resumasher run under OpenCode used qwen3.6-35b-a3b
(local Ollama-class model). The PDF rendered correctly but the
transcript showed three shell errors and one parser gap that strong
models silently paper over. All four are host-agnostic and reproduce
the bug class behind #44/#45/#46/#50: SKILL.md prescribed a shell
idiom that didn't survive contact with real sub-agent text.

1. SKILL.md Phase 0: prescribe `mkdir -p $STUDENT_CWD/.resumasher`
   before the config.json heredoc. Fresh student folders have no
   .resumasher/ dir; without the mkdir the redirect fails silently
   and the next phase runs against an empty config.

2. orchestration.py read-resume: error hard at the CLI boundary
   when called with empty/whitespace-only path. Pre-fix `Path("")`
   resolved to "." and produced IsADirectoryError deep in the
   stack while the orchestrator continued with a 0-byte resume.txt.
   Now exits 2 with a FAILURE message naming the likely cause
   ($RESUME_PATH not re-derived in fresh Bash call).

3. SKILL.md Phase 2: prescribe `cat > file << 'HEREDOC'` for
   saving sub-agent text outputs (quoted-delimiter heredoc is
   byte-literal and immune to apostrophes/dollar-signs/backticks).
   Explicitly forbid the broken `var='...'; echo "$var" > file`
   pattern with the apostrophe failure mode called out in prose.

4. orchestration.py extractor regexes: tolerate markdown-bold
   variants around sentinel keys (`**ROLE:**`, `**KEY**:`,
   `KEY: **value**`, `**KEY:** **value**`). Switched from greedy
   `\*{0,2}` groups (which mis-parsed `**KEY:** **value**`) to a
   `[\s*]*` character class around each token — robust to all
   four bold variants plus plain.

Tests: 12 parametrized markdown-bold cases per extractor; new
test_skill_md_weak_model_guards.py pins the SKILL.md prescriptions
so future edits don't regress; CLI-level read-resume empty-path
test asserts exit 2 + the named-cause stderr message.

All 450 deterministic tests green (was 435 + 15 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second qwen3.6-35b OpenCode run (samples-issue42/session-ses_235c.md)
completed the full nine-phase pipeline end-to-end but surfaced four
more weak-model behaviors that the SKILL.md prescriptions were too
soft to prevent.

1. commands/resumasher.md slash-command shim. OpenCode's `/resumasher
   jd.md` was silently dropping the argument because we shipped no
   command file — OpenCode falls back to skill auto-discovery and
   pastes SKILL.md as a user message. Six-line shim + install.sh
   detection (gates on `command -v opencode`, honors XDG_CONFIG_HOME)
   + README + 5 deterministic tests.

2. Sub-agent dispatch section reordered. Qwen tried Claude Code's
   `subagent_type="general-purpose"` first and got rejected on
   OpenCode. SKILL.md now leads the dispatch bullet with an explicit
   "use the entry that matches the CLI you're actually running in"
   callout, names the rejection error verbatim, and inlines the
   `NOT "general-purpose"` clarification on the OpenCode entry.

3. Phase 3 forbids manual fit-field writes. Qwen bypassed
   `extract-fit-fields` and wrote each field via individual `echo`
   commands, shipping `Entry/Junior` (not in our enum) and a
   200-char recommendation prose to telemetry. SKILL.md Phase 3
   now explicitly forbids manual writes with the rationale and the
   run reference inline.

4. Sub-agent prompt section forbids prompt rewriting. Qwen rewrote
   the cover-letter prompt (only one of five), inverting "Start
   with a greeting H1" to "End with" and producing a PDF where the
   salutation rendered as a giant H1 at the bottom. SKILL.md now
   has a load-bearing "Pass $PROMPT AS-IS — do not paraphrase,
   summarize, shorten, or rewrite" instruction.

Filed #55 (v0.5 epic) for templated per-host SKILL.md generation —
that would address Finding (2) by removing cross-host enumeration
ambiguity, but doesn't touch the other three findings, so it's the
right move for v0.5+ rather than this PR.

All 455 deterministic tests pass (was 450 + 5 new shim tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
earino and others added 2 commits April 26, 2026 16:32
…#53)

A third qwen3.6-35b OpenCode run (samples-issue42/session-ses_2359.md)
exposed that OpenCode caps tool output at 51,200 bytes by default
(packages/opencode/src/tool/truncate.ts: MAX_BYTES = 50 * 1024). Our
SKILL.md is ~82KB. When the cap is too low:

  - OpenCode truncates the skill output at ~38% (preserves the first
    50,726 bytes — exactly the 50KB cap minus a partial trailing line)
  - The rest is replaced with a "Use the Task tool to read the rest"
    meta-instruction, which weak models routinely ignore
  - Phases 7-9 prescriptions never reach the model
  - qwen produces wrong PDF filenames (`tailored-resume.pdf` instead
    of `resume.pdf`), only renders 2 of 3 PDFs (no interview-prep.pdf),
    and ships skeletal Phase 9 telemetry with the wrong flag name
    (`--recommendation` instead of `--fit-recommendation`)

Strong cloud models (Claude, GPT-5) usually recover by inferring the
missing phases. The truncation is configurable: `tool_output.max_bytes`
in `~/.config/opencode/opencode.json` (or XDG-equivalent). 100KB is
double the default and fits SKILL.md plus growth headroom.

install.sh now READS the user's opencode config (never writes to it)
via the venv Python and prints a heads-up if the cap is below SKILL.md's
size, with the exact JSON snippet to add. Read-only contract is pinned
by a test that asserts no write redirect exists in the OpenCode
detection block.

README OpenCode section adds the same prescription with the rationale
and a link to the OpenCode source. Issue #55 (templated per-host
SKILL.md) downgrades from "urgent" to "optimization" — no longer
required for correctness once the cap is raised.

All 456 deterministic tests pass (was 455 + 1 new cap-detection test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cp1252 (#53)

Windows CI on PR #54 caught the emoji `⚠️` (U+26A0 + U+FE0F) I added in
the previous commit. Two cooperating issues:

1. tests/test_install_python_probe.py:169 reads install.sh with a bare
   `Path.read_text()`. Python on Windows defaults that to cp1252, which
   can't decode the emoji's UTF-8 byte 0x8f at position 7802. Same bug
   class as v0.4.2's stdout encoding fix (#34). Test now passes
   `encoding="utf-8"` explicitly with a comment naming the failure mode.

2. The emoji wasn't pulling its weight as UX. Cohort is ~90% Windows,
   where Git Bash and cmd render emoji unreliably. Replaced `⚠️` with
   plain `NOTE:` in the install.sh warning. ASCII renders consistently
   on every terminal.

The two fixes are belt-and-suspenders: the test fix prevents this
specific class of failure from ever recurring; the emoji removal
removes the immediate trigger AND improves the install output for
the cohort's actual platform mix. install.sh has only em-dashes as
non-ASCII now (3-byte UTF-8 that survives cp1252 decode without
hitting the 0x8f land mine).

All 456 deterministic tests pass locally; the previously failing
test_install_sh_invokes_pip_via_python_module_form now decodes
install.sh cleanly under both UTF-8 and cp1252 simulation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@earino earino merged commit e87f705 into main Apr 26, 2026
6 checks passed
@earino earino deleted the feature/issue-53-opencode-support branch April 26, 2026 17:53
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.

Add OpenCode as a fourth supported host

1 participant