Skip to content

Deterministic /loop parsing bypassed in opencode run — documented guards (missing prompt, cron rejection, unknown flags) never fire #18

Description

@jkrandom-sudo

Summary

When /loop is invoked via opencode run (both headless and -i interactive mode), the plugin's deterministic command parser (handleUserCommand, wired to the command.execute.before hook) never runs. The raw $ARGUMENTS go straight to the model, which improvises via the loop_schedule tool. As a result, every documented input guard in the README is silently bypassed in this mode.

Environment

  • opencode 1.18.3 (also reproduces the wiring described for opencode run instances in the README)
  • opencode-plugin-loop 0.7.2, installed globally per README (plugin: ["opencode-plugin-loop"] in ~/.config/opencode/opencode.json, command defined with template: "$ARGUMENTS")
  • macOS (darwin), Node v24
  • Model: deepseek-v4-flash via OpenAI-compatible provider (behavior is model-dependent by construction — that's the point of this issue)

Minimal reproduction

mkdir /tmp/loop-test && cd /tmp/loop-test

# Case 1: bare interval — README says this must return
# "❌ Missing prompt after interval \"5m\". Usage: /loop <interval> <prompt> — see /loop help."
opencode run "/loop 5m"

# Case 2: cron syntax — README says this must return
# "❌ Cron expressions are not supported. ..."
opencode run "/loop */5 * * * * check something"

Expected (per README, "Upgrading to 0.4.0" section)

scheduling-like input that used to silently create an Adaptive task (cron syntax, bare intervals like /loop 5m, unknown flags) now returns an explicit error pointing at /loop help.

This is implemented in src/scheduler.ts (handleUserCommand, e.g. the looksLikeCron rejection at line ~300), reachable only through the command.execute.before hook in src/index.ts.

Actual

Case 1 — the model creates a real fixed task with a self-invented placeholder prompt:

⚙ loop_schedule {"action":"create","intervalMs":300000,"mode":"fixed",
  "prompt":"The user requested a loop every 5 minutes. Ask them what they'd like to do."}
Created `/loop` task `0jg2mox8` — fires every 5 minutes.

Case 2 — the cron expression is silently converted to a fixed interval instead of rejected:

⚙ loop_schedule {"action":"create","prompt":"check something","intervalMs":300000,"mode":"fixed"}
Created loop task `1rvh3oge` — runs every 5 minutes (fixed mode) ...

Task persisted in .opencode/cache/loop/tasks.json (mode fixed, intervalMs 300000). Reproduced identically with opencode run -i under a pseudo-TTY, so this is not specific to headless mode.

Additionally, /loop help in opencode run prints model-generated help (derived from the tool schema) rather than the canonical LOOP_HELP text from src/scheduler.ts.

Impact

In opencode run, all deterministic behavior documented in the README — missing-prompt errors, cron rejection, unknown-flag rejection, exact help text — is replaced by model improvisation. Junk tasks with meta-prompts ("Ask them what they'd like to do") actually get scheduled and would fire every 5 minutes in a long-lived process.

Suspected cause

command.execute.before is not emitted by opencode when a command is executed through opencode run (with or without -i), so src/index.ts's interception of input.command === "loop" never happens and the command template ($ARGUMENTS) is sent to the model unguarded. I could not verify the full TUI path headlessly; if the hook fires correctly there, the plugin may need a run-mode fallback (e.g. input validation inside loop_schedule itself, mirroring handleUserCommand's guards).

Notes

  • Existing test suite (164 tests) passes; ephemeral task cleanup, instance lock takeover, and per-session scoping all verified working in real opencode run usage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    fix-meTrigger mini-swe-agent auto-fix

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions