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
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
|
data/lib/aidp/cli.rb
CHANGED
|
@@ -146,6 +146,16 @@ module Aidp
|
|
|
146
146
|
|
|
147
147
|
class << self
|
|
148
148
|
extend Aidp::MessageDisplay::ClassMethods
|
|
149
|
+
extend Aidp::RescueLogging
|
|
150
|
+
|
|
151
|
+
# Explicit singleton delegator (defensive: ensure availability even if extend fails to attach)
|
|
152
|
+
def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
153
|
+
Aidp::RescueLogging.log_rescue(error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Store last parsed options for access by UI components (e.g., verbose flag)
|
|
157
|
+
@last_options = nil
|
|
158
|
+
attr_accessor :last_options
|
|
149
159
|
|
|
150
160
|
def create_prompt
|
|
151
161
|
::TTY::Prompt.new
|
|
@@ -156,6 +166,7 @@ module Aidp
|
|
|
156
166
|
return run_subcommand(args) if subcommand?(args)
|
|
157
167
|
|
|
158
168
|
options = parse_options(args)
|
|
169
|
+
self.last_options = options
|
|
159
170
|
|
|
160
171
|
if options[:help]
|
|
161
172
|
display_message(options[:parser].to_s, type: :info)
|
|
@@ -324,6 +335,7 @@ module Aidp
|
|
|
324
335
|
opts.on("-h", "--help", "Show this help message") { options[:help] = true }
|
|
325
336
|
opts.on("-v", "--version", "Show version information") { options[:version] = true }
|
|
326
337
|
opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
|
|
338
|
+
opts.on("--verbose", "Show detailed prompts and raw provider responses during guided workflow") { options[:verbose] = true }
|
|
327
339
|
|
|
328
340
|
opts.separator ""
|
|
329
341
|
opts.separator "Examples:"
|
|
@@ -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
|
|
@@ -18,21 +18,27 @@ module Aidp
|
|
|
18
18
|
# Callers should set skip_persistence: true for test/dry-run scenarios
|
|
19
19
|
@skip_persistence = skip_persistence
|
|
20
20
|
ensure_state_directory
|
|
21
|
+
Aidp.log_debug("state_persistence", "initialized", mode: @mode, skip: @skip_persistence, dir: @state_dir)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def has_state?
|
|
24
25
|
return false if @skip_persistence
|
|
25
|
-
File.exist?(@state_file)
|
|
26
|
+
exists = File.exist?(@state_file)
|
|
27
|
+
Aidp.log_debug("state_persistence", "has_state?", exists: exists, file: @state_file) if exists
|
|
28
|
+
exists
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def load_state
|
|
29
32
|
return {} if @skip_persistence || !has_state?
|
|
30
33
|
|
|
31
34
|
with_lock do
|
|
35
|
+
Aidp.log_debug("state_persistence", "load_state.start", file: @state_file)
|
|
32
36
|
content = File.read(@state_file)
|
|
33
|
-
JSON.parse(content, symbolize_names: true)
|
|
37
|
+
parsed = JSON.parse(content, symbolize_names: true)
|
|
38
|
+
Aidp.log_debug("state_persistence", "load_state.success", keys: parsed.keys.size, file: @state_file)
|
|
39
|
+
parsed
|
|
34
40
|
rescue JSON::ParserError => e
|
|
35
|
-
|
|
41
|
+
Aidp.log_warn("state_persistence", "parse_error", error: e.message, file: @state_file)
|
|
36
42
|
{}
|
|
37
43
|
end
|
|
38
44
|
end
|
|
@@ -41,8 +47,10 @@ module Aidp
|
|
|
41
47
|
return if @skip_persistence
|
|
42
48
|
|
|
43
49
|
with_lock do
|
|
50
|
+
Aidp.log_debug("state_persistence", "save_state.start", keys: state_data.keys.size)
|
|
44
51
|
state_with_metadata = add_metadata(state_data)
|
|
45
52
|
write_atomically(state_with_metadata)
|
|
53
|
+
Aidp.log_debug("state_persistence", "save_state.written", file: @state_file, size: state_with_metadata.keys.size)
|
|
46
54
|
end
|
|
47
55
|
end
|
|
48
56
|
|
|
@@ -50,7 +58,9 @@ module Aidp
|
|
|
50
58
|
return if @skip_persistence
|
|
51
59
|
|
|
52
60
|
with_lock do
|
|
61
|
+
Aidp.log_debug("state_persistence", "clear_state.start", file: @state_file)
|
|
53
62
|
File.delete(@state_file) if File.exist?(@state_file)
|
|
63
|
+
Aidp.log_debug("state_persistence", "clear_state.done", file: @state_file)
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
|
|
@@ -66,8 +76,10 @@ module Aidp
|
|
|
66
76
|
|
|
67
77
|
def write_atomically(state_with_metadata)
|
|
68
78
|
temp_file = "#{@state_file}.tmp"
|
|
79
|
+
Aidp.log_debug("state_persistence", "write_atomically.start", temp: temp_file)
|
|
69
80
|
File.write(temp_file, JSON.pretty_generate(state_with_metadata))
|
|
70
81
|
File.rename(temp_file, @state_file)
|
|
82
|
+
Aidp.log_debug("state_persistence", "write_atomically.rename", file: @state_file)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
def ensure_state_directory
|
|
@@ -76,45 +88,50 @@ module Aidp
|
|
|
76
88
|
|
|
77
89
|
def with_lock(&block)
|
|
78
90
|
return yield if @skip_persistence
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
result = acquire_lock_with_timeout(&block)
|
|
92
|
+
result
|
|
81
93
|
ensure
|
|
82
94
|
cleanup_lock_file
|
|
83
95
|
end
|
|
84
96
|
|
|
85
97
|
def acquire_lock_with_timeout(&block)
|
|
86
|
-
|
|
87
|
-
timeout = 30
|
|
98
|
+
timeout = ENV["AIDP_STATE_LOCK_TIMEOUT"]&.to_f || ((ENV["RSPEC_RUNNING"] == "true") ? 1.0 : 30.0)
|
|
88
99
|
start_time = Time.now
|
|
89
|
-
|
|
100
|
+
attempt_result = nil
|
|
90
101
|
while (Time.now - start_time) < timeout
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
acquired, attempt_result = try_acquire_lock(&block)
|
|
103
|
+
return attempt_result if acquired
|
|
93
104
|
sleep_briefly
|
|
94
105
|
end
|
|
95
|
-
|
|
96
|
-
raise_lock_timeout_error unless lock_acquired
|
|
106
|
+
raise_lock_timeout_error(timeout)
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
def try_acquire_lock(&block)
|
|
100
110
|
File.open(@lock_file, File::CREAT | File::EXCL | File::WRONLY) do |_lock|
|
|
101
|
-
|
|
102
|
-
true
|
|
111
|
+
Aidp.log_debug("state_persistence", "lock.acquired", file: @lock_file)
|
|
112
|
+
[true, yield]
|
|
103
113
|
end
|
|
104
114
|
rescue Errno::EEXIST
|
|
105
|
-
|
|
115
|
+
Aidp.log_debug("state_persistence", "lock.busy", file: @lock_file)
|
|
116
|
+
[false, nil]
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
def sleep_briefly
|
|
109
|
-
sleep(0.
|
|
120
|
+
sleep(ENV["AIDP_STATE_LOCK_SLEEP"]&.to_f || 0.05)
|
|
110
121
|
end
|
|
111
122
|
|
|
112
|
-
def raise_lock_timeout_error
|
|
113
|
-
|
|
123
|
+
def raise_lock_timeout_error(timeout)
|
|
124
|
+
# Prefer explicit error class; fall back if not defined yet
|
|
125
|
+
error_class = defined?(Aidp::Errors::StateError) ? Aidp::Errors::StateError : RuntimeError
|
|
126
|
+
Aidp.log_error("state_persistence", "lock.timeout", file: @lock_file, waited: timeout)
|
|
127
|
+
raise error_class, "Could not acquire state lock within #{timeout} seconds"
|
|
114
128
|
end
|
|
115
129
|
|
|
116
130
|
def cleanup_lock_file
|
|
117
|
-
|
|
131
|
+
if File.exist?(@lock_file)
|
|
132
|
+
File.delete(@lock_file)
|
|
133
|
+
Aidp.log_debug("state_persistence", "lock.cleaned", file: @lock_file)
|
|
134
|
+
end
|
|
118
135
|
end
|
|
119
136
|
end
|
|
120
137
|
end
|
|
@@ -251,7 +251,10 @@ module Aidp
|
|
|
251
251
|
def select_guided_workflow
|
|
252
252
|
# Use the guided agent to help user select workflow
|
|
253
253
|
# Don't pass prompt so it uses EnhancedInput with full readline support
|
|
254
|
-
|
|
254
|
+
verbose_flag = (defined?(Aidp::CLI) && Aidp::CLI.respond_to?(:last_options) && Aidp::CLI.last_options) ? Aidp::CLI.last_options[:verbose] : false
|
|
255
|
+
# Fallback: store verbose in an env for easier access if options not available
|
|
256
|
+
verbose = verbose_flag || ENV["AIDP_VERBOSE"] == "1"
|
|
257
|
+
guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, verbose: verbose)
|
|
255
258
|
result = guided_agent.select_workflow
|
|
256
259
|
|
|
257
260
|
# Store user input for later use
|
|
@@ -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/logger.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Aidp
|
|
|
33
33
|
attr_reader :level, :json_format
|
|
34
34
|
|
|
35
35
|
def initialize(project_dir = Dir.pwd, config = {})
|
|
36
|
-
@project_dir = project_dir
|
|
36
|
+
@project_dir = sanitize_project_dir(project_dir)
|
|
37
37
|
@config = config
|
|
38
38
|
@level = determine_log_level
|
|
39
39
|
@json_format = config[:json] || false
|
|
@@ -195,6 +195,18 @@ module Aidp
|
|
|
195
195
|
def redact_hash(hash)
|
|
196
196
|
hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
|
|
197
197
|
end
|
|
198
|
+
|
|
199
|
+
# Guard against accidentally passing stream sentinel strings or invalid characters
|
|
200
|
+
# that would create odd top-level directories like "<STDERR>".
|
|
201
|
+
def sanitize_project_dir(dir)
|
|
202
|
+
return Dir.pwd if dir.nil?
|
|
203
|
+
str = dir.to_s
|
|
204
|
+
if str.empty? || str.match?(/[<>|]/) || str.match?(/[\x00-\x1F]/)
|
|
205
|
+
Kernel.warn "[AIDP Logger] Invalid project_dir '#{str}' - falling back to #{Dir.pwd}"
|
|
206
|
+
return Dir.pwd
|
|
207
|
+
end
|
|
208
|
+
str
|
|
209
|
+
end
|
|
198
210
|
end
|
|
199
211
|
|
|
200
212
|
# Module-level logger accessor
|
data/lib/aidp/rescue_logging.rb
CHANGED
|
@@ -13,24 +13,35 @@ module Aidp
|
|
|
13
13
|
# - includes error class, message
|
|
14
14
|
# - optional fallback and extra context hash merged in
|
|
15
15
|
module RescueLogging
|
|
16
|
+
# Instance-level helper (made public so extend works for singleton contexts)
|
|
16
17
|
def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
18
|
+
Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Module-level access (Aidp::RescueLogging.log_rescue) for direct calls if desired
|
|
22
|
+
def self.log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
|
|
23
|
+
Aidp::RescueLogging.__log_rescue_impl(self, error, component: component, action: action, fallback: fallback, level: level, **context)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Internal implementation shared by instance & module forms
|
|
27
|
+
def self.__log_rescue_impl(context_object, error, component:, action:, fallback:, level:, **extra)
|
|
17
28
|
data = {
|
|
18
29
|
error_class: error.class.name,
|
|
19
30
|
error_message: error.message,
|
|
20
31
|
action: action
|
|
21
32
|
}
|
|
22
33
|
data[:fallback] = fallback if fallback
|
|
23
|
-
data.merge!(
|
|
34
|
+
data.merge!(extra) unless extra.empty?
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
begin
|
|
37
|
+
if context_object.respond_to?(:debug_log)
|
|
38
|
+
context_object.debug_log("⚠️ Rescue in #{component}: #{action}", level: level, data: data)
|
|
39
|
+
else
|
|
40
|
+
Aidp.logger.send(level, component, "Rescued exception during #{action}", **data)
|
|
41
|
+
end
|
|
42
|
+
rescue => logging_error
|
|
43
|
+
warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
|
|
30
44
|
end
|
|
31
|
-
rescue => logging_error
|
|
32
|
-
# Last resort: avoid raising from logging path - fall back to STDERR
|
|
33
|
-
warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
|
|
34
45
|
end
|
|
35
46
|
end
|
|
36
47
|
end
|