Skip to content

Wire opencode interactive through its agent abstraction#23

Merged
willwashburn merged 3 commits intomainfrom
opencode-agent-launch
Apr 23, 2026
Merged

Wire opencode interactive through its agent abstraction#23
willwashburn merged 3 commits intomainfrom
opencode-agent-launch

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Apr 22, 2026

Summary

Opencode interactive was broken on two counts. Personas would launch but their system prompt got dumped into the TUI input box as pending user text, and the declared model silently fell back to opencode's default regardless of what the persona specified.

Root causes, per opencode's own CLI help and https://opencode.ai/config.json schema:

  1. --prompt = pre-fill the chat input with a user message. Not the agent's instructions.
  2. -m/--model wants full provider/model form (e.g. opencode/gpt-5-nano). Harness-kit was stripping the opencode/ prefix, leaving a bare model name opencode could not resolve.

The correct pathway is opencode's agent abstraction: define agent.<id>.{ model, prompt, mode } in an opencode.json under cwd, select it at launch with --agent <id>.

Changes

  • packages/harness-kit — extend InteractiveSpec with configFiles: InteractiveConfigFile[] (relative path + contents) so the opencode branch can emit the per-session opencode.json. Builder now takes personaId as the agent name. Claude/codex return an empty array.
  • packages/cli — materialize spec.configFiles into the mount dir via onBeforeLaunch so opencode finds opencode.json at its cwd. The non-mount opencode path (only reachable under --install-in-repo) would pollute the user's real repo root, so it degrades: stripAgentFlag removes --agent <id> from argv, warns, and launches opencode with its default agent. Documented trade-off: --install-in-repo cannot apply the persona's prompt; the default (mount) path handles it cleanly.

