aidp 0.22.0 → 0.23.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e07daf05fddb6301340e9a01b1c6d8eea60ddcf53b2ec2d56e914be610cd67a7
4
- data.tar.gz: 21f26ed81af8f996cbb50c4a4dfe7603230f29499399c134c7a83492042a39e2
3
+ metadata.gz: 1dedfe324bb5255c43e42a4ac3f716bb99f9db453ae436a1e2aa57b68754a28f
4
+ data.tar.gz: 24b5a225e474c14dabad326455e4d5286e175dc6a929a1bffab4ffb29c1c063c
5
5
  SHA512:
6
- metadata.gz: 45a1c8c35dbf2f3f672f1990d652c238dd050b3dae3a96edf4d69960475a9e9ee27f1f4ced5ed4a6b0639de6256f56c78d25931e08adacf05ed2aa444e23c9fe
7
- data.tar.gz: 85560929fa2c4a1b51a862e8c541ed92d85e51a3c025eede695a5211c7b30b0a2115c098969fbfbb3a7e4237d5d0c074d194f0df0c50b7cdb025a75e7fc9249a
6
+ metadata.gz: b842c15a0169d3b3c7361cbdf633b5bb66d0189f40fb16e12099436b3db7eda3e983fb624326b2160dd018d63090feae2b92042f46758990c31cef9172252d36
7
+ data.tar.gz: 1c614a680d74d56fff4561a62eee471ab1d8f7b8b2cc801786da25a63c12b142948f8e418477a4b479b3b119abe2275b954d9202662f7acbb36cdd82401a68eb
data/README.md CHANGED
@@ -48,8 +48,8 @@ AIDP provides first-class devcontainer support for sandboxed, secure AI agent ex
48
48
 
49
49
  - **Network Security**: Strict firewall with allowlisted domains only
50
50
  - **Sandboxed Environment**: Isolated from your host system
51
- - **Elevated Permissions**: AI agents can run with full permissions inside the container
52
51
  - **Consistent Setup**: Same environment across all developers
52
+ - **Automatic Management**: AIDP can generate and update your devcontainer configuration
53
53
 
54
54
  ### For AIDP Development
55
55
 
@@ -73,43 +73,54 @@ See [.devcontainer/README.md](.devcontainer/README.md) for complete documentatio
73
73
 
74
74
  ### Generating Devcontainers for Your Projects
75
75
 
76
- Use `aidp init` to generate a devcontainer for any project:
76
+ AIDP can automatically generate and manage devcontainer configurations through the interactive wizard:
77
77
 
78
78
  ```bash
79
- # Initialize project with devcontainer
80
- aidp init
79
+ # Launch the interactive configuration wizard
80
+ aidp config --interactive
81
81
 
82
- # When prompted:
83
- # "Generate devcontainer configuration for sandboxed development?" Yes
82
+ # During the wizard, you'll be asked:
83
+ # - Whether you want AIDP to manage your devcontainer configuration
84
+ # - If you want to add custom ports beyond auto-detected ones
84
85
 
85
- # Or use the flag directly
86
- aidp init --with-devcontainer
86
+ # The wizard will detect ports based on your project type and generate
87
+ # a complete devcontainer.json configuration
87
88
  ```
88
89
 
89
- This creates:
90
+ You can also manage devcontainer configuration manually:
91
+
92
+ ```yaml
93
+ # .aidp/aidp.yml
94
+ devcontainer:
95
+ manage: true
96
+ custom_ports:
97
+ - number: 3000
98
+ label: "Application Server"
99
+ - number: 5432
100
+ label: "PostgreSQL"
101
+ ```
90
102
 
91
- - `.devcontainer/Dockerfile` - Customized for your project's language/framework
92
- - `.devcontainer/devcontainer.json` - VS Code configuration and extensions
93
- - `.devcontainer/init-firewall.sh` - Network security rules
94
- - `.devcontainer/README.md` - Setup and usage documentation
103
+ Then apply the configuration:
95
104
 
96
- ### Elevated Permissions in Devcontainers
105
+ ```bash
106
+ # Preview changes
107
+ aidp devcontainer diff
97
108
 
98
- When running inside a devcontainer, you can enable elevated permissions for AI agents:
109
+ # Apply configuration
110
+ aidp devcontainer apply
99
111
 
100
- ```yaml
101
- # aidp.yml
102
- devcontainer:
103
- enabled: true
104
- full_permissions_when_in_devcontainer: true # Run all providers with full permissions
112
+ # List backups
113
+ aidp devcontainer list-backups
105
114
 
