@aron98/load-lock 1.5.0 → 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/README.md +61 -14
- package/dist/cli/copies.d.ts +3 -2
- package/dist/cli/copies.js +32 -3
- package/dist/cli/hook.js +48 -4
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +67 -13
- package/dist/cli/main.js +62 -5
- package/dist/cli/setup.d.ts +131 -0
- package/dist/cli/setup.js +942 -0
- package/dist/cli/update.d.ts +5 -1
- package/dist/cli/update.js +16 -3
- package/dist/core/daemon.d.ts +29 -3
- package/dist/core/daemon.js +111 -11
- package/dist/core/persist.d.ts +3 -0
- package/dist/core/persist.js +49 -10
- package/dist/core/state.d.ts +9 -0
- package/dist/core/state.js +41 -3
- package/dist/mcp/server.d.ts +4 -3
- package/dist/mcp/server.js +29 -6
- package/dist/policy/text.d.ts +13 -3
- package/dist/policy/text.js +14 -3
- package/dist/shared/agent-guide.d.ts +3 -0
- package/dist/shared/agent-guide.js +16 -0
- package/dist/shared/client.js +5 -3
- package/dist/shared/drift.d.ts +4 -2
- package/dist/shared/drift.js +24 -6
- package/dist/shared/identity.d.ts +51 -0
- package/dist/shared/identity.js +116 -0
- package/dist/shared/pkginfo.d.ts +9 -0
- package/dist/shared/pkginfo.js +16 -0
- package/dist/shared/protocol.d.ts +22 -0
- package/dist/shared/protocol.js +15 -0
- package/dist/shared/runtimes.d.ts +41 -0
- package/dist/shared/runtimes.js +81 -0
- package/dist/shared/version.d.ts +6 -0
- package/dist/shared/version.js +9 -1
- package/dist/test/ace-mcp.test.d.ts +1 -0
- package/dist/test/ace-mcp.test.js +157 -0
- package/dist/test/ace-opencode.test.d.ts +1 -0
- package/dist/test/ace-opencode.test.js +287 -0
- package/dist/test/check-pins.test.d.ts +1 -0
- package/dist/test/check-pins.test.js +108 -0
- package/dist/test/compat.test.d.ts +1 -0
- package/dist/test/compat.test.js +190 -0
- package/dist/test/hook.test.js +179 -1
- package/dist/test/identity.test.d.ts +1 -0
- package/dist/test/identity.test.js +155 -0
- package/dist/test/install.test.js +135 -1
- package/dist/test/launcher.test.d.ts +1 -0
- package/dist/test/launcher.test.js +222 -0
- package/dist/test/mcp.test.js +76 -7
- package/dist/test/plugin.test.d.ts +1 -0
- package/dist/test/plugin.test.js +144 -0
- package/dist/test/runtimes.test.d.ts +1 -0
- package/dist/test/runtimes.test.js +282 -0
- package/dist/test/setup.test.d.ts +1 -0
- package/dist/test/setup.test.js +354 -0
- package/dist/test/update-command.test.js +71 -0
- package/dist/test/update.test.js +33 -1
- package/docs/ADAPTERS.md +3 -2
- package/docs/AGENT_GUIDE.md +36 -2
- package/docs/ARCHITECTURE.md +122 -7
- package/docs/COMPATIBILITY.md +142 -13
- package/docs/OPERATIONS.md +292 -14
- package/package.json +3 -2
- package/skills/load-lock/SKILL.md +62 -0
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -99,8 +99,13 @@ wrong-state error), and a failed start kills the process group immediately.
|
|
|
99
99
|
Background execution is handled honestly: the helper returns after *the whole
|
|
100
100
|
process group* finishes, never merely when a shell returns a task id.
|
|
101
101
|
|
|
102
|
-
**Identity.** Per-agent identity
|
|
103
|
-
|
|
102
|
+
**Identity.** Per-agent identity is derived from the harness session
|
|
103
|
+
(issues #49/#50): an explicit `agent_id` / `--agent` / `LOAD_LOCK_AGENT`
|
|
104
|
+
wins, else a harness session id (OpenCode `sessionID`, Claude Code
|
|
105
|
+
`CLAUDE_CODE_SESSION_ID`, hook `session_id`), else the stable harness
|
|
106
|
+
ancestor process on Linux, so every wiring of one session (CLI, MCP
|
|
107
|
+
server, hooks) acts as one agent (details in
|
|
108
|
+
[COMPATIBILITY.md](COMPATIBILITY.md#session-identity-per-harness)); every entry also records an agent label,
|
|
104
109
|
cwd, op key (for idempotent duplicate suppression), and reason. Idempotency:
|
|
105
110
|
a duplicate `acquire` from the same agent for the same pending op key returns
|
|
106
111
|
the existing request rather than a second one; multiple distinct ops per
|
|
@@ -118,10 +123,86 @@ same request ids (idempotent re-acquire keeps working), entries offered at
|
|
|
118
123
|
crash time restart as queued and are re-offered with fresh tokens — so a
|
|
119
124
|
restart no longer silently drops a queue position. The daemon auto-spawns on
|
|
120
125
|
the next client connection, and a *self-ordered* restart (version handshake:
|
|
121
|
-
a newer client than the daemon)
|
|
122
|
-
|
|
126
|
+
a newer client than the daemon) persists state and respawns onto the newer
|
|
127
|
+
client's copy (#53; same argv when that copy can't be validated) — see the update lifecycle in
|
|
123
128
|
[OPERATIONS.md](OPERATIONS.md#update-lifecycle).
|
|
124
129
|
|
|
130
|
+
**Persisted format compatibility (issue #51).** Several runtime copies may
|
|
131
|
+
share one state dir, so a downgrade is possible: a newer daemon dies and an
|
|
132
|
+
older client starts its older copy, which must then load files the newer one
|
|
133
|
+
wrote. `config.json` carries a top-level `formatVersion` (absent = 0, the
|
|
134
|
+
pre-versioning format); the journal stays a bare JSON array (what every
|
|
135
|
+
released daemon reads) with `formatVersion` stamped on each entry. The
|
|
136
|
+
promise, in both directions:
|
|
137
|
+
|
|
138
|
+
- Format changes are **additive**. A reader never rejects a file because its
|
|
139
|
+
`formatVersion` is higher than its own; it uses what it understands.
|
|
140
|
+
- Unknown fields are **never dropped destructively**: top-level and
|
|
141
|
+
`config`/pool/guidance fields of `config.json` survive a rewrite (e.g. a
|
|
142
|
+
`configure`), and unknown fields on a journal entry are written back while
|
|
143
|
+
that entry lives. Each writer stamps its *own* `formatVersion`, so readers
|
|
144
|
+
must treat fields from a newer format as possibly stale.
|
|
145
|
+
- Unknown journal entry kinds (a `state` this version doesn't know) and
|
|
146
|
+
malformed entries are **skipped**, never fatal; the count is reported in the
|
|
147
|
+
`daemon_recovered` event. A held entry with an unknown `phase` is kept as an
|
|
148
|
+
untracked permit with a fresh lease (capacity stays accounted; the lease
|
|
149
|
+
reclaims it if nobody heartbeats).
|
|
150
|
+
- An incompatible change would use a **new file name**, never reinterpret
|
|
151
|
+
`config.json` / `journal.json`.
|
|
152
|
+
|
|
153
|
+
## Three layers: daemon, runtime copies, harness wiring
|
|
154
|
+
|
|
155
|
+
load-lock reaches a machine through many entry points (a skill, an MCP
|
|
156
|
+
registration, a harness plugin, ACE, `npm i -g` + `load-lock install`), and
|
|
157
|
+
users mix them across harnesses and over time. The design rule (issue #47):
|
|
158
|
+
**any combination, installed in any order, gives one working budget.** It
|
|
159
|
+
holds because load-lock is split into three layers with different
|
|
160
|
+
multiplicity:
|
|
161
|
+
|
|
162
|
+
| Layer | Per machine | What it is | Rule |
|
|
163
|
+
| --- | --- | --- | --- |
|
|
164
|
+
| **Daemon** | exactly 1 (one socket, one state dir) | the long-lived process that owns all state, started on demand by whichever client connects first (`src/shared/client.ts`) | the newest copy runs it |
|
|
165
|
+
| **Runtime copies** | many | installed copies of the npm package: global npm/bun, the npx cache, OpenCode's plugin cache, a git checkout; the Claude Code plugin's launcher picks one of them | copies interoperate in both directions; none "owns" the daemon |
|
|
166
|
+
| **Harness wiring** | ideally 1 per harness, possibly several | MCP registrations, hooks, policy text, skill copies, written by a plugin, ACE, `load-lock install` or by hand | duplicates are **harmless**, not merely discouraged |
|
|
167
|
+
|
|
168
|
+
**Duplicates are harmless.** Each overlap is defused where the singleton
|
|
169
|
+
lives, not by hoping the user cleans up:
|
|
170
|
+
|
|
171
|
+
- *Hooks*: two wirings of the same hook (loose + plugin, two plugins, ACE)
|
|
172
|
+
both fire, but the daemon's `hook_once` lets only the first act — one
|
|
173
|
+
policy injection, one Stop continuation, one idle watcher per session
|
|
174
|
+
(see "Duplicate hook wiring is harmless" below).
|
|
175
|
+
- *MCP servers*: two registrations in one session (e.g. ACE's `mcp.json` +
|
|
176
|
+
the plugin's bundled server) are two processes with the **same**
|
|
177
|
+
session-derived identity (see Identity above), so an `acquire` through
|
|
178
|
+
one shows up in `pending` through the other. Both talk to the one daemon.
|
|
179
|
+
- *Skill copies*: identical text; the skill only tells the agent how to
|
|
180
|
+
reach the daemon.
|
|
181
|
+
|
|
182
|
+
Housekeeping is still offered: `load-lock setup` lists every integration
|
|
183
|
+
per harness and, with `--yes`, converges each harness to one wiring at its
|
|
184
|
+
highest rung (see [OPERATIONS.md](OPERATIONS.md#load-lock-setup-what-is-installed-where)).
|
|
185
|
+
Correctness never depends on running it.
|
|
186
|
+
|
|
187
|
+
**Compatibility promise between copies.** Because several copies of
|
|
188
|
+
different versions talk to one daemon and one state dir:
|
|
189
|
+
|
|
190
|
+
- every client envelope carries its version and package root; a strictly
|
|
191
|
+
newer client makes the daemon persist state and respawn onto that
|
|
192
|
+
client's copy (#52/#53, see "Runtime copies and the restart hint"
|
|
193
|
+
below), with one attempt per version;
|
|
194
|
+
- the persisted files are versioned and additive in both directions, so
|
|
195
|
+
an older daemon started after a newer one died reads the newer files
|
|
196
|
+
safely (#51, "Persisted format compatibility" above);
|
|
197
|
+
- newer client features degrade against an older daemon instead of
|
|
198
|
+
failing: hooks without `hook_once` act as before, `wait_for_completion`
|
|
199
|
+
falls back to an events-based poll.
|
|
200
|
+
|
|
201
|
+
The **install ladder** built on top of this (skill → MCP server → harness
|
|
202
|
+
plugin, plus `npm i -g` + `load-lock install`) is described in
|
|
203
|
+
[OPERATIONS.md](OPERATIONS.md#the-install-ladder); which rung each harness
|
|
204
|
+
reaches is in [COMPATIBILITY.md](COMPATIBILITY.md#rung-per-harness).
|
|
205
|
+
|
|
125
206
|
## Notification honesty
|
|
126
207
|
|
|
127
208
|
A critical constraint discovered by research: **no harness reliably pushes
|
|
@@ -168,7 +249,8 @@ mechanisms, and labels exactly which apply per harness:
|
|
|
168
249
|
calls `ctx.session.synthetic(...)`, delivering the offer (with token)
|
|
169
250
|
to an idle session at its next execution boundary — it never
|
|
170
251
|
interrupts an in-flight shell command.
|
|
171
|
-
- Claude Code (
|
|
252
|
+
- Claude Code (live-verified in Docker with the loose install; the
|
|
253
|
+
plugin's hooks are not yet live-verified, #63): a `Stop` hook checks for cwd-matching
|
|
172
254
|
offers and continues the session with a claim instruction; an async
|
|
173
255
|
`await-offer` watcher with `asyncRewake: true` wakes idle sessions.
|
|
174
256
|
- Codex CLI (live-verified in Docker): `Stop` hook with `decision: "block"`
|
|
@@ -191,8 +273,10 @@ and restored after context compaction:
|
|
|
191
273
|
- **OpenCode**: plugin `context` hook injects the brief on every model call
|
|
192
274
|
(compaction-proof, live-verified), plus a registered skill with the full
|
|
193
275
|
guide.
|
|
194
|
-
- **Claude Code**: `SessionStart` hook (
|
|
195
|
-
|
|
276
|
+
- **Claude Code**: `SessionStart` hook (re-fires after `compact`), from
|
|
277
|
+
either the `load-lock-plugin` plugin or the loose install; the loose
|
|
278
|
+
install adds a `~/.claude/CLAUDE.md` section + skill, the plugin ships
|
|
279
|
+
neither (full guide via the `agent_guide` MCP tool).
|
|
196
280
|
- **Codex CLI**: `SessionStart` hook (matcher `compact`) + `SubagentStart`
|
|
197
281
|
hook + `~/.codex/AGENTS.md` section (hooks require the interactive
|
|
198
282
|
hook-trust review once; live-verified in Docker).
|
|
@@ -203,6 +287,21 @@ Policy is short (one brief, no repeated reminders), reinforced by tool
|
|
|
203
287
|
names/descriptions, and never interrupts ordinary work with approval
|
|
204
288
|
prompts.
|
|
205
289
|
|
|
290
|
+
**Duplicate hook wiring is harmless (issue #54).** Loose hooks, a plugin
|
|
291
|
+
and ACE wiring may all be installed at once, so one harness event can fire
|
|
292
|
+
the same load-lock hook several times. The daemon is the singleton, so it
|
|
293
|
+
drops duplicates: each hook handler first calls `hook_once` with the key
|
|
294
|
+
`(harness, session_id, event, occurrence)`; only the first caller acts, the
|
|
295
|
+
rest exit 0 silently. The occurrence separates legitimate repeats:
|
|
296
|
+
SessionStart uses its `source` (a later `compact` re-injects the policy),
|
|
297
|
+
Stop hooks (Claude Code, Codex, Cursor) use the offer token being delivered
|
|
298
|
+
(each offer becomes at most one continuation per session). Keys expire on
|
|
299
|
+
their own (default 60s) and are never persisted; the `await-offer` watcher
|
|
300
|
+
instead *holds* its key for its connection's lifetime, so a session has at
|
|
301
|
+
most one live watcher and the key frees when the watcher exits. Hooks fail
|
|
302
|
+
open: no session id, a daemon without `hook_once` (`unknown method`), or
|
|
303
|
+
any error means the hook acts exactly as before.
|
|
304
|
+
|
|
206
305
|
## Failure modes covered
|
|
207
306
|
|
|
208
307
|
| Failure | Behavior |
|
|
@@ -217,6 +316,22 @@ prompts.
|
|
|
217
316
|
| Config change while offers outstanding | Capacity increase admits queued agents immediately; capacity decrease lets holders finish and delays new admissions; outstanding offers keep their reservations |
|
|
218
317
|
| Duplicate claims/releases | Idempotent or precise `stale_token`/`offer_expired` errors, never double effects |
|
|
219
318
|
|
|
319
|
+
### Runtime copies and the restart hint
|
|
320
|
+
|
|
321
|
+
`daemon.json` records the daemon's package root and version, and
|
|
322
|
+
`runtimes.json` the copies seen via the handshake (#52). When a strictly
|
|
323
|
+
newer client connects, the daemon respawns onto that client's copy (#53)
|
|
324
|
+
if the envelope's `clientRoot` names a `@aron98/load-lock` package whose
|
|
325
|
+
on-disk version equals the claimed `clientVersion` and which ships
|
|
326
|
+
`dist/core/daemon-main.js`; otherwise it reruns its own argv. The
|
|
327
|
+
`LOAD_LOCK_RESTARTED_FOR` marker keeps one restart per version either way.
|
|
328
|
+
|
|
329
|
+
Security: the hint arrives over the daemon's socket, which only the same
|
|
330
|
+
user can reach. Following it grants no privilege the client did not
|
|
331
|
+
already have — that client could run the same file itself. The checks
|
|
332
|
+
exist to avoid respawning onto garbage (wrong package, mismatched version,
|
|
333
|
+
missing entry), not as a trust boundary.
|
|
334
|
+
|
|
220
335
|
## Extension points
|
|
221
336
|
|
|
222
337
|
The protocol already carries weights (a request may ask for >1 units of
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -46,6 +46,30 @@ Status labels mean exactly this:
|
|
|
46
46
|
| **omac** (agent sandbox; verified with 0.11.0) | N/A — omac is a sandbox runtime, not a harness: the agent inside it uses whichever harness adapter applies (OpenCode, Claude Code, …) | via the inner harness | via the inner harness | via the inner harness | via the inner harness | Full for the sandbox grants: `load-lock uninstall --harness omac` removes exactly the entries install added (match-own-entries, empty sections pruned) | **Sandbox-profile grants installed + live-verified** (`install --harness omac`: `filesystem.allow_unix_dir` += state dir, `filesystem.read` += package tree when needed, `environment.allow_vars` += `LOAD_LOCK_*`) |
|
|
47
47
|
| **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) |
|
|
48
48
|
|
|
49
|
+
## Rung per harness
|
|
50
|
+
|
|
51
|
+
The install ladder (OPERATIONS.md, "The install ladder") has rungs 0 = skill
|
|
52
|
+
only, 1 = MCP server, 2 = harness plugin, plus the loose
|
|
53
|
+
`npm i -g` + `load-lock install --harness <h>` wiring. **Rung 0 works
|
|
54
|
+
everywhere** an agent can read the skill and run a shell: it only needs
|
|
55
|
+
the CLI (on PATH or through the skill's pinned `npx`), so it is not
|
|
56
|
+
repeated per row. `load-lock setup` reports the rung it finds per harness
|
|
57
|
+
as `plugin`, `loose`, `MCP only`, `skill only` or `not installed`.
|
|
58
|
+
|
|
59
|
+
| Harness | MCP server (1) | Plugin (2) | Loose `install` | Highest rung |
|
|
60
|
+
| --- | --- | --- | --- | --- |
|
|
61
|
+
| **OpenCode** | generic stdio registration works; the plugin's native `load_lock_*` tools are better | npm plugin `@aron98/load-lock` (also listed on ACE via `ace/opencode/`) — live-verified | `install --harness opencode` registers that same npm plugin | **plugin** |
|
|
62
|
+
| **Claude Code** | ACE `ace/mcp` or a manual stdio registration | `load-lock-plugin` from this repo's marketplace — contract-tested, passes `claude plugin validate --strict`; not yet live-verified as a plugin (pending #63); ACE's skills-dir install unverified (#63/#66) | `install --harness claude-code`: MCP + hooks + `CLAUDE.md` section + skill — live-verified in Docker | **plugin** (loose gives the same capability) |
|
|
63
|
+
| **Codex CLI** | `[mcp_servers]` entry in `~/.codex/config.toml` | none shipped | `install --harness codex`: MCP + hooks + `AGENTS.md` section — live-verified in Docker | **loose** |
|
|
64
|
+
| **Cursor** | project `.cursor/mcp.json` | none shipped | `install --harness cursor --project <dir>`: MCP + stop hook + rule — contract-tested | **loose** |
|
|
65
|
+
| **Gemini CLI, Copilot CLI, Cline, Roo Code, Goose** | the harness's MCP config | none shipped | `install --harness <h>`: MCP + instructions/rule file, no hooks — config-tested | **loose** (rung 1 plus a policy file; no idle wakeup) |
|
|
66
|
+
| **omac** | n/a (sandbox, not a harness) | — | `install --harness omac`: sandbox-profile grants — live-verified | the inner harness's rung |
|
|
67
|
+
| **Aider** | no MCP support | none | none | **0** (CLI only) |
|
|
68
|
+
|
|
69
|
+
"Highest rung" is what `load-lock setup --yes` converges a harness to once
|
|
70
|
+
it has some load-lock wiring (OPERATIONS.md, "Converging"). Mixing rungs in
|
|
71
|
+
one harness is safe (ARCHITECTURE.md, "Three layers").
|
|
72
|
+
|
|
49
73
|
## How each integration tier works
|
|
50
74
|
|
|
51
75
|
### Tier 1 — OpenCode (native plugin, live-verified)
|
|
@@ -73,6 +97,23 @@ tool round-trip (acquire → queued → wakeup → claim → release), execution
|
|
|
73
97
|
boundary delivery without interrupting a running `sleep`, skill visible,
|
|
74
98
|
and graceful handling of a deliberately missed offer (paused → cancel).
|
|
75
99
|
|
|
100
|
+
#### ACE listing (opencode, issue #79)
|
|
101
|
+
|
|
102
|
+
ACE ([TNG/ace](https://github.com/TNG/ace)) cannot detect the root
|
|
103
|
+
`package.json` as an OpenCode plugin, because its entry point is the
|
|
104
|
+
gitignored `dist/`. The OpenCode listing therefore comes from the dedicated
|
|
105
|
+
folder `ace/opencode/`. It is a thin pointer: `index.js` re-exports
|
|
106
|
+
`@aron98/load-lock`, and `package.json` (`load-lock-opencode`, private) pins
|
|
107
|
+
that dependency to the release version through `scripts/check-pins.mjs`.
|
|
108
|
+
ACE finds the folder through its npm-package heuristic (a `main` that exists
|
|
109
|
+
in the git tree, plus `opencode` in the name or keywords). ACE marks every
|
|
110
|
+
such detection as **"verify before registering"**, so that note is expected.
|
|
111
|
+
`src/test/ace-opencode.test.ts` mirrors the heuristic (TNG/ace@b703410) and
|
|
112
|
+
asserts that this repo yields exactly one OpenCode plugin, at `ace/opencode`.
|
|
113
|
+
Users install the published package as usual (a `plugins` entry
|
|
114
|
+
`@aron98/load-lock` in `opencode.json`), not the folder itself. The
|
|
115
|
+
end-to-end ACE install is not yet live-verified (#66).
|
|
116
|
+
|
|
76
117
|
#### Multiple copies of load-lock (opencode)
|
|
77
118
|
|
|
78
119
|
`install --harness opencode` registers the **bare package name** in
|
|
@@ -97,12 +138,20 @@ Which copy is authoritative for what:
|
|
|
97
138
|
- **The daemon's code** — whichever copy spawned it (usually the CLI copy,
|
|
98
139
|
since auto-spawn and `service start` run from the CLI client code). The
|
|
99
140
|
version handshake makes a daemon self-restart onto updated code when a
|
|
100
|
-
newer client contacts it
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
141
|
+
newer client contacts it. Since #53 the respawn moves onto the newer
|
|
142
|
+
client's own copy (validated from its `clientRoot` hint), so a daemon
|
|
143
|
+
spawned from a different copy than the one you updated follows the
|
|
144
|
+
newer client. Clients older than #52 send no `clientRoot`; for them the
|
|
145
|
+
respawn reuses the daemon's original argv and only picks up an in-place
|
|
146
|
+
update of its spawning copy — `load-lock service restart` (since #38)
|
|
147
|
+
re-spawns it from the current CLI copy.
|
|
148
|
+
- **npx-launched copies** (since #57) — skill / ACE / pinned MCP entries run
|
|
149
|
+
`npx -y @aron98/load-lock@<pin>`, which installs a copy under the npm
|
|
150
|
+
cache at `~/.npm/_npx/<hash>/node_modules/@aron98/load-lock/` (or under
|
|
151
|
+
`$npm_config_cache/_npx`; `%LocalAppData%\npm-cache\_npx` on Windows).
|
|
152
|
+
`doctor` lists these as `npx-cache`. They are owned by the pin that
|
|
153
|
+
launches them: `load-lock update` refuses to touch them and points at
|
|
154
|
+
bumping the pinned version (or the ACE artifact) instead.
|
|
106
155
|
|
|
107
156
|
The mirror drift — a *newer* adapter copy (opencode refreshed its cache)
|
|
108
157
|
with an *older* CLI on PATH — surfaces at adapter init (issue #38): the
|
|
@@ -135,6 +184,51 @@ observed live on this machine before the feature existed.
|
|
|
135
184
|
idle Claude. The token is deliberately NOT in that message (stderr may be
|
|
136
185
|
shown); the agent claims via `pending`/`claim`.
|
|
137
186
|
|
|
187
|
+
**Tool names depend on the wiring.** A loose install (`load-lock install`)
|
|
188
|
+
registers the MCP server as `load-lock`, so Claude Code exposes
|
|
189
|
+
`mcp__load-lock__<tool>`. A plugin-bundled server is namespaced by plugin
|
|
190
|
+
and server key, `mcp__plugin_<plugin>_<server>__<tool>` (per the Claude Code
|
|
191
|
+
MCP docs), e.g. `mcp__plugin_load-lock-plugin_load-lock__claim` for the
|
|
192
|
+
`load-lock-plugin` Claude Code plugin. Every text
|
|
193
|
+
load-lock injects (policy brief, hook claim/wakeup messages, CLAUDE.md /
|
|
194
|
+
AGENTS.md sections, skill) therefore names tools neutrally ("the load-lock
|
|
195
|
+
`claim` tool") instead of hard-coding a prefix.
|
|
196
|
+
|
|
197
|
+
**Claude Code plugin** (`/plugin install load-lock-plugin@load-lock`, see
|
|
198
|
+
OPERATIONS "Claude Code plugin"): the same MCP server and hooks, bundled
|
|
199
|
+
and run through the dependency-free launcher; no skill, the detailed guide
|
|
200
|
+
comes from the `agent_guide` tool. Its manifest, hooks file and
|
|
201
|
+
marketplace entry are contract-tested (`src/test/plugin.test.ts`) and pass
|
|
202
|
+
`claude plugin validate --strict` (Claude Code 2.1.312), and the hook
|
|
203
|
+
commands are tested the way the plugin invokes them. Hooks use shell form
|
|
204
|
+
(one `command` string, `${CLAUDE_PLUGIN_ROOT}` quoted; #63).
|
|
205
|
+
|
|
206
|
+
**Plugin live verification** (#63; Claude Code 2.1.312, 2026-10-29, Linux
|
|
207
|
+
host): a real session with a throwaway `CLAUDE_CONFIG_DIR` and `HOME`,
|
|
208
|
+
load-lock isolated by `LOAD_LOCK_STATE_DIR` / `LOAD_LOCK_SOCKET`, the
|
|
209
|
+
plugin installed from this repo's marketplace with `claude plugin
|
|
210
|
+
marketplace add <checkout>` + `claude plugin install
|
|
211
|
+
load-lock-plugin@load-lock`, and the checkout's CLI first on `PATH`.
|
|
212
|
+
Headless `claude -p` and an interactive pty session. Status: **plugin
|
|
213
|
+
live-verified** for the checks marked observed:
|
|
214
|
+
|
|
215
|
+
| Check | Result |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| Shell-form hooks run, launcher takes the PATH branch | Observed (`load-lock launcher: path 1.5.0` on every hook) |
|
|
218
|
+
| SessionStart policy at startup | Observed (model quoted the isolated daemon's `load-lock now:` line) |
|
|
219
|
+
| SessionStart policy after `/compact` | Observed (`SessionStart:compact` hook ran; model quoted the fresh `1/1` line where the startup brief said `0/1`) |
|
|
220
|
+
| MCP tools named `mcp__plugin_load-lock-plugin_load-lock__<tool>` | Observed (all 19 tools listed under that prefix; server `plugin:load-lock-plugin:load-lock` connected) |
|
|
221
|
+
| `agent_guide` tool | Observed (called; returned the guide) |
|
|
222
|
+
| Stop offer continuation | Observed (headless: Stop hook returned `decision: block` with the offer token; model claimed and released) |
|
|
223
|
+
| `await-offer` idle wakeup (`async` + `asyncRewake`) | Observed (interactive: session idle ~18s, offer arrived, the watcher's exit 2 woke the session with a task notification; model ran `pending` → `claim` → `release`) |
|
|
224
|
+
| Daemon started by the plugin | Observed (fresh state dir; the daemon came up during session start with `CLAUDE_PLUGIN_ROOT` in its environment and `daemon.json` naming the PATH copy). Which plugin component spawned it first (hook or MCP server) was not determined. |
|
|
225
|
+
| Session-derived identity | Observed for the plugin MCP server (requests carried agent `claude-code:<session id>` matching the session's transcript). That hooks read the same `session_id` from stdin was not separately inspected. |
|
|
226
|
+
| Loose install + plugin together | Not observed (not run; covered by the #54 dedupe tests only) |
|
|
227
|
+
| Launcher npx / known-copy branches inside a live session | Not observed (only the PATH branch ran) |
|
|
228
|
+
|
|
229
|
+
The TUI labels the asyncRewake wake as "Stop hook blocking error from
|
|
230
|
+
command SessionStart:startup"; the message text is the watcher's.
|
|
231
|
+
|
|
138
232
|
Contract tests cover all hook stdin/stdout shapes against the documented
|
|
139
233
|
harness contracts. Claude Code is additionally **live-verified** (in Docker,
|
|
140
234
|
via `docker/run-claude-verify.sh` against the real `claude` CLI): SessionStart
|
|
@@ -267,7 +361,7 @@ without a dedicated installer.
|
|
|
267
361
|
Idle policy: these harnesses get no push notifications, so the PRIMARY
|
|
268
362
|
idle mechanism is the MCP `wait_for_turn` tool — a bounded, honest sleep
|
|
269
363
|
until it is the caller's turn. Queued agents call
|
|
270
|
-
|
|
364
|
+
the load-lock `wait_for_turn` tool (optionally with `timeout_ms`, default
|
|
271
365
|
600000, capped by the machine's `maxTurnWaitMs` config) and get a
|
|
272
366
|
structured result on every path: `granted` (the offer was auto-claimed —
|
|
273
367
|
`permit_token` included), `still_queued` (timeout, with queue position
|
|
@@ -333,7 +427,7 @@ UI** — where no UI surface exists, we say so rather than claim parity.
|
|
|
333
427
|
| Harness | In-harness dashboard | Also available |
|
|
334
428
|
| --- | --- | --- |
|
|
335
429
|
| **OpenCode** | Native TUI panel: `/load-lock` opens `load-lock.dashboard` **in-window** as a side panel (also opened by clicking the sidebar widget; deliberately **not** bound to any key — issue #28: the old `gl` sequence swallowed plain `g` typing, and the candidate `ctrl+g` is the host's `session.first` default) with pools, holder cards, offer claim-window countdowns, queue, paused, event feed, `inspect_machine`; home-footer `lock 1/1` indicator on the home screen plus a clickable, health-colored `load-lock 0/3` widget in the session sidebar; `x` force-release behind select + confirm. Implemented as the package's `./tui` companion plugin (main plugin unchanged). Live-verified: opencode 2.0.29 (panel render, palette/slash command, confirm-gated force-release) and opencode 2.0.32 in a real pty (sidebar widget render, click-to-open, in-window presentation, plain `g` reaches the composer, `ctrl+g` not intercepted by load-lock). | `load-lock dashboard` (alias `top`) |
|
|
336
|
-
| **Claude Code** | None in-app — Claude Code
|
|
430
|
+
| **Claude Code** | None in-app — load-lock ships no in-app view for Claude Code: `load-lock-plugin` bundles the MCP server + hooks only, and the loose install adds no UI either. 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 dashboard` (any terminal) |
|
|
337
431
|
| **Codex CLI** | None in-app — same reason. `load_lock_status` MCP tool; `SessionStart`/`SubagentStart` machine summary. | `load-lock dashboard` (alias `top`) |
|
|
338
432
|
| **Gemini CLI** | None in-app (no hooks, no UI plugin surface). `load_lock_status` MCP tool at tool boundaries. | `load-lock dashboard` (alias `top`) |
|
|
339
433
|
| **Copilot CLI, Cursor, Cline, Roo Code, Goose** | None in-app. MCP `status` tool where the MCP server is configured. | `load-lock dashboard` (alias `top`) |
|
|
@@ -348,14 +442,49 @@ reconnect, view-model shaping, frame rendering) is covered by
|
|
|
348
442
|
|
|
349
443
|
## Session identity per harness
|
|
350
444
|
|
|
445
|
+
The daemon routes offers, `pending` and op-key duplicate suppression by agent
|
|
446
|
+
id, so each harness session needs exactly one id, whichever wiring (CLI, MCP
|
|
447
|
+
server, hook) made the call. The default identity of the CLI (issue #49) and
|
|
448
|
+
of the MCP server (issue #50) comes from the shared resolver in
|
|
449
|
+
`src/shared/identity.ts`, first match wins:
|
|
450
|
+
|
|
451
|
+
1. explicit override: `--agent` (CLI), `agent_id` (MCP tools), or
|
|
452
|
+
`LOAD_LOCK_AGENT`;
|
|
453
|
+
2. a harness session env var exported to the processes the harness spawns:
|
|
454
|
+
`CLAUDE_CODE_SESSION_ID` → `claude-code:<session_id>` (the same value hooks
|
|
455
|
+
receive as `session_id`; verified on Claude Code 2.1.x), and
|
|
456
|
+
`OPENCODE_SESSION_ID` → the bare `sessionID` (exported by OpenCode's bash
|
|
457
|
+
tool; verified on opencode 2.1.1). The bare id is exactly the native
|
|
458
|
+
plugin's agent id, so CLI calls from an OpenCode shell share the plugin's
|
|
459
|
+
identity;
|
|
460
|
+
3. the stable harness ancestor process (Linux): walk up from the CLI's parent
|
|
461
|
+
past shells and launcher wrappers (`sh`/`bash`/`zsh`/…, `env`, `timeout`,
|
|
462
|
+
`npm`/`npx`, nested `load-lock`) to the first other ancestor, identified as
|
|
463
|
+
`proc:<pid>:<start time>` (pid-reuse safe via `/proc/<pid>/stat`), labelled
|
|
464
|
+
`<comm>:<pid>`. This covers harnesses without a verified env var (Codex,
|
|
465
|
+
Cursor, Gemini CLI, …), since one session = one long-lived harness process;
|
|
466
|
+
4. otherwise (no harness ancestor, or no `/proc` — macOS/Windows without a
|
|
467
|
+
session env var) the legacy default: `cli:$USER@$HOSTNAME` for the CLI, a
|
|
468
|
+
random per-process `agt_…` id for the MCP server.
|
|
469
|
+
|
|
470
|
+
So two MCP registrations in one session (plugin-bundled + standalone) share
|
|
471
|
+
one id: an `acquire` through one shows in `pending` through the other, and
|
|
472
|
+
both heartbeat the same id (idempotent). The CLI and the MCP server of one
|
|
473
|
+
session share an id too when both see the same session env var or the same
|
|
474
|
+
harness ancestor; whether a harness exports its session env var to MCP
|
|
475
|
+
server processes is not verified for every harness.
|
|
476
|
+
|
|
477
|
+
Labels stay human-readable: session-derived ids get a short holder label
|
|
478
|
+
(`claude-code:ee690007`, `codex:12345`); explicit ids label themselves.
|
|
479
|
+
|
|
351
480
|
| Harness | Identity | Notes |
|
|
352
481
|
| --- | --- | --- |
|
|
353
|
-
| OpenCode | plugin context `sessionID` (also MCP `_meta` when used) | automatic |
|
|
354
|
-
| Claude Code | hooks get `session_id`; MCP tools take explicit `agent_id` |
|
|
355
|
-
| Codex | hooks get `session_id`; MCP tools take explicit `agent_id` | same |
|
|
356
|
-
| Cursor | hooks get `conversation_id` (no `cwd` — `workspace_roots[0]` identifies the request); MCP tools take explicit `agent_id` | same |
|
|
482
|
+
| OpenCode | plugin context `sessionID` (also MCP `_meta` when used); CLI via `OPENCODE_SESSION_ID` (same id) | automatic |
|
|
483
|
+
| Claude Code | hooks get `session_id`; CLI and MCP server via `CLAUDE_CODE_SESSION_ID`, else the harness ancestor; MCP tools also take explicit `agent_id` | automatic; explicit `agent_id` still wins |
|
|
484
|
+
| Codex | hooks get `session_id`; CLI and MCP server via harness ancestor process; MCP tools also take explicit `agent_id` | same |
|
|
485
|
+
| Cursor | hooks get `conversation_id` (no `cwd` — `workspace_roots[0]` identifies the request); CLI and MCP server via harness ancestor process; MCP tools also take explicit `agent_id` | same |
|
|
357
486
|
| omac (sandbox) | inner harness identity + `LOAD_LOCK_LABEL_PREFIX=omac` | dashboard shows holders as `omac:<label>` |
|
|
358
|
-
| Tier 3 | explicit `agent_id` argument | falls back to per-server-process id |
|
|
487
|
+
| Tier 3 | explicit `agent_id` argument; CLI and MCP server via harness ancestor process | MCP falls back to a per-server-process id only when no session is found |
|
|
359
488
|
|
|
360
489
|
## Platform matrix
|
|
361
490
|
|