Known scope limits

  • --install-in-repo with opencode loses persona prompt application in this PR. Fix is possible (either write opencode.json into a scratch dir we still resolve from, or accept writing into the user's repo with cleanup on exit) but orthogonal to the primary bug and deferred.
  • Claude and codex paths are unchanged beyond adding configFiles: [] and threading personaId through.

Test plan

  • corepack pnpm run check green — harness-kit 26, workload-router 37, cli 47
  • Interactive smoke: npm run dev:cli agent frontend-implementer (opencode tier) — confirm (a) TUI shows no pending user text, (b) persona's declared model appears in the model indicator rather than opencode's default, (c) agent responds using the persona's system prompt
  • npm run dev:cli --install-in-repo agent frontend-implementer — confirm warning fires and opencode still launches

🤖 Generated with Claude Code


Open in Devin Review

Opencode interactive was launching with `--model <stripped>` +
`--prompt <systemPrompt>`, which failed on two counts:

1. `--prompt` pre-fills the TUI input buffer with the string as a *user*
   message, not the agent's instructions. The persona's system prompt
   ended up sitting unsent in the chat field.
2. `-m`/`--model` expects `provider/model` per opencode's own docs.
   Stripping the `opencode/` prefix left a bare model name that opencode
   could not resolve; it silently fell back to its default provider +
   model regardless of what the persona declared.

Opencode's actual system-prompt + model pathway is its agent
abstraction (https://opencode.ai/config.json): `agent.<id>.{ model,
prompt, mode }`, selected at launch with `--agent <id>`. Switching to
that shape:

- harness-kit: extend InteractiveSpec with `configFiles` so the opencode
  branch can emit a per-session `opencode.json` carrying the persona's
  agent definition (full provider/model + prompt). Builder now takes
  `personaId` as the agent name. Claude and codex return an empty
  configFiles array — only opencode needs it today.
- cli: materialize spec.configFiles into the mount dir via
  `onBeforeLaunch` so opencode finds `opencode.json` at its cwd. The
  non-mount opencode path (only reachable under --install-in-repo)
  would pollute the user's repo root, so it degrades: strip --agent,
  warn, and launch with opencode's default agent. Documented trade-off
  is that --install-in-repo cannot apply the persona's prompt; mount
  mode (the default) handles this cleanly.

Tests: opencode test replaced + two new tests for the opencode.json
shape and claude/codex configFiles=[]; new cli tests for stripAgentFlag
covering the happy path and a defensive case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@relayfile/local-mount mirrors the real repo into the mount and syncs
changes back on exit. Without hiding them, the opencode.json written by
onBeforeLaunch would (a) get masked on the way in by any pre-existing
opencode.json in the user's repo, and (b) sync back out on exit and
pollute the user's working tree.

Add spec.configFiles[].path to ignoredPatterns dynamically after the
initial assignment — keeps the fix generic for any future configFile
producer rather than hardcoding opencode.json into the pinned static
SKILL_INSTALL_IGNORED_PATTERNS set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

LGTM 🚀

Clean fix addressing the root causes of both opencode interactive bugs:

What this fixes:

  1. ✅ was incorrectly pre-filling TUI as user message instead of setting agent instructions
  2. ✅ Stripped model names caused silent fallback to opencode defaults

Architecture:

  • Clean separation of concerns: harness-kit defines WHAT config files needed, CLI decides WHERE to write them
  • opencode.json structure correctly follows https://opencode.ai/config.json schema
  • Degraded path for with clear warning to users

Edge cases handled well:

  • Config files hidden from mount-mirror in both directions (prevents masking existing + polluting user's repo)
  • stripAgentFlag handles all argv positions correctly
  • Trailing without value preserved as defensive measure
  • Dynamic ignoredPatterns keeps fix generic for future configFile producers

Tests:

  • All 26 harness-kit tests passing, new coverage for opencode.json shape and stripAgentFlag behavior

Minor suggestion (non-blocking): Consider adding a test case for multiple flags in stripAgentFlag, though unlikely in practice.

Great work on the thorough commit messages and inline documentation too.

Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

LGTM

Clean fix addressing the root causes of both opencode interactive bugs:

  • --prompt was incorrectly pre-filling TUI as user message instead of setting agent instructions
  • Stripped model names caused silent fallback to opencode defaults

Architecture: Clean separation of concerns - harness-kit defines WHAT config files needed, CLI decides WHERE to write them. opencode.json structure correctly follows https://opencode.ai/config.json schema. Degraded path for --install-in-repo with clear warning to users.

Edge cases handled well: Config files hidden from mount-mirror in both directions (prevents masking existing + polluting user's repo). stripAgentFlag handles all argv positions correctly. Trailing --agent without value preserved as defensive measure. Dynamic ignoredPatterns keeps fix generic for future configFile producers.

Tests: All 26 harness-kit tests passing, new coverage for opencode.json shape and stripAgentFlag behavior.

Minor suggestion (non-blocking): Consider adding a test case for multiple --agent flags in stripAgentFlag, though unlikely in practice.

Great work on the thorough commit messages and inline documentation too.

This comment was marked as resolved.

Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

LGTM. Clean fix for both opencode interactive bugs: --prompt misuse and stripped model names. Great test coverage and edge case handling.

Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

Code Review: Wire opencode interactive through its agent abstraction

Summary

This PR correctly fixes two significant bugs in opencode interactive mode:

  1. System prompt dumped as user input — Previously --prompt was used, which opencode treats as a user message pre-fill, not agent instructions
  2. Silent model fallback — Stripping the opencode/ provider prefix left an unresolvable bare model name

The solution using opencode's agent abstraction (opencode.json + --agent <id>) is the architecturally correct approach.


🔴 Requested Changes

1. Missing Exhaustiveness Check in buildInteractiveSpec (Line 214, harness.ts)

The switch statement on the Harness union type lacks a final default case or exhaustiveness check. While all three current harnesses are handled, TypeScript's strict mode won't catch future additions to the Harness union.

Suggestion: Add a default case that throws:

default: {
  const _exhaustiveCheck: never = harness;
  throw new Error(`Unhandled harness: ${_exhaustiveCheck}`);
}

This provides compile-time safety when new harnesses are added.

2. Comment/Documentation Staleness (Lines 93-98, harness.ts)

The JSDoc comment for buildInteractiveSpec still describes the old opencode behavior:

"The opencode branch uses opencode's own --prompt flag..."

This is now outdated and misleading. Please update to reflect the new agent-based approach.


⚠️ Observations (Non-blocking)

3. Degraded User Experience for --install-in-repo

The PR acknowledges this trade-off, but I want to flag it: users running --install-in-repo with opencode personas will see a warning and lose the persona's system prompt. This is acceptable for now, but consider documenting this prominently in the CLI help text or adding a "known limitations" section.

4. Test Coverage

The new tests for stripAgentFlag and the opencode configFiles generation are thorough. However, there's no test for the CLI-level integration — specifically the warning emission when degradeConfigFiles triggers. Consider adding a test in cli.test.ts that verifies the warning is printed when opencode + --install-in-repo is used.


✅ Looks Good

  • Clean separation between harness-kit (pure data) and CLI (I/O)
  • The configFiles abstraction is generic and will support future harnesses
  • stripAgentFlag is well-tested with edge cases (trailing --agent, surrounding args)
  • The dynamic ignoredPatterns push for configFiles (lines 587-589 in cli.ts) prevents sync-back pollution elegantly

Reviewed by Hermes Agent

barryollama (APPROVED with requested changes):
- Add exhaustiveness guard to buildInteractiveSpec's switch so future
  Harness union additions fail compile-time instead of silently falling
  through at runtime.
- Rewrite the stale JSDoc paragraph that still described the old opencode
  `--prompt` flag behavior; document the agent-abstraction shape and why
  --prompt / bare -m are deliberately avoided.

copilot-pull-request-reviewer inline threads:
- stripAgentFlag: JSDoc now accurately describes "removes every --agent
  pair", matching the implementation. Note why strip-all is the safer
  idempotent behavior even though the current producer emits one pair.
- configFiles materialization: mkdir -p parent directories before
  writeFileSync so nested relative paths don't throw ENOENT. Gate every
  write on assertSafeRelativePath, which rejects empty / absolute paths
  and any segment equal to `..` — prevents a malformed persona from
  escaping the mount via `join()` and overwriting files outside the
  sandbox.

Tests: new coverage for multi-agent stripping, safe-path acceptance, and
safe-path rejection (empty, absolute, traversal). Exhaustiveness branch
is covered by TypeScript itself; no runtime test added since reaching
the throw requires a cast through `never`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit 5be70a0 into main Apr 23, 2026
1 check passed
@willwashburn willwashburn deleted the opencode-agent-launch branch April 23, 2026 02:10
@willwashburn willwashburn restored the opencode-agent-launch branch April 23, 2026 03:51
willwashburn added a commit that referenced this pull request Apr 23, 2026
…/harness-kit@0.2.0 @agentworkforce/cli@0.3.0

Reconciliation of the 2026-04-23 publish-run race: all three packages
published to npm successfully, but the workflow's final `git push
origin HEAD --follow-tags` was rejected because PR #22 (persona-maker)
merged to main during the job. Tags shipped but the chore(release)
commit was orphaned at 3b5b8f3.

This cherry-picks that commit back onto main and replaces the
auto-generated CLI changelog stub with a handwritten entry covering
PRs #20, #22, #23, #24.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants