gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,346 @@
1
+ """
2
+ Prompt template loader with multi-level override support.
3
+
4
+ Implements prompt loading with precedence:
5
+ 1. Inline config (deprecated, for backwards compatibility)
6
+ 2. Config path (explicit path in config)
7
+ 3. Project file (.gobby/prompts/)
8
+ 4. Global file (~/.gobby/prompts/)
9
+ 5. Bundled default (src/gobby/prompts/defaults/)
10
+ 6. Python constant (strangler fig fallback)
11
+ """
12
+
13
+ import logging
14
+ import re
15
+ from collections.abc import Callable
16
+ from functools import lru_cache
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import yaml
21
+
22
+ from .models import PromptTemplate
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Default location for bundled prompts
27
+ DEFAULTS_DIR = Path(__file__).parent / "defaults"
28
+
29
+
30
+ class PromptLoader:
31
+ """Loads prompt templates from multiple sources with override precedence.
32
+
33
+ Usage:
34
+ loader = PromptLoader(project_dir=Path("."))
35
+ template = loader.load("expansion/system")
36
+ rendered = loader.render("expansion/system", {"tdd_mode": True})
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ project_dir: Path | None = None,
42
+ global_dir: Path | None = None,
43
+ defaults_dir: Path | None = None,
44
+ ):
45
+ """Initialize the prompt loader.
46
+
47
+ Args:
48
+ project_dir: Project root directory (for .gobby/prompts)
49
+ global_dir: Global config directory (defaults to ~/.gobby)
50
+ defaults_dir: Directory for bundled defaults (auto-detected)
51
+ """
52
+ self.project_dir = project_dir
53
+ self.global_dir = global_dir or Path.home() / ".gobby"
54
+ self.defaults_dir = defaults_dir or DEFAULTS_DIR
55
+
56
+ # Build search paths in priority order
57
+ self._search_paths: list[Path] = []
58
+ if project_dir:
59
+ self._search_paths.append(project_dir / ".gobby" / "prompts")
60
+ self._search_paths.append(self.global_dir / "prompts")
61
+ self._search_paths.append(self.defaults_dir)
62
+
63
+ # Template cache
64
+ self._cache: dict[str, PromptTemplate] = {}
65
+
66
+ # Fallback registry for strangler fig pattern
67
+ self._fallbacks: dict[str, Callable[[], str]] = {}
68
+
69
+ def register_fallback(self, path: str, getter: Callable[[], str]) -> None:
70
+ """Register a Python constant fallback for a template path.
71
+
72
+ Used for strangler fig pattern - if template file doesn't exist,
73
+ fall back to the original Python constant.
74
+
75
+ Args:
76
+ path: Template path (e.g., "expansion/system")
77
+ getter: Callable that returns the fallback string
78
+ """
79
+ self._fallbacks[path] = getter
80
+
81
+ def clear_cache(self) -> None:
82
+ """Clear the template cache."""
83
+ self._cache.clear()
84
+
85
+ def _find_template_file(self, path: str) -> Path | None:
86
+ """Find a template file in search paths.
87
+
88
+ Args:
89
+ path: Template path (e.g., "expansion/system")
90
+
91
+ Returns:
92
+ Path to template file if found, None otherwise
93
+ """
94
+ # Add .md extension if not present
95
+ if not path.endswith(".md"):
96
+ path = f"{path}.md"
97
+
98
+ for search_dir in self._search_paths:
99
+ template_path = search_dir / path
100
+ if template_path.exists():
101
+ return template_path
102
+
103
+ return None
104
+
105
+ def _parse_frontmatter(self, content: str) -> tuple[dict[str, Any], str]:
106
+ """Parse YAML frontmatter from template content.
107
+
108
+ Args:
109
+ content: Raw file content
110
+
111
+ Returns:
112
+ Tuple of (frontmatter dict, body content)
113
+ """
114
+ # Match YAML frontmatter between --- markers
115
+ frontmatter_pattern = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
116
+ match = frontmatter_pattern.match(content)
117
+
118
+ if match:
119
+ try:
120
+ frontmatter = yaml.safe_load(match.group(1)) or {}
121
+ body = content[match.end() :]
122
+ return frontmatter, body
123
+ except yaml.YAMLError as e:
124
+ logger.warning(f"Failed to parse frontmatter: {e}")
125
+ return {}, content
126
+
127
+ return {}, content
128
+
129
+ def load(self, path: str) -> PromptTemplate:
130
+ """Load a prompt template by path.
131
+
132
+ Args:
133
+ path: Template path (e.g., "expansion/system")
134
+
135
+ Returns:
136
+ PromptTemplate instance
137
+
138
+ Raises:
139
+ FileNotFoundError: If template not found and no fallback registered
140
+ """
141
+ # Check cache first
142
+ if path in self._cache:
143
+ return self._cache[path]
144
+
145
+ # Try to find template file
146
+ template_file = self._find_template_file(path)
147
+
148
+ if template_file:
149
+ content = template_file.read_text(encoding="utf-8")
150
+ frontmatter, body = self._parse_frontmatter(content)
151
+
152
+ template = PromptTemplate.from_frontmatter(
153
+ name=path,
154
+ frontmatter=frontmatter,
155
+ content=body.strip(),
156
+ source_path=template_file,
157
+ )
158
+ self._cache[path] = template
159
+ logger.debug(f"Loaded prompt template '{path}' from {template_file}")
160
+ return template
161
+
162
+ # Fall back to registered Python constant
163
+ if path in self._fallbacks:
164
+ fallback_content = self._fallbacks[path]()
165
+ template = PromptTemplate(
166
+ name=path,
167
+ description=f"Fallback for {path}",
168
+ content=fallback_content,
169
+ source_path=None,
170
+ )
171
+ self._cache[path] = template
172
+ logger.debug(f"Using fallback for prompt template '{path}'")
173
+ return template
174
+
175
+ raise FileNotFoundError(f"Prompt template not found: {path}")
176
+
177
+ def render(
178
+ self,
179
+ path: str,
180
+ context: dict[str, Any] | None = None,
181
+ strict: bool = False,
182
+ ) -> str:
183
+ """Load and render a template with context.
184
+
185
+ Args:
186
+ path: Template path
187
+ context: Variables to inject into template
188
+ strict: If True, raise on missing required variables
189
+
190
+ Returns:
191
+ Rendered template string
192
+
193
+ Raises:
194
+ FileNotFoundError: If template not found
195
+ ValueError: If strict=True and required variables missing
196
+ """
197
+ template = self.load(path)
198
+ ctx = template.get_default_context()
199
+
200
+ if context:
201
+ ctx.update(context)
202
+
203
+ # Validate required variables
204
+ if strict:
205
+ errors = template.validate_context(ctx)
206
+ if errors:
207
+ raise ValueError(f"Template validation failed: {'; '.join(errors)}")
208
+
209
+ # Render with Jinja2
210
+ return self._render_jinja(template.content, ctx)
211
+
212
+ def _render_jinja(self, template_str: str, context: dict[str, Any]) -> str:
213
+ """Render a template string with Jinja2.
214
+
215
+ Uses a safe subset of Jinja2 features.
216
+
217
+ Args:
218
+ template_str: Template content with Jinja2 syntax
219
+ context: Context dict for rendering
220
+
221
+ Returns:
222
+ Rendered string
223
+ """
224
+ try:
225
+ from jinja2 import Environment, StrictUndefined, UndefinedError
226
+
227
+ # Create a restricted Jinja2 environment
228
+ env = Environment( # nosec B701 - generating raw text prompts, not HTML
229
+ autoescape=False,
230
+ undefined=StrictUndefined,
231
+ # Disable dangerous features
232
+ extensions=[],
233
+ )
234
+
235
+ # Add safe filters
236
+ env.filters["default"] = lambda v, d="": d if v is None else v
237
+
238
+ template = env.from_string(template_str)
239
+ return template.render(**context)
240
+
241
+ except UndefinedError as e:
242
+ logger.warning(f"Template rendering error (undefined variable): {e}")
243
+ # Fall back to simple string formatting for undefined vars
244
+ return self._render_simple(template_str, context)
245
+ except ImportError:
246
+ # Jinja2 not available, use simple formatting
247
+ logger.debug("Jinja2 not available, using simple format")
248
+ return self._render_simple(template_str, context)
249
+ except Exception as e:
250
+ logger.warning(f"Template rendering error: {e}")
251
+ return self._render_simple(template_str, context)
252
+
253
+ def _render_simple(self, template_str: str, context: dict[str, Any]) -> str:
254
+ """Simple string formatting fallback.
255
+
256
+ Handles {variable} placeholders using str.format().
257
+
258
+ Args:
259
+ template_str: Template with {var} placeholders
260
+ context: Context dict
261
+
262
+ Returns:
263
+ Rendered string
264
+ """
265
+ try:
266
+ return template_str.format(**context)
267
+ except KeyError:
268
+ # Return as-is if formatting fails
269
+ return template_str
270
+
271
+ def exists(self, path: str) -> bool:
272
+ """Check if a template exists.
273
+
274
+ Args:
275
+ path: Template path
276
+
277
+ Returns:
278
+ True if template exists (file or fallback)
279
+ """
280
+ return self._find_template_file(path) is not None or path in self._fallbacks
281
+
282
+ def list_templates(self, category: str | None = None) -> list[str]:
283
+ """List available template paths.
284
+
285
+ Args:
286
+ category: Optional category to filter (e.g., "expansion")
287
+
288
+ Returns:
289
+ List of template paths
290
+ """
291
+ templates: set[str] = set()
292
+
293
+ for search_dir in self._search_paths:
294
+ if not search_dir.exists():
295
+ continue
296
+
297
+ for md_file in search_dir.rglob("*.md"):
298
+ rel_path = md_file.relative_to(search_dir)
299
+ # Remove .md extension for path
300
+ template_path = str(rel_path.with_suffix(""))
301
+
302
+ if category is None or template_path.startswith(f"{category}/"):
303
+ templates.add(template_path)
304
+
305
+ # Add registered fallbacks
306
+ for fallback_path in self._fallbacks:
307
+ if category is None or fallback_path.startswith(f"{category}/"):
308
+ templates.add(fallback_path)
309
+
310
+ return sorted(templates)
311
+
312
+
313
+ # Module-level cached loader instance
314
+ @lru_cache(maxsize=1)
315
+ def get_default_loader() -> PromptLoader:
316
+ """Get or create the default prompt loader.
317
+
318
+ Returns:
319
+ Cached PromptLoader instance
320
+ """
321
+ return PromptLoader()
322
+
323
+
324
+ def load_prompt(path: str) -> PromptTemplate:
325
+ """Convenience function to load a prompt using default loader.
326
+
327
+ Args:
328
+ path: Template path
329
+
330
+ Returns:
331
+ PromptTemplate
332
+ """
333
+ return get_default_loader().load(path)
334
+
335
+
336
+ def render_prompt(path: str, context: dict[str, Any] | None = None) -> str:
337
+ """Convenience function to render a prompt using default loader.
338
+
339
+ Args:
340
+ path: Template path
341
+ context: Variables for rendering
342
+
343
+ Returns:
344
+ Rendered string
345
+ """
346
+ return get_default_loader().render(path, context)
@@ -0,0 +1,113 @@
1
+ """
2
+ Prompt template data models.
3
+
4
+ Contains dataclasses for representing prompt templates with metadata.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any, Literal
10
+
11
+
12
+ @dataclass
13
+ class VariableSpec:
14
+ """Specification for a template variable.
15
+
16
+ Attributes:
17
+ type: Variable type (str, int, bool, list, dict)
18
+ default: Default value if not provided
19
+ description: Human-readable description
20
+ required: Whether the variable must be provided
21
+ """
22
+
23
+ type: Literal["str", "int", "bool", "list", "dict"] = "str"
24
+ default: Any = None
25
+ description: str = ""
26
+ required: bool = False
27
+
28
+
29
+ @dataclass
30
+ class PromptTemplate:
31
+ """A loaded prompt template with metadata.
32
+
33
+ Attributes:
34
+ name: Template identifier (e.g., "expansion/system")
35
+ description: Human-readable description
36
+ variables: Variable specifications from frontmatter
37
+ content: Raw template content (with Jinja2 syntax)
38
+ source_path: Path the template was loaded from (None for fallbacks)
39
+ version: Template version for compatibility checking
40
+ """
41
+
42
+ name: str
43
+ description: str = ""
44
+ variables: dict[str, VariableSpec] = field(default_factory=dict)
45
+ content: str = ""
46
+ source_path: Path | None = None
47
+ version: str = "1.0"
48
+
49
+ @classmethod
50
+ def from_frontmatter(
51
+ cls,
52
+ name: str,
53
+ frontmatter: dict[str, Any],
54
+ content: str,
55
+ source_path: Path | None = None,
56
+ ) -> "PromptTemplate":
57
+ """Create a PromptTemplate from parsed frontmatter.
58
+
59
+ Args:
60
+ name: Template name/path
61
+ frontmatter: Parsed YAML frontmatter
62
+ content: Template content after frontmatter
63
+ source_path: Source file path
64
+
65
+ Returns:
66
+ PromptTemplate instance
67
+ """
68
+ # Parse variables from frontmatter
69
+ variables: dict[str, VariableSpec] = {}
70
+ if "variables" in frontmatter:
71
+ for var_name, var_spec in frontmatter["variables"].items():
72
+ if isinstance(var_spec, dict):
73
+ variables[var_name] = VariableSpec(
74
+ type=var_spec.get("type", "str"),
75
+ default=var_spec.get("default"),
76
+ description=var_spec.get("description", ""),
77
+ required=var_spec.get("required", False),
78
+ )
79
+ else:
80
+ # Simple form: just a default value
81
+ variables[var_name] = VariableSpec(default=var_spec)
82
+
83
+ return cls(
84
+ name=name,
85
+ description=frontmatter.get("description", ""),
86
+ variables=variables,
87
+ content=content,
88
+ source_path=source_path,
89
+ version=frontmatter.get("version", "1.0"),
90
+ )
91
+
92
+ def get_default_context(self) -> dict[str, Any]:
93
+ """Get default values for all variables.
94
+
95
+ Returns:
96
+ Dict of variable names to their default values
97
+ """
98
+ return {name: spec.default for name, spec in self.variables.items()}
99
+
100
+ def validate_context(self, context: dict[str, Any]) -> list[str]:
101
+ """Validate that required variables are provided.
102
+
103
+ Args:
104
+ context: Context dict being passed to render
105
+
106
+ Returns:
107
+ List of error messages (empty if valid)
108
+ """
109
+ errors: list[str] = []
110
+ for name, spec in self.variables.items():
111
+ if spec.required and name not in context:
112
+ errors.append(f"Required variable '{name}' not provided")
113
+ return errors
gobby/py.typed ADDED
File without changes