@bojackduy/opencode-loopd 1.5.2 → 1.6.0

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 CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0 (2026-08-24) — Hardened Lifecycle
4
+
5
+ > Merge `feat/goal-evaluation` (43 files, +5516 / -1001 since `1.5.3` `5461e0d`). Hardens prompt correlation, maintenance recovery, workspace serialization, and evaluator verification.
6
+
7
+ ### Added
8
+
9
+ - **Prompt correlation & generation fencing** — each turn pre-generates `activePromptMessageID` (`msg-` UUID), correlates `message.updated`/`message.part.updated` by `runGeneration` + `idleCandidateGeneration`; stale `session.idle` can never release a newer lease (`src/application/loop-engine.ts`, `src/domain/runtime.ts:85`, `src/server/worker-session.ts:62`).
10
+ - **Transcript anchoring** — finalization requires latest user prompt == engine prompt **and** assistant response `completedAt` (`src/application/loop-engine.ts:438` `inspectPromptTurn`); prevents phantom completions.
11
+ - **Workspace serialization** — single active `workspaceWrite:true` writer enforced on `start`/`resume`/`retry` with rollback; safe default `workspaceWrite=true`; `goal-policy.ts:37` shared by tool + control bus (`src/application/goal-service.ts:79`, `src/application/control-worker.ts:158`).
12
+ - **Contract validation** — `defaultAgent`/`defaultChecks` plugin options (`src/server/plugin.ts:152` `parsePluginDefaults`), mandatory `checks` for writers, `checkCwd` defaults to project root for writers / `artifactDir` for readers (`src/application/goal-policy.ts:16`, `src/domain/goal.ts:97` `maxEvaluatorRejections`).
13
+ - **Verification tracking** — new domain `src/domain/verification.ts:4` `VerificationAttempt` (id, sequence, runGeneration, cwd, checks with `stdout`/`stderr`), bounded `recentVerificationAttempts` (10), exposed in `get_goal` runtime.
14
+ - **Evaluator rejection event** — `goal.completion_rejected` (`src/domain/events.ts:52`) with `attemptID`, `rejectionCount`, `failureSummary`; `freeRetryPending` prevents `budgetTurnCount` charge for <3 rejections, `blocked` after 3 (`src/server/goal-tools.ts:298`).
15
+ - **Recovery tool** — `nudge_goal` force re-prompts stuck `active` worker: clears `activeRunID`/`idleCandidate`/`activePromptMessageID`/lease (`phase→idle`) and calls `continueTurn({force:true})` (`src/application/goal-service.ts:696`, `src/server/owner-tools.ts:334`).
16
+ - **Tool-call guard** — `activeToolCallIDs: string[]` tracked via `tool.execute.before/after` (`src/server/plugin.ts:67`), blocks idle finalization while tools run (`src/domain/runtime.ts:244` `hasActiveToolCalls`).
17
+ - **Maintenance (30s)** — auto-clears stale `phase=idle + activeRunID`, bounds `unknown` status polls (`unknownStatusCount` threshold 3 → one `notifyOwner` per episode via `workerUnreachableNotifiedAt`), recovers counters on success, `active`-only polling (`src/application/loop-engine.ts:694`).
18
+ - **Per-goal mutex** — `withGoalOperation(goalID)` serializes `continueTurn`/`pause`/`resume`/`retry`/`clear`/`nudge` and re-checks `leaseIsValid` inside `turn.acquire` transaction (`src/application/goal-service.ts:64`).
19
+ - **State migrations v3→v5** — `turnCount → budgetTurnCount + runCount + runGeneration`, `freeRetryPending`, verification fields, `workspaceWrite` default, `activePromptObservedAt`/`unknownStatusCount` (`src/infrastructure/state-repository.ts:178`).
20
+ - **Docs** — `skills/loopd/SKILL.md` expanded Contract & Evaluation Semantics, Hardened loop guarantees, Owner/Worker tool refs, Safety Patterns; `docs/planning/remaining-work.md`, `docs/reports/final-report.md`, `docs/bugs/bugs-new.md`, `docs/development/testing-context.md`, `commands/goal.md` updated.
21
+
22
+ ### Changed
23
+
24
+ - **Loop engine idle** — two-stage debounce `CONFIRM_IDLE_DURATION_MS=2000` requires two `session.idle` 2s apart with no `lastActivityAt` in between; `session.status` handler removed in favor of `session.idle` + `message.*` activity (`src/application/loop-engine.ts:245`, `src/application/loop-engine.ts:183` test).
25
+ - **Budget split** — `turnCount` → `budgetTurnCount` (charged) vs `runCount` (monotonic) vs `runGeneration` (fence); `freeRetryPending` path does not bump budget (`src/domain/runtime.ts:39`, `src/application/goal-service.ts:361`).
26
+ - **Host adapter** — `sessionStatus` returns `unknown` (was `idle`) for missing status; `promptWorker` sends `messageID` and collapses duplicate `msg-` prefixes; `readMessages` returns `messageID`/`parentMessageID`/`completedAt` (`src/server/host-adapter.ts:88`, `src/server/host-adapter.ts:113`).
27
+ - **State repository locking** — `acquireLock` uses `fs.open wx` direct (was `tmp+rename` TOCTOU); `releaseLock` re-verifies `acquiredAt`/`pid` before `rm`; stale `LOCK_STALE_MS=10s`, retry 25ms backoff (`src/infrastructure/state-repository.ts:68`).
28
+ - **Goal tools descriptions** — all 5 tools rewritten to reflect host-acceptance semantics (rejection/free retry/blocked, `workspaceWrite` serialization, `checkCwd`) (`src/server/goal-tools.ts:35`, `src/server/owner-tools.ts:19`).
29
+ - **Review fixes (PR follow-up `392ecee`)** — `runCompletionChecks` now captures `stdout`+`stderr` (was stderr-only), `VerificationAttempt` stores both, `lastRejectionDetails` includes both; `releaseLock` race fixed; `turnCount` migration properly deletes key; `withGoalOperation` documented; `maxEvaluatorRejections` added to `GoalConfig` (`src/domain/goal.ts:97`).
30
+
31
+ ### Fixed
32
+
33
+ - **Race: prompt to subagent session** (`5a13398`, `e41fb84`) — `promptWorker` `messageID` double-prefix (`msg-msg-`) collapsed; `FakeHost` same.
34
+ - **Race: withGoalOperation gate leak** — `gate` never-rejects chain + deletion guard documented.
35
+ - **Race: state lock** — `acquireLock` TOCTOU and `releaseLock` fresh-acquirer deletion fixed.
36
+ - **Turn accounting** — evaluator rejection no longer `turnCount--` hack; `budgetTurnCount` unchanged, `freeRetryPending` consumed once.
37
+ - **Stdout loss** — completion check `stdout` now preserved in rejection evidence (`src/server/goal-tools.ts:253`).
38
+ - **Blocked needed message** — `recordPromptFailure` now contextual: API unavailable only for `blockImmediately`, else `Fix error and retry_goal` (`src/application/goal-service.ts:92`).
39
+ - **Dashboard keybinding** — `o` for open child, `L` log toggle (`5921ec5`).
40
+ - **Docs refs** — kebab paths `bugs/bugs-new.md`, reports index (`12ad22e`, `78c63b3`).
41
+
42
+ ### Tests & Build
43
+
44
+ - 159 tests pass (15 files, 386 expects); new suites: `goal-policy.test.ts:43`, `loop-engine` generation-fence/transcript-anchor/unknown-status/stale-run, `goal-service` acquire-race/concurrent/pause-fence/nudge/workspace-serialization, `state-store` v5 migration.
45
+ - `dist/server.js` 129KB / `dist/tui.js` 66KB rebuilt (`07ad6c0` `bun run build`).
46
+
47
+ ## 1.0.1 (2026-08-21)
48
+
49
+ ### Changed
50
+
51
+ - Removed redundant `ask_user` tool and `awaiting_user` status. Workers now use OpenCode's native `question` tool for clarification — questions appear in the TUI footer as blocker tabs, no goal status change needed.
52
+ - Removed `:answer` dashboard command (native question answers flow through OpenCode).
53
+
3
54
  ## 1.0.0 (2026-08-21)
