@bojackduy/opencode-loopd 1.10.3 → 1.10.5
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 +15 -0
- package/commands/goal.md +4 -2
- package/dist/server.js +1717 -82
- package/dist/tui.js +1437 -906
- package/package.json +1 -1
- package/skills/loopd/SKILL.md +203 -20
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@bojackduy/opencode-loopd",
|
|
4
|
-
"version": "1.10.
|
|
4
|
+
"version": "1.10.5",
|
|
5
5
|
"description": "Codex-inspired background goal engine for OpenCode — autonomous subagents, engine-driven loop, child worker sessions and modal TUI dashboard. Like Claude Code loop for OpenCode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "AGPL-3.0-or-later",
|
package/skills/loopd/SKILL.md
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loopd
|
|
3
|
-
description: "Agent skill for loopd background goals. Teaches
|
|
3
|
+
description: "Agent skill for loopd: background AI goals (multi-turn autonomous workers with checks) AND standalone command sessions (raw interactive processes with a fullscreen terminal UI). Teaches which one to use, how to create/inspect/steer them, and the dashboard that shows both."
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.3.0"
|
|
6
6
|
status: active
|
|
7
|
-
tags: [opencode, loop, goal, background, automation]
|
|
7
|
+
tags: [opencode, loop, goal, background, automation, command, terminal, pty]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
# Loopd Background Goals
|
|
10
|
+
# Loopd: Background Goals & Command Sessions
|
|
11
11
|
|
|
12
|
-
Loopd
|
|
12
|
+
Loopd gives OpenCode two independent background primitives that share one dashboard:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
| | **Goal** | **Command Session** |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| What it is | An autonomous AI worker looping turns | A raw OS process (no AI) |
|
|
17
|
+
| Use for | Multi-step AI work: implement, fix, research, write | Run/watch/type into one process: dev server, `test --watch`, REPL, log tail, build, script |
|
|
18
|
+
| Created with | `loopd_create_goal` | `loopd_command_start` |
|
|
19
|
+
| Finishes via | `complete_goal` after deterministic `checks` pass | Process exits, or `loopd_command_terminate` |
|
|
20
|
+
| Has | objective, checks, agent, model, budgets, turns | argv, cwd, stdin/stdout, exit code |
|
|
21
|
+
| Monitor in | `/loop` dashboard, **Goals** tab | `/loop` or `/commands`, **Commands** tab → `o` for fullscreen terminal |
|
|
22
|
+
| Link between them | — | optional `goal_id` (display only) + `loopd_command_await` (explicit wake) |
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
**Pick a Goal** when the user wants something *figured out or built* across many turns with a definition of done.
|
|
25
|
+
**Pick a Command Session** when the user wants to *run and watch/interact with a process* — no AI reasoning loop needed, just stdin/stdout.
|
|
26
|
+
|
|
27
|
+
## When to Use Loopd (Goals)
|
|
28
|
+
|
|
29
|
+
Use a goal when:
|
|
17
30
|
|
|
18
31
|
- A task takes many turns and would block the main chat
|
|
19
|
-
- You want autonomous work that continues while the user does other things
|
|
32
|
+
- You want autonomous AI work that continues while the user does other things
|
|
20
33
|
- A task needs progress tracking, completion checks, or blocking
|
|
21
34
|
- You want a dashboard to monitor and steer background work
|
|
22
35
|
|
|
23
|
-
Do not use
|
|
36
|
+
Do not use a goal when:
|
|
24
37
|
|
|
25
38
|
- The task is trivial (1-3 turns)
|
|
26
39
|
- The user needs to answer questions at every step
|
|
27
40
|
- The task modifies production systems without review gates
|
|
41
|
+
- The user just wants to run/watch/type into a process — use a **Command Session** (below) instead; it has no agent overhead
|
|
28
42
|
|
|
29
43
|
## Goal Lifecycle
|
|
30
44
|
|
|
@@ -46,8 +60,8 @@ Every goal has an **immutable contract** at creation — the source of truth for
|
|
|
46
60
|
|
|
47
61
|
* **Objective** — semantic requirements (free text, self-contained). The worker derives concrete requirements from it.
|
|
48
62
|
* **Checks** — deterministic shell commands that **must pass** for `complete_goal` to be accepted. For `workspaceWrite:true` goals they are **mandatory** (explicit `checks` or plugin `defaultChecks`), and they run from `checkCwd` (writers default to project root; artifact-only jobs run from their `artifactDir`).
|
|
49
|
-
* **Agent** — any OpenCode agent name: built-in (`build`, `plan`, `explore`, `general`), file `~/.config/opencode/agents/*.md`, or `opencode.jsonc` `agent.*`. Discover with `opencode agent list`. Prefer `subagent`-mode agents for workers; `primary`-mode agents work but may expect user interaction. Resolution: explicit arg → **calling session's live agent** (read at creation) → `defaultAgent` → OpenCode global default. So a parent in Plan spawns a Plan worker unless told otherwise.
|
|
50
|
-
* **Model** — any model as `"providerID/modelID"` (e.g. `openai/gpt-5.6-sol`, `ollama/qwen3.8:27b`, `openrouter/google/gemma-4-31b-it:free`). Discover with `opencode models [provider]`. Sent on every worker prompt. Resolution: explicit arg → **calling session's live model** → `defaultModel` → agent config → session history → global default. Caveat: OpenCode exposes no live-model field for history-derived models, so those fall through to agent config.
|
|
63
|
+
* **Agent** — any OpenCode agent name: built-in (`build`, `plan`, `explore`, `general`), file `~/.config/opencode/agents/*.md`, or `opencode.jsonc` `agent.*`. Discover with `opencode agent list`. Prefer `subagent`-mode agents for workers; `primary`-mode agents work but may expect user interaction. Resolution: explicit arg → **calling session's live agent** (read at creation) → `defaultAgent` → OpenCode global default. So a parent in Plan spawns a Plan worker unless told otherwise. **Omit by default — inherit.** Do NOT pass `agent` unless the caller explicitly requests a different identity or the task needs it; explicit values freeze identity and break session upgrades.
|
|
64
|
+
* **Model** — any model as `"providerID/modelID"` (e.g. `openai/gpt-5.6-sol`, `ollama/qwen3.8:27b`, `openrouter/google/gemma-4-31b-it:free`). Discover with `opencode models [provider]`. Sent on every worker prompt. Resolution: explicit arg → **calling session's live model** → `defaultModel` → agent config → session history → global default. Caveat: OpenCode exposes no live-model field for history-derived models, so those fall through to agent config. **Omit by default — inherit.** Do NOT pass `model` unless explicitly requested.
|
|
51
65
|
* **WorkspaceWrite** — `true` (default) = may touch the shared repo; **only one active writer at a time** is allowed (enforced on `start`/`resume`/`retry` with rollback). Set `false` explicitly for artifact-only/read-only work to allow concurrency.
|
|
52
66
|
* **Budgets** — `tokenBudget` (tokens) and `costBudget` (dollars, e.g. `0.5`). When either is exceeded the engine aborts the worker mid-turn and sets `budget_limited`. The worker is stopped immediately, not just at the next turn boundary.
|
|
53
67
|
* **Limits** — `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `maxEvaluatorRejections` (default 3), `timeoutMs`, `compactEvery`, `progressFile`.
|
|
@@ -73,8 +87,9 @@ Use `loopd_create_goal` **after** clarifying the objective with the user (what /
|
|
|
73
87
|
loopd_create_goal({
|
|
74
88
|
name: "short-name",
|
|
75
89
|
objective: "Detailed description of what the goal should accomplish.",
|
|
76
|
-
agent:
|
|
77
|
-
|
|
90
|
+
// agent/model: omit by default to inherit calling session — only pass when explicitly requested
|
|
91
|
+
// agent: "researcher",
|
|
92
|
+
// model: "openai/gpt-5.6-sol",
|
|
78
93
|
costBudget: 0.5, // optional dollars; aborts worker when exceeded
|
|
79
94
|
checks: ["npm test"], // mandatory if workspaceWrite:true (or configure defaultChecks)
|
|
80
95
|
checkCwd: "/project/root", // optional; writers default to project root, readers to artifactDir
|
|
@@ -188,17 +203,167 @@ Abort the worker *session only* (`A` / `:abort` in TUI). Keeps goal+transcript+s
|
|
|
188
203
|
|
|
189
204
|
Owner-side equivalents of TUI `:force` / `:block`. Bypass checks — use only when you have verified manually that the artifact is correct or that the blocker is real.
|
|
190
205
|
|
|
206
|
+
## Command Sessions (Standalone Interactive Processes)
|
|
207
|
+
|
|
208
|
+
A **command session** is a raw OS process (spawned via `bun-pty`, falling back to a plain pipe) — no agent, no turns, no checks. It is completely independent from goals: starting/stopping one never pauses, blocks, or completes any goal, and vice versa. Use it whenever the request is "run X and let me watch/type into it" rather than "figure out/build X". By default, natural exits (including code 0), timeouts, and missing processes notify the owner; manual termination stays silent. Notification tails are sanitized for chat, while the retained terminal log remains raw. A finished command can still be opened to read its saved output; Ctrl+C/input cannot affect it.
|
|
209
|
+
|
|
210
|
+
Typical uses: dev servers (`npm run dev`), watch mode (`npm test -- --watch`), REPLs (`python3`, `node`), log tails (`tail -f`), one-off scripts, interactive shells.
|
|
211
|
+
|
|
212
|
+
### Prefer commands over the built-in shell
|
|
213
|
+
|
|
214
|
+
**Rule: if it might take a while or never return, start it as a command — never run it in the built-in shell/bash tool.** The built-in shell blocks your entire turn until the process exits: a 5-minute `npm install` freezes you mid-turn (and can hit tool timeouts, losing the output). `loopd_command_start` returns immediately with a `command_id` and pushes you on exit or pattern match, so you stay responsive.
|
|
215
|
+
|
|
216
|
+
Use a command (not the built-in shell) for: installs (`npm/pip/brew/cargo install`, downloads), builds, full test suites, migrations, dev servers, watchers, log tails, REPLs, anything interactive, or anything whose duration you can't guarantee is seconds. The built-in shell is only for quick one-shots (`ls`, `git status`, a single fast test).
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
User: install the dependencies and set up the project.
|
|
220
|
+
# WRONG: built-in shell `npm install` — blocks your turn for minutes.
|
|
221
|
+
# RIGHT:
|
|
222
|
+
Agent: loopd_command_start({ title: "npm install", command: "npm", args: ["install"], notify_on_exit: true })
|
|
223
|
+
# → returns immediately; you keep working, and get pushed when it finishes.
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Agent Tools
|
|
227
|
+
|
|
228
|
+
| Tool | Purpose |
|
|
229
|
+
|------|---------|
|
|
230
|
+
| `loopd_command_start` | Spawn `{title, command, args, cwd?, goal_id?, cols?, rows?, env?, timeout_seconds?, shell?, watch_filter?, watch_until?, watch_ignore_case?, watch_until_action?}`. Requests OpenCode's `bash` permission first. Returns `command_id` + host `capabilities` (`spawn`/`write`/`interruptSignal`/`terminate`/`resize`/`terminalEmulation`). `env` passes values to the child but persists names only (`envKeys`); `timeout_seconds` (positive integer) kills via SIGTERM→SIGKILL with `endReason: timeout` + owner notify; `shell:true` spawns via `/bin/sh -c` (POSIX-quoted join; prefer argv form for untrusted input). `watch_filter`/`watch_until` arm line-level notifications at spawn (see Watching below); invalid `watch_until_action` (anything but `"stop"`/`"keep"`) or bad regexes fail the start before spawning. |
|
|
231
|
+
| `loopd_command_list` | List commands owned by the calling session. Summaries include the watch spec (`watchFilter`/`watchUntil`/`watchIgnoreCase`/`watchUntilAction`) plus the live `watchState` snapshot (`state`/`matches`/`pushes`/`droppedLines`). |
|
|
232
|
+
| `loopd_command_watch` | Set, replace, or clear a watch on an ALREADY-RUNNING command (`{command_id, watch_filter?, watch_until?, watch_ignore_case?, watch_until_action?, clear?}`). Replacing resets counters/state to `active`. Fails closed on bad regexes (previous watch untouched) and returns `ok:false` for terminal commands (watch is meaningless after exit) or another session's commands. |
|
|
233
|
+
| `loopd_command_get` | Read status + a bounded output snapshot (`offset_bytes` to page, default 64KB/cap 256KB) — for progress checks on plain one-shot commands. For never-exiting processes prefer a watch + `loopd_command_await` (pushes come to you; no repeated reads). With `pattern` (+`ignore_case`), only regex-matching lines return (ANSI-stripped for matching, originals kept; dangerous nested-quantifier patterns are rejected) and `offset_bytes`/`limit_bytes` page over matches. |
|
|
234
|
+
| `loopd_command_write` | Send raw stdin bytes (include your own trailing `\n` for line-buffered programs). |
|
|
235
|
+
| `loopd_command_interrupt` | Deliver SIGINT (Ctrl+C as a signal). A process that traps it may keep running — that's correct, not a failure. |
|
|
236
|
+
| `loopd_command_terminate` | SIGTERM, escalating to SIGKILL. The only way a command actually stops (closing a view never does). With `remove:true`, deletes the record+log in the same call (works even when already terminal). |
|
|
237
|
+
| `loopd_command_remove` | Delete a finished command's record + log. Refuses while `running` (terminate first). |
|
|
238
|
+
| `loopd_command_resize` | Set terminal size. Applied live to the real PTY winsize when the host supports it; stored-only (never applied) on the pipe fallback — check `capabilities.resize`. |
|
|
239
|
+
| `loopd_command_await` | **The only command→goal edge.** Explicit opt-in: makes `goal_id` wake up exactly once when `command_id` reaches a terminal state (`exited`/`terminated`/`missing`), with exit code/signal/last-4KB output as evidence. With `until` (+`ignore_case`), the goal instead wakes ONCE on the first output line matching the regex (matched lines + bounded tail as evidence) and the command keeps running — for "wait until the dev server prints ready" without repeated reads. Invalid/dangerous `until` patterns are rejected; an `until` already present in the log fires immediately. Merely passing `goal_id` to `loopd_command_start` links for *display only* — it never wakes anything by itself. |
|
|
240
|
+
|
|
241
|
+
### Lifecycle
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
running → exited (process ended on its own; exit code kept; endReason: exit)
|
|
245
|
+
running → terminated (explicit terminate/kill; signal kept; endReason: terminate)
|
|
246
|
+
running → terminated (timeout_seconds deadline; endReason: timeout — always notifies the owner)
|
|
247
|
+
running → missing (plugin restart found no live execution — log retained; endReason: missing)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- **Detach ≠ terminate.** Closing the fullscreen terminal page, switching tabs, or the TUI restarting never stops the process. Only `loopd_command_terminate` (or the process exiting on its own) does.
|
|
251
|
+
- **`terminate ≠ remove`.** `remove` deletes the record + log and refuses while `running`.
|
|
252
|
+
- Every response carries the active backend's `capabilities` so you never have to guess (`terminalEmulation` is always `false` — the host captures a byte stream; the TUI's terminal page emulates a screen client-side from it).
|
|
253
|
+
|
|
254
|
+
### Monitoring in the Dashboard
|
|
255
|
+
|
|
256
|
+
`/loop` and `/commands` open the **same** shared dashboard — `/loop` focuses the Goals tab, `/commands` focuses Commands. Switch with `Tab` (toggle) or directionally `h` (Goals) / `l` (Commands); `j`/`k`/`g`/`G` select within the active tab. The Commands tab is owner-scoped (only the current session's commands).
|
|
257
|
+
|
|
258
|
+
On the Commands tab, `o` **never** opens a chat session or a dialog — it closes the popup and navigates to a dedicated fullscreen terminal page inside OpenCode (route `opencode.loopd.terminal`). There:
|
|
259
|
+
|
|
260
|
+
- Keystrokes forward immediately as raw input — no line-submit box.
|
|
261
|
+
- `Ctrl+C` is interrupt input to the *process* (never closes OpenCode).
|
|
262
|
+
- `Ctrl+]` detaches back to where you came from — the process keeps running.
|
|
263
|
+
- Paste forwards raw bytes immediately.
|
|
264
|
+
- The viewport is measured and resizes both the emulator and the real PTY.
|
|
265
|
+
|
|
266
|
+
Single-key helpers on the Commands tab: `X` force-kill (SIGKILL now, no grace — for hung processes), `R` restart with the same spec (new ID, old record kept; env values are never persisted so restarted env commands lose their env), `x` remove finished (running commands get a hint, never a kill). Colon commands (before opening fullscreen): `:new <command> [args...]` (quoted args preserved, e.g. `:new bash -c "echo hi"`), `:interrupt`, `:terminate`, `:kill`, `:restart`, `:remove`, `:await <goalID>`, and bare text + Enter writes stdin to the selected command. Goal-only keys (`p`/`r`/`A`/`N`) never fire on the Commands tab; `R`/`x` are tab-scoped (retry/clear on Goals, restart/remove-done on Commands).
|
|
267
|
+
|
|
268
|
+
### Example: Watching a Dev Server
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
User: Start the dev server and let me see the logs.
|
|
272
|
+
|
|
273
|
+
Agent: loopd_command_start({ title: "dev-server", command: "npm", args: ["run", "dev"] })
|
|
274
|
+
# → {ok:true, command:{id:"...", status:"running", ...}, capabilities:{...}}
|
|
275
|
+
Agent: "Started — open /loop, Tab to Commands, press o to watch live, or I can set a watch so it pings you on interesting lines."
|
|
276
|
+
|
|
277
|
+
# Later, to check on it without opening the TUI:
|
|
278
|
+
loopd_command_get({ command_id: "..." })
|
|
279
|
+
# → status + latest output
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Watching Long-Running Commands
|
|
283
|
+
|
|
284
|
+
For never-exiting processes (dev servers, `test --watch`, log tails), **watch instead of re-reading**: the owner gets pushed on match/exit with no repeated `loopd_command_get` calls.
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
# Arm at spawn…
|
|
288
|
+
loopd_command_start({ title: "dev", command: "npm", args: ["run", "dev"],
|
|
289
|
+
watch_filter: "ERROR|WARN", watch_until: "ready in",
|
|
290
|
+
watch_until_action: "keep" })
|
|
291
|
+
# …or attach to an already-running command:
|
|
292
|
+
loopd_command_watch({ command_id: "...", watch_filter: "ERROR|WARN" })
|
|
293
|
+
loopd_command_watch({ command_id: "...", watch_until: "ready in", watch_until_action: "keep" })
|
|
294
|
+
loopd_command_watch({ command_id: "...", clear: true }) # drop the watch
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- **`watch_filter`** (regex on ANSI-stripped lines): only matching lines buffer toward an owner push. No filter = every non-blank line counts.
|
|
298
|
+
- **`watch_until`** (regex on the filter-surviving stream — filter first, then until): first match notifies the owner immediately with the matched lines.
|
|
299
|
+
- **`watch_until_action`**: `"stop"` (default) terminates the command on first until-match (`endReason: until`, exactly one owner message total — the later exit ping is suppressed); `"keep"` notifies once but keeps running (until fires once, the filter stream continues, a later exit still notifies normally).
|
|
300
|
+
- **Replacing** a watch (calling `loopd_command_watch` again) resets counters/state to `active`. **Clearing** drops spec + counters + the in-memory watcher. Watching a terminal command returns `ok:false`. Bad regexes fail closed — the previous watch is left untouched.
|
|
301
|
+
- **Notifications are bounded** (so chatty processes can't spam you): matched lines coalesce into one push per ~2s window (≤20 lines / ≤4KB each); at most 30 pushes per command (`budget-exhausted`, one notice, process keeps running); over 100 lines/sec suspends the watch (`flood-suspended`, one notice with dropped count, process keeps running). Every push is prefixed `[watch "<title>"]`.
|
|
302
|
+
- **Dashboard**: the Commands tab shows a badge per command — `watch:filter`, `until:stop-armed` / `until:keep-armed`, `until-matched`, `flood-suspended`, `budget-exhausted` — plus the `endReason` (`timeout`/`until`/…) on terminal rows; the detail panel shows the watch spec with `matches`/`pushes`/`dropped` counters.
|
|
303
|
+
|
|
304
|
+
### Migrating from PTY Sessions (`pty_*` tools)
|
|
305
|
+
|
|
306
|
+
| Before (`pty_*`) | After (`loopd_command_*`) | Notes |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `pty_spawn({cmd, args, cwd})` | `loopd_command_start({title, command, args, cwd, env?, timeout_seconds?, shell?, notify_on_exit?, watch_filter?, watch_until?, watch_ignore_case?, watch_until_action?})` | `env` passes values but persists names only (`envKeys`); `timeout_seconds` kills via SIGTERM→SIGKILL with `endReason: timeout` and always notifies the owner (in-memory timer, never resurrected across restarts); `shell:true` joins via `/bin/sh -c`; `notify_on_exit` overrides the auto policy (auto = notify on non-zero exit, `missing`, `timeout`, or runs ≥ ~2min; quick successes stay silent) |
|
|
309
|
+
| `pty_write({id, data})` | `loopd_command_write({command_id, input})` | Same raw-stdin bytes; include your own trailing `\n` |
|
|
310
|
+
| `pty_read({id, pattern?})` | `loopd_command_get({command_id, pattern?, ignore_case?, offset_bytes?, limit_bytes?})` | `pattern` matches ANSI-stripped text, originals returned; pages over matches; dangerous patterns rejected. For continuous monitoring prefer a watch over repeated reads |
|
|
311
|
+
| `pty_list()` | `loopd_command_list()` | Owner-scoped (your session's commands only — same set as the TUI Commands tab); summaries carry `watchState` + `endReason` |
|
|
312
|
+
| `pty_kill({id, cleanup})` | `loopd_command_terminate({command_id, remove?})` | `remove:true` deletes record+log atomically (even when already terminal); plain `terminate` keeps the record for `loopd_command_remove` later |
|
|
313
|
+
| `notifyOnExit` / `timeoutSeconds` spawn opts | `notify_on_exit` / `timeout_seconds` args | Same shape, two loopd differences: the auto-notify policy above (not plain true/false), and timeout stamps `endReason: timeout` instead of a generic kill |
|
|
314
|
+
|
|
315
|
+
### Migrating from Sentinel Watches (`sentinel_monitor`)
|
|
316
|
+
|
|
317
|
+
| Sentinel | Loopd | Notes |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `filter` / `until` patterns | `watch_filter` / `watch_until` (+`watch_ignore_case`) | Same filter-first-then-until pipeline; same dangerous-regex guard; attach at start or later via `loopd_command_watch` |
|
|
320
|
+
| Steer delivery (message routed back into the agent turn) | Owner prompt push (`[watch "<title>"] …`) | Loopd notifies the owning session directly; a goal that must *act* on output should use `loopd_command_await` (`until` variant) instead |
|
|
321
|
+
| Flood/batch limits | Coalesce (~2s) + per-push bounds + 30-push budget + 100-lines/sec flood suspend | All bounded, all one-notice, process always keeps running (sentinel SIGTERMs on flood — loopd deliberately doesn't) |
|
|
322
|
+
| Until auto-stop | `watch_until_action: "stop"` (default) → `endReason: until` | `"keep"` is the explicit opt-out; `until-matched` shows as a dashboard badge either way |
|
|
323
|
+
| Shell-string spawn | `shell:true` (`command`+`args` joined POSIX-quoted via `/bin/sh -c`) | Prefer argv form for untrusted input |
|
|
324
|
+
|
|
325
|
+
### Example: A Goal That Waits On a Background Build
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
# Inside a goal worker:
|
|
329
|
+
loopd_command_start({ title: "full-build", command: "npm", args: ["run", "build"] })
|
|
330
|
+
# → command_id: "abc123"
|
|
331
|
+
loopd_command_await({ command_id: "abc123", goal_id: <this goal's id> })
|
|
332
|
+
# The worker can now idle; the engine wakes it exactly once when the build
|
|
333
|
+
# finishes, with exit code + last 4KB of output as evidence — no polling.
|
|
334
|
+
```
|
|
335
|
+
|
|
191
336
|
## Dashboard Commands
|
|
192
337
|
|
|
193
|
-
Open the dashboard with `/loop` or
|
|
338
|
+
Open the shared dashboard with `/loop` (or `<leader>o`) — focuses the **Goals** tab — or `/commands` — focuses the **Commands** tab. Same popup, same component, two entry points.
|
|
339
|
+
|
|
340
|
+
### Switching Tabs
|
|
194
341
|
|
|
195
|
-
|
|
342
|
+
| Key | Action |
|
|
343
|
+
|-----|--------|
|
|
344
|
+
| `Tab` | Toggle Goals ↔ Commands |
|
|
345
|
+
| `h` | Select Goals directionally |
|
|
346
|
+
| `l` | Select Commands directionally |
|
|
347
|
+
|
|
348
|
+
`j`/`k`/`g`/`G` select within whichever tab is active. Goal controls (`p`/`r`/`A`/`N`) only fire on the Goals tab and are refused with a hint on Commands; `R`/`x` are tab-scoped (retry/clear on Goals, restart/remove-done on Commands).
|
|
349
|
+
|
|
350
|
+
### Commands Tab — Keyboard Shortcuts
|
|
351
|
+
|
|
352
|
+
| Key | Action |
|
|
353
|
+
|-----|--------|
|
|
354
|
+
| `X` | Force-kill selected command (SIGKILL now, no grace) |
|
|
355
|
+
| `R` | Restart selected command with the same spec (new ID) |
|
|
356
|
+
| `x` | Remove selected command — finished only (running gets a hint) |
|
|
357
|
+
| `o` | Open fullscreen terminal page |
|
|
358
|
+
| `Ctrl+C` | Interrupt selected command (SIGINT delivery, never a kill) |
|
|
359
|
+
|
|
360
|
+
### Goals Tab — Keyboard Shortcuts (Normal Mode)
|
|
196
361
|
|
|
197
362
|
| Key | Action | Agent equivalent |
|
|
198
363
|
|-----|--------|-----------------|
|
|
199
364
|
| `j` / `k` | Move selection down / up | — (UI only) |
|
|
200
365
|
| `g` / `G` | Jump to top / bottom | — |
|
|
201
|
-
| `o` | Open
|
|
366
|
+
| `o` | Open the selected goal's native worker session | `read_goal_transcript` / `inspect_background_goal` |
|
|
202
367
|
| `L` | Toggle log view | — |
|
|
203
368
|
| `?` | Toggle help | — |
|
|
204
369
|
| `c` | Toggle done (show/hide completed) | — |
|
|
@@ -208,7 +373,7 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
208
373
|
| `Ctrl+N` | Switch to normal mode | — |
|
|
209
374
|
| `:` | Enter command mode | — |
|
|
210
375
|
|
|
211
|
-
### Command Mode (`:` prefix)
|
|
376
|
+
### Goals Tab — Command Mode (`:` prefix)
|
|
212
377
|
|
|
213
378
|
| Command | Description | Agent equivalent |
|
|
214
379
|
|---------|-------------|-----------------|
|
|
@@ -223,7 +388,25 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
223
388
|
|
|
224
389
|
> Goal creation (`:goal start`) was removed from the dashboard — create goals via `/goal` in the parent chat so the agent can clarify the objective first.
|
|
225
390
|
|
|
226
|
-
|
|
391
|
+
### Commands Tab — Keyboard & Command Mode
|
|
392
|
+
|
|
393
|
+
| Key / Command | Action | Agent equivalent |
|
|
394
|
+
|-----|--------|-----------------|
|
|
395
|
+
| `j` / `k` / `g` / `G` | Select a command session | — |
|
|
396
|
+
| `o` | Close the popup, open the selected command in the **fullscreen terminal page** | — |
|
|
397
|
+
| `Ctrl+C` | Interrupt (SIGINT) the selected command | `loopd_command_interrupt` |
|
|
398
|
+
| `:new <command> [args...]` | Start a new command (quoted args preserved) | `loopd_command_start` |
|
|
399
|
+
| `:interrupt` | SIGINT the selected command | `loopd_command_interrupt` |
|
|
400
|
+
| `:terminate` | SIGTERM→SIGKILL the selected command | `loopd_command_terminate` |
|
|
401
|
+
| `:kill` | SIGKILL now, no grace (same as `X`) | — (no agent equivalent; agents use `loopd_command_terminate`) |
|
|
402
|
+
| `:restart` | Stop + respawn same spec, new ID (same as `R`) | — (no agent equivalent) |
|
|
403
|
+
| `:remove` | Delete a finished command's record + log | `loopd_command_remove` |
|
|
404
|
+
| bare text + Enter | Write that line as stdin to the selected command | `loopd_command_write` |
|
|
405
|
+
| `q` | Detach (view closes; command keeps running) | — |
|
|
406
|
+
|
|
407
|
+
Inside the **fullscreen terminal page** (reached via `o`): every keystroke forwards immediately as raw input (no line-submit box), `Ctrl+C` is interrupt input to the process, `Ctrl+]` detaches back to where you came from without stopping the process. Press `?` on the Commands tab to see force-kill, restart, and removal shortcuts before opening the terminal.
|
|
408
|
+
|
|
409
|
+
**Scrolling:** both lists are a `<scrollbox>` capped at 10 rows — short lists sit compact with no gap, long lists scroll and follow `j`/`k`/`g`/`G` via `scrollChildIntoView`. Detail + input stay pinned below in both cases. Empty list shows the fallback tip.
|
|
227
410
|
|
|
228
411
|
## Safety Patterns
|
|
229
412
|
|
|
@@ -238,6 +421,7 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
238
421
|
9. **Recover stuck workers explicitly** — If `inspect` shows `phase=running` with `unknownStatusCount ≥3` or `lastActivityAt` far in the past, use `nudge_goal` (full steering) for a healthy-but-stuck worker or `abort_goal_worker` for a spinning/burning one. `send_goal_input` alone is now immediate for active goals, but it is still bare words, not steering.
|
|
239
422
|
10. **One writer, one check suite** — Don’t run concurrent `start` calls that touch the same files from parallel chats; chain them sequentially or mark the second as `workspaceWrite:false`.
|
|
240
423
|
11. **Registry is the source of truth** — `src/domain/interaction-registry.ts` lists every `LoopCommand` with its TUI and agent bindings. `test/domain/parity.test.ts` fails CI if they drift. When adding an interaction: add one row to `INTERACTIONS`, one handler in `control-worker.ts`, and both surfaces light up.
|
|
424
|
+
12. **Goal vs Command Session — pick by shape of the task, not by habit** — "Run/watch/type into a process" (dev server, test watch, REPL, log tail, one-off script) is a `loopd_command_start` job with zero agent overhead, even if it will run a long time. Reach for `loopd_create_goal` only when the task genuinely needs multi-turn AI reasoning against a checkable definition of done. Don't spin up a goal just to babysit a process — use `loopd_command_await` if a goal needs to *wait* on one.
|
|
241
425
|
|
|
242
426
|
## Example: Creating and Monitoring a Goal
|
|
243
427
|
|
|
@@ -254,8 +438,7 @@ Agent: Got it — creating with a contract.
|
|
|
254
438
|
loopd_create_goal({
|
|
255
439
|
name: "docs-write",
|
|
256
440
|
objective: "Write a README.md covering: what it is, install, usage, architecture, and examples. Then create CHANGELOG.md with a v1.0.0 entry.",
|
|
257
|
-
agent
|
|
258
|
-
model: "openai/gpt-5.6-sol",
|
|
441
|
+
// omit agent/model to inherit calling session — only pass when explicitly requested
|
|
259
442
|
costBudget: 0.5,
|
|
260
443
|
checks: ["test -f README.md", "test -f CHANGELOG.md"],
|
|
261
444
|
checkCwd: "/Users/you/project", // explicit for writers; defaults to project root
|