@aron98/load-lock 0.1.2 → 1.1.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 (53) hide show
  1. package/README.md +40 -12
  2. package/demo/demo.sh +1 -1
  3. package/dist/adapters/opencode/plugin.js +39 -18
  4. package/dist/adapters/opencode/tui.d.ts +61 -0
  5. package/dist/adapters/opencode/tui.js +358 -0
  6. package/dist/cli/hook.js +35 -1
  7. package/dist/cli/install.js +708 -24
  8. package/dist/cli/main.js +130 -61
  9. package/dist/cli/top.d.ts +7 -0
  10. package/dist/cli/top.js +99 -0
  11. package/dist/core/daemon.d.ts +25 -4
  12. package/dist/core/daemon.js +67 -30
  13. package/dist/core/persist.js +5 -12
  14. package/dist/core/state.js +7 -0
  15. package/dist/dashboard/feed.d.ts +74 -0
  16. package/dist/dashboard/feed.js +256 -0
  17. package/dist/dashboard/frame.d.ts +13 -0
  18. package/dist/dashboard/frame.js +95 -0
  19. package/dist/dashboard/view.d.ts +149 -0
  20. package/dist/dashboard/view.js +214 -0
  21. package/dist/mcp/server.d.ts +2 -1
  22. package/dist/mcp/server.js +54 -19
  23. package/dist/policy/text.d.ts +1 -1
  24. package/dist/policy/text.js +3 -0
  25. package/dist/shared/client.d.ts +10 -0
  26. package/dist/shared/client.js +41 -14
  27. package/dist/shared/protocol.d.ts +29 -0
  28. package/dist/shared/transport.d.ts +40 -0
  29. package/dist/shared/transport.js +136 -0
  30. package/dist/shared/wait-turn.d.ts +29 -0
  31. package/dist/shared/wait-turn.js +315 -0
  32. package/dist/test/cli.test.d.ts +1 -0
  33. package/dist/test/cli.test.js +99 -0
  34. package/dist/test/dashboard.test.d.ts +1 -0
  35. package/dist/test/dashboard.test.js +406 -0
  36. package/dist/test/hook.test.js +53 -3
  37. package/dist/test/install.test.d.ts +1 -0
  38. package/dist/test/install.test.js +658 -0
  39. package/dist/test/mcp.test.js +36 -1
  40. package/dist/test/platform.test.d.ts +1 -0
  41. package/dist/test/platform.test.js +193 -0
  42. package/dist/test/testutil.js +4 -1
  43. package/dist/test/tui.test.d.ts +1 -0
  44. package/dist/test/tui.test.js +89 -0
  45. package/dist/test/wait-turn.test.d.ts +1 -0
  46. package/dist/test/wait-turn.test.js +376 -0
  47. package/docs/ADAPTERS.md +5 -2
  48. package/docs/AGENT_GUIDE.md +34 -1
  49. package/docs/ARCHITECTURE.md +25 -5
  50. package/docs/COMPATIBILITY.md +217 -26
  51. package/docs/OPERATIONS.md +335 -8
  52. package/package.json +25 -1
  53. package/skills/load-lock/SKILL.md +4 -0
@@ -3,19 +3,35 @@
3
3
  ## Install
4
4
 
5
5
  Requirements: Node 20+, Linux (macOS works with the limitations noted below;
6
- Windows would need a named-pipe adapter — not implemented).
6
+ Windows has a named-pipe transport adapter — implemented and unit-tested on
7
+ Linux via platform injection, not yet verified on a Windows machine; see the
8
+ Windows section at the bottom).
7
9
 
8
10
  ```sh
9
11
  npm install -g @aron98/load-lock # or: npm install -g ./aron98-load-lock-<ver>.tgz
10
12
  load-lock install --harness opencode
11
13
  load-lock install --harness claude-code
12
14
  load-lock install --harness codex
15
+ load-lock install --harness gemini-cli
16
+ load-lock install --harness copilot-cli
17
+ load-lock install --harness cline
18
+ load-lock install --harness roo-code
19
+ load-lock install --harness goose
20
+ load-lock install --harness cursor --project /path/to/repo
13
21
  load-lock install --harness generic # prints MCP snippets for other harnesses
14
22
  ```
15
23
 
