agent-loop 0.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 +7 -0
- data/Prompt.md +15 -0
- data/Readme.md +32 -0
- data/agent-loop.gemspec +19 -0
- data/bin/agent-loop +6 -0
- data/lib/agent_loop/cli.rb +42 -0
- data/lib/agent_loop/runner.rb +103 -0
- data/lib/agent_loop/version.rb +5 -0
- data/lib/agent_loop/watcher.rb +55 -0
- data/lib/agent_loop.rb +5 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 915989d4e9cf002298cd6b27243146113f0b04cab3e759c55e6c5b333cf1b997
|
|
4
|
+
data.tar.gz: 80cbf0ccdbc645a60af30dc7ab22cdc0f8c54957e0ec8829681a8ab8160da1a4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9dab320aafd99d76c6f4204bb8e7ec4063c720c33d341f7f78423bdd766c0ab1892b21e0c4950a545738cf587067db94d3a399b386b0a49fbc9f8796e13e1ded
|
|
7
|
+
data.tar.gz: '049da19643c2b415967185fb67ed782ef04735e547bde74a8581620842a8aba03b88cf2e83377d582b8ab2465fab2c87cac799ff3562361941303cd483055f1e'
|
data/Prompt.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Instructions
|
|
2
|
+
|
|
3
|
+
You are to execute the following steps:
|
|
4
|
+
1. Take the first unchecked task from the list below
|
|
5
|
+
2. Do the work in that task
|
|
6
|
+
3. Update this Prompt.md and check that task off on the list
|
|
7
|
+
4. Write "done" to the file `${TASK_FILE}`
|
|
8
|
+
|
|
9
|
+
If there are no unchecked tasks remaining, write the text "done" to the file `${STATUS_FILE}` using the Bash tool.
|
|
10
|
+
|
|
11
|
+
## Tasks
|
|
12
|
+
- [ ] Make a file
|
|
13
|
+
- [ ] Put a "Hello World" program in the file made from the previous task
|
|
14
|
+
- [ ] Run the program
|
|
15
|
+
|
data/Readme.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Agent Loop
|
|
2
|
+
|
|
3
|
+
A command line utility that runs Claude Code CLI in a loop to work tasks one by one from a Prompt.md
|
|
4
|
+
|
|
5
|
+
## Project Goals
|
|
6
|
+
1. Can be cloned down and run with minimal setup
|
|
7
|
+
1. This project can work in existing repos with minimal configuration
|
|
8
|
+
1. Zero dependencies other than Claude Code and bash
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bash <(curl -fsSL https://raw.githubusercontent.com/BrandonMathis/agent-loop/main/install.sh)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This downloads `start_agent_loop.sh` and `Prompt.md` into your current directory.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
1. Edit `Prompt.md` with your tasks
|
|
20
|
+
2. Run agent loop script
|
|
21
|
+
|
|
22
|
+
**Human Assisted**
|
|
23
|
+
```
|
|
24
|
+
./start_agent_loop.sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Fully Automated**
|
|
28
|
+
```
|
|
29
|
+
./start_agent_loop.sh --dangerous
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When running this script with the `--dangerous` flag you are giving claude code cli [full permission to do whatever it pleases via the `--dangerously-skip-permissions`](https://code.claude.com/docs/en/settings#permission-settings) flag. Please proceed with caution and consider all possible risks.
|
data/agent-loop.gemspec
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative "lib/agent_loop/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "agent-loop"
|
|
5
|
+
spec.version = AgentLoop::VERSION
|
|
6
|
+
spec.authors = ["Brandon Mathis"]
|
|
7
|
+
spec.summary = "Run Claude Code CLI in a loop, working tasks one by one from a Prompt.md"
|
|
8
|
+
spec.homepage = "https://github.com/BrandonMathis/agent-loop"
|
|
9
|
+
spec.license = "MIT"
|
|
10
|
+
|
|
11
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
12
|
+
|
|
13
|
+
spec.files = Dir["lib/**/*.rb", "bin/*", "*.md", "*.gemspec"]
|
|
14
|
+
spec.bindir = "bin"
|
|
15
|
+
spec.executables = ["agent-loop"]
|
|
16
|
+
spec.require_paths = ["lib"]
|
|
17
|
+
|
|
18
|
+
spec.add_dependency "thor", "~> 1.2"
|
|
19
|
+
end
|
data/bin/agent-loop
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "runner"
|
|
5
|
+
|
|
6
|
+
module AgentLoop
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
desc "start", "Run the agent loop in the current directory"
|
|
9
|
+
long_desc <<~LONGDESC
|
|
10
|
+
Reads Prompt.md (or the file given by --prompt), expands ${STATUS_FILE}
|
|
11
|
+
and ${TASK_FILE} placeholders, and runs `claude` in a loop until the
|
|
12
|
+
agent writes "done" to the status file.
|
|
13
|
+
LONGDESC
|
|
14
|
+
|
|
15
|
+
method_option :dangerous,
|
|
16
|
+
type: :boolean,
|
|
17
|
+
default: false,
|
|
18
|
+
desc: "Pass --dangerously-skip-permissions to claude"
|
|
19
|
+
|
|
20
|
+
method_option :prompt,
|
|
21
|
+
type: :string,
|
|
22
|
+
default: "Prompt.md",
|
|
23
|
+
aliases: "-p",
|
|
24
|
+
desc: "Path to the prompt template file"
|
|
25
|
+
|
|
26
|
+
method_option :poll_interval,
|
|
27
|
+
type: :numeric,
|
|
28
|
+
default: 0.5,
|
|
29
|
+
aliases: "-i",
|
|
30
|
+
desc: "Seconds between watcher poll checks"
|
|
31
|
+
|
|
32
|
+
def start
|
|
33
|
+
Runner.new(options).run
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
default_task :start
|
|
37
|
+
|
|
38
|
+
def self.exit_on_failure?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "watcher"
|
|
6
|
+
|
|
7
|
+
module AgentLoop
|
|
8
|
+
class Runner
|
|
9
|
+
DONE = "done"
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@dangerous = options[:dangerous] || options["dangerous"] || false
|
|
13
|
+
@prompt_file = options[:prompt] || options["prompt"] || "Prompt.md"
|
|
14
|
+
@poll_interval = (options[:poll_interval] || options["poll_interval"] || 0.5).to_f
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
setup_temp_files
|
|
19
|
+
trap_signals
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
break if status_done?
|
|
23
|
+
|
|
24
|
+
abort "Error: #{@prompt_file} not found." unless File.exist?(@prompt_file)
|
|
25
|
+
|
|
26
|
+
FileUtils.rm_f(@status_file)
|
|
27
|
+
puts "Running agent loop iteration..."
|
|
28
|
+
|
|
29
|
+
run_iteration(expand_prompt)
|
|
30
|
+
|
|
31
|
+
break if status_done?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts "AGENT_STATUS is 'done'. Stopping loop."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def setup_temp_files
|
|
40
|
+
loop_id = Digest::SHA1.hexdigest("#{Dir.pwd}\n")[0, 8]
|
|
41
|
+
@status_file = "/tmp/AGENT_STATUS_#{loop_id}"
|
|
42
|
+
@task_file = "/tmp/AGENT_TASK_#{loop_id}"
|
|
43
|
+
FileUtils.rm_f([@status_file, @task_file])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def trap_signals
|
|
47
|
+
handler = proc do
|
|
48
|
+
puts "Interrupted. Stopping loop."
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
trap("INT", handler)
|
|
52
|
+
trap("TERM", handler)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def expand_prompt
|
|
56
|
+
File.read(@prompt_file)
|
|
57
|
+
.gsub("${STATUS_FILE}", @status_file)
|
|
58
|
+
.gsub("${TASK_FILE}", @task_file)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run_iteration(prompt_text)
|
|
62
|
+
stdin_r, stdin_w = IO.pipe
|
|
63
|
+
stdin_w.write(prompt_text)
|
|
64
|
+
stdin_w.close
|
|
65
|
+
|
|
66
|
+
flags = @dangerous ? ["--dangerously-skip-permissions"] : []
|
|
67
|
+
agent_pid = spawn("claude", *flags, in: stdin_r)
|
|
68
|
+
stdin_r.close
|
|
69
|
+
|
|
70
|
+
watcher = Watcher.new(
|
|
71
|
+
status_file: @status_file,
|
|
72
|
+
task_file: @task_file,
|
|
73
|
+
poll_interval: @poll_interval
|
|
74
|
+
).start(agent_pid) do |signal, pid|
|
|
75
|
+
case signal
|
|
76
|
+
when :task_done
|
|
77
|
+
puts "AGENT_TASK is 'done'. Killing agent (PID #{pid})."
|
|
78
|
+
when :status_done
|
|
79
|
+
puts "AGENT_STATUS is 'done'. Killing agent (PID #{pid})."
|
|
80
|
+
end
|
|
81
|
+
kill_process(pid)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
_, status = Process.waitpid2(agent_pid)
|
|
85
|
+
watcher.stop
|
|
86
|
+
|
|
87
|
+
if status.exitstatus == 0 && !status_done?
|
|
88
|
+
puts "Agent exited cleanly. Stopping loop."
|
|
89
|
+
exit 0
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def kill_process(pid)
|
|
94
|
+
Process.kill("TERM", pid)
|
|
95
|
+
rescue Errno::ESRCH
|
|
96
|
+
# Process already gone
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def status_done?
|
|
100
|
+
File.exist?(@status_file) && File.read(@status_file).strip == DONE
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentLoop
|
|
4
|
+
class Watcher
|
|
5
|
+
DONE = "done"
|
|
6
|
+
|
|
7
|
+
def initialize(status_file:, task_file:, poll_interval: 0.5)
|
|
8
|
+
@status_file = status_file
|
|
9
|
+
@task_file = task_file
|
|
10
|
+
@poll_interval = poll_interval
|
|
11
|
+
@thread = nil
|
|
12
|
+
@stop = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Starts the background watcher thread. Yields :task_done or :status_done
|
|
16
|
+
# to the block when a signal file is detected.
|
|
17
|
+
def start(agent_pid, &callback)
|
|
18
|
+
@stop = false
|
|
19
|
+
@thread = Thread.new do
|
|
20
|
+
loop do
|
|
21
|
+
break if @stop
|
|
22
|
+
|
|
23
|
+
if done?(@task_file)
|
|
24
|
+
File.delete(@task_file) if File.exist?(@task_file)
|
|
25
|
+
callback.call(:task_done, agent_pid)
|
|
26
|
+
break
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if done?(@status_file)
|
|
30
|
+
callback.call(:status_done, agent_pid)
|
|
31
|
+
break
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sleep @poll_interval
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stop
|
|
41
|
+
@stop = true
|
|
42
|
+
@thread&.join(2)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def join
|
|
46
|
+
@thread&.join
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def done?(path)
|
|
52
|
+
File.exist?(path) && File.read(path).strip == DONE
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/agent_loop.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agent-loop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brandon Mathis
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.2'
|
|
27
|
+
description:
|
|
28
|
+
email:
|
|
29
|
+
executables:
|
|
30
|
+
- agent-loop
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- Prompt.md
|
|
35
|
+
- Readme.md
|
|
36
|
+
- agent-loop.gemspec
|
|
37
|
+
- bin/agent-loop
|
|
38
|
+
- lib/agent_loop.rb
|
|
39
|
+
- lib/agent_loop/cli.rb
|
|
40
|
+
- lib/agent_loop/runner.rb
|
|
41
|
+
- lib/agent_loop/version.rb
|
|
42
|
+
- lib/agent_loop/watcher.rb
|
|
43
|
+
homepage: https://github.com/BrandonMathis/agent-loop
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata: {}
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.0.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.4.19
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Run Claude Code CLI in a loop, working tasks one by one from a Prompt.md
|
|
66
|
+
test_files: []
|