14 KiB
Phase 19A — OpenCode Foundation
Goal
Add the non-UI foundation for OpenCode integration:
- OpenCode config discovery and safe JSON/JSONC helpers.
- OpenCode MCP/instructions/plugin previews and status classification.
- Shared speech safety utilities reusable by Claude hooks and the future OpenCode plugin.
This phase should not install a working OpenCode plugin, change Desktop Integrations UI, or extend openpets configure --agent opencode yet. It prepares the safe primitives for those later phases.
Non-goals
- No OpenCode runtime plugin implementation.
- No Desktop Integrations OpenCode card.
- No CLI
--agent opencodeuser-facing command yet. - No writes to real user OpenCode config during tests.
- No changes to public MCP tools; they remain exactly
openpets_status,openpets_say,openpets_react. - No fork or modification of
v1/opencode/.
User-visible/manual outcome
No direct user-visible feature is expected yet.
Developers should be able to run checks and see that OpenPets can safely build and classify OpenCode config previews in isolated temp fixtures.
Acceptance criteria
- Add an OpenCode integration package/module, likely
packages/opencode, with build/check scripts. - Provide pure helpers for:
- best-effort OpenCode executable detection data shape;
- global/project OpenCode config candidate paths;
- JSON/JSONC parsing and update planning;
- OpenPets MCP config preview;
- OpenPets instruction config preview;
- OpenPets plugin config preview;
- installed/missing/stale/error status classification.
- OpenCode project config candidate order for existing files follows OpenCode's own MCP add behavior:
opencode.jsonopencode.jsonc.opencode/opencode.json.opencode/opencode.jsonc
- If no project config exists, helpers should plan creation of
.opencode/opencode.jsonc. - Status helpers must scan all relevant project config files, not only the chosen write target, because OpenCode can merge top-level and
.opencodeconfigs. - Multiple or conflicting OpenPets entries must be classified explicitly instead of silently choosing one.
- Global config discovery is read/status-only in 19A and must define:
OPENCODE_CONFIG_DIRoverride support;- default config directory derived from platform/XDG conventions;
- candidate files
config.json,opencode.json, andopencode.jsonc; - no global writes by default in this phase.
- Published mode is the default for project config previews:
{
"mcp": {
"openpets": {
"type": "local",
"command": ["npx", "-y", "@open-pets/cli@0.0.0", "mcp", "--pet", "fixer"],
"enabled": true
}
}
}
- Local-dev previews may use
node <workspace>/.../cli/dist/index.js, but project-local absolute paths must not be the default. - The CLI package version must be an injected parameter to preview builders.
packages/opencodemust not import@open-pets/cli, so the later CLI dependency on@open-pets/opencodedoes not create a cycle. - No-pet MCP preview is valid and means the default OpenPets MCP target:
{
"type": "local",
"command": ["npx", "-y", "@open-pets/cli@0.0.0", "mcp"],
"enabled": true
}
- Config safety helpers must support:
- max config size cap;
- rejecting symlinked config files;
- rejecting symlinked relevant parent directories where practical;
- no write plan on invalid JSON/JSONC;
- no write plan on invalid OpenCode field types such as
mcp: [],instructions: "x", orplugin: {}; - backup-before-write path planning;
- temp-file + rename write planning.
- Write planning must require a validated project root and reject project-root symlink/escape cases.
- Backup and temp paths must use unique names, avoid overwrite, use exclusive temp-file creation, and keep temp files in the same directory as the target.
- File mode should be private where meaningful on the current platform.
- Existing unrelated OpenCode config keys are preserved by update helpers.
- Existing non-OpenPets MCP/plugin/instruction entries are preserved.
- Managed ownership rules must be explicit:
- current managed MCP entry = exact expected OpenPets command shape for the selected mode/pet;
- stale managed MCP entry = recognizable OpenPets package/path/command but outdated;
- custom/foreign
mcp.openpets= present but not managed; classify separately and do not overwrite/remove by default; - current managed instruction entry = expected OpenPets instruction path plus managed file block;
- stale managed instruction entry = recognizable OpenPets path/block but outdated;
- custom/foreign OpenPets-like instruction/plugin entries = classify separately and do not overwrite/remove by default.
- Existing OpenPets-managed entries are idempotently recognized.
- Stale OpenPets-managed entries can be classified as
needs_updatewithout overwriting in this phase's public surface. - Shared speech utilities are extracted or introduced so Claude and OpenCode can share:
- speech categories;
- message picking;
- validation rules.
- Throttling extraction is optional in 19A. If included, it must include exact parity tests for Claude state paths, categories, cooldowns, and OpenCode namespacing. Otherwise, defer throttling to Phase 19B.
- Claude hook behavior remains unchanged after extraction/introduction.
- Speech validation still rejects:
- empty or >140 character messages;
- newlines;
- code-like content;
- URLs;
- path-like content;
- secret-looking assignments.
- Tests must not mutate real Claude settings, real OpenCode config, or real OpenPets user data.
Proposed files/directories
Likely new files:
packages/opencode/package.jsonpackages/opencode/tsconfig.jsonpackages/opencode/src/index.tspackages/opencode/src/opencode-config.tspackages/opencode/src/opencode-previews.tspackages/opencode/src/opencode-status.tspackages/opencode/src/check-opencode-foundation.ts
Possible new shared files/package:
packages/agent-events/package.jsonpackages/agent-events/src/index.ts- or a smaller shared module inside an existing package if adding a package is unnecessary.
If a shared package is added, it must not depend on Claude, OpenCode, CLI, desktop, or MCP packages.
Likely changed files:
pnpm-workspace.yamlif a new package is added.packages/claude/src/hooks.tspackages/claude/src/hook-messages.tspackages/claude/src/check-claude-hooks.ts- root/package check wiring if needed.
Technical approach
OpenCode config facts from v1/opencode/
Config.Info.mcpexists inv1/opencode/packages/opencode/src/config/config.tslines 220-229.- Local MCP config shape is in
v1/opencode/packages/opencode/src/config/mcp.tslines 5-19:type: "local"command: string[]- optional
environment,enabled,timeout.
- OpenCode's interactive
mcp addresolves project config candidates inv1/opencode/packages/opencode/src/cli/cmd/mcp.tslines 399-415. - OpenCode's
mcp addusesjsonc-parserupdate edits inv1/opencode/packages/opencode/src/cli/cmd/mcp.tslines 417-431. - OpenCode config supports
instructions: string[]inv1/opencode/packages/opencode/src/config/config.tslines 238-240. - OpenCode config supports
pluginarray inv1/opencode/packages/opencode/src/config/config.tsline 159.
Config helpers
Use OpenCode-specific config shapes, not Claude shapes.
MCP preview should produce:
{
type: "local",
command: ["npx", "-y", "@open-pets/cli@<version>", "mcp", "--pet", petId],
enabled: true,
}
Instructions preview should plan:
- project:
.opencode/openpets.mdplusinstructions: [".opencode/openpets.md"]; - global: a safe OpenCode config dir path decided in later desktop phase.
Plugin preview should be intentionally minimal in 19A because the runtime contract is finalized in Phase 19B. It may build the planned config slot and preserve existing entries, but installed/stale plugin classification should be limited to exact/recognizable OpenPets specs already known. Ambiguous plugin entries must be classified as custom/foreign and left untouched.
Safe write model
This phase can implement actual helper functions for isolated fixture writes, but tests must only write to temp directories.
Rules:
- Refuse to parse/write oversized config files.
- Refuse symlinked config files.
- Refuse symlinked relevant parent directories, including top-level config parents and
.opencode, when creating or updating config. - Refuse symlinked or escaping project roots.
- On parse errors, return an error status and do not produce a write operation.
- On invalid known field types, return an error status and do not produce a write operation.
- For update helpers, create backups before replacing/removing/updating.
- Use an exclusive temp file in the same directory and rename for final write.
- Generate backup/temp names that do not overwrite existing files.
Shared speech utilities
Keep Claude behavior equivalent. Move pure validation and message selection first; avoid over-refactoring hook runtime.
The shared module should be neutral, not named Claude-specific, so Phase 19B can import it from the OpenCode plugin without circular dependencies.
If throttling is moved in this phase, throttle storage must remain namespaced by agent/integration so Claude and OpenCode do not overwrite each other's cooldown state. If this makes 19A too broad, leave throttling in Claude for now and add a clear Phase 19B follow-up.
Risks and tradeoffs
- Adding a new package increases workspace wiring but keeps OpenCode code isolated.
- Direct JSONC editing is necessary because OpenCode's
mcp addis interactive, but it creates data-loss risk; mitigate with parse guards, backups, and atomic writes. - Plugin config shape may need adjustment in Phase 19B after runtime smoke tests; keep plugin preview centralized so future correction is one-file.
- Shared speech extraction can accidentally change Claude behavior; keep tests focused on parity.
Security/privacy notes
- Do not read or write real user OpenCode/Claude files in tests.
- Do not expose prompt text, command text, code, logs, file paths, URLs, secrets, or tool output through shared speech utilities.
- Validate pet ids with the same strict rules as Claude.
- Treat config paths as untrusted; reject symlinks and oversized files.
- Preserve unrelated user config.
Test/check plan
pnpm --filter @open-pets/opencode checkpnpm --filter @open-pets/claude checkpnpm --filter @open-pets/desktop checkif packaging/check contracts are touched.
Specific tests/checks:
- Project candidate selection with each possible existing config file.
- No existing config plans
.opencode/opencode.jsonc. - Status scanning sees duplicate/conflicting OpenPets entries across top-level and
.opencodeconfigs. - Global discovery covers
OPENCODE_CONFIG_DIR, platform/XDG default path,config.json,opencode.json, andopencode.jsonc. - MCP preview with and without selected pet.
- Local-dev preview uses
nodecommand only when explicitly requested. - JSONC update preserves unrelated fields and comments where practical.
- Invalid known field types return error and no write plan:
mcp: [],instructions: "x",plugin: {}. - Existing matching OpenPets entries classify as installed.
- Existing stale OpenPets entries classify as needs update.
- Custom/foreign
mcp.openpetsclassifies separately and is not treated as managed. - Non-OpenPets entries are preserved.
- Invalid JSONC returns error and no write plan.
- Oversized config returns error.
- Symlinked config, symlinked top-level parent, symlinked
.opencode, or symlinked/escaping project root returns error. - Backup and temp names are unique and do not overwrite existing files.
- Temp writes use exclusive creation in the same directory as the target.
- Backup/temp write helpers write only inside temp fixtures.
- Shared speech validator parity with Claude hook expectations.
- If throttling is extracted, Claude throttle path/category/cooldown parity is covered.
- Cross-platform path fixtures cover POSIX paths, Windows-style paths, spaces in paths,
XDG_CONFIG_HOME, andOPENCODE_CONFIG_DIR. - Claude hook checks still pass.
Manual verification guide
After implementation, manually verify:
- Run
pnpm --filter @open-pets/opencode check. - Run
pnpm --filter @open-pets/claude check. - If desktop/package files changed, run
pnpm --filter @open-pets/desktop check. - Run any fixture/manual check with temp
HOME,XDG_CONFIG_HOME, andOPENCODE_CONFIG_DIR. - Inspect generated fixture outputs from tests, if any, and confirm OpenCode config shape matches the expected MCP/instructions/plugin previews.
- Confirm no real
~/.config/opencode,~/.claude, or OpenPets user state changed.
Oracle plan review
Oracle reviewed the initial Phase 19A spec and found the subphase cut good, but not fully implementation-ready until config ownership, precedence, and write contracts were tightened.
Blockers raised:
- Global config scope was contradictory.
- Write target order and full load/status scanning were underspecified.
- Managed ownership rules were missing.
- JSON/JSONC write contract needed invalid type, symlink, atomicity, backup, and mode details.
- No-pet MCP preview behavior was undefined.
- Package dependency/version source could create cycles.
- Shared speech throttling extraction was too broad.
- Plugin preview/status was premature without a finalized plugin runtime contract.
Oracle feedback disposition
- Fixed: Defined global config discovery/status as read-only in 19A with
OPENCODE_CONFIG_DIR, default path, and candidate files. - Fixed: Required status helpers to scan all relevant project config files and classify multiple/conflicting entries.
- Fixed: Added explicit managed/current/stale/custom ownership rules for MCP, instructions, and plugin-like entries.
- Fixed: Added invalid known field type errors, project-root checks, parent symlink rejection, unique backups/temp files, exclusive same-directory temp writes, and private mode requirement where meaningful.
- Fixed: Defined no-pet MCP preview as default OpenPets MCP target without
--pet. - Fixed: Required package version injection so
packages/opencodedoes not import@open-pets/cli. - Fixed: Narrowed shared speech extraction to categories/messages/validation; throttling is optional only with parity tests, otherwise deferred.
- Fixed: Limited plugin preview/status to minimal exact/recognizable specs and custom/foreign preservation until Phase 19B finalizes runtime contract.