teddy-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. teddy_cli-0.1.0.dist-info/LICENSE +677 -0
  2. teddy_cli-0.1.0.dist-info/METADATA +33 -0
  3. teddy_cli-0.1.0.dist-info/RECORD +143 -0
  4. teddy_cli-0.1.0.dist-info/WHEEL +4 -0
  5. teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
  6. teddy_executor/__init__.py +1 -0
  7. teddy_executor/__main__.py +335 -0
  8. teddy_executor/adapters/__init__.py +0 -0
  9. teddy_executor/adapters/inbound/__init__.py +0 -0
  10. teddy_executor/adapters/inbound/cli_formatter.py +107 -0
  11. teddy_executor/adapters/inbound/cli_helpers.py +249 -0
  12. teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
  13. teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
  14. teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
  15. teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
  16. teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
  17. teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
  18. teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
  19. teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
  20. teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
  21. teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
  22. teddy_executor/adapters/outbound/__init__.py +7 -0
  23. teddy_executor/adapters/outbound/console_interactor.py +212 -0
  24. teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
  25. teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
  26. teddy_executor/adapters/outbound/console_tooling.py +62 -0
  27. teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
  28. teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
  29. teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
  30. teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
  31. teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
  32. teddy_executor/adapters/outbound/shell_adapter.py +344 -0
  33. teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
  34. teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
  35. teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
  36. teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
  37. teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
  38. teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
  39. teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
  40. teddy_executor/container.py +333 -0
  41. teddy_executor/core/__init__.py +0 -0
  42. teddy_executor/core/domain/__init__.py +0 -0
  43. teddy_executor/core/domain/models/__init__.py +44 -0
  44. teddy_executor/core/domain/models/action_ports.py +28 -0
  45. teddy_executor/core/domain/models/change_set.py +10 -0
  46. teddy_executor/core/domain/models/exceptions.py +40 -0
  47. teddy_executor/core/domain/models/execution_report.py +65 -0
  48. teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
  49. teddy_executor/core/domain/models/plan.py +85 -0
  50. teddy_executor/core/domain/models/planning_ports.py +43 -0
  51. teddy_executor/core/domain/models/project_context.py +56 -0
  52. teddy_executor/core/domain/models/report_assembly_data.py +18 -0
  53. teddy_executor/core/domain/models/session.py +17 -0
  54. teddy_executor/core/domain/models/shell_output.py +12 -0
  55. teddy_executor/core/domain/models/web_search_results.py +26 -0
  56. teddy_executor/core/ports/__init__.py +0 -0
  57. teddy_executor/core/ports/inbound/__init__.py +0 -0
  58. teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
  59. teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
  60. teddy_executor/core/ports/inbound/init.py +15 -0
  61. teddy_executor/core/ports/inbound/plan_parser.py +52 -0
  62. teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
  63. teddy_executor/core/ports/inbound/plan_validator.py +26 -0
  64. teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
  65. teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
  66. teddy_executor/core/ports/outbound/__init__.py +34 -0
  67. teddy_executor/core/ports/outbound/config_service.py +29 -0
  68. teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
  69. teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
  70. teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
  71. teddy_executor/core/ports/outbound/llm_client.py +90 -0
  72. teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
  73. teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
  74. teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
  75. teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
  76. teddy_executor/core/ports/outbound/session_manager.py +97 -0
  77. teddy_executor/core/ports/outbound/session_repository.py +65 -0
  78. teddy_executor/core/ports/outbound/shell_executor.py +24 -0
  79. teddy_executor/core/ports/outbound/system_environment.py +25 -0
  80. teddy_executor/core/ports/outbound/time_service.py +28 -0
  81. teddy_executor/core/ports/outbound/user_interactor.py +126 -0
  82. teddy_executor/core/ports/outbound/web_scraper.py +24 -0
  83. teddy_executor/core/ports/outbound/web_searcher.py +25 -0
  84. teddy_executor/core/services/__init__.py +0 -0
  85. teddy_executor/core/services/action_changeset_builder.py +90 -0
  86. teddy_executor/core/services/action_diff_manager.py +110 -0
  87. teddy_executor/core/services/action_dispatcher.py +142 -0
  88. teddy_executor/core/services/action_executor.py +209 -0
  89. teddy_executor/core/services/action_factory.py +197 -0
  90. teddy_executor/core/services/action_parser_complex.py +216 -0
  91. teddy_executor/core/services/action_parser_strategies.py +84 -0
  92. teddy_executor/core/services/context_service.py +437 -0
  93. teddy_executor/core/services/edit_simulator.py +128 -0
  94. teddy_executor/core/services/execution_orchestrator.py +295 -0
  95. teddy_executor/core/services/execution_report_assembler.py +62 -0
  96. teddy_executor/core/services/init_service.py +80 -0
  97. teddy_executor/core/services/markdown_plan_parser.py +309 -0
  98. teddy_executor/core/services/markdown_report_formatter.py +143 -0
  99. teddy_executor/core/services/parser_infrastructure.py +222 -0
  100. teddy_executor/core/services/parser_metadata.py +153 -0
  101. teddy_executor/core/services/parser_reporting.py +267 -0
  102. teddy_executor/core/services/plan_validator.py +82 -0
  103. teddy_executor/core/services/planning_service.py +242 -0
  104. teddy_executor/core/services/prompt_manager.py +146 -0
  105. teddy_executor/core/services/session_lifecycle_manager.py +228 -0
  106. teddy_executor/core/services/session_loop_guard.py +46 -0
  107. teddy_executor/core/services/session_orchestrator.py +538 -0
  108. teddy_executor/core/services/session_planner.py +43 -0
  109. teddy_executor/core/services/session_pruning_service.py +438 -0
  110. teddy_executor/core/services/session_replanner.py +105 -0
  111. teddy_executor/core/services/session_repository.py +194 -0
  112. teddy_executor/core/services/session_service.py +529 -0
  113. teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
  114. teddy_executor/core/services/validation_rules/__init__.py +4 -0
  115. teddy_executor/core/services/validation_rules/edit.py +207 -0
  116. teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
  117. teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
  118. teddy_executor/core/services/validation_rules/execute.py +37 -0
  119. teddy_executor/core/services/validation_rules/filesystem.py +73 -0
  120. teddy_executor/core/services/validation_rules/helpers.py +178 -0
  121. teddy_executor/core/services/validation_rules/message.py +29 -0
  122. teddy_executor/core/utils/__init__.py +1 -0
  123. teddy_executor/core/utils/diff.py +57 -0
  124. teddy_executor/core/utils/io.py +75 -0
  125. teddy_executor/core/utils/markdown.py +131 -0
  126. teddy_executor/core/utils/serialization.py +39 -0
  127. teddy_executor/core/utils/string.py +351 -0
  128. teddy_executor/prompts.py +45 -0
  129. teddy_executor/registries/__init__.py +1 -0
  130. teddy_executor/registries/infrastructure.py +147 -0
  131. teddy_executor/registries/reviewer.py +57 -0
  132. teddy_executor/registries/validators.py +47 -0
  133. teddy_executor/resources/__init__.py +1 -0
  134. teddy_executor/resources/config/.gitignore +2 -0
  135. teddy_executor/resources/config/__init__.py +1 -0
  136. teddy_executor/resources/config/config.yaml +49 -0
  137. teddy_executor/resources/config/init.context +5 -0
  138. teddy_executor/resources/config/prompts/architect.xml +462 -0
  139. teddy_executor/resources/config/prompts/assistant.xml +336 -0
  140. teddy_executor/resources/config/prompts/debugger.xml +456 -0
  141. teddy_executor/resources/config/prompts/developer.xml +481 -0
  142. teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
  143. teddy_executor/resources/config/prompts/prototyper.xml +425 -0
