teddy-cli 0.1.17.dev1302__tar.gz → 0.1.17.dev1306__tar.gz

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 (162) hide show
  1. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/PKG-INFO +1 -1
  2. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/pyproject.toml +1 -1
  3. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/session_cli_handlers.py +88 -74
  4. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/context_service.py +3 -5
  5. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/markdown_report_formatter.py +2 -2
  6. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/parser_reporting.py +9 -8
  7. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_lifecycle_manager.py +2 -3
  8. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/templates/execution_report.md.j2 +9 -27
  9. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/edit.py +6 -7
  10. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/markdown.py +54 -0
  11. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/.gitignore +0 -0
  12. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/LICENSE +0 -0
  13. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/README.md +0 -0
  14. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/__init__.py +0 -0
  15. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/__main__.py +0 -0
  16. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/__init__.py +0 -0
  17. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/__init__.py +0 -0
  18. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/cli_formatter.py +0 -0
  19. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/cli_helpers.py +0 -0
  20. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer.py +0 -0
  21. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +0 -0
  22. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +0 -0
  23. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +0 -0
  24. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +0 -0
  25. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +0 -0
  26. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +0 -0
  27. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +0 -0
  28. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/__init__.py +0 -0
  29. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/console_interactor.py +0 -0
  30. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/console_interactor_ask_loop.py +0 -0
  31. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/console_interactor_helpers.py +0 -0
  32. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/console_tooling.py +0 -0
  33. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/filesystem_helpers.py +0 -0
  34. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/litellm_adapter.py +0 -0
  35. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/local_file_system_adapter.py +0 -0
  36. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +0 -0
  37. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/openrouter_hydrator.py +0 -0
  38. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/shell_adapter.py +0 -0
  39. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/shell_command_builder.py +0 -0
  40. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/suppressed_logging.py +0 -0
  41. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/system_environment_adapter.py +0 -0
  42. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/system_environment_inspector.py +0 -0
  43. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/system_time_adapter.py +0 -0
  44. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/terminal_quit_key_listener.py +0 -0
  45. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/web_scraper_adapter.py +0 -0
  46. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/web_searcher_adapter.py +0 -0
  47. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/adapters/outbound/yaml_config_adapter.py +0 -0
  48. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/container.py +0 -0
  49. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/__init__.py +0 -0
  50. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/__init__.py +0 -0
  51. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/__init__.py +0 -0
  52. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/action_ports.py +0 -0
  53. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/change_set.py +0 -0
  54. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/drift_report.py +0 -0
  55. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/exceptions.py +0 -0
  56. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/execution_report.py +0 -0
  57. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/orchestrator_ports.py +0 -0
  58. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/plan.py +0 -0
  59. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/planning_ports.py +0 -0
  60. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/project_context.py +0 -0
  61. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/report_assembly_data.py +0 -0
  62. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/session.py +0 -0
  63. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/shell_output.py +0 -0
  64. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/domain/models/web_search_results.py +0 -0
  65. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/__init__.py +0 -0
  66. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/__init__.py +0 -0
  67. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/edit_simulator.py +0 -0
  68. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/get_context_use_case.py +0 -0
  69. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/init.py +0 -0
  70. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/plan_parser.py +0 -0
  71. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/plan_reviewer.py +0 -0
  72. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/plan_validator.py +0 -0
  73. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/planning_use_case.py +0 -0
  74. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/inbound/run_plan_use_case.py +0 -0
  75. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/__init__.py +0 -0
  76. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/config_service.py +0 -0
  77. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/environment_inspector.py +0 -0
  78. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/execution_report_assembler.py +0 -0
  79. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/file_system_manager.py +0 -0
  80. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/llm_client.py +0 -0
  81. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/markdown_report_formatter.py +0 -0
  82. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/prompt_manager.py +0 -0
  83. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/quit_key_listener.py +0 -0
  84. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/repo_tree_generator.py +0 -0
  85. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/session_loop_guard.py +0 -0
  86. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/session_manager.py +0 -0
  87. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/session_repository.py +0 -0
  88. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/shell_executor.py +0 -0
  89. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/system_environment.py +0 -0
  90. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/time_service.py +0 -0
  91. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/user_interactor.py +0 -0
  92. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/web_scraper.py +0 -0
  93. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/ports/outbound/web_searcher.py +0 -0
  94. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/__init__.py +0 -0
  95. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_changeset_builder.py +0 -0
  96. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_diff_manager.py +0 -0
  97. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_dispatcher.py +0 -0
  98. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_executor.py +0 -0
  99. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_factory.py +0 -0
  100. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_parser_complex.py +0 -0
  101. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/action_parser_strategies.py +0 -0
  102. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/edit_simulator.py +0 -0
  103. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/execution_orchestrator.py +0 -0
  104. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/execution_report_assembler.py +0 -0
  105. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/init_service.py +0 -0
  106. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/markdown_plan_parser.py +0 -0
  107. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/parser_infrastructure.py +0 -0
  108. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/parser_metadata.py +0 -0
  109. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/plan_validator.py +0 -0
  110. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/planning_service.py +0 -0
  111. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/prompt_manager.py +0 -0
  112. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_loop_guard.py +0 -0
  113. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_orchestrator.py +0 -0
  114. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_planner.py +0 -0
  115. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_pruning_service.py +0 -0
  116. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_replanner.py +0 -0
  117. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_repository.py +0 -0
  118. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/session_service.py +0 -0
  119. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/update_checker.py +0 -0
  120. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/__init__.py +0 -0
  121. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/edit_matcher.py +0 -0
  122. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +0 -0
  123. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/execute.py +0 -0
  124. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/filesystem.py +0 -0
  125. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/helpers.py +0 -0
  126. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/services/validation_rules/message.py +0 -0
  127. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/__init__.py +0 -0
  128. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/diff.py +0 -0
  129. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/interrupt_guard.py +0 -0
  130. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/io.py +0 -0
  131. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/paths.py +0 -0
  132. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/serialization.py +0 -0
  133. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/session_notices.py +0 -0
  134. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/stdin_ownership.py +0 -0
  135. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/string.py +0 -0
  136. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/core/utils/terminal.py +0 -0
  137. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/prompts.py +0 -0
  138. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/registries/__init__.py +0 -0
  139. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/registries/infrastructure.py +0 -0
  140. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/registries/reviewer.py +0 -0
  141. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/registries/validators.py +0 -0
  142. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/MRP.md +0 -0
  143. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/__init__.py +0 -0
  144. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/__init__.py +0 -0
  145. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/config.yaml +0 -0
  146. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/init.context +0 -0
  147. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/architect.md +0 -0
  148. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/assistant.md +0 -0
  149. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/debugger.md +0 -0
  150. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/developer.md +0 -0
  151. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/pathfinder.md +0 -0
  152. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/config/prompts/prototyper.md +0 -0
  153. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/ARCHITECTURE.md +0 -0
  154. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/PROJECT.md +0 -0
  155. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/case-file.md +0 -0
  156. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/ci.md +0 -0
  157. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/component-design.md +0 -0
  158. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/makefile.md +0 -0
  159. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/milestone.md +0 -0
  160. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/pre-commit.md +0 -0
  161. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/specification-document.md +0 -0
  162. {teddy_cli-0.1.17.dev1302 → teddy_cli-0.1.17.dev1306}/src/teddy_executor/resources/templates/vertical-slice.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: teddy-cli
