emdash-core 0.1.33__py3-none-any.whl → 0.1.60__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 (67) hide show
  1. emdash_core/agent/agents.py +93 -23
  2. emdash_core/agent/background.py +481 -0
  3. emdash_core/agent/hooks.py +419 -0
  4. emdash_core/agent/inprocess_subagent.py +114 -10
  5. emdash_core/agent/mcp/config.py +78 -2
  6. emdash_core/agent/prompts/main_agent.py +88 -1
  7. emdash_core/agent/prompts/plan_mode.py +65 -44
  8. emdash_core/agent/prompts/subagents.py +96 -8
  9. emdash_core/agent/prompts/workflow.py +215 -50
  10. emdash_core/agent/providers/models.py +1 -1
  11. emdash_core/agent/providers/openai_provider.py +10 -0
  12. emdash_core/agent/research/researcher.py +154 -45
  13. emdash_core/agent/runner/agent_runner.py +157 -19
  14. emdash_core/agent/runner/context.py +28 -9
  15. emdash_core/agent/runner/sdk_runner.py +29 -2
  16. emdash_core/agent/skills.py +81 -1
  17. emdash_core/agent/toolkit.py +87 -11
  18. emdash_core/agent/toolkits/__init__.py +117 -18
  19. emdash_core/agent/toolkits/base.py +87 -2
  20. emdash_core/agent/toolkits/explore.py +18 -0
  21. emdash_core/agent/toolkits/plan.py +18 -0
  22. emdash_core/agent/tools/__init__.py +2 -0
  23. emdash_core/agent/tools/coding.py +344 -52
  24. emdash_core/agent/tools/lsp.py +361 -0
  25. emdash_core/agent/tools/skill.py +21 -1
  26. emdash_core/agent/tools/task.py +27 -23
  27. emdash_core/agent/tools/task_output.py +262 -32
  28. emdash_core/agent/verifier/__init__.py +11 -0
  29. emdash_core/agent/verifier/manager.py +295 -0
  30. emdash_core/agent/verifier/models.py +97 -0
  31. emdash_core/{swarm/worktree_manager.py → agent/worktree.py} +19 -1
  32. emdash_core/api/agent.py +451 -5
  33. emdash_core/api/research.py +3 -3
  34. emdash_core/api/router.py +0 -4
  35. emdash_core/context/longevity.py +197 -0
  36. emdash_core/context/providers/explored_areas.py +83 -39
  37. emdash_core/context/reranker.py +35 -144
  38. emdash_core/context/simple_reranker.py +500 -0
  39. emdash_core/context/tool_relevance.py +84 -0
  40. emdash_core/core/config.py +8 -0
  41. emdash_core/graph/__init__.py +8 -1
  42. emdash_core/graph/connection.py +24 -3
  43. emdash_core/graph/writer.py +7 -1
  44. emdash_core/ingestion/repository.py +17 -198
  45. emdash_core/models/agent.py +14 -0
  46. emdash_core/server.py +1 -6
  47. emdash_core/sse/stream.py +16 -1
  48. emdash_core/utils/__init__.py +0 -2
  49. emdash_core/utils/git.py +103 -0
  50. emdash_core/utils/image.py +147 -160
  51. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/METADATA +7 -5
  52. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/RECORD +54 -58
  53. emdash_core/api/swarm.py +0 -223
  54. emdash_core/db/__init__.py +0 -67
  55. emdash_core/db/auth.py +0 -134
  56. emdash_core/db/models.py +0 -91
  57. emdash_core/db/provider.py +0 -222
  58. emdash_core/db/providers/__init__.py +0 -5
  59. emdash_core/db/providers/supabase.py +0 -452
  60. emdash_core/swarm/__init__.py +0 -17
  61. emdash_core/swarm/merge_agent.py +0 -383
  62. emdash_core/swarm/session_manager.py +0 -274
  63. emdash_core/swarm/swarm_runner.py +0 -226
  64. emdash_core/swarm/task_definition.py +0 -137
  65. emdash_core/swarm/worker_spawner.py +0 -319
  66. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/WHEEL +0 -0
  67. {emdash_core-0.1.33.dist-info → emdash_core-0.1.60.dist-info}/entry_points.txt +0 -0
emdash_core/api/agent.py CHANGED
@@ -38,6 +38,7 @@ def _run_sdk_agent(
38
38
  session_id: str,
39
39
  emitter,
40
40
  plan_mode: bool = False,
41
+ images: list = None,
41
42
  ):
42
43
  """Run the agent using Anthropic Agent SDK.
43
44
 
@@ -72,7 +73,7 @@ def _run_sdk_agent(
72
73
  # Run async agent in sync context
73
74
  async def run_async():
74
75
  response_text = ""
75
- async for event in runner.run(message):
76
+ async for event in runner.run(message, images=images):
76
77
  if event.get("type") == "text":
77
78
  response_text += event.get("content", "")
78
79
  return response_text
@@ -95,6 +96,8 @@ def _run_agent_sync(
95
96
  images: list = None,
96
97
  plan_mode: bool = False,
97
98
  use_sdk: bool = None,
99
+ history: list = None,
100
+ use_worktree: bool = False,
98
101
  ):
99
102
  """Run the agent synchronously (in thread pool).
100
103
 
@@ -103,6 +106,10 @@ def _run_agent_sync(
103
106
 
104
107
  For Claude models with use_sdk=True, uses the Anthropic Agent SDK.
105
108
  For other models, uses the standard AgentRunner with OpenAI-compatible API.
109
+
110
+ Args:
111
+ history: Optional list of previous messages to pre-populate conversation
112
+ use_worktree: If True, creates a git worktree for isolated changes
106
113
  """
107
114
  try:
108
115
  _ensure_emdash_importable()
@@ -140,8 +147,22 @@ def _run_agent_sync(
140
147
  emitter = AgentEventEmitter(agent_name="Emdash Code")
141
148
  emitter.add_handler(SSEBridgeHandler(sse_handler))
142
149
 
150
+ # Add hook handler for user-defined hooks
151
+ from ..agent.hooks import HookHandler, get_hook_manager
152
+ hook_manager = get_hook_manager()
153
+ hook_manager.set_session_id(session_id)
154
+ emitter.add_handler(HookHandler(hook_manager))
155
+
143
156
  # Use SDK for Claude models if enabled
144
157
  if use_sdk and is_claude_model(model):
158
+ # Convert images for SDK if provided
159
+ sdk_images = None
160
+ if images:
161
+ import base64
162
+ sdk_images = [
163
+ {"data": base64.b64decode(img.data), "format": img.format}
164
+ for img in images
165
+ ]
145
166
  return _run_sdk_agent(
146
167
  message=message,
147
168
  model=model,
@@ -149,20 +170,42 @@ def _run_agent_sync(
149
170
  session_id=session_id,
150
171
  emitter=emitter,
151
172
  plan_mode=plan_mode,
173
+ images=sdk_images,
152
174
  )
153
175
 
154
176
  # Standard path: use AgentRunner with OpenAI-compatible API
177
+ # Get repo_root from config (set by server on startup)
178
+ from pathlib import Path
179
+ from ..config import get_config
180
+ from ..utils.logger import log
181
+ config = get_config()
182
+ repo_root = Path(config.repo_root) if config.repo_root else Path.cwd()
183
+ log.info(f"Agent API: config.repo_root={config.repo_root}, resolved repo_root={repo_root}")
184
+
185
+ # Create worktree for isolated changes if requested
186
+ worktree_info = None
187
+ if use_worktree and not plan_mode:
188
+ from ..agent.worktree import WorktreeManager
189
+ try:
190
+ worktree_manager = WorktreeManager(repo_root)
191
+ # Use session_id as task slug (truncated for safety)
192
+ task_slug = session_id[:20] if len(session_id) > 20 else session_id
193
+ worktree_info = worktree_manager.create_worktree(task_slug, force=True)
194
+ repo_root = worktree_info.path
195
+ log.info(f"Created worktree at {repo_root} on branch {worktree_info.branch}")
196
+ except Exception as e:
197
+ log.warning(f"Failed to create worktree: {e}. Running in main repo.")
198
+ worktree_info = None
199
+
155
200
  # Create toolkit with plan_mode if requested
156
201
  # When in plan mode, generate a plan file path so write_to_file is available
157
202
  plan_file_path = None
158
203
  if plan_mode:
159
- from pathlib import Path
160
- repo_root = Path.cwd()
161
204
  plan_file_path = str(repo_root / ".emdash" / "plan.md")
162
205
  # Ensure .emdash directory exists
163
206
  (repo_root / ".emdash").mkdir(exist_ok=True)
164
207
 
165
- toolkit = AgentToolkit(plan_mode=plan_mode, plan_file_path=plan_file_path)
208
+ toolkit = AgentToolkit(repo_root=repo_root, plan_mode=plan_mode, plan_file_path=plan_file_path)
166
209
 
167
210
  runner = AgentRunner(
168
211
  toolkit=toolkit,
@@ -172,20 +215,59 @@ def _run_agent_sync(
172
215
  emitter=emitter,
173
216
  )
174
217
 
218
+ # Inject pre-loaded conversation history if provided
219
+ if history:
220
+ runner._messages = list(history)
221
+ log.info(f"Injected {len(history)} messages from saved session")
222
+
175
223
  # Store session state BEFORE running (so it exists even if interrupted)
176
224
  _sessions[session_id] = {
177
225
  "runner": runner,
178
226
  "message_count": 1,
179
227
  "model": model,
180
228
  "plan_mode": plan_mode,
229
+ "worktree_info": worktree_info, # Will be None if not using worktree
181
230
  }
182
231
 
232
+ # Set up autosave callback if enabled via env var
233
+ import os
234
+ import json
235
+ if os.environ.get("EMDASH_SESSION_AUTOSAVE", "").lower() == "true":
236
+ sessions_dir = repo_root / ".emdash" / "sessions"
237
+ sessions_dir.mkdir(parents=True, exist_ok=True)
238
+ autosave_path = sessions_dir / "_autosave.json"
239
+
240
+ def autosave_callback(messages):
241
+ """Save messages to autosave file after each iteration."""
242
+ try:
243
+ # Limit to last 10 messages
244
+ trimmed = messages[-10:] if len(messages) > 10 else messages
245
+ autosave_data = {
246
+ "name": "_autosave",
247
+ "messages": trimmed,
248
+ "model": model,
249
+ "mode": "plan" if plan_mode else "code",
250
+ "session_id": session_id,
251
+ }
252
+ with open(autosave_path, "w") as f:
253
+ json.dump(autosave_data, f, indent=2, default=str)
254
+ log.debug(f"Autosaved {len(trimmed)} messages to {autosave_path}")
255
+ except Exception as e:
256
+ log.debug(f"Autosave failed: {e}")
257
+
258
+ runner._on_iteration_callback = autosave_callback
259
+ log.info("Session autosave enabled")
260
+
183
261
  # Convert image data if provided
184
262
  agent_images = None
185
263
  if images:
264
+ import base64
186
265
  from ..agent.providers.base import ImageContent
187
266
  agent_images = [
188
- ImageContent(data=img.data, format=img.format)
267
+ ImageContent(
268
+ image_data=base64.b64decode(img.data),
269
+ format=img.format
270
+ )
189
271
  for img in images
190
272
  ]
191
273
 
@@ -215,6 +297,7 @@ async def _run_agent_async(
215
297
  model = request.model or config.default_model
216
298
  max_iterations = request.options.max_iterations
217
299
  plan_mode = request.options.mode == AgentMode.PLAN
300
+ use_worktree = request.options.use_worktree
218
301
 
219
302
  # Emit session start
220
303
  sse_handler.emit(EventType.SESSION_START, {
@@ -223,6 +306,7 @@ async def _run_agent_async(
223
306
  "session_id": session_id,
224
307
  "query": request.message,
225
308
  "mode": request.options.mode.value,
309
+ "use_worktree": use_worktree,
226
310
  })
227
311
 
228
312
  loop = asyncio.get_event_loop()
@@ -239,6 +323,9 @@ async def _run_agent_async(
239
323
  session_id,
240
324
  request.images,
241
325
  plan_mode,
326
+ None, # use_sdk (auto-detect)
327
+ request.history, # Pre-loaded conversation history
328
+ use_worktree,
242
329
  )
243
330
 
244
331
  # Emit session end
@@ -411,6 +498,129 @@ async def delete_session(session_id: str):
411
498
  raise HTTPException(status_code=404, detail="Session not found")
412
499
 
413
500
 
501
+ @router.get("/chat/{session_id}/export")
502
+ async def export_session(session_id: str, limit: int = 10):
503
+ """Export session messages for persistence.
504
+
505
+ Args:
506
+ session_id: The session ID
507
+ limit: Maximum number of messages to return (default 10)
508
+
509
+ Returns:
510
+ JSON with messages array and metadata
511
+ """
512
+ if session_id not in _sessions:
513
+ raise HTTPException(status_code=404, detail="Session not found")
514
+
515
+ session = _sessions[session_id]
516
+ runner = session.get("runner")
517
+
518
+ if not runner:
519
+ return {
520
+ "session_id": session_id,
521
+ "messages": [],
522
+ "message_count": 0,
523
+ "model": session.get("model"),
524
+ "mode": "plan" if session.get("plan_mode") else "code",
525
+ }
526
+
527
+ # Get messages from runner
528
+ messages = getattr(runner, "_messages", [])
529
+
530
+ # Trim to limit (most recent)
531
+ if len(messages) > limit:
532
+ messages = messages[-limit:]
533
+
534
+ return {
535
+ "session_id": session_id,
536
+ "messages": messages,
537
+ "message_count": len(messages),
538
+ "model": session.get("model"),
539
+ "mode": "plan" if session.get("plan_mode") else "code",
540
+ }
541
+
542
+
543
+ @router.post("/chat/{session_id}/compact")
544
+ async def compact_session(session_id: str):
545
+ """Compact the session's message history using LLM summarization.
546
+
547
+ This manually triggers the same compaction that happens automatically
548
+ when context reaches 80% capacity.
549
+
550
+ Returns:
551
+ JSON with the summary text and stats
552
+ """
553
+ if session_id not in _sessions:
554
+ raise HTTPException(status_code=404, detail="Session not found")
555
+
556
+ session = _sessions[session_id]
557
+ runner = session.get("runner")
558
+
559
+ if not runner:
560
+ raise HTTPException(status_code=400, detail="Session has no active runner")
561
+
562
+ # Get current messages
563
+ messages = getattr(runner, "_messages", [])
564
+ if len(messages) <= 5:
565
+ return {
566
+ "compacted": False,
567
+ "reason": "Not enough messages to compact (need more than 5)",
568
+ "message_count": len(messages),
569
+ }
570
+
571
+ # Import compaction utilities
572
+ from ..agent.runner.context import compact_messages_with_llm, estimate_context_tokens
573
+ from ..agent.events import AgentEventEmitter
574
+
575
+ # Create a simple emitter that captures the summary
576
+ class SummaryCapture:
577
+ def __init__(self):
578
+ self.summary = None
579
+
580
+ def emit_thinking(self, text):
581
+ pass # Ignore thinking events
582
+
583
+ emitter = SummaryCapture()
584
+
585
+ # Estimate current tokens
586
+ original_tokens = estimate_context_tokens(messages)
587
+
588
+ # Compact messages
589
+ compacted_messages = compact_messages_with_llm(
590
+ messages,
591
+ emitter,
592
+ target_tokens=int(original_tokens * 0.5),
593
+ )
594
+
595
+ # Extract the summary from the compacted messages
596
+ summary_text = None
597
+ for msg in compacted_messages:
598
+ if msg.get("role") == "assistant" and "[Context Summary]" in str(msg.get("content", "")):
599
+ content = msg.get("content", "")
600
+ # Extract text between [Context Summary] and [End Summary]
601
+ start = content.find("[Context Summary]") + len("[Context Summary]")
602
+ end = content.find("[End Summary]")
603
+ if end > start:
604
+ summary_text = content[start:end].strip()
605
+ break
606
+
607
+ # Update runner's messages
608
+ runner._messages = compacted_messages
609
+
610
+ # Estimate new tokens
611
+ new_tokens = estimate_context_tokens(compacted_messages)
612
+
613
+ return {
614
+ "compacted": True,
615
+ "summary": summary_text,
616
+ "original_message_count": len(messages),
617
+ "new_message_count": len(compacted_messages),
618
+ "original_tokens": original_tokens,
619
+ "new_tokens": new_tokens,
620
+ "reduction_percent": round((1 - new_tokens / original_tokens) * 100, 1) if original_tokens > 0 else 0,
621
+ }
622
+
623
+
414
624
  @router.get("/chat/{session_id}/plan")
415
625
  async def get_pending_plan(session_id: str):
416
626
  """Get the pending plan for a session, if any.
@@ -860,3 +1070,239 @@ async def reject_plan_mode(session_id: str, feedback: str = ""):
860
1070
  "X-Session-ID": session_id,
861
1071
  },
862
1072
  )
1073
+
1074
+
1075
+ @router.get("/chat/{session_id}/todos")
1076
+ async def get_todos(session_id: str):
1077
+ """Get the current todo list for a session.
1078
+
1079
+ Returns the agent's task list including status of each item.
1080
+ """
1081
+ if session_id not in _sessions:
1082
+ raise HTTPException(status_code=404, detail="Session not found")
1083
+
1084
+ # Get todos from TaskState singleton
1085
+ from ..agent.tools.tasks import TaskState
1086
+ state = TaskState.get_instance()
1087
+
1088
+ todos = state.get_all_tasks()
1089
+
1090
+ # Count by status
1091
+ pending = sum(1 for t in todos if t["status"] == "pending")
1092
+ in_progress = sum(1 for t in todos if t["status"] == "in_progress")
1093
+ completed = sum(1 for t in todos if t["status"] == "completed")
1094
+
1095
+ return {
1096
+ "session_id": session_id,
1097
+ "todos": todos,
1098
+ "summary": {
1099
+ "total": len(todos),
1100
+ "pending": pending,
1101
+ "in_progress": in_progress,
1102
+ "completed": completed,
1103
+ },
1104
+ }
1105
+
1106
+
1107
+ @router.post("/chat/{session_id}/todos")
1108
+ async def add_todo(session_id: str, title: str, description: str = ""):
1109
+ """Add a new todo item to the agent's task list.
1110
+
1111
+ This allows users to inject tasks for the agent to work on.
1112
+ """
1113
+ if session_id not in _sessions:
1114
+ raise HTTPException(status_code=404, detail="Session not found")
1115
+
1116
+ if not title or not title.strip():
1117
+ raise HTTPException(status_code=400, detail="Title is required")
1118
+
1119
+ # Add todo via TaskState singleton
1120
+ from ..agent.tools.tasks import TaskState
1121
+ state = TaskState.get_instance()
1122
+
1123
+ task = state.add_task(title=title.strip(), description=description.strip())
1124
+
1125
+ return {
1126
+ "session_id": session_id,
1127
+ "task": task.to_dict(),
1128
+ "total_tasks": len(state.tasks),
1129
+ }
1130
+
1131
+
1132
+ # ==================== Worktree Management ====================
1133
+
1134
+
1135
+ @router.get("/chat/{session_id}/worktree")
1136
+ async def get_worktree_status(session_id: str):
1137
+ """Get the worktree status for a session.
1138
+
1139
+ Returns information about whether the session is using a worktree,
1140
+ the branch name, and any uncommitted changes.
1141
+ """
1142
+ if session_id not in _sessions:
1143
+ raise HTTPException(status_code=404, detail="Session not found")
1144
+
1145
+ session = _sessions[session_id]
1146
+ worktree_info = session.get("worktree_info")
1147
+
1148
+ if not worktree_info:
1149
+ return {
1150
+ "session_id": session_id,
1151
+ "has_worktree": False,
1152
+ }
1153
+
1154
+ # Check for uncommitted changes in the worktree
1155
+ import subprocess
1156
+ try:
1157
+ result = subprocess.run(
1158
+ ["git", "status", "--porcelain"],
1159
+ cwd=str(worktree_info.path),
1160
+ capture_output=True,
1161
+ text=True,
1162
+ )
1163
+ has_changes = bool(result.stdout.strip())
1164
+ changes = result.stdout.strip().split("\n") if has_changes else []
1165
+ except Exception:
1166
+ has_changes = False
1167
+ changes = []
1168
+
1169
+ return {
1170
+ "session_id": session_id,
1171
+ "has_worktree": True,
1172
+ "worktree_path": str(worktree_info.path),
1173
+ "branch": worktree_info.branch,
1174
+ "base_branch": worktree_info.base_branch,
1175
+ "has_changes": has_changes,
1176
+ "changes": changes,
1177
+ }
1178
+
1179
+
1180
+ @router.post("/chat/{session_id}/worktree/apply")
1181
+ async def apply_worktree_changes(session_id: str, commit_message: str = None):
1182
+ """Apply worktree changes to the main branch.
1183
+
1184
+ This merges the worktree branch into the base branch and cleans up.
1185
+ """
1186
+ from ..utils.logger import log
1187
+
1188
+ if session_id not in _sessions:
1189
+ raise HTTPException(status_code=404, detail="Session not found")
1190
+
1191
+ session = _sessions[session_id]
1192
+ worktree_info = session.get("worktree_info")
1193
+
1194
+ if not worktree_info:
1195
+ raise HTTPException(status_code=400, detail="Session is not using a worktree")
1196
+
1197
+ import subprocess
1198
+ from pathlib import Path
1199
+
1200
+ try:
1201
+ worktree_path = worktree_info.path
1202
+ branch = worktree_info.branch
1203
+ base_branch = worktree_info.base_branch
1204
+
1205
+ # First, commit any uncommitted changes in the worktree
1206
+ result = subprocess.run(
1207
+ ["git", "status", "--porcelain"],
1208
+ cwd=str(worktree_path),
1209
+ capture_output=True,
1210
+ text=True,
1211
+ )
1212
+ if result.stdout.strip():
1213
+ # Stage all changes
1214
+ subprocess.run(["git", "add", "-A"], cwd=str(worktree_path), check=True)
1215
+ # Commit
1216
+ msg = commit_message or f"Agent session {session_id[:8]} changes"
1217
+ subprocess.run(
1218
+ ["git", "commit", "-m", msg],
1219
+ cwd=str(worktree_path),
1220
+ check=True,
1221
+ )
1222
+ log.info(f"Committed changes in worktree: {msg}")
1223
+
1224
+ # Get the main repo root (parent of .emdash-worktrees)
1225
+ from ..config import get_config
1226
+ config = get_config()
1227
+ main_repo = Path(config.repo_root) if config.repo_root else Path.cwd()
1228
+
1229
+ # Merge the worktree branch into base branch
1230
+ subprocess.run(
1231
+ ["git", "checkout", base_branch],
1232
+ cwd=str(main_repo),
1233
+ check=True,
1234
+ )
1235
+ subprocess.run(
1236
+ ["git", "merge", branch, "--no-ff", "-m", f"Merge {branch}"],
1237
+ cwd=str(main_repo),
1238
+ check=True,
1239
+ )
1240
+ log.info(f"Merged {branch} into {base_branch}")
1241
+
1242
+ # Clean up the worktree
1243
+ from ..agent.worktree import WorktreeManager
1244
+ worktree_manager = WorktreeManager(main_repo)
1245
+ worktree_manager.remove_worktree(worktree_info.task_slug)
1246
+ log.info(f"Removed worktree {worktree_info.task_slug}")
1247
+
1248
+ # Clear worktree info from session
1249
+ session["worktree_info"] = None
1250
+
1251
+ return {
1252
+ "session_id": session_id,
1253
+ "success": True,
1254
+ "message": f"Changes from {branch} merged into {base_branch}",
1255
+ }
1256
+
1257
+ except subprocess.CalledProcessError as e:
1258
+ log.error(f"Failed to apply worktree changes: {e}")
1259
+ raise HTTPException(
1260
+ status_code=500,
1261
+ detail=f"Failed to apply changes: {e.stderr if hasattr(e, 'stderr') else str(e)}"
1262
+ )
1263
+ except Exception as e:
1264
+ log.error(f"Error applying worktree changes: {e}")
1265
+ raise HTTPException(status_code=500, detail=str(e))
1266
+
1267
+
1268
+ @router.delete("/chat/{session_id}/worktree")
1269
+ async def discard_worktree(session_id: str):
1270
+ """Discard worktree changes and clean up.
1271
+
1272
+ This removes the worktree and branch without merging.
1273
+ """
1274
+ from ..utils.logger import log
1275
+
1276
+ if session_id not in _sessions:
1277
+ raise HTTPException(status_code=404, detail="Session not found")
1278
+
1279
+ session = _sessions[session_id]
1280
+ worktree_info = session.get("worktree_info")
1281
+
1282
+ if not worktree_info:
1283
+ raise HTTPException(status_code=400, detail="Session is not using a worktree")
1284
+
1285
+ try:
1286
+ from pathlib import Path
1287
+ from ..config import get_config
1288
+ from ..agent.worktree import WorktreeManager
1289
+
1290
+ config = get_config()
1291
+ main_repo = Path(config.repo_root) if config.repo_root else Path.cwd()
1292
+
1293
+ worktree_manager = WorktreeManager(main_repo)
1294
+ worktree_manager.remove_worktree(worktree_info.task_slug)
1295
+ log.info(f"Discarded worktree {worktree_info.task_slug}")
1296
+
1297
+ # Clear worktree info from session
1298
+ session["worktree_info"] = None
1299
+
1300
+ return {
1301
+ "session_id": session_id,
1302
+ "success": True,
1303
+ "message": f"Worktree {worktree_info.task_slug} discarded",
1304
+ }
1305
+
1306
+ except Exception as e:
1307
+ log.error(f"Error discarding worktree: {e}")
1308
+ raise HTTPException(status_code=500, detail=str(e))
@@ -43,9 +43,9 @@ def _run_research_sync(
43
43
  import sys
44
44
  from pathlib import Path
45
45
 
46
- repo_root = Path(__file__).parent.parent.parent.parent.parent
47
- if str(repo_root) not in sys.path:
48
- sys.path.insert(0, str(repo_root))
46
+ from ..config import get_config
47
+ config = get_config()
48
+ repo_root = Path(config.repo_root) if config.repo_root else Path.cwd()
49
49
 
50
50
  try:
51
51
  from ..agent.research.agent import ResearchAgent
emdash_core/api/router.py CHANGED
@@ -19,7 +19,6 @@ from . import (
19
19
  research,
20
20
  review,
21
21
  embed,
22
- swarm,
23
22
  rules,
24
23
  context,
25
24
  feature,
@@ -67,9 +66,6 @@ api_router.include_router(review.router)
67
66
  # Embeddings
68
67
  api_router.include_router(embed.router)
69
68
 
70
- # Multi-agent
71
- api_router.include_router(swarm.router)
72
-
73
69
  # Configuration
74
70
  api_router.include_router(rules.router)
75
71
  api_router.include_router(context.router)