aidp 0.20.0 → 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/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/init/doc_generator.rb +6 -0
- 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 +19 -4
- data/templates/planning/generate_llm_style_guide.md +97 -14
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4fbf6c86d50426f952b62da4b353cce6560aa5321c33d641a972a3af5989456
|
|
4
|
+
data.tar.gz: da684c72ccdc0d4770d3029a0977f592a9f9baf8e1ce4c14df47ccfbda559b38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf57b96f9393e533508624fabe2ca3bdc8e49a241a68f6404c144d124ab0c19c4f3cea415e8e8e2f4a7d3145e06e23f86f2207e61414b8397ea929304ae29f96
|
|
7
|
+
data.tar.gz: f15ca608bda7e5ed3358c9b633b7d415ce943e99d9a10a74f114de3e31373daec0d4ae52d01000bbb42c6950b252e50ad13daa176c4d32932a6f98b2153a678b
|
|
@@ -4,20 +4,30 @@ require "json"
|
|
|
4
4
|
require "net/http"
|
|
5
5
|
require "uri"
|
|
6
6
|
require "open3"
|
|
7
|
+
require "timeout"
|
|
7
8
|
|
|
8
9
|
module Aidp
|
|
9
10
|
# Handles importing GitHub issues into AIDP work loops
|
|
10
11
|
class IssueImporter
|
|
11
12
|
include Aidp::MessageDisplay
|
|
12
13
|
|
|
14
|
+
COMPONENT = "issue_importer"
|
|
15
|
+
|
|
13
16
|
# Initialize the importer
|
|
14
17
|
#
|
|
15
18
|
# @param gh_available [Boolean, nil] (test-only) forcibly sets whether gh CLI is considered
|
|
16
19
|
# available. When nil (default) we auto-detect. This enables deterministic specs without
|
|
17
20
|
# depending on developer environment.
|
|
18
21
|
def initialize(gh_available: nil, enable_bootstrap: true)
|
|
19
|
-
|
|
22
|
+
disabled_via_env = ENV["AIDP_DISABLE_GH_CLI"] == "1"
|
|
23
|
+
@gh_available = if disabled_via_env
|
|
24
|
+
false
|
|
25
|
+
else
|
|
26
|
+
gh_available.nil? ? gh_cli_available? : gh_available
|
|
27
|
+
end
|
|
20
28
|
@enable_bootstrap = enable_bootstrap
|
|
29
|
+
Aidp.log_debug(COMPONENT, "Initialized importer", gh_available: @gh_available, enable_bootstrap: @enable_bootstrap, disabled_via_env: disabled_via_env)
|
|
30
|
+
Aidp.log_debug(COMPONENT, "GitHub CLI disabled via env flag") if disabled_via_env
|
|
21
31
|
end
|
|
22
32
|
|
|
23
33
|
def import_issue(identifier)
|
|
@@ -81,16 +91,28 @@ module Aidp
|
|
|
81
91
|
return nil unless match
|
|
82
92
|
|
|
83
93
|
owner, repo, number = match[1], match[2], match[3]
|
|
94
|
+
Aidp.log_debug(COMPONENT, "Fetching issue data", owner: owner, repo: repo, number: number, via: (@gh_available ? "gh_cli" : "api"))
|
|
84
95
|
|
|
85
96
|
# First try GitHub CLI if available (works for private repos)
|
|
86
97
|
if @gh_available
|
|
87
98
|
display_message("🔍 Fetching issue via GitHub CLI...", type: :info)
|
|
99
|
+
Aidp.log_debug(COMPONENT, "Attempting GitHub CLI fetch", owner: owner, repo: repo, number: number)
|
|
88
100
|
issue_data = fetch_via_gh_cli(owner, repo, number)
|
|
89
|
-
|
|
101
|
+
if issue_data
|
|
102
|
+
Aidp.log_debug(COMPONENT, "GitHub CLI fetch succeeded", owner: owner, repo: repo, number: number)
|
|
103
|
+
return issue_data
|
|
104
|
+
end
|
|
105
|
+
Aidp.log_debug(COMPONENT, "GitHub CLI fetch failed, falling back to API", owner: owner, repo: repo, number: number)
|
|
90
106
|
end
|
|
91
107
|
|
|
92
108
|
# Fallback to public API
|
|
93
109
|
display_message("🔍 Fetching issue via GitHub API...", type: :info)
|
|
110
|
+
Aidp.log_debug(COMPONENT, "Fetching issue via GitHub API", owner: owner, repo: repo, number: number)
|
|
111
|
+
fixture = test_fixture(owner, repo, number)
|
|
112
|
+
if fixture
|
|
113
|
+
Aidp.log_debug(COMPONENT, "Using test fixture for issue fetch", owner: owner, repo: repo, number: number, status: fixture["status"])
|
|
114
|
+
return handle_test_fixture(fixture, owner, repo, number)
|
|
115
|
+
end
|
|
94
116
|
fetch_via_api(owner, repo, number)
|
|
95
117
|
end
|
|
96
118
|
|
|
@@ -101,9 +123,12 @@ module Aidp
|
|
|
101
123
|
"--json", "title,body,labels,milestone,comments,state,assignees,number,url"
|
|
102
124
|
]
|
|
103
125
|
|
|
104
|
-
|
|
126
|
+
Aidp.log_debug(COMPONENT, "Running gh cli", owner: owner, repo: repo, number: number, command: cmd.join(" "))
|
|
127
|
+
stdout, stderr, status = capture3_with_timeout(*cmd, timeout: gh_cli_timeout)
|
|
128
|
+
Aidp.log_debug(COMPONENT, "Completed gh cli", owner: owner, repo: repo, number: number, exitstatus: status.exitstatus)
|
|
105
129
|
|
|
106
130
|
unless status.success?
|
|
131
|
+
Aidp.log_warn(COMPONENT, "GitHub CLI fetch failed", owner: owner, repo: repo, number: number, exitstatus: status.exitstatus, error: stderr.strip)
|
|
107
132
|
display_message("⚠️ GitHub CLI failed: #{stderr.strip}", type: :warn)
|
|
108
133
|
return nil
|
|
109
134
|
end
|
|
@@ -112,9 +137,14 @@ module Aidp
|
|
|
112
137
|
data = JSON.parse(stdout)
|
|
113
138
|
normalize_gh_cli_data(data)
|
|
114
139
|
rescue JSON::ParserError => e
|
|
140
|
+
Aidp.log_warn(COMPONENT, "GitHub CLI response parse failed", owner: owner, repo: repo, number: number, error: e.message)
|
|
115
141
|
display_message("❌ Failed to parse GitHub CLI response: #{e.message}", type: :error)
|
|
116
142
|
nil
|
|
117
143
|
end
|
|
144
|
+
rescue Timeout::Error
|
|
145
|
+
Aidp.log_warn(COMPONENT, "GitHub CLI timed out", owner: owner, repo: repo, number: number, timeout: gh_cli_timeout)
|
|
146
|
+
display_message("⚠️ GitHub CLI timed out after #{gh_cli_timeout}s, falling back to API", type: :warn)
|
|
147
|
+
nil
|
|
118
148
|
end
|
|
119
149
|
|
|
120
150
|
def fetch_via_api(owner, repo, number)
|
|
@@ -281,6 +311,103 @@ module Aidp
|
|
|
281
311
|
false
|
|
282
312
|
end
|
|
283
313
|
|
|
314
|
+
def capture3_with_timeout(*cmd, timeout:)
|
|
315
|
+
stdout_str = +""
|
|
316
|
+
stderr_str = +""
|
|
317
|
+
status = nil
|
|
318
|
+
wait_thr = nil
|
|
319
|
+
|
|
320
|
+
Timeout.timeout(timeout) do
|
|
321
|
+
Open3.popen3(*cmd) do |stdin, stdout_io, stderr_io, thread|
|
|
322
|
+
wait_thr = thread
|
|
323
|
+
stdin.close
|
|
324
|
+
stdout_str = stdout_io.read
|
|
325
|
+
stderr_str = stderr_io.read
|
|
326
|
+
status = thread.value
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
[stdout_str, stderr_str, status]
|
|
331
|
+
rescue Timeout::Error
|
|
332
|
+
terminate_process(wait_thr)
|
|
333
|
+
raise
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def terminate_process(wait_thr)
|
|
337
|
+
return unless wait_thr&.alive?
|
|
338
|
+
|
|
339
|
+
begin
|
|
340
|
+
Process.kill("TERM", wait_thr.pid)
|
|
341
|
+
rescue Errno::ESRCH
|
|
342
|
+
return
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
begin
|
|
346
|
+
wait_thr.join(1)
|
|
347
|
+
rescue
|
|
348
|
+
nil
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
return unless wait_thr.alive?
|
|
352
|
+
|
|
353
|
+
begin
|
|
354
|
+
Process.kill("KILL", wait_thr.pid)
|
|
355
|
+
rescue Errno::ESRCH
|
|
356
|
+
return
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
begin
|
|
360
|
+
wait_thr.join(1)
|
|
361
|
+
rescue
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def gh_cli_timeout
|
|
367
|
+
Integer(ENV.fetch("AIDP_GH_CLI_TIMEOUT", 5))
|
|
368
|
+
rescue ArgumentError, TypeError
|
|
369
|
+
5
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def test_fixture(owner, repo, number)
|
|
373
|
+
fixtures_raw = ENV["AIDP_TEST_ISSUE_FIXTURES"]
|
|
374
|
+
return nil unless fixtures_raw
|
|
375
|
+
|
|
376
|
+
fixtures = JSON.parse(fixtures_raw)
|
|
377
|
+
fixtures["#{owner}/#{repo}##{number}"]
|
|
378
|
+
rescue JSON::ParserError => e
|
|
379
|
+
Aidp.log_warn(COMPONENT, "Invalid issue fixtures JSON", error: e.message)
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def handle_test_fixture(fixture, owner, repo, number)
|
|
384
|
+
status = fixture["status"].to_i
|
|
385
|
+
case status
|
|
386
|
+
when 200
|
|
387
|
+
data = fixture.fetch("data", {})
|
|
388
|
+
Aidp.log_debug(COMPONENT, "Returning fixture issue data", owner: owner, repo: repo, number: number)
|
|
389
|
+
normalize_api_data(stringify_keys(data))
|
|
390
|
+
when 404
|
|
391
|
+
Aidp.log_warn(COMPONENT, "Fixture indicates issue not found", owner: owner, repo: repo, number: number)
|
|
392
|
+
display_message("❌ Issue not found (may be private)", type: :error)
|
|
393
|
+
nil
|
|
394
|
+
when 403
|
|
395
|
+
Aidp.log_warn(COMPONENT, "Fixture indicates API rate limit", owner: owner, repo: repo, number: number)
|
|
396
|
+
display_message("❌ API rate limit exceeded", type: :error)
|
|
397
|
+
nil
|
|
398
|
+
else
|
|
399
|
+
Aidp.log_warn(COMPONENT, "Fixture indicates API error", owner: owner, repo: repo, number: number, status: status)
|
|
400
|
+
display_message("❌ GitHub API error: #{status}", type: :error)
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def stringify_keys(hash)
|
|
406
|
+
hash.each_with_object({}) do |(k, v), result|
|
|
407
|
+
result[k.to_s] = v
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
284
411
|
def perform_bootstrap(issue_data)
|
|
285
412
|
return if ENV["AIDP_DISABLE_BOOTSTRAP"] == "1"
|
|
286
413
|
return unless @enable_bootstrap
|
|
@@ -1110,6 +1110,9 @@ module Aidp
|
|
|
1110
1110
|
|
|
1111
1111
|
# Apply defaults to configuration
|
|
1112
1112
|
def self.apply_defaults(config)
|
|
1113
|
+
# Guard against non-hash config (e.g., when YAML parsing fails)
|
|
1114
|
+
return config unless config.is_a?(Hash)
|
|
1115
|
+
|
|
1113
1116
|
result = deep_dup(config)
|
|
1114
1117
|
|
|
1115
1118
|
# Apply harness defaults
|
|
@@ -108,10 +108,16 @@ module Aidp
|
|
|
108
108
|
# Last resort: try any available provider
|
|
109
109
|
next_provider = find_any_available_provider
|
|
110
110
|
if next_provider
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
# Only attempt switch if it's actually a different provider
|
|
112
|
+
if next_provider != old_provider
|
|
113
|
+
success = set_current_provider(next_provider, reason, context)
|
|
114
|
+
if success
|
|
115
|
+
log_provider_switch(old_provider, next_provider, reason, context)
|
|
116
|
+
return next_provider
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
# Same provider - no switch possible
|
|
120
|
+
Aidp.logger.debug("provider_manager", "Only provider available is current provider", provider: next_provider)
|
|
115
121
|
end
|
|
116
122
|
end
|
|
117
123
|
|
|
@@ -132,6 +138,14 @@ module Aidp
|
|
|
132
138
|
# Treat capacity/resource exhaustion like rate limit for fallback purposes
|
|
133
139
|
Aidp.logger.warn("provider_manager", "Resource/quota exhaustion detected", classified_from: error_type)
|
|
134
140
|
switch_provider("rate_limit", error_details.merge(classified_from: error_type))
|
|
141
|
+
when "empty_response"
|
|
142
|
+
# Empty response indicates provider failure, try next provider
|
|
143
|
+
Aidp.logger.warn("provider_manager", "Empty response from provider", classified_from: error_type)
|
|
144
|
+
switch_provider("provider_failure", error_details.merge(classified_from: error_type))
|
|
145
|
+
when "provider_error"
|
|
146
|
+
# Generic provider error, try next provider
|
|
147
|
+
Aidp.logger.warn("provider_manager", "Provider error detected", classified_from: error_type)
|
|
148
|
+
switch_provider("provider_failure", error_details.merge(classified_from: error_type))
|
|
135
149
|
when "authentication"
|
|
136
150
|
switch_provider("authentication_error", error_details)
|
|
137
151
|
when "network"
|
|
@@ -1504,7 +1518,13 @@ module Aidp
|
|
|
1504
1518
|
|
|
1505
1519
|
# Log provider switch
|
|
1506
1520
|
def log_provider_switch(from_provider, to_provider, reason, context)
|
|
1507
|
-
|
|
1521
|
+
if from_provider == to_provider
|
|
1522
|
+
# Same provider - this indicates no fallback was possible
|
|
1523
|
+
display_message("⚠️ Provider switch failed: #{from_provider} → #{to_provider} (#{reason})", type: :warning)
|
|
1524
|
+
display_message(" No alternative providers available", type: :warning)
|
|
1525
|
+
else
|
|
1526
|
+
display_message("🔄 Provider switch: #{from_provider} → #{to_provider} (#{reason})", type: :info)
|
|
1527
|
+
end
|
|
1508
1528
|
if context.any?
|
|
1509
1529
|
display_message(" Context: #{context.inspect}", type: :muted)
|
|
1510
1530
|
end
|
|
@@ -1513,7 +1533,31 @@ module Aidp
|
|
|
1513
1533
|
# Log no providers available
|
|
1514
1534
|
def log_no_providers_available(reason, context)
|
|
1515
1535
|
display_message("❌ No providers available for switching (#{reason})", type: :error)
|
|
1516
|
-
|
|
1536
|
+
|
|
1537
|
+
# Check if we have any fallback providers configured
|
|
1538
|
+
harness_fallbacks = if @configuration.respond_to?(:fallback_providers)
|
|
1539
|
+
Array(@configuration.fallback_providers).compact
|
|
1540
|
+
else
|
|
1541
|
+
[]
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
all_providers = configured_providers
|
|
1545
|
+
|
|
1546
|
+
if harness_fallbacks.empty? && all_providers.size <= 1
|
|
1547
|
+
display_message(" No fallback providers configured in aidp.yml", type: :warning)
|
|
1548
|
+
display_message(" 💡 Add fallback providers to enable automatic failover:", type: :info)
|
|
1549
|
+
display_message(" harness:", type: :muted)
|
|
1550
|
+
display_message(" fallback_providers:", type: :muted)
|
|
1551
|
+
display_message(" - anthropic", type: :muted)
|
|
1552
|
+
display_message(" - gemini", type: :muted)
|
|
1553
|
+
display_message(" Run 'aidp config --interactive' to configure providers", type: :info)
|
|
1554
|
+
else
|
|
1555
|
+
display_message(" All providers are rate limited, unhealthy, or circuit breaker open", type: :warning)
|
|
1556
|
+
if harness_fallbacks.any?
|
|
1557
|
+
display_message(" Configured fallbacks: #{harness_fallbacks.join(", ")}", type: :muted)
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1517
1561
|
if context.any?
|
|
1518
1562
|
display_message(" Context: #{context.inspect}", type: :muted)
|
|
1519
1563
|
end
|
|
@@ -112,6 +112,12 @@ module Aidp
|
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
Generated from template `planning/generate_llm_style_guide.md` with repository-aware adjustments.
|
|
115
|
+
|
|
116
|
+
**Note**: For comprehensive AIDP projects, consider creating both:
|
|
117
|
+
1. `STYLE_GUIDE.md` - Detailed explanations with examples and rationale
|
|
118
|
+
2. `LLM_STYLE_GUIDE.md` - Quick reference with `STYLE_GUIDE:line-range` cross-references
|
|
119
|
+
|
|
120
|
+
See `planning/generate_llm_style_guide.md` for the two-guide approach.
|
|
115
121
|
GUIDE
|
|
116
122
|
|
|
117
123
|
File.write(File.join(@project_dir, STYLE_GUIDE_PATH), content)
|
data/lib/aidp/version.rb
CHANGED
|
@@ -209,6 +209,7 @@ module Aidp
|
|
|
209
209
|
number: raw["number"],
|
|
210
210
|
title: raw["title"],
|
|
211
211
|
body: raw["body"] || "",
|
|
212
|
+
author: raw.dig("author", "login") || raw["author"],
|
|
212
213
|
comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
|
|
213
214
|
labels: Array(raw["labels"]).map { |label| label.is_a?(Hash) ? label["name"] : label },
|
|
214
215
|
state: raw["state"],
|
|
@@ -223,6 +224,7 @@ module Aidp
|
|
|
223
224
|
number: raw["number"],
|
|
224
225
|
title: raw["title"],
|
|
225
226
|
body: raw["body"] || "",
|
|
227
|
+
author: raw.dig("user", "login"),
|
|
226
228
|
comments: Array(raw["comments"]).map { |comment| normalize_comment(comment) },
|
|
227
229
|
labels: Array(raw["labels"]).map { |label| label["name"] },
|
|
228
230
|
state: raw["state"],
|
|
@@ -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
|
|
|
@@ -236,18 +236,28 @@ module Aidp
|
|
|
236
236
|
result
|
|
237
237
|
rescue => e
|
|
238
238
|
message = e.message.to_s
|
|
239
|
+
|
|
240
|
+
# Classify error type for better handling
|
|
239
241
|
classified = if message =~ /resource[_ ]exhausted/i || message =~ /\[resource_exhausted\]/i
|
|
240
242
|
"resource_exhausted"
|
|
241
243
|
elsif message =~ /quota[_ ]exceeded/i || message =~ /\[quota_exceeded\]/i
|
|
242
244
|
"quota_exceeded"
|
|
245
|
+
elsif /empty response/i.match?(message)
|
|
246
|
+
"empty_response"
|
|
247
|
+
else
|
|
248
|
+
"provider_error"
|
|
243
249
|
end
|
|
244
250
|
|
|
245
|
-
if
|
|
246
|
-
|
|
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
|
+
|
|
247
255
|
if @provider_manager.respond_to?(:switch_provider_for_error)
|
|
256
|
+
# Try to switch to fallback provider
|
|
248
257
|
switched = @provider_manager.switch_provider_for_error(classified, stderr: message)
|
|
258
|
+
|
|
249
259
|
if switched && switched != provider_name
|
|
250
|
-
display_message("↩️ Switched to provider '#{switched}'", type: :info)
|
|
260
|
+
display_message("↩️ Switched to provider '#{switched}' – retrying with same prompt", type: :info)
|
|
251
261
|
retry
|
|
252
262
|
elsif switched == provider_name
|
|
253
263
|
# ProviderManager could not advance; mark current as rate limited to encourage next attempt to move on.
|
|
@@ -256,13 +266,18 @@ module Aidp
|
|
|
256
266
|
@provider_manager.mark_rate_limited(provider_name)
|
|
257
267
|
next_provider = @provider_manager.switch_provider("rate_limit_forced", previous_error: message)
|
|
258
268
|
if next_provider && next_provider != provider_name
|
|
259
|
-
display_message("↩️ Switched to provider '#{next_provider}' (forced)", type: :info)
|
|
269
|
+
display_message("↩️ Switched to provider '#{next_provider}' (forced) – retrying with same prompt", type: :info)
|
|
260
270
|
retry
|
|
261
271
|
end
|
|
262
272
|
end
|
|
263
273
|
end
|
|
264
274
|
end
|
|
265
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
|
|
266
281
|
raise
|
|
267
282
|
end
|
|
268
283
|
end
|
|
@@ -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
|