aidp 0.15.1 ā 0.16.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/README.md +47 -0
- data/lib/aidp/analyze/error_handler.rb +14 -15
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +853 -6
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +20 -0
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/repl_macros.rb +776 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +20 -16
- data/lib/aidp/harness/provider_manager.rb +56 -49
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/init/doc_generator.rb +75 -10
- data/lib/aidp/init/project_analyzer.rb +154 -26
- data/lib/aidp/init/runner.rb +263 -10
- data/lib/aidp/jobs/background_runner.rb +15 -5
- data/lib/aidp/logger.rb +11 -0
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +220 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- metadata +17 -7
- data/lib/aidp/analyze/prioritizer.rb +0 -403
- data/lib/aidp/analyze/report_generator.rb +0 -582
- data/lib/aidp/cli/checkpoint_command.rb +0 -98
data/lib/aidp/init/runner.rb
CHANGED
|
@@ -12,20 +12,54 @@ module Aidp
|
|
|
12
12
|
class Runner
|
|
13
13
|
include Aidp::MessageDisplay
|
|
14
14
|
|
|
15
|
-
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil)
|
|
15
|
+
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil, options: {})
|
|
16
16
|
@project_dir = project_dir
|
|
17
17
|
@prompt = prompt
|
|
18
18
|
@analyzer = analyzer || ProjectAnalyzer.new(project_dir)
|
|
19
19
|
@doc_generator = doc_generator || DocGenerator.new(project_dir)
|
|
20
|
+
@options = options
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def run
|
|
23
24
|
display_message("š Running aidp init project analysis...", type: :info)
|
|
24
|
-
analysis = @analyzer.analyze
|
|
25
|
-
|
|
25
|
+
analysis = @analyzer.analyze(explain_detection: @options[:explain_detection])
|
|
26
|
+
|
|
27
|
+
if @options[:explain_detection]
|
|
28
|
+
display_detailed_analysis(analysis)
|
|
29
|
+
else
|
|
30
|
+
display_summary(analysis)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Dry run: skip preferences and generation
|
|
34
|
+
if @options[:dry_run]
|
|
35
|
+
display_message("\nš Dry run mode - no files will be written.", type: :info)
|
|
36
|
+
return {
|
|
37
|
+
analysis: analysis,
|
|
38
|
+
preferences: {},
|
|
39
|
+
generated_files: []
|
|
40
|
+
}
|
|
41
|
+
end
|
|
26
42
|
|
|
27
43
|
preferences = gather_preferences
|
|
28
44
|
|
|
45
|
+
# Offer preview before writing
|
|
46
|
+
if @options[:preview] || ask_yes_no_with_context(
|
|
47
|
+
"Preview generated files before saving?",
|
|
48
|
+
context: "Shows a summary of what will be written to docs/",
|
|
49
|
+
default: false
|
|
50
|
+
)
|
|
51
|
+
preview_generated_docs(analysis, preferences)
|
|
52
|
+
|
|
53
|
+
unless ask_yes_no("Proceed with writing these files?", default: true)
|
|
54
|
+
display_message("\nā Cancelled. No files were written.", type: :info)
|
|
55
|
+
return {
|
|
56
|
+
analysis: analysis,
|
|
57
|
+
preferences: preferences,
|
|
58
|
+
generated_files: []
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
29
63
|
@doc_generator.generate(analysis: analysis, preferences: preferences)
|
|
30
64
|
|
|
31
65
|
display_message("\nš Generated documentation:", type: :info)
|
|
@@ -52,24 +86,170 @@ module Aidp
|
|
|
52
86
|
frameworks = analysis[:frameworks]
|
|
53
87
|
tests = analysis[:test_frameworks]
|
|
54
88
|
config_files = analysis[:config_files]
|
|
89
|
+
tooling = analysis[:tooling]
|
|
90
|
+
|
|
91
|
+
# Extract high-confidence frameworks (>= 0.7)
|
|
92
|
+
confident_frameworks = frameworks.select { |f| f[:confidence] >= 0.7 }.map { |f| f[:name] }
|
|
93
|
+
uncertain_frameworks = frameworks.select { |f| f[:confidence] < 0.7 }
|
|
94
|
+
|
|
95
|
+
# Extract high-confidence test frameworks (>= 0.7)
|
|
96
|
+
confident_tests = tests.select { |t| t[:confidence] >= 0.7 }.map { |t| t[:name] }
|
|
97
|
+
|
|
98
|
+
# Extract high-confidence tooling (>= 0.7)
|
|
99
|
+
confident_tooling = tooling.select { |t| t[:confidence] >= 0.7 }.map { |t| format_tool(t[:tool]) }
|
|
55
100
|
|
|
56
101
|
display_message("\nš Repository Snapshot", type: :highlight)
|
|
57
102
|
display_message(" Languages: #{languages.empty? ? "Unknown" : languages.join(", ")}", type: :info)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
103
|
+
|
|
104
|
+
if confident_frameworks.any?
|
|
105
|
+
display_message(" Frameworks: #{confident_frameworks.join(", ")}", type: :info)
|
|
106
|
+
else
|
|
107
|
+
display_message(" Frameworks: None confidently detected", type: :info)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if uncertain_frameworks.any?
|
|
111
|
+
uncertain_list = uncertain_frameworks.map { |f| "#{f[:name]} (#{(f[:confidence] * 100).round}%)" }.join(", ")
|
|
112
|
+
display_message(" Possible frameworks: #{uncertain_list}", type: :info)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if confident_tests.any?
|
|
116
|
+
display_message(" Test suites: #{confident_tests.join(", ")}", type: :info)
|
|
117
|
+
else
|
|
118
|
+
display_message(" Test suites: Not found", type: :info)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if confident_tooling.any?
|
|
122
|
+
display_message(" Quality tools: #{confident_tooling.join(", ")}", type: :info)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
display_message(" Config files: #{config_files.empty? ? "None detected" : config_files.size} found", type: :info)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def display_detailed_analysis(analysis)
|
|
129
|
+
display_message("\nš Detailed Detection Analysis", type: :highlight)
|
|
130
|
+
display_message("=" * 60, type: :info)
|
|
131
|
+
|
|
132
|
+
# Languages
|
|
133
|
+
display_message("\nš Languages (by file size):", type: :highlight)
|
|
134
|
+
if analysis[:languages].any?
|
|
135
|
+
total_size = analysis[:languages].values.sum
|
|
136
|
+
analysis[:languages].each do |lang, size|
|
|
137
|
+
percentage = ((size.to_f / total_size) * 100).round(1)
|
|
138
|
+
display_message(" ⢠#{lang}: #{percentage}%", type: :info)
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
display_message(" None detected", type: :info)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Frameworks
|
|
145
|
+
display_message("\nšÆ Frameworks:", type: :highlight)
|
|
146
|
+
if analysis[:frameworks].any?
|
|
147
|
+
analysis[:frameworks].each do |fw|
|
|
148
|
+
confidence_pct = (fw[:confidence] * 100).round
|
|
149
|
+
display_message(" ⢠#{fw[:name]} (#{confidence_pct}% confidence):", type: :info)
|
|
150
|
+
fw[:evidence].each do |evidence|
|
|
151
|
+
display_message(" - #{evidence}", type: :info)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
display_message(" None detected", type: :info)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Test Frameworks
|
|
159
|
+
display_message("\nš§Ŗ Test Frameworks:", type: :highlight)
|
|
160
|
+
if analysis[:test_frameworks].any?
|
|
161
|
+
analysis[:test_frameworks].each do |test|
|
|
162
|
+
confidence_pct = (test[:confidence] * 100).round
|
|
163
|
+
display_message(" ⢠#{test[:name]} (#{confidence_pct}% confidence):", type: :info)
|
|
164
|
+
test[:evidence].each do |evidence|
|
|
165
|
+
display_message(" - #{evidence}", type: :info)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
display_message(" None detected", type: :info)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Tooling
|
|
173
|
+
display_message("\nš§ Quality Tooling:", type: :highlight)
|
|
174
|
+
if analysis[:tooling].any?
|
|
175
|
+
analysis[:tooling].each do |tool_data|
|
|
176
|
+
tool_name = format_tool(tool_data[:tool])
|
|
177
|
+
confidence_pct = (tool_data[:confidence] * 100).round
|
|
178
|
+
display_message(" ⢠#{tool_name} (#{confidence_pct}% confidence):", type: :info)
|
|
179
|
+
tool_data[:evidence].each do |evidence|
|
|
180
|
+
display_message(" - #{evidence}", type: :info)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
display_message(" None detected", type: :info)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Key Directories
|
|
188
|
+
display_message("\nš Key Directories:", type: :highlight)
|
|
189
|
+
if analysis[:key_directories].any?
|
|
190
|
+
analysis[:key_directories].each do |dir|
|
|
191
|
+
display_message(" ⢠#{dir}", type: :info)
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
display_message(" None detected", type: :info)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Config Files
|
|
198
|
+
display_message("\nāļø Configuration Files:", type: :highlight)
|
|
199
|
+
if analysis[:config_files].any?
|
|
200
|
+
analysis[:config_files].each do |file|
|
|
201
|
+
display_message(" ⢠#{file}", type: :info)
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
display_message(" None detected", type: :info)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Repository Stats
|
|
208
|
+
display_message("\nš Repository Stats:", type: :highlight)
|
|
209
|
+
stats = analysis[:repo_stats]
|
|
210
|
+
display_message(" ⢠Total files: #{stats[:total_files]}", type: :info)
|
|
211
|
+
display_message(" ⢠Total directories: #{stats[:total_directories]}", type: :info)
|
|
212
|
+
display_message(" ⢠Documentation: #{stats[:docs_present] ? "Present" : "Not found"}", type: :info)
|
|
213
|
+
display_message(" ⢠CI/CD config: #{stats[:has_ci_config] ? "Present" : "Not found"}", type: :info)
|
|
214
|
+
display_message(" ⢠Containerization: #{stats[:has_containerization] ? "Present" : "Not found"}", type: :info)
|
|
215
|
+
|
|
216
|
+
display_message("\n" + "=" * 60, type: :info)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def format_tool(tool)
|
|
220
|
+
tool.to_s.split("_").map(&:capitalize).join(" ")
|
|
61
221
|
end
|
|
62
222
|
|
|
63
223
|
def gather_preferences
|
|
64
|
-
display_message("\nāļø
|
|
224
|
+
display_message("\nāļø Configuration Options", type: :highlight)
|
|
225
|
+
display_message("The following questions will help customize the generated documentation.", type: :info)
|
|
226
|
+
display_message("Press Enter to accept defaults shown in brackets.\n", type: :info)
|
|
65
227
|
|
|
66
228
|
{
|
|
67
|
-
adopt_new_conventions:
|
|
68
|
-
|
|
69
|
-
|
|
229
|
+
adopt_new_conventions: ask_yes_no_with_context(
|
|
230
|
+
"Make these conventions official for this repository?",
|
|
231
|
+
context: "This saves the detected patterns to LLM_STYLE_GUIDE.md and guides future AI-assisted work.",
|
|
232
|
+
default: true
|
|
233
|
+
),
|
|
234
|
+
stricter_linters: ask_yes_no_with_context(
|
|
235
|
+
"Enable stricter linting rules in the quality plan?",
|
|
236
|
+
context: "Recommends failing CI on linting violations for better code quality.",
|
|
237
|
+
default: false
|
|
238
|
+
),
|
|
239
|
+
migrate_styles: ask_yes_no_with_context(
|
|
240
|
+
"Plan gradual migration of legacy code to new style guide?",
|
|
241
|
+
context: "Adds migration tasks to CODE_QUALITY_PLAN.md for incremental improvements.",
|
|
242
|
+
default: false
|
|
243
|
+
)
|
|
70
244
|
}
|
|
71
245
|
end
|
|
72
246
|
|
|
247
|
+
def ask_yes_no_with_context(question, context:, default:)
|
|
248
|
+
display_message("\n#{question}", type: :info)
|
|
249
|
+
display_message(" ā¹ļø #{context}", type: :info)
|
|
250
|
+
ask_yes_no(question, default: default)
|
|
251
|
+
end
|
|
252
|
+
|
|
73
253
|
def ask_yes_no(question, default:)
|
|
74
254
|
@prompt.yes?(question) do |q|
|
|
75
255
|
q.default default ? "yes" : "no"
|
|
@@ -78,6 +258,79 @@ module Aidp
|
|
|
78
258
|
# Compatibility with simplified prompts in tests (e.g. TestPrompt)
|
|
79
259
|
default
|
|
80
260
|
end
|
|
261
|
+
|
|
262
|
+
def preview_generated_docs(analysis, preferences)
|
|
263
|
+
display_message("\nš Preview of Generated Documentation", type: :highlight)
|
|
264
|
+
display_message("=" * 60, type: :info)
|
|
265
|
+
|
|
266
|
+
# LLM_STYLE_GUIDE summary
|
|
267
|
+
display_message("\n1. docs/LLM_STYLE_GUIDE.md", type: :highlight)
|
|
268
|
+
confident_frameworks = analysis[:frameworks].select { |f| f[:confidence] >= 0.7 }.map { |f| f[:name] }
|
|
269
|
+
display_message(" - Detected frameworks: #{confident_frameworks.any? ? confident_frameworks.join(", ") : "None"}", type: :info)
|
|
270
|
+
display_message(" - Adoption status: #{preferences[:adopt_new_conventions] ? "Official conventions" : "Optional reference"}", type: :info)
|
|
271
|
+
|
|
272
|
+
# PROJECT_ANALYSIS summary
|
|
273
|
+
display_message("\n2. docs/PROJECT_ANALYSIS.md", type: :highlight)
|
|
274
|
+
display_message(" - Languages: #{analysis[:languages].keys.join(", ")}", type: :info)
|
|
275
|
+
display_message(" - Total frameworks detected: #{analysis[:frameworks].size}", type: :info)
|
|
276
|
+
display_message(" - Test frameworks: #{analysis[:test_frameworks].map { |t| t[:name] }.join(", ") || "None"}", type: :info)
|
|
277
|
+
|
|
278
|
+
# CODE_QUALITY_PLAN summary
|
|
279
|
+
display_message("\n3. docs/CODE_QUALITY_PLAN.md", type: :highlight)
|
|
280
|
+
tooling_count = analysis[:tooling].count { |t| t[:confidence] >= 0.7 }
|
|
281
|
+
display_message(" - Quality tools detected: #{tooling_count}", type: :info)
|
|
282
|
+
display_message(" - Stricter linting: #{preferences[:stricter_linters] ? "Yes" : "No"}", type: :info)
|
|
283
|
+
display_message(" - Migration planning: #{preferences[:migrate_styles] ? "Yes" : "No"}", type: :info)
|
|
284
|
+
|
|
285
|
+
# Validation warnings
|
|
286
|
+
validate_tooling(analysis)
|
|
287
|
+
|
|
288
|
+
display_message("\n" + "=" * 60, type: :info)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def validate_tooling(analysis)
|
|
292
|
+
display_message("\nš Validation", type: :highlight)
|
|
293
|
+
|
|
294
|
+
warnings = []
|
|
295
|
+
|
|
296
|
+
# Check if detected tools actually exist
|
|
297
|
+
analysis[:tooling].select { |t| t[:confidence] >= 0.7 }.each do |tool_data|
|
|
298
|
+
tool_name = tool_data[:tool].to_s
|
|
299
|
+
|
|
300
|
+
# Try to find the tool command
|
|
301
|
+
tool_command = case tool_name
|
|
302
|
+
when "rubocop", "standardrb", "eslint", "prettier", "stylelint", "flake8", "black", "pytest", "jest"
|
|
303
|
+
tool_name
|
|
304
|
+
when "cargo_fmt"
|
|
305
|
+
"cargo"
|
|
306
|
+
when "gofmt"
|
|
307
|
+
"gofmt"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
next unless tool_command
|
|
311
|
+
|
|
312
|
+
# Check if command exists
|
|
313
|
+
unless system("which #{tool_command} > /dev/null 2>&1")
|
|
314
|
+
warnings << " ā ļø #{format_tool(tool_data[:tool])} detected but command '#{tool_command}' not found in PATH"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Check if test commands can be inferred
|
|
319
|
+
if analysis[:test_frameworks].empty?
|
|
320
|
+
warnings << " ā ļø No test framework detected - consider adding one for better quality assurance"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Check for CI configuration
|
|
324
|
+
unless analysis[:repo_stats][:has_ci_config]
|
|
325
|
+
warnings << " ā¹ļø No CI configuration detected - consider adding GitHub Actions or similar"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
if warnings.any?
|
|
329
|
+
warnings.each { |warning| display_message(warning, type: :info) }
|
|
330
|
+
else
|
|
331
|
+
display_message(" ā
All detected tools validated successfully", type: :success)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
81
334
|
end
|
|
82
335
|
end
|
|
83
336
|
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require "time"
|
|
6
7
|
require_relative "../rescue_logging"
|
|
8
|
+
require_relative "../concurrency"
|
|
7
9
|
|
|
8
10
|
module Aidp
|
|
9
11
|
module Jobs
|
|
@@ -69,7 +71,14 @@ module Aidp
|
|
|
69
71
|
|
|
70
72
|
# Wait for child to fork
|
|
71
73
|
Process.detach(pid)
|
|
72
|
-
|
|
74
|
+
|
|
75
|
+
# Wait for daemon to write PID file (with timeout)
|
|
76
|
+
begin
|
|
77
|
+
Aidp::Concurrency::Wait.for_file(pid_file, timeout: 5, interval: 0.05)
|
|
78
|
+
rescue Aidp::Concurrency::TimeoutError
|
|
79
|
+
# PID file not created - daemon may have failed to start
|
|
80
|
+
# Continue anyway, metadata will reflect this
|
|
81
|
+
end
|
|
73
82
|
|
|
74
83
|
# Save job metadata in parent process
|
|
75
84
|
save_job_metadata(job_id, pid, mode, options)
|
|
@@ -191,7 +200,7 @@ module Aidp
|
|
|
191
200
|
job_id: job_id,
|
|
192
201
|
pid: pid,
|
|
193
202
|
mode: mode,
|
|
194
|
-
started_at: Time.now,
|
|
203
|
+
started_at: Time.now.iso8601,
|
|
195
204
|
status: "running",
|
|
196
205
|
options: options.except(:prompt) # Don't save prompt object
|
|
197
206
|
}
|
|
@@ -203,6 +212,7 @@ module Aidp
|
|
|
203
212
|
metadata_file = File.join(@jobs_dir, job_id, "metadata.yml")
|
|
204
213
|
return nil unless File.exist?(metadata_file)
|
|
205
214
|
|
|
215
|
+
# Return raw metadata with times as ISO8601 strings to avoid unsafe class loading
|
|
206
216
|
YAML.load_file(metadata_file)
|
|
207
217
|
rescue
|
|
208
218
|
nil
|
|
@@ -220,7 +230,7 @@ module Aidp
|
|
|
220
230
|
def mark_job_completed(job_id, result)
|
|
221
231
|
update_job_metadata(job_id, {
|
|
222
232
|
status: "completed",
|
|
223
|
-
completed_at: Time.now,
|
|
233
|
+
completed_at: Time.now.iso8601,
|
|
224
234
|
result: result
|
|
225
235
|
})
|
|
226
236
|
end
|
|
@@ -228,7 +238,7 @@ module Aidp
|
|
|
228
238
|
def mark_job_failed(job_id, error)
|
|
229
239
|
update_job_metadata(job_id, {
|
|
230
240
|
status: "failed",
|
|
231
|
-
completed_at: Time.now,
|
|
241
|
+
completed_at: Time.now.iso8601,
|
|
232
242
|
error: {
|
|
233
243
|
message: error.message,
|
|
234
244
|
class: error.class.name,
|
|
@@ -240,7 +250,7 @@ module Aidp
|
|
|
240
250
|
def mark_job_stopped(job_id)
|
|
241
251
|
update_job_metadata(job_id, {
|
|
242
252
|
status: "stopped",
|
|
243
|
-
completed_at: Time.now
|
|
253
|
+
completed_at: Time.now.iso8601
|
|
244
254
|
})
|
|
245
255
|
end
|
|
246
256
|
|
data/lib/aidp/logger.rb
CHANGED
|
@@ -104,10 +104,21 @@ module Aidp
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def create_logger(path)
|
|
107
|
+
# Ensure parent directory exists before creating logger
|
|
108
|
+
dir = File.dirname(path)
|
|
109
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
110
|
+
|
|
107
111
|
logger = ::Logger.new(path, @max_files, @max_size)
|
|
108
112
|
logger.level = ::Logger::DEBUG # Control at write level instead
|
|
109
113
|
logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
|
|
110
114
|
logger
|
|
115
|
+
rescue => e
|
|
116
|
+
# Fall back to STDERR if file logging fails
|
|
117
|
+
warn "[AIDP Logger] Failed to create log file at #{path}: #{e.message}. Falling back to STDERR."
|
|
118
|
+
logger = ::Logger.new($stderr)
|
|
119
|
+
logger.level = ::Logger::DEBUG
|
|
120
|
+
logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
|
|
121
|
+
logger
|
|
111
122
|
end
|
|
112
123
|
|
|
113
124
|
def write_entry(level, component, message, metadata)
|
data/lib/aidp/providers/codex.rb
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Skills
|
|
7
|
+
# Composes skills with templates to create complete prompts
|
|
8
|
+
#
|
|
9
|
+
# The Composer combines skill content (WHO the agent is and WHAT capabilities
|
|
10
|
+
# they have) with template content (WHEN/HOW to execute a specific task).
|
|
11
|
+
#
|
|
12
|
+
# Composition structure:
|
|
13
|
+
# 1. Skill content (persona, expertise, philosophy)
|
|
14
|
+
# 2. Separator
|
|
15
|
+
# 3. Template content (task-specific instructions)
|
|
16
|
+
#
|
|
17
|
+
# @example Basic composition
|
|
18
|
+
# composer = Composer.new
|
|
19
|
+
# prompt = composer.compose(
|
|
20
|
+
# skill: repository_analyst_skill,
|
|
21
|
+
# template: "Analyze the repository..."
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @example Template-only (no skill)
|
|
25
|
+
# prompt = composer.compose(template: "Do this task...")
|
|
26
|
+
class Composer
|
|
27
|
+
# Separator between skill and template content
|
|
28
|
+
SKILL_TEMPLATE_SEPARATOR = "\n\n---\n\n"
|
|
29
|
+
|
|
30
|
+
# Compose a skill and template into a complete prompt
|
|
31
|
+
#
|
|
32
|
+
# @param skill [Skill, nil] Skill to include (optional)
|
|
33
|
+
# @param template [String] Template content
|
|
34
|
+
# @param options [Hash] Optional parameters for template variable replacement
|
|
35
|
+
# @return [String] Composed prompt
|
|
36
|
+
def compose(template:, skill: nil, options: {})
|
|
37
|
+
Aidp.log_debug(
|
|
38
|
+
"skills",
|
|
39
|
+
"Composing prompt",
|
|
40
|
+
skill_id: skill&.id,
|
|
41
|
+
template_length: template.length,
|
|
42
|
+
options_count: options.size
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Replace template variables
|
|
46
|
+
rendered_template = render_template(template, options: options)
|
|
47
|
+
|
|
48
|
+
# If no skill, return template only
|
|
49
|
+
unless skill
|
|
50
|
+
Aidp.log_debug("skills", "Template-only composition", template_length: rendered_template.length)
|
|
51
|
+
return rendered_template
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Compose skill + template
|
|
55
|
+
composed = [
|
|
56
|
+
skill.content,
|
|
57
|
+
SKILL_TEMPLATE_SEPARATOR,
|
|
58
|
+
"# Current Task",
|
|
59
|
+
"",
|
|
60
|
+
rendered_template
|
|
61
|
+
].join("\n")
|
|
62
|
+
|
|
63
|
+
Aidp.log_debug(
|
|
64
|
+
"skills",
|
|
65
|
+
"Composed prompt with skill",
|
|
66
|
+
skill_id: skill.id,
|
|
67
|
+
total_length: composed.length,
|
|
68
|
+
skill_length: skill.content.length,
|
|
69
|
+
template_length: rendered_template.length
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
composed
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Render a template with variable substitution
|
|
76
|
+
#
|
|
77
|
+
# Replaces {{variable}} placeholders with values from options hash
|
|
78
|
+
#
|
|
79
|
+
# @param template [String] Template content
|
|
80
|
+
# @param options [Hash] Variable values for substitution
|
|
81
|
+
# @return [String] Rendered template
|
|
82
|
+
def render_template(template, options: {})
|
|
83
|
+
return template if options.empty?
|
|
84
|
+
|
|
85
|
+
rendered = template.dup
|
|
86
|
+
|
|
87
|
+
options.each do |key, value|
|
|
88
|
+
placeholder = "{{#{key}}}"
|
|
89
|
+
rendered = rendered.gsub(placeholder, value.to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Log if there are unreplaced placeholders
|
|
93
|
+
remaining_placeholders = extract_placeholders(rendered)
|
|
94
|
+
if remaining_placeholders.any?
|
|
95
|
+
Aidp.log_warn(
|
|
96
|
+
"skills",
|
|
97
|
+
"Unreplaced template variables",
|
|
98
|
+
placeholders: remaining_placeholders
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
rendered
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Compose multiple skills with a template
|
|
106
|
+
#
|
|
107
|
+
# Note: This is for future use when skill composition is supported.
|
|
108
|
+
# Currently raises an error as it's not implemented in v1.
|
|
109
|
+
#
|
|
110
|
+
# @param skills [Array<Skill>] Skills to compose
|
|
111
|
+
# @param template [String] Template content
|
|
112
|
+
# @param options [Hash] Template variables
|
|
113
|
+
# @return [String] Composed prompt
|
|
114
|
+
# @raise [NotImplementedError] Skill composition not yet supported
|
|
115
|
+
def compose_multiple(skills:, template:, options: {})
|
|
116
|
+
raise NotImplementedError, "Multiple skill composition not yet supported in v1"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Preview what a composed prompt would look like
|
|
120
|
+
#
|
|
121
|
+
# Returns a hash with skill content, template content, and full composition
|
|
122
|
+
# for inspection without executing.
|
|
123
|
+
#
|
|
124
|
+
# @param skill [Skill, nil] Skill to include
|
|
125
|
+
# @param template [String] Template content
|
|
126
|
+
# @param options [Hash] Template variables
|
|
127
|
+
# @return [Hash] Preview with :skill, :template, :composed, :metadata
|
|
128
|
+
def preview(template:, skill: nil, options: {})
|
|
129
|
+
rendered_template = render_template(template, options: options)
|
|
130
|
+
composed = compose(skill: skill, template: template, options: options)
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
skill: skill ? {
|
|
134
|
+
id: skill.id,
|
|
135
|
+
name: skill.name,
|
|
136
|
+
content: skill.content,
|
|
137
|
+
length: skill.content.length
|
|
138
|
+
} : nil,
|
|
139
|
+
template: {
|
|
140
|
+
content: rendered_template,
|
|
141
|
+
length: rendered_template.length,
|
|
142
|
+
variables: options.keys
|
|
143
|
+
},
|
|
144
|
+
composed: {
|
|
145
|
+
content: composed,
|
|
146
|
+
length: composed.length
|
|
147
|
+
},
|
|
148
|
+
metadata: {
|
|
149
|
+
has_skill: !skill.nil?,
|
|
150
|
+
separator_used: !skill.nil?,
|
|
151
|
+
unreplaced_vars: extract_placeholders(composed)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def extract_placeholders(text)
|
|
159
|
+
return [] if text.nil? || text.empty?
|
|
160
|
+
|
|
161
|
+
scanner = StringScanner.new(text)
|
|
162
|
+
placeholders = []
|
|
163
|
+
|
|
164
|
+
while scanner.skip_until(/\{\{/)
|
|
165
|
+
fragment = scanner.scan_until(/\}\}/)
|
|
166
|
+
break unless fragment
|
|
167
|
+
|
|
168
|
+
placeholder = fragment[0...-2]
|
|
169
|
+
next if placeholder.nil? || placeholder.empty? || placeholder.include?("{")
|
|
170
|
+
|
|
171
|
+
placeholders << placeholder
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
placeholders
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|