agent-lock 2.0.0 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa831ef3bd2d74a6b65c1131ebebec51d914bd92199681d71fef99b568b75faf
4
- data.tar.gz: bc0d2afc62e90a2e610f0dd4995e05e944ff00cd9a4a9ee2e59a6ad3483a463d
3
+ metadata.gz: d1f106a6394f81598c390390a2d83aeb051b9e22d799c23129eeed6aba0f0822
4
+ data.tar.gz: d31b63c4c1469eee913e33ca102dcb35f108d7b1aabb24408ec8bdd8b92958d4
5
5
  SHA512:
6
- metadata.gz: d986bcb837514045683037ebaecd7ddabc4059cf00a9118c8d9998f4ec3ced0e3378ab854a1b21eaaf0a0ad4fce2473d18f8fd994287ac550f5a778ec74171da
7
- data.tar.gz: 0a1925e8747601cfa147260642eb863c8c7ede69895622ee50c9c5a8c13f5c1656618e699aa80c5bdc11edc53f3728f6401cb08cf7d93bbfe3eca71b023ba736
6
+ metadata.gz: 8a947455256070a8bc2eb2211f63fdcc970b64e25d479ed57724ac17dbd39a28c4317fcf28465fa91b58942f4a24231af72190c34519640c905c4e04a254b38a
7
+ data.tar.gz: ce09be390c7cbbd93315e7b359fd30efbb53fb064d8b98b849fcb8e3fa59756fb85eec0c46b0c23d837aa87aa5d815d726c152bf4be56f44d48163eaa402310d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.1.0]
6
+
7
+ ### Added
8
+
9
+ - Help screens come from `dry-cli-help`: `alock -h` now opens with the gem's title and a description of what it does and how the two backends are chosen, lists commands in registration order, and wraps everything at 85 columns. The hand-written backend banner that used to print above dry-cli's own help is gone.
10
+
11
+ ### Changed
12
+
13
+ - Finding a Redis now cascades: `REDIS_URL` is tried first when it is set, then `redis://127.0.0.1:6379/0`, and only when neither answers does a virgin tree default to the file store. Previously `REDIS_URL` replaced the local default rather than preceding it, so a shared Redis being down sent the tree straight to the filesystem even with one running locally. `RedisStore.url` now reports the URL that actually answered.
14
+
5
15
  ## [0.2.0]
6
16
 
7
17
  ### Added
data/README.md CHANGED
@@ -1,9 +1,18 @@
1
- # Agent::Lock (v0.2.0)
1
+ # Agent::Lock (v2.0.1)
2
2
 
