ara-cli 0.1.10.0__py3-none-any.whl → 0.1.13.3__py3-none-any.whl

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 (140) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +270 -103
  3. ara_cli/ara_command_action.py +106 -63
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/config.py +221 -0
  11. ara_cli/ara_subcommands/convert.py +43 -0
  12. ara_cli/ara_subcommands/create.py +75 -0
  13. ara_cli/ara_subcommands/delete.py +22 -0
  14. ara_cli/ara_subcommands/extract.py +22 -0
  15. ara_cli/ara_subcommands/fetch.py +41 -0
  16. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  17. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  18. ara_cli/ara_subcommands/fetch_templates.py +19 -0
  19. ara_cli/ara_subcommands/list.py +139 -0
  20. ara_cli/ara_subcommands/list_tags.py +25 -0
  21. ara_cli/ara_subcommands/load.py +48 -0
  22. ara_cli/ara_subcommands/prompt.py +136 -0
  23. ara_cli/ara_subcommands/read.py +47 -0
  24. ara_cli/ara_subcommands/read_status.py +20 -0
  25. ara_cli/ara_subcommands/read_user.py +20 -0
  26. ara_cli/ara_subcommands/reconnect.py +27 -0
  27. ara_cli/ara_subcommands/rename.py +22 -0
  28. ara_cli/ara_subcommands/scan.py +14 -0
  29. ara_cli/ara_subcommands/set_status.py +22 -0
  30. ara_cli/ara_subcommands/set_user.py +22 -0
  31. ara_cli/ara_subcommands/template.py +16 -0
  32. ara_cli/artefact_autofix.py +154 -63
  33. ara_cli/artefact_converter.py +256 -0
  34. ara_cli/artefact_models/artefact_model.py +106 -25
  35. ara_cli/artefact_models/artefact_templates.py +20 -10
  36. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  37. ara_cli/artefact_models/feature_artefact_model.py +31 -1
  38. ara_cli/artefact_models/userstory_artefact_model.py +15 -3
  39. ara_cli/artefact_scan.py +2 -2
  40. ara_cli/chat.py +283 -80
  41. ara_cli/chat_agent/__init__.py +0 -0
  42. ara_cli/chat_agent/agent_process_manager.py +155 -0
  43. ara_cli/chat_script_runner/__init__.py +0 -0
  44. ara_cli/chat_script_runner/script_completer.py +23 -0
  45. ara_cli/chat_script_runner/script_finder.py +41 -0
  46. ara_cli/chat_script_runner/script_lister.py +36 -0
  47. ara_cli/chat_script_runner/script_runner.py +36 -0
  48. ara_cli/chat_web_search/__init__.py +0 -0
  49. ara_cli/chat_web_search/web_search.py +263 -0
  50. ara_cli/commands/agent_run_command.py +98 -0
  51. ara_cli/commands/fetch_agents_command.py +106 -0
  52. ara_cli/commands/fetch_scripts_command.py +43 -0
  53. ara_cli/commands/fetch_templates_command.py +39 -0
  54. ara_cli/commands/fetch_templates_commands.py +39 -0
  55. ara_cli/commands/list_agents_command.py +39 -0
  56. ara_cli/commands/read_command.py +17 -4
  57. ara_cli/completers.py +180 -0
  58. ara_cli/constants.py +2 -0
  59. ara_cli/directory_navigator.py +37 -4
  60. ara_cli/file_loaders/text_file_loader.py +2 -2
  61. ara_cli/global_file_lister.py +5 -15
  62. ara_cli/llm_utils.py +58 -0
  63. ara_cli/prompt_chat.py +20 -4
  64. ara_cli/prompt_extractor.py +199 -76
  65. ara_cli/prompt_handler.py +160 -59
  66. ara_cli/tag_extractor.py +38 -18
  67. ara_cli/template_loader.py +3 -2
  68. ara_cli/template_manager.py +52 -21
  69. ara_cli/templates/global-scripts/hello_global.py +1 -0
  70. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  71. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  72. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  73. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  74. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  75. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  76. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  77. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  78. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  79. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  80. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  81. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  82. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  83. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  84. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  85. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  86. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  87. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  88. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  89. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  90. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  91. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  92. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  93. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  94. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  95. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  96. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  97. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  98. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  99. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  100. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  101. ara_cli/version.py +1 -1
  102. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/METADATA +34 -1
  103. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/RECORD +123 -54
  104. tests/test_ara_command_action.py +31 -19
  105. tests/test_ara_config.py +177 -90
  106. tests/test_artefact_autofix.py +170 -97
  107. tests/test_artefact_autofix_integration.py +495 -0
  108. tests/test_artefact_converter.py +357 -0
  109. tests/test_artefact_extraction.py +564 -0
  110. tests/test_artefact_scan.py +1 -1
  111. tests/test_chat.py +162 -126
  112. tests/test_chat_givens_images.py +603 -0
  113. tests/test_chat_script_runner.py +454 -0
  114. tests/test_global_file_lister.py +1 -1
  115. tests/test_llm_utils.py +164 -0
  116. tests/test_prompt_chat.py +343 -0
  117. tests/test_prompt_extractor.py +683 -0
  118. tests/test_prompt_handler.py +12 -4
  119. tests/test_tag_extractor.py +19 -13
  120. tests/test_web_search.py +467 -0
  121. ara_cli/ara_command_parser.py +0 -605
  122. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  123. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  124. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  125. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  126. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  127. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  128. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  129. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  130. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  131. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  132. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  133. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  134. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  135. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  136. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  137. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  138. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/WHEEL +0 -0
  139. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/entry_points.txt +0 -0
  140. {ara_cli-0.1.10.0.dist-info → ara_cli-0.1.13.3.dist-info}/top_level.txt +0 -0