106
- # Or enable per-provider
107
- permissions:
108
- skip_permission_checks:
109
- - claude # Adds --dangerously-skip-permissions for Claude Code
115
+ # Restore from backup
116
+ aidp devcontainer restore 0
110
117
  ```
111
118
 
112
- AIDP automatically detects when it's running in a devcontainer and adjusts agent permissions accordingly. This is safe because the container is sandboxed from your host system.
119
+ See [docs/DEVELOPMENT_CONTAINER.md](docs/DEVELOPMENT_CONTAINER.md) for complete devcontainer management documentation.
120
+
121
+ ### Devcontainer Detection
122
+
123
+ AIDP automatically detects when it's running inside a devcontainer and adjusts its behavior accordingly. This detection uses multiple heuristics including environment variables, filesystem markers, and cgroup information. See [DevcontainerDetector](lib/aidp/utils/devcontainer_detector.rb) for implementation details.
113
124
 
114
125
  ## Core Features
115
126
 
@@ -190,6 +201,101 @@ aidp ws rm issue-123-fix-auth --delete-branch
190
201
 
191
202
  See [Workstreams Guide](docs/WORKSTREAMS.md) for detailed usage.
192
203
 
204
+ ### Watch Mode (Automated GitHub Integration)
205
+
206
+ AIDP can automatically monitor GitHub repositories and respond to labeled issues, creating plans and executing implementations autonomously:
207
+
208
+ ```bash
209
+ # Start watch mode for a repository
210
+ aidp watch https://github.com/owner/repo/issues
211
+
212
+ # Optional: specify polling interval, provider, and verbose output
213
+ aidp watch owner/repo --interval 60 --provider claude --verbose
214
+
215
+ # Run a single cycle (useful for CI/testing)
216
+ aidp watch owner/repo --once
217
+ ```
218
+
219
+ **Label Workflow:**
220
+
221
+ AIDP uses a smart label-based workflow to manage the lifecycle of automated issue resolution:
222
+
223
+ 1. **Planning Phase** (`aidp-plan` label):
224
+ - Add this label to an issue to trigger plan generation
225
+ - AIDP generates an implementation plan with task breakdown and clarifying questions
226
+ - Posts the plan as a comment on the issue
227
+ - Automatically removes the `aidp-plan` label
228
+
229
+ 2. **Review & Clarification**:
230
+ - **If questions exist**: AIDP adds `aidp-needs-input` label and waits for user response
231
+ - User responds to questions in a comment
232
+ - User manually removes `aidp-needs-input` and adds `aidp-build` to proceed
233
+ - **If no questions**: AIDP adds `aidp-ready` label, indicating it's ready to build
234
+ - User can review the plan before proceeding
235
+ - User manually adds `aidp-build` label when ready
236
+
237
+ 3. **Implementation Phase** (`aidp-build` label):
238
+ - Triggers autonomous implementation via work loops
239
+ - Creates a feature branch and commits changes
240
+ - Runs tests and linters with automatic fixes
241
+ - **If clarification needed during implementation**:
242
+ - Posts clarification questions as a comment
243
+ - Automatically removes `aidp-build` label and adds `aidp-needs-input`
244
+ - Preserves work-in-progress for later resumption
245
+ - User responds to questions, then manually removes `aidp-needs-input` and re-adds `aidp-build`
246
+ - **On success**:
247
+ - Posts completion comment with summary
248
+ - Automatically removes the `aidp-build` label
249
+
250
+ **Customizable Labels:**
251
+
252
+ All label names are configurable to match your repository's existing label scheme. Configure via the interactive wizard or manually in `aidp.yml`:
253
+
254
+ ```yaml
255
+ # .aidp/aidp.yml
256
+ watch:
257
+ labels:
258
+ plan_trigger: aidp-plan # Label to trigger plan generation
259
+ needs_input: aidp-needs-input # Label when plan needs user input
260
+ ready_to_build: aidp-ready # Label when plan is ready to build
261
+ build_trigger: aidp-build # Label to trigger implementation
262
+ ```
263
+
264
+ Run `aidp config --interactive` and enable watch mode to configure labels interactively.
265
+
266
+ **Safety Features:**
267
+
268
+ - **Public Repository Protection**: Disabled by default for public repos (require explicit opt-in)
269
+ - **Author Allowlist**: Restrict automation to trusted GitHub users only
270
+ - **Container Requirement**: Optionally require sandboxed environment
271
+ - **Force Override**: `--force` flag to bypass safety checks (dangerous!)
272
+
273
+ **Safety Configuration:**
274
+
275
+ ```yaml
276
+ # .aidp/aidp.yml
277
+ watch:
278
+ safety:
279
+ allow_public_repos: true # Required for public repositories
280
+ author_allowlist: # Only these users can trigger automation
281
+ - trusted-maintainer
282
+ - team-member
283
+ require_container: true # Require devcontainer/Docker environment
284
+ ```
285
+
286
+ Run `aidp config --interactive` and enable watch mode to configure safety settings interactively.
287
+
288
+ **Clarification Requests:**
289
+
290
+ AIDP can automatically request clarification when it needs more information during implementation. This works in both watch mode and interactive mode:
291
+
292
+ - **Watch Mode**: Posts clarification questions as a GitHub comment, updates labels to `aidp-needs-input`, and waits for user response
293
+ - **Interactive Mode**: Prompts the user directly in the terminal to answer questions before continuing
294
+
295
+ This ensures AIDP never gets stuck - if it needs more information, it will ask for it rather than making incorrect assumptions or failing silently.
296
+
297
+ See [Watch Mode Guide](docs/FULLY_AUTOMATIC_MODE.md) and [Watch Mode Safety](docs/WATCH_MODE_SAFETY.md) for complete documentation.
298
+
193
299
  ## Command Reference
194
300
 
195
301
  ### Copilot Mode
@@ -263,6 +369,20 @@ aidp ws rm <slug> --delete-branch # Also delete git branch
263
369
  aidp ws rm <slug> --force # Skip confirmation
264
370
  ```
