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.
Files changed (28) hide show
  1. code_puppy/agents/agent_manager.py +34 -2
  2. code_puppy/agents/base_agent.py +61 -4
  3. code_puppy/callbacks.py +125 -0
  4. code_puppy/messaging/rich_renderer.py +13 -7
  5. code_puppy/model_factory.py +63 -258
  6. code_puppy/model_utils.py +33 -1
  7. code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
  8. code_puppy/plugins/antigravity_oauth/utils.py +2 -3
  9. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
  10. code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
  11. code_puppy/plugins/ralph/__init__.py +13 -0
  12. code_puppy/plugins/ralph/agents.py +433 -0
  13. code_puppy/plugins/ralph/commands.py +208 -0
  14. code_puppy/plugins/ralph/loop_controller.py +285 -0
  15. code_puppy/plugins/ralph/models.py +125 -0
  16. code_puppy/plugins/ralph/register_callbacks.py +133 -0
  17. code_puppy/plugins/ralph/state_manager.py +322 -0
  18. code_puppy/plugins/ralph/tools.py +451 -0
  19. code_puppy/tools/__init__.py +31 -0
  20. code_puppy/tools/agent_tools.py +1 -1
  21. code_puppy/tools/command_runner.py +23 -9
  22. {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
  23. {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/RECORD +28 -20
  24. {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
  25. {code_puppy-0.0.374.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
  26. {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
  27. {code_puppy-0.0.374.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
  28. {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