emdash-core 0.1.7__py3-none-any.whl → 0.1.25__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 (41) hide show
  1. emdash_core/__init__.py +6 -1
  2. emdash_core/agent/events.py +29 -0
  3. emdash_core/agent/prompts/__init__.py +5 -0
  4. emdash_core/agent/prompts/main_agent.py +22 -2
  5. emdash_core/agent/prompts/plan_mode.py +126 -0
  6. emdash_core/agent/prompts/subagents.py +11 -7
  7. emdash_core/agent/prompts/workflow.py +138 -43
  8. emdash_core/agent/providers/base.py +4 -0
  9. emdash_core/agent/providers/models.py +7 -0
  10. emdash_core/agent/providers/openai_provider.py +74 -2
  11. emdash_core/agent/runner.py +556 -34
  12. emdash_core/agent/skills.py +319 -0
  13. emdash_core/agent/toolkit.py +48 -0
  14. emdash_core/agent/tools/__init__.py +3 -2
  15. emdash_core/agent/tools/modes.py +197 -53
  16. emdash_core/agent/tools/search.py +4 -0
  17. emdash_core/agent/tools/skill.py +193 -0
  18. emdash_core/agent/tools/spec.py +61 -94
  19. emdash_core/agent/tools/tasks.py +15 -78
  20. emdash_core/api/agent.py +7 -7
  21. emdash_core/api/index.py +1 -1
  22. emdash_core/api/projectmd.py +4 -2
  23. emdash_core/api/router.py +2 -0
  24. emdash_core/api/skills.py +241 -0
  25. emdash_core/checkpoint/__init__.py +40 -0
  26. emdash_core/checkpoint/cli.py +175 -0
  27. emdash_core/checkpoint/git_operations.py +250 -0
  28. emdash_core/checkpoint/manager.py +231 -0
  29. emdash_core/checkpoint/models.py +107 -0
  30. emdash_core/checkpoint/storage.py +201 -0
  31. emdash_core/config.py +1 -1
  32. emdash_core/core/config.py +18 -2
  33. emdash_core/graph/schema.py +5 -5
  34. emdash_core/ingestion/orchestrator.py +19 -10
  35. emdash_core/models/agent.py +1 -1
  36. emdash_core/server.py +42 -0
  37. emdash_core/sse/stream.py +1 -0
  38. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/METADATA +1 -2
  39. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/RECORD +41 -31
  40. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/entry_points.txt +1 -0
  41. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/WHEEL +0 -0
@@ -77,23 +77,41 @@ Creates a structured spec document with requirements and acceptance criteria."""
77
77
  ToolResult with spec info
78
78
  """
79
79
  try:
80
- spec = Spec(
81
- title=title,
82
- summary=summary,
83
- requirements=requirements,
84
- acceptance_criteria=acceptance_criteria,
85
- technical_notes=technical_notes or [],
86
- dependencies=dependencies or [],
87
- open_questions=open_questions or [],
88
- )
80
+ # Build markdown content from structured fields
81
+ content_parts = []
82
+
83
+ if summary:
84
+ content_parts.append(f"> {summary}\n")
85
+
86
+ if requirements:
87
+ content_parts.append("## Requirements")
88
+ content_parts.extend(f"- {req}" for req in requirements)
89
+ content_parts.append("")
90
+
91
+ if acceptance_criteria:
92
+ content_parts.append("## Acceptance Criteria")
93
+ content_parts.extend(f"- {crit}" for crit in acceptance_criteria)
94
+ content_parts.append("")
95
+
96
+ if technical_notes:
97
+ content_parts.append("## Technical Notes")
98
+ content_parts.extend(f"- {note}" for note in technical_notes)
99
+ content_parts.append("")
100
+
101
+ if dependencies:
102
+ content_parts.append("## Dependencies")
103
+ content_parts.extend(f"- {dep}" for dep in dependencies)
104
+ content_parts.append("")
105
+
106
+ if open_questions:
107
+ content_parts.append("## Open Questions")
108
+ content_parts.extend(f"- {q}" for q in open_questions)
109
+ content_parts.append("")
89
110
 
90
- # Validate
91
- errors = spec.validate()
92
- if errors:
93
- return ToolResult.error_result(
94
- f"Spec validation failed: {', '.join(errors)}",
95
- suggestions=["Ensure all required fields are provided"],
96
- )
111
+ content = "\n".join(content_parts)
112
+
113
+ # Create spec with title and content only (matches Spec schema)
114
+ spec = Spec(title=title, content=content)
97
115
 
