ralphx 0.2.2__py3-none-any.whl → 0.3.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 (45) hide show
  1. ralphx/__init__.py +1 -1
  2. ralphx/api/main.py +9 -1
  3. ralphx/api/routes/auth.py +730 -65
  4. ralphx/api/routes/config.py +3 -56
  5. ralphx/api/routes/export_import.py +795 -0
  6. ralphx/api/routes/loops.py +4 -4
  7. ralphx/api/routes/planning.py +19 -5
  8. ralphx/api/routes/projects.py +84 -2
  9. ralphx/api/routes/templates.py +115 -2
  10. ralphx/api/routes/workflows.py +22 -22
  11. ralphx/cli.py +21 -6
  12. ralphx/core/auth.py +346 -171
  13. ralphx/core/database.py +615 -167
  14. ralphx/core/executor.py +0 -3
  15. ralphx/core/loop.py +15 -2
  16. ralphx/core/loop_templates.py +69 -3
  17. ralphx/core/planning_service.py +109 -21
  18. ralphx/core/preview.py +9 -25
  19. ralphx/core/project_db.py +175 -75
  20. ralphx/core/project_export.py +469 -0
  21. ralphx/core/project_import.py +670 -0
  22. ralphx/core/sample_project.py +430 -0
  23. ralphx/core/templates.py +46 -9
  24. ralphx/core/workflow_executor.py +35 -5
  25. ralphx/core/workflow_export.py +606 -0
  26. ralphx/core/workflow_import.py +1149 -0
  27. ralphx/examples/sample_project/DESIGN.md +345 -0
  28. ralphx/examples/sample_project/README.md +37 -0
  29. ralphx/examples/sample_project/guardrails.md +57 -0
  30. ralphx/examples/sample_project/stories.jsonl +10 -0
  31. ralphx/mcp/__init__.py +6 -2
  32. ralphx/mcp/registry.py +3 -3
  33. ralphx/mcp/server.py +99 -29
  34. ralphx/mcp/tools/__init__.py +4 -0
  35. ralphx/mcp/tools/help.py +204 -0
  36. ralphx/mcp/tools/workflows.py +114 -32
  37. ralphx/mcp_server.py +6 -2
  38. ralphx/static/assets/index-0ovNnfOq.css +1 -0
  39. ralphx/static/assets/index-CY9s08ZB.js +251 -0
  40. ralphx/static/assets/index-CY9s08ZB.js.map +1 -0
  41. ralphx/static/index.html +14 -0
  42. {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/METADATA +34 -12
  43. {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/RECORD +45 -30
  44. {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/WHEEL +0 -0
  45. {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/entry_points.txt +0 -0
@@ -849,14 +849,14 @@ async def create_simple_loop(slug: str, request: CreateSimpleLoopRequest):
849
849
  status_code=status.HTTP_400_BAD_REQUEST,
850
850
  detail=f"Source loop '{source_loop}' not found in this project.",
851
851
  )
852
- namespace = source_loop # Use source loop name as namespace
852
+ source_loop_name = source_loop # Use source loop name
853
853
  elif request.stories_source and request.stories_source.type == "content":
854
- # For JSONL uploads, derive namespace from request or loop_id
855
- namespace = request.stories_source.namespace or loop_id.replace("-", "_")
854
+ # For JSONL uploads, derive source from request or loop_id
855
+ source_loop_name = request.stories_source.namespace or loop_id.replace("-", "_")
856
856
 
857
857
  config_yaml = generate_simple_implementation_config(
858
858
  name=loop_id,
859
- namespace=namespace,
859
+ source_loop=source_loop_name,
860
860
  display_name=display_name,
861
861
  description=description,
862
862
  )
@@ -269,9 +269,18 @@ async def stream_planning_response(slug: str, workflow_id: str):
269
269
  detail="Planning session is not active",
270
270
  )
271
271
 
272
- # Get workflow for context
272
+ # Get workflow and current step for context
273
273
  workflow = pdb.get_workflow(workflow_id)
274
274
 
275
+ # Get the step to access its config (tools, model, timeout)
276
+ step = pdb.get_workflow_step(session["step_id"])
277
+ step_config = step.get("config", {}) if step else {}
278
+
279
+ # Extract configuration from step
280
+ allowed_tools = step_config.get("allowedTools", [])
281
+ model = step_config.get("model", "opus") # Default to opus for design docs
282
+ timeout = step_config.get("timeout", 180)
283
+
275
284
  async def generate_response():
276
285
  """Generate streaming response from Claude."""
277
286
  import json
@@ -290,7 +299,12 @@ async def stream_planning_response(slug: str, workflow_id: str):
290
299
  accumulated = ""
291
300
 
292
301
  try:
293
- async for event in service.stream_response(messages):
302
+ async for event in service.stream_response(
303
+ messages,
304
+ model=model,
305
+ tools=allowed_tools if allowed_tools else None,
306
+ timeout=timeout,
307
+ ):
294
308
  if event.type == AdapterEvent.TEXT:
295
309
  text = event.text or ""
296
310
  accumulated += text
@@ -405,9 +419,9 @@ async def complete_planning_session(
405
419
 
406
420
  # Get workflow info
407
421
  workflow = pdb.get_workflow(workflow_id)
408
- namespace = workflow["namespace"]
409
422
 
410
423
  # Save artifacts as project resources
424
+ # Use workflow_id for unique filenames (namespace was removed in schema v16)
411
425
  from pathlib import Path
412
426
  from datetime import datetime
413
427
 
@@ -416,7 +430,7 @@ async def complete_planning_session(
416
430
  resource_path = Path(project["path"]) / ".ralphx" / "resources"
417
431
  resource_path.mkdir(parents=True, exist_ok=True)
418
432
 
419
- doc_filename = f"design-doc-{namespace}.md"
433
+ doc_filename = f"design-doc-{workflow_id}.md"
420
434
  doc_path = resource_path / doc_filename
421
435
  doc_path.write_text(artifacts["design_doc"])
422
436
 
@@ -442,7 +456,7 @@ async def complete_planning_session(
442
456
  resource_path = Path(project["path"]) / ".ralphx" / "resources"
443
457
  resource_path.mkdir(parents=True, exist_ok=True)
444
458
 
445
- guardrails_filename = f"guardrails-{namespace}.md"
459
+ guardrails_filename = f"guardrails-{workflow_id}.md"
446
460
  guardrails_path = resource_path / guardrails_filename
447
461
  guardrails_path.write_text(artifacts["guardrails"])
448
462
 
@@ -33,12 +33,22 @@ class ProjectResponse(BaseModel):
33
33
  path: str
34
34
  design_doc: Optional[str] = None
35
35
  created_at: str
36
+ path_valid: bool = True # Whether the project directory exists
36
37
 
37
38
  model_config = {"from_attributes": True}
38
39
 
39
40
  @classmethod
40
- def from_project(cls, project: Project) -> "ProjectResponse":
41
- """Create from Project model."""
41
+ def from_project(cls, project: Project, check_path: bool = True) -> "ProjectResponse":
42
+ """Create from Project model.
43
+
44
+ Args:
45
+ project: Project model instance.
46
+ check_path: If True, check if project path exists.
47
+ """
48
+ path_valid = True
49
+ if check_path:
50
+ path_valid = Path(project.path).exists()
51
+
42
52
  return cls(
43
53
  id=project.id,
44
54
  slug=project.slug,
@@ -46,6 +56,7 @@ class ProjectResponse(BaseModel):
46
56
  path=str(project.path),
47
57
  design_doc=project.design_doc,
48
58
  created_at=project.created_at.isoformat() if project.created_at else "",
59
+ path_valid=path_valid,
49
60
  )
50
61
 
51
62
 
@@ -68,6 +79,13 @@ class ProjectWithStats(ProjectResponse):
68
79
  stats: ProjectStats = Field(default_factory=ProjectStats)
69
80
 
70
81
 
82
+ class ProjectUpdate(BaseModel):
83
+ """Request model for updating a project."""
84
+
85
+ name: Optional[str] = Field(None, description="Human-readable name")
86
+ path: Optional[str] = Field(None, description="New path to project directory (for relinking)")
87
+
88
+
71
89
  def get_manager() -> ProjectManager:
72
90
  """Get project manager instance."""
73
91
  return ProjectManager()
@@ -165,11 +183,75 @@ async def get_project(slug: str):
165
183
  path=base.path,
166
184
  design_doc=base.design_doc,
167
185
  created_at=base.created_at,
186
+ path_valid=base.path_valid,
168
187
  stats=stats,
169
188
  )
170
189
  return response
171
190
 
172
191
 
192
+ @router.patch("/{slug}", response_model=ProjectResponse)
193
+ async def update_project(slug: str, data: ProjectUpdate):
194
+ """Update a project's metadata or relink its path.
195
+
196
+ Use this to:
197
+ - Rename a project
198
+ - Relink a project to a new directory (when original path moved/missing)
199
+ """
200
+ manager = get_manager()
201
+ project = manager.get_project(slug)
202
+
203
+ if not project:
204
+ raise HTTPException(
205
+ status_code=status.HTTP_404_NOT_FOUND,
206
+ detail=f"Project not found: {slug}",
207
+ )
208
+
209
+ # Handle path update (relink)
210
+ if data.path is not None:
211
+ new_path = Path(data.path)
212
+
213
+ # Validate new path exists
214
+ if not new_path.exists():
215
+ raise HTTPException(
216
+ status_code=status.HTTP_400_BAD_REQUEST,
217
+ detail=f"New path does not exist: {data.path}",
218
+ )
219
+
220
+ if not new_path.is_dir():
221
+ raise HTTPException(
222
+ status_code=status.HTTP_400_BAD_REQUEST,
223
+ detail=f"New path is not a directory: {data.path}",
224
+ )
225
+
226
+ # Check if new path already has .ralphx directory
227
+ ralphx_dir = new_path / ".ralphx"
228
+ if ralphx_dir.exists():
229
+ # Warn if it has a different project (check project.db)
230
+ # For now, allow it - user is responsible for ensuring correct path
231
+ pass
232
+ else:
233
+ # Create .ralphx directory at new location
234
+ from ralphx.core.workspace import ensure_project_ralphx
235
+ try:
236
+ ensure_project_ralphx(new_path)
237
+ except Exception as e:
238
+ raise HTTPException(
239
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
240
+ detail=f"Failed to initialize project at new path: {e}",
241
+ )
242
+
243
+ # Update path in global database
244
+ manager.global_db.update_project(slug, path=str(new_path.resolve()))
245
+
246
+ # Handle name update
247
+ if data.name is not None:
248
+ manager.global_db.update_project(slug, name=data.name)
249
+
250
+ # Return updated project
251
+ project = manager.get_project(slug)
252
+ return ProjectResponse.from_project(project)
253
+
254
+
173
255
  @router.delete("/{slug}", status_code=status.HTTP_204_NO_CONTENT)
174
256
  async def delete_project(
175
257
  slug: str,
@@ -4,7 +4,7 @@ Templates are global, read-only, and shipped with RalphX.
4
4
  No authentication required - templates are public.
5
5
 
6
6
  Includes:
7
- - Loop templates (research, implementation, etc.)
7
+ - Loop templates (extractgen_requirements, implementation, etc.)
8
8
  - Loop builder templates (planning, implementation with Phase 1)
9
9
  - Permission templates (planning, implementation, read_only, etc.)
10
10
  """
@@ -21,6 +21,8 @@ from ralphx.core.loop_templates import (
21
21
  create_loop_from_template,
22
22
  get_loop_template,
23
23
  list_loop_templates,
24
+ PLANNING_EXTRACT_PROMPT,
25
+ IMPLEMENTATION_IMPLEMENT_PROMPT,
24
26
  )
25
27
  from ralphx.core.permission_templates import (
26
28
  apply_template_to_loop,
@@ -85,7 +87,7 @@ async def get_template_by_name(name: str) -> TemplateDetail:
85
87
  No authentication required - templates are public.
86
88
 
87
89
  Args:
88
- name: Template name (e.g., 'research', 'implementation')
90
+ name: Template name (e.g., 'extractgen_requirements', 'implementation')
89
91
 
90
92
  Returns:
91
93
  Full template with config and YAML representation
@@ -407,3 +409,114 @@ async def apply_permission_template_endpoint(
407
409
  status_code=status.HTTP_400_BAD_REQUEST,
408
410
  detail=str(e),
409
411
  )
412
+
413
+
414
+ # ============================================================================
415
+ # Step Prompts (System Prompts for Workflow Steps)
416
+ # ============================================================================
417
+
418
+ # Template variable documentation for each loop type
419
+ TEMPLATE_VARIABLES = {
420
+ "generator": {
421
+ "{{input_item.title}}": {
422
+ "description": "Title of the current item (not commonly used in generator)",
423
+ "required": False,
424
+ },
425
+ "{{existing_stories}}": {
426
+ "description": "List of already-generated stories to avoid duplicates",
427
+ "required": True,
428
+ },
429
+ "{{total_stories}}": {
430
+ "description": "Count of stories generated so far",
431
+ "required": False,
432
+ },
433
+ "{{category_stats}}": {
434
+ "description": "Statistics by category for ID assignment",
435
+ "required": False,
436
+ },
437
+ "{{inputs_list}}": {
438
+ "description": "List of input documents available",
439
+ "required": False,
440
+ },
441
+ },
442
+ "consumer": {
443
+ "{{input_item.title}}": {
444
+ "description": "Title of the story being implemented",
445
+ "required": True,
446
+ },
447
+ "{{input_item.content}}": {
448
+ "description": "Full story content",
449
+ "required": True,
450
+ },
451
+ "{{input_item.metadata}}": {
452
+ "description": "Story metadata (priority, category, etc.)",
453
+ "required": False,
454
+ },
455
+ "{{implemented_summary}}": {
456
+ "description": "Summary of previously implemented stories",
457
+ "required": False,
458
+ },
459
+ },
460
+ }
461
+
462
+
463
+ class TemplateVariableInfo(BaseModel):
464
+ """Information about a template variable."""
465
+
466
+ name: str
467
+ description: str
468
+ required: bool
469
+
470
+
471
+ class StepPromptResponse(BaseModel):
472
+ """Response for default step prompt endpoint."""
473
+
474
+ prompt: str
475
+ loop_type: str
476
+ display_name: str
477
+ variables: list[TemplateVariableInfo]
478
+
479
+
480
+ @router.get("/step-prompts/{loop_type}", response_model=StepPromptResponse)
481
+ async def get_default_step_prompt(loop_type: str):
482
+ """Get default system prompt for a step type.
483
+
484
+ Args:
485
+ loop_type: Either 'generator' or 'consumer'
486
+
487
+ Returns:
488
+ The default prompt content along with template variable documentation.
489
+ """
490
+ prompts = {
491
+ "generator": {
492
+ "prompt": PLANNING_EXTRACT_PROMPT,
493
+ "display_name": "Generator (Story Extraction)",
494
+ },
495
+ "consumer": {
496
+ "prompt": IMPLEMENTATION_IMPLEMENT_PROMPT,
497
+ "display_name": "Consumer (Implementation)",
498
+ },
499
+ }
500
+
501
+ if loop_type not in prompts:
502
+ raise HTTPException(
503
+ status_code=status.HTTP_404_NOT_FOUND,
504
+ detail=f"Unknown loop type: {loop_type}. Must be 'generator' or 'consumer'.",
505
+ )
506
+
507
+ prompt_info = prompts[loop_type]
508
+ variables = TEMPLATE_VARIABLES.get(loop_type, {})
509
+
510
+ return StepPromptResponse(
511
+ prompt=prompt_info["prompt"].strip(),
512
+ loop_type=loop_type,
513
+ display_name=prompt_info["display_name"],
514
+ variables=[
515
+ TemplateVariableInfo(
516
+ name=name,
517
+ description=info["description"],
518
+ required=info["required"],
519
+ )
520
+ for name, info in variables.items()
521
+ ],
522
+ )
@@ -62,7 +62,6 @@ class WorkflowResponse(BaseModel):
62
62
  id: str
63
63
  template_id: Optional[str] = None
64
64
  name: str
65
- namespace: str
66
65
  status: str
67
66
  current_step: int
68
67
  created_at: str
@@ -126,6 +125,7 @@ class CreateStepRequest(BaseModel):
126
125
  step_type: str = Field(..., pattern=r"^(interactive|autonomous)$")
127
126
  description: Optional[str] = None
128
127
  loop_type: Optional[str] = None
128
+ template: Optional[str] = Field(None, max_length=100) # Template name (e.g., 'webgen_requirements')
129
129
  skippable: bool = False
130
130
  # Autonomous step execution settings (step config overrides template defaults)
131
131
  model: Optional[str] = Field(None, pattern=r"^(sonnet|sonnet-1m|opus|haiku)$")
@@ -135,6 +135,8 @@ class CreateStepRequest(BaseModel):
135
135
  max_iterations: Optional[int] = Field(None, ge=0, le=10000) # 0 = unlimited
136
136
  cooldown_between_iterations: Optional[int] = Field(None, ge=0, le=300) # seconds
137
137
  max_consecutive_errors: Optional[int] = Field(None, ge=1, le=100)
138
+ # Custom prompt (autonomous steps only)
139
+ custom_prompt: Optional[str] = Field(None, max_length=50000)
138
140
 
139
141
 
140
142
  class UpdateStepRequest(BaseModel):
@@ -147,6 +149,7 @@ class UpdateStepRequest(BaseModel):
147
149
  step_type: Optional[str] = Field(None, pattern=r"^(interactive|autonomous)$")
148
150
  description: Optional[str] = None
149
151
  loop_type: Optional[str] = None
152
+ template: Optional[str] = Field(None, max_length=100) # Template name (e.g., 'webgen_requirements')
150
153
  skippable: Optional[bool] = None
151
154
  # Autonomous step execution settings (step config overrides template defaults)
152
155
  model: Optional[str] = Field(None, pattern=r"^(sonnet|sonnet-1m|opus|haiku)$")
@@ -156,6 +159,8 @@ class UpdateStepRequest(BaseModel):
156
159
  max_iterations: Optional[int] = Field(None, ge=0, le=10000) # 0 = unlimited
157
160
  cooldown_between_iterations: Optional[int] = Field(None, ge=0, le=300) # seconds
158
161
  max_consecutive_errors: Optional[int] = Field(None, ge=1, le=100)
162
+ # Custom prompt (autonomous steps only)
163
+ custom_prompt: Optional[str] = Field(None, max_length=50000)
159
164
 
160
165
 
161
166
  # Valid tools for autonomous steps
@@ -216,23 +221,6 @@ def _get_project_db(slug: str) -> ProjectDatabase:
216
221
  return ProjectDatabase(project["path"])
217
222
 
218
223
 
219
- def _generate_namespace(name: str) -> str:
220
- """Generate a valid namespace from a workflow name."""
221
- import re
222
-
223
- # Convert to lowercase, replace spaces with dashes
224
- ns = name.lower().replace(" ", "-")
225
- # Remove invalid characters
226
- ns = re.sub(r"[^a-z0-9_-]", "", ns)
227
- # Ensure it starts with a letter
228
- if not ns or not ns[0].isalpha():
229
- ns = "w" + ns
230
- # Truncate to 64 chars and add unique suffix
231
- ns = ns[:56]
232
- suffix = uuid.uuid4().hex[:7]
233
- return f"{ns}-{suffix}"
234
-
235
-
236
224
  def _workflow_to_response(
237
225
  workflow: dict, steps: list[dict], pdb: Optional[ProjectDatabase] = None
238
226
  ) -> WorkflowResponse:
@@ -360,7 +348,6 @@ def _workflow_to_response(
360
348
  id=workflow["id"],
361
349
  template_id=workflow.get("template_id"),
362
350
  name=workflow["name"],
363
- namespace=workflow["namespace"],
364
351
  status=workflow["status"],
365
352
  current_step=workflow["current_step"],
366
353
  created_at=workflow["created_at"],
@@ -505,9 +492,8 @@ async def create_workflow(slug: str, request: CreateWorkflowRequest):
505
492
  """
506
493
  pdb = _get_project_db(slug)
507
494
 
508
- # Generate unique ID and namespace
495
+ # Generate unique ID
509
496
  workflow_id = f"wf-{uuid.uuid4().hex[:12]}"
510
- namespace = _generate_namespace(request.name)
511
497
 
512
498
  # Get template steps if template specified (templates still use "phases" internally)
513
499
  template_steps = []
@@ -525,7 +511,6 @@ async def create_workflow(slug: str, request: CreateWorkflowRequest):
525
511
  workflow = pdb.create_workflow(
526
512
  id=workflow_id,
527
513
  name=request.name,
528
- namespace=namespace,
529
514
  template_id=request.template_id,
530
515
  status="draft",
531
516
  )
@@ -1068,9 +1053,12 @@ async def create_step(slug: str, workflow_id: str, request: CreateStepRequest):
1068
1053
  )
1069
1054
 
1070
1055
  # Build config - include autonomous settings only for autonomous steps
1056
+ # Note: Strip template to normalize whitespace; empty/whitespace-only becomes None
1057
+ stripped_template = request.template.strip() if request.template else None
1071
1058
  config: dict[str, Any] = {
1072
1059
  "description": request.description,
1073
1060
  "loopType": request.loop_type,
1061
+ "template": stripped_template if stripped_template else None,
1074
1062
  "skippable": request.skippable,
1075
1063
  }
1076
1064
 
@@ -1172,6 +1160,10 @@ async def update_step(slug: str, workflow_id: str, step_id: int, request: Update
1172
1160
  config_updates["description"] = request.description
1173
1161
  if request.loop_type is not None:
1174
1162
  config_updates["loopType"] = request.loop_type
1163
+ if request.template is not None:
1164
+ # Empty string or whitespace-only means clear template, otherwise store stripped value
1165
+ stripped_template = request.template.strip()
1166
+ config_updates["template"] = stripped_template if stripped_template else None
1175
1167
  if request.skippable is not None:
1176
1168
  config_updates["skippable"] = request.skippable
1177
1169
 
@@ -1190,6 +1182,13 @@ async def update_step(slug: str, workflow_id: str, step_id: int, request: Update
1190
1182
  config_updates["cooldown_between_iterations"] = request.cooldown_between_iterations
1191
1183
  if request.max_consecutive_errors is not None:
1192
1184
  config_updates["max_consecutive_errors"] = request.max_consecutive_errors
1185
+ # Custom prompt
1186
+ if request.custom_prompt is not None:
1187
+ # Empty string means clear custom prompt
1188
+ if request.custom_prompt.strip():
1189
+ config_updates["customPrompt"] = request.custom_prompt
1190
+ else:
1191
+ config_updates["customPrompt"] = None
1193
1192
  elif request.step_type == "interactive":
1194
1193
  # Changing from autonomous to interactive: clear autonomous-only config
1195
1194
  config_updates["model"] = None
@@ -1198,6 +1197,7 @@ async def update_step(slug: str, workflow_id: str, step_id: int, request: Update
1198
1197
  config_updates["max_iterations"] = None
1199
1198
  config_updates["cooldown_between_iterations"] = None
1200
1199
  config_updates["max_consecutive_errors"] = None
1200
+ config_updates["customPrompt"] = None
1201
1201
 
1202
1202
  if config_updates:
1203
1203
  current_config = step.get("config") or {}
ralphx/cli.py CHANGED
@@ -1315,16 +1315,31 @@ def list_imports(
1315
1315
  def mcp_server() -> None:
1316
1316
  """Start the MCP server for Claude Code integration.
1317
1317
 
1318
- This command starts the MCP (Model Context Protocol) server that allows
1319
- Claude Code to interact with RalphX projects, loops, and work items.
1318
+ This command starts the MCP (Model Context Protocol) server that exposes
1319
+ 67 tools for full RalphX management through Claude Code.
1320
1320
 
1321
1321
  To add RalphX to Claude Code, run:
1322
- claude mcp add ralphx -- ralphx mcp
1322
+ Linux/Mac: claude mcp add ralphx -e PYTHONDONTWRITEBYTECODE=1 -- "$(which ralphx)" mcp
1323
+ Mac (zsh): if "which" fails, first run: conda init zsh && source ~/.zshrc
1324
+ Windows: find path with "where.exe ralphx", then:
1325
+ claude mcp add ralphx -e PYTHONDONTWRITEBYTECODE=1 -- C:\\path\\to\\ralphx.exe mcp
1323
1326
 
1324
1327
  Then in Claude Code, you can ask Claude to:
1325
- - List your RalphX projects
1326
- - Start/stop loops
1327
- - View and manage work items
1328
+ - Manage projects (add, remove, list, diagnose)
1329
+ - Control loops (start, stop, configure, validate)
1330
+ - Create and run workflows (multi-step task pipelines)
1331
+ - Track work items (user stories, tasks, research notes)
1332
+ - Monitor runs and view logs
1333
+ - Set up permissions and guardrails
1334
+ - Import content and manage resources
1335
+ - Run system health checks and diagnostics
1336
+
1337
+ Example prompts:
1338
+ "List my RalphX projects"
1339
+ "Start the planning loop on my-app"
1340
+ "Create a workflow for implementing the auth feature"
1341
+ "Why did the last run fail?"
1342
+ "Check if my system is set up correctly"
1328
1343
  """
1329
1344
  from ralphx.mcp_server import main as mcp_main
1330
1345