aidp 0.15.2 โ 0.16.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 +4 -4
- data/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +14 -15
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +812 -3
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +20 -0
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/repl_macros.rb +776 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +19 -15
- data/lib/aidp/harness/provider_manager.rb +47 -41
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/jobs/background_runner.rb +15 -5
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +220 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- metadata +17 -4
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Skills
|
5
|
+
# Registry for managing available skills
|
6
|
+
#
|
7
|
+
# The Registry loads skills from multiple search paths and provides
|
8
|
+
# lookup, filtering, and management capabilities.
|
9
|
+
#
|
10
|
+
# Skills are loaded from:
|
11
|
+
# 1. Built-in skills directory (project_root/skills/)
|
12
|
+
# 2. Custom skills directory (.aidp/skills/)
|
13
|
+
#
|
14
|
+
# Custom skills with matching IDs override built-in skills.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# registry = Registry.new(project_dir: "/path/to/project")
|
18
|
+
# registry.load_skills
|
19
|
+
# skill = registry.find("repository_analyst")
|
20
|
+
#
|
21
|
+
# @example With provider filtering
|
22
|
+
# registry = Registry.new(project_dir: "/path/to/project", provider: "anthropic")
|
23
|
+
# registry.load_skills
|
24
|
+
# skills = registry.all # Only anthropic-compatible skills
|
25
|
+
class Registry
|
26
|
+
attr_reader :project_dir, :provider
|
27
|
+
|
28
|
+
# Initialize a new skills registry
|
29
|
+
#
|
30
|
+
# @param project_dir [String] Root directory of the project
|
31
|
+
# @param provider [String, nil] Optional provider name for filtering
|
32
|
+
def initialize(project_dir:, provider: nil)
|
33
|
+
@project_dir = project_dir
|
34
|
+
@provider = provider
|
35
|
+
@skills = {}
|
36
|
+
@loaded = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# Load skills from all search paths
|
40
|
+
#
|
41
|
+
# Skills are loaded in order:
|
42
|
+
# 1. Built-in skills (project_root/skills/)
|
43
|
+
# 2. Custom skills (.aidp/skills/) - override built-in
|
44
|
+
#
|
45
|
+
# @return [Integer] Number of skills loaded
|
46
|
+
def load_skills
|
47
|
+
Aidp.log_debug("skills", "Loading skills", project_dir: project_dir, provider: provider)
|
48
|
+
|
49
|
+
@skills = {}
|
50
|
+
|
51
|
+
# Load built-in skills first
|
52
|
+
builtin_skills = load_from_path(builtin_skills_path)
|
53
|
+
builtin_skills.each { |skill| register_skill(skill, source: :builtin) }
|
54
|
+
|
55
|
+
# Load custom skills (override built-in if IDs match)
|
56
|
+
custom_skills = load_from_path(custom_skills_path)
|
57
|
+
custom_skills.each { |skill| register_skill(skill, source: :custom) }
|
58
|
+
|
59
|
+
@loaded = true
|
60
|
+
|
61
|
+
Aidp.log_info("skills", "Loaded skills", count: @skills.size, provider: provider)
|
62
|
+
@skills.size
|
63
|
+
end
|
64
|
+
|
65
|
+
# Find a skill by ID
|
66
|
+
#
|
67
|
+
# @param skill_id [String] Skill identifier
|
68
|
+
# @return [Skill, nil] Skill if found, nil otherwise
|
69
|
+
def find(skill_id)
|
70
|
+
load_skills unless loaded?
|
71
|
+
@skills[skill_id.to_s]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get all registered skills
|
75
|
+
#
|
76
|
+
# @return [Array<Skill>] Array of all skills
|
77
|
+
def all
|
78
|
+
load_skills unless loaded?
|
79
|
+
@skills.values
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get skills matching a search query
|
83
|
+
#
|
84
|
+
# @param query [String] Search query (searches id, name, description, keywords, expertise)
|
85
|
+
# @return [Array<Skill>] Matching skills
|
86
|
+
def search(query)
|
87
|
+
load_skills unless loaded?
|
88
|
+
all.select { |skill| skill.matches?(query) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get skills by keyword
|
92
|
+
#
|
93
|
+
# @param keyword [String] Keyword to match
|
94
|
+
# @return [Array<Skill>] Skills with matching keyword
|
95
|
+
def by_keyword(keyword)
|
96
|
+
load_skills unless loaded?
|
97
|
+
all.select { |skill| skill.keywords.include?(keyword) }
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get skills compatible with a specific provider
|
101
|
+
#
|
102
|
+
# @param provider_name [String] Provider name
|
103
|
+
# @return [Array<Skill>] Compatible skills
|
104
|
+
def compatible_with(provider_name)
|
105
|
+
load_skills unless loaded?
|
106
|
+
all.select { |skill| skill.compatible_with?(provider_name) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Check if a skill exists
|
110
|
+
#
|
111
|
+
# @param skill_id [String] Skill identifier
|
112
|
+
# @return [Boolean] True if skill exists
|
113
|
+
def exists?(skill_id)
|
114
|
+
find(skill_id) != nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if skills have been loaded
|
118
|
+
#
|
119
|
+
# @return [Boolean] True if loaded
|
120
|
+
def loaded?
|
121
|
+
@loaded
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get count of registered skills
|
125
|
+
#
|
126
|
+
# @return [Integer] Number of skills
|
127
|
+
def count
|
128
|
+
load_skills unless loaded?
|
129
|
+
@skills.size
|
130
|
+
end
|
131
|
+
|
132
|
+
# Reload skills from disk
|
133
|
+
#
|
134
|
+
# @return [Integer] Number of skills loaded
|
135
|
+
def reload
|
136
|
+
@loaded = false
|
137
|
+
load_skills
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get skill IDs grouped by source
|
141
|
+
#
|
142
|
+
# @return [Hash] Hash with :builtin and :custom arrays
|
143
|
+
def by_source
|
144
|
+
load_skills unless loaded?
|
145
|
+
|
146
|
+
{
|
147
|
+
builtin: @skills.values.select { |s| builtin_skill?(s) }.map(&:id),
|
148
|
+
custom: @skills.values.select { |s| custom_skill?(s) }.map(&:id)
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Register a skill in the registry
|
155
|
+
#
|
156
|
+
# @param skill [Skill] Skill to register
|
157
|
+
# @param source [Symbol] Source type (:builtin or :custom)
|
158
|
+
def register_skill(skill, source:)
|
159
|
+
if @skills.key?(skill.id)
|
160
|
+
Aidp.log_debug(
|
161
|
+
"skills",
|
162
|
+
"Overriding skill",
|
163
|
+
skill_id: skill.id,
|
164
|
+
old_source: @skills[skill.id].source_path,
|
165
|
+
new_source: skill.source_path
|
166
|
+
)
|
167
|
+
end
|
168
|
+
|
169
|
+
@skills[skill.id] = skill
|
170
|
+
|
171
|
+
Aidp.log_debug(
|
172
|
+
"skills",
|
173
|
+
"Registered skill",
|
174
|
+
skill_id: skill.id,
|
175
|
+
source: source,
|
176
|
+
version: skill.version
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Load skills from a directory path
|
181
|
+
#
|
182
|
+
# @param path [String] Directory path
|
183
|
+
# @return [Array<Skill>] Loaded skills
|
184
|
+
def load_from_path(path)
|
185
|
+
return [] unless Dir.exist?(path)
|
186
|
+
Loader.load_from_directory(path, provider: provider)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Get built-in skills path
|
190
|
+
#
|
191
|
+
# @return [String] Path to built-in skills directory
|
192
|
+
def builtin_skills_path
|
193
|
+
File.join(project_dir, "skills")
|
194
|
+
end
|
195
|
+
|
196
|
+
# Get custom skills path
|
197
|
+
#
|
198
|
+
# @return [String] Path to custom skills directory
|
199
|
+
def custom_skills_path
|
200
|
+
File.join(project_dir, ".aidp", "skills")
|
201
|
+
end
|
202
|
+
|
203
|
+
# Check if skill is from built-in directory
|
204
|
+
#
|
205
|
+
# @param skill [Skill] Skill to check
|
206
|
+
# @return [Boolean] True if built-in
|
207
|
+
def builtin_skill?(skill)
|
208
|
+
skill.source_path.start_with?(builtin_skills_path)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Check if skill is from custom directory
|
212
|
+
#
|
213
|
+
# @param skill [Skill] Skill to check
|
214
|
+
# @return [Boolean] True if custom
|
215
|
+
def custom_skill?(skill)
|
216
|
+
skill.source_path.start_with?(custom_skills_path)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../errors"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Skills
|
7
|
+
# Represents a skill/persona with metadata and content
|
8
|
+
#
|
9
|
+
# A Skill encapsulates an agent's persona, expertise, and capabilities.
|
10
|
+
# Skills are loaded from SKILL.md files with YAML frontmatter.
|
11
|
+
#
|
12
|
+
# @example Creating a skill
|
13
|
+
# skill = Skill.new(
|
14
|
+
# id: "repository_analyst",
|
15
|
+
# name: "Repository Analyst",
|
16
|
+
# description: "Expert in version control analysis",
|
17
|
+
# version: "1.0.0",
|
18
|
+
# expertise: ["git analysis", "code metrics"],
|
19
|
+
# keywords: ["git", "metrics"],
|
20
|
+
# when_to_use: ["Analyzing repository history"],
|
21
|
+
# when_not_to_use: ["Writing new code"],
|
22
|
+
# compatible_providers: ["anthropic", "openai"],
|
23
|
+
# content: "You are a Repository Analyst...",
|
24
|
+
# source_path: "/path/to/SKILL.md"
|
25
|
+
# )
|
26
|
+
class Skill
|
27
|
+
attr_reader :id, :name, :description, :version, :expertise, :keywords,
|
28
|
+
:when_to_use, :when_not_to_use, :compatible_providers,
|
29
|
+
:content, :source_path
|
30
|
+
|
31
|
+
# Initialize a new Skill
|
32
|
+
#
|
33
|
+
# @param id [String] Unique identifier for the skill
|
34
|
+
# @param name [String] Human-readable name
|
35
|
+
# @param description [String] Brief one-line description
|
36
|
+
# @param version [String] Semantic version (e.g., "1.0.0")
|
37
|
+
# @param expertise [Array<String>] List of expertise areas
|
38
|
+
# @param keywords [Array<String>] Search/filter keywords
|
39
|
+
# @param when_to_use [Array<String>] Guidance for when to use this skill
|
40
|
+
# @param when_not_to_use [Array<String>] Guidance for when NOT to use
|
41
|
+
# @param compatible_providers [Array<String>] Compatible provider names
|
42
|
+
# @param content [String] The skill content (markdown)
|
43
|
+
# @param source_path [String] Path to source SKILL.md file
|
44
|
+
def initialize(
|
45
|
+
id:,
|
46
|
+
name:,
|
47
|
+
description:,
|
48
|
+
version:,
|
49
|
+
content:, source_path:, expertise: [],
|
50
|
+
keywords: [],
|
51
|
+
when_to_use: [],
|
52
|
+
when_not_to_use: [],
|
53
|
+
compatible_providers: []
|
54
|
+
)
|
55
|
+
@id = id
|
56
|
+
@name = name
|
57
|
+
@description = description
|
58
|
+
@version = version
|
59
|
+
@expertise = Array(expertise)
|
60
|
+
@keywords = Array(keywords)
|
61
|
+
@when_to_use = Array(when_to_use)
|
62
|
+
@when_not_to_use = Array(when_not_to_use)
|
63
|
+
@compatible_providers = Array(compatible_providers)
|
64
|
+
@content = content
|
65
|
+
@source_path = source_path
|
66
|
+
|
67
|
+
validate!
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check if this skill is compatible with a given provider
|
71
|
+
#
|
72
|
+
# @param provider_name [String] Provider name (e.g., "anthropic")
|
73
|
+
# @return [Boolean] True if compatible or no restrictions defined
|
74
|
+
def compatible_with?(provider_name)
|
75
|
+
return true if compatible_providers.empty?
|
76
|
+
compatible_providers.include?(provider_name.to_s.downcase)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check if this skill matches a search query
|
80
|
+
#
|
81
|
+
# Searches across: id, name, description, expertise, keywords
|
82
|
+
#
|
83
|
+
# @param query [String] Search query (case-insensitive)
|
84
|
+
# @return [Boolean] True if skill matches the query
|
85
|
+
def matches?(query)
|
86
|
+
return true if query.nil? || query.strip.empty?
|
87
|
+
|
88
|
+
query_lower = query.downcase
|
89
|
+
searchable_text = [
|
90
|
+
id,
|
91
|
+
name,
|
92
|
+
description,
|
93
|
+
expertise,
|
94
|
+
keywords
|
95
|
+
].flatten.join(" ").downcase
|
96
|
+
|
97
|
+
searchable_text.include?(query_lower)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get a summary of this skill for display
|
101
|
+
#
|
102
|
+
# @return [Hash] Summary with key skill metadata
|
103
|
+
def summary
|
104
|
+
{
|
105
|
+
id: id,
|
106
|
+
name: name,
|
107
|
+
description: description,
|
108
|
+
version: version,
|
109
|
+
expertise_areas: expertise.size,
|
110
|
+
keywords: keywords,
|
111
|
+
providers: compatible_providers.empty? ? "all" : compatible_providers.join(", ")
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
# Get full details of this skill for display
|
116
|
+
#
|
117
|
+
# @return [Hash] Detailed skill information
|
118
|
+
def details
|
119
|
+
{
|
120
|
+
id: id,
|
121
|
+
name: name,
|
122
|
+
description: description,
|
123
|
+
version: version,
|
124
|
+
expertise: expertise,
|
125
|
+
keywords: keywords,
|
126
|
+
when_to_use: when_to_use,
|
127
|
+
when_not_to_use: when_not_to_use,
|
128
|
+
compatible_providers: compatible_providers,
|
129
|
+
source: source_path,
|
130
|
+
content_length: content.length
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Return string representation
|
135
|
+
#
|
136
|
+
# @return [String] Skill representation
|
137
|
+
def to_s
|
138
|
+
"Skill[#{id}](#{name} v#{version})"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return inspection string
|
142
|
+
#
|
143
|
+
# @return [String] Detailed inspection
|
144
|
+
def inspect
|
145
|
+
"#<Aidp::Skills::Skill id=#{id} name=\"#{name}\" version=#{version} " \
|
146
|
+
"source=#{source_path}>"
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Validate required fields
|
152
|
+
#
|
153
|
+
# @raise [Aidp::Errors::ValidationError] if validation fails
|
154
|
+
def validate!
|
155
|
+
raise Aidp::Errors::ValidationError, "Skill id is required" if id.nil? || id.strip.empty?
|
156
|
+
raise Aidp::Errors::ValidationError, "Skill name is required" if name.nil? || name.strip.empty?
|
157
|
+
raise Aidp::Errors::ValidationError, "Skill description is required" if description.nil? || description.strip.empty?
|
158
|
+
raise Aidp::Errors::ValidationError, "Skill version is required" if version.nil? || version.strip.empty?
|
159
|
+
raise Aidp::Errors::ValidationError, "Skill content is required" if content.nil? || content.strip.empty?
|
160
|
+
raise Aidp::Errors::ValidationError, "Skill source_path is required" if source_path.nil? || source_path.strip.empty?
|
161
|
+
|
162
|
+
# Validate version format (simple semantic version check)
|
163
|
+
unless version.match?(/^\d+\.\d+\.\d+/)
|
164
|
+
raise Aidp::Errors::ValidationError, "Skill version must be in format X.Y.Z (e.g., 1.0.0)"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Validate id format (lowercase, alphanumeric, underscores only)
|
168
|
+
unless id.match?(/^[a-z0-9_]+$/)
|
169
|
+
raise Aidp::Errors::ValidationError, "Skill id must be lowercase alphanumeric with underscores only"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/aidp/skills.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "skills/skill"
|
4
|
+
require_relative "skills/loader"
|
5
|
+
require_relative "skills/registry"
|
6
|
+
require_relative "skills/composer"
|
7
|
+
|
8
|
+
module Aidp
|
9
|
+
# Skills subsystem for managing agent personas and capabilities
|
10
|
+
#
|
11
|
+
# Skills define WHO the agent is (persona) and WHAT capabilities they have.
|
12
|
+
# This is separate from templates/procedures which define WHEN and HOW
|
13
|
+
# to execute specific tasks.
|
14
|
+
#
|
15
|
+
# @example Loading and using skills
|
16
|
+
# registry = Aidp::Skills::Registry.new(project_dir: Dir.pwd)
|
17
|
+
# registry.load_skills
|
18
|
+
#
|
19
|
+
# skill = registry.find("repository_analyst")
|
20
|
+
# composer = Aidp::Skills::Composer.new
|
21
|
+
# prompt = composer.compose(skill: skill, template: "Analyze the repo...")
|
22
|
+
#
|
23
|
+
# @example Creating a custom skill
|
24
|
+
# # Create .aidp/skills/my_skill/SKILL.md with YAML frontmatter
|
25
|
+
# # It will automatically override built-in skills with matching ID
|
26
|
+
module Skills
|
27
|
+
# Error raised when a skill is not found
|
28
|
+
class SkillNotFoundError < StandardError; end
|
29
|
+
end
|
30
|
+
end
|
data/lib/aidp/version.rb
CHANGED
@@ -6,6 +6,7 @@ require "time"
|
|
6
6
|
require_relative "../message_display"
|
7
7
|
require_relative "../execute/prompt_manager"
|
8
8
|
require_relative "../harness/runner"
|
9
|
+
require_relative "../worktree"
|
9
10
|
|
10
11
|
module Aidp
|
11
12
|
module Watch
|
@@ -17,10 +18,11 @@ module Aidp
|
|
17
18
|
BUILD_LABEL = "aidp-build"
|
18
19
|
IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
|
19
20
|
|
20
|
-
def initialize(repository_client:, state_store:, project_dir: Dir.pwd)
|
21
|
+
def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true)
|
21
22
|
@repository_client = repository_client
|
22
23
|
@state_store = state_store
|
23
24
|
@project_dir = project_dir
|
25
|
+
@use_workstreams = use_workstreams
|
24
26
|
end
|
25
27
|
|
26
28
|
def process(issue)
|
@@ -30,27 +32,36 @@ module Aidp
|
|
30
32
|
plan_data = ensure_plan_data(number)
|
31
33
|
return unless plan_data
|
32
34
|
|
35
|
+
slug = workstream_slug_for(issue)
|
33
36
|
branch_name = branch_name_for(issue)
|
34
|
-
@state_store.record_build_status(number, status: "running", details: {branch: branch_name, started_at: Time.now.utc.iso8601})
|
37
|
+
@state_store.record_build_status(number, status: "running", details: {branch: branch_name, workstream: slug, started_at: Time.now.utc.iso8601})
|
35
38
|
|
36
39
|
ensure_git_repo!
|
37
40
|
base_branch = detect_base_branch
|
38
41
|
|
39
|
-
|
42
|
+
if @use_workstreams
|
43
|
+
workstream_path = setup_workstream(slug: slug, branch_name: branch_name, base_branch: base_branch)
|
44
|
+
working_dir = workstream_path
|
45
|
+
else
|
46
|
+
checkout_branch(base_branch, branch_name)
|
47
|
+
working_dir = @project_dir
|
48
|
+
end
|
49
|
+
|
40
50
|
prompt_content = build_prompt(issue: issue, plan_data: plan_data)
|
41
|
-
write_prompt(prompt_content)
|
51
|
+
write_prompt(prompt_content, working_dir: working_dir)
|
42
52
|
|
43
53
|
user_input = build_user_input(issue: issue, plan_data: plan_data)
|
44
|
-
result = run_harness(user_input: user_input)
|
54
|
+
result = run_harness(user_input: user_input, working_dir: working_dir)
|
45
55
|
|
46
56
|
if result[:status] == "completed"
|
47
|
-
handle_success(issue: issue, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data)
|
57
|
+
handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
|
48
58
|
else
|
49
|
-
handle_failure(issue: issue, result: result)
|
59
|
+
handle_failure(issue: issue, slug: slug, result: result)
|
50
60
|
end
|
51
61
|
rescue => e
|
52
62
|
display_message("โ Implementation failed: #{e.message}", type: :error)
|
53
63
|
@state_store.record_build_status(issue[:number], status: "failed", details: {error: e.message})
|
64
|
+
cleanup_workstream(slug) if @use_workstreams && slug
|
54
65
|
raise
|
55
66
|
end
|
56
67
|
|
@@ -96,9 +107,56 @@ module Aidp
|
|
96
107
|
display_message("๐ฟ Checked out #{branch_name}", type: :info)
|
97
108
|
end
|
98
109
|
|
99
|
-
def
|
110
|
+
def workstream_slug_for(issue)
|
100
111
|
slug = issue[:title].to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
101
|
-
"
|
112
|
+
"issue-#{issue[:number]}-#{slug[0, 32]}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def branch_name_for(issue)
|
116
|
+
"aidp/#{workstream_slug_for(issue)}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def setup_workstream(slug:, branch_name:, base_branch:)
|
120
|
+
# Check if workstream already exists
|
121
|
+
existing = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
|
122
|
+
if existing
|
123
|
+
display_message("๐ Reusing existing workstream: #{slug}", type: :info)
|
124
|
+
Dir.chdir(existing[:path]) do
|
125
|
+
run_git(["checkout", existing[:branch]])
|
126
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
127
|
+
end
|
128
|
+
return existing[:path]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Create new workstream
|
132
|
+
display_message("๐ฟ Creating workstream: #{slug}", type: :info)
|
133
|
+
result = Aidp::Worktree.create(
|
134
|
+
slug: slug,
|
135
|
+
project_dir: @project_dir,
|
136
|
+
branch: branch_name,
|
137
|
+
base_branch: base_branch
|
138
|
+
)
|
139
|
+
|
140
|
+
if result[:success]
|
141
|
+
display_message("โ
Workstream created at #{result[:path]}", type: :success)
|
142
|
+
result[:path]
|
143
|
+
else
|
144
|
+
raise "Failed to create workstream: #{result[:message]}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def cleanup_workstream(slug)
|
149
|
+
return unless slug
|
150
|
+
|
151
|
+
display_message("๐งน Cleaning up workstream: #{slug}", type: :info)
|
152
|
+
result = Aidp::Worktree.remove(slug: slug, project_dir: @project_dir, force: true)
|
153
|
+
if result[:success]
|
154
|
+
display_message("โ
Workstream removed", type: :success)
|
155
|
+
else
|
156
|
+
display_message("โ ๏ธ Failed to remove workstream: #{result[:message]}", type: :warn)
|
157
|
+
end
|
158
|
+
rescue => e
|
159
|
+
display_message("โ ๏ธ Error cleaning up workstream: #{e.message}", type: :warn)
|
102
160
|
end
|
103
161
|
|
104
162
|
def build_prompt(issue:, plan_data:)
|
@@ -141,8 +199,8 @@ module Aidp
|
|
141
199
|
"_Unable to parse comment thread._"
|
142
200
|
end
|
143
201
|
|
144
|
-
def write_prompt(content)
|
145
|
-
prompt_manager = Aidp::Execute::PromptManager.new(
|
202
|
+
def write_prompt(content, working_dir: @project_dir)
|
203
|
+
prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
|
146
204
|
prompt_manager.write(content)
|
147
205
|
display_message("๐ Wrote PROMPT.md with implementation contract", type: :info)
|
148
206
|
end
|
@@ -156,23 +214,24 @@ module Aidp
|
|
156
214
|
}.delete_if { |_k, v| v.nil? || v.empty? }
|
157
215
|
end
|
158
216
|
|
159
|
-
def run_harness(user_input:)
|
217
|
+
def run_harness(user_input:, working_dir: @project_dir)
|
160
218
|
options = {
|
161
219
|
selected_steps: [IMPLEMENTATION_STEP],
|
162
220
|
workflow_type: :watch_mode,
|
163
221
|
user_input: user_input
|
164
222
|
}
|
165
|
-
runner = Aidp::Harness::Runner.new(
|
223
|
+
runner = Aidp::Harness::Runner.new(working_dir, :execute, options)
|
166
224
|
runner.run
|
167
225
|
end
|
168
226
|
|
169
|
-
def handle_success(issue:, branch_name:, base_branch:, plan_data:)
|
170
|
-
stage_and_commit(issue)
|
171
|
-
pr_url = create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch)
|
227
|
+
def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
|
228
|
+
stage_and_commit(issue, working_dir: working_dir)
|
229
|
+
pr_url = create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
|
172
230
|
|
231
|
+
workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
|
173
232
|
comment = <<~COMMENT
|
174
233
|
โ
Implementation complete for ##{issue[:number]}.
|
175
|
-
- Branch: `#{branch_name}
|
234
|
+
- Branch: `#{branch_name}`#{workstream_note}
|
176
235
|
- Pull Request: #{pr_url}
|
177
236
|
|
178
237
|
Summary:
|
@@ -183,32 +242,38 @@ module Aidp
|
|
183
242
|
@state_store.record_build_status(
|
184
243
|
issue[:number],
|
185
244
|
status: "completed",
|
186
|
-
details: {branch: branch_name, pr_url: pr_url}
|
245
|
+
details: {branch: branch_name, workstream: slug, pr_url: pr_url}
|
187
246
|
)
|
188
247
|
display_message("๐ Posted completion comment for issue ##{issue[:number]}", type: :success)
|
248
|
+
|
249
|
+
# Keep workstream for review - don't auto-cleanup on success
|
250
|
+
if @use_workstreams
|
251
|
+
display_message("โน๏ธ Workstream #{slug} preserved for review. Remove with: aidp ws rm #{slug}", type: :muted)
|
252
|
+
end
|
189
253
|
end
|
190
254
|
|
191
|
-
def handle_failure(issue:, result:)
|
255
|
+
def handle_failure(issue:, slug:, result:)
|
192
256
|
message = result[:message] || "Unknown failure"
|
257
|
+
workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
|
193
258
|
comment = <<~COMMENT
|
194
259
|
โ Implementation attempt for ##{issue[:number]} failed.
|
195
260
|
|
196
261
|
Status: #{result[:status]}
|
197
262
|
Details: #{message}
|
198
263
|
|
199
|
-
Please review the repository for partial changes
|
264
|
+
Please review the repository for partial changes.#{workstream_note}
|
200
265
|
COMMENT
|
201
266
|
@repository_client.post_comment(issue[:number], comment)
|
202
267
|
@state_store.record_build_status(
|
203
268
|
issue[:number],
|
204
269
|
status: "failed",
|
205
|
-
details: {message: message}
|
270
|
+
details: {message: message, workstream: slug}
|
206
271
|
)
|
207
272
|
display_message("โ ๏ธ Build failure recorded for issue ##{issue[:number]}", type: :warn)
|
208
273
|
end
|
209
274
|
|
210
|
-
def stage_and_commit(issue)
|
211
|
-
Dir.chdir(
|
275
|
+
def stage_and_commit(issue, working_dir: @project_dir)
|
276
|
+
Dir.chdir(working_dir) do
|
212
277
|
status_output = run_git(%w[status --porcelain])
|
213
278
|
if status_output.strip.empty?
|
214
279
|
display_message("โน๏ธ No file changes detected after work loop.", type: :muted)
|
@@ -222,9 +287,9 @@ module Aidp
|
|
222
287
|
end
|
223
288
|
end
|
224
289
|
|
225
|
-
def create_pull_request(issue:, branch_name:, base_branch:)
|
290
|
+
def create_pull_request(issue:, branch_name:, base_branch:, working_dir: @project_dir)
|
226
291
|
title = "aidp: Resolve ##{issue[:number]} - #{issue[:title]}"
|
227
|
-
test_summary = gather_test_summary
|
292
|
+
test_summary = gather_test_summary(working_dir: working_dir)
|
228
293
|
body = <<~BODY
|
229
294
|
## Summary
|
230
295
|
- Automated resolution for ##{issue[:number]}
|
@@ -244,8 +309,8 @@ module Aidp
|
|
244
309
|
extract_pr_url(output)
|
245
310
|
end
|
246
311
|
|
247
|
-
def gather_test_summary
|
248
|
-
Dir.chdir(
|
312
|
+
def gather_test_summary(working_dir: @project_dir)
|
313
|
+
Dir.chdir(working_dir) do
|
249
314
|
log_path = File.join(".aidp", "logs", "test_runner.log")
|
250
315
|
return "- Fix-forward harness executed; refer to #{log_path}" unless File.exist?(log_path)
|
251
316
|
|
@@ -257,7 +322,7 @@ module Aidp
|
|
257
322
|
end
|
258
323
|
end
|
259
324
|
rescue
|
260
|
-
"- Fix-forward harness
|
325
|
+
"- Fix-forward harness extracted successfully."
|
261
326
|
end
|
262
327
|
|
263
328
|
def extract_pr_url(output)
|
data/lib/aidp/watch/runner.rb
CHANGED
@@ -18,7 +18,7 @@ module Aidp
|
|
18
18
|
|
19
19
|
DEFAULT_INTERVAL = 30
|
20
20
|
|
21
|
-
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, prompt: TTY::Prompt.new)
|
21
|
+
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new)
|
22
22
|
@prompt = prompt
|
23
23
|
@interval = interval
|
24
24
|
@once = once
|
@@ -35,7 +35,8 @@ module Aidp
|
|
35
35
|
@build_processor = BuildProcessor.new(
|
36
36
|
repository_client: @repository_client,
|
37
37
|
state_store: @state_store,
|
38
|
-
project_dir: project_dir
|
38
|
+
project_dir: project_dir,
|
39
|
+
use_workstreams: use_workstreams
|
39
40
|
)
|
40
41
|
end
|
41
42
|
|