@@ -140,16 +140,11 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
140
140
 
141
141
 
142
142
  def run_agent(prompt, artefact_class):
143
- from pydantic_ai import Agent
144
-
145
- # gpt-4o
146
- # anthropic:claude-3-7-sonnet-20250219
147
- # anthropic:claude-4-sonnet-20250514
148
- agent = Agent(
149
- model="anthropic:claude-4-sonnet-20250514",
150
- output_type=artefact_class,
151
- instrument=True,
152
- )
143
+ from ara_cli.llm_utils import create_pydantic_ai_agent
144
+
145
+ # Use the shared agent creation logic which respects configuration and validation
146
+ agent = create_pydantic_ai_agent(output_type=artefact_class, instrument=True)
147
+
153
148
  result = agent.run_sync(prompt)
154
149
  return result.output
155
150
 
@@ -161,7 +156,7 @@ def write_corrected_artefact(file_path, corrected_text):
161
156
 
162
157
 
163
158
  def ask_for_correct_contribution(
164
- artefact_info: Optional[tuple[str, str]] = None
159
+ artefact_info: Optional[tuple[str, str]] = None,
165
160
  ) -> tuple[str, str]:
166
161
  """
167
162
  Ask the user to provide a valid contribution when no match can be found.
@@ -199,8 +194,12 @@ def ask_for_correct_contribution(
199
194
  return name, classifier
200
195
 
201
196
 
202
- def ask_for_contribution_choice(choices: List[str], artefact_info: Optional[tuple[str, str]] = None) -> Optional[str]:
203
- artefact_name, artefact_classifier = artefact_info if artefact_info else (None, None)
197
+ def ask_for_contribution_choice(
198
+ choices: List[str], artefact_info: Optional[tuple[str, str]] = None
199
+ ) -> Optional[str]:
200
+ artefact_name, artefact_classifier = (
201
+ artefact_info if artefact_info else (None, None)
202
+ )
204
203
  message = "Found multiple close matches for the contribution"
205
204
  if artefact_name and artefact_classifier:
206
205
  message += f" of the {artefact_classifier} '{artefact_name}'"
@@ -248,7 +247,11 @@ def ask_for_rule_choice(matches: List[str]) -> Optional[str]:
248
247
 
249
248
 
250
249
  def _update_rule(
251
- artefact: Artefact, name: str, classifier: str, classified_file_info: dict, delete_if_not_found: bool = False
250
+ artefact: Artefact,
251
+ name: str,
252
+ classifier: str,
253
+ classified_file_info: dict,
254
+ delete_if_not_found: bool = False,
252
255
  ) -> None:
253
256
  """Updates the rule in the contribution if a close match is found."""
254
257
  rule = artefact.contribution.rule
@@ -317,7 +320,9 @@ def set_closest_contribution(
317
320
  classifier = contribution.classifier
318
321
  rule = contribution.rule
319
322
 
320
- classified_file_info = populate_classified_artefact_info(classified_artefact_info=classified_file_info)
323
+ classified_file_info = populate_classified_artefact_info(
324
+ classified_artefact_info=classified_file_info
325
+ )
321
326
 
322
327
  all_artefact_names = extract_artefact_names_of_classifier(
323
328
  classified_files=classified_file_info, classifier=classifier
@@ -385,12 +390,12 @@ def fix_scenario_placeholder_mismatch(
385
390
  lines = artefact_text.splitlines()
386
391
  new_lines = []
387
392
  i = 0
388
-
393
+
389
394
  while i < len(lines):
390
395
  line = lines[i]
391
396
  stripped_line = line.strip()
392
-
393
- if stripped_line.startswith('Scenario:'):
397
+
398
+ if stripped_line.startswith("Scenario:"):
394
399
  scenario_lines, next_index = _extract_scenario_block(lines, i)
395
400
  processed_lines = _process_scenario_block(scenario_lines)
396
401
  new_lines.extend(processed_lines)
@@ -398,7 +403,7 @@ def fix_scenario_placeholder_mismatch(
398
403
  else:
399
404
  new_lines.append(line)
400
405
  i += 1
401
-
406
+
402
407
  return "\n".join(new_lines)
403
408
 
404
409
 
@@ -406,20 +411,20 @@ def _extract_scenario_block(lines: list, start_index: int) -> tuple[list, int]:
406
411
  """Extract all lines belonging to a scenario block."""
407
412
  scenario_lines = [lines[start_index]]
408
413
  j = start_index + 1
409
-
414
+
410
415
  while j < len(lines):
411
416
  next_line = lines[j].strip()
412
417
  if _is_scenario_boundary(next_line):
413
418
  break
414
419
  scenario_lines.append(lines[j])
415
420
  j += 1
416
-
421
+
417
422
  return scenario_lines, j
418
423
 
419
424
 
420
425
  def _is_scenario_boundary(line: str) -> bool:
421
426
  """Check if a line marks the boundary of a scenario block."""
422
- boundaries = ['Scenario:', 'Scenario Outline:', 'Background:', 'Feature:']
427
+ boundaries = ["Scenario:", "Scenario Outline:", "Background:", "Feature:"]
423
428
  return any(line.startswith(boundary) for boundary in boundaries)
424
429
 
425
430
 
@@ -427,38 +432,38 @@ def _process_scenario_block(scenario_lines: list) -> list:
427
432
  """Process a scenario block and convert to outline if placeholders are found."""
428
433
  if not scenario_lines:
429
434
  return scenario_lines
430
-
435
+
431
436
  first_line = scenario_lines[0]
432
437
  indentation = _get_line_indentation(first_line)
433
438
  placeholders = _extract_placeholders_from_scenario(scenario_lines[1:])
434
-
439
+
435
440
  if not placeholders:
436
441
  return scenario_lines
437
-
442
+
438
443
  return _convert_to_scenario_outline(scenario_lines, placeholders, indentation)
439
444
 
440
445
 
441
446
  def _get_line_indentation(line: str) -> str:
442
447
  """Get the indentation of a line."""
443
- return line[:len(line) - len(line.lstrip())]
448
+ return line[: len(line) - len(line.lstrip())]
444
449
 
445
450
 
446
451
  def _extract_placeholders_from_scenario(step_lines: list) -> set:
447
452
  """Extract placeholders from scenario step lines, ignoring docstrings."""
448
453
  placeholders = set()
449
454
  in_docstring = False
450
-
455
+
451
456
  for line in step_lines:
452
457
  step_line = line.strip()
453
458
  if not step_line:
454
459
  continue
455
-
460
+
456
461
  in_docstring = _update_docstring_state(step_line, in_docstring)
457
-
462
+
458
463
  if not in_docstring and '"""' not in step_line:
459
- found = re.findall(r'<([^>]+)>', step_line)
464
+ found = re.findall(r"<([^>]+)>", step_line)
460
465
  placeholders.update(found)
461
-
466
+
462
467
  return placeholders
463
468
 
464
469
 
@@ -469,34 +474,36 @@ def _update_docstring_state(line: str, current_state: bool) -> bool:
469
474
  return current_state
470
475
 
471
476
 
472
- def _convert_to_scenario_outline(scenario_lines: list, placeholders: set, indentation: str) -> list:
477
+ def _convert_to_scenario_outline(
478
+ scenario_lines: list, placeholders: set, indentation: str
479
+ ) -> list:
473
480
  """Convert scenario lines to scenario outline format with examples table."""
474
481
  first_line = scenario_lines[0]
475
- title = first_line.strip()[len('Scenario:'):].strip()
476
-
482
+ title = first_line.strip()[len("Scenario:") :].strip()
483
+
477
484
  new_lines = [f"{indentation}Scenario Outline: {title}"]
478
485
  new_lines.extend(scenario_lines[1:])
479
486
  new_lines.append("")
480
-
487
+
481
488
  examples_lines = _create_examples_table(placeholders, indentation)
482
489
  new_lines.extend(examples_lines)
483
-
490
+
484
491
  return new_lines
