Wire opencode interactive through its agent abstraction#23
Wire opencode interactive through its agent abstraction#23willwashburn merged 3 commits intomainfrom
Conversation
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>
@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>
barryollama
left a comment
There was a problem hiding this comment.
LGTM 🚀
Clean fix addressing the root causes of both opencode interactive bugs:
What this fixes:
- ✅ 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 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.
barryollama
left a comment
There was a problem hiding this comment.
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.
barryollama
left a comment
There was a problem hiding this comment.
LGTM. Clean fix for both opencode interactive bugs: --prompt misuse and stripped model names. Great test coverage and edge case handling.
barryollama
left a comment
There was a problem hiding this comment.
Code Review: Wire opencode interactive through its agent abstraction
Summary
This PR correctly fixes two significant bugs in opencode interactive mode:
- System prompt dumped as user input — Previously
--promptwas used, which opencode treats as a user message pre-fill, not agent instructions - 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
--promptflag..."
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
configFilesabstraction is generic and will support future harnesses stripAgentFlagis well-tested with edge cases (trailing--agent, surrounding args)- The dynamic
ignoredPatternspush forconfigFiles(lines 587-589 incli.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>
…/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>
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.jsonschema:--prompt= pre-fill the chat input with a user message. Not the agent's instructions.-m/--modelwants fullprovider/modelform (e.g.opencode/gpt-5-nano). Harness-kit was stripping theopencode/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 anopencode.jsonunder cwd, select it at launch with--agent <id>.Changes
packages/harness-kit— extendInteractiveSpecwithconfigFiles: InteractiveConfigFile[](relative path + contents) so the opencode branch can emit the per-sessionopencode.json. Builder now takespersonaIdas the agent name. Claude/codex return an empty array.packages/cli— materializespec.configFilesinto the mount dir viaonBeforeLaunchso opencode findsopencode.jsonat its cwd. The non-mount opencode path (only reachable under--install-in-repo) would pollute the user's real repo root, so it degrades:stripAgentFlagremoves--agent <id>from argv, warns, and launches opencode with its default agent. Documented trade-off:--install-in-repocannot apply the persona's prompt; the default (mount) path handles it cleanly.Known scope limits
--install-in-repowith 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.configFiles: []and threadingpersonaIdthrough.Test plan
corepack pnpm run checkgreen — harness-kit 26, workload-router 37, cli 47npm 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 promptnpm run dev:cli --install-in-repo agent frontend-implementer— confirm warning fires and opencode still launches🤖 Generated with Claude Code