3
- Version: 0.1.17.dev1302
3
+ Version: 0.1.17.dev1306
4
4
  Summary: The local-first, test-driven agentic engineering harness. Because you should own the means of development.
5
5
  Author: Raphael Atteritano
6
6
  License: AGPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "teddy-cli"
3
- version = "0.1.17.dev1302"
3
+ version = "0.1.17.dev1306"
4
4
  description = "The local-first, test-driven agentic engineering harness. Because you should own the means of development."
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -241,6 +241,71 @@ def _run_health_checks() -> None:
241
241
  _check_git_initialized()
242
242
 
243
243
 
244
+ def _start_background_update_check(container: Container) -> list[str]:
245
+ """Start the non-blocking version check and report the update-check keys.
246
+
247
+ Both session entry points share this preamble: it spawns the daemon
248
+ version-fetch thread, then emits the cached update notification (if any).
249
+ The returned list names the checks that fired so the shared disable-footer
250
+ can report them.
251
+ """
252
+ import threading
253
+
254
+ cache_path = find_project_root() / ".teddy" / ".update_cache.json"
255
+ thread = threading.Thread(
256
+ target=background_check,
257
+ args=(cache_path,),
258
+ daemon=True,
259
+ )
260
+ thread.start()
261
+ return _display_update_notification(container, cache_path)
262
+
263
+
264
+ def _run_startup_preamble(
265
+ container: Container,
266
+ fired: list[str],
267
+ agent: Optional[str] = None,
268
+ setup_editor: Optional[bool] = None,
269
+ setup_api_key: Optional[bool] = None,
270
+ ) -> None:
271
+ """Run the shared startup preamble for both session entry points.
272
+
273
+ The order is fixed so the banner layout cannot drift: health checks, the
274
+ ``Checking configurations...`` line, the preflight check (whose returned
275
+ keys extend ``fired``), then the shared disable-footer. ``start`` forwards
276
+ its ``agent`` so the preflight validates the agent prompt; ``resume``
277
+ leaves it ``None`` and validates no agent.
278
+ """
279
+ _run_health_checks()
280
+ typer.echo("Checking configurations...", err=True)
281
+ fired.extend(
282
+ _run_cli_preflight_check(
283
+ container,
284
+ agent=agent,
285
+ setup_editor=setup_editor,
286
+ setup_api_key=setup_api_key,
287
+ )
288
+ )
289
+ _display_checks_footer(fired)
290
+
291
+
292
+ def _handle_startup_error(container: Container, error: Exception) -> None:
293
+ """Print a startup error and exit with code 1 (shared by both entry points).
294
+
295
+ A ``ConfigurationError`` additionally prints the configuration path hint.
296
+ """
297
+ from teddy_executor.core.domain.models.exceptions import ConfigurationError
298
+
299
+ if isinstance(error, ConfigurationError):
300
+ config_service = container.resolve(IConfigService)
301
+ config_path = config_service.get_config_path()
302
+ typer.echo(f"Error: {error}", err=True)
303
+ typer.echo(f"Please update your configuration at: {config_path}", err=True)
304
+ else:
305
+ typer.echo(f"Error: {error}", err=True)
306
+ raise typer.Exit(code=1)
307
+
308
+
244
309
  def _determine_session_name(name: Optional[str], message: Optional[str]) -> str:
245
310
  """Determine session name (slugify message if name is missing)."""
246
311
  if name:
@@ -565,17 +630,7 @@ def handle_new_session( # noqa: PLR0913
565
630
  setup_api_key: Optional[bool] = None,
566
631
  ):
567
632
  """Logic for the 'start' command."""
568
- import threading
569
-
570
- # Start non-blocking background version check
571
- cache_path = find_project_root() / ".teddy" / ".update_cache.json"
572
- thread = threading.Thread(
573
- target=background_check,
574
- args=(cache_path,),
575
- daemon=True,
576
- )
577
- thread.start()
578
- fired = _display_update_notification(container, cache_path)
633
+ fired = _start_background_update_check(container)
579
634
 
580
635
  try:
581
636
  # 0. Pipeline mode requires an initial message
@@ -587,20 +642,14 @@ def handle_new_session( # noqa: PLR0913
587
642
  # 1. Ensure project is initialized
588
643
  container.resolve(IInitUseCase).ensure_initialized()
589
644
 
590
- # 2. Health checks (advisory, non-blocking)
591
- _run_health_checks()
592
-
593
- # 2. Pre-flight checks (Fail-fast before user interaction)
594
- typer.echo("Checking configurations...", err=True)
595
- fired.extend(
596
- _run_cli_preflight_check(
597
- container,
598
- agent=agent,
599
- setup_editor=setup_editor,
600
- setup_api_key=setup_api_key,
601
- )
645
+ # 2. Shared startup preamble (health checks + preflight + footer)
646
+ _run_startup_preamble(
647
+ container,
648
+ fired,
649
+ agent=agent,
650
+ setup_editor=setup_editor,
651
+ setup_api_key=setup_api_key,
602
652
  )
603
- _display_checks_footer(fired)
604
653
  _echo_config_success(container, agent, model=model)
605
654
 
606
655
  session_manager: ISessionManager = container.resolve(ISessionManager)
@@ -635,18 +684,7 @@ def handle_new_session( # noqa: PLR0913
635
684
  )
636
685
 
637
686
  except Exception as e:
638
- from teddy_executor.core.domain.models.exceptions import ConfigurationError
639
-
640
- if isinstance(e, ConfigurationError):
641
- from teddy_executor.core.ports.outbound.config_service import IConfigService
642
-
643
- config_service = container.resolve(IConfigService)
644
- config_path = config_service.get_config_path()
645
- typer.echo(f"Error: {e}", err=True)
646
- typer.echo(f"Please update your configuration at: {config_path}", err=True)
647
- else:
648
- typer.echo(f"Error: {e}", err=True)
649
- raise typer.Exit(code=1)
687
+ _handle_startup_error(container, e)
650
688
 
651
689
 
652
690
  def _echo_config_success(
@@ -1109,32 +1147,16 @@ def handle_resume_session( # noqa: PLR0913
1109
1147
  setup_api_key: Optional[bool] = None,
1110
1148
  ):
1111
1149
  """Logic for the 'resume' command."""
1112
- import threading
1113
-
1114
- # Start non-blocking background version check
1115
- cache_path = find_project_root() / ".teddy" / ".update_cache.json"
1116
- thread = threading.Thread(
1117
- target=background_check,
1118
- args=(cache_path,),
1119
- daemon=True,
1120
- )
1121
- thread.start()
1122
- fired = _display_update_notification(container, cache_path)
1150
+ fired = _start_background_update_check(container)
1123
1151
 
1124
1152
  try:
1125
- # 1. Health checks (advisory, non-blocking)
1126
- _run_health_checks()
1127
-
1128
- # 2. Pre-flight checks
1129
- typer.echo("Checking configurations...", err=True)
1130
- fired.extend(
1131
- _run_cli_preflight_check(
1132
- container,
1133
- setup_editor=setup_editor,
1134
- setup_api_key=setup_api_key,
1135
- )
1153
+ # 1. Shared startup preamble (health checks + preflight + footer)
1154
+ _run_startup_preamble(
1155
+ container,
1156
+ fired,
1157
+ setup_editor=setup_editor,
1158
+ setup_api_key=setup_api_key,
1136
1159
  )
1137
- _display_checks_footer(fired)
1138
1160
 
1139
1161
  # 2. Resolve session name
1140
1162
  session_name = _resolve_session_name(container, path)
@@ -1144,9 +1166,12 @@ def handle_resume_session( # noqa: PLR0913
1144
1166
  container, session_name, model=model, api_key=api_key, agent=agent
1145
1167
  )
1146
1168
 
1147
- # 4. Display session path (below the Agent/Model identity block)
1148
- session_relative_path = str(Path(".teddy") / "sessions" / session_name)
1149
- typer.echo(f"\nResuming session: {session_relative_path}")
1169
+ # 4. Display session path (below the Agent/Model identity block).
1170
+ # `.as_posix()` keeps the banner separator-identical on every OS
1171
+ # (Windows would otherwise emit backslashes) and matches the POSIX
1172
+ # f-string that `create_session` returns for the `start` banner.
1173
+ session_relative_path = (Path(".teddy") / "sessions" / session_name).as_posix()
1174
+ typer.echo(f"Resuming session: {session_relative_path}")
1150
1175
 
1151
1176
  # 5. Agent switch: if -a/--agent provided, update session agent permanently
1152
1177
  if agent:
@@ -1163,15 +1188,4 @@ def handle_resume_session( # noqa: PLR0913
1163
1188
  )
1164
1189
 
1165
1190
  except Exception as e:
1166
- from teddy_executor.core.domain.models.exceptions import ConfigurationError
1167
-
1168
- if isinstance(e, ConfigurationError):
1169
- from teddy_executor.core.ports.outbound.config_service import IConfigService
1170
-
1171
- config_service = container.resolve(IConfigService)
1172
- config_path = config_service.get_config_path()
1173
- typer.echo(f"Error: {e}", err=True)
1174
- typer.echo(f"Please update your configuration at: {config_path}", err=True)
1175
- else:
1176
- typer.echo(f"Error: {e}", err=True)
1177
- raise typer.Exit(code=1)
1191
+ _handle_startup_error(container, e)
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Dict, List, Optional, Sequence
5
5
  from teddy_executor.core.domain.models import ProjectContext, ContextItem
6
6
  from teddy_executor.core.utils.markdown import (
7
- get_fence_for_content,
7
+ embed_in_fence,
8
8
  get_language_from_path,
9
9
  is_session_file_path,
10
10
  get_session_history_display_name,
@@ -396,8 +396,7 @@ class ContextService(IGetContextUseCase):
396
396
  content = file_contents.get(path)
397
397
  if content is not None:
398
398
  lang = get_language_from_path(path)
399
- fence = get_fence_for_content(content)
400
- parts.append(f"{fence}{lang}\n{content}\n{fence}")
399
+ parts.append(embed_in_fence(content, lang))
401
400
  else:
402
401
  parts.append("```\n--- FILE NOT FOUND ---\n```")
403
402
  return parts
@@ -471,7 +470,6 @@ class ContextService(IGetContextUseCase):
471
470
  content = file_contents.get(path) or ""
472
471
  content_str = content.strip()
473
472
  lang = get_language_from_path(path)
474
- fence = get_fence_for_content(content_str)
475
473
  parts.append(f"\n### {disp_name}")
476
- parts.append(f"{fence}{lang}\n{content_str}\n{fence}")
474
+ parts.append(embed_in_fence(content_str, lang))
477
475
  return parts
@@ -7,7 +7,7 @@ from teddy_executor.core.ports.outbound.markdown_report_formatter import (
7
7
  IMarkdownReportFormatter,
8
8
  )
9
9
  from teddy_executor.core.utils.markdown import (
10
- get_fence_for_content,
10
+ embed_in_fence,
11
11
  get_language_from_path,
12
12
  )
13
13
 
@@ -37,7 +37,7 @@ class MarkdownReportFormatter(IMarkdownReportFormatter):
37
37
  autoescape=False, # nosec B701
38
38
  )
39
39
  env.filters["basename"] = os.path.basename
40
- env.filters["fence"] = get_fence_for_content
40
+ env.filters["embed_fence"] = embed_in_fence
41
41
  env.filters["language_from_path"] = get_language_from_path
42
42
 
43
43
  MarkdownReportFormatter._cached_env = env
@@ -10,7 +10,7 @@ from teddy_executor.core.services.parser_infrastructure import (
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from mistletoe.block_token import Document
13
- from teddy_executor.core.utils.markdown import get_fence_for_content
13
+ from teddy_executor.core.utils.markdown import embed_in_fence
14
14
 
15
15
  # Maximum length for AST node previews in error reports
16
16
  MAX_PREVIEW_LENGTH = 60
@@ -48,9 +48,10 @@ def _format_expected_structure() -> str:
48
48
  "[004] Heading (Level 2: Action Plan) —or— Heading (Level 2: Message)",
49
49
  "[005...] (If Action Plan) Heading (Level 3: Action Type) and (Action-specific AST nodes)",
50
50
  ]
51
- content = "\n".join(lines) + "\n"
52
- fence = get_fence_for_content(content)
53
- return f"### Expected Response Structure (MRP) \n{fence}text\n{content}{fence}\n"
51
+ content = "\n".join(lines)
52
+ return (
53
+ f"### Expected Response Structure (MRP) \n{embed_in_fence(content, 'text')}\n"
54
+ )
54
55
 
55
56
 
56
57
  def format_node_name(node: Any) -> str:
@@ -137,9 +138,9 @@ def format_hybrid_ast_view(
137
138
  error_ids = {id(e.offending_node) for e in errors if e.offending_node}
138
139
  error_map = {id(e.offending_node): e.message for e in errors if e.offending_node}
139
140
  ast_text = _render_ast_view(doc, error_ids, error_map)
141
+ body = ast_text.rstrip("\n")
140
142
 
141
- fence = get_fence_for_content(ast_text)
142
- return f"### Plan AST with Highlighted Failures\n{fence}text\n{ast_text}{fence}\n"
143
+ return f"### Plan AST with Highlighted Failures\n{embed_in_fence(body, 'text')}\n"
143
144
 
144
145
 
145
146
  def get_action_type_from_node(plan: Plan, offending_node: Any) -> str:
@@ -270,8 +271,8 @@ def format_structural_mismatch_msg(
270
271
 
271
272
  cutoff = _get_failure_cutoff_idx(children, mismatch_idx, offending_ids)
272
273
  ast_text = _render_ast_view(doc, error_ids_set, error_map, cutoff)
273
- fence = get_fence_for_content(ast_text)
274
- msg += f"{fence}text\n{ast_text}{fence}\n"
274
+ body = ast_text.rstrip("\n")
275
+ msg += f"{embed_in_fence(body, 'text')}\n"
275
276
 
276
277
  msg += "\n**Hint:** Parsing often fails due to improper Code Block Formatting.\n"
277
278
  return msg
@@ -17,7 +17,7 @@ from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
17
17
  from teddy_executor.core.ports.inbound.run_plan_use_case import IRunPlanUseCase
18
18
  from teddy_executor.core.ports.outbound.session_manager import SessionState
19
19
  from teddy_executor.core.utils.io import Tee as _Tee
20
- from teddy_executor.core.utils.markdown import get_fence_for_content
20
+ from teddy_executor.core.utils.markdown import embed_in_fence
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
@@ -241,8 +241,7 @@ class SessionLifecycleManager:
241
241
  f"Cannot append user request: report not found at {report_path}"
242
242
  )
243
243
  content = str(self._file_system_manager.read_file(report_path)).rstrip("\n")
244
- fence = get_fence_for_content(message)
245
- content += f"\n\n## User Request\n{fence}text\n{message}\n{fence}\n"
244
+ content += f"\n\n## User Request\n{embed_in_fence(message, 'text')}\n"
246
245
  self._file_system_manager.write_file(report_path, content)
247
246
 
248
247
  def _consume_awaiting_reply( # noqa: PLR0913
@@ -15,9 +15,7 @@
15
15
  {% else %}
16
16
  ### [{{ path }}]({% if not path.startswith('/') %}/{% endif %}{{ path }})
17
17
  {% endif %}
18
- {{ content | fence }}{{ path | language_from_path }}
19
- {{ content }}
20
- {{ content | fence }}
18
+ {{ content | embed_fence(path | language_from_path) }}
21
19
  {% endmacro %}
22
20
 
23
21
  {% macro render_action_summary(log) %}
@@ -63,36 +61,26 @@
63
61
  {% if log.action_type.upper() == 'MESSAGE' and log.details %}
64
62
  {% if log.details is mapping and log.details.get('content') %}
65
63
  - **User Reply:**
66
- {{ log.details.content | trim | fence }}
67
- {{ log.details.content | trim }}
68
- {{ log.details.content | trim | fence }}
64
+ {{ log.details.content | trim | embed_fence('') }}
69
65
  {% else %}
70
66
  - **User Reply:**
71
- {{ log.details | trim | fence }}
72
- {{ log.details | trim }}
73
- {{ log.details | trim | fence }}
67
+ {{ log.details | trim | embed_fence('') }}
74
68
  {% endif %}
75
69
 
76
70
  {% elif log.details and log.details is mapping -%}
77
71
  {% if log.details.get("stdout") -%}
78
72
  #### `stdout`
79
- {{ log.details.stdout | trim | fence }}text
80
- {{ log.details.stdout | trim }}
81
- {{ log.details.stdout | trim | fence }}
73
+ {{ log.details.stdout | trim | embed_fence('text') }}
82
74
 
83
75
  {% endif -%}
84
76
  {% if log.details.get("stderr") -%}
85
77
  #### `stderr`
86
- {{ log.details.stderr | trim | fence }}text
87
- {{ log.details.stderr | trim }}
88
- {{ log.details.stderr | trim | fence }}
78
+ {{ log.details.stderr | trim | embed_fence('text') }}
89
79
 
90
80
  {% endif -%}
91
81
  {% if log.details.get("diff") -%}
92
82
  #### `diff`
93
- {{ log.details.diff | trim | fence }}diff
94
- {{ log.details.diff | trim }}
95
- {{ log.details.diff | trim | fence }}
83
+ {{ log.details.diff | trim | embed_fence('diff') }}
96
84
 
97
85
  {% endif -%}
98
86
  {% endif %}
@@ -229,9 +217,7 @@
229
217
  {% if cmd %}
230
218
 
231
219
  - **Command:** {% if '\n' in cmd %}
232
- {{ '\n' }}{{ cmd | fence }}shell
233
- {{ cmd }}
234
- {{ cmd | fence }}
220
+ {{ '\n' }}{{ cmd | embed_fence('shell') }}
235
221
  {% else %}`{{ cmd }}`{% endif %}
236
222
  {% endif %}
237
223
 
@@ -280,9 +266,7 @@ _**Hint:** NOW you MUST use READ on the most promising results_
280
266
  {% endif %}
281
267
 
282
268
  {% if log in report_ns.inline_logs %}
283
- {{ log.details.content | trim | fence }}
284
- {{ log.details.content | trim }}
285
- {{ log.details.content | trim | fence }}
269
+ {{ log.details.content | trim | embed_fence('') }}
286
270
  {% endif %}
287
271
 
288
272
  {{ render_action_blocks(log) }}
@@ -299,7 +283,5 @@ _**Hint:** NOW you MUST use READ on the most promising results_
299
283
  {% if report.user_request %}
300
284
 
301
285
  ## User Request
302
- {{ report.user_request | fence }}text
303
- {{ report.user_request }}
304
- {{ report.user_request | fence }}
286
+ {{ report.user_request | embed_fence('text') }}
305
287
  {% endif %}
@@ -22,7 +22,7 @@ from teddy_executor.core.services.validation_rules.helpers import (
22
22
  resolve_similarity_threshold,
23
23
  validate_path_is_safe,
24
24
  )
25
- from teddy_executor.core.utils.markdown import get_fence_for_content
25
+ from teddy_executor.core.utils.markdown import embed_in_fence
26
26
 
27
27
 
28
28
  class EditActionValidator(BaseActionValidator):
@@ -137,7 +137,7 @@ def _validate_single_edit(
137
137
  effective_threshold = (
138
138
  threshold if threshold is not None else DEFAULT_SIMILARITY_THRESHOLD
139
139
  )
140
- fence = get_fence_for_content(find_block)
140
+ find_embed = embed_in_fence(find_block, "")
141
141
 
142
142
  if is_ambiguous and not match_all and score >= effective_threshold:
143
143
  errors.append(
@@ -146,7 +146,7 @@ def _validate_single_edit(
146
146
  f"The `FIND` block is ambiguous in: {file_path}\n"
147
147
  f"**Similarity Score:** {score:.2f}\n"
148
148
  f"**FIND Block:**\n"
149
- f"{fence}\n{find_block}\n{fence}\n"
149
+ f"{find_embed}\n"
150
150
  "**Hint:** Please provide a larger FIND block to uniquely identify the section, refactor the code to avoid duplication. Alternatively you can use `Match All: true` to change all occurrences in the file at once."
151
151
  ),
152
152
  file_path=str(file_path),
@@ -159,16 +159,15 @@ def _validate_single_edit(
159
159
  f"**Similarity Score:** {score:.2f}\n"
160
160
  f"**Similarity Threshold:** {effective_threshold:.2f}\n"
161
161
  f"**FIND Block:**\n"
162
- f"{fence}\n{find_block}\n{fence}\n"
162
+ f"{find_embed}\n"
163
163
  )
164
164
  if diff_text:
165
165
  # Prepend standard diff headers for high-clarity diagnostics
166
166
  # ndiff(find, actual) means '-' is Provided and '+' is Actual
167
167
  formatted_diff = f"--- Provided\n+++ Actual\n{diff_text}"
168
- diff_fence = get_fence_for_content(formatted_diff)
169
168
  error_msg += (
170
- f"**Closest Match Diff:**\n{diff_fence}diff\n"
171
- f"{formatted_diff}\n{diff_fence}\n"
169
+ f"**Closest Match Diff:**\n"
170
+ f"{embed_in_fence(formatted_diff, 'diff')}\n"
172
171
  )
173
172
 
174
173
  hint = _get_already_applied_hint(
@@ -85,6 +85,60 @@ def get_fence_for_content(content: str) -> str:
85
85
  return "`" * fence_length
86
86
 
87
87
 
88
+ # A Markdown code fence: up to 3 leading spaces, then a run of 3+ backticks or
89
+ # tildes, then an optional info string (opening) or trailing whitespace (closing).
90
+ _FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
91
+
92
+
93
+ def strip_trailing_open_fence(content: str) -> str:
94
+ """Remove a trailing, UNCLOSED opening code fence from ``content``.
95
+
96
+ Walks the fence structure: if the content ends INSIDE an open code block,
97
+ that dangling opening fence line is removed. Balanced content -- including a
98
+ legitimate nested fence (a 4-backtick fence wrapping a 3-backtick fence) --
99
+ is returned byte-for-byte unchanged.
100
+ """
101
+ lines = content.split("\n")
102
+ open_char: str | None = None
103
+ open_len = 0
104
+ open_index: int | None = None
105
+
106
+ for index, line in enumerate(lines):
107
+ match = _FENCE_RE.match(line)
108
+ if match is None:
109
+ continue
110
+ fence = match.group(1)
111
+ info = match.group(2)
112
+ if open_char is None:
113
+ # Opening fence (any run >= 3 starts a block).
114
+ open_char = fence[0]
115
+ open_len = len(fence)
116
+ open_index = index
117
+ elif fence[0] == open_char and len(fence) >= open_len and not info.strip():
118
+ # Closing fence: same char, run at least as long, no info string.
119
+ open_char = None
120
+ open_len = 0
121
+ open_index = None
122
+
123
+ if open_index is None:
124
+ return content
125
+ # The content ends inside an open block: strip the dangling opening fence.
126
+ del lines[open_index]
127
+ return "\n".join(lines)
128
+
129
+
130
+ def embed_in_fence(content: str, lang: str) -> str:
131
+ """Sanitize ``content``, then wrap it in a safe, dynamically-sized fence.
132
+
133
+ Strips a trailing UNCLOSED fence BEFORE deriving the wrapper fence, so a
134
+ degenerate run cannot amplify the wrapper. The wrapper fence is sized from
135
+ the sanitized content, so it can never collide with content fences.
136
+ """
137
+ sanitized = strip_trailing_open_fence(content)
138
+ fence = get_fence_for_content(sanitized)
139
+ return f"{fence}{lang}\n{sanitized}\n{fence}"
140
+
141
+
88
142
  def get_session_history_display_name(path: str) -> str | None:
89
143
  """
90
144
  Returns human-readable display name if it's a recognized session history file.