485
492
 
486
493
 
487
494
  def _create_examples_table(placeholders: set, base_indentation: str) -> list:
488
495
  """Create the Examples table for the scenario outline."""
489
496
  examples_indentation = base_indentation + " "
490
- table_indentation = examples_indentation + " "
491
-
497
+ table_indentation = examples_indentation + " "
498
+
492
499
  sorted_placeholders = sorted(placeholders)
493
500
  header = "| " + " | ".join(sorted_placeholders) + " |"
494
501
  sample_row = "| " + " | ".join(f"<{p}_value>" for p in sorted_placeholders) + " |"
495
-
502
+
496
503
  return [
497
504
  f"{examples_indentation}Examples:",
498
505
  f"{table_indentation}{header}",
499
- f"{table_indentation}{sample_row}"
506
+ f"{table_indentation}{sample_row}",
500
507
  ]
501
508
 
502
509
 
@@ -539,7 +546,9 @@ def fix_contribution(
539
546
  classified_artefact_info: dict,
540
547
  **kwargs,
541
548
  ):
542
- classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
549
+ classified_artefact_info = populate_classified_artefact_info(
550
+ classified_artefact_info=classified_artefact_info
551
+ )
543
552
  artefact = artefact_class.deserialize(artefact_text)
544
553
  artefact, _ = set_closest_contribution(artefact)
545
554
  artefact_text = artefact.serialize()
@@ -553,7 +562,9 @@ def fix_rule(
553
562
  classified_artefact_info: dict,
554
563
  **kwargs,
555
564
  ):
556
- classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
565
+ classified_artefact_info = populate_classified_artefact_info(
566
+ classified_artefact_info=classified_artefact_info
567
+ )
557
568
  artefact = artefact_class.deserialize(artefact_text)
558
569
  contribution = artefact.contribution
559
570
  assert contribution is not None
@@ -562,11 +573,13 @@ def fix_rule(
562
573
  name=contribution.artefact_name,
563
574
  classifier=contribution.classifier,
564
575
  classified_file_info=classified_artefact_info,
565
- delete_if_not_found=True
576
+ delete_if_not_found=True,
577
+ )
578
+ feedback_message = (
579
+ f"Updating contribution of {artefact._artefact_type().value} "
580
+ f"'{artefact.title}' to {contribution.classifier} "
581
+ f"'{contribution.artefact_name}' "
566
582
  )
