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,451 @@
1
+ """Ralph plugin tools - registered via the register_tools callback."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import List
6
+
7
+ from pydantic_ai import RunContext
8
+
9
+ from .models import ProgressEntry
10
+ from .state_manager import get_state_manager
11
+
12
+ # ============================================================================
13
+ # TOOL OUTPUT TYPES
14
+ # ============================================================================
15
+
16
+
17
+ @dataclass
18
+ class RalphStoryOutput:
19
+ """Output for getting the current story."""
20
+
21
+ story_id: str | None
22
+ title: str | None
23
+ description: str | None
24
+ acceptance_criteria: List[str]
25
+ priority: int | None
26
+ requires_ui_verification: bool
27
+ all_complete: bool
28
+ error: str | None = None
29
+
30
+
31
+ @dataclass
32
+ class RalphStatusOutput:
33
+ """Output for status checks."""
34
+
35
+ success: bool
36
+ message: str
37
+ progress_summary: str | None = None
38
+ stories_remaining: int = 0
39
+ all_complete: bool = False
40
+
41
+
42
+ @dataclass
43
+ class RalphPRDOutput:
44
+ """Output for reading the full PRD."""
45
+
46
+ success: bool
47
+ project: str | None = None
48
+ branch_name: str | None = None
49
+ description: str | None = None
50
+ stories: List[dict] | None = None
51
+ progress_summary: str | None = None
52
+ error: str | None = None
53
+
54
+
55
+ @dataclass
56
+ class RalphPatternsOutput:
57
+ """Output for reading codebase patterns."""
58
+
59
+ patterns: str
60
+ has_patterns: bool
61
+
62
+
63
+ # ============================================================================
64
+ # TOOL REGISTRATION FUNCTIONS
65
+ # ============================================================================
66
+
67
+
68
+ def register_ralph_get_current_story(agent) -> None:
69
+ """Register the tool to get the current story to work on."""
70
+
71
+ @agent.tool
72
+ def ralph_get_current_story(context: RunContext) -> RalphStoryOutput:
73
+ """Get the next user story to work on from prd.json.
74
+
75
+ This tool reads the prd.json file and returns the highest-priority
76
+ story that hasn't been completed yet (passes=false).
77
+
78
+ Returns:
79
+ RalphStoryOutput containing:
80
+ - story_id: The story ID (e.g., "US-001")
81
+ - title: Story title
82
+ - description: Full story description
83
+ - acceptance_criteria: List of criteria to satisfy
84
+ - priority: Story priority (lower = higher priority)
85
+ - requires_ui_verification: True if story needs browser testing
86
+ - all_complete: True if ALL stories are done
87
+ - error: Error message if something went wrong
88
+ """
89
+ manager = get_state_manager()
90
+
91
+ if not manager.prd_exists():
92
+ return RalphStoryOutput(
93
+ story_id=None,
94
+ title=None,
95
+ description=None,
96
+ acceptance_criteria=[],
97
+ priority=None,
98
+ requires_ui_verification=False,
99
+ all_complete=False,
100
+ error="No prd.json found in current directory. Create one first with /ralph prd",
101
+ )
102
+
103
+ if manager.all_stories_complete():
104
+ return RalphStoryOutput(
105
+ story_id=None,
106
+ title=None,
107
+ description=None,
108
+ acceptance_criteria=[],
109
+ priority=None,
110
+ requires_ui_verification=False,
111
+ all_complete=True,
112
+ error=None,
113
+ )
114
+
115
+ story = manager.get_next_story()
116
+ if story is None:
117
+ return RalphStoryOutput(
118
+ story_id=None,
119
+ title=None,
120
+ description=None,
121
+ acceptance_criteria=[],
122
+ priority=None,
123
+ requires_ui_verification=False,
124
+ all_complete=True,
125
+ error=None,
126
+ )
127
+
128
+ return RalphStoryOutput(
129
+ story_id=story.id,
130
+ title=story.title,
131
+ description=story.description,
132
+ acceptance_criteria=story.acceptance_criteria,
133
+ priority=story.priority,
134
+ requires_ui_verification=story.has_ui_verification(),
135
+ all_complete=False,
136
+ error=None,
137
+ )
138
+
139
+
140
+ def register_ralph_mark_story_complete(agent) -> None:
141
+ """Register the tool to mark a story as complete."""
142
+
143
+ @agent.tool
144
+ def ralph_mark_story_complete(
145
+ context: RunContext,
146
+ story_id: str,
147
+ notes: str | None = None,
148
+ ) -> RalphStatusOutput:
149
+ """Mark a user story as complete (passes=true) in prd.json.
150
+
151
+ Call this AFTER you have:
152
+ 1. Implemented all the acceptance criteria
153
+ 2. Verified the code compiles/typechecks
154
+ 3. Run any required tests
155
+ 4. Committed the changes
156
+
157
+ For UI stories, also ensure browser verification passed.
158
+
159
+ Args:
160
+ story_id: The story ID to mark complete (e.g., "US-001")
161
+ notes: Optional notes about the implementation
162
+
163
+ Returns:
164
+ RalphStatusOutput with success status and message
165
+ """
166
+ manager = get_state_manager()
167
+
168
+ success, message = manager.mark_story_complete(story_id, notes or "")
169
+
170
+ prd = manager.read_prd()
171
+ remaining = 0
172
+ all_complete = False
173
+ progress = None
174
+
175
+ if prd:
176
+ remaining = sum(1 for s in prd.user_stories if not s.passes)
177
+ all_complete = prd.all_complete()
178
+ progress = prd.get_progress_summary()
179
+
180
+ return RalphStatusOutput(
181
+ success=success,
182
+ message=message,
183
+ progress_summary=progress,
184
+ stories_remaining=remaining,
185
+ all_complete=all_complete,
186
+ )
187
+
188
+
189
+ def register_ralph_log_progress(agent) -> None:
190
+ """Register the tool to log progress to progress.txt."""
191
+
192
+ @agent.tool
193
+ def ralph_log_progress(
194
+ context: RunContext,
195
+ story_id: str,
196
+ summary: str,
197
+ files_changed: List[str] | None = None,
198
+ learnings: List[str] | None = None,
199
+ ) -> RalphStatusOutput:
200
+ """Append a progress entry to progress.txt.
201
+
202
+ Call this after completing a story to record:
203
+ - What was implemented
204
+ - Which files were changed
205
+ - Any learnings for future iterations
206
+
207
+ The learnings are especially important for helping future iterations
208
+ understand patterns and avoid mistakes.
209
+
210
+ Args:
211
+ story_id: The story ID that was completed
212
+ summary: Brief summary of what was implemented
213
+ files_changed: List of files that were modified
214
+ learnings: List of learnings/patterns discovered
215
+
216
+ Returns:
217
+ RalphStatusOutput with success status
218
+ """
219
+ manager = get_state_manager()
220
+
221
+ entry = ProgressEntry(
222
+ timestamp=datetime.now(),
223
+ story_id=story_id,
224
+ summary=summary,
225
+ files_changed=files_changed or [],
226
+ learnings=learnings or [],
227
+ )
228
+
229
+ success = manager.append_progress(entry)
230
+
231
+ return RalphStatusOutput(
232
+ success=success,
233
+ message="Progress logged successfully"
234
+ if success
235
+ else "Failed to log progress",
236
+ )
237
+
238
+
239
+ def register_ralph_check_all_complete(agent) -> None:
240
+ """Register the tool to check if all stories are complete."""
241
+
242
+ @agent.tool
243
+ def ralph_check_all_complete(context: RunContext) -> RalphStatusOutput:
244
+ """Check if all user stories in prd.json are complete.
245
+
246
+ Use this to determine if the Ralph loop should exit.
247
+ When all stories are complete, you should output:
248
+ <promise>COMPLETE</promise>
249
+
250
+ Returns:
251
+ RalphStatusOutput with all_complete flag
252
+ """
253
+ manager = get_state_manager()
254
+
255
+ if not manager.prd_exists():
256
+ return RalphStatusOutput(
257
+ success=False,
258
+ message="No prd.json found",
259
+ all_complete=False,
260
+ )
261
+
262
+ prd = manager.read_prd()
263
+ if prd is None:
264
+ return RalphStatusOutput(
265
+ success=False,
266
+ message="Failed to read prd.json",
267
+ all_complete=False,
268
+ )
269
+
270
+ all_complete = prd.all_complete()
271
+ remaining = sum(1 for s in prd.user_stories if not s.passes)
272
+
273
+ return RalphStatusOutput(
274
+ success=True,
275
+ message="All stories complete!"
276
+ if all_complete
277
+ else f"{remaining} stories remaining",
278
+ progress_summary=prd.get_progress_summary(),
279
+ stories_remaining=remaining,
280
+ all_complete=all_complete,
281
+ )
282
+
283
+
284
+ def register_ralph_read_prd(agent) -> None:
285
+ """Register the tool to read the full PRD."""
286
+
287
+ @agent.tool
288
+ def ralph_read_prd(context: RunContext) -> RalphPRDOutput:
289
+ """Read the full prd.json file and return its contents.
290
+
291
+ Use this to understand the overall project and see all stories.
292
+
293
+ Returns:
294
+ RalphPRDOutput with project details and all stories
295
+ """
296
+ manager = get_state_manager()
297
+
298
+ if not manager.prd_exists():
299
+ return RalphPRDOutput(
300
+ success=False,
301
+ error="No prd.json found in current directory",
302
+ )
303
+
304
+ prd = manager.read_prd()
305
+ if prd is None:
306
+ return RalphPRDOutput(
307
+ success=False,
308
+ error="Failed to parse prd.json",
309
+ )
310
+
311
+ return RalphPRDOutput(
312
+ success=True,
313
+ project=prd.project,
314
+ branch_name=prd.branch_name,
315
+ description=prd.description,
316
+ stories=[s.to_dict() for s in prd.user_stories],
317
+ progress_summary=prd.get_progress_summary(),
318
+ )
319
+
320
+
321
+ def register_ralph_read_patterns(agent) -> None:
322
+ """Register the tool to read codebase patterns from progress.txt."""
323
+
324
+ @agent.tool
325
+ def ralph_read_patterns(context: RunContext) -> RalphPatternsOutput:
326
+ """Read the Codebase Patterns section from progress.txt.
327
+
328
+ These patterns were discovered by previous iterations and contain
329
+ important context about the codebase. Read this BEFORE starting
330
+ work on a new story.
331
+
332
+ Returns:
333
+ RalphPatternsOutput with patterns text
334
+ """
335
+ manager = get_state_manager()
336
+ patterns = manager.read_codebase_patterns()
337
+
338
+ return RalphPatternsOutput(
339
+ patterns=patterns if patterns else "No patterns recorded yet.",
340
+ has_patterns=bool(patterns),
341
+ )
342
+
343
+
344
+ def register_ralph_add_pattern(agent) -> None:
345
+ """Register the tool to add a codebase pattern."""
346
+
347
+ @agent.tool
348
+ def ralph_add_pattern(context: RunContext, pattern: str) -> RalphStatusOutput:
349
+ """Add a reusable pattern to the Codebase Patterns section.
350
+
351
+ Only add patterns that are GENERAL and REUSABLE, not story-specific.
352
+
353
+ Good examples:
354
+ - "Use `sql<number>` template for aggregations"
355
+ - "Always use `IF NOT EXISTS` for migrations"
356
+ - "Export types from actions.ts for UI components"
357
+
358
+ Bad examples (too specific):
359
+ - "Added login button to header" (story-specific)
360
+ - "Fixed bug in user.ts" (not a pattern)
361
+
362
+ Args:
363
+ pattern: The pattern to record
364
+
365
+ Returns:
366
+ RalphStatusOutput with success status
367
+ """
368
+ manager = get_state_manager()
369
+ success = manager.add_codebase_pattern(pattern)
370
+
371
+ return RalphStatusOutput(
372
+ success=success,
373
+ message="Pattern added" if success else "Failed to add pattern",
374
+ )
375
+
376
+
377
+ def register_ralph_run_loop(agent) -> None:
378
+ """Register the tool to run the Ralph autonomous loop."""
379
+
380
+ @agent.tool
381
+ async def ralph_run_loop(
382
+ context: RunContext,
383
+ max_iterations: int = 10,
384
+ ) -> RalphStatusOutput:
385
+ """Start the Ralph autonomous loop to implement all pending stories.
386
+
387
+ This runs the full Ralph loop, executing one story per iteration
388
+ until all stories are complete or max_iterations is reached.
389
+
390
+ Each iteration:
391
+ 1. Gets the next pending story from prd.json
392
+ 2. Invokes the ralph-orchestrator agent with a fresh session
393
+ 3. The orchestrator implements and commits the story
394
+ 4. Checks if all stories are complete
395
+
396
+ Args:
397
+ max_iterations: Maximum number of iterations (default 10)
398
+
399
+ Returns:
400
+ RalphStatusOutput with success status and final progress
401
+ """
402
+ from .loop_controller import run_ralph_loop
403
+
404
+ try:
405
+ result = await run_ralph_loop(max_iterations=max_iterations)
406
+
407
+ return RalphStatusOutput(
408
+ success=result.get("success", False),
409
+ message=result.get("message", "Loop completed"),
410
+ progress_summary=result.get("message"),
411
+ stories_remaining=0 if result.get("all_complete") else -1,
412
+ all_complete=result.get("all_complete", False),
413
+ )
414
+ except Exception as e:
415
+ return RalphStatusOutput(
416
+ success=False,
417
+ message=f"Loop failed: {str(e)}",
418
+ all_complete=False,
419
+ )
420
+
421
+
422
+ # ============================================================================
423
+ # TOOL PROVIDER FOR CALLBACK
424
+ # ============================================================================
425
+
426
+
427
+ def get_ralph_tools() -> List[dict]:
428
+ """Get all Ralph tools for registration via the register_tools callback.
429
+
430
+ Returns:
431
+ List of tool definitions with name and register_func.
432
+ """
433
+ return [
434
+ {
435
+ "name": "ralph_get_current_story",
436
+ "register_func": register_ralph_get_current_story,
437
+ },
438
+ {
439
+ "name": "ralph_mark_story_complete",
440
+ "register_func": register_ralph_mark_story_complete,
441
+ },
442
+ {"name": "ralph_log_progress", "register_func": register_ralph_log_progress},
443
+ {
444
+ "name": "ralph_check_all_complete",
445
+ "register_func": register_ralph_check_all_complete,
446
+ },
447
+ {"name": "ralph_read_prd", "register_func": register_ralph_read_prd},
448
+ {"name": "ralph_read_patterns", "register_func": register_ralph_read_patterns},
449
+ {"name": "ralph_add_pattern", "register_func": register_ralph_add_pattern},
450
+ {"name": "ralph_run_loop", "register_func": register_ralph_run_loop},
451
+ ]
@@ -1,3 +1,4 @@
1
+ from code_puppy.callbacks import on_register_tools
1
2
  from code_puppy.messaging import emit_warning
2
3
  from code_puppy.tools.agent_tools import register_invoke_agent, register_list_agents
3
4
 
@@ -169,6 +170,34 @@ TOOL_REGISTRY = {
169
170
  }
170
171
 
171
172
 
173
+ def _load_plugin_tools() -> None:
174
+ """Load tools registered by plugins via the register_tools callback.
175
+
176
+ This merges plugin-provided tools into the TOOL_REGISTRY.
177
+ Called lazily when tools are first accessed.
178
+ """
179
+ try:
180
+ results = on_register_tools()
181
+ for result in results:
182
+ if result is None:
183
+ continue
184
+ # Each result should be a list of tool definitions
185
+ tools_list = result if isinstance(result, list) else [result]
186
+ for tool_def in tools_list:
187
+ if (
188
+ isinstance(tool_def, dict)
189
+ and "name" in tool_def
190
+ and "register_func" in tool_def
191
+ ):
192
+ tool_name = tool_def["name"]
193
+ register_func = tool_def["register_func"]
194
+ if callable(register_func):
195
+ TOOL_REGISTRY[tool_name] = register_func
196
+ except Exception:
197
+ # Don't let plugin failures break core functionality
198
+ pass
199
+
200
+
172
201
  def register_tools_for_agent(agent, tool_names: list[str]):
173
202
  """Register specific tools for an agent based on tool names.
