@aron98/load-lock 1.5.0 → 1.6.1
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 +62 -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 +206 -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 +93 -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/OPERATIONS.md
CHANGED
|
@@ -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** —
|
|
36
|
-
`~/.config/opencode/
|
|
37
|
-
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
(
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
3
|
+
"version": "1.6.1",
|
|
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.1`
|
|
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.1 <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
|