@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.
Files changed (66) hide show
  1. package/README.md +61 -14
  2. package/dist/cli/copies.d.ts +3 -2
  3. package/dist/cli/copies.js +32 -3
  4. package/dist/cli/hook.js +48 -4
  5. package/dist/cli/install.d.ts +2 -0
  6. package/dist/cli/install.js +67 -13
  7. package/dist/cli/main.js +62 -5
  8. package/dist/cli/setup.d.ts +131 -0
  9. package/dist/cli/setup.js +942 -0
  10. package/dist/cli/update.d.ts +5 -1
  11. package/dist/cli/update.js +16 -3
  12. package/dist/core/daemon.d.ts +29 -3
  13. package/dist/core/daemon.js +111 -11
  14. package/dist/core/persist.d.ts +3 -0
  15. package/dist/core/persist.js +49 -10
  16. package/dist/core/state.d.ts +9 -0
  17. package/dist/core/state.js +41 -3
  18. package/dist/mcp/server.d.ts +4 -3
  19. package/dist/mcp/server.js +29 -6
  20. package/dist/policy/text.d.ts +13 -3
  21. package/dist/policy/text.js +14 -3
  22. package/dist/shared/agent-guide.d.ts +3 -0
  23. package/dist/shared/agent-guide.js +16 -0
  24. package/dist/shared/client.js +5 -3
  25. package/dist/shared/drift.d.ts +4 -2
  26. package/dist/shared/drift.js +24 -6
  27. package/dist/shared/identity.d.ts +51 -0
  28. package/dist/shared/identity.js +116 -0
  29. package/dist/shared/pkginfo.d.ts +9 -0
  30. package/dist/shared/pkginfo.js +16 -0
  31. package/dist/shared/protocol.d.ts +22 -0
  32. package/dist/shared/protocol.js +15 -0
  33. package/dist/shared/runtimes.d.ts +41 -0
  34. package/dist/shared/runtimes.js +81 -0
  35. package/dist/shared/version.d.ts +6 -0
  36. package/dist/shared/version.js +9 -1
  37. package/dist/test/ace-mcp.test.d.ts +1 -0
  38. package/dist/test/ace-mcp.test.js +157 -0
  39. package/dist/test/ace-opencode.test.d.ts +1 -0
  40. package/dist/test/ace-opencode.test.js +287 -0
  41. package/dist/test/check-pins.test.d.ts +1 -0
  42. package/dist/test/check-pins.test.js +108 -0
  43. package/dist/test/compat.test.d.ts +1 -0
  44. package/dist/test/compat.test.js +190 -0
  45. package/dist/test/hook.test.js +179 -1
  46. package/dist/test/identity.test.d.ts +1 -0
  47. package/dist/test/identity.test.js +155 -0
  48. package/dist/test/install.test.js +135 -1
  49. package/dist/test/launcher.test.d.ts +1 -0
  50. package/dist/test/launcher.test.js +222 -0
  51. package/dist/test/mcp.test.js +76 -7
  52. package/dist/test/plugin.test.d.ts +1 -0
  53. package/dist/test/plugin.test.js +144 -0
  54. package/dist/test/runtimes.test.d.ts +1 -0
  55. package/dist/test/runtimes.test.js +282 -0
  56. package/dist/test/setup.test.d.ts +1 -0
  57. package/dist/test/setup.test.js +354 -0
  58. package/dist/test/update-command.test.js +71 -0
  59. package/dist/test/update.test.js +33 -1
  60. package/docs/ADAPTERS.md +3 -2
  61. package/docs/AGENT_GUIDE.md +36 -2
  62. package/docs/ARCHITECTURE.md +122 -7
  63. package/docs/COMPATIBILITY.md +142 -13
  64. package/docs/OPERATIONS.md +292 -14
  65. package/package.json +3 -2
  66. package/skills/load-lock/SKILL.md +62 -0
@@ -7,6 +7,49 @@ Windows has a named-pipe transport adapter — implemented and unit-tested on
7
7
  Linux via platform injection, not yet verified on a Windows machine; see the
8
8
  Windows section at the bottom).
9
9
 
