vibe-fabric 0.3.2 → 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.
- package/.claude/prompts/analyze/assess.md +208 -0
- package/.claude/prompts/analyze/synthesize.md +239 -0
- package/.claude/prompts/analyze/task-analyze-repo.md +260 -0
- package/.claude/prompts/gaps/assess.md +203 -0
- package/.claude/prompts/gaps/synthesize.md +180 -0
- package/.claude/prompts/gaps/task-analyze-module.md +198 -0
- package/.claude/prompts/prd/assess.md +156 -0
- package/.claude/prompts/prd/synthesize.md +191 -0
- package/.claude/prompts/prd/task-capture-feature.md +133 -0
- package/.claude/prompts/prd/task-deprecate.md +84 -0
- package/.claude/prompts/prd/task-document-code.md +110 -0
- package/.claude/prompts/prd/task-fix-format.md +102 -0
- package/.claude/prompts/prd/task-update-status.md +89 -0
- package/.claude/prompts/scope/assess.md +201 -0
- package/.claude/prompts/scope/synthesize.md +270 -0
- package/.claude/prompts/scope/task-analyze-prd.md +125 -0
- package/.claude/prompts/scope/task-check-deps.md +188 -0
- package/.claude/prompts/scope/task-create-scope.md +207 -0
- package/.claude/prompts/scope/task-recommend.md +146 -0
- package/.claude/prompts/scope/task-review-scope.md +191 -0
- package/.claude/prompts/scope/task-validate.md +203 -0
- package/dist/cli/commands/analyze.d.ts +33 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +243 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/config/get.d.ts +9 -0
- package/dist/cli/commands/config/get.d.ts.map +1 -0
- package/dist/cli/commands/config/get.js +69 -0
- package/dist/cli/commands/config/get.js.map +1 -0
- package/dist/cli/commands/config/list.d.ts +24 -0
- package/dist/cli/commands/config/list.d.ts.map +1 -0
- package/dist/cli/commands/config/list.js +146 -0
- package/dist/cli/commands/config/list.js.map +1 -0
- package/dist/cli/commands/config/set.d.ts +14 -0
- package/dist/cli/commands/config/set.d.ts.map +1 -0
- package/dist/cli/commands/config/set.js +111 -0
- package/dist/cli/commands/config/set.js.map +1 -0
- package/dist/cli/commands/repo/list.d.ts +26 -0
- package/dist/cli/commands/repo/list.d.ts.map +1 -0
- package/dist/cli/commands/repo/list.js +197 -0
- package/dist/cli/commands/repo/list.js.map +1 -0
- package/dist/cli/commands/repo/remove.d.ts +29 -0
- package/dist/cli/commands/repo/remove.d.ts.map +1 -0
- package/dist/cli/commands/repo/remove.js +219 -0
- package/dist/cli/commands/repo/remove.js.map +1 -0
- package/dist/cli/commands/report.d.ts +16 -0
- package/dist/cli/commands/report.d.ts.map +1 -0
- package/dist/cli/commands/report.js +160 -0
- package/dist/cli/commands/report.js.map +1 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +25 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +77 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/project.d.ts.map +1 -1
- package/dist/core/project.js +49 -1
- package/dist/core/project.js.map +1 -1
- package/dist/core/repo/templates/claude-agents.d.ts.map +1 -1
- package/dist/core/repo/templates/claude-agents.js +136 -28
- package/dist/core/repo/templates/claude-agents.js.map +1 -1
- package/dist/core/repo/templates/claude-prompts.d.ts +1 -1
- package/dist/core/repo/templates/claude-prompts.d.ts.map +1 -1
- package/dist/core/repo/templates/claude-prompts.js +412 -157
- package/dist/core/repo/templates/claude-prompts.js.map +1 -1
- package/dist/core/repo/templates/claude-scripts.d.ts.map +1 -1
- package/dist/core/repo/templates/claude-scripts.js +555 -94
- package/dist/core/repo/templates/claude-scripts.js.map +1 -1
- package/dist/core/report.d.ts +25 -0
- package/dist/core/report.d.ts.map +1 -0
- package/dist/core/report.js +702 -0
- package/dist/core/report.js.map +1 -0
- package/dist/types/report.d.ts +158 -0
- package/dist/types/report.d.ts.map +1 -0
- package/dist/types/report.js +7 -0
- package/dist/types/report.js.map +1 -0
- package/package.json +2 -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
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
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
|
-
#
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
72
|
-
return
|
|
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
|
|
108
|
+
"""Save state atomically using temp file + rename."""
|
|
77
109
|
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
STATE_FILE.
|
|
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
|
|
82
|
-
"""
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
state
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
524
|
+
if not state:
|
|
525
|
+
console.print("[dim]Ralph is not active[/dim]")
|
|
526
|
+
return
|
|
139
527
|
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
# 3. Create the plan file
|
|
582
|
+
def cancel_session() -> None:
|
|
583
|
+
"""Cancel active Ralph session."""
|
|
584
|
+
state = load_state()
|
|
164
585
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
174
|
-
console.print("[
|
|
598
|
+
if not state:
|
|
599
|
+
console.print("[red]No session to resume[/red]")
|
|
175
600
|
return
|
|
176
601
|
|
|
177
|
-
state.
|
|
178
|
-
|
|
179
|
-
|
|
602
|
+
if state.phase == "complete":
|
|
603
|
+
console.print("[dim]Session already complete[/dim]")
|
|
604
|
+
return
|
|
180
605
|
|
|
181
|
-
console.print("[
|
|
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(
|
|
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("--
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|