@bojackduy/opencode-loopd 1.9.2 → 1.9.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/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.9.2",
4
+ "version": "1.9.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",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "scripts": {
64
64
  "typecheck": "tsc --noEmit",
65
- "build:server": "bun build src/server/plugin.ts --outfile dist/server.js --target bun --external @opencode-ai/plugin --external @opencode-ai/plugin/tool",
65
+ "build:server": "bun build src/server/plugin.ts --outfile dist/server.js --target bun --external @opencode-ai/plugin --external @opencode-ai/plugin/tool --external @opencode-ai/plugin/v2/promise",
66
66
  "build:tui": "bun scripts/build-tui.ts",
67
67
  "build": "bun run build:server && bun run build:tui",
68
68
  "clean": "rm -rf dist",
@@ -73,13 +73,13 @@
73
73
  "release:major": "bun run typecheck && bun test && npm version major -m \"chore: release %s\" && git push && git push origin $(git describe --tags --abbrev=0)"
74
74
  },
75
75
  "peerDependencies": {
76
- "@opencode-ai/plugin": ">=1.4.0 <2",
76
+ "@opencode-ai/plugin": ">=1.4.0 <3",
77
77
  "@opentui/core": "*",
78
78
  "@opentui/solid": "*",
79
79
  "solid-js": "*"
80
80
  },
81
81
  "devDependencies": {
82
- "@opencode-ai/plugin": "^1.18.15",
82
+ "@opencode-ai/plugin": "^1.18.30",
83
83
  "@opentui/core": "^0.4.1",
84
84
  "@opentui/solid": "^0.4.1",
85
85
  "@types/bun": "^1.3.14",
@@ -2,7 +2,7 @@
2
2
  name: loopd
3
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."
4
4
  metadata:
5
- version: "1.0.0"
5
+ version: "1.1.0"
6
6
  status: active
7
7
  tags: [opencode, loop, goal, background, automation]
8
8
  ---
@@ -38,26 +38,32 @@ created → active → complete
38
38
 
39
39
  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.
40
40
 
41
+ `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.
42
+
41
43
  ## Contract & Evaluation Semantics
42
44
 
43
45
  Every goal has an **immutable contract** at creation — the source of truth for completion:
44
46
 
45
47
  * **Objective** — semantic requirements (free text, self-contained). The worker derives concrete requirements from it.
46
48
  * **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`).
47
- * **Agent** — which subagent model runs the worker (`sloppy-agent`, `smart-agent`, …). Required unless `defaultAgent` is configured in `opencode.jsonc` plugin options.
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. Optional — falls back to parent session's agent, or plugin `defaultAgent`.
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; omit to use the agent/session default, or configure `defaultModel` in `opencode.jsonc`.
48
51
  * **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.
49
- * **Limits** — `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `maxEvaluatorRejections` (default 3), `timeoutMs`, `compactEvery`, `progressFile`.
52
+ * **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
+ * **Limits** — `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `maxEvaluatorRejections` (default 3), `timeoutMs`, `compactEvery`, `progressFile`.
50
54
 
51
55
  **Who decides completion:**
52
56
  * **Host is the acceptance authority** — it runs `checks` deterministically. If any check fails, `complete_goal` is **rejected** (`ok:false`, `rejectionCount++`, `freeRetryPending=true` for <3 rejections, `blocked` after 3). The worker gets the exact failure in the next steering.
53
57
  * **Model is the proposer** — it must self-audit via `## COMPLETION REVIEW` (derive requirements → locate evidence → judge `proves|contradicts|incomplete|missing`) and only call `complete_goal` when every requirement is proved. If objective and checks conflict, it must `block_goal`, not silently violate either.
54
58
 
