teddy-cli 0.1.17.dev1400__tar.gz → 0.1.17.dev1402__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 (166) hide show
  1. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/PKG-INFO +1 -1
  2. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/pyproject.toml +1 -1
  3. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/__main__.py +17 -2
  4. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/session_cli_handlers.py +176 -5
  5. teddy_cli-0.1.17.dev1402/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +138 -0
  6. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/yaml_config_adapter.py +65 -4
  7. teddy_cli-0.1.17.dev1402/src/teddy_executor/core/domain/models/config_schema.py +356 -0
  8. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_factory.py +2 -2
  9. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/helpers.py +2 -4
  10. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/infrastructure.py +6 -2
  11. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/config.yaml +13 -3
  12. teddy_cli-0.1.17.dev1400/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +0 -81
  13. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/.gitignore +0 -0
  14. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/LICENSE +0 -0
  15. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/README.md +0 -0
  16. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/__init__.py +0 -0
  17. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/__init__.py +0 -0
  18. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/__init__.py +0 -0
  19. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/cli_formatter.py +0 -0
  20. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/cli_helpers.py +0 -0
  21. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer.py +0 -0
  22. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +0 -0
  23. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +0 -0
  24. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +0 -0
  25. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +0 -0
  26. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +0 -0
  27. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +0 -0
  28. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +0 -0
  29. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/__init__.py +0 -0
  30. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor.py +0 -0
  31. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor_ask_loop.py +0 -0
  32. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor_helpers.py +0 -0
  33. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_tooling.py +0 -0
  34. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/filesystem_helpers.py +0 -0
  35. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/litellm_adapter.py +0 -0
  36. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/local_file_system_adapter.py +0 -0
  37. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/openrouter_hydrator.py +0 -0
  38. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/shell_adapter.py +0 -0
  39. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/shell_command_builder.py +0 -0
  40. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/suppressed_logging.py +0 -0
  41. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_environment_adapter.py +0 -0
  42. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_environment_inspector.py +0 -0
  43. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_time_adapter.py +0 -0
  44. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/terminal_quit_key_listener.py +0 -0
  45. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/web_scraper_adapter.py +0 -0
  46. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/web_searcher_adapter.py +0 -0
  47. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/container.py +0 -0
  48. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/__init__.py +0 -0
  49. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/__init__.py +0 -0
  50. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/__init__.py +0 -0
  51. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/action_ports.py +0 -0
  52. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/change_set.py +0 -0
  53. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/drift_report.py +0 -0
  54. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/exceptions.py +0 -0
  55. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/execution_report.py +0 -0
  56. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/orchestrator_ports.py +0 -0
  57. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/plan.py +0 -0
  58. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/planning_ports.py +0 -0
  59. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/project_context.py +0 -0
  60. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/report_assembly_data.py +0 -0
  61. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/session.py +0 -0
  62. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/shell_output.py +0 -0
  63. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/web_search_results.py +0 -0
  64. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/__init__.py +0 -0
  65. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/__init__.py +0 -0
  66. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/edit_simulator.py +0 -0
  67. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/get_context_use_case.py +0 -0
  68. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/init.py +0 -0
  69. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_parser.py +0 -0
  70. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_reviewer.py +0 -0
  71. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_validator.py +0 -0
  72. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/planning_use_case.py +0 -0
  73. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/run_plan_use_case.py +0 -0
  74. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/__init__.py +0 -0
  75. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/config_service.py +0 -0
  76. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/environment_inspector.py +0 -0
  77. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/execution_report_assembler.py +0 -0
  78. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/file_system_manager.py +0 -0
  79. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/llm_client.py +0 -0
  80. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/markdown_report_formatter.py +0 -0
  81. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/prompt_manager.py +0 -0
  82. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/quit_key_listener.py +0 -0
  83. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/repo_tree_generator.py +0 -0
  84. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_loop_guard.py +0 -0
  85. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_manager.py +0 -0
  86. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_repository.py +0 -0
  87. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/shell_executor.py +0 -0
  88. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/system_environment.py +0 -0
  89. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/time_service.py +0 -0
  90. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/user_interactor.py +0 -0
  91. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/web_scraper.py +0 -0
  92. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/web_searcher.py +0 -0
  93. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/__init__.py +0 -0
  94. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_changeset_builder.py +0 -0
  95. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_diff_manager.py +0 -0
  96. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_dispatcher.py +0 -0
  97. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_executor.py +0 -0
  98. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_parser_complex.py +0 -0
  99. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_parser_strategies.py +0 -0
  100. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/context_service.py +0 -0
  101. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/edit_simulator.py +0 -0
  102. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/execution_orchestrator.py +0 -0
  103. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/execution_report_assembler.py +0 -0
  104. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/init_service.py +0 -0
  105. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/markdown_plan_parser.py +0 -0
  106. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/markdown_report_formatter.py +0 -0
  107. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_infrastructure.py +0 -0
  108. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_metadata.py +0 -0
  109. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_reporting.py +0 -0
  110. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/plan_validator.py +0 -0
  111. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/planning_service.py +0 -0
  112. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/prompt_manager.py +0 -0
  113. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_lifecycle_manager.py +0 -0
  114. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_loop_guard.py +0 -0
  115. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_orchestrator.py +0 -0
  116. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_planner.py +0 -0
  117. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_pruning_service.py +0 -0
  118. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_replanner.py +0 -0
  119. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_repository.py +0 -0
  120. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_service.py +0 -0
  121. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/templates/execution_report.md.j2 +0 -0
  122. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/update_checker.py +0 -0
  123. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/__init__.py +0 -0
  124. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/edit.py +0 -0
  125. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/edit_matcher.py +0 -0
  126. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +0 -0
  127. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/execute.py +0 -0
  128. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/filesystem.py +0 -0
  129. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/message.py +0 -0
  130. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/__init__.py +0 -0
  131. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/diff.py +0 -0
  132. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/editor_names.py +0 -0
  133. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/interrupt_guard.py +0 -0
  134. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/interruptible_call.py +0 -0
  135. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/io.py +0 -0
  136. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/markdown.py +0 -0
  137. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/paths.py +0 -0
  138. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/serialization.py +0 -0
  139. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/session_notices.py +0 -0
  140. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/stdin_ownership.py +0 -0
  141. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/string.py +0 -0
  142. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/terminal.py +0 -0
  143. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/prompts.py +0 -0
  144. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/__init__.py +0 -0
  145. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/reviewer.py +0 -0
  146. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/validators.py +0 -0
  147. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/MRP.md +0 -0
  148. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/__init__.py +0 -0
  149. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/__init__.py +0 -0
  150. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/init.context +0 -0
  151. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/architect.md +0 -0
  152. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/assistant.md +0 -0
  153. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/debugger.md +0 -0
  154. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/developer.md +0 -0
  155. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/pathfinder.md +0 -0
  156. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/prototyper.md +0 -0
  157. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/ARCHITECTURE.md +0 -0
  158. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/PROJECT.md +0 -0
  159. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/case-file.md +0 -0
  160. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/ci.md +0 -0
  161. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/component-design.md +0 -0
  162. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/makefile.md +0 -0
  163. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/milestone.md +0 -0
  164. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/pre-commit.md +0 -0
  165. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/specification-document.md +0 -0
  166. {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/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.dev1400
3
+ Version: 0.1.17.dev1402
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.dev1400"
3
+ version = "0.1.17.dev1402"
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 = [
@@ -11,6 +11,7 @@ import typer
11
11
  from teddy_executor.core.domain.models import (
12
12
  ExecutionReport,
13
13
  )
14
+ from teddy_executor.core.domain.models.exceptions import ConfigurationError
14
15
  from teddy_executor.core.ports.outbound.config_service import IConfigService
15
16
  from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
16
17
 
@@ -129,6 +130,20 @@ def _display_init_drift(container) -> None:
129
130
  _display_checks_footer(_display_drift_notification(container))
130
131
 
131
132
 
133
+ def _resolve_config_or_exit(container) -> IConfigService:
134
+ """Resolve the config service, failing fast on an invalid configuration.
135
+
136
+ A value that cannot be coerced to its declared type raises
137
+ ``ConfigurationError`` during adapter construction. Printing the message
138
+ and exiting keeps the failure visible instead of escaping as a traceback.
139
+ """
140
+ try:
141
+ return container.resolve(IConfigService)
142
+ except ConfigurationError as error:
143
+ typer.secho(f"Error: {error}", fg=typer.colors.RED, err=True)
144
+ raise typer.Exit(code=1) from error
145
+
146
+
132
147
  def _ensure_project_initialized(container, root_dir: str | None = None) -> None:
133
148
  """Lazily performs project anchoring and initialization.
134
149
 
@@ -246,7 +261,7 @@ def start( # noqa: PLR0913
246
261
 
247
262
  container = get_container()
248
263
  _ensure_project_initialized(container)
249
- config_service = container.resolve(IConfigService)
264
+ config_service = _resolve_config_or_exit(container)
250
265
 
251
266
  from teddy_executor.adapters.inbound.cli_helpers import normalize_context_values
252
267
 
@@ -538,7 +553,7 @@ def resume( # noqa: PLR0913
538
553
 
539
554
  container = get_container()
540
555
  _ensure_project_initialized(container)
541
- config_service = container.resolve(IConfigService)
556
+ config_service = _resolve_config_or_exit(container)
542
557
 
543
558
  interactive = not (_resolve_yolo(yolo, config_service) or pipeline)
544
559
  system_env = container.resolve(ISystemEnvironment)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  import shutil
3
4
  import signal
4
5
  import subprocess # nosec B404
@@ -244,10 +245,179 @@ def _check_git_initialized() -> None:
244
245
  )
245
246
 
246
247
 
247
- def _run_health_checks() -> None:
248
- """Run all startup health checks and display notifications if needed."""
249
- _ensure_commit_hooks()
250
- _check_git_initialized()
248
+ def _warn_disabled_workspace_steps(config_service: IConfigService) -> None:
249
+ """Print one warning per disabled workspace step with a real consequence.
250
+
251
+ A disabled hook installer warns because the unskippable post-commit test
252
+ gate then depends on a hook that never landed. A disabled git init warns
253
+ only when the workspace is not already a repository, because an existing
254
+ repository needs no initialization.
255
+ """
256
+ if not config_service.get_setting("workspace.pre-commit", True):
257
+ typer.secho(
258
+ "⚠ Pre-commit hook installation is disabled. "
259
+ "The post-commit test gate will not run.",
260
+ fg=typer.colors.YELLOW,
261
+ err=True,
262
+ )
263
+ init_disabled = not config_service.get_setting("workspace.init", True)
264
+ if init_disabled and not (Path.cwd() / ".git").exists():
265
+ typer.secho(
266
+ "⚠ Git repository not initialized. Version control actions will fail.",
267
+ fg=typer.colors.YELLOW,
268
+ err=True,
269
+ )
270
+
271
+
272
+ def _run_health_checks(config_service: IConfigService) -> None:
273
+ """Run each startup health check the workspace toggles allow.
274
+
275
+ A disabled step is skipped, and ``_warn_disabled_workspace_steps`` reports
276
+ the ones that carry a real consequence.
277
+ """
278
+ if config_service.get_setting("workspace.pre-commit", True):
279
+ _ensure_commit_hooks()
280
+ if config_service.get_setting("workspace.init", True):
281
+ _check_git_initialized()
282
+ _warn_disabled_workspace_steps(config_service)
283
+
284
+
285
+ _GIT_PULL_TIMEOUT_SECONDS = 15
286
+ _PULL_SKIPPED_WARNING = "⚠ Workspace pull skipped; continuing with the current branch."
287
+ _PULL_TIMEOUT_WARNING = "⚠ Workspace pull timed out; continuing without it."
288
+ _IN_PROGRESS_GIT_MARKERS = (
289
+ "rebase-merge",
290
+ "rebase-apply",
291
+ "MERGE_HEAD",
292
+ "CHERRY_PICK_HEAD",
293
+ )
294
+
295
+
296
+ def _pull_workspace_on_start(
297
+ config_service: IConfigService,
298
+ run_func: Optional[Callable[..., Any]] = None,
299
+ ) -> None:
300
+ """Fast-forward the workspace before starting a new session.
301
+
302
+ The pull runs only when ``workspace.pull`` is enabled and the workspace is
303
+ a Git repository, so a disabled toggle or a non-repository directory stays
304
+ silent. A detached HEAD, a missing upstream, or an in-progress operation
305
+ also stays silent, because there is nothing safe to fast-forward. A dirty
306
+ worktree or a failed pull reports one warning and leaves the workspace
307
+ unchanged. A successful fast-forward names the number of commits it moved.
308
+
309
+ The Git process is injected through ``run_func`` so every decision is
310
+ testable without spawning a real repository.
311
+ """
312
+ if not config_service.get_setting("workspace.pull", True):
313
+ return
314
+ if not (Path.cwd() / ".git").exists():
315
+ return
316
+ git_run = run_func or subprocess.run
317
+ try:
318
+ _fast_forward_workspace(git_run)
319
+ except subprocess.TimeoutExpired:
320
+ typer.secho(_PULL_TIMEOUT_WARNING, fg=typer.colors.YELLOW, err=True)
321
+
322
+
323
+ def _fast_forward_workspace(git_run: Callable[..., Any]) -> None:
324
+ """Run the pull decision table through the injected Git runner.
325
+
326
+ Each precondition is probed before the pull so an unsafe state never
327
+ reaches ``git pull``. A dirty worktree reports one warning, while every
328
+ other unsafe state stays silent because it has nothing to report.
329
+ """
330
+ skip_reason = _pull_skip_reason(git_run)
331
+ if skip_reason is not None:
332
+ if skip_reason:
333
+ typer.secho(skip_reason, fg=typer.colors.YELLOW, err=True)
334
+ return
335
+ before = _read_head(git_run)
336
+ result = git_run(
337
+ ["git", "pull", "--ff-only"],
338
+ env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
339
+ capture_output=True,
340
+ text=True,
341
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
342
+ )
343
+ if result.returncode != 0:
344
+ typer.secho(_PULL_SKIPPED_WARNING, fg=typer.colors.YELLOW, err=True)
345
+ return
346
+ _report_fast_forward(git_run, before, _read_head(git_run))
347
+
348
+
349
+ def _pull_skip_reason(git_run: Callable[..., Any]) -> Optional[str]:
350
+ """Return the outcome text to print, or None when the pull may proceed.
351
+
352
+ An empty string marks a silent skip, because the workspace holds no safe
353
+ fast-forward and reporting it would only add noise. A dirty worktree
354
+ returns the skipped warning, because tracked changes can conflict.
355
+ """
356
+ git_dir = Path.cwd() / ".git"
357
+ if any((git_dir / marker).exists() for marker in _IN_PROGRESS_GIT_MARKERS):
358
+ return ""
359
+ detached = git_run(
360
+ ["git", "symbolic-ref", "-q", "HEAD"],
361
+ capture_output=True,
362
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
363
+ )
364
+ if detached.returncode != 0:
365
+ return ""
366
+ upstream = git_run(
367
+ ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
368
+ capture_output=True,
369
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
370
+ )
371
+ if upstream.returncode != 0:
372
+ return ""
373
+ status = git_run(
374
+ ["git", "status", "--porcelain"],
375
+ capture_output=True,
376
+ text=True,
377
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
378
+ )
379
+ tracked_changes = [
380
+ line
381
+ for line in status.stdout.splitlines()
382
+ if line and not line.startswith("??")
383
+ ]
384
+ if tracked_changes:
385
+ return _PULL_SKIPPED_WARNING
386
+ return None
387
+
388
+
389
+ def _read_head(git_run: Callable[..., Any]) -> str:
390
+ """Read the current commit id through the injected Git runner."""
391
+ return git_run(
392
+ ["git", "rev-parse", "HEAD"],
393
+ capture_output=True,
394
+ text=True,
395
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
396
+ ).stdout.strip()
397
+
398
+
399
+ def _report_fast_forward(git_run: Callable[..., Any], before: str, after: str) -> None:
400
+ """Report how many commits the fast-forward moved, or stay silent.
401
+
402
+ A pull that moves no commit has nothing to report, so it prints nothing.
403
+ """
404
+ if after == before:
405
+ return
406
+ count_output = git_run(
407
+ ["git", "rev-list", "--count", f"{before}..{after}"],
408
+ capture_output=True,
409
+ text=True,
410
+ timeout=_GIT_PULL_TIMEOUT_SECONDS,
411
+ ).stdout.strip()
412
+ commits = int(count_output) if count_output else 0
413
+ if commits <= 0:
414
+ return
415
+ noun = "commit" if commits == 1 else "commits"
416
+ typer.secho(
417
+ f"Workspace fast-forwarded ({commits} {noun})",
418
+ fg=typer.colors.GREEN,
419
+ err=True,
420
+ )
251
421
 
252
422
 
253
423
  def _start_background_update_check(container: Container) -> list[str]:
@@ -285,7 +455,7 @@ def _run_startup_preamble(
285
455
  its ``agent`` so the preflight validates the agent prompt; ``resume``
286
456
  leaves it ``None`` and validates no agent.
287
457
  """
288
- _run_health_checks()
458
+ _run_health_checks(container.resolve(IConfigService))
289
459
  typer.echo("Checking configurations...", err=True)
290
460
  fired.extend(
291
461
  _run_cli_preflight_check(
@@ -692,6 +862,7 @@ def handle_new_session( # noqa: PLR0913
692
862
  setup_editor=setup_editor,
693
863
  setup_api_key=setup_api_key,
694
864
  )
865
+ _pull_workspace_on_start(container.resolve(IConfigService))
695
866
  _echo_config_success(container, agent, model=model)
696
867
 
697
868
  session_manager: ISessionManager = container.resolve(ISessionManager)
@@ -0,0 +1,138 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ from teddy_executor.core.ports.outbound.repo_tree_generator import IRepoTreeGenerator
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ UNREADABLE_MARKER = "[... directory not expanded (unreadable): 0 folders, 0 files]"
9
+
10
+
11
+ class LocalRepoTreeGenerator(IRepoTreeGenerator):
12
+ """
13
+ An adapter that generates a file tree for the local repository,
14
+ respecting .gitignore and .teddyignore rules.
15
+ """
16
+
17
+ def __init__(self, root_dir: str = ".", max_depth: int = 0):
18
+ from teddy_executor.adapters.outbound.filesystem_helpers import (
19
+ load_ignore_spec,
20
+ )
21
+
22
+ assert max_depth >= 0, f"max_depth must be non-negative, got {max_depth}"
23
+ self.root_dir = Path(root_dir).resolve()
24
+ self.ignore_spec = load_ignore_spec(self.root_dir)
25
+ self._max_depth = max_depth
26
+
27
+ def generate_tree(self) -> str:
28
+ """
29
+ Generates a string representation of the file tree in a single pass.
30
+ """
31
+ sections: list[tuple[str, str, list[str]]] = []
32
+ root_names = self._collect_directory(self.root_dir, "", sections)
33
+ # Sections sort by their relative path, and the root resolves to ".",
34
+ # so a top-level name below "." (for example "!important") renders
35
+ # before the root block. The "." key preserves that exact order.
36
+ sections.append((".", "", root_names))
37
+ sections.sort(key=lambda item: item[0])
38
+
39
+ rendered: list[str] = []
40
+ for _sort_key, relative_path, names in sections:
41
+ if not names:
42
+ continue
43
+ if relative_path:
44
+ rendered.append("\n".join([f"./{relative_path}:"] + names))
45
+ else:
46
+ rendered.append("\n".join(names))
47
+ return "\n\n".join(rendered)
48
+
49
+ def _collect_directory(
50
+ self,
51
+ directory: str | Path,
52
+ relative_path: str,
53
+ sections: list[tuple[str, str, list[str]]],
54
+ depth: int = 0,
55
+ ) -> list[str]:
56
+ """
57
+ Collects one directory's visible names and recurses into its children.
58
+
59
+ A directory at or beyond the configured depth limit renders one
60
+ omission marker instead of expanding, so truncation is never silent.
61
+ An unreadable directory renders an explicit marker rather than an
62
+ empty section, so it never looks like a readable empty directory.
63
+ Directory and symlink flags come from the scan cache, so each entry
64
+ costs no additional stat call. Symlinks are listed as files and are
65
+ never followed, which keeps circular links from recursing.
66
+ """
67
+ if self._max_depth > 0 and depth >= self._max_depth:
68
+ marker = self._depth_limit_marker(directory, relative_path)
69
+ return [marker] if marker else []
70
+
71
+ entries = self._scan_visible_entries(directory, relative_path)
72
+ if entries is None:
73
+ return [UNREADABLE_MARKER]
74
+
75
+ names: list[str] = []
76
+ for entry, is_directory, child_path in entries:
77
+ names.append(entry.name)
78
+ if is_directory:
79
+ child_names = self._collect_directory(
80
+ entry.path, child_path, sections, depth + 1
81
+ )
82
+ sort_key = child_path.replace("/", os.sep).lower()
83
+ sections.append((sort_key, child_path, child_names))
84
+
85
+ names.sort(key=str.lower)
86
+ return names
87
+
88
+ def _scan_visible_entries(
89
+ self, directory: str | Path, relative_path: str
90
+ ) -> list[tuple[os.DirEntry, bool, str]] | None:
91
+ """
92
+ Returns one visible immediate child per entry after ignore rules.
93
+
94
+ The scan is materialized before its context closes, so each DirEntry
95
+ still answers its cached directory flag. An unreadable directory
96
+ returns ``None`` so callers can tell it apart from a readable empty
97
+ one, which returns an empty list.
98
+ """
99
+ try:
100
+ with os.scandir(directory) as iterator:
101
+ entries = list(iterator)
102
+ except PermissionError:
103
+ logger.debug("Skipped unreadable directory %s", directory, exc_info=True)
104
+ return None
105
+ visible: list[tuple[os.DirEntry, bool, str]] = []
106
+ for entry in entries:
107
+ is_directory = entry.is_dir(follow_symlinks=False)
108
+ child_path = (
109
+ f"{relative_path}/{entry.name}" if relative_path else entry.name
110
+ )
111
+ match_path = (child_path + "/") if is_directory else child_path
112
+ if self.ignore_spec.match_file(match_path):
113
+ continue
114
+ visible.append((entry, is_directory, child_path))
115
+ return visible
116
+
117
+ def _depth_limit_marker(self, directory: str | Path, relative_path: str) -> str:
118
+ """
119
+ Renders the omission marker for a directory at the depth limit.
120
+
121
+ Counts only the visible immediate children after ignore rules. An
122
+ unreadable directory renders the unreadable marker instead. Returns an
123
+ empty string when no child is visible, so a genuinely empty directory
124
+ renders no section rather than a misleading marker.
125
+ """
126
+ entries = self._scan_visible_entries(directory, relative_path)
127
+ if entries is None:
128
+ return UNREADABLE_MARKER
129
+ folders = sum(1 for _entry, is_directory, _path in entries if is_directory)
130
+ files = len(entries) - folders
131
+ if not folders and not files:
132
+ return ""
133
+ folder_noun = "folder" if folders == 1 else "folders"
134
+ file_noun = "file" if files == 1 else "files"
135
+ return (
136
+ f"[... directory not expanded (depth limit {self._max_depth}): "
137
+ f"{folders} {folder_noun}, {files} {file_noun}]"
138
+ )
@@ -5,6 +5,11 @@ from importlib import resources
5
5
  from typing import Any, Dict, Optional
6
6
  import yaml
7
7
  from dotenv import dotenv_values, set_key
8
+ from teddy_executor.core.domain.models.config_schema import (
9
+ construct_app_config,
10
+ is_declared_key,
11
+ read_declared_value,
12
+ )
8
13
  from teddy_executor.core.domain.models.exceptions import ConfigurationError
9
14
  from teddy_executor.core.ports.outbound.config_service import IConfigService
10
15
 
@@ -14,6 +19,17 @@ _INTERPOLATION_PATTERN = re.compile(
14
19
 
15
20
  _MERGE_TAG = "tag:yaml.org,2002:merge"
16
21
 
22
+ # Legacy ``execution.<key>`` names and the canonical group each one now lives in.
23
+ _LEGACY_EXECUTION_KEY_GROUPS = {
24
+ "default_timeout_seconds": "execute",
25
+ "max_output_lines": "execute",
26
+ "similarity_threshold": "edit",
27
+ }
28
+
29
+ # ``llm.*`` keys pass straight through to LiteLLM and stay on the live walk,
30
+ # because a mid-run ``.env`` write must remain visible on the same adapter.
31
+ _PASSTHROUGH_KEY_PREFIX = "llm."
32
+
17
33
 
18
34
  def _strict_load(text: str, source_path: str) -> Any:
19
35
  """Parse ``text`` and reject a repeated mapping key at any depth."""
@@ -76,6 +92,15 @@ class YamlConfigAdapter(IConfigService):
76
92
  else:
77
93
  self._config_path = config_path
78
94
  self._config: Dict[str, Any] = self._load_layered_config()
95
+ self._refresh_typed_view()
96
+
97
+ def _refresh_typed_view(self) -> None:
98
+ """Rebuild the typed view from the current merged cache.
99
+
100
+ The view is a frozen snapshot, so it must be reconstructed whenever
101
+ the merged cache changes, or a read would return a stale value.
102
+ """
103
+ self.app_config = construct_app_config(self._maybe_interpolate(self._config))
79
104
 
80
105
  def _load_layered_config(self) -> Dict[str, Any]:
81
106
  """Loads the baseline config and merges it with the user config."""
@@ -114,10 +139,35 @@ class YamlConfigAdapter(IConfigService):
114
139
  try:
115
140
  with open(self._config_path, "r", encoding="utf-8") as f:
116
141
  data = _strict_load(f.read(), self._config_path)
117
- return data if isinstance(data, dict) else {}
142
+ user_config = data if isinstance(data, dict) else {}
118
143
  except (yaml.YAMLError, OSError):
119
144
  return {}
120
145
 
146
+ self._migrate_legacy_execution_keys(user_config)
147
+ return user_config
148
+
149
+ def _migrate_legacy_execution_keys(self, user_config: Dict[str, Any]) -> None:
150
+ """Copies tuned legacy ``execution.*`` values to their canonical groups.
151
+
152
+ A user file written before the execute/edit split still holds these
153
+ keys. The copy runs on the in-memory user dict before the baseline
154
+ merge, so the canonical key resolves while the user file stays
155
+ untouched. An absent or non-mapping legacy parent is left alone, a
156
+ ``None`` value is skipped, and an existing canonical key wins.
157
+ """
158
+ legacy = user_config.get("execution")
159
+ if not isinstance(legacy, dict):
160
+ return
161
+ for legacy_key, group in _LEGACY_EXECUTION_KEY_GROUPS.items():
162
+ value = legacy.get(legacy_key)
163
+ if value is None:
164
+ continue
165
+ target = user_config.get(group)
166
+ if not isinstance(target, dict):
167
+ target = {}
168
+ user_config[group] = target
169
+ target.setdefault(legacy_key, value)
170
+
121
171
  def _merge_dicts(self, base: Dict[str, Any], overrides: Dict[str, Any]) -> None:
122
172
  """Recursively merges overrides into base. Prunes keys set to None."""
123
173
  for key, value in overrides.items():
@@ -135,15 +185,24 @@ class YamlConfigAdapter(IConfigService):
135
185
  """
136
186
  Retrieves a configuration value by its key from the loaded YAML.
137
187
  Supports nested keys using dot notation (e.g., 'outer.inner').
188
+
189
+ A schema-declared key resolves through the typed view, so its value is
190
+ coerced to the declared type. Every other key resolves through the live
191
+ dict walk, which covers undeclared keys and the ``llm.*`` passthrough.
138
192
  """
139
193
  if not key:
140
194
  return default
141
195
 
142
- # 1. Try exact match first (highest priority: top-level user overrides)
196
+ # 1. Declared keys resolve through the typed view. Passthrough keys are
197
+ # excluded because a construction-time snapshot would go stale.
198
+ if not key.startswith(_PASSTHROUGH_KEY_PREFIX) and is_declared_key(key):
199
+ return read_declared_value(self.app_config, key)
200
+
201
+ # 2. Try exact match first (highest priority: top-level user overrides)
143
202
  if key in self._config:
144
203
  return self._maybe_interpolate(self._config[key])
145
204
 
146
- # 2. Try nested resolution (standard hierarchical structure)
205
+ # 3. Try nested resolution (standard hierarchical structure)
147
206
  parts = key.split(".")
148
207
  result = self._resolve_nested(parts)
149
208
 
@@ -270,10 +329,12 @@ class YamlConfigAdapter(IConfigService):
270
329
  f.write(text)
271
330
 
272
331
  # 4. Rebuild the merged cache from disk so a subsequent get_setting()
273
- # on THIS adapter resolves exactly what a fresh adapter resolves.
332
+ # on THIS adapter resolves exactly what a fresh adapter resolves,
333
+ # then refresh the typed view so app_config reflects the write.
274
334
  merged = self._load_layered_config()
275
335
  self._config.clear()
276
336
  self._config.update(merged)
337
+ self._refresh_typed_view()
277
338
 
278
339
  def _scalar_repr(self, value: Any) -> str:
279
340
  """Render a scalar value as YAML text with no document-end marker."""