265
371
 
372
+ ### Configuration Commands
373
+
374
+ ```bash
375
+ # Interactive configuration wizard (recommended)
376
+ aidp config --interactive # Configure all settings including watch mode
377
+
378
+ # Legacy setup wizard
379
+ aidp --setup-config # Re-run basic setup wizard
380
+
381
+ # Help and version
382
+ aidp --help # Show all commands
383
+ aidp --version # Show version
384
+ ```
385
+
266
386
  ### System Commands
267
387
 
268
388
  ```bash
@@ -275,11 +395,6 @@ aidp providers
275
395
  # Harness state management
276
396
  aidp harness status
277
397
  aidp harness reset
278
-
279
- # Configuration
280
- aidp --setup-config # Re-run setup wizard
281
- aidp --help # Show all commands
282
- aidp --version # Show version
283
398
  ```
284
399
 
285
400
  ## AI Providers
@@ -291,7 +406,6 @@ AIDP intelligently manages multiple providers with automatic switching:
291
406
  - **Cursor CLI** - IDE-integrated provider for code-specific tasks
292
407
  - **Gemini CLI** - Google's Gemini command-line interface for general tasks
293
408
  - **GitHub Copilot CLI** - GitHub's AI pair programmer command-line interface
294
- - **macOS UI** - macOS-specific UI automation provider
295
409
  - **OpenCode** - Alternative open-source code generation provider
296
410
 
297
411
  The system automatically switches providers when:
data/lib/aidp/cli.rb CHANGED
@@ -1231,7 +1231,7 @@ module Aidp
1231
1231
 
1232
1232
  def run_watch_command(args)
1233
1233
  if args.empty?
1234
- display_message("Usage: aidp watch <issues_url> [--interval SECONDS] [--provider NAME] [--once] [--no-workstreams]", type: :info)
1234
+ display_message("Usage: aidp watch <issues_url> [--interval SECONDS] [--provider NAME] [--once] [--no-workstreams] [--force] [--verbose]", type: :info)
1235
1235
  return
1236
1236
  end
1237
1237
 
@@ -1240,6 +1240,8 @@ module Aidp
1240
1240
  provider_name = nil
1241
1241
  once = false
1242
1242
  use_workstreams = true # Default to using workstreams
1243
+ force = false
1244
+ verbose = false
1243
1245
 
1244
1246
  until args.empty?
1245
1247
  token = args.shift
@@ -1253,11 +1255,20 @@ module Aidp
1253
1255
  once = true
1254
1256
  when "--no-workstreams"
1255
1257
  use_workstreams = false
1258
+ when "--force"
1259
+ force = true
1260
+ when "--verbose"
1261
+ verbose = true
1256
1262
  else
1257
1263
  display_message("⚠️ Unknown watch option: #{token}", type: :warn)
1258
1264
  end
1259
1265
  end
1260
1266
 
1267
+ # Load watch safety configuration
1268
+ config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
1269
+ config = config_manager.config || {}
1270
+ watch_config = config[:watch] || config["watch"] || {}
1271
+
1261
1272
  runner = Aidp::Watch::Runner.new(
1262
1273
  issues_url: issues_url,
1263
1274
  interval: interval.positive? ? interval : Aidp::Watch::Runner::DEFAULT_INTERVAL,
@@ -1265,7 +1276,10 @@ module Aidp
1265
1276
  project_dir: Dir.pwd,
1266
1277
  once: once,
1267
1278
  use_workstreams: use_workstreams,
1268
- prompt: create_prompt
1279
+ prompt: create_prompt,
1280
+ safety_config: watch_config,
1281
+ force: force,
1282
+ verbose: verbose
1269
1283
  )
1270
1284
  runner.start
1271
1285
  rescue ArgumentError => e
@@ -27,11 +27,12 @@ module Aidp
27
27
  waiting_for_rate_limit: "waiting_for_rate_limit",
28
28
  stopped: "stopped",
29
29
  completed: "completed",
30
- error: "error"
30
+ error: "error",
31
+ needs_clarification: "needs_clarification"
31
32
  }.freeze
32
33
 
33
34
  # Public accessors for testing and integration
34
- attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager
35
+ attr_reader :current_provider, :current_step, :user_input, :execution_log, :provider_manager, :clarification_questions
35
36
 
36
37
  def initialize(project_dir, mode = :analyze, options = {})
37
38
  @project_dir = project_dir
@@ -132,7 +133,9 @@ module Aidp
132
133
  cleanup
133
134
  end
134
135
 
135
- {status: @state, message: get_completion_message}
136
+ result = {status: @state, message: get_completion_message}
137
+ result[:clarification_questions] = @clarification_questions if @clarification_questions
138
+ result
136
139
  end
