vibe-fabric 0.3.3 → 0.4.0

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 (53) hide show
  1. package/dist/cli/commands/analyze.d.ts +33 -0
  2. package/dist/cli/commands/analyze.d.ts.map +1 -0
  3. package/dist/cli/commands/analyze.js +243 -0
  4. package/dist/cli/commands/analyze.js.map +1 -0
  5. package/dist/cli/commands/config/get.d.ts +9 -0
  6. package/dist/cli/commands/config/get.d.ts.map +1 -0
  7. package/dist/cli/commands/config/get.js +69 -0
  8. package/dist/cli/commands/config/get.js.map +1 -0
  9. package/dist/cli/commands/config/list.d.ts +24 -0
  10. package/dist/cli/commands/config/list.d.ts.map +1 -0
  11. package/dist/cli/commands/config/list.js +146 -0
  12. package/dist/cli/commands/config/list.js.map +1 -0
  13. package/dist/cli/commands/config/set.d.ts +14 -0
  14. package/dist/cli/commands/config/set.d.ts.map +1 -0
  15. package/dist/cli/commands/config/set.js +111 -0
  16. package/dist/cli/commands/config/set.js.map +1 -0
  17. package/dist/cli/commands/repo/list.d.ts +26 -0
  18. package/dist/cli/commands/repo/list.d.ts.map +1 -0
  19. package/dist/cli/commands/repo/list.js +197 -0
  20. package/dist/cli/commands/repo/list.js.map +1 -0
  21. package/dist/cli/commands/repo/remove.d.ts +29 -0
  22. package/dist/cli/commands/repo/remove.d.ts.map +1 -0
  23. package/dist/cli/commands/repo/remove.js +219 -0
  24. package/dist/cli/commands/repo/remove.js.map +1 -0
  25. package/dist/cli/commands/report.d.ts +16 -0
  26. package/dist/cli/commands/report.d.ts.map +1 -0
  27. package/dist/cli/commands/report.js +160 -0
  28. package/dist/cli/commands/report.js.map +1 -0
  29. package/dist/cli/index.js +14 -0
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/core/config.d.ts +25 -0
  32. package/dist/core/config.d.ts.map +1 -1
  33. package/dist/core/config.js +77 -0
  34. package/dist/core/config.js.map +1 -1
  35. package/dist/core/repo/templates/claude-agents.d.ts.map +1 -1
  36. package/dist/core/repo/templates/claude-agents.js +136 -28
  37. package/dist/core/repo/templates/claude-agents.js.map +1 -1
  38. package/dist/core/repo/templates/claude-prompts.d.ts +1 -1
  39. package/dist/core/repo/templates/claude-prompts.d.ts.map +1 -1
  40. package/dist/core/repo/templates/claude-prompts.js +412 -157
  41. package/dist/core/repo/templates/claude-prompts.js.map +1 -1
  42. package/dist/core/repo/templates/claude-scripts.d.ts.map +1 -1
  43. package/dist/core/repo/templates/claude-scripts.js +555 -94
  44. package/dist/core/repo/templates/claude-scripts.js.map +1 -1
  45. package/dist/core/report.d.ts +25 -0
  46. package/dist/core/report.d.ts.map +1 -0
  47. package/dist/core/report.js +702 -0
  48. package/dist/core/report.js.map +1 -0
  49. package/dist/types/report.d.ts +158 -0
  50. package/dist/types/report.d.ts.map +1 -0
  51. package/dist/types/report.js +7 -0
  52. package/dist/types/report.js.map +1 -0
  53. package/package.json +1 -1
@@ -15,191 +15,652 @@ Ralph Runner - Autonomous multi-session executor for vibe-fabric.
15
15
 
16
16
  This script manages autonomous execution loops for implementing plans
17
17
  or creating plans from scopes. It handles:
18
- - Session state persistence
19
- - Progress tracking
20
- - Error recovery
21
- - Notification hooks
18
+ - One task per Claude session to prevent context exhaustion
19
+ - Session state persistence with atomic writes
20
+ - Progress tracking and resume capability
21
+ - Error recovery with retry logic
22
+ - Blocker detection (PLAN_BLOCKED marker)
22
23
 
23
24
  Usage:
24
25
  uv run ralph-runner.py --mode execute --plan PLAN-ID --loop
25
26
  uv run ralph-runner.py --mode plan --scope SCOPE-ID --loop
26
27
  uv run ralph-runner.py --status
27
28
  uv run ralph-runner.py --cancel