55
- **Hardened loop guarantees (1da88dc):**
56
- * **Prompt correlation** — each turn’s prompt gets a `msg-` UUID (`activePromptMessageID`); the engine correlates `message.updated`/`message.part.updated` by that ID and by `runGeneration`. Stale events never release a newer lease.
57
- * **Generation fencing** — `idleCandidateAt` is tied to `idleCandidateGeneration`; a new prompt clears the candidate. Finalization also requires the transcript anchor: the latest user prompt must be the engine’s prompt **and** its assistant response must be completed.
58
- * **Maintenance (30s)** — auto-repairs `phase=idle + activeRunID` (stale lease), bounds `unknown` status polls (`unknownStatusCount` threshold 3 → one `notifyOwner` per episode, `workerUnreachableNotifiedAt` deduped), and recovers counters on success. `active`-only polling; `blocked`/`paused` never auto-continue.
59
- * **Per-goal mutex** — `withGoalOperation(goalID)` serializes `continueTurn`/`pause`/`resume`/`retry`/`clear`/`nudge` per goal, and `turn.acquire` re-checks `leaseIsValid` inside the transaction for exclusivity.
60
- * **Lease** — `acquireLease`/`releaseLease` + `timeoutMs` (default 5 min); prompt failures release the lease and schedule `waiting_retry` with exponential backoff.
59
+ **Hardened loop guarantees:**
60
+ * **Prompt correlation** — each turn’s prompt gets a `msg-` UUID (`activePromptMessageID`); the engine correlates `message.updated`/`message.part.updated` by that ID and by `runGeneration`. Stale events never release a newer lease. After an assistant message completes, trailing `part.updated` events no longer reset activity (they are rendering echoes, not work).
61
+ * **Generation fencing** — `idleCandidateAt` is tied to `idleCandidateGeneration`; a new prompt clears the candidate. Finalization also requires the transcript anchor: the latest user prompt must be the engine’s prompt **and** its assistant response must be completed — unless the event anchor (`activeAssistantCompletedAt`) already proved completion, which covers long turns whose prompt aged out of the tail window.
62
+ * **Idle-unconfirmed & stuck watchdogs** — `idle.confirm-failed` emits once per generation when a turn cannot confirm. If an idle-but-unconfirmable turn stays quiet for `idleUnconfirmedMs` (default 5m), the engine emits `run.stuck` and notifies once. If a `busy`/`retry` turn stays quiet past `stuckRunningMs` (default 10m) with an expired lease, it also notifies once. Both are notify-only; a separate auto-recovery path self-heals `running`-phase stalls on idle workers after `idleRecoverMs` (default 3m) by generation-fenced lease release and continuation (same as a manual `nudge`).
63
+ * **Maintenance (30s)** — continuously folds live worker usage (tokens/cost/time) into goal totals, enforces budgets mid-turn (abort + `budget_limited`), auto-repairs `phase=idle + activeRunID` (stale lease), bounds `unknown` status polls (`unknownStatusCount` threshold 3 → one `notifyOwner` per episode, `workerUnreachableNotifiedAt` deduped), and recovers counters on success. `active`-only polling; `blocked`/`paused` never auto-continue.
64
+ * **Per-goal mutex** — `withGoalOperation(goalID)` serializes `continueTurn`/`pause`/`resume`/`retry`/`clear`/`nudge`/`abortWorker`/`sendUserMessage` per goal, and `turn.acquire` re-checks `leaseIsValid` inside the transaction for exclusivity.
65
+ * **Lease** — `acquireLease`/`releaseLease` + `timeoutMs` (default 5 min); prompt failures release the lease and schedule `waiting_retry` with exponential backoff. Terminal-class prompt errors (context overflow, unknown model/agent, auth) block immediately and abort the session instead of retrying.
66
+ * **Interaction registry** — `src/domain/interaction-registry.ts` is the single source of truth. Every TUI key/slash and every agent tool for the same `LoopCommand` is defined once; `test/domain/parity.test.ts` fails CI if they drift.
61
67
 
62
68
  ## Creating a Goal
63
69
 
