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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeTaskMaster
4
+ VERSION = '0.2.0'
5
+ 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: []