29
+ uv run ralph-runner.py --resume --loop
28
30
  """
29
31
 
30
32
  import argparse
31
33
  import json
32
34
  import os
35
+ import re
36
+ import shutil
37
+ import subprocess
33
38
  import sys
39
+ import time
34
40
  from datetime import datetime
35
41
  from pathlib import Path
36
- from typing import Optional
42
+ from typing import Optional, Literal
37
43
 
38
44
  try:
39
45
  from pydantic import BaseModel
40
46
  from rich.console import Console
41
47
  from rich.panel import Panel
48
+ from rich.progress import Progress, SpinnerColumn, TextColumn
49
+ from rich.table import Table
42
50
  except ImportError:
43
51
  print("Dependencies not available. Run with: uv run ralph-runner.py")
44
52
  sys.exit(1)
45
53
 
46
54
  console = Console()
47
55
 
48
- # State file location
56
+ # Configuration
49
57
  STATE_FILE = Path(".claude/ralph-state.json")
58
+ PROMPT_FILE = Path(".claude/ralph-prompt.local.md")
59
+ CLAUDE_CACHE_DIR = Path.home() / ".claude" / "cache" / "conversations"
60
+ SESSION_TIMEOUT = 1800 # 30 minutes
61
+ INTER_SESSION_DELAY = 3 # seconds between sessions
62
+ MAX_RETRIES = 1 # retry failed tasks once
63
+
64
+
65
+ class TaskState(BaseModel):
66
+ """State for a single task in the plan."""
67
+ id: int
68
+ status: Literal["pending", "in_progress", "completed", "blocked"]
69
+ started_at: Optional[str] = None
70
+ completed_at: Optional[str] = None
71
+ error: Optional[str] = None
72
+ retries: int = 0
50
73
 
51
74
 
52
75
  class RalphState(BaseModel):
53
76
  """Persistent state for Ralph sessions."""
54
- active: bool = False
55
- mode: str = "execute" # execute or plan
56
- target_id: str = "" # PLAN-ID or SCOPE-ID
57
- session_number: int = 0
58
- started_at: Optional[str] = None
77
+ mode: Literal["execute", "plan"]
78
+ plan_id: Optional[str] = None
79
+ plan_path: Optional[str] = None
80
+ scope_id: Optional[str] = None
81
+ scope_path: Optional[str] = None
82
+ started_at: str
83
+ phase: Literal[
84
+ "research_pending", "executing", "cleanup_pending", "complete",
85
+ "extraction_pending", "planning", "validation_pending"
86
+ ]
87
+ current_task: int = 0
88
+ total_tasks: int = 0
89
+ research_complete: bool = False
90
+ tasks: list[TaskState] = []
59
91
  last_session_at: Optional[str] = None
60
92
  sessions_completed: int = 0
61
- status: str = "idle" # idle, running, paused, completed, failed
93
+ modules: list[str] = [] # For plan mode: identified modules
62
94
 
63
95
 
64
- def load_state() -> RalphState:
96
+ def load_state() -> Optional[RalphState]:
65
97
  """Load state from file."""
66
98
  if STATE_FILE.exists():
67
99
  try:
68
- data = json.loads(STATE_FILE.read_text())
69
- return RalphState(**data)
70
- except Exception:
71
- return RalphState()
72
- return RalphState()
100
+ return RalphState.model_validate_json(STATE_FILE.read_text())
101
+ except Exception as e:
102
+ console.print(f"[red]Error loading state: {e}[/red]")
103
+ return None
104
+ return None
73
105
 
74
106
 
75
107
  def save_state(state: RalphState) -> None:
76
- """Save state to file."""
108
+ """Save state atomically using temp file + rename."""
77
109
  STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
78
- STATE_FILE.write_text(state.model_dump_json(indent=2))
110
+ temp_file = STATE_FILE.with_suffix('.tmp')
111
+ temp_file.write_text(state.model_dump_json(indent=2))
112
+ temp_file.rename(STATE_FILE)
79
113
 
80
114
 
81
- def show_status() -> None:
82
- """Display current Ralph status."""
83
- state = load_state()
115
+ def delete_state() -> None:
116
+ """Clean up state and temp files."""
117
+ if STATE_FILE.exists():
118
+ STATE_FILE.unlink()
119
+ if PROMPT_FILE.exists():
120
+ PROMPT_FILE.unlink()
84
121
 
85
- if not state.active:
86
- console.print("[dim]Ralph is not active[/dim]")
122
+
123
+ def clear_claude_cache() -> None:
124
+ """Clear Claude conversation cache for fresh context (REQ-2)."""
125
+ if CLAUDE_CACHE_DIR.exists():
126
+ try:
127
+ shutil.rmtree(CLAUDE_CACHE_DIR)
128
+ console.print("[dim]Cleared Claude cache for fresh context[/dim]")
129
+ except Exception as e:
130
+ console.print(f"[yellow]Warning: Could not clear cache: {e}[/yellow]")
131
+
132
+
133
+ def load_prompt_template(name: str) -> str:
134
+ """Load a prompt template from .claude/prompts/."""
135
+ prompt_path = Path(f".claude/prompts/{name}.md")
136
+ if not prompt_path.exists():
137
+ raise FileNotFoundError(f"Prompt template not found: {prompt_path}")
138
+ return prompt_path.read_text()
139
+
140
+
141
+ def count_tasks_in_plan(plan_path: str) -> int:
142
+ """Count implementation tasks/phases in a plan file."""
143
+ try:
144
+ content = Path(plan_path).read_text()
145
+ # Count phase/task markers in the plan
146
+ phases = re.findall(r'^##+ (?:Phase|Task|Step) \\d+', content, re.MULTILINE)
147
+ if phases:
148
+ return len(phases)
149
+ # Fallback: count numbered list items in Implementation Steps section
150
+ impl_match = re.search(r'## Implementation Steps\\n(.*?)(?=\\n## |$)', content, re.DOTALL)
151
+ if impl_match:
152
+ steps = re.findall(r'^\\d+\\.', impl_match.group(1), re.MULTILINE)
153
+ return len(steps) if steps else 5
154
+ return 5 # Default estimate
155
+ except Exception:
156
+ return 5
157
+
158
+
159
+ def check_blocked(plan_path: str) -> Optional[str]:
160
+ """Check if plan has PLAN_BLOCKED marker. Returns blocker description if found."""
161
+ try:
162
+ content = Path(plan_path).read_text()
163
+ match = re.search(r'PLAN_BLOCKED:\\s*(.+?)(?:\\n|$)', content)
164
+ if match:
165
+ return match.group(1).strip()
166
+ return None
167
+ except Exception:
168
+ return None
169
+
170
+
171
+ def generate_execute_prompt(state: RalphState) -> str:
172
+ """Generate prompt for execute mode based on current phase."""
173
+ if state.phase == "research_pending":
174
+ template = load_prompt_template("ralph-research")
175
+ return template.replace("{PLAN_PATH}", state.plan_path or "")
176
+ elif state.phase == "executing":
177
+ template = load_prompt_template("ralph-execution")
178
+ return (template
179
+ .replace("{PLAN_PATH}", state.plan_path or "")
180
+ .replace("{CURRENT_TASK}", str(state.current_task))
181
+ .replace("{TOTAL_TASKS}", str(state.total_tasks)))
182
+ elif state.phase == "cleanup_pending":
183
+ template = load_prompt_template("ralph-cleanup")
184
+ return template.replace("{PLAN_PATH}", state.plan_path or "")
185
+ return ""
186
+
187
+
188
+ def generate_plan_prompt(state: RalphState) -> str:
189
+ """Generate prompt for plan mode based on current phase."""
190
+ if state.phase == "extraction_pending":
191
+ template = load_prompt_template("ralph-plan-extraction")
192
+ return template.replace("{SCOPE_PATH}", state.scope_path or "")
193
+ elif state.phase == "planning":
194
+ template = load_prompt_template("ralph-plan-module")
195
+ module_name = state.modules[state.current_task - 1] if state.current_task <= len(state.modules) else "unknown"
196
+ return (template
197
+ .replace("{SCOPE_PATH}", state.scope_path or "")
198
+ .replace("{MODULE_INDEX}", str(state.current_task))
199
+ .replace("{MODULE_NAME}", module_name))
200
+ elif state.phase == "validation_pending":
201
+ template = load_prompt_template("ralph-plan-validation")
202
+ return template.replace("{SCOPE_PATH}", state.scope_path or "")
203
+ return ""
204
+
205
+
206
+ def run_claude_session(prompt: str) -> tuple[bool, str]:
207
+ """
208
+ Run a Claude Code session with the given prompt.
209
+ Returns (success, output).
210
+ """
211
+ # Write prompt to file for reference
212
+ PROMPT_FILE.parent.mkdir(parents=True, exist_ok=True)
213
+ PROMPT_FILE.write_text(prompt)
214
+
215
+ try:
216
+ # Run Claude with --print flag to capture output
217
+ result = subprocess.run(
218
+ ["claude", "--print", "-p", prompt],
219
+ capture_output=True,
220
+ text=True,
221
+ timeout=SESSION_TIMEOUT
222
+ )
223
+ output = result.stdout + result.stderr
224
+ return result.returncode == 0, output
225
+ except subprocess.TimeoutExpired:
226
+ return False, f"Session timed out after {SESSION_TIMEOUT}s"
227
+ except FileNotFoundError:
228
+ return False, "Claude CLI not found. Install Claude Code first."
229
+ except Exception as e:
230
+ return False, str(e)
231
+
232
+
233
+ def run_execute_loop(state: RalphState, loop: bool) -> None:
234
+ """Run the execution loop for execute mode."""
235
+ while True:
236
+ console.print(f"\\n[bold cyan]━━━ Session {state.sessions_completed + 1} ━━━[/bold cyan]")
237
+ console.print(f"Phase: [yellow]{state.phase}[/yellow], Task: {state.current_task}/{state.total_tasks}")
238
+
239
+ # Clear cache for fresh context (REQ-2)
240
+ clear_claude_cache()
241
+
242
+ # Generate prompt based on phase
243
+ try:
244
+ prompt = generate_execute_prompt(state)
245
+ except FileNotFoundError as e:
246
+ console.print(f"[red]Error: {e}[/red]")
247
+ break
248
+
249
+ if not prompt:
250
+ console.print("[red]No prompt generated for phase[/red]")
251
+ break
252
+
253
+ # Run Claude session with spinner
254
+ with Progress(
255
+ SpinnerColumn(),
256
+ TextColumn("[progress.description]{task.description}"),
257
+ console=console,
258
+ ) as progress:
259
+ desc = {
260
+ "research_pending": "Gathering research context...",
261
+ "executing": f"Executing task {state.current_task}...",
262
+ "cleanup_pending": "Running cleanup and verification...",
263
+ }.get(state.phase, "Running session...")
264
+ task = progress.add_task(desc, total=None)
265
+ success, output = run_claude_session(prompt)
266
+
267
+ if not success:
268
+ console.print(f"[red]Session failed: {output}[/red]")
269
+
270
+ # Retry logic for task execution
271
+ if state.phase == "executing" and state.current_task <= len(state.tasks):
272
+ current = state.tasks[state.current_task - 1]
273
+ if current.retries < MAX_RETRIES:
274
+ current.retries += 1
275
+ current.error = output
276
+ save_state(state)
277
+ console.print(f"[yellow]Retrying task {state.current_task} (attempt {current.retries + 1})...[/yellow]")
278
+ time.sleep(INTER_SESSION_DELAY)
279
+ continue
280
+ else:
281
+ current.status = "blocked"
282
+ current.error = output
283
+ save_state(state)
284
+ console.print(f"[red]Task {state.current_task} failed after retries[/red]")
285
+ break
286
+
287
+ # Update state based on phase
288
+ state.sessions_completed += 1
289
+ state.last_session_at = datetime.now().isoformat()
290
+
291
+ if state.phase == "research_pending":
292
+ state.research_complete = True
293
+ state.phase = "executing"
294
+ state.current_task = 1
295
+ if state.tasks:
296
+ state.tasks[0].status = "in_progress"
297
+ state.tasks[0].started_at = datetime.now().isoformat()
298
+ console.print("[green]Research complete. Starting task execution.[/green]")
299
+
300
+ elif state.phase == "executing":
301
+ # Mark current task complete
302
+ if state.tasks and state.current_task <= len(state.tasks):
303
+ state.tasks[state.current_task - 1].status = "completed"
304
+ state.tasks[state.current_task - 1].completed_at = datetime.now().isoformat()
305
+
306
+ # Check for blockers
307
+ if state.plan_path:
308
+ blocker = check_blocked(state.plan_path)
309
+ if blocker:
310
+ console.print(f"[red]PLAN_BLOCKED detected: {blocker}[/red]")
311
+ console.print("[yellow]Pausing execution. Resolve blocker and use --resume.[/yellow]")
312
+ save_state(state)
313
+ break
314
+
315
+ # Move to next task or cleanup
316
+ if state.current_task >= state.total_tasks:
317
+ state.phase = "cleanup_pending"
318
+ console.print("[green]All tasks complete. Starting cleanup phase.[/green]")
319
+ else:
320
+ state.current_task += 1
321
+ if state.current_task <= len(state.tasks):
322
+ state.tasks[state.current_task - 1].status = "in_progress"
323
+ state.tasks[state.current_task - 1].started_at = datetime.now().isoformat()
324
+ console.print(f"[dim]Moving to task {state.current_task}[/dim]")
325
+
326
+ elif state.phase == "cleanup_pending":
327
+ state.phase = "complete"
328
+ console.print("[bold green]Ralph execution complete![/bold green]")
329
+
330
+ # Move plan to completed folder
331
+ if state.plan_path:
332
+ plan_path = Path(state.plan_path)
333
+ completed_dir = plan_path.parent.parent / "completed"
334
+ if completed_dir.exists() and plan_path.exists():
335
+ dest = completed_dir / plan_path.name
336
+ plan_path.rename(dest)
337
+ console.print(f"[dim]Plan moved to {dest}[/dim]")
338
+
339
+ delete_state()
340
+ break
341
+
342
+ save_state(state)
343
+
344
+ if not loop:
345
+ console.print("[dim]Single step mode. Exiting. Use --resume --loop to continue.[/dim]")
346
+ break
347
+
348
+ # Delay between sessions
349
+ console.print(f"[dim]Waiting {INTER_SESSION_DELAY}s before next session...[/dim]")
350
+ time.sleep(INTER_SESSION_DELAY)
351
+
352
+
353
+ def run_plan_loop(state: RalphState, loop: bool) -> None:
354
+ """Run the planning loop for plan mode."""
355
+ while True:
356
+ console.print(f"\\n[bold cyan]━━━ Session {state.sessions_completed + 1} ━━━[/bold cyan]")
357
+ console.print(f"Phase: [yellow]{state.phase}[/yellow]")
358
+
359
+ # Clear cache for fresh context
360
+ clear_claude_cache()
361
+
362
+ # Generate prompt based on phase
363
+ try:
364
+ prompt = generate_plan_prompt(state)
365
+ except FileNotFoundError as e:
366
+ console.print(f"[red]Error: {e}[/red]")
367
+ break
368
+
369
+ if not prompt:
370
+ console.print("[red]No prompt generated for phase[/red]")
371
+ break
372
+
373
+ # Run Claude session
374
+ with Progress(
375
+ SpinnerColumn(),
376
+ TextColumn("[progress.description]{task.description}"),
377
+ console=console,
378
+ ) as progress:
379
+ desc = {
380
+ "extraction_pending": "Extracting modules from scope...",
381
+ "planning": f"Planning module {state.current_task}...",
382
+ "validation_pending": "Validating plan coverage...",
383
+ }.get(state.phase, "Running session...")
384
+ task = progress.add_task(desc, total=None)
385
+ success, output = run_claude_session(prompt)
386
+
387
+ if not success:
388
+ console.print(f"[red]Session failed: {output}[/red]")
389
+ break
390
+
391
+ # Update state based on phase
392
+ state.sessions_completed += 1
393
+ state.last_session_at = datetime.now().isoformat()
394
+
395
+ if state.phase == "extraction_pending":
396
+ # Parse modules from output (simplified)
397
+ state.phase = "planning"
398
+ state.current_task = 1
399
+ state.total_tasks = len(state.modules) if state.modules else 3
400
+ console.print(f"[green]Extraction complete. Planning {state.total_tasks} modules.[/green]")
401
+
402
+ elif state.phase == "planning":
403
+ if state.current_task >= state.total_tasks:
404
+ state.phase = "validation_pending"
405
+ console.print("[green]Module planning complete. Starting validation.[/green]")
406
+ else:
407
+ state.current_task += 1
408
+ console.print(f"[dim]Moving to module {state.current_task}[/dim]")
409
+
410
+ elif state.phase == "validation_pending":
411
+ state.phase = "complete"
412
+ console.print("[bold green]Ralph planning complete![/bold green]")
413
+ delete_state()
414
+ break
415
+
416
+ save_state(state)
417
+
418
+ if not loop:
419
+ console.print("[dim]Single step mode. Exiting.[/dim]")
420
+ break
421
+
422
+ time.sleep(INTER_SESSION_DELAY)
423
+
424
+
425
+ def find_plan_path(plan_id: str) -> Optional[str]:
426
+ """Find the plan file path from ID."""
427
+ # Check active plans first
428
+ active_path = Path(f"vibe/implementation-plans/active/{plan_id}.md")
429
+ if active_path.exists():
430
+ return str(active_path)
431
+
432
+ # Check if it's a full path
433
+ if Path(plan_id).exists():
434
+ return plan_id
435
+
436
+ # Search for matching files
437
+ for pattern in ["vibe/implementation-plans/active/*.md", "vibe/implementation-plans/*.md"]:
438
+ for p in Path(".").glob(pattern):
439
+ if plan_id in p.name:
440
+ return str(p)
441
+
442
+ return None
443
+
444
+
445
+ def find_scope_path(scope_id: str) -> Optional[str]:
446
+ """Find the scope file path from ID."""
447
+ # Check incoming scopes
448
+ incoming_path = Path(f"vibe/incoming-scopes/{scope_id}.md")
449
+ if incoming_path.exists():
450
+ return str(incoming_path)
451
+
452
+ # Check if it's a full path
453
+ if Path(scope_id).exists():
454
+ return scope_id
455
+
456
+ # Search for matching files
457
+ for p in Path("vibe/incoming-scopes").glob("*.md"):
458
+ if scope_id in p.name:
459
+ return str(p)
460
+
461
+ return None
462
+
463
+
464
+ def start_execute(plan_id: str, loop: bool) -> None:
465
+ """Start execution mode for a plan."""
466
+ # Find plan file
467
+ plan_path = find_plan_path(plan_id)
468
+ if not plan_path:
469
+ console.print(f"[red]Plan not found: {plan_id}[/red]")
470
+ console.print("Expected location: vibe/implementation-plans/active/{plan_id}.md")
87
471
  return
88
472
 
89
- status_color = {
90
- "running": "green",
91
- "paused": "yellow",
92
- "completed": "blue",
93
- "failed": "red",
94
- }.get(state.status, "white")
473
+ # Count tasks
474
+ total_tasks = count_tasks_in_plan(plan_path)
95
475
 
96
- console.print(Panel(
97
- f"[bold]Mode:[/bold] {state.mode}\\n"
98
- f"[bold]Target:[/bold] {state.target_id}\\n"
99
- f"[bold]Status:[/bold] [{status_color}]{state.status}[/{status_color}]\\n"
100
- f"[bold]Sessions:[/bold] {state.sessions_completed}\\n"
101
- f"[bold]Started:[/bold] {state.started_at or 'N/A'}\\n"
102
- f"[bold]Last Session:[/bold] {state.last_session_at or 'N/A'}",
103
- title="Ralph Status",
104
- ))
476
+ # Create state
477
+ state = RalphState(
478
+ mode="execute",
479
+ plan_id=plan_id,
480
+ plan_path=plan_path,
481
+ started_at=datetime.now().isoformat(),
482
+ phase="research_pending",
483
+ total_tasks=total_tasks,
484
+ tasks=[TaskState(id=i+1, status="pending") for i in range(total_tasks)]
485
+ )
105
486
 
487
+ save_state(state)
488
+ console.print(f"[green]Starting Ralph execute for {plan_id}[/green]")
489
+ console.print(f"Plan: {plan_path}")
490
+ console.print(f"Tasks: {total_tasks}")
491
+
492
+ run_execute_loop(state, loop)
106
493
 
107
- def start_session(mode: str, target_id: str, loop: bool) -> None:
108
- """Start a Ralph session."""
109
- state = load_state()
110
494
 
111
- if state.active and state.status == "running":
112
- console.print("[red]Ralph is already running. Use --cancel first.[/red]")
495
+ def start_plan(scope_id: str, loop: bool) -> None:
496
+ """Start plan mode for a scope."""
497
+ # Find scope file
498
+ scope_path = find_scope_path(scope_id)
499
+ if not scope_path:
500
+ console.print(f"[red]Scope not found: {scope_id}[/red]")
501
+ console.print("Expected location: vibe/incoming-scopes/{scope_id}.md")
113
502
  return
114
503
 
115
- state.active = True
116
- state.mode = mode
117
- state.target_id = target_id
118
- state.status = "running"
119
- state.started_at = datetime.now().isoformat()
120
- state.session_number += 1
504
+ # Create state
505
+ state = RalphState(
506
+ mode="plan",
507
+ scope_id=scope_id,
508
+ scope_path=scope_path,
509
+ started_at=datetime.now().isoformat(),
510
+ phase="extraction_pending",
511
+ )
121
512
 
122
513
  save_state(state)
514
+ console.print(f"[green]Starting Ralph plan for {scope_id}[/green]")
515
+ console.print(f"Scope: {scope_path}")
123
516
 
124
- console.print(f"[green]Starting Ralph {mode} for {target_id}[/green]")
517
+ run_plan_loop(state, loop)
125
518
 
126
- if mode == "execute":
127
- run_execute_session(target_id, state.session_number)
128
- else:
129
- run_plan_session(target_id, state.session_number)
130
519
 
131
- # Update state after session
520
+ def show_status() -> None:
521
+ """Display current Ralph status with task details."""
132
522
  state = load_state()
133
- state.sessions_completed += 1
134
- state.last_session_at = datetime.now().isoformat()
135
523
 
136
- if not loop:
137
- state.active = False
138
- state.status = "completed"
524
+ if not state:
525
+ console.print("[dim]Ralph is not active[/dim]")
526
+ return
139
527
 
140
- save_state(state)
528
+ # Count task statuses
529
+ completed = sum(1 for t in state.tasks if t.status == "completed")
530
+ blocked = sum(1 for t in state.tasks if t.status == "blocked")
531
+ in_progress = sum(1 for t in state.tasks if t.status == "in_progress")
141
532
 
533
+ # Status color
534
+ status_color = {
535
+ "research_pending": "yellow",
536
+ "executing": "green",
537
+ "cleanup_pending": "blue",
538
+ "complete": "cyan",
539
+ "extraction_pending": "yellow",
540
+ "planning": "green",
541
+ "validation_pending": "blue",
542
+ }.get(state.phase, "white")
543
+
544
+ # Build task table
545
+ task_lines = []
546
+ for t in state.tasks[:15]: # Show first 15 tasks
547
+ icon = {
548
+ "completed": "[green]✓[/green]",
549
+ "in_progress": "[yellow]▶[/yellow]",
550
+ "blocked": "[red]✗[/red]",
551
+ "pending": "[dim]○[/dim]"
552
+ }[t.status]
553
+ task_lines.append(f" {icon} Task {t.id}: {t.status}")
554
+
555
+ if len(state.tasks) > 15:
556
+ task_lines.append(f" ... and {len(state.tasks) - 15} more tasks")
557
+
558
+ # Build panel
559
+ content = (
560
+ f"[bold]Mode:[/bold] {state.mode}\\n"
561
+ f"[bold]Target:[/bold] {state.plan_id or state.scope_id}\\n"
562
+ f"[bold]Path:[/bold] {state.plan_path or state.scope_path}\\n"
563
+ f"[bold]Phase:[/bold] [{status_color}]{state.phase}[/{status_color}]\\n"
564
+ f"[bold]Progress:[/bold] {completed}/{state.total_tasks} tasks"
565
+ )
142
566
 
143
- def run_execute_session(plan_id: str, session_number: int) -> None:
144
- """Run an execution session."""
145
- console.print(f"[cyan]Session {session_number}: Executing {plan_id}[/cyan]")
567
+ if blocked > 0:
568
+ content += f" [red]({blocked} blocked)[/red]"
146
569
 
147
- # In a real implementation, this would:
148
- # 1. Call Claude Code with the research agent
149
- # 2. Call Claude Code with execution prompts
150
- # 3. Call Claude Code with cleanup agent
570
+ content += (
571
+ f"\\n[bold]Sessions:[/bold] {state.sessions_completed}\\n"
572
+ f"[bold]Started:[/bold] {state.started_at}\\n"
573
+ f"[bold]Last Session:[/bold] {state.last_session_at or 'N/A'}"
574
+ )
151
575
 
152
- console.print("[yellow]Note: Ralph loop not fully implemented yet.[/yellow]")
153
- console.print("Use /implement-plan for interactive execution.")
576
+ if task_lines:
577
+ content += "\\n\\n[bold]Tasks:[/bold]\\n" + "\\n".join(task_lines)
154
578
 
579
+ console.print(Panel(content, title="Ralph Status"))
155
580
 
156
- def run_plan_session(scope_id: str, session_number: int) -> None:
157
- """Run a planning session."""
158
- console.print(f"[cyan]Session {session_number}: Planning from {scope_id}[/cyan]")
159
581
 
160
- # In a real implementation, this would:
161
- # 1. Read the scope
162
- # 2. Call Claude Code with planning prompts
163
- # 3. Create the plan file
582
+ def cancel_session() -> None:
583
+ """Cancel active Ralph session."""
584
+ state = load_state()
164
585
 
165
- console.print("[yellow]Note: Ralph loop not fully implemented yet.[/yellow]")
166
- console.print("Use /plan-from-scope for interactive planning.")
586
+ if not state:
587
+ console.print("[dim]No active Ralph session[/dim]")
588
+ return
167
589
 
590
+ delete_state()
591
+ console.print("[yellow]Ralph session cancelled[/yellow]")
168
592
 
169
- def cancel_session() -> None:
170
- """Cancel the active Ralph session."""
593
+
594
+ def resume_session(loop: bool) -> None:
595
+ """Resume an existing Ralph session."""
171
596
  state = load_state()
172
597
 
173
- if not state.active:
174
- console.print("[dim]No active Ralph session[/dim]")
598
+ if not state:
599
+ console.print("[red]No session to resume[/red]")
175
600
  return
176
601
 
177
- state.active = False
178
- state.status = "cancelled"
179
- save_state(state)
602
+ if state.phase == "complete":
603
+ console.print("[dim]Session already complete[/dim]")
604
+ return
180
605
 
181
- console.print("[yellow]Ralph session cancelled[/yellow]")
606
+ console.print(f"[green]Resuming {state.mode} for {state.plan_id or state.scope_id}[/green]")
607
+ console.print(f"Phase: {state.phase}, Progress: {state.current_task}/{state.total_tasks}")
608
+
609
+ if state.mode == "execute":
610
+ run_execute_loop(state, loop)
611
+ else:
612
+ run_plan_loop(state, loop)
182
613
 
183
614
 
184
615
  def main() -> None:
185
- parser = argparse.ArgumentParser(description="Ralph autonomous executor")
616
+ parser = argparse.ArgumentParser(
617
+ description="Ralph autonomous executor - Multi-session task execution for vibe-fabric",
618
+ formatter_class=argparse.RawDescriptionHelpFormatter,
619
+ epilog="""
620
+ Examples:
621
+ uv run ralph-runner.py --mode execute --plan PLAN-001 --loop
622
+ uv run ralph-runner.py --mode plan --scope SCOPE-042 --loop
623
+ uv run ralph-runner.py --status
624
+ uv run ralph-runner.py --resume --loop
625
+ uv run ralph-runner.py --cancel
626
+ """
627
+ )
186
628
  parser.add_argument("--mode", choices=["execute", "plan"], help="Execution mode")
187
629
  parser.add_argument("--plan", help="Plan ID for execute mode")
188
630
  parser.add_argument("--scope", help="Scope ID for plan mode")
189
- parser.add_argument("--loop", action="store_true", help="Run in loop mode")
190
- parser.add_argument("--status", action="store_true", help="Show status")
631
+ parser.add_argument("--loop", action="store_true", help="Run in loop mode until complete")
632
+ parser.add_argument("--step", action="store_true", help="Run single step only (debug)")
633
+ parser.add_argument("--status", action="store_true", help="Show current status")
191
634
  parser.add_argument("--cancel", action="store_true", help="Cancel active session")
635
+ parser.add_argument("--resume", action="store_true", help="Resume existing session")
192
636
 
193
637
  args = parser.parse_args()
194
638
 
639
+ # Handle --step as inverse of --loop
640
+ loop_mode = args.loop and not args.step
641
+
195
642
  if args.status:
196
643
  show_status()
197
644
  elif args.cancel:
198
645
  cancel_session()
646
+ elif args.resume:
647
+ resume_session(loop_mode)
199
648
  elif args.mode == "execute" and args.plan:
200
- start_session("execute", args.plan, args.loop)
649
+ # Check for existing session
650
+ existing = load_state()
651
+ if existing and existing.phase != "complete":
652
+ console.print("[yellow]Existing session found. Use --resume to continue or --cancel first.[/yellow]")
653
+ show_status()
654
+ return
655
+ start_execute(args.plan, loop_mode)
201
656
  elif args.mode == "plan" and args.scope:
202
- start_session("plan", args.scope, args.loop)
657
+ # Check for existing session
658
+ existing = load_state()
659
+ if existing and existing.phase != "complete":
660
+ console.print("[yellow]Existing session found. Use --resume to continue or --cancel first.[/yellow]")
661
+ show_status()
662
+ return
663
+ start_plan(args.scope, loop_mode)
203
664
  else:
204
665
  parser.print_help()
205
666