aidp 0.19.1 → 0.21.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/lib/aidp/cli/issue_importer.rb +130 -3
- data/lib/aidp/cli.rb +12 -0
- data/lib/aidp/harness/config_schema.rb +3 -0
- data/lib/aidp/harness/config_validator.rb +1 -0
- data/lib/aidp/harness/provider_manager.rb +50 -6
- data/lib/aidp/harness/state/persistence.rb +36 -19
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
- data/lib/aidp/init/doc_generator.rb +6 -0
- data/lib/aidp/logger.rb +13 -1
- data/lib/aidp/rescue_logging.rb +20 -9
- data/lib/aidp/setup/wizard.rb +277 -56
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/repository_client.rb +2 -0
- data/lib/aidp/watch/repository_safety_checker.rb +214 -0
- data/lib/aidp/watch/runner.rb +25 -1
- data/lib/aidp/workflows/guided_agent.rb +115 -15
- data/lib/aidp.rb +1 -0
- data/templates/planning/generate_llm_style_guide.md +97 -14
- metadata +2 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "open3"
|
|
6
|
+
require_relative "../message_display"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Watch
|
|
10
|
+
# Validates watch mode safety requirements for public repositories
|
|
11
|
+
# and enforces author allowlists to prevent untrusted input execution.
|
|
12
|
+
class RepositorySafetyChecker
|
|
13
|
+
include Aidp::MessageDisplay
|
|
14
|
+
|
|
15
|
+
class UnsafeRepositoryError < StandardError; end
|
|
16
|
+
class UnauthorizedAuthorError < StandardError; end
|
|
17
|
+
|
|
18
|
+
def initialize(repository_client:, config: {})
|
|
19
|
+
@repository_client = repository_client
|
|
20
|
+
@config = config
|
|
21
|
+
@repo_visibility_cache = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if watch mode is safe to run for this repository
|
|
25
|
+
# @param force [Boolean] Skip safety checks (dangerous!)
|
|
26
|
+
# @return [Boolean] true if safe, raises error otherwise
|
|
27
|
+
def validate_watch_mode_safety!(force: false)
|
|
28
|
+
Aidp.log_debug("repository_safety", "validate watch mode safety",
|
|
29
|
+
repo: @repository_client.full_repo,
|
|
30
|
+
force: force)
|
|
31
|
+
|
|
32
|
+
# Skip checks if forced (user takes responsibility)
|
|
33
|
+
if force
|
|
34
|
+
display_message("⚠️ Watch mode safety checks BYPASSED (--force)", type: :warning)
|
|
35
|
+
return true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check repository visibility
|
|
39
|
+
unless repository_safe_for_watch_mode?
|
|
40
|
+
raise UnsafeRepositoryError, unsafe_repository_message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if running in safe environment
|
|
44
|
+
unless safe_environment?
|
|
45
|
+
display_message("⚠️ Watch mode running outside container/sandbox", type: :warning)
|
|
46
|
+
display_message(" Consider using a containerized environment for additional safety", type: :muted)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
display_message("✅ Watch mode safety checks passed", type: :success)
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if an issue author is allowed to trigger automated work
|
|
54
|
+
# @param issue [Hash] Issue data with :author or :assignees
|
|
55
|
+
# @return [Boolean] true if authorized
|
|
56
|
+
def author_authorized?(issue)
|
|
57
|
+
author = extract_author(issue)
|
|
58
|
+
return false unless author
|
|
59
|
+
|
|
60
|
+
# If no allowlist configured, allow all (backward compatible)
|
|
61
|
+
return true if author_allowlist.empty?
|
|
62
|
+
|
|
63
|
+
# Check if author is in allowlist
|
|
64
|
+
authorized = author_allowlist.include?(author)
|
|
65
|
+
|
|
66
|
+
Aidp.log_debug("repository_safety", "check author authorization",
|
|
67
|
+
author: author,
|
|
68
|
+
authorized: authorized,
|
|
69
|
+
allowlist_size: author_allowlist.size)
|
|
70
|
+
|
|
71
|
+
authorized
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if an issue should be processed based on author authorization
|
|
75
|
+
# @param issue [Hash] Issue data
|
|
76
|
+
# @param enforce [Boolean] Raise error if unauthorized
|
|
77
|
+
# @return [Boolean] true if should process
|
|
78
|
+
def should_process_issue?(issue, enforce: true)
|
|
79
|
+
unless author_authorized?(issue)
|
|
80
|
+
author = extract_author(issue)
|
|
81
|
+
if enforce && author_allowlist.any?
|
|
82
|
+
raise UnauthorizedAuthorError,
|
|
83
|
+
"Issue ##{issue[:number]} author '#{author}' not in allowlist. " \
|
|
84
|
+
"Add to watch.safety.author_allowlist in aidp.yml to allow."
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
display_message("⏭️ Skipping issue ##{issue[:number]} - author '#{author}' not authorized",
|
|
88
|
+
type: :muted)
|
|
89
|
+
return false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def repository_safe_for_watch_mode?
|
|
98
|
+
# Check if repository is private
|
|
99
|
+
return true if repository_private?
|
|
100
|
+
|
|
101
|
+
# Public repositories require explicit opt-in
|
|
102
|
+
if public_repos_allowed?
|
|
103
|
+
display_message("⚠️ Watch mode enabled for PUBLIC repository", type: :warning)
|
|
104
|
+
display_message(" Ensure you trust all contributors and have proper safety measures", type: :muted)
|
|
105
|
+
return true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def repository_private?
|
|
112
|
+
# Cache visibility check to avoid repeated API calls
|
|
113
|
+
return @repo_visibility_cache[:private] if @repo_visibility_cache.key?(:private)
|
|
114
|
+
|
|
115
|
+
is_private = if @repository_client.gh_available?
|
|
116
|
+
check_visibility_via_gh
|
|
117
|
+
else
|
|
118
|
+
check_visibility_via_api
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@repo_visibility_cache[:private] = is_private
|
|
122
|
+
Aidp.log_debug("repository_safety", "repository visibility check",
|
|
123
|
+
repo: @repository_client.full_repo,
|
|
124
|
+
private: is_private)
|
|
125
|
+
|
|
126
|
+
is_private
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def check_visibility_via_gh
|
|
130
|
+
cmd = ["gh", "repo", "view", @repository_client.full_repo, "--json", "visibility"]
|
|
131
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
132
|
+
|
|
133
|
+
unless status.success?
|
|
134
|
+
Aidp.log_warn("repository_safety", "failed to check repo visibility via gh",
|
|
135
|
+
error: stderr.strip)
|
|
136
|
+
# Assume public if we can't determine (safer default)
|
|
137
|
+
return false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
data = JSON.parse(stdout)
|
|
141
|
+
data["visibility"]&.downcase == "private"
|
|
142
|
+
rescue JSON::ParserError => e
|
|
143
|
+
Aidp.log_error("repository_safety", "failed to parse gh repo response", error: e.message)
|
|
144
|
+
false # Assume public on error
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def check_visibility_via_api
|
|
148
|
+
uri = URI("https://api.github.com/repos/#{@repository_client.full_repo}")
|
|
149
|
+
response = Net::HTTP.get_response(uri)
|
|
150
|
+
|
|
151
|
+
unless response.code == "200"
|
|
152
|
+
Aidp.log_warn("repository_safety", "failed to check repo visibility via API",
|
|
153
|
+
status: response.code)
|
|
154
|
+
# Assume public if we can't determine
|
|
155
|
+
return false
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
data = JSON.parse(response.body)
|
|
159
|
+
data["private"] == true
|
|
160
|
+
rescue => e
|
|
161
|
+
Aidp.log_error("repository_safety", "failed to check repo visibility", error: e.message)
|
|
162
|
+
false # Assume public on error
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def public_repos_allowed?
|
|
166
|
+
@config.dig(:safety, :allow_public_repos) == true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def author_allowlist
|
|
170
|
+
@author_allowlist ||= Array(@config.dig(:safety, :author_allowlist)).compact.map(&:to_s)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def safe_environment?
|
|
174
|
+
# Check if running in a container
|
|
175
|
+
in_container? || @config.dig(:safety, :require_container) == false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def in_container?
|
|
179
|
+
# Check for container indicators
|
|
180
|
+
File.exist?("/.dockerenv") ||
|
|
181
|
+
File.exist?("/run/.containerenv") ||
|
|
182
|
+
ENV["AIDP_ENV"] == "development" # devcontainer
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def extract_author(issue)
|
|
186
|
+
# Try different author fields
|
|
187
|
+
issue[:author] ||
|
|
188
|
+
issue.dig(:assignees, 0) ||
|
|
189
|
+
issue["author"] ||
|
|
190
|
+
issue.dig("assignees", 0)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def unsafe_repository_message
|
|
194
|
+
<<~MSG
|
|
195
|
+
🛑 Watch mode is DISABLED for public repositories by default.
|
|
196
|
+
|
|
197
|
+
Running automated code execution on untrusted public input is dangerous!
|
|
198
|
+
|
|
199
|
+
To enable watch mode for this public repository, add to your aidp.yml:
|
|
200
|
+
|
|
201
|
+
watch:
|
|
202
|
+
safety:
|
|
203
|
+
allow_public_repos: true
|
|
204
|
+
author_allowlist: # Only these users can trigger automation
|
|
205
|
+
- trusted_maintainer
|
|
206
|
+
- another_admin
|
|
207
|
+
require_container: true # Require sandboxed environment
|
|
208
|
+
|
|
209
|
+
Alternatively, use --force to bypass this check (NOT RECOMMENDED).
|
|
210
|
+
MSG
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "tty-prompt"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../message_display"
|
|
6
6
|
require_relative "repository_client"
|
|
7
|
+
require_relative "repository_safety_checker"
|
|
7
8
|
require_relative "state_store"
|
|
8
9
|
require_relative "plan_generator"
|
|
9
10
|
require_relative "plan_processor"
|
|
@@ -18,14 +19,19 @@ module Aidp
|
|
|
18
19
|
|
|
19
20
|
DEFAULT_INTERVAL = 30
|
|
20
21
|
|
|
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
|
+
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
23
|
@prompt = prompt
|
|
23
24
|
@interval = interval
|
|
24
25
|
@once = once
|
|
25
26
|
@project_dir = project_dir
|
|
27
|
+
@force = force
|
|
26
28
|
|
|
27
29
|
owner, repo = RepositoryClient.parse_issues_url(issues_url)
|
|
28
30
|
@repository_client = RepositoryClient.new(owner: owner, repo: repo, gh_available: gh_available)
|
|
31
|
+
@safety_checker = RepositorySafetyChecker.new(
|
|
32
|
+
repository_client: @repository_client,
|
|
33
|
+
config: safety_config
|
|
34
|
+
)
|
|
29
35
|
@state_store = StateStore.new(project_dir: project_dir, repository: "#{owner}/#{repo}")
|
|
30
36
|
@plan_processor = PlanProcessor.new(
|
|
31
37
|
repository_client: @repository_client,
|
|
@@ -41,6 +47,9 @@ module Aidp
|
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
def start
|
|
50
|
+
# Validate safety requirements before starting
|
|
51
|
+
@safety_checker.validate_watch_mode_safety!(force: @force)
|
|
52
|
+
|
|
44
53
|
display_message("👀 Watch mode enabled for #{@repository_client.full_repo}", type: :highlight)
|
|
45
54
|
display_message("Polling every #{@interval} seconds. Press Ctrl+C to stop.", type: :muted)
|
|
46
55
|
|
|
@@ -51,6 +60,9 @@ module Aidp
|
|
|
51
60
|
end
|
|
52
61
|
rescue Interrupt
|
|
53
62
|
display_message("\n⏹️ Watch mode interrupted by user", type: :warning)
|
|
63
|
+
rescue RepositorySafetyChecker::UnsafeRepositoryError => e
|
|
64
|
+
display_message("\n#{e.message}", type: :error)
|
|
65
|
+
raise
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
private
|
|
@@ -66,7 +78,13 @@ module Aidp
|
|
|
66
78
|
next unless issue_has_label?(issue, PlanProcessor::PLAN_LABEL)
|
|
67
79
|
|
|
68
80
|
detailed = @repository_client.fetch_issue(issue[:number])
|
|
81
|
+
|
|
82
|
+
# Check author authorization before processing
|
|
83
|
+
next unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
84
|
+
|
|
69
85
|
@plan_processor.process(detailed)
|
|
86
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
87
|
+
Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
|
|
70
88
|
end
|
|
71
89
|
end
|
|
72
90
|
|
|
@@ -79,7 +97,13 @@ module Aidp
|
|
|
79
97
|
next if status["status"] == "completed"
|
|
80
98
|
|
|
81
99
|
detailed = @repository_client.fetch_issue(issue[:number])
|
|
100
|
+
|
|
101
|
+
# Check author authorization before processing
|
|
102
|
+
next unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
103
|
+
|
|
82
104
|
@build_processor.process(detailed)
|
|
105
|
+
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
106
|
+
Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
|
|
83
107
|
end
|
|
84
108
|
end
|
|
85
109
|
|
|
@@ -18,7 +18,7 @@ module Aidp
|
|
|
18
18
|
|
|
19
19
|
class ConversationError < StandardError; end
|
|
20
20
|
|
|
21
|
-
def initialize(project_dir, prompt: nil, use_enhanced_input: true)
|
|
21
|
+
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false)
|
|
22
22
|
@project_dir = project_dir
|
|
23
23
|
|
|
24
24
|
# Use EnhancedInput with Reline for full readline-style key bindings
|
|
@@ -32,6 +32,9 @@ module Aidp
|
|
|
32
32
|
@provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
|
|
33
33
|
@conversation_history = []
|
|
34
34
|
@user_input = {}
|
|
35
|
+
@invalid_planning_responses = 0
|
|
36
|
+
@verbose = verbose
|
|
37
|
+
@debug_env = ENV["DEBUG"] == "1" || ENV["DEBUG"] == "2"
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
# Main entry point for guided workflow selection
|
|
@@ -79,6 +82,7 @@ module Aidp
|
|
|
79
82
|
iteration += 1
|
|
80
83
|
# Ask AI for next question based on current plan
|
|
81
84
|
question_response = get_planning_questions(plan)
|
|
85
|
+
emit_verbose_iteration(plan, question_response)
|
|
82
86
|
|
|
83
87
|
# Debug: show raw provider response and parsed result
|
|
84
88
|
debug_log("Planning iteration #{iteration} provider response", level: :debug, data: {
|
|
@@ -131,9 +135,10 @@ module Aidp
|
|
|
131
135
|
end
|
|
132
136
|
|
|
133
137
|
response = call_provider_for_analysis(system_prompt, user_prompt)
|
|
134
|
-
parsed =
|
|
138
|
+
parsed = safe_parse_planning_response(response)
|
|
135
139
|
# Attach raw response for debug
|
|
136
140
|
parsed[:raw_response] = response
|
|
141
|
+
emit_verbose_raw_prompt(system_prompt, user_prompt, response)
|
|
137
142
|
parsed
|
|
138
143
|
end
|
|
139
144
|
|
|
@@ -231,24 +236,87 @@ module Aidp
|
|
|
231
236
|
result
|
|
232
237
|
rescue => e
|
|
233
238
|
message = e.message.to_s
|
|
239
|
+
|
|
240
|
+
# Classify error type for better handling
|
|
234
241
|
classified = if message =~ /resource[_ ]exhausted/i || message =~ /\[resource_exhausted\]/i
|
|
235
242
|
"resource_exhausted"
|
|
236
243
|
elsif message =~ /quota[_ ]exceeded/i || message =~ /\[quota_exceeded\]/i
|
|
237
244
|
"quota_exceeded"
|
|
245
|
+
elsif /empty response/i.match?(message)
|
|
246
|
+
"empty_response"
|
|
247
|
+
else
|
|
248
|
+
"provider_error"
|
|
238
249
|
end
|
|
239
250
|
|
|
240
|
-
if
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
# Attempt fallback if we haven't exhausted all providers
|
|
252
|
+
if attempts < max_attempts
|
|
253
|
+
display_message("⚠️ Provider '#{provider_name}' failed (#{classified.tr("_", " ")}) – attempting fallback...", type: :warning)
|
|
254
|
+
|
|
255
|
+
if @provider_manager.respond_to?(:switch_provider_for_error)
|
|
256
|
+
# Try to switch to fallback provider
|
|
257
|
+
switched = @provider_manager.switch_provider_for_error(classified, stderr: message)
|
|
258
|
+
|
|
259
|
+
if switched && switched != provider_name
|
|
260
|
+
display_message("↩️ Switched to provider '#{switched}' – retrying with same prompt", type: :info)
|
|
261
|
+
retry
|
|
262
|
+
elsif switched == provider_name
|
|
263
|
+
# ProviderManager could not advance; mark current as rate limited to encourage next attempt to move on.
|
|
264
|
+
Aidp.logger.debug("guided_agent", "provider_switch_noop", provider: provider_name, reason: classified)
|
|
265
|
+
if @provider_manager.respond_to?(:mark_rate_limited)
|
|
266
|
+
@provider_manager.mark_rate_limited(provider_name)
|
|
267
|
+
next_provider = @provider_manager.switch_provider("rate_limit_forced", previous_error: message)
|
|
268
|
+
if next_provider && next_provider != provider_name
|
|
269
|
+
display_message("↩️ Switched to provider '#{next_provider}' (forced) – retrying with same prompt", type: :info)
|
|
270
|
+
retry
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
246
274
|
end
|
|
247
275
|
end
|
|
276
|
+
|
|
277
|
+
# If we get here, either we've exhausted all attempts or couldn't switch providers
|
|
278
|
+
if attempts >= max_attempts
|
|
279
|
+
display_message("❌ All providers exhausted after #{attempts} attempts", type: :error)
|
|
280
|
+
end
|
|
248
281
|
raise
|
|
249
282
|
end
|
|
250
283
|
end
|
|
251
284
|
|
|
285
|
+
# Verbose output helpers
|
|
286
|
+
def emit_verbose_raw_prompt(system_prompt, user_prompt, raw_response)
|
|
287
|
+
return unless @verbose || @debug_env
|
|
288
|
+
if @verbose
|
|
289
|
+
display_message("\n--- Prompt Sent (Planning) ---", type: :muted)
|
|
290
|
+
display_message(system_prompt.strip, type: :muted)
|
|
291
|
+
display_message(user_prompt.strip, type: :muted)
|
|
292
|
+
display_message("--- Raw Provider Response ---", type: :muted)
|
|
293
|
+
display_message(raw_response.to_s.strip, type: :muted)
|
|
294
|
+
display_message("------------------------------\n", type: :muted)
|
|
295
|
+
elsif @debug_env
|
|
296
|
+
Aidp.logger.debug("guided_agent", "planning_prompt", system: system_prompt.strip, user: user_prompt.strip)
|
|
297
|
+
Aidp.logger.debug("guided_agent", "planning_raw_response", raw: raw_response.to_s.strip)
|
|
298
|
+
end
|
|
299
|
+
rescue => e
|
|
300
|
+
Aidp.logger.warn("guided_agent", "Failed verbose prompt emit", error: e.message)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def emit_verbose_iteration(plan, question_response)
|
|
304
|
+
return unless @verbose || @debug_env
|
|
305
|
+
summary = {complete: question_response[:complete], questions: question_response[:questions], reasoning: question_response[:reasoning], error: question_response[:error]}
|
|
306
|
+
if @verbose
|
|
307
|
+
display_message("\n=== Planning Iteration Summary ===", type: :info)
|
|
308
|
+
display_message("Questions: #{(summary[:questions] || []).join(" | ")}", type: :info)
|
|
309
|
+
display_message("Complete? #{summary[:complete]}", type: :info)
|
|
310
|
+
display_message("Reasoning: #{summary[:reasoning]}", type: :muted) if summary[:reasoning]
|
|
311
|
+
display_message("Error: #{summary[:error]}", type: :warning) if summary[:error]
|
|
312
|
+
display_message("=================================", type: :info)
|
|
313
|
+
elsif @debug_env
|
|
314
|
+
Aidp.logger.debug("guided_agent", "iteration_summary", summary: summary, plan_progress_keys: plan.keys)
|
|
315
|
+
end
|
|
316
|
+
rescue => e
|
|
317
|
+
Aidp.logger.warn("guided_agent", "Failed verbose iteration emit", error: e.message)
|
|
318
|
+
end
|
|
319
|
+
|
|
252
320
|
def validate_provider_configuration!
|
|
253
321
|
configured = @provider_manager.configured_providers
|
|
254
322
|
if configured.nil? || configured.empty?
|
|
@@ -307,20 +375,53 @@ module Aidp
|
|
|
307
375
|
json_match = response_text.match(/```json\s*(\{.*?\})\s*```/m) ||
|
|
308
376
|
response_text.match(/(\{.*\})/m)
|
|
309
377
|
|
|
310
|
-
unless json_match
|
|
311
|
-
return {complete: false, questions: ["Could you tell me more about your requirements?"]}
|
|
312
|
-
end
|
|
313
|
-
|
|
378
|
+
return {error: :invalid_format} unless json_match
|
|
314
379
|
JSON.parse(json_match[1], symbolize_names: true)
|
|
315
380
|
rescue JSON::ParserError
|
|
316
|
-
{
|
|
381
|
+
{error: :invalid_format}
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Provides structured fallback sequence when provider keeps returning invalid planning JSON.
|
|
385
|
+
# After exceeding sequence length, switches to manual entry question.
|
|
386
|
+
def safe_parse_planning_response(response_text)
|
|
387
|
+
parsed = parse_planning_response(response_text)
|
|
388
|
+
return parsed unless parsed.is_a?(Hash) && parsed[:error] == :invalid_format
|
|
389
|
+
|
|
390
|
+
@invalid_planning_responses += 1
|
|
391
|
+
fallback_sequence = [
|
|
392
|
+
"Provide scope (key features) and primary users.",
|
|
393
|
+
"List 3-5 key functional requirements and any technical constraints.",
|
|
394
|
+
"Supply any non-functional requirements (performance/security) or type 'skip'."
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
if @invalid_planning_responses <= fallback_sequence.size
|
|
398
|
+
{complete: false, questions: [fallback_sequence[@invalid_planning_responses - 1]], reasoning: "Fallback due to invalid provider response (format)", error: :fallback}
|
|
399
|
+
else
|
|
400
|
+
display_message("[ERROR] Provider returned invalid planning JSON #{@invalid_planning_responses} times. Enter combined plan details manually.", type: :error)
|
|
401
|
+
{complete: false, questions: ["Enter plan details manually (features; users; requirements; constraints) or type 'skip'"], reasoning: "Manual recovery mode", error: :manual_recovery}
|
|
402
|
+
end
|
|
317
403
|
end
|
|
318
404
|
|
|
319
405
|
def update_plan_from_answer(plan, question, answer)
|
|
320
406
|
# Simple heuristic-based plan updates
|
|
321
407
|
# In a more sophisticated implementation, use AI to categorize answers
|
|
322
408
|
|
|
323
|
-
|
|
409
|
+
# IMPORTANT: Check manual recovery sentinel prompt first so it isn't misclassified
|
|
410
|
+
# by broader keyword heuristics (e.g., it contains the word 'users').
|
|
411
|
+
if question.start_with?("Enter plan details manually")
|
|
412
|
+
unless answer.strip.downcase == "skip"
|
|
413
|
+
parts = answer.split(/;|\|/).map(&:strip).reject(&:empty?)
|
|
414
|
+
features, users, requirements, constraints = parts
|
|
415
|
+
plan[:scope][:included] ||= []
|
|
416
|
+
plan[:scope][:included] << features if features
|
|
417
|
+
plan[:users][:personas] ||= []
|
|
418
|
+
plan[:users][:personas] << users if users
|
|
419
|
+
plan[:requirements][:functional] ||= []
|
|
420
|
+
plan[:requirements][:functional] << requirements if requirements
|
|
421
|
+
plan[:constraints][:technical] ||= []
|
|
422
|
+
plan[:constraints][:technical] << constraints if constraints
|
|
423
|
+
end
|
|
424
|
+
elsif question.downcase.include?("scope") || question.downcase.include?("include")
|
|
324
425
|
plan[:scope][:included] ||= []
|
|
325
426
|
plan[:scope][:included] << answer
|
|
326
427
|
elsif question.downcase.include?("user") || question.downcase.include?("who")
|
|
@@ -338,7 +439,6 @@ module Aidp
|
|
|
338
439
|
elsif question.downcase.include?("complete") || question.downcase.include?("done") || question.downcase.include?("success")
|
|
339
440
|
plan[:completion_criteria] << answer
|
|
340
441
|
else
|
|
341
|
-
# General information
|
|
342
442
|
plan[:additional_context] ||= []
|
|
343
443
|
plan[:additional_context] << {question: question, answer: answer}
|
|
344
444
|
end
|
data/lib/aidp.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "aidp/core_ext/class_attribute"
|
|
|
7
7
|
require_relative "aidp/version"
|
|
8
8
|
require_relative "aidp/config"
|
|
9
9
|
require_relative "aidp/util"
|
|
10
|
+
require_relative "aidp/rescue_logging"
|
|
10
11
|
require_relative "aidp/message_display"
|
|
11
12
|
require_relative "aidp/concurrency"
|
|
12
13
|
require_relative "aidp/setup/wizard"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Generate LLM Style Guide
|
|
2
2
|
|
|
3
|
-
Your task is to create a project-specific **LLM_STYLE_GUIDE.md** that will be used by AI agents working on this project.
|
|
3
|
+
Your task is to create a project-specific **LLM_STYLE_GUIDE.md** and **STYLE_GUIDE.md** that will be used by AI agents working on this project. The LLM guide should be concise and actionable, while the full guide provides detailed context and rationale.
|
|
4
4
|
|
|
5
5
|
## Context
|
|
6
6
|
|
|
@@ -13,9 +13,27 @@ You have access to the project directory. Examine the codebase to understand:
|
|
|
13
13
|
- Project structure
|
|
14
14
|
- Dependencies and frameworks
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Two-Guide Approach
|
|
17
17
|
|
|
18
|
-
Create
|
|
18
|
+
Create **TWO** complementary guides:
|
|
19
|
+
|
|
20
|
+
### 1. STYLE_GUIDE.md (Detailed Reference)
|
|
21
|
+
|
|
22
|
+
- Comprehensive explanations with examples
|
|
23
|
+
- Rationale and context for each guideline
|
|
24
|
+
- Deep dives into patterns and anti-patterns
|
|
25
|
+
- Located at `docs/STYLE_GUIDE.md`
|
|
26
|
+
|
|
27
|
+
### 2. LLM_STYLE_GUIDE.md (Quick Reference)
|
|
28
|
+
|
|
29
|
+
- Ultra-concise bullet points for quick scanning
|
|
30
|
+
- Each guideline references back to STYLE_GUIDE.md with line numbers
|
|
31
|
+
- Format: `Guideline text. STYLE_GUIDE:start-end`
|
|
32
|
+
- Located at `docs/LLM_STYLE_GUIDE.md`
|
|
33
|
+
|
|
34
|
+
## Requirements for LLM_STYLE_GUIDE.md
|
|
35
|
+
|
|
36
|
+
Create a file at `docs/LLM_STYLE_GUIDE.md` with the following sections, **adding line number references to the detailed guide**:
|
|
19
37
|
|
|
20
38
|
### 1. Core Engineering Rules
|
|
21
39
|
|
|
@@ -88,32 +106,97 @@ Create a file at `docs/LLM_STYLE_GUIDE.md` with the following sections:
|
|
|
88
106
|
|
|
89
107
|
## Output Format
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
### STYLE_GUIDE.md Format
|
|
110
|
+
|
|
111
|
+
The detailed guide should be:
|
|
112
|
+
|
|
113
|
+
- **Comprehensive**: Full explanations with context and rationale
|
|
114
|
+
- **Example-driven**: Show code examples for each pattern
|
|
115
|
+
- **Well-structured**: Clear section headers that can be referenced by line number
|
|
116
|
+
- **Numbered sections**: Use consistent heading levels for easy reference
|
|
117
|
+
|
|
118
|
+
### LLM_STYLE_GUIDE.md Format
|
|
119
|
+
|
|
120
|
+
The concise guide should be:
|
|
92
121
|
|
|
93
122
|
- **Concise**: Use bullet points and tables where possible
|
|
123
|
+
- **Cross-referenced**: Every guideline includes `STYLE_GUIDE:start-end` reference
|
|
94
124
|
- **Specific**: Reference actual code patterns from this project
|
|
95
125
|
- **Actionable**: Provide clear do's and don'ts
|
|
96
126
|
- **Scannable**: Use headers, lists, and formatting for easy reference
|
|
97
127
|
|
|
128
|
+
## Creating Cross-References
|
|
129
|
+
|
|
130
|
+
### Step 1: Write STYLE_GUIDE.md first
|
|
131
|
+
|
|
132
|
+
```markdown
|
|
133
|
+
## Code Organization # Line 18
|
|
134
|
+
|
|
135
|
+
### Class Structure # Line 20
|
|
136
|
+
...detailed explanation...
|
|
137
|
+
|
|
138
|
+
### File Organization # Line 45
|
|
139
|
+
...detailed explanation...
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Step 2: Add references to LLM_STYLE_GUIDE.md
|
|
143
|
+
|
|
144
|
+
For each guideline in the LLM guide, add the line range from STYLE_GUIDE.md:
|
|
145
|
+
|
|
146
|
+
```markdown
|
|
147
|
+
## 1. Core Engineering Rules
|
|
148
|
+
|
|
149
|
+
- Small objects, clear roles. Avoid god classes. `STYLE_GUIDE:18-50`
|
|
150
|
+
- Methods: do one thing; extract early. `STYLE_GUIDE:108-117`
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Step 3: Verify line numbers
|
|
154
|
+
|
|
155
|
+
Use `grep -n "^##" docs/STYLE_GUIDE.md` to find section headers and their line numbers.
|
|
156
|
+
|
|
98
157
|
## Example Structure
|
|
99
158
|
|
|
159
|
+
### STYLE_GUIDE.md
|
|
160
|
+
|
|
161
|
+
```markdown
|
|
162
|
+
# Project Style Guide
|
|
163
|
+
|
|
164
|
+
## Code Organization
|
|
165
|
+
|
|
166
|
+
Detailed explanation of code organization principles...
|
|
167
|
+
|
|
168
|
+
### Single Responsibility Principle
|
|
169
|
+
|
|
170
|
+
Detailed explanation with examples...
|
|
171
|
+
|
|
172
|
+
## Testing Guidelines
|
|
173
|
+
|
|
174
|
+
Comprehensive testing approach...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### LLM_STYLE_GUIDE.md
|
|
178
|
+
|
|
100
179
|
```markdown
|
|
101
|
-
# Project LLM Style
|
|
180
|
+
# Project LLM Style Cheat Sheet
|
|
102
181
|
|
|
103
|
-
>
|
|
182
|
+
> Ultra-concise rules for automated coding agents.
|
|
104
183
|
|
|
105
184
|
## 1. Core Engineering Rules
|
|
106
|
-
- [Specific rule based on this project]
|
|
107
|
-
- [Another specific rule]
|
|
108
185
|
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
|
|
112
|
-
|
|
186
|
+
- Small objects, clear roles. `STYLE_GUIDE:18-50`
|
|
187
|
+
- Methods: do one thing; extract early. `STYLE_GUIDE:108-117`
|
|
188
|
+
|
|
189
|
+
## 2. Testing Contracts
|
|
113
190
|
|
|
114
|
-
|
|
191
|
+
- Test public behavior only. `STYLE_GUIDE:1022-1261`
|
|
192
|
+
- Mock external boundaries only. `STYLE_GUIDE:1022-1261`
|
|
115
193
|
```
|
|
116
194
|
|
|
117
195
|
## Deliverable
|
|
118
196
|
|
|
119
|
-
Create
|
|
197
|
+
Create **BOTH** files:
|
|
198
|
+
|
|
199
|
+
1. `docs/STYLE_GUIDE.md` - Comprehensive guide with detailed explanations
|
|
200
|
+
2. `docs/LLM_STYLE_GUIDE.md` - Quick reference with line number cross-references to the detailed guide
|
|
201
|
+
|
|
202
|
+
The guides should work together: AI agents read the concise LLM guide for quick decisions, then reference the detailed STYLE_GUIDE for context and examples when needed.
|
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.
|
|
4
|
+
version: 0.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -379,6 +379,7 @@ files:
|
|
|
379
379
|
- lib/aidp/watch/plan_generator.rb
|
|
380
380
|
- lib/aidp/watch/plan_processor.rb
|
|
381
381
|
- lib/aidp/watch/repository_client.rb
|
|
382
|
+
- lib/aidp/watch/repository_safety_checker.rb
|
|
382
383
|
- lib/aidp/watch/runner.rb
|
|
383
384
|
- lib/aidp/watch/state_store.rb
|
|
384
385
|
- lib/aidp/workflows/definitions.rb
|