aidp 0.27.0 → 0.28.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 +89 -0
- data/lib/aidp/cli/models_command.rb +5 -6
- data/lib/aidp/cli.rb +10 -8
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/debug_mixin.rb +23 -1
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +209 -17
- data/lib/aidp/execute/workflow_selector.rb +2 -25
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +0 -5
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +27 -19
- data/lib/aidp/harness/enhanced_runner.rb +1 -4
- data/lib/aidp/harness/error_handler.rb +1 -72
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +74 -2
- data/lib/aidp/providers/base.rb +25 -1
- data/lib/aidp/providers/codex.rb +26 -3
- data/lib/aidp/providers/cursor.rb +16 -0
- data/lib/aidp/providers/gemini.rb +13 -0
- data/lib/aidp/providers/github_copilot.rb +17 -0
- data/lib/aidp/providers/kilocode.rb +11 -0
- data/lib/aidp/providers/opencode.rb +11 -0
- data/lib/aidp/setup/wizard.rb +249 -39
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +7 -43
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +98 -17
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- metadata +39 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../logger"
|
|
4
|
+
require_relative "../parsers/document_parser"
|
|
5
|
+
require_relative "../parsers/feedback_data_parser"
|
|
6
|
+
require_relative "../generators/mvp_scope_generator"
|
|
7
|
+
require_relative "../generators/user_test_plan_generator"
|
|
8
|
+
require_relative "../generators/marketing_report_generator"
|
|
9
|
+
require_relative "../generators/iteration_plan_generator"
|
|
10
|
+
require_relative "../generators/legacy_research_planner"
|
|
11
|
+
require_relative "../analyzers/feedback_analyzer"
|
|
12
|
+
require_relative "../mappers/persona_mapper"
|
|
13
|
+
|
|
14
|
+
module Aidp
|
|
15
|
+
module Planning
|
|
16
|
+
module Builders
|
|
17
|
+
# Orchestrates agile planning workflows
|
|
18
|
+
# Coordinates MVP scoping, user testing, feedback analysis, and iteration planning
|
|
19
|
+
class AgilePlanBuilder
|
|
20
|
+
def initialize(
|
|
21
|
+
ai_decision_engine:,
|
|
22
|
+
config: nil,
|
|
23
|
+
prompt: nil,
|
|
24
|
+
document_parser: nil,
|
|
25
|
+
mvp_scope_generator: nil,
|
|
26
|
+
user_test_plan_generator: nil,
|
|
27
|
+
marketing_report_generator: nil,
|
|
28
|
+
feedback_analyzer: nil,
|
|
29
|
+
iteration_plan_generator: nil,
|
|
30
|
+
legacy_research_planner: nil,
|
|
31
|
+
persona_mapper: nil
|
|
32
|
+
)
|
|
33
|
+
@ai_decision_engine = ai_decision_engine
|
|
34
|
+
@config = config || Aidp::Config.agile_config
|
|
35
|
+
@prompt = prompt || TTY::Prompt.new
|
|
36
|
+
@document_parser = document_parser || Parsers::DocumentParser.new(ai_decision_engine: ai_decision_engine)
|
|
37
|
+
@mvp_scope_generator = mvp_scope_generator || Generators::MVPScopeGenerator.new(
|
|
38
|
+
ai_decision_engine: ai_decision_engine,
|
|
39
|
+
prompt: @prompt,
|
|
40
|
+
config: @config
|
|
41
|
+
)
|
|
42
|
+
@user_test_plan_generator = user_test_plan_generator || Generators::UserTestPlanGenerator.new(
|
|
43
|
+
ai_decision_engine: ai_decision_engine,
|
|
44
|
+
config: @config
|
|
45
|
+
)
|
|
46
|
+
@marketing_report_generator = marketing_report_generator || Generators::MarketingReportGenerator.new(
|
|
47
|
+
ai_decision_engine: ai_decision_engine,
|
|
48
|
+
config: @config
|
|
49
|
+
)
|
|
50
|
+
@feedback_analyzer = feedback_analyzer || Analyzers::FeedbackAnalyzer.new(
|
|
51
|
+
ai_decision_engine: ai_decision_engine,
|
|
52
|
+
config: @config
|
|
53
|
+
)
|
|
54
|
+
@iteration_plan_generator = iteration_plan_generator || Generators::IterationPlanGenerator.new(
|
|
55
|
+
ai_decision_engine: ai_decision_engine,
|
|
56
|
+
config: @config
|
|
57
|
+
)
|
|
58
|
+
@legacy_research_planner = legacy_research_planner || Generators::LegacyResearchPlanner.new(
|
|
59
|
+
ai_decision_engine: ai_decision_engine,
|
|
60
|
+
prompt: @prompt,
|
|
61
|
+
config: @config
|
|
62
|
+
)
|
|
63
|
+
@persona_mapper = persona_mapper || Mappers::PersonaMapper.new(
|
|
64
|
+
ai_decision_engine: ai_decision_engine,
|
|
65
|
+
config: @config,
|
|
66
|
+
mode: :agile
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Build complete MVP plan from PRD
|
|
71
|
+
# @param prd_path [String] Path to PRD document
|
|
72
|
+
# @param user_priorities [Array<String>] Optional user priorities
|
|
73
|
+
# @return [Hash] Complete MVP plan with all artifacts
|
|
74
|
+
def build_mvp_plan(prd_path:, user_priorities: nil)
|
|
75
|
+
Aidp.log_debug("agile_plan_builder", "build_mvp_plan", prd_path: prd_path)
|
|
76
|
+
|
|
77
|
+
# Parse PRD
|
|
78
|
+
prd = parse_prd(prd_path)
|
|
79
|
+
|
|
80
|
+
# Generate MVP scope
|
|
81
|
+
@prompt.say("Generating MVP scope...")
|
|
82
|
+
mvp_scope = @mvp_scope_generator.generate(prd: prd, user_priorities: user_priorities)
|
|
83
|
+
Aidp.log_debug("agile_plan_builder", "mvp_scope_generated",
|
|
84
|
+
must_have: mvp_scope[:mvp_features].size,
|
|
85
|
+
deferred: mvp_scope[:deferred_features].size)
|
|
86
|
+
|
|
87
|
+
# Generate user test plan
|
|
88
|
+
@prompt.say("Creating user testing plan...")
|
|
89
|
+
test_plan = @user_test_plan_generator.generate(mvp_scope: mvp_scope)
|
|
90
|
+
Aidp.log_debug("agile_plan_builder", "test_plan_generated",
|
|
91
|
+
stages: test_plan[:testing_stages].size)
|
|
92
|
+
|
|
93
|
+
# Generate marketing report
|
|
94
|
+
@prompt.say("Generating marketing materials...")
|
|
95
|
+
marketing_report = @marketing_report_generator.generate(mvp_scope: mvp_scope)
|
|
96
|
+
Aidp.log_debug("agile_plan_builder", "marketing_report_generated",
|
|
97
|
+
messages: marketing_report[:key_messages].size)
|
|
98
|
+
|
|
99
|
+
@prompt.ok("MVP plan complete!")
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
mvp_scope: mvp_scope,
|
|
103
|
+
test_plan: test_plan,
|
|
104
|
+
marketing_report: marketing_report,
|
|
105
|
+
metadata: {
|
|
106
|
+
generated_at: Time.now.iso8601,
|
|
107
|
+
workflow: "agile_mvp"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Analyze feedback data
|
|
113
|
+
# @param feedback_path [String] Path to feedback data file (CSV/JSON/markdown)
|
|
114
|
+
# @return [Hash] Feedback analysis results
|
|
115
|
+
def analyze_feedback(feedback_path:)
|
|
116
|
+
Aidp.log_debug("agile_plan_builder", "analyze_feedback", feedback_path: feedback_path)
|
|
117
|
+
|
|
118
|
+
# Parse feedback data
|
|
119
|
+
@prompt.say("Parsing feedback data...")
|
|
120
|
+
parser = Parsers::FeedbackDataParser.new(file_path: feedback_path)
|
|
121
|
+
feedback_data = parser.parse
|
|
122
|
+
|
|
123
|
+
@prompt.say("Analyzing #{feedback_data[:response_count]} responses...")
|
|
124
|
+
|
|
125
|
+
# Analyze with AI
|
|
126
|
+
analysis = @feedback_analyzer.analyze(feedback_data)
|
|
127
|
+
Aidp.log_debug("agile_plan_builder", "feedback_analyzed",
|
|
128
|
+
findings: analysis[:findings].size,
|
|
129
|
+
recommendations: analysis[:recommendations].size)
|
|
130
|
+
|
|
131
|
+
@prompt.ok("Feedback analysis complete!")
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
analysis: analysis,
|
|
135
|
+
metadata: {
|
|
136
|
+
generated_at: Time.now.iso8601,
|
|
137
|
+
workflow: "agile_feedback_analysis"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Plan next iteration based on feedback
|
|
143
|
+
# @param feedback_analysis_path [String] Path to feedback analysis document
|
|
144
|
+
# @param current_mvp_path [String] Optional path to current MVP scope
|
|
145
|
+
# @return [Hash] Iteration plan
|
|
146
|
+
def plan_next_iteration(feedback_analysis_path:, current_mvp_path: nil)
|
|
147
|
+
Aidp.log_debug("agile_plan_builder", "plan_next_iteration",
|
|
148
|
+
feedback_path: feedback_analysis_path,
|
|
149
|
+
mvp_path: current_mvp_path)
|
|
150
|
+
|
|
151
|
+
# Parse feedback analysis
|
|
152
|
+
@prompt.say("Loading feedback analysis...")
|
|
153
|
+
feedback_analysis = parse_feedback_analysis(feedback_analysis_path)
|
|
154
|
+
|
|
155
|
+
# Parse current MVP if provided
|
|
156
|
+
current_mvp = nil
|
|
157
|
+
if current_mvp_path
|
|
158
|
+
@prompt.say("Loading current MVP scope...")
|
|
159
|
+
current_mvp = parse_mvp_scope(current_mvp_path)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Generate iteration plan
|
|
163
|
+
@prompt.say("Planning next iteration...")
|
|
164
|
+
iteration_plan = @iteration_plan_generator.generate(
|
|
165
|
+
feedback_analysis: feedback_analysis,
|
|
166
|
+
current_mvp: current_mvp
|
|
167
|
+
)
|
|
168
|
+
Aidp.log_debug("agile_plan_builder", "iteration_plan_generated",
|
|
169
|
+
improvements: iteration_plan[:improvements].size,
|
|
170
|
+
tasks: iteration_plan[:tasks].size)
|
|
171
|
+
|
|
172
|
+
@prompt.ok("Iteration plan complete!")
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
iteration_plan: iteration_plan,
|
|
176
|
+
metadata: {
|
|
177
|
+
generated_at: Time.now.iso8601,
|
|
178
|
+
workflow: "agile_iteration"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Plan user research for existing/legacy codebase
|
|
184
|
+
# @param codebase_path [String] Path to codebase directory
|
|
185
|
+
# @param language [String] Optional primary language
|
|
186
|
+
# @param known_users [String] Optional known user segments
|
|
187
|
+
# @return [Hash] Research plan
|
|
188
|
+
def plan_legacy_research(codebase_path:, language: nil, known_users: nil)
|
|
189
|
+
Aidp.log_debug("agile_plan_builder", "plan_legacy_research",
|
|
190
|
+
codebase_path: codebase_path)
|
|
191
|
+
|
|
192
|
+
@prompt.say("Analyzing codebase...")
|
|
193
|
+
research_plan = @legacy_research_planner.generate(
|
|
194
|
+
codebase_path: codebase_path,
|
|
195
|
+
language: language,
|
|
196
|
+
known_users: known_users
|
|
197
|
+
)
|
|
198
|
+
Aidp.log_debug("agile_plan_builder", "research_plan_generated",
|
|
199
|
+
features: research_plan[:current_features].size,
|
|
200
|
+
questions: research_plan[:research_questions].size)
|
|
201
|
+
|
|
202
|
+
# Generate user test plan based on research priorities
|
|
203
|
+
@prompt.say("Creating user testing plan...")
|
|
204
|
+
# Create a minimal MVP scope structure for test plan generation
|
|
205
|
+
mvp_scope_for_testing = {
|
|
206
|
+
mvp_features: research_plan[:current_features],
|
|
207
|
+
metadata: {
|
|
208
|
+
user_priorities: ["Understand existing user experience", "Identify pain points", "Discover improvement opportunities"]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
test_plan = @user_test_plan_generator.generate(mvp_scope: mvp_scope_for_testing)
|
|
212
|
+
|
|
213
|
+
@prompt.ok("Legacy research plan complete!")
|
|
214
|
+
|
|
215
|
+
{
|
|
216
|
+
research_plan: research_plan,
|
|
217
|
+
test_plan: test_plan,
|
|
218
|
+
metadata: {
|
|
219
|
+
generated_at: Time.now.iso8601,
|
|
220
|
+
workflow: "agile_legacy_research"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Write all artifacts to files
|
|
226
|
+
# @param plan_data [Hash] Plan data from any workflow
|
|
227
|
+
# @param output_dir [String] Directory to write files
|
|
228
|
+
def write_artifacts(plan_data, output_dir: ".aidp/docs")
|
|
229
|
+
Aidp.log_debug("agile_plan_builder", "write_artifacts", output_dir: output_dir)
|
|
230
|
+
|
|
231
|
+
FileUtils.mkdir_p(output_dir)
|
|
232
|
+
|
|
233
|
+
artifacts_written = []
|
|
234
|
+
|
|
235
|
+
# Write MVP scope if present
|
|
236
|
+
if plan_data[:mvp_scope]
|
|
237
|
+
path = File.join(output_dir, "MVP_SCOPE.md")
|
|
238
|
+
content = @mvp_scope_generator.format_as_markdown(plan_data[:mvp_scope])
|
|
239
|
+
File.write(path, content)
|
|
240
|
+
artifacts_written << path
|
|
241
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "MVP_SCOPE.md")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Write test plan if present
|
|
245
|
+
if plan_data[:test_plan]
|
|
246
|
+
path = File.join(output_dir, "USER_TEST_PLAN.md")
|
|
247
|
+
content = @user_test_plan_generator.format_as_markdown(plan_data[:test_plan])
|
|
248
|
+
File.write(path, content)
|
|
249
|
+
artifacts_written << path
|
|
250
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "USER_TEST_PLAN.md")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Write marketing report if present
|
|
254
|
+
if plan_data[:marketing_report]
|
|
255
|
+
path = File.join(output_dir, "MARKETING_REPORT.md")
|
|
256
|
+
content = @marketing_report_generator.format_as_markdown(plan_data[:marketing_report])
|
|
257
|
+
File.write(path, content)
|
|
258
|
+
artifacts_written << path
|
|
259
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "MARKETING_REPORT.md")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Write feedback analysis if present
|
|
263
|
+
if plan_data[:analysis]
|
|
264
|
+
path = File.join(output_dir, "USER_FEEDBACK_ANALYSIS.md")
|
|
265
|
+
content = @feedback_analyzer.format_as_markdown(plan_data[:analysis])
|
|
266
|
+
File.write(path, content)
|
|
267
|
+
artifacts_written << path
|
|
268
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "USER_FEEDBACK_ANALYSIS.md")
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Write iteration plan if present
|
|
272
|
+
if plan_data[:iteration_plan]
|
|
273
|
+
path = File.join(output_dir, "NEXT_ITERATION_PLAN.md")
|
|
274
|
+
content = @iteration_plan_generator.format_as_markdown(plan_data[:iteration_plan])
|
|
275
|
+
File.write(path, content)
|
|
276
|
+
artifacts_written << path
|
|
277
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "NEXT_ITERATION_PLAN.md")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Write research plan if present
|
|
281
|
+
if plan_data[:research_plan]
|
|
282
|
+
path = File.join(output_dir, "LEGACY_USER_RESEARCH_PLAN.md")
|
|
283
|
+
content = @legacy_research_planner.format_as_markdown(plan_data[:research_plan])
|
|
284
|
+
File.write(path, content)
|
|
285
|
+
artifacts_written << path
|
|
286
|
+
Aidp.log_debug("agile_plan_builder", "wrote_artifact", file: "LEGACY_USER_RESEARCH_PLAN.md")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
@prompt.ok("Wrote #{artifacts_written.size} artifacts to #{output_dir}")
|
|
290
|
+
artifacts_written
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
def parse_prd(prd_path)
|
|
296
|
+
Aidp.log_debug("agile_plan_builder", "parse_prd", path: prd_path)
|
|
297
|
+
|
|
298
|
+
unless File.exist?(prd_path)
|
|
299
|
+
raise ArgumentError, "PRD file not found: #{prd_path}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
content = File.read(prd_path)
|
|
303
|
+
|
|
304
|
+
# Simple PRD structure extraction
|
|
305
|
+
{
|
|
306
|
+
content: content,
|
|
307
|
+
path: prd_path,
|
|
308
|
+
type: :prd,
|
|
309
|
+
metadata: {
|
|
310
|
+
parsed_at: Time.now.iso8601
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def parse_feedback_analysis(analysis_path)
|
|
316
|
+
Aidp.log_debug("agile_plan_builder", "parse_feedback_analysis", path: analysis_path)
|
|
317
|
+
|
|
318
|
+
unless File.exist?(analysis_path)
|
|
319
|
+
raise ArgumentError, "Feedback analysis file not found: #{analysis_path}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
content = File.read(analysis_path)
|
|
323
|
+
|
|
324
|
+
# Extract key sections from markdown
|
|
325
|
+
{
|
|
326
|
+
content: content,
|
|
327
|
+
summary: extract_section(content, "Executive Summary"),
|
|
328
|
+
findings: extract_list_items(content, "Key Findings"),
|
|
329
|
+
recommendations: extract_list_items(content, "Recommendations"),
|
|
330
|
+
priority_issues: extract_list_items(content, "Priority Issues"),
|
|
331
|
+
metadata: {
|
|
332
|
+
parsed_at: Time.now.iso8601
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def parse_mvp_scope(mvp_path)
|
|
338
|
+
Aidp.log_debug("agile_plan_builder", "parse_mvp_scope", path: mvp_path)
|
|
339
|
+
|
|
340
|
+
unless File.exist?(mvp_path)
|
|
341
|
+
raise ArgumentError, "MVP scope file not found: #{mvp_path}"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
content = File.read(mvp_path)
|
|
345
|
+
|
|
346
|
+
{
|
|
347
|
+
content: content,
|
|
348
|
+
mvp_features: extract_features(content, "MVP Features"),
|
|
349
|
+
deferred_features: extract_features(content, "Deferred Features"),
|
|
350
|
+
metadata: {
|
|
351
|
+
parsed_at: Time.now.iso8601
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def extract_section(content, heading)
|
|
357
|
+
# Simple section extraction
|
|
358
|
+
if content =~ /## #{heading}\s*\n\n(.+?)(\n## |$)/m
|
|
359
|
+
$1.strip
|
|
360
|
+
else
|
|
361
|
+
""
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def extract_list_items(content, heading)
|
|
366
|
+
section = extract_section(content, heading)
|
|
367
|
+
# Extract bullet points or numbered items
|
|
368
|
+
section.scan(/^[-*]\s+(.+)$/).flatten
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def extract_features(content, heading)
|
|
372
|
+
# Simple feature extraction from markdown headings
|
|
373
|
+
section_start = content.index(/## #{heading}/)
|
|
374
|
+
return [] unless section_start
|
|
375
|
+
|
|
376
|
+
section = content[section_start..]
|
|
377
|
+
next_section = section.index(/\n## /, 1)
|
|
378
|
+
section = section[0...next_section] if next_section
|
|
379
|
+
|
|
380
|
+
# Extract feature names from h3 headings
|
|
381
|
+
features = section.scan(/### \d+\.\s+(.+)/).flatten
|
|
382
|
+
features.map { |name| {name: name, description: "Feature from #{heading}"} }
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../logger"
|
|
4
|
+
require_relative "../parsers/document_parser"
|
|
5
|
+
require_relative "../generators/wbs_generator"
|
|
6
|
+
require_relative "../generators/gantt_generator"
|
|
7
|
+
require_relative "../mappers/persona_mapper"
|
|
8
|
+
|
|
9
|
+
module Aidp
|
|
10
|
+
module Planning
|
|
11
|
+
module Builders
|
|
12
|
+
# Orchestrates generation of complete project plan
|
|
13
|
+
# Coordinates all planning components (parsing, generating, mapping)
|
|
14
|
+
class ProjectPlanBuilder
|
|
15
|
+
def initialize(
|
|
16
|
+
ai_decision_engine:,
|
|
17
|
+
config: nil,
|
|
18
|
+
document_parser: nil,
|
|
19
|
+
wbs_generator: nil,
|
|
20
|
+
gantt_generator: nil,
|
|
21
|
+
persona_mapper: nil
|
|
22
|
+
)
|
|
23
|
+
@ai_decision_engine = ai_decision_engine
|
|
24
|
+
@config = config || Aidp::Config.waterfall_config
|
|
25
|
+
@document_parser = document_parser || Parsers::DocumentParser.new(ai_decision_engine: ai_decision_engine)
|
|
26
|
+
@wbs_generator = wbs_generator || Generators::WBSGenerator.new(config: @config)
|
|
27
|
+
@gantt_generator = gantt_generator || Generators::GanttGenerator.new(config: @config)
|
|
28
|
+
@persona_mapper = persona_mapper || Mappers::PersonaMapper.new(ai_decision_engine: ai_decision_engine, config: @config)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Build project plan from existing documentation (ingestion path)
|
|
32
|
+
# @param docs_path [String] Path to existing documentation
|
|
33
|
+
# @return [Hash] Complete project plan
|
|
34
|
+
def build_from_ingestion(docs_path)
|
|
35
|
+
Aidp.log_debug("project_plan_builder", "build_from_ingestion", path: docs_path)
|
|
36
|
+
|
|
37
|
+
# Parse existing docs
|
|
38
|
+
parsed_docs = @document_parser.parse_directory(docs_path)
|
|
39
|
+
Aidp.log_debug("project_plan_builder", "parsed_docs", count: parsed_docs.size)
|
|
40
|
+
|
|
41
|
+
# Extract PRD and design docs
|
|
42
|
+
prd = parsed_docs.find { |d| d[:type] == :prd }
|
|
43
|
+
tech_design = parsed_docs.find { |d| d[:type] == :design }
|
|
44
|
+
|
|
45
|
+
# Generate plan components
|
|
46
|
+
build_plan_components(prd, tech_design)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build project plan from scratch (generation path)
|
|
50
|
+
# @param requirements [Hash] User-provided requirements
|
|
51
|
+
# @return [Hash] Complete project plan
|
|
52
|
+
def build_from_scratch(requirements)
|
|
53
|
+
Aidp.log_debug("project_plan_builder", "build_from_scratch")
|
|
54
|
+
|
|
55
|
+
# In production, this would use AI to generate PRD and design docs
|
|
56
|
+
# For now, we'll create minimal structure from requirements
|
|
57
|
+
prd = structure_requirements_as_prd(requirements)
|
|
58
|
+
tech_design = nil # Will be generated by AI templates
|
|
59
|
+
|
|
60
|
+
build_plan_components(prd, tech_design)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Assemble complete project plan document
|
|
64
|
+
# @param components [Hash] All plan components
|
|
65
|
+
# @return [String] Complete PROJECT_PLAN.md content
|
|
66
|
+
def assemble_project_plan(components)
|
|
67
|
+
Aidp.log_debug("project_plan_builder", "assemble_project_plan")
|
|
68
|
+
|
|
69
|
+
output = ["# Project Plan", ""]
|
|
70
|
+
output << "Generated: #{Time.now.iso8601}"
|
|
71
|
+
output << ""
|
|
72
|
+
|
|
73
|
+
# Executive Summary
|
|
74
|
+
output << "## Executive Summary"
|
|
75
|
+
output << ""
|
|
76
|
+
output << "This document provides a comprehensive project plan including:"
|
|
77
|
+
output << "- Work Breakdown Structure (WBS)"
|
|
78
|
+
output << "- Gantt Chart with Critical Path"
|
|
79
|
+
output << "- Task Dependencies and Milestones"
|
|
80
|
+
output << "- Persona/Agent Assignments"
|
|
81
|
+
output << ""
|
|
82
|
+
|
|
83
|
+
# WBS Section
|
|
84
|
+
output << "## Work Breakdown Structure"
|
|
85
|
+
output << ""
|
|
86
|
+
output << components[:wbs_markdown]
|
|
87
|
+
output << ""
|
|
88
|
+
|
|
89
|
+
# Gantt Chart Section
|
|
90
|
+
output << "## Timeline and Gantt Chart"
|
|
91
|
+
output << ""
|
|
92
|
+
output << "```mermaid"
|
|
93
|
+
output << components[:gantt_mermaid]
|
|
94
|
+
output << "```"
|
|
95
|
+
output << ""
|
|
96
|
+
|
|
97
|
+
# Critical Path
|
|
98
|
+
output << "## Critical Path"
|
|
99
|
+
output << ""
|
|
100
|
+
output << "The following tasks form the critical path (longest sequence):"
|
|
101
|
+
output << ""
|
|
102
|
+
components[:critical_path].each_with_index do |task_id, idx|
|
|
103
|
+
output << "#{idx + 1}. #{task_id}"
|
|
104
|
+
end
|
|
105
|
+
output << ""
|
|
106
|
+
|
|
107
|
+
# Persona Assignments
|
|
108
|
+
output << "## Persona Assignments"
|
|
109
|
+
output << ""
|
|
110
|
+
output << "Tasks are assigned to the following personas:"
|
|
111
|
+
output << ""
|
|
112
|
+
persona_summary = summarize_persona_assignments(components[:persona_assignments])
|
|
113
|
+
output << persona_summary
|
|
114
|
+
output << ""
|
|
115
|
+
|
|
116
|
+
# Metadata
|
|
117
|
+
output << "## Metadata"
|
|
118
|
+
output << ""
|
|
119
|
+
output << "- **Total Phases:** #{components[:wbs][:metadata][:phase_count]}"
|
|
120
|
+
output << "- **Total Tasks:** #{components[:wbs][:metadata][:total_tasks]}"
|
|
121
|
+
output << "- **Critical Path Length:** #{components[:gantt][:metadata][:critical_path_length]} tasks"
|
|
122
|
+
output << "- **Personas Used:** #{components[:persona_assignments][:metadata][:personas_used].size}"
|
|
123
|
+
output << ""
|
|
124
|
+
|
|
125
|
+
output.join("\n")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def build_plan_components(prd, tech_design)
|
|
131
|
+
# Generate WBS
|
|
132
|
+
wbs = @wbs_generator.generate(prd: prd, tech_design: tech_design)
|
|
133
|
+
wbs_markdown = @wbs_generator.format_as_markdown(wbs)
|
|
134
|
+
|
|
135
|
+
# Generate Gantt chart
|
|
136
|
+
gantt = @gantt_generator.generate(wbs: wbs)
|
|
137
|
+
|
|
138
|
+
# Extract tasks for persona assignment
|
|
139
|
+
tasks = gantt[:tasks]
|
|
140
|
+
|
|
141
|
+
# Assign personas
|
|
142
|
+
persona_assignments = @persona_mapper.assign_personas(tasks)
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
prd: prd,
|
|
146
|
+
tech_design: tech_design,
|
|
147
|
+
wbs: wbs,
|
|
148
|
+
wbs_markdown: wbs_markdown,
|
|
149
|
+
gantt: gantt,
|
|
150
|
+
gantt_mermaid: gantt[:mermaid],
|
|
151
|
+
critical_path: gantt[:critical_path],
|
|
152
|
+
persona_assignments: persona_assignments
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def structure_requirements_as_prd(requirements)
|
|
157
|
+
{
|
|
158
|
+
type: :prd,
|
|
159
|
+
sections: {
|
|
160
|
+
"problem_statement" => requirements[:problem] || "Problem to solve",
|
|
161
|
+
"goals" => requirements[:goals] || "Project goals",
|
|
162
|
+
"success_criteria" => requirements[:success_criteria] || "Success metrics"
|
|
163
|
+
},
|
|
164
|
+
raw_content: requirements.to_s
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def summarize_persona_assignments(assignments)
|
|
169
|
+
# Group by persona
|
|
170
|
+
by_persona = {}
|
|
171
|
+
assignments[:assignments].each do |task_id, assignment|
|
|
172
|
+
persona = assignment[:persona]
|
|
173
|
+
by_persona[persona] ||= []
|
|
174
|
+
by_persona[persona] << assignment[:task]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Format summary
|
|
178
|
+
output = []
|
|
179
|
+
by_persona.each do |persona, tasks|
|
|
180
|
+
output << "### #{persona} (#{tasks.size} tasks)"
|
|
181
|
+
output << ""
|
|
182
|
+
tasks.each do |task|
|
|
183
|
+
output << "- #{task}"
|
|
184
|
+
end
|
|
185
|
+
output << ""
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
output.join("\n")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|