@aron98/load-lock 0.1.2 → 1.0.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.
Files changed (49) hide show
  1. package/README.md +37 -12
  2. package/demo/demo.sh +1 -1
  3. package/dist/adapters/opencode/plugin.js +39 -18
  4. package/dist/adapters/opencode/tui.d.ts +31 -0
  5. package/dist/adapters/opencode/tui.js +268 -0
  6. package/dist/cli/hook.js +35 -1
  7. package/dist/cli/install.js +708 -24
  8. package/dist/cli/main.js +36 -61
  9. package/dist/cli/top.d.ts +7 -0
  10. package/dist/cli/top.js +98 -0
  11. package/dist/core/daemon.d.ts +25 -4
  12. package/dist/core/daemon.js +67 -30
  13. package/dist/core/persist.js +5 -12
  14. package/dist/core/state.js +7 -0
  15. package/dist/dashboard/feed.d.ts +74 -0
  16. package/dist/dashboard/feed.js +256 -0
  17. package/dist/dashboard/frame.d.ts +12 -0
  18. package/dist/dashboard/frame.js +94 -0
  19. package/dist/dashboard/view.d.ts +148 -0
  20. package/dist/dashboard/view.js +214 -0
  21. package/dist/mcp/server.d.ts +2 -1
  22. package/dist/mcp/server.js +54 -19
  23. package/dist/policy/text.d.ts +1 -1
  24. package/dist/policy/text.js +3 -0
  25. package/dist/shared/client.d.ts +10 -0
  26. package/dist/shared/client.js +41 -14
  27. package/dist/shared/protocol.d.ts +29 -0
  28. package/dist/shared/transport.d.ts +40 -0
  29. package/dist/shared/transport.js +136 -0
  30. package/dist/shared/wait-turn.d.ts +29 -0
  31. package/dist/shared/wait-turn.js +315 -0
  32. package/dist/test/dashboard.test.d.ts +1 -0
  33. package/dist/test/dashboard.test.js +405 -0
  34. package/dist/test/hook.test.js +53 -3
  35. package/dist/test/install.test.d.ts +1 -0
  36. package/dist/test/install.test.js +658 -0
  37. package/dist/test/mcp.test.js +36 -1
  38. package/dist/test/platform.test.d.ts +1 -0
  39. package/dist/test/platform.test.js +193 -0
  40. package/dist/test/testutil.js +4 -1
  41. package/dist/test/wait-turn.test.d.ts +1 -0
  42. package/dist/test/wait-turn.test.js +376 -0
  43. package/docs/ADAPTERS.md +5 -2
  44. package/docs/AGENT_GUIDE.md +33 -1
  45. package/docs/ARCHITECTURE.md +25 -5
  46. package/docs/COMPATIBILITY.md +215 -26
  47. package/docs/OPERATIONS.md +313 -8
  48. package/package.json +25 -1
  49. package/skills/load-lock/SKILL.md +4 -0
@@ -32,6 +32,8 @@ acquire(reason, op_key)
32
32
  ├─ granted → you hold a permit → do the heavy work now
33
33
  ├─ queued → keep doing LIGHT work; offers reach you at your next
34
34
  │ load-lock tool call, or via a wakeup message
35
+ │ no light work left? wait_for_turn() = "sleep until it
36
+ │ is my turn" (bounded; auto-claims the offer for you)
35
37
  └─ ...
36
38
  offer arrives (token, expiry) → claim(token) BEFORE the window closes
37
39
  claim → held → start the heavy work
@@ -71,6 +73,36 @@ Notes on the details:
71
73
  explicitly `rejoin(request_id)`. If you know you can't start in time,
72
74
  `decline` instead of letting it expire.
73
75
 
76
+ ### Sleeping until your turn (`wait_for_turn`)
77
+
78
+ Rule 2 says keep doing light work while queued — but sometimes there is
79
+ nothing useful left. `wait_for_turn(request_id?, timeout_ms?)` is the honest
80
+ way to idle: a **bounded blocking sleep** over one of your queued, offered
81
+ or paused requests (default: your most urgent one). When capacity reaches
82
+ you mid-sleep, the offer is **auto-claimed inside the claim window** and
83
+ you wake holding the permit (`granted`, `permit_token` + `auto_claimed:
84
+ true` in the response).
85
+
86
+ Every resolution is a structured result, never an opaque error:
87
+
88
+ - `granted` — your turn came; the permit is already claimed for you.
89
+ - `still_queued` — the bound expired; you get your queue position, queue
90
+ length and who is holding the capacity. Call it again to keep sleeping.
91
+ - `paused` — your offer expired or was declined meanwhile; `next_action`
92
+ has the rejoin instructions.
93
+ - `cancelled` — the request was cancelled/declined/reclaimed meanwhile.
94
+ - `not_found` — the request ended, or the daemon restarted (queues are
95
+ ephemeral); acquire again.
96
+
97
+ The sleep is bounded and **safe by construction**: it holds no permit and
98
+ no capacity, cancelling the tool call just stops the local sleep (nothing
99
+ is orphaned — your request keeps its queue position), and a daemon restart
100
+ mid-sleep resolves promptly with `not_found` instead of hanging. `timeout_ms`
101
+ defaults to 600000 (10 minutes) and is clamped to the machine's
102
+ `maxTurnWaitMs` cap (`get_config` shows it; `configure` can set it, valid
103
+ range 1000–3600000 ms). `timeout_ms: 0` means check-only. The CLI form is
104
+ `load-lock wait --id <request-id> --timeout <ms>`.
105
+
74
106
  ## 3. Running heavy work (lifecycle options)