174
203
 
@@ -178,6 +207,7 @@ def register_tools_for_agent(agent, tool_names: list[str]):
178
207
  """
179
208
  from code_puppy.config import get_universal_constructor_enabled
180
209
 
210
+ _load_plugin_tools()
181
211
  for tool_name in tool_names:
182
212
  # Handle UC tools (prefixed with "uc:")
183
213
  if tool_name.startswith("uc:"):
@@ -337,4 +367,5 @@ def get_available_tool_names() -> list[str]:
337
367
  Returns:
338
368
  List of all tool names that can be registered.
339
369
  """
370
+ _load_plugin_tools()
340
371
  return list(TOOL_REGISTRY.keys())
@@ -469,7 +469,7 @@ def register_invoke_agent(agent):
469
469
  model = ModelFactory.get_model(model_name, models_config)
470
470
 
471
471
  # Create a temporary agent instance to avoid interfering with current agent state
472
- instructions = agent_config.get_system_prompt()
472
+ instructions = agent_config.get_full_system_prompt()
473
473
 
474
474
  # Add AGENTS.md content to subagents
475
475
  puppy_rules = agent_config.load_puppy_rules()
@@ -647,7 +647,7 @@ def run_shell_command_streaming(
647
647
  line = process.stdout.readline()
648
648
  if not line: # EOF
649
649
  break
650
- line = line.rstrip("\n\r")
650
+ line = line.rstrip("\n")
651
651
  line = _truncate_line(line)
652
652
  stdout_lines.append(line)
653
653
  if not silent:
@@ -660,7 +660,7 @@ def run_shell_command_streaming(
660
660
  try:
661
661
  remaining = process.stdout.read()
662
662
  if remaining:
663
- for line in remaining.splitlines():
663
+ for line in remaining.split("\n"):
664
664
  line = _truncate_line(line)
665
665
  stdout_lines.append(line)
666
666
  if not silent:
@@ -683,7 +683,7 @@ def run_shell_command_streaming(
683
683
  line = process.stdout.readline()
684
684
  if not line: # EOF
685
685
  break
686
- line = line.rstrip("\n\r")
686
+ line = line.rstrip("\n")
687
687
  line = _truncate_line(line)
688
688
  stdout_lines.append(line)
689
689
  if not silent:
@@ -716,7 +716,7 @@ def run_shell_command_streaming(
716
716
  line = process.stderr.readline()
717
717
  if not line: # EOF
718
718
  break
719
- line = line.rstrip("\n\r")
719
+ line = line.rstrip("\n")
720
720
  line = _truncate_line(line)
721
721
  stderr_lines.append(line)
722
722
  if not silent:
@@ -729,7 +729,7 @@ def run_shell_command_streaming(
729
729
  try:
730
730
  remaining = process.stderr.read()
731
731
  if remaining:
732
- for line in remaining.splitlines():
732
+ for line in remaining.split("\n"):
733
733
  line = _truncate_line(line)
734
734
  stderr_lines.append(line)
735
735
  if not silent:
@@ -751,7 +751,7 @@ def run_shell_command_streaming(
751
751
  line = process.stderr.readline()
752
752
  if not line: # EOF
753
753
  break
754
- line = line.rstrip("\n\r")
754
+ line = line.rstrip("\n")
755
755
  line = _truncate_line(line)
756
756
  stderr_lines.append(line)
757
757
  if not silent:
@@ -1165,6 +1165,11 @@ async def _execute_shell_command(
1165
1165
  )
1166
1166
  )
1167
1167
 
1168
+ # Pause spinner during shell command so \r output can work properly
1169
+ from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
1170
+
1171
+ pause_all_spinners()
1172
+
1168
1173
  # Acquire shared keyboard context - Ctrl-X/Ctrl-C will kill ALL running commands
1169
1174
  # This is reference-counted: listener starts on first command, stops on last
1170
1175
  _acquire_keyboard_context()
@@ -1172,6 +1177,7 @@ async def _execute_shell_command(
1172
1177
  return await _run_command_inner(command, cwd, timeout, group_id, silent=silent)
1173
1178
  finally:
1174
1179
  _release_keyboard_context()
1180
+ resume_all_spinners()
1175
1181
 
1176
1182
 
1177
1183
  def _run_command_sync(
@@ -1192,18 +1198,26 @@ def _run_command_sync(
1192
1198
  else:
1193
1199
  preexec_fn = os.setsid if hasattr(os, "setsid") else None
1194
1200
 
1201
+ import io
1202
+
1195
1203
  process = subprocess.Popen(
1196
1204
  command,
1197
1205
  shell=True,
1198
1206
  stdout=subprocess.PIPE,
1199
1207
  stderr=subprocess.PIPE,
1200
- text=True,
1201
1208
  cwd=cwd,
1202
- bufsize=1,
1203
- universal_newlines=True,
1209
+ bufsize=0, # Unbuffered for real-time output
1204
1210
  preexec_fn=preexec_fn,
1205
1211
  creationflags=creationflags,
1206
1212
  )
1213
+
1214
+ # Wrap pipes with TextIOWrapper that preserves \r (newline='' disables translation)
1215
+ process.stdout = io.TextIOWrapper(
1216
+ process.stdout, newline="", encoding="utf-8", errors="replace"
1217
+ )
1218
+ process.stderr = io.TextIOWrapper(
1219
+ process.stderr, newline="", encoding="utf-8", errors="replace"
1220
+ )
1207
1221
  _register_process(process)
1208
1222
  try:
1209
1223
  return run_shell_command_streaming(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.374
3
+ Version: 0.0.375
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy