@bojackduy/opencode-loopd 1.10.1 → 1.10.4
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/README.md +13 -6
- package/commands/goal.md +2 -0
- package/dist/server.js +3921 -188
- package/dist/tui.js +12128 -2274
- package/package.json +5 -1
- package/skills/loopd/SKILL.md +171 -14
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.4",
|
|
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",
|
|
@@ -87,5 +87,9 @@
|
|
|
87
87
|
"@types/node": "^26.2.0",
|
|
88
88
|
"solid-js": "^1.9.13",
|
|
89
89
|
"typescript": "^5.7.0"
|
|
90
|
+
},
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"@xterm/headless": "^6.0.0",
|
|
93
|
+
"bun-pty": "^0.4.10"
|
|
90
94
|
}
|
|
91
95
|
}
|
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
|
|
|
@@ -188,17 +202,143 @@ Abort the worker *session only* (`A` / `:abort` in TUI). Keeps goal+transcript+s
|
|
|
188
202
|
|
|
189
203
|
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
204
|
|
|
205
|
+
## Command Sessions (Standalone Interactive Processes)
|
|
206
|
+
|
|
207
|
+
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".
|
|
208
|
+
|
|
209
|
+
Typical uses: dev servers (`npm run dev`), watch mode (`npm test -- --watch`), REPLs (`python3`, `node`), log tails (`tail -f`), one-off scripts, interactive shells.
|
|
210
|
+
|
|
211
|
+
### Agent Tools
|
|
212
|
+
|
|
213
|
+
| Tool | Purpose |
|
|
214
|
+
|------|---------|
|
|
215
|
+
| `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. |
|
|
216
|
+
| `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`). |
|
|
217
|
+
| `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. |
|
|
218
|
+
| `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. |
|
|
219
|
+
| `loopd_command_write` | Send raw stdin bytes (include your own trailing `\n` for line-buffered programs). |
|
|
220
|
+
| `loopd_command_interrupt` | Deliver SIGINT (Ctrl+C as a signal). A process that traps it may keep running — that's correct, not a failure. |
|
|
221
|
+
| `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). |
|
|
222
|
+
| `loopd_command_remove` | Delete a finished command's record + log. Refuses while `running` (terminate first). |
|
|
223
|
+
| `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`. |
|
|
224
|
+
| `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. |
|
|
225
|
+
|
|
226
|
+
### Lifecycle
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
running → exited (process ended on its own; exit code kept; endReason: exit)
|
|
230
|
+
running → terminated (explicit terminate/kill; signal kept; endReason: terminate)
|
|
231
|
+
running → terminated (timeout_seconds deadline; endReason: timeout — always notifies the owner)
|
|
232
|
+
running → missing (plugin restart found no live execution — log retained; endReason: missing)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- **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.
|
|
236
|
+
- **`terminate ≠ remove`.** `remove` deletes the record + log and refuses while `running`.
|
|
237
|
+
- 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).
|
|
238
|
+
|
|
239
|
+
### Monitoring in the Dashboard
|
|
240
|
+
|
|
241
|
+
`/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).
|
|
242
|
+
|
|
243
|
+
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:
|
|
244
|
+
|
|
245
|
+
- Keystrokes forward immediately as raw input — no line-submit box.
|
|
246
|
+
- `Ctrl+C` is interrupt input to the *process* (never closes OpenCode).
|
|
247
|
+
- `Ctrl+]` detaches back to where you came from — the process keeps running.
|
|
248
|
+
- Paste forwards raw bytes immediately.
|
|
249
|
+
- The viewport is measured and resizes both the emulator and the real PTY.
|
|
250
|
+
|
|
251
|
+
Colon commands available directly on the Commands tab (before opening fullscreen): `:new <command> [args...]` (quoted args preserved, e.g. `:new bash -c "echo hi"`), `:interrupt`, `:terminate`, `:remove`, and bare text + Enter writes stdin to the selected command. Goal controls (`p`/`r`/`R`/`x`/`A`/`N`) never fire on the Commands tab.
|
|
252
|
+
|
|
253
|
+
### Example: Watching a Dev Server
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
User: Start the dev server and let me see the logs.
|
|
257
|
+
|
|
258
|
+
Agent: loopd_command_start({ title: "dev-server", command: "npm", args: ["run", "dev"] })
|
|
259
|
+
# → {ok:true, command:{id:"...", status:"running", ...}, capabilities:{...}}
|
|
260
|
+
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."
|
|
261
|
+
|
|
262
|
+
# Later, to check on it without opening the TUI:
|
|
263
|
+
loopd_command_get({ command_id: "..." })
|
|
264
|
+
# → status + latest output
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Watching Long-Running Commands
|
|
268
|
+
|
|
269
|
+
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.
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
# Arm at spawn…
|
|
273
|
+
loopd_command_start({ title: "dev", command: "npm", args: ["run", "dev"],
|
|
274
|
+
watch_filter: "ERROR|WARN", watch_until: "ready in",
|
|
275
|
+
watch_until_action: "keep" })
|
|
276
|
+
# …or attach to an already-running command:
|
|
277
|
+
loopd_command_watch({ command_id: "...", watch_filter: "ERROR|WARN" })
|
|
278
|
+
loopd_command_watch({ command_id: "...", watch_until: "ready in", watch_until_action: "keep" })
|
|
279
|
+
loopd_command_watch({ command_id: "...", clear: true }) # drop the watch
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
- **`watch_filter`** (regex on ANSI-stripped lines): only matching lines buffer toward an owner push. No filter = every non-blank line counts.
|
|
283
|
+
- **`watch_until`** (regex on the filter-surviving stream — filter first, then until): first match notifies the owner immediately with the matched lines.
|
|
284
|
+
- **`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).
|
|
285
|
+
- **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.
|
|
286
|
+
- **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>"]`.
|
|
287
|
+
- **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.
|
|
288
|
+
|
|
289
|
+
### Migrating from PTY Sessions (`pty_*` tools)
|
|
290
|
+
|
|
291
|
+
| Before (`pty_*`) | After (`loopd_command_*`) | Notes |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| `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) |
|
|
294
|
+
| `pty_write({id, data})` | `loopd_command_write({command_id, input})` | Same raw-stdin bytes; include your own trailing `\n` |
|
|
295
|
+
| `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 |
|
|
296
|
+
| `pty_list()` | `loopd_command_list()` | Owner-scoped (your session's commands only — same set as the TUI Commands tab); summaries carry `watchState` + `endReason` |
|
|
297
|
+
| `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 |
|
|
298
|
+
| `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 |
|
|
299
|
+
|
|
300
|
+
### Migrating from Sentinel Watches (`sentinel_monitor`)
|
|
301
|
+
|
|
302
|
+
| Sentinel | Loopd | Notes |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| `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` |
|
|
305
|
+
| 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 |
|
|
306
|
+
| 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) |
|
|
307
|
+
| 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 |
|
|
308
|
+
| Shell-string spawn | `shell:true` (`command`+`args` joined POSIX-quoted via `/bin/sh -c`) | Prefer argv form for untrusted input |
|
|
309
|
+
|
|
310
|
+
### Example: A Goal That Waits On a Background Build
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
# Inside a goal worker:
|
|
314
|
+
loopd_command_start({ title: "full-build", command: "npm", args: ["run", "build"] })
|
|
315
|
+
# → command_id: "abc123"
|
|
316
|
+
loopd_command_await({ command_id: "abc123", goal_id: <this goal's id> })
|
|
317
|
+
# The worker can now idle; the engine wakes it exactly once when the build
|
|
318
|
+
# finishes, with exit code + last 4KB of output as evidence — no polling.
|
|
319
|
+
```
|
|
320
|
+
|
|
191
321
|
## Dashboard Commands
|
|
192
322
|
|
|
193
|
-
Open the dashboard with `/loop` or
|
|
323
|
+
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.
|
|
324
|
+
|
|
325
|
+
### Switching Tabs
|
|
326
|
+
|
|
327
|
+
| Key | Action |
|
|
328
|
+
|-----|--------|
|
|
329
|
+
| `Tab` | Toggle Goals ↔ Commands |
|
|
330
|
+
| `h` | Select Goals directionally |
|
|
331
|
+
| `l` | Select Commands directionally |
|
|
194
332
|
|
|
195
|
-
|
|
333
|
+
`j`/`k`/`g`/`G` select within whichever tab is active. Goal controls (`p`/`r`/`R`/`x`/`A`/`N`) only fire on the Goals tab and are refused with a hint on Commands.
|
|
334
|
+
|
|
335
|
+
### Goals Tab — Keyboard Shortcuts (Normal Mode)
|
|
196
336
|
|
|
197
337
|
| Key | Action | Agent equivalent |
|
|
198
338
|
|-----|--------|-----------------|
|
|
199
339
|
| `j` / `k` | Move selection down / up | — (UI only) |
|
|
200
340
|
| `g` / `G` | Jump to top / bottom | — |
|
|
201
|
-
| `o` | Open
|
|
341
|
+
| `o` | Open the selected goal's native worker session | `read_goal_transcript` / `inspect_background_goal` |
|
|
202
342
|
| `L` | Toggle log view | — |
|
|
203
343
|
| `?` | Toggle help | — |
|
|
204
344
|
| `c` | Toggle done (show/hide completed) | — |
|
|
@@ -208,7 +348,7 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
208
348
|
| `Ctrl+N` | Switch to normal mode | — |
|
|
209
349
|
| `:` | Enter command mode | — |
|
|
210
350
|
|
|
211
|
-
### Command Mode (`:` prefix)
|
|
351
|
+
### Goals Tab — Command Mode (`:` prefix)
|
|
212
352
|
|
|
213
353
|
| Command | Description | Agent equivalent |
|
|
214
354
|
|---------|-------------|-----------------|
|
|
@@ -223,7 +363,23 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
223
363
|
|
|
224
364
|
> 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
365
|
|
|
226
|
-
|
|
366
|
+
### Commands Tab — Keyboard & Command Mode
|
|
367
|
+
|
|
368
|
+
| Key / Command | Action | Agent equivalent |
|
|
369
|
+
|-----|--------|-----------------|
|
|
370
|
+
| `j` / `k` / `g` / `G` | Select a command session | — |
|
|
371
|
+
| `o` | Close the popup, open the selected command in the **fullscreen terminal page** | — |
|
|
372
|
+
| `Ctrl+C` | Interrupt (SIGINT) the selected command | `loopd_command_interrupt` |
|
|
373
|
+
| `:new <command> [args...]` | Start a new command (quoted args preserved) | `loopd_command_start` |
|
|
374
|
+
| `:interrupt` | SIGINT the selected command | `loopd_command_interrupt` |
|
|
375
|
+
| `:terminate` | SIGTERM→SIGKILL the selected command | `loopd_command_terminate` |
|
|
376
|
+
| `:remove` | Delete a finished command's record + log | `loopd_command_remove` |
|
|
377
|
+
| bare text + Enter | Write that line as stdin to the selected command | `loopd_command_write` |
|
|
378
|
+
| `q` | Detach (view closes; command keeps running) | — |
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
**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
383
|
|
|
228
384
|
## Safety Patterns
|
|
229
385
|
|
|
@@ -238,6 +394,7 @@ Open the dashboard with `/loop` or <leader>o.
|
|
|
238
394
|
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
395
|
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
396
|
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.
|
|
397
|
+
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
398
|
|
|
242
399
|
## Example: Creating and Monitoring a Goal
|
|
243
400
|
|