## Scope extends beyond opencode — systematic `build_exec_args()` audit While investigating the opencode dispatch bug, I audited **all 28 integrations** and found that the root cause is not specific to opencode — it's a systematic architectural weakness in how `build_exec_args()` is inherited. ### Current state Only **4 out of 15 CLI-capable integrations** have verified, correct `build_exec_args()` implementations: | Integration | Base class | CLI invocation | Status | |---|---|---|---| | **Claude** | `SkillsIntegration` | `claude -p "prompt" --model X --output-format json` | ✅ Verified (`-p` is documented) | | **Gemini** | `TomlIntegration` | `gemini -p "prompt" -m X --output-format json` | ✅ Verified (`-p` and `-m` are documented) | | **Codex** | `SkillsIntegration` | `codex exec "prompt" --model X --json` | ✅ Custom override with `exec` subcommand | | **Copilot** | `IntegrationBase` | `copilot -p "prompt" --agent X --yolo --model X --output-format json` | ✅ Custom override with dynamic `--yolo` | The remaining **11 integrations with `requires_cli: True`** all inherit `build_exec_args()` from their base class without verification that the CLI actually supports the generated flags: | Integration | Inherited invocation | Risk | |---|---|---| | **OpenCode** | `opencode -p "prompt" --model X --output-format json` | ❌ **Confirmed broken** — should be `opencode run "prompt" -m X` | | **Amp** | `amp -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Auggie** | `auggie -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **CodeBuddy** | `codebuddy -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Forge** | `forge -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **iFlow** | `iflow -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Junie** | `junie -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Kiro CLI** | `kiro-cli -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Pi** | `pi -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **QoderCLI** | `qodercli -p "prompt" --model X --output-format json` | ⚠️ Unverified | | **Qwen** | `qwen -p "prompt" --model X --output-format json` | ⚠️ Unverified | Additionally, **3 SkillsIntegration-based CLIs** (Kimi, Shai, Vibe) and **1 TomlIntegration-based CLI** (Tabnine) inherit the same `-p` pattern without verification. There's also a data inconsistency: **Goose** has `requires_cli: True` but inherits from `YamlIntegration`, which doesn't override `build_exec_args()` — so it returns `None`, meaning CLI dispatch silently fails despite claiming CLI support. ### Root cause `MarkdownIntegration.build_exec_args()` hardcodes three assumptions that don't hold for all CLIs: 1. **Prompt mechanism**: `-p` flag (works for Claude/Gemini) vs. `run`/`exec` subcommand (opencode/codex) vs. other formats 2. **Model flag**: `--model` long form (most) vs. `-m` short form (Gemini, opencode) vs. not supported 3. **JSON output**: `--output-format json` (most) vs. `--json` (Codex) vs. `-f json` (opencode) vs. not supported Every deviation requires a full method override, which is easy to forget (as the opencode bug shows). ### Proposed fix: Declarative batch-mode attributes Instead of requiring each integration to override `build_exec_args()`, introduce class-level attributes on `IntegrationBase` that make the common variations declarative: ```python class IntegrationBase(ABC): exec_mode: str = "flag" # "flag" | "subcommand" | "none" exec_prompt_flag: str = "-p" # flag used when exec_mode="flag" exec_subcommand: str = "" # subcommand when exec_mode="subcommand" exec_model_flag: str = "--model" # "--model" or "-m" exec_json_args: tuple[str, ...] = ("--output-format", "json") # or ("--json",) or () ``` Then `build_exec_args()` on `IntegrationBase` uses these attributes, and most integrations need zero method overrides: ```python # OpencodeIntegration — just attributes, no method override class OpencodeIntegration(MarkdownIntegration): key = "opencode" exec_mode = "subcommand" exec_subcommand = "run" exec_model_flag = "-m" exec_json_args = () ... # CodexIntegration — just attributes, no method override class CodexIntegration(SkillsIntegration): key = "codex" exec_mode = "subcommand" exec_subcommand = "exec" exec_json_args = ("--json",) ... ``` Complex cases like Copilot (dynamic `--yolo` flag) still override `build_exec_args()` directly. This makes CLI invocation patterns **visible at a glance** in the integration class, eliminates an entire class of bugs, and reduces boilerplate from ~15 lines per override to ~4 lines of attributes. --- *Originally posted as a comment on [#2409](https://github.com/github/spec-kit/issues/2409#issuecomment-4344752659)*