@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.
- package/README.md +37 -12
- package/demo/demo.sh +1 -1
- package/dist/adapters/opencode/plugin.js +39 -18
- package/dist/adapters/opencode/tui.d.ts +31 -0
- package/dist/adapters/opencode/tui.js +268 -0
- package/dist/cli/hook.js +35 -1
- package/dist/cli/install.js +708 -24
- package/dist/cli/main.js +36 -61
- package/dist/cli/top.d.ts +7 -0
- package/dist/cli/top.js +98 -0
- package/dist/core/daemon.d.ts +25 -4
- package/dist/core/daemon.js +67 -30
- package/dist/core/persist.js +5 -12
- package/dist/core/state.js +7 -0
- package/dist/dashboard/feed.d.ts +74 -0
- package/dist/dashboard/feed.js +256 -0
- package/dist/dashboard/frame.d.ts +12 -0
- package/dist/dashboard/frame.js +94 -0
- package/dist/dashboard/view.d.ts +148 -0
- package/dist/dashboard/view.js +214 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +54 -19
- package/dist/policy/text.d.ts +1 -1
- package/dist/policy/text.js +3 -0
- package/dist/shared/client.d.ts +10 -0
- package/dist/shared/client.js +41 -14
- package/dist/shared/protocol.d.ts +29 -0
- package/dist/shared/transport.d.ts +40 -0
- package/dist/shared/transport.js +136 -0
- package/dist/shared/wait-turn.d.ts +29 -0
- package/dist/shared/wait-turn.js +315 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +405 -0
- package/dist/test/hook.test.js +53 -3
- package/dist/test/install.test.d.ts +1 -0
- package/dist/test/install.test.js +658 -0
- package/dist/test/mcp.test.js +36 -1
- package/dist/test/platform.test.d.ts +1 -0
- package/dist/test/platform.test.js +193 -0
- package/dist/test/testutil.js +4 -1
- package/dist/test/wait-turn.test.d.ts +1 -0
- package/dist/test/wait-turn.test.js +376 -0
- package/docs/ADAPTERS.md +5 -2
- package/docs/AGENT_GUIDE.md +33 -1
- package/docs/ARCHITECTURE.md +25 -5
- package/docs/COMPATIBILITY.md +215 -26
- package/docs/OPERATIONS.md +313 -8
- package/package.json +25 -1
- package/skills/load-lock/SKILL.md +4 -0
package/docs/AGENT_GUIDE.md
CHANGED
|
@@ -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 #
|
|
139
|
+
load-lock wait --id <request-id> --timeout 120000 # sleep until your turn → auto-claim
|
|
108
140
|
```
|
|
109
141
|
|
|
110
142
|
## 4. Reading state
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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.
|
|
135
|
-
|
|
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 (
|
|
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
|
|
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.
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -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 (
|
|
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 +
|
|
36
|
-
| **Gemini CLI** | Yes (`settings.json` mcpServers) | `GEMINI.md` global (config `context.fileName`) | Via global GEMINI.md (always loaded) | No
|
|
37
|
-
| **GitHub Copilot CLI** | Yes (`copilot mcp
|
|
38
|
-
| **Cursor** | Yes (`.cursor/mcp.json
|
|
39
|
-
| **Cline** | Yes (`cline_mcp_settings.json
|
|
40
|
-
| **Roo Code** | Yes (
|
|
41
|
-
| **Goose** | Yes (`~/.config/goose/config.yaml`) | `.goosehints` (global
|
|
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,
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
(
|
|
115
|
-
|
|
116
|
-
|
|
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.
|