@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/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.3",
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",
@@ -1,30 +1,44 @@
1
1
  ---
2
2
  name: loopd
3
- description: "Agent skill for loopd background goals. Teaches when to create goals, how to inspect/steer/pause them, and how to send instructions to workers."
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.2.0"
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 runs long-running or autonomous tasks as background goals, each with a dedicated worker session. The parent chat stays interactive while work happens behind the scenes.
12
+ Loopd gives OpenCode two independent background primitives that share one dashboard:
13
13
 
14
- ## When to Use Loopd
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
- Use loopd when:
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 loopd when:
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: "researcher", // any agent name; optional, inherits calling session then defaultAgent
77
- model: "openai/gpt-5.6-sol", // any provider/model; optional, inherits calling session then defaultModel
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 <leader>o.
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
- ### Keyboard Shortcuts (Normal Mode)
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 child (view details) | `read_goal_transcript` / `inspect_background_goal` |
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
- **Scrolling:** the goal list is 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.
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: "researcher",
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