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.
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/PKG-INFO +1 -1
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/pyproject.toml +1 -1
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/__main__.py +17 -2
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/session_cli_handlers.py +176 -5
- teddy_cli-0.1.17.dev1402/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +138 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/yaml_config_adapter.py +65 -4
- teddy_cli-0.1.17.dev1402/src/teddy_executor/core/domain/models/config_schema.py +356 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_factory.py +2 -2
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/helpers.py +2 -4
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/infrastructure.py +6 -2
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/config.yaml +13 -3
- teddy_cli-0.1.17.dev1400/src/teddy_executor/adapters/outbound/local_repo_tree_generator.py +0 -81
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/.gitignore +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/LICENSE +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/README.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/cli_formatter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/cli_helpers.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor_ask_loop.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_interactor_helpers.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/console_tooling.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/filesystem_helpers.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/litellm_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/local_file_system_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/openrouter_hydrator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/shell_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/shell_command_builder.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/suppressed_logging.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_environment_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_environment_inspector.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/system_time_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/terminal_quit_key_listener.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/web_scraper_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/adapters/outbound/web_searcher_adapter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/container.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/action_ports.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/change_set.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/drift_report.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/exceptions.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/execution_report.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/orchestrator_ports.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/plan.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/planning_ports.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/project_context.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/report_assembly_data.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/session.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/shell_output.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/domain/models/web_search_results.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/edit_simulator.py +0 -0
- {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
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/init.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_parser.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_reviewer.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/plan_validator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/inbound/planning_use_case.py +0 -0
- {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
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/config_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/environment_inspector.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/execution_report_assembler.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/file_system_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/llm_client.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/markdown_report_formatter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/prompt_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/quit_key_listener.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/repo_tree_generator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_loop_guard.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/session_repository.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/shell_executor.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/system_environment.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/time_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/user_interactor.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/web_scraper.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/ports/outbound/web_searcher.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_changeset_builder.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_diff_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_dispatcher.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_executor.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_parser_complex.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/action_parser_strategies.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/context_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/edit_simulator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/execution_orchestrator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/execution_report_assembler.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/init_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/markdown_plan_parser.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/markdown_report_formatter.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_infrastructure.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_metadata.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/parser_reporting.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/plan_validator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/planning_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/prompt_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_lifecycle_manager.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_loop_guard.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_orchestrator.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_planner.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_pruning_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_replanner.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_repository.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/session_service.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/templates/execution_report.md.j2 +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/update_checker.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/edit.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/edit_matcher.py +0 -0
- {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
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/execute.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/filesystem.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/services/validation_rules/message.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/diff.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/editor_names.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/interrupt_guard.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/interruptible_call.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/io.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/markdown.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/paths.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/serialization.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/session_notices.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/stdin_ownership.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/string.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/core/utils/terminal.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/prompts.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/reviewer.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/registries/validators.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/MRP.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/__init__.py +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/init.context +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/architect.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/assistant.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/debugger.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/developer.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/pathfinder.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/config/prompts/prototyper.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/ARCHITECTURE.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/PROJECT.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/case-file.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/ci.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/component-design.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/makefile.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/milestone.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/pre-commit.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/specification-document.md +0 -0
- {teddy_cli-0.1.17.dev1400 → teddy_cli-0.1.17.dev1402}/src/teddy_executor/resources/templates/vertical-slice.md +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
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."""
|