aidp 0.39.8 → 0.40.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 +32 -1
- data/lib/aidp/cli.rb +1 -0
- data/lib/aidp/harness/config_manager.rb +34 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -2
- data/lib/aidp/jobs/background_runner.rb +76 -10
- data/lib/aidp/setup/wizard.rb +33 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +136 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2414ec5ab6a21100ba8ccb1d6ae8ddc6bb137a5403b069ff941707c17c9ce9fa
|
|
4
|
+
data.tar.gz: fb0401b3daf671719690b5c5347418eab335e8090dac38ebe08aef43a921430d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c033c034fed453eb4daa2108bb653f3da5007e0d49a792d4a3d9b4f976d63de3c796647da98d3131ea4b81d41425b7aea19ab4e94e743b2eb59936eea9dcad59
|
|
7
|
+
data.tar.gz: 70ebc3d5e2f809e21398321b7b26c315309a849a35ab4be5742d58650a123a4826c69e64c0b9e8fbc0aef7402da7a99a9970c0d49e83e516aab436e57187d974
|
data/README.md
CHANGED
|
@@ -42,6 +42,37 @@ You can re-run the wizard manually:
|
|
|
42
42
|
aidp --setup-config
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Run With Docker
|
|
46
|
+
|
|
47
|
+
Use the standalone Docker image when you want Aidp without a local Ruby
|
|
48
|
+
toolchain. Build the image once, then mount any project into `/workspace`.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
docker build -t aidp:local .
|
|
52
|
+
|
|
53
|
+
docker run --rm -it \
|
|
54
|
+
--env-file .env \
|
|
55
|
+
-v "$(pwd)":/workspace \
|
|
56
|
+
-v "$(pwd)/.aidp":/workspace/.aidp \
|
|
57
|
+
aidp:local
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Watch mode uses the same image:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
docker run --rm -it \
|
|
64
|
+
--env-file .env \
|
|
65
|
+
-v "$(pwd)":/workspace \
|
|
66
|
+
-v "$(pwd)/.aidp":/workspace/.aidp \
|
|
67
|
+
aidp:local watch https://github.com/<owner>/<repo>/issues
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If you supply `GH_TOKEN` or `GITHUB_TOKEN`, the image can use the bundled `gh`
|
|
71
|
+
CLI for read-only access to private repos. Public repositories continue to work
|
|
72
|
+
without GitHub credentials. See [docs/DOCKER_QUICKSTART.md](docs/DOCKER_QUICKSTART.md)
|
|
73
|
+
for environment variables, wrapper scripts, network options, and multi-arch
|
|
74
|
+
`buildx` examples.
|
|
75
|
+
|
|
45
76
|
## Devcontainer Support
|
|
46
77
|
|
|
47
78
|
AIDP provides first-class devcontainer support for sandboxed, secure AI agent execution. Devcontainers offer:
|
|
@@ -116,7 +147,7 @@ aidp devcontainer list-backups
|
|
|
116
147
|
aidp devcontainer restore 0
|
|
117
148
|
```
|
|
118
149
|
|
|
119
|
-
See [docs/how-to/DEVELOPMENT_CONTAINER.md](docs/how-to/DEVELOPMENT_CONTAINER.md) for complete devcontainer management documentation.
|
|
150
|
+
See [docs/how-to/DEVELOPMENT_CONTAINER.md](docs/how-to/DEVELOPMENT_CONTAINER.md) for complete devcontainer management documentation. If you do not want VS Code or a local Ruby toolchain, see [Run With Docker](#run-with-docker).
|
|
120
151
|
|
|
121
152
|
### Devcontainer Detection
|
|
122
153
|
|
data/lib/aidp/cli.rb
CHANGED
|
@@ -347,6 +347,7 @@ module Aidp
|
|
|
347
347
|
opts.on("-v", "--version", "Show version information") { options[:version] = true }
|
|
348
348
|
opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
|
|
349
349
|
opts.on("--verbose", "Show detailed prompts and raw provider responses during guided workflow") { options[:verbose] = true }
|
|
350
|
+
opts.on("--style STYLE", "Override PRD style for guided planning (detailed, balanced, quick_sketch)") { |style| options[:style] = style }
|
|
350
351
|
opts.on("--quiet", "Suppress non-critical output (incompatible with --verbose and --interactive)") { options[:quiet] = true }
|
|
351
352
|
# Undocumented: Quick exit launch test for CI/CD validation
|
|
352
353
|
opts.on("--launch-test", nil) { options[:launch_test] = true }
|
|
@@ -10,6 +10,16 @@ module Aidp
|
|
|
10
10
|
class ConfigManager
|
|
11
11
|
include ProviderTypeChecker
|
|
12
12
|
|
|
13
|
+
DEFAULT_PRD_GENERATION = {
|
|
14
|
+
interaction_style: "balanced",
|
|
15
|
+
default_style: "balanced",
|
|
16
|
+
max_question_rounds: {
|
|
17
|
+
detailed: 30,
|
|
18
|
+
balanced: 10,
|
|
19
|
+
quick_sketch: 3
|
|
20
|
+
}
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
13
23
|
attr_reader :project_dir
|
|
14
24
|
|
|
15
25
|
def initialize(project_dir = Dir.pwd)
|
|
@@ -195,6 +205,24 @@ module Aidp
|
|
|
195
205
|
}
|
|
196
206
|
end
|
|
197
207
|
|
|
208
|
+
def prd_generation_config(options = {})
|
|
209
|
+
config = config(options)
|
|
210
|
+
raw_config = config&.dig(:prd_generation) || config&.dig("prd_generation") || {}
|
|
211
|
+
raw_rounds = raw_config[:max_question_rounds] || raw_config["max_question_rounds"] || {}
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
interaction_style: raw_config[:interaction_style] || raw_config["interaction_style"] || DEFAULT_PRD_GENERATION[:interaction_style],
|
|
215
|
+
default_style: raw_config[:default_style] || raw_config["default_style"] || DEFAULT_PRD_GENERATION[:default_style],
|
|
216
|
+
max_question_rounds: DEFAULT_PRD_GENERATION[:max_question_rounds].merge(
|
|
217
|
+
symbolize_keys(raw_rounds)
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def prd_interaction_style(options = {})
|
|
223
|
+
prd_generation_config(options)[:interaction_style]
|
|
224
|
+
end
|
|
225
|
+
|
|
198
226
|
# Get provider models
|
|
199
227
|
def provider_models(provider_name, options = {})
|
|
200
228
|
provider_config = provider_config(provider_name, options)
|
|
@@ -351,6 +379,12 @@ module Aidp
|
|
|
351
379
|
|
|
352
380
|
private
|
|
353
381
|
|
|
382
|
+
def symbolize_keys(hash)
|
|
383
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
384
|
+
memo[key.to_sym] = value
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
354
388
|
def load_config_with_options(options)
|
|
355
389
|
# Apply different loading strategies based on options
|
|
356
390
|
if options[:mode]
|
|
@@ -237,10 +237,12 @@ module Aidp
|
|
|
237
237
|
def select_guided_workflow
|
|
238
238
|
# Use the guided agent to help user select workflow
|
|
239
239
|
# Don't pass prompt so it uses EnhancedInput with full readline support
|
|
240
|
-
|
|
240
|
+
cli_options = (defined?(Aidp::CLI) && Aidp::CLI.respond_to?(:last_options) && Aidp::CLI.last_options) ? Aidp::CLI.last_options : {}
|
|
241
|
+
verbose_flag = cli_options[:verbose]
|
|
242
|
+
interaction_style = cli_options[:style]
|
|
241
243
|
# Fallback: store verbose in an env for easier access if options not available
|
|
242
244
|
verbose = verbose_flag || ENV["AIDP_VERBOSE"] == "1"
|
|
243
|
-
guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, verbose: verbose)
|
|
245
|
+
guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, verbose: verbose, interaction_style: interaction_style)
|
|
244
246
|
result = guided_agent.select_workflow
|
|
245
247
|
|
|
246
248
|
# Store user input for later use
|
|
@@ -13,6 +13,8 @@ module Aidp
|
|
|
13
13
|
# Manages background execution of work loops
|
|
14
14
|
# Runs harness in daemon process and tracks job metadata
|
|
15
15
|
class BackgroundRunner
|
|
16
|
+
StartError = Class.new(StandardError)
|
|
17
|
+
|
|
16
18
|
include Aidp::MessageDisplay
|
|
17
19
|
include Aidp::RescueLogging
|
|
18
20
|
include Aidp::SafeDirectory
|
|
@@ -34,9 +36,11 @@ module Aidp
|
|
|
34
36
|
# Start a background job
|
|
35
37
|
# Returns job_id
|
|
36
38
|
def start(mode, options = {})
|
|
39
|
+
ensure_jobs_directory
|
|
37
40
|
job_id = generate_job_id
|
|
38
|
-
log_file =
|
|
39
|
-
pid_file =
|
|
41
|
+
log_file = job_file_path(job_id, "output.log")
|
|
42
|
+
pid_file = job_file_path(job_id, "job.pid")
|
|
43
|
+
raise_start_error unless log_file && pid_file
|
|
40
44
|
|
|
41
45
|
# Create job directory
|
|
42
46
|
FileUtils.mkdir_p(File.dirname(log_file))
|
|
@@ -109,6 +113,9 @@ module Aidp
|
|
|
109
113
|
metadata = load_job_metadata(job_id)
|
|
110
114
|
return nil unless metadata
|
|
111
115
|
|
|
116
|
+
log_file = job_log_path(job_id)
|
|
117
|
+
return nil unless log_file
|
|
118
|
+
|
|
112
119
|
# Check if process is still running
|
|
113
120
|
pid = metadata[:pid]
|
|
114
121
|
running = pid && process_running?(pid)
|
|
@@ -125,7 +132,7 @@ module Aidp
|
|
|
125
132
|
started_at: metadata[:started_at],
|
|
126
133
|
completed_at: metadata[:completed_at],
|
|
127
134
|
checkpoint: checkpoint,
|
|
128
|
-
log_file:
|
|
135
|
+
log_file: log_file
|
|
129
136
|
}
|
|
130
137
|
end
|
|
131
138
|
|
|
@@ -169,12 +176,12 @@ module Aidp
|
|
|
169
176
|
|
|
170
177
|
# Get job logs
|
|
171
178
|
def job_logs(job_id, options = {})
|
|
172
|
-
log_file =
|
|
179
|
+
log_file = job_log_path(job_id)
|
|
180
|
+
return nil unless log_file
|
|
173
181
|
return nil unless File.exist?(log_file)
|
|
174
182
|
|
|
175
183
|
if options[:tail]
|
|
176
|
-
|
|
177
|
-
`tail -n #{lines} #{log_file}`
|
|
184
|
+
tail_job_logs(log_file, options[:lines])
|
|
178
185
|
else
|
|
179
186
|
File.read(log_file)
|
|
180
187
|
end
|
|
@@ -182,7 +189,8 @@ module Aidp
|
|
|
182
189
|
|
|
183
190
|
# Follow job logs in real-time
|
|
184
191
|
def follow_job_logs(job_id)
|
|
185
|
-
log_file =
|
|
192
|
+
log_file = job_log_path(job_id)
|
|
193
|
+
return unless log_file
|
|
186
194
|
return unless File.exist?(log_file)
|
|
187
195
|
|
|
188
196
|
# Use tail -f to follow logs
|
|
@@ -195,6 +203,10 @@ module Aidp
|
|
|
195
203
|
@jobs_dir = safe_mkdir_p(@jobs_dir, component_name: "BackgroundRunner")
|
|
196
204
|
end
|
|
197
205
|
|
|
206
|
+
def raise_start_error
|
|
207
|
+
raise StartError, "Unable to create or access background job directory: #{@jobs_dir}"
|
|
208
|
+
end
|
|
209
|
+
|
|
198
210
|
def generate_job_id
|
|
199
211
|
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
200
212
|
random = SecureRandom.hex(4)
|
|
@@ -202,7 +214,8 @@ module Aidp
|
|
|
202
214
|
end
|
|
203
215
|
|
|
204
216
|
def save_job_metadata(job_id, pid, mode, options)
|
|
205
|
-
metadata_file =
|
|
217
|
+
metadata_file = job_metadata_path(job_id)
|
|
218
|
+
return unless metadata_file
|
|
206
219
|
|
|
207
220
|
metadata = {
|
|
208
221
|
job_id: job_id,
|
|
@@ -217,7 +230,8 @@ module Aidp
|
|
|
217
230
|
end
|
|
218
231
|
|
|
219
232
|
def load_job_metadata(job_id)
|
|
220
|
-
metadata_file =
|
|
233
|
+
metadata_file = job_metadata_path(job_id)
|
|
234
|
+
return nil unless metadata_file
|
|
221
235
|
return nil unless File.exist?(metadata_file)
|
|
222
236
|
|
|
223
237
|
# Return raw metadata with times as ISO8601 strings to avoid unsafe class loading
|
|
@@ -231,7 +245,8 @@ module Aidp
|
|
|
231
245
|
return unless metadata
|
|
232
246
|
|
|
233
247
|
metadata.merge!(updates)
|
|
234
|
-
metadata_file =
|
|
248
|
+
metadata_file = job_metadata_path(job_id)
|
|
249
|
+
return unless metadata_file
|
|
235
250
|
File.write(metadata_file, metadata.to_yaml)
|
|
236
251
|
end
|
|
237
252
|
|
|
@@ -298,6 +313,57 @@ module Aidp
|
|
|
298
313
|
metadata[:status] || "completed"
|
|
299
314
|
end
|
|
300
315
|
end
|
|
316
|
+
|
|
317
|
+
def job_log_path(job_id)
|
|
318
|
+
job_file_path(job_id, "output.log")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def job_metadata_path(job_id)
|
|
322
|
+
job_file_path(job_id, "metadata.yml")
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def job_file_path(job_id, file_name)
|
|
326
|
+
return unless Dir.exist?(@jobs_dir)
|
|
327
|
+
|
|
328
|
+
jobs_dir = File.realpath(@jobs_dir)
|
|
329
|
+
resolved_job_dir = resolved_job_dir_path(jobs_dir, job_id)
|
|
330
|
+
return unless resolved_job_dir
|
|
331
|
+
return unless resolved_job_dir.start_with?("#{jobs_dir}/")
|
|
332
|
+
|
|
333
|
+
resolved_job_file_path(resolved_job_dir, file_name)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def tail_job_logs(log_file, requested_lines)
|
|
337
|
+
lines = Integer(requested_lines || 50, exception: false)
|
|
338
|
+
lines = 50 unless lines&.positive?
|
|
339
|
+
|
|
340
|
+
IO.popen(["tail", "-n", lines.to_s, log_file], &:read)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def resolved_job_dir_path(jobs_dir, job_id)
|
|
344
|
+
job_dir = File.join(@jobs_dir, job_id.to_s)
|
|
345
|
+
|
|
346
|
+
if File.exist?(job_dir)
|
|
347
|
+
File.realpath(job_dir)
|
|
348
|
+
else
|
|
349
|
+
File.join(jobs_dir, job_id.to_s)
|
|
350
|
+
end
|
|
351
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def resolved_job_file_path(job_dir, file_name)
|
|
356
|
+
candidate = File.join(job_dir, file_name)
|
|
357
|
+
return candidate unless File.exist?(candidate) || File.symlink?(candidate)
|
|
358
|
+
return if File.symlink?(candidate)
|
|
359
|
+
|
|
360
|
+
resolved_file = File.realpath(candidate)
|
|
361
|
+
return unless resolved_file.start_with?("#{job_dir}/")
|
|
362
|
+
|
|
363
|
+
resolved_file
|
|
364
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
301
367
|
end
|
|
302
368
|
end
|
|
303
369
|
end
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -23,6 +23,16 @@ module Aidp
|
|
|
23
23
|
LEGACY_TIER_ALIASES = {
|
|
24
24
|
advanced: :pro
|
|
25
25
|
}.freeze
|
|
26
|
+
PRD_STYLE_OPTIONS = {
|
|
27
|
+
"Detailed - deep questioning, longer sessions" => "detailed",
|
|
28
|
+
"Balanced - moderate questioning (recommended)" => "balanced",
|
|
29
|
+
"Quick Sketch - minimal questions, Aidp fills gaps automatically" => "quick_sketch"
|
|
30
|
+
}.freeze
|
|
31
|
+
DEFAULT_PRD_QUESTION_ROUNDS = {
|
|
32
|
+
detailed: 30,
|
|
33
|
+
balanced: 10,
|
|
34
|
+
quick_sketch: 3
|
|
35
|
+
}.freeze
|
|
26
36
|
|
|
27
37
|
attr_reader :project_dir, :prompt, :dry_run
|
|
28
38
|
# Expose state for testability
|
|
@@ -49,6 +59,7 @@ module Aidp
|
|
|
49
59
|
configure_harness_settings
|
|
50
60
|
configure_thinking_tiers
|
|
51
61
|
configure_work_loop
|
|
62
|
+
configure_prd_generation
|
|
52
63
|
configure_branching
|
|
53
64
|
configure_artifacts
|
|
54
65
|
configure_nfrs
|
|
@@ -492,6 +503,27 @@ module Aidp
|
|
|
492
503
|
configure_vcs_behavior
|
|
493
504
|
end
|
|
494
505
|
|
|
506
|
+
def configure_prd_generation
|
|
507
|
+
prompt.say("\n🧭 PRD generation style")
|
|
508
|
+
prompt.say("-" * 40)
|
|
509
|
+
|
|
510
|
+
existing = get(%i[prd_generation]) || {}
|
|
511
|
+
current_style = existing[:interaction_style] || existing[:default_style] || "balanced"
|
|
512
|
+
default_label = PRD_STYLE_OPTIONS.key(current_style) || PRD_STYLE_OPTIONS.key("balanced")
|
|
513
|
+
|
|
514
|
+
selected_style = prompt.select("Choose PRD interaction style:", default: default_label) do |menu|
|
|
515
|
+
PRD_STYLE_OPTIONS.each do |label, value|
|
|
516
|
+
menu.choice label, value
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
set(%i[prd_generation], {
|
|
521
|
+
interaction_style: selected_style,
|
|
522
|
+
default_style: selected_style,
|
|
523
|
+
max_question_rounds: DEFAULT_PRD_QUESTION_ROUNDS
|
|
524
|
+
})
|
|
525
|
+
end
|
|
526
|
+
|
|
495
527
|
# Configure generic deterministic commands for work loop
|
|
496
528
|
# Replaces the old category-specific configure_test_commands and configure_linting
|
|
497
529
|
def configure_commands
|
|
@@ -1913,6 +1945,7 @@ module Aidp
|
|
|
1913
1945
|
.sub(/^schema_version:/, "# Tracks configuration migrations\nschema_version:")
|
|
1914
1946
|
.sub(/^providers:/, "# Provider configuration (no secrets stored)\nproviders:")
|
|
1915
1947
|
.sub(/^work_loop:/, "# Work loop execution settings\nwork_loop:")
|
|
1948
|
+
.sub(/^prd_generation:/, "# PRD questioning depth and autonomy\nprd_generation:")
|
|
1916
1949
|
.sub(/^nfrs:/, "# Non-functional requirements to reference during planning\nnfrs:")
|
|
1917
1950
|
.sub(/^logging:/, "# Logging configuration\nlogging:")
|
|
1918
1951
|
.sub(/^modes:/, "# Defaults for background/watch/quick modes\nmodes:")
|
data/lib/aidp/version.rb
CHANGED
|
@@ -19,11 +19,32 @@ module Aidp
|
|
|
19
19
|
|
|
20
20
|
class ConversationError < StandardError; end
|
|
21
21
|
|
|
22
|
+
PLANNING_STYLES = {
|
|
23
|
+
"detailed" => {
|
|
24
|
+
label: "Detailed",
|
|
25
|
+
max_question_rounds: 30,
|
|
26
|
+
autonomy: "low",
|
|
27
|
+
question_strategy: "Ask 1-2 sharply scoped questions per round. Avoid assumptions unless the user confirms them."
|
|
28
|
+
},
|
|
29
|
+
"balanced" => {
|
|
30
|
+
label: "Balanced",
|
|
31
|
+
max_question_rounds: 10,
|
|
32
|
+
autonomy: "medium",
|
|
33
|
+
question_strategy: "Ask 1-3 efficient questions per round. Make reasonable decisions when the context is already clear."
|
|
34
|
+
},
|
|
35
|
+
"quick_sketch" => {
|
|
36
|
+
label: "Quick Sketch",
|
|
37
|
+
max_question_rounds: 3,
|
|
38
|
+
autonomy: "high",
|
|
39
|
+
question_strategy: "Ask only the highest-leverage missing questions. Prefer inferring obvious MVP details from repository context and existing configuration."
|
|
40
|
+
}
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
22
43
|
# Expose for testability
|
|
23
44
|
attr_reader :project_dir, :conversation_history, :user_input, :config_manager
|
|
24
45
|
attr_writer :provider_manager
|
|
25
46
|
|
|
26
|
-
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false, config_manager: nil, provider_manager: nil)
|
|
47
|
+
def initialize(project_dir, prompt: nil, use_enhanced_input: true, verbose: false, interaction_style: nil, config_manager: nil, provider_manager: nil)
|
|
27
48
|
@project_dir = project_dir
|
|
28
49
|
|
|
29
50
|
# Use EnhancedInput with Reline for full readline-style key bindings
|
|
@@ -39,6 +60,7 @@ module Aidp
|
|
|
39
60
|
@user_input = {}
|
|
40
61
|
@verbose = verbose
|
|
41
62
|
@debug_env = debug_level >= Aidp::DebugMixin::DEBUG_BASIC
|
|
63
|
+
@planning_preferences = resolve_planning_preferences(interaction_style)
|
|
42
64
|
end
|
|
43
65
|
|
|
44
66
|
# Main entry point for guided workflow selection
|
|
@@ -60,6 +82,7 @@ module Aidp
|
|
|
60
82
|
# Plan-and-execute: iterative planning followed by execution
|
|
61
83
|
def plan_and_execute_workflow
|
|
62
84
|
display_message("\n📋 Plan Phase", type: :highlight)
|
|
85
|
+
display_message("PRD style: #{planning_style[:interaction_style]}", type: :info)
|
|
63
86
|
display_message("I'll ask clarifying questions to understand your needs.\n", type: :info)
|
|
64
87
|
|
|
65
88
|
# Step 1: Iterative planning conversation
|
|
@@ -77,11 +100,25 @@ module Aidp
|
|
|
77
100
|
|
|
78
101
|
def iterative_planning
|
|
79
102
|
goal = user_goal
|
|
80
|
-
plan = {
|
|
103
|
+
plan = {
|
|
104
|
+
goal: goal,
|
|
105
|
+
scope: {},
|
|
106
|
+
users: {},
|
|
107
|
+
requirements: {},
|
|
108
|
+
constraints: {},
|
|
109
|
+
completion_criteria: [],
|
|
110
|
+
assumptions: [],
|
|
111
|
+
metadata: {
|
|
112
|
+
interaction_style: planning_style[:interaction_style],
|
|
113
|
+
autonomy: planning_style[:autonomy],
|
|
114
|
+
max_question_rounds: planning_style[:max_question_rounds]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
81
117
|
|
|
82
118
|
@conversation_history << {role: "user", content: goal}
|
|
83
119
|
|
|
84
120
|
iteration = 0
|
|
121
|
+
max_rounds = planning_style[:max_question_rounds]
|
|
85
122
|
loop do
|
|
86
123
|
iteration += 1
|
|
87
124
|
# Ask AI for next question based on current plan
|
|
@@ -96,6 +133,7 @@ module Aidp
|
|
|
96
133
|
|
|
97
134
|
# If AI says plan is complete, confirm with user
|
|
98
135
|
if question_response[:complete]
|
|
136
|
+
plan[:assumptions].concat(Array(question_response[:assumptions])).uniq!
|
|
99
137
|
display_message("\n✅ Plan Summary", type: :highlight)
|
|
100
138
|
display_plan_summary(plan)
|
|
101
139
|
|
|
@@ -118,14 +156,14 @@ module Aidp
|
|
|
118
156
|
update_plan_from_answer(plan, question, answer)
|
|
119
157
|
end
|
|
120
158
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
display_message("[WARNING] Planning loop exceeded 10 iterations. Provider may be returning generic responses.", type: :warning)
|
|
159
|
+
if iteration >= max_rounds
|
|
160
|
+
display_message("[WARNING] Planning loop exceeded #{max_rounds} iterations for #{planning_style[:interaction_style]}.", type: :warning)
|
|
124
161
|
display_message("Continuing with the plan information gathered so far...", type: :info)
|
|
125
162
|
break
|
|
126
163
|
end
|
|
127
164
|
end
|
|
128
165
|
|
|
166
|
+
apply_style_assumptions(plan)
|
|
129
167
|
plan
|
|
130
168
|
end
|
|
131
169
|
|
|
@@ -142,6 +180,7 @@ module Aidp
|
|
|
142
180
|
|
|
143
181
|
response = call_provider_for_analysis(system_prompt, user_prompt)
|
|
144
182
|
parsed = parse_planning_response(response)
|
|
183
|
+
parsed[:assumptions] = Array(parsed[:assumptions]).compact
|
|
145
184
|
# Attach raw response for debug
|
|
146
185
|
parsed[:raw_response] = response
|
|
147
186
|
emit_verbose_raw_prompt(system_prompt, user_prompt, response)
|
|
@@ -397,16 +436,19 @@ module Aidp
|
|
|
397
436
|
You are a planning assistant helping gather requirements through clarifying questions.
|
|
398
437
|
|
|
399
438
|
Your role:
|
|
400
|
-
1.
|
|
439
|
+
1. #{planning_style[:question_strategy]}
|
|
401
440
|
2. Build towards a complete understanding of: scope, users, requirements, constraints
|
|
402
441
|
3. Determine when enough information has been gathered
|
|
403
442
|
4. Be concise to preserve context window
|
|
443
|
+
5. Operate in "#{planning_style[:interaction_style]}" mode with #{planning_style[:autonomy]} autonomy
|
|
444
|
+
6. When you make an assumption, list it explicitly in "assumptions"
|
|
404
445
|
|
|
405
446
|
Response Format (JSON):
|
|
406
447
|
{
|
|
407
448
|
"complete": true/false,
|
|
408
449
|
"questions": ["question 1", "question 2"],
|
|
409
|
-
"reasoning": "brief explanation of what you're trying to learn"
|
|
450
|
+
"reasoning": "brief explanation of what you're trying to learn",
|
|
451
|
+
"assumptions": ["assumption 1"]
|
|
410
452
|
}
|
|
411
453
|
|
|
412
454
|
If complete is true, the plan is ready for execution.
|
|
@@ -470,6 +512,28 @@ module Aidp
|
|
|
470
512
|
end
|
|
471
513
|
end
|
|
472
514
|
|
|
515
|
+
def apply_style_assumptions(plan)
|
|
516
|
+
inferred_assumptions = []
|
|
517
|
+
|
|
518
|
+
if plan[:users].empty?
|
|
519
|
+
plan[:users][:personas] = [default_persona_for_style]
|
|
520
|
+
inferred_assumptions << "Primary user persona inferred as #{default_persona_for_style}."
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
if plan[:completion_criteria].empty?
|
|
524
|
+
criterion = default_completion_criterion
|
|
525
|
+
plan[:completion_criteria] << criterion
|
|
526
|
+
inferred_assumptions << "Success metric inferred as #{criterion}"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
inferred_assumptions.concat(repository_default_assumptions)
|
|
530
|
+
plan[:assumptions].concat(inferred_assumptions).uniq!
|
|
531
|
+
|
|
532
|
+
inferred_assumptions.each do |assumption|
|
|
533
|
+
display_message("Inferred missing detail: #{assumption}", type: :muted)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
473
537
|
def display_plan_summary(plan)
|
|
474
538
|
display_message("Goal: #{plan[:goal]}", type: :info)
|
|
475
539
|
display_message("\nScope:", type: :highlight) if plan[:scope].any?
|
|
@@ -533,6 +597,10 @@ module Aidp
|
|
|
533
597
|
prd_content = <<~PRD
|
|
534
598
|
# Product Requirements Document
|
|
535
599
|
|
|
600
|
+
## Planning Metadata
|
|
601
|
+
- PRD Style: #{plan.dig(:metadata, :interaction_style) || planning_style[:interaction_style]}
|
|
602
|
+
- Agent Autonomy: #{plan.dig(:metadata, :autonomy) || planning_style[:autonomy]}
|
|
603
|
+
|
|
536
604
|
## Goal
|
|
537
605
|
#{plan[:goal]}
|
|
538
606
|
|
|
@@ -551,6 +619,9 @@ module Aidp
|
|
|
551
619
|
## Completion Criteria
|
|
552
620
|
#{plan[:completion_criteria].map { |c| "- #{c}" }.join("\n")}
|
|
553
621
|
|
|
622
|
+
## Assumptions & Inferred Details
|
|
623
|
+
#{plan[:assumptions]&.any? ? plan[:assumptions].map { |item| "- #{item}" }.join("\n") : "None"}
|
|
624
|
+
|
|
554
625
|
## Additional Context
|
|
555
626
|
#{plan[:additional_context]&.map { |ctx| "**#{ctx[:question]}**: #{ctx[:answer]}" }&.join("\n\n")}
|
|
556
627
|
|
|
@@ -611,6 +682,64 @@ module Aidp
|
|
|
611
682
|
end
|
|
612
683
|
end.join("\n\n")
|
|
613
684
|
end
|
|
685
|
+
|
|
686
|
+
def planning_style
|
|
687
|
+
@planning_preferences
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def resolve_planning_preferences(override_style)
|
|
691
|
+
config = @config_manager.respond_to?(:prd_generation_config) ? @config_manager.prd_generation_config : {}
|
|
692
|
+
configured_style = override_style || config[:interaction_style] || config["interaction_style"]
|
|
693
|
+
interaction_style = normalize_interaction_style(configured_style || "balanced")
|
|
694
|
+
configured_rounds = config[:max_question_rounds] || config["max_question_rounds"] || {}
|
|
695
|
+
defaults = PLANNING_STYLES.fetch(interaction_style)
|
|
696
|
+
|
|
697
|
+
defaults.merge(
|
|
698
|
+
interaction_style: interaction_style,
|
|
699
|
+
max_question_rounds: configured_rounds[interaction_style.to_sym] || configured_rounds[interaction_style] || defaults[:max_question_rounds]
|
|
700
|
+
)
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def normalize_interaction_style(style)
|
|
704
|
+
normalized = style.to_s.strip.downcase.tr(" ", "_")
|
|
705
|
+
return normalized if PLANNING_STYLES.key?(normalized)
|
|
706
|
+
|
|
707
|
+
"balanced"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def default_persona_for_style
|
|
711
|
+
if planning_style[:interaction_style] == "quick_sketch"
|
|
712
|
+
"Prototype users validating the MVP flow"
|
|
713
|
+
else
|
|
714
|
+
"Primary end users for this feature"
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def default_completion_criterion
|
|
719
|
+
if planning_style[:interaction_style] == "quick_sketch"
|
|
720
|
+
"A thin MVP slice is implemented and demoable end-to-end."
|
|
721
|
+
else
|
|
722
|
+
"The feature meets the documented requirements and is ready for implementation planning."
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def repository_default_assumptions
|
|
727
|
+
config = @config_manager.respond_to?(:config) ? (@config_manager.config || {}) : {}
|
|
728
|
+
assumptions = []
|
|
729
|
+
commands = config.dig(:work_loop, :commands) || config.dig("work_loop", "commands")
|
|
730
|
+
nfrs = config.dig(:nfrs) || config.dig("nfrs")
|
|
731
|
+
|
|
732
|
+
if commands.is_a?(Array) && commands.any?
|
|
733
|
+
command_label = (commands.size == 1) ? "command" : "commands"
|
|
734
|
+
assumptions << "Validation will reuse #{commands.size} configured work loop #{command_label}."
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
if nfrs.is_a?(Hash) && nfrs.any?
|
|
738
|
+
assumptions << "Repository-level non-functional requirements from aidp.yml will apply."
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
assumptions
|
|
742
|
+
end
|
|
614
743
|
end
|
|
615
744
|
end
|
|
616
745
|
end
|