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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/models_command.rb +5 -6
  4. data/lib/aidp/cli.rb +10 -8
  5. data/lib/aidp/config.rb +54 -0
  6. data/lib/aidp/debug_mixin.rb +23 -1
  7. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  8. data/lib/aidp/execute/repl_macros.rb +2 -2
  9. data/lib/aidp/execute/steps.rb +94 -1
  10. data/lib/aidp/execute/work_loop_runner.rb +209 -17
  11. data/lib/aidp/execute/workflow_selector.rb +2 -25
  12. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  13. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  14. data/lib/aidp/harness/config_manager.rb +0 -5
  15. data/lib/aidp/harness/config_schema.rb +8 -0
  16. data/lib/aidp/harness/configuration.rb +27 -19
  17. data/lib/aidp/harness/enhanced_runner.rb +1 -4
  18. data/lib/aidp/harness/error_handler.rb +1 -72
  19. data/lib/aidp/harness/provider_factory.rb +11 -2
  20. data/lib/aidp/harness/state_manager.rb +0 -7
  21. data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
  22. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  23. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  24. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  25. data/lib/aidp/harness/user_interface.rb +0 -58
  26. data/lib/aidp/init/runner.rb +7 -2
  27. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  28. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  29. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  30. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  31. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  32. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  33. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  34. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  35. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  36. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  37. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  38. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  39. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  40. data/lib/aidp/provider_manager.rb +8 -32
  41. data/lib/aidp/providers/aider.rb +264 -0
  42. data/lib/aidp/providers/anthropic.rb +74 -2
  43. data/lib/aidp/providers/base.rb +25 -1
  44. data/lib/aidp/providers/codex.rb +26 -3
  45. data/lib/aidp/providers/cursor.rb +16 -0
  46. data/lib/aidp/providers/gemini.rb +13 -0
  47. data/lib/aidp/providers/github_copilot.rb +17 -0
  48. data/lib/aidp/providers/kilocode.rb +11 -0
  49. data/lib/aidp/providers/opencode.rb +11 -0
  50. data/lib/aidp/setup/wizard.rb +249 -39
  51. data/lib/aidp/version.rb +1 -1
  52. data/lib/aidp/watch/build_processor.rb +211 -30
  53. data/lib/aidp/watch/change_request_processor.rb +128 -14
  54. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  55. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  56. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  57. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  58. data/lib/aidp/watch/plan_generator.rb +7 -43
  59. data/lib/aidp/watch/plan_processor.rb +7 -6
  60. data/lib/aidp/watch/repository_client.rb +245 -17
  61. data/lib/aidp/watch/review_processor.rb +98 -17
  62. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  63. data/lib/aidp/watch/runner.rb +181 -29
  64. data/lib/aidp/watch/state_store.rb +22 -1
  65. data/lib/aidp/workflows/definitions.rb +147 -0
  66. data/lib/aidp/workstream_cleanup.rb +245 -0
  67. data/lib/aidp/worktree.rb +19 -0
  68. data/templates/aidp.yml.example +57 -0
  69. data/templates/implementation/generate_tdd_specs.md +213 -0
  70. data/templates/implementation/iterative_implementation.md +122 -0
  71. data/templates/planning/agile/analyze_feedback.md +183 -0
  72. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  73. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  74. data/templates/planning/agile/generate_marketing_report.md +162 -0
  75. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  76. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  77. data/templates/planning/agile/ingest_feedback.md +174 -0
  78. data/templates/planning/assemble_project_plan.md +113 -0
  79. data/templates/planning/assign_personas.md +108 -0
  80. data/templates/planning/create_tasks.md +52 -6
  81. data/templates/planning/generate_gantt.md +86 -0
  82. data/templates/planning/generate_wbs.md +85 -0
  83. data/templates/planning/initialize_planning_mode.md +70 -0
  84. data/templates/skills/README.md +2 -2
  85. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  86. data/templates/skills/product_manager/SKILL.md +177 -0
  87. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  88. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  89. data/templates/skills/ux_researcher/SKILL.md +222 -0
  90. metadata +39 -1
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../logger"
4
+ require "yaml"
5
+
6
+ module Aidp
7
+ module Planning
8
+ module Mappers
9
+ # Maps tasks to personas using Zero Framework Cognition (ZFC)
10
+ # NO heuristics, NO regex, NO keyword matching - pure AI decision making
11
+ class PersonaMapper
12
+ def initialize(ai_decision_engine:, config: nil, mode: :waterfall)
13
+ @ai_decision_engine = ai_decision_engine
14
+ @config = config
15
+ @mode = mode
16
+ end
17
+
18
+ # Assign personas to tasks using ZFC
19
+ # @param task_list [Array<Hash>] List of tasks to assign
20
+ # @param available_personas [Array<String>] Available persona names
21
+ # @return [Hash] Persona assignments
22
+ def assign_personas(task_list, available_personas: nil)
23
+ Aidp.log_debug("persona_mapper", "assign_personas", task_count: task_list.size)
24
+
25
+ available_personas ||= default_personas
26
+
27
+ assignments = {}
28
+
29
+ task_list.each do |task|
30
+ persona = assign_task_to_persona(task, available_personas)
31
+ assignments[task[:id] || task[:name]] = {
32
+ persona: persona,
33
+ task: task[:name],
34
+ phase: task[:phase],
35
+ rationale: "AI-determined based on task characteristics"
36
+ }
37
+
38
+ Aidp.log_debug("persona_mapper", "assigned", task: task[:name], persona: persona)
39
+ end
40
+
41
+ {
42
+ assignments: assignments,
43
+ metadata: {
44
+ generated_at: Time.now.iso8601,
45
+ total_assignments: assignments.size,
46
+ personas_used: assignments.values.map { |a| a[:persona] }.uniq
47
+ }
48
+ }
49
+ end
50
+
51
+ # Generate persona_map.yml configuration
52
+ # @param assignments [Hash] Persona assignments
53
+ # @return [String] YAML configuration
54
+ def generate_persona_map(assignments)
55
+ Aidp.log_debug("persona_mapper", "generate_persona_map")
56
+
57
+ config = {
58
+ "version" => "1.0",
59
+ "generated_at" => Time.now.iso8601,
60
+ "assignments" => format_assignments_for_yaml(assignments[:assignments])
61
+ }
62
+
63
+ YAML.dump(config)
64
+ end
65
+
66
+ private
67
+
68
+ # Assign a single task to the best persona using AI decision engine
69
+ # This is the ZFC pattern - meaning/decisions go to AI, not code
70
+ def assign_task_to_persona(task, available_personas)
71
+ Aidp.log_debug("persona_mapper", "assign_task", task: task[:name])
72
+
73
+ # Use AI decision engine to determine best persona
74
+ # NO regex, NO keyword matching, NO heuristics!
75
+ decision = @ai_decision_engine.decide(
76
+ context: "persona assignment",
77
+ prompt: build_assignment_prompt(task, available_personas),
78
+ data: {
79
+ task_name: task[:name],
80
+ task_description: task[:description],
81
+ task_phase: task[:phase],
82
+ task_effort: task[:effort],
83
+ available_personas: available_personas
84
+ },
85
+ schema: {
86
+ type: "string",
87
+ enum: available_personas
88
+ }
89
+ )
90
+
91
+ Aidp.log_debug("persona_mapper", "ai_decision", persona: decision)
92
+ decision
93
+ end
94
+
95
+ def build_assignment_prompt(task, personas)
96
+ <<~PROMPT
97
+ Assign this task to the most appropriate persona based on the task characteristics.
98
+
99
+ Task: #{task[:name]}
100
+ Description: #{task[:description]}
101
+ Phase: #{task[:phase]}
102
+ Effort: #{task[:effort]}
103
+
104
+ Available Personas: #{personas.join(", ")}
105
+
106
+ Consider:
107
+ - Task type and complexity
108
+ - Required skills and expertise
109
+ - Phase of project (requirements, design, implementation, etc.)
110
+ - Technical vs. product focus
111
+
112
+ Return ONLY the persona name, nothing else.
113
+ PROMPT
114
+ end
115
+
116
+ def default_personas
117
+ case @mode
118
+ when :agile
119
+ agile_personas
120
+ when :waterfall
121
+ waterfall_personas
122
+ else
123
+ waterfall_personas # Default to waterfall
124
+ end
125
+ end
126
+
127
+ def waterfall_personas
128
+ [
129
+ "product_strategist",
130
+ "architect",
131
+ "senior_developer",
132
+ "qa_engineer",
133
+ "devops_engineer",
134
+ "tech_writer"
135
+ ]
136
+ end
137
+
138
+ def agile_personas
139
+ [
140
+ "product_manager",
141
+ "ux_researcher",
142
+ "architect",
143
+ "senior_developer",
144
+ "qa_engineer",
145
+ "devops_engineer",
146
+ "tech_writer",
147
+ "marketing_strategist"
148
+ ]
149
+ end
150
+
151
+ def format_assignments_for_yaml(assignments)
152
+ assignments.transform_values do |assignment|
153
+ {
154
+ "persona" => assignment[:persona],
155
+ "task" => assignment[:task],
156
+ "phase" => assignment[:phase]
157
+ }
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../logger"
4
+
5
+ module Aidp
6
+ module Planning
7
+ module Parsers
8
+ # Parses existing documentation files to extract structured information
9
+ # Uses Zero Framework Cognition (ZFC) for semantic analysis
10
+ class DocumentParser
11
+ def initialize(ai_decision_engine: nil)
12
+ @ai_decision_engine = ai_decision_engine
13
+ end
14
+
15
+ # Parse a single file and detect its structure
16
+ # @param file_path [String] Path to the markdown file
17
+ # @return [Hash] Parsed document with type and sections
18
+ def parse_file(file_path)
19
+ Aidp.log_debug("document_parser", "parse_file", path: file_path)
20
+
21
+ unless File.exist?(file_path)
22
+ Aidp.log_error("document_parser", "file_not_found", path: file_path)
23
+ raise ArgumentError, "File not found: #{file_path}"
24
+ end
25
+
26
+ content = File.read(file_path)
27
+ Aidp.log_debug("document_parser", "read_content", size: content.length)
28
+
29
+ {
30
+ path: file_path,
31
+ type: detect_document_type(content),
32
+ sections: extract_sections(content),
33
+ raw_content: content
34
+ }
35
+ end
36
+
37
+ # Parse all markdown files in a directory
38
+ # @param dir_path [String] Directory path
39
+ # @return [Array<Hash>] Array of parsed documents
40
+ def parse_directory(dir_path)
41
+ Aidp.log_debug("document_parser", "parse_directory", path: dir_path)
42
+
43
+ unless Dir.exist?(dir_path)
44
+ Aidp.log_error("document_parser", "directory_not_found", path: dir_path)
45
+ raise ArgumentError, "Directory not found: #{dir_path}"
46
+ end
47
+
48
+ markdown_files = Dir.glob(File.join(dir_path, "**", "*.md"))
49
+ Aidp.log_debug("document_parser", "found_files", count: markdown_files.size)
50
+
51
+ markdown_files.map { |file| parse_file(file) }
52
+ end
53
+
54
+ private
55
+
56
+ # Detect document type using ZFC (AI decision engine)
57
+ # Returns :prd, :design, :adr, :task_list, or :unknown
58
+ def detect_document_type(content)
59
+ Aidp.log_debug("document_parser", "detect_document_type")
60
+
61
+ # Use AI decision engine if available (ZFC pattern)
62
+ if @ai_decision_engine
63
+ decision = @ai_decision_engine.decide(
64
+ context: "document classification",
65
+ prompt: "Classify this document as PRD, technical design, ADR, task list, or unknown",
66
+ data: {content: content.slice(0, 2000)}, # First 2000 chars
67
+ schema: {
68
+ type: "string",
69
+ enum: ["prd", "design", "adr", "task_list", "unknown"]
70
+ }
71
+ )
72
+ Aidp.log_debug("document_parser", "ai_classification", type: decision)
73
+ return decision.to_sym
74
+ end
75
+
76
+ # Fallback: simple heuristics when AI not available
77
+ # This is acceptable as fallback, but ZFC is preferred
78
+ type = classify_by_heuristics(content)
79
+ Aidp.log_debug("document_parser", "heuristic_classification", type: type)
80
+ type
81
+ end
82
+
83
+ # Fallback classification using basic heuristics
84
+ def classify_by_heuristics(content)
85
+ lower_content = content.downcase
86
+
87
+ return :prd if lower_content.include?("product requirements") ||
88
+ lower_content.include?("user stories") ||
89
+ lower_content.include?("success criteria")
90
+
91
+ return :design if lower_content.include?("technical design") ||
92
+ lower_content.include?("system architecture") ||
93
+ lower_content.include?("component design")
94
+
95
+ return :adr if lower_content.include?("decision record") ||
96
+ lower_content.include?("adr") ||
97
+ lower_content.match?(/##?\s+status/i)
98
+
99
+ return :task_list if lower_content.include?("task list") ||
100
+ lower_content.include?("- [ ]") ||
101
+ lower_content.match?(/\d+\.\s+\[[ x]\]/i)
102
+
103
+ :unknown
104
+ end
105
+
106
+ # Extract markdown sections from content
107
+ # Returns hash of section_name => section_content
108
+ def extract_sections(content)
109
+ Aidp.log_debug("document_parser", "extract_sections")
110
+
111
+ sections = {}
112
+ current_section = nil
113
+ current_content = []
114
+
115
+ content.each_line do |line|
116
+ if line.match?(/^##?\s+(.+)/)
117
+ # Save previous section
118
+ if current_section
119
+ sections[current_section] = current_content.join.strip
120
+ end
121
+
122
+ # Start new section
123
+ current_section = line.match(/^##?\s+(.+)/)[1].strip.downcase.gsub(/\s+/, "_")
124
+ current_content = []
125
+ elsif current_section
126
+ current_content << line
127
+ end
128
+ end
129
+
130
+ # Save last section
131
+ if current_section
132
+ sections[current_section] = current_content.join.strip
133
+ end
134
+
135
+ Aidp.log_debug("document_parser", "extracted_sections", count: sections.size)
136
+ sections
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "json"
5
+ require_relative "../../logger"
6
+
7
+ module Aidp
8
+ module Planning
9
+ module Parsers
10
+ # Parse feedback data from multiple formats (CSV, JSON, markdown)
11
+ # Normalizes data into consistent structure for analysis
12
+ class FeedbackDataParser
13
+ class FeedbackParseError < StandardError; end
14
+
15
+ def initialize(file_path:)
16
+ @file_path = file_path
17
+ @format = detect_format
18
+ end
19
+
20
+ # Parse feedback file and return normalized structure
21
+ # @return [Hash] Normalized feedback data
22
+ def parse
23
+ Aidp.log_debug("feedback_data_parser", "parsing", file: @file_path, format: @format)
24
+
25
+ case @format
26
+ when :csv
27
+ parse_csv
28
+ when :json
29
+ parse_json
30
+ when :markdown
31
+ parse_markdown
32
+ else
33
+ raise FeedbackParseError, "Unsupported format: #{@format}"
34
+ end
35
+ rescue => e
36
+ Aidp.log_error("feedback_data_parser", "parse_failed", error: e.message, file: @file_path)
37
+ raise FeedbackParseError, "Failed to parse feedback file: #{e.message}"
38
+ end
39
+
40
+ private
41
+
42
+ def detect_format
43
+ ext = File.extname(@file_path).downcase
44
+
45
+ case ext
46
+ when ".csv"
47
+ :csv
48
+ when ".json"
49
+ :json
50
+ when ".md", ".markdown"
51
+ :markdown
52
+ else
53
+ raise FeedbackParseError, "Unknown file extension: #{ext}"
54
+ end
55
+ end
56
+
57
+ def parse_csv
58
+ Aidp.log_debug("feedback_data_parser", "parsing_csv")
59
+
60
+ unless File.exist?(@file_path)
61
+ raise FeedbackParseError, "File not found: #{@file_path}"
62
+ end
63
+
64
+ rows = CSV.read(@file_path, headers: true)
65
+ responses = rows.map { |row| normalize_csv_row(row) }
66
+
67
+ {
68
+ format: :csv,
69
+ source_file: @file_path,
70
+ parsed_at: Time.now.iso8601,
71
+ response_count: responses.size,
72
+ responses: responses,
73
+ metadata: extract_csv_metadata(rows)
74
+ }
75
+ end
76
+
77
+ def parse_json
78
+ Aidp.log_debug("feedback_data_parser", "parsing_json")
79
+
80
+ unless File.exist?(@file_path)
81
+ raise FeedbackParseError, "File not found: #{@file_path}"
82
+ end
83
+
84
+ data = JSON.parse(File.read(@file_path))
85
+
86
+ # Support both array of responses and object with responses key
87
+ responses = if data.is_a?(Array)
88
+ data
89
+ elsif data.is_a?(Hash) && data["responses"]
90
+ data["responses"]
91
+ else
92
+ raise FeedbackParseError, "Invalid JSON structure: expected array or {responses: [...]}"
93
+ end
94
+
95
+ normalized_responses = responses.map { |r| normalize_json_response(r) }
96
+
97
+ {
98
+ format: :json,
99
+ source_file: @file_path,
100
+ parsed_at: Time.now.iso8601,
101
+ response_count: normalized_responses.size,
102
+ responses: normalized_responses,
103
+ metadata: extract_json_metadata(data)
104
+ }
105
+ end
106
+
107
+ def parse_markdown
108
+ Aidp.log_debug("feedback_data_parser", "parsing_markdown")
109
+
110
+ unless File.exist?(@file_path)
111
+ raise FeedbackParseError, "File not found: #{@file_path}"
112
+ end
113
+
114
+ content = File.read(@file_path)
115
+ responses = extract_markdown_responses(content)
116
+
117
+ {
118
+ format: :markdown,
119
+ source_file: @file_path,
120
+ parsed_at: Time.now.iso8601,
121
+ response_count: responses.size,
122
+ responses: responses,
123
+ metadata: extract_markdown_metadata(content)
124
+ }
125
+ end
126
+
127
+ def normalize_csv_row(row)
128
+ {
129
+ respondent_id: row["id"] || row["respondent_id"] || row["user_id"],
130
+ timestamp: row["timestamp"] || row["date"] || row["submitted_at"],
131
+ rating: parse_rating(row["rating"] || row["score"]),
132
+ feedback_text: row["feedback"] || row["comments"] || row["response"],
133
+ feature: row["feature"] || row["area"],
134
+ sentiment: row["sentiment"],
135
+ tags: parse_tags(row["tags"]),
136
+ raw_data: row.to_h
137
+ }
138
+ end
139
+
140
+ def normalize_json_response(response)
141
+ {
142
+ respondent_id: response["id"] || response["respondent_id"] || response["user_id"],
143
+ timestamp: response["timestamp"] || response["date"] || response["submitted_at"],
144
+ rating: parse_rating(response["rating"] || response["score"]),
145
+ feedback_text: response["feedback"] || response["comments"] || response["response"] || response["text"],
146
+ feature: response["feature"] || response["area"] || response["category"],
147
+ sentiment: response["sentiment"],
148
+ tags: parse_tags(response["tags"]),
149
+ raw_data: response
150
+ }
151
+ end
152
+
153
+ def extract_markdown_responses(content)
154
+ # Simple markdown parser that looks for response sections
155
+ # Format: ## Response N or ### Respondent: ID
156
+ responses = []
157
+ current_response = nil
158
+
159
+ content.each_line do |line|
160
+ if line =~ /^##+ Response (\d+)/i || line =~ /^##+ Respondent:?\s*(.+)/i
161
+ responses << current_response if current_response
162
+ current_response = {text: "", metadata: {}}
163
+ elsif current_response
164
+ # Extract key-value pairs like **Rating:** 5
165
+ if line =~ /\*\*(.+?):\*\*\s*(.+)/
166
+ key = $1.downcase.strip
167
+ value = $2.strip
168
+ current_response[:metadata][key] = value
169
+ else
170
+ current_response[:text] += line
171
+ end
172
+ end
173
+ end
174
+
175
+ responses << current_response if current_response
176
+
177
+ responses.map do |resp|
178
+ {
179
+ respondent_id: resp[:metadata]["id"] || resp[:metadata]["respondent"],
180
+ timestamp: resp[:metadata]["timestamp"] || resp[:metadata]["date"],
181
+ rating: parse_rating(resp[:metadata]["rating"] || resp[:metadata]["score"]),
182
+ feedback_text: resp[:text].strip,
183
+ feature: resp[:metadata]["feature"] || resp[:metadata]["area"],
184
+ sentiment: resp[:metadata]["sentiment"],
185
+ tags: parse_tags(resp[:metadata]["tags"]),
186
+ raw_data: resp[:metadata]
187
+ }
188
+ end
189
+ end
190
+
191
+ def parse_rating(value)
192
+ return nil if value.nil? || value.to_s.strip.empty?
193
+
194
+ # Handle numeric ratings, star ratings, etc.
195
+ if value.to_s =~ /^(\d+)(?:\/\d+)?$/
196
+ $1.to_i
197
+ elsif value.to_s =~ /^(\d+)\s*stars?$/i
198
+ $1.to_i
199
+ else
200
+ value.to_s
201
+ end
202
+ end
203
+
204
+ def parse_tags(value)
205
+ return [] if value.nil? || value.to_s.strip.empty?
206
+
207
+ if value.is_a?(Array)
208
+ value
209
+ elsif value.is_a?(String)
210
+ value.split(/[,;]/).map(&:strip).reject(&:empty?)
211
+ else
212
+ []
213
+ end
214
+ end
215
+
216
+ def extract_csv_metadata(rows)
217
+ {
218
+ total_rows: rows.size,
219
+ columns: rows.headers,
220
+ has_timestamps: rows.headers.any? { |h| h&.match?(/timestamp|date/i) },
221
+ has_ratings: rows.headers.any? { |h| h&.match?(/rating|score/i) }
222
+ }
223
+ end
224
+
225
+ def extract_json_metadata(data)
226
+ metadata = data.is_a?(Hash) ? data.except("responses") : {}
227
+
228
+ {
229
+ survey_name: metadata["survey_name"] || metadata["name"],
230
+ survey_id: metadata["survey_id"] || metadata["id"],
231
+ created_at: metadata["created_at"] || metadata["timestamp"],
232
+ additional_fields: metadata.keys - ["responses", "survey_name", "name", "id", "survey_id", "created_at", "timestamp"]
233
+ }
234
+ end
235
+
236
+ def extract_markdown_metadata(content)
237
+ # Extract YAML front matter if present
238
+ if content =~ /^---\s*\n(.*?)\n---\s*\n/m
239
+ begin
240
+ YAML.safe_load($1, permitted_classes: [Date, Time, Symbol]) || {}
241
+ rescue => e
242
+ Aidp.log_debug("feedback_data_parser", "yaml_parse_failed", error: e.message)
243
+ {}
244
+ end
245
+ else
246
+ {}
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -7,15 +7,10 @@ module Aidp
7
7
  class ProviderManager
8
8
  class << self
9
9
  def get_provider(provider_type, options = {})
10
- # Use harness factory if available
11
- if options[:use_harness] != false
12
- factory = get_harness_factory
13
- return factory.create_provider(provider_type, options) if factory
14
- end
10
+ factory = get_harness_factory
11
+ raise "Harness factory not available" unless factory
15
12
 
16
- # Fallback to legacy method
17
- prompt = options[:prompt] || TTY::Prompt.new
18
- create_legacy_provider(provider_type, prompt: prompt)
13
+ factory.create_provider(provider_type, options)
19
14
  end
20
15
 
21
16
  def load_from_config(config = {}, options = {})
@@ -62,7 +57,7 @@ module Aidp
62
57
  factory = get_harness_factory
63
58
  return [] unless factory
64
59
 
65
- enabled_names = factory.get_enabled_providers(options)
60
+ enabled_names = factory.enabled_providers(options)
66
61
  factory.create_providers(enabled_names, options)
67
62
  end
68
63
 
@@ -71,7 +66,7 @@ module Aidp
71
66
  factory = get_harness_factory
72
67
  return false unless factory
73
68
 
74
- factory.get_configured_providers(options).include?(provider_name.to_s)
69
+ factory.configured_providers(options).include?(provider_name.to_s)
75
70
  end
76
71
 
77
72
  # Check if provider is enabled
@@ -79,7 +74,7 @@ module Aidp
79
74
  factory = get_harness_factory
80
75
  return false unless factory
81
76
 
82
- factory.get_enabled_providers(options).include?(provider_name.to_s)
77
+ factory.enabled_providers(options).include?(provider_name.to_s)
83
78
  end
84
79
 
85
80
  # Get provider capabilities
@@ -87,7 +82,7 @@ module Aidp
87
82
  factory = get_harness_factory
88
83
  return [] unless factory
89
84
 
90
- factory.get_provider_capabilities(provider_name, options)
85
+ factory.provider_capabilities(provider_name, options)
91
86
  end
92
87
 
93
88
  # Check if provider supports feature
@@ -103,7 +98,7 @@ module Aidp
103
98
  factory = get_harness_factory
104
99
  return [] unless factory
105
100
 
106
- factory.get_provider_models(provider_name, options)
101
+ factory.provider_models(provider_name, options)
107
102
  end
108
103
 
109
104
  # Validate provider configuration
@@ -131,25 +126,6 @@ module Aidp
131
126
  def reload_config
132
127
  @harness_factory&.reload_config
133
128
  end
134
-
135
- private
136
-
137
- def create_legacy_provider(provider_type, prompt: TTY::Prompt.new)
138
- case provider_type
139
- when "cursor"
140
- Aidp::Providers::Cursor.new(prompt: prompt)
141
- when "anthropic", "claude"
142
- Aidp::Providers::Anthropic.new(prompt: prompt)
143
- when "gemini"
144
- Aidp::Providers::Gemini.new(prompt: prompt)
145
- when "kilocode"
146
- Aidp::Providers::Kilocode.new(prompt: prompt)
147
- when "github_copilot"
148
- Aidp::Providers::GithubCopilot.new(prompt: prompt)
149
- when "codex"
150
- Aidp::Providers::Codex.new(prompt: prompt)
151
- end
152
- end
153
129
  end
154
130
  end
155
131
  end