agent-lock 0.2.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.
- checksums.yaml +7 -0
- data/.claude/CLAUDE.md +1 -0
- data/.envrc +2 -0
- data/.rubocop_todo.yml +90 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +54 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +374 -0
- data/Rakefile +12 -0
- data/exe/agent-lock +8 -0
- data/exe/alock +9 -0
- data/justfile +127 -0
- data/lib/agent/lock/cli/commands/acquire.rb +87 -0
- data/lib/agent/lock/cli/commands/base.rb +148 -0
- data/lib/agent/lock/cli/commands/break.rb +36 -0
- data/lib/agent/lock/cli/commands/check.rb +56 -0
- data/lib/agent/lock/cli/commands/completion.rb +50 -0
- data/lib/agent/lock/cli/commands/list.rb +70 -0
- data/lib/agent/lock/cli/commands/mine.rb +35 -0
- data/lib/agent/lock/cli/commands/note.rb +37 -0
- data/lib/agent/lock/cli/commands/release.rb +33 -0
- data/lib/agent/lock/cli/commands/release_all.rb +24 -0
- data/lib/agent/lock/cli/commands/resume.rb +36 -0
- data/lib/agent/lock/cli/commands/skill.rb +75 -0
- data/lib/agent/lock/cli/commands/version.rb +20 -0
- data/lib/agent/lock/cli/commands/whoami.rb +83 -0
- data/lib/agent/lock/cli.rb +80 -0
- data/lib/agent/lock/error.rb +10 -0
- data/lib/agent/lock/freeze.rb +109 -0
- data/lib/agent/lock/identity.rb +166 -0
- data/lib/agent/lock/launcher.rb +126 -0
- data/lib/agent/lock/manager.rb +313 -0
- data/lib/agent/lock/process_info.rb +60 -0
- data/lib/agent/lock/record.rb +216 -0
- data/lib/agent/lock/scope.rb +173 -0
- data/lib/agent/lock/skill.rb +104 -0
- data/lib/agent/lock/store/file_system_store.rb +161 -0
- data/lib/agent/lock/store/redis_store.rb +225 -0
- data/lib/agent/lock/store.rb +104 -0
- data/lib/agent/lock/tree.rb +126 -0
- data/lib/agent/lock/version.rb +7 -0
- data/lib/agent/lock.rb +30 -0
- data/sig/agent/lock.rbs +6 -0
- data/skills/agent-lock/SKILL.md +59 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 13449d302065507190249f6bcf9cea27e2e4ce283a1aa422213122951826d78e
|
|
4
|
+
data.tar.gz: db759d6540ea353eda999c77eb02d2b6d8d5887f86e8fad52036ef93774124b6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2f56d58d5250a8dda75cae4f6a39b741f2186864bf91296954b6996e36b9cf9984413ddb19c92d96570f2286bfc0b88feea58d1ade5e7fcbb2a8e595e74450b5
|
|
7
|
+
data.tar.gz: 9421c3c1397eedbd49fbe3ac735aec9f649ef7938bf4f0d16e2bb70368278e5ff60caf5dc291356e43adef931f23f34c3632f120ed8731a78c182be2b9e2ce7e
|
data/.claude/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../AGENTS.md
|
data/.envrc
ADDED
data/.rubocop_todo.yml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# This configuration was generated by
|
|
2
|
+
# `rubocop --auto-gen-config --no-auto-gen-timestamp`
|
|
3
|
+
# using RuboCop version 1.91.0.
|
|
4
|
+
# The point is for the user to remove these configuration records
|
|
5
|
+
# one by one as the offenses are removed from the code base.
|
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
+
|
|
9
|
+
# Offense count: 1
|
|
10
|
+
Gemspec/RequiredRubyVersion:
|
|
11
|
+
Exclude:
|
|
12
|
+
- 'agent-lock.gemspec'
|
|
13
|
+
|
|
14
|
+
# Offense count: 3
|
|
15
|
+
# Configuration parameters: Prefixes, AllowedPatterns.
|
|
16
|
+
# Prefixes: when, with, without
|
|
17
|
+
RSpec/ContextWording:
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'spec/agent/lock/tree_spec.rb'
|
|
20
|
+
- 'spec/support/aruba.rb'
|
|
21
|
+
- 'spec/support/checkout.rb'
|
|
22
|
+
|
|
23
|
+
# Offense count: 3
|
|
24
|
+
# Configuration parameters: IgnoredMetadata.
|
|
25
|
+
RSpec/DescribeClass:
|
|
26
|
+
Exclude:
|
|
27
|
+
- '**/spec/features/**/*'
|
|
28
|
+
- '**/spec/requests/**/*'
|
|
29
|
+
- '**/spec/routing/**/*'
|
|
30
|
+
- '**/spec/system/**/*'
|
|
31
|
+
- '**/spec/views/**/*'
|
|
32
|
+
- 'spec/agent/lock/concurrency_spec.rb'
|
|
33
|
+
- 'spec/agent/lock/loading_spec.rb'
|
|
34
|
+
- 'spec/agent/lock/manager_spec.rb'
|
|
35
|
+
|
|
36
|
+
# Offense count: 1
|
|
37
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
38
|
+
# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
|
|
39
|
+
# SupportedStyles: described_class, explicit
|
|
40
|
+
RSpec/DescribedClass:
|
|
41
|
+
Exclude:
|
|
42
|
+
- 'spec/agent/lock/manager_spec.rb'
|
|
43
|
+
|
|
44
|
+
# Offense count: 46
|
|
45
|
+
# Configuration parameters: CountAsOne.
|
|
46
|
+
RSpec/ExampleLength:
|
|
47
|
+
Max: 20
|
|
48
|
+
|
|
49
|
+
# Offense count: 2
|
|
50
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
51
|
+
RSpec/IncludeExamples:
|
|
52
|
+
Exclude:
|
|
53
|
+
- 'spec/agent/lock/concurrency_spec.rb'
|
|
54
|
+
|
|
55
|
+
# Offense count: 5
|
|
56
|
+
# Configuration parameters: AssignmentOnly.
|
|
57
|
+
RSpec/InstanceVariable:
|
|
58
|
+
Exclude:
|
|
59
|
+
- 'spec/agent/lock/tree_spec.rb'
|
|
60
|
+
- 'spec/support/checkout.rb'
|
|
61
|
+
|
|
62
|
+
# Offense count: 2
|
|
63
|
+
RSpec/LeakyLocalVariable:
|
|
64
|
+
Exclude:
|
|
65
|
+
- 'spec/agent/lock/loading_spec.rb'
|
|
66
|
+
|
|
67
|
+
# Offense count: 2
|
|
68
|
+
# Configuration parameters: EnforcedStyle.
|
|
69
|
+
# SupportedStyles: have_received, receive
|
|
70
|
+
RSpec/MessageSpies:
|
|
71
|
+
Exclude:
|
|
72
|
+
- 'spec/agent/lock/freeze_spec.rb'
|
|
73
|
+
- 'spec/agent/lock/store_spec.rb'
|
|
74
|
+
|
|
75
|
+
# Offense count: 1
|
|
76
|
+
RSpec/MultipleDescribes:
|
|
77
|
+
Exclude:
|
|
78
|
+
- 'spec/agent/lock/manager_spec.rb'
|
|
79
|
+
|
|
80
|
+
# Offense count: 6
|
|
81
|
+
RSpec/MultipleExpectations:
|
|
82
|
+
Max: 2
|
|
83
|
+
|
|
84
|
+
# Offense count: 1
|
|
85
|
+
# Configuration parameters: AllowedClasses.
|
|
86
|
+
Style/OneClassPerFile:
|
|
87
|
+
Exclude:
|
|
88
|
+
- 'spec/**/*'
|
|
89
|
+
- 'test/**/*'
|
|
90
|
+
- 'lib/agent/lock/cli.rb'
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for AI coding agents (Claude Code, Codex, Cursor, etc.) working in this repository. See `README.md` for what the gem does and how it works; this file covers only what an agent needs to know that isn't obvious from reading the code.
|
|
4
|
+
|
|
5
|
+
## Checks before calling anything done
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# check if redis is running
|
|
9
|
+
echo 'INFO' | redis-cli | grep redis_version
|
|
10
|
+
redis_version:8.8.0 # success
|
|
11
|
+
|
|
12
|
+
# when redis is not running, it return this:
|
|
13
|
+
Could not connect to Redis at 127.0.0.1:6379: Connection refused
|
|
14
|
+
|
|
15
|
+
# default backend (redis) if it's available
|
|
16
|
+
AGENT_LOCK_TEST_BACKEND=redis
|
|
17
|
+
bundle exec rspec
|
|
18
|
+
|
|
19
|
+
# same suite, against (file-system) locking
|
|
20
|
+
AGENT_LOCK_TEST_BACKEND=file bundle exec rspec
|
|
21
|
+
|
|
22
|
+
# lint
|
|
23
|
+
bundle exec rubocop
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Both spec runs and rubocop must be clean. CI (`.github/workflows/main.yml`) runs the suite once per backend plus rubocop as a third, independent job — a change that only passes one backend, or that is only checked against one, is not verified.
|
|
27
|
+
|
|
28
|
+
A `justfile` exists for the rest (`just test`, `just ci`, `just format`, `just publish`, `just release`). **`just lint` currently names `standardrb`, which is not a dependency of this gem** — that recipe does not work; use `bundle exec rubocop` instead until it's reconciled.
|
|
29
|
+
|
|
30
|
+
## The dual-backend test suite
|
|
31
|
+
|
|
32
|
+
`spec/spec_helper.rb` pins every example to one backend via `AGENT_LOCK_BACKEND`, read from `AGENT_LOCK_TEST_BACKEND` (default `file`). This exists because the library's default backend picks Redis when one answers locally: without the pin, running the suite on a machine with Redis running writes real lock records into that Redis, under whatever tree digest a throwaway checkout hashes to, with nothing to ever clean them up. It has happened.
|
|
33
|
+
|
|
34
|
+
If you add a spec that is inherently about one backend's own on-disk or on-Redis shape (not just the `Store` contract both satisfy), pin it to that backend explicitly regardless of `AGENT_LOCK_TEST_BACKEND` — see `manager_spec.rb`'s "the store the locks live in" block for the pattern.
|
|
35
|
+
|
|
36
|
+
Redis-backed tests use a fixed test database (`RedisHelpers::TEST_REDIS_URL`, `spec/support/redis.rb`), never whatever `REDIS_URL` happens to be set to in the shell — a `flush_test_redis!` runs after every example. Do not make that depend on an ambient env var again; an unrelated project's `.envrc` exporting `REDIS_URL` is exactly the failure mode that guards against, and it has bitten this exact repo's own tooling (see the git log for "Never let ambient REDIS_URL decide what the suite flushes").
|
|
37
|
+
|
|
38
|
+
## Naming: `FileSystemStore` / `RedisStore`, not `FileSystem` / `Redis`
|
|
39
|
+
|
|
40
|
+
The two backend classes are named with a `Store` suffix (`Agent::Lock::Store::FileSystemStore`, `Agent::Lock::Store::RedisStore`), deliberately, so that `RedisStore.new` inside `lib/agent/lock/store/redis_store.rb` is never confused with `Redis.new` (the `redis` gem's own client class) at a glance. Any reference to the gem's `Redis` class from inside `Agent::Lock::Store::RedisStore` still needs a leading `::` to escape the enclosing module, but keeping the class itself named `RedisStore` avoids the sharper version of that trap, where an *unqualified* `Redis` inside a class literally named `Redis` silently resolves to itself.
|
|
41
|
+
|
|
42
|
+
## Backend selection is a security-relevant path
|
|
43
|
+
|
|
44
|
+
`Store.for` decides a virgin tree's backend once and records it in a marker file; every later process in that tree is bound to it regardless of what Redis is doing. That marker is claimed atomically (`O_CREAT|O_EXCL`) so two processes racing to open the same virgin tree can't end up on different backends and silently stop seeing each other's locks — worse than no lock at all. See `lib/agent/lock/store.rb`'s moduledoc before changing anything in `.for`, `.claim`, or `.default_backend`.
|
|
45
|
+
|
|
46
|
+
## Branch stacking
|
|
47
|
+
|
|
48
|
+
`kig/add-autocomplete` (PR #4) is currently rebased on top of `kig/fix-concurrent-agent-locking` (PR #3), not on `main`, to keep them from diverging into a large manual conflict resolution later. Its diff against `main` will look large — it includes PR #3's commits — until PR #3 merges. If you rebase either branch again, verify both spec backends and rubocop afterward per the checks above, and force-push with `--force-with-lease`.
|
|
49
|
+
|
|
50
|
+
## Known gaps
|
|
51
|
+
|
|
52
|
+
## (not yet fixed, tracked via PR review comments on PR #3)
|
|
53
|
+
|
|
54
|
+
`Manager`'s store-wide mutex covers `acquire`/`resume` but not `note`, `release`, `break_lock`, `release_all`, or the bare `reap` in `list`/`mine`; those still do read-modify-write outside it. The Redis mutex lease is not renewed while its critical section runs, so a slow `reap` (which can shell out to `ps` per local holder) can outlive it. Treat any change that widens what runs inside those bare `reap` calls, or that makes a critical section slower, as touching this.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.2.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- An `agent-lock` skill, shipped in the gem under `skills/`, that teaches an agent to claim files: naming itself on every call, running `alock` in the checkout it writes, and claiming its own files inside an orchestrator's lock. `alock skill install [--into DIR] [--force]` copies it into a skills directory, and `alock skill path` prints where the bundled copy is. `--for claude` installs into `~/.claude/skills`; without `--for` or `--into`, `~/.agents/skills` is the default for most other agents.
|
|
10
|
+
- `AGENT_LOCK_MUTEX_TIMEOUT`, the seconds a claim waits for the store's mutex.
|
|
11
|
+
- `whoami`, which prints the name this session signs locks with, its parent, and where each came from.
|
|
12
|
+
- A virgin tree now defaults to the Redis backend when one answers on `REDIS_URL`, and the file store when none does, rather than always defaulting to file. `AGENT_LOCK_BACKEND` still overrides it, and a tree that already has a backend recorded stays on it regardless of what Redis is doing. Two processes racing to decide the default for the same virgin tree are made to agree: the marker recording the choice is claimed with `O_CREAT|O_EXCL`, and every process builds from whichever value actually lands on disk rather than its own guess. See `README.md#backends`.
|
|
13
|
+
- Colorized `--help` and error output via `pastel`, disabled automatically when stdout is not a terminal.
|
|
14
|
+
- `AGENT_LOCK_TEST_BACKEND`, which runs the whole spec suite against Redis instead of the file store. CI now runs the suite once per backend.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The packaged gem no longer includes `.plans/`.
|
|
19
|
+
- `redis` and `pastel` are now runtime dependencies of the gem, rather than gems you install yourself to opt into the Redis backend: deciding the default now means probing for Redis whether or not you asked for it.
|
|
20
|
+
- A sub-agent that sets `AGENT_ID` but not `AGENT_PARENT_ID` takes its session's own name as its parent: `CLAUDE_SESSION_ID` if set, else the fingerprint. Claude Code runs sub-agents inside the parent's own process, so without this every sub-agent signed locks as the parent and none could block another.
|
|
21
|
+
- Ownership no longer runs upwards. `mine`, `release`, `release-all` and `note` cover your own locks and your sub-agents', never your parent's, so a sub-agent finishing up no longer releases the orchestrator's claim.
|
|
22
|
+
- A sub-agent asking for exactly its parent's scope is refused with exit 1 and told to claim something narrower.
|
|
23
|
+
- A scope that is empty, or that resolves outside the tree, is refused with exit 2. An empty scope used to mean the whole tree, and `/etc/passwd` used to become a lock on the tree-local file `passwd`. Absolute paths inside the tree, globs included, are made relative.
|
|
24
|
+
- `list` counts only live locks as held, lists interrupted ones separately, and tags a live lock past the stale window `STALE`, as do `check` and `mine`. `--json` carries `stale`.
|
|
25
|
+
- Hints and error messages name the program that was run, `alock` or `agent-lock`.
|
|
26
|
+
- `whoami` without `AGENT_ID` warns that every sub-agent of the session shares that name.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- A sub-agent claiming inside its parent's lock was told `ALREADY YOURS` and nothing was recorded, so two siblings could both claim the same file. The sub-agent now gets a lock of its own, which blocks its siblings.
|
|
31
|
+
- Claims on overlapping scopes with different names raced: the conflict scan and the write were separate steps, and `O_EXCL` or `SET NX` only guard an identical scope. Ten parallel claims of `lib/**` and `lib/aN.rb` left up to eleven overlapping locks. Every claim now scans and writes under one store-wide mutex: `flock` on the file store, a leased `SET NX PX` key released by compare-and-delete on Redis.
|
|
32
|
+
- Holding one file and asking for a wider scope answered `ALREADY YOURS` and wrote nothing, so the rest of the wider scope stayed open. Only a lock that contains the requested scope counts as already yours now.
|
|
33
|
+
- A live holder's lock was reaped once it was older than the stale window, measured from when it was taken, so an agent working for more than two hours lost its lock while still writing. A lock on this machine is now cleared only when its holder has exited; one from another machine expires when untouched past the window, and `note` counts as a touch.
|
|
34
|
+
- The `resume` and `break` hints printed before the listing they belong to whenever stdout and stderr went down one pipe, which is how an agent harness reads them. Stdout is flushed before a hint is written.
|
|
35
|
+
- A scope starting with a dot, such as `.plans/**` or `.github/**`, produced a lock file whose name also started with one, and the file store's `Dir.glob` skipped it. The lock was written and then enumerated nowhere, so `list`, `mine` and `check` reported it absent and a second session was free to acquire an overlapping scope. Both agents were told they held it.
|
|
36
|
+
|
|
37
|
+
## [0.1.0]
|
|
38
|
+
|
|
39
|
+
First release.
|
|
40
|
+
|
|
41
|
+
- `acquire`, `release`, `check`, `list`, `mine`, `release-all`, `break`, plus `note` and `resume` for work a crash interrupted.
|
|
42
|
+
- Session identity: `AGENT_ID`, else `CLAUDE_SESSION_ID`, else a fingerprint of the first ancestor process that is not a shell, so a lock survives the fresh shell an agent harness starts for every command.
|
|
43
|
+
- Locks live in `.git/agent-locks`, one store per repository, shared by every worktree and untrackable by git.
|
|
44
|
+
- Glob scopes, and a conflict rule that refuses rather than guesses.
|
|
45
|
+
- Sub-agents work inside a parent's claim; siblings block each other.
|
|
46
|
+
- Stale locks are reaped by liveness, and orphaned rather than deleted when they carry notes.
|
|
47
|
+
- Optional Redis backend, chosen explicitly and recorded per tree.
|
|
48
|
+
- Optional `--enforce` freeze via `chflags uchg` on macOS.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Konstantin Gredeskoul
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Agent::Lock (v0.2.0)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kigster/agent-lock/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
Advisory file locks for coding agents that share a checkout.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
`agent-lock` is a gem with one executable, `alock` (also installed as `agent-lock`). An agent runs `alock acquire <scope>` before it writes, and learns in one reply whether anybody else is working there: who, since when, and doing what. It works for separate sessions in one checkout, and for the sub-agents of a single session, which is the harder case.
|
|
10
|
+
|
|
11
|
+
Git does not help here. Two agents on one branch and one working tree never produce a conflict: the second writer simply wins, and the first one's work is gone without an error anywhere. A lock stops that, but only if every writer can be named reliably, even though an agent harness runs each command in a brand new shell. Making that name stable, and keeping claims correct when several agents ask at the same moment, is most of what this gem does.
|
|
12
|
+
|
|
13
|
+
## Who it is for
|
|
14
|
+
|
|
15
|
+
- **People running several coding agents at once** (Claude Code, Codex, Cursor, a terminal of your own) in the same checkout.
|
|
16
|
+
|
|
17
|
+
- **Orchestrators that fan out sub-agents** into one worktree, each taking a different part of it.
|
|
18
|
+
|
|
19
|
+
- **Harnesses** that launch agents over a repository and need them to stay out of each other's way.
|
|
20
|
+
|
|
21
|
+
Not for work that can have a checkout of its own. A git worktree per agent removes the sharing altogether, which is better than coordinating it. The locks are advisory: they protect a file only from agents that check. `--enforce` exists for the few files that must not move at all.
|
|
22
|
+
|
|
23
|
+
## How to use it
|
|
24
|
+
|
|
25
|
+
### Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem install agent-lock
|
|
29
|
+
alock version # alock must be on PATH
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or add `gem "agent-lock"` to a Gemfile.
|
|
33
|
+
|
|
34
|
+
### Claim before you write
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
alock acquire "lib/billing/**" "rewriting the invoices" # claim a corner of the tree
|
|
38
|
+
alock check "lib/billing/tax.rb" # exits 1 if somebody else holds it
|
|
39
|
+
alock note "lib/billing/**" "totals done, specs red" # where you are, for whoever comes next
|
|
40
|
+
alock list # everything held in this repository
|
|
41
|
+
alock release-all # when you are done
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
A refusal tells you who and what, not just that you lost:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ alock acquire lib/billing/tax.rb
|
|
48
|
+
REFUSED, do not write here
|
|
49
|
+
HELD lib/billing/** by luke-backend since 2026-09-09T21:04:11Z
|
|
50
|
+
intent: rewriting the invoices
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Exit 1 means do not write there. Exit 2 means the command could not run at all.
|
|
54
|
+
|
|
55
|
+
### Fan out to sub-agents in one worktree
|
|
56
|
+
|
|
57
|
+
The orchestrator claims the area it hands out, running `alock` bare. Each sub-agent then claims its own part of it, naming itself on every call:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
alock acquire "src/**" "fanning out the billing work" # orchestrator
|
|
61
|
+
|
|
62
|
+
AGENT_ID=billing-a alock acquire "src/billing/**" "tax rounding" # sub-agent A
|
|
63
|
+
AGENT_ID=billing-b alock acquire "src/billing/tax.rb" "..." # sub-agent B: REFUSED, held by billing-a
|
|
64
|
+
AGENT_ID=billing-a alock release-all # A's own locks, never the orchestrator's
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```mermaid
|
|
68
|
+
sequenceDiagram
|
|
69
|
+
participant O as Orchestrator (alo)
|
|
70
|
+
participant A as Sub-agent billing-a
|
|
71
|
+
participant B as Sub-agent billing-b
|
|
72
|
+
participant S as Lock store (.git/agent-locks)
|
|
73
|
+
O->>S: acquire src/**
|
|
74
|
+
S-->>O: ACQUIRED
|
|
75
|
+
par fan out
|
|
76
|
+
A->>S: AGENT_ID=billing-a acquire src/billing/**
|
|
77
|
+
S-->>A: ACQUIRED, parent inferred as the orchestrator
|
|
78
|
+
and
|
|
79
|
+
B->>S: AGENT_ID=billing-b acquire src/billing/tax.rb
|
|
80
|
+
S-->>B: REFUSED, held by billing-a
|
|
81
|
+
end
|
|
82
|
+
B->>S: AGENT_ID=billing-b acquire src/shipping/**
|
|
83
|
+
S-->>B: ACQUIRED
|
|
84
|
+
A->>S: AGENT_ID=billing-a release-all
|
|
85
|
+
Note over O,S: the orchestrator's src/** survives its children's release-all
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Three things matter, and each one fails silently if skipped:
|
|
89
|
+
|
|
90
|
+
1. **The name goes on every call.** Sub-agents run inside the parent's process and would sign every lock as the parent. Each call is also a fresh shell, so an `export AGENT_ID` from an earlier call is gone. `AGENT_ID=<name> alock whoami` shows the name a lock would be signed with.
|
|
91
|
+
|
|
92
|
+
1. **Run `alock` in the checkout being written**, with `cd` or `--dir`. A lock in another repository protects nothing.
|
|
93
|
+
|
|
94
|
+
1. **Sub-agents claim even inside the orchestrator's lock.** The orchestrator's lock keeps other sessions out; only a sub-agent's own claim keeps its siblings out.
|
|
95
|
+
|
|
96
|
+
### Teach your agents
|
|
97
|
+
|
|
98
|
+
The gem ships a skill that teaches an agent all of the above, so the rules reach the agents rather than living only in this README. Install it into the skills directory your agent reads:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
alock skill install # default: into ~/.agents/skills, for most agents
|
|
102
|
+
alock skill install --for claude # into ~/.claude/skills instead
|
|
103
|
+
alock skill install --into some/path # anywhere else, --for is ignored if both are given
|
|
104
|
+
alock skill path # where the bundled copy is
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`install` refuses to overwrite a copy that differs (`--force` replaces it) and never touches a symlink, since a symlink is some other installer's.
|
|
108
|
+
|
|
109
|
+
If skills on your machine are installed from repositories by a manifest, name this one as a source instead, so the skill is recorded along with where it came from. For example:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- name: agent-lock
|
|
113
|
+
type: skills
|
|
114
|
+
repo: git@github.com:kigster/agent-lock.git
|
|
115
|
+
path: skills
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then put the rule where every agent reads it, such as `CLAUDE.md` or `AGENTS.md`:
|
|
119
|
+
|
|
120
|
+
```markdown
|
|
121
|
+
Several agents may work in this checkout at once. Before creating or editing
|
|
122
|
+
files, load the agent-lock skill and claim the narrowest scope that covers your
|
|
123
|
+
writes:
|
|
124
|
+
|
|
125
|
+
alock acquire <scope> "<what you are doing>"
|
|
126
|
+
|
|
127
|
+
If it refuses, work somewhere else. A sub-agent prefixes every call with its
|
|
128
|
+
own name: AGENT_ID=<sub-agent-name> alock acquire ... Release with
|
|
129
|
+
`alock release-all` when you are done.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Advisory locks work because everybody checks. That is why the rule belongs in the instructions your agents load, and not only here.
|
|
133
|
+
|
|
134
|
+
## How it works
|
|
135
|
+
|
|
136
|
+
### Identity comes from the session
|
|
137
|
+
|
|
138
|
+
In order of preference:
|
|
139
|
+
|
|
140
|
+
| Source | When it applies |
|
|
141
|
+
| :------------------ | :------------------------------------------------------ |
|
|
142
|
+
| `AGENT_ID` | A human or a harness named this session on purpose |
|
|
143
|
+
| `CLAUDE_SESSION_ID` | A harness that exports it, since it survives `--resume` |
|
|
144
|
+
| Fingerprint | Neither of the above is set |
|
|
145
|
+
|
|
146
|
+
The fingerprint walks up from the current process until it finds an ancestor that is not a shell. That is the `claude` or `codex` process driving the session, or the terminal a person is typing in. Its pid, start time, uid and hostname are hashed into a name like `claude-1f4c8a02`, which stays the same for as long as the session lives and differs from anybody else's.
|
|
147
|
+
|
|
148
|
+
> [!NOTE]
|
|
149
|
+
> The start time is in the hash on purpose. Pids get recycled, and a new session that lands on a dead one's number should not inherit its locks.
|
|
150
|
+
|
|
151
|
+
`alock whoami` prints the name this session would sign a lock with, its parent, and where each came from. Run it before the first claim if in doubt.
|
|
152
|
+
|
|
153
|
+
### Locks live inside `.git`
|
|
154
|
+
|
|
155
|
+
The store is `$(git rev-parse --git-common-dir)/agent-locks`.
|
|
156
|
+
|
|
157
|
+
| Property | Why it matters |
|
|
158
|
+
| :-------------------------------------------- | :----------------------------------------------- |
|
|
159
|
+
| Git cannot track anything in `.git` | No repository needs a `.gitignore` entry |
|
|
160
|
+
| `git clean -xdf` cannot reach it | A cleanup does not silently drop every lock |
|
|
161
|
+
| Every worktree resolves to the same directory | One store serves the whole repository |
|
|
162
|
+
| It is deleted with the checkout | Nothing outlives the repo in your home directory |
|
|
163
|
+
|
|
164
|
+
Outside a git repository the store falls back to `~/.agent-locks`, keyed by a digest of the tree.
|
|
165
|
+
|
|
166
|
+
### A lock is a document, not a flag
|
|
167
|
+
|
|
168
|
+
```markdown
|
|
169
|
+
---
|
|
170
|
+
agent_id: luke-backend
|
|
171
|
+
scope: workflow/**
|
|
172
|
+
status: active
|
|
173
|
+
pid: 69232
|
|
174
|
+
created_at: 2026-09-09T21:04:11Z
|
|
175
|
+
---
|
|
176
|
+
rewriting the installer
|
|
177
|
+
|
|
178
|
+
## Progress
|
|
179
|
+
- 2026-09-09T21:14:02Z installer rewritten, specs still red
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
An agent that runs into this learns who holds the scope and what they are doing, so it can decide whether to wait or to work somewhere else. A human can read the same file in an editor.
|
|
183
|
+
|
|
184
|
+
### Scopes are globs
|
|
185
|
+
|
|
186
|
+
| Scope | Means |
|
|
187
|
+
| :------------ | :------------------------------------------------- |
|
|
188
|
+
| `**` | The whole tree |
|
|
189
|
+
| `workflow/**` | Everything under `workflow` |
|
|
190
|
+
| `workflow` | The same thing. A directory means everything in it |
|
|
191
|
+
| `lib/cli.rb` | One file |
|
|
192
|
+
|
|
193
|
+
Two scopes conflict when either one's fixed part contains the other's. So `workflow/**` conflicts with `workflow/lib/cli.rb`, and `docs/**` does not conflict with `workflow/**`.
|
|
194
|
+
|
|
195
|
+
> [!TIP]
|
|
196
|
+
> The rule deliberately refuses more often than it strictly must. Working out whether two globs can ever match the same path has answers nobody can predict, and the price of guessing wrong is somebody's lost work.
|
|
197
|
+
|
|
198
|
+
A scope is read from where you stand, globs included, and an absolute path inside the tree is made relative. So `$PWD/lib/**` and `lib/**` are the same claim, and `*.rb` typed in `lib/` is `lib/*.rb`. Only `.`, `*` and `**` mean the whole tree from anywhere. Two things are refused with exit 2 rather than guessed at:
|
|
199
|
+
|
|
200
|
+
| Scope | Why it is refused |
|
|
201
|
+
| :--------------------------- | :--------------------------------------------------------------------------------------------------------- |
|
|
202
|
+
| `""` | Usually an unset variable. Claiming the whole tree by accident blocks everybody; write `**` if you mean it |
|
|
203
|
+
| `/etc/passwd`, `../other/**` | Outside the tree, so no lock in this store can protect it |
|
|
204
|
+
|
|
205
|
+
### Sub-agents are a family, not one holder
|
|
206
|
+
|
|
207
|
+
Ownership and blocking are separate questions, and conflating them is what lets sibling sub-agents overwrite each other.
|
|
208
|
+
|
|
209
|
+
| Relationship | May claim an overlapping scope | Appears in `mine`, can be released |
|
|
210
|
+
| :---------------------- | :-------------------------------------------------------- | :--------------------------------- |
|
|
211
|
+
| Yourself | Yes, it is already yours | Yes |
|
|
212
|
+
| Your parent session | Yes, inside its claim, and the claim is recorded as yours | No |
|
|
213
|
+
| A sub-agent you spawned | No, you handed that scope out | Yes, so cleanup takes them with it |
|
|
214
|
+
| A sibling sub-agent | No | No |
|
|
215
|
+
|
|
216
|
+
A sub-agent claiming inside its parent's lock gets a lock of its own, and that lock is what keeps its siblings out. Asking for exactly the parent's scope is refused, since that would leave nothing for a sibling to be kept out of; claim something narrower. A sub-agent's `release-all` gives back its own locks and its children's, never its parent's.
|
|
217
|
+
|
|
218
|
+
#### Sub-agents that share their parent's process
|
|
219
|
+
|
|
220
|
+
Claude Code runs sub-agents inside the parent's own `claude` process, and sets nothing in the environment that tells them apart. They therefore share the parent's fingerprint, and without help every one of them would sign locks as the parent and none would ever block another.
|
|
221
|
+
|
|
222
|
+
So a sub-agent names itself, on every call, since each command runs in a fresh shell and an `export` does not survive to the next one:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
AGENT_ID=luke-backend alock whoami # id luke-backend, parent claude-1f4c8a02 (inferred)
|
|
226
|
+
AGENT_ID=luke-backend alock acquire lib/billing/** "invoices"
|
|
227
|
+
AGENT_ID=luke-backend alock release-all
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
When `AGENT_ID` is set and `AGENT_PARENT_ID` is not, the parent is taken to be the session's own name, `CLAUDE_SESSION_ID` if the harness exports it and the fingerprint otherwise, which is the orchestrator running bare `alock`. Set `AGENT_PARENT_ID` explicitly when the parent named itself too.
|
|
231
|
+
|
|
232
|
+
> [!NOTE]
|
|
233
|
+
> The same rule applies in a terminal. Setting `AGENT_ID` there makes the terminal's own session your parent, so a bare `alock` typed in that terminal can release what you claimed under the name.
|
|
234
|
+
|
|
235
|
+
### A crash does not strand the tree
|
|
236
|
+
|
|
237
|
+
A lock is cleared out of the way when its holder is provably gone: the process that signed it has exited, on this machine. A holder on another machine cannot be checked, so its lock is trusted until nobody has touched it for `AGENT_LOCK_STALE_MINUTES` (120 by default). Every `note` counts as a touch. What happens next depends on whether anything was written down:
|
|
238
|
+
|
|
239
|
+
| The lock has | What happens to it |
|
|
240
|
+
| :----------- | :----------------------------------------------- |
|
|
241
|
+
| No notes | Deleted. There is nothing to come back to |
|
|
242
|
+
| Notes | Orphaned. The claim is void, the record survives |
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
$ alock acquire workflow/**
|
|
246
|
+
INTERRUPTED WORK on workflow/**, left by luke-backend
|
|
247
|
+
alock resume workflow/** # take it back, notes and all
|
|
248
|
+
alock break workflow/** # throw it away and start over
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> [!WARNING]
|
|
252
|
+
> An orphan blocks nobody, but `acquire` will not silently overwrite one, because the notes inside it may be the only record of half-finished work. Choose `resume` or `break`.
|
|
253
|
+
|
|
254
|
+
A live holder's lock is never cleared, however old it is. An agent may legitimately work on one scope for hours, and deleting its lock underneath it would hand its files to the next agent while it is still writing them. Past the stale window `list`, `check` and `mine` tag it `STALE` instead, and taking it is a `break` that somebody announces first.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
$ alock list
|
|
258
|
+
Locks held (2):
|
|
259
|
+
app/** orchestrator 2026-09-11T16:04:56Z
|
|
260
|
+
fanning out
|
|
261
|
+
docs/** slow-agent 2026-09-11T13:04:55Z STALE
|
|
262
|
+
rewriting the guides
|
|
263
|
+
Interrupted (1):
|
|
264
|
+
db/** crashed-agent 2026-09-11T15:54:55Z INTERRUPTED
|
|
265
|
+
splitting the migrations
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Only live claims are counted as held. `--json` gives every record its `status` and a `stale` flag.
|
|
269
|
+
|
|
270
|
+
## Commands
|
|
271
|
+
|
|
272
|
+
| Command | Does | Exit code |
|
|
273
|
+
| :------------------------- | :----------------------------------------------------- | :----------------------------------------------------- |
|
|
274
|
+
| `acquire <scope> [intent]` | Claim a scope | 1 if held, interrupted, or exactly your parent's scope |
|
|
275
|
+
| `release <scope>` | Give it back | 1 if it belongs to somebody else |
|
|
276
|
+
| `check <scope>` | Report who holds it | 1 if held by another session |
|
|
277
|
+
| `note <scope> <text>` | Record progress inside a lock you hold | 1 if you do not hold it |
|
|
278
|
+
| `resume <scope>` | Take back work a crash interrupted | 1 if there is nothing to resume |
|
|
279
|
+
| `list` | Every lock in the store | 0 |
|
|
280
|
+
| `mine` | What this session and its sub-agents hold | 0 |
|
|
281
|
+
| `release-all` | Everything this session and its sub-agents hold | 0 |
|
|
282
|
+
| `break <scope>` | Take a live lock away from its holder | 0 |
|
|
283
|
+
| `whoami` | The name this session signs locks with, and its parent | 0 |
|
|
284
|
+
| `skill install` | Copy the bundled skill into a skills directory | 1 if a different copy or a symlink is there |
|
|
285
|
+
| `skill path` | Where the bundled skill is | 0 |
|
|
286
|
+
| `completion bash\|zsh` | Print a shell completion script | 1 for any other shell |
|
|
287
|
+
|
|
288
|
+
Flags:
|
|
289
|
+
|
|
290
|
+
| Flag | Where | Does |
|
|
291
|
+
| :----------- | :-------------------------------- | :-------------------------------------- |
|
|
292
|
+
| `--json` | `check`, `list`, `mine`, `whoami` | Machine-readable output |
|
|
293
|
+
| `--dir PATH` | Everywhere | Work as if run from `PATH` |
|
|
294
|
+
| `--enforce` | `acquire` | Also make the matched files unwritable |
|
|
295
|
+
| `--force` | `acquire` | Permit `--enforce` on a very wide scope |
|
|
296
|
+
|
|
297
|
+
Any command exits 2 when it cannot run at all: a scope that is empty or outside the tree, a backend mismatch, or a store it cannot reach.
|
|
298
|
+
|
|
299
|
+
## Shell completion
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
alock completion bash > "$(brew --prefix)/etc/bash_completion.d/alo"
|
|
303
|
+
alock completion zsh > "${fpath[1]}/_alo"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Names every command and flag `alock` currently knows, since the script is generated from the same registry the CLI runs, rather than hand-maintained separately from it.
|
|
307
|
+
|
|
308
|
+
## Configuration
|
|
309
|
+
|
|
310
|
+
| Variable | Default | Does |
|
|
311
|
+
| :------------------------- | :-------------------------------------- | :----------------------------------------------------------------------- |
|
|
312
|
+
| `AGENT_ID` | fingerprint | Name this session yourself |
|
|
313
|
+
| `AGENT_PARENT_ID` | the fingerprint, when `AGENT_ID` is set | The session that spawned this one |
|
|
314
|
+
| `AGENT_LOCK_DIR` | `.git/agent-locks` | Keep locks somewhere else |
|
|
315
|
+
| `AGENT_LOCK_STALE_MINUTES` | `120` | When a lock is tagged `STALE`, and when one from another machine expires |
|
|
316
|
+
| `AGENT_LOCK_MUTEX_TIMEOUT` | `15` | Seconds a claim waits for the store's mutex before giving up with exit 2 |
|
|
317
|
+
| `AGENT_LOCK_BACKEND` | `redis` if one answers, else `file` | `file` or `redis` |
|
|
318
|
+
| `AGENT_LOCK_TTL_SECONDS` | `0` | Redis expiry. `0` means no TTL |
|
|
319
|
+
| `REDIS_URL` | `redis://127.0.0.1:6379/0` | Where Redis is |
|
|
320
|
+
|
|
321
|
+
## Backends
|
|
322
|
+
|
|
323
|
+
Redis stores the same documents as the file store, and buys two things a filesystem cannot: its mutex holds across machines, and a TTL expires an abandoned lock without anybody having to reason about liveness. A tree defaults to Redis when one answers on `REDIS_URL`, and falls back to the file store, which needs nothing installed, when none does.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
AGENT_LOCK_BACKEND=redis alock acquire workflow/** # force it, rather than autodetect
|
|
327
|
+
AGENT_LOCK_BACKEND=file alock acquire workflow/** # or force the file store instead
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Either way, every claim checks for conflicts and writes its lock while holding one mutex for the whole store. Refusing an atomic write of an identical scope is not enough on its own: `lib/**` and `lib/cli.rb` are different keys, and two agents claiming them at the same moment would both find the store empty and both win. The file store takes an exclusive `flock` on `.mutex` beside the locks. Redis takes `agent-lock-mutex:<tree digest>` with `SET NX PX` and a random token, and gives it back with a compare-and-delete script, so a process whose lease ran out cannot release somebody else's.
|
|
331
|
+
|
|
332
|
+
> [!CAUTION]
|
|
333
|
+
> Picking a default from whether Redis happens to answer is still never a runtime auto-*switch*. If one agent found a running Redis and switched to it while the agent beside it did not, the two would take locks in different stores, see nothing of each other, and both report success — worse than no lock at all. The first store created in a tree records which backend it is, in a marker file, and every later process in that tree is bound to it regardless of what Redis is doing next; a mismatch stops the run rather than silently picking the other one.
|
|
334
|
+
>
|
|
335
|
+
> That default is decided at the one moment two processes could otherwise race: both find a virgin tree, both probe Redis, and a flaky answer could hand them different defaults before either writes the marker. The marker is claimed atomically — the first `O_CREAT|O_EXCL` wins — and every process builds from whatever ends up on disk, never from its own guess, so a race can land on either backend but never on a split.
|
|
336
|
+
|
|
337
|
+
The `redis` gem ships as a dependency of this one now, since deciding the default means probing for it. If you never want that probe, or never install a local Redis, set `AGENT_LOCK_BACKEND=file` yourself.
|
|
338
|
+
|
|
339
|
+
## Freezing files, on macOS
|
|
340
|
+
|
|
341
|
+
`acquire --enforce` also runs `chflags uchg` on every matched file, which makes them unwritable by anything, whether it checks for locks or not.
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
alock acquire "config/credentials/**" "keys must not move during the migration" --enforce
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
> [!WARNING]
|
|
348
|
+
> This is opt-in for three reasons. It is macOS only, since the Linux equivalent (`chattr +i`) requires root. It denies the holder too, so it suits a freeze rather than a file you are editing. And a session that dies leaves the files frozen, at which point `git checkout` and `rm -rf` start failing with "Operation not permitted".
|
|
349
|
+
>
|
|
350
|
+
> Every frozen path is written into the lock, so `release` and `break` thaw them without needing the process that froze them to still exist.
|
|
351
|
+
|
|
352
|
+
## Development
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
bin/setup
|
|
356
|
+
bundle exec rspec # 228 examples, against the file backend
|
|
357
|
+
AGENT_LOCK_TEST_BACKEND=redis bundle exec rspec # the same suite, against Redis instead
|
|
358
|
+
bundle exec rubocop
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The suite runs against one backend at a time, picked by `AGENT_LOCK_TEST_BACKEND` rather than the machine's own default, so it stays deterministic whether or not Redis happens to be running: only the examples that test one backend's own on-disk or on-Redis shape care which one that is, and a Redis run defaults to database 15 so it never touches whatever database a developer's own Redis work lives in. CI runs both, plus rubocop as a third, independent job.
|
|
362
|
+
|
|
363
|
+
A `justfile` covers the rest: `just test`, `just test-coverage`, `just ci` (rubocop then coverage), `just format` (autocorrect, then regenerate `.rubocop_todo.yml`), and `just publish`/`just release` for cutting a version. `just lint` currently names `standardrb`, which is not one of this gem's dependencies; use `bundle exec rubocop` until that recipe is reconciled with the rest of the project's tooling.
|
|
364
|
+
|
|
365
|
+
The library decides and the CLI prints. Every verb is a method on `Manager` that returns a result and prints nothing, so the whole lifecycle can be tested without capturing output. `Launcher` takes `argv`, `stdin`, `stdout`, `stderr` and `kernel` as arguments, and nothing below it calls `puts` or a receiverless `exit`, which is what lets Aruba run the CLI end to end inside the test process instead of forking a Ruby per example.
|
|
366
|
+
|
|
367
|
+
## License
|
|
368
|
+
|
|
369
|
+
MIT
|
|
370
|
+
|
|
371
|
+
## Authors
|
|
372
|
+
|
|
373
|
+
- Konstantin Gredeskoul @kigster
|
|
374
|
+
- Claude Code, @claude
|
data/Rakefile
ADDED
data/exe/agent-lock
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "agent/lock"
|
|
5
|
+
|
|
6
|
+
# The name is passed in, not read inside the gem, so that every hint the CLI
|
|
7
|
+
# prints tells the user to type what they actually typed.
|
|
8
|
+
Agent::Lock::Launcher.new(ARGV, program: File.basename($PROGRAM_NAME)).execute!
|
data/exe/alock
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The short name, for the times you are typing it twenty times an hour.
|
|
5
|
+
require "agent/lock"
|
|
6
|
+
|
|
7
|
+
# The name is passed in, not read inside the gem, so that every hint the CLI
|
|
8
|
+
# prints tells the user to type what they actually typed.
|
|
9
|
+
Agent::Lock::Launcher.new(ARGV, program: File.basename($PROGRAM_NAME)).execute!
|