@@ -67,7 +73,9 @@ Use `loopd_create_goal` **after** clarifying the objective with the user (what /
67
73
  loopd_create_goal({
68
74
  name: "short-name",
69
75
  objective: "Detailed description of what the goal should accomplish.",
70
- agent: "smart-agent", // required unless plugin defaultAgent is configured
76
+ agent: "researcher", // any agent name; optional, falls back to parent/defaultAgent
77
+ model: "openai/gpt-5.6-sol", // any provider/model; optional, falls back to parent/defaultModel
78
+ costBudget: 0.5, // optional dollars; aborts worker when exceeded
71
79
  checks: ["npm test"], // mandatory if workspaceWrite:true (or configure defaultChecks)
72
80
  checkCwd: "/project/root", // optional; writers default to project root, readers to artifactDir
73
81
  workspaceWrite: true, // default true; set false explicitly for artifact-only/read-only
@@ -81,9 +89,9 @@ loopd_create_goal({
81
89
  })
82
90
  ```
83
91
 
84
- Returns `ok:true` with `goalID`, `workerSessionID`, `artifactDir`, `agent`, `checks`, `workspaceWrite`, `defaultsApplied:{agent,checks}`. On contract violation you get `ok:false` with `errorCode: "missing_agent"` or `"missing_checks"` or `"already active"` (writer serialization).
92
+ Returns `ok:true` with `goalID`, `workerSessionID`, `artifactDir`, `agent`, `model`, `costBudget`, `checks`, `workspaceWrite`, `defaultsApplied:{agent,model,checks}`. On contract violation you get `ok:false` with `errorCode: "missing_agent"` or `"missing_checks"` or `"already active"` (writer serialization) or `"invalid_cost_budget"`.
85
93
 
86
- The goal starts immediately. The user can monitor it via `/loop` (<leader>o). Plugin options `defaultAgent` / `defaultChecks` in `opencode.jsonc` can supply defaults so callers don’t have to repeat them.
94
+ 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.
87
95
 
88
96
  ## Worker Tools (Running Inside the Goal)
89
97
 
@@ -127,23 +135,19 @@ block_goal({
127
135
  })
128
136
  ```
129
137
 
130
- ### Worker questions vs send_goal_input
131
-
132
- | Tool | Who calls it | Direction | When to use |
133
- |------|-------------|-----------|-------------|
134
- | `question` (OpenCode builtin) | Worker (inside goal) | Worker → User | Worker needs info only the user can provide. Question appears in the TUI footer as a blocker tab. |
135
- | `send_goal_input` | Owner (parent chat) | User → Worker | Owner sends an instruction, answer, or redirect to the worker. |
138
+ ### Worker questions vs owner steering
136
139
 
137
- **question** — The worker uses OpenCode's built-in `question` tool when it's genuinely stuck or ambiguous. The question appears in the TUI footer (blocker tab) and the user answers there. No goal status change needed — the worker stays busy until answered.
140
+ | Tool | Who calls it | Direction | Turns | When to use |
141
+ |------|-------------|-----------|-------|-------------|
142
+ | `question` (OpenCode builtin) | Worker (inside goal) | Worker → User | — | Worker needs info only the user can provide. Question appears in the TUI footer as a blocker tab. |
143
+ | `send_goal_input` | Owner (parent chat) | User → Worker | **Bare, immediate** | Owner sends short words as their own turn — no steering wrapper, delivered now if active, queued if paused/blocked. For lightweight redirects. |
144
+ | `nudge_goal` | Owner (parent chat) | User → Worker | **Full steering** | Owner forces a re-prompt with the full continuation prompt (contract + progress + verification). Use when the worker is stuck but healthy. |
138
145
 
139
- **send_goal_input** — The owner calls this to steer the worker. Messages are injected into the worker's next continuation prompt. Use it to:
140
- - Redirect the worker to a different approach
141
- - Refine scope or add constraints
142
- - Provide missing context
146
+ **Two prompt kinds:** engine/nudge turns carry full steering (contract, history, verification); `:send` / `send_goal_input` turns carry *only* your words. Short directives therefore fit small-context models where a 100k steering bundle would overflow.
143
147
 
144
148
  ## Owner Tools (Parent Chat)
145
149
 
146
- All are filtered by `ownerSessionID` (only the session that created the goal sees it). They are the **only** way to recover a stuck worker — they mutate the engine state (Entity B) and, when needed, force the bridge to the real subagent session (Entity A).
150
+ All are filtered by `ownerSessionID` (only the session that created the goal sees it). They are the **parity surface** for TUI — every TUI action has an equivalent tool here, enforced by the interaction registry.
147
151
 
148
152
  ### list_background_goals
149
153
 
@@ -151,7 +155,7 @@ Lists all `active` goals owned by this session (name, status, phase, turn, last
151
155
 
152
156
  ### inspect_background_goal
153
157
 
154
- Shows detailed contract + runtime: `objective`, `config{agent,checks,checkCwd,workspaceWrite,limits,artifactDir}`, `lastProgress`, `completionEvidence`, `blocker`, `runtime{phase,runCount,budgetTurnCount,runGeneration,evaluatorRejectionCount,lastActivityAt,activePromptMessageID,unknownStatusCount}`.
158
+ Shows detailed contract + runtime: `objective`, `config{agent,model,checks,checkCwd,workspaceWrite,limits,artifactDir,costBudget}`, `lastProgress`, `completionEvidence`, `blocker`, `runtime{phase,runCount,budgetTurnCount,runGeneration,evaluatorRejectionCount,lastActivityAt,activePromptMessageID,unknownStatusCount,workerAbortedAt}`. Plus live `transcriptTail`, `activeToolCallIDs`, `progressHistory`, `artifactSummary`, `pendingInbox`.
155
159
 
156
160
  ### read_goal_transcript
157
161
 
@@ -159,11 +163,19 @@ Reads the last N messages from the worker session (`role`, `content`, `timestamp
159
163
 
160
164
  ### send_goal_input
161
165
 
162
- Appends to the goal’s inbox file; the next `continueTurn` injects it as `## USER INSTRUCTIONS`. **Does not itself re-prompt** — the engine does on next idle/maintenance.
166
+ Sends bare words as their own turn (no steering wrapper). Delivers immediately if the goal is active, otherwise queues for the next active turn.
167
+
168
+ ```
169
+ send_goal_input({ goal_id: "…", message: "use X, not Y" })
170
+ ```
163
171
 
164
172
  ### nudge_goal
165
173
 
166
- Force re-prompts a stuck worker even if `sessionStatus` is not `idle`. Clears stale `activeRunID`/`idleCandidateAt`/`activePromptMessageID`/lease, sets `phase=idle`, and calls `continueTurn({force:true})`. Use when `unknownStatusCount` ≥3 or the worker is `running` with no activity. Returns `{ok, message}`.
174
+ Force re-prompts a stuck active worker with **full steering** even if `sessionStatus` is not `idle`. Clears stale `activeRunID`/`idleCandidateAt`/`activePromptMessageID`/lease, sets `phase=idle`, and calls `continueTurn({force:true})`. Use when the worker is `running` with no activity or the dashboard shows a stuck/worker-aborted state. Returns `{ok, message}`.
175
+
176
+ ### abort_goal_worker
177
+
178
+ Abort the worker *session only* (`A` / `:abort` in TUI). Keeps goal+transcript+session browsable, clears the run lease. Use for compaction-spin or stuck runs that keep burning tokens without progress. Status unchanged; active goals resume next turn in the same session.
167
179
 
168
180
  ### pause_goal / resume_goal / retry / clear
169
181
 
@@ -172,52 +184,60 @@ Force re-prompts a stuck worker even if `sessionStatus` is not `idle`. Clears st
172
184
  * `retry` (via `resume` on `blocked`) — `blocked → active`, resets `consecutiveFailures`/`forceFinishRequested`.
173
185
  * `clear_goal` — aborts worker and removes `goal` + `runtime` + ledger entry (cannot be undone).
174
186
 
187
+ ### force_complete_goal / force_block_goal
188
+
189
+ 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
+
175
191
  ## Dashboard Commands
176
192
 
177
193
  Open the dashboard with `/loop` or <leader>o.
178
194
 
179
195
  ### Keyboard Shortcuts (Normal Mode)
180
196
 
181
- | Key | Action |
182
- |-----|--------|
183
- | `j` / `k` | Move selection down / up |
184
- | `g` / `G` | Jump to top / bottom |
185
- | `o` | Open child (view details) |
186
- | `L` | Toggle log view |
187
- | `?` | Toggle help |
188
- | `Ctrl+N` | Switch to normal mode |
189
- | `:` | Enter command mode |
197
+ | Key | Action | Agent equivalent |
198
+ |-----|--------|-----------------|
199
+ | `j` / `k` | Move selection down / up | — (UI only) |
200
+ | `g` / `G` | Jump to top / bottom | — |
201
+ | `o` | Open child (view details) | `read_goal_transcript` / `inspect_background_goal` |
202
+ | `L` | Toggle log view | — |
203
+ | `?` | Toggle help | — |
204
+ | `c` | Toggle done (show/hide completed) | — |
205
+ | `p` / `r` / `R` / `x` | Pause / resume / retry / clear | `pause_goal` / `resume_goal` / `clear_goal` |
206
+ | `A` | Abort worker only (N / :abort) | `abort_goal_worker` |
207
+ | `N` | Nudge with full steering | `nudge_goal` |
208
+ | `Ctrl+N` | Switch to normal mode | — |
209
+ | `:` | Enter command mode | — |
190
210
 
191
211
  ### Command Mode (`:` prefix)
192
212
 
193
- | Command | Description |
194
- |---------|-------------|
195
- | `:send <message>` | Send instruction to the selected goal's worker |
196
- | `:open` | Open the selected goal's child session |
197
- | `:force <summary> --evidence <text>` | Force-complete (bypass verification checks) |
198
- | `:block <reason> --needed <text>` | Force-block the selected goal |
199
- | `:pause` | Pause the selected goal |
200
- | `:resume` | Resume the paused goal |
201
- | `:retry` | Retry the blocked goal |
202
- | `:clear` | Clear the selected goal |
203
- | `:logs` | Toggle log view |
204
- | `:help` | Show help |
205
- | `:q` / `:close` | Close dashboard |
213
+ | Command | Description | Agent equivalent |
214
+ |---------|-------------|-----------------|
215
+ | `:send <message>` | Send bare words now as their own turn (no steering) | `send_goal_input` |
216
+ | `:nudge` | Force re-prompt now with full steering | `nudge_goal` |
217
+ | `:open` | Open the selected goal's child session | — |
218
+ | `:force <summary> --evidence <text>` | Force-complete (bypass verification checks) | `force_complete_goal` |
219
+ | `:block <reason> --needed <text>` | Force-block the selected goal | `force_block_goal` |
220
+ | `:abort` | Abort worker session now (status unchanged; A) | `abort_goal_worker` |
221
+ | `:pause` / `:resume` / `:retry` / `:clear` | Quick controls (also p/r/R/x) | `pause_goal` / `resume_goal` / `clear_goal` |
222
+ | `:logs` / `:help` / `:q` | Toggle logs / help / close | — |
206
223
 
207
224
  > 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.
208
225
 
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.
227
+
209
228
  ## Safety Patterns
210
229
 
211
- 1. **Always call `get_goal` first** — Read the full contract (objective + checks + limits) before doing any work. The `COMPLETION REVIEW` and `HOST VERDICT` in steering are authoritative.
230
+ 1. **Always call `get_goal` first** — Read the full contract (objective + checks + limits + budgets) before doing any work. The `COMPLETION REVIEW` and `HOST VERDICT` in steering are authoritative.
212
231
  2. **Progress after durable changes** — Call `report_goal_progress` after file writes or verifications (resets `consecutiveFailures`/`noProgressCount`), not after thinking.
213
232
  3. **Complete with evidence — host decides** — Never call `complete_goal` without concrete proof. The host will reject it if `checks` fail; you’ll get `Rejection #N` with exact `stderr` and a **free retry** (`budgetTurnCount` not charged for N<3). After 3 rejections the goal is `blocked`.
214
233
  4. **Block for real blockers only** — Don’t block for things you can figure out. If objective and `checks` appear contradictory, `block_goal` — don’t silently violate either.
215
234
  5. **Use checks for verification — mandatory for writers** — `workspaceWrite:true` goals **require** `checks` (explicit or `defaultChecks`). Checks run from `checkCwd` (writers → project root by default). `["npm test","bun run typecheck"]` is a good default.
216
235
  6. **Serialize workspace edits** — Keep `workspaceWrite:true` (the default) for code/repo changes. The engine allows **only one active writer**; a second `start`/`resume`/`retry` fails with `already active`. Use `workspaceWrite:false` explicitly for artifact-only research to allow concurrency.
217
- 7. **Set safety budgets** — `maxTurns` (default 50), `maxNoProgress`, `maxFailures` prevent runaways. On `maxTurns`/`maxNoProgress` the engine injects `FINAL REPORT REQUIRED`; if ignored, it `blocked (force-finish ignored)` after the next idle.
236
+ 7. **Set safety budgets** — `maxTurns` (default 50), `costBudget` (dollars), `tokenBudget` (tokens), `maxNoProgress`, `maxFailures` prevent runaways. Budgets are enforced **continuously** in maintenance, not just at turn boundaries — the worker is aborted mid-turn when exceeded. On `maxTurns`/`maxNoProgress` the engine injects `FINAL REPORT REQUIRED`; if ignored, it `blocked (force-finish ignored)` after the next idle.
218
237
  8. **Compact periodically** — `compactEvery` keeps the worker session’s context manageable.
219
- 9. **Recover stuck workers explicitly** — If `inspect` shows `phase=running` with `unknownStatusCount ≥3` or `lastActivityAt` far in the past, use `nudge_goal` (force re-prompt) or `pause`/`resume`. `send_goal_input` alone does not re-prompt.
238
+ 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.
220
239
  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
+ 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.
221
241
 
222
242
  ## Example: Creating and Monitoring a Goal
223
243
 
@@ -234,7 +254,9 @@ Agent: Got it — creating with a contract.
234
254
  loopd_create_goal({
235
255
  name: "docs-write",
236
256
  objective: "Write a README.md covering: what it is, install, usage, architecture, and examples. Then create CHANGELOG.md with a v1.0.0 entry.",
237
- agent: "smart-agent",
257
+ agent: "researcher",
258
+ model: "openai/gpt-5.6-sol",
259
+ costBudget: 0.5,
238
260
  checks: ["test -f README.md", "test -f CHANGELOG.md"],
239
261
  checkCwd: "/Users/you/project", // explicit for writers; defaults to project root
240
262
  workspaceWrite: true, // default true — serialized
@@ -257,3 +279,16 @@ get_goal() → reads objective
257
279
  # ... writes CHANGELOG.md
258
280
  complete_goal({ summary: "Docs complete", evidence: "README.md and CHANGELOG.md exist" })
259
281
  ```
282
+
283
+ **Owner steering mid-run (either transport):**
284
+ ```
285
+ # TUI: :send use pnpm, not npm
286
+ # Agent: send_goal_input({ message: "use pnpm, not npm" })
287
+ # → bare turn, delivered now, no steering wrapper — fits small contexts
288
+
289
+ # TUI: N or :nudge ↔ Agent: nudge_goal()
290
+ # → full steering turn, forced even if not idle
291
+
292
+ # TUI: A or :abort ↔ Agent: abort_goal_worker()
293
+ # → kill the session only, keep transcript, next active turn reuses it
294
+ ```