98
116
  # Store in state
99
117
  state = SpecState.get_instance()
@@ -195,14 +213,8 @@ Returns the spec in markdown format."""
195
213
  return ToolResult.success_result(
196
214
  data={
197
215
  "title": spec.title,
198
- "summary": spec.summary,
199
- "requirements": spec.requirements,
200
- "acceptance_criteria": spec.acceptance_criteria,
201
- "technical_notes": spec.technical_notes,
202
- "dependencies": spec.dependencies,
203
- "open_questions": spec.open_questions,
216
+ "content": spec.content,
204
217
  "markdown": spec.to_markdown(),
205
- "is_complete": spec.is_complete(),
206
218
  },
207
219
  )
208
220
 
@@ -216,7 +228,7 @@ class UpdateSpecTool(BaseTool):
216
228
 
217
229
  name = "update_spec"
218
230
  description = """Update the current feature specification.
219
- Add or modify requirements, criteria, or other fields."""
231
+ Append content or replace the entire spec content."""
220
232
  category = ToolCategory.PLANNING
221
233
 
222
234
  def __init__(self, connection=None):
@@ -225,24 +237,17 @@ Add or modify requirements, criteria, or other fields."""
225
237
 
226
238
  def execute(
227
239
  self,
228
- add_requirements: Optional[list[str]] = None,
229
- add_acceptance_criteria: Optional[list[str]] = None,
230
- add_technical_notes: Optional[list[str]] = None,
231
- add_dependencies: Optional[list[str]] = None,
232
- add_open_questions: Optional[list[str]] = None,
233
- resolve_questions: Optional[list[str]] = None,
234
- update_summary: Optional[str] = None,
240
+ append_content: Optional[str] = None,
241
+ replace_content: Optional[str] = None,
242
+ update_title: Optional[str] = None,
243
+ **kwargs,
235
244
  ) -> ToolResult:
236
245
  """Update the current spec.
237
246
 
238
247
  Args:
239
- add_requirements: Requirements to add
240
- add_acceptance_criteria: Criteria to add
241
- add_technical_notes: Notes to add
242
- add_dependencies: Dependencies to add
243
- add_open_questions: Questions to add
244
- resolve_questions: Questions to mark as resolved
245
- update_summary: New summary text
248
+ append_content: Content to append to existing spec
249
+ replace_content: Content to replace entire spec content
250
+ update_title: New title for the spec
246
251
 
247
252
  Returns:
248
253
  ToolResult with updated spec
@@ -257,28 +262,15 @@ Add or modify requirements, criteria, or other fields."""
257
262
 
258
263
  spec = state.current_spec
259
264
 
260
- # Add new items
261
- if add_requirements:
262
- spec.requirements.extend(add_requirements)
263
- if add_acceptance_criteria:
264
- spec.acceptance_criteria.extend(add_acceptance_criteria)
265
- if add_technical_notes:
266
- spec.technical_notes.extend(add_technical_notes)
267
- if add_dependencies:
268
- spec.dependencies.extend(add_dependencies)
269
- if add_open_questions:
270
- spec.open_questions.extend(add_open_questions)
271
-
272
- # Resolve questions
273
- if resolve_questions:
274
- spec.open_questions = [
275
- q for q in spec.open_questions
276
- if q not in resolve_questions
277
- ]
278
-
279
- # Update summary
280
- if update_summary:
281
- spec.summary = update_summary
265
+ # Update title if provided
266
+ if update_title:
267
+ spec.title = update_title
268
+
269
+ # Replace or append content
270
+ if replace_content is not None:
271
+ spec.content = replace_content
272
+ elif append_content:
273
+ spec.content = spec.content + "\n\n" + append_content
282
274
 
283
275
  # Save if configured
284
276
  if state.save_path:
@@ -290,10 +282,7 @@ Add or modify requirements, criteria, or other fields."""
290
282
  return ToolResult.success_result(
291
283
  data={
292
284
  "title": spec.title,
293
- "requirements_count": len(spec.requirements),
294
- "acceptance_criteria_count": len(spec.acceptance_criteria),
295
- "open_questions_count": len(spec.open_questions),
296
- "is_complete": spec.is_complete(),
285
+ "content": spec.content,
297
286
  "markdown": spec.to_markdown(),
298
287
  },
299
288
  )
