@bybrawe/opencode-loop 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +159 -20
- package/commands/loop-ask.md +5 -0
- package/commands/loop-cmd.md +5 -0
- package/commands/loop-command.md +5 -0
- package/commands/loop-prompt.md +5 -0
- package/commands/loop-shell.md +5 -0
- package/package.json +6 -3
- package/src/index.js +79 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.6
|
|
4
|
+
|
|
5
|
+
- Added clearer README examples for prompt loops, scheduled question loops, OpenCode slash-command loops, shell loops, first-run timing, and idle-safe behavior.
|
|
6
|
+
- Stabilized `/loop-command 200m /compact` by routing `/compact` and `/summarize` through OpenCode TUI compact handling instead of treating them as custom prompt commands.
|
|
7
|
+
- Clarified that command loops wait for idle and should be used for slash commands such as `/compact`.
|
|
8
|
+
- Kept fallback compatibility with older SDK-style calls where possible.
|
|
9
|
+
|
|
10
|
+
## 0.5.5
|
|
11
|
+
|
|
12
|
+
- Added explicit loop action types for prompt loops, scheduled question/check loops, OpenCode slash-command loops, and shell-command loops.
|
|
13
|
+
- Added `/loop-command` and `/loop-cmd` for scheduled OpenCode commands such as `/compact`. These wait for the first interval by default and run only when OpenCode is idle.
|
|
14
|
+
- Added `/loop-ask` for recurring check prompts such as “did you run tests, tsc --noEmit, and build?” It waits for the first interval by default.
|
|
15
|
+
- Added `/loop-prompt` to force prompt mode.
|
|
16
|
+
- Added `/loop-shell` for recurring shell commands.
|
|
17
|
+
- Added `/loop` type flags: `--prompt`, `--ask`, `--command`, `--cmd`, `--slash`, `--shell`, and `--compact`.
|
|
18
|
+
- Clarified README examples for scheduled `/compact`, hourly quality checks, and idle-safe timing.
|
|
19
|
+
- Added `publishConfig.access=public` and normalized npm bin paths.
|
|
20
|
+
|
|
3
21
|
## 0.5.4
|
|
4
22
|
|
|
5
23
|
- Fixed compatibility with recent OpenCode SDK/TUI call shapes.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ NPM package name: **@bybrawe/opencode-loop**
|
|
|
11
11
|
|
|
12
12
|
## Current status
|
|
13
13
|
|
|
14
|
-
**v0.5.
|
|
14
|
+
**v0.5.6 fixes the recent OpenCode update compatibility issue and adds clear loop action types.** OpenCode Loop has been updated for the current OpenCode SDK/TUI call shape and idle/status behavior. The TUI loop waits for the session to become idle, debounces idle events, and avoids starting a new run while OpenCode is still busy or queued. It now separates prompt loops, scheduled question loops, OpenCode slash-command loops, shell loops, and compact loops so commands like `/compact` are not sent as normal chat text.
|
|
15
15
|
|
|
16
16
|
The known update-related symptoms from older builds are fixed:
|
|
17
17
|
|
|
@@ -19,6 +19,7 @@ The known update-related symptoms from older builds are fixed:
|
|
|
19
19
|
- a loop job is replaced but does not fire after OpenCode becomes idle
|
|
20
20
|
- repeated `/loop` commands create confusing queued turns
|
|
21
21
|
- prompt, shell, or toast calls silently fail after OpenCode SDK changes
|
|
22
|
+
- `/compact` accidentally being treated as a normal agent prompt
|
|
22
23
|
- intermittent `Tool execution aborted` behavior caused by triggering a new turn too early
|
|
23
24
|
|
|
24
25
|
The TUI loop is still intentionally session-bound: it runs while OpenCode is open and the current session emits status/idle events. For long-running background work after closing the terminal or OpenCode, use `opencode-loopd`.
|
|
@@ -78,7 +79,7 @@ OpenCode Loop is designed for developers searching for:
|
|
|
78
79
|
- **Failure control** with `--max-failures` and `--pause-on-verify-fail`.
|
|
79
80
|
- **Runtime control** with `--max-runtime 6h`.
|
|
80
81
|
- **Run limit support** with `--max-runs`.
|
|
81
|
-
- **
|
|
82
|
+
- **Wait-before-first-run support** with `--no-now` for TUI loops and `--sleep-first` for daemon mode.
|
|
82
83
|
- **Checkpoints** with `--checkpoint-only` or `--git-checkpoint`.
|
|
83
84
|
- **Safe mode** with `--safe` and prompt-level destructive command warnings.
|
|
84
85
|
- **Branch setup** with `--branch ai-loop`.
|
|
@@ -263,34 +264,92 @@ If the commands do not appear:
|
|
|
263
264
|
|
|
264
265
|
## Quick start
|
|
265
266
|
|
|
267
|
+
### The 5 examples most people need
|
|
268
|
+
|
|
269
|
+
Auto-continue every time OpenCode finishes a turn:
|
|
270
|
+
|
|
266
271
|
```text
|
|
267
272
|
/loop 0s continue from progress.md and implement the next unfinished TODO
|
|
268
273
|
```
|
|
269
274
|
|
|
270
|
-
|
|
275
|
+
Run now, then continue every 1 minute when OpenCode is idle:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
/loop 1m continue the project
|
|
279
|
+
```
|
|
271
280
|
|
|
272
|
-
|
|
281
|
+
Wait 1 minute before the first run, then continue every 1 minute when idle:
|
|
273
282
|
|
|
274
283
|
```text
|
|
275
|
-
/loop
|
|
284
|
+
/loop 1m --no-now continue the project
|
|
276
285
|
```
|
|
277
286
|
|
|
278
|
-
|
|
287
|
+
Run OpenCode compact every 200 minutes, but only when OpenCode is idle:
|
|
279
288
|
|
|
280
289
|
```text
|
|
281
|
-
/loop 200m
|
|
290
|
+
/loop-command 200m /compact
|
|
282
291
|
```
|
|
283
292
|
|
|
284
|
-
|
|
293
|
+
Ask a recurring check question every hour:
|
|
285
294
|
|
|
286
295
|
```text
|
|
287
|
-
/loop
|
|
296
|
+
/loop-ask 1h did you run tests, tsc --noEmit, and build? If not, run them and fix any error.
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Run a real shell command every 10 minutes when idle:
|
|
300
|
+
|
|
301
|
+
```text
|
|
302
|
+
/loop-shell 10m npm test
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### What “when idle” means
|
|
306
|
+
|
|
307
|
+
OpenCode Loop is idle-safe. It does **not** intentionally start a new agent turn while OpenCode is already busy.
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
|
|
311
|
+
```text
|
|
312
|
+
/loop 5m continue the project
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
If 5 minutes pass while OpenCode is still working, the job becomes due but waits. As soon as OpenCode becomes idle, the loop sends the prompt. This avoids queued turns, aborted tools, and overlapping responses.
|
|
316
|
+
|
|
317
|
+
### Prompt loops vs command loops
|
|
318
|
+
|
|
319
|
+
Use a prompt loop when you want to ask or instruct the agent:
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
/loop 1h check if tests, tsc --noEmit, and build pass. If not, fix them.
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Use a command loop when the action starts with `/` and should be executed as an OpenCode command:
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
/loop-command 200m /compact
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Do not use this for compacting:
|
|
332
|
+
|
|
333
|
+
```text
|
|
334
|
+
/loop 200m /compact
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
That can look like a chat prompt in older versions or unclear setups. Prefer `/loop-command 200m /compact` or the shortcut below:
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
/loop-compact 200m
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Safer development loop
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
/loop 0s --name dev --ask-never --safe --no-overlap --batch 5 --compact-every 200m --checkpoint-only --progress-file progress.md Treat progress.md as the main project state file. Continue with the next unfinished TODO, implement it, mark completed items with [x], add useful follow-up TODOs when you discover them, run tests/lint/build when available, and keep going while work remains.
|
|
288
347
|
```
|
|
289
348
|
|
|
290
|
-
|
|
349
|
+
### Test-fix loop
|
|
291
350
|
|
|
292
351
|
```text
|
|
293
|
-
/loop
|
|
352
|
+
/loop 0s --name testfix --ask-never --safe --verify "npm test" Continue from progress.md. If tests fail, analyze the failure, fix it, and run the tests again.
|
|
294
353
|
```
|
|
295
354
|
|
|
296
355
|
## Background daemon
|
|
@@ -388,7 +447,12 @@ Use `--name` to manage separate named loops, or `--multi` when you intentionally
|
|
|
388
447
|
|
|
389
448
|
| Command | Purpose |
|
|
390
449
|
|---|---|
|
|
391
|
-
| `/loop <interval> <
|
|
450
|
+
| `/loop <interval> <prompt>` | Add an idle/interval prompt loop |
|
|
451
|
+
| `/loop-command <interval> <slash-command>` | Schedule an OpenCode slash command such as `/compact` |
|
|
452
|
+
| `/loop-cmd <interval> <slash-command>` | Alias for `/loop-command` |
|
|
453
|
+
| `/loop-ask <interval> <prompt/question>` | Schedule a recurring question/check prompt; first run waits for the interval by default |
|
|
454
|
+
| `/loop-prompt <interval> <prompt>` | Force prompt mode, even when you want explicit prompt-loop naming |
|
|
455
|
+
| `/loop-shell <interval> <shell-command>` | Schedule a shell command loop |
|
|
392
456
|
| `/loop-help` | Show usage help inside OpenCode |
|
|
393
457
|
| `/loop-status` | Show active loop jobs |
|
|
394
458
|
| `/loop-logs` | Show recent loop log entries |
|
|
@@ -423,6 +487,27 @@ Use `--name` to manage separate named loops, or `--multi` when you intentionally
|
|
|
423
487
|
|
|
424
488
|
The plugin is **idle-driven**. It checks jobs when OpenCode becomes idle, so it avoids intentionally starting a second agent turn on top of an active one.
|
|
425
489
|
|
|
490
|
+
If the interval expires while OpenCode is busy, the loop becomes due and waits. It runs on the next idle event.
|
|
491
|
+
|
|
492
|
+
## Loop types
|
|
493
|
+
|
|
494
|
+
OpenCode Loop has separate types for prompts, OpenCode slash commands, and shell commands. This matters because `/compact` should be executed as an OpenCode command, not sent to the agent as text.
|
|
495
|
+
|
|
496
|
+
| Type | Use | Example | First run |
|
|
497
|
+
|---|---|---|---|
|
|
498
|
+
| Prompt loop | Ask or instruct the agent repeatedly | `/loop 1h check whether tests, tsc --noEmit, and build pass` | Now by default |
|
|
499
|
+
| Scheduled question | Ask/check on a timer | `/loop-ask 1h did you run tests and build?` | After the interval by default |
|
|
500
|
+
| Slash-command loop | Run OpenCode commands like `/compact` | `/loop-command 200m /compact` | After the interval by default |
|
|
501
|
+
| Shell loop | Run real terminal commands | `/loop-shell 10m npm test` | After the interval by default |
|
|
502
|
+
|
|
503
|
+
You can still force the type on `/loop`:
|
|
504
|
+
|
|
505
|
+
```text
|
|
506
|
+
/loop 200m --command /compact
|
|
507
|
+
/loop 10m --shell npm test
|
|
508
|
+
/loop 1h --prompt did you run tests, tsc --noEmit, and build?
|
|
509
|
+
```
|
|
510
|
+
|
|
426
511
|
## Actions
|
|
427
512
|
|
|
428
513
|
### Prompt action
|
|
@@ -459,18 +544,40 @@ Do not run destructive commands, force pushes, production deploys, or database r
|
|
|
459
544
|
|
|
460
545
|
### Slash command action
|
|
461
546
|
|
|
547
|
+
Use `/loop-command` when the action should be executed as an OpenCode command instead of sent to the agent as text.
|
|
548
|
+
|
|
462
549
|
```text
|
|
463
|
-
/loop 200m /compact
|
|
464
|
-
/loop 15m /review current changes
|
|
550
|
+
/loop-command 200m /compact
|
|
551
|
+
/loop-command 15m /review current changes
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
`/compact` and `/summarize` are treated specially and routed to OpenCode's TUI compact action. Custom command templates such as `/review` are routed through OpenCode's session command API. If OpenCode is busy when the interval expires, the command waits and runs when the session becomes idle.
|
|
555
|
+
|
|
556
|
+
Equivalent forced type on `/loop`:
|
|
557
|
+
|
|
558
|
+
```text
|
|
559
|
+
/loop 200m --command /compact
|
|
465
560
|
```
|
|
466
561
|
|
|
467
|
-
|
|
562
|
+
For most users, the clearer form is preferred:
|
|
563
|
+
|
|
564
|
+
```text
|
|
565
|
+
/loop-command 200m /compact
|
|
566
|
+
```
|
|
468
567
|
|
|
469
568
|
### Shell action
|
|
470
569
|
|
|
570
|
+
Use `/loop-shell` for real terminal commands.
|
|
571
|
+
|
|
572
|
+
```text
|
|
573
|
+
/loop-shell 10m npm test
|
|
574
|
+
/loop-shell 30m pnpm lint
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Equivalent forced type on `/loop`:
|
|
578
|
+
|
|
471
579
|
```text
|
|
472
|
-
/loop 10m
|
|
473
|
-
/loop 30m $pnpm lint
|
|
580
|
+
/loop 10m --shell npm test
|
|
474
581
|
```
|
|
475
582
|
|
|
476
583
|
Shell actions starting with `!` or `$` run through the OpenCode shell tool.
|
|
@@ -692,10 +799,17 @@ Parse and preview a loop without saving it.
|
|
|
692
799
|
|
|
693
800
|
### `--now` and `--no-now`
|
|
694
801
|
|
|
695
|
-
By default
|
|
802
|
+
By default `/loop` prompt jobs are due immediately. Use `--no-now` to wait for the first interval.
|
|
803
|
+
|
|
804
|
+
```text
|
|
805
|
+
/loop 1h --no-now did you run tests, tsc --noEmit, and build?
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Command-style shortcuts such as `/loop-command`, `/loop-ask`, and `/loop-shell` wait for the first interval by default. Use `--now` if you want them to run immediately too.
|
|
696
809
|
|
|
697
810
|
```text
|
|
698
|
-
/loop 200m
|
|
811
|
+
/loop-command 200m /compact
|
|
812
|
+
/loop-command 200m --now /compact
|
|
699
813
|
```
|
|
700
814
|
|
|
701
815
|
## Recommended recipes
|
|
@@ -727,9 +841,19 @@ opencode-loopd --project . --every 5m --prompt-file loop-prompt.md
|
|
|
727
841
|
### Compact loop
|
|
728
842
|
|
|
729
843
|
```text
|
|
730
|
-
/loop 200m
|
|
844
|
+
/loop-command 200m /compact
|
|
731
845
|
```
|
|
732
846
|
|
|
847
|
+
This compacts every 200 minutes when OpenCode is idle. It does not interrupt an active agent turn.
|
|
848
|
+
|
|
849
|
+
### Scheduled quality-check question
|
|
850
|
+
|
|
851
|
+
```text
|
|
852
|
+
/loop-ask 1h did you run tests, tsc --noEmit, and build? If not, run them now and fix errors.
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
This asks the agent every hour when idle. If the hour passes while the agent is working, the question waits and is sent after the current turn finishes.
|
|
856
|
+
|
|
733
857
|
### Prompt file loop
|
|
734
858
|
|
|
735
859
|
```text
|
|
@@ -833,6 +957,21 @@ Improve the application in small safe steps.
|
|
|
833
957
|
|
|
834
958
|
## Changelog highlights
|
|
835
959
|
|
|
960
|
+
### v0.5.6
|
|
961
|
+
|
|
962
|
+
- Added clearer README examples for prompt loops, scheduled question loops, command loops, shell loops, first-run timing, and idle-safe behavior.
|
|
963
|
+
- Stabilized `/loop-command 200m /compact` by routing compact/summarize through OpenCode TUI compact handling instead of treating it as a custom prompt command.
|
|
964
|
+
- Clarified that command loops wait for idle and should be used for slash commands such as `/compact`.
|
|
965
|
+
|
|
966
|
+
## v0.5.5
|
|
967
|
+
|
|
968
|
+
- Added explicit loop action types: prompt, scheduled question, OpenCode slash command, and shell command.
|
|
969
|
+
- Added `/loop-command` / `/loop-cmd` for commands such as `/compact`; these wait for the first interval by default.
|
|
970
|
+
- Added `/loop-ask` for recurring checks such as “did you run tests, tsc --noEmit, and build?”; it waits for the first interval by default.
|
|
971
|
+
- Added `/loop-shell` for recurring shell commands.
|
|
972
|
+
- Added `--prompt`, `--ask`, `--command`, `--cmd`, `--slash`, `--shell`, and `--compact` type flags for `/loop`.
|
|
973
|
+
- Clarified idle behavior: if a job becomes due while OpenCode is busy, it waits and runs on the next idle event.
|
|
974
|
+
|
|
836
975
|
### v0.5.4
|
|
837
976
|
|
|
838
977
|
- Fixed compatibility with recent OpenCode SDK/TUI call shapes.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Schedule a recurring prompt/question. Usage: /loop-ask 1h did you run tests?
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Loop ask scheduler received. The OpenCode Loop plugin handles this recurring prompt locally. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Alias for /loop-command. Usage: /loop-cmd 200m /compact
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Loop command scheduler received. The OpenCode Loop plugin handles this slash-command schedule locally. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Schedule an OpenCode slash command loop. Usage: /loop-command 200m /compact
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Loop command scheduler received. The OpenCode Loop plugin handles this slash-command schedule locally. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Schedule a recurring prompt loop. Usage: /loop-prompt 1h continue the project
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Loop prompt scheduler received. The OpenCode Loop plugin handles this recurring prompt locally. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Schedule a recurring shell command. Usage: /loop-shell 10m npm test
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Loop shell scheduler received. The OpenCode Loop plugin handles this shell schedule locally. Do not explain this command, do not search the web, do not create scheduler files, and do not run extra shell commands because of this command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Claude Code style /loop command and background daemon for OpenCode: auto-continue, progress.md TODO automation, compact scheduling, verification, checkpoints, and safe autonomous coding loops.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -55,7 +55,10 @@
|
|
|
55
55
|
"@opencode-ai/plugin": ">=1.4.0"
|
|
56
56
|
},
|
|
57
57
|
"bin": {
|
|
58
|
-
"opencode-loop": "scripts/install-node.mjs",
|
|
59
|
-
"opencode-loopd": "scripts/loopd.mjs"
|
|
58
|
+
"opencode-loop": "./scripts/install-node.mjs",
|
|
59
|
+
"opencode-loopd": "./scripts/loopd.mjs"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
60
63
|
}
|
|
61
64
|
}
|
package/src/index.js
CHANGED
|
@@ -149,7 +149,7 @@ function parseLoopArgs(raw, defaults = {}) {
|
|
|
149
149
|
} else if (first) {
|
|
150
150
|
const parsedDuration = parseDuration(first)
|
|
151
151
|
if (parsedDuration !== null) intervalMs = parsedDuration
|
|
152
|
-
else if (intervalMs === null) return { ok: false, error: "Usage: /loop 0s <prompt> | /loop 5m <prompt> | /loop 200m /compact | /loop 10m
|
|
152
|
+
else if (intervalMs === null) return { ok: false, error: "Usage: /loop 0s <prompt> | /loop 5m <prompt> | /loop-command 200m /compact | /loop-shell 10m npm test | /loop --watch progress.md <prompt>" }
|
|
153
153
|
else rest = input
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -160,6 +160,7 @@ function parseLoopArgs(raw, defaults = {}) {
|
|
|
160
160
|
id: `${now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`,
|
|
161
161
|
name: defaults.name,
|
|
162
162
|
action: defaults.action || "",
|
|
163
|
+
kind: defaults.kind || undefined,
|
|
163
164
|
intervalMs,
|
|
164
165
|
immediate: defaults.immediate ?? true,
|
|
165
166
|
maxRuns: defaults.maxRuns ?? 0,
|
|
@@ -218,6 +219,13 @@ function parseLoopArgs(raw, defaults = {}) {
|
|
|
218
219
|
;[found, rest] = takeFlag(rest, "--dry-run"); if (found) job.dryRun = true
|
|
219
220
|
;[found, rest] = takeFlag(rest, "--multi"); if (found) job.multi = true
|
|
220
221
|
;[found, rest] = takeFlag(rest, "--replace"); if (found) job.multi = false
|
|
222
|
+
;[found, rest] = takeFlag(rest, "--prompt"); if (found) job.kind = "prompt"
|
|
223
|
+
;[found, rest] = takeFlag(rest, "--ask"); if (found) job.kind = "prompt"
|
|
224
|
+
;[found, rest] = takeFlag(rest, "--command"); if (found) job.kind = "command"
|
|
225
|
+
;[found, rest] = takeFlag(rest, "--cmd"); if (found) job.kind = "command"
|
|
226
|
+
;[found, rest] = takeFlag(rest, "--slash"); if (found) job.kind = "command"
|
|
227
|
+
;[found, rest] = takeFlag(rest, "--shell"); if (found) job.kind = "shell"
|
|
228
|
+
;[found, rest] = takeFlag(rest, "--compact"); if (found) job.kind = "compact"
|
|
221
229
|
|
|
222
230
|
;[value, rest] = takeFlagValue(rest, "--name"); if (value !== undefined) job.name = value.trim()
|
|
223
231
|
;[value, rest] = takeFlagValue(rest, "--max-runs"); if (value !== undefined) job.maxRuns = parsePositiveInt(value, 0)
|
|
@@ -327,6 +335,38 @@ function fireSdk(client, label, method, modernArgs, legacyArgs) {
|
|
|
327
335
|
.catch((error) => log(client, "warn", `${label} failed`, { error: sdkErrorMessage(error) }))
|
|
328
336
|
}
|
|
329
337
|
|
|
338
|
+
async function executeTuiCommand(client, command) {
|
|
339
|
+
if (!client?.tui?.executeCommand) throw new Error("client.tui.executeCommand is not available")
|
|
340
|
+
return await sdkCall(
|
|
341
|
+
client.tui.executeCommand.bind(client.tui),
|
|
342
|
+
{ command },
|
|
343
|
+
{ body: { command } },
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function compactTuiCommandName(command = "compact") {
|
|
348
|
+
const normalized = String(command || "compact").replace(/^\/+/, "").trim().toLowerCase()
|
|
349
|
+
if (normalized === "compact" || normalized === "summarize") return "session.compact"
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function compactSession(client, sessionID) {
|
|
354
|
+
try {
|
|
355
|
+
await executeTuiCommand(client, "session.compact")
|
|
356
|
+
return true
|
|
357
|
+
} catch (error) {
|
|
358
|
+
await log(client, "warn", "tui session.compact failed", { error: sdkErrorMessage(error) })
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await sdkCall(client.session.summarize.bind(client.session), { sessionID }, { path: { id: sessionID }, body: {} })
|
|
362
|
+
return true
|
|
363
|
+
} catch (error) {
|
|
364
|
+
await log(client, "warn", "session.summarize fallback failed", { error: sdkErrorMessage(error) })
|
|
365
|
+
}
|
|
366
|
+
await toast(client, "Could not run /compact from loop. Check OpenCode version and active TUI session.", "error")
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
|
|
330
370
|
async function log(client, level, message, extra) {
|
|
331
371
|
try {
|
|
332
372
|
await sdkCall(
|
|
@@ -362,13 +402,17 @@ function wasHandled(sessionID, name, args) {
|
|
|
362
402
|
}
|
|
363
403
|
|
|
364
404
|
function commandName(name) { return String(name || "") }
|
|
365
|
-
function isPreset(name) { return ["loop-dev", "loop-testfix", "loop-compact", "loop-progress", "loop-safe-dev"].includes(name) }
|
|
405
|
+
function isPreset(name) { return ["loop-dev", "loop-testfix", "loop-compact", "loop-progress", "loop-safe-dev", "loop-command", "loop-cmd", "loop-prompt", "loop-ask", "loop-shell"].includes(name) }
|
|
366
406
|
function presetDefaults(name, args) {
|
|
367
407
|
const [maybeDuration, rest] = splitFirst(args)
|
|
368
408
|
const parsed = parseDuration(maybeDuration)
|
|
369
409
|
const intervalMs = parsed === null ? 0 : parsed
|
|
370
410
|
const extra = parsed === null ? String(args || "").trim() : rest
|
|
371
|
-
if (name === "loop-compact") return { intervalMs: intervalMs || parseDuration("200m"), action: extra || "/compact", name: "compact", immediate: false }
|
|
411
|
+
if (name === "loop-compact") return { intervalMs: intervalMs || parseDuration("200m"), action: extra || "/compact", kind: "compact", name: "compact", immediate: false }
|
|
412
|
+
if (name === "loop-command" || name === "loop-cmd") return { intervalMs, action: extra, kind: "command", name: "command", immediate: false }
|
|
413
|
+
if (name === "loop-prompt") return { intervalMs, action: extra, kind: "prompt", name: "prompt", immediate: true }
|
|
414
|
+
if (name === "loop-ask") return { intervalMs, action: extra, kind: "prompt", name: "ask", immediate: false }
|
|
415
|
+
if (name === "loop-shell") return { intervalMs, action: extra, kind: "shell", name: "shell", immediate: false }
|
|
372
416
|
if (name === "loop-testfix") return { intervalMs, name: "testfix", safe: true, askNever: true, verifyCommand: extra || "npm test", action: `Run the project tests. Fix failures. Re-run the tests. Test command hint: ${extra || "npm test"}` }
|
|
373
417
|
if (name === "loop-progress") return { intervalMs, name: "progress", safe: true, askNever: true, progressFile: "progress.md", action: extra || "Read progress.md and continue the next unfinished TODO. Mark completed TODOs with [x]. Add useful TODOs when you discover them." }
|
|
374
418
|
if (name === "loop-safe-dev") return { intervalMs, name: "safe-dev", safe: true, askNever: true, noOverlap: true, checkpointOnly: true, batch: 5, progressFile: "progress.md", action: extra || "Develop the project from progress.md. Work in small safe batches. Mark completed TODOs with [x]. Add new ideas to progress.md. Run tests/lint/build if available." }
|
|
@@ -377,6 +421,7 @@ function presetDefaults(name, args) {
|
|
|
377
421
|
|
|
378
422
|
function jobLabel(job) {
|
|
379
423
|
const title = job.name ? `${job.name}: ` : ""
|
|
424
|
+
const kind = job.kind ? ` [${job.kind}]` : ""
|
|
380
425
|
const limit = job.maxRuns > 0 ? `, max ${job.maxRuns}` : ""
|
|
381
426
|
const runtime = job.maxRuntimeMs > 0 ? `, runtime ${durationToText(job.maxRuntimeMs)}` : ""
|
|
382
427
|
const timeout = job.timeoutMs > 0 ? `, timeout ${durationToText(job.timeoutMs)}` : ""
|
|
@@ -387,7 +432,7 @@ function jobLabel(job) {
|
|
|
387
432
|
const stopFile = job.stopFile ? ", stop-file" : ""
|
|
388
433
|
const watch = job.watchPaths?.length ? `, watch ${job.watchPaths.join(",")}` : ""
|
|
389
434
|
const paused = job.paused ? ", paused" : ""
|
|
390
|
-
return `${title}${durationToText(job.intervalMs)} -> ${job.action || `[prompt-file: ${job.promptFile}]`}${limit}${runtime}${timeout}${compact}${verify}${preflight}${failures}${stopFile}${watch}${paused}`
|
|
435
|
+
return `${title}${durationToText(job.intervalMs)}${kind} -> ${job.action || `[prompt-file: ${job.promptFile}]`}${limit}${runtime}${timeout}${compact}${verify}${preflight}${failures}${stopFile}${watch}${paused}`
|
|
391
436
|
}
|
|
392
437
|
|
|
393
438
|
function matchJob(job, target, index) {
|
|
@@ -448,9 +493,14 @@ function dangerousShell(command) {
|
|
|
448
493
|
return [/\brm\s+-rf\b/, /\bgit\s+reset\b/, /\bgit\s+clean\b/, /\bgit\s+push\b/, /\bdel\s+\/s\b/, /\brmdir\s+\/s\b/, /\bformat\b/, /\bterraform\s+destroy\b/, /\bkubectl\s+delete\b/, /\bdeploy\b.*\bproduction\b/].some((pattern) => pattern.test(text))
|
|
449
494
|
}
|
|
450
495
|
|
|
451
|
-
function actionKind(action) {
|
|
496
|
+
function actionKind(action, job = {}) {
|
|
452
497
|
const text = String(action || "").trim()
|
|
498
|
+
const forced = String(job.kind || "").trim().toLowerCase()
|
|
499
|
+
if (forced === "compact") return "compact"
|
|
453
500
|
if (text === "/compact" || text === "/summarize") return "compact"
|
|
501
|
+
if (forced === "prompt" || forced === "ask") return "prompt"
|
|
502
|
+
if (forced === "command" || forced === "cmd" || forced === "slash") return "command"
|
|
503
|
+
if (forced === "shell") return "shell"
|
|
454
504
|
if (text.startsWith("/")) return "command"
|
|
455
505
|
if (text.startsWith("!") || text.startsWith("$")) return "shell"
|
|
456
506
|
return "prompt"
|
|
@@ -502,11 +552,10 @@ async function maybeCompact(client, sessionID, job) {
|
|
|
502
552
|
const dueRuns = job.compactEveryRuns > 0 && (job.runCount || 0) > 0 && (job.runCount || 0) % job.compactEveryRuns === 0 && job.lastCompactRunCount !== job.runCount
|
|
503
553
|
const dueTime = job.compactEveryMs > 0 && (!job.lastCompactAt || now() - job.lastCompactAt >= job.compactEveryMs)
|
|
504
554
|
if (!dueRuns && !dueTime) return job
|
|
505
|
-
|
|
506
|
-
await sdkCall(client.session.summarize.bind(client.session), { sessionID }, { path: { id: sessionID }, body: {} })
|
|
555
|
+
if (await compactSession(client, sessionID)) {
|
|
507
556
|
job.lastCompactAt = now()
|
|
508
557
|
job.lastCompactRunCount = job.runCount || 0
|
|
509
|
-
}
|
|
558
|
+
}
|
|
510
559
|
return job
|
|
511
560
|
}
|
|
512
561
|
|
|
@@ -708,18 +757,28 @@ async function finalizeActiveRun(directory, client, sessionID) {
|
|
|
708
757
|
|
|
709
758
|
async function fireAction(directory, client, sessionID, job) {
|
|
710
759
|
const action = String(job.action || "").trim()
|
|
711
|
-
const kind = actionKind(action)
|
|
760
|
+
const kind = actionKind(action, job)
|
|
712
761
|
if (kind === "compact") {
|
|
713
|
-
await
|
|
714
|
-
return { startsAssistantTurn:
|
|
762
|
+
const ok = await compactSession(client, sessionID)
|
|
763
|
+
return { startsAssistantTurn: ok }
|
|
715
764
|
}
|
|
716
765
|
if (kind === "command") {
|
|
717
|
-
const
|
|
718
|
-
|
|
766
|
+
const normalized = action.startsWith("/") ? action.slice(1) : action
|
|
767
|
+
const [command, argumentsText] = splitFirst(normalized)
|
|
768
|
+
if (!command) {
|
|
769
|
+
await toast(client, "Loop command action is empty. Example: /loop-command 200m /compact", "warning")
|
|
770
|
+
return { startsAssistantTurn: false }
|
|
771
|
+
}
|
|
772
|
+
const tuiCommand = compactTuiCommandName(command)
|
|
773
|
+
if (tuiCommand) {
|
|
774
|
+
await executeTuiCommand(client, tuiCommand)
|
|
775
|
+
return { startsAssistantTurn: true }
|
|
776
|
+
}
|
|
777
|
+
await sdkCall(client.session.command.bind(client.session), { sessionID, command, arguments: argumentsText }, { path: { id: sessionID }, body: { command, arguments: argumentsText } })
|
|
719
778
|
return { startsAssistantTurn: true }
|
|
720
779
|
}
|
|
721
780
|
if (kind === "shell") {
|
|
722
|
-
const command = action.
|
|
781
|
+
const command = action.replace(/^[!$]\s*/, "").trim()
|
|
723
782
|
if (job.safe && dangerousShell(command)) {
|
|
724
783
|
await toast(client, `Blocked dangerous shell command in safe mode: ${command}`, "error")
|
|
725
784
|
await appendLoopLog(directory, "blocked", { sessionID, job: job.name || job.id, command })
|
|
@@ -835,6 +894,7 @@ function sameLoopDefinition(a, b) {
|
|
|
835
894
|
return (a.name || "") === (b.name || "") &&
|
|
836
895
|
Number(a.intervalMs || 0) === Number(b.intervalMs || 0) &&
|
|
837
896
|
normalizeActionForCompare(a.action) === normalizeActionForCompare(b.action) &&
|
|
897
|
+
normalizeActionForCompare(a.kind) === normalizeActionForCompare(b.kind) &&
|
|
838
898
|
normalizeActionForCompare(a.promptFile) === normalizeActionForCompare(b.promptFile)
|
|
839
899
|
}
|
|
840
900
|
|
|
@@ -911,9 +971,11 @@ async function helpLoop(client, sessionID) {
|
|
|
911
971
|
await say(client, sessionID, [
|
|
912
972
|
"OpenCode Loop help:",
|
|
913
973
|
"/loop 0s <prompt> Claude Code style auto-continue",
|
|
914
|
-
"/loop 5m --ask-never --safe <prompt> interval autonomous loop",
|
|
915
|
-
"/loop 200m
|
|
916
|
-
"/loop
|
|
974
|
+
"/loop 5m --ask-never --safe <prompt> interval autonomous prompt loop",
|
|
975
|
+
"/loop-command 200m /compact OpenCode slash-command loop, waits for idle",
|
|
976
|
+
"/loop-ask 1h did you run tests and tsc --noEmit? scheduled question/check prompt",
|
|
977
|
+
"/loop-shell 10m npm test shell loop, waits for idle",
|
|
978
|
+
"/loop 200m --command /compact same as command loop",
|
|
917
979
|
"/loop 0s --verify \"npm test\" <prompt> verify after each assistant turn",
|
|
918
980
|
"/loop 0s --prompt-file loop-prompt.md load prompt from a file",
|
|
919
981
|
"/loop 0s --max-runtime 6h --max-failures 3 <task> stop safely after limits",
|