aidp 0.40.0 → 0.42.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2414ec5ab6a21100ba8ccb1d6ae8ddc6bb137a5403b069ff941707c17c9ce9fa
4
- data.tar.gz: fb0401b3daf671719690b5c5347418eab335e8090dac38ebe08aef43a921430d
3
+ metadata.gz: f44b47ad0d31015794ec7b2286dc659e85061eb3e6dad8c89ae018976e0d9b1d
4
+ data.tar.gz: 6d8b28a431c3d55e037e32d0e54e583313af4d495982578b72ab3374155323c5
5
5
  SHA512:
6
- metadata.gz: c033c034fed453eb4daa2108bb653f3da5007e0d49a792d4a3d9b4f976d63de3c796647da98d3131ea4b81d41425b7aea19ab4e94e743b2eb59936eea9dcad59
7
- data.tar.gz: 70ebc3d5e2f809e21398321b7b26c315309a849a35ab4be5742d58650a123a4826c69e64c0b9e8fbc0aef7402da7a99a9970c0d49e83e516aab436e57187d974
6
+ metadata.gz: d2f838a10c7789f69a253858d0d71aee754a24020722a845eba38cd29eb764be247fee615427afe67f13618cec384565c04cf0f25a25ddd2493962df0df3e0af
7
+ data.tar.gz: 616bf1a6910461b907c5ab99bccb8dddc3312ee7816550d22d31c395b9faecb90e97bd4e5a032ba594a5d78538d9199a0ffbee203a8fe5c08ae8f784ae47b84e
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "shellwords"
5
+
6
+ module Aidp
7
+ module Execute
8
+ # Creates and refreshes concise feature/tool notes under docs/
9
+ class ProjectKnowledgeManager
10
+ MAX_TOOL_NOTES = 5
11
+ FEATURE_ROOT_SEGMENTS = %w[app client lib packages server src].freeze
12
+
13
+ def initialize(project_dir)
14
+ @project_dir = project_dir
15
+ end
16
+
17
+ def sync!(step_name:, task_description:, affected_files:, tool_commands:, feature_identifier: nil)
18
+ sync_feature_note(
19
+ feature_identifier: feature_identifier,
20
+ task_description: task_description,
21
+ affected_files: affected_files
22
+ )
23
+ sync_tool_notes(step_name: step_name, affected_files: affected_files, tool_commands: tool_commands)
24
+ end
25
+
26
+ private
27
+
28
+ def sync_feature_note(feature_identifier:, task_description:, affected_files:)
29
+ return if affected_files.empty?
30
+
31
+ feature_id = resolve_feature_id(
32
+ feature_identifier: feature_identifier,
33
+ affected_files: affected_files
34
+ )
35
+ return if feature_id.empty?
36
+
37
+ path = File.join(@project_dir, "docs", "features", "#{feature_id}.md")
38
+ markers = affected_files.map { |file| "<#{feature_id}:#{file}>" }
39
+ learning = bullet_line("#{Time.now.utc.iso8601}: #{learning_summary(task_description)}")
40
+
41
+ content = if File.exist?(path)
42
+ update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning)
43
+ else
44
+ build_feature_note(feature_id: feature_id, markers: markers, learning: learning)
45
+ end
46
+
47
+ write_note(path, content)
48
+ end
49
+
50
+ def sync_tool_notes(step_name:, affected_files:, tool_commands:)
51
+ tool_commands.first(MAX_TOOL_NOTES).each do |command|
52
+ tool_id = tool_identifier(command)
53
+ next if tool_id.empty?
54
+
55
+ path = File.join(@project_dir, "docs", "tools", "#{tool_id}.md")
56
+ commands = [bullet_line("`#{command}`")]
57
+ markers = affected_files.map { |file| "<#{tool_id}:#{file}>" }
58
+ learning = bullet_line("#{Time.now.utc.iso8601}: Used `#{command}` during `#{step_name}`.")
59
+
60
+ content = if File.exist?(path)
61
+ update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning, commands: commands)
62
+ else
63
+ build_tool_note(commands: commands, tool_id: tool_id, markers: markers, learning: learning)
64
+ end
65
+
66
+ write_note(path, content)
67
+ end
68
+ end
69
+
70
+ def build_feature_note(feature_id:, markers:, learning:)
71
+ <<~MARKDOWN
72
+ # #{humanize(feature_id)}
73
+
74
+ ## Paths
75
+ #{markers.join("\n")}
76
+
77
+ ## Purpose
78
+ - Concise feature context for work touching these paths.
79
+
80
+ ## Users
81
+ - Add the end users or personas when they become clear.
82
+
83
+ ## Known Gaps
84
+ - Add concrete gaps when discovered.
85
+
86
+ ## Key Decisions
87
+ - Add behavior-affecting decisions when they become clear.
88
+
89
+ ## Recent Learnings
90
+ #{learning}
91
+ MARKDOWN
92
+ end
93
+
94
+ def build_tool_note(commands:, tool_id:, markers:, learning:)
95
+ paths_section = markers.empty? ? "- Add affected paths as they become known." : markers.join("\n")
96
+
97
+ <<~MARKDOWN
98
+ # #{humanize(tool_id)}
99
+
100
+ ## Commands
101
+ #{commands.join("\n")}
102
+
103
+ ## Paths
104
+ #{paths_section}
105
+
106
+ ## Intended Use
107
+ - Run this tool while validating or updating related paths.
108
+
109
+ ## Known Gaps
110
+ - Add environment quirks or failure modes when discovered.
111
+
112
+ ## Recent Learnings
113
+ #{learning}
114
+ MARKDOWN
115
+ end
116
+
117
+ def update_existing_note(content, markers:, learning:, commands: [])
118
+ updated = ensure_section_entries(content, "Commands", commands)
119
+ updated = ensure_section_entries(updated, "Paths", markers)
120
+ ensure_section_entries(updated, "Recent Learnings", [learning], normalizer: method(:normalize_learning_entry))
121
+ end
122
+
123
+ def ensure_section_entries(content, section_name, entries, normalizer: nil)
124
+ return content if entries.empty?
125
+
126
+ existing_section = content.match(/(## #{Regexp.escape(section_name)}\n)(.*?)(?=\n## |\z)/m)
127
+ return append_new_section(content, section_name, entries) unless existing_section
128
+
129
+ body = existing_section[2].to_s
130
+ existing_entries = body.lines.map(&:strip).reject(&:empty?)
131
+ normalized_existing = normalize_section_entries(existing_entries, normalizer)
132
+ missing = entries.reject do |entry|
133
+ normalized_existing.include?(normalize_section_entry(entry, normalizer))
134
+ end
135
+ return content if missing.empty?
136
+
137
+ replacement = "#{existing_section[1]}#{body.rstrip}\n#{missing.join("\n")}\n"
138
+ content.sub(existing_section[0], replacement)
139
+ end
140
+
141
+ def append_new_section(content, section_name, entries)
142
+ [content.rstrip, "", "## #{section_name}", entries.join("\n"), ""].join("\n")
143
+ end
144
+
145
+ def write_note(path, content)
146
+ FileUtils.mkdir_p(File.dirname(path))
147
+ File.write(path, content)
148
+ end
149
+
150
+ def resolve_feature_id(feature_identifier:, affected_files:)
151
+ explicit_id = normalize_id(feature_identifier)
152
+ return explicit_id unless explicit_id.empty?
153
+
154
+ derived_id = normalize_id(feature_path_identifier(affected_files))
155
+ return derived_id unless derived_id.empty?
156
+
157
+ ""
158
+ end
159
+
160
+ def feature_path_identifier(affected_files)
161
+ feature_files = affected_files.select { |file| feature_path?(file) }
162
+ segments = feature_files.map { |file| trimmed_feature_segments(file) }
163
+ return "" if segments.empty?
164
+
165
+ shared_segments = meaningful_segments(common_path_segments(segments))
166
+ return shared_segments.join("_") unless shared_segments.empty?
167
+
168
+ meaningful_segments(segments.first).join("_")
169
+ end
170
+
171
+ def feature_path?(file)
172
+ path_segments = file.to_s.split("/")
173
+ FEATURE_ROOT_SEGMENTS.include?(path_segments.first) && path_segments.length > 1
174
+ end
175
+
176
+ def common_path_segments(all_segments)
177
+ shared = []
178
+ max_length = all_segments.map(&:length).min
179
+
180
+ max_length.times do |index|
181
+ segment = all_segments.first[index]
182
+ break unless all_segments.all? { |segments| segments[index] == segment }
183
+
184
+ shared << segment
185
+ end
186
+
187
+ shared
188
+ end
189
+
190
+ def meaningful_segments(path_segments)
191
+ path_segments.map { |segment| segment.sub(/\.[^.]+\z/, "") }
192
+ .reject { |segment| generic_path_segment?(segment) }
193
+ .reject(&:empty?)
194
+ end
195
+
196
+ def trimmed_feature_segments(file)
197
+ segments = file.to_s.split("/")
198
+ return segments unless generic_path_segment?(segments.first)
199
+
200
+ segments.drop(1)
201
+ end
202
+
203
+ def generic_path_segment?(segment)
204
+ %w[app client docs lib packages server spec src test].include?(segment)
205
+ end
206
+
207
+ def normalize_id(value)
208
+ value.to_s.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_|_\z/, "")
209
+ end
210
+
211
+ def learning_summary(task_description)
212
+ single_line = task_description.to_s.encode("UTF-8", invalid: :replace, undef: :replace)
213
+ .lines
214
+ .map(&:strip)
215
+ .reject(&:empty?)
216
+ .join(" ")
217
+ .gsub(/\s+/, " ")
218
+
219
+ return "Updated feature context." if single_line.empty?
220
+ return single_line if single_line.length <= 200
221
+
222
+ "#{single_line[0, 197].rstrip}..."
223
+ end
224
+
225
+ def normalize_section_entries(entries, normalizer)
226
+ entries.map { |entry| normalize_section_entry(entry, normalizer) }
227
+ end
228
+
229
+ def normalize_section_entry(entry, normalizer)
230
+ return entry unless normalizer
231
+
232
+ normalizer.call(entry)
233
+ end
234
+
235
+ def normalize_learning_entry(entry)
236
+ entry.to_s.sub(/\A-\s*/, "").sub(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z:\s*/, "")
237
+ end
238
+
239
+ def tool_identifier(command)
240
+ raw_command = command.to_s.strip
241
+ tokens = Shellwords.split(raw_command)
242
+ executable = unwrap_tool_command(strip_env_assignments(tokens))
243
+ normalize_id(executable)
244
+ rescue ArgumentError
245
+ normalize_id(raw_command.split(/\s+/).first)
246
+ end
247
+
248
+ def unwrap_tool_command(tokens)
249
+ command_tokens = tokens.dup
250
+
251
+ loop do
252
+ return "" if command_tokens.empty?
253
+
254
+ case command_tokens.first
255
+ when "mise"
256
+ command_tokens = unwrap_mise_exec(command_tokens)
257
+ when "env", "/usr/bin/env"
258
+ command_tokens = unwrap_env(command_tokens)
259
+ when "bundle"
260
+ return File.basename(command_tokens.first, ".rb") unless command_tokens[1] == "exec"
261
+
262
+ command_tokens = command_tokens.drop(2)
263
+ when "ruby"
264
+ return File.basename(command_tokens[1], ".rb") if command_tokens[1]
265
+
266
+ command_tokens = []
267
+ else
268
+ return File.basename(command_tokens.first, ".rb")
269
+ end
270
+ end
271
+ end
272
+
273
+ def strip_env_assignments(tokens)
274
+ tokens.drop_while { |token| token.include?("=") && !token.start_with?("/", "./", "../") }
275
+ end
276
+
277
+ def unwrap_mise_exec(tokens)
278
+ return tokens unless tokens[1] == "exec"
279
+
280
+ separator_index = tokens.index("--")
281
+ remaining = separator_index ? tokens.drop(separator_index + 1) : tokens.drop(2)
282
+ strip_env_assignments(remaining)
283
+ end
284
+
285
+ def unwrap_env(tokens)
286
+ remaining = tokens.drop(1)
287
+ remaining = remaining.drop_while { |token| token.start_with?("-") }
288
+ strip_env_assignments(remaining)
289
+ end
290
+
291
+ def humanize(value)
292
+ value.to_s.split(/[_\s]+/).map(&:capitalize).join(" ")
293
+ end
294
+
295
+ def bullet_line(value)
296
+ "- #{value}"
297
+ end
298
+ end
299
+ end
300
+ end
@@ -8,9 +8,11 @@ require_relative "guard_policy"
8
8
  require_relative "work_loop_unit_scheduler"
9
9
  require_relative "deterministic_unit"
10
10
  require_relative "agent_signal_parser"
11
+ require_relative "project_knowledge_manager"
11
12
  require_relative "steps"
12
13
  require_relative "../harness/test_runner"
13
14
  require_relative "../errors"
15
+ require_relative "../prompts/prompt_template_manager"
14
16
  require_relative "../style_guide/selector"
15
17
  require_relative "../security"
16
18
 
@@ -44,7 +46,7 @@ module Aidp
44
46
  # Expose state for testability
45
47
  attr_accessor :iteration_count, :step_name, :options, :persistent_tasklist
46
48
  attr_reader :project_dir, :current_state, :state_history, :test_runner, :prompt_manager, :checkpoint
47
- attr_writer :guard_policy, :prompt_manager, :style_guide_selector
49
+ attr_writer :guard_policy, :prompt_manager, :prompt_template_manager, :style_guide_selector
48
50
 
49
51
  MAX_ITERATIONS = 50 # Safety limit
50
52
  CHECKPOINT_INTERVAL = 5 # Record checkpoint every N iterations
@@ -60,7 +62,10 @@ module Aidp
60
62
  @checkpoint = Checkpoint.new(project_dir)
61
63
  @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
62
64
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
65
+ @project_knowledge_manager = options[:project_knowledge_manager] || ProjectKnowledgeManager.new(project_dir)
63
66
  @work_context = {}
67
+ @executed_tool_commands = []
68
+ @agentic_execution_results = []
64
69
  @persistent_tasklist = PersistentTasklist.new(project_dir)
65
70
  @iteration_count = 0
66
71
  @step_name = nil
@@ -87,6 +92,7 @@ module Aidp
87
92
 
88
93
  # FIX for issue #391: Initialize prompt evaluator for iteration threshold assessment
89
94
  @prompt_evaluator = options[:prompt_evaluator] || PromptEvaluator.new(config)
95
+ @prompt_template_manager = options[:prompt_template_manager] || Prompts::PromptTemplateManager.new(project_dir: project_dir)
90
96
 
91
97
  # Initialize security adapter for Rule of Two enforcement
92
98
  @security_adapter = options[:security_adapter] || Aidp::Security::WorkLoopAdapter.new(project_dir: project_dir)
@@ -98,6 +104,8 @@ module Aidp
98
104
  def execute_step(step_name, step_spec, context = {})
99
105
  @step_name = step_name
100
106
  @work_context = context
107
+ @executed_tool_commands = []
108
+ @agentic_execution_results = []
101
109
  @iteration_count = 0
102
110
  transition_to(:ready)
103
111
 
@@ -570,6 +578,7 @@ module Aidp
570
578
  tier: @thinking_depth_manager.current_tier
571
579
  }
