code-puppy 0.0.373__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 (44) hide show
  1. code_puppy/agents/agent_creator_agent.py +49 -1
  2. code_puppy/agents/agent_helios.py +122 -0
  3. code_puppy/agents/agent_manager.py +60 -4
  4. code_puppy/agents/base_agent.py +61 -4
  5. code_puppy/agents/json_agent.py +30 -7
  6. code_puppy/callbacks.py +125 -0
  7. code_puppy/command_line/colors_menu.py +2 -0
  8. code_puppy/command_line/command_handler.py +1 -0
  9. code_puppy/command_line/config_commands.py +3 -1
  10. code_puppy/command_line/uc_menu.py +890 -0
  11. code_puppy/config.py +29 -0
  12. code_puppy/messaging/messages.py +18 -0
  13. code_puppy/messaging/rich_renderer.py +48 -7
  14. code_puppy/messaging/subagent_console.py +0 -1
  15. code_puppy/model_factory.py +63 -258
  16. code_puppy/model_utils.py +33 -1
  17. code_puppy/plugins/antigravity_oauth/register_callbacks.py +106 -1
  18. code_puppy/plugins/antigravity_oauth/utils.py +2 -3
  19. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +85 -3
  20. code_puppy/plugins/claude_code_oauth/register_callbacks.py +88 -0
  21. code_puppy/plugins/ralph/__init__.py +13 -0
  22. code_puppy/plugins/ralph/agents.py +433 -0
  23. code_puppy/plugins/ralph/commands.py +208 -0
  24. code_puppy/plugins/ralph/loop_controller.py +285 -0
  25. code_puppy/plugins/ralph/models.py +125 -0
  26. code_puppy/plugins/ralph/register_callbacks.py +133 -0
  27. code_puppy/plugins/ralph/state_manager.py +322 -0
  28. code_puppy/plugins/ralph/tools.py +451 -0
  29. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  30. code_puppy/plugins/universal_constructor/models.py +138 -0
  31. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  32. code_puppy/plugins/universal_constructor/registry.py +304 -0
  33. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  34. code_puppy/tools/__init__.py +169 -1
  35. code_puppy/tools/agent_tools.py +1 -1
  36. code_puppy/tools/command_runner.py +23 -9
  37. code_puppy/tools/universal_constructor.py +889 -0
  38. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/METADATA +1 -1
  39. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/RECORD +44 -28
  40. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models.json +0 -0
  41. {code_puppy-0.0.373.data → code_puppy-0.0.375.data}/data/code_puppy/models_dev_api.json +0 -0
  42. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/WHEEL +0 -0
  43. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/entry_points.txt +0 -0
  44. {code_puppy-0.0.373.dist-info → code_puppy-0.0.375.dist-info}/licenses/LICENSE +0 -0
@@ -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