137
140
 
138
141
  # Pause the harness execution
@@ -248,12 +251,23 @@ module Aidp
248
251
  end
249
252
 
250
253
  def handle_user_feedback_request(result)
251
- @state = STATES[:waiting_for_user]
252
- log_execution("Waiting for user feedback")
253
-
254
254
  # Extract questions from result
255
255
  questions = @condition_detector.extract_questions(result)
256
256
 
257
+ # Check if we're in watch mode (non-interactive)
258
+ if @options[:workflow_type] == :watch_mode
259
+ # Store questions for later retrieval and set state to needs_clarification
260
+ @clarification_questions = questions
261
+ @state = STATES[:needs_clarification]
262
+ log_execution("Clarification needed in watch mode", {question_count: questions.size})
263
+ # Don't continue - exit the loop so we can return this status
264
+ return
265
+ end
266
+
267
+ # Interactive mode: collect feedback from user
268
+ @state = STATES[:waiting_for_user]
269
+ log_execution("Waiting for user feedback")
270
+
257
271
  # Collect user input
258
272
  user_responses = @user_interface.collect_feedback(questions)
259
273
 
@@ -10,9 +10,10 @@ module Aidp
10
10
  class BackupManager
11
11
  class BackupError < StandardError; end
12
12
 
13
- def initialize(project_dir)
13
+ def initialize(project_dir, clock: Time)
14
14
  @project_dir = project_dir
15
15
  @backup_dir = File.join(project_dir, ".aidp", "backups", "devcontainer")
16
+ @clock = clock
16
17
  end
17
18
 
18
19
  # Create a backup of the devcontainer file
@@ -26,7 +27,7 @@ module Aidp
26
27
 
27
28
  ensure_backup_directory_exists
28
29
 
29
- timestamp = Time.now.utc.strftime("%Y%m%d_%H%M%S")
30
+ timestamp = current_time.utc.strftime("%Y%m%d_%H%M%S")
30
31
  backup_filename = "devcontainer-#{timestamp}.json"
31
32
  backup_path = File.join(@backup_dir, backup_filename)
32
33
 
@@ -163,11 +164,17 @@ module Aidp
163
164
  end
164
165
 
165
166
  def parse_timestamp(timestamp_str)
166
- return Time.now if timestamp_str.nil?
167
+ return current_time if timestamp_str.nil?
167
168
 
168
169
  Time.strptime(timestamp_str, "%Y%m%d_%H%M%S")
169
170
  rescue ArgumentError
170
- Time.now
171
+ current_time
172
+ end
173
+
174
+ attr_reader :clock
175
+
176
+ def current_time
177
+ clock.respond_to?(:call) ? clock.call : clock.now
171
178
  end
172
179
  end
173
180
  end
@@ -786,6 +786,79 @@ module Aidp
786
786
  watch_enabled: watch,
787
787
  quick_mode_default: quick_mode
788
788
  })
789
+
790
+ # Configure watch mode settings if enabled
791
+ configure_watch_mode if watch
792
+ end
793
+
794
+ def configure_watch_mode
795
+ prompt.say("\n👀 Watch Mode Configuration")
796
+ prompt.say("-" * 40)
797
+
798
+ configure_watch_safety
799
+ configure_watch_labels
800
+ end
801
+
802
+ def configure_watch_safety
803
+ prompt.say("\n🔒 Watch mode safety settings")
804
+ existing = get([:watch, :safety]) || {}
805
+
806
+ allow_public_repos = prompt.yes?(
807
+ "Allow watch mode on public repositories?",
808
+ default: existing.fetch(:allow_public_repos, false)
809
+ )
810
+
811
+ prompt.say("\n📝 Author allowlist (GitHub usernames allowed to trigger watch mode)")
812
+ prompt.say(" Leave empty to allow all authors (not recommended for public repos)")
813
+ author_allowlist = ask_list(
814
+ "Author allowlist (comma-separated GitHub usernames)",
815
+ existing[:author_allowlist] || [],
816
+ allow_empty: true
817
+ )
818
+
819
+ require_container = prompt.yes?(
820
+ "Require watch mode to run in a container?",
821
+ default: existing.fetch(:require_container, true)
822
+ )
823
+
824
+ set([:watch, :safety], {
825
+ allow_public_repos: allow_public_repos,
826
+ author_allowlist: author_allowlist,
827
+ require_container: require_container
828
+ })
829
+ end
830
+
831
+ def configure_watch_labels
832
+ prompt.say("\n🏷️ Watch mode label configuration")
833
+ prompt.say(" Configure GitHub issue labels that trigger watch mode actions")
834
+ existing = get([:watch, :labels]) || {}
835
+
836
+ plan_trigger = ask_with_default(
837
+ "Label to trigger plan generation",
838
+ existing[:plan_trigger] || "aidp-plan"
839
+ )
840
+
841
+ needs_input = ask_with_default(
842
+ "Label for plans needing user input",
843
+ existing[:needs_input] || "aidp-needs-input"
844
+ )
845
+
846
+ ready_to_build = ask_with_default(
847
+ "Label for plans ready to build",
848
+ existing[:ready_to_build] || "aidp-ready"
849
+ )
850
+
851
+ build_trigger = ask_with_default(
852
+ "Label to trigger implementation",
853
+ existing[:build_trigger] || "aidp-build"
854
+ )
855
+
856
+ set([:watch, :labels], {
857
+ plan_trigger: plan_trigger,
858
+ needs_input: needs_input,
859
+ ready_to_build: ready_to_build,
860
+ build_trigger: build_trigger
861
+ })
789
862
  end
