@bojackduy/opencode-loopd 1.9.3 → 1.9.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/commands/goal.md CHANGED
@@ -20,7 +20,8 @@ When you have enough to write a concrete contract:
20
20
  - `workspaceWrite` — default `true` (safe — may touch the shared repo; only one active writer allowed — second `start`/`resume` fails with `already active`); set `false` explicitly for artifact-only/read-only work to allow concurrency
21
21
  - `checkCwd` — optional directory where `checks` run; writers default to project root, artifact-only jobs default to their `artifactDir`
22
22
  - `progressFile` — optional path to a markdown progress file (defaults to `<artifactDir>/progress.md`)
23
- - limits — optional `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `compactEvery`, `timeoutMs`
23
+ - `costBudget` — max dollars before `budget_limited` (e.g. `0.5`); enforced continuously, worker aborted mid-turn when exceeded
24
+ - limits — optional `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `compactEvery`, `timeoutMs` (also `costBudget` / `tokenBudget` for hard caps)
24
25
  2. After it returns (`ok:true` with `goalID`/`artifactDir`/`defaultsApplied`, or `ok:false` with `errorCode: "missing_agent"|"missing_checks"|"already active"`), tell the user the goal is running in the background and they can monitor it with `/loop` (or <leader>o). On `missing_*`, explain the contract violation and ask for the missing piece; on `already active`, tell them to `pause`/`clear` the current writer or use `workspaceWrite:false`.
25
26
 
26
27
  Important: the worker session runs autonomously — do not try to do the goal's work in this chat. This chat only creates the goal. Completion is host-judged: if `checks` fail, the worker will see `HOST VERDICT: COMPLETION REJECTED` with exact `stderr` and must fix the behavior (not just rewrite evidence) before retrying.
package/dist/tui.js CHANGED
@@ -1755,7 +1755,7 @@ function LoopDashboard(props) {
1755
1755
  return _el$162;
1756
1756
  })())] : (() => {
1757
1757
  var _el$165 = _$createElement("span");
1758
- _$insertNode(_el$165, _$createTextNode(`parent default`));
1758
+ _$insertNode(_el$165, _$createTextNode(`parent`));
1759
1759
  _$effect((_$p) => _$setProp(_el$165, "style", {
1760
1760
  fg: theme().textMuted
1761
1761
  }, _$p));
@@ -1793,7 +1793,7 @@ function LoopDashboard(props) {
1793
1793
  return _el$169;
1794
1794
  })()] : (() => {
1795
1795
  var _el$170 = _$createElement("span");
1796
- _$insertNode(_el$170, _$createTextNode(`session default`));
1796
+ _$insertNode(_el$170, _$createTextNode(`parent`));
1797
1797
  _$effect((_$p) => _$setProp(_el$170, "style", {
1798
1798
  fg: theme().textMuted
1799
1799
  }, _$p));
@@ -1815,29 +1815,29 @@ function LoopDashboard(props) {
1815
1815
  return _el$153;
1816
1816
  })(), (() => {
1817
1817
  var _el$155 = _$createElement("span");
1818
- _$insert(_el$155, () => formatTokens(goal().tokensUsed));
1818
+ _$insert(_el$155, () => formatCost(goal().costUsed));
1819
1819
  _$effect((_$p) => _$setProp(_el$155, "style", {
1820
- fg: theme().warning,
1820
+ fg: theme().success,
1821
1821
  bold: true
1822
1822
  }, _$p));
1823
1823
  return _el$155;
1824
1824
  })(), (() => {
1825
1825
  var _el$156 = _$createElement("span");
1826
- _$insertNode(_el$156, _$createTextNode(` tokens \xB7 `));
1826
+ _$insertNode(_el$156, _$createTextNode(` \xB7 `));
1827
1827
  _$effect((_$p) => _$setProp(_el$156, "style", {
1828
1828
  fg: theme().textMuted
1829
1829
  }, _$p));
1830
1830
  return _el$156;
1831
1831
  })(), (() => {
1832
1832
  var _el$158 = _$createElement("span");
1833
- _$insert(_el$158, () => formatCost(goal().costUsed));
1833
+ _$insert(_el$158, () => formatTokens(goal().tokensUsed));
1834
1834
  _$effect((_$p) => _$setProp(_el$158, "style", {
1835
- fg: theme().success,
1835
+ fg: theme().warning,
1836
1836
  bold: true
1837
1837
  }, _$p));
1838
1838
  return _el$158;
1839
1839
  })(), (() => {
1840
- var _el$159 = _$createElement("span"), _el$160 = _$createTextNode(` \xB7 `);
1840
+ var _el$159 = _$createElement("span"), _el$160 = _$createTextNode(` tokens \xB7 `);
1841
1841
  _$insertNode(_el$159, _el$160);
1842
1842
  _$insert(_el$159, () => formatDuration(goal().timeUsedSeconds), null);
1843
1843
  _$effect((_$p) => _$setProp(_el$159, "style", {
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.3",
4
+ "version": "1.9.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",
@@ -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
+ ```