@bojackduy/opencode-loopd 1.10.4 → 1.11.2

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.4",
4
+ "version": "1.11.2",
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",
@@ -52,16 +52,18 @@ created → active → complete
52
52
 
53
53
  Only `active` goals own live worker runs. `blocked`/`paused`/`budget_limited` wait for an explicit owner transition (`resume`/`retry`/`nudge`). The loop is engine-driven, not parent-driven.
54
54
 
55
+ **Manual (`interactive`) mode:** pass `interactive: true` to `loopd_create_goal` (or `:interactive` on the dashboard) for tasks where the worker stops and waits for the next input — the engine then never starts turns on its own (no idle recovery, no retry-due turns). Every turn comes from explicit steering (`:send`, nudge, resume, retry). Budget enforcement still runs. Dashboard shows `✋ MANUAL`. Toggle back with `:interactive` to re-enable auto.
56
+
55
57
  `abort_goal_worker` (TUI `A` / `abort_goal_worker` tool) aborts the worker *session only* — status stays the same, transcript remains browsable, the next active turn reuses the same session. Use it for compaction-spin or stuck runs that keep burning tokens without progress.
56
58
 
57
59
  ## Contract & Evaluation Semantics
58
60
 
59
- Every goal has an **immutable contract** at creation — the source of truth for completion:
61
+ Every goal has a **completion contract** at creation — the source of truth for completion. Model identity can be changed safely via owner tools without resetting that contract or its budgets:
60
62
 
61
63
  * **Objective** — semantic requirements (free text, self-contained). The worker derives concrete requirements from it.
62
64
  * **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`).
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.
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.
65
+ * **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.
66
+ * **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.
65
67
  * **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.
66
68
  * **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.
67
69
  * **Limits** — `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `maxEvaluatorRejections` (default 3), `timeoutMs`, `compactEvery`, `progressFile`.
