@aron98/load-lock 0.0.0-stage → 0.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -2
  3. package/demo/demo.sh +112 -0
  4. package/demo/sim-agent.mjs +149 -0
  5. package/dist/adapters/opencode/plugin.d.ts +18 -0
  6. package/dist/adapters/opencode/plugin.js +301 -0
  7. package/dist/cli/hook.d.ts +1 -0
  8. package/dist/cli/hook.js +141 -0
  9. package/dist/cli/install.d.ts +14 -0
  10. package/dist/cli/install.js +377 -0
  11. package/dist/cli/main.d.ts +2 -0
  12. package/dist/cli/main.js +461 -0
  13. package/dist/core/daemon-main.d.ts +2 -0
  14. package/dist/core/daemon-main.js +27 -0
  15. package/dist/core/daemon.d.ts +79 -0
  16. package/dist/core/daemon.js +526 -0
  17. package/dist/core/persist.d.ts +11 -0
  18. package/dist/core/persist.js +66 -0
  19. package/dist/core/state.d.ts +203 -0
  20. package/dist/core/state.js +909 -0
  21. package/dist/core/sysinfo.d.ts +2 -0
  22. package/dist/core/sysinfo.js +123 -0
  23. package/dist/mcp/main.d.ts +2 -0
  24. package/dist/mcp/main.js +4 -0
  25. package/dist/mcp/server.d.ts +27 -0
  26. package/dist/mcp/server.js +352 -0
  27. package/dist/policy/text.d.ts +30 -0
  28. package/dist/policy/text.js +58 -0
  29. package/dist/shared/client.d.ts +30 -0
  30. package/dist/shared/client.js +227 -0
  31. package/dist/shared/protocol.d.ts +333 -0
  32. package/dist/shared/protocol.js +22 -0
  33. package/dist/test/core.test.d.ts +1 -0
  34. package/dist/test/core.test.js +281 -0
  35. package/dist/test/daemon.test.d.ts +1 -0
  36. package/dist/test/daemon.test.js +384 -0
  37. package/dist/test/hook.test.d.ts +1 -0
  38. package/dist/test/hook.test.js +126 -0
  39. package/dist/test/mcp.test.d.ts +1 -0
  40. package/dist/test/mcp.test.js +139 -0
  41. package/dist/test/run-helper.test.d.ts +1 -0
  42. package/dist/test/run-helper.test.js +84 -0
  43. package/dist/test/testutil.d.ts +27 -0
  44. package/dist/test/testutil.js +111 -0
  45. package/docs/ADAPTERS.md +118 -0
  46. package/docs/AGENT_GUIDE.md +142 -0
  47. package/docs/ARCHITECTURE.md +195 -0
  48. package/docs/COMPATIBILITY.md +116 -0
  49. package/docs/OPERATIONS.md +126 -0
  50. package/package.json +102 -4
  51. package/skills/load-lock/SKILL.md +73 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aron98
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # load-lock
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/aron98/load-lock/actions/workflows/ci.yml/badge.svg)](https://github.com/aron98/load-lock/actions/workflows/ci.yml)
4
+
5
+ **A machine-wide heavy-work budget for parallel coding agents.**
6
+
7
+ When several agents work side by side on one machine — separate sessions,
8
+ repos, git worktrees, or harnesses (OpenCode, Codex CLI, Claude Code, ...) —
9
+ their builds and test suites stomp on each other, thrash the caches, and
10
+ starve each other's cores. load-lock gives them one shared counting
11
+ semaphore: acquire a permit before heavy work, keep doing light work while
12
+ queued, receive a FIFO offer when capacity frees, claim it, run, release.
13
+ It is cooperative and advisory — no command wrapping, no automatic shell
14
+ classification, no approval interruptions.
15
+
16
+ ```
17
+ agent A (OpenCode) agent B (Codex) agent C (Claude Code)
18
+ acquire "cargo test" acquire "npm test" acquire "build docs"
19
+ granted ✓ runs queued → keeps coding queued → keeps coding
20
+ ... offer → claim ✓ runs (misses window → paused)
21
+ release ✓ release ✓ rejoin → offer → claim ✓
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - **One budget for the whole machine** — every client of any harness shares
27
+ one capacity pool regardless of repo/worktree/cwd. Default: pool `heavy`,
28
+ capacity 1; fully runtime-configurable by any agent via versioned CAS
29
+ updates (persisted with author + rationale).
30
+ - **Atomic acquire-or-queue** — single daemon event loop; a release can
31
+ never slip between a failed check and queue registration.
32
+ - **FIFO offers with opaque claim tokens** — outstanding offers reserve
33
+ capacity; expired offers pass capacity to the next waiter and pause the
34
+ absent request until it explicitly rejoins (no endless recycling).
35
+ - **Real process-lifecycle cleanup** — permits attach to actual processes
36
+ (pid + process group + `/proc` start time, PID-reuse-safe). Background
37
+ jobs outliving the shell keep their permit until the whole group exits.
38
+ Never auto-expired merely for being old. Untracked work uses an honest,
39
+ documented heartbeat-lease tradeoff. Explicit release is always
40
+ available and idempotent.
41
+ - **Crash-safe** — agent crashes release tracked permits on confirmed
42
+ process death; daemon restarts reconcile a journal against real liveness
43
+ and auto-respawn transparently.
44
+ - **Honest notifications** — tool-boundary offer delivery everywhere; real
45
+ idle wakeup where the harness supports it (live-verified on OpenCode;
46
+ hook-based on Claude Code and Codex CLI); durable event ids for
47
+ reconnects. A notification never authorizes work — only `claim` does.
48
+ - **Policy that actually reaches the model** — installed per harness via
49
+ its real mechanism (plugin context hook, SessionStart hooks incl.
50
+ compact matchers, global instruction files), plus a detailed skill/guide.
51
+ - **Resource-aware** — `inspect_machine` reports cgroup/container CPU and
52
+ memory limits and current pressure; never assumes cores = capacity.
53
+
54
+ ## Quickstart
55
+
56
+ ```sh
57
+ npm install -g @aron98/load-lock
58
+ load-lock install --harness opencode # or claude-code | codex | generic
59
+ load-lock doctor
60
+ ```
61
+
62
+ From then on, agents see the policy and tools automatically:
63
+ OpenCode plugin (`load_lock_*` tools), Claude Code / Codex MCP tools
64
+ (`mcp__load-lock__*`) plus session hooks. Any other MCP-capable harness can
65
+ use the generic stdio server; plain shells use the CLI:
66
+
67
+ ```sh
68
+ load-lock run --reason "nightly build" -- npm run build # acquire→run→release
69
+ load-lock status # machine-wide view
70
+ load-lock configure --capacity 2 --reason "16-core host" --author me
71
+ ```
72
+
73
+ ## Layout
74
+
75
+ ```
76
+ src/core/ daemon + state machine + persistence + machine inspection
77
+ src/mcp/ MCP stdio server (universal adapter)
78
+ src/adapters/ OpenCode native plugin
79
+ src/cli/ CLI incl. installer, hook handlers, run/wait helpers
80
+ src/test/ 32 tests: state machine, real multi-process daemon tests,
81
+ MCP integration, hook contracts, run helper
82
+ demo/ reproducible 3-worktree demo (three agents, one pool)
83
+ docs/ ARCHITECTURE / COMPATIBILITY / OPERATIONS / AGENT_GUIDE
84
+ skills/ packaged skill
85
+ ```
86
+
87
+ ## Documentation
88
+
89
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — design, why a dedicated
90
+ service (vs WorkOS Airlock / mic-lock / flock wrappers), atomicity,
91
+ restart semantics, notification honesty, lifecycle tradeoffs.
92
+ - [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md) — per-harness capability
93
+ matrix with implemented / contract-tested / proposed labels.
94
+ - [docs/OPERATIONS.md](docs/OPERATIONS.md) — install, config,
95
+ troubleshooting, recovery, daemon lifecycle.
96
+ - [docs/AGENT_GUIDE.md](docs/AGENT_GUIDE.md) — the playbook for agents.
97
+ - [docs/ADAPTERS.md](docs/ADAPTERS.md) — how to write an adapter for a new
98
+ harness, using the published library surface.
99
+
100
+ ## Status
101
+
102
+ - Core daemon/protocol/CLI: implemented, 32 automated tests passing
103
+ (multi-process daemons, real processes, restarts, races, expiry,
104
+ PID-reuse, subprocess survival).
105
+ - OpenCode adapter: implemented and **live-verified** (tools, policy
106
+ injection, compaction-proof context hook, execution-boundary wakeup
107
+ with tokens, skill).
108
+ - Claude Code + Codex CLI adapters: implemented, contract-tested against
109
+ documented hook JSON; not yet live-verified (harnesses not installed on
110
+ the verification machine).
111
+ - Gemini CLI, Copilot CLI, Cursor, Cline, Roo Code, Goose, Aider:
112
+ researched; MCP fallback tier (Aider: manual CLI tier). See
113
+ COMPATIBILITY.md for the matrix.
114
+
115
+ Linux-first; macOS works with documented liveness-check limitations;
116
+ Windows would need a named-pipe transport adapter (planned, not built).
package/demo/demo.sh ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ # load-lock demo: three agents in three git worktrees compete for one
3
+ # machine-wide heavy-work budget (capacity 1) while a shared daemon arbitrates.
4
+ #
5
+ # Fully scripted, reproducible, no network. Run: bash demo/demo.sh
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ CLI=(node "$ROOT/dist/cli/main.js")
10
+ DEMO="$(mktemp -d /tmp/opencode/ll-demo-XXXXXX)"
11
+
12
+ cleanup() {
13
+ "${CLI[@]}" service stop >/dev/null 2>&1 || true
14
+ rm -rf "$DEMO"
15
+ }
16
+ trap cleanup EXIT
17
+
18
+ say() { printf '\n\033[1m==== %s ====\033[0m\n' "$*"; }
19
+ line() { printf -- '---- %s\n' "$*"; }
20
+
21
+ # Poll `load-lock status` (human output) until it matches $2, else fail.
22
+ wait_for() {
23
+ local desc="$1" pattern="$2" i
24
+ for i in $(seq 1 150); do
25
+ if "${CLI[@]}" status 2>/dev/null | grep -qE "$pattern"; then return 0; fi
26
+ sleep 0.1
27
+ done
28
+ echo "TIMED OUT waiting for: $desc" >&2
29
+ "${CLI[@]}" status || true
30
+ exit 1
31
+ }
32
+
33
+ say "build"
34
+ (cd "$ROOT" && npm run build --silent)
35
+
36
+ say "workspace: git repo + 3 worktrees in $DEMO"
37
+ git init -q "$DEMO/repo"
38
+ git -C "$DEMO/repo" config user.email demo@load-lock
39
+ git -C "$DEMO/repo" config user.name demo
40
+ echo 'fn main() { println!("heavy"); }' > "$DEMO/repo/main.rs"
41
+ git -C "$DEMO/repo" add -A
42
+ git -C "$DEMO/repo" commit -qm "init"
43
+ for w in a b c; do
44
+ git -C "$DEMO/repo" worktree add -q "$DEMO/wt-$w" -b "wt-$w"
45
+ done
46
+ line "3 worktrees, one repo — 3 'agents' will share ONE budget regardless of checkout"
47
+
48
+ # Isolated daemon for the demo (does not touch any real state).
49
+ export LOAD_LOCK_STATE_DIR="$DEMO/state"
50
+ export LOAD_LOCK_SOCKET="$DEMO/state/daemon.sock"
51
+
52
+ say "start the shared daemon"
53
+ "${CLI[@]}" service start
54
+
55
+ say "machine inventory (what agents are told to reason with)"
56
+ "${CLI[@]}" inspect --json | node -e '
57
+ let s=""; process.stdin.on("data", d => (s += d)).on("end", () => {
58
+ const j = JSON.parse(s);
59
+ console.log(`cpu: ${j.cpu.logicalCores} logical / ${j.cpu.effectiveCores} effective (${j.cpu.cgroupVersion})`);
60
+ console.log(`memory: ${(j.memory.totalBytes/2**30).toFixed(1)} GiB total / ${(j.memory.effectiveLimitBytes/2**30).toFixed(1)} GiB effective limit`);
61
+ console.log(`pressure: ${j.pressure.note}`);
62
+ });'
63
+
64
+ say "configure: capacity 1, short claim window (2s) for a snappy demo"
65
+ "${CLI[@]}" configure --capacity 1 --offer-timeout-ms 2000 \
66
+ --reason "demo: single heavy slot; short offer window to show expiry" --author "demo-operator" >/dev/null
67
+ line "any agent may propose changes like this via configure (versioned CAS); the demo pins it"
68
+
69
+ say "launch agent A (gets the only slot) — then B and C, which queue"
70
+ (cd "$DEMO/wt-a" && exec node "$ROOT/demo/sim-agent.mjs" --role A --agent sim-a --label "A build (wt-a)") &
71
+ PID_A=$!
72
+ wait_for "A holds the only slot" '1/1 in use'
73
+ (cd "$DEMO/wt-b" && exec node "$ROOT/demo/sim-agent.mjs" --role B --agent sim-b --label "B test (wt-b)") &
74
+ PID_B=$!
75
+ wait_for "B is queued" '1 queued'
76
+ (cd "$DEMO/wt-c" && exec node "$ROOT/demo/sim-agent.mjs" --role C --agent sim-c --label "C build (wt-c)") &
77
+ PID_C=$!
78
+ wait_for "C is queued too" '2 queued'
79
+
80
+ say "snapshot: one heavy job running, two agents queued (all in different worktrees)"
81
+ "${CLI[@]}" status
82
+
83
+ say "agents run on their own — watching the hand-off from A to B (strict FIFO)"
84
+ wait_for "A released; B now holds the permit" 'held: B test'
85
+ line "A finished its build; the daemon reserved the slot for the FIFO head (B) and made an offer"
86
+ wait_for "B finished; C gets an offer — but C is busy and will miss it" 'held: C build|1 paused'
87
+ line "C deliberately let its offer expire: the request is PAUSED, not recycled"
88
+ wait_for "capacity back to idle (C paused, nobody holds)" '0/1 in use'
89
+ "${CLI[@]}" status
90
+
91
+ say "C rejoins when actually ready, claims, runs"
92
+ wait_for "all three agents finished (capacity idle)" ': 0/1 in use'
93
+ wait "$PID_A" "$PID_B" "$PID_C"
94
+
95
+ say "final state"
96
+ "${CLI[@]}" status
97
+ "${CLI[@]}" get-config | node -e '
98
+ let s=""; process.stdin.on("data", d => (s += d)).on("end", () => {
99
+ const j = JSON.parse(s);
100
+ console.log(`config version ${j.config.version}; capacity(heavy)=${j.config.pools.heavy.capacity}; offer window ${j.config.offerTimeoutMs}ms`);
101
+ console.log("change history:");
102
+ for (const h of j.history) console.log(` v${h.version} by ${h.author}: ${h.reason}`);
103
+ });'
104
+
105
+ say "event log (durable identities — every grant, offer, claim, release)"
106
+ "${CLI[@]}" events --json | node -e '
107
+ let s=""; process.stdin.on("data", d => (s += d)).on("end", () => {
108
+ const j = JSON.parse(s);
109
+ for (const ev of j.events) console.log(` #${ev.id} ${ev.type.padEnd(13)} ${ev.entry?.agentLabel ?? ev.targetAgent ?? ""}`);
110
+ });'
111
+
112
+ printf '\n\033[1mDemo complete:\033[0m capacity 1 was never exceeded; offers went out strictly in FIFO order;\nC missed its claim window, was paused (not endlessly recycled), and rejoined explicitly.\n'
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Simulated coding agent for the load-lock demo.
4
+ *
5
+ * Each instance is a separate OS process playing one agent from one harness,
6
+ * working inside one git worktree. It talks to the shared load-lock daemon
7
+ * exactly the way a scripted agent (e.g. aider, a shell-driven flow) would:
8
+ * via the `load-lock` CLI. Real agents would use MCP tools or the native
9
+ * OpenCode plugin instead — same daemon, same protocol, same FIFO.
10
+ *
11
+ * Usage (normally launched by demo.sh, not directly):
12
+ * cd <worktree> && node sim-agent.mjs --role A --agent sim-a --label "A (wt-a)"
13
+ *
14
+ * Roles:
15
+ * A — gets the permit immediately, runs heavy work, releases.
16
+ * B — queues, does light work meanwhile, claims its FIFO offer, runs heavy work.
17
+ * C — queues, deliberately MISSES its claim window (offer expires → paused),
18
+ * then explicitly rejoins once actually ready, claims, runs heavy work.
19
+ */
20
+ import { spawnSync } from "node:child_process";
21
+ import { join } from "node:path";
22
+ import { parseArgs } from "node:util";
23
+
24
+ const { values } = parseArgs({
25
+ options: {
26
+ role: { type: "string" },
27
+ agent: { type: "string" },
28
+ label: { type: "string" },
29
+ },
30
+ });
31
+ const role = values.role;
32
+ const agent = values.agent;
33
+ const label = values.label ?? agent;
34
+ if (!role || !agent) {
35
+ console.error("sim-agent: --role and --agent are required");
36
+ process.exit(64);
37
+ }
38
+
39
+ const CLI = join(import.meta.dirname, "..", "dist", "cli", "main.js");
40
+
41
+ /** Run the load-lock CLI; returns {code, stdout, stderr, json}. */
42
+ function ll(args) {
43
+ const r = spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8", timeout: 120_000 });
44
+ let json = null;
45
+ try {
46
+ json = JSON.parse(r.stdout);
47
+ } catch {
48
+ /* human output */
49
+ }
50
+ return { code: r.status, stdout: r.stdout, stderr: r.stderr, json };
51
+ }
52
+
53
+ const log = (msg) => console.log(`[${label}] ${msg}`);
54
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
55
+ const fail = (msg) => {
56
+ console.error(`[${label}] FATAL: ${msg}`);
57
+ process.exit(70);
58
+ };
59
+
60
+ const ACQ = ["acquire", "--agent", agent, "--label", label];
61
+
62
+ /** Poll own pending offers until one appears (or bail after ~30s). */
63
+ async function waitForOffer() {
64
+ for (let i = 0; i < 100; i++) {
65
+ const p = ll(["pending", "--agent", agent]);
66
+ const offer = p.json?.offers?.[0];
67
+ if (offer) return offer;
68
+ await sleep(300);
69
+ }
70
+ fail("no offer arrived within 30s");
71
+ }
72
+
73
+ // ---------------------------------------------------------------------- role A
74
+ async function roleA() {
75
+ log("I need to run a heavy job: cargo build (simulated with `sleep 2`)");
76
+ const acq = ll([...ACQ, "--op-key", "build-wt-a", "--reason", "cargo build (simulated)"]);
77
+ if (acq.code !== 0 || acq.json?.state !== "granted") {
78
+ fail(`expected an immediate grant, got: ${acq.stdout.trim() || acq.stderr.trim()}`);
79
+ }
80
+ const permit = acq.json.entry.permitToken;
81
+ log(`capacity was free: permit granted immediately (${permit.slice(0, 12)}…). Starting heavy work.`);
82
+ const run = ll(["run", "--permit", permit, "--", "sleep", "2"]);
83
+ log(`heavy work finished (exit ${run.code}); the run helper released the permit with the job's outcome.`);
84
+ }
85
+
86
+ // ---------------------------------------------------------------------- role B
87
+ async function roleB() {
88
+ log("I need to run a heavy job: cargo test (simulated with `sleep 1.5`)");
89
+ const acq = ll([...ACQ, "--op-key", "build-wt-b", "--reason", "cargo test (simulated)"]);
90
+ if (acq.json?.state !== "queued") fail(`expected to queue, got: ${acq.stdout.trim()}`);
91
+ log(
92
+ `machine is busy: registered in the FIFO queue at position ${acq.json.entry.position} ` +
93
+ `(request ${acq.json.entry.requestId.slice(0, 12)}…). No permit yet.`,
94
+ );
95
+ log("continuing useful LIGHT work meanwhile — no permit needed for these:");
96
+ for (let i = 1; i <= 3; i++) {
97
+ await sleep(400);
98
+ log(` light work ${i}/3: grep TODOs in my worktree`);
99
+ }
100
+ log("blocking on my queued request (bounded wait; a real agent would just check pending at the next tool call)…");
101
+ const w = ll(["wait", "--id", acq.json.entry.requestId, "--agent", agent, "--timeout", "30000"]);
102
+ if (w.code !== 0) fail(`wait failed: ${w.stderr.trim() || w.stdout.trim()}`);
103
+ const permit = w.json.permit_token;
104
+ log(`FIFO offer arrived and was claimed (permit ${permit.slice(0, 12)}…). Starting heavy work.`);
105
+ const run = ll(["run", "--permit", permit, "--", "sleep", "1.5"]);
106
+ log(`heavy work finished (exit ${run.code}); permit released.`);
107
+ }
108
+
109
+ // ---------------------------------------------------------------------- role C
110
+ async function roleC() {
111
+ log("I need to run a heavy job too: cargo build (simulated with `sleep 1`)");
112
+ const acq = ll([...ACQ, "--op-key", "build-wt-c", "--reason", "cargo build (simulated)"]);
113
+ if (acq.json?.state !== "queued") fail(`expected to queue, got: ${acq.stdout.trim()}`);
114
+ log(`machine is busy: queued at position ${acq.json.entry.position}. Doing light work while I wait.`);
115
+ const offer = await waitForOffer();
116
+ log(
117
+ `offer arrived (claim window ~2s, expires ${new Date(offer.offerExpiresAt).toISOString()}) ` +
118
+ "— but I'm mid-edit on a file; I will deliberately NOT claim it yet.",
119
+ );
120
+ await sleep(3200); // longer than the 2s claim window
121
+ const p = ll(["pending", "--agent", agent]);
122
+ const paused = p.json?.paused?.[0];
123
+ if (!paused) fail("expected my request to be PAUSED after missing the claim window");
124
+ log(
125
+ "claim window elapsed → my request is PAUSED (the machine did not wait for me; " +
126
+ "capacity moved on, and I am not endlessly recycled).",
127
+ );
128
+ log("I'm ready now — explicitly rejoining the queue.");
129
+ ll(["rejoin", "--id", acq.json.entry.requestId]);
130
+ const offer2 = await waitForOffer();
131
+ log(`re-offered (request retained its FIFO identity). Claiming now, before the window lapses.`);
132
+ const claim = ll(["claim", "--offer", offer2.offerToken]);
133
+ if (claim.code !== 0) fail(`claim failed: ${claim.stderr.trim() || claim.stdout.trim()}`);
134
+ const permit = claim.json.entry.permitToken;
135
+ log(`claimed (${permit.slice(0, 12)}…). Starting heavy work.`);
136
+ const run = ll(["run", "--permit", permit, "--", "sleep", "1"]);
137
+ log(`heavy work finished (exit ${run.code}); permit released.`);
138
+ }
139
+
140
+ // -------------------------------------------------------------------------------
141
+ try {
142
+ if (role === "A") await roleA();
143
+ else if (role === "B") await roleB();
144
+ else if (role === "C") await roleC();
145
+ else fail(`unknown role "${role}"`);
146
+ log("done. ✅");
147
+ } catch (e) {
148
+ fail(e?.stack ?? String(e));
149
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * load-lock native plugin for OpenCode V2.
3
+ *
4
+ * Installed to ~/.config/opencode/plugins/load-lock/ (see cli/install.ts).
5
+ * Provides:
6
+ * - native tools in the `load_lock` namespace (acquire/claim/release/...)
7
+ * - run_with_permit / job_status / job_stop: full execution lifecycle
8
+ * - automatic policy injection on every model call via the session
9
+ * "context" hook (compaction-proof by construction)
10
+ * - idle wakeup: daemon watch -> ctx.session.synthetic when an offer
11
+ * targets an OpenCode session
12
+ * - automatic per-session heartbeats (untracked leases never lapse while
13
+ * the OpenCode service lives; the model never has to remember)
14
+ * - a registered skill with the detailed agent guide
15
+ */
16
+ import { Plugin } from "@opencode/plugin";
17
+ declare const _default: Plugin.Plugin;
18
+ export default _default;