790
863
 
791
864
  # -------------------------------------------
@@ -816,6 +889,7 @@ module Aidp
816
889
  .sub(/^nfrs:/, "# Non-functional requirements to reference during planning\nnfrs:")
817
890
  .sub(/^logging:/, "# Logging configuration\nlogging:")
818
891
  .sub(/^modes:/, "# Defaults for background/watch/quick modes\nmodes:")
892
+ .sub(/^watch:/, "# Watch mode safety and label configuration\nwatch:")
819
893
  end
820
894
 
821
895
  def display_preview(yaml_content)
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.22.0"
4
+ VERSION = "0.23.0"
5
5
  end
@@ -15,14 +15,22 @@ module Aidp
15
15
  class BuildProcessor
16
16
  include Aidp::MessageDisplay
17
17
 
18
- BUILD_LABEL = "aidp-build"
18
+ DEFAULT_BUILD_LABEL = "aidp-build"
19
+ DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
19
20
  IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
20
21
 
21
- def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true)
22
+ attr_reader :build_label, :needs_input_label
23
+
24
+ def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true, verbose: false, label_config: {})
22
25
  @repository_client = repository_client
23
26
  @state_store = state_store
24
27
  @project_dir = project_dir
25
28
  @use_workstreams = use_workstreams
29
+ @verbose = verbose
30
+
31
+ # Load label configuration
32
+ @build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
33
+ @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
26
34
  end
27
35
 
28
36
  def process(issue)
@@ -55,6 +63,8 @@ module Aidp
55
63
 
56
64
  if result[:status] == "completed"
57
65
  handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
66
+ elsif result[:status] == "needs_clarification"
67
+ handle_clarification_request(issue: issue, slug: slug, result: result)
58
68
  else
59
69
  handle_failure(issue: issue, slug: slug, result: result)
60
70
  end
@@ -203,15 +213,33 @@ module Aidp
203
213
  prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
204
214
  prompt_manager.write(content)
205
215
  display_message("📝 Wrote PROMPT.md with implementation contract", type: :info)
216
+
217
+ if @verbose
218
+ display_message("\n--- Implementation Prompt ---", type: :muted)
219
+ display_message(content.strip, type: :muted)
220
+ display_message("--- End Prompt ---\n", type: :muted)
221
+ end
206
222
  end
207
223
 
208
224
  def build_user_input(issue:, plan_data:)
209
225
  tasks = Array(plan_value(plan_data, "tasks"))
210
- {
226
+ user_input = {
211
227
  "Implementation Contract" => plan_value(plan_data, "summary").to_s,
212
228
  "Tasks" => tasks.map { |task| "- #{task}" }.join("\n"),
213
229
  "Issue URL" => issue[:url]
214
230
  }.delete_if { |_k, v| v.nil? || v.empty? }
231
+
232
+ if @verbose
233
+ display_message("\n--- User Input for Harness ---", type: :muted)
234
+ user_input.each do |key, value|
235
+ display_message("#{key}:", type: :muted)
236
+ display_message(value, type: :muted)
237
+ display_message("", type: :muted)
238
+ end
239
+ display_message("--- End User Input ---\n", type: :muted)
240
+ end
241
+
242
+ user_input
215
243
  end
216
244
 
217
245
  def run_harness(user_input:, working_dir: @project_dir)
@@ -220,8 +248,20 @@ module Aidp
220
248
  workflow_type: :watch_mode,
221
249
  user_input: user_input
222
250
  }
251
+
252
+ display_message("🚀 Running harness in execute mode...", type: :info) if @verbose
253
+
223
254
  runner = Aidp::Harness::Runner.new(working_dir, :execute, options)
224
- runner.run
255
+ result = runner.run
256
+
257
+ if @verbose
258
+ display_message("\n--- Harness Result ---", type: :muted)
259
+ display_message("Status: #{result[:status]}", type: :muted)
260
+ display_message("Message: #{result[:message]}", type: :muted) if result[:message]
261
+ display_message("--- End Result ---\n", type: :muted)
262
+ end
263
+
264
+ result
225
265
  end
226
266
 
227
267
  def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
@@ -257,12 +297,62 @@ module Aidp
257
297
  )
258
298
  display_message("🎉 Posted completion comment for issue ##{issue[:number]}", type: :success)
259
299
 
300
+ # Remove build label after successful completion
301
+ begin
302
+ @repository_client.remove_labels(issue[:number], @build_label)
303
+ display_message("🏷️ Removed '#{@build_label}' label after completion", type: :info)
304
+ rescue => e
305
+ display_message("⚠️ Failed to remove build label: #{e.message}", type: :warn)
306
+ # Don't fail the process if label removal fails
307
+ end
308
+
260
309
  # Keep workstream for review - don't auto-cleanup on success
