@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 +2 -1
- package/dist/tui.js +8 -8
- package/package.json +1 -1
- package/skills/loopd/SKILL.md +88 -53
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
|
-
-
|
|
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
|
|
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(`
|
|
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, () =>
|
|
1818
|
+
_$insert(_el$155, () => formatCost(goal().costUsed));
|
|
1819
1819
|
_$effect((_$p) => _$setProp(_el$155, "style", {
|
|
1820
|
-
fg: theme().
|
|
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(`
|
|
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, () =>
|
|
1833
|
+
_$insert(_el$158, () => formatTokens(goal().tokensUsed));
|
|
1834
1834
|
_$effect((_$p) => _$setProp(_el$158, "style", {
|
|
1835
|
-
fg: theme().
|
|
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.
|
|
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",
|
package/skills/loopd/SKILL.md
CHANGED
|
@@ -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.
|
|
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** —
|
|
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
|
-
|
|
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
|
|
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
|
-
* **
|
|
59
|
-
* **
|
|
60
|
-
* **
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
-
**
|
|
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 **
|
|
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
|
-
|
|
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
|
|
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
|
-
| `
|
|
189
|
-
|
|
|
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
|
|
196
|
-
| `:
|
|
197
|
-
| `:
|
|
198
|
-
| `:
|
|
199
|
-
| `:
|
|
200
|
-
| `:
|
|
201
|
-
| `:retry` |
|
|
202
|
-
| `:
|
|
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` (
|
|
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: "
|
|
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
|
+
```
|