572
580
  )
581
+ record_agentic_execution_result(agent_result)
573
582
 
574
583
  requested = AgentSignalParser.extract_next_unit(agent_result[:output])
575
584
 
@@ -603,6 +612,7 @@ module Aidp
603
612
  tier: @thinking_depth_manager.current_tier
604
613
  }
605
614
  )
615
+ record_agentic_execution_result(agent_result)
606
616
 
607
617
  requested = AgentSignalParser.extract_next_unit(agent_result[:output])
608
618
 
@@ -776,11 +786,13 @@ module Aidp
776
786
  if @config.respond_to?(:commands) && @config.commands.any?
777
787
  # New phase-based execution
778
788
  each_unit_results = @test_runner.run_commands_for_phase(:each_unit)
789
+ record_phase_commands(:each_unit, each_unit_results)
779
790
  all_results[:each_unit] = each_unit_results
780
791
 
781
792
  # Run on_completion commands only if agent marked work complete
782
793
  if agent_marked_complete?(agent_result)
783
794
  on_completion_results = @test_runner.run_commands_for_phase(:on_completion)
795
+ record_phase_commands(:on_completion, on_completion_results)
784
796
  all_results[:on_completion] = on_completion_results
785
797
  else
786
798
  all_results[:on_completion] = {
@@ -808,6 +820,7 @@ module Aidp
808
820
  return {success: true, output: "", failures: [], required_failures: []} unless @config.respond_to?(:commands)
809
821
 
810
822
  full_loop_results = @test_runner.run_commands_for_phase(:full_loop)
823
+ record_phase_commands(:full_loop, full_loop_results)
811
824
 
812
825
  Aidp.log_debug("work_loop", "ran_full_loop_commands",
813
826
  success: full_loop_results[:success],
@@ -819,13 +832,17 @@ module Aidp
819
832
  # Run commands using legacy category-based approach (backwards compatibility)
820
833
  def run_legacy_category_commands(agent_result)
821
834
  test_results = @test_runner.run_tests
835
+ record_legacy_category_commands(:tests)
822
836
  lint_results = @test_runner.run_linters
837
+ record_legacy_category_commands(:lints)
823
838
  build_results = @test_runner.run_builds
839
+ record_legacy_category_commands(:builds)
824
840
  doc_results = @test_runner.run_documentation
841
+ record_legacy_category_commands(:docs)
825
842
 
826
843
  # Run formatters only if agent marked work complete (per issue #234)
827
844
  formatter_results = if agent_marked_complete?(agent_result)
828
- @test_runner.run_formatters
845
+ @test_runner.run_formatters.tap { record_legacy_category_commands(:formatters) }
829
846
  else
830
847
  {success: true, output: "Formatters: Skipped (work not complete)", failures: [], required_failures: []}
831
848
  end
@@ -957,20 +974,8 @@ module Aidp
957
974
 
958
975
  # Extract files that will be affected by this work
959
976
  def extract_affected_files(context, user_input)
960
- files = []
961
-
962
- # From user input (e.g., "update lib/user.rb")
963
- user_input&.scan(/[\w\/]+\.rb/)&.each do |file|
964
- files << file
965
- end
966
-
967
- # From deterministic outputs
968
- context[:deterministic_outputs]&.each do |output|
969
- if output[:output_path]&.end_with?(".rb")
970
- files << output[:output_path]
971
- end
972
- end
973
-
977
+ files = extract_paths_from_text(user_input)
978
+ files.concat(extract_output_paths(context[:deterministic_outputs]))
974
979
  files.uniq
975
980
  end
976
981
 
@@ -1039,71 +1044,20 @@ module Aidp
1039
1044
  end
1040
1045
 
1041
1046
  def build_initial_prompt_content(template:, prd:, style_guide:, user_input:, step_name:, deterministic_outputs:, previous_agent_summary:, task_description:, additional_context:)
1042
- parts = []
1043
-
1044
- parts << "# Work Loop: #{step_name}"
1045
- parts << ""
1046
- parts << "## Instructions"
1047
- parts << "You are working in a work loop. Your responsibilities:"
1048
- parts << "1. Read this file (`.aidp/PROMPT.md`) to understand what needs to be done"
1049
- parts << "2. Complete the work described below"
1050
- parts << "3. **IMPORTANT**: Edit this `.aidp/PROMPT.md` file to:"
1051
- parts << " - Remove completed items"
1052
- parts << " - Update with current status"
1053
- parts << " - Keep it concise (remove unnecessary context)"
1054
- parts << " - Mark the step COMPLETE when 100% done"
1055
- parts << "4. After you finish, tests and linters will run automatically"
1056
- parts << "5. If tests/linters fail, you'll see the errors in the next iteration"
1057
- parts << ""
1058
- parts << "## Completion Criteria"
1059
- parts << "Mark this step COMPLETE by adding this line to `.aidp/PROMPT.md`:"
1060
- parts << "```"
1061
- parts << "STATUS: COMPLETE"
1062
- parts << "```"
1063
- parts << ""
1064
-
1065
- if user_input && !user_input.empty?
1066
- parts << "## User Input"
1067
- parts << user_input
1068
- parts << ""
1069
- end
1070
-
1071
- if previous_agent_summary && !previous_agent_summary.empty?
1072
- parts << "## Previous Agent Summary"
1073
- parts << previous_agent_summary
1074
- parts << ""
1075
- end
1076
-
1077
- unless deterministic_outputs.empty?
1078
- parts << "## Recent Deterministic Outputs"
1079
- deterministic_outputs.each do |entry|
1080
- parts << "- #{entry[:name]} (status: #{entry[:status]})"
1081
- parts << " Output: #{entry[:output_path] || "n/a"}"
1082
- end
1083
- parts << ""
1084
- end
1085
-
1086
- if style_guide
1087
- parts << "## LLM Style Guide"
1088
- parts << style_guide
1089
- parts << ""
1090
- end
1091
-
1092
- if prd
1093
- parts << "## Product Requirements (PRD)"
1094
- parts << prd
1095
- parts << ""
1096
- end
1097
-
1098
- parts << "## Task Template"
1099
- parts << interpolate_task_template(
1100
- template,
1101
- task_description: task_description,
1102
- additional_context: additional_context
1047
+ @prompt_template_manager.render(
1048
+ "work_loop/initial_prompt",
1049
+ STEP_NAME: step_name,
1050
+ USER_INPUT_SECTION: optional_section("## User Input", user_input),
1051
+ PREVIOUS_AGENT_SUMMARY_SECTION: optional_section("## Previous Agent Summary", previous_agent_summary),
1052
+ DETERMINISTIC_OUTPUTS_SECTION: deterministic_outputs_section(deterministic_outputs),
1053
+ STYLE_GUIDE_SECTION: optional_section("## LLM Style Guide", style_guide),
1054
+ PRD_SECTION: optional_section("## Product Requirements (PRD)", prd),
1055
+ TASK_TEMPLATE: interpolate_task_template(
1056
+ template,
1057
+ task_description: task_description,
1058
+ additional_context: additional_context
1059
+ )
1103
1060
  )
1104
- parts << ""
1105
-
1106
- parts.join("\n")
1107
1061
  end
1108
1062
 
1109
1063
  def send_to_agent(selected_provider: nil, selected_model: nil)
@@ -1159,6 +1113,7 @@ module Aidp
1159
1113
  execute_block.call
1160
1114
  end
1161
1115
  end
1116
+ .tap { |result| record_agentic_execution_result(result) }
1162
1117
  end
1163
1118
 
1164
1119
  def display_iteration_overview(provider_name, model_name, prompt_length, checks_summary = nil)
@@ -1325,88 +1280,14 @@ module Aidp
1325
1280
 
1326
1281
  # FIX for issue #391: Enhanced work loop header with upfront task filing requirements
1327
1282
  def build_work_loop_header(step_name, iteration)
1328
- parts = []
1329
- parts << "# Work Loop: #{step_name} (Iteration #{iteration})"
1330
- parts << ""
1331
- parts << "## Instructions"
1332
- parts << "You are working in a work loop. Your responsibilities:"
1333
- parts << "1. **FIRST**: File tasks for all work items (see Task Filing section below)"
1334
- parts << "2. Read the task description below to understand what needs to be done"
1335
- parts << "3. **Write/edit CODE files** to implement the required changes"
1336
- parts << "4. Run tests to verify your changes work correctly"
1337
- parts << "5. Update task status as you complete items"
1338
- parts << "6. When ALL tasks are complete and tests pass, mark the step COMPLETE"
1339
- parts << ""
1340
- parts << "## Important Notes"
1341
- parts << "- You have full file system access - create and edit files as needed"
1342
- parts << "- The working directory is: #{@project_dir}"
1343
- parts << "- After you finish, tests and linters will run automatically"
1344
- parts << "- If tests/linters fail, you'll see the errors in the next iteration and can fix them"
1345
- parts << ""
1346
- parts << "## ⚠️ Code Changes Required"
1347
- parts << "**IMPORTANT**: This implementation requires actual code changes."
1348
- parts << "- Documentation-only changes will NOT be accepted as complete"
1349
- parts << "- Configuration-only changes will NOT be accepted as complete"
1350
- parts << "- You must modify/create code files (.rb, .py, .js, etc.) to implement the feature/fix"
1351
- parts << "- Tests should accompany code changes"
1352
- parts << ""
1353
-
1354
- if @config.task_completion_required?
1355
- parts << "## Task Filing (REQUIRED - DO THIS FIRST)"
1356
- parts << "**CRITICAL**: This work loop requires task tracking. You MUST file tasks before implementation."
1357
- parts << ""
1358
- parts << "### Step 1: File Tasks Immediately"
1359
- parts << "In your FIRST iteration, analyze the requirements and file tasks for ALL work:"
1360
- parts << ""
1361
- parts << "```text"
1362
- parts << "File task: \"Implement [feature/fix description]\" priority: high tags: implementation"
1363
- parts << "File task: \"Add unit tests for [feature]\" priority: high tags: testing"
1364
- parts << "File task: \"Add integration tests if needed\" priority: medium tags: testing"
1365
- parts << "```"
1366
- parts << ""
1367
- parts << "### Step 2: Work Through Tasks"
1368
- parts << "- Pick the highest priority pending task"
1369
- parts << "- Implement it completely"
1370
- parts << "- Mark it done: `Update task: task_id status: done`"
1371
- parts << "- Repeat until all tasks are complete"
1372
- parts << ""
1373
- parts << "### Step 3: Complete the Work Loop"
1374
- parts << "Only after ALL tasks are done:"
1375
- parts << "- Verify tests pass"
1376
- parts << "- Add STATUS: COMPLETE to `.aidp/PROMPT.md`"
1377
- parts << ""
1378
- parts << "### Task Rules"
1379
- parts << "- **At least ONE task must be filed** - completion blocked without tasks"
1380
- parts << "- **At least ONE task must be DONE** - completion blocked if all abandoned"
1381
- parts << "- **Substantive work required** - doc-only changes rejected"
1382
- parts << ""
1383
- parts << "**Important**: Tasks exist due to careful planning. Do NOT abandon tasks due to"
1384
- parts << "perceived complexity - these factors were considered during planning. Only abandon"
1385
- parts << "when truly obsolete (requirements changed, duplicate, external blockers)."
1386
- parts << ""
1387
- parts << "### Task Filing Examples"
1388
- parts << "- `File task: \"Implement user authentication\" priority: high tags: security,auth`"
1389
- parts << "- `File task: \"Add tests for login flow\" priority: medium tags: testing`"
1390
- parts << "- `File task: \"Update documentation\" priority: low tags: docs`"
1391
- parts << ""
1392
- parts << "### Task Status Update Examples"
1393
- parts << "- `Update task: task_123_abc status: in_progress`"
1394
- parts << "- `Update task: task_456_def status: done`"
1395
- parts << "- `Update task: task_789_ghi status: abandoned reason: \"Requirements changed\"`"
1396
- parts << ""
1397
- end
1398
-
1399
- parts << "## Completion Criteria"
1400
- parts << "Mark this step COMPLETE by adding these lines to `.aidp/PROMPT.md`:"
1401
- parts << "```"
1402
- parts << "STATUS: COMPLETE"
1403
- if @config.task_completion_required?
1404
- parts << ""
1405
- parts << "Update task: task_xxx_yyy status: done # Mark ALL your tasks as done"
1406
- end
1407
- parts << "```"
1408
- parts << ""
1409
- parts.join("\n")
1283
+ @prompt_template_manager.render(
1284
+ "work_loop/header",
1285
+ STEP_NAME: step_name,
1286
+ ITERATION: iteration,
1287
+ PROJECT_DIR: @project_dir,
1288
+ TASK_FILING_SECTION: task_filing_section,
1289
+ TASK_COMPLETION_LINE: task_completion_line
1290
+ )
1410
1291
  end
1411
1292
 
1412
1293
  def iteration_context_metadata
@@ -1638,10 +1519,283 @@ module Aidp
1638
1519
  end
1639
1520
 
1640
1521
  def archive_and_cleanup
1522
+ sync_project_knowledge
1641
1523
  @prompt_manager.archive(@step_name)
1642
1524
  @prompt_manager.delete
1643
1525
  end
1644
1526
 
1527
+ def sync_project_knowledge
1528
+ affected_files = knowledge_affected_files
1529
+ return if affected_files.empty?
1530
+
1531
+ @project_knowledge_manager.sync!(
1532
+ step_name: @step_name,
1533
+ feature_identifier: knowledge_feature_identifier,
1534
+ task_description: build_task_description(format_user_input(@work_context[:user_input]), @work_context),
1535
+ affected_files: affected_files,
1536
+ tool_commands: knowledge_tool_commands
1537
+ )
1538
+ rescue => e
1539
+ Aidp.logger.warn("work_loop", "project_knowledge_sync_failed",
1540
+ step: @step_name,
1541
+ error: e.message)
1542
+ end
1543
+
1544
+ def knowledge_affected_files
1545
+ files = Array(@work_context[:affected_files])
1546
+ files.concat(extract_affected_files(@work_context, format_user_input(@work_context[:user_input])))
1547
+ files.concat(metadata_affected_files)
1548
+ files.concat(get_changed_files) if files.empty?
1549
+ files.map(&:to_s).select { |path| knowledge_path_candidate?(path) }.uniq
1550
+ end
1551
+
1552
+ def extract_paths_from_text(text)
1553
+ return [] if text.nil?
1554
+
1555
+ text.to_s.split.filter_map do |token|
1556
+ path = trim_path_token(token)
1557
+ path if valid_path_candidate?(path)
1558
+ end
1559
+ end
1560
+
1561
+ def extract_output_paths(outputs)
1562
+ Array(outputs).filter_map do |output|
1563
+ path = output[:output_path].to_s.strip
1564
+ path unless path.empty?
1565
+ end
1566
+ end
1567
+
1568
+ def knowledge_tool_commands
1569
+ metadata_tool_commands.concat(@executed_tool_commands).uniq
1570
+ end
1571
+
1572
+ def record_agentic_execution_result(result)
1573
+ return unless result.is_a?(Hash)
1574
+
1575
+ @agentic_execution_results << result
1576
+ end
1577
+
1578
+ def knowledge_feature_identifier
1579
+ user_input = @work_context[:user_input]
1580
+
1581
+ @work_context[:feature_identifier] ||
1582
+ @work_context[:feature_id] ||
1583
+ extract_feature_identifier(user_input)
1584
+ end
1585
+
1586
+ def record_phase_commands(phase, results)
1587
+ commands_by_name = Array(@config.commands_for_phase(phase)).each_with_object({}) do |entry, indexed|
1588
+ indexed[entry[:name]] = entry
1589
+ end
1590
+ executed = results.fetch(:results_by_command, {}).keys.filter_map do |name|
1591
+ commands_by_name[name]&.fetch(:command, nil)
1592
+ end
1593
+ executed = commands_by_name.values.map { |entry| entry[:command] } if executed.empty? && commands_by_name.any?
1594
+ @executed_tool_commands.concat(executed.compact)
1595
+ end
1596
+
1597
+ def record_legacy_category_commands(category)
1598
+ return unless @test_runner.respond_to?(:planned_commands)
1599
+
1600
+ commands = Array(@test_runner.planned_commands[category]).filter_map do |entry|
1601
+ entry.is_a?(String) ? entry : entry[:command]
1602
+ end
1603
+ @executed_tool_commands.concat(commands)
1604
+ end
1605
+
1606
+ def extract_feature_identifier(user_input)
1607
+ return unless user_input.is_a?(Hash)
1608
+
1609
+ user_input["feature_identifier"] ||
1610
+ user_input[:feature_identifier] ||
1611
+ user_input["feature_id"] ||
1612
+ user_input[:feature_id]
1613
+ end
1614
+
1615
+ def metadata_affected_files
1616
+ metadata_entries = metadata_value_objects
1617
+ files = extract_file_entries(metadata_entries, :affected_files)
1618
+ files.concat(extract_file_entries(metadata_entries, :changed_files))
1619
+ files.concat(extract_file_entries(metadata_entries, :edited_files))
1620
+ files.concat(extract_file_entries(metadata_entries, :files_changed))
1621
+ files.concat(extract_file_entries(metadata_entries, :modified_files))
1622
+ files.concat(extract_file_entries(metadata_entries, :touched_files))
1623
+ files.uniq
1624
+ end
1625
+
1626
+ def metadata_tool_commands
1627
+ metadata_entries = metadata_value_objects
1628
+ commands = extract_command_entries(metadata_entries, :tool_commands)
1629
+ commands.concat(extract_command_entries(metadata_entries, :tool_usage))
1630
+ commands.concat(extract_command_entries(metadata_entries, :tool_calls))
1631
+ commands.concat(extract_command_entries(metadata_entries, :tools_used))
1632
+ commands.concat(extract_command_entries(metadata_entries, :commands))
1633
+ commands.uniq
1634
+ end
1635
+
1636
+ def metadata_value_objects
1637
+ Array(@agentic_execution_results).flat_map do |result|
1638
+ [
1639
+ result,
1640
+ indifferent_hash_value(result, :metadata),
1641
+ indifferent_hash_value(result, :output)
1642
+ ]
1643
+ end.compact.select { |entry| entry.is_a?(Hash) }
1644
+ end
1645
+
1646
+ def extract_file_entries(entries, key)
1647
+ entries.flat_map do |entry|
1648
+ Array(indifferent_hash_value(entry, key)).flat_map do |value|
1649
+ case value
1650
+ when String
1651
+ [value]
1652
+ when Hash
1653
+ candidate = file_candidate_from_hash(value)
1654
+ candidate ? [candidate] : []
1655
+ else
1656
+ []
1657
+ end
1658
+ end
1659
+ end
1660
+ end
1661
+
1662
+ def extract_command_entries(entries, key)
1663
+ entries.flat_map do |entry|
1664
+ Array(indifferent_hash_value(entry, key)).flat_map do |value|
1665
+ case value
1666
+ when String
1667
+ [value]
1668
+ when Hash
1669
+ candidate = command_candidate_from_hash(value)
1670
+ candidate ? [candidate] : []
1671
+ else
1672
+ []
1673
+ end
1674
+ end
1675
+ end.reject(&:empty?)
1676
+ end
1677
+
1678
+ def indifferent_hash_value(hash, key)
1679
+ hash[key] || hash[key.to_s]
1680
+ end
1681
+
1682
+ def file_candidate_from_hash(hash)
1683
+ %i[path file file_path output_path relative_path].each do |key|
1684
+ candidate = indifferent_hash_value(hash, key).to_s.strip
1685
+ return candidate unless candidate.empty?
1686
+ end
1687
+
1688
+ nil
1689
+ end
1690
+
1691
+ def command_candidate_from_hash(hash)
1692
+ %i[command tool name identifier].each do |key|
1693
+ candidate = indifferent_hash_value(hash, key).to_s.strip
1694
+ return candidate unless candidate.empty?
1695
+ end
1696
+
1697
+ nil
1698
+ end
1699
+
1700
+ def trim_path_token(token)
1701
+ text = token.to_s
1702
+ start_index = 0
1703
+ end_index = text.length - 1
1704
+
1705
+ start_index += 1 while start_index <= end_index && !path_character?(text[start_index])
1706
+ end_index -= 1 while end_index >= start_index && !path_character?(text[end_index])
1707
+
1708
+ return "" if start_index > end_index
1709
+
1710
+ text[start_index..end_index]
1711
+ end
1712
+
1713
+ def knowledge_path_candidate?(path)
1714
+ return false if path.empty?
1715
+ return false if internal_generated_path?(path)
1716
+
1717
+ true
1718
+ end
1719
+
1720
+ def internal_generated_path?(path)
1721
+ path == ".aidp" || path.start_with?(".aidp/")
1722
+ end
1723
+
1724
+ def path_character?(char)
1725
+ alphanumeric_character?(char) || ["_", ".", "/", "-"].include?(char)
1726
+ end
1727
+
1728
+ def valid_path_candidate?(path)
1729
+ return false if path.empty?
1730
+ return false if standalone_version_token?(path)
1731
+
1732
+ segments = path.split("/")
1733
+ return false if segments.empty? || segments.any?(&:empty?)
1734
+
1735
+ filename = segments.last
1736
+ return false unless valid_filename_candidate?(filename, path.include?("/"))
1737
+
1738
+ segments.all? { |segment| valid_path_segment?(segment) }
1739
+ end
1740
+
1741
+ def standalone_version_token?(path)
1742
+ return false if path.include?("/")
1743
+
1744
+ components = path.split(".")
1745
+ return false if components.length < 2
1746
+
1747
+ components.all? { |component| version_component?(component) }
1748
+ end
1749
+
1750
+ def version_component?(component)
1751
+ component.match?(/\Av?\d+\z/i)
1752
+ end
1753
+
1754
+ def valid_path_segment?(segment)
1755
+ return false if segment == "." || segment == ".."
1756
+
1757
+ segment.each_char.all? do |char|
1758
+ alphanumeric_character?(char) || ["_", ".", "-"].include?(char)
1759
+ end
1760
+ end
1761
+
1762
+ def valid_filename_candidate?(filename, nested_path)
1763
+ return valid_extension_filename?(filename) if filename.include?(".")
1764
+
1765
+ return true if nested_path
1766
+
1767
+ known_extensionless_filename?(filename)
1768
+ end
1769
+
1770
+ def valid_extension_filename?(filename)
1771
+ name, _separator, extension = filename.rpartition(".")
1772
+ return false if name.to_s.empty? || extension.to_s.empty?
1773
+
1774
+ extension.each_char.all? { |char| alphanumeric_character?(char) }
1775
+ end
1776
+
1777
+ def known_extensionless_filename?(filename)
1778
+ %w[
1779
+ Dockerfile
1780
+ Gemfile
1781
+ Procfile
1782
+ Rakefile
1783
+ Makefile
1784
+ Brewfile
1785
+ Podfile
1786
+ Fastfile
1787
+ Appfile
1788
+ Guardfile
1789
+ Vagrantfile
1790
+ ].include?(filename)
1791
+ end
1792
+
1793
+ def alphanumeric_character?(char)
1794
+ char.between?("a", "z") ||
1795
+ char.between?("A", "Z") ||
1796
+ char.between?("0", "9")
1797
+ end
1798
+
1645
1799
  def load_template(template_name)
1646
1800
  return "" unless template_name
1647
1801
 
@@ -2131,51 +2285,48 @@ module Aidp
2131
2285
 
2132
2286
  # Append task completion requirement to PROMPT.md
2133
2287
  def append_task_requirement_to_prompt(message)
2134
- task_requirement = []
2135
-
2136
- task_requirement << "## Task Completion Requirement"
2137
- task_requirement << ""
2138
- task_requirement << "**CRITICAL**: #{message}"
2139
- task_requirement << ""
2140
- task_requirement << "### How to Complete Tasks"
2141
- task_requirement << ""
2142
- task_requirement << "Update task status using these signals in your output:"
2143
- task_requirement << ""
2144
- task_requirement << "**Creating tasks:**"
2145
- task_requirement << "```"
2146
- task_requirement << 'File task: "Implement feature X" priority: high tags: feature,backend'
2147
- task_requirement << 'File task: "Add tests for feature X" priority: medium tags: testing'
2148
- task_requirement << "```"
2149
- task_requirement << ""
2150
- task_requirement << "**Updating task status:**"
2151
- task_requirement << "```"
2152
- task_requirement << "Update task: task_123_abc status: in_progress"
2153
- task_requirement << "Update task: task_123_abc status: done"
2154
- task_requirement << 'Update task: task_456_def status: abandoned reason: "Requirements changed"'
2155
- task_requirement << "```"
2156
- task_requirement << ""
2157
- task_requirement << "**Task states:**"
2158
- task_requirement << "- ⏳ **pending** - Not started yet"
2159
- task_requirement << "- 🚧 **in_progress** - Currently working on it"
2160
- task_requirement << "- ✅ **done** - Completed successfully"
2161
- task_requirement << "- ❌ **abandoned** - Not doing this (requires reason)"
2162
- task_requirement << ""
2163
- task_requirement << "**Completion requirement:**"
2164
- task_requirement << "All tasks for this session must be marked as DONE or ABANDONED (with reason) before the work loop can complete."
2165
- task_requirement << ""
2166
- task_requirement << "**Action Required**: Review the current task list and update status for all tasks."
2167
- task_requirement << ""
2168
-
2169
2288
  # Append to PROMPT.md - ensure directory exists
2170
- begin
2171
- current_prompt = @prompt_manager.read || ""
2172
- updated_prompt = current_prompt + "\n\n---\n\n" + task_requirement.join("\n")
2173
- @prompt_manager.write(updated_prompt, step_name: @step_name)
2174
- display_message(" [TASK_REQ] Added task completion requirement to PROMPT.md", type: :warning)
2175
- rescue => e
2176
- Aidp.log_warn("work_loop", "Failed to append task requirement to PROMPT.md", error: e.message)
2177
- display_message(" [TASK_REQ] Warning: Could not update PROMPT.md: #{e.message}", type: :warning)
2289
+ current_prompt = @prompt_manager.read || ""
2290
+ task_requirement = @prompt_template_manager.render(
2291
+ "work_loop/task_completion_requirement",
2292
+ MESSAGE: message
2293
+ )
2294
+ updated_prompt = current_prompt + "\n\n---\n\n" + task_requirement
2295
+ @prompt_manager.write(updated_prompt, step_name: @step_name)
2296
+ display_message(" [TASK_REQ] Added task completion requirement to PROMPT.md", type: :warning)
2297
+ rescue => e
2298
+ Aidp.log_warn("work_loop", "Failed to append task requirement to PROMPT.md", error: e.message)
2299
+ display_message(" [TASK_REQ] Warning: Could not update PROMPT.md: #{e.message}", type: :warning)
2300
+ end
2301
+
2302
+ def optional_section(title, body)
2303
+ return "" if body.nil? || body.empty?
2304
+
2305
+ [title, body, ""].join("\n")
2306
+ end
2307
+
2308
+ def deterministic_outputs_section(deterministic_outputs)
2309
+ return "" if deterministic_outputs.empty?
2310
+
2311
+ lines = ["## Recent Deterministic Outputs"]
2312
+ deterministic_outputs.each do |entry|
2313
+ lines << "- #{entry[:name]} (status: #{entry[:status]})"
2314
+ lines << " Output: #{entry[:output_path] || "n/a"}"
2178
2315
  end
2316
+ lines << ""
2317
+ lines.join("\n")
2318
+ end
2319
+
2320
+ def task_filing_section
2321
+ return "" unless @config.task_completion_required?
2322
+
2323
+ @prompt_template_manager.render("work_loop/task_filing")
2324
+ end
2325
+
2326
+ def task_completion_line
2327
+ return "" unless @config.task_completion_required?
2328
+
2329
+ "Update task: task_xxx_yyy status: done # Mark ALL your tasks as done\n"
2179
2330
  end
2180
2331
 
2181
2332
  # Validate changes against guard policy
@@ -115,6 +115,8 @@ module Aidp
115
115
  def meets_threshold?(fragment, score, thresholds)
116
116
  threshold = if fragment.class.name.include?("Fragment") && fragment.respond_to?(:heading)
117
117
  thresholds[:style_guide] || 0.75
118
+ elsif fragment.respond_to?(:linked_paths)
119
+ thresholds[:knowledge] || 0.72
118
120
  elsif fragment.respond_to?(:category)
119
121
  thresholds[:templates] || 0.8
120
122
  elsif fragment.respond_to?(:file_path) && fragment.respond_to?(:type)
@@ -248,6 +250,8 @@ module Aidp
248
250
  case type
249
251
  when :style_guide
250
252
  item[:fragment].class.name.include?("Fragment") && item[:fragment].respond_to?(:heading)
253
+ when :project_knowledge
254
+ item[:fragment].respond_to?(:linked_paths)
251
255
  when :template
252
256
  item[:fragment].respond_to?(:category) && !item[:fragment].respond_to?(:type)
253
257
  when :code
@@ -272,6 +276,7 @@ module Aidp
272
276
  over_budget: over_budget?,
273
277
  by_type: {
274
278
  style_guide: fragments_by_type(:style_guide).count,
279
+ project_knowledge: fragments_by_type(:project_knowledge).count,
275
280
  templates: fragments_by_type(:template).count,
276
281
  code: fragments_by_type(:code).count
277
282
  }
@@ -6,6 +6,7 @@ require_relative "source_code_fragmenter"
6
6
  require_relative "relevance_scorer"
7
7
  require_relative "context_composer"
8
8
  require_relative "prompt_builder"
9
+ require_relative "project_knowledge_indexer"
9
10
 
10
11
  module Aidp
11
12
  module PromptOptimization
@@ -38,6 +39,7 @@ module Aidp
38
39
  @style_guide_indexer = nil
39
40
  @template_indexer = nil
40
41
  @fragmenter = nil
42
+ @project_knowledge_indexer = nil
41
43
  @scorer = nil
42
44
  @composer = nil
43
45
  @builder = nil
@@ -100,6 +102,7 @@ module Aidp
100
102
  @style_guide_indexer = nil
101
103
  @template_indexer = nil
102
104
  @fragmenter = nil
105
+ @project_knowledge_indexer = nil
103
106
  @stats.reset!
104
107
  end
105
108
 
@@ -127,6 +130,10 @@ module Aidp
127
130
  template_indexer.index!
128
131
  fragments.concat(template_indexer.templates)
129
132
 
133
+ # Project knowledge fragments
134
+ project_knowledge_indexer.index!
135
+ fragments.concat(project_knowledge_indexer.fragments)
136
+
130
137
  # Source code fragments (only for affected files)
131
138
  if affected_files && !affected_files.empty?
132
139
  code_fragments = fragmenter.fragment_files(affected_files)
@@ -180,6 +187,10 @@ module Aidp
180
187
  @fragmenter ||= SourceCodeFragmenter.new(project_dir: @project_dir)
181
188
  end
182
189
 
190
+ def project_knowledge_indexer
191
+ @project_knowledge_indexer ||= ProjectKnowledgeIndexer.new(project_dir: @project_dir)
192
+ end
193
+
183
194
  # Get or create relevance scorer (cached)
184
195
  def scorer
185
196
  @scorer ||= RelevanceScorer.new
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module PromptOptimization
5
+ # Indexes concise feature and tool notes stored under docs/
6
+ #
7
+ # These notes use inline path markers like <feature_id:lib/file.rb> so
8
+ # they can be found quickly by ripgrep and loaded for matching files.
9
+ class ProjectKnowledgeIndexer
10
+ KNOWLEDGE_DIRS = {
11
+ feature: File.join("docs", "features"),
12
+ tool: File.join("docs", "tools")
13
+ }.freeze
14
+
15
+ MARKER_PATTERN = /<([a-z0-9_]+):([^>\n]+)>/
16
+
17
+ attr_reader :fragments, :project_dir
18
+
19
+ def initialize(project_dir:)
20
+ @project_dir = project_dir
21
+ @fragments = []
22
+ end
23
+
24
+ def index!
25
+ @fragments = []
26
+
27
+ KNOWLEDGE_DIRS.each do |note_type, relative_dir|
28
+ index_directory(note_type, File.join(project_dir, relative_dir))
29
+ end
30
+
31
+ @fragments
32
+ end
33
+
34
+ private
35
+
36
+ def index_directory(note_type, directory)
37
+ return unless Dir.exist?(directory)
38
+
39
+ Dir.glob(File.join(directory, "**", "*.md")).sort.each do |file_path|
40
+ @fragments << parse_file(file_path, note_type)
41
+ end
42
+ end
43
+
44
+ def parse_file(file_path, note_type)
45
+ content = File.read(file_path, encoding: "UTF-8")
46
+ title = extract_title(content, file_path)
47
+ markers = content.scan(MARKER_PATTERN)
48
+ note_id = markers.first&.first || File.basename(file_path, ".md")
49
+
50
+ ProjectKnowledgeFragment.new(
51
+ id: note_id,
52
+ title: title,
53
+ note_type: note_type,
54
+ file_path: file_path,
55
+ content: content,
56
+ linked_paths: markers.map { |(_, path)| path.strip }.uniq,
57
+ tags: extract_tags(note_type, content)
58
+ )
59
+ end
60
+
61
+ def extract_title(content, file_path)
62
+ heading = content.lines.find { |line| line.start_with?("# ") }
63
+ return heading.sub(/^#\s+/, "").strip if heading
64
+
65
+ File.basename(file_path, ".md").split("_").map(&:capitalize).join(" ")
66
+ end
67
+
68
+ def extract_tags(note_type, content)
69
+ text = content.downcase
70
+ tags = [note_type.to_s, "project_knowledge"]
71
+ tags << "testing" if text.match?(/rspec|rubocop|test|lint|spec/)
72
+ tags << "documentation" if text.match?(/doc|guide|markdown/)
73
+ tags << "implementation" if text.match?(/implement|feature|behavior/)
74
+ tags << "filesystem" if text.match?(/file|path|directory|workspace/)
75
+ tags << "external_service" if text.match?(/api|provider|service|github|slack/)
76
+ tags.uniq
77
+ end
78
+ end
79
+
80
+ class ProjectKnowledgeFragment
81
+ attr_reader :id, :title, :note_type, :file_path, :content, :linked_paths, :tags
82
+
83
+ def initialize(id:, title:, note_type:, file_path:, content:, linked_paths:, tags:)
84
+ @id = id
85
+ @title = title
86
+ @note_type = note_type
87
+ @file_path = file_path
88
+ @content = content
89
+ @linked_paths = linked_paths
90
+ @tags = tags
91
+ end
92
+
93
+ def estimated_tokens
94
+ (content.length / 4.0).ceil
95
+ end
96
+ end
97
+ end
98
+ end
@@ -31,11 +31,13 @@ module Aidp
31
31
 
32
32
  # Group fragments by type
33
33
  style_guide_fragments = composition_result.fragments_by_type(:style_guide)
34
+ project_knowledge_fragments = composition_result.fragments_by_type(:project_knowledge)
34
35
  template_fragments = composition_result.fragments_by_type(:template)
35
36
  code_fragments = composition_result.fragments_by_type(:code)
36
37
 
37
38
  # Add relevant sections if they have content
38
39
  sections << build_style_guide_section(style_guide_fragments) unless style_guide_fragments.empty?
40
+ sections << build_project_knowledge_section(project_knowledge_fragments) unless project_knowledge_fragments.empty?
39
41
  sections << build_template_section(template_fragments) unless template_fragments.empty?
40
42
  sections << build_code_section(code_fragments) unless code_fragments.empty?
41
43
 
@@ -139,6 +141,25 @@ module Aidp
139
141
  lines.join("\n")
140
142
  end
141
143
 
144
+ def build_project_knowledge_section(fragments)
145
+ lines = ["# Project Knowledge"]
146
+ lines << ""
147
+ lines << "Concise notes associated with the affected files:"
148
+ lines << ""
149
+
150
+ fragments.each do |item|
151
+ fragment = item[:fragment]
152
+ lines << "## #{fragment.title}"
153
+ lines << ""
154
+ lines << "**Type**: #{fragment.note_type}"
155
+ lines << ""
156
+ lines << fragment.content
157
+ lines << ""
158
+ end
159
+
160
+ lines.join("\n")
161
+ end
162
+
142
163
  # Build code context section
143
164
  #
144
165
  # @param fragments [Array<Hash>] Code fragments
@@ -198,6 +219,7 @@ module Aidp
198
219
  lines << "## Fragments by Type"
199
220
  lines << ""
200
221
  lines << "- **Style Guide Sections**: #{summary[:by_type][:style_guide]}"
222
+ lines << "- **Project Knowledge Notes**: #{summary[:by_type][:project_knowledge]}"
201
223
  lines << "- **Templates**: #{summary[:by_type][:templates]}"
202
224
  lines << "- **Code Fragments**: #{summary[:by_type][:code]}"
203
225
  lines << ""
@@ -105,6 +105,13 @@ module Aidp
105
105
  def score_file_location_match(fragment, context)
106
106
  return 0.5 unless context.affected_files && !context.affected_files.empty?
107
107
 
108
+ if fragment.respond_to?(:linked_paths) && fragment.linked_paths.any?
109
+ matches_affected_file = context.affected_files.any? do |affected_file|
110
+ knowledge_path_match?(fragment.linked_paths, affected_file)
111
+ end
112
+ return matches_affected_file ? 1.0 : 0.1
113
+ end
114
+
108
115
  # Only code fragments have file_path
109
116
  return 0.5 unless fragment.respond_to?(:file_path)
110
117
 
@@ -114,6 +121,16 @@ module Aidp
114
121
  end) ? 1.0 : 0.1
115
122
  end
116
123
 
124
+ def knowledge_path_match?(linked_paths, affected_file)
125
+ linked_paths.any? do |linked_path|
126
+ normalized_link = linked_path.sub(%r{\A\./}, "")
127
+ normalized_file = affected_file.to_s.sub(%r{\A\./}, "")
128
+ normalized_file == normalized_link ||
129
+ normalized_file.start_with?("#{normalized_link}/") ||
130
+ normalized_link.start_with?("#{normalized_file}/")
131
+ end
132
+ end
133
+
117
134
  # Score based on work loop step
118
135
  #
119
136
  # @param fragment [Fragment] Fragment to score
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.40.0"
4
+ VERSION = "0.42.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -407,6 +407,7 @@ files:
407
407
  - lib/aidp/execute/interactive_repl.rb
408
408
  - lib/aidp/execute/persistent_tasklist.rb
409
409
  - lib/aidp/execute/progress.rb
410
+ - lib/aidp/execute/project_knowledge_manager.rb
410
411
  - lib/aidp/execute/prompt_evaluator.rb
411
412
  - lib/aidp/execute/prompt_manager.rb
412
413
  - lib/aidp/execute/repl_macros.rb
@@ -526,6 +527,7 @@ files:
526
527
  - lib/aidp/pr_worktree_manager.rb
527
528
  - lib/aidp/prompt_optimization/context_composer.rb
528
529
  - lib/aidp/prompt_optimization/optimizer.rb
530
+ - lib/aidp/prompt_optimization/project_knowledge_indexer.rb
529
531
  - lib/aidp/prompt_optimization/prompt_builder.rb
530
532
  - lib/aidp/prompt_optimization/relevance_scorer.rb
531
533
  - lib/aidp/prompt_optimization/source_code_fragmenter.rb