sugarai 3.1.1.dev5__tar.gz → 3.1.1.dev6__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 (90) hide show
  1. {sugarai-3.1.1.dev5/sugarai.egg-info → sugarai-3.1.1.dev6}/PKG-INFO +11 -1
  2. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/README.md +10 -0
  3. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/pyproject.toml +1 -1
  4. sugarai-3.1.1.dev6/sugar/config/__init__.py +6 -0
  5. sugarai-3.1.1.dev6/sugar/config/prompt_config.py +135 -0
  6. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/issue_responder.py +16 -1
  7. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6/sugarai.egg-info}/PKG-INFO +11 -1
  8. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/SOURCES.txt +2 -0
  9. sugarai-3.1.1.dev6/tests/test_prompt_config.py +359 -0
  10. sugarai-3.1.1.dev5/sugar/config/__init__.py +0 -5
  11. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/LICENSE +0 -0
  12. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/setup.cfg +0 -0
  13. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/__init__.py +0 -0
  14. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/__version__.py +0 -0
  15. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/__init__.py +0 -0
  16. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/base.py +0 -0
  17. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/hooks.py +0 -0
  18. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/subagent_manager.py +0 -0
  19. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/tools.py +0 -0
  20. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/__init__.py +0 -0
  21. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/api_keys.py +0 -0
  22. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/tiers.py +0 -0
  23. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/usage.py +0 -0
  24. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/config/issue_responder_config.py +0 -0
  25. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/core/__init__.py +0 -0
  26. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/core/loop.py +0 -0
  27. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/__init__.py +0 -0
  28. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/code_quality.py +0 -0
  29. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/error_monitor.py +0 -0
  30. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/github_watcher.py +0 -0
  31. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/test_coverage.py +0 -0
  32. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/__init__.py +0 -0
  33. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/agent_sdk_executor.py +0 -0
  34. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/base.py +0 -0
  35. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/claude_wrapper.py +0 -0
  36. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/structured_request.py +0 -0
  37. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/integrations/__init__.py +0 -0
  38. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/integrations/github.py +0 -0
  39. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/__init__.py +0 -0
  40. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/adaptive_scheduler.py +0 -0
  41. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/feedback_processor.py +0 -0
  42. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/main.py +0 -0
  43. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/mcp/__init__.py +0 -0
  44. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/mcp/server.py +0 -0
  45. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/__init__.py +0 -0
  46. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/agent_router.py +0 -0
  47. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/task_orchestrator.py +0 -0
  48. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/__init__.py +0 -0
  49. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/base.py +0 -0
  50. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/default.py +0 -0
  51. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/__init__.py +0 -0
  52. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/coordinator.py +0 -0
  53. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/diff_validator.py +0 -0
  54. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/evidence.py +0 -0
  55. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/failure_handler.py +0 -0
  56. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/functional_verifier.py +0 -0
  57. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/preflight_checks.py +0 -0
  58. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/success_criteria.py +0 -0
  59. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/test_validator.py +0 -0
  60. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/truth_enforcer.py +0 -0
  61. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/__init__.py +0 -0
  62. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/issue_response_manager.py +0 -0
  63. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/task_type_manager.py +0 -0
  64. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/work_queue.py +0 -0
  65. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/__init__.py +0 -0
  66. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/git_operations.py +0 -0
  67. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/toon_encoder.py +0 -0
  68. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/workflow/__init__.py +0 -0
  69. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/workflow/orchestrator.py +0 -0
  70. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/dependency_links.txt +0 -0
  71. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/entry_points.txt +0 -0
  72. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/requires.txt +0 -0
  73. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/top_level.txt +0 -0
  74. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_agent.py +0 -0
  75. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_billing.py +0 -0
  76. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_cli.py +0 -0
  77. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_core_loop.py +0 -0
  78. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_goose_integration.py +0 -0
  79. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_hold_functionality.py +0 -0
  80. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_hooks.py +0 -0
  81. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_issue_responder_cli.py +0 -0
  82. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_issue_response_integration.py +0 -0
  83. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_orchestration.py +0 -0
  84. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_profiles.py +0 -0
  85. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_quality_gates.py +0 -0
  86. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_storage.py +0 -0
  87. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_subagent_manager.py +0 -0
  88. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_task_types.py +0 -0
  89. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_toon_encoder.py +0 -0
  90. {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_v3_benchmarks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sugarai
3
- Version: 3.1.1.dev5
3
+ Version: 3.1.1.dev6
4
4
  Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
5
5
  Author-email: Steven Leggett <contact@roboticforce.io>
6
6
  License: MIT
@@ -228,6 +228,16 @@ sugar issue respond 42 --post
228
228
 
229
229
  The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
230
230
 
231
+ **Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
232
+
233
+ ```json
234
+ {
235
+ "instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
236
+ "guidelines": ["Always search the codebase first", "Include file paths"],
237
+ "constraints": ["Never share API keys", "Don't promise release dates"]
238
+ }
239
+ ```
240
+
231
241
  **Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
232
242
 
233
243
  ## How It Works
@@ -174,6 +174,16 @@ sugar issue respond 42 --post
174
174
 
175
175
  The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
176
176
 
177
+ **Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
178
+
179
+ ```json
180
+ {
181
+ "instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
182
+ "guidelines": ["Always search the codebase first", "Include file paths"],
183
+ "constraints": ["Never share API keys", "Don't promise release dates"]
184
+ }
185
+ ```
186
+
177
187
  **Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
178
188
 
179
189
  ## How It Works
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sugarai"
7
- version = "3.1.1.dev5"
7
+ version = "3.1.1.dev6"
8
8
  description = "Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,6 @@
1
+ """Sugar configuration loaders."""
2
+
3
+ from .issue_responder_config import IssueResponderConfig
4
+ from .prompt_config import PersonaConfig, PromptConfig
5
+
6
+ __all__ = ["IssueResponderConfig", "PersonaConfig", "PromptConfig"]
@@ -0,0 +1,135 @@
1
+ """
2
+ Prompt configuration for Issue Responder
3
+
4
+ Loads custom prompt configuration from .sugar/prompts/issue_responder.json
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class PersonaConfig:
18
+ """Agent persona configuration"""
19
+
20
+ role: Optional[str] = None
21
+ goal: Optional[str] = None
22
+ expertise: List[str] = field(default_factory=list)
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: dict) -> "PersonaConfig":
26
+ """Create PersonaConfig from a dictionary.
27
+
28
+ Args:
29
+ data: Dictionary containing persona configuration
30
+
31
+ Returns:
32
+ PersonaConfig instance
33
+ """
34
+ return cls(
35
+ role=data.get("role"),
36
+ goal=data.get("goal"),
37
+ expertise=data.get("expertise", []),
38
+ )
39
+
40
+
41
+ @dataclass
42
+ class PromptConfig:
43
+ """Custom prompt configuration for Issue Responder"""
44
+
45
+ instructions: str
46
+ name: Optional[str] = None
47
+ description: Optional[str] = None
48
+ persona: Optional[PersonaConfig] = None
49
+ guidelines: List[str] = field(default_factory=list)
50
+ constraints: List[str] = field(default_factory=list)
51
+
52
+ @classmethod
53
+ def from_dict(cls, data: dict) -> "PromptConfig":
54
+ """Load from dictionary"""
55
+ persona_data = data.get("persona")
56
+ persona = PersonaConfig.from_dict(persona_data) if persona_data else None
57
+
58
+ return cls(
59
+ instructions=data.get("instructions", ""),
60
+ name=data.get("name"),
61
+ description=data.get("description"),
62
+ persona=persona,
63
+ guidelines=data.get("guidelines", []),
64
+ constraints=data.get("constraints", []),
65
+ )
66
+
67
+ @classmethod
68
+ def load_from_file(cls, config_path: Path = None) -> Optional["PromptConfig"]:
69
+ """
70
+ Load from JSON file.
71
+
72
+ Default path: .sugar/prompts/issue_responder.json
73
+ Returns None if file doesn't exist (use defaults)
74
+ """
75
+ if config_path is None:
76
+ config_path = Path(".sugar/prompts/issue_responder.json")
77
+
78
+ if not config_path.exists():
79
+ logger.debug(f"No custom prompt config at {config_path}")
80
+ return None
81
+
82
+ try:
83
+ with open(config_path) as f:
84
+ data = json.load(f)
85
+ return cls.from_dict(data)
86
+ except json.JSONDecodeError as e:
87
+ logger.error(f"Invalid JSON in {config_path}: {e}")
88
+ return None
89
+ except Exception as e:
90
+ logger.error(f"Error loading prompt config: {e}")
91
+ return None
92
+
93
+ def validate(self) -> List[str]:
94
+ """Validate configuration, return list of errors"""
95
+ errors = []
96
+ if not self.instructions or not self.instructions.strip():
97
+ errors.append("instructions is required and cannot be empty")
98
+ return errors
99
+
100
+ def build_system_prompt_additions(self) -> str:
101
+ """
102
+ Build the system prompt additions from this config.
103
+ Returns a string to be added to the base system prompt.
104
+ """
105
+ parts = []
106
+
107
+ # Add persona section
108
+ if self.persona:
109
+ persona_parts = []
110
+ if self.persona.role:
111
+ persona_parts.append(f"**Role**: {self.persona.role}")
112
+ if self.persona.goal:
113
+ persona_parts.append(f"**Goal**: {self.persona.goal}")
114
+ if self.persona.expertise:
115
+ persona_parts.append(
116
+ f"**Expertise**: {', '.join(self.persona.expertise)}"
117
+ )
118
+ if persona_parts:
119
+ parts.append("## Your Persona\n" + "\n".join(persona_parts))
120
+
121
+ # Add instructions
122
+ if self.instructions:
123
+ parts.append(f"## Project-Specific Instructions\n{self.instructions}")
124
+
125
+ # Add guidelines
126
+ if self.guidelines:
127
+ guidelines_text = "\n".join(f"- {g}" for g in self.guidelines)
128
+ parts.append(f"## Guidelines\n{guidelines_text}")
129
+
130
+ # Add constraints
131
+ if self.constraints:
132
+ constraints_text = "\n".join(f"- {c}" for c in self.constraints)
133
+ parts.append(f"## Constraints\n{constraints_text}")
134
+
135
+ return "\n\n".join(parts)
@@ -15,6 +15,7 @@ import re
15
15
  from dataclasses import dataclass, field
16
16
  from typing import Any, Dict, List, Optional
17
17
 
18
+ from ..config import PromptConfig
18
19
  from .base import BaseProfile, ProfileConfig
19
20
 
20
21
  logger = logging.getLogger(__name__)
@@ -110,7 +111,7 @@ class IssueResponderProfile(BaseProfile):
110
111
  if repo:
111
112
  repo_info = f"\nRepository: {repo}"
112
113
 
113
- return f"""You are Sugar, an AI assistant that helps respond to GitHub issues.
114
+ base_prompt = f"""You are Sugar, an AI assistant that helps respond to GitHub issues.
114
115
 
115
116
  {repo_info}
116
117
 
@@ -156,6 +157,20 @@ Rate your confidence in your response:
156
157
  Only auto-post responses with confidence >= 0.8.
157
158
  """
158
159
 
160
+ # Load custom prompt config if available
161
+ custom_config = PromptConfig.load_from_file()
162
+ if custom_config:
163
+ errors = custom_config.validate()
164
+ if errors:
165
+ logger.warning(f"Invalid prompt config: {errors}")
166
+ else:
167
+ custom_additions = custom_config.build_system_prompt_additions()
168
+ if custom_additions:
169
+ base_prompt += f"\n\n# Custom Configuration\n\n{custom_additions}"
170
+ logger.info("Loaded custom prompt configuration")
171
+
172
+ return base_prompt
173
+
159
174
  async def process_input(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
160
175
  """Process issue data for analysis"""
161
176
  issue = input_data.get("issue", {})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sugarai
3
- Version: 3.1.1.dev5
3
+ Version: 3.1.1.dev6
4
4
  Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
5
5
  Author-email: Steven Leggett <contact@roboticforce.io>
6
6
  License: MIT
@@ -228,6 +228,16 @@ sugar issue respond 42 --post
228
228
 
229
229
  The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
230
230
 
231
+ **Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
232
+
233
+ ```json
234
+ {
235
+ "instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
236
+ "guidelines": ["Always search the codebase first", "Include file paths"],
237
+ "constraints": ["Never share API keys", "Don't promise release dates"]
238
+ }
239
+ ```
240
+
231
241
  **Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
232
242
 
233
243
  ## How It Works
@@ -15,6 +15,7 @@ sugar/billing/tiers.py
15
15
  sugar/billing/usage.py
16
16
  sugar/config/__init__.py
17
17
  sugar/config/issue_responder_config.py
18
+ sugar/config/prompt_config.py
18
19
  sugar/core/__init__.py
19
20
  sugar/core/loop.py
20
21
  sugar/discovery/__init__.py
@@ -77,6 +78,7 @@ tests/test_issue_responder_cli.py
77
78
  tests/test_issue_response_integration.py
78
79
  tests/test_orchestration.py
79
80
  tests/test_profiles.py
81
+ tests/test_prompt_config.py
80
82
  tests/test_quality_gates.py
81
83
  tests/test_storage.py
82
84
  tests/test_subagent_manager.py
@@ -0,0 +1,359 @@
1
+ """
2
+ Tests for prompt configuration system
3
+ """
4
+
5
+ import json
6
+ import pytest
7
+ from pathlib import Path
8
+
9
+ from sugar.config.prompt_config import PersonaConfig, PromptConfig
10
+
11
+
12
+ class TestPersonaConfig:
13
+ """Test PersonaConfig dataclass"""
14
+
15
+ def test_default_values(self):
16
+ """Test default values for PersonaConfig"""
17
+ persona = PersonaConfig()
18
+
19
+ assert persona.role is None
20
+ assert persona.goal is None
21
+ assert persona.expertise == []
22
+
23
+ def test_from_dict_with_full_data(self):
24
+ """Test from_dict with all fields populated"""
25
+ data = {
26
+ "role": "Senior QA Engineer",
27
+ "goal": "Ensure high-quality test coverage",
28
+ "expertise": ["pytest", "playwright", "test automation"],
29
+ }
30
+
31
+ persona = PersonaConfig.from_dict(data)
32
+
33
+ assert persona.role == "Senior QA Engineer"
34
+ assert persona.goal == "Ensure high-quality test coverage"
35
+ assert persona.expertise == ["pytest", "playwright", "test automation"]
36
+
37
+ def test_from_dict_with_partial_data(self):
38
+ """Test from_dict with only some fields"""
39
+ data = {"role": "QA Engineer"}
40
+
41
+ persona = PersonaConfig.from_dict(data)
42
+
43
+ assert persona.role == "QA Engineer"
44
+ assert persona.goal is None
45
+ assert persona.expertise == []
46
+
47
+ def test_from_dict_with_empty_dict(self):
48
+ """Test from_dict with empty dictionary"""
49
+ persona = PersonaConfig.from_dict({})
50
+
51
+ assert persona.role is None
52
+ assert persona.goal is None
53
+ assert persona.expertise == []
54
+
55
+
56
+ class TestPromptConfig:
57
+ """Test PromptConfig dataclass"""
58
+
59
+ def test_default_values(self):
60
+ """Test default values for PromptConfig with required instructions"""
61
+ config = PromptConfig(instructions="Always write tests first")
62
+
63
+ assert config.instructions == "Always write tests first"
64
+ assert config.name is None
65
+ assert config.description is None
66
+ assert config.persona is None
67
+ assert config.guidelines == []
68
+ assert config.constraints == []
69
+
70
+ def test_from_dict_with_full_data(self):
71
+ """Test from_dict with all fields populated"""
72
+ data = {
73
+ "name": "Test-First Development",
74
+ "description": "TDD approach for all features",
75
+ "instructions": "Write tests before implementation",
76
+ "persona": {
77
+ "role": "Senior QA Engineer",
78
+ "goal": "Ensure code quality",
79
+ "expertise": ["pytest", "TDD"],
80
+ },
81
+ "guidelines": ["Use descriptive test names", "Follow AAA pattern"],
82
+ "constraints": ["Minimum 80% coverage", "No skipped tests"],
83
+ }
84
+
85
+ config = PromptConfig.from_dict(data)
86
+
87
+ assert config.name == "Test-First Development"
88
+ assert config.description == "TDD approach for all features"
89
+ assert config.instructions == "Write tests before implementation"
90
+ assert config.persona is not None
91
+ assert config.persona.role == "Senior QA Engineer"
92
+ assert config.persona.goal == "Ensure code quality"
93
+ assert config.persona.expertise == ["pytest", "TDD"]
94
+ assert config.guidelines == ["Use descriptive test names", "Follow AAA pattern"]
95
+ assert config.constraints == ["Minimum 80% coverage", "No skipped tests"]
96
+
97
+ def test_from_dict_with_partial_data(self):
98
+ """Test from_dict with only required fields"""
99
+ data = {"instructions": "Follow project coding standards"}
100
+
101
+ config = PromptConfig.from_dict(data)
102
+
103
+ assert config.instructions == "Follow project coding standards"
104
+ assert config.name is None
105
+ assert config.description is None
106
+ assert config.persona is None
107
+ assert config.guidelines == []
108
+ assert config.constraints == []
109
+
110
+ def test_from_dict_without_persona(self):
111
+ """Test from_dict when persona is not provided"""
112
+ data = {
113
+ "instructions": "Use black for formatting",
114
+ "guidelines": ["Follow PEP 8"],
115
+ }
116
+
117
+ config = PromptConfig.from_dict(data)
118
+
119
+ assert config.instructions == "Use black for formatting"
120
+ assert config.persona is None
121
+ assert config.guidelines == ["Follow PEP 8"]
122
+
123
+ def test_validation_missing_instructions(self):
124
+ """Test validation fails when instructions is missing"""
125
+ config = PromptConfig(instructions="")
126
+
127
+ errors = config.validate()
128
+
129
+ assert len(errors) == 1
130
+ assert "instructions is required and cannot be empty" in errors[0]
131
+
132
+ def test_validation_empty_instructions(self):
133
+ """Test validation fails when instructions is whitespace only"""
134
+ config = PromptConfig(instructions=" ")
135
+
136
+ errors = config.validate()
137
+
138
+ assert len(errors) == 1
139
+ assert "instructions is required and cannot be empty" in errors[0]
140
+
141
+ def test_validation_valid_config(self):
142
+ """Test validation passes for valid config"""
143
+ config = PromptConfig(
144
+ instructions="Follow coding standards",
145
+ guidelines=["Use type hints", "Write docstrings"],
146
+ )
147
+
148
+ errors = config.validate()
149
+
150
+ assert len(errors) == 0
151
+
152
+
153
+ class TestLoadFromFile:
154
+ """Test file loading functionality"""
155
+
156
+ def test_load_from_file_missing_file(self, tmp_path):
157
+ """Test load_from_file returns None for missing file"""
158
+ config_path = tmp_path / "nonexistent.json"
159
+
160
+ config = PromptConfig.load_from_file(config_path)
161
+
162
+ assert config is None
163
+
164
+ def test_load_from_file_invalid_json(self, tmp_path):
165
+ """Test load_from_file returns None for invalid JSON"""
166
+ config_path = tmp_path / "invalid.json"
167
+ config_path.write_text("{ invalid json }")
168
+
169
+ config = PromptConfig.load_from_file(config_path)
170
+
171
+ assert config is None
172
+
173
+ def test_load_from_file_valid_json(self, tmp_path):
174
+ """Test load_from_file returns config for valid JSON"""
175
+ config_path = tmp_path / "config.json"
176
+ data = {
177
+ "name": "Project Config",
178
+ "instructions": "Follow project guidelines",
179
+ "persona": {"role": "Developer", "goal": "Build quality software"},
180
+ "guidelines": ["Write clean code"],
181
+ "constraints": ["No magic numbers"],
182
+ }
183
+ config_path.write_text(json.dumps(data))
184
+
185
+ config = PromptConfig.load_from_file(config_path)
186
+
187
+ assert config is not None
188
+ assert config.name == "Project Config"
189
+ assert config.instructions == "Follow project guidelines"
190
+ assert config.persona is not None
191
+ assert config.persona.role == "Developer"
192
+ assert config.guidelines == ["Write clean code"]
193
+ assert config.constraints == ["No magic numbers"]
194
+
195
+ def test_load_from_file_minimal_valid_json(self, tmp_path):
196
+ """Test load_from_file with minimal valid JSON"""
197
+ config_path = tmp_path / "minimal.json"
198
+ data = {"instructions": "Minimal config"}
199
+ config_path.write_text(json.dumps(data))
200
+
201
+ config = PromptConfig.load_from_file(config_path)
202
+
203
+ assert config is not None
204
+ assert config.instructions == "Minimal config"
205
+ assert config.persona is None
206
+ assert config.guidelines == []
207
+ assert config.constraints == []
208
+
209
+ def test_load_from_file_default_path(self, tmp_path, monkeypatch):
210
+ """Test load_from_file uses default path when not specified"""
211
+ # Change to tmp_path directory
212
+ monkeypatch.chdir(tmp_path)
213
+
214
+ # Create default path
215
+ default_dir = tmp_path / ".sugar" / "prompts"
216
+ default_dir.mkdir(parents=True)
217
+ config_path = default_dir / "issue_responder.json"
218
+ data = {"instructions": "Default config"}
219
+ config_path.write_text(json.dumps(data))
220
+
221
+ config = PromptConfig.load_from_file()
222
+
223
+ assert config is not None
224
+ assert config.instructions == "Default config"
225
+
226
+
227
+ class TestBuildSystemPromptAdditions:
228
+ """Test build_system_prompt_additions functionality"""
229
+
230
+ def test_minimal_config_with_only_instructions(self):
231
+ """Test returns formatted instructions for minimal config"""
232
+ config = PromptConfig(instructions="Follow coding standards")
233
+
234
+ result = config.build_system_prompt_additions()
235
+
236
+ assert "## Project-Specific Instructions\nFollow coding standards" in result
237
+ assert "## Your Persona" not in result
238
+ assert "## Guidelines" not in result
239
+ assert "## Constraints" not in result
240
+
241
+ def test_includes_persona_section(self):
242
+ """Test includes persona section when persona provided"""
243
+ config = PromptConfig(
244
+ instructions="Write tests",
245
+ persona=PersonaConfig(
246
+ role="QA Engineer",
247
+ goal="Ensure quality",
248
+ expertise=["pytest", "testing"],
249
+ ),
250
+ )
251
+
252
+ result = config.build_system_prompt_additions()
253
+
254
+ assert "## Your Persona" in result
255
+ assert "**Role**: QA Engineer" in result
256
+ assert "**Goal**: Ensure quality" in result
257
+ assert "**Expertise**: pytest, testing" in result
258
+
259
+ def test_includes_guidelines_when_provided(self):
260
+ """Test includes guidelines section when provided"""
261
+ config = PromptConfig(
262
+ instructions="Write clean code",
263
+ guidelines=["Use type hints", "Write docstrings", "Follow PEP 8"],
264
+ )
265
+
266
+ result = config.build_system_prompt_additions()
267
+
268
+ assert "## Guidelines" in result
269
+ assert "- Use type hints" in result
270
+ assert "- Write docstrings" in result
271
+ assert "- Follow PEP 8" in result
272
+
273
+ def test_includes_constraints_when_provided(self):
274
+ """Test includes constraints section when provided"""
275
+ config = PromptConfig(
276
+ instructions="Follow rules",
277
+ constraints=[
278
+ "No TODO comments",
279
+ "Max line length 88",
280
+ "Type all functions",
281
+ ],
282
+ )
283
+
284
+ result = config.build_system_prompt_additions()
285
+
286
+ assert "## Constraints" in result
287
+ assert "- No TODO comments" in result
288
+ assert "- Max line length 88" in result
289
+ assert "- Type all functions" in result
290
+
291
+ def test_combines_all_sections_properly(self):
292
+ """Test combines all sections in correct order"""
293
+ config = PromptConfig(
294
+ instructions="Main instructions here",
295
+ persona=PersonaConfig(role="Developer", goal="Build features"),
296
+ guidelines=["Guideline 1", "Guideline 2"],
297
+ constraints=["Constraint 1"],
298
+ )
299
+
300
+ result = config.build_system_prompt_additions()
301
+
302
+ # Check all sections are present
303
+ assert "## Your Persona" in result
304
+ assert "## Project-Specific Instructions" in result
305
+ assert "## Guidelines" in result
306
+ assert "## Constraints" in result
307
+
308
+ # Check order (persona, instructions, guidelines, constraints)
309
+ persona_idx = result.index("## Your Persona")
310
+ instructions_idx = result.index("## Project-Specific Instructions")
311
+ guidelines_idx = result.index("## Guidelines")
312
+ constraints_idx = result.index("## Constraints")
313
+
314
+ assert persona_idx < instructions_idx < guidelines_idx < constraints_idx
315
+
316
+ # Check sections are separated by double newlines
317
+ assert "\n\n" in result
318
+
319
+ def test_persona_with_partial_fields(self):
320
+ """Test persona section with only some fields populated"""
321
+ config = PromptConfig(
322
+ instructions="Instructions",
323
+ persona=PersonaConfig(role="Developer"), # Only role, no goal/expertise
324
+ )
325
+
326
+ result = config.build_system_prompt_additions()
327
+
328
+ assert "## Your Persona" in result
329
+ assert "**Role**: Developer" in result
330
+ assert "**Goal**:" not in result
331
+ assert "**Expertise**:" not in result
332
+
333
+ def test_empty_persona_not_included(self):
334
+ """Test persona section not included if all fields are None/empty"""
335
+ config = PromptConfig(
336
+ instructions="Instructions",
337
+ persona=PersonaConfig(), # All fields None/empty
338
+ )
339
+
340
+ result = config.build_system_prompt_additions()
341
+
342
+ # Persona section should not be included if no fields are populated
343
+ assert "## Your Persona" not in result
344
+
345
+ def test_section_formatting(self):
346
+ """Test proper markdown formatting of sections"""
347
+ config = PromptConfig(
348
+ instructions="These are the instructions.\nMultiline instructions work.",
349
+ guidelines=["First guideline", "Second guideline"],
350
+ )
351
+
352
+ result = config.build_system_prompt_additions()
353
+
354
+ # Check instructions preserve newlines
355
+ assert "These are the instructions.\nMultiline instructions work." in result
356
+
357
+ # Check guidelines are bulleted
358
+ assert "- First guideline" in result
359
+ assert "- Second guideline" in result
@@ -1,5 +0,0 @@
1
- """Sugar configuration loaders."""
2
-
3
- from .issue_responder_config import IssueResponderConfig
4
-
5
- __all__ = ["IssueResponderConfig"]
File without changes
File without changes
File without changes