567
- feedback_message = (f"Updating contribution of {artefact._artefact_type().value} "
568
- f"'{artefact.title}' to {contribution.classifier} "
569
- f"'{contribution.artefact_name}' ")
570
583
  rule = contribution.rule
571
584
  if rule:
572
585
  feedback_message += f"with rule '{rule}'"
@@ -576,7 +589,47 @@ def fix_rule(
576
589
  return artefact.serialize()
577
590
 
578
591
 
579
- def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
592
+ def fix_misplaced_content(file_path: str, artefact_text: str, **kwargs) -> str:
593
+ """
594
+ Deterministically fixes content like 'Rule:' or 'Estimate:' misplaced in the description.
595
+ """
596
+ lines = artefact_text.splitlines()
597
+
598
+ desc_start_idx = -1
599
+ for i, line in enumerate(lines):
600
+ if line.strip().startswith("Description:"):
601
+ desc_start_idx = i
602
+ break
603
+
604
+ if desc_start_idx == -1:
605
+ return artefact_text # No description, nothing to fix.
606
+
607
+ pre_desc_lines = lines[:desc_start_idx]
608
+ desc_line = lines[desc_start_idx]
609
+ post_desc_lines = lines[desc_start_idx + 1 :]
610
+
611
+ misplaced_content = []
612
+ new_post_desc_lines = []
613
+
614
+ for line in post_desc_lines:
615
+ if line.strip().startswith("Rule:") or line.strip().startswith("Estimate:"):
616
+ misplaced_content.append(line)
617
+ else:
618
+ new_post_desc_lines.append(line)
619
+
620
+ if not misplaced_content:
621
+ return artefact_text
622
+
623
+ # Rebuild the file content
624
+ final_lines = (
625
+ pre_desc_lines + misplaced_content + [""] + [desc_line] + new_post_desc_lines
626
+ )
627
+ return "\n".join(final_lines)
628
+
629
+
630
+ def should_skip_issue(
631
+ deterministic_issue, deterministic, non_deterministic, file_path
632
+ ) -> bool:
580
633
  if not non_deterministic and not deterministic_issue:
581
634
  print(f"Skipping non-deterministic fix for {file_path} as per request.")
582
635
  return True
@@ -585,15 +638,23 @@ def should_skip_issue(deterministic_issue, deterministic, non_deterministic, fil
585
638
  return True
586
639
  return False
587
640
 
641
+
588
642
  def determine_attempt_count(single_pass, file_path) -> int:
589
643
  if single_pass:
590
644
  print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
591
645
  return 1
592
646
  return 3
593
647
 
648
+
594
649
  def apply_deterministic_fix(
595
- deterministic, deterministic_issue, file_path, artefact_text, artefact_class, classified_artefact_info,
596
- deterministic_markers_to_functions, corrected_text
650
+ deterministic,
651
+ deterministic_issue,
652
+ file_path,
653
+ artefact_text,
654
+ artefact_class,
655
+ classified_artefact_info,
656
+ deterministic_markers_to_functions,
657
+ corrected_text,
597
658
  ) -> str:
598
659
  if deterministic and deterministic_issue:
599
660
  print(f"Applying deterministic fix for '{deterministic_issue}'...")
@@ -606,9 +667,16 @@ def apply_deterministic_fix(
606
667
  )
607
668
  return corrected_text
608
669
 
670
+
609
671
  def apply_non_deterministic_fix(
610
- non_deterministic, deterministic_issue, corrected_text,
611
- artefact_type, current_reason, file_path, artefact_text, artefact_class
672
+ non_deterministic,
673
+ deterministic_issue,
674
+ corrected_text,
675
+ artefact_type,
676
+ current_reason,
677
+ file_path,
678
+ artefact_text,
679
+ artefact_class,
612
680
  ) -> Optional[str]:
613
681
  """
614
682
  Applies LLM fix. Return None in case of an exception
@@ -622,10 +690,11 @@ def apply_non_deterministic_fix(
622
690
  corrected_artefact = run_agent(prompt, artefact_class)
623
691
  corrected_text = corrected_artefact.serialize()
624
692
  except Exception as e:
625
- print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
693
+ print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
626
694
  return None
627
695
  return corrected_text
628
696
 
697
+
629
698
  def attempt_autofix_loop(
630
699
  file_path: str,
631
700
  artefact_type,
@@ -651,7 +720,7 @@ def attempt_autofix_loop(
651
720
  print(
652
721
  f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
653
722
  )
654
- print(f" Reason: {current_reason}")
723
+ print(f" Reason: {current_reason}")
655
724
 
656
725
  artefact_text = read_artefact(file_path)
657
726
  if artefact_text is None:
@@ -666,35 +735,53 @@ def attempt_autofix_loop(
666
735
  None,
667
736
  )
668
737
 
669
- if should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path):
738
+ if should_skip_issue(
739
+ deterministic_issue, deterministic, non_deterministic, file_path
740
+ ):
670
741
  return False
671
742
 
672
743
  corrected_text = None
673
744
 
674
745
  corrected_text = apply_deterministic_fix(
675
- deterministic, deterministic_issue, file_path, artefact_text,
676
- artefact_class, classified_artefact_info,
677
- deterministic_markers_to_functions, corrected_text
746
+ deterministic,
747
+ deterministic_issue,
748
+ file_path,
749
+ artefact_text,
750
+ artefact_class,
751
+ classified_artefact_info,
752
+ deterministic_markers_to_functions,
753
+ corrected_text,
678
754
  )
679
755
  corrected_text = apply_non_deterministic_fix(
680
- non_deterministic, deterministic_issue, corrected_text,
681
- artefact_type, current_reason, file_path, artefact_text, artefact_class
756
+ non_deterministic,
757
+ deterministic_issue,
758
+ corrected_text,
759
+ artefact_type,
760
+ current_reason,
761
+ file_path,
762
+ artefact_text,
763
+ artefact_class,
682
764
  )
683
765
 
684
766
  if corrected_text is None or corrected_text.strip() == artefact_text.strip():
685
767
  print(
686
- " Fixing attempt did not alter the file. Stopping to prevent infinite loop."
768
+ " Fixing attempt did not alter the file. Stopping to prevent infinite loop."
687
769
  )
688
770
  return False
689
771
 
690
772
  write_corrected_artefact(file_path, corrected_text)
691
773
 
692
- print(" File modified. Re-classifying artefact information for next check...")
693
- classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
774
+ print(
775
+ " File modified. Re-classifying artefact information for next check..."
776
+ )
777
+ classified_artefact_info = populate_classified_artefact_info(
778
+ classified_artefact_info, force=True
779
+ )
694
780
 
695
781
  print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
696
782
  return False
697
783
 
784
+
698
785
  def apply_autofix(
699
786
  file_path: str,
700
787
  classifier: str,
@@ -713,13 +800,17 @@ def apply_autofix(
713
800
  "Invalid Contribution Reference": fix_contribution,
714
801
  "Rule Mismatch": fix_rule,
715
802
  "Scenario Contains Placeholders": fix_scenario_placeholder_mismatch,
803
+ "Found 'Rule:' inside description": fix_misplaced_content,
804
+ "Found 'Estimate:' inside description": fix_misplaced_content,
716
805
  }
717
806
 
718
807
  artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
719
808
  if artefact_type is None or artefact_class is None:
720
809
  return False
721
810
 
722
- classified_artefact_info = populate_classified_artefact_info(classified_artefact_info)
811
+ classified_artefact_info = populate_classified_artefact_info(
812
+ classified_artefact_info
813
+ )
723
814
  max_attempts = determine_attempt_count(single_pass, file_path)
724
815
 
725
816
  return attempt_autofix_loop(