10
+ ### The install ladder
11
+
12
+ load-lock can be installed at several levels ("rungs"). Each rung works on
13
+ its own, and a higher rung only adds capability. Installing more than one,
14
+ in any order, is safe: there is still one daemon and one budget, one
15
+ identity per session, and duplicate hooks are dropped by the daemon (see
16
+ [ARCHITECTURE.md](ARCHITECTURE.md#three-layers-daemon-runtime-copies-harness-wiring)).
17
+
18
+ | Rung | What you install | Runtime it runs | What the agent gets |
19
+ | --- | --- | --- | --- |
20
+ | 0 | **Skill only** (`skills/load-lock/SKILL.md`) | the load-lock MCP tools if listed, else `load-lock` on PATH, else `npx -y @aron98/load-lock@<version>` (pinned in the skill) | the full permit lifecycle through the CLI (`run`, `wait`, `acquire` / `claim` / `release`, `cancel`); the first call starts the daemon. No idle wakeup, no policy injection, nothing after compaction |
21
+ | 1 | **MCP server** (`npx -y @aron98/load-lock@<version> mcp`) | the pinned npx copy | the tools; your offers in every load-lock tool result; `wait_for_turn` / `wait_for_completion` bounded sleeps; the policy in the MCP `instructions` where the harness shows them; the `agent_guide` tool |
22
+ | 2 | **Harness plugin**: Claude Code `load-lock-plugin`, OpenCode npm plugin `@aron98/load-lock` | Claude Code: the plugin launcher (PATH CLI if new enough, else a known copy, else pinned npx); OpenCode: its own plugin cache copy | everything the harness supports: tools, policy at session start that survives compaction, offer wakeups at turn ends and while idle |
23
+ | — | **`npm i -g` + `load-lock install --harness <h>`** | the global CLI | the same wiring as loose config files. The only route for harnesses without a plugin, and the escape hatch where plugins aren't allowed |
24
+
25
+ Each rung points at the next one once: the skill, the MCP `instructions`
26
+ and the session-start brief end with a one-line pointer to
27
+ [`load-lock setup`](#load-lock-setup-what-is-installed-where), which lists
28
+ what is installed where and converges it. The rung each harness can reach
29
+ is in [COMPATIBILITY.md](COMPATIBILITY.md#rung-per-harness).
30
+
31
+ How to install each rung:
32
+
33
+ - **Skill (0).** Through ACE (the `load-lock` skill artifact, see
34
+ [Installing via ACE](#installing-via-ace)), or copy `skills/load-lock/`
35
+ (shipped in the npm package and the repo) into your harness's skills
36
+ directory. `load-lock install --harness claude-code` also copies it to
37
+ `~/.claude/skills/load-lock/`.
38
+ - **MCP server (1).** Through ACE (the `ace/mcp` artifact), or register a
39
+ stdio server named `load-lock` with command `npx` and args
40
+ `["-y", "@aron98/load-lock@<version>", "mcp"]`, exactly what
41
+ `ace/mcp/mcp.json` declares. `load-lock mcp` is the same server as the
42
+ `load-lock-mcp` binary. With a global install,
43
+ `load-lock install --harness generic` prints a snippet that runs that
44
+ copy instead of npx.
45
+ - **Plugin (2).** Claude Code: see [Claude Code plugin](#claude-code-plugin).
46
+ OpenCode: add `@aron98/load-lock` to the `plugins` array of
47
+ `~/.config/opencode/opencode.json` (OpenCode downloads and updates it),
48
+ or let `load-lock install --harness opencode` register it.
49
+ - **Loose wiring.** The installers below.
50
+
51
+ ### `npm i -g` + `load-lock install`
52
+
10
53
  ```sh
11
54
  npm install -g @aron98/load-lock # or: npm install -g ./aron98-load-lock-<ver>.tgz
12
55
  load-lock install --harness opencode
@@ -32,13 +75,23 @@ round-trips in `src/test/install.test.ts` for all three (an earlier matcher
32
75
  never matched its own entries — duplicate installs and hooks that survived
33
76
  uninstall — see issue #19); not live-verified inside the real CLIs.
34
77
 
35
- - **opencode** — writes a plugin stub to
36
- `~/.config/opencode/plugins/load-lock/` (npm-installed against the global
37
- package), registers it in `~/.config/opencode/opencode.json`.
78
+ - **opencode** — registers the published package `@aron98/load-lock` in
79
+ the `plugins` array of `~/.config/opencode/opencode.json` (OpenCode
80
+ installs and updates it itself) and removes the legacy plugin stub
81
+ `~/.config/opencode/plugins/load-lock/` if an older install left one.
38
82
  - **claude-code** — adds `mcpServers.load-lock` to `~/.claude.json`; hooks
39
83
  (`SessionStart`, `Stop`, async `await-offer`) to `~/.claude/settings.json`;
40
84
  a marked section to `~/.claude/CLAUDE.md`; copies the skill to
41
- `~/.claude/skills/load-lock/`.
85
+ `~/.claude/skills/load-lock/`. A skill dir it didn't write (an ACE
86
+ install with an `.ace.json` stamp, or a symlink) is left alone by both
87
+ `install` and `uninstall`, which say so (#89). With the
88
+ [Claude Code plugin](#claude-code-plugin) enabled in
89
+ `~/.claude/settings.json` (`enabledPlugins["load-lock-plugin@…"]: true`),
90
+ `install` instead removes the loose MCP server, hooks and `CLAUDE.md`
91
+ section, reporting each step, and only installs the skill (the plugin
92
+ ships none); `--legacy` installs the loose wiring anyway. `uninstall`
93
+ removes loose entries only and names the `/plugin uninstall …` command
94
+ for the plugin (#62).
42
95
  - **codex** — writes `~/.codex/hooks.json` (`SessionStart`, `SubagentStart`,
43
96
  `Stop`), appends `[mcp_servers.load-lock]` to `~/.codex/config.toml` if
44
97
  absent, adds a marked section to `~/.codex/AGENTS.md`. **Then run `/hooks`
@@ -83,6 +136,135 @@ uninstall — see issue #19); not live-verified inside the real CLIs.
83
136
 
84
137
  `load-lock uninstall --harness <harness>` reverses any of these.
85
138
 
139
+ ### `load-lock setup`: what is installed where
140
+
141
+ `load-lock setup [--project <dir>] [--json]` lists every load-lock
142
+ integration it finds, for every harness `install` supports (the project
143
+ defaults to the cwd). It is **read-only**: it never writes a file.
144
+
145
+ For each harness it reports:
146
+
147
+ - **what** it found: plugins (Claude Code `enabledPlugins` entries named
148
+ `load-lock-plugin@…`, OpenCode `plugin` / `plugins` entries), MCP
149
+ registrations (user and project scope, including ACE's `.mcp.json`
150
+ `servers` patch), loose hooks, policy sections / rule files, skill
151
+ copies, and omac sandbox grants;
152
+ - **who** wrote each entry: `installer` (the exact shape `load-lock
153
+ install` writes), `harness` (the harness's own plugin state), `ace`
154
+ (an `.ace.json` stamp sits next to it), or `other` (manual, npx, …);
155
+ - **which copy** an entry runs, and whether that copy is gone (stale);
156
+ - the **rung**: `plugin`, `loose` (`load-lock install` wiring), `MCP only`,
157
+ `skill only`, or `not installed`;
158
+ - **findings**: loose hooks next to an active plugin, the MCP server
159
+ registered more than once (a Claude Code plugin's bundled server counts),
160
+ hooks from more than one copy, duplicate hook entries, several skill
161
+ copies, stale entries, disabled plugins.
162
+
163
+ It ends with the runtime copies: those on disk (as in `doctor`), the copy
164
+ the daemon runs (`daemon.json`), and the copies the daemon has seen.
165
+
166
+ An ACE-installed Claude plugin (unzipped into `.claude/skills/<name>/`) is
167
+ listed but not counted as an active plugin: whether Claude Code loads it
168
+ there is not verified yet (#63).
169
+
170
+ #### Converging: `load-lock setup --yes`
171
+
172
+ After the inventory, `setup` prints a **plan** that brings every harness
173
+ that already has some load-lock wiring to the recommended layout: the
174
+ highest rung available, exactly one of each. Without `--yes` it is a dry
175
+ run. `--yes` applies it (`--json` adds `plan` and `applied`). Harnesses with
176
+ nothing installed are left alone; the plan names them once.
177
+
178
+ The plan only uses moves the installers can reverse:
179
+
180
+ | Situation | Step |
181
+ |---|---|
182
+ | Claude Code plugin enabled **and** loose `install` wiring | `remove-loose`: drop the loose MCP server, hooks and `CLAUDE.md` section (the plugin provides them). The skill stays: the plugin ships none. Undo: `load-lock install --harness claude-code --legacy`. |
183
+ | Installer wiring incomplete, pointing at a missing copy, or hooks from several copies / duplicated | `install`: remove the installer-shaped hooks of every copy (and a stale codex `[mcp_servers.load-lock]` block), then `load-lock install --harness <h>` from the running copy |
184
+ | Only an MCP registration or only the skill | `install` (moves up to the installer's rung) |
185
+ | Anything not written by `load-lock install` (plugin state, ACE entries, manual / npx entries, extra skill copies) | `manual`: printed with what to remove or run (`/plugin uninstall …`), never applied |
186
+
187
+ Guards, each turning an `install` into a `manual` step:
188
+
189
+ - a foreign entry sits in the slot `install` would overwrite (e.g. a
190
+ hand-written `mcpServers.load-lock`);
191
+ - the Claude Code skill directory is managed by ACE (`.ace.json`);
192
+ - the running copy lives in the npx or opencode cache, which npm may prune
193
+ (install globally first);
194
+ - `cursor` / `roo-code` write into the project, so they are only refreshed
195
+ when the project already has their installer wiring.
196
+
197
+ `load-lock uninstall --harness <h>` still removes everything load-lock
198
+ added, after a converge as before it. The MCP instructions and the
199
+ session-start brief end with a one-line pointer to `load-lock setup`.
200
+
201
+ ### Claude Code plugin
202
+
203
+ Instead of `install --harness claude-code`, Claude Code can load load-lock
204
+ as a plugin (issue #61). The repo root is a marketplace
205
+ (`.claude-plugin/marketplace.json`) listing one plugin, `load-lock-plugin`,
206
+ in `plugins/claude-code/`:
207
+
208
+ ```text
209
+ /plugin marketplace add aron98/load-lock
210
+ /plugin install load-lock-plugin@load-lock
211
+ ```
212
+
213
+ - **MCP server** `load-lock`, declared inline in `plugin.json` and started
214
+ through the launcher (`--bin load-lock-mcp`). Tools are named
215
+ `mcp__plugin_load-lock-plugin_load-lock__<tool>`.
216
+ - **Hooks** in `hooks/hooks.json`, the same three as the loose install:
217
+ `SessionStart` (policy + machine summary), `Stop` (offer continuation)
218
+ and the `async` + `asyncRewake` `await-offer` watcher. Each runs in shell
219
+ form, one command string `node "${CLAUDE_PLUGIN_ROOT}/scripts/load-lock.mjs"
220
+ hook claude-code <event>` with the root quoted (#63). Shell form is the
221
+ long-documented hook shape; exec form (`command` + `args`) has no
222
+ documented minimum Claude Code version. The string has no shell
223
+ metacharacters, so ACE's `plugin_hooks_command_safe` check passes.
224
+ - **No skill and no CLAUDE.md section.** The policy arrives through the
225
+ SessionStart hook (re-fired after compaction), and the full guide through
226
+ the `agent_guide` MCP tool. Install the `load-lock` skill separately if
227
+ wanted. The plugin is not named `load-lock`, so it doesn't clash with the
228
+ skill on ACE (#64).
229
+ - **Runtime**: the plugin ships no `dist/`. See [Plugin launcher](#plugin-launcher)
230
+ for how hooks and the MCP server find one.
231
+ - `plugin.json` `version` is pinned to `package.json` by `check-pins`. A
232
+ pinned version keeps users on it until the next release changes it.
233
+
234
+ Combining the plugin with a loose `install --harness claude-code` is safe
235
+ (one session identity, duplicate hooks dropped by the daemon, #54), but
236
+ registers two MCP servers; keep one. Once the plugin is enabled, re-running
237
+ `load-lock install --harness claude-code` (or `load-lock setup --yes`) moves
238
+ the loose MCP server, hooks and `CLAUDE.md` section out (`--legacy` keeps
239
+ them), and `load-lock doctor` reports the mode (`claudeCodeMode`: `plugin`,
240
+ `loose`, `both` or `none`) and warns on `both`.
241
+
242
+ ### Installing via ACE
243
+
244
+ [ACE](https://github.com/TNG/ace) imports artifacts from this repository.
245
+ ACE does not edit harness config itself: its `install_artifact` tool hands
246
+ the agent a download plus instructions, and the agent unpacks and wires it
247
+ (findings in issue #64). This repo provides one artifact per folder:
248
+
249
+ | Folder | ACE kind | What installing it gives you |
250
+ | --- | --- | --- |
251
+ | `skills/load-lock/` | skill `load-lock` | rung 0; unpacked into the harness's skills directory (e.g. `.claude/skills/load-lock/`) |
252
+ | `ace/mcp/` | MCP server `load-lock` (local, stdio) | rung 1; ACE returns a patch with `npx -y @aron98/load-lock@<version> mcp` for the agent to merge into the MCP config (e.g. `.mcp.json`) |
253
+ | `plugins/claude-code/` | Claude Code plugin `load-lock-plugin` | rung 2; ACE unpacks Claude plugins as skills-directory plugins (`.claude/skills/load-lock-plugin/`), not through `/plugin install` |
254
+ | `ace/opencode/` | OpenCode plugin (pointer) | rung 2; install the published `@aron98/load-lock` into `opencode.json` as above, not the folder (see [COMPATIBILITY.md](COMPATIBILITY.md#ace-listing-opencode-issue-79)) |
255
+
256
+ Install the MCP server **or** the Claude Code plugin, not both: the plugin
257
+ already bundles the server (two registrations are harmless, just
258
+ redundant). The plugin is not named `load-lock`, so it doesn't unpack into
259
+ the skill's directory. ACE updates what it installed (it writes an
260
+ `.ace.json` stamp next to each bundle); `load-lock setup` reports those
261
+ entries as `ace` and never rewrites them.
262
+
263
+ Not yet verified (#63, #66): whether Claude Code loads a plugin from a
264
+ skills directory (and resolves `${CLAUDE_PLUGIN_ROOT}` there), and the ACE
265
+ install end to end on a clean machine. Until then `load-lock setup` lists an
266
+ ACE-installed Claude plugin but does not count it as an active plugin.
267
+
86
268
  ## The daemon
87
269
 
88
270
  - State dir: `~/.local/state/load-lock` (Windows: `%LOCALAPPDATA%\load-lock`;
@@ -96,7 +278,17 @@ uninstall — see issue #19); not live-verified inside the real CLIs.
96
278
  - Also honored by the shared client: `LOAD_LOCK_LABEL_PREFIX` (prefix for
97
279
  holder labels; see Sandboxes).
98
280
  - Files: `config.json` (settings + history), `journal.json` (held permits +
99
- queued requests), `daemon.json` (pid), `spawn.lock` (bootstrap race guard).
281
+ queued requests), `daemon.json` (pid, plus since #52 the daemon's
282
+ `packageRoot` and `version`), `runtimes.json` (known runtime copies, see
283
+ below), `spawn.lock` (bootstrap race guard).
284
+ - **Known runtime copies** (since #52): every client envelope carries its
285
+ package root (`clientRoot`); the daemon validates it (a package.json
286
+ naming `@aron98/load-lock`; the version is read from disk, not trusted
287
+ from the client) and records `{path, version, lastSeen}` in
288
+ `runtimes.json`, deduped by real path, pruning copies whose path is gone.
289
+ Small entry points (skill, plugin launchers) can read `daemon.json` /
290
+ `runtimes.json` to find an existing runtime without npx; `doctor` lists
291
+ them as `daemonRuntime` and `knownRuntimes`.
100
292
 
101
293
  ```sh
102
294
  load-lock service status # hello + pid + daemon version
@@ -117,6 +309,59 @@ reports both lines; a failure names the phase and reason
117
309
  Stopping when nothing runs is not an error: the start phase simply spawns a
118
310
  fresh daemon.
119
311
 
312
+ ## Plugin launcher
313
+
314
+ Plugin and ACE bundles are git folders without `dist/`, so their hooks and
315
+ MCP server go through a committed, dependency-free launcher,
316
+ `plugins/claude-code/scripts/load-lock.mjs` (issue #60; plain ESM, Node
317
+ built-ins only, no build step):
318
+
319
+ ```sh
320
+ node plugins/claude-code/scripts/load-lock.mjs hook claude-code stop # any `load-lock` CLI args
321
+ node plugins/claude-code/scripts/load-lock.mjs --bin load-lock-mcp # the MCP server
322
+ ```
323
+
324
+ It runs the first runtime it finds:
325
+
326
+ 1. `load-lock` on PATH, when its version is at least the launcher's pin;
327
+ 2. else the newest known copy at least the pin from the state dir
328
+ (`runtimes.json`, `daemon.json`'s `packageRoot`; see #52 above);
329
+ 3. else `npx --yes --package=@aron98/load-lock@<pin> -- <bin>`.
330
+
331
+ Branches 1 and 2 run the copy's compiled entry (`dist/cli/main.js` /
332
+ `dist/mcp/main.js`) with the launcher's own `node`, so no shell and no
333
+ `.cmd` shim is involved. For npx it prefers the `npx-cli.js` shipped next to
334
+ `node`, falling back to `npx` (POSIX) or `npx.cmd` through a shell
335
+ (Windows). The pin is kept equal to `package.json` by `check-pins`.
336
+
337
+ Hook mode (`hook ...`) never breaks or stalls a session:
338
+
339
+ - npx runs with `--offline` (npm cache only), so a cold cache fails in
340
+ well under a second instead of downloading. The MCP server's npx start
341
+ is online and warms the cache for later hooks.
342
+ - The run is bounded by `LOAD_LOCK_LAUNCHER_TIMEOUT_MS` (default 15000;
343
+ the process group is killed). Not applied to the long-lived
344
+ `await-offer` watcher.
345
+ - Any failure exits 0; exit 2 passes through (the `asyncRewake` wakeup).
346
+ A failed SessionStart that printed nothing prints one install hint
347
+ instead of the policy.
348
+
349
+ The MCP server mode passes the runtime's exit code through and reports a
350
+ missing runtime on stderr. `LOAD_LOCK_LAUNCHER_DEBUG=1` prints the chosen
351
+ branch to stderr.
352
+
353
+ **Measured latency (PATH branch).** On a Linux box (Node 22, 32 cores, warm
354
+ file cache, daemon running), median over 20 runs:
355
+
356
+ | Hook | Direct `node dist/cli/main.js` | Through the launcher |
357
+ |---|---|---|
358
+ | `hook claude-code stop` | 31 ms | 52 ms |
359
+ | `hook claude-code session-start` | 32 ms | 52 ms |
360
+
361
+ So the launcher adds about 20 ms (one extra Node start plus PATH/package.json
362
+ lookups). A warm npx (`--offline`) run adds about 0.2 s; a cold one fails in
363
+ about 0.2 s.
364
+
120
365
  ## Update lifecycle
121
366
 
122
367
  The daemon is long-lived, so an update (`npm i -g @aron98/load-lock@latest`,
@@ -127,9 +372,13 @@ code. The **version handshake** closes that gap (v1.3.0+):
127
372
  carry `daemonVersion` (the package version of the code the daemon runs).
128
373
  - When a **newer** client contacts an older daemon, the daemon finishes
129
374
  answering, then **self-restarts**: it persists state + journal, drains all
130
- connections, respawns itself with the same argv (`process.execPath` +
131
- original arguments, so it runs whatever is now on disk at the same path),
132
- and exits. The respawn inherits a `LOAD_LOCK_RESTARTED_FOR` marker so
375
+ connections, respawns, and exits. Since #53 the respawn runs the **newer
376
+ client's copy** when the envelope's `clientRoot` checks out (a
377
+ `@aron98/load-lock` package.json whose on-disk version equals the
378
+ claimed `clientVersion`, plus a `dist/core/daemon-main.js`), so a daemon
379
+ spawned by an old global CLI moves onto e.g. a newer npx copy. Without a
380
+ valid hint it falls back to the same argv (`process.execPath` + original
381
+ arguments, so it runs whatever is now on disk at the same path). The respawn inherits a `LOAD_LOCK_RESTARTED_FOR` marker so
133
382
  repeated contact with the same version cannot cause a restart loop — one
134
383
  respawn attempt per version.
135
384
  - Clients that were connected see a disconnect and simply reconnect to the
@@ -156,6 +405,7 @@ detected package manager — or refuses honestly rather than guessing:
156
405
  | bun global (`~/.bun/install/global/…`) | `bun i -g @aron98/load-lock@latest` |
157
406
  | npm global (under `npm root -g`) | `npm i -g @aron98/load-lock@latest` |
158
407
  | opencode's npm-cache copy | refuses — opencode manages this copy and refreshes it on its own schedule |
408
+ | npx cache copy (`~/.npm/_npx/<hash>/…`, since #57) | refuses — it is whatever version the launching `npx -y @aron98/load-lock@<pin>` pins; bump that pin (MCP/skill config or ACE artifact) |
159
409
  | a git checkout (nearest `.git`) | refuses — update it via your git workflow (`git pull` etc.) |
160
410
  | anything else | refuses — "cannot tell how this copy was installed" |
161
411
 
@@ -186,12 +436,18 @@ on stderr and in the injected context / MCP `instructions`. Notify only —
186
436
  that was already running keeps the *old* adapter code in memory until the
187
437
  harness itself restarts, so the notice fires from the new copy at its init
188
438
  — sessions started before a refresh see it on their next restart.
189
-
190
- Limits, stated honestly: the respawn re-runs the **same argv** — it picks up
191
- an updated copy only if the daemon's launching copy was updated *in place*
192
- (the normal `npm i -g` case). A daemon spawned from a different copy (e.g.
193
- opencode's private cache copy) keeps running that copy's code after a
194
- restart; see the multi-copy notes in
439
+ The same check covers **npx-vs-PATH skew** (#57): an MCP server launched
440
+ via a pinned `npx -y @aron98/load-lock@<pin>` runs from the npx cache, and
441
+ when that copy is newer than the PATH CLI the notice names it as the npx
442
+ copy. (The reverse — a pin older than the PATH CLI — is not flagged here;
443
+ the daemon's version handshake already keeps the daemon on the newer code.)
444
+
445
+ Limits, stated honestly: the respawn moves onto the newer client's copy
446
+ only when that client sends a valid `clientRoot` (#52/#53). Clients older
447
+ than #52 don't, so the respawn re-runs the **same argv** and picks up an
448
+ updated copy only if the daemon's launching copy was updated *in place*
449
+ (the normal `npm i -g` case); `load-lock service restart` moves it onto
450
+ the current CLI copy. See the multi-copy notes in
195
451
  [COMPATIBILITY.md](COMPATIBILITY.md#multiple-copies-of-load-lock-opencode).
196
452
  Pre-1.3.0 daemons do not answer the handshake and never self-restart —
197
453
  `service restart` (or `service stop` followed by any load-lock command)
@@ -1050,3 +1306,25 @@ Caveats to watch for (unverified expectations):
1050
1306
  `LOAD_LOCK_SOCKET` at a different pipe name if that is a problem.
1051
1307
  - `inspect_machine` cgroup detection reads `/sys/fs/cgroup` and will
1052
1308
  degrade gracefully (`cgroupVersion: "none"`), reporting raw host numbers.
1309
+
1310
+ ## Releasing (maintainers)
1311
+
1312
+ Publishing is automated: publishing a GitHub Release runs
1313
+ `.github/workflows/publish.yml` (npm Trusted Publishing; read its comments
1314
+ before changing the flow). The release commit itself is manual:
1315
+
1316
+ 1. Bump `version` in `package.json` (and `package-lock.json`), e.g.
1317
+ `npm version 1.6.0 --no-git-tag-version`.
1318
+ 2. Sync every pinned `@aron98/load-lock@<version>` to it:
1319
+ `npm run check-pins -- --write` (alias for
1320
+ `node scripts/check-pins.mjs --write`), then `npm run check-pins` must
1321
+ print `pins ok`. Commit the bump and the pin updates together.
1322
+ 3. Merge, then publish a GitHub Release tagged `v<version>`.
1323
+
1324
+ Pinned files (the skill's npx fallback, ACE `ace/mcp/mcp.json` and
1325
+ `ace/opencode/package.json`, the Claude Code plugin's `plugin.json` and
1326
+ launcher) are declared in the
1327
+ `PINNED` list at the top of `scripts/check-pins.mjs`; add an entry whenever
1328
+ a new artifact pins a version. CI (`pins` job in `ci.yml`) fails on any
1329
+ pin that differs from `package.json`, on a declared file that is missing,
1330
+ and on a declared file that no longer contains a pin.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aron98/load-lock",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Machine-wide counting semaphore coordinating resource-intensive work across parallel coding agents (OpenCode, Codex CLI, Claude Code, ...)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -88,7 +88,8 @@
88
88
  "scripts": {
89
89
  "build": "tsc -p tsconfig.json",
90
90
  "test": "tsc -p tsconfig.json && node --test dist/test/*.test.js",
91
- "typecheck": "tsc -p tsconfig.json --noEmit"
91
+ "typecheck": "tsc -p tsconfig.json --noEmit",
92
+ "check-pins": "node scripts/check-pins.mjs"
92
93
  },
93
94
  "engines": {
94
95
  "node": ">=20"
@@ -1,3 +1,17 @@
1
+ ---
2
+ name: load-lock
3
+ description: >-
4
+ Teach parallel AI coding agents sharing one machine to take turns on heavy
5
+ work such as full builds and test suites. Agents acquire a machine-wide
6
+ permit before resource-intensive local work, keep doing light work while
7
+ queued FIFO, sleep honestly with bounded waits instead of polling, and
8
+ release the permit when done. Use when several agents on one machine risk
9
+ overloading it by starting heavy work in the same minute. If load-lock is
10
+ not installed, this skill runs it via `npx -y @aron98/load-lock@1.6.0`
11
+ (downloads the pinned npm package), and load-lock keeps its shared state in
12
+ ~/.local/state/load-lock (a small per-user daemon started on first use).
13
+ ---
14
+
1
15
  # load-lock: machine-wide heavy-work coordination
2
16
 
3
17
  All coding agents on this machine (any harness, any repo/worktree) share one
@@ -5,6 +19,54 @@ small budget of heavy-work permits. Before resource-intensive local work, a
5
19
  polite agent acquires a permit; everyone else keeps doing light work until an
6
20
  offer arrives.
7
21
 
22
+ ## How to call load-lock
23
+
24
+ Use the first of these that works; they all talk to the same daemon:
25
+
26
+ 1. **load-lock MCP tools** (`acquire`, `wait_for_turn`, `run_with_permit`,
27
+ ...), if your harness lists them. The rest of this skill names them.
28
+ 2. **`load-lock` on PATH**: check with `load-lock version`.
29
+ 3. **Pinned npx**: `npx -y @aron98/load-lock@1.6.0 <command>` (Node >= 20).
30
+ It downloads that exact npm package once into the npx cache. Below,
31
+ read `load-lock` as this whole prefix. `npm install -g @aron98/load-lock`
32
+ makes it permanent.
33
+
34
+ The first call starts the daemon on its own (shared state in
35
+ `~/.local/state/load-lock`, or `$XDG_STATE_HOME/load-lock`); you never start
36
+ it yourself.
37
+
38
+ ### CLI-only use (no MCP tools)
39
+
40
+ `load-lock help` lists every command. The safest flow is `load-lock run`,
41
+ which acquires a permit, runs the command, and releases it when the command
42
+ exits (success or failure):
43
+
44
+ ```sh
45
+ load-lock run --reason "full test suite" --op-key test/all -- npm test
46
+ ```
47
+
48
+ - Exit code **68** with `queued at position N (request <id>)` means you are
49
+ not allowed to start yet. Keep doing light work. Out of light work? Sleep
50
+ honestly with `load-lock wait --id <request-id> --timeout 600000`: it
51
+ wakes you on your turn, claims the offer, and prints a `permit_token`.
52
+ Then `load-lock run --permit <permit_token> -- npm test`. A `wait` that
53
+ exits 68 again (still queued) is a checkpoint: call it again.
54
+ - No longer need a queued request: `load-lock cancel --id <request-id>`.
55
+ Your own offers, queue entries and paused requests: `load-lock pending`.
56
+
57
+ **Identity.** The daemon routes offers and `pending` per agent. The CLI
58
+ derives your agent id from your harness session (Claude Code and OpenCode
59
+ session env vars; on Linux, otherwise your harness process), so every call
60
+ from one session counts as one agent, and other sessions are other agents.
61
+ If `pending` does not show a request you made, pin the id yourself: set
62
+ `LOAD_LOCK_AGENT=<unique-name>` (or pass `--agent <unique-name>`) on every
63
+ call.
64
+
65
+ Skill-only use has no idle wakeup (nothing tells you an offer arrived while
66
+ you sit idle; use `load-lock wait`) and no policy reminder after context
67
+ compaction. To get both, install the harness integration once:
68
+ `load-lock setup` (or `load-lock install --harness <name>`).
69
+
8
70
  ## When to use it
9
71
 
10
72
  **Heavy** (acquire first): full builds, `cargo build`/`npm run build`, whole