261
310
  if @use_workstreams
262
311
  display_message("ℹ️ Workstream #{slug} preserved for review. Remove with: aidp ws rm #{slug}", type: :muted)
263
312
  end
264
313
  end
265
314
 
315
+ def handle_clarification_request(issue:, slug:, result:)
316
+ questions = result[:clarification_questions] || []
317
+ workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
318
+
319
+ # Build comment with questions
320
+ comment_parts = []
321
+ comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
322
+ comment_parts << ""
323
+ comment_parts << "The AI agent needs additional information to proceed with implementation:"
324
+ comment_parts << ""
325
+ questions.each_with_index do |question, index|
326
+ comment_parts << "#{index + 1}. #{question}"
327
+ end
328
+ comment_parts << ""
329
+ comment_parts << "**Next Steps**: Please reply with answers to the questions above. Once resolved, remove the `#{@needs_input_label}` label and add the `#{@build_label}` label to resume implementation."
330
+ comment_parts << ""
331
+ comment_parts << workstream_note.to_s
332
+
333
+ comment = comment_parts.join("\n")
334
+ @repository_client.post_comment(issue[:number], comment)
335
+
336
+ # Update labels: remove build trigger, add needs input
337
+ begin
338
+ @repository_client.replace_labels(
339
+ issue[:number],
340
+ old_labels: [@build_label],
341
+ new_labels: [@needs_input_label]
342
+ )
343
+ display_message("🏷️ Updated labels: removed '#{@build_label}', added '#{@needs_input_label}' (needs clarification)", type: :info)
344
+ rescue => e
345
+ display_message("⚠️ Failed to update labels for issue ##{issue[:number]}: #{e.message}", type: :warn)
346
+ end
347
+
348
+ @state_store.record_build_status(
349
+ issue[:number],
350
+ status: "needs_clarification",
351
+ details: {questions: questions, workstream: slug}
352
+ )
353
+ display_message("💬 Posted clarification request for issue ##{issue[:number]}", type: :success)
354
+ end
355
+
266
356
  def handle_failure(issue:, slug:, result:)
267
357
  message = result[:message] || "Unknown failure"
268
358
  workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
@@ -26,8 +26,9 @@ module Aidp
26
26
  Focus on concrete engineering tasks. Ensure questions are actionable.
27
27
  PROMPT
28
28
 
29
- def initialize(provider_name: nil)
29
+ def initialize(provider_name: nil, verbose: false)
30
30
  @provider_name = provider_name
31
+ @verbose = verbose
31
32
  end
32
33
 
33
34
  def generate(issue)
@@ -67,7 +68,21 @@ module Aidp
67
68
 
68
69
  def generate_with_provider(provider, issue)
69
70
  payload = build_prompt(issue)
71
+
72
+ if @verbose
73
+ display_message("\n--- Plan Generation Prompt ---", type: :muted)
74
+ display_message(payload.strip, type: :muted)
75
+ display_message("--- End Prompt ---\n", type: :muted)
76
+ end
77
+
70
78
  response = provider.send_message(prompt: payload)
79
+
80
+ if @verbose
81
+ display_message("\n--- Provider Response ---", type: :muted)
82
+ display_message(response.strip, type: :muted)
83
+ display_message("--- End Response ---\n", type: :muted)
84
+ end
85
+
71
86
  parsed = parse_structured_response(response)
72
87
 
73
88
  return parsed if parsed
@@ -11,13 +11,32 @@ module Aidp
11
11
  class PlanProcessor
12
12
  include Aidp::MessageDisplay
13
13
 
14
- PLAN_LABEL = "aidp-plan"
14
+ # Default label names
15
+ DEFAULT_PLAN_LABEL = "aidp-plan"
16
+ DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
17
+ DEFAULT_READY_LABEL = "aidp-ready"
18
+ DEFAULT_BUILD_LABEL = "aidp-build"
19
+
15
20
  COMMENT_HEADER = "## 🤖 AIDP Plan Proposal"
16
21
 
17
- def initialize(repository_client:, state_store:, plan_generator:)
22
+ attr_reader :plan_label, :needs_input_label, :ready_label, :build_label
23
+
24
+ def initialize(repository_client:, state_store:, plan_generator:, label_config: {})
18
25
  @repository_client = repository_client
19
26
  @state_store = state_store
20
27
  @plan_generator = plan_generator
28
+
29
+ # Load label configuration with defaults
30
+ @plan_label = label_config[:plan_trigger] || label_config["plan_trigger"] || DEFAULT_PLAN_LABEL
31
+ @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
32
+ @ready_label = label_config[:ready_to_build] || label_config["ready_to_build"] || DEFAULT_READY_LABEL
33
+ @build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
34
+ end
35
+
36
+ # For backward compatibility
37
+ def self.plan_label_from_config(config)
38
+ labels = config[:labels] || config["labels"] || {}
39
+ labels[:plan_trigger] || labels["plan_trigger"] || DEFAULT_PLAN_LABEL
21
40
  end
