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 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.
@@ -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,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "agent_loop"
5
+
6
+ AgentLoop::CLI.start(ARGV)
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentLoop
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "agent_loop/version"
4
+ require_relative "agent_loop/runner"
5
+ require_relative "agent_loop/cli"
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: []