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.
- ralphx/__init__.py +1 -1
- ralphx/api/main.py +9 -1
- ralphx/api/routes/auth.py +730 -65
- ralphx/api/routes/config.py +3 -56
- ralphx/api/routes/export_import.py +795 -0
- ralphx/api/routes/loops.py +4 -4
- ralphx/api/routes/planning.py +19 -5
- ralphx/api/routes/projects.py +84 -2
- ralphx/api/routes/templates.py +115 -2
- ralphx/api/routes/workflows.py +22 -22
- ralphx/cli.py +21 -6
- ralphx/core/auth.py +346 -171
- ralphx/core/database.py +615 -167
- ralphx/core/executor.py +0 -3
- ralphx/core/loop.py +15 -2
- ralphx/core/loop_templates.py +69 -3
- ralphx/core/planning_service.py +109 -21
- ralphx/core/preview.py +9 -25
- ralphx/core/project_db.py +175 -75
- ralphx/core/project_export.py +469 -0
- ralphx/core/project_import.py +670 -0
- ralphx/core/sample_project.py +430 -0
- ralphx/core/templates.py +46 -9
- ralphx/core/workflow_executor.py +35 -5
- ralphx/core/workflow_export.py +606 -0
- ralphx/core/workflow_import.py +1149 -0
- ralphx/examples/sample_project/DESIGN.md +345 -0
- ralphx/examples/sample_project/README.md +37 -0
- ralphx/examples/sample_project/guardrails.md +57 -0
- ralphx/examples/sample_project/stories.jsonl +10 -0
- ralphx/mcp/__init__.py +6 -2
- ralphx/mcp/registry.py +3 -3
- ralphx/mcp/server.py +99 -29
- ralphx/mcp/tools/__init__.py +4 -0
- ralphx/mcp/tools/help.py +204 -0
- ralphx/mcp/tools/workflows.py +114 -32
- ralphx/mcp_server.py +6 -2
- ralphx/static/assets/index-0ovNnfOq.css +1 -0
- ralphx/static/assets/index-CY9s08ZB.js +251 -0
- ralphx/static/assets/index-CY9s08ZB.js.map +1 -0
- ralphx/static/index.html +14 -0
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/METADATA +34 -12
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/RECORD +45 -30
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/WHEEL +0 -0
- {ralphx-0.2.2.dist-info → ralphx-0.3.5.dist-info}/entry_points.txt +0 -0
ralphx/api/routes/loops.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
855
|
-
|
|
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
|
-
|
|
859
|
+
source_loop=source_loop_name,
|
|
860
860
|
display_name=display_name,
|
|
861
861
|
description=description,
|
|
862
862
|
)
|
ralphx/api/routes/planning.py
CHANGED
|
@@ -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(
|
|
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-{
|
|
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-{
|
|
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
|
|
ralphx/api/routes/projects.py
CHANGED
|
@@ -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,
|
ralphx/api/routes/templates.py
CHANGED
|
@@ -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 (
|
|
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., '
|
|
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
|
+
)
|
ralphx/api/routes/workflows.py
CHANGED
|
@@ -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
|
|
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
|
|
1319
|
-
|
|
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
|
-
-
|
|
1326
|
-
-
|
|
1327
|
-
-
|
|
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
|
|