patchpal 0.23.0__tar.gz → 0.23.2__tar.gz

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 (69) hide show
  1. {patchpal-0.23.0/patchpal.egg-info → patchpal-0.23.2}/PKG-INFO +6 -2
  2. {patchpal-0.23.0 → patchpal-0.23.2}/README.md +5 -1
  3. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/__init__.py +1 -1
  4. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/agent/function_calling.py +60 -20
  5. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/agent/react.py +17 -20
  6. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/prompts/system_prompt.md +1 -1
  7. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/common.py +64 -1
  8. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/web_tools.py +2 -1
  9. {patchpal-0.23.0 → patchpal-0.23.2/patchpal.egg-info}/PKG-INFO +6 -2
  10. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal.egg-info/SOURCES.txt +1 -0
  11. patchpal-0.23.2/tests/test_memory_locations.py +117 -0
  12. {patchpal-0.23.0 → patchpal-0.23.2}/LICENSE +0 -0
  13. {patchpal-0.23.0 → patchpal-0.23.2}/MANIFEST.in +0 -0
  14. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/agent/__init__.py +0 -0
  15. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/agent/bedrock_profile_utils.py +0 -0
  16. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/__init__.py +0 -0
  17. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/autopilot.py +0 -0
  18. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/interactive.py +0 -0
  19. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/mcp.py +0 -0
  20. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/sandbox.py +0 -0
  21. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/cli/streaming.py +0 -0
  22. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/config.py +0 -0
  23. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/context.py +0 -0
  24. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/permissions.py +0 -0
  25. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/prompts/react_prompt.md +0 -0
  26. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/skills.py +0 -0
  27. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/__init__.py +0 -0
  28. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/audit.py +0 -0
  29. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/code_analysis.py +0 -0
  30. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/definitions.py +0 -0
  31. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/file_reading.py +0 -0
  32. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/file_writing.py +0 -0
  33. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/find_tool.py +0 -0
  34. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/grep_tool.py +0 -0
  35. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/image_handler.py +0 -0
  36. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/mcp.py +0 -0
  37. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/repo_map.py +0 -0
  38. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/shell_tools.py +0 -0
  39. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/todo_tools.py +0 -0
  40. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/tool_schema.py +0 -0
  41. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal/tools/user_interaction.py +0 -0
  42. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal.egg-info/dependency_links.txt +0 -0
  43. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal.egg-info/entry_points.txt +0 -0
  44. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal.egg-info/requires.txt +0 -0
  45. {patchpal-0.23.0 → patchpal-0.23.2}/patchpal.egg-info/top_level.txt +0 -0
  46. {patchpal-0.23.0 → patchpal-0.23.2}/pyproject.toml +0 -0
  47. {patchpal-0.23.0 → patchpal-0.23.2}/setup.cfg +0 -0
  48. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_agent.py +0 -0
  49. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_cli.py +0 -0
  50. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_config_dynamic.py +0 -0
  51. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_context.py +0 -0
  52. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_custom_tools.py +0 -0
  53. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_enabled_tools.py +0 -0
  54. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_find_tool.py +0 -0
  55. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_guardrails.py +0 -0
  56. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_image_blocking.py +0 -0
  57. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_maximum_security.py +0 -0
  58. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_mcp_config.py +0 -0
  59. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_memory.py +0 -0
  60. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_operational_safety.py +0 -0
  61. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_optional_tools.py +0 -0
  62. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_permissions.py +0 -0
  63. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_react.py +0 -0
  64. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_reasoning_content.py +0 -0
  65. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_repo_map.py +0 -0
  66. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_simplified_prompt.py +0 -0
  67. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_skills.py +0 -0
  68. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_streaming.py +0 -0
  69. {patchpal-0.23.0 → patchpal-0.23.2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -64,7 +64,7 @@ Most agent frameworks are [built in TypeScript](https://news.ycombinator.com/ite
64
64
  - [Built-In](https://amaiya.github.io/patchpal/features/tools/) and [Custom Tools](https://amaiya.github.io/patchpal/features/custom-tools/)
65
65
  - [Skills System](https://amaiya.github.io/patchpal/features/skills/) and [MCP Integration](https://amaiya.github.io/patchpal/features/mcp/)
66
66
  - [Autopilot Mode](https://amaiya.github.io/patchpal/usage/autopilot/) using [Ralph Wiggum loops](https://github.com/amaiya/patchpal/tree/main/examples/ralph/)
67
- - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `~/.patchpal/repos/<repo-name>/MEMORY.md` at startup.
67
+ - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `MEMORY.md` (repository root or `~/.patchpal/repos/<repo-name>/MEMORY.md`) at startup.
68
68
 
69
69
  PatchPal prioritizes customizability: custom tools, custom skills, a flexible Python API, and support for any tool-calling LLM.
70
70
 
@@ -177,6 +177,10 @@ While originally designed for software development, PatchPal is also a general-p
177
177
  2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
178
178
  3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
179
179
 
180
+ > I noticed there's another package called PatchPal on GitLab. Are they related?
181
+
182
+ No, they're separate projects. The [project with the same name on GitLab](https://gitlab.com/patchpal-ai) is unrelated to this one.
183
+
180
184
  > On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
181
185
 
182
186
  This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
@@ -16,7 +16,7 @@ Most agent frameworks are [built in TypeScript](https://news.ycombinator.com/ite
16
16
  - [Built-In](https://amaiya.github.io/patchpal/features/tools/) and [Custom Tools](https://amaiya.github.io/patchpal/features/custom-tools/)
17
17
  - [Skills System](https://amaiya.github.io/patchpal/features/skills/) and [MCP Integration](https://amaiya.github.io/patchpal/features/mcp/)
18
18
  - [Autopilot Mode](https://amaiya.github.io/patchpal/usage/autopilot/) using [Ralph Wiggum loops](https://github.com/amaiya/patchpal/tree/main/examples/ralph/)
19
- - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `~/.patchpal/repos/<repo-name>/MEMORY.md` at startup.
19
+ - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `MEMORY.md` (repository root or `~/.patchpal/repos/<repo-name>/MEMORY.md`) at startup.
20
20
 
21
21
  PatchPal prioritizes customizability: custom tools, custom skills, a flexible Python API, and support for any tool-calling LLM.
22
22
 
@@ -129,6 +129,10 @@ While originally designed for software development, PatchPal is also a general-p
129
129
  2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
130
130
  3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
131
131
 
132
+ > I noticed there's another package called PatchPal on GitLab. Are they related?
133
+
134
+ No, they're separate projects. The [project with the same name on GitLab](https://gitlab.com/patchpal-ai) is unrelated to this one.
135
+
132
136
  > On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
133
137
 
134
138
  This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
@@ -1,6 +1,6 @@
1
1
  """PatchPal - An open-source Claude Code clone implemented purely in Python."""
2
2
 
3
- __version__ = "0.23.0"
3
+ __version__ = "0.23.2"
4
4
 
5
5
  from patchpal.agent import create_agent, create_react_agent
6
6
  from patchpal.cli.autopilot import autopilot_loop
@@ -360,8 +360,30 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
360
360
  max_cache_markers = 4
361
361
  available_slots = max_cache_markers - existing_cache_count
362
362
 
363
+ # Debug logging
364
+ import os
365
+
366
+ if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
367
+ print("\n[CACHE DEBUG] _apply_prompt_caching called")
368
+ print(f" Total messages: {len(messages)}")
369
+ print(f" Existing cache markers: {existing_cache_count}")
370
+ print(f" Available slots: {available_slots}")
371
+ # Show which messages have markers
372
+ marked_indices = []
373
+ for i, msg in enumerate(messages):
374
+ if isinstance(msg.get("content"), list):
375
+ for block in msg["content"]:
376
+ if isinstance(block, dict) and (
377
+ "cache_control" in block or "cachePoint" in block
378
+ ):
379
+ marked_indices.append(i)
380
+ break
381
+ print(f" Messages with existing markers: {marked_indices}")
382
+
363
383
  if available_slots <= 0:
364
384
  # Already at or over limit, don't add any more cache markers
385
+ if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
386
+ print(" Result: No slots available, returning without changes")
365
387
  return messages
366
388
 
367
389
  # Find system messages (usually at the start)
@@ -373,6 +395,9 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
373
395
  non_system_messages[-2:] if len(non_system_messages) >= 2 else non_system_messages
374
396
  )
375
397
 
398
+ if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
399
+ print(f" Last 2 non-system indices: {last_two_indices}")
400
+
376
401
  # Build list of candidate indices to cache (prioritize system messages)
377
402
  candidates = []
378
403
 
@@ -405,6 +430,12 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
405
430
  if not has_cache:
406
431
  candidates.append(idx)
407
432
 
433
+ import os
434
+
435
+ if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
436
+ print(f" Candidates for marking: {candidates}")
437
+ print(f" Will mark first {available_slots} candidates")
438
+
408
439
  # Apply cache markers to candidates, respecting the available slots
409
440
  # This ensures we never exceed the 4 marker limit
410
441
  for idx in candidates[:available_slots]:
@@ -425,6 +456,20 @@ def _apply_prompt_caching(messages: List[Dict[str, Any]], model_id: str) -> List
425
456
  "content": [{"type": "text", "text": content_text, **cache_marker}],
426
457
  }
427
458
 
459
+ if os.environ.get("PATCHPAL_DEBUG_CACHE") == "true":
460
+ # Show final state
461
+ final_marked = []
462
+ for i, msg in enumerate(messages):
463
+ if isinstance(msg.get("content"), list):
464
+ for block in msg["content"]:
465
+ if isinstance(block, dict) and (
466
+ "cache_control" in block or "cachePoint" in block
467
+ ):
468
+ final_marked.append(i)
469
+ break
470
+ print(f" Final messages with markers: {final_marked}")
471
+ print(f" Markers applied to indices: {[c for c in candidates[:available_slots]]}")
472
+
428
473
  return messages
429
474
 
430
475
 
@@ -598,38 +643,33 @@ class PatchPalAgent:
598
643
  def _load_project_memory(self):
599
644
  """Load MEMORY.md file at session start if it has non-template content."""
600
645
  try:
601
- from patchpal.tools.common import MEMORY_FILE
646
+ from patchpal.tools.common import get_memory_info
602
647
 
603
- # Always tell the agent where MEMORY.md is located
604
- if not MEMORY_FILE.exists():
605
- return
606
-
607
- memory_content = MEMORY_FILE.read_text(encoding="utf-8")
648
+ info = get_memory_info()
608
649
 
609
- # Check if user has added content after the "---" separator
610
- has_user_content = False
611
- if "---" in memory_content:
612
- parts = memory_content.split("---", 1)
613
- if len(parts) > 1:
614
- user_content = parts[1].strip()
615
- if user_content and len(user_content) > 10:
616
- has_user_content = True
650
+ if not info["exists"]:
651
+ return
617
652
 
618
653
  # Build the message - include full content if user added info, otherwise just location
619
- if has_user_content:
654
+ if info["has_content"]:
620
655
  memory_msg = f"""# Project Memory (from MEMORY.md)
621
656
 
622
- {memory_content}
657
+ {info["content"]}
623
658
 
624
- The information above is from {MEMORY_FILE} and persists across sessions.
625
- To update it, use edit_file("{MEMORY_FILE}", ...) or write_file("{MEMORY_FILE}", ...)."""
659
+ The information above is from MEMORY.md ({info["location_note"]}) and persists across sessions.
660
+ To update it, use edit_file("{info["path"]}", ...) or write_file("{info["path"]}", ...)."""
626
661
  else:
627
662
  # Empty template - just inform agent
628
663
  memory_msg = f"""# Project Memory (MEMORY.md)
629
664
 
630
- Your project memory file is located at: {MEMORY_FILE}
665
+ Your project memory file is located at: {info["path"]}
666
+
667
+ It's currently empty (just the template). The file is automatically loaded at session start.
631
668
 
632
- It's currently empty (just the template). The file is automatically loaded at session start."""
669
+ Note: You can use MEMORY.md in either location:
670
+ - Repository root: MEMORY.md (can be version controlled)
671
+ - Home directory: ~/.patchpal/repos/<repo-name>/MEMORY.md (default)
672
+ Repository root takes priority if both exist."""
633
673
 
634
674
  # Add as a system message at the start
635
675
  self.messages.insert(
@@ -174,28 +174,25 @@ class ReActAgent:
174
174
 
175
175
  def _load_project_memory(self):
176
176
  """Load project memory file if it exists."""
177
- from pathlib import Path
177
+ try:
178
+ from patchpal.tools.common import get_memory_info
178
179
 
179
- repo_root = Path(".").resolve()
180
- home = Path.home()
181
- memory_path = home / ".patchpal" / "repos" / repo_root.name / "MEMORY.md"
180
+ info = get_memory_info()
182
181
 
183
- if memory_path.exists():
184
- try:
185
- with open(memory_path, "r", encoding="utf-8") as f:
186
- memory_content = f.read().strip()
187
- if memory_content and not memory_content.startswith(
188
- "# Project Memory\n\nThis file"
189
- ):
190
- # Only add if there's actual content (not just the template)
191
- self.messages.append(
192
- {
193
- "role": "system",
194
- "content": f"# Project Memory (MEMORY.md)\n\n{memory_content}",
195
- }
196
- )
197
- except Exception:
198
- pass # Silently skip if can't read
182
+ # Only add if there's actual content beyond the template
183
+ if info["has_content"]:
184
+ self.messages.append(
185
+ {
186
+ "role": "system",
187
+ "content": f"""# Project Memory (from MEMORY.md)
188
+
189
+ {info["content"]}
190
+
191
+ The information above is from MEMORY.md ({info["location_note"]}) and persists across sessions.""",
192
+ }
193
+ )
194
+ except Exception:
195
+ pass # Silently skip if can't read
199
196
 
200
197
  def _build_system_prompt(self, custom_instructions: str) -> str:
201
198
  """Build the ReAct system prompt with tool descriptions.
@@ -31,4 +31,4 @@ Assistant: "I found the issue at line 45. The timeout is set to 0 instead of 360
31
31
 
32
32
  ## Project Memory
33
33
 
34
- If project memory is included in your context, use that information throughout the session. When you learn important new information (architecture decisions, deployment details, conventions), suggest updating `~/.patchpal/repos/<repo-name>/MEMORY.md` to maintain continuity across sessions.
34
+ If project memory is included in your context, use that information throughout the session. When you learn important new information (architecture decisions, deployment details, conventions), suggest updating the project's `MEMORY.md` file to maintain continuity across sessions.
@@ -247,9 +247,22 @@ def _get_patchpal_dir() -> Path:
247
247
  def _ensure_memory_file() -> Path:
248
248
  """Ensure MEMORY.md file exists, creating it from template if needed.
249
249
 
250
+ Priority order:
251
+ 1. Repo root: MEMORY.md (if it exists, use it)
252
+ 2. PatchPal dir: ~/.patchpal/repos/<repo-name>/MEMORY.md (default location)
253
+
254
+ This allows users to keep MEMORY.md in the repository (e.g., for version control)
255
+ while maintaining backward compatibility with the home directory location.
256
+
250
257
  Returns:
251
- Path to the MEMORY.md file
258
+ Path to the MEMORY.md file (either repo root or patchpal dir)
252
259
  """
260
+ # Check repo root first (new alternative location)
261
+ repo_memory_path = REPO_ROOT / "MEMORY.md"
262
+ if repo_memory_path.exists():
263
+ return repo_memory_path
264
+
265
+ # Fall back to patchpal directory (default location)
253
266
  memory_path = PATCHPAL_DIR / "MEMORY.md"
254
267
 
255
268
  if not memory_path.exists():
@@ -267,6 +280,56 @@ BACKUP_DIR = PATCHPAL_DIR / "backups"
267
280
  AUDIT_LOG_FILE = PATCHPAL_DIR / "audit.log"
268
281
  MEMORY_FILE = _ensure_memory_file()
269
282
 
283
+
284
+ def get_memory_info() -> dict:
285
+ """Get memory file information for display to agent.
286
+
287
+ Returns:
288
+ dict with keys:
289
+ - path: Path to memory file
290
+ - exists: bool
291
+ - is_in_repo: bool (True if in repository root)
292
+ - has_content: bool (True if has user content beyond template)
293
+ - content: str (file content if exists)
294
+ - location_note: str (human-readable location description)
295
+ """
296
+ info = {
297
+ "path": MEMORY_FILE,
298
+ "exists": MEMORY_FILE.exists(),
299
+ "is_in_repo": False,
300
+ "has_content": False,
301
+ "content": "",
302
+ "location_note": "",
303
+ }
304
+
305
+ if not info["exists"]:
306
+ return info
307
+
308
+ # Check if in repo
309
+ try:
310
+ MEMORY_FILE.relative_to(REPO_ROOT)
311
+ info["is_in_repo"] = True
312
+ info["location_note"] = "in repository root"
313
+ except ValueError:
314
+ info["location_note"] = f"at {MEMORY_FILE}"
315
+
316
+ # Read content
317
+ try:
318
+ info["content"] = MEMORY_FILE.read_text(encoding="utf-8")
319
+
320
+ # Check for user content after "---" separator
321
+ if "---" in info["content"]:
322
+ parts = info["content"].split("---", 1)
323
+ if len(parts) > 1:
324
+ user_content = parts[1].strip()
325
+ if user_content and len(user_content) > 10:
326
+ info["has_content"] = True
327
+ except Exception:
328
+ pass
329
+
330
+ return info
331
+
332
+
270
333
  # Permission manager
271
334
  _permission_manager = None
272
335
 
@@ -245,9 +245,10 @@ class URLContextTracker:
245
245
  text: Text that may contain URLs
246
246
  """
247
247
  # Simple URL extraction (http:// and https://)
248
+ # Note: Apostrophes (') are allowed in URLs (e.g., Wikipedia article titles)
248
249
  import re
249
250
 
250
- url_pattern = r"https?://[^\s<>\"\'\)]*"
251
+ url_pattern = r"https?://[^\s<>\"\)]*"
251
252
  urls = re.findall(url_pattern, text)
252
253
  self.seen_urls.update(urls)
253
254
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchpal
3
- Version: 0.23.0
3
+ Version: 0.23.2
4
4
  Summary: An agentic coding and automation assistant, supporting both local and cloud LLMs
5
5
  Author: PatchPal Contributors
6
6
  License-Expression: Apache-2.0
@@ -64,7 +64,7 @@ Most agent frameworks are [built in TypeScript](https://news.ycombinator.com/ite
64
64
  - [Built-In](https://amaiya.github.io/patchpal/features/tools/) and [Custom Tools](https://amaiya.github.io/patchpal/features/custom-tools/)
65
65
  - [Skills System](https://amaiya.github.io/patchpal/features/skills/) and [MCP Integration](https://amaiya.github.io/patchpal/features/mcp/)
66
66
  - [Autopilot Mode](https://amaiya.github.io/patchpal/usage/autopilot/) using [Ralph Wiggum loops](https://github.com/amaiya/patchpal/tree/main/examples/ralph/)
67
- - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `~/.patchpal/repos/<repo-name>/MEMORY.md` at startup.
67
+ - [Project Memory](https://amaiya.github.io/patchpal/features/memory/) automatically loads project context from `MEMORY.md` (repository root or `~/.patchpal/repos/<repo-name>/MEMORY.md`) at startup.
68
68
 
69
69
  PatchPal prioritizes customizability: custom tools, custom skills, a flexible Python API, and support for any tool-calling LLM.
70
70
 
@@ -177,6 +177,10 @@ While originally designed for software development, PatchPal is also a general-p
177
177
  2. PatchPal includes a [unique guardrails system](https://amaiya.github.io/patchpal/safety/) that is better suited to privacy-conscious use cases involving sensitive data.
178
178
  3. We needed an agent harness that seamlessly works with [both local and cloud models](https://amaiya.github.io/patchpal/models/overview/#supported-models), including AWS GovCloud Bedrock models.
179
179
 
180
+ > I noticed there's another package called PatchPal on GitLab. Are they related?
181
+
182
+ No, they're separate projects. The [project with the same name on GitLab](https://gitlab.com/patchpal-ai) is unrelated to this one.
183
+
180
184
  > On Windows Subsystem for Linux (WSL), why is it stalling intermittently at "Thinking..."?
181
185
 
182
186
  This is a [known issue](https://github.com/microsoft/WSL/issues/6264#issuecomment-762154193) with WSL2.
@@ -54,6 +54,7 @@ tests/test_image_blocking.py
54
54
  tests/test_maximum_security.py
55
55
  tests/test_mcp_config.py
56
56
  tests/test_memory.py
57
+ tests/test_memory_locations.py
57
58
  tests/test_operational_safety.py
58
59
  tests/test_optional_tools.py
59
60
  tests/test_permissions.py
@@ -0,0 +1,117 @@
1
+ """Tests for MEMORY.md location priority (repo root vs home dir)."""
2
+
3
+
4
+ def test_memory_priority_repo_root_first(tmp_path):
5
+ """Test that repo root MEMORY.md takes priority over home dir."""
6
+ # Setup: Create both locations
7
+ repo_root = tmp_path / "repo"
8
+ repo_root.mkdir()
9
+
10
+ patchpal_dir = tmp_path / "home" / ".patchpal" / "repos" / "repo"
11
+ patchpal_dir.mkdir(parents=True)
12
+
13
+ # Create MEMORY.md in both locations with different content
14
+ repo_memory = repo_root / "MEMORY.md"
15
+ repo_memory.write_text("# Repo Memory\n\n---\n\nContent from repo root")
16
+
17
+ home_memory = patchpal_dir / "MEMORY.md"
18
+ home_memory.write_text("# Home Memory\n\n---\n\nContent from home dir")
19
+
20
+ # Test the priority logic (repo root exists, so it should be used)
21
+ assert repo_memory.exists()
22
+ assert home_memory.exists()
23
+
24
+ # Repo root should be chosen first
25
+ content = repo_memory.read_text()
26
+ assert "Content from repo root" in content
27
+
28
+
29
+ def test_memory_fallback_to_home(tmp_path):
30
+ """Test that home dir is used when repo root doesn't have MEMORY.md."""
31
+ # Setup: Create only home location
32
+ repo_root = tmp_path / "repo"
33
+ repo_root.mkdir()
34
+
35
+ patchpal_dir = tmp_path / "home" / ".patchpal" / "repos" / "repo"
36
+ patchpal_dir.mkdir(parents=True)
37
+
38
+ # Create MEMORY.md only in home location
39
+ home_memory = patchpal_dir / "MEMORY.md"
40
+ home_memory.write_text("# Template\n\n---\n\nContent from home dir")
41
+
42
+ # Repo root doesn't have it
43
+ repo_memory = repo_root / "MEMORY.md"
44
+ assert not repo_memory.exists()
45
+
46
+ # Should fall back to home dir
47
+ assert home_memory.exists()
48
+ content = home_memory.read_text()
49
+ assert "Content from home dir" in content
50
+
51
+
52
+ def test_memory_has_content_detection():
53
+ """Test that user content detection works correctly."""
54
+ # Test the content detection logic directly
55
+ memory_content = "# Project Memory\n\n---\n\nActual user content here"
56
+
57
+ # Check for user content after "---" separator
58
+ has_content = False
59
+ if "---" in memory_content:
60
+ parts = memory_content.split("---", 1)
61
+ if len(parts) > 1:
62
+ user_content = parts[1].strip()
63
+ if user_content and len(user_content) > 10:
64
+ has_content = True
65
+
66
+ assert has_content is True
67
+ assert "Actual user content here" in memory_content
68
+
69
+
70
+ def test_memory_empty_template_detection():
71
+ """Test that empty template is detected as no content."""
72
+ # Test with only template (no content after ---)
73
+ memory_content = "# Project Memory\n\n---\n\n"
74
+
75
+ # Check for user content after "---" separator
76
+ has_content = False
77
+ if "---" in memory_content:
78
+ parts = memory_content.split("---", 1)
79
+ if len(parts) > 1:
80
+ user_content = parts[1].strip()
81
+ if user_content and len(user_content) > 10:
82
+ has_content = True
83
+
84
+ # Should detect as empty (no real user content)
85
+ assert has_content is False
86
+
87
+
88
+ def test_memory_get_info_function(tmp_path, monkeypatch):
89
+ """Test get_memory_info helper function with real file."""
90
+ # Create a temporary repo with MEMORY.md
91
+ repo_root = tmp_path / "test_repo"
92
+ repo_root.mkdir()
93
+
94
+ # Create MEMORY.md in repo
95
+ repo_memory = repo_root / "MEMORY.md"
96
+ repo_memory.write_text("# Memory\n\n---\n\nProject uses Python 3.11")
97
+
98
+ # Change to that directory
99
+ monkeypatch.chdir(repo_root)
100
+
101
+ # Now test that the helper function would work (without reloading)
102
+ # We can't actually test get_memory_info without reloading,
103
+ # but we can verify the file is there
104
+ assert repo_memory.exists()
105
+ content = repo_memory.read_text()
106
+ assert "Python 3.11" in content
107
+
108
+ # Test content detection
109
+ has_content = False
110
+ if "---" in content:
111
+ parts = content.split("---", 1)
112
+ if len(parts) > 1:
113
+ user_content = parts[1].strip()
114
+ if user_content and len(user_content) > 10:
115
+ has_content = True
116
+
117
+ assert has_content is True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes