claude-task-master 0.2.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/CLAUDE.md +192 -0
- data/README.md +231 -0
- data/bin/claude-task-master +6 -0
- data/lib/claude_task_master/claude.rb +305 -0
- data/lib/claude_task_master/cli.rb +537 -0
- data/lib/claude_task_master/github.rb +345 -0
- data/lib/claude_task_master/loop.rb +250 -0
- data/lib/claude_task_master/pr_comment.rb +170 -0
- data/lib/claude_task_master/state.rb +183 -0
- data/lib/claude_task_master/version.rb +5 -0
- data/lib/claude_task_master.rb +17 -0
- metadata +141 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeTaskMaster
|
|
4
|
+
# Represents a GitHub PR review comment
|
|
5
|
+
# Detects CodeRabbit, Copilot, and other review bot comments
|
|
6
|
+
class PRComment
|
|
7
|
+
ACTIONABLE_SEVERITIES = %w[major critical warning].freeze
|
|
8
|
+
|
|
9
|
+
# Known bot authors
|
|
10
|
+
CODERABBIT_BOT = 'coderabbitai[bot]'
|
|
11
|
+
COPILOT_BOT = 'github-copilot[bot]'
|
|
12
|
+
KNOWN_BOTS = [CODERABBIT_BOT, COPILOT_BOT].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :id, :file_path, :line, :start_line, :body, :author,
|
|
15
|
+
:created_at, :updated_at, :html_url, :resolved
|
|
16
|
+
|
|
17
|
+
def initialize(attrs = {})
|
|
18
|
+
@id = attrs[:id] || attrs['id']
|
|
19
|
+
@file_path = attrs[:path] || attrs['path']
|
|
20
|
+
@line = attrs[:line] || attrs['line']
|
|
21
|
+
@start_line = attrs[:start_line] || attrs['start_line']
|
|
22
|
+
@body = attrs[:body] || attrs['body']
|
|
23
|
+
@author = extract_author(attrs[:user] || attrs['user'])
|
|
24
|
+
@created_at = attrs[:created_at] || attrs['created_at']
|
|
25
|
+
@updated_at = attrs[:updated_at] || attrs['updated_at']
|
|
26
|
+
@html_url = attrs[:html_url] || attrs['html_url']
|
|
27
|
+
@resolved = attrs[:resolved] || attrs['resolved']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create collection from API response
|
|
31
|
+
def self.from_api_response(data)
|
|
32
|
+
data = [data] unless data.is_a?(Array)
|
|
33
|
+
data.map { |item| new(item) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Line range as string (e.g., "40-42" or "42")
|
|
37
|
+
def line_range
|
|
38
|
+
@line_range ||= begin
|
|
39
|
+
start = start_line || line
|
|
40
|
+
start == line ? line.to_s : "#{start}-#{line}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parse severity from CodeRabbit/Copilot comment body
|
|
45
|
+
def severity
|
|
46
|
+
@severity ||= parse_severity
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract summary from comment body
|
|
50
|
+
def summary
|
|
51
|
+
@summary ||= extract_summary
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if comment requires attention
|
|
55
|
+
def actionable?
|
|
56
|
+
ACTIONABLE_SEVERITIES.include?(severity)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Bot detection
|
|
60
|
+
def from_coderabbit?
|
|
61
|
+
author == CODERABBIT_BOT
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def from_copilot?
|
|
65
|
+
author == COPILOT_BOT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def from_bot?
|
|
69
|
+
KNOWN_BOTS.include?(author) || author&.end_with?('[bot]')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def from_human?
|
|
73
|
+
!from_bot?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Resolved status
|
|
77
|
+
def resolved?
|
|
78
|
+
@resolved == true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def unresolved?
|
|
82
|
+
@resolved == false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if comment has committable suggestion
|
|
86
|
+
def has_suggestion?
|
|
87
|
+
body&.include?('```suggestion') || false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Extract suggestion code from body
|
|
91
|
+
def suggestion_code
|
|
92
|
+
@suggestion_code ||= begin
|
|
93
|
+
return nil unless body&.include?('```suggestion')
|
|
94
|
+
|
|
95
|
+
match = body.match(/```suggestion[^\n]*\n(.*?)\n```/m)
|
|
96
|
+
match ? match[1] : nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Serialize to hash
|
|
101
|
+
def to_h
|
|
102
|
+
{
|
|
103
|
+
id: id,
|
|
104
|
+
file_path: file_path,
|
|
105
|
+
line_range: line_range,
|
|
106
|
+
author: author,
|
|
107
|
+
severity: severity,
|
|
108
|
+
summary: summary,
|
|
109
|
+
actionable: actionable?,
|
|
110
|
+
from_bot: from_bot?,
|
|
111
|
+
resolved: resolved?,
|
|
112
|
+
has_suggestion: has_suggestion?,
|
|
113
|
+
html_url: html_url
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def extract_author(user)
|
|
120
|
+
return user if user.is_a?(String)
|
|
121
|
+
return nil unless user
|
|
122
|
+
|
|
123
|
+
user[:login] || user['login']
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_severity
|
|
127
|
+
return 'info' unless body
|
|
128
|
+
|
|
129
|
+
case body
|
|
130
|
+
when /❌.*Critical/m, /\*\*Critical\*\*/i
|
|
131
|
+
'critical'
|
|
132
|
+
when /⚠️.*Warning/m, /\*\*Warning\*\*/i
|
|
133
|
+
'warning'
|
|
134
|
+
when /🟠 Major/m, /\*\*Major\*\*/i
|
|
135
|
+
'major'
|
|
136
|
+
when /🔵 Trivial/m, /\*\*Trivial\*\*/i
|
|
137
|
+
'trivial'
|
|
138
|
+
when /🛠️ Refactor/m, /refactor suggestion/i
|
|
139
|
+
'refactor'
|
|
140
|
+
when /🧹 Nitpick/m, /nitpick/i
|
|
141
|
+
'nitpick'
|
|
142
|
+
when /suggestion:/i, /consider:/i
|
|
143
|
+
'suggestion'
|
|
144
|
+
else
|
|
145
|
+
'info'
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def extract_summary
|
|
150
|
+
return nil unless body
|
|
151
|
+
|
|
152
|
+
# Try to extract bolded summary line
|
|
153
|
+
match = body.match(/\*\*(.+?)\*\*/)
|
|
154
|
+
return match[1] if match
|
|
155
|
+
|
|
156
|
+
# Fallback to first non-empty line, skipping metadata
|
|
157
|
+
line = body.lines.reject { |l| l.strip.empty? || l.strip.start_with?('_', '<', '<!--') }.first
|
|
158
|
+
line&.strip&.truncate(100)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Monkey-patch String for truncate
|
|
164
|
+
class String
|
|
165
|
+
def truncate(max_length, omission: '...')
|
|
166
|
+
return self if length <= max_length
|
|
167
|
+
|
|
168
|
+
"#{self[0, max_length - omission.length]}#{omission}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module ClaudeTaskMaster
|
|
7
|
+
# Manages state persistence in .claude-task-master/
|
|
8
|
+
# All state is file-based for easy inspection and resumption
|
|
9
|
+
class State
|
|
10
|
+
GOAL_FILE = 'goal.txt'
|
|
11
|
+
CRITERIA_FILE = 'criteria.txt'
|
|
12
|
+
PLAN_FILE = 'plan.md'
|
|
13
|
+
STATE_FILE = 'state.json'
|
|
14
|
+
PROGRESS_FILE = 'progress.md'
|
|
15
|
+
CONTEXT_FILE = 'context.md'
|
|
16
|
+
LOGS_DIR = 'logs'
|
|
17
|
+
|
|
18
|
+
attr_reader :dir
|
|
19
|
+
|
|
20
|
+
def initialize(project_dir = Dir.pwd)
|
|
21
|
+
@dir = File.join(project_dir, STATE_DIR)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Initialize state directory for new project
|
|
25
|
+
def init(goal:, criteria:)
|
|
26
|
+
FileUtils.mkdir_p(@dir)
|
|
27
|
+
FileUtils.mkdir_p(logs_dir)
|
|
28
|
+
|
|
29
|
+
write_file(GOAL_FILE, goal)
|
|
30
|
+
write_file(CRITERIA_FILE, criteria)
|
|
31
|
+
write_file(PROGRESS_FILE, "# Progress\n\n_Started: #{Time.now.iso8601}_\n\n")
|
|
32
|
+
write_file(CONTEXT_FILE, "# Context\n\n_Learnings accumulated across sessions._\n\n")
|
|
33
|
+
|
|
34
|
+
save_state(
|
|
35
|
+
status: 'planning',
|
|
36
|
+
current_task: nil,
|
|
37
|
+
session_count: 0,
|
|
38
|
+
pr_number: nil,
|
|
39
|
+
started_at: Time.now.iso8601,
|
|
40
|
+
updated_at: Time.now.iso8601
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if state directory exists (for resume)
|
|
45
|
+
def exists?
|
|
46
|
+
File.directory?(@dir) && File.exist?(state_path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Load machine state
|
|
50
|
+
def load_state
|
|
51
|
+
return nil unless File.exist?(state_path)
|
|
52
|
+
|
|
53
|
+
JSON.parse(File.read(state_path), symbolize_names: true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Save machine state
|
|
57
|
+
def save_state(data)
|
|
58
|
+
data[:updated_at] = Time.now.iso8601
|
|
59
|
+
File.write(state_path, JSON.pretty_generate(data))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Update specific state fields
|
|
63
|
+
def update_state(**fields)
|
|
64
|
+
current = load_state || {}
|
|
65
|
+
save_state(current.merge(fields))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Read goal
|
|
69
|
+
def goal
|
|
70
|
+
read_file(GOAL_FILE)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Read success criteria
|
|
74
|
+
def criteria
|
|
75
|
+
read_file(CRITERIA_FILE)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Read plan
|
|
79
|
+
def plan
|
|
80
|
+
read_file(PLAN_FILE)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Write plan
|
|
84
|
+
def save_plan(content)
|
|
85
|
+
write_file(PLAN_FILE, content)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Read progress notes
|
|
89
|
+
def progress
|
|
90
|
+
read_file(PROGRESS_FILE)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Append to progress
|
|
94
|
+
def append_progress(content)
|
|
95
|
+
current = progress || ''
|
|
96
|
+
write_file(PROGRESS_FILE, "#{current}\n#{content}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Read accumulated context
|
|
100
|
+
def context
|
|
101
|
+
read_file(CONTEXT_FILE)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Append to context
|
|
105
|
+
def append_context(content)
|
|
106
|
+
current = context || ''
|
|
107
|
+
write_file(CONTEXT_FILE, "#{current}\n#{content}")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Log a session
|
|
111
|
+
def log_session(session_num, content)
|
|
112
|
+
filename = format('session-%03d.md', session_num)
|
|
113
|
+
File.write(File.join(logs_dir, filename), content)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get next session number
|
|
117
|
+
def next_session_number
|
|
118
|
+
existing = Dir.glob(File.join(logs_dir, 'session-*.md'))
|
|
119
|
+
existing.empty? ? 1 : existing.size + 1
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if success criteria met (Claude writes SUCCESS to state)
|
|
123
|
+
def success?
|
|
124
|
+
state = load_state
|
|
125
|
+
state && state[:status] == 'success'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if blocked
|
|
129
|
+
def blocked?
|
|
130
|
+
state = load_state
|
|
131
|
+
state && state[:status] == 'blocked'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Build context string for Claude
|
|
135
|
+
def build_context
|
|
136
|
+
state = load_state || {}
|
|
137
|
+
|
|
138
|
+
<<~CONTEXT
|
|
139
|
+
# Current State
|
|
140
|
+
|
|
141
|
+
## Goal
|
|
142
|
+
#{goal}
|
|
143
|
+
|
|
144
|
+
## Success Criteria
|
|
145
|
+
#{criteria}
|
|
146
|
+
|
|
147
|
+
## Status
|
|
148
|
+
- Phase: #{state[:status] || 'unknown'}
|
|
149
|
+
- Current task: #{state[:current_task] || 'none'}
|
|
150
|
+
- PR: #{state[:pr_number] ? "##{state[:pr_number]}" : 'none'}
|
|
151
|
+
- Session: #{state[:session_count] || 0}
|
|
152
|
+
|
|
153
|
+
## Plan
|
|
154
|
+
#{plan || '_No plan yet. Generate one first._'}
|
|
155
|
+
|
|
156
|
+
## Context from Previous Sessions
|
|
157
|
+
#{context || '_No context yet._'}
|
|
158
|
+
|
|
159
|
+
## Recent Progress
|
|
160
|
+
#{progress || '_No progress yet._'}
|
|
161
|
+
CONTEXT
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def state_path
|
|
167
|
+
File.join(@dir, STATE_FILE)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def logs_dir
|
|
171
|
+
File.join(@dir, LOGS_DIR)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def read_file(filename)
|
|
175
|
+
path = File.join(@dir, filename)
|
|
176
|
+
File.exist?(path) ? File.read(path) : nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def write_file(filename, content)
|
|
180
|
+
File.write(File.join(@dir, filename), content)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeTaskMaster
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class ConfigError < Error; end
|
|
6
|
+
class ClaudeError < Error; end
|
|
7
|
+
|
|
8
|
+
STATE_DIR = '.claude-task-master'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative 'claude_task_master/version'
|
|
12
|
+
require_relative 'claude_task_master/state'
|
|
13
|
+
require_relative 'claude_task_master/claude'
|
|
14
|
+
require_relative 'claude_task_master/pr_comment'
|
|
15
|
+
require_relative 'claude_task_master/github'
|
|
16
|
+
require_relative 'claude_task_master/loop'
|
|
17
|
+
require_relative 'claude_task_master/cli'
|
metadata
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: claude-task-master
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- developerz.ai
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: tty-spinner
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.9'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.9'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pastel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.8'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.8'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: octokit
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '10.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '10.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.12'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.60'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.60'
|
|
96
|
+
description: |
|
|
97
|
+
A lightweight harness that keeps Claude Code working autonomously until
|
|
98
|
+
success criteria are met. Supports any code review system (CodeRabbit,
|
|
99
|
+
GitHub Copilot, etc.) and any CI provider.
|
|
100
|
+
|
|
101
|
+
The loop: plan -> work -> check -> work -> check -> done
|
|
102
|
+
email:
|
|
103
|
+
- hello@developerz.ai
|
|
104
|
+
executables:
|
|
105
|
+
- claude-task-master
|
|
106
|
+
extensions: []
|
|
107
|
+
extra_rdoc_files: []
|
|
108
|
+
files:
|
|
109
|
+
- CLAUDE.md
|
|
110
|
+
- README.md
|
|
111
|
+
- bin/claude-task-master
|
|
112
|
+
- lib/claude_task_master.rb
|
|
113
|
+
- lib/claude_task_master/claude.rb
|
|
114
|
+
- lib/claude_task_master/cli.rb
|
|
115
|
+
- lib/claude_task_master/github.rb
|
|
116
|
+
- lib/claude_task_master/loop.rb
|
|
117
|
+
- lib/claude_task_master/pr_comment.rb
|
|
118
|
+
- lib/claude_task_master/state.rb
|
|
119
|
+
- lib/claude_task_master/version.rb
|
|
120
|
+
homepage: https://github.com/developerz-ai/claude-task-master
|
|
121
|
+
licenses:
|
|
122
|
+
- MIT
|
|
123
|
+
metadata: {}
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 3.1.0
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 3.6.9
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: Autonomous task loop for Claude Code
|
|
141
|
+
test_files: []
|