code-puppy 0.0.374__py3-none-any.whl → 0.0.375__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.
- code_puppy/agents/agent_manager.py +34 -2
- code_puppy/agents/base_agent.py +61 -4
- code_puppy/callbacks.py +125 -0
- code_puppy/messaging/rich_renderer.py +13 -7
- code_puppy/model_factory.py +63 -258
- code_puppy/model_utils.py +33 -1
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
- code_puppy/plugins/antigravity_oauth/utils.py +2 -3
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
- code_puppy/plugins/ralph/__init__.py +13 -0
- code_puppy/plugins/ralph/agents.py +433 -0
- code_puppy/plugins/ralph/commands.py +208 -0
- code_puppy/plugins/ralph/loop_controller.py +285 -0
- code_puppy/plugins/ralph/models.py +125 -0
- code_puppy/plugins/ralph/register_callbacks.py +133 -0
- code_puppy/plugins/ralph/state_manager.py +322 -0
- code_puppy/plugins/ralph/tools.py +451 -0
- code_puppy/tools/__init__.py +31 -0
- code_puppy/tools/agent_tools.py +1 -1
- code_puppy/tools/command_runner.py +23 -9
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/RECORD +28 -20
- {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Ralph Plugin - Autonomous AI agent loop for completing PRDs.
|
|
2
|
+
|
|
3
|
+
This module registers all Ralph callbacks:
|
|
4
|
+
- register_tools: Ralph-specific tools for PRD management
|
|
5
|
+
- register_agents: PRD Generator, Converter, and Orchestrator agents
|
|
6
|
+
- custom_command: /ralph slash commands
|
|
7
|
+
- custom_command_help: Help entries for Ralph commands
|
|
8
|
+
- agent_response_complete: Detect completion signal for loop termination
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from code_puppy.callbacks import register_callback
|
|
15
|
+
from code_puppy.messaging import emit_info, emit_success
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# TOOL REGISTRATION
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _provide_tools() -> List[Dict[str, Any]]:
|
|
26
|
+
"""Provide Ralph tools via the register_tools callback."""
|
|
27
|
+
from .tools import get_ralph_tools
|
|
28
|
+
|
|
29
|
+
logger.debug("Ralph plugin: providing tools")
|
|
30
|
+
return get_ralph_tools()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# AGENT REGISTRATION
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _provide_agents() -> List[Dict[str, Any]]:
|
|
39
|
+
"""Provide Ralph agents via the register_agents callback."""
|
|
40
|
+
from .agents import get_ralph_agents
|
|
41
|
+
|
|
42
|
+
logger.debug("Ralph plugin: providing agents")
|
|
43
|
+
return get_ralph_agents()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# COMMAND REGISTRATION
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _provide_command_help() -> List[Tuple[str, str]]:
|
|
52
|
+
"""Provide help entries for Ralph commands."""
|
|
53
|
+
from .commands import get_ralph_help
|
|
54
|
+
|
|
55
|
+
return get_ralph_help()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _handle_command(command: str, name: str) -> Optional[Any]:
|
|
59
|
+
"""Handle /ralph commands."""
|
|
60
|
+
from .commands import handle_ralph_command
|
|
61
|
+
|
|
62
|
+
return handle_ralph_command(command, name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# COMPLETION DETECTION
|
|
67
|
+
# ============================================================================
|
|
68
|
+
|
|
69
|
+
# Track completion state for the loop controller
|
|
70
|
+
_ralph_completion_detected = False
|
|
71
|
+
_ralph_last_session_id: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_ralph_complete() -> bool:
|
|
75
|
+
"""Check if Ralph has signaled completion."""
|
|
76
|
+
return _ralph_completion_detected
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def reset_ralph_completion() -> None:
|
|
80
|
+
"""Reset the completion flag for a new run."""
|
|
81
|
+
global _ralph_completion_detected, _ralph_last_session_id
|
|
82
|
+
_ralph_completion_detected = False
|
|
83
|
+
_ralph_last_session_id = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _on_agent_complete(
|
|
87
|
+
agent_name: str,
|
|
88
|
+
response_text: str,
|
|
89
|
+
session_id: Optional[str] = None,
|
|
90
|
+
metadata: Optional[dict] = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Handle agent response completion.
|
|
93
|
+
|
|
94
|
+
This detects the <promise>COMPLETE</promise> signal from the
|
|
95
|
+
Ralph Orchestrator and sets the completion flag.
|
|
96
|
+
"""
|
|
97
|
+
global _ralph_completion_detected, _ralph_last_session_id
|
|
98
|
+
|
|
99
|
+
# Only track ralph-orchestrator completions
|
|
100
|
+
if agent_name != "ralph-orchestrator":
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
logger.debug(f"Ralph plugin: orchestrator completed (session={session_id})")
|
|
104
|
+
|
|
105
|
+
# Check for completion signal
|
|
106
|
+
if response_text and "<promise>COMPLETE</promise>" in response_text:
|
|
107
|
+
_ralph_completion_detected = True
|
|
108
|
+
_ralph_last_session_id = session_id
|
|
109
|
+
|
|
110
|
+
emit_success("🎉 Ralph has completed all user stories!")
|
|
111
|
+
emit_info("All tasks in prd.json are now marked as passes: true")
|
|
112
|
+
logger.info("Ralph completion signal detected - all stories complete")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# REGISTER ALL CALLBACKS
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
# Tools
|
|
120
|
+
register_callback("register_tools", _provide_tools)
|
|
121
|
+
|
|
122
|
+
# Agents
|
|
123
|
+
register_callback("register_agents", _provide_agents)
|
|
124
|
+
|
|
125
|
+
# Commands
|
|
126
|
+
register_callback("custom_command", _handle_command)
|
|
127
|
+
register_callback("custom_command_help", _provide_command_help)
|
|
128
|
+
|
|
129
|
+
# Completion detection
|
|
130
|
+
register_callback("agent_response_complete", _on_agent_complete)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
logger.info("Ralph plugin: all callbacks registered successfully")
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""State management for Ralph plugin - handles prd.json and progress.txt."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from .models import PRDConfig, ProgressEntry, UserStory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RalphStateManager:
|
|
13
|
+
"""Manages Ralph's persistent state files."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, working_dir: Optional[str] = None):
|
|
16
|
+
"""Initialize the state manager.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
working_dir: Directory containing prd.json and progress.txt.
|
|
20
|
+
Defaults to current working directory.
|
|
21
|
+
"""
|
|
22
|
+
self.working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
23
|
+
self.prd_file = self.working_dir / "prd.json"
|
|
24
|
+
self.progress_file = self.working_dir / "progress.txt"
|
|
25
|
+
self.archive_dir = self.working_dir / "archive"
|
|
26
|
+
self.last_branch_file = self.working_dir / ".ralph-last-branch"
|
|
27
|
+
|
|
28
|
+
# =========================================================================
|
|
29
|
+
# PRD.JSON OPERATIONS
|
|
30
|
+
# =========================================================================
|
|
31
|
+
|
|
32
|
+
def prd_exists(self) -> bool:
|
|
33
|
+
"""Check if prd.json exists."""
|
|
34
|
+
return self.prd_file.exists()
|
|
35
|
+
|
|
36
|
+
def read_prd(self) -> Optional[PRDConfig]:
|
|
37
|
+
"""Read and parse prd.json.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
PRDConfig if file exists and is valid, None otherwise.
|
|
41
|
+
"""
|
|
42
|
+
if not self.prd_file.exists():
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
with open(self.prd_file, "r", encoding="utf-8") as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
return PRDConfig.from_dict(data)
|
|
49
|
+
except (json.JSONDecodeError, IOError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def write_prd(self, prd: PRDConfig) -> bool:
|
|
53
|
+
"""Write PRDConfig to prd.json.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
prd: The PRD configuration to write.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if successful, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
with open(self.prd_file, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(prd.to_json(indent=2))
|
|
64
|
+
return True
|
|
65
|
+
except IOError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def get_next_story(self) -> Optional[UserStory]:
|
|
69
|
+
"""Get the next story to work on.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The highest priority story with passes=False, or None if all done.
|
|
73
|
+
"""
|
|
74
|
+
prd = self.read_prd()
|
|
75
|
+
if prd is None:
|
|
76
|
+
return None
|
|
77
|
+
return prd.get_next_story()
|
|
78
|
+
|
|
79
|
+
def mark_story_complete(self, story_id: str, notes: str = "") -> Tuple[bool, str]:
|
|
80
|
+
"""Mark a story as complete (passes=True).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
story_id: The ID of the story to mark complete.
|
|
84
|
+
notes: Optional notes to add to the story.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Tuple of (success, message).
|
|
88
|
+
"""
|
|
89
|
+
prd = self.read_prd()
|
|
90
|
+
if prd is None:
|
|
91
|
+
return False, "No prd.json found"
|
|
92
|
+
|
|
93
|
+
for story in prd.user_stories:
|
|
94
|
+
if story.id == story_id:
|
|
95
|
+
story.passes = True
|
|
96
|
+
if notes:
|
|
97
|
+
story.notes = notes
|
|
98
|
+
if self.write_prd(prd):
|
|
99
|
+
return True, f"Marked {story_id} as complete"
|
|
100
|
+
else:
|
|
101
|
+
return False, "Failed to write prd.json"
|
|
102
|
+
|
|
103
|
+
return False, f"Story {story_id} not found"
|
|
104
|
+
|
|
105
|
+
def all_stories_complete(self) -> bool:
|
|
106
|
+
"""Check if all stories are complete."""
|
|
107
|
+
prd = self.read_prd()
|
|
108
|
+
if prd is None:
|
|
109
|
+
return False
|
|
110
|
+
return prd.all_complete()
|
|
111
|
+
|
|
112
|
+
def get_status_summary(self) -> str:
|
|
113
|
+
"""Get a formatted status summary of the PRD."""
|
|
114
|
+
prd = self.read_prd()
|
|
115
|
+
if prd is None:
|
|
116
|
+
return "No prd.json found in current directory"
|
|
117
|
+
|
|
118
|
+
lines = [
|
|
119
|
+
f"📋 **Project:** {prd.project}",
|
|
120
|
+
f"🌿 **Branch:** {prd.branch_name}",
|
|
121
|
+
f"📝 **Description:** {prd.description}",
|
|
122
|
+
f"📊 **Progress:** {prd.get_progress_summary()}",
|
|
123
|
+
"",
|
|
124
|
+
"**Stories:**",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
for story in sorted(prd.user_stories, key=lambda s: s.priority):
|
|
128
|
+
status = "✅" if story.passes else "⏳"
|
|
129
|
+
lines.append(f" {status} [{story.id}] {story.title}")
|
|
130
|
+
|
|
131
|
+
return "\n".join(lines)
|
|
132
|
+
|
|
133
|
+
# =========================================================================
|
|
134
|
+
# PROGRESS.TXT OPERATIONS
|
|
135
|
+
# =========================================================================
|
|
136
|
+
|
|
137
|
+
def init_progress_file(self) -> None:
|
|
138
|
+
"""Initialize progress.txt with header if it doesn't exist."""
|
|
139
|
+
if self.progress_file.exists():
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
header = f"""# Ralph Progress Log
|
|
143
|
+
Started: {datetime.now().strftime("%Y-%m-%d %H:%M")}
|
|
144
|
+
|
|
145
|
+
## Codebase Patterns
|
|
146
|
+
<!-- Add reusable patterns discovered during implementation here -->
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
"""
|
|
150
|
+
with open(self.progress_file, "w", encoding="utf-8") as f:
|
|
151
|
+
f.write(header)
|
|
152
|
+
|
|
153
|
+
def append_progress(self, entry: ProgressEntry) -> bool:
|
|
154
|
+
"""Append a progress entry to progress.txt.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
entry: The progress entry to append.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if successful, False otherwise.
|
|
161
|
+
"""
|
|
162
|
+
self.init_progress_file()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
with open(self.progress_file, "a", encoding="utf-8") as f:
|
|
166
|
+
f.write("\n" + entry.to_markdown() + "\n")
|
|
167
|
+
return True
|
|
168
|
+
except IOError:
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def read_progress(self) -> str:
|
|
172
|
+
"""Read the entire progress.txt file."""
|
|
173
|
+
if not self.progress_file.exists():
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
with open(self.progress_file, "r", encoding="utf-8") as f:
|
|
178
|
+
return f.read()
|
|
179
|
+
except IOError:
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
def read_codebase_patterns(self) -> str:
|
|
183
|
+
"""Extract the Codebase Patterns section from progress.txt."""
|
|
184
|
+
content = self.read_progress()
|
|
185
|
+
if not content:
|
|
186
|
+
return ""
|
|
187
|
+
|
|
188
|
+
# Find the Codebase Patterns section
|
|
189
|
+
pattern_start = content.find("## Codebase Patterns")
|
|
190
|
+
if pattern_start == -1:
|
|
191
|
+
return ""
|
|
192
|
+
|
|
193
|
+
# Find the end of the patterns section (next ## or ---)
|
|
194
|
+
pattern_end = content.find("\n---", pattern_start)
|
|
195
|
+
if pattern_end == -1:
|
|
196
|
+
pattern_end = content.find("\n## ", pattern_start + 20)
|
|
197
|
+
if pattern_end == -1:
|
|
198
|
+
pattern_end = len(content)
|
|
199
|
+
|
|
200
|
+
return content[pattern_start:pattern_end].strip()
|
|
201
|
+
|
|
202
|
+
def add_codebase_pattern(self, pattern: str) -> bool:
|
|
203
|
+
"""Add a pattern to the Codebase Patterns section.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
pattern: The pattern to add (e.g., "Use X for Y").
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if successful, False otherwise.
|
|
210
|
+
"""
|
|
211
|
+
self.init_progress_file()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
content = self.read_progress()
|
|
215
|
+
|
|
216
|
+
# Find insertion point (after "## Codebase Patterns" line)
|
|
217
|
+
marker = "## Codebase Patterns"
|
|
218
|
+
idx = content.find(marker)
|
|
219
|
+
if idx == -1:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
# Find end of that line
|
|
223
|
+
line_end = content.find("\n", idx)
|
|
224
|
+
if line_end == -1:
|
|
225
|
+
line_end = len(content)
|
|
226
|
+
|
|
227
|
+
# Insert the pattern
|
|
228
|
+
new_line = f"\n- {pattern}"
|
|
229
|
+
new_content = content[:line_end] + new_line + content[line_end:]
|
|
230
|
+
|
|
231
|
+
with open(self.progress_file, "w", encoding="utf-8") as f:
|
|
232
|
+
f.write(new_content)
|
|
233
|
+
return True
|
|
234
|
+
except IOError:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# =========================================================================
|
|
238
|
+
# ARCHIVING
|
|
239
|
+
# =========================================================================
|
|
240
|
+
|
|
241
|
+
def should_archive(self, new_branch: str) -> bool:
|
|
242
|
+
"""Check if we should archive the current run before starting a new one.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
new_branch: The branch name for the new PRD.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if current run should be archived.
|
|
249
|
+
"""
|
|
250
|
+
if not self.prd_file.exists():
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
if not self.last_branch_file.exists():
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
last_branch = self.last_branch_file.read_text().strip()
|
|
258
|
+
return last_branch != new_branch and len(self.read_progress()) > 100
|
|
259
|
+
except IOError:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def archive_current_run(self) -> Optional[str]:
|
|
263
|
+
"""Archive the current prd.json and progress.txt.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Path to the archive folder, or None if archiving failed.
|
|
267
|
+
"""
|
|
268
|
+
prd = self.read_prd()
|
|
269
|
+
if prd is None:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
# Create archive folder name
|
|
273
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
274
|
+
folder_name = prd.branch_name.replace("ralph/", "").replace("/", "-")
|
|
275
|
+
archive_folder = self.archive_dir / f"{date_str}-{folder_name}"
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
archive_folder.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
|
|
280
|
+
if self.prd_file.exists():
|
|
281
|
+
shutil.copy(self.prd_file, archive_folder / "prd.json")
|
|
282
|
+
|
|
283
|
+
if self.progress_file.exists():
|
|
284
|
+
shutil.copy(self.progress_file, archive_folder / "progress.txt")
|
|
285
|
+
|
|
286
|
+
return str(archive_folder)
|
|
287
|
+
except IOError:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def update_last_branch(self, branch_name: str) -> None:
|
|
291
|
+
"""Update the last branch file."""
|
|
292
|
+
try:
|
|
293
|
+
self.last_branch_file.write_text(branch_name)
|
|
294
|
+
except IOError:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
def reset_for_new_run(self) -> None:
|
|
298
|
+
"""Reset progress.txt for a new run."""
|
|
299
|
+
if self.progress_file.exists():
|
|
300
|
+
self.progress_file.unlink()
|
|
301
|
+
self.init_progress_file()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Global instance for convenience
|
|
305
|
+
_default_manager: Optional[RalphStateManager] = None
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_state_manager(working_dir: Optional[str] = None) -> RalphStateManager:
|
|
309
|
+
"""Get the state manager instance.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
working_dir: Optional working directory. If None, uses cwd.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
RalphStateManager instance.
|
|
316
|
+
"""
|
|
317
|
+
global _default_manager
|
|
318
|
+
if working_dir:
|
|
319
|
+
return RalphStateManager(working_dir)
|
|
320
|
+
if _default_manager is None:
|
|
321
|
+
_default_manager = RalphStateManager()
|
|
322
|
+
return _default_manager
|