@@ -87,8 +89,9 @@ Use `loopd_create_goal` **after** clarifying the objective with the user (what /
87
89
  loopd_create_goal({
88
90
  name: "short-name",
89
91
  objective: "Detailed description of what the goal should accomplish.",
90
- agent: "researcher", // any agent name; optional, inherits calling session then defaultAgent
91
- model: "openai/gpt-5.6-sol", // any provider/model; optional, inherits calling session then defaultModel
92
+ // agent/model: omit by default to inherit calling session — only pass when explicitly requested
93
+ // agent: "researcher",
94
+ // model: "openai/gpt-5.6-sol",
92
95
  costBudget: 0.5, // optional dollars; aborts worker when exceeded
93
96
  checks: ["npm test"], // mandatory if workspaceWrite:true (or configure defaultChecks)
94
97
  checkCwd: "/project/root", // optional; writers default to project root, readers to artifactDir
@@ -107,6 +110,40 @@ Returns `ok:true` with `goalID`, `workerSessionID`, `artifactDir`, `agent`, `mod
107
110
 
108
111
  The goal starts immediately. The user can monitor it via `/loop` (<leader>o). Plugin options `defaultAgent` / `defaultModel` / `defaultChecks` in `opencode.jsonc` can supply defaults so callers don’t have to repeat them.
109
112
 
113
+ ## Owner Agent: Discover Models and Recover the Same Goal
114
+
115
+ Use agent tools, not a new goal or a human dashboard action, to change a worker's provider/model:
116
+
117
+ 1. `loopd_list_models({})` returns public provider/model IDs, names, usable/connected status, context/output limits, host switching capability, and identities of **this owner session's** goals.
118
+ 2. `inspect_background_goal({goal_id, includeTranscript:false})` shows assigned model, `modelSwitch` (pending/last/failure), `modelFallback` (attempted/status), and `lastProviderLimit` (local observation source/time/model/status/retryAt).
119
+ 3. Select a positively usable catalog model and call `switch_goal_model({goal_id, model:"provider/model"})`. Keep the existing goal ID and worker session. **Never spawn a duplicate goal to switch providers.**
120
+
121
+ **Quota caveat:** the inspected v1/v2 host catalogs do not expose remaining provider quota or reset times. Results explicitly say `quota.status:"unknown"` and `quota.capability:"unsupported"`, with source and observation time. Connected/enabled credentials do **not** establish balance. `lastProviderLimit` is an observed 429/quota failure, not live balance data; `retryAt` is derived only from a supplied Retry-After value, not a fabricated quota reset. Request headers, auth tokens, keys, and raw provider records are not returned. Older/missing host APIs report unsupported; failed catalog requests report unavailable and cannot validate switches.
122
+
123
+ ### Manual recovery through tools
124
+
125
+ ```text
126
+ loopd_list_models({})
127
+ inspect_background_goal({goal_id:"existing-goal", includeTranscript:false})
128
+ switch_goal_model({goal_id:"existing-goal", model:"connected-provider/usable-model"})
129
+ # If and only if the goal is blocked by a provider quota/rate limit:
130
+ switch_goal_model({goal_id:"existing-goal", model:"connected-provider/usable-model", resume:true})
131
+ ```
132
+
133
+ Switch results distinguish `applied`, `deferred`, and `unsupported`. Busy/retrying/unconfirmable workers defer until a confirmed-idle continuation; there is no implicit abort. v1 supports explicit model identity on the **next prompt** (reported deferred); v2 supports an idle-session model switch. Future prompts and compaction use the switched assignment. Default switching does not unpause, resume, or wake interactive goals. `resume:true` explicitly starts a permitted turn only for a provider-limit-blocked goal, never paused, budget-limited, or unrelated-blocked goals. If the worker is still busy, the switch remains deferred and resume must be requested again when safe. Budgets, failure/rejection counters, checks, progress, inbox, topology, transcript, and session ID are preserved.
134
+
135
+ ### Opt-in ordered fallback
136
+
137
+ ```text
138
+ loopd_create_goal({
139
+ name:"research", objective:"Research and verify the report", workspaceWrite:false,
140
+ fallbackModels:["provider-b/usable-model", "provider-c/usable-model"],
141
+ interactive:true
142
+ })
143
+ ```
144
+
145
+ First discover and validate the desired alternatives. `fallbackModels` defaults empty; at most 16 entries, deduplicated in order. No arbitrary paid/provider switch is made. Only positively classified provider quota/rate-limit failures prepare an alternative; network/authentication/context errors, loop turn/cost limits, and user pauses do not. Each configured alternative is considered once across restarts, avoiding depleted-provider cycles. Unsupported/unavailable/exhausted fallback status is visible in inspection. Normal active goals apply a prepared alternative on the next permitted retry; blocked goals still require explicit retry/resume. Interactive goals may record/prepare fallback but **never automatically start another turn**: the owner must send input, nudge, retry, or explicitly resume. A rejected pending switch blocks safely rather than endlessly retrying or creating a replacement worker; inspect, select a usable model, switch, and retry the same goal.
146
+
110
147
  ## Worker Tools (Running Inside the Goal)
111
148
 
112
149
  These tools are available to the worker session doing the actual work:
@@ -204,10 +241,24 @@ Owner-side equivalents of TUI `:force` / `:block`. Bypass checks — use only wh
204
241
 
205
242
  ## Command Sessions (Standalone Interactive Processes)
206
243
 
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".
244
+ 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.
208
245
 
209
246
  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
247
 
248
+ ### Prefer commands over the built-in shell
249
+
250
+ **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.
251
+
252
+ 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).
253
+
254
+ ```
255
+ User: install the dependencies and set up the project.
256
+ # WRONG: built-in shell `npm install` — blocks your turn for minutes.
257
+ # RIGHT:
258
+ Agent: loopd_command_start({ title: "npm install", command: "npm", args: ["install"], notify_on_exit: true })
259
+ # → returns immediately; you keep working, and get pushed when it finishes.
260
+ ```
261
+
211
262
  ### Agent Tools
212
263
 
213
264
  | Tool | Purpose |
@@ -248,7 +299,7 @@ On the Commands tab, `o` **never** opens a chat session or a dialog — it close
248
299
  - Paste forwards raw bytes immediately.
249
300
  - The viewport is measured and resizes both the emulator and the real PTY.
250
301
 
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.
302
+ 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).
252
303
 
253
304
  ### Example: Watching a Dev Server
254
305
 
@@ -330,7 +381,17 @@ Open the shared dashboard with `/loop` (or `<leader>o`) — focuses the **Goals*
330
381
  | `h` | Select Goals directionally |
331
382
  | `l` | Select Commands directionally |
332
383
 
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.
384
+ `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).
385
+
386
+ ### Commands Tab — Keyboard Shortcuts
387
+
388
+ | Key | Action |
389
+ |-----|--------|
390
+ | `X` | Force-kill selected command (SIGKILL now, no grace) |
391
+ | `R` | Restart selected command with the same spec (new ID) |
392
+ | `x` | Remove selected command — finished only (running gets a hint) |
393
+ | `o` | Open fullscreen terminal page |
394
+ | `Ctrl+C` | Interrupt selected command (SIGINT delivery, never a kill) |
334
395
 
335
396
  ### Goals Tab — Keyboard Shortcuts (Normal Mode)
336
397
 
@@ -359,6 +420,9 @@ Open the shared dashboard with `/loop` (or `<leader>o`) — focuses the **Goals*
359
420
  | `:block <reason> --needed <text>` | Force-block the selected goal | `force_block_goal` |
360
421
  | `:abort` | Abort worker session now (status unchanged; A) | `abort_goal_worker` |
361
422
  | `:pause` / `:resume` / `:retry` / `:clear` | Quick controls (also p/r/R/x) | `pause_goal` / `resume_goal` / `clear_goal` |
423
+ | `:interactive` | Toggle manual mode (engine never starts turns) | `interactive: true` at creation |
424
+ | `:model <provider/model>` | Switch this goal's model (same worker/session) | `switch_goal_model` |
425
+ | `:models` | Show provider/model inventory + quota status | `loopd_list_models` |
362
426
  | `:logs` / `:help` / `:q` | Toggle logs / help / close | — |
363
427
 
364
428
  > 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.
@@ -373,11 +437,13 @@ Open the shared dashboard with `/loop` (or `<leader>o`) — focuses the **Goals*
373
437
  | `:new <command> [args...]` | Start a new command (quoted args preserved) | `loopd_command_start` |
374
438
  | `:interrupt` | SIGINT the selected command | `loopd_command_interrupt` |
375
439
  | `:terminate` | SIGTERM→SIGKILL the selected command | `loopd_command_terminate` |
440
+ | `:kill` | SIGKILL now, no grace (same as `X`) | — (no agent equivalent; agents use `loopd_command_terminate`) |
441
+ | `:restart` | Stop + respawn same spec, new ID (same as `R`) | — (no agent equivalent) |
376
442
  | `:remove` | Delete a finished command's record + log | `loopd_command_remove` |
377
443
  | bare text + Enter | Write that line as stdin to the selected command | `loopd_command_write` |
378
444
  | `q` | Detach (view closes; command keeps running) | — |
379
445
 
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.
446
+ 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.
381
447
 
382
448
  **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.
383
449
 
@@ -411,8 +477,7 @@ Agent: Got it — creating with a contract.
411
477
  loopd_create_goal({
412
478
  name: "docs-write",
413
479
  objective: "Write a README.md covering: what it is, install, usage, architecture, and examples. Then create CHANGELOG.md with a v1.0.0 entry.",
414
- agent: "researcher",
415
- model: "openai/gpt-5.6-sol",
480
+ // omit agent/model to inherit calling session — only pass when explicitly requested
416
481
  costBudget: 0.5,
417
482
  checks: ["test -f README.md", "test -f CHANGELOG.md"],
418
483
  checkCwd: "/Users/you/project", // explicit for writers; defaults to project root