75
107
 
76
108
  How you start the work determines how reliably the permit cleans up after
@@ -104,7 +136,7 @@ load-lock acquire --reason "cargo test" --op-key "test/core" # prints JSON
104
136
  load-lock run --permit <permit-token> -- cargo test --quiet # tracks + releases
105
137
  # or acquire+run in one go:
106
138
  load-lock run --reason "nightly build" -- npm run build
107
- load-lock wait --id <request-id> --timeout 120000 # bounded wait → auto-claim
139
+ load-lock wait --id <request-id> --timeout 120000 # sleep until your turn → auto-claim
108
140
  ```
109
141
 
110
142
  ## 4. Reading state
@@ -42,7 +42,8 @@ and inherit unrelated constraints.
42
42
 
43
43
  **One daemon, one budget.** A single `load-lock` daemon process per machine
44
44
  owns all state. Every client — any harness, any repo, any worktree — talks to
45
- the same Unix-domain socket, so all sessions share one capacity budget
45
+ the same Unix-domain socket (named pipe `\\.\pipe\load-lock` on Windows),
46
+ so all sessions share one capacity budget
46
47
  regardless of working directory. The default configuration is one pool
47
48
  `heavy` with capacity 1; pools, capacity, and timings are changed at runtime
48
49
  through `configure` (versioned compare-and-swap, persisted with author +
@@ -131,8 +132,18 @@ mechanisms, and labels exactly which apply per harness:
131
132
  offer at its next tool call, without polling.
132
133
  2. **Bounded blocking wait.** `acquire(wait_ms)` blocks briefly (default
133
134
  5s) inside the tool call and auto-claims if capacity frees during the
134
- wait. The CLI `wait` command and MCP `wait` pattern provide longer
135
- bounded waits. No model-authored busy-polling required.
135
+ wait. For longer sleeps there is one shared client-side
136
+ implementation, `waitForTurn` (`src/shared/wait-turn.ts`), surfaced as
137
+ the CLI `wait` command, the MCP `wait_for_turn` tool and the OpenCode
138
+ plugin's `load_lock_wait_for_turn` tool: it polls light
139
+ events+pending pulls on the local socket, auto-claims inside the
140
+ offer window, returns structured results on every path (granted /
141
+ still_queued with position / paused / cancelled / not_found), is
142
+ bounded by `timeout_ms` (capped by the `maxTurnWaitMs` config), and —
143
+ being purely client-side — holds no permit, consumes no capacity, and
144
+ can never orphan a daemon-side waiter when the harness cancels the
145
+ tool mid-sleep. Daemon restarts are detected via the event cursor and
146
+ resolve bounded. No model-authored busy-polling required.
136
147
  3. **Idle wakeup where the harness genuinely supports it.**
137
148
  - OpenCode (live-verified): the native plugin watches daemon events and
138
149
  calls `ctx.session.synthetic(...)`, delivering the offer (with token)
@@ -141,7 +152,7 @@ mechanisms, and labels exactly which apply per harness:
141
152
  - Claude Code (contract-tested): a `Stop` hook checks for cwd-matching
142
153
  offers and continues the session with a claim instruction; an async
143
154
  `await-offer` watcher with `asyncRewake: true` wakes idle sessions.
144
- - Codex CLI (contract-tested): `Stop` hook with `decision: "block"`
155
+ - Codex CLI (live-verified in Docker): `Stop` hook with `decision: "block"`
145
156
  continuation. Note: Codex async background hooks do **not** start a new
146
157
  turn — documented as a limitation.
147
158
  4. **Durable event identities.** All events have monotonic IDs; agents can
@@ -164,7 +175,8 @@ and restored after context compaction:
164
175
  - **Claude Code**: `SessionStart` hook (matchers include `compact`) +
165
176
  `~/.claude/CLAUDE.md` section + skill.
166
177
  - **Codex CLI**: `SessionStart` hook (matcher `compact`) + `SubagentStart`
167
- hook + `~/.codex/AGENTS.md` section (hooks require `/hooks` trust review).
178
+ hook + `~/.codex/AGENTS.md` section (hooks require the interactive
179
+ hook-trust review once; live-verified in Docker).
168
180
  - Other harnesses: MCP server `instructions` + a global instructions file
169
181
  snippet where the harness supports one (see COMPATIBILITY.md).
170
182
 
@@ -193,3 +205,11 @@ capacity; admission is FIFO with head-of-line blocking) and multiple pools;
193
205
  the machine currently uses one pool by default but accepts arbitrary pool
194
206
  names through the same code paths. Adding a second budget (e.g. `network`)
195
207
  is configuration, not code.
208
+
209
+ The read-only status surface also extends without protocol changes: the
210
+ `status` + `events` (durable ids) + `watch` + `inspect` methods are enough
211
+ to build live viewers. `load-lock top` and the OpenCode TUI panel
212
+ (`src/dashboard/`) are exactly that — pure view-model shaping over the
213
+ library client (`./shared/client`), never CLI-output scraping, and never
214
+ acquiring a permit themselves. Any future harness UI can reuse the same
215
+ `src/dashboard/` pipeline.
@@ -11,6 +11,10 @@ Status labels mean exactly this:
11
11
  against the harness's documented hook/tool JSON contracts
12
12
  (`src/test/hook.test.ts`, `src/test/mcp.test.ts`), but the harness itself
13
13
  was not installed during verification.
14
+ - **Implemented + config-tested** — installer shipped; install/uninstall
15
+ round-trip tested against a temp HOME and validated against the harness's
16
+ documented config schemas (`src/test/install.test.ts`), but the harness
17
+ itself was not installed during verification.
14
18
  - **Proposed / fallback** — research-verified harness capabilities only; no
15
19
  adapter shipped beyond the generic MCP path.
16
20
 
@@ -31,14 +35,14 @@ Status labels mean exactly this:
31
35
  | Harness | MCP tools | Policy injection | Compaction restore | Idle wakeup | Async push into context | Cleanup fidelity | Overall |
32
36
  | --- | --- | --- | --- | --- | --- | --- | --- |
33
37
  | **OpenCode** (v2) | Native plugin, `load_lock_*` (better than MCP) | Context hook every model call | Yes (hook re-fires per model call) | Yes — `session.synthetic` at execution boundary (live-verified; never interrupts in-flight shell) | Yes (live-verified) | Full: `run_with_permit` tracked jobs + session heartbeats | **Implemented + live-verified** |
34
- | **Claude Code** | MCP `mcp__load-lock__*` (user scope) | `SessionStart` hook plain-stdout context | Yes (SessionStart matcher `compact`) | Yes — `Stop` hook decision-block + async `await-offer` (`asyncRewake`) | Yes, via asyncRewake (contract-tested) | Full via MCP `run_with_permit` + server heartbeats | **Implemented + contract-tested** |
35
- | **Codex CLI** | MCP `mcp__load-lock__*` (config.toml) | `SessionStart` + `SubagentStart` additionalContext; `~/.codex/AGENTS.md` | Yes (SessionStart matcher `compact`) | Yes — `Stop` hook `decision:"block"` continuation | Partial: async background hooks do NOT start a new turn (documented limitation); stop-hook covers turn boundaries | Full via MCP `run_with_permit` + server heartbeats | **Implemented + contract-tested** (hooks need `/hooks` trust review) |
36
- | **Gemini CLI** | Yes (`settings.json` mcpServers) | `GEMINI.md` global (config `context.fileName`) | Via global GEMINI.md (always loaded) | No hook mechanism exists | No | Via MCP `run_with_permit` only | Proposed (MCP fallback) |
37
- | **GitHub Copilot CLI** | Yes (`copilot mcp add`) | AGENTS.md/GEMINI.md discovery + custom instructions | Via global instructions file | Hooks reported in ecosystem content but **not verified in official docs** — treat as unavailable | Unverified | Via MCP `run_with_permit` only | Proposed (MCP fallback) |
38
- | **Cursor** | Yes (`.cursor/mcp.json` + global) | `.cursor/rules/*.md` Always rules | Rules re-apply (Always) | `stop` hook exists but **beta** with a less-documented JSON contract — not shipped | Beta/unverified | Via MCP `run_with_permit` only | Proposed (MCP fallback; hook tier possible later) |
39
- | **Cline** | Yes (`cline_mcp_settings.json`) | `.clinerules/` + `~/.cline/rules` | Rules always loaded | No hooks | No | Via MCP `run_with_permit` only | Proposed (MCP fallback) |
40
- | **Roo Code** | Yes (Roo MCP settings) | `.roo/` rule dirs | Rules always loaded | No hooks | No | Via MCP `run_with_permit` only | Proposed (MCP fallback) |
41
- | **Goose** | Yes (`~/.config/goose/config.yaml`) | `.goosehints` (global + project) | Hints always loaded | No hooks | No | Via MCP `run_with_permit` only | Proposed (MCP fallback) |
38
+ | **Claude Code** | MCP `mcp__load-lock__*` (user scope) — live-verified round-trip | `SessionStart` hook plain-stdout context (live-verified: model quoted the brief verbatim) | Yes (SessionStart matcher `compact`) — live-verified after `/compact` | Yes — `Stop` hook decision-block + async `await-offer` (`asyncRewake`), both live-verified | Yes, via asyncRewake (live-verified; the wake text itself is not rendered by the TTY, but the wake drives the full claim lifecycle) | Full via MCP `run_with_permit` + server heartbeats (live-verified incl. a job outliving the claude process) | **Implemented + live-verified** |
39
+ | **Codex CLI** | MCP `mcp__load-lock__*` (config.toml) | `SessionStart` + `SubagentStart` additionalContext; `~/.codex/AGENTS.md` | Yes (SessionStart matcher `compact`) | Yes — `Stop` hook `decision:"block"` continuation | Partial: async background hooks do NOT start a new turn (documented limitation); stop-hook covers turn boundaries | Full via MCP `run_with_permit` + server heartbeats | **Implemented + live-verified** (interactive TUI in Docker; exec-mode MCP blocked upstream — see Tier 2 notes) |
40
+ | **Gemini CLI** | Yes (`settings.json` mcpServers, first-class installer) | `GEMINI.md` global (config `context.fileName`) | Via global GEMINI.md (always loaded) | No hooks — `wait_for_turn` bounded sleep is the primary idle mechanism | No | Via MCP `run_with_permit` only | **Implemented (fallback tier, config-tested)** |
41
+ | **GitHub Copilot CLI** | Yes (`~/.copilot/mcp-config.json`, merge-only installer; verify with `copilot mcp list`) | `~/.copilot/copilot-instructions.md` (marked section) | Via global instructions file | Hooks reported in ecosystem content but **not verified in official docs** — none installed; `wait_for_turn` bounded sleep is the primary idle mechanism | Unverified | Via MCP `run_with_permit` only | **Implemented (fallback tier, config-tested)** |
42
+ | **Cursor** | Yes (project `.cursor/mcp.json`, merge-only installer) | `.cursor/rules/load-lock.mdc` (dedicated Always-apply rule file) | Rules re-apply (Always) | Yes — `stop` hook `{"followup_message":...}` continuation (hooks v1, officially documented) | No async watcher shipped; stop hook covers turn boundaries | Via MCP `run_with_permit` only | **Implemented + contract-tested** (hooks tier; not live-verified) |
43
+ | **Cline** | Yes (`cline_mcp_settings.json`, merge-only installer) | `~/.cline/rules/load-lock.md` (dedicated rule file) | Rules always loaded | No hooks — `wait_for_turn` bounded sleep is the primary idle mechanism | No | Via MCP `run_with_permit` only | **Implemented (fallback tier, config-tested)** |
44
+ | **Roo Code** | Yes (project `.roo/mcp.json` + existing global `mcp_settings.json`, merge-only installer) | `~/.roo/rules/load-lock.md` (dedicated rule file) | Rules always loaded | No hooks — `wait_for_turn` bounded sleep is the primary idle mechanism | No | Via MCP `run_with_permit` only | **Implemented (fallback tier, config-tested)** |
45
+ | **Goose** | Yes (`~/.config/goose/config.yaml` `extensions` block, merge-only installer) | `.goosehints` (global, marked section) | Hints always loaded | No hooks — `wait_for_turn` bounded sleep is the primary idle mechanism | No | Via MCP `run_with_permit` only | **Implemented (fallback tier, config-tested)** |
42
46
  | **Aider** | **No MCP support** | CONVENTIONS.md / `.aider.conf.yml` | Read each run | N/A | N/A | CLI-only: same-shell `load-lock acquire ... && cmd; load-lock release ...` (manual) | Proposed (manual CLI tier) |
43
47
 
44
48
  ## How each integration tier works
@@ -52,14 +56,21 @@ Global plugin (`load-lock install --harness opencode`) registers:
52
56
  - a `load-lock` skill with the full agent guide,
53
57
  - daemon event watcher → `ctx.session.synthetic` offer messages with the
54
58
  real claim token, delivered at execution boundaries,
55
- - per-session heartbeats.
59
+ - per-session heartbeats,
60
+ - the package's `./tui` export: a **live lock-status dashboard panel**
61
+ (issue #10) inside the opencode TUI — `/load-lock` opens the
62
+ `load-lock.dashboard` session panel (pools, holder cards, offer
63
+ countdowns, queue, paused, event feed, `inspect_machine`), a home-footer
64
+ `lock 1/1·Q2` indicator, and an `x` force-release flow gated behind an
65
+ explicit select + confirm (the panel itself is read-only and never
66
+ acquires a permit).
56
67
 
57
68
  Verified live on opencode v2.0.29: plugin load, policy visible to the model,
58
69
  tool round-trip (acquire → queued → wakeup → claim → release), execution-
59
70
  boundary delivery without interrupting a running `sleep`, skill visible,
60
71
  and graceful handling of a deliberately missed offer (paused → cancel).
61
72
 
62
- ### Tier 2 — Claude Code / Codex CLI (hooks + MCP, contract-tested)
73
+ ### Tier 2 — Claude Code / Codex CLI (hooks + MCP, live-verified)
63
74
 
64
75
  `load-lock install --harness claude-code|codex` configures the MCP server
65
76
  (user scope) plus hooks:
@@ -76,20 +87,152 @@ and graceful handling of a deliberately missed offer (paused → cancel).
76
87
  shown); the agent claims via `pending`/`claim`.
77
88
 
78
89
  Contract tests cover all hook stdin/stdout shapes against the documented
79
- harness contracts. Live verification pending a machine with the harness
80
- installed. Codex requires the user to trust the hooks once via `/hooks`
81
- (the installer prints this).
90
+ harness contracts. Claude Code is additionally **live-verified** (in Docker,
91
+ via `docker/run-claude-verify.sh` against the real `claude` CLI): SessionStart
92
+ policy brief in context at startup; the same brief re-injected after
93
+ `/compact` (the model quoted the fresh "load-lock now:" line, not a stale
94
+ copy); the full MCP round-trip acquire → queued → offer → claim →
95
+ `run_with_permit` → auto-release; the `Stop` hook decision-block live (async
96
+ watcher disabled so the stop path was exercised directly); the async
97
+ `await-offer` (`asyncRewake`) watcher waking an idle interactive session,
98
+ which then claimed and released; a `Task`-tool subagent using the MCP tools
99
+ and seeing the policy; and a `run_with_permit` background job that
100
+ outlived the `claude` process and was auto-released. One nuance: the TUI does
101
+ not render the async watcher's stderr wake text — the wake still works (the
102
+ session resumes and the claim round-trip succeeds), only the message is not
103
+ shown.
82
104
 
83
- ### Tier 3 — MCP-only fallback (Gemini CLI, Copilot CLI, Cursor, Cline, Roo Code, Goose)
105
+ Codex CLI (0.163.0) is live-verified end-to-end in Docker
106
+ (`docker/verify-codex.sh`, driven through a real pty TUI session via
107
+ `docker/codex-pty.py`): SessionStart policy injection visible to the model,
108
+ MCP `status`/`acquire`/`claim`/`release` round-trip, FIFO queueing behind a
109
+ competing CLI holder, and Stop-hook `decision:"block"` wakeup that continues
110
+ the session with the claim instruction. The verification drives the real
111
+ interactive hook-trust review (the "Hooks need review" dialog) once —
112
+ `--dangerously-bypass-hook-trust` is deliberately NOT used because it forces
113
+ codex embedded mode, which never injects MCP tools (real users trust the
114
+ hooks once via `/hooks`; the installer prints this). Note: async background
115
+ hooks do not wake an idle Codex session — the Stop hook covers turn
116
+ boundaries only (verified as such).
117
+
118
+ Two upstream/provider constraints shaped the harness:
119
+
120
+ - Headless `codex exec` cannot inject MCP tools at all
121
+ (openai/codex#38689), so live verification drives the interactive TUI
122
+ through a pty.
123
+ - codex 0.163.0 declares MCP tools to the Responses API using the
124
+ `namespace` tool type; the trustedtokens/GLM endpoint silently drops
125
+ namespace tools, which made MCP tools invisible to the model even in the
126
+ TUI. The verification container therefore fronts the API with
127
+ `docker/codex-api-shim.js`, which rewrites namespaces to flat
128
+ `mcp__<server>__<tool>` function tools on the wire (request, SSE
129
+ response, and conversation history). This is a verification-harness
130
+ workaround for the provider, not a change to load-lock itself.
131
+
132
+ ### Tier 2 (contract-tested) — Cursor (hooks + MCP)
133
+
134
+ Cursor 1.7 (Sept 2025) introduced hooks as a beta with an under-documented
135
+ JSON contract — the original reason Cursor sat on the MCP-fallback tier
136
+ (issue #6: "do not ship against an unstable contract"). As of 2026-10-19 the
137
+ official docs at https://cursor.com/docs/hooks fully document the hooks
138
+ system with no beta caveats (per-event reference, common input schema, exit
139
+ codes, config-source priority, cloud-agent support, third-party hook
140
+ compatibility), so the contract is now treated as stable and the adapter
141
+ below shipped (issue #6).
142
+
143
+ `load-lock install --harness cursor --project <dir>` wires (all
144
+ project-level, merge-only, reversible by uninstall):
145
+
146
+ - **MCP server** — `mcpServers.load-lock` (stdio, absolute node path) in
147
+ `<project>/.cursor/mcp.json` (documented project MCP config).
148
+ - **Stop hook** — flat hooks-v1 entry
149
+ `{"type":"command","command":"<node> <cli> hook cursor stop"}` in
150
+ `<project>/.cursor/hooks.json` (`{"version":1,"hooks":{"stop":[...]}}`;
151
+ `version` is added only if absent, user entries preserved).
152
+ - **Rule** — dedicated `<project>/.cursor/rules/load-lock.mdc` with YAML
153
+ frontmatter `alwaysApply: true` (note: plain `.md` files in
154
+ `.cursor/rules` are ignored by Cursor — the rule must be `.mdc`).
155
+
156
+ The handler's documented stop contract: stdin carries `workspace_roots`
157
+ (no `cwd`) and `loop_count` (number of prior stop-hook followups for the
158
+ conversation); stdout JSON `{"followup_message":"..."}` is auto-submitted as
159
+ the next user message — Cursor's loop-style idle wakeup, analogous to
160
+ Claude/Codex `decision:"block"`. The handler is silent unless the daemon has
161
+ an offer matching `workspace_roots[0]`, and silent when `loop_count > 0`
162
+ (Cursor's equivalent of `stop_hook_active` — prevents wakeup loops;
163
+ Cursor's own `loop_limit` default of 5 per script is a second backstop).
164
+
165
+ **Verification status: contract-tested, not live-verified.** The stdin
166
+ parsing, followup payload, loop guard and silence paths are tested against
167
+ the documented contract in `src/test/hook.test.ts`; the install/uninstall
168
+ round-trip (user config preserved, idempotent re-install, clean bare-project
169
+ uninstall) is tested in `src/test/install.test.ts`. No Cursor install
170
+ exists on the verification machine, so end-to-end behavior inside the real
171
+ editor is unverified — the installer output says so explicitly. Sources:
172
+ official docs (https://cursor.com/docs/hooks, checked 2026-10-19), 1.7
173
+ release announcement (forum.cursor.com, Sept 2025), GitButler deep-dive
174
+ (Sept 2025), InfoQ coverage (Oct 2025).
175
+
176
+ ### Tier 3 — MCP-only fallback (Gemini CLI, Copilot CLI, Cline, Roo Code, Goose)
84
177
 
85
178
  The MCP server itself carries the policy in its `instructions`, every tool
86
179
  result surfaces pending offers (tool-boundary delivery), and
87
- `inspect_machine`/`get_config`/`configure` work unchanged. Where the harness
88
- supports global instruction files (GEMINI.md / AGENTS.md / rules / hints),
89
- `load-lock install --harness generic` prints a snippet to add. Missing:
90
- idle wakeup and automatic context re-injection — queued agents discover
91
- offers at their next tool call or via a bounded `wait`. This is the honest
92
- fallback; we do not pretend these harnesses got push notifications.
180
+ `inspect_machine`/`get_config`/`configure` work unchanged.
181
+
182
+ **Every tier-3 MCP harness ships a first-class merge-only installer**
183
+ (Gemini CLI landed with issue #1; Copilot CLI, Cline, Roo Code
184
+ and Goose with issue #2). Cursor moved up to the hooks tier with its own
185
+ installer in issue #6 (see the Cursor section above). Each installer merges
186
+ into the harness's real config file
187
+ and never deletes anything the user wrote; `load-lock uninstall` removes
188
+ exactly the entries the installer added:
189
+
190
+ - **Gemini CLI** — `mcpServers.load-lock` (stdio) into
191
+ `~/.gemini/settings.json` + a marked policy section in
192
+ `~/.gemini/GEMINI.md`.
193
+ - **Copilot CLI** — `mcpServers.load-lock` (`type: local`) into
194
+ `~/.copilot/mcp-config.json` (verify with `copilot mcp list`) + a marked
195
+ section in `~/.copilot/copilot-instructions.md`. Hooks are reported in
196
+ ecosystem content but unverified in official docs, so the installer ships
197
+ none — no idle wakeup is claimed.
198
+ - **Cline** — `mcpServers.load-lock` into the MCP settings file
199
+ (`~/.cline/data/settings/cline_mcp_settings.json`, or the path in
200
+ `CLINE_MCP_SETTINGS_PATH`) + a dedicated `~/.cline/rules/load-lock.md`
201
+ (our own file, so uninstall is a simple delete).
202
+ - **Roo Code** — `mcpServers.load-lock` into `<project>/.roo/mcp.json`
203
+ (pass `--project`) plus every **existing** global `mcp_settings.json`
204
+ (VS Code globalStorage for Roo/Zoo; existing files only — the installer
205
+ never guesses storage paths) + a dedicated `~/.roo/rules/load-lock.md`.
206
+ - **Goose** — an `extensions.load-lock` stdio block (marker-tagged
207
+ block-mapping YAML) into `~/.config/goose/config.yaml`
208
+ (XDG_CONFIG_HOME respected) + a marked section in
209
+ `~/.config/goose/.goosehints`.
210
+
211
+ Install/uninstall round-trips for all five are tested against a temp HOME
212
+ and validated against the harnesses' documented config schemas
213
+ (`src/test/install.test.ts`), but not live-verified against real
214
+ harnesses — hence *config-tested*.
215
+
216
+ `load-lock install --harness generic` still prints snippets for harnesses
217
+ without a dedicated installer.
218
+ Idle policy: these harnesses get no push notifications, so the PRIMARY
219
+ idle mechanism is the MCP `wait_for_turn` tool — a bounded, honest sleep
220
+ until it is the caller's turn. Queued agents call
221
+ `mcp__load-lock__wait_for_turn` (optionally with `timeout_ms`, default
222
+ 600000, capped by the machine's `maxTurnWaitMs` config) and get a
223
+ structured result on every path: `granted` (the offer was auto-claimed —
224
+ `permit_token` included), `still_queued` (timeout, with queue position
225
+ and holders), `paused`/`cancelled`, or `not_found` (e.g. the daemon
226
+ restarted). The wait itself holds no permit and consumes no capacity;
227
+ harness tool cancellation aborts it cleanly. (Codex CLI note: async
228
+ background hooks never wake an idle session there either — the Stop hook
229
+ covers turn boundaries only — so the same `wait_for_turn` sleep is the
230
+ honest pattern for Codex subagents without hook wiring.)
231
+
232
+ Still missing on this tier: automatic context re-injection — there is no
233
+ push channel, so a woken agent relies on tool results (which carry
234
+ pending offers) rather than harness messages. We do not pretend these
235
+ harnesses got push notifications.
93
236
 
94
237
  ### Tier 4 — Aider (manual CLI)
95
238
 
@@ -97,6 +240,28 @@ No MCP/hooks; nothing to install. Agents (or humans) use the CLI directly in
97
240
  the shell: `load-lock acquire ... && load-lock run -- <cmd>` handles
98
241
  acquire→run→release in one command line.
99
242
 
243
+ ## Live dashboard surfaces (issue #10)
244
+
245
+ `load-lock top` is portable and harness-agnostic: any integration that
246
+ spawns the daemon gets the same live dashboard in any terminal. The table
247
+ below is deliberately honest about what each harness gets **inside its own
248
+ UI** — where no UI surface exists, we say so rather than claim parity.
249
+
250
+ | Harness | In-harness dashboard | Also available |
251
+ | --- | --- | --- |
252
+ | **OpenCode** | Native TUI panel: `/load-lock` opens `load-lock.dashboard` (session panel, full-screen) with pools, holder cards, offer claim-window countdowns, queue, paused, event feed, `inspect_machine`; home-footer `lock 1/1` indicator; `x` force-release behind select + confirm. Implemented as the package's `./tui` companion plugin (main plugin unchanged). Live-verified against opencode 2.0.29: panel render + palette/slash command + confirm-gated force-release exercised in the real TUI. | `load-lock top` |
253
+ | **Claude Code** | None in-app — Claude Code has no plugin UI surface. The model can report status via the `load_lock_status` MCP tool at tool boundaries, and each `SessionStart` hook injects a one-line machine summary. | `load-lock top` (any terminal) |
254
+ | **Codex CLI** | None in-app — same reason. `load_lock_status` MCP tool; `SessionStart`/`SubagentStart` machine summary. | `load-lock top` |
255
+ | **Gemini CLI** | None in-app (no hooks, no UI plugin surface). `load_lock_status` MCP tool at tool boundaries. | `load-lock top` |
256
+ | **Copilot CLI, Cursor, Cline, Roo Code, Goose** | None in-app. MCP `status` tool where the MCP server is configured. | `load-lock top` |
257
+ | **Aider** | None (no MCP either). | `load-lock top` |
258
+
259
+ All dashboard surfaces are **read-only viewers that never acquire a
260
+ permit**; the single admin action (force-release) is gated behind an
261
+ explicit confirm. Data plumbing (event-cursor resume, daemon-restart
262
+ reconnect, view-model shaping, frame rendering) is covered by
263
+ `src/test/dashboard.test.ts`.
264
+
100
265
  ## Session identity per harness
101
266
 
102
267
  | Harness | Identity | Notes |
@@ -104,13 +269,37 @@ acquire→run→release in one command line.
104
269
  | OpenCode | plugin context `sessionID` (also MCP `_meta` when used) | automatic |
105
270
  | Claude Code | hooks get `session_id`; MCP tools take explicit `agent_id` | policy instructs agents to pass it |
106
271
  | Codex | hooks get `session_id`; MCP tools take explicit `agent_id` | same |
272
+ | Cursor | hooks get `conversation_id` (no `cwd` — `workspace_roots[0]` identifies the request); MCP tools take explicit `agent_id` | same |
107
273
  | Tier 3 | explicit `agent_id` argument | falls back to per-server-process id |
108
274
 
275
+ ## Platform matrix
276
+
277
+ | Platform | Status | Notes |
278
+ | --- | --- | --- |
279
+ | **Linux** | Implemented + live-verified | Primary target; all daemon/protocol/CLI tests and the 3-worktree demo run here. `/proc`-based liveness with start-time PID-reuse defense. |
280
+ | **macOS** | Implemented, limitations documented | Works; liveness falls back from `/proc` start times to `kill(pid, 0)` (weaker PID-reuse defense), no cgroup limits. See OPERATIONS.md. |
281
+ | **Windows** | Implemented, unit-tested on Linux — **not Windows-verified** | Named-pipe transport (`\\.\pipe\load-lock`), `%LOCALAPPDATA%` state dir, `kill(pid, 0)` liveness, `taskkill /T` job-tree kills. Covered by `src/test/platform.test.ts` via platform injection; no Windows machine was available for end-to-end verification. See the Windows procedure in OPERATIONS.md. |
282
+
109
283
  ## Verified vs proposed — summary counts
110
284
 
111
- - Live-verified: OpenCode (full adapter, including wakeup).
112
- - Contract-tested: Claude Code, Codex CLI hook/MCP contracts.
113
- - Core daemon/protocol/CLI: 32 automated tests + scripted 3-worktree demo
114
- (three independent agent processes, real git worktrees, one pool).
115
- - Proposed (researched capabilities, generic adapter only): Gemini CLI,
116
- Copilot CLI, Cursor, Cline, Roo Code, Goose, Aider.
285
+ - Live-verified: OpenCode (full adapter, including wakeup); Claude Code
286
+ (full adapter in Docker: policy injection, compact restore, Stop-hook and
287
+ asyncRewake wakeup paths, subagents, background-job cleanup); Codex CLI
288
+ (Docker, interactive TUI: policy injection, MCP round-trip, FIFO queueing
289
+ behind a CLI holder, Stop-hook decision-block wakeup; exec-mode MCP
290
+ injection blocked upstream — see Tier 2 notes).
291
+ - Contract-tested: Cursor (hooks-v1 stop-hook adapter + installer, tested
292
+ against the officially documented hooks JSON contract; no Cursor install
293
+ on the verification machine — see the Cursor section under Tier 2).
294
+ - Config-tested: Gemini CLI, Copilot CLI, Cline, Roo Code, Goose (installer
295
+ round-trips, temp HOME; real harnesses not installed on the verification
296
+ machine).
297
+ - Core daemon/protocol/CLI: 68 automated tests + scripted 3-worktree demo
298
+ (three independent agent processes, real git worktrees, one pool), including
299
+ 9 dashboard tests (view-model shaping, event-cursor resume across a daemon
300
+ restart, reconnect, and a `load-lock top --once` render smoke test) and
301
+ 9 bounded-wait tests (multi-process `wait_for_turn`: FIFO wake with
302
+ auto-claim, timeout/pause/cancel/restart/abort paths, `maxTurnWaitMs` cap,
303
+ MCP snake_case round trip).
304
+ - Proposed (researched capabilities, generic adapter only):
305
+ Aider.