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.
@@ -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
@@ -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 = parse_planning_response(response)
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 classified && attempts < max_attempts
241
- display_message("⚠️ Provider '#{provider_name}' #{classified.tr("_", " ")} – attempting fallback...", type: :warning)
242
- switched = @provider_manager.switch_provider_for_error(classified, stderr: message) if @provider_manager.respond_to?(:switch_provider_for_error)
243
- if switched && switched != provider_name
244
- display_message("↩️ Switched to provider '#{switched}'", type: :info)
245
- retry
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
- {complete: false, questions: ["Could you tell me more about your requirements?"]}
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
- if question.downcase.include?("scope") || question.downcase.include?("include")
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. This guide should be concise, actionable, and tailored to this specific codebase.
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
- ## Requirements
16
+ ## Two-Guide Approach
17
17
 
18
- Create a file at `docs/LLM_STYLE_GUIDE.md` with the following sections:
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
- The LLM_STYLE_GUIDE.md should be:
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 Guide
180
+ # Project LLM Style Cheat Sheet
102
181
 
103
- > Concise rules for AI agents working on [Project Name]. Based on [Language/Framework].
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
- ## 2. Naming & Structure
110
- - Classes: [convention]
111
- - Files: [convention]
112
- - [etc.]
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
- [Continue with all sections...]
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 `docs/LLM_STYLE_GUIDE.md` with all the sections above, tailored specifically to this project's codebase, languages, and frameworks.
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.19.1
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