4
55
 
5
56
  ### Features
@@ -21,11 +72,3 @@
21
72
  - Parent visibility layer: owner tools read child transcript, progress, and state
22
73
  - Bidirectional messaging: inbox system for user→child instructions
23
74
  - Awaiting_user status: child can pause and ask questions
24
-
25
-
26
- ## 1.0.1 (2026-08-21)
27
-
28
- ### Changed
29
-
30
- - Removed redundant `ask_user` tool and `awaiting_user` status. Workers now use OpenCode's native `question` tool for clarification — questions appear in the TUI footer as blocker tabs, no goal status change needed.
31
- - Removed `:answer` dashboard command (native question answers flow through OpenCode).
package/README.md CHANGED
@@ -102,6 +102,21 @@ Or for local checkout:
102
102
  }
103
103
  ```
104
104
 
105
+ For safe code-editing defaults, configure the server plugin with an agent and
106
+ verification commands:
107
+
108
+ ```json
109
+ {
110
+ "plugin": [[
111
+ "@bojackduy/opencode-loopd",
112
+ {
113
+ "defaultAgent": "smart-agent",
114
+ "defaultChecks": ["bun test", "bun run typecheck"]
115
+ }
116
+ ]]
117
+ }
118
+ ```
119
+
105
120
  `opencode-loopd` exposes:
106
121
  - `opencode-loopd/server` → engine plugin
107
122
  - `opencode-loopd/tui` → dashboard plugin
@@ -231,11 +246,19 @@ Goals accept:
231
246
  | `timeoutMs` | `300000` | Per-turn lease (5 min) |
232
247
  | `compactEvery` | — | Compact child every N turns |
233
248
  | `checks` | `[]` | Shell commands that must pass for `complete_goal` |
249
+ | `checkCwd` | artifact directory | Directory where completion checks run; workspace writes default to project root |
250
+ | `workspaceWrite` | `true` | Marks shared-workspace mutation; only one active workspace writer is allowed. Set `false` explicitly for artifact-only/read-only work |
251
+ | `agent` | `defaultAgent` | Worker agent; goal creation fails if neither is configured nor supplied |
234
252
  | `progressFile` | `<artifactDir>/progress.md` | Transaction state file |
235
253
  | `artifactDir` | `goals/<id>/` | Auto — override only if objective names another dir |
236
254
 
237
255
  Artifacts, logs, and state live under `.opencode/loopd/` (project-local). Locks live in `/tmp/loopd-locks/<project-hash>/` to avoid snapshot noise.
238
256
 
257
+ Workspace-writing goals must have deterministic checks and are serialized to
258
+ prevent concurrent agents or `git stash` operations from overwriting each
259
+ other. Artifact-only research goals may run concurrently by setting
260
+ `workspaceWrite: false` explicitly.
261
+
239
262
  ## Dashboard tips
240
263
 
241
264
  - Press `:` then type bare text → sent as `send_goal_input` to the selected goal.
package/commands/goal.md CHANGED
@@ -4,17 +4,22 @@ description: Create a new background loop goal. Asks clarifying questions, then
4
4
 
5
5
  Create a new background loop goal using the `loopd_create_goal` tool.
6
6
 
7
- First, gather what you need to craft a good goal:
8
- - If the user gave a vague objective, ask 1–3 short clarifying questions (what to accomplish, where, and how they'll verify it).
7
+ A goal is a **contract**: `objective` (semantic requirements) + `checks` (deterministic host acceptance) + `agent`/`workspaceWrite`/`checkCwd`/`limits`. The host is the acceptance authority; the worker proposes completion and the host rejects it if `checks` fail (free retry <3, `blocked` after 3).
8
+
9
+ First, gather what you need to craft a good contract:
10
+ - If the user gave a vague objective, ask 1–3 short clarifying questions (what to accomplish, where, and how they'll verify it — i.e., what `checks` should be).
9
11
  - If the user was specific, skip straight to creating it.
10
12
 
11
- When you have enough to write a concrete objective:
13
+ When you have enough to write a concrete contract:
12
14
  1. Call `loopd_create_goal` with:
13
- - `name` — a short slug (e.g. "pdf-notes")
14
- - `objective` — a precise, self-contained statement including verification criteria
15
- - `checks` — optional shell commands that must pass before the goal can be marked complete (e.g. `["npm test"]`)
16
- - `progressFile` — optional path to a markdown progress file
17
- - limitsoptional `maxTurns`, `maxNoProgress`, `maxFailures`, `compactEvery`, `timeoutMs`
18
- 2. After it returns, tell the user the goal is running in the background and they can monitor it with `/loop` (or <leader>d).
15
+ - `name` — a short slug (e.g. "pdf-notes")
16
+ - `objective` — a precise, self-contained statement including verification criteria
17
+ - `agent` — required unless the plugin has `defaultAgent` configured; determines which model runs the worker
18
+ - `checks` — shell commands that must pass before `complete_goal` is accepted; **mandatory when `workspaceWrite:true`** (the default) — or configure `defaultChecks` in `opencode.jsonc`
19
+ - `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
20
+ - `checkCwd` optional directory where `checks` run; writers default to project root, artifact-only jobs default to their `artifactDir`
21
+ - `progressFile` — optional path to a markdown progress file (defaults to `<artifactDir>/progress.md`)
22
+ - limits — optional `maxTurns` (default 50), `maxNoProgress`, `maxFailures`, `compactEvery`, `timeoutMs`
23
+ 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>d). 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`.
19
24
 
20
- Important: the worker session runs autonomously — do not try to do the goal's work in this chat. This chat only creates the goal.
25
+ 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.