3
3
  [![Ruby](https://github.com/kigster/agent-lock/actions/workflows/main.yml/badge.svg)](https://github.com/kigster/agent-lock/actions/workflows/main.yml)
4
4
 
5
5
  Advisory locks on files and folders with pluggable backends (redis and file-system provided out the box, with redis being the default if it's available locally). This locking tool is meant to be used by the coding agents that share a checkout.
6
6
 
7
+ > [!NOTE]
8
+ >
9
+ > Please note that `agent-lock` is part of a three-part system:
10
+ >
11
+ > 1. `agentilda` is the Ruby Gem, which is a CLI tool that creates and manages the `.plans` folder, and comes with eight or so specialized agents that take a spec.md file and work through it until it's a set of PRs open, reviewed, and passing on your CI. It does not automatically merge anything.
12
+ > 2. [`agentilda-ai-setup`](https://kigster/agentilda-ai-setup) is the GitHub repo that's a mixture of BASH and Ruby installers. It's comes with the [`configuration.yml`](https://github.com/kigster/agentilda-ai-setup/blob/main/configuration.example.yml) file, which lists the installation commands for the coding agents you'd like to install locally, any other executables you might want (for instance, it installs `bt` — braintrust's CLI utility), and then you can list any number of Github Repos and use them to install skills, plugins, commands from them, specifying exactly which you want to install and which you want to exclude. Moreover you can specify a sub-directory of a github repo to install from.
13
+ >
14
+ > Together, the three repos, after installation provide you with the consistent way to replicate your `~/.agents` and `~/.claude` folders on multiple computers, and to also replicate a consistent agentic software team workflow across many projects.
15
+
7
16
  ## What it is
8
17
 
9
18
  `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.
@@ -316,11 +325,13 @@ Names every command and flag `alock` currently knows, since the script is genera
316
325
  | `AGENT_LOCK_MUTEX_TIMEOUT` | `15` | Seconds a claim waits for the store's mutex before giving up with exit 2 |
317
326
  | `AGENT_LOCK_BACKEND` | `redis` if one answers, else `file` | `file` or `redis` |
318
327
  | `AGENT_LOCK_TTL_SECONDS` | `0` | Redis expiry. `0` means no TTL |
319
- | `REDIS_URL` | `redis://127.0.0.1:6379/0` | Where Redis is |
328
+ | `REDIS_URL` | unset | A shared Redis, tried before `redis://127.0.0.1:6379/0` |
320
329
 
321
330
  ## Backends
322
331
 
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.
332
+ 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, and falls back to the file store, which needs nothing installed, when none does.
333
+
334
+ The search for a Redis runs in a fixed order: `REDIS_URL` first, when it is set, then `redis://127.0.0.1:6379/0`. A `REDIS_URL` nobody answers on is not the end of it, since the usual reason to set one is a shared instance that is occasionally down, and a local Redis is still a better lock store than the filesystem. Only when neither answers does the tree land on the file store. `alock list` names the store in use, so it is never a guess which one you are on.
324
335
 
325
336
  ```bash
326
337
  AGENT_LOCK_BACKEND=redis alock acquire workflow/** # force it, rather than autodetect
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "manager"
4
+ require_relative "version"
4
5
  require "dry/cli"
6
+ require "dry/cli/help"
5
7
 
6
8
  module Agent
7
9
  module Lock
@@ -15,6 +17,32 @@ module Agent
15
17
  # line, so guessing it wrong is not a cosmetic mistake.
16
18
  PROGRAM_NAME = "alock"
17
19
 
20
+ # What `alock -h` says before it lists the commands. Wrapped by
21
+ # dry-cli-help at HELP_WIDTH rather than the terminal's width, so the
22
+ # screen reads the same in a wide terminal, a narrow one, and a log.
23
+ HELP_WIDTH = 85
24
+
25
+ DESCRIPTION = <<~TEXT
26
+ Advisory locks for AI coding agents working concurrently in one checkout.
27
+ An agent claims the file or directory it is about to write, and every other
28
+ agent that checks sees who holds it and why. Nothing is enforced: the locks
29
+ work because every agent checks before it writes.
30
+
31
+ Locks live in one of two backends. Redis, when one answers on REDIS_URL or
32
+ on localhost:6379, holds across machines and can expire an abandoned lock.
33
+ The file store, used when no Redis answers, needs nothing installed. The
34
+ first process in a tree records which backend it chose, and every later
35
+ process in that tree stays on it. Set AGENT_LOCK_BACKEND to `redis` or
36
+ `file` to choose one yourself.
37
+ TEXT
38
+
39
+ Dry::CLI::Help.configure do
40
+ title "Agent Lock, Version #{Agent::Lock::VERSION}"
41
+ description DESCRIPTION
42
+ epilogue "Documentation: https://github.com/kigster/agent-lock"
43
+ width HELP_WIDTH
44
+ end
45
+
18
46
  # A registry whose commands are already bound to this launcher, so a
19
47
  # command writes to the streams it was given rather than to the process's.
20
48
  #
@@ -70,8 +70,6 @@ module Agent
70
70
  # @return [void] always exits, with 0 unless something said otherwise
71
71
  # rubocop:disable-next Metrics/AbcSize
72
72
  def execute!
73
- backend_banner if %w[-h --help].intersect?(argv)
74
-
75
73
  code = 0
76
74
  Dry::CLI.new(CLI.registry_for(self)).call(arguments: argv, out: stdout, err: stderr)
77
75
  rescue SystemExit => e
@@ -93,34 +91,6 @@ module Agent
93
91
  def p(msg = "")
94
92
  stdout.puts(msg)
95
93
  end
96
-
97
- # The two backends, ahead of dry-cli's own `--help` for each command, so
98
- # a reader learns the shape of the choice before any subcommand's flags.
99
- #
100
- # @return [void]
101
- # rubocop:disable-next Metrics/AbcSize
102
- def backend_banner
103
- p(bold(yellow("Agent Lock, Version #{green(Agent::Lock::VERSION)}")))
104
- p
105
- p(bold(blue("Usage:")))
106
- p(" alock [command [ subcommand ]] [options]")
107
- p
108
- p(bold(blue("Description:")))
109
- p(" This is a CLI utility aimed at the agents working concurrently in the same")
110
- p(" environment, sharing filesystem, worktrees, etc. Agent Lock allows fine-grained")
111
- p(" and effective locking, and can use multiple backends to store and maintain locks.")
112
- p
113
- p(cyan(" • Redis-Based Locking"))
114
- p(" This mechanism uses locally running Redis instance to coordinate access to shared")
115
- p(" resources (default, if Redis is available and accessible).")
116
- p
117
- p(cyan(" • File System Locking"))
118
- p(" This mechanism uses file system locks to coordinate access to shared resources.")
119
- p
120
- p(" You can set the environment variable #{yellow("AGENT_LOCK_BACKEND")} to either")
121
- p(" 'redis' or 'file' to override the default.")
122
- p
123
- end
124
94
  end
125
95
  end
126
96
  end
@@ -20,10 +20,14 @@ module Agent
20
20
  # markdown document either way.
21
21
  #
22
22
  # Picked with AGENT_LOCK_BACKEND=redis, or by default when one answers on
23
- # REDIS_URL. See Store's moduledoc for how that default is decided.
23
+ # REDIS_URL, or failing that on the local default. See Store's moduledoc
24
+ # for how that default is decided.
24
25
  class RedisStore
25
26
  NAMESPACE = "agent-lock"
26
27
 
28
+ # Tried after REDIS_URL, or alone when it is unset.
29
+ LOCAL_URL = "redis://127.0.0.1:6379/0"
30
+
27
31
  # How long the store mutex outlives a holder that died holding it. A
28
32
  # claim's critical section is a SCAN and one SET, so ten seconds is
29
33
  # somebody gone, not somebody busy.
@@ -206,15 +210,59 @@ module Agent
206
210
  end
207
211
 
208
212
  class << self
209
- def url = ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/0")
213
+ # The URL of the Redis actually in use, once one has answered, or
214
+ # the first one that will be tried before then.
215
+ #
216
+ # @return [String]
217
+ def url = @url || candidate_urls.first
218
+
219
+ # Where to look, in order: `REDIS_URL` when set, then the local
220
+ # default. A `REDIS_URL` that nobody answers on is not the end of
221
+ # the search, because the usual reason it is set is a shared
222
+ # instance that is sometimes down, and a local one is still better
223
+ # than falling all the way back to the filesystem.
224
+ #
225
+ # @return [Array<String>]
226
+ def candidate_urls
227
+ configured = ENV["REDIS_URL"].to_s.strip
228
+ [configured.empty? ? nil : configured, LOCAL_URL].compact.uniq
229
+ end
210
230
 
231
+ # Tries each candidate URL in turn and keeps the first that answers.
232
+ #
211
233
  # @return Array[RedisClient,NilClass,Exception] the client if it could be created,
212
- # or the error that prevented it
234
+ # or the error that prevented it (the last one tried)
213
235
  def create_client
214
- @client ||= ::Redis.new(url: url).tap do |client|
215
- _version = client.info["redis_version"]
236
+ return [@client, nil] if @client
237
+
238
+ error = nil
239
+ candidate_urls.each do |candidate|
240
+ client, error = connect(candidate)
241
+ next unless client
242
+
243
+ @url = candidate
244
+ @client = client
245
+ return [@client, nil]
216
246
  end
217
- [@client, nil]
247
+ [nil, error]
248
+ end
249
+
250
+ # Drops the memoized client, so the next `create_client` probes again.
251
+ #
252
+ # @return [void]
253
+ def reset!
254
+ @client = nil
255
+ @url = nil
256
+ end
257
+
258
+ private
259
+
260
+ # @param candidate [String]
261
+ # @return Array[RedisClient,NilClass,Exception]
262
+ def connect(candidate)
263
+ client = ::Redis.new(url: candidate)
264
+ client.info["redis_version"]
265
+ [client, nil]
218
266
  rescue Redis::CannotConnectError, Redis::BaseError, Errno::ECONNREFUSED, SocketError => e
219
267
  [nil, e]
220
268
  end
@@ -12,7 +12,8 @@ module Agent
12
12
  #
13
13
  # AGENT_LOCK_BACKEND wins when set. Otherwise a tree that already has a
14
14
  # marker keeps using it. A virgin tree defaults to Redis when one answers
15
- # locally, file when none does.
15
+ # on REDIS_URL or, failing that, on the local default; file when neither
16
+ # does.
16
17
  #
17
18
  # That default is still never a runtime auto-*switch*: once a marker
18
19
  # exists, every later process in that tree is bound to it regardless of
@@ -90,7 +91,8 @@ module Agent
90
91
 
91
92
  def marker_path(tree) = File.join(tree.store_dir, MARKER)
92
93
 
93
- # @return [String] "redis" when one answers on REDIS_URL, "file" otherwise
94
+ # @return [String] "redis" when one answers on REDIS_URL or, failing
95
+ # that, on RedisStore::LOCAL_URL; "file" when neither does
94
96
  def default_backend
95
97
  local_redis_available? ? "redis" : "file"
96
98
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Agent
4
4
  module Lock
5
- VERSION = "2.0.0"
5
+ VERSION = "2.1.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -93,15 +93,22 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '5.0'
96
- description: 'This compact ruby gem is purely CI utility: it''s meant to be used with
97
- the team of AI agentsexecuting along the plan you approved. Using worktrees and
98
- parallelism it''s easy to 10x the speed of develpoment of software even compared
99
- to a single agent working on it. Plus each agent can specialize. With that, these
100
- agents need a shared locking mechanism to ensure they are not working on the same
101
- directory, same worktree, and soon. This gem is exaclty that tool. The gem distributed
102
- as ''agent-lock'', and offers the shortened binary called ''alock'' (although ''agent-lock''
103
- works as well). The best way to understand the gem is to install and run it with
104
- ''alock --help''. Furthermore, each subcommand has an additional help.'
96
+ description: 'This compact ruby gem is purely CLI utility: it''s meant to be used
97
+ by a team of AI agentsexecuting along one or more the plans in a given repo. Using
98
+ worktrees and parallelism it''s easy to 10x the speed of develpoment of software
99
+ development compared to even a single agent working on it. Plus each agent can specialize.
100
+ Such agents require globsl shared locking mechanism to ensure they are not working
101
+ on the same directory, or the same files in same worktree. That is exactly what
102
+ this gem does. The CLI binary you invoke is called ''alock''which comes with sub-command
103
+ ''completion'' which you can load for BASH or ZSH. However, you are not very likely
104
+ going toinvoke this gem directly. It''s used by an Agentic Workflow gem ''agentilda''
105
+ to protect shared resources. To try the entire system, it''s recommended to download
106
+ the repo https://github.com/kigster/agentilda-ai-setup which both installs the two
107
+ gems, and offers a configuration file that installs a set of coding agents, skills,
108
+ plugins, commands, from various guthub folders, or via running commands, and so
109
+ on. In other words the repo''s purpose is to ensure your agentic setup is identical
110
+ from machine to machine, and by modifying the config file you can pick and choose
111
+ your skills, your AGENT.md/CLAUDE.md file and so on.'
105
112
  email:
106
113
  - kigster@gmail.com
107
114
  executables:
@@ -175,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
182
  - !ruby/object:Gem::Version
176
183
  version: '0'
177
184
  requirements: []
178
- rubygems_version: 4.0.20
185
+ rubygems_version: 4.0.21
179
186
  specification_version: 4
180
187
  summary: Advisory locks to prevent race conditions and general mayhem when multiple
181
188
  AI agents unknowinly modify the same souce three.