@@ -0,0 +1,295 @@
1
+ import logging
2
+ from datetime import datetime
3
+
4
+ from typing import Any, Optional
5
+
6
+ from teddy_executor.core.domain.models import (
7
+ ActionData,
8
+ ActionLog,
9
+ ExecutionReport,
10
+ Plan,
11
+ ReportAssemblyData,
12
+ ActionStatus,
13
+ )
14
+ from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
15
+ from teddy_executor.core.ports.inbound.run_plan_use_case import IRunPlanUseCase
16
+ from teddy_executor.core.domain.models.orchestrator_ports import OrchestratorPorts
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ExecutionOrchestrator(IRunPlanUseCase):
22
+ def __init__(
23
+ self,
24
+ ports: OrchestratorPorts,
25
+ ):
26
+ self._plan_parser = ports.plan_parser
27
+ self._plan_validator = ports.plan_validator
28
+ self._action_executor = ports.action_executor
29
+ self._file_system_manager = ports.file_system_manager
30
+ self._report_assembler = ports.report_assembler
31
+ self._user_interactor = ports.user_interactor
32
+ self._plan_reviewer = ports.plan_reviewer
33
+
34
+ def _perform_interactive_review(
35
+ self,
36
+ plan: Plan,
37
+ interactive: bool,
38
+ project_context: Optional[Any] = None,
39
+ ) -> Plan | None:
40
+ """Allows the user to review and modify the plan before execution."""
41
+ # Scenario: Communication Turns (MESSAGE) bypass TUI
42
+ # Single-action communication turns bypass approval for fluid conversation.
43
+ if plan.is_communication_turn():
44
+ return plan
45
+
46
+ # We only call the bulk review (TUI) if interactive is True AND a reviewer is present.
47
+ if interactive and self._plan_reviewer:
48
+ reviewed_plan = self._plan_reviewer.review(
49
+ plan, project_context=project_context
50
+ )
51
+ # Harden against Mocks in tests: if it's not a Plan or None, use the original plan
52
+ if reviewed_plan is not None and not isinstance(reviewed_plan, Plan):
53
+ return plan
54
+ return reviewed_plan
55
+ return plan
56
+
57
+ def _process_plan_actions(self, plan: Plan, interactive: bool) -> list[ActionLog]:
58
+ """Iterates through actions and dispatches them."""
59
+ # Reset file hashes at the start of each plan execution to prevent
60
+ # stale hashes from previous turns from causing false pre-check failures.
61
+ self._action_executor.reset_file_hashes()
62
+
63
+ action_logs = []
64
+ halt_execution = False
65
+ for action in plan.actions:
66
+ action_log, should_halt = self._handle_action_in_loop(
67
+ action, plan, interactive, halt_execution
68
+ )
69
+ action_logs.append(action_log)
70
+ if should_halt:
71
+ halt_execution = True
72
+ return action_logs
73
+
74
+ def _handle_action_in_loop(
75
+ self, action: ActionData, plan: Plan, interactive: bool, halt_execution: bool
76
+ ) -> tuple[ActionLog, bool]:
77
+ """Logic for processing a single action within the execution loop."""
78
+ if halt_execution:
79
+ return (
80
+ self._action_executor.handle_skipped_action(
81
+ action,
82
+ "Skipped because a previous action failed. (Hint: use 'Allow Failure: true' in EXECUTE actions to proceed even with non-zero exit codes.)",
83
+ ),
84
+ True,
85
+ )
86
+
87
+ if action.executed and action.action_log:
88
+ should_halt = (
89
+ action.action_log.status == ActionStatus.FAILURE
90
+ and not action.params.get("allow_failure")
91
+ )
92
+ return action.action_log, should_halt
93
+
94
+ if not action.selected:
95
+ reason = "User deselected this action in the plan reviewer."
96
+ return (
97
+ self._action_executor.handle_skipped_action(action, reason),
98
+ False,
99
+ )
100
+
101
+ try:
102
+ action_log, captured_message = self._dispatch_single_action(
103
+ action, plan, interactive
104
+ )
105
+ except Exception as e:
106
+ action_log = self._action_executor.handle_failed_action(action, str(e))
107
+ captured_message = ""
108
+
109
+ if captured_message:
110
+ plan.metadata["user_request"] = captured_message
111
+
112
+ should_halt = (
113
+ action_log.status == ActionStatus.FAILURE
114
+ and not action.params.get("allow_failure")
115
+ )
116
+ return action_log, should_halt
117
+
118
+ def _dispatch_single_action(
119
+ self, action: Any, plan: Plan, interactive: bool
120
+ ) -> tuple[ActionLog, str]:
121
+ """Handles the review and dispatch of a single action."""
122
+ agent_name = plan.metadata.get("Agent") or plan.metadata.get("agent")
123
+ reviewer_handled = False
124
+ should_dispatch = True
125
+ captured_message = ""
126
+
127
+ # Scenario: Communication Turns (MESSAGE) bypass confirmation
128
+ # Single communication actions bypass approval for fluid conversation.
129
+ is_communication_action = plan.is_communication_turn()
130
+
131
+ if interactive and self._plan_reviewer and not is_communication_action:
132
+ should_dispatch, captured_message = self._plan_reviewer.review_action(
133
+ action, len(plan.actions), agent_name=agent_name
134
+ )
135
+ reviewer_handled = True
136
+
137
+ if not should_dispatch:
138
+ reason = "User skipped this action in the plan reviewer."
139
+ return (
140
+ self._action_executor.handle_skipped_action(action, reason),
141
+ "",
142
+ )
143
+
144
+ if reviewer_handled:
145
+ # Reviewer (TUI) handled approval, execute immediately skipping isolation.
146
+ action_log, dispatch_message = self._action_executor.confirm_and_dispatch(
147
+ action,
148
+ interactive=False,
149
+ total_actions=len(plan.actions),
150
+ agent_name=agent_name,
151
+ is_session=plan.is_session,
152
+ skip_isolation=True,
153
+ )
154
+ return action_log, dispatch_message or captured_message
155
+
156
+ # Fallback to ActionExecutor interaction if no reviewer is present.
157
+ return self._action_executor.confirm_and_dispatch(
158
+ action,
159
+ interactive=interactive,
160
+ total_actions=len(plan.actions),
161
+ agent_name=agent_name,
162
+ is_session=plan.is_session,
163
+ )
164
+
165
+ def _resolve_plan(
166
+ self,
167
+ plan: Optional[Plan],
168
+ plan_content: Optional[str],
169
+ plan_path: Optional[str],
170
+ ) -> tuple[Plan, Optional[str]]:
171
+ """Resolves the plan from content, path, or object, creating a temp file if needed."""
172
+ import tempfile
173
+
174
+ if plan:
175
+ return plan, None
176
+
177
+ temp_path = None
178
+ if plan_content is not None:
179
+ if not plan_path:
180
+ import os
181
+
182
+ fd, temp_path = tempfile.mkstemp(
183
+ prefix="teddy_manual_plan_", suffix=".md"
184
+ )
185
+ os.close(fd)
186
+ self._file_system_manager.write_file(temp_path, plan_content)
187
+ plan_path = temp_path
188
+ return (
189
+ self._plan_parser.parse(plan_content, plan_path=plan_path),
190
+ temp_path,
191
+ )
192
+
193
+ if plan_path is not None:
194
+ content = self._file_system_manager.read_file(plan_path)
195
+ return self._plan_parser.parse(content, plan_path=plan_path), None
196
+
197
+ raise ValueError("Must provide either plan, plan_content, or plan_path")
198
+
199
+ def _handle_aborted_execution(
200
+ self, plan: Plan, start_time: datetime, message: Optional[str]
201
+ ) -> ExecutionReport:
202
+ """Generates a report for an execution aborted by the user."""
203
+ from dataclasses import replace
204
+ from teddy_executor.core.domain.models import RunStatus
205
+
206
+ # If a message was captured in the TUI (via 'm' key), propagate it.
207
+ resolved_message = message or plan.metadata.get("user_request")
208
+
209
+ action_logs = []
210
+ for a in plan.actions:
211
+ if a.executed and a.action_log:
212
+ action_logs.append(a.action_log)
213
+ else:
214
+ action_logs.append(
215
+ self._action_executor.handle_skipped_action(
216
+ a, "Execution aborted by user."
217
+ )
218
+ )
219
+
220
+ report = self._report_assembler.assemble(
221
+ ReportAssemblyData(
222
+ plan=plan,
223
+ action_logs=action_logs,
224
+ start_time=start_time,
225
+ message=resolved_message,
226
+ )
227
+ )
228
+ return replace(
229
+ report, run_summary=replace(report.run_summary, status=RunStatus.ABORTED)
230
+ )
231
+
232
+ def execute( # noqa: PLR0913
233
+ self,
234
+ plan: Optional[Plan] = None,
235
+ plan_content: Optional[str] = None,
236
+ plan_path: Optional[str] = None,
237
+ interactive: bool = True,
238
+ message: Optional[str] = None,
239
+ project_context: Optional[Any] = None,
240
+ ) -> ExecutionReport:
241
+ import os
242
+
243
+ temp_plan_path = None
244
+ try:
245
+ plan, temp_plan_path = self._resolve_plan(plan, plan_content, plan_path)
246
+ start_time = datetime.now()
247
+
248
+ validation_errors = self._plan_validator.validate(plan)
249
+ if validation_errors:
250
+ error_msgs = "\n---\n".join(e.message for e in validation_errors)
251
+ raise InvalidPlanError(
252
+ f"Plan failed logical validation:\n{error_msgs}",
253
+ offending_nodes=[
254
+ e.offending_node for e in validation_errors if e.offending_node
255
+ ],
256
+ validation_errors=validation_errors,
257
+ )
258
+
259
+ reviewed_plan = self._perform_interactive_review(
260
+ plan, interactive, project_context=project_context
261
+ )
262
+ if reviewed_plan is None:
263
+ return self._handle_aborted_execution(plan, start_time, message)
264
+
265
+ action_logs = self._process_plan_actions(reviewed_plan, interactive)
266
+ return self._report_assembler.assemble(
267
+ ReportAssemblyData(
268
+ plan=reviewed_plan,
269
+ action_logs=action_logs,
270
+ start_time=start_time,
271
+ message=message,
272
+ is_session=reviewed_plan.is_session,
273
+ )
274
+ )
275
+ finally:
276
+ if temp_plan_path and os.path.exists(temp_plan_path):
277
+ try:
278
+ os.remove(temp_plan_path)
279
+ except Exception as e:
280
+ logger.debug(
281
+ "Failed to clean up temporary plan file %s: %s",
282
+ temp_plan_path,
283
+ e,
284
+ )
285
+
286
+ def resume(
287
+ self,
288
+ session_name: str,
289
+ interactive: bool = True,
290
+ project_context: Optional[Any] = None,
291
+ ) -> tuple[str, Optional[ExecutionReport]]:
292
+ """Stateless orchestrator does not support session resumption."""
293
+ raise NotImplementedError(
294
+ "Session operations are not supported in stateless ExecutionOrchestrator."
295
+ )
@@ -0,0 +1,62 @@
1
+ from datetime import datetime
2
+ from typing import Sequence
3
+
4
+ from teddy_executor.core.domain.models import (
5
+ ActionLog,
6
+ ActionStatus,
7
+ ExecutionReport,
8
+ ReportAssemblyData,
9
+ RunStatus,
10
+ RunSummary,
11
+ )
12
+ from teddy_executor.core.ports.outbound.execution_report_assembler import (
13
+ IExecutionReportAssembler,
14
+ )
15
+
16
+
17
+ class ExecutionReportAssembler(IExecutionReportAssembler):
18
+ """
19
+ Concrete implementation of the report assembly logic.
20
+ """
21
+
22
+ def assemble(
23
+ self,
24
+ data: ReportAssemblyData,
25
+ ) -> ExecutionReport:
26
+ """
27
+ Calculates the final run status and constructs a complete ExecutionReport.
28
+ """
29
+ summary = RunSummary(
30
+ status=self._determine_overall_status(data.action_logs),
31
+ start_time=data.start_time,
32
+ end_time=datetime.now(),
33
+ )
34
+ return ExecutionReport(
35
+ run_summary=summary,
36
+ plan_title=data.plan.title,
37
+ rationale=data.plan.rationale,
38
+ user_request=data.message or data.plan.metadata.get("user_request"),
39
+ is_session=data.is_session or data.plan.is_session,
40
+ metadata=data.plan.metadata,
41
+ original_actions=data.plan.actions,
42
+ action_logs=data.action_logs,
43
+ )
44
+
45
+ def _determine_overall_status(self, action_logs: Sequence[ActionLog]) -> RunStatus:
46
+ """Determines the final run status based on the hierarchy of action outcomes."""
47
+ if not action_logs:
48
+ return RunStatus.SUCCESS
49
+
50
+ statuses = [log.status for log in action_logs]
51
+ if ActionStatus.FAILURE in statuses:
52
+ return RunStatus.FAILURE
53
+
54
+ # Success takes precedence: if any action succeeded, the run is a success.
55
+ if ActionStatus.SUCCESS in statuses:
56
+ return RunStatus.SUCCESS
57
+
58
+ # If every single action was skipped, the run is skipped.
59
+ if statuses and all(s == ActionStatus.SKIPPED for s in statuses):
60
+ return RunStatus.SKIPPED
61
+
62
+ return RunStatus.SUCCESS
@@ -0,0 +1,80 @@
1
+ import os
2
+ from importlib import resources
3
+ from teddy_executor.core.ports.inbound.init import IInitUseCase
4
+ from teddy_executor.core.ports.outbound.file_system_manager import IFileSystemManager
5
+
6
+
7
+ class InitService(IInitUseCase):
8
+ """
9
+ Service for initializing projects.
10
+ """
11
+
12
+ def __init__(self, file_system: IFileSystemManager, config_dir: str | None = None):
13
+ self._file_system = file_system
14
+ # Find the config directory relative to the package root if not provided
15
+ if config_dir:
16
+ self._config_dir = config_dir
17
+ else:
18
+ # Use importlib.resources to find the bundled config templates
19
+ resource_path = resources.files("teddy_executor.resources.config")
20
+ # Ensure we resolve to an absolute string for compatibility with the FileSystem port
21
+ self._config_dir = os.path.abspath(str(resource_path))
22
+
23
+ def _get_default_content(self, filename: str) -> str | None:
24
+ """Loads default content from the config directory using the file system port."""
25
+ try:
26
+ target_path = os.path.join(self._config_dir, filename)
27
+ if self._file_system.path_exists(target_path):
28
+ return self._file_system.read_file(target_path)
29
+ except Exception: # nosec B110
30
+ pass
31
+
32
+ return None
33
+
34
+ def _init_prompts(self) -> None:
35
+ """Copies bundled prompt XMLs to .teddy/prompts/ if missing."""
36
+ prompts_dir = ".teddy/prompts"
37
+ if not self._file_system.path_exists(prompts_dir):
38
+ self._file_system.create_directory(prompts_dir)
39
+
40
+ prompt_files = [
41
+ "architect.xml",
42
+ "assistant.xml",
43
+ "debugger.xml",
44
+ "developer.xml",
45
+ "pathfinder.xml",
46
+ "prototyper.xml",
47
+ ]
48
+ for fname in prompt_files:
49
+ target_path = f"{prompts_dir}/{fname}"
50
+ if not self._file_system.path_exists(target_path):
51
+ content = self._get_default_content(f"prompts/{fname}")
52
+ if content is not None:
53
+ self._file_system.write_file(target_path, content)
54
+
55
+ def ensure_initialized(self) -> None:
56
+ """
57
+ Checks for and creates the .teddy directory and default files.
58
+ """
59
+ if not self._file_system.path_exists(".teddy"):
60
+ self._file_system.create_directory(".teddy")
61
+
62
+ gitignore_path = ".teddy/.gitignore"
63
+ if not self._file_system.path_exists(gitignore_path):
64
+ content = self._get_default_content(".gitignore")
65
+ if content is not None:
66
+ self._file_system.write_file(gitignore_path, content)
67
+
68
+ config_path = ".teddy/config.yaml"
69
+ if not self._file_system.path_exists(config_path):
70
+ content = self._get_default_content("config.yaml")
71
+ if content is not None:
72
+ self._file_system.write_file(config_path, content)
73
+
74
+ init_context_path = ".teddy/init.context"
75
+ if not self._file_system.path_exists(init_context_path):
76
+ content = self._get_default_content("init.context")
77
+ if content is not None:
78
+ self._file_system.write_file(init_context_path, content)
79
+
80
+ self._init_prompts()