diff --git a/docs/devflow/opencode-port/02-proposal.md b/docs/devflow/opencode-port/02-proposal.md new file mode 100644 index 000000000000..a6873c1db16e --- /dev/null +++ b/docs/devflow/opencode-port/02-proposal.md @@ -0,0 +1,511 @@ +# OpenCode Port for Devflow + +## Problem + +Devflow currently has a Claude Code plugin and a nominal OpenCode installer +target, but the OpenCode target is not functionally equivalent. + +Evidence from the current codebase: +- `.claude-plugin/plugin.json` is the only plugin manifest. +- `manifests/install-modules.json` installs only rules and agents for the + OpenCode target. +- `scripts/install/install.py` copies OpenCode files directly to + `~/.opencode`, while current OpenCode uses `~/.config/opencode` for global + config and `.opencode/` for project-local config. +- A disposable OpenCode install fails validation with `opencode agent list` + because existing agent frontmatter uses Claude-style `tools: ["Read", ...]`, + while OpenCode expects permission/tool objects. +- Claude hook configuration in `hooks/hooks.json` uses `CLAUDE_PLUGIN_ROOT` and + Claude hook event names. OpenCode does not consume that file directly. + +The biggest correctness risk is rule loading. Devflow rules are mandatory, but +current OpenCode does not document support for `~/.claude/rules/`. OpenCode +does support: +- `AGENTS.md` in the project. +- `~/.config/opencode/AGENTS.md` globally. +- `CLAUDE.md` and `~/.claude/CLAUDE.md` as compatibility fallbacks. +- `opencode.json` `instructions` entries for explicit instruction files and + glob patterns. +- `~/.claude/skills/` compatibility for skills. + +There is an upstream OpenCode PR, `anomalyco/opencode#10090`, that adds +context-aware `.claude/rules/` compatibility, but it is open and must not be a +dependency for the initial port. The initial port must explicitly wire rules +through OpenCode-supported mechanisms. + +## Design + +### Strategic Direction + +OpenCode is the long-term target harness for devflow. Claude Code support +remains important, but it is not the destination architecture. + +The purpose of maintaining Claude Code compatibility is pragmatic: +- Claude Code is the current working harness. +- Many users have adherence problems with Claude Code and benefit from + devflow's structural safeguards. +- Existing Claude Code workflows prove the value of rules, agents, skills, + hooks, and commands as a coherent system. + +The port must preserve the workflows, structure, agents, and enforcement model +that make devflow useful, but new architectural decisions should bias toward +OpenCode as the primary platform. Claude Code should be treated as one adapter +over shared devflow primitives, not as the canonical runtime model. + +This matters because devflow exists specifically to compensate for errant or +non-adherent coding agents. Behavioral prompts are not enough. The port is not +complete when OpenCode can merely read the prompts; it is complete when +OpenCode can enforce the workflow boundaries that make the system reliable. + +Future work may add another agent or harness beyond OpenCode. The structure +introduced here should make that possible by keeping devflow's core contracts +harness-neutral and moving harness-specific behavior into adapters. + +### Goal + +Support Claude Code and OpenCode from the same devflow repository without +forking the rule, agent, skill, command, or hook logic. + +Claude Code remains supported for current users and for people who need +adherence safeguards in that harness. OpenCode becomes the primary long-term +target with a first-class adapter layer. + +### Non-Goals + +- Do not replace the Python enforcement hooks during this port. +- Do not require unreleased OpenCode PRs for the baseline `/flow` experience. +- Do not make OpenCode consume `hooks/hooks.json` directly. +- Do not duplicate canonical rules, agents, skills, or hook scripts by hand. + +### Porting Strategy + +Keep canonical devflow content in the existing directories: + +``` +rules/ canonical mandatory instructions +agents/ canonical agent prompts +skills/ canonical reusable skills +commands/ canonical slash-command prompts where portable +hooks/ canonical enforcement and telemetry scripts +scripts/ canonical runtime helpers +``` + +Add harness adapters at install time: +- Claude adapter preserves current behavior. +- OpenCode adapter transforms paths and frontmatter into OpenCode-native + formats. +- OpenCode plugin adapter maps OpenCode plugin events to the canonical devflow + hook payload consumed by existing Python scripts. + +### OpenCode Layout + +Default global install root: + +``` +~/.config/opencode/ + AGENTS.md + opencode.json + agents/ + commands/ + skills/ + plugins/devflow.js + devflow/ + install-state.json + rules/ + hooks/ + scripts/ + kaizen/ +``` + +Project-local install root when requested: + +``` +.opencode/ + AGENTS.md + opencode.json + agents/ + commands/ + skills/ + plugins/devflow.js + devflow/ + rules/ + hooks/ + scripts/ +``` + +The installer must support `--root` for tests and must not hardcode the user's +real home directory into test expectations. + +### Rule Loading Contract + +OpenCode must receive devflow rules through supported mechanisms, not through +assumed `~/.claude/rules/` compatibility. + +The OpenCode installer writes an `AGENTS.md` that says devflow rules are +mandatory and points to the installed rule files. It also writes or merges an +`opencode.json` `instructions` list containing the installed rule paths. + +Example generated config: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "instructions": [ + "devflow/rules/devflow.md", + "devflow/rules/proposals.md", + "devflow/rules/research.md", + "devflow/rules/accountability.md", + "devflow/rules/maven.md" + ] +} +``` + +Generated `AGENTS.md` must include a short fail-closed instruction: + +```markdown +# Devflow + +The files listed in `opencode.json` `instructions` under `devflow/rules/` are +mandatory constraints. If they are unavailable, stop and report that the +devflow OpenCode installation is broken instead of proceeding. +``` + +This makes rule loading explicit and testable. If upstream `.claude/rules/` +compatibility lands later, it can be an optimization, not the source of truth. + +### Agent Conversion + +Existing agents use Claude frontmatter: + +```yaml +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +model: sonnet +``` + +OpenCode agents need OpenCode frontmatter: + +```yaml +mode: subagent +permission: + read: allow + edit: allow + bash: allow + grep: allow + glob: allow +``` + +The installer transforms agent frontmatter for the OpenCode target. + +Mapping: + +| Claude Tool | OpenCode Permission | +|---|---| +| `Read` | `read: allow` | +| `Write`, `Edit`, `MultiEdit` | `edit: allow` | +| `Bash` | `bash: allow` | +| `Grep` | `grep: allow` | +| `Glob` | `glob: allow` | +| `Task` | `task: allow` | +| `Skill` | `skill: allow` | +| `WebFetch` | `webfetch: allow` | +| `WebSearch` | `websearch: allow` | + +Model aliases (`opus`, `sonnet`) must either be omitted for OpenCode or mapped +through installer configuration. The initial port should omit model overrides +unless a target model is explicitly configured, so OpenCode inherits the user's +chosen model. + +### Skills + +OpenCode natively supports agent skills using `skills//SKILL.md` with +the same basic `name` and `description` frontmatter used by devflow. + +The OpenCode target should install all devflow skills under `skills/`, not only +agents and rules. This includes workflow phase skills and cross-cutting skills +such as TDD, systematic debugging, completion reports, parallel agents, code +review, Maven optimization, SVG diagrams, quality audit, and documentation. + +### Commands + +OpenCode supports markdown commands under `commands/`. The initial command +port should include `/flow` first because it is prompt-driven and does not +depend on Claude stop-hook behavior. + +OpenCode command conversion must: +- Preserve `description`. +- Drop Claude-only fields such as `argument-hint` if OpenCode ignores or + rejects them. +- Drop or rewrite `allowed-tools` because OpenCode uses `permission`, not + Claude command-level tool gates. +- Replace `${CLAUDE_PLUGIN_ROOT}` usage with OpenCode-compatible generated + paths or avoid shell blocks entirely. + +`/loop`, `/loop-status`, and `/loop-cancel` are phase two because the current +loop model depends on Claude `Stop` hook behavior. + +### OpenCode Plugin Adapter + +OpenCode plugins are JavaScript/TypeScript modules. The port adds a generated +or source-controlled plugin adapter at `plugins/opencode/devflow.js` and +installs it into OpenCode's plugin directory. + +The adapter responsibilities: +- Subscribe to `tool.execute.before` and invoke canonical pre-tool hook chain. +- Subscribe to `tool.execute.after` and invoke canonical post-tool telemetry + hook chain. +- Normalize OpenCode tool input into the canonical devflow JSON shape: + +```json +{ + "harness": "opencode", + "session_id": "...", + "agent_id": "...", + "agent_type": "...", + "tool_name": "Write", + "tool_input": {}, + "cwd": "..." +} +``` + +- Convert OpenCode tool names to canonical names: + +| OpenCode Tool | Canonical Tool | +|---|---| +| `read` | `Read` | +| `write` | `Write` | +| `edit` | `Edit` | +| `apply_patch` | `MultiEdit` or `ApplyPatch` after hook support is added | +| `bash` | `Bash` | +| `grep` | `Grep` | +| `glob` | `Glob` | +| `task` | `Task` | +| `skill` | `Skill` | +| `webfetch` | `WebFetch` | +| `websearch` | `WebSearch` | + +- Run Python hooks as subprocesses with stdin JSON. +- If any pre-tool hook exits `2`, throw from `tool.execute.before` so OpenCode + blocks the tool call. +- Never let telemetry hook failures block work. + +The adapter must not rely on OpenCode consuming Claude's `hooks/hooks.json`. +Instead, it may reuse the hook chain definitions by reading `hooks/hooks.json` +as devflow-owned configuration and translating matcher names internally. + +### Enforcement Gaps and Upstream Dependencies + +Baseline OpenCode port can enforce tool calls with the documented +`tool.execute.before` hook. Exact Claude parity needs upstream work. + +It is acceptable to maintain a devflow OpenCode fork while upstream PRs are +pending, provided the fork uses a small, explicit compatibility patch stack and +every absorbed PR has a devflow-specific reason. Do not absorb broad unrelated +OpenCode feature work. + +### OpenCode Fork Patch Stack + +The fork should start from the latest upstream `anomalyco/opencode` `dev` +branch, then apply a curated set of PRs in compatibility layers. + +#### Must Absorb + +These PRs close direct gaps for devflow enforcement, loop behavior, or hook +correctness. + +| PR | Status | Reason | +|---|---|---| +| `anomalyco/opencode#16598` | open | Adds `session.stopping`, the closest equivalent to Claude `Stop` hook. Required for `/loop` re-entry parity. | +| `anomalyco/opencode#15412` | open | Adds parent agent context to hook inputs. Required for reliable orchestrator/subagent boundary enforcement and agent telemetry. | +| `anomalyco/opencode#19470` | open | Wires `permission.ask` plugin hook. Allows devflow policy to participate in permission decisions before normal UI prompting. | +| `anomalyco/opencode#22654` | open | Exposes `ask()` inside `tool.execute.before`. Allows pre-tool hooks to request approval instead of only allowing or throwing. | +| `anomalyco/opencode#20053` | open | Allows plugin hooks to mutate tool call args before execution. Needed if the devflow adapter normalizes or rewrites tool arguments. | +| `anomalyco/opencode#21150` | open | Fires `tool.execute.after` after MCP output assembly. Ensures telemetry observes final tool output state. | + +#### Should Absorb + +These PRs improve lifecycle coverage and plugin reliability. They are not +strictly required for the first blocking hook adapter, but they make the fork +closer to Claude Code/devflow parity. + +| PR | Status | Reason | +|---|---|---| +| `anomalyco/opencode#15224` | open | Adds `session.start`, a `SessionStart`-like lifecycle hook for session tracking and startup context. | +| `anomalyco/opencode#23650` | open | Adds `session.turn.completed`, useful for telemetry, phase timing, and future review UI. | +| `anomalyco/opencode#19519` | open | Lets `tool.execute.after` hooks inject AI-visible messages. Useful for post-tool hook feedback. | +| `anomalyco/opencode#21773` | open | Adds `messageID` and `agent` to `shell.env` context. Useful for session-aware subprocess environment injection. | +| `anomalyco/opencode#21776` | open | Adds `bash.commands` timeout exemption hook. Useful if devflow helper CLIs legitimately run longer than normal bash timeouts. | +| `anomalyco/opencode#17517` | open | Awaits plugin event hooks and handles errors in database effects. Stability improvement for hook-dependent integrations. | + +#### Rules and Claude Compatibility + +These PRs affect how instructions/rules are discovered. They are useful, but +mandatory devflow rule loading must still be explicit through `opencode.json` +`instructions`. + +| PR | Status | Recommendation | +|---|---|---| +| `anomalyco/opencode#18903` | open | Absorb if we want native `.opencode/rules/*.{md,mdc}` loading. Small and low-risk. | +| `anomalyco/opencode#10090` | open | Absorb only if we want broader context-aware `.claude/rules/` compatibility. Larger and more ambitious than required for devflow. | +| `anomalyco/opencode#6990` | closed | Use as reference for `.claude/commands/` compatibility. Do not absorb as-is; it includes unresolved `allowed-tools` translation work. | + +#### Reference Only + +These PRs are useful for design context but should not be absorbed wholesale. + +| PR | Status | Reason | +|---|---|---| +| `anomalyco/opencode#11525` | closed | Claims all Claude Code hooks, but describes non-blocking hooks. Devflow requires blocking pre-tool enforcement. Mine for event names and payload ideas only. | +| `anomalyco/opencode#9272` | open | Similar to `session.stopping`, but `#16598` is the better fit for Claude `Stop` parity. Absorb only if it composes cleanly. | +| `anomalyco/opencode#19453` | open | Overlaps with `#19470`. Pick one implementation; prefer `#19470` based on clearer test description. | +| `anomalyco/opencode#20009` | open | Overlaps with `#20053`. Pick one implementation after conflict review; start with `#20053`. | + +Recommended fork application order: + +1. Apply lifecycle hooks: `#15224`, `#16598`, `#23650`. +2. Apply hook context improvements: `#15412`, `#21773`. +3. Apply permission and tool-hook correctness: `#19470`, `#22654`, `#20053`, `#21150`. +4. Apply optional rule discovery: `#18903` or `#10090`, not both initially unless conflict-free. +5. Evaluate optional stability improvements: `#17517`, `#21776`, `#19519`. + +The fork must maintain a compatibility manifest documenting each absorbed PR, +its upstream URL, commit SHA in the fork, and devflow feature that depends on +it. This prevents the fork from becoming an untracked OpenCode distribution. + +Earlier high-value upstream PR summary: + +| PR | Status | Relevance | +|---|---|---| +| `anomalyco/opencode#16598` | open | Adds `session.stopping`, the closest equivalent to Claude `Stop` loop re-entry. Needed for `/loop` parity. | +| `anomalyco/opencode#15412` | open | Adds parent agent context to hook inputs. Useful for orchestrator/subagent boundary enforcement. | +| `anomalyco/opencode#19470` | open | Wires `permission.ask` plugin hook. Useful for policy-controlled permission decisions. | +| `anomalyco/opencode#22654` | open | Exposes `ask()` inside `tool.execute.before`. Useful for interactive policy decisions. | +| `anomalyco/opencode#20053` | open | Allows plugins to mutate tool call args before execution. Useful for future argument normalization. | +| `anomalyco/opencode#23650` | open | Adds per-turn completion event. Useful for telemetry and review UI, not required for baseline. | +| `anomalyco/opencode#10090` | open | Adds `.claude/rules/` compatibility. Useful but not required because this port explicitly uses `instructions`. | +| `anomalyco/opencode#11525` | closed | Full Claude-style hook proposal. Useful as reference only; not viable as a dependency because it was closed and described non-blocking hooks. | + +Initial implementation must not block on these PRs. `/loop` parity should be +tracked as blocked or partial until `session.stopping` or equivalent behavior +exists in a released OpenCode version. + +### Telemetry + +Existing telemetry scripts hardcode `harness = claude` in several inserts. +They must read `harness` from hook input, defaulting to `claude` for backward +compatibility. + +The OpenCode adapter must provide: +- `harness: opencode` +- session ID when available +- agent identity when available +- canonical tool name +- normalized file path or command + +### Installer Changes + +Update `manifests/install-modules.json`: +- Add OpenCode to skill module targets. +- Add OpenCode to relevant command targets after command conversion exists. +- Add OpenCode-specific plugin adapter module. +- Add OpenCode-specific settings/config module. + +Update `scripts/install/install.py`: +- Change default OpenCode root to `~/.config/opencode`. +- Add `--target opencode --project-local` or equivalent for `.opencode/` installs. +- Add content transforms for agents and commands. +- Generate or merge `opencode.json` safely. +- Generate `AGENTS.md` if absent, or install `devflow/AGENTS.md` and reference it + from `opencode.json` `instructions` if merge safety is a concern. +- Preserve install-state tracking for every generated and copied file. + +### Testing Strategy + +Installer tests must validate actual OpenCode compatibility, not just file +presence. + +Required tests: +- `./install.sh --target claude --root ` still installs the current Claude + layout. +- `./install.sh --target opencode --root ` installs OpenCode layout. +- `OPENCODE_CONFIG_DIR= opencode agent list` succeeds. +- Installed OpenCode `opencode.json` contains all mandatory rule files in + `instructions`. +- Installed OpenCode `AGENTS.md` contains the fail-closed rule-loading contract. +- Every installed OpenCode agent has valid OpenCode frontmatter. +- OpenCode skills are discoverable by path and have valid `SKILL.md` + frontmatter. +- OpenCode plugin adapter blocks a synthetic disallowed write when a canonical + hook exits `2`. +- OpenCode plugin adapter does not block when telemetry hooks fail. +- Existing hook shell tests continue to pass unchanged. + +## Acceptance Criteria + +1. `./install.sh --target opencode --root ` installs to an OpenCode-valid + directory structure rooted at the provided root. +2. The default OpenCode global install root is `~/.config/opencode`, not + `~/.opencode`. +3. The OpenCode install writes or merges `opencode.json` with explicit + `instructions` entries for every mandatory devflow rule file. +4. The OpenCode install writes an `AGENTS.md` fail-closed rule-loading contract. +5. OpenCode install does not rely on `~/.claude/rules/` compatibility. +6. `OPENCODE_CONFIG_DIR= opencode agent list` succeeds after install. +7. All OpenCode-installed agents use valid OpenCode frontmatter and permission + syntax. +8. Model aliases in canonical agent files do not break OpenCode agent loading. +9. OpenCode installs all devflow skills under `skills/` with valid `SKILL.md` + frontmatter. +10. OpenCode installs `/flow` as a usable command without unresolved + `${CLAUDE_PLUGIN_ROOT}` references. +11. Claude install behavior remains unchanged and its existing install tests + continue to pass. +12. An OpenCode plugin adapter exists and is installed for the OpenCode target. +13. The adapter maps `tool.execute.before` to canonical pre-tool hook execution. +14. The adapter blocks an OpenCode tool call when a canonical pre-tool hook exits + `2`. +15. The adapter maps `tool.execute.after` to canonical post-tool telemetry hook + execution. +16. Telemetry records use `harness = opencode` for OpenCode-originated events. +17. Telemetry failures in OpenCode adapter do not block work. +18. The adapter normalizes OpenCode tool names and tool inputs into canonical + devflow hook JSON. +19. Existing Python hook tests pass without requiring separate OpenCode-specific + hook implementations. +20. `/loop` support is explicitly documented as partial unless the released + OpenCode version includes a `session.stopping`-equivalent hook. + +## Alternatives Considered + +### Rely on `~/.claude/rules/` in OpenCode + +Rejected for the initial port. Current OpenCode documentation lists +`CLAUDE.md`, `~/.claude/CLAUDE.md`, and `~/.claude/skills/` compatibility, but +not `~/.claude/rules/`. Upstream PR `#10090` may add this later, but mandatory +rules must not depend on unreleased behavior. + +### Duplicate OpenCode-Specific Agents and Commands + +Rejected. It would work initially but create drift. The correct approach is +canonical markdown plus target-specific transforms. + +### Port Hooks Directly to JavaScript + +Rejected for the initial port. It duplicates enforcement logic and increases +the chance Claude and OpenCode diverge. A JS adapter around existing Python +hooks is smaller and keeps behavior shared. + +### Wait for Full Claude Hook Compatibility Upstream + +Rejected. Baseline `/flow`, rules, agents, skills, and tool enforcement can be +ported using existing OpenCode plugin hooks. Only `/loop` parity should wait +for or contribute to upstream lifecycle hooks. + +### Use OpenCode Permissions Only, Without Devflow Hooks + +Rejected. OpenCode permissions provide useful coarse controls, but devflow +requires proposal lifecycle checks, TDD commit ordering, artifact sequencing, +and phase-aware enforcement. Those remain devflow hook responsibilities. + +## Reset History + +This section is populated automatically when a reset occurs. diff --git a/docs/devflow/opencode-port/03-plan.md b/docs/devflow/opencode-port/03-plan.md new file mode 100644 index 000000000000..7989cd15f2a7 --- /dev/null +++ b/docs/devflow/opencode-port/03-plan.md @@ -0,0 +1,506 @@ +# Implementation Plan: OpenCode Port for Devflow + +**Proposal:** 02-proposal.md +**Status:** Draft + +## Operating Principle + +OpenCode is the long-term target. Claude Code remains supported because it is +the current working harness and because users with Claude Code adherence +problems need the safeguards now. Do not make new architecture Claude-shaped +unless OpenCode can consume the same contract through an adapter. + +The port has two parallel tracks: + +1. **Devflow adapter work** in this repository: install transforms, OpenCode + plugin adapter, rule loading, agent conversion, parity tests. +2. **OpenCode fork work** in a fork of `anomalyco/opencode`: curated PR stack + for missing lifecycle and plugin primitives, rebased regularly onto upstream + `dev`. + +No port work is complete unless both tracks have documentation that explains +what changed, what gap it closes, and what parity remains missing. + +## Current Claude Code Enforcement Surface + +The current Claude Code plugin is not just prompts. It has three layers. + +### Behavioral Layer + +| Primitive | Current Claude Mechanism | OpenCode Target | +|---|---|---| +| Rules | Installed rule files plus agent startup instructions | Explicit `opencode.json` `instructions` plus fail-closed `AGENTS.md` | +| Agents | Markdown agents with Claude `tools` arrays | Markdown agents with OpenCode `permission` objects | +| Skills | `skills//SKILL.md` | Native OpenCode skills under `skills//SKILL.md` | +| Commands | Claude markdown commands under `commands/` | OpenCode markdown commands with converted frontmatter | + +### Structural Layer + +Current hook registration is in `hooks/hooks.json`. Python hook commands now +point at `src/devflow/...` with `PYTHONPATH="${CLAUDE_PLUGIN_ROOT}/src"`. + +| Claude Event | Current Hook Chain | Purpose | OpenCode Equivalent | +|---|---|---|---| +| `PreToolUse` all tools | `hooks/reinforce.sh` | Reinforce core principles before tool use | Prefer `session.start`, `system transform`, or compaction hook; avoid per-tool spam if OpenCode can keep instructions stable | +| `PreToolUse Edit/Write/MultiEdit` | `orchestrator_dispatch`, `worktree_isolation`, `criteria_coverage_gate`, `commit_order`, `phase_gate`, `workflow_gate` | Block direct orchestrator writes, enforce worktree, criteria, TDD order, phase, workflow gates | OpenCode `tool.execute.before` adapter, blocking by throwing on hook exit 2 | +| `PreToolUse Bash` | `orchestrator_dispatch`, `worktree_isolation`, `commit_order`, `block_bash_file_writes`, `test_before_commit`, `artifact_sequence_check`, `initiative_structure_check`, `initiative_content_check` | Block orchestrator shell, worktree bypass, TDD/order violations, bash file writes, bad commits/artifacts | OpenCode `tool.execute.before` adapter for `bash` | +| `PostToolUse Edit/Write/MultiEdit` | `record_tool_call`, `record_artifact_event` | Tool telemetry and artifact event telemetry | OpenCode `tool.execute.after` adapter | +| `PostToolUse Bash` | `record_tool_call`, `record_phase_timing` | Shell telemetry and phase timing | OpenCode `tool.execute.after` adapter; prefer PR `#21150` for final MCP output timing | +| `PostToolUse Read/Grep/Glob` | `record_tool_call` | Read/search telemetry | OpenCode `tool.execute.after` adapter | +| `SessionStart` | `record_session`, `session_summary` | Session telemetry and summaries | OpenCode `session.start` from PR `#15224`, plus existing session events if available | +| `SessionStart compact` | `reinforce-on-compact.sh` | Reinject core rules after compaction | OpenCode `experimental.session.compacting` plugin hook | +| `SubagentStart` | `record_agent_start` | Agent dispatch telemetry | OpenCode task/tool hooks with PR `#15412` parent agent context | +| `SubagentStop` | `log_agent_completion` | Agent completion telemetry for gates | OpenCode task/session events; may need fork support for child session status | +| `Stop` | `stop-hook.sh` | Loop continuation / promise checking | OpenCode `session.stopping` from PR `#16598` | + +### Verification Layer + +The current workflow relies on independent reviewers, verifiers, completion +reports, and phase gates. These are mostly prompt/agent/skill contracts and can +carry to OpenCode once agents, skills, commands, and task invocation work. + +## OpenCode-Specific Enforcement Design Changes + +Some parts should work differently in OpenCode while preserving the same level +of enforcement. + +1. **Use OpenCode permissions as a first line of defense.** + Claude relies heavily on hooks to stop the orchestrator from editing. In + OpenCode, define a `devflow-orchestrator` primary agent with `edit: deny`, + constrained `bash`, and `task` allowed only for devflow subagents. Keep hook + enforcement as the authoritative second line. + +2. **Use `opencode.json` `instructions` for mandatory rules.** + Do not depend on implicit `.claude/rules/` compatibility. OpenCode can read + explicit instruction files now. This is more deterministic than rule folder + discovery. + +3. **Use native OpenCode skill discovery.** + Skills already match the `SKILL.md` structure. Install them natively under + OpenCode's `skills/` directory instead of routing them through Claude paths. + +4. **Convert agents at install time.** + Keep canonical agent markdown, but generate OpenCode-valid frontmatter. + Avoid `model: opus` and `model: sonnet` aliases unless the user supplies an + explicit OpenCode model mapping. + +5. **Map OpenCode `apply_patch` explicitly.** + Claude has `MultiEdit`; OpenCode has `apply_patch`. The adapter must parse + patch paths and run the same write enforcement. Treating `apply_patch` as a + simple edit without extracting paths would be an enforcement gap. + +6. **Prefer lifecycle hooks over per-tool reinforcement.** + Claude uses `reinforce.sh` on every pre-tool event. In OpenCode, use + `session.start` and compaction hooks for stable context. Keep per-tool + reinforcement only if adherence measurements show regression. + +7. **Implement loop continuation with `session.stopping`.** + Claude's `Stop` hook returns continuation prompts. OpenCode should use the + `session.stopping` PR behavior: plugin injects a follow-up user message and + sets stop false when the devflow loop should continue. + +8. **Make telemetry harness-aware.** + Existing telemetry must record `harness=opencode` for OpenCode events. The + adapter should supply the harness field, and Python telemetry should default + to Claude only when no harness is provided. + +9. **Keep hook scripts as the shared enforcement core.** + The OpenCode plugin should adapt event payloads and subprocess execution; + it should not fork enforcement logic into JavaScript unless a Python hook is + impossible to reuse. + +## Documentation System + +Add and maintain the following flat artifacts under +`docs/devflow/opencode-port/`. + +| Artifact | Purpose | Update Rule | +|---|---|---| +| `02-proposal.md` | Contract for the port | Update only through proposal review/change control | +| `03-plan.md` | Execution plan and task breakdown | Update whenever tasks or sequencing change | +| `opencode-fork-prs.md` | PR absorption manifest for the OpenCode fork | Update before and after applying any upstream PR | +| `parity-matrix.md` | Claude Code vs OpenCode parity status | Update after each implementation milestone | +| `gap-log.md` | Detailed gap ledger with owner, severity, and closure evidence | Update whenever a new gap is found or closed | +| `rebase-log.md` | Fork rebase history and conflicts | Update on every fork rebase onto upstream OpenCode | +| `verification-log.md` | Commands run and evidence collected | Update before claiming any parity milestone | + +These files are not optional project management ceremony. They are the control +surface that prevents the fork and adapter from drifting. + +## OpenCode Fork Workflow + +### Branches + +Use a dedicated fork with this branch model: + +| Branch | Purpose | +|---|---| +| `upstream-dev` | Mirror of `anomalyco/opencode:dev`; never commit directly | +| `devflow/base` | Fast-forward or reset mirror of `upstream-dev` for rebases | +| `devflow/pr--` | One branch per absorbed upstream PR | +| `devflow/hojo` | Linear stack of absorbed PRs plus minimal devflow-specific glue | +| `devflow/release` | Tested branch used by devflow users | + +### Applying PRs + +For each upstream PR: + +1. Record it in `opencode-fork-prs.md` with upstream PR number, title, URL, + status, reason, expected devflow gap closure, and risk. +2. Apply it to an isolated `devflow/pr-*` branch first. +3. Run upstream OpenCode tests relevant to that PR. +4. Merge or cherry-pick into `devflow/hojo` only after tests pass. +5. Record resulting fork commit SHA in `opencode-fork-prs.md`. +6. Update `gap-log.md` and `parity-matrix.md` with what actually improved. + +### Monitoring Upstream PRs + +Add a small script or manual checklist that runs: + +```bash +gh pr view --repo anomalyco/opencode --json number,state,mergedAt,closedAt,headRefOid,title,url +``` + +For every PR in `opencode-fork-prs.md`, track: + +| Field | Meaning | +|---|---| +| `upstreamState` | `OPEN`, `MERGED`, `CLOSED` | +| `upstreamHead` | Last seen upstream head SHA | +| `forkCommit` | Commit SHA applied in our fork | +| `absorbedVersion` | OpenCode fork branch/release containing it | +| `dropWhen` | Condition for removing local patch, usually upstream merge plus rebase | + +If a PR merges upstream, the next rebase should remove the local copy and note +the removal in `rebase-log.md`. If a PR closes unmerged, decide whether to keep +it as a devflow-owned patch or replace it. + +### Rebase Cadence + +Rebase `devflow/hojo` onto upstream `dev` at least weekly while the +port is active, and immediately before any devflow release that depends on the +fork. + +Every rebase must update `rebase-log.md` with: +- upstream base SHA before and after +- local patch count before and after +- conflicts encountered +- resolution summary +- tests run +- PRs dropped because upstream merged them +- PRs retained because upstream still lacks them + +## Initial Upstream PR Stack + +### Must Absorb + +| PR | Gap Closed | Validation | +|---|---|---| +| `#16598` `session.stopping` | `/loop` continuation / Claude `Stop` parity | Synthetic plugin injects one follow-up message and stops on second pass | +| `#15412` parent agent context | Agent identity and orchestrator/subagent boundary tracking | Tool hook input includes agent and parent agent for subagent tool call | +| `#19470` `permission.ask` | Policy participation in permission flow | Plugin can allow, deny, and fall back to ask | +| `#22654` `ask()` in `tool.execute.before` | Interactive pre-tool enforcement path | Pre-tool plugin can ask instead of throwing | +| `#20053` mutable tool args | Argument normalization before execution | Plugin mutation changes executed args | +| `#21150` post-MCP after hook timing | Accurate post-tool telemetry | `tool.execute.after` sees assembled MCP output | + +### Should Absorb + +| PR | Gap Closed | Validation | +|---|---|---| +| `#15224` `session.start` | SessionStart-like telemetry/context injection | Plugin receives first-message session start | +| `#23650` turn completed event | Per-turn telemetry and future review UX | Event fires after tool loop, before compaction | +| `#19519` AI-visible post-tool messages | Hook feedback to agent | Post-tool hook injects visible message | +| `#21773` shell env context | Agent/session-aware hook subprocess env | `shell.env` receives message and agent | +| `#21776` bash command timeout exemptions | Long-running devflow helper CLI support | Registered command runs without normal timeout | +| `#17517` plugin event awaiting/error handling | Plugin reliability | Plugin async errors do not corrupt database effects | + +### Rules PR Choice + +Start with `#18903` if we want a small native `.opencode/rules/*` loader. +Evaluate `#10090` only after baseline port works because it is broader and may +conflict with instruction loading. Devflow mandatory rules still use explicit +`opencode.json` `instructions` either way. + +## Criteria Mapping + +| AC# | Criterion | Tasks | Status | +|---|---|---|---| +| 1 | OpenCode install works with `--root ` | T2, T3, T4 | Not Started | +| 2 | Default root is `~/.config/opencode` | T2 | Not Started | +| 3 | `opencode.json` instructions include mandatory rules | T3 | Not Started | +| 4 | `AGENTS.md` fail-closed rule contract exists | T3 | Not Started | +| 5 | No reliance on `~/.claude/rules/` | T3, T12 | Not Started | +| 6 | `opencode agent list` succeeds | T4 | Not Started | +| 7 | Agents use OpenCode frontmatter | T4 | Not Started | +| 8 | Model aliases do not break OpenCode | T4 | Not Started | +| 9 | Skills install under OpenCode | T5 | Not Started | +| 10 | `/flow` command installs without Claude placeholders | T6 | Not Started | +| 11 | Claude install remains unchanged | T2, T13 | Not Started | +| 12 | OpenCode plugin adapter exists | T7 | Not Started | +| 13 | Adapter maps pre-tool hooks | T8 | Not Started | +| 14 | Adapter blocks on hook exit 2 | T8 | Not Started | +| 15 | Adapter maps post-tool telemetry hooks | T9 | Not Started | +| 16 | Telemetry records `harness=opencode` | T9 | Not Started | +| 17 | Telemetry failures do not block | T9 | Not Started | +| 18 | Adapter normalizes tool names and inputs | T7, T8, T9 | Not Started | +| 19 | Existing Python hook tests pass | T1, T13 | Not Started | +| 20 | `/loop` support documented as partial unless fork hook exists | T10, T12 | Not Started | + +## Task Definitions + +### T1: Establish Baseline and Freeze Current Enforcement Surface + +- **Criteria:** AC19 +- **Depends:** none +- **Files:** + - `docs/devflow/opencode-port/parity-matrix.md` (new) + - `docs/devflow/opencode-port/gap-log.md` (new) + - `docs/devflow/opencode-port/verification-log.md` (new) + +**Steps:** +1. RED: Add a parity matrix row for every hook/event in `hooks/hooks.json` and + mark OpenCode status as `unknown`. +2. GREEN: Run the current Python hook test suite with `PYTHONPATH=src` and + record exact passing/failing commands in `verification-log.md`. +3. COMMIT: Commit documentation first, then any fixes required to make baseline + tests pass. + +**Done when:** Baseline Claude enforcement surface is documented and current +hook tests have recorded evidence. + +### T2: Correct OpenCode Install Root and Preserve Claude Install + +- **Criteria:** AC1, AC2, AC11 +- **Depends:** T1 +- **Files:** + - `scripts/install/install.py` (edit) + - `tests/test-install.sh` or pytest equivalent (edit) + +**Steps:** +1. RED: Add installer test expecting OpenCode default global root semantics and + `--root` override behavior. +2. GREEN: Change OpenCode default root to `~/.config/opencode` and preserve + Claude behavior. +3. COMMIT: test first, then implementation. + +**Done when:** Claude and OpenCode install tests pass and OpenCode no longer +defaults to `~/.opencode`. + +### T3: Add Explicit OpenCode Rule Loading + +- **Criteria:** AC3, AC4, AC5 +- **Depends:** T2 +- **Files:** + - `scripts/install/install.py` (edit) + - `manifests/install-modules.json` (edit) + - tests for generated `opencode.json` and `AGENTS.md` (new/edit) + +**Steps:** +1. RED: Test OpenCode install produces `opencode.json` with mandatory + `instructions` entries and a fail-closed `AGENTS.md`. +2. GREEN: Generate or safely merge `opencode.json`; generate `AGENTS.md` rule + contract. +3. COMMIT: test first, then implementation. + +**Done when:** Mandatory rules are explicitly loaded without `.claude/rules/`. + +### T4: Convert Agents for OpenCode + +- **Criteria:** AC6, AC7, AC8 +- **Depends:** T3 +- **Files:** + - `scripts/install/install.py` (edit) + - agent transform tests (new/edit) + +**Steps:** +1. RED: Test disposable OpenCode install fails if agent frontmatter contains + Claude `tools: ["Read"]` arrays. +2. GREEN: Transform agent frontmatter to OpenCode `permission` objects and omit + unsupported model aliases unless configured. +3. COMMIT: test first, then implementation. + +**Done when:** `OPENCODE_CONFIG_DIR= opencode agent list` succeeds. + +### T5: Install Skills for OpenCode + +- **Criteria:** AC9 +- **Depends:** T3 +- **Files:** + - `manifests/install-modules.json` (edit) + - install tests (edit) + +**Steps:** +1. RED: Test OpenCode install includes representative workflow and domain + skills. +2. GREEN: Add OpenCode to skill module targets. +3. COMMIT: test first, then implementation. + +**Done when:** OpenCode install contains all devflow skills under `skills/`. + +### T6: Port `/flow` Command First + +- **Criteria:** AC10 +- **Depends:** T3, T4, T5 +- **Files:** + - `commands/flow.md` or generated OpenCode command transform (edit) + - install tests (edit) + +**Steps:** +1. RED: Test OpenCode installed `/flow` command contains no + `${CLAUDE_PLUGIN_ROOT}`, `allowed-tools`, or unsupported Claude-only fields. +2. GREEN: Add command transform or OpenCode-specific generated command. +3. COMMIT: test first, then implementation. + +**Done when:** OpenCode can load `/flow` command from installed config. + +### T7: Build OpenCode Plugin Adapter Skeleton + +- **Criteria:** AC12, AC18 +- **Depends:** T1 +- **Files:** + - `plugins/opencode/devflow.js` or `plugins/opencode/devflow.ts` (new) + - `scripts/install/install.py` (edit) + - install tests (edit) + +**Steps:** +1. RED: Test OpenCode install includes the plugin adapter and plugin can load in + a minimal OpenCode config. +2. GREEN: Create adapter with path resolution, subprocess execution helper, and + canonical payload builder. +3. COMMIT: test first, then implementation. + +**Done when:** Adapter loads without changing behavior. + +### T8: Implement Blocking Pre-Tool Enforcement Adapter + +- **Criteria:** AC13, AC14, AC18 +- **Depends:** T7 +- **Files:** + - `plugins/opencode/devflow.js` or `.ts` (edit) + - adapter tests (new) + +**Steps:** +1. RED: Synthetic OpenCode `tool.execute.before` payload for disallowed write + does not block. +2. GREEN: Map OpenCode tools to canonical hook payloads and throw when any + canonical hook exits 2. +3. COMMIT: test first, then implementation. + +**Done when:** Synthetic disallowed writes and bash file writes are blocked. + +### T9: Implement Post-Tool Telemetry Adapter + +- **Criteria:** AC15, AC16, AC17, AC18 +- **Depends:** T7 +- **Files:** + - `plugins/opencode/devflow.js` or `.ts` (edit) + - `src/devflow/telemetry/*.py` where harness is hardcoded (edit) + - adapter/telemetry tests (new/edit) + +**Steps:** +1. RED: Test telemetry records use `claude` when invoked from OpenCode payload. +2. GREEN: Pass and persist `harness=opencode`; make telemetry hook failures + non-blocking in adapter. +3. COMMIT: test first, then implementation. + +**Done when:** OpenCode tool telemetry records as OpenCode and failures do not +block. + +### T10: Add `/loop` Parity Behind Fork Capability Check + +- **Criteria:** AC20 +- **Depends:** T7, fork PR `#16598` +- **Files:** + - OpenCode plugin adapter (edit) + - command transform for `loop.md` (edit) + - `docs/devflow/opencode-port/parity-matrix.md` (edit) + +**Steps:** +1. RED: Document `/loop` as partial without `session.stopping` support. +2. GREEN: If fork supports `session.stopping`, implement one-shot loop + continuation using devflow loop state and promise detection. +3. COMMIT: documentation first, then implementation. + +**Done when:** `/loop` is either working on the fork or explicitly marked as +partial with exact blocker. + +### T11: Establish OpenCode Fork Tracking + +- **Criteria:** supports all fork-dependent ACs +- **Depends:** none +- **Files:** + - `docs/devflow/opencode-port/opencode-fork-prs.md` (new) + - `docs/devflow/opencode-port/rebase-log.md` (new) + - optional monitoring script under `scripts/` (new) + +**Steps:** +1. RED: Create PR tracker with all required PRs marked `not applied` and no + fork SHA. +2. GREEN: Add monitoring command/process and fill initial upstream state. +3. COMMIT: documentation first, then optional script. + +**Done when:** Every candidate PR has status, rationale, gap mapping, and +monitoring fields. + +### T12: Update User-Facing Documentation + +- **Criteria:** AC5, AC20 +- **Depends:** T3, T6, T10 +- **Files:** + - `docs/guide.md` (edit) + - `AGENTS.md` if directory structure changes (edit) + +**Steps:** +1. RED: Identify stale claims that OpenCode is fully supported. +2. GREEN: Document OpenCode support level, install command, rule loading, fork + requirement if applicable, and partial `/loop` status. +3. COMMIT: documentation change. + +**Done when:** User docs accurately describe OpenCode support and limitations. + +### T13: Full Verification Matrix + +- **Criteria:** AC1-AC20 +- **Depends:** T1-T12 +- **Files:** + - `docs/devflow/opencode-port/verification-log.md` (edit) + - `docs/devflow/opencode-port/parity-matrix.md` (edit) + +**Steps:** +1. Run Claude install tests. +2. Run OpenCode install tests. +3. Run `OPENCODE_CONFIG_DIR= opencode agent list`. +4. Run Python hook tests with `PYTHONPATH=src`. +5. Run OpenCode adapter tests. +6. Record all evidence. + +**Done when:** Every acceptance criterion is mapped to passing evidence or an +explicit documented limitation. + +## Agent Context + +Implementation agents must treat OpenCode as the strategic target and Claude +Code as a compatibility target. Do not remove Claude support. Do not duplicate +canonical prompts or enforcement logic. Prefer transforms and adapters. + +Important commands: + +```bash +PYTHONPATH=src python3 -m pytest +./install.sh --target claude --root /tmp/devflow-claude-test install +./install.sh --target opencode --root /tmp/devflow-opencode-test install +OPENCODE_CONFIG_DIR=/tmp/devflow-opencode-test opencode agent list +``` + +Important constraints: +- No claim of OpenCode parity without evidence in `verification-log.md`. +- No absorbed OpenCode fork PR without an entry in `opencode-fork-prs.md`. +- No dropped fork patch without an entry in `rebase-log.md`. +- No implicit rule loading. Mandatory rules must be explicit in `opencode.json` + `instructions`. +- No JavaScript copy of Python enforcement logic unless a specific hook cannot + be adapted. + +## Reset History + +This section is populated automatically when a reset occurs. diff --git a/docs/devflow/opencode-port/build-commands.md b/docs/devflow/opencode-port/build-commands.md new file mode 100644 index 000000000000..63a424e4a9d9 --- /dev/null +++ b/docs/devflow/opencode-port/build-commands.md @@ -0,0 +1,152 @@ +# Build and Test Commands + +Use this file as the command reference for OpenCode fork work. Update it when a +new verification command is discovered or when a command has an important +environment requirement. + +## Environment + +Required tools: + +```bash +node --version +npm --version +bun --version +``` + +Current local setup: +- Bun installed with `brew install oven-sh/bun/bun`. +- Verified Bun version: `1.3.13`. +- OpenCode root: `/Users/jvanzyl/js/jopen/hojo-opencode`. + +## Dependency Install + +Run from the OpenCode root: + +```bash +bun install +``` + +Known behavior: +- `bun install` can update `bun.lock` even when no source change is intended. +- After switching among OpenCode PR branches, stale `node_modules` can resolve an + older `effect` version than `bun.lock`; rerun `bun install` before treating + Effect import/type errors as code failures. +- On `devflow/pr-15224-session-start`, it added missing lock entries for the + PR's demo workspace package. +- On `devflow/pr-16598-session-stopping`, it changed the `ghostty-web` resolved + Git SHA. +- Treat lockfile drift as a separate review item. Do not silently include it in + PR absorption unless it is required and documented. + +## Narrow Package Checks + +Run from package directories, not with `bun --cwd`; Bun 1.3.13 did not accept +the attempted `bun --cwd "packages/opencode" run typecheck` command form. + +Plugin package: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/plugin +bun run typecheck +bun run build +``` + +OpenCode package: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun run typecheck +bun run build +``` + +Observed results: +- `packages/plugin`: `bun run typecheck` runs `tsgo --noEmit`. +- `packages/plugin`: `bun run build` runs `tsc`. +- `packages/opencode`: `bun run typecheck` runs `tsgo --noEmit`. +- `packages/opencode`: `bun run build` runs `bun run script/build.ts` and builds + target binaries. + +## PR-Specific Tests + +Session stopping PR `#16598`: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun test test/plugin/session-stopping.test.ts +``` + +Observed result on 2026-05-03: +- 4 pass, 0 fail, 10 assertions. +- On `devflow/hojo`, the adapted Effect-path hook test has 2 pass, 0 fail, + 4 assertions. + +Session start PR `#15224`: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun test test/plugin/session-start.test.ts +``` + +Observed result on 2026-05-03: +- On `devflow/hojo`, the adapted Effect-path hook test has 1 pass, 0 fail, + 2 assertions. +- The test verifies plugin-provided context is injected into the first session + model call and not injected into a later model call in the same session. + +Parent agent context PR `#15412`: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun test test/plugin/parent-agent.test.ts +``` + +Observed result on 2026-05-03: +- 5 pass, 0 fail, 5 assertions. + +Permission ask PR `#19470`: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun test test/permission/next.test.ts +``` + +Observed result on 2026-05-03: +- 76 pass, 1 fail, 108 assertions. +- Failing test: `permission requests stay isolated by directory`. +- This PR must not be integrated into `devflow/hojo` until the failure is + understood or fixed. + +## Root Commands + +Root typecheck: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode +bun run typecheck +``` + +Root test command is intentionally not useful: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode +bun test +``` + +The root `package.json` script says `test: echo 'do not run tests from root' && +exit 1`. Use package-level or file-level tests instead. + +## Git Hygiene + +Before switching branches or integrating PRs: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode +``` + +Current policy: +- `docs/devflow/opencode-port/` is our tracking documentation and should be + carried on the devflow fork branch. +- Generated lockfile changes from dependency install should be reviewed + separately and not mixed into a PR absorption without an entry in + `verification-log.md` and `opencode-fork-prs.md`. diff --git a/docs/devflow/opencode-port/experiment-runbook.md b/docs/devflow/opencode-port/experiment-runbook.md new file mode 100644 index 000000000000..4d252668b8bf --- /dev/null +++ b/docs/devflow/opencode-port/experiment-runbook.md @@ -0,0 +1,135 @@ +# OpenCode Experiment Runbook + +Use this runbook to validate devflow experiments through the local OpenCode fork. +Keep runs isolated from the real `~/.opencode` config unless the task explicitly +asks for a real cutover rehearsal. + +## Paths + +- OpenCode fork: `/Users/jvanzyl/js/jopen/hojo-opencode` +- Devflow repo: `/Users/jvanzyl/js/ig/devflow2` +- Experiment suite: `/Users/jvanzyl/js/ig/devflow-experiments` +- Native OpenCode binary after local build: `/Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode/dist/opencode-darwin-arm64/bin/opencode` + +## Build The Binary + +Build a native single-platform binary from the OpenCode package: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode +bun run script/build.ts --single --skip-install +``` + +Do not use root `bun test`; the root package intentionally refuses it. Use +package-level checks documented in `build-commands.md`. + +## Prepare One Fresh Experiment + +Create a new project copy and a fresh OpenCode config root: + +```bash +ROOT=$(mktemp -d "/tmp/devflow-built-exp-root.XXXXXX") +PARENT=$(mktemp -d "/tmp/devflow-built-exp-project.XXXXXX") +PROJECT="$PARENT/0018-refactor-extract-method" + +cp -R "/Users/jvanzyl/js/ig/devflow-experiments/templates/0018-refactor-extract-method" "$PROJECT" +/Users/jvanzyl/js/ig/devflow2/install.sh --target opencode --root "$ROOT" + +git init "$PROJECT" +git -C "$PROJECT" add . +git -C "$PROJECT" commit -m "Initial experiment template" + +printf 'ROOT=%s\nPROJECT=%s\n' "$ROOT" "$PROJECT" +``` + +Keep the printed `ROOT` and `PROJECT`; all following commands use them. + +## Run The Experiment + +Use the experiment prompt after the `---` separator in `EXPERIMENT-PROMPT.md`. +For `0018-refactor-extract-method`, the prompt is: + +```text +Refactor the OrderProcessor class per the proposal at docs/work/20260428-refactor-extract-method/02-proposal.md. Existing tests must continue to pass. + +/flow --autonomous --start-phase 04 --path refactor +``` + +Run OpenCode with the isolated config: + +```bash +OPENCODE_CONFIG_DIR="$ROOT" /Users/jvanzyl/js/jopen/hojo-opencode/packages/opencode/dist/opencode-darwin-arm64/bin/opencode run --format json --dangerously-skip-permissions --dir "$PROJECT" '' +``` + +If the run stops early but makes progress, resume with a short continuation +prompt against the same `PROJECT` and `ROOT`. Do not modify generated work by +hand unless the task is to debug the harness itself. + +When dispatching `devflow-tester`, include explicit write scope. Valid test +write paths are Java tests under `src/test/java/...` and Python tests under +`tests/...` or `src/test/python/...`. If the experiment starts after phase 00, +also verify `.devflow/phase.json` contains `initiative_path`; otherwise the +criteria coverage gate blocks even valid tester writes. + +## Monitor Progress + +Do not wait for a long `opencode run` timeout to learn whether an experiment is +stalled. Poll the run while it is active: + +```bash +git -C "$PROJECT" status --short --branch +PGPASSWORD=devflow psql -h localhost -p 15433 -U devflow -d devflow -c "SELECT timestamp,sessionid,agenttype,toolname,filepath,command,blocked,blockreason FROM devflow_tool_calls WHERE timestamp > now() - interval '10 minutes' ORDER BY timestamp DESC LIMIT 60;" +PGPASSWORD=devflow psql -h localhost -p 15433 -U devflow -d devflow -c "SELECT sessionid,harness,sessiontype,status,startedat FROM devflow_sessions WHERE startedat > now() - interval '10 minutes' ORDER BY startedat DESC LIMIT 20;" +``` + +Treat these as stop-and-diagnose signals: + +- Any `blocked=true` row. +- No new telemetry for more than a minute while an OpenCode process is alive. +- No git diff after a tester/programmer write task claims progress. +- `mvn test` still reports the original test count after a RED test task. +- A commit made outside OpenCode during an experiment. External commits bypass + devflow hooks, so `.devflow/commit-order-state.json` may not record + `testCommitted:true` and later programmer writes can be falsely blocked. + Exception: a final telemetry-only commit may be needed after all verification, + because committing `.devflow/workflow-state.json` through OpenCode records one + more commit event and dirties that same file again. + +## Verify The Result + +Run these from the experiment project: + +```bash +mvn package +bash bin/measure-adherence.sh +git status --short +git log --oneline --decorate -10 +``` + +Expected success shape for `0018-refactor-extract-method`: + +- `mvn package` reports `BUILD SUCCESS`. +- The scorer reports `Score: 6/6 (100%)`. +- `git status --short` is clean. +- The history contains separate test/refactor/finalization commits. + +## Full Suite Notes + +Run one clean single-experiment smoke before attempting the full suite. When the +single smoke is green, use the same isolated-config pattern for additional +templates under `/Users/jvanzyl/js/ig/devflow-experiments/templates/`. + +The stock experiment harness in `bin/run-experiment.py` was originally written +for Claude Code. Until it has an explicit OpenCode mode, prefer manual isolated +template runs or update the harness in a separate, reviewed change. + +## Evidence To Capture + +Record results in `verification-log.md` with: + +- OpenCode branch and binary path. +- Devflow commit used for install. +- Experiment template name. +- `ROOT` and `PROJECT` paths if they still exist. +- Final Maven/scorer/status results. +- Any resumed prompts or harness defects discovered. diff --git a/docs/devflow/opencode-port/gap-log.md b/docs/devflow/opencode-port/gap-log.md new file mode 100644 index 000000000000..3fb501a452ad --- /dev/null +++ b/docs/devflow/opencode-port/gap-log.md @@ -0,0 +1,25 @@ +# OpenCode Port Gap Log + +This file records every known gap between current Claude Code devflow behavior +and OpenCode behavior. Do not close a gap without evidence. + +| ID | Gap | Severity | Source | Closing Work | Upstream PR | Status | Evidence | +|---|---|---|---|---|---|---|---| +| G1 | OpenCode install rejects Claude-style agent `tools` array | P0 | Disposable `OPENCODE_CONFIG_DIR` install test | Agent frontmatter transform | None | Open | TBD | +| G2 | Mandatory rules are not guaranteed through `~/.claude/rules/` | P0 | OpenCode docs | `opencode.json` `instructions` plus `AGENTS.md` | Optional `#18903`/`#10090` | Open | TBD | +| G3 | OpenCode target currently installs only rules and agents | P0 | `manifests/install-modules.json` | Add skills, commands, plugin adapter, config modules | None | Open | TBD | +| G4 | Claude `hooks/hooks.json` is not consumed by OpenCode | P0 | OpenCode plugin model | Build JS/TS adapter that invokes canonical Python hooks | `#22654`/`#20053` behavior supports adapter substrate | Partial | `tool.execute.before` now exposes `ask()` and propagates mutated args before execution. The devflow adapter still needs to be implemented. | +| G5 | `/loop` depends on Claude `Stop` hook | P1 | `hooks/stop-hook.sh` | Use OpenCode `session.stopping` | `#16598` | Partial | `session.stopping` integrated on `devflow/hojo`; `bun test test/plugin/session-stopping.test.ts`, plugin typecheck, and opencode typecheck pass. Gap remains open until `/loop` command/config adapter uses it. | +| G6 | Telemetry scripts must record `harness=opencode` | P1 | Current telemetry behavior | Pass harness through adapter and telemetry scripts | None | Open | TBD | +| G7 | OpenCode `apply_patch` has no Claude `MultiEdit` equivalent payload | P1 | OpenCode tool model | Parse patch paths in adapter | None | Open | TBD | +| G8 | Subagent identity/parent context is insufficient for policy | P1 | Hook payload needs | Absorb parent agent context PR | `#15412` | Partial | `#15412` integrated on `devflow/hojo`; `bun test test/plugin/parent-agent.test.ts`, plugin typecheck, and opencode typecheck pass. Gap remains open until the devflow adapter consumes the context. | +| G9 | SessionStart parity is missing for startup telemetry/context | P2 | Hook lifecycle gap | Use OpenCode `session.start` and adapter output context | `#15224` | Partial | `session.start` integrated on `devflow/hojo`; `bun test test/plugin/session-start.test.ts`, affected plugin tests, plugin typecheck/build, and opencode typecheck pass. Gap remains open until the devflow adapter consumes it. | +| G10 | Post-tool hook timing for MCP/plugin tools may miss final output | P2 | Hook timing gap | Absorb after-MCP PR | `#21150` | Partial | Local Effect-path adaptation makes MCP `tool.execute.after` run after output assembly and lets mutations affect returned output. Gap remains open until devflow telemetry adapter consumes it. | + +## Gap Closure Requirements + +A gap is closed only when all are true: +- The implementation or fork PR is applied. +- A test or manual verification command proves the behavior. +- `parity-matrix.md` is updated. +- `verification-log.md` records the evidence. diff --git a/docs/devflow/opencode-port/handoff-opencode-fork.md b/docs/devflow/opencode-port/handoff-opencode-fork.md new file mode 100644 index 000000000000..0e778308f720 --- /dev/null +++ b/docs/devflow/opencode-port/handoff-opencode-fork.md @@ -0,0 +1,109 @@ +# Handoff: OpenCode Fork Track + +## Objective + +Work on the OpenCode fork track for devflow. OpenCode is the long-term target +harness. Claude Code remains supported, but new compatibility work should make +OpenCode capable of enforcing the same workflow boundaries. + +## Working Directory + +Use this repository for OpenCode work: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode +``` + +Current local state: +- Cloned from `https://github.com/anomalyco/opencode`. +- Current branch after clone: `dev`. +- Upstream default branch: `dev`. +- Current base SHA: `387220f368ca3a31d94b4be3937d9d825ebd888c`. +- Local branches created: `devflow/base`, `devflow/hojo`. +- No `jvanzyl/opencode` GitHub fork existed at initialization time. + +Do not push until a fork remote is intentionally created or selected. + +## Devflow Planning Docs + +Use these docs in the devflow repo as the source of truth: + +```bash +cd /Users/jvanzyl/js/jopen/hojo-opencode +docs/devflow/opencode-port/02-proposal.md +docs/devflow/opencode-port/03-plan.md +docs/devflow/opencode-port/opencode-fork-prs.md +docs/devflow/opencode-port/parity-matrix.md +docs/devflow/opencode-port/gap-log.md +docs/devflow/opencode-port/rebase-log.md +docs/devflow/opencode-port/verification-log.md +docs/devflow/opencode-port/build-commands.md +docs/devflow/opencode-port/experiment-runbook.md +``` + +Update the docs before claiming any compatibility improvement. +Use `build-commands.md` for exact package-level build and test commands. +Use `experiment-runbook.md` for isolated OpenCode/devflow experiment runs. + +## First Fork Task + +Start by establishing the fork tracking mechanics, not by applying patches. + +1. In `/Users/jvanzyl/js/jopen/hojo-opencode`, verify clean status: + + ```bash + git status --short --branch + git branch --list 'devflow/*' + ``` + +2. Decide whether to create a GitHub fork remote. If yes, create it explicitly + and document the remote in `opencode-fork-prs.md`. + +3. For each required PR in `opencode-fork-prs.md`, capture current upstream + state with: + + ```bash + gh pr view --repo anomalyco/opencode --json number,state,mergedAt,closedAt,headRefOid,title,url + ``` + +4. Do not apply any PR until its tracker row has upstream state, head SHA, + devflow gap, expected validation, and risk. + +## Required PR Stack + +Apply in isolated branches first, then stack into `devflow/hojo`. + +1. Lifecycle hooks: `#15224`, `#16598`, `#23650`. +2. Hook context improvements: `#15412`, `#21773`. +3. Permission/tool hook correctness: `#19470`, `#22654`, `#20053`, `#21150`. +4. Optional rules loader: `#18903` first; evaluate `#10090` later. + +Required PRs for baseline parity: +- `#16598` session.stopping +- `#15412` parent agent context +- `#19470` permission.ask +- `#22654` ask in tool.execute.before +- `#20053` mutable tool args +- `#21150` post-MCP tool.execute.after timing + +## Documentation Discipline + +Every absorbed PR must update: +- `opencode-fork-prs.md`: upstream state, upstream head, fork branch, fork commit, status. +- `gap-log.md`: gap status and closure evidence. +- `parity-matrix.md`: parity status for affected area. +- `verification-log.md`: commands/tests run. +- `rebase-log.md`: only when rebasing `devflow/hojo` onto upstream. + +If a PR merges upstream, remove the local patch at the next rebase and record +the drop in `rebase-log.md`. + +## Guardrails + +- Keep the patch stack small and explicit. +- Do not absorb broad unrelated OpenCode features. +- Do not make devflow depend on implicit `.claude/rules/` loading; mandatory + rules still come through `opencode.json` `instructions`. +- Do not duplicate devflow Python enforcement logic into OpenCode unless a hook + cannot be adapted. +- No parity claim without evidence in `verification-log.md`. diff --git a/docs/devflow/opencode-port/opencode-fork-prs.md b/docs/devflow/opencode-port/opencode-fork-prs.md new file mode 100644 index 000000000000..6e7ad9bea86c --- /dev/null +++ b/docs/devflow/opencode-port/opencode-fork-prs.md @@ -0,0 +1,74 @@ +# OpenCode Fork PR Tracker + +This file tracks every upstream OpenCode PR absorbed into the devflow OpenCode +fork. Do not apply or retain a fork patch unless it has an entry here. + +## Fork Branches + +| Branch | Purpose | Current SHA | Last Updated | +|---|---|---:|---| +| `upstream-dev` | Mirror of `anomalyco/opencode:dev` | `387220f368ca3a31d94b4be3937d9d825ebd888c` | 2026-05-03 | +| `devflow/base` | Rebase base for devflow patch stack | `387220f368ca3a31d94b4be3937d9d825ebd888c` | 2026-05-03 | +| `devflow/hojo` | Curated compatibility patch stack | Integrated through `session.start` local Effect-path adaptation | 2026-05-03 | +| `devflow/release` | Tested branch consumed by devflow users | TBD | TBD | + +Local clone: `/Users/jvanzyl/js/jopen/hojo-opencode`. + +Remote state: cloned from `https://github.com/anomalyco/opencode`. No +`jvanzyl/opencode` GitHub fork existed when this tracker was initialized. +Before pushing devflow branches, create or choose a fork remote explicitly. + +## Required PRs + +| PR | Title | Upstream State | Upstream Head | Fork Branch | Fork Commit | Devflow Gap Closed | Risk | Drop When | Status | +|---|---|---|---|---|---|---|---|---|---| +| `#16598` | `feat: add session.stopping hook for plugins` | OPEN | `e19f58e5458a9cdf91645c0adb6827b438af1329` | `devflow/pr-16598-session-stopping` | Local Effect-path adaptation on `devflow/hojo` | `/loop` continuation / Claude `Stop` parity | Medium: loop re-entry can create infinite loops if plugin state is wrong | Upstream merges and rebase includes it | Integrated | +| `#15412` | `feat(plugin): include parent agent context in hook inputs` | OPEN | `0b389890a6d3c8f0dc5aceb9a5427def1f2934fb` | `devflow/pr-15412-parent-agent-context` | `22441217f`, `c56adc7bf`, `a8df203ff` on `devflow/hojo` | Agent identity and parent-agent context for boundary enforcement | Medium: hook payload shape conflicts possible | Upstream merges and rebase includes it | Integrated | +| `#19470` | `feat(opencode): wire permission.ask plugin hook` | OPEN | `e115ed5ef6171f193e83441469631107f901e666` | `devflow/pr-19470-permission-ask` | `e115ed5ef6171f193e83441469631107f901e666` | Plugin participation in permission decisions | Medium: permission flow is security-sensitive | Upstream merges and rebase includes it | Failing Tests | +| `#22654` | `feat(plugin): expose ask() on tool.execute.before hook` | OPEN | `d6b78a2fa9d1d5c77c3419686057a7767cca151f` | TBD | `9a827917d` local adaptation on `devflow/hojo` | Interactive pre-tool enforcement path | Medium: depends on permission API shape | Upstream merges and rebase includes it | Integrated | +| `#20053` | `fix: Allow plugin hooks to mutate tool call args before context creation` | OPEN | `9803c23bdbce96aa25d822451a320e508f32a14b` | TBD | `9a827917d` local adaptation on `devflow/hojo` | Argument normalization before execution | High: touches tool execution path | Upstream merges and rebase includes it | Integrated | +| `#21150` | `fix(session): fire tool.execute.after hook after MCP output assembly` | OPEN | `5ed52021b4ba8e3358aeb315915230995e26e682` | TBD | `9a827917d` local adaptation on `devflow/hojo` | Accurate post-tool telemetry | Low/Medium: timing-sensitive | Upstream merges and rebase includes it | Integrated | + +## Strong Candidates + +| PR | Title | Upstream State | Upstream Head | Fork Branch | Fork Commit | Devflow Gap Closed | Risk | Decision | Status | +|---|---|---|---|---|---|---|---|---|---| +| `#15224` | `feat(plugin): add session.start hook for session initialization` | OPEN | `d71089b5c41cd86369a11874b7c37a4856acd1a4` | `devflow/pr-15224-session-start` | Local Effect-path adaptation on `devflow/hojo` | SessionStart-like event | Low/Medium | Upstream merges and rebase includes it | Integrated | +| `#23650` | `feat: add session.turn.completed bus event for plugin hooks` | OPEN | `4c644353f4d350672b1f86b2718cbbf04730a145` | `devflow/pr-23650-turn-completed` | `9ca1de490` on `devflow/hojo` | Per-turn telemetry and future review UX | Low | Upstream merges and rebase includes it | Integrated | +| `#19519` | `feat: allow tool.execute.after hooks to inject AI-visible messages` | OPEN | `4e19d1474fd793e1e79876e16e9a5cc84fdd9b24` | TBD | TBD | Hook feedback visible to agent | Medium | Defer until core adapter works | Not Applied | +| `#21773` | `feat(bash): expand shell.env hook context with messageID and agent` | OPEN | `ded5a9bb03096d07e56c6c45b2305b5c58aba3dc` | TBD | TBD | Session/agent-aware hook subprocess env | Low | Absorb with context improvements | Not Applied | +| `#21776` | `feat(plugin): bash.commands hook for CLI command timeout exemption` | OPEN | `581e483d67f25649335914f80685429e88efeb9b` | TBD | TBD | Long-running devflow helper CLI support | Low | Defer until needed | Not Applied | +| `#17517` | `fix: await plugin event hooks and handle errors in database effects` | OPEN | `7afb805f5bc121699282fcacc9658d4f6b802067` | TBD | TBD | Plugin reliability | Medium | Absorb if clean | Not Applied | + +## Rules Compatibility Candidates + +| PR | Title | Upstream State | Upstream Head | Fork Branch | Fork Commit | Devflow Gap Closed | Decision | Status | +|---|---|---|---|---|---|---|---|---| +| `#18903` | `feat(instructions): load .opencode/rules/*.{md,mdc}` | OPEN | `d45d0a27b37138039ace946b8be33882791374fd` | TBD | TBD | Native `.opencode/rules` loading | Prefer small rules loader if needed | Not Applied | +| `#10090` | `feat(smart-rules): add context-aware rule injection system with Claude Code compatibility` | OPEN | `1c59cfc3a7a3f5bade7fe368aae0853d8a34aae2` | TBD | TBD | Context-aware `.claude/rules` compatibility | Evaluate after baseline; do not depend on it | Not Applied | + +## Reference Only + +| PR | Title | Reason Not Absorbed | +|---|---|---| +| `#11525` | `feat: Add complete native hook system with all 12 Claude Code hooks` | Closed, non-blocking hooks. Devflow needs blocking enforcement. | +| `#9272` | `feat(hook): session.before.idle` | Similar to `#16598`; only absorb if it composes cleanly. | +| `#19453` | `fix(opencode): add permission.ask plugin hook back` | Overlaps with `#19470`; choose one. | +| `#20009` | `fix(opencode): allow tool hooks to replace call arguments` | Overlaps with `#20053`; choose one after review. | +| `#6990` | `feat: add .claude/commands/ compatibility for command discovery` | Closed and includes unresolved `allowed-tools` translation work. Use as reference only. | + +## Monitoring Command + +For each tracked PR: + +```bash +gh pr view --repo anomalyco/opencode --json number,state,mergedAt,closedAt,headRefOid,title,url +``` + +Update this file whenever upstream state or head SHA changes. + +## Local Adaptation Notes + +- `#15224`, `#16598`, `#22654`, `#20053`, and `#21150` target older prompt-loop shapes or lack the parent-agent context already present on `devflow/hojo`. Prefer the local Effect-path adaptations over direct cherry-picks unless upstream converges on the same current architecture. +- `#19470` remains excluded because its isolated branch has a failing permission isolation test. +- `#15224` direct cherry-pick created a large `packages/opencode/src/session/prompt.ts` conflict. The local adaptation adds the plugin hook type, triggers it before the first normal session model call, and prepends returned context to the system prompt. diff --git a/docs/devflow/opencode-port/parity-matrix.md b/docs/devflow/opencode-port/parity-matrix.md new file mode 100644 index 000000000000..202101fa701e --- /dev/null +++ b/docs/devflow/opencode-port/parity-matrix.md @@ -0,0 +1,39 @@ +# Claude Code to OpenCode Parity Matrix + +Status values: `Not Started`, `Partial`, `Blocked`, `Parity`, `Not Applicable`. + +| Area | Claude Code Behavior | OpenCode Target Behavior | Required OpenCode/Fork Support | Current Status | Evidence | +|---|---|---|---|---|---| +| Mandatory rules | Rules installed and loaded through Claude/plugin context | Rules explicitly listed in `opencode.json` `instructions`; fail-closed `AGENTS.md` | None | Not Started | TBD | +| Agents | Markdown agents with Claude `tools` arrays | Markdown agents with OpenCode `permission` objects | None | Not Started | Disposable install currently fails on Claude `tools` array | +| Skills | `skills//SKILL.md` | Native OpenCode skills | None | Not Started | TBD | +| `/flow` command | Claude command markdown | OpenCode command markdown with converted frontmatter | None | Not Started | TBD | +| `/loop` command | Claude `Stop` hook re-enters loop | OpenCode `session.stopping` injects follow-up message | PR `#16598` | Partial | `session.stopping` hook integrated on `devflow/hojo`; targeted test and package typechecks pass. Command/frontmatter adapter work remains. | +| Pre-tool edit/write enforcement | Claude `PreToolUse` hook chain blocks exit 2 | OpenCode `tool.execute.before` adapter throws on exit 2 | Existing plugin hook plus adapter; `#22654`/`#20053` behavior | Partial | Hook input now exposes `ask()` and propagates mutated args before tool execution. Devflow adapter not implemented yet. | +| Pre-tool bash enforcement | Claude `PreToolUse Bash` hook chain blocks exit 2 | OpenCode `tool.execute.before` adapter throws on exit 2 | Existing plugin hook plus adapter; `#22654`/`#20053` behavior | Partial | Hook substrate is ready for blocking/interactive enforcement; adapter not implemented yet. | +| `apply_patch` enforcement | Claude uses `MultiEdit` | Adapter parses OpenCode `apply_patch` patch paths and applies write enforcement | None | Not Started | TBD | +| Post-tool telemetry | Claude `PostToolUse` hooks | OpenCode `tool.execute.after` adapter | PR `#21150` improves MCP timing | Partial | MCP `tool.execute.after` now receives assembled final output and can mutate returned output. Devflow telemetry adapter not implemented yet. | +| Session start telemetry | Claude `SessionStart` | OpenCode `session.start` | PR `#15224` | Partial | `session.start` hook integrated on `devflow/hojo`; targeted test verifies first-call context injection. Devflow adapter not implemented yet. | +| Compaction reinforcement | Claude compact matcher reinjects rules | OpenCode compaction plugin hook | Existing `experimental.session.compacting` | Not Started | TBD | +| Subagent start telemetry | Claude `SubagentStart` | OpenCode task/tool/session context | PR `#15412` | Partial | `#15412` integrated on `devflow/hojo`; `bun test test/plugin/parent-agent.test.ts` and package typechecks pass. Adapter work remains. | +| Subagent stop telemetry | Claude `SubagentStop` | OpenCode child session/task completion event | Needs validation | Not Started | TBD | +| Orchestrator lockdown | Hook blocks main-session write/bash | OpenCode primary agent permissions plus hook adapter | PR `#15412` helps identity | Partial | Parent-agent context is now available to hooks; enforcement adapter not implemented yet. | +| Worktree isolation | Hook blocks source writes in main worktree | Same hook via adapter; optional native session cwd support | None initially | Not Started | TBD | +| TDD commit ordering | Bash/write hooks enforce test-before-impl commits | Same Python hook via adapter | None | Not Started | TBD | +| Test-before-commit | Bash hook enforces tests passed after changes | Same Python hook via adapter | None | Not Started | TBD | +| Artifact sequencing | Bash commit hook validates docs/work artifacts | Same Python hook via adapter | None | Not Started | TBD | +| Initiative content checks | Bash commit hook validates artifact content | Same Python hook via adapter | None | Not Started | TBD | +| Criteria coverage gate | Write hooks block under-reviewed implementation | Same Python hook via adapter | None | Not Started | TBD | + +## Parity Levels + +| Level | Meaning | +|---|---| +| L0 | OpenCode install does not load or rejects config | +| L1 | Rules, agents, skills, and `/flow` load | +| L2 | Blocking pre-tool enforcement works for write/edit/bash | +| L3 | Telemetry and lifecycle hooks work | +| L4 | `/loop` and subagent lifecycle parity work on fork | +| L5 | OpenCode is primary target with Claude compatibility maintained | + +Current target milestone: L2 before attempting `/loop` parity. diff --git a/docs/devflow/opencode-port/rebase-log.md b/docs/devflow/opencode-port/rebase-log.md new file mode 100644 index 000000000000..c52875a74d9c --- /dev/null +++ b/docs/devflow/opencode-port/rebase-log.md @@ -0,0 +1,24 @@ +# OpenCode Fork Rebase Log + +Record every rebase of the devflow OpenCode fork onto upstream OpenCode. + +## Template + +### YYYY-MM-DD: Rebase `devflow/hojo` onto upstream `dev` + +| Field | Value | +|---|---| +| Previous upstream SHA | TBD | +| New upstream SHA | TBD | +| Previous hojo SHA | TBD | +| New hojo SHA | TBD | +| Local patch count before | TBD | +| Local patch count after | TBD | +| PRs dropped because upstream merged | TBD | +| PRs retained | TBD | +| Conflicts | TBD | +| Tests run | TBD | +| Result | TBD | + +Notes: +- TBD diff --git a/docs/devflow/opencode-port/verification-log.md b/docs/devflow/opencode-port/verification-log.md new file mode 100644 index 000000000000..fabe1d330b1a --- /dev/null +++ b/docs/devflow/opencode-port/verification-log.md @@ -0,0 +1,71 @@ +# OpenCode Port Verification Log + +Record verification evidence here before claiming any parity milestone. + +## Baseline Commands + +| Date | Command | Result | Notes | +|---|---|---|---| +| 2026-05-03 | `brew install bun` | FAIL | Homebrew core did not provide `bun` in this setup. | +| 2026-05-03 | `brew install oven-sh/bun/bun` | PASS | Installed Bun 1.3.13 from official tap. | +| 2026-05-03 | `bun --version` | PASS | `1.3.13`. | +| 2026-05-03 | `bun install` on `devflow/pr-15224-session-start` | PASS | Installed dependencies. `bun.lock` gained missing workspace entries for PR demo package. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/pr-15224-session-start` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/pr-15224-session-start` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run build` in `packages/plugin` on `devflow/pr-15224-session-start` | PASS | `tsc` completed successfully. | +| 2026-05-03 | `bun run build` in `packages/opencode` on `devflow/pr-15224-session-start` | PASS | Built all target binaries successfully. | +| 2026-05-03 | `git cherry-pick 2661c24f0d593cd3844b199ecfc0a176a8e5e48d` onto `devflow/hojo` | FAIL | Large conflict in `packages/opencode/src/session/prompt.ts`; PR is based on older prompt architecture. Cherry-pick aborted. Treat `#15224` as requiring a fresh minimal implementation rather than direct absorption. | +| 2026-05-03 | `bun install` on `devflow/pr-16598-session-stopping` | PASS | Installed dependencies. Generated `bun.lock` drift for `ghostty-web`; drift was removed from worktree after testing. | +| 2026-05-03 | `bun test test/plugin/session-stopping.test.ts` in `packages/opencode` on `devflow/pr-16598-session-stopping` | PASS | 4 tests passed, 0 failed, 10 assertions. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/pr-16598-session-stopping` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/pr-16598-session-stopping` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun install` on `devflow/pr-23650-turn-completed` | PASS | Installed dependencies. Generated untracked `packages/opencode/src/provider/models-snapshot.ts`; generated artifact was removed from worktree after testing. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/pr-23650-turn-completed` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/pr-23650-turn-completed` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun install` on `devflow/pr-15412-parent-agent-context` | PASS | Installed dependencies with no tracked worktree changes. | +| 2026-05-03 | `bun test test/plugin/parent-agent.test.ts` in `packages/opencode` on `devflow/pr-15412-parent-agent-context` | PASS | 5 tests passed, 0 failed, 5 assertions. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/pr-15412-parent-agent-context` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/pr-15412-parent-agent-context` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun install` on `devflow/pr-19470-permission-ask` | PASS | Installed dependencies. Generated `ghostty-web` lockfile drift; drift was removed from worktree after testing. | +| 2026-05-03 | `bun test test/permission/next.test.ts` in `packages/opencode` on `devflow/pr-19470-permission-ask` | FAIL | 76 pass, 1 fail, 108 assertions. Failing test: `permission requests stay isolated by directory`; failure raises `PermissionRejectedError`. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/pr-19470-permission-ask` | PASS | `tsgo --noEmit` completed successfully despite failing behavioral test. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/pr-19470-permission-ask` | PASS | `tsgo --noEmit` completed successfully despite failing behavioral test. | +| 2026-05-03 | `git cherry-pick eb815acc0805ad48590638ea6ebf230ba3e8721f d8a368a820f06292c6c54f207c4d5d6956bcda60` onto `devflow/hojo` | PASS | Integrated parent-agent hook input commit and session.turn.completed event commit. | +| 2026-05-03 | `git cherry-pick e64832dac680fb37c4cff88a55548c7d593d287f 890b87c4dc69b651722fb6ca2b25d4e7f495b86e` onto `devflow/hojo` | PASS | Resolved `packages/opencode/src/session/prompt.ts` conflict by preserving current shell runner flow and adding `parentAgent`, `messageID`, `agent`, and `parentAgent` hook context fields. | +| 2026-05-03 | `bun test test/plugin/parent-agent.test.ts` in `packages/opencode` on `devflow/hojo` before dependency refresh | FAIL | Stale `node_modules` resolved `effect@4.0.0-beta.42`; current lockfile pins `effect@4.0.0-beta.59`. | +| 2026-05-03 | `bun install` on `devflow/hojo` | PASS | Refreshed dependencies from `bun.lock`; no tracked lockfile change remained. | +| 2026-05-03 | `bun test test/plugin/parent-agent.test.ts` in `packages/opencode` on `devflow/hojo` | PASS | 5 tests passed, 0 failed, 5 assertions. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/hojo` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/hojo` before schema fix | FAIL | `Session.Event.TurnCompleted` used a Zod object where `BusEvent.define` expects Effect `Schema`. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/hojo` after schema fix | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `git cherry-pick 79739438c 7f67ccee1 daf9c9614 0296cc9ec bbd1c13e4 59c7a932b 634baac71 c233a989d e19f58e54` onto `devflow/hojo` | FAIL | First commit conflicted in `packages/opencode/src/session/prompt.ts` because the PR targets the older async prompt loop; cherry-pick was aborted and replaced with a minimal Effect-path adaptation. | +| 2026-05-03 | `bun test test/plugin/session-stopping.test.ts` in `packages/opencode` on `devflow/hojo` | PASS | 2 tests passed, 0 failed, 4 assertions. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` on `devflow/hojo` after `session.stopping` adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` on `devflow/hojo` after `session.stopping` adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | Final narrow validation on `devflow/hojo`: `bun test test/plugin/parent-agent.test.ts` in `packages/opencode` | PASS | 5 tests passed, 0 failed, 5 assertions. | +| 2026-05-03 | Final narrow validation on `devflow/hojo`: `bun test test/plugin/session-stopping.test.ts` in `packages/opencode` | PASS | 2 tests passed, 0 failed, 4 assertions. | +| 2026-05-03 | Final narrow validation on `devflow/hojo`: `bun run typecheck` in `packages/plugin` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | Final narrow validation on `devflow/hojo`: `bun run build` in `packages/plugin` | PASS | `tsc` completed successfully. | +| 2026-05-03 | Final narrow validation on `devflow/hojo`: `bun run typecheck` in `packages/opencode` | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `gh pr diff 22654`, `gh pr diff 20053`, and `gh pr diff 21150` | PASS | Behavior was applicable but diffs target older/currently divergent prompt code. Integrated as local Effect-path adaptation in `9a827917d`. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` after tool hook adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` after tool hook adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun test test/plugin/parent-agent.test.ts test/plugin/session-stopping.test.ts` in `packages/opencode` after tool hook adaptation | PASS | 7 tests passed, 0 failed, 9 assertions. | +| 2026-05-03 | `bun test test/plugin/session-start.test.ts` in `packages/opencode` after `session.start` adaptation | PASS | 1 test passed, 0 failed, 2 assertions. Verifies returned context is injected on the first session model call and not on a later model call in the same session. | +| 2026-05-03 | `bun test test/plugin/parent-agent.test.ts test/plugin/session-stopping.test.ts test/plugin/session-start.test.ts` in `packages/opencode` after `session.start` adaptation | PASS | 8 tests passed, 0 failed, 11 assertions. | +| 2026-05-03 | `bun run typecheck` in `packages/opencode` after `session.start` adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run typecheck` in `packages/plugin` after `session.start` adaptation | PASS | `tsgo --noEmit` completed successfully. | +| 2026-05-03 | `bun run build` in `packages/plugin` after `session.start` adaptation | PASS | `tsc` completed successfully. | +| TBD | `PYTHONPATH=src python3 -m pytest` | TBD | Current hook/unit baseline | +| TBD | `./install.sh --target claude --root /tmp/devflow-claude-test install` | TBD | Claude install regression | +| TBD | `./install.sh --target opencode --root /tmp/devflow-opencode-test install` | TBD | OpenCode install | +| TBD | `OPENCODE_CONFIG_DIR=/tmp/devflow-opencode-test opencode agent list` | TBD | OpenCode agent config validation | + +## Milestone Evidence + +| Milestone | Evidence | Status | +|---|---|---| +| L1: Rules/agents/skills/flow load | TBD | Not Started | +| L2: Blocking enforcement works | TBD | Not Started | +| L3: Telemetry/lifecycle works | TBD | Not Started | +| L4: Loop/subagent parity works | TBD | Not Started | diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index a05b273e4489..e9cb2f9047f1 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -302,6 +302,11 @@ export const RunCommand = effectCmd({ type: "boolean", describe: "auto-approve permissions that are not explicitly denied (dangerous!)", default: false, + }) + .option("yolo", { + type: "boolean", + describe: "alias for --dangerously-skip-permissions", + default: false, }), handler: Effect.fn("Cli.run")(function* (args) { const agentSvc = yield* Agent.Service @@ -441,10 +446,18 @@ export const RunCommand = effectCmd({ const events = await sdk.event.subscribe() let error: string | undefined - async function loop() { + async function loop(runSessionID: string) { const toggles = new Map() + const runSessionIDs = new Set([runSessionID]) for await (const event of events.stream) { + if (event.type === "session.created" || event.type === "session.updated") { + const info = event.properties.info + if (event.properties.sessionID && info.parentID && runSessionIDs.has(info.parentID)) { + runSessionIDs.add(event.properties.sessionID) + } + } + if ( event.type === "message.updated" && event.properties.info.role === "assistant" && @@ -459,7 +472,7 @@ export const RunCommand = effectCmd({ if (event.type === "message.part.updated") { const part = event.properties.part - if (part.sessionID !== sessionID) continue + if (part.sessionID !== runSessionID) continue if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) { if (emit("tool_use", { part })) continue @@ -523,7 +536,7 @@ export const RunCommand = effectCmd({ if (event.type === "session.error") { const props = event.properties - if (props.sessionID !== sessionID || !props.error) continue + if (props.sessionID !== runSessionID || !props.error) continue let err = String(props.error.name) if ("data" in props.error && props.error.data && "message" in props.error.data) { err = String(props.error.data.message) @@ -535,7 +548,7 @@ export const RunCommand = effectCmd({ if ( event.type === "session.status" && - event.properties.sessionID === sessionID && + event.properties.sessionID === runSessionID && event.properties.status.type === "idle" ) { break @@ -543,9 +556,9 @@ export const RunCommand = effectCmd({ if (event.type === "permission.asked") { const permission = event.properties - if (permission.sessionID !== sessionID) continue + if (!runSessionIDs.has(permission.sessionID)) continue - if (args["dangerously-skip-permissions"]) { + if (args["dangerously-skip-permissions"] || args.yolo) { await sdk.permission.reply({ requestID: permission.id, reply: "once", @@ -635,7 +648,7 @@ export const RunCommand = effectCmd({ } await share(sdk, sessionID) - loop().catch((e) => { + loop(sessionID).catch((e) => { console.error(e) process.exit(1) }) diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx index 8a229ffaba69..c7a5c20ca89f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/args.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/args.tsx @@ -7,6 +7,7 @@ export interface Args { continue?: boolean sessionID?: string fork?: boolean + yolo?: boolean } export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d43edd2dd5d7..ced5724dd803 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -85,6 +85,7 @@ import * as Model from "../../util/model" import { formatTranscript } from "../../util/transcript" import { UI } from "@/cli/ui.ts" import { useTuiConfig } from "../../context/tui-config" +import { useArgs } from "../../context/args" import { getScrollAcceleration } from "../../util/scroll" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" import { DialogGoUpsell } from "../../component/dialog-go-upsell" @@ -124,6 +125,7 @@ export function Session() { const event = useEvent() const project = useProject() const tuiConfig = useTuiConfig() + const args = useArgs() const kv = useKV() const { theme } = useTheme() const promptRef = usePromptRef() @@ -182,6 +184,19 @@ export function Session() { const toast = useToast() const sdk = useSDK() const editor = useEditorContext() + const autoApprovedPermissions = new Set() + + createEffect(() => { + if (!args.yolo) return + for (const request of permissions()) { + if (autoApprovedPermissions.has(request.id)) continue + autoApprovedPermissions.add(request.id) + void sdk.client.permission.reply({ + requestID: request.id, + reply: "once", + }) + } + }) createEffect(() => { const sessionID = route.sessionID diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 384b6fc4ff57..d9d6261e7599 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -112,6 +112,16 @@ export const TuiThreadCommand = cmd({ .option("agent", { type: "string", describe: "agent to use", + }) + .option("dangerously-skip-permissions", { + type: "boolean", + describe: "auto-approve permissions that are not explicitly denied (dangerous!)", + default: false, + }) + .option("yolo", { + type: "boolean", + describe: "alias for --dangerously-skip-permissions", + default: false, }), handler: async (args) => { // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it. @@ -247,6 +257,7 @@ export const TuiThreadCommand = cmd({ model: args.model, prompt, fork: args.fork, + yolo: args["dangerously-skip-permissions"] || args.yolo, }, }) } finally { diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 82fca570f4cf..629d24f43d68 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -22,8 +22,9 @@ export const files = Effect.fn("ConfigPaths.projectFiles")(function* ( export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) { const afs = yield* AppFileSystem.Service + const customConfigDir = Flag.OPENCODE_CONFIG_DIR return unique([ - Global.Path.config, + customConfigDir ?? Global.Path.config, ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? yield* afs.up({ targets: [".opencode"], @@ -31,12 +32,13 @@ export const directories = Effect.fn("ConfigPaths.directories")(function* (direc stop: worktree, }) : []), - ...(yield* afs.up({ - targets: [".opencode"], - start: Global.Path.home, - stop: Global.Path.home, - })), - ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), + ...(customConfigDir + ? [] + : yield* afs.up({ + targets: [".opencode"], + start: Global.Path.home, + stop: Global.Path.home, + })), ]) }) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index e76583f2d347..d0448fa1fa92 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -163,6 +163,7 @@ const live: Layer.Layer< { sessionID: input.sessionID, agent: input.agent.name, + parentAgent: input.user.parentAgent, model: input.model, provider: item, message: input.user, @@ -183,6 +184,7 @@ const live: Layer.Layer< { sessionID: input.sessionID, agent: input.agent.name, + parentAgent: input.user.parentAgent, model: input.model, provider: item, message: input.user, diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 5f97074b20c0..d52d6d085ac4 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -390,6 +390,7 @@ export const User = Schema.Struct({ }), ), agent: Schema.String, + parentAgent: Schema.optional(Schema.String), model: Schema.Struct({ providerID: ProviderID, modelID: ModelID, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0590fc38274c..ed444e284f9b 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -374,6 +374,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the processor: Pick bypassAgentCheck: boolean messages: MessageV2.WithParts[] + parentAgent?: string }) { using _ = log.time("resolveTools") const tools: Record = {} @@ -387,6 +388,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the callID: options.toolCallId, extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps }, agent: input.agent.name, + parentAgent: input.parentAgent, messages: input.messages, metadata: (val) => input.processor.updateToolCall(options.toolCallId, (match) => { @@ -425,13 +427,33 @@ NOTE: At any point in time through this workflow you should feel free to ask the execute(args, options) { return run.promise( Effect.gen(function* () { - const ctx = context(args, options) + const out = { args } + const ask = (req: any) => + run.promise( + permission + .ask({ + ...req, + sessionID: input.session.id, + tool: { messageID: input.processor.message.id, callID: options.toolCallId }, + ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []), + }) + .pipe(Effect.orDie), + ) yield* plugin.trigger( "tool.execute.before", - { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, - { args }, + { + tool: item.id, + sessionID: input.session.id, + messageID: input.processor.message.id, + callID: options.toolCallId, + agent: input.agent.name, + parentAgent: input.parentAgent, + ask, + }, + out, ) - const result = yield* item.execute(args, ctx) + const ctx = context(out.args, options) + const result = yield* item.execute(out.args, ctx) const output = { ...result, attachments: result.attachments?.map((attachment) => ({ @@ -443,7 +465,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the } yield* plugin.trigger( "tool.execute.after", - { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args }, + { + tool: item.id, + sessionID: ctx.sessionID, + messageID: input.processor.message.id, + callID: ctx.callID, + args: out.args, + agent: ctx.agent, + parentAgent: input.parentAgent, + }, output, ) if (options.abortSignal?.aborted) { @@ -466,15 +496,35 @@ NOTE: At any point in time through this workflow you should feel free to ask the item.execute = (args, opts) => run.promise( Effect.gen(function* () { - const ctx = context(args, opts) + const out = { args } + const ask = (req: any) => + run.promise( + permission + .ask({ + ...req, + sessionID: input.session.id, + tool: { messageID: input.processor.message.id, callID: opts.toolCallId }, + ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []), + }) + .pipe(Effect.orDie), + ) yield* plugin.trigger( "tool.execute.before", - { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId }, - { args }, + { + tool: key, + sessionID: input.session.id, + messageID: input.processor.message.id, + callID: opts.toolCallId, + agent: input.agent.name, + parentAgent: input.parentAgent, + ask, + }, + out, ) + const ctx = context(out.args, opts) const result: Awaited>> = yield* Effect.gen(function* () { yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }) - return yield* Effect.promise(() => execute(args, opts)) + return yield* Effect.promise(() => execute(out.args, opts)) }).pipe( Effect.withSpan("Tool.execute", { attributes: { @@ -485,12 +535,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the }, }), ) - yield* plugin.trigger( - "tool.execute.after", - { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args }, - result, - ) - const textParts: string[] = [] const attachments: Omit[] = [] for (const contentItem of result.content) { @@ -534,6 +578,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the })), content: result.content, } + yield* plugin.trigger( + "tool.execute.after", + { + tool: key, + sessionID: ctx.sessionID, + messageID: input.processor.message.id, + callID: opts.toolCallId, + args: out.args, + agent: ctx.agent, + parentAgent: input.parentAgent, + }, + output, + ) if (opts.abortSignal?.aborted) { yield* input.processor.completeToolCall(opts.toolCallId, output) } @@ -598,12 +655,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the subagent_type: task.agent, command: task.command, } - yield* plugin.trigger( - "tool.execute.before", - { tool: TaskTool.id, sessionID, callID: part.id }, - { args: taskArgs }, - ) - const taskAgent = yield* agents.get(task.agent) if (!taskAgent) { const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) @@ -612,12 +663,38 @@ NOTE: At any point in time through this workflow you should feel free to ask the yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() }) throw error } + const out = { args: taskArgs } + yield* plugin.trigger( + "tool.execute.before", + { + tool: TaskTool.id, + sessionID, + messageID: assistantMessage.id, + callID: part.id, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, + ask: (req: any) => + Effect.runPromise( + permission + .ask({ + ...req, + sessionID, + tool: { messageID: assistantMessage.id, callID: part.id }, + ruleset: Permission.merge(taskAgent.permission, session.permission ?? []), + }) + .pipe(Effect.orDie), + ), + }, + out, + ) + const nextTaskArgs = out.args let error: Error | undefined const taskAbort = new AbortController() const result = yield* taskTool - .execute(taskArgs, { + .execute(nextTaskArgs, { agent: task.agent, + parentAgent: lastUser.agent, messageID: assistantMessage.id, sessionID, abort: taskAbort.signal, @@ -679,7 +756,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the yield* plugin.trigger( "tool.execute.after", - { tool: TaskTool.id, sessionID, callID: part.id, args: taskArgs }, + { + tool: TaskTool.id, + sessionID, + messageID: assistantMessage.id, + callID: part.id, + args: nextTaskArgs, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, + }, result, ) @@ -692,7 +777,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the ...part, state: { status: "completed", - input: part.state.input, + input: nextTaskArgs, title: result.title, metadata: result.metadata, output: result.output, @@ -713,7 +798,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the end: Date.now(), }, metadata: part.state.status === "pending" ? undefined : part.state.metadata, - input: part.state.input, + input: nextTaskArgs, }, } satisfies MessageV2.ToolPart) } @@ -726,6 +811,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the role: "user", time: { created: Date.now() }, agent: lastUser.agent, + parentAgent: lastUser.parentAgent, model: lastUser.model, } yield* sessions.updateMessage(summaryUserMsg) @@ -764,6 +850,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the time: { created: Date.now() }, role: "user", agent: input.agent, + parentAgent: input.parentAgent, model: { providerID: model.providerID, modelID: model.modelID }, } yield* sessions.updateMessage(userMsg) @@ -857,7 +944,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the Effect.gen(function* () { const shellEnv = yield* plugin.trigger( "shell.env", - { cwd, sessionID: input.sessionID, callID: part.callID }, + { + cwd, + sessionID: input.sessionID, + messageID: part.messageID, + callID: part.callID, + agent: input.agent, + parentAgent: input.parentAgent, + }, { env: {} }, ) const cmd = ChildProcess.make(sh, args, { @@ -947,6 +1041,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the time: { created: Date.now() }, tools: input.tools, agent: ag.name, + parentAgent: input.parentAgent, model: { providerID: model.providerID, modelID: model.modelID, @@ -1442,6 +1537,22 @@ NOTE: At any point in time through this workflow you should feel free to ask the !hasToolCalls && lastUser.id < lastAssistant.id ) { + const hook = yield* plugin.trigger( + "session.stopping", + { sessionID }, + { stop: true, message: undefined as string | undefined }, + ) + if (!hook.stop && hook.message) { + yield* slog.info("session.stopping prevented stop", { sessionID }) + yield* createUserMessage({ + sessionID, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, + model: lastUser.model, + parts: [{ type: "text", text: hook.message }], + }) + continue + } yield* slog.info("exiting loop") break } @@ -1495,6 +1606,20 @@ NOTE: At any point in time through this workflow you should feel free to ask the const maxSteps = agent.steps ?? Infinity const isLastStep = step >= maxSteps msgs = yield* insertReminders({ messages: msgs, agent, session }) + const sessionStartContext = !msgs.some((m) => m.info.role === "assistant") + ? (yield* plugin.trigger( + "session.start", + { + sessionID, + directory: session.directory, + projectID: session.projectID, + parentSessionID: session.parentID, + agent: lastUser.agent, + parentAgent: lastUser.parentAgent, + }, + { context: [] as string[] }, + )).context + : [] const msg: MessageV2.Assistant = { id: MessageID.ascending(), @@ -1529,6 +1654,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the tools: lastUser.tools, processor: handle, bypassAgentCheck, + parentAgent: lastUser.parentAgent, messages: msgs, }) @@ -1570,7 +1696,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the instruction.system().pipe(Effect.orDie), MessageV2.toModelMessagesEffect(msgs, model), ]) - const system = [...env, ...instructions, ...(skills ? [skills] : [])] + const system = [...sessionStartContext, ...env, ...instructions, ...(skills ? [skills] : [])] const format = lastUser.format ?? { type: "text" as const } if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) const result = yield* handle.process({ @@ -1621,6 +1747,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the continue } + // Notify plugins that the turn is complete + yield* bus.publish(Session.Event.TurnCompleted, { sessionID }) + yield* compaction.prune({ sessionID }).pipe(Effect.ignore, Effect.forkIn(scope)) return yield* lastAssistant(sessionID) }, @@ -1808,6 +1937,7 @@ export const PromptInput = Schema.Struct({ messageID: Schema.optional(MessageID), model: Schema.optional(ModelRef), agent: Schema.optional(Schema.String), + parentAgent: Schema.optional(Schema.String), noReply: Schema.optional(Schema.Boolean), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({ description: @@ -1837,6 +1967,7 @@ export const ShellInput = Schema.Struct({ sessionID: SessionID, messageID: Schema.optional(MessageID), agent: Schema.String, + parentAgent: Schema.optional(Schema.String), model: Schema.optional(ModelRef), command: Schema.String, }).pipe(withStatics((s) => ({ zod: zod(s) }))) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 09d2c8c3c3ab..c3de23562610 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -3,7 +3,6 @@ import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Decimal } from "decimal.js" -import z from "zod" import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -342,6 +341,12 @@ export const Event = { error: MessageV2.Assistant.fields.error, }), ), + TurnCompleted: BusEvent.define( + "session.turn.completed", + Schema.Struct({ + sessionID: SessionID, + }), + ), } export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) { diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index d3ca542684de..7a8f4df1421a 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -411,7 +411,14 @@ export const ShellTool = Tool.define( const shellEnv = Effect.fn("ShellTool.shellEnv")(function* (ctx: Tool.Context, cwd: string) { const extra = yield* plugin.trigger( "shell.env", - { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, + { + cwd, + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + agent: ctx.agent, + parentAgent: ctx.parentAgent, + }, { env: {} }, ) return { diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index e58ea9b122cf..9aaa6270d9dd 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -140,6 +140,7 @@ export const TaskTool = Tool.define( providerID: model.providerID, }, agent: next.name, + parentAgent: ctx.agent, tools: { ...(canTodo ? {} : { todowrite: false }), ...(canTask ? {} : { task: false }), diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 4b9ea8774a40..70327d491a31 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -16,6 +16,7 @@ export type Context = { sessionID: SessionID messageID: MessageID agent: string + parentAgent?: string abort: AbortSignal callID?: string extra?: { [key: string]: unknown } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 0a522b085049..750050e5dd95 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -2320,6 +2320,46 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { }) }) +describe("OPENCODE_CONFIG_DIR isolation", () => { + test("does not scan home .opencode when custom config dir is set", async () => { + const originalConfigDir = process.env["OPENCODE_CONFIG_DIR"] + const originalTestHome = process.env["OPENCODE_TEST_HOME"] + + try { + await using homeTmp = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, ".opencode"), { recursive: true }) + }, + }) + await using configDirTmp = await tmpdir() + await using projectTmp = await tmpdir() + + process.env["OPENCODE_TEST_HOME"] = homeTmp.path + process.env["OPENCODE_CONFIG_DIR"] = configDirTmp.path + + await WithInstance.provide({ + directory: projectTmp.path, + fn: async () => { + const directories = await listDirs() + expect(directories).toContain(configDirTmp.path) + expect(directories).not.toContain(path.join(homeTmp.path, ".opencode")) + }, + }) + } finally { + if (originalConfigDir === undefined) { + delete process.env["OPENCODE_CONFIG_DIR"] + } else { + process.env["OPENCODE_CONFIG_DIR"] = originalConfigDir + } + if (originalTestHome === undefined) { + delete process.env["OPENCODE_TEST_HOME"] + } else { + process.env["OPENCODE_TEST_HOME"] = originalTestHome + } + } + }) +}) + describe("OPENCODE_CONFIG_CONTENT token substitution", () => { test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] diff --git a/packages/opencode/test/fixture/session-start-plugin.ts b/packages/opencode/test/fixture/session-start-plugin.ts new file mode 100644 index 000000000000..4d82697b9981 --- /dev/null +++ b/packages/opencode/test/fixture/session-start-plugin.ts @@ -0,0 +1,12 @@ +export default async () => ({ + "session.start": (input: { parentSessionID?: string; agent?: string; parentAgent?: string }, output: { context: string[] }) => { + output.context.push( + [ + "session start context", + `parent=${input.parentSessionID ?? "none"}`, + `agent=${input.agent ?? "none"}`, + `parentAgent=${input.parentAgent ?? "none"}`, + ].join(" "), + ) + }, +}) diff --git a/packages/opencode/test/plugin/parent-agent.test.ts b/packages/opencode/test/plugin/parent-agent.test.ts new file mode 100644 index 000000000000..001ce31dc1f0 --- /dev/null +++ b/packages/opencode/test/plugin/parent-agent.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test" +import { SessionPrompt } from "../../src/session/prompt" +import { MessageV2 } from "../../src/session/message-v2" +import { Identifier } from "../../src/id/id" + +const SESSION_ID = Identifier.descending("session") +const MESSAGE_ID = Identifier.ascending("message") + +describe("parentAgent hook input", () => { + test("PromptInput accepts parentAgent", () => { + const input = SessionPrompt.PromptInput.zod.parse({ + sessionID: SESSION_ID, + agent: "scout", + parentAgent: "coder", + parts: [{ type: "text", text: "test" }], + }) + expect(input.parentAgent).toBe("coder") + }) + + test("PromptInput parentAgent is optional", () => { + const input = SessionPrompt.PromptInput.zod.parse({ + sessionID: SESSION_ID, + agent: "scout", + parts: [{ type: "text", text: "test" }], + }) + expect(input.parentAgent).toBeUndefined() + }) + + test("ShellInput accepts parentAgent", () => { + const input = SessionPrompt.ShellInput.zod.parse({ + sessionID: SESSION_ID, + agent: "coder", + parentAgent: "orchestrator", + command: "ls", + }) + expect(input.parentAgent).toBe("orchestrator") + }) + + test("UserMessage stores parentAgent", () => { + const msg = MessageV2.User.zod.parse({ + id: MESSAGE_ID, + sessionID: SESSION_ID, + role: "user", + time: { created: Date.now() }, + agent: "scout", + parentAgent: "coder", + model: { providerID: "anthropic", modelID: "claude-sonnet-4-6" }, + }) + expect(msg.parentAgent).toBe("coder") + }) + + test("UserMessage parentAgent is optional", () => { + const msg = MessageV2.User.zod.parse({ + id: MESSAGE_ID, + sessionID: SESSION_ID, + role: "user", + time: { created: Date.now() }, + agent: "coder", + model: { providerID: "openai", modelID: "gpt-5.4" }, + }) + expect(msg.parentAgent).toBeUndefined() + }) +}) diff --git a/packages/opencode/test/plugin/session-start.test.ts b/packages/opencode/test/plugin/session-start.test.ts new file mode 100644 index 000000000000..5413818b30bc --- /dev/null +++ b/packages/opencode/test/plugin/session-start.test.ts @@ -0,0 +1,120 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import path from "path" +import { pathToFileURL } from "url" +import { Session } from "@/session/session" +import { SessionPrompt } from "../../src/session/prompt" +import { ModelID, ProviderID } from "../../src/provider/schema" +import { provideTmpdirServer } from "../fixture/fixture" +import { testEffect } from "../lib/effect" +import { TestLLMServer } from "../lib/llm-server" + +const ref = { + providerID: ProviderID.make("test"), + modelID: ModelID.make("test-model"), +} + +const plugin = pathToFileURL(path.join(__dirname, "../fixture/session-start-plugin.ts")).href + +const providerCfg = (url: string) => ({ + plugin: [plugin], + provider: { + test: { + name: "Test", + id: "test", + env: [], + npm: "@ai-sdk/openai-compatible", + models: { + "test-model": { + id: "test-model", + name: "Test Model", + attachment: false, + reasoning: false, + temperature: false, + tool_call: true, + release_date: "2025-01-01", + limit: { context: 100000, output: 10000 }, + cost: { input: 0, output: 0 }, + options: {}, + }, + }, + options: { + apiKey: "test-key", + baseURL: url, + }, + }, + }, +}) + +const it = testEffect( + Layer.mergeAll(TestLLMServer.layer, SessionPrompt.defaultLayer, Session.defaultLayer, CrossSpawnSpawner.defaultLayer), +) + +describe("session.start hook", () => { + it.live("injects context only before the first session model call", () => + provideTmpdirServer( + ({ llm }) => + Effect.gen(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ title: "session start test" }) + + yield* llm.text("first response") + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + model: ref, + parts: [{ type: "text", text: "first user" }], + }) + + yield* llm.text("second response") + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + model: ref, + parts: [{ type: "text", text: "second user" }], + }) + + const bodies = yield* llm.inputs + const first = bodies.find((body) => JSON.stringify(body).includes("first user")) + const second = bodies.find((body) => JSON.stringify(body).includes("second user")) + + expect(JSON.stringify(first)).toContain("session start context") + expect(JSON.stringify(first)).toContain("parent=none") + expect(JSON.stringify(first)).toContain("agent=build") + expect(JSON.stringify(second)).not.toContain("session start context") + }), + { git: true, config: providerCfg }, + ), + ) + + it.live("includes parent session and agent context for child sessions", () => + provideTmpdirServer( + ({ llm }) => + Effect.gen(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const parent = yield* sessions.create({ title: "parent session" }) + const child = yield* sessions.create({ title: "child session", parentID: parent.id }) + + yield* llm.text("child response") + yield* prompt.prompt({ + sessionID: child.id, + agent: "build", + parentAgent: "general", + model: ref, + parts: [{ type: "text", text: "child user" }], + }) + + const bodies = yield* llm.inputs + const body = bodies.find((item) => JSON.stringify(item).includes("child user")) + + expect(JSON.stringify(body)).toContain(`parent=${parent.id}`) + expect(JSON.stringify(body)).toContain("agent=build") + expect(JSON.stringify(body)).toContain("parentAgent=general") + }), + { git: true, config: providerCfg }, + ), + ) +}) diff --git a/packages/opencode/test/plugin/session-stopping.test.ts b/packages/opencode/test/plugin/session-stopping.test.ts new file mode 100644 index 000000000000..baa28f8d27bc --- /dev/null +++ b/packages/opencode/test/plugin/session-stopping.test.ts @@ -0,0 +1,90 @@ +import { afterAll, describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import path from "path" +import { pathToFileURL } from "url" +import { provideTmpdirInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS +process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" + +const { Plugin } = await import("../../src/plugin/index") +const it = testEffect(Layer.mergeAll(Plugin.defaultLayer, CrossSpawnSpawner.defaultLayer)) + +afterAll(() => { + if (disableDefault === undefined) { + delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS + return + } + process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault +}) + +function withProject(source: string, self: Effect.Effect) { + return provideTmpdirInstance((dir) => + Effect.gen(function* () { + const file = path.join(dir, "plugin.ts") + yield* Effect.all( + [ + Effect.promise(() => Bun.write(file, source)), + Effect.promise(() => + Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify( + { + $schema: "https://opencode.ai/config.json", + plugin: [pathToFileURL(file).href], + }, + null, + 2, + ), + ), + ), + ], + { discard: true, concurrency: 2 }, + ) + return yield* self + }), + ) +} + +const triggerStopping = Effect.fn("SessionStoppingTest.triggerStopping")(function* () { + const plugin = yield* Plugin.Service + return yield* plugin.trigger( + "session.stopping", + { sessionID: "test-session" }, + { stop: true, message: undefined as string | undefined }, + ) +}) + +describe("session.stopping hook", () => { + it.live("allows plugins to prevent stop and inject a message", () => + withProject( + [ + "export default async () => ({", + ' "session.stopping": (_input, output) => {', + " output.stop = false", + ' output.message = "workflow gate"', + " },", + "})", + "", + ].join("\n"), + Effect.gen(function* () { + const out = yield* triggerStopping() + expect(out.stop).toBe(false) + expect(out.message).toBe("workflow gate") + }), + ), + ) + + it.live("keeps stop true when no plugin handles it", () => + withProject( + ["export default async () => ({})", ""].join("\n"), + Effect.gen(function* () { + const out = yield* triggerStopping() + expect(out.stop).toBe(true) + expect(out.message).toBeUndefined() + }), + ), + ) +}) diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 2e96dd980179..7fa5d1c542a5 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -216,6 +216,13 @@ export type ProviderHook = { models?: (provider: ProviderV2, ctx: ProviderHookContext) => Promise> } +export type ToolExecuteBeforeAsk = (input: { + permission: string + patterns: string[] + always: string[] + metadata: Record +}) => Promise + /** @deprecated Use AuthOAuthResult instead. */ export type AuthOuathResult = AuthOAuthResult @@ -244,7 +251,14 @@ export interface Hooks { * Modify parameters sent to LLM */ "chat.params"?: ( - input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage }, + input: { + sessionID: string + agent: string + parentAgent?: string + model: Model + provider: ProviderContext + message: UserMessage + }, output: { temperature: number topP: number @@ -254,7 +268,14 @@ export interface Hooks { }, ) => Promise "chat.headers"?: ( - input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage }, + input: { + sessionID: string + agent: string + parentAgent?: string + model: Model + provider: ProviderContext + message: UserMessage + }, output: { headers: Record }, ) => Promise "permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise @@ -262,16 +283,54 @@ export interface Hooks { input: { command: string; sessionID: string; arguments: string }, output: { parts: Part[] }, ) => Promise + "session.stopping"?: ( + input: { sessionID: string }, + output: { stop: boolean; message?: string }, + ) => Promise + "session.start"?: ( + input: { + sessionID: string + directory: string + projectID: string + parentSessionID?: string + agent?: string + parentAgent?: string + }, + output: { context: string[] }, + ) => Promise "tool.execute.before"?: ( - input: { tool: string; sessionID: string; callID: string }, + input: { + tool: string + sessionID: string + messageID: string + callID: string + agent?: string + parentAgent?: string + ask: ToolExecuteBeforeAsk + }, output: { args: any }, ) => Promise "shell.env"?: ( - input: { cwd: string; sessionID?: string; callID?: string }, + input: { + cwd: string + sessionID?: string + messageID?: string + callID?: string + agent?: string + parentAgent?: string + }, output: { env: Record }, ) => Promise "tool.execute.after"?: ( - input: { tool: string; sessionID: string; callID: string; args: any }, + input: { + tool: string + sessionID: string + messageID: string + callID: string + args: any + agent?: string + parentAgent?: string + }, output: { title: string output: string diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 74c5844626ee..f40385c2c7de 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -3141,6 +3141,7 @@ export class Session2 extends HeyApiClient { modelID: string } agent?: string + parentAgent?: string noReply?: boolean tools?: { [key: string]: boolean @@ -3163,6 +3164,7 @@ export class Session2 extends HeyApiClient { { in: "body", key: "messageID" }, { in: "body", key: "model" }, { in: "body", key: "agent" }, + { in: "body", key: "parentAgent" }, { in: "body", key: "noReply" }, { in: "body", key: "tools" }, { in: "body", key: "format" }, @@ -3494,6 +3496,7 @@ export class Session2 extends HeyApiClient { modelID: string } agent?: string + parentAgent?: string noReply?: boolean tools?: { [key: string]: boolean @@ -3516,6 +3519,7 @@ export class Session2 extends HeyApiClient { { in: "body", key: "messageID" }, { in: "body", key: "model" }, { in: "body", key: "agent" }, + { in: "body", key: "parentAgent" }, { in: "body", key: "noReply" }, { in: "body", key: "tools" }, { in: "body", key: "format" }, @@ -3608,6 +3612,7 @@ export class Session2 extends HeyApiClient { workspace?: string messageID?: string agent?: string + parentAgent?: string model?: { providerID: string modelID: string @@ -3626,6 +3631,7 @@ export class Session2 extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "messageID" }, { in: "body", key: "agent" }, + { in: "body", key: "parentAgent" }, { in: "body", key: "model" }, { in: "body", key: "command" }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index caa3d4c76770..01c8eb1095f5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -391,6 +391,7 @@ export type UserMessage = { diffs: Array } agent: string + parentAgent?: string model: { providerID: string modelID: string @@ -5243,6 +5244,7 @@ export type SessionPromptData = { modelID: string } agent?: string + parentAgent?: string noReply?: boolean tools?: { [key: string]: boolean @@ -5569,6 +5571,7 @@ export type SessionPromptAsyncData = { modelID: string } agent?: string + parentAgent?: string noReply?: boolean tools?: { [key: string]: boolean @@ -5666,6 +5669,7 @@ export type SessionShellData = { body?: { messageID?: string agent: string + parentAgent?: string model?: { providerID: string modelID: string