fix(engines): invoke opencode run and require file changes (#913) - #1008
Conversation
|
Claude finished @frankbria's task in 8m 4s —— View job Precision bug-hunting review — PR #1008 (opencode run invocation)
Findings (2)
Delivered as 2 inline comments (with a committable suggestion on the minor) + one summary comment. No code was modified; no builds/tests run. Did not approve or request changes. |
GLM precision review — PR #1008
Scope: concrete defects only (logic / data-handling / regressions). Style, architecture, naming, and test coverage are covered by the general-purpose review and intentionally not repeated here. |
…smoke fixture's unborn HEAD (#913) Review feedback (both bot reviewers, independently): [major] The prompt moved from stdin to a positional argv entry, but Linux caps a single argv entry at MAX_ARG_STRLEN (128 KiB) while the context packager budgets 100K tokens (~400 KB). Confirmed empirically: /bin/true with a 200 KB argument raises OSError(E2BIG), so every large-prompt run would have failed before opencode started. The positional stays for normal prompts (the form verified end-to-end against the CLI); an oversized one is omitted from argv and sent on stdin, which opencode reads when no positional is given — confirmed by its own prompt_submit log carrying the piped text verbatim. [minor] The smoke fixture's `git init` left an unborn HEAD, so _git_head returned None, in_git_repo was False, and require_file_changes never fired — the writes-nothing assertion was passing vacuously. Added a baseline commit.
|
Claude finished @frankbria's task in 3m 41s —— View job Precision bug-hunting review — PR #1008 (opencode run invocation)Synchronize event. Verified the two findings posted on the prior commit are resolved in
Prior findings — both resolved in
|
| prior finding | status | basis |
|---|---|---|
major — prompt in argv → E2BIG on large prompts |
✅ fixed | opencode.py:88-100 now omits the prompt from argv when ≥ 128 KiB and pipes it via get_stdin(); build_command/get_stdin both key off the same _prompt_exceeds_argv, so the two decisions can't disagree. Threshold >= 128*1024 matches the kernel's len >= MAX_ARG_STRLEN check exactly (no off-by-one). |
minor — smoke repo fixture had no commit → unborn HEAD → guard never fired |
✅ fixed | test_opencode_smoke_913.py:48-53 now makes an empty baseline commit. |
New finding (1)
| severity | file:line | finding |
|---|---|---|
| minor | tests/core/adapters/test_opencode_smoke_913.py:133-141 |
except subprocess.TimeoutExpired wraps adapter.run(...), but SubprocessAdapter.run swallows TimeoutExpired internally and returns AgentResult(status="failed", error="Process timed out after …s") — it never propagates, so the skip is dead code and the documented "skip on timeout" never fires for this test. |
Failure scenario: an opencode hang/outage during test_a_run_that_writes_nothing_is_not_reported_completed does not skip at 240 s. adapter.run() catches TimeoutExpired itself (subprocess_adapter.py:173-185) and returns failed, so the except on :140 is unreachable. The run blocks for the adapter default DEFAULT_TIMEOUT_S = 1800 (30 min) before returning failed, which then coincidentally satisfies result.status != "completed". The PR's stated known-limitation — "two model-dependent smoke tests skip on timeout… so an upstream opencode outage does not red the suite" — is only true for the other test (it calls subprocess.run directly on :87, which does raise). OpenCodeAdapter.__init__ also doesn't forward timeout_s, so the 240 s bound is unreachable through adapter.run(); the suggestion below fixes the dead handler — bounding it to 240 s additionally needs the adapter to accept timeout_s.
result = adapter.run(
"task-smoke",
"Reply with the single word ACKNOWLEDGED. Do not create, edit or "
"delete any files.",
repo,
)
if (result.error or "").startswith("Process timed out"):
pytest.skip("opencode did not complete in time")
Non-finding (noted, not a defect)
The large-prompt stdin path (opencode run with no positional, prompt piped) is exercised only by mocked unit tests; no binary-gated smoke test feeds an oversized prompt through adapter.run(). I could not verify opencode's actual stdin-with-no-positional behavior (binary not installed in this review env), and the PR asserts verification via the prompt_submit log — so per the precision mandate I am not posting it as a defect. Flagging only as a residual-contract risk the maintainers may want to cover with a smoke assertion analogous to the small-prompt one.
No production-code defects found in the synchronized changes; the one item above is test-only.
Review feedback addressed (ccc820c)Both reviewers independently reported the same two defects. Both verified against the real system before fixing, both confirmed, both fixed. [major] Prompt in argv →
|
| AC | Evidence |
|---|---|
build_command produces opencode run <message> |
['/home/frankbria/.opencode/bin/opencode', 'run', 'fix the failing test']; CLI help contains opencode run = True, contains --non-interactive = False |
require_file_changes=True blocks a zero-work exit-0 run |
exit-0 binary writing nothing → status failed, modified_files: [], error "exited successfully but modified no files" |
tests no longer assert --non-interactive |
grep-clean; the CLI-help assertion replaces it and fails if opencode ever adds the flag back |
| binary-gated smoke test against the real CLI | 3 passed, 1 xfailed — the xfail is #1007 (opencode ignores the subprocess cwd), pinned deliberately |
Plus the regression this round fixes, shown as an outcome rather than a claim:
prompt size : 200,000 bytes
argv : ['.../opencode', 'run'] largest entry: 38 bytes
prompt routed to stdin : True
OLD (prompt in argv) : OSError: Argument list too long
NEW (prompt on stdin) : exec OK, no E2BIG
tests/core/adapters/ — 17 passed, 1 xfailed. ruff check clean.
…xcept (#913) GLM review [minor]: SubprocessAdapter.run catches TimeoutExpired itself and returns status='failed', so the smoke test's except was dead code — the documented skip-on-timeout could never fire. Worse, OpenCodeAdapter never forwarded timeout_s, so an opencode hang would sit on the 30-minute default and then satisfy 'status != completed' for the wrong reason. Forward timeout_s (same knob KilocodeAdapter already exposes), bound the smoke run to 240s, and detect the timeout from the returned error instead.
|
Claude finished @frankbria's task in 4m 2s —— View job Precision bug-hunting review — PR #1008 (opencode run invocation)Synchronize event on
Prior findings — all resolved
New findings✅ GLM review: no defects found. Hunt covered the production diff and its real edges:
Non-finding (unchanged from last round, not a defect): the oversized-prompt → stdin transport has no binary-gated smoke coverage (only mocked unit tests). Per the precision mandate I will not assert unverified CLI behavior; #915 is the right home for it. No production-code or test defects in the synchronized changes. |
Third round addressed (112035d)[minor] dead The reviewer's second-order point is the one that actually bites, and it was right: Fix: forward On the noted non-finding (stdin path has no binary-gated coverage)Fair, and I am recording it as a limitation rather than papering over it. opencode's backend is returning The natural home for that assertion is #915 (P0.21), which builds the binary-gated tier across all engines and is blocked by this PR.
|
|
Claude finished @frankbria's task in 2m 23s —— View job Precision bug-hunting review — PR #1008 (opencode run invocation)Synchronize event on
What this sync actually changed
The opencode path is byte-identical to the commit already reviewed clean across three prior rounds:
No conflict markers in any opencode file or its tests. The Codex rewrite is self-contained in Prior findings — all still resolved (unchanged code carries the prior fixes forward)
New findings✅ GLM review: no defects found. Re-hunted the opencode diff and its real edges against the checked-out tree:
The merged-in Codex rewrite is out of scope for this PR — it is the subject of a separate, already-landed change (#1009) and was reviewed there. I did not re-review it here, consistent with this PR's scope (the opencode invocation fix). No production-code or test defects in the synchronized opencode changes. |
Closes #913.
Problem
The adapter ran
opencode --non-interactivewith the prompt on stdin. No such flag exists. Verified against opencode 1.18.7:--non-interactiveis absent from the option listopencode run [message..], with the message as a positional, not stdinSo the delegated run did no work. And because
OpenCodeAdapterdid not setrequire_file_changes=True, an exit-0 no-op was reportedcompleted, gates ran on an unchanged tree, and the task could be marked DONE with no code written — the #739 false-completion class the claude-code adapter was already patched for.Fix
opencode --non-interactiveopencode run <message>get_stdinreturnsNone, so it is not sent twice)completedrequire_file_changes=True→failed--autois off by defaultI first added
--autoon the reasoning that a headless coding agent needs permission auto-approval. Testing showed that is not true — a plainopencode runwrites files fine under the default permission config. Since opencode documents--autoas "auto-approve permissions that are not explicitly denied (dangerous!)", and the prompt is derived from repository content, defaulting it on would add exactly the exposure #905–#907 closed. It is now opt-in (OpenCodeAdapter(auto_approve=True)).Where an operator's opencode config does deny writes, the run produces no file changes and
require_file_changesturns that into a loud failure rather than a silent false completion — so declining the flag degrades safely.Tests
Unit (
tests/core/adapters/test_opencode.py) — the--non-interactiveassertion is gone; new tests pin["opencode", "run", prompt],get_stdin() is None,require_file_changes, and the--autodefault.The autouse fixture returned
[]modified files, which under the new guard made three unrelated tests exercise the false-completion path instead of the behaviour they name. It now represents a run that did work, and the no-work case has its own tests (both directions: zero changes → notcompleted; changes →completed).Binary-gated smoke (
tests/core/adapters/test_opencode_smoke_913.py, criterion 4) — runs the adapter's ownbuild_commandoutput against the installed CLI, skipped whenopencodeis absent:opencode runexists and--non-interactivedoes not — asserted against the CLI's own help, so it fails if a future release moves the headless entry pointcompletedThe smoke test found a defect the issue does not cover — filed as #1007 [P0.27]
opencode ignores the subprocess
cwd. EverySubprocessAdaptertargets a workspace viacwd=workspace_path; opencode resolves its project directory from the parent process instead. Running the smoke test from the repo root, opencode reportedCreated \smoke.txt`and the file appeared in **/home/frankbria/projects/codeframe/`** — the pytest process's cwd — not the temp workspace. Reproduced twice.Under
codeframe servethat means a delegated task can edit CodeFRAME's own checkout while the task's workspace is untouched.require_file_changesdoes not save it: the guard inspects the workspace, finds nothing, and fails the run — correctly — while the edits have already landed elsewhere.opencode run --dir <workspace>looks like the remedy, but two attempts to verify it timed out (240s, 280s) and wrote nothing — and a plainopencode runtimed out immediately afterwards too, so the timeouts track opencode instability today, not--dir. I did not ship an unverified flag: assuming a CLI contract without checking is precisely what produced this issue. Filed as #1007 with the reproduction, and the smoke testxfails on exactly that condition so it converts into the regression test when #1007 is fixed.Known limitation
The two model-dependent smoke tests
skipon timeout rather than fail, so an upstream opencode outage does not red the suite. The CLI-contract assertions (help text, old invocation) are hard and deterministic.