emdash-core 0.1.7__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 (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,153 @@
1
+ """Agent mode management tools.
2
+
3
+ Provides tools for switching between different agent modes
4
+ (exploration, planning, coding, etc).
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Optional
10
+
11
+ from .base import BaseTool, ToolResult, ToolCategory
12
+
13
+
14
+ class AgentMode(Enum):
15
+ """Available agent modes."""
16
+
17
+ EXPLORATION = "exploration"
18
+ PLANNING = "planning"
19
+ CODING = "coding"
20
+ REVIEW = "review"
21
+ DEBUG = "debug"
22
+
23
+
24
+ @dataclass
25
+ class ModeState:
26
+ """Singleton state for agent mode."""
27
+
28
+ current_mode: AgentMode = AgentMode.EXPLORATION
29
+ mode_context: dict = None
30
+
31
+ _instance: Optional["ModeState"] = None
32
+
33
+ def __post_init__(self):
34
+ if self.mode_context is None:
35
+ self.mode_context = {}
36
+
37
+ @classmethod
38
+ def get_instance(cls) -> "ModeState":
39
+ """Get the singleton instance."""
40
+ if cls._instance is None:
41
+ cls._instance = cls()
42
+ return cls._instance
43
+
44
+ @classmethod
45
+ def reset(cls) -> None:
46
+ """Reset the singleton instance."""
47
+ cls._instance = None
48
+
49
+
50
+ class SwitchModeTool(BaseTool):
51
+ """Tool for switching between agent modes."""
52
+
53
+ name = "switch_mode"
54
+ description = """Switch the agent to a different operating mode.
55
+
56
+ Modes:
57
+ - exploration: General codebase exploration and understanding
58
+ - planning: Feature planning and specification writing
59
+ - coding: Active code editing and implementation
60
+ - review: Code review and feedback
61
+ - debug: Debugging and issue investigation"""
62
+ category = ToolCategory.PLANNING
63
+
64
+ def __init__(self, connection=None):
65
+ """Initialize without requiring connection."""
66
+ self.connection = connection
67
+
68
+ def execute(
69
+ self,
70
+ mode: str,
71
+ context: Optional[str] = None,
72
+ ) -> ToolResult:
73
+ """Switch to a new mode.
74
+
75
+ Args:
76
+ mode: Target mode name
77
+ context: Optional context for the new mode
78
+
79
+ Returns:
80
+ ToolResult indicating success
81
+ """
82
+ try:
83
+ new_mode = AgentMode(mode.lower())
84
+ except ValueError:
85
+ valid_modes = [m.value for m in AgentMode]
86
+ return ToolResult.error_result(
87
+ f"Invalid mode: {mode}",
88
+ suggestions=[f"Valid modes: {', '.join(valid_modes)}"],
89
+ )
90
+
91
+ state = ModeState.get_instance()
92
+ old_mode = state.current_mode
93
+ state.current_mode = new_mode
94
+
95
+ if context:
96
+ state.mode_context[new_mode.value] = context
97
+
98
+ return ToolResult.success_result(
99
+ data={
100
+ "previous_mode": old_mode.value,
101
+ "current_mode": new_mode.value,
102
+ "context": context,
103
+ },
104
+ )
105
+
106
+ def get_schema(self) -> dict:
107
+ """Get OpenAI function schema."""
108
+ return self._make_schema(
109
+ properties={
110
+ "mode": {
111
+ "type": "string",
112
+ "enum": [m.value for m in AgentMode],
113
+ "description": "Target mode to switch to",
114
+ },
115
+ "context": {
116
+ "type": "string",
117
+ "description": "Optional context for the new mode",
118
+ },
119
+ },
120
+ required=["mode"],
121
+ )
122
+
123
+
124
+ class GetModeTool(BaseTool):
125
+ """Tool for getting current agent mode."""
126
+
127
+ name = "get_mode"
128
+ description = "Get the current agent operating mode and its context."
129
+ category = ToolCategory.PLANNING
130
+
131
+ def __init__(self, connection=None):
132
+ """Initialize without requiring connection."""
133
+ self.connection = connection
134
+
135
+ def execute(self) -> ToolResult:
136
+ """Get current mode.
137
+
138
+ Returns:
139
+ ToolResult with current mode info
140
+ """
141
+ state = ModeState.get_instance()
142
+
143
+ return ToolResult.success_result(
144
+ data={
145
+ "current_mode": state.current_mode.value,
146
+ "context": state.mode_context.get(state.current_mode.value),
147
+ "available_modes": [m.value for m in AgentMode],
148
+ },
149
+ )
150
+
151
+ def get_schema(self) -> dict:
152
+ """Get OpenAI function schema."""
153
+ return self._make_schema(properties={}, required=[])
@@ -0,0 +1,206 @@
1
+ """Planning tool for exploration strategy."""
2
+
3
+ from typing import Optional
4
+
5
+ from .base import BaseTool, ToolResult, ToolCategory
6
+ from ...utils.logger import log
7
+
8
+
9
+ # Exploration strategies
10
+ STRATEGIES = {
11
+ "understand_feature": {
12
+ "description": "Understand how a feature works",
13
+ "steps": [
14
+ {"tool": "semantic_search", "purpose": "Find entry points"},
15
+ {"tool": "expand_node", "purpose": "Understand main components"},
16
+ {"tool": "get_callers", "purpose": "See usage patterns"},
17
+ {"tool": "get_file_dependencies", "purpose": "Map module structure"},
18
+ ],
19
+ },
20
+ "debug_issue": {
21
+ "description": "Debug a bug or issue",
22
+ "steps": [
23
+ {"tool": "grep", "purpose": "Find error messages or symptoms"},
24
+ {"tool": "semantic_search", "purpose": "Find related code"},
25
+ {"tool": "get_callees", "purpose": "Trace execution path"},
26
+ {"tool": "expand_node", "purpose": "Examine suspicious code"},
27
+ ],
28
+ },
29
+ "assess_change_impact": {
30
+ "description": "Assess impact of a proposed change",
31
+ "steps": [
32
+ {"tool": "get_callers", "purpose": "Find all usages"},
33
+ {"tool": "get_impact_analysis", "purpose": "Assess blast radius"},
34
+ {"tool": "get_file_dependencies", "purpose": "Find affected modules"},
35
+ {"tool": "get_class_hierarchy", "purpose": "Check inheritance impact"},
36
+ ],
37
+ },
38
+ "onboard_codebase": {
39
+ "description": "Get oriented in a new codebase",
40
+ "steps": [
41
+ {"tool": "get_communities", "purpose": "Understand major areas"},
42
+ {"tool": "get_top_pagerank", "purpose": "Find key components"},
43
+ {"tool": "get_area_importance", "purpose": "Map directory structure"},
44
+ {"tool": "semantic_search", "purpose": "Explore specific topics"},
45
+ ],
46
+ },
47
+ "find_similar_code": {
48
+ "description": "Find code similar to a reference",
49
+ "steps": [
50
+ {"tool": "expand_node", "purpose": "Understand the reference"},
51
+ {"tool": "semantic_search", "purpose": "Find similar patterns"},
52
+ {"tool": "get_neighbors", "purpose": "Explore related code"},
53
+ ],
54
+ },
55
+ }
56
+
57
+
58
+ class PlanExplorationTool(BaseTool):
59
+ """Create an exploration plan for a goal."""
60
+
61
+ name = "plan_exploration"
62
+ description = """Create a structured exploration plan for understanding code.
63
+
64
+ Available strategies:
65
+ - understand_feature: Learn how a feature works
66
+ - debug_issue: Track down a bug
67
+ - assess_change_impact: Evaluate change risk
68
+ - onboard_codebase: Get oriented in new code
69
+ - find_similar_code: Find similar patterns
70
+
71
+ Or provide a custom goal and get a tailored plan."""
72
+ category = ToolCategory.PLANNING
73
+
74
+ def execute(
75
+ self,
76
+ goal: str,
77
+ strategy: Optional[str] = None,
78
+ context: Optional[str] = None,
79
+ constraints: Optional[list[str]] = None,
80
+ use_case: Optional[str] = None,
81
+ ) -> ToolResult:
82
+ """Create an exploration plan.
83
+
84
+ Args:
85
+ goal: What you want to understand or accomplish
86
+ strategy: Optional predefined strategy to use
87
+ context: Additional context about the goal
88
+ constraints: Constraints to consider
89
+ use_case: Optional use case hint (e.g., "spec", "debug", "review")
90
+
91
+ Returns:
92
+ ToolResult with exploration plan
93
+ """
94
+ # Map use_case to strategy if strategy not provided
95
+ if use_case and not strategy:
96
+ use_case_mapping = {
97
+ "spec": "understand_feature",
98
+ "debug": "debug_issue",
99
+ "review": "assess_change_impact",
100
+ "onboard": "onboard_codebase",
101
+ }
102
+ strategy = use_case_mapping.get(use_case)
103
+ try:
104
+ # Use predefined strategy if specified
105
+ if strategy and strategy in STRATEGIES:
106
+ strat = STRATEGIES[strategy]
107
+ return ToolResult.success_result(
108
+ data={
109
+ "goal": goal,
110
+ "strategy": strategy,
111
+ "description": strat["description"],
112
+ "steps": strat["steps"],
113
+ "context": context,
114
+ "constraints": constraints,
115
+ },
116
+ )
117
+
118
+ # Infer strategy from goal
119
+ inferred = self._infer_strategy(goal)
120
+
121
+ if inferred and inferred in STRATEGIES:
122
+ strat = STRATEGIES[inferred]
123
+ return ToolResult.success_result(
124
+ data={
125
+ "goal": goal,
126
+ "strategy": inferred,
127
+ "description": strat["description"],
128
+ "steps": strat["steps"],
129
+ "context": context,
130
+ "constraints": constraints,
131
+ "inferred": True,
132
+ },
133
+ )
134
+
135
+ # Generic exploration plan
136
+ return ToolResult.success_result(
137
+ data={
138
+ "goal": goal,
139
+ "strategy": "custom",
140
+ "description": "Custom exploration plan",
141
+ "steps": [
142
+ {"tool": "semantic_search", "purpose": f"Search for '{goal}'"},
143
+ {"tool": "expand_node", "purpose": "Examine top results"},
144
+ {"tool": "get_neighbors", "purpose": "Explore connections"},
145
+ ],
146
+ "context": context,
147
+ "constraints": constraints,
148
+ "available_strategies": list(STRATEGIES.keys()),
149
+ },
150
+ )
151
+
152
+ except Exception as e:
153
+ log.exception("Plan exploration failed")
154
+ return ToolResult.error_result(f"Planning failed: {str(e)}")
155
+
156
+ def _infer_strategy(self, goal: str) -> Optional[str]:
157
+ """Infer strategy from goal text."""
158
+ goal_lower = goal.lower()
159
+
160
+ if any(word in goal_lower for word in ["bug", "error", "issue", "fix", "debug"]):
161
+ return "debug_issue"
162
+
163
+ if any(word in goal_lower for word in ["how", "understand", "learn", "feature"]):
164
+ return "understand_feature"
165
+
166
+ if any(word in goal_lower for word in ["change", "modify", "refactor", "impact"]):
167
+ return "assess_change_impact"
168
+
169
+ if any(word in goal_lower for word in ["new", "onboard", "overview", "structure"]):
170
+ return "onboard_codebase"
171
+
172
+ if any(word in goal_lower for word in ["similar", "like", "pattern", "example"]):
173
+ return "find_similar_code"
174
+
175
+ return None
176
+
177
+ def get_schema(self) -> dict:
178
+ """Get OpenAI function schema."""
179
+ return self._make_schema(
180
+ properties={
181
+ "goal": {
182
+ "type": "string",
183
+ "description": "What you want to understand or accomplish",
184
+ },
185
+ "strategy": {
186
+ "type": "string",
187
+ "enum": list(STRATEGIES.keys()),
188
+ "description": "Optional predefined strategy to use",
189
+ },
190
+ "context": {
191
+ "type": "string",
192
+ "description": "Additional context about the goal",
193
+ },
194
+ "constraints": {
195
+ "type": "array",
196
+ "items": {"type": "string"},
197
+ "description": "Constraints to consider",
198
+ },
199
+ "use_case": {
200
+ "type": "string",
201
+ "enum": ["spec", "debug", "review", "onboard"],
202
+ "description": "Use case hint to guide strategy selection",
203
+ },
204
+ },
205
+ required=["goal"],
206
+ )
@@ -0,0 +1,135 @@
1
+ """WritePlan tool for Plan sub-agents.
2
+
3
+ Allows writing implementation plans to a restricted directory (.emdash/plans/).
4
+ This is the only write operation available to Plan agents.
5
+ """
6
+
7
+ from pathlib import Path
8
+ import re
9
+
10
+ from .base import BaseTool, ToolResult, ToolCategory
11
+ from ...utils.logger import log
12
+
13
+
14
+ class WritePlanTool(BaseTool):
15
+ """Write implementation plan to .emdash/plans/ directory only.
16
+
17
+ This tool is restricted to writing markdown files only to the
18
+ .emdash/plans/ directory. It cannot write to any other location.
19
+ """
20
+
21
+ name = "write_plan"
22
+ description = """Write or update an implementation plan markdown file.
23
+
24
+ Plans are saved to .emdash/plans/<filename>.md in the repository.
25
+ Use this to document implementation strategies, architectural decisions,
26
+ and step-by-step plans.
27
+
28
+ Example filenames: "auth-refactor.md", "api-redesign.md", "feature-plan.md"
29
+ """
30
+ category = ToolCategory.PLANNING
31
+
32
+ def __init__(self, repo_root: Path, connection=None):
33
+ """Initialize with repo root.
34
+
35
+ Args:
36
+ repo_root: Root directory of the repository
37
+ connection: Optional connection (not used)
38
+ """
39
+ self.repo_root = repo_root.resolve()
40
+ self.plans_dir = self.repo_root / ".emdash" / "plans"
41
+ self.connection = connection
42
+
43
+ def execute(
44
+ self,
45
+ filename: str = "",
46
+ content: str = "",
47
+ **kwargs,
48
+ ) -> ToolResult:
49
+ """Write a plan file.
50
+
51
+ Args:
52
+ filename: Plan filename (e.g., "auth-refactor.md")
53
+ content: Markdown content for the plan
54
+
55
+ Returns:
56
+ ToolResult indicating success or error
57
+ """
58
+ # Validate filename
59
+ if not filename:
60
+ return ToolResult.error_result("Filename is required")
61
+
62
+ # Ensure .md extension
63
+ if not filename.endswith(".md"):
64
+ filename = f"{filename}.md"
65
+
66
+ # Sanitize filename - remove path components and invalid chars
67
+ safe_filename = Path(filename).name
68
+ safe_filename = re.sub(r'[<>:"/\\|?*]', '-', safe_filename)
69
+
70
+ if not safe_filename or safe_filename.startswith('.'):
71
+ return ToolResult.error_result(
72
+ f"Invalid filename: {filename}",
73
+ suggestions=["Use alphanumeric characters, hyphens, underscores"],
74
+ )
75
+
76
+ # Build full path
77
+ plan_path = self.plans_dir / safe_filename
78
+
79
+ # Validate path is within plans directory (prevent traversal)
80
+ try:
81
+ plan_path.resolve().relative_to(self.plans_dir.resolve())
82
+ except ValueError:
83
+ return ToolResult.error_result(
84
+ "Path traversal not allowed",
85
+ suggestions=["Provide a simple filename without directory paths"],
86
+ )
87
+
88
+ try:
89
+ # Create plans directory if needed
90
+ self.plans_dir.mkdir(parents=True, exist_ok=True)
91
+
92
+ # Check if updating existing file
93
+ is_update = plan_path.exists()
94
+
95
+ # Write the plan
96
+ plan_path.write_text(content)
97
+
98
+ log.info(
99
+ "Plan {} written: {}",
100
+ "updated" if is_update else "created",
101
+ plan_path,
102
+ )
103
+
104
+ return ToolResult.success_result(
105
+ data={
106
+ "path": str(plan_path.relative_to(self.repo_root)),
107
+ "filename": safe_filename,
108
+ "bytes_written": len(content),
109
+ "is_update": is_update,
110
+ },
111
+ )
112
+
113
+ except PermissionError:
114
+ return ToolResult.error_result(
115
+ f"Permission denied writing to {plan_path}",
116
+ )
117
+ except Exception as e:
118
+ log.exception("Failed to write plan")
119
+ return ToolResult.error_result(f"Failed to write plan: {e}")
120
+
121
+ def get_schema(self) -> dict:
122
+ """Get OpenAI function schema."""
123
+ return self._make_schema(
124
+ properties={
125
+ "filename": {
126
+ "type": "string",
127
+ "description": "Plan filename (e.g., 'auth-refactor.md', 'feature-plan')",
128
+ },
129
+ "content": {
130
+ "type": "string",
131
+ "description": "Markdown content for the implementation plan",
132
+ },
133
+ },
134
+ required=["filename", "content"],
135
+ )