You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+25-35Lines changed: 25 additions & 35 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,8 +7,6 @@
7
7
8
8
A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Claude Code's `/loop`. Each `/loop` task is bound to the session that created it — never leaks to other sessions.
9
9
10
-
> **Upgrading to 0.4.0?** Two behavior changes to know about: (1) since 0.3.0, tasks die with the opencode process by default (`ephemeralTasks: false` restores persistence) — the upgrade drops the pre-0.3.0 `tasks.json` once; (2) 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`.
@@ -17,10 +15,10 @@ A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Cla
17
15
-**`/loop 30s --once <prompt>`** — one-shot: fires once, then auto-cancels
18
16
-**`/loop help`** — full usage, flags, and examples in the terminal
19
17
-**Claude Code-style flags** — `--cancel/--list/--status/--pause/--resume/--stop/--stop-all` map to the matching subcommand
20
-
-**Per-session scoping** — tasks are bound to a `sessionID`; other sessions never seeor fire them
21
-
-**Subcommands** — `list | status | cancel | stop | pause | resume | stop-all` (session-scoped; add `--all`to cross sessions; bare `stop` cancels every task in scope)
22
-
-**Internal ticker** — 5s loop drives task firing (no longer depends on `session.idle` events)
23
-
-**Prompt fidelity** — flags (`--once`, `--all`, `--jitter=*`) are only recognized before the prompt begins; `--` forces the rest to be treated as prompt text verbatim, and whitespace/newlines are preserved
18
+
-**Per-session scoping** — tasks are bound to a `sessionID`; other sessions never see, fire, or manage them
19
+
-**Subcommands** — `list | status | cancel | stop | pause | resume | stop-all` (all scoped to the current session; bare `stop` cancels every task in the session)
20
+
-**Internal ticker** — 5s loop drives task firing
21
+
-**Prompt fidelity** — flags (`--once`, `--jitter=*`) are only recognized before the prompt begins; `--` forces the rest to be treated as prompt text verbatim, and whitespace/newlines are preserved
24
22
-**Per-process instance coordination** — plugin instances inside one process (case-variant plugin paths, per-command `opencode run` instances) elect a single leader so tasks never double-fire; a second OpenCode process in the same project fires its own tasks independently, and merge-writes prevent task loss
25
23
-**Inflight guard** — double-set at ticker and `fireTask` level prevents double-firing even if opencode hot-reloads the plugin
26
24
-**Wall-clock scheduling** — fixed tasks anchor to fire start; model-turn duration never inflates the interval
@@ -29,7 +27,7 @@ A drop-in `/loop` command for [opencode](https://opencode.ai), modeled after Cla
29
27
-**Configurable Jitter** — deterministic Fixed-task offset, controllable per command, tool call, or programmatic default
30
28
-**Auto-expire** — tasks idle for more than 7 days are removed on load (active tasks never expire)
31
29
-**Max 50 concurrent tasks**
32
-
-**LLM-callable tools** — `loop_schedule`, `loop_status` (session-bound by default)
30
+
-**LLM-callable tools** — `loop_schedule`, `loop_status` (scoped to the calling session)
33
31
-**Inline results, Claude Code style** — every `/loop` result (create, list, cancel, pause, resume, stop-all, failures) is presented by the model directly in the conversation, in the user's own language — task lists render as a markdown table. No dialogs, no toasts
34
32
35
33
## Requirements
@@ -80,7 +78,7 @@ Server config (`~/.config/opencode/opencode.json`):
80
78
"plugin": ["opencode-plugin-loop"],
81
79
"command": {
82
80
"loop": {
83
-
"description": "Run prompts on a schedule. Intervals: s/m/h/d. Subcommands: help | list | status | cancel <id> | pause <id> | resume <id> | stop-all (add --all to cross sessions)",
81
+
"description": "Run prompts on a schedule. Intervals: s/m/h/d. Subcommands: help | list | status | cancel <id> | pause <id> | resume <id> | stop-all (all scoped to the current session)",
84
82
"template": "$ARGUMENTS",
85
83
"agent": "build"
86
84
}
@@ -120,10 +118,10 @@ Re-run `npm run build` after editing `src/`, then restart OpenCode to load the r
120
118
/loop 30s --once remind me to stretch # one-shot: fires once, then auto-cancels
121
119
```
122
120
123
-
Fixed tasks use deterministic Jitter by default for backward compatibility. Add
121
+
Fixed tasks use deterministic Jitter by default. Add
124
122
`--jitter=false` for an exact interval or `--jitter=true` to enable it explicitly.
125
123
Flags are only recognized **before the prompt begins**: anything after the first
126
-
prompt word — including text that looks like `--once`, `--all`, or `--jitter=*` —
124
+
prompt word — including text that looks like `--once` or `--jitter=*` —
127
125
is part of the prompt and is preserved verbatim (whitespace and newlines
128
126
included). Use `--` to force everything after it to be treated as prompt text:
129
127
@@ -148,7 +146,7 @@ The natural-language form runs the request immediately in the current model turn
148
146
149
147
Adaptive-to-Fixed conversion defaults to `jitterEnabled: false`, so an explicit cadence remains exact.
150
148
151
-
The fallback is written before the prompt is injected. A successful `reschedule` therefore replaces the fallback and is not overwritten after the model finishes. The preferred `delayMs` is relative to tool-call time, avoiding epoch arithmetic. An in-range model delay is stored exactly without Jitter; only an out-of-range request is clamped to the task's configured minimum or maximum delay. Fixed and Maintenance rescheduling remains unchanged. The legacy absolute `nextDueAtMs`remains supported, but passing it together with `delayMs` returns an error without changing the task.
149
+
The fallback is written before the prompt is injected. A successful `reschedule` therefore replaces the fallback and is not overwritten after the model finishes. The preferred `delayMs` is relative to tool-call time, avoiding epoch arithmetic. An in-range model delay is stored exactly without Jitter; only an out-of-range request is clamped to the task's configured minimum or maximum delay. Fixed and Maintenance rescheduling remains unchanged. An absolute `nextDueAtMs`is also accepted, but passing it together with `delayMs` returns an error without changing the task.
152
150
153
151
### Bare `/loop` — custom default prompt
154
152
Create `.opencode/loop.md` (project) or `<user>/.opencode/loop.md` (user) with your maintenance instructions:
@@ -160,25 +158,21 @@ address each one. If everything is green, say so in one line.
160
158
161
159
### Subcommands
162
160
163
-
All subcommands are **session-scoped by default**. Add `--all`to operate across all sessions.
161
+
All subcommands are **scoped to the current session** — tasks created in other sessions are invisible to them, exactly like Claude Code's per-session `/loop` jobs.
164
162
165
163
```
166
164
/loop help # full usage, flags, and examples
167
165
/loop list # show tasks in current session
168
-
/loop list --all # show all sessions (with [s:xxxx] tags)
169
166
/loop status # alias for list
170
167
/loop cancel <taskId> # cancel one task in current session
171
-
/loop cancel <taskId> --all # override scope
172
168
/loop stop <taskId> # alias for cancel
173
169
/loop stop # cancel ALL tasks in current session
174
-
/loop stop --all # cancel ALL tasks across sessions
175
170
/loop pause <taskId> # pause one
176
171
/loop resume <taskId> # resume one (re-arms per mode)
177
172
/loop stop-all # cancel all tasks in current session
178
-
/loop stop-all --all # cancel ALL tasks across sessions
179
173
```
180
174
181
-
If you try `cancel <id>` for a task owned by another session, you'll get a refusal with a hint to add `--all`. The same strict scoping applies to `loop_schedule` and `loop_status` tools.
175
+
Trying to manage a task owned by another session reports "No task `<id>` in this session" — switch to that session to manage it. The same strict scoping applies to the`loop_schedule` and `loop_status` tools.
182
176
183
177
### Migrating from Claude Code
184
178
@@ -189,7 +183,7 @@ If you try `cancel <id>` for a task owned by another session, you'll get a refus
189
183
| cancel/list via cron tools |`/loop cancel <id>`, `/loop list`|
190
184
|`--cancel`, `--list`, `--stop`| accepted — mapped to `cancel`, `list`, `stop`|
| jobs die when the session ends | same default since 0.3.0 (`ephemeralTasks: false` opts out) |
186
+
| jobs die when the session ends | same default (`ephemeralTasks: false` opts out) |
193
187
| cron expressions (`*/5 * * * *`) | not supported — use `5m` form (explicit error) |
194
188
195
189
Two behavioral differences worth knowing: tasks only fire for the **currently active session** (switch sessions and the others wait; switch back and they catch up once), and fixed tasks fire on a 5-second ticker rather than exact wall-clock cron times (up to one ticker period late).
@@ -207,7 +201,7 @@ No dialogs, no toasts — the conversation is the only output surface.
207
201
208
202
### Programmatic (LLM tools)
209
203
210
-
The plugin registers two LLM-callable tools. Both are session-bound by default; pass `all: true` to cross.
204
+
The plugin registers two LLM-callable tools. Both are scoped to the calling session.
211
205
212
206
```typescript
213
207
loop_schedule({
@@ -220,7 +214,7 @@ loop_schedule({
220
214
loop_schedule({
221
215
action: "cancel",
222
216
taskId: "abc12345",
223
-
//refuses if taskId belongs to another session (pass all: true to override)
217
+
//only works for tasks created in the calling session
224
218
})
225
219
226
220
loop_schedule({
@@ -236,8 +230,7 @@ loop_schedule({
236
230
jitterEnabled: false, // default for Adaptive-to-Fixed conversion
237
231
})
238
232
239
-
loop_status({}) // current session only
240
-
loop_status({ all: true }) // all sessions
233
+
loop_status({}) // current session
241
234
```
242
235
243
236
## Configuration
@@ -262,13 +255,10 @@ task records its owning process (`ownerPid` + start time) in `tasks.json`. On
262
255
load, tasks whose owner process is confirmed dead — e.g. after that OpenCode
263
256
process exits — are dropped, so loop tasks never outlive the process that
264
257
created them (the same lifecycle as Claude Code's `/loop`). Tasks owned by
265
-
other **live** OpenCode processes in the same project are kept: they remain
266
-
visible and manageable via `--all`, and each process fires only its own tasks.
in the plugin options to restore the previous behavior of persisting tasks
269
-
across process restarts. Note that upgrading from a release without per-task
270
-
owner tracking drops tasks that carry no owner identity once, since their
271
-
writer cannot be verified.
258
+
other **live** OpenCode processes in the same project are left untouched, and
259
+
each process fires only its own tasks. Same-process plugin reloads keep their
260
+
tasks. Set `ephemeralTasks: false` in the plugin options to persist tasks
261
+
across process restarts.
272
262
273
263
Adaptive minimum and maximum delays are persisted on each task. The random fallback
274
264
and any model-requested `reschedule` are both constrained by that task's bounds. Jitter
@@ -277,8 +267,8 @@ is not added to a model-selected Adaptive time.
277
267
For programmatic composition, `LoopConfig.defaultJitterEnabled` controls newly
278
268
created Fixed tasks and defaults to `true`. An explicit command
279
269
`--jitter=true|false` or tool argument `jitterEnabled` overrides that default.
280
-
Existing persisted Fixed tasks without a `jitterEnabled` field retain the legacy
281
-
Jitter-on behavior. Because the ticker checks every 5 seconds, actual prompt
270
+
Persisted Fixed tasks without a `jitterEnabled` field are treated as
271
+
Jitter-on. Because the ticker checks every 5 seconds, actual prompt
282
272
injection can occur up to one ticker period after an exact due time.
283
273
284
274
## Per-session architecture
@@ -292,7 +282,7 @@ Each `/loop` task carries a `sessionID` field:
292
282
| User runs `/loop` in session B | Session B becomes active; A's task waits |
293
283
|`session.deleted` for session A | All A's tasks cancelled automatically |
294
284
| Plugin reload (`opencode` hot-reload) | Old tickers stop, new ticker starts; in-flight tasks guarded by `inflight` Set |
295
-
| Process restart (new pid) | With `ephemeralTasks` enabled (default), tasks whose owner process is dead are dropped on load, while tasks owned by other live processes are kept; with it disabled, tasks resume as before |
285
+
| Process restart (new pid) | With `ephemeralTasks` enabled (default), tasks whose owner process is dead are dropped on load, while tasks owned by other live processes are left untouched; with it disabled, tasks resume as before |
296
286
| Old `tasks.json` without `sessionID`| Dropped on load (with log message) |
297
287
298
288
## Storage
@@ -303,15 +293,15 @@ Tasks persist to `.opencode/cache/loop/tasks.json` (per project). Fire history i
303
293
304
294
### Package entrypoints
305
295
306
-
Current releases expose separate `opencode-plugin-loop/server` and `opencode-plugin-loop/tui` entrypoints so OpenCode installs that auto-load both keep working (the TUI entrypoint is a no-op since results are presented inline). The root export remains the v1-compatible server module for backward compatibility. Programmatic consumers should use the named factory:
296
+
The package exposes separate `opencode-plugin-loop/server` and `opencode-plugin-loop/tui` entrypoints so OpenCode installs that auto-load both keep working (the TUI entrypoint is a no-op since results are presented inline). The root export is the same server module. Programmatic consumers should use the named factory:
307
297
308
298
```typescript
309
299
import { LoopPlugin } from"opencode-plugin-loop"
310
300
```
311
301
312
-
### Task lines overlap the input area
302
+
### Results presentation
313
303
314
-
Older releases wrote `/loop`results directly to the terminal or into native dialogs/toasts. Upgrade to the current release: every result is presented inline by the model, and runtime diagnostics go to OpenCode's structured application log.
304
+
Every `/loop`result is presented inline by the model — nothing is written directly to the terminal, and no dialogs or toasts are used. Runtime diagnostics go to OpenCode's structured application log.
315
305
316
306
Also make sure the plugin is installed from only one source. OpenCode loads npm plugins from `opencode.json` and copied plugins under `~/.config/opencode/plugins/` independently, even when they have the same package name.
Copy file name to clipboardExpand all lines: commands/loop.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
---
2
-
description: Run prompts on a schedule. Natural-language Adaptive requests run immediately and the model decides the next check; explicit intervals support --jitter=true|false and --once. Subcommands add --all to cross sessions. See /loop help.
description: Run prompts on a schedule. Natural-language Adaptive requests run immediately and the model decides the next check; explicit intervals support --jitter=true|false and --once. Tasks are scoped to the current session. See /loop help.
0 commit comments