22
41
 
23
42
  def process(issue)
@@ -35,14 +54,39 @@ module Aidp
35
54
 
36
55
  display_message("💬 Posted plan comment for issue ##{number}", type: :success)
37
56
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
57
+
58
+ # Update labels: remove plan trigger, add appropriate status label
59
+ update_labels_after_plan(number, plan_data)
38
60
  end
39
61
 
40
62
  private
41
63
 
64
+ def update_labels_after_plan(number, plan_data)
65
+ questions = Array(plan_data[:questions])
66
+ has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
67
+
68
+ # Determine which label to add based on whether there are questions
69
+ new_label = has_questions ? @needs_input_label : @ready_label
70
+ status_text = has_questions ? "needs input" : "ready to build"
71
+
72
+ begin
73
+ @repository_client.replace_labels(
74
+ number,
75
+ old_labels: [@plan_label],
76
+ new_labels: [new_label]
77
+ )
78
+ display_message("🏷️ Updated labels: removed '#{@plan_label}', added '#{new_label}' (#{status_text})", type: :info)
79
+ rescue => e
80
+ display_message("⚠️ Failed to update labels for issue ##{number}: #{e.message}", type: :warn)
81
+ # Don't fail the whole process if label update fails
82
+ end
83
+ end
84
+
42
85
  def build_comment(issue:, plan:)
43
86
  summary = plan[:summary].to_s.strip
44
87
  tasks = Array(plan[:tasks])
45
88
  questions = Array(plan[:questions])
89
+ has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
46
90
 
47
91
  parts = []
48
92
  parts << COMMENT_HEADER
@@ -59,7 +103,14 @@ module Aidp
59
103
  parts << "### Clarifying Questions"
60
104
  parts << format_numbered(questions, placeholder: "_No questions identified_")
61
105
  parts << ""
62
- parts << "Please reply inline with answers to the questions above. Once the discussion is resolved, apply the `aidp-build` label to begin implementation."
106
+
107
+ # Add instructions based on whether there are questions
108
+ parts << if has_questions
109
+ "**Next Steps**: Please reply with answers to the questions above. Once resolved, remove the `#{@needs_input_label}` label and add the `#{@build_label}` label to begin implementation."
110
+ else
111
+ "**Next Steps**: This plan is ready for implementation. Add the `#{@build_label}` label to begin."
112
+ end
113
+
63
114
  parts.join("\n")
64
115
  end
65
116
 
@@ -65,6 +65,20 @@ module Aidp
65
65
  gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number) : raise("GitHub CLI not available - cannot create PR")
66
66
  end
67
67
 
68
+ def add_labels(number, *labels)
69
+ gh_available? ? add_labels_via_gh(number, labels.flatten) : add_labels_via_api(number, labels.flatten)
70
+ end
71
+
72
+ def remove_labels(number, *labels)
73
+ gh_available? ? remove_labels_via_gh(number, labels.flatten) : remove_labels_via_api(number, labels.flatten)
74
+ end
75
+
76
+ def replace_labels(number, old_labels:, new_labels:)
77
+ # Remove old labels and add new ones atomically where possible
78
+ remove_labels(number, *old_labels) unless old_labels.empty?
79
+ add_labels(number, *new_labels) unless new_labels.empty?
80
+ end
81
+
68
82
  private
69
83
 
70
84
  def list_issues_via_gh(labels:, state:)
@@ -180,6 +194,66 @@ module Aidp
180
194
  stdout.strip
181
195
  end
182
196
 
197
+ def add_labels_via_gh(number, labels)
198
+ return if labels.empty?
199
+
200
+ cmd = ["gh", "issue", "edit", number.to_s, "--repo", full_repo]
201
+ labels.each { |label| cmd += ["--add-label", label] }
202
+
203
+ stdout, stderr, status = Open3.capture3(*cmd)
204
+ raise "Failed to add labels via gh: #{stderr.strip}" unless status.success?
205
+
206
+ stdout.strip
207
+ end
208
+
209
+ def add_labels_via_api(number, labels)
210
+ return if labels.empty?
211
+
212
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/labels")
213
+ request = Net::HTTP::Post.new(uri)
214
+ request["Content-Type"] = "application/json"
215
+ request.body = JSON.dump({labels: labels})
216
+
217
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
218
+ http.request(request)
219
+ end
220
+
221
+ raise "Failed to add labels via API (#{response.code})" unless response.code.start_with?("2")
222
+ response.body
223
+ end
224
+
225
+ def remove_labels_via_gh(number, labels)
226
+ return if labels.empty?
227
+
228
+ cmd = ["gh", "issue", "edit", number.to_s, "--repo", full_repo]
229
+ labels.each { |label| cmd += ["--remove-label", label] }
230
+
231
+ stdout, stderr, status = Open3.capture3(*cmd)
232
+ raise "Failed to remove labels via gh: #{stderr.strip}" unless status.success?
233
+
234
+ stdout.strip
235
+ end
236
+
237
+ def remove_labels_via_api(number, labels)
238
+ return if labels.empty?
239
+
240
+ labels.each do |label|
241
+ # URL encode the label name
242
+ encoded_label = URI.encode_www_form_component(label)
243
+ uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/labels/#{encoded_label}")
244
+ request = Net::HTTP::Delete.new(uri)
245
+
246
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
247
+ http.request(request)
248
+ end
249
+
250
+ # 404 is OK - label didn't exist
251
+ unless response.code.start_with?("2") || response.code == "404"
252
+ raise "Failed to remove label '#{label}' via API (#{response.code})"
253
+ end
254
+ end
255
+ end
256
+
183
257
  def normalize_issue(raw)
