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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +47 -0
  3. data/lib/aidp/analyze/error_handler.rb +14 -15
  4. data/lib/aidp/analyze/runner.rb +27 -5
  5. data/lib/aidp/analyze/steps.rb +4 -0
  6. data/lib/aidp/cli/jobs_command.rb +2 -1
  7. data/lib/aidp/cli.rb +812 -3
  8. data/lib/aidp/concurrency/backoff.rb +148 -0
  9. data/lib/aidp/concurrency/exec.rb +192 -0
  10. data/lib/aidp/concurrency/wait.rb +148 -0
  11. data/lib/aidp/concurrency.rb +71 -0
  12. data/lib/aidp/config.rb +20 -0
  13. data/lib/aidp/daemon/runner.rb +9 -8
  14. data/lib/aidp/debug_mixin.rb +1 -0
  15. data/lib/aidp/errors.rb +12 -0
  16. data/lib/aidp/execute/interactive_repl.rb +102 -11
  17. data/lib/aidp/execute/repl_macros.rb +776 -2
  18. data/lib/aidp/execute/runner.rb +27 -5
  19. data/lib/aidp/execute/steps.rb +2 -0
  20. data/lib/aidp/harness/config_loader.rb +24 -2
  21. data/lib/aidp/harness/enhanced_runner.rb +16 -2
  22. data/lib/aidp/harness/error_handler.rb +1 -1
  23. data/lib/aidp/harness/provider_info.rb +19 -15
  24. data/lib/aidp/harness/provider_manager.rb +47 -41
  25. data/lib/aidp/harness/runner.rb +3 -11
  26. data/lib/aidp/harness/state/persistence.rb +1 -6
  27. data/lib/aidp/harness/state_manager.rb +115 -7
  28. data/lib/aidp/harness/status_display.rb +11 -18
  29. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  30. data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
  31. data/lib/aidp/harness/user_interface.rb +12 -15
  32. data/lib/aidp/jobs/background_runner.rb +15 -5
  33. data/lib/aidp/providers/codex.rb +0 -1
  34. data/lib/aidp/providers/cursor.rb +0 -1
  35. data/lib/aidp/providers/github_copilot.rb +0 -1
  36. data/lib/aidp/providers/opencode.rb +0 -1
  37. data/lib/aidp/skills/composer.rb +178 -0
  38. data/lib/aidp/skills/loader.rb +205 -0
  39. data/lib/aidp/skills/registry.rb +220 -0
  40. data/lib/aidp/skills/skill.rb +174 -0
  41. data/lib/aidp/skills.rb +30 -0
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +93 -28
  44. data/lib/aidp/watch/runner.rb +3 -2
  45. data/lib/aidp/workstream_executor.rb +244 -0
  46. data/lib/aidp/workstream_state.rb +212 -0
  47. data/lib/aidp/worktree.rb +208 -0
  48. data/lib/aidp.rb +6 -0
  49. 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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.15.2"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -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
- checkout_branch(base_branch, branch_name)
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 branch_name_for(issue)
110
+ def workstream_slug_for(issue)
100
111
  slug = issue[:title].to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
101
- "aidp/issue-#{issue[:number]}-#{slug[0, 32]}"
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(@project_dir)
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(@project_dir, :execute, options)
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. The branch has been left intact for debugging.
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(@project_dir) do
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(@project_dir) do
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 executed successfully."
325
+ "- Fix-forward harness extracted successfully."
261
326
  end
262
327
 
263
328
  def extract_pr_url(output)
@@ -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