@bojackduy/opencode-loopd 1.10.5 → 1.12.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/CHANGELOG.md +2 -0
- package/README.md +24 -1
- package/dist/server.js +918 -23
- package/dist/tui.js +861 -674
- package/package.json +1 -1
- package/skills/loopd/SKILL.md +67 -1
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.
|
|
4
|
+
"version": "1.12.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",
|
package/skills/loopd/SKILL.md
CHANGED
|
@@ -52,11 +52,13 @@ 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
|
|
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`).
|
|
@@ -108,6 +110,65 @@ Returns `ok:true` with `goalID`, `workerSessionID`, `artifactDir`, `agent`, `mod
|
|
|
108
110
|
|
|
109
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.
|
|
110
112
|
|
|
113
|
+
## Owner Agent: Discover Models and Recover the Same Goal
|
|
114
|
+
|
|
115
|
+
### Agent listing and same-goal switching
|
|
116
|
+
|
|
117
|
+
`loopd_list_models({})` also returns `agents` (public name, description, mode: `primary`, `subagent`, or OpenCode's `all`) and independent `agentCatalog` capability/source/switching metadata. `assignedGoals` includes this owner's assigned agents and `agentSwitch` pending/last/failure state. Missing v1 `client.agent.list` reports an empty inventory with capability `unsupported`; failed discovery reports `unavailable`. Neither permits an unvalidated switch.
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
loopd_list_models({})
|
|
121
|
+
switch_goal_identity({goal_id:"existing-goal", agent:"researcher"})
|
|
122
|
+
# Only when explicitly resuming a blocked goal:
|
|
123
|
+
switch_goal_identity({goal_id:"existing-goal", agent:"researcher", resume:true})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Agent switching is owner-only and validates the exact available name. It retains goal ID, worker session, transcript, progress, inbox, checks, budgets, counters, interactive mode and topology. Busy/unknown workers queue a persisted request until the next permitted confirmed-idle turn. v2 switches the idle session via `session.switchAgent`; v1 uses that SDK API if present, otherwise reports `deferred` and sends the assignment on the next prompt. Switching alone never wakes an interactive goal, unpauses or resumes. `resume:true` is valid only for blocked goals; if still busy, it remains deferred and blocked until an explicit retry/resume when safe. Completed goals cannot switch. Pending failures block once without creating a replacement worker.
|
|
127
|
+
|
|
128
|
+
On the dashboard **Goals tab**, `:agents` lists names/modes and `:agent <name>` changes the selected goal's agent. Both are refused on the Commands tab (never forwarded as process stdin). Goal rows display `🤖 agent-name` alongside the model.
|
|
129
|
+
|
|
130
|
+
Use agent tools, not a new goal or a human dashboard action, to change a worker's provider/model:
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
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).
|
|
134
|
+
3. Select a positively usable catalog model and call `switch_goal_identity({goal_id, model:"provider/model"})`. Keep the existing goal ID and worker session. **Never spawn a duplicate goal to switch providers.**
|
|
135
|
+
|
|
136
|
+
**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.
|
|
137
|
+
|
|
138
|
+
### Manual recovery through tools
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
loopd_list_models({})
|
|
142
|
+
inspect_background_goal({goal_id:"existing-goal", includeTranscript:false})
|
|
143
|
+
switch_goal_identity({goal_id:"existing-goal", model:"connected-provider/usable-model"})
|
|
144
|
+
# If and only if the goal is blocked by a provider quota/rate limit:
|
|
145
|
+
switch_goal_identity({goal_id:"existing-goal", model:"connected-provider/usable-model", resume:true})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
### Combined identity switching
|
|
151
|
+
|
|
152
|
+
`switch_goal_identity` replaces the separate model/agent tools. Provide at least one of `model` or `agent`; omitted fields stay unchanged. To change both together:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
switch_goal_identity({goal_id:"existing-goal", model:"connected-provider/usable-model", agent:"researcher"})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Combined switches validate both catalogs before touching the host, serialize with all goal operations, and persist both assignments in one state mutation. If the second host change fails, the first is rolled back, including next-prompt assignments. If the host itself rejects rollback, the operation reports failure: inspect the existing live session before retrying; do not create a replacement. Hosts cannot expose a single atomic two-field API, so live host changes are sequential under the goal lock, with no intervening worker prompt. Busy workers persist the pair as `pendingIdentity` and apply it together at the next permitted idle turn. Discovery exposes `assignedGoals[].pendingIdentity`. A later single-field request updates that field of the pending pair. Combined `resume:true` is explicit and valid only for blocked goals; model-only resume retains the provider-limit requirement. No switch resets budgets or failure counters, replaces a worker, or implicitly wakes an interactive goal. Dashboard `:model` and `:agent` both dispatch the unified bus operation.
|
|
159
|
+
|
|
160
|
+
### Opt-in ordered fallback
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
loopd_create_goal({
|
|
164
|
+
name:"research", objective:"Research and verify the report", workspaceWrite:false,
|
|
165
|
+
fallbackModels:["provider-b/usable-model", "provider-c/usable-model"],
|
|
166
|
+
interactive:true
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
111
172
|
## Worker Tools (Running Inside the Goal)
|
|
112
173
|
|
|
113
174
|
These tools are available to the worker session doing the actual work:
|
|
@@ -384,6 +445,11 @@ Open the shared dashboard with `/loop` (or `<leader>o`) — focuses the **Goals*
|
|
|
384
445
|
| `:block <reason> --needed <text>` | Force-block the selected goal | `force_block_goal` |
|
|
385
446
|
| `:abort` | Abort worker session now (status unchanged; A) | `abort_goal_worker` |
|
|
386
447
|
| `:pause` / `:resume` / `:retry` / `:clear` | Quick controls (also p/r/R/x) | `pause_goal` / `resume_goal` / `clear_goal` |
|
|
448
|
+
| `:interactive` | Toggle manual mode (engine never starts turns) | `interactive: true` at creation |
|
|
449
|
+
| `:model <provider/model>` | Switch this goal's model (same worker/session) | `switch_goal_identity` |
|
|
450
|
+
| `:models` | Show provider/model inventory + quota status | `loopd_list_models` |
|
|
451
|
+
| `:agent <name>` | Switch this goal's agent (same worker/session; Goals tab only) | `switch_goal_identity` |
|
|
452
|
+
| `:agents` | List available agent names/modes and host capabilities (Goals tab only) | `loopd_list_models` |
|
|
387
453
|
| `:logs` / `:help` / `:q` | Toggle logs / help / close | — |
|
|
388
454
|
|
|
389
455
|
> 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.
|