@@ -302,39 +291,17 @@ Add or modify requirements, criteria, or other fields."""
302
291
  """Get OpenAI function schema."""
303
292
  return self._make_schema(
304
293
  properties={
305
- "add_requirements": {
306
- "type": "array",
307
- "items": {"type": "string"},
308
- "description": "Requirements to add",
309
- },
310
- "add_acceptance_criteria": {
311
- "type": "array",
312
- "items": {"type": "string"},
313
- "description": "Acceptance criteria to add",
314
- },
315
- "add_technical_notes": {
316
- "type": "array",
317
- "items": {"type": "string"},
318
- "description": "Technical notes to add",
319
- },
320
- "add_dependencies": {
321
- "type": "array",
322
- "items": {"type": "string"},
323
- "description": "Dependencies to add",
324
- },
325
- "add_open_questions": {
326
- "type": "array",
327
- "items": {"type": "string"},
328
- "description": "Open questions to add",
294
+ "append_content": {
295
+ "type": "string",
296
+ "description": "Markdown content to append to existing spec",
329
297
  },
330
- "resolve_questions": {
331
- "type": "array",
332
- "items": {"type": "string"},
333
- "description": "Questions to mark as resolved",
298
+ "replace_content": {
299
+ "type": "string",
300
+ "description": "Markdown content to replace entire spec content",
334
301
  },
335
- "update_summary": {
302
+ "update_title": {
336
303
  "type": "string",
337
- "description": "New summary text",
304
+ "description": "New title for the spec",
338
305
  },
339
306
  },
340
307
  required=[],
@@ -1,6 +1,6 @@
1
1
  """Task management tools for agent workflows."""
2
2
 
3
- from dataclasses import dataclass, field
3
+ from dataclasses import dataclass
4
4
  from enum import Enum
5
5
  from typing import Optional
6
6
 
@@ -23,9 +23,6 @@ class Task:
23
23
  title: str
24
24
  description: str = ""
25
25
  status: TaskStatus = TaskStatus.PENDING
26
- complexity: str = "M" # T-shirt size: S, M, L, XL
27
- files: list[str] = field(default_factory=list) # Target files for this task
28
- checklist: list[dict] = field(default_factory=list) # Subtasks: [{text, done}]
29
26
 
30
27
  def to_dict(self) -> dict:
31
28
  return {
@@ -33,9 +30,6 @@ class Task:
33
30
  "title": self.title,
34
31
  "description": self.description,
35
32
  "status": self.status.value,
36
- "complexity": self.complexity,
37
- "files": self.files,
38
- "checklist": self.checklist,
39
33
  }
40
34
 
41
35
 
@@ -68,26 +62,17 @@ class TaskState:
68
62
  self,
69
63
  title: str,
70
64
  description: str = "",
71
- complexity: str = "M",
72
- files: list[str] = None,
73
- checklist: list[str] = None,
74
65
  ) -> Task:
75
66
  """Add a new task.
76
67
 
77
68
  Args:
78
69
  title: Task title
79
70
  description: Detailed description
80
- complexity: T-shirt size (S, M, L, XL)
81
- files: List of target file paths
82
- checklist: List of subtask strings (converted to {text, done} dicts)
83
71
  """
84
72
  task = Task(
85
73
  id=str(self._next_id),
86
74
  title=title,
87
75
  description=description,
88
- complexity=complexity,
89
- files=files or [],
90
- checklist=[{"text": item, "done": False} for item in (checklist or [])],
91
76
  )
92
77
  self._next_id += 1
93
78
  self.tasks.append(task)
@@ -139,18 +124,15 @@ class TaskManagementTool(BaseTool):
139
124
 
140
125
 
141
126
  class WriteTodoTool(TaskManagementTool):
142
- """Create a new sub-task for complex work breakdown."""
127
+ """Create a new task for tracking work."""
143
128
 
144
129
  name = "write_todo"
145
- description = "Create a new task with target files and checklist items. Use reset=true to clear all existing tasks first."
130
+ description = "Create a new task. Use reset=true to clear all existing tasks first."
146
131
 
147
132
  def execute(
148
133
  self,
149
134
  title: str,
150
135
  description: str = "",
151
- complexity: str = "M",
152
- files: list[str] = None,
153
- checklist: list[str] = None,
154
136
  reset: bool = False,
155
137
  **kwargs,
156
138
  ) -> ToolResult:
@@ -159,9 +141,6 @@ class WriteTodoTool(TaskManagementTool):
159
141
  Args:
160
142
  title: Short task title
161
143
  description: Detailed description (optional)
162
- complexity: T-shirt size (S, M, L, XL)
163
- files: Target file paths this task will modify
164
- checklist: List of subtask items to track
165
144
  reset: If true, clear all existing tasks before adding this one
166
145
 
167
146
  Returns:
@@ -177,9 +156,6 @@ class WriteTodoTool(TaskManagementTool):
177
156
  task = self.state.add_task(
178
157
  title=title.strip(),
179
158
  description=description.strip() if description else "",
180
- complexity=complexity,
181
- files=files or [],
182
- checklist=checklist or [],
183
159
  )
184
160
 
185
161
  return ToolResult.success_result({
@@ -201,21 +177,6 @@ class WriteTodoTool(TaskManagementTool):
201
177
  "type": "string",
202
178
  "description": "Detailed description of what needs to be done",
203
179
  },
204
- "complexity": {
205
- "type": "string",
206
- "enum": ["S", "M", "L", "XL"],
207
- "description": "T-shirt size complexity: S (trivial), M (moderate), L (significant), XL (complex)",
208
- },
209
- "files": {
210
- "type": "array",
211
- "items": {"type": "string"},
212
- "description": "Target file paths this task will modify",
213
- },
214
- "checklist": {
215
- "type": "array",
216
- "items": {"type": "string"},
217
- "description": "List of subtask items to track (e.g., ['Add imports', 'Update function', 'Add tests'])",
218
- },
219
180
  "reset": {
220
181
  "type": "boolean",
221
182
  "description": "Set to true to clear all existing tasks before adding this one",
@@ -227,39 +188,30 @@ class WriteTodoTool(TaskManagementTool):
227
188
 
228
189
 
229
190
  class UpdateTodoListTool(TaskManagementTool):
230
- """Update task status or checklist items."""
191
+ """Update task status."""
231
192
 
232
193
  name = "update_todo_list"
233
- description = "Update task status, mark checklist items done, or add files. Auto-creates tasks if they don't exist."
194
+ description = "Update task status. Auto-creates tasks if they don't exist."
234
195
 
235
196
  def execute(
236
197
  self,
237
198
  task_id: str,
238
199
  status: str = None,
239
- checklist_done: list[int] = None,
240
- checklist: list[int] = None, # Alias for checklist_done
241
- files: list[str] = None,
242
200
  title: str = "",
243
201
  description: str = "",
244
202
  **kwargs, # Ignore unexpected params from LLM
245
203
  ) -> ToolResult:
246
- """Update task status or checklist items.
204
+ """Update task status.
247
205
 
248
206
  Args:
249
207
  task_id: Task ID to update (e.g., "1", "2")
250
208
  status: New status (pending, in_progress, completed)
251
- checklist_done: Indices of checklist items to mark done (0-based)
252
- checklist: Alias for checklist_done
253
- files: Additional files to add to the task
254
209
  title: Optional title for auto-created tasks
255
210
  description: Optional description for auto-created tasks
256
211
 
257
212
  Returns:
258
213
  ToolResult with updated task list
259
214
  """
260
- # Handle checklist alias
261
- if checklist and not checklist_done:
262
- checklist_done = checklist
263
215
  task = self.state.get_task(task_id)
264
216
 
265
217
  # Auto-create task if not found
@@ -276,7 +228,6 @@ class UpdateTodoListTool(TaskManagementTool):
276
228
  title=title or f"Task {task_id}",
277
229
  status=new_status,
278
230
  description=description,
279
- files=files or [],
280
231
  )
281
232
  self.state.tasks.append(task)
282
233
  return ToolResult.success_result({
@@ -295,18 +246,6 @@ class UpdateTodoListTool(TaskManagementTool):
295
246
  f"Invalid status: {status}. Use: pending, in_progress, completed"
296
247
  )
297
248
 
298
- # Mark checklist items as done
299
- if checklist_done:
300
- for idx in checklist_done:
301
- if 0 <= idx < len(task.checklist):
302
- task.checklist[idx]["done"] = True
303
-
304
- # Add files
305
- if files:
306
- for f in files:
307
- if f not in task.files:
308
- task.files.append(f)
309
-
310
249
  return ToolResult.success_result({
311
250
  "task_id": task_id,
312
251
  "task": task.to_dict(),
@@ -326,16 +265,6 @@ class UpdateTodoListTool(TaskManagementTool):
326
265
  "enum": ["pending", "in_progress", "completed"],
327
266
  "description": "New status for the task",
328
267
  },
329
- "checklist_done": {
330
- "type": "array",
331
- "items": {"type": "integer"},
332
- "description": "Indices of checklist items to mark done (0-based)",
333
- },
334
- "files": {
335
- "type": "array",
336
- "items": {"type": "string"},
337
- "description": "Additional file paths to add to the task",
338
- },
339
268
  "title": {
340
269
  "type": "string",
341
270
  "description": "Task title (used if auto-creating)",
@@ -353,7 +282,13 @@ class AskFollowupQuestionTool(TaskManagementTool):
353
282
  """Request clarification from the user."""
354
283
 
355
284
  name = "ask_followup_question"
356
- description = "Ask the user a clarifying question. Use this when you need more information to proceed."
285
+ description = """Ask the user a clarifying question when you need more information to proceed.
286
+
287
+ CRITICAL CONSTRAINTS:
288
+ - Only ask ONE question at a time - never call this tool multiple times in parallel
289
+ - Only ask AFTER doing research first (read files, search code, explore the codebase)
290
+ - Questions should be informed by what you've learned, not generic upfront questionnaires
291
+ - If you need multiple pieces of information, ask the most important one first"""
357
292
 
358
293
  def execute(
359
294
  self,
@@ -452,3 +387,5 @@ class AttemptCompletionTool(TaskManagementTool):
452
387
  },
453
388
  required=["summary"],
454
389
  )
390
+
391
+
emdash_core/api/agent.py CHANGED
@@ -73,6 +73,13 @@ def _run_agent_sync(
73
73
  emitter=emitter,
74
74
  )
75
75
 
76
+ # Store session state BEFORE running (so it exists even if interrupted)
77
+ _sessions[session_id] = {
78
+ "runner": runner,
79
+ "message_count": 1,
80
+ "model": model,
81
+ }
82
+
76
83
  # Convert image data if provided
77
84
  agent_images = None
78
85
  if images:
@@ -85,13 +92,6 @@ def _run_agent_sync(
85
92
  # Run the agent
86
93
  response = runner.run(message, images=agent_images)
87
94
 
88
- # Store session state
89
- _sessions[session_id] = {
90
- "runner": runner,
91
- "message_count": 1,
92
- "model": model,
93
- }
94
-
95
95
  return response
96
96
 
97
97
  except Exception as e:
emdash_core/api/index.py CHANGED
@@ -64,7 +64,7 @@ def _run_index_sync(
64
64
  # Create orchestrator (uses configured connection)
65
65
  orchestrator = IngestionOrchestrator()
66
66
 
67
- sse_handler.emit(EventType.PROGRESS, {"step": "Parsing codebase", "percent": 10})
67
+ sse_handler.emit(EventType.PROGRESS, {"step": "Indexing codebase", "percent": 10})
68
68
 
69
69
  # Progress callback to emit SSE events during parsing
70
70
  def progress_callback(step: str, percent: float):
@@ -105,7 +105,7 @@ def _generate_projectmd_sync(
105
105
  runner = AgentRunner(
106
106
  model=model,
107
107
  verbose=True,
108
- max_iterations=30,
108
+ max_iterations=100,
109
109
  emitter=emitter,
110
110
  )
111
111
 
@@ -116,7 +116,9 @@ def _generate_projectmd_sync(
116
116
  3. Key files and their purposes
117
117
  4. How to get started
118
118
 
119
- Use the available tools to explore the codebase, then write a comprehensive PROJECT.md."""
119
+ Use the available tools to explore the codebase structure and key files.
120
+ After exploration, write a comprehensive PROJECT.md document.
121
+ IMPORTANT: After exploring, output the complete PROJECT.md content as your final response."""
120
122
 
121
123
  content = runner.run(prompt)
122
124
 
emdash_core/api/router.py CHANGED
@@ -24,6 +24,7 @@ from . import (
24
24
  context,
25
25
  feature,
26
26
  projectmd,
27
+ skills,
27
28
  )
28
29
 
29
30
  api_router = APIRouter(prefix="/api")
@@ -37,6 +38,7 @@ api_router.include_router(auth.router)
37
38
  # Agent operations
38
39
  api_router.include_router(agent.router)
39
40
  api_router.include_router(agents.router)
41
+ api_router.include_router(skills.router)
40
42
 
41
43
  # Database management
42
44
  api_router.include_router(db.router)