16
24
  What each installer does (all merges are non-destructive; uninstall removes
17
25
  exactly the entries it added):
18
26
 
27
+ Hook-bearing installers (claude-code, codex, cursor) recognize their own
28
+ hook entries by the JSON-quoted CLI path embedded in the hook command, so
29
+ re-running an installer is idempotent and uninstall removes exactly our
30
+ entries, leaving user hooks alone. This is contract-tested via temp-HOME
31
+ round-trips in `src/test/install.test.ts` for all three (an earlier matcher
32
+ never matched its own entries — duplicate installs and hooks that survived
33
+ uninstall — see issue #19); not live-verified inside the real CLIs.
34
+
19
35
  - **opencode** — writes a plugin stub to
20
36
  `~/.config/opencode/plugins/load-lock/` (npm-installed against the global
21
37
  package), registers it in `~/.config/opencode/opencode.json`.
@@ -27,15 +43,52 @@ exactly the entries it added):
27
43
  `Stop`), appends `[mcp_servers.load-lock]` to `~/.codex/config.toml` if
28
44
  absent, adds a marked section to `~/.codex/AGENTS.md`. **Then run `/hooks`
29
45
  inside Codex once to trust the hooks.**
30
- - **generic** — prints MCP stdio config snippets (Gemini CLI, Cursor,
31
- Cline/Roo, Goose) plus a suggested instructions-file section.
46
+ - **gemini-cli** — writes `mcpServers.load-lock` (stdio: `node` + absolute
47
+ path to `dist/mcp/main.js`) into `~/.gemini/settings.json`, adds a marked
48
+ section to `~/.gemini/GEMINI.md`. MCP-fallback tier: Gemini CLI has no
49
+ hooks and no async push, so there is no idle wakeup — offers surface at
50
+ tool boundaries.
51
+ - **copilot-cli** — merges `mcpServers.load-lock` (`type: local`) into
52
+ `~/.copilot/mcp-config.json` (verify with `copilot mcp list`) and adds a
53
+ marked section to `~/.copilot/copilot-instructions.md`. MCP-fallback
54
+ tier: hooks are reported in ecosystem content but not verified in
55
+ official docs, so none are installed — no idle wakeup is claimed; offers
56
+ surface at tool boundaries.
57
+ - **cline** — merges `mcpServers.load-lock` into
58
+ `~/.cline/data/settings/cline_mcp_settings.json` (or the path in
59
+ `CLINE_MCP_SETTINGS_PATH`) and writes a dedicated
60
+ `~/.cline/rules/load-lock.md`. MCP-fallback tier: no hooks, no async
61
+ push — no idle wakeup.
62
+ - **roo-code** — merges `mcpServers.load-lock` into `<project>/.roo/mcp.json`
63
+ (the CLI's cwd unless `--project` is passed) and into every **existing**
64
+ global `mcp_settings.json` (VS Code globalStorage; existing files only —
65
+ never created), plus a dedicated `~/.roo/rules/load-lock.md`. MCP-fallback
66
+ tier: no hooks, no async push — no idle wakeup.
67
+ - **goose** — merges an `extensions.load-lock` stdio block (marker-tagged
68
+ block-mapping YAML; refuses flow-style `extensions:`) into
69
+ `~/.config/goose/config.yaml` (XDG_CONFIG_HOME respected) and adds a
70
+ marked section to `~/.config/goose/.goosehints`. MCP-fallback tier: no
71
+ hooks, no async push — no idle wakeup.
72
+ - **cursor** — project-level (pass `--project`, defaults to cwd): merges
73
+ `mcpServers.load-lock` into `<project>/.cursor/mcp.json`, merges a flat
74
+ hooks-v1 stop entry into `<project>/.cursor/hooks.json` (`version` added
75
+ only if absent; user entries preserved), writes a dedicated Always-apply
76
+ rule `<project>/.cursor/rules/load-lock.mdc`. Hooks tier: the stop hook
77
+ returns `{"followup_message":...}` so Cursor continues the session when an
78
+ offer arrives. Contract-tested against the documented hooks JSON
79
+ (https://cursor.com/docs/hooks); not live-verified.
80
+ - **generic** — prints MCP stdio config snippets (harnesses without a
81
+ dedicated installer) plus a suggested
82
+ instructions-file section.
32
83
 
33
84
  `load-lock uninstall --harness <harness>` reverses any of these.
34
85
 
35
86
  ## The daemon
36
87
 
37
- - State dir: `~/.local/state/load-lock` (override `LOAD_LOCK_STATE_DIR`).
38
- - Socket: `<state dir>/daemon.sock` (override `LOAD_LOCK_SOCKET`).
88
+ - State dir: `~/.local/state/load-lock` (Windows: `%LOCALAPPDATA%\load-lock`;
89
+ override `LOAD_LOCK_STATE_DIR`).
90
+ - Socket: `<state dir>/daemon.sock` (Windows: named pipe `\\.\pipe\load-lock`;
91
+ override `LOAD_LOCK_SOCKET`).
39
92
  - **Auto-spawns** on first client connection; no service unit needed.
40
93
  - Files: `config.json` (settings + history), `journal.json` (held permits),
41
94
  `daemon.json` (pid), `spawn.lock` (bootstrap race guard).
@@ -61,8 +114,9 @@ load-lock get-config
61
114
  ```
62
115
 
63
116
  Defaults: one pool `heavy` capacity 1 · offer window 30s · blocking wait 5s ·
117
+ turn-wait cap (`maxTurnWaitMs`, bounds `wait_for_turn`/`load-lock wait`) 10min ·
64
118
  start deadline 120s · untracked lease 15min. Valid ranges:
65
- offer window 1s–10min, wait 0–10min, start deadline 1s–1h, lease 10s–24h.
119
+ offer window 1s–10min, wait 0–10min, turn-wait cap 1s–1h, start deadline 1s–1h, lease 10s–24h.
66
120
 
67
121
  Semantics of changes:
68
122
 
@@ -76,9 +130,89 @@ limits (containers respected — we never assume 2 cores means 2 jobs),
76
130
  available memory, and current load/memory pressure with an advisory
77
131
  suggestion. Agents are expected to consult it before proposing capacity.
78
132
 
133
+ ## Dashboard: watching the budget live
134
+
135
+ Two read-only viewers surface the machine-wide state in real time. Both use
136
+ the library's own client + protocol (`./shared/client`, `./shared/protocol`)
137
+ over the daemon's watch event stream — neither scrapes CLI output — and
138
+ both resume cleanly across daemon restarts (event-cursor `events since` +
139
+ journal recovery).
140
+
141
+ **The viewers never acquire a permit.** Watching the lock is light work by
142
+ construction: they only ever call `status` / `events` / `inspect` / `watch`.
143
+ The one admin action they expose — force-release — sits behind an explicit
144
+ select + confirm step and never runs automatically.
145
+
146
+ ### `load-lock dashboard` (portable, any terminal)
147
+
148
+ ```sh
149
+ load-lock dashboard # full-screen live dashboard (q quit, r resync)
150
+ load-lock dashboard --once # render one frame and exit (pipes, scripts, CI logs)
151
+ ```
152
+
153
+ `top` still works as a hidden compatibility alias for `dashboard` (same
154
+ command, same flags) — existing scripts keep running; new docs and muscle
155
+ memory should use `dashboard`.
156
+
157
+ Shows pools with capacity vs in-use, holder cards (label, reason, phase,
158
+ pid/pgid, age, start-deadline or lease countdown), offer cards with the
159
+ claim-window countdown, FIFO queue positions, paused requests, the recent
160
+ event feed, and the `inspect_machine` panel. It reconnects with backoff if
161
+ the daemon dies, and a daemon restart is announced and re-synced (durable
162
+ event ids are per daemon lifetime; the cursor resets on restart).
163
+
164
+ Zero extra dependencies: it renders plain text lines over the standard
165
+ alternate screen, so it works anywhere the CLI works.
166
+
167
+ ### OpenCode TUI panel
168
+
169
+ The OpenCode installer also ships the package's `./tui` plugin export (a
170
+ companion to the main plugin, which is unchanged). Inside the opencode TUI:
171
+
172
+ - `/load-lock` (or the command palette, or the `gl` keybinding) opens the
173
+ **load-lock.dashboard** session panel **in-window** as a side panel —
174
+ same data as `load-lock dashboard`, themed, live-updating, with the rest
175
+ of the TUI layout still visible beside it.
176
+ - Every session screen shows an at-a-glance widget at the bottom of the
177
+ right sidebar: `load-lock 0/3` colored by pool health (green ok, yellow
178
+ full/queued, red daemon down) plus a hint line. Clicking it opens the
179
+ dashboard; `gl` is the fallback for terminals that don't deliver mouse
180
+ events. The home screen keeps its `lock 1/1·Q2` footer indicator — the
181
+ sidebar widget only renders on session screens, so the two never
182
+ double-render.
183
+ - `r` resyncs; `x` opens force-release, which requires an explicit
184
+ select + confirm — the panel itself is read-only.
185
+ - `Esc` closes the panel (the footer has always advertised this). It is
186
+ deliberately inactive while a dialog — force-release, the command
187
+ palette — is open, so Esc keeps cancelling the dialog instead of
188
+ closing the panel underneath it.
189
+
190
+ Verified live against opencode 2.0.32 (real pty session): the sidebar
191
+ widget renders with live daemon data, a mouse press on the widget opens
192
+ the dashboard in-window with the chat still visible beside it, `gl` opens
193
+ it as well, and the `/load-lock` command appears in the slash suggestions.
194
+ Earlier live verification (2.0.29): panel render, palette/slash entry
195
+ points, and the confirm-gated force-release flow. Click handling itself
196
+ is contract-level from opentui (mouse reporting enabled by default,
197
+ mouse-down events bubble to slot renderables) — the pty test above
198
+ exercises the full path; the `gl` binding works regardless.
199
+ Plugins configured in `opencode.json` that expose `./tui` are loaded
200
+ automatically by the CLI, so the published package needs no extra config.
201
+ For local development against a checkout, opencode also auto-discovers
202
+ plugins laid out like a package under `<project>/.opencode/plugins/<name>/`
203
+ (`index.ts` re-exporting the main plugin, `tui.ts` re-exporting
204
+ `./dist/adapters/opencode/tui.js`).
205
+
206
+ ### Notes for other harnesses
207
+
208
+ `load-lock dashboard` is the dashboard for every harness (it only needs
209
+ the daemon, which every integration spawns). Harnesses without an in-app
210
+ UI surface get exactly that — no parity claims; see `docs/COMPATIBILITY.md`.
211
+
79
212
  ## Diagnostics & recovery
80
213
 
81
214
  ```sh
215
+ load-lock help # one-screen usage of every command
82
216
  load-lock doctor # state dir, socket, daemon reachability, effective cores, pressure
83
217
  load-lock status # human view: usage, holders w/ phase+pid, offers, queue, paused
84
218
  load-lock pending # your own offers/queue entries (JSON)
@@ -117,10 +251,203 @@ Recovery policy summary:
117
251
  | Config change lost | Configuration is CAS-versioned: fetch `get_config`, retry with the fresh `version` on `version_conflict`. |
118
252
  | After OS reboot | Nothing to do: state dir persists config; daemon respawns on first use; journal reconciliation releases permits whose processes died. |
119
253
 
254
+ ## Containers / WSL2
255
+
256
+ ### Docker — verified
257
+
258
+ Verified 2026-10-16 with Docker Engine 29.1.3 on Linux (amd64) and
259
+ `node:24-bookworm-slim`, running load-lock 0.1.2 installed from the packed
260
+ tarball (`npm pack`, then `npm install -g --omit=dev <tgz>` inside the
261
+ container). Two runs, both in containers started with `--cpus=1.5
262
+ --memory=2g`:
263
+
264
+ - a sanity-check container (`load-lock doctor`, `load-lock inspect`,
265
+ auto-spawn proof, `attach-pid` round trip), and
266
+ - the full `demo/demo.sh` (which needs bash + git + node; the slim image
267
+ needs `apt-get install -y git` first).
268
+
269
+ **Cgroup v2 quota detection.** `load-lock inspect` inside the constrained
270
+ container reported the *container* limits, not the host's:
271
+
272
+ ```json
273
+ "cpu": { "logicalCores": 2, "cgroupVersion": "v2", "cgroupCpuQuota": 150000,
274
+ "cgroupCpuPeriod": 100000, "effectiveCores": 1 },
275
+ "memory": { "totalBytes": 8195100672, "cgroupMemoryLimitBytes": 2147483648,
276
+ "effectiveLimitBytes": 2147483648 }
277
+ ```
278
+
279
+ `/sys/fs/cgroup/cpu.max` reads `150000 100000` inside the container; the 2
280
+ logical cores are still visible (`os.cpus()`/`nproc` are not namespaced),
281
+ but `effectiveCores` is 1 (floor of the 1.5-CPU quota) and
282
+ `effectiveLimitBytes` is exactly the 2 GiB `--memory=2g` limit while
283
+ `totalBytes` shows the ~8 GiB host view. Pressure and the inspect
284
+ suggestion therefore scale to the container, not the host — exactly the
285
+ "don't assume the host's cores" behavior the inspect output promises.
286
+
287
+ **Auto-spawn + default state dir.** With no `LOAD_LOCK_*` environment
288
+ variables set, the first CLI call auto-spawned the daemon on the
289
+ container-default `~/.local/state/load-lock` (state dir contained
290
+ `daemon.json`, `daemon.sock`, `journal.json`; `service status` reported
291
+ `pid 35, protocol v1, config version 1`). No special env or volumes
292
+ needed.
293
+
294
+ **Process tracking.** `/proc`-based tracking works unchanged in the
295
+ container: `attach-pid` recorded the target with its
296
+ `/proc/<pid>/stat` start time (`pidStartTime`), `recover list` reported
297
+ `tracked process (pid 3367) is running`, and after the process was killed
298
+ the daemon's 5s sweep auto-released the permit (`pool "heavy": 0/1 in
299
+ use`). `run_with_permit` and journal reconciliation use the same mechanism.
300
+
301
+ **demo.sh.** The scripted three-agent demo ran to completion (exit 0)
302
+ inside the same plain node:24 container. Its inventory line reflected
303
+ container limits — `cpu: 2 logical / 1 effective (v2)`, `memory: 7.6 GiB
304
+ total / 2.0 GiB effective limit` — and the full flow played out: A
305
+ granted, B and C queued, strict FIFO hand-off, C's offer expired →
306
+ `paused` (not recycled), explicit `rejoin` → claim → run → release; 21
307
+ durable events.
308
+
309
+ Limitations observed (all inherent to containers, not load-lock bugs):
310
+
311
+ - `/proc/loadavg` and CPU inventory are not namespaced: `loadPerEffectiveCore`
312
+ reflects host load, so a quiet container on a busy host can still show
313
+ "high" load pressure, and `nproc` shows the host CPU set. The cgroup quota
314
+ (`effectiveCores`) is the honest number to reason with.
315
+ - State dir is per-container: with `docker run --rm` the daemon, config
316
+ history and journal vanish with the container; the next use just
317
+ respawns with defaults (fine for ephemeral CI containers; volume-mount
318
+ `~/.local/state/load-lock` if you want persistence).
319
+
320
+ Reproduce from a checkout (needs `dist/` built; tarball from `npm pack`):
321
+
322
+ ```sh
323
+ npm ci && npm run build && npm pack --pack-destination /tmp
324
+ docker run --rm --cpus=1.5 --memory=2g \
325
+ -v "$PWD":/src:ro -v /tmp:/pkg:ro \
326
+ node:24 bash -c '
327
+ cp -a /src /work && cd /work
328
+ apt-get update -qq && apt-get install -y -qq git
329
+ npm install -g --omit=dev /pkg/aron98-load-lock-<version>.tgz
330
+ load-lock doctor && load-lock inspect
331
+ bash demo/demo.sh'
332
+ ```
333
+
334
+ ### WSL2 — procedure (NOT verified here)
335
+
336
+ **No WSL2 environment was available on the verification machine** (the
337
+ Docker results above were produced on a plain Linux VM). Nothing in this
338
+ subsection has been executed by us; it is a ready-to-run procedure with
339
+ *expected* outcomes, to be verified and recorded by someone with a WSL2
340
+ setup.
341
+
342
+ 1. Use a WSL2 distro with Node 20+ (e.g. Ubuntu 24.04). Check the cgroup
343
+ layout inside the distro:
344
+
345
+ ```sh
346
+ stat -fc %T /sys/fs/cgroup # expect cgroup2fs (cgroup v2)
347
+ cat /sys/fs/cgroup/cpu.max /sys/fs/cgroup/memory.max
348
+ ```
349
+
350
+ 2. Run load-lock on the distro itself (not in Docker):
351
+
352
+ ```sh
353
+ load-lock doctor && load-lock inspect
354
+ ```
355
+
356
+ Expected: WSL2 presents a full VM, so `inspect` reports the *WSL VM's*
357
+ resources, not the Windows host's. `.wslconfig` caps (`memory=`,
358
+ `processors=`) are enforced by the VM itself (vCPU count / VM RAM), not
359
+ via cgroup quotas — so `cgroupVersion` should read `v2` on a
360
+ systemd-enabled distro, `cgroupCpuQuota` typically `null` (`max`), and
361
+ `effectiveCores` = the `.wslconfig` `processors=` value with
362
+ `memory.totalBytes`/`effectiveLimitBytes` ≈ the `memory=` cap. If the
363
+ distro runs without systemd the cgroup controllers may not be mounted;
364
+ expect `cgroupVersion` to degrade to `none` and the raw VM numbers to be
365
+ reported — either way, no host overstating.
366
+
367
+ 3. With Docker inside WSL2 (docker-ce in the distro, or Docker Desktop's
368
+ WSL2 backend), re-run the constrained-container check from the verified
369
+ Docker procedure above. Expected: containers get their own nested
370
+ cgroup, so `inspect` should report the container quota exactly as in the
371
+ verified results above, regardless of the extra nesting level.
372
+
373
+ 4. Run `demo/demo.sh` in the distro (needs bash, git, node) and in a plain
374
+ `node:24` container under WSL2's Docker.
375
+
376
+ Caveats to watch for (unverified expectations):
377
+
378
+ - Nested cgroups (Docker Desktop's WSL2 backend adds a level) must not mask
379
+ the container's own `cpu.max`/`memory.max` — that's what `inspect` reads.
380
+ - `/proc/loadavg` reflects the WSL VM, not Windows.
381
+ - Keep the state dir on the Linux filesystem (`~/.local/state/load-lock`),
382
+ not on `/mnt/c` (drvfs/9p is slow and lock semantics differ).
383
+
120
384
  ## macOS / Windows notes
121
385
 
122
386
  - macOS: no cgroup limits; process-liveness checks fall back from
123
387
  `/proc/<pid>/stat` start times to `kill(pid, 0)` (weaker PID-reuse
124
388
  defense — document and prefer `run_with_permit`). Everything else works.
125
- - Windows: Unix-domain sockets are not used; a named-pipe transport adapter
126
- would be required (planned, not implemented).
389
+
390
+ ### Windows — procedure (NOT verified here)
391
+
392
+ **No Windows environment was available on the verification machine** (all
393
+ development and testing happened on Linux). The transport adapter is
394
+ implemented in `src/shared/transport.ts` and its logic is covered by
395
+ `src/test/platform.test.ts` (endpoint resolution, pipe naming, probe,
396
+ stale-cleanup, liveness, and job-tree kill paths, exercised via platform
397
+ injection on Linux) — but nothing in this subsection has been executed on
398
+ Windows. It is a ready-to-run procedure with *expected* outcomes, to be
399
+ verified and recorded by someone with a Windows setup.
400
+
401
+ What the adapter does:
402
+
403
+ - Transport: clients and the daemon talk over the named pipe
404
+ `\\.\pipe\load-lock` (Node's `net` module accepts pipe paths in
405
+ `connect`/`createServer` natively). No Unix-domain socket is involved.
406
+ - State dir: `%LOCALAPPDATA%\load-lock` (falls back to
407
+ `%USERPROFILE%\AppData\Local\load-lock`); `LOAD_LOCK_STATE_DIR` and
408
+ `LOAD_LOCK_SOCKET` overrides still win, as on POSIX.
409
+ - Daemon claim: a second daemon probes the pipe before listening and
410
+ exits with "denied" if a live daemon answers (same semantics as the
411
+ POSIX socket claim). Because a named pipe is a kernel object, there is
412
+ no stale-socket file to clean up.
413
+
414
+ 1. Install Node 20+ on Windows and install the package:
415
+
416
+ ```powershell
417
+ npm install -g @aron98/load-lock
418
+ load-lock install --harness <harness>
419
+ load-lock doctor
420
+ ```
421
+
422
+ Expected: `doctor` reports the `%LOCALAPPDATA%\load-lock` state dir and
423
+ reaches the daemon over the pipe (it probes by connecting, since a pipe
424
+ is not a filesystem entry).
425
+
426
+ 2. Run two `load-lock acquire` in separate shells, then `load-lock dashboard --once`.
427
+
428
+ Expected: the same queue/permit behavior as on Linux — the pipe carries
429
+ the identical NDJSON protocol.
430
+
431
+ 3. Start a permit-tracked job (`run_with_permit` / `load-lock run`), kill
432
+ the job, and confirm the permit auto-releases via the sweep.
433
+
434
+ 4. Run `load-lock service stop`, then any tool call.
435
+
436
+ Expected: the daemon exits and the next client auto-spawns a fresh one.
437
+
438
+ Caveats to watch for (unverified expectations):
439
+
440
+ - **No PID-reuse defense.** Windows has no `/proc`-style start time, so
441
+ `isProcessAlive` can only use `kill(pid, 0)` semantics. A recycled PID
442
+ could be mistaken for the original job — the same limitation as macOS,
443
+ documented here rather than solved.
444
+ - **No process-group scan.** The `/proc`-based process-group scan does not
445
+ exist on Windows. Tracked permits release when the main PID dies even if
446
+ child processes survive; job stop/kill uses `taskkill /PID <pid> /T`
447
+ (`/F` for SIGKILL) to take down the tree.
448
+ - **Pipe name is machine-wide.** Unlike POSIX, where each user gets their
449
+ own state dir and socket, a named pipe named `\\.\pipe\load-lock` is
450
+ machine-global — two Windows users on one host share one daemon. Point
451
+ `LOAD_LOCK_SOCKET` at a different pipe name if that is a problem.
452
+ - `inspect_machine` cgroup detection reads `/sys/fs/cgroup` and will
453
+ degrade gracefully (`cgroupVersion: "none"`), reporting raw host numbers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aron98/load-lock",
3
- "version": "0.1.2",
3
+ "version": "1.1.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",
@@ -72,6 +72,10 @@
72
72
  "types": "./dist/core/daemon.d.ts",
73
73
  "default": "./dist/core/daemon.js"
74
74
  },
75
+ "./tui": {
76
+ "types": "./dist/adapters/opencode/tui.d.ts",
77
+ "default": "./dist/adapters/opencode/tui.js"
78
+ },
75
79
  "./dist/*": "./dist/*"
76
80
  },
77
81
  "files": [
@@ -97,8 +101,28 @@
97
101
  "@opencode/plugin": "^2.0.29",
98
102
  "zod": "^3.24.0"
99
103
  },
104
+ "peerDependencies": {
105
+ "@opentui/core": ">=0.5.8",
106
+ "@opentui/solid": ">=0.5.8",
107
+ "solid-js": ">=1.9.0"
108
+ },
109
+ "peerDependenciesMeta": {
110
+ "@opentui/core": {
111
+ "optional": true
112
+ },
113
+ "@opentui/solid": {
114
+ "optional": true
115
+ },
116
+ "solid-js": {
117
+ "optional": true
118
+ }
119
+ },
100
120
  "devDependencies": {
121
+ "@opencode/theme": "2.0.29",
122
+ "@opentui/core": "0.5.17",
123
+ "@opentui/solid": "0.5.17",
101
124
  "@types/node": "^24.0.0",
125
+ "solid-js": "1.9.12",
102
126
  "typescript": "^5.9.0"
103
127
  }
104
128
  }
@@ -23,6 +23,10 @@ Judge by *expected* CPU/memory/IO impact, not by command name. When unsure,
23
23
  `granted` (you hold a permit; start heavy work now) or `queued`
24
24
  (keep doing light work; an offer will arrive).
25
25
  2. While queued: **continue useful light work**. Do not idle-poll.
26
+ Out of light work? `wait_for_turn(request_id?, timeout_ms?)` — a
27
+ bounded blocking sleep until your turn that auto-claims the offer and
28
+ wakes you with the permit_token (holds no capacity; cancellation
29
+ leaves nothing behind).
26
30
  3. When an offer arrives (harness wakeup or `pending`): `claim(offer_token)`
27
31
  inside the claim window (default 30s). Expired offers are rejected; your
28
32
  request pauses until you `rejoin`.