184
258
  {
185
259
  number: raw["number"],
@@ -163,16 +163,25 @@ module Aidp
163
163
  end
164
164
 
165
165
  def public_repos_allowed?
166
- @config.dig(:safety, :allow_public_repos) == true
166
+ # Support both string and symbol keys
167
+ safety_config = @config[:safety] || @config["safety"] || {}
168
+ (safety_config[:allow_public_repos] || safety_config["allow_public_repos"]) == true
167
169
  end
168
170
 
169
171
  def author_allowlist
170
- @author_allowlist ||= Array(@config.dig(:safety, :author_allowlist)).compact.map(&:to_s)
172
+ # Support both string and symbol keys
173
+ safety_config = @config[:safety] || @config["safety"] || {}
174
+ @author_allowlist ||= Array(
175
+ safety_config[:author_allowlist] || safety_config["author_allowlist"]
176
+ ).compact.map(&:to_s)
171
177
  end
172
178
 
173
179
  def safe_environment?
174
180
  # Check if running in a container
175
- in_container? || @config.dig(:safety, :require_container) == false
181
+ # Support both string and symbol keys
182
+ safety_config = @config[:safety] || @config["safety"] || {}
183
+ require_container = safety_config[:require_container] || safety_config["require_container"]
184
+ in_container? || require_container == false
176
185
  end
177
186
 
178
187
  def in_container?
@@ -19,12 +19,13 @@ module Aidp
19
19
 
20
20
  DEFAULT_INTERVAL = 30
21
21
 
22
- 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, safety_config: {}, force: false)
22
+ 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, safety_config: {}, force: false, verbose: false)
23
23
  @prompt = prompt
24
24
  @interval = interval
25
25
  @once = once
26
26
  @project_dir = project_dir
27
27
  @force = force
28
+ @verbose = verbose
28
29
 
29
30
  owner, repo = RepositoryClient.parse_issues_url(issues_url)
30
31
  @repository_client = RepositoryClient.new(owner: owner, repo: repo, gh_available: gh_available)
@@ -33,16 +34,23 @@ module Aidp
33
34
  config: safety_config
34
35
  )
35
36
  @state_store = StateStore.new(project_dir: project_dir, repository: "#{owner}/#{repo}")
37
+
38
+ # Extract label configuration from safety_config (it's actually the full watch config)
39
+ label_config = safety_config[:labels] || safety_config["labels"] || {}
40
+
36
41
  @plan_processor = PlanProcessor.new(
37
42
  repository_client: @repository_client,
38
43
  state_store: @state_store,
39
- plan_generator: PlanGenerator.new(provider_name: provider_name)
44
+ plan_generator: PlanGenerator.new(provider_name: provider_name, verbose: verbose),
45
+ label_config: label_config
40
46
  )
41
47
  @build_processor = BuildProcessor.new(
42
48
  repository_client: @repository_client,
43
49
  state_store: @state_store,
44
50
  project_dir: project_dir,
45
- use_workstreams: use_workstreams
51
+ use_workstreams: use_workstreams,
52
+ verbose: verbose,
53
+ label_config: label_config
46
54
  )
47
55
  end
48
56
 
@@ -73,9 +81,10 @@ module Aidp
73
81
  end
74
82
 
75
83
  def process_plan_triggers
76
- issues = @repository_client.list_issues(labels: [PlanProcessor::PLAN_LABEL], state: "open")
84
+ plan_label = @plan_processor.plan_label
85
+ issues = @repository_client.list_issues(labels: [plan_label], state: "open")
77
86
  issues.each do |issue|
78
- next unless issue_has_label?(issue, PlanProcessor::PLAN_LABEL)
87
+ next unless issue_has_label?(issue, plan_label)
79
88
 
80
89
  detailed = @repository_client.fetch_issue(issue[:number])
81
90
 
@@ -89,9 +98,10 @@ module Aidp
89
98
  end
90
99
 
91
100
  def process_build_triggers
92
- issues = @repository_client.list_issues(labels: [BuildProcessor::BUILD_LABEL], state: "open")
101
+ build_label = @build_processor.build_label
102
+ issues = @repository_client.list_issues(labels: [build_label], state: "open")
93
103
  issues.each do |issue|
94
- next unless issue_has_label?(issue, BuildProcessor::BUILD_LABEL)
104
+ next unless issue_has_label?(issue, build_label)
95
105
 
96
106
  status = @state_store.build_status(issue[:number])
97
107
  next if status["status"] == "completed"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan