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
@@ -65,6 +65,14 @@ class MCPServerConfig:
65
65
  # Default to .emdash/index/kuzu_db in cwd
66
66
  default_path = Path.cwd() / ".emdash" / "index" / "kuzu_db"
67
67
  return str(default_path)
68
+ # Check for cclsp config path - use emdash config default
69
+ if var_name == "CCLSP_CONFIG_PATH":
70
+ env_val = os.getenv(var_name)
71
+ if env_val:
72
+ return env_val
73
+ # Default to .emdash/cclsp.json in cwd
74
+ default_path = Path.cwd() / ".emdash" / "cclsp.json"
75
+ return str(default_path)
68
76
  # Fall back to environment variable
69
77
  return os.getenv(var_name, "")
70
78
 
@@ -244,6 +252,8 @@ def get_default_mcp_servers() -> dict[str, MCPServerConfig]:
244
252
  """
245
253
  # Check if graph MCP is enabled via env flag
246
254
  enable_graph_mcp = os.getenv("ENABLE_GRAPH_MCP", "false").lower() == "true"
255
+ # Check if LSP is enabled via env flag (disabled by default)
256
+ enable_cclsp = os.getenv("USE_LSP", "false").lower() == "true"
247
257
 
248
258
  return {
249
259
  "github": MCPServerConfig(
@@ -266,6 +276,16 @@ def get_default_mcp_servers() -> dict[str, MCPServerConfig]:
266
276
  enabled=enable_graph_mcp,
267
277
  timeout=30,
268
278
  ),
279
+ "cclsp": MCPServerConfig(
280
+ name="cclsp",
281
+ command="npx",
282
+ args=["cclsp@latest"],
283
+ env={
284
+ "CCLSP_CONFIG_PATH": "${CCLSP_CONFIG_PATH}",
285
+ },
286
+ enabled=enable_cclsp, # Disabled by default, enable with USE_LSP=true
287
+ timeout=60, # LSP operations can take longer
288
+ ),
269
289
  }
270
290
 
271
291
 
@@ -297,6 +317,62 @@ def ensure_mcp_config(path: Path) -> MCPConfigFile:
297
317
  MCPConfigFile (loaded or newly created)
298
318
  """
299
319
  if path.exists():
300
- return MCPConfigFile.load(path)
320
+ config = MCPConfigFile.load(path)
301
321
  else:
302
- return create_default_mcp_config(path)
322
+ config = create_default_mcp_config(path)
323
+
324
+ # Ensure cclsp.json config exists if cclsp is enabled
325
+ if config.servers.get("cclsp") and config.servers["cclsp"].enabled:
326
+ cclsp_config_path = path.parent / "cclsp.json"
327
+ ensure_cclsp_config(cclsp_config_path)
328
+
329
+ return config
330
+
331
+
332
+ def get_default_cclsp_config() -> dict:
333
+ """Get the default cclsp configuration for TypeScript/React projects.
334
+
335
+ Returns:
336
+ Dictionary with cclsp configuration
337
+ """
338
+ return {
339
+ "servers": [
340
+ {
341
+ "extensions": ["ts", "tsx", "js", "jsx"],
342
+ "command": ["npx", "typescript-language-server", "--stdio"],
343
+ "rootDir": ".",
344
+ },
345
+ {
346
+ "extensions": ["py", "pyi"],
347
+ "command": ["pylsp"],
348
+ "rootDir": ".",
349
+ },
350
+ ]
351
+ }
352
+
353
+
354
+ def create_cclsp_config(path: Path) -> None:
355
+ """Create a default cclsp configuration file.
356
+
357
+ Args:
358
+ path: Path to save the cclsp.json file
359
+ """
360
+ config = get_default_cclsp_config()
361
+
362
+ # Ensure directory exists
363
+ path.parent.mkdir(parents=True, exist_ok=True)
364
+
365
+ with open(path, "w") as f:
366
+ json.dump(config, f, indent=2)
367
+
368
+ log.info(f"Created default cclsp config at {path}")
369
+
370
+
371
+ def ensure_cclsp_config(path: Path) -> None:
372
+ """Ensure cclsp config exists, creating default if needed.
373
+
374
+ Args:
375
+ path: Path to the cclsp.json file
376
+ """
377
+ if not path.exists():
378
+ create_cclsp_config(path)
@@ -10,6 +10,8 @@ from .workflow import (
10
10
  OUTPUT_GUIDELINES,
11
11
  PARALLEL_EXECUTION,
12
12
  TODO_LIST_GUIDANCE,
13
+ VERIFICATION_AND_CRITIQUE,
14
+ ACTION_NOT_ANNOUNCEMENT,
13
15
  )
14
16
 
15
17
  # Base system prompt template with placeholder for tools
@@ -20,7 +22,7 @@ _BASE_PROMPT = """You are a code exploration and implementation assistant. You o
20
22
 
21
23
  # Main agent system prompt - same for both code and plan modes
22
24
  # Main agent is always an orchestrator that delegates to subagents
23
- BASE_SYSTEM_PROMPT = _BASE_PROMPT + WORKFLOW_PATTERNS + PARALLEL_EXECUTION + EXPLORATION_STRATEGY + TODO_LIST_GUIDANCE + OUTPUT_GUIDELINES
25
+ BASE_SYSTEM_PROMPT = _BASE_PROMPT + WORKFLOW_PATTERNS + PARALLEL_EXECUTION + EXPLORATION_STRATEGY + TODO_LIST_GUIDANCE + ACTION_NOT_ANNOUNCEMENT + VERIFICATION_AND_CRITIQUE + OUTPUT_GUIDELINES
24
26
 
25
27
  # Legacy aliases
26
28
  CODE_MODE_PROMPT = BASE_SYSTEM_PROMPT
@@ -37,12 +39,22 @@ def build_system_prompt(toolkit) -> str:
37
39
  Complete system prompt string
38
40
  """
39
41
  tools_section = build_tools_section(toolkit)
42
+ agents_section = build_agents_section(toolkit)
40
43
  skills_section = build_skills_section()
41
44
  rules_section = build_rules_section()
45
+ session_section = build_session_context_section(toolkit)
42
46
 
43
47
  # Main agent always uses the same prompt - it orchestrates and delegates
44
48
  prompt = BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
45
49
 
50
+ # Add session context section first (repo, branch, status)
51
+ if session_section:
52
+ prompt += "\n" + session_section
53
+
54
+ # Add agents section so main agent knows what agents are available
55
+ if agents_section:
56
+ prompt += "\n" + agents_section
57
+
46
58
  # Add rules section if there are rules defined
47
59
  if rules_section:
48
60
  prompt += "\n" + rules_section
@@ -54,6 +66,51 @@ def build_system_prompt(toolkit) -> str:
54
66
  return prompt
55
67
 
56
68
 
69
+ def build_session_context_section(toolkit) -> str:
70
+ """Build the session context section with repo, branch, and git status.
71
+
72
+ Args:
73
+ toolkit: The agent toolkit (to access repo_root)
74
+
75
+ Returns:
76
+ Formatted string with session context, or empty string if not in a git repo
77
+ """
78
+ from ...utils.git import (
79
+ get_repo_name,
80
+ get_current_branch,
81
+ get_git_status_summary,
82
+ )
83
+
84
+ repo_root = getattr(toolkit, '_repo_root', None)
85
+ if not repo_root:
86
+ return ""
87
+
88
+ repo_name = get_repo_name(repo_root)
89
+ branch = get_current_branch(repo_root)
90
+ status = get_git_status_summary(repo_root)
91
+
92
+ # Only include if we have at least some git info
93
+ if not any([repo_name, branch, status]):
94
+ return ""
95
+
96
+ lines = [
97
+ "## Session Context",
98
+ "",
99
+ ]
100
+
101
+ if repo_name:
102
+ lines.append(f"- **Repository**: {repo_name}")
103
+ if branch:
104
+ lines.append(f"- **Branch**: {branch}")
105
+ if status:
106
+ lines.append(f"- **Git Status**: {status}")
107
+
108
+ lines.append(f"- **Working Directory**: {repo_root}")
109
+ lines.append("")
110
+
111
+ return "\n".join(lines)
112
+
113
+
57
114
  def build_rules_section() -> str:
58
115
  """Build the rules section of the system prompt.
59
116
 
@@ -80,6 +137,36 @@ def build_skills_section() -> str:
80
137
  return registry.get_skills_for_prompt()
81
138
 
82
139
 
140
+ def build_agents_section(toolkit) -> str:
141
+ """Build the agents section describing available sub-agents.
142
+
143
+ Args:
144
+ toolkit: The agent toolkit (to access repo_root)
145
+
146
+ Returns:
147
+ Formatted string with agent descriptions, or empty string if none
148
+ """
149
+ from ..toolkits import get_agents_with_descriptions
150
+
151
+ repo_root = getattr(toolkit, '_repo_root', None)
152
+ agents = get_agents_with_descriptions(repo_root)
153
+
154
+ if not agents:
155
+ return ""
156
+
157
+ lines = [
158
+ "## Available Agents",
159
+ "",
160
+ "Use the `task` tool to delegate work to these specialized agents:",
161
+ "",
162
+ ]
163
+
164
+ for agent in agents:
165
+ lines.append(f"- **{agent['name']}**: {agent['description']}")
166
+
167
+ return "\n".join(lines)
168
+
169
+
83
170
  def build_tools_section(toolkit) -> str:
84
171
  """Build the tools section of the system prompt from registered tools.
85
172
 
@@ -62,21 +62,45 @@ Even for new features, explore first to understand:
62
62
  - Where similar features exist
63
63
  - What conventions to follow
64
64
 
65
- Use these tools directly:
65
+ **Direct Tools (for targeted queries):**
66
66
  - `glob` - Find files by pattern (e.g., `glob(pattern="**/*.py")`)
67
67
  - `grep` - Search file contents (e.g., `grep(pattern="class User", path="src/")`)
68
68
  - `read_file` - Read specific files
69
69
  - `semantic_search` - Find conceptually related code
70
70
 
71
- For deep parallel exploration, you can spawn Explore agents:
71
+ **Parallel Explore Agents (for complex tasks):**
72
+ For non-trivial tasks, launch up to 3 Explore agents IN PARALLEL to speed up exploration:
73
+
72
74
  ```python
75
+ # Launch multiple agents in a SINGLE response for parallel execution
73
76
  task(
74
77
  subagent_type="Explore",
75
78
  prompt="Find all authentication-related files and patterns",
76
79
  description="Explore auth patterns"
77
80
  )
81
+ task(
82
+ subagent_type="Explore",
83
+ prompt="Find all API endpoint definitions and routing",
84
+ description="Explore API routes"
85
+ )
86
+ task(
87
+ subagent_type="Explore",
88
+ prompt="Find all database models and schemas",
89
+ description="Explore data models"
90
+ )
78
91
  ```
79
92
 
93
+ **When to use parallel agents:**
94
+ - Task touches multiple subsystems (auth, API, database, etc.)
95
+ - Codebase is large or unfamiliar
96
+ - Requirements span multiple domains
97
+ - You need to understand architectural patterns quickly
98
+
99
+ **When to use direct tools:**
100
+ - Simple, targeted queries ("find the User class")
101
+ - You already know where to look
102
+ - Quick file reads or pattern matches
103
+
80
104
  ### Phase 2: CLARIFY (Only After Exploring)
81
105
  If requirements are still unclear AFTER exploration, ask focused questions.
82
106
 
@@ -94,6 +118,27 @@ Consider:
94
118
  - Edge cases and error handling
95
119
  - Verification/testing strategy
96
120
 
121
+ **For complex architectural decisions**, you can launch a Plan agent:
122
+
123
+ ```python
124
+ task(
125
+ subagent_type="Plan",
126
+ prompt="Design the authentication system architecture considering: existing patterns found in auth/, session management in services/session.py, and the need for OAuth2 support",
127
+ description="Design auth architecture"
128
+ )
129
+ ```
130
+
131
+ **When to use Plan agents:**
132
+ - Multiple valid architectural approaches exist
133
+ - Trade-offs need careful analysis
134
+ - Design impacts multiple subsystems
135
+ - You need expert-level design recommendations
136
+
137
+ **When to synthesize directly:**
138
+ - Implementation is straightforward
139
+ - Clear patterns exist to follow
140
+ - Single file or localized changes
141
+
97
142
  ### Phase 4: REVIEW
98
143
  Before finalizing, verify the plan is complete and actionable.
99
144
 
@@ -124,52 +169,27 @@ exit_plan()
124
169
 
125
170
  ## PLAN FILE FORMAT
126
171
 
127
- Your plan must be written to `{plan_file_path}` and include:
172
+ Your plan must be written to `{plan_file_path}`. Use **compact formatting** - no blank lines between headers and content:
128
173
 
129
174
  ```markdown
130
- # Implementation Plan: <Title>
131
-
175
+ # <Title>
132
176
  ## Summary
133
- <1-2 sentence overview of what will be implemented>
134
-
177
+ <1-2 sentence overview>
135
178
  ## Approach
136
- <High-level strategy - describe WHAT changes, not HOW (no code)>
137
-
138
- ### For Bug Fixes:
139
- - Root cause analysis
140
- - Fix location and strategy
141
- - Regression prevention
142
-
143
- ### For New Features:
144
- - Architecture decisions
145
- - Component breakdown
146
- - Integration points
147
-
148
- ### For Refactors:
149
- - Current state problems
150
- - Target state benefits
151
- - Migration strategy
152
-
153
- ## Implementation Steps
179
+ <High-level strategy - WHAT changes, not HOW>
180
+ - For bugs: root cause, fix location, regression prevention
181
+ - For features: architecture, components, integration points
182
+ - For refactors: current problems, target benefits, migration
183
+ ## Steps
154
184
  1. <Step 1 - what to do, which file>
155
185
  2. <Step 2 - what to do, which file>
156
- ...
157
-
158
186
  ## Critical Files
159
- List the 3-5 most important files with brief justification:
160
-
161
187
  | File | Purpose |
162
188
  |------|---------|
163
- | `path/to/file1.py` | <why this file is critical> |
164
- | `path/to/file2.py` | <why this file is critical> |
165
- ...
166
-
189
+ | `path/to/file.py` | <why critical> |
167
190
  ## Verification
168
- - [ ] <Specific test or check to verify correctness>
169
- - [ ] <Another verification step>
170
- ...
171
-
172
- ## Risks & Considerations
191
+ - [ ] <Test or check to verify>
192
+ ## Risks
173
193
  - <Potential issue and mitigation>
174
194
  ```
175
195
 
@@ -179,8 +199,8 @@ List the 3-5 most important files with brief justification:
179
199
 
180
200
  ```
181
201
  ┌─────────────────┐
182
- │ 1. EXPLORE │ ◄─── Use tools directly: glob, grep, read_file
183
- (your tools)
202
+ │ 1. EXPLORE │ ◄─── Direct tools OR up to 3 Explore agents in parallel
203
+ (tools/agents)
184
204
  └────────┬────────┘
185
205
  │ Codebase understood
186
206
 
@@ -191,8 +211,8 @@ List the 3-5 most important files with brief justification:
191
211
  │ Requirements clear
192
212
 
193
213
  ┌─────────────────┐
194
- │ 3. DESIGN │ ◄─── Synthesize findings into approach
195
- (your analysis) │
214
+ │ 3. DESIGN │ ◄─── Direct synthesis OR Plan agent for complex decisions
215
+ │(analysis/agent) │
196
216
  └────────┬────────┘
197
217
  │ Design complete
198
218
 
@@ -247,8 +267,9 @@ Only call `exit_plan` when ALL of these are true:
247
267
  ## CORRECT BEHAVIOR
248
268
 
249
269
  - Use `ask_followup_question` for clarifying requirements
250
- - Use your tools directly (glob, grep, read_file) for exploration
251
- - Use `task(subagent_type="Explore", ...)` only for deep parallel exploration
270
+ - Use your tools directly (glob, grep, read_file) for simple, targeted exploration
271
+ - Use `task(subagent_type="Explore", ...)` for complex tasks - launch up to 3 in parallel
272
+ - Use `task(subagent_type="Plan", ...)` for complex architectural decisions
252
273
  - Write plan to `{plan_file_path}` before exiting
253
274
  - Use `exit_plan` to request plan approval
254
275
  - Focus on WHAT to change, not HOW (no code snippets)
@@ -178,12 +178,19 @@ SUBAGENT_PROMPTS = {
178
178
  "Research": RESEARCH_PROMPT,
179
179
  }
180
180
 
181
+ # Built-in agent descriptions (for main agent's system prompt)
182
+ BUILTIN_AGENTS = {
183
+ "Explore": "Fast codebase exploration - searches files, reads code, finds patterns",
184
+ "Plan": "Designs implementation plans - analyzes architecture, writes to .emdash/plans/",
185
+ }
186
+
181
187
 
182
- def get_subagent_prompt(subagent_type: str) -> str:
188
+ def get_subagent_prompt(subagent_type: str, repo_root=None) -> str:
183
189
  """Get the system prompt for a sub-agent type.
184
190
 
185
191
  Args:
186
- subagent_type: Type of agent (e.g., "Explore", "Plan")
192
+ subagent_type: Type of agent (e.g., "Explore", "Plan", or custom agent name)
193
+ repo_root: Repository root for finding custom agents
187
194
 
188
195
  Returns:
189
196
  System prompt string
@@ -191,9 +198,90 @@ def get_subagent_prompt(subagent_type: str) -> str:
191
198
  Raises:
192
199
  ValueError: If agent type is not known
193
200
  """
194
- if subagent_type not in SUBAGENT_PROMPTS:
195
- available = list(SUBAGENT_PROMPTS.keys())
196
- raise ValueError(
197
- f"Unknown agent type: {subagent_type}. Available: {available}"
198
- )
199
- return SUBAGENT_PROMPTS[subagent_type]
201
+ from pathlib import Path
202
+
203
+ # Check built-in agents first
204
+ if subagent_type in SUBAGENT_PROMPTS:
205
+ return SUBAGENT_PROMPTS[subagent_type]
206
+
207
+ # Check custom agents
208
+ from ..toolkits import get_custom_agent
209
+ custom_agent = get_custom_agent(subagent_type, repo_root)
210
+ if custom_agent:
211
+ prompt_parts = [custom_agent.system_prompt]
212
+
213
+ # Inject rules if specified
214
+ if custom_agent.rules:
215
+ rules_content = _load_rules_by_names(custom_agent.rules, repo_root)
216
+ if rules_content:
217
+ prompt_parts.append(f"\n\n## Rules\n\n{rules_content}")
218
+
219
+ # Inject skills if specified
220
+ if custom_agent.skills:
221
+ skills_content = _load_skills_by_names(custom_agent.skills, repo_root)
222
+ if skills_content:
223
+ prompt_parts.append(f"\n\n## Skills\n\n{skills_content}")
224
+
225
+ return "".join(prompt_parts)
226
+
227
+ # Not found
228
+ available = list(SUBAGENT_PROMPTS.keys())
229
+ raise ValueError(
230
+ f"Unknown agent type: {subagent_type}. Available: {available}"
231
+ )
232
+
233
+
234
+ def _load_rules_by_names(rule_names: list[str], repo_root=None) -> str:
235
+ """Load specific rules by name.
236
+
237
+ Args:
238
+ rule_names: List of rule names to load
239
+ repo_root: Repository root
240
+
241
+ Returns:
242
+ Combined rules content
243
+ """
244
+ from pathlib import Path
245
+
246
+ rules_dir = (repo_root or Path.cwd()) / ".emdash" / "rules"
247
+ if not rules_dir.exists():
248
+ return ""
249
+
250
+ parts = []
251
+ for name in rule_names:
252
+ rule_file = rules_dir / f"{name}.md"
253
+ if rule_file.exists():
254
+ try:
255
+ content = rule_file.read_text().strip()
256
+ if content:
257
+ parts.append(content)
258
+ except Exception:
259
+ pass
260
+
261
+ return "\n\n---\n\n".join(parts)
262
+
263
+
264
+ def _load_skills_by_names(skill_names: list[str], repo_root=None) -> str:
265
+ """Load specific skills by name and return their instructions.
266
+
267
+ Args:
268
+ skill_names: List of skill names to load
269
+ repo_root: Repository root
270
+
271
+ Returns:
272
+ Combined skills instructions
273
+ """
274
+ from pathlib import Path
275
+ from ..skills import SkillRegistry
276
+
277
+ skills_dir = (repo_root or Path.cwd()) / ".emdash" / "skills"
278
+ registry = SkillRegistry.get_instance()
279
+ registry.load_skills(skills_dir)
280
+
281
+ parts = []
282
+ for name in skill_names:
283
+ skill = registry.get_skill(name)
284
+ if skill and skill.instructions:
285
+ parts.append(f"### {skill.name}\n\n{skill.instructions}")
286
+
287
+ return "\n\n".join(parts)