sugarai 3.1.1.dev4__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.dev4/sugarai.egg-info → sugarai-3.1.1.dev6}/PKG-INFO +11 -1
  2. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/README.md +10 -0
  3. {sugarai-3.1.1.dev4 → 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/issue_responder_config.py +143 -0
  6. sugarai-3.1.1.dev6/sugar/config/prompt_config.py +135 -0
  7. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/discovery/github_watcher.py +154 -2
  8. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/executor/agent_sdk_executor.py +80 -0
  9. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/main.py +23 -0
  10. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/profiles/issue_responder.py +16 -1
  11. sugarai-3.1.1.dev6/sugar/storage/__init__.py +5 -0
  12. sugarai-3.1.1.dev6/sugar/storage/issue_response_manager.py +258 -0
  13. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6/sugarai.egg-info}/PKG-INFO +11 -1
  14. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugarai.egg-info/SOURCES.txt +6 -0
  15. sugarai-3.1.1.dev6/tests/test_issue_response_integration.py +1042 -0
  16. sugarai-3.1.1.dev6/tests/test_prompt_config.py +359 -0
  17. sugarai-3.1.1.dev4/sugar/utils/__init__.py +0 -0
  18. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/LICENSE +0 -0
  19. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/setup.cfg +0 -0
  20. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/__init__.py +0 -0
  21. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/__version__.py +0 -0
  22. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/agent/__init__.py +0 -0
  23. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/agent/base.py +0 -0
  24. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/agent/hooks.py +0 -0
  25. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/agent/subagent_manager.py +0 -0
  26. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/agent/tools.py +0 -0
  27. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/billing/__init__.py +0 -0
  28. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/billing/api_keys.py +0 -0
  29. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/billing/tiers.py +0 -0
  30. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/billing/usage.py +0 -0
  31. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/core/__init__.py +0 -0
  32. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/core/loop.py +0 -0
  33. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/discovery/__init__.py +0 -0
  34. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/discovery/code_quality.py +0 -0
  35. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/discovery/error_monitor.py +0 -0
  36. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/discovery/test_coverage.py +0 -0
  37. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/executor/__init__.py +0 -0
  38. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/executor/base.py +0 -0
  39. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/executor/claude_wrapper.py +0 -0
  40. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/executor/structured_request.py +0 -0
  41. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/integrations/__init__.py +0 -0
  42. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/integrations/github.py +0 -0
  43. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/learning/__init__.py +0 -0
  44. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/learning/adaptive_scheduler.py +0 -0
  45. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/learning/feedback_processor.py +0 -0
  46. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/mcp/__init__.py +0 -0
  47. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/mcp/server.py +0 -0
  48. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/orchestration/__init__.py +0 -0
  49. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/orchestration/agent_router.py +0 -0
  50. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/orchestration/task_orchestrator.py +0 -0
  51. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/profiles/__init__.py +0 -0
  52. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/profiles/base.py +0 -0
  53. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/profiles/default.py +0 -0
  54. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/__init__.py +0 -0
  55. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/coordinator.py +0 -0
  56. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/diff_validator.py +0 -0
  57. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/evidence.py +0 -0
  58. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/failure_handler.py +0 -0
  59. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/functional_verifier.py +0 -0
  60. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/preflight_checks.py +0 -0
  61. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/success_criteria.py +0 -0
  62. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/test_validator.py +0 -0
  63. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/quality_gates/truth_enforcer.py +0 -0
  64. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/storage/task_type_manager.py +0 -0
  65. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/storage/work_queue.py +0 -0
  66. {sugarai-3.1.1.dev4/sugar/storage → sugarai-3.1.1.dev6/sugar/utils}/__init__.py +0 -0
  67. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/utils/git_operations.py +0 -0
  68. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/utils/toon_encoder.py +0 -0
  69. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/workflow/__init__.py +0 -0
  70. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugar/workflow/orchestrator.py +0 -0
  71. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugarai.egg-info/dependency_links.txt +0 -0
  72. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugarai.egg-info/entry_points.txt +0 -0
  73. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugarai.egg-info/requires.txt +0 -0
  74. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/sugarai.egg-info/top_level.txt +0 -0
  75. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_agent.py +0 -0
  76. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_billing.py +0 -0
  77. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_cli.py +0 -0
  78. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_core_loop.py +0 -0
  79. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_goose_integration.py +0 -0
  80. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_hold_functionality.py +0 -0
  81. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_hooks.py +0 -0
  82. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_issue_responder_cli.py +0 -0
  83. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_orchestration.py +0 -0
  84. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_profiles.py +0 -0
  85. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_quality_gates.py +0 -0
  86. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_storage.py +0 -0
  87. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_subagent_manager.py +0 -0
  88. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_task_types.py +0 -0
  89. {sugarai-3.1.1.dev4 → sugarai-3.1.1.dev6}/tests/test_toon_encoder.py +0 -0
  90. {sugarai-3.1.1.dev4 → 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.dev4
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.dev4"
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,143 @@
1
+ """Configuration loader for the Issue Responder feature."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Optional
5
+ import yaml
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass
10
+ class IssueResponderConfig:
11
+ """Configuration for AI-powered automatic issue responses.
12
+
13
+ Attributes:
14
+ enabled: Enable automatic issue responses
15
+ auto_post_threshold: Minimum confidence score (0.0-1.0) to auto-post responses
16
+ max_response_length: Maximum characters per response
17
+ response_delay_seconds: Delay before posting (0 = immediate)
18
+ rate_limit_per_hour: Maximum responses per hour (0 = unlimited)
19
+ respond_to_labels: Only respond to issues with these labels (empty = all)
20
+ skip_labels: Never respond to issues with these labels
21
+ skip_bot_issues: Skip issues created by bots
22
+ handle_follow_ups: How to handle follow-up comments - "ignore" | "queue" | "auto"
23
+ model: Claude model to use for generating responses
24
+ """
25
+
26
+ enabled: bool = False
27
+ auto_post_threshold: float = 0.8
28
+ max_response_length: int = 2000
29
+ response_delay_seconds: int = 0
30
+ rate_limit_per_hour: int = 10
31
+ respond_to_labels: List[str] = field(default_factory=list)
32
+ skip_labels: List[str] = field(
33
+ default_factory=lambda: ["wontfix", "duplicate", "stale"]
34
+ )
35
+ skip_bot_issues: bool = True
36
+ handle_follow_ups: str = "ignore" # "ignore" | "queue" | "auto"
37
+ model: str = "claude-sonnet-4-20250514"
38
+
39
+ @classmethod
40
+ def from_dict(cls, data: dict) -> "IssueResponderConfig":
41
+ """Create IssueResponderConfig from a dictionary.
42
+
43
+ Args:
44
+ data: Dictionary containing configuration values
45
+
46
+ Returns:
47
+ IssueResponderConfig instance
48
+
49
+ Example:
50
+ >>> config_data = {
51
+ ... "enabled": True,
52
+ ... "auto_post_threshold": 0.9,
53
+ ... "respond_to_labels": ["question", "help-wanted"]
54
+ ... }
55
+ >>> config = IssueResponderConfig.from_dict(config_data)
56
+ """
57
+ # Filter to only include known fields
58
+ valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
59
+ filtered_data = {k: v for k, v in data.items() if k in valid_fields}
60
+ return cls(**filtered_data)
61
+
62
+ @classmethod
63
+ def load_from_file(cls, config_path: str) -> "IssueResponderConfig":
64
+ """Load IssueResponderConfig from a YAML configuration file.
65
+
66
+ Args:
67
+ config_path: Path to the YAML configuration file
68
+
69
+ Returns:
70
+ IssueResponderConfig instance with values from the file
71
+
72
+ Raises:
73
+ FileNotFoundError: If the config file doesn't exist
74
+ KeyError: If the 'issue_responder' section is missing
75
+ yaml.YAMLError: If the YAML file is malformed
76
+
77
+ Example:
78
+ >>> config = IssueResponderConfig.load_from_file(".sugar/config.yaml")
79
+ """
80
+ config_file = Path(config_path)
81
+
82
+ if not config_file.exists():
83
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
84
+
85
+ with open(config_file, "r") as f:
86
+ config_data = yaml.safe_load(f)
87
+
88
+ # Navigate to the issue_responder section
89
+ if "sugar" not in config_data:
90
+ raise KeyError("Configuration file missing 'sugar' section")
91
+
92
+ if "issue_responder" not in config_data["sugar"]:
93
+ raise KeyError("Configuration file missing 'sugar.issue_responder' section")
94
+
95
+ issue_responder_data = config_data["sugar"]["issue_responder"]
96
+ return cls.from_dict(issue_responder_data)
97
+
98
+ def validate(self) -> List[str]:
99
+ """Validate configuration values and return list of errors.
100
+
101
+ Returns:
102
+ List of validation error messages (empty if valid)
103
+
104
+ Example:
105
+ >>> config = IssueResponderConfig(auto_post_threshold=1.5)
106
+ >>> errors = config.validate()
107
+ >>> if errors:
108
+ ... print("Invalid configuration:", errors)
109
+ """
110
+ errors = []
111
+
112
+ # Validate auto_post_threshold
113
+ if not 0.0 <= self.auto_post_threshold <= 1.0:
114
+ errors.append(
115
+ f"auto_post_threshold must be between 0.0 and 1.0, got {self.auto_post_threshold}"
116
+ )
117
+
118
+ # Validate max_response_length
119
+ if self.max_response_length <= 0:
120
+ errors.append(
121
+ f"max_response_length must be positive, got {self.max_response_length}"
122
+ )
123
+
124
+ # Validate response_delay_seconds
125
+ if self.response_delay_seconds < 0:
126
+ errors.append(
127
+ f"response_delay_seconds must be non-negative, got {self.response_delay_seconds}"
128
+ )
129
+
130
+ # Validate rate_limit_per_hour
131
+ if self.rate_limit_per_hour < 0:
132
+ errors.append(
133
+ f"rate_limit_per_hour must be non-negative, got {self.rate_limit_per_hour}"
134
+ )
135
+
136
+ # Validate handle_follow_ups
137
+ valid_follow_up_values = {"ignore", "queue", "auto"}
138
+ if self.handle_follow_ups not in valid_follow_up_values:
139
+ errors.append(
140
+ f"handle_follow_ups must be one of {valid_follow_up_values}, got '{self.handle_follow_ups}'"
141
+ )
142
+
143
+ return errors
@@ -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)
@@ -11,6 +11,9 @@ import os
11
11
  from datetime import datetime, timedelta, timezone
12
12
  from typing import List, Dict, Any, Optional
13
13
 
14
+ from ..storage import IssueResponseManager
15
+ from ..config import IssueResponderConfig
16
+
14
17
  # Optional PyGithub import
15
18
  try:
16
19
  from github import Github, GithubException
@@ -25,11 +28,20 @@ logger = logging.getLogger(__name__)
25
28
  class GitHubWatcher:
26
29
  """Monitor GitHub repository for issues and pull requests"""
27
30
 
28
- def __init__(self, config: dict):
31
+ def __init__(
32
+ self,
33
+ config: dict,
34
+ issue_responder_config: Optional[IssueResponderConfig] = None,
35
+ ):
29
36
  self.config = config
30
37
  self.enabled = config.get("enabled", False)
31
38
  self.repo_name = config.get("repo", "")
32
39
  self.auth_method = config.get("auth_method", "auto")
40
+ self.issue_responder_config = issue_responder_config
41
+
42
+ # Initialize IssueResponseManager
43
+ db_path = config.get("db_path", ".sugar/sugar.db")
44
+ self.issue_response_manager = IssueResponseManager(db_path)
33
45
 
34
46
  if not self.enabled:
35
47
  return
@@ -156,6 +168,20 @@ class GitHubWatcher:
156
168
  work_items = []
157
169
 
158
170
  try:
171
+ # Initialize issue response manager
172
+ await self.issue_response_manager.initialize()
173
+
174
+ # Load issue responder config if not provided in constructor
175
+ responder_config = self.issue_responder_config
176
+ if responder_config is None:
177
+ try:
178
+ responder_config = IssueResponderConfig.load_from_file(
179
+ ".sugar/config.yaml"
180
+ )
181
+ except (FileNotFoundError, KeyError):
182
+ # If config doesn't exist, use default (disabled)
183
+ responder_config = IssueResponderConfig()
184
+
159
185
  gh_command = self.config.get("gh_cli", {}).get("command", "gh")
160
186
  issue_labels = self.config.get("issue_labels", ["bug", "enhancement"])
161
187
 
@@ -163,6 +189,7 @@ class GitHubWatcher:
163
189
  self._log_label_filtering_mode(issue_labels)
164
190
 
165
191
  # Get all open issues first (we'll filter by labels after)
192
+ # Include author field for bot detection
166
193
  cmd = [
167
194
  gh_command,
168
195
  "issue",
@@ -174,7 +201,7 @@ class GitHubWatcher:
174
201
  "--limit",
175
202
  "50", # Get more issues to filter from
176
203
  "--json",
177
- "number,title,body,labels,assignees,comments,createdAt,updatedAt,url",
204
+ "number,title,body,labels,assignees,comments,createdAt,updatedAt,url,author",
178
205
  ]
179
206
 
180
207
  # Note: We don't use --label flag here because it uses AND logic
@@ -211,6 +238,49 @@ class GitHubWatcher:
211
238
  )
212
239
 
213
240
  for issue in filtered_issues:
241
+ # Check if issue responder is enabled and should create response work item
242
+ if responder_config.enabled and self._should_respond_to_issue(
243
+ issue, responder_config
244
+ ):
245
+ # Check if we've already responded to this issue
246
+ has_responded = await self.issue_response_manager.has_responded(
247
+ self.repo_name, issue["number"], "initial"
248
+ )
249
+
250
+ if not has_responded:
251
+ # Create issue response work item
252
+ response_work_item = {
253
+ "type": "issue_response",
254
+ "title": f"Respond to issue #{issue['number']}: {issue['title']}",
255
+ "source_file": f"github://issues/{issue['number']}",
256
+ "priority": 2, # Medium priority
257
+ "context": {
258
+ "github_issue": {
259
+ "number": issue["number"],
260
+ "title": issue["title"],
261
+ "body": issue.get("body", ""),
262
+ "user": {
263
+ "login": issue.get("author", {}).get(
264
+ "login", "unknown"
265
+ )
266
+ },
267
+ "labels": [
268
+ label["name"]
269
+ for label in issue.get("labels", [])
270
+ ],
271
+ "created_at": issue["createdAt"],
272
+ },
273
+ "repo": self.repo_name,
274
+ "response_type": "initial",
275
+ },
276
+ }
277
+ work_items.append(response_work_item)
278
+ logger.debug(
279
+ f"Created issue_response work item for #{issue['number']}"
280
+ )
281
+ continue
282
+
283
+ # Create standard work item if not creating a response item
214
284
  work_item = self._create_work_item_from_issue_data(issue)
215
285
  if work_item:
216
286
  work_items.append(work_item)
@@ -225,6 +295,20 @@ class GitHubWatcher:
225
295
  work_items = []
226
296
 
227
297
  try:
298
+ # Initialize issue response manager
299
+ await self.issue_response_manager.initialize()
300
+
301
+ # Load issue responder config if not provided in constructor
302
+ responder_config = self.issue_responder_config
303
+ if responder_config is None:
304
+ try:
305
+ responder_config = IssueResponderConfig.load_from_file(
306
+ ".sugar/config.yaml"
307
+ )
308
+ except (FileNotFoundError, KeyError):
309
+ # If config doesn't exist, use default (disabled)
310
+ responder_config = IssueResponderConfig()
311
+
228
312
  # Get recent issues
229
313
  since = datetime.now(timezone.utc) - timedelta(days=7)
230
314
  repo = self.github.get_repo(self.repo_name)
@@ -263,8 +347,50 @@ class GitHubWatcher:
263
347
  "createdAt": issue.created_at.isoformat(),
264
348
  "updatedAt": issue.updated_at.isoformat(),
265
349
  "url": issue.html_url,
350
+ "author": {"login": issue.user.login},
266
351
  }
267
352
 
353
+ # Check if issue responder is enabled and should create response work item
354
+ if responder_config.enabled and self._should_respond_to_issue(
355
+ issue_data, responder_config
356
+ ):
357
+ # Check if we've already responded to this issue
358
+ has_responded = await self.issue_response_manager.has_responded(
359
+ self.repo_name, issue.number, "initial"
360
+ )
361
+
362
+ if not has_responded:
363
+ # Create issue response work item
364
+ response_work_item = {
365
+ "type": "issue_response",
366
+ "title": f"Respond to issue #{issue.number}: {issue.title}",
367
+ "source_file": f"github://issues/{issue.number}",
368
+ "priority": 2, # Medium priority
369
+ "context": {
370
+ "github_issue": {
371
+ "number": issue.number,
372
+ "title": issue.title,
373
+ "body": issue.body or "",
374
+ "user": {"login": issue.user.login},
375
+ "labels": [label.name for label in issue.labels],
376
+ "created_at": issue.created_at.isoformat(),
377
+ },
378
+ "repo": self.repo_name,
379
+ "response_type": "initial",
380
+ },
381
+ }
382
+ work_items.append(response_work_item)
383
+ logger.debug(
384
+ f"Created issue_response work item for #{issue.number}"
385
+ )
386
+ processed_count += 1
387
+
388
+ # Limit to 10 issues after filtering
389
+ if processed_count >= 10:
390
+ break
391
+ continue
392
+
393
+ # Create standard work item if not creating a response item
268
394
  work_item = self._create_work_item_from_issue_data(issue_data)
269
395
  if work_item:
270
396
  work_items.append(work_item)
@@ -552,6 +678,32 @@ class GitHubWatcher:
552
678
  logger.error(f"Error using PyGithub to assign: {e}")
553
679
  return False
554
680
 
681
+ def _should_respond_to_issue(
682
+ self, issue: dict, config: IssueResponderConfig
683
+ ) -> bool:
684
+ """Check if we should create a response work item for this issue"""
685
+ # Check bot issues
686
+ if config.skip_bot_issues:
687
+ author = issue.get("author", {}).get("login", "")
688
+ if author.endswith("[bot]") or "[bot]" in author:
689
+ return False
690
+
691
+ # Check skip labels
692
+ issue_labels = [l.get("name", "") for l in issue.get("labels", [])]
693
+ for skip_label in config.skip_labels:
694
+ if skip_label in issue_labels:
695
+ return False
696
+
697
+ # Check respond_to_labels (if not empty, issue must have one of these)
698
+ if config.respond_to_labels:
699
+ has_matching_label = any(
700
+ label in issue_labels for label in config.respond_to_labels
701
+ )
702
+ if not has_matching_label:
703
+ return False
704
+
705
+ return True
706
+
555
707
  def _should_include_issue_by_labels(
556
708
  self, issue_labels: list, config_labels: list, original_config: list
557
709
  ) -> bool:
@@ -16,6 +16,10 @@ from typing import Any, Dict, List, Optional
16
16
 
17
17
  from .base import BaseExecutor, ExecutionResult
18
18
  from ..agent import SugarAgent, SugarAgentConfig
19
+ from ..storage import IssueResponseManager
20
+ from ..profiles import IssueResponderProfile
21
+ from ..config import IssueResponderConfig
22
+ from ..integrations import GitHubClient
19
23
 
20
24
  logger = logging.getLogger(__name__)
21
25
 
@@ -83,6 +87,76 @@ class AgentSDKExecutor(BaseExecutor):
83
87
  )
84
88
  return self._agent
85
89
 
90
+ async def _execute_issue_response(self, work_item: Dict) -> Dict:
91
+ """Execute an issue response task using IssueResponderProfile"""
92
+ # Extract issue data from work_item context
93
+ context = work_item.get("context", {})
94
+ issue_data = context.get("github_issue", {})
95
+ repo = context.get("repo", "")
96
+
97
+ # Load config
98
+ config = IssueResponderConfig.load_from_file(".sugar/config.yaml")
99
+
100
+ # Create profile with config settings
101
+ profile = IssueResponderProfile()
102
+ profile.config.settings["max_response_length"] = config.max_response_length
103
+ profile.config.settings["auto_post_threshold"] = config.auto_post_threshold
104
+
105
+ # Process input
106
+ input_data = {"issue": issue_data, "repo": repo}
107
+ processed = await profile.process_input(input_data)
108
+
109
+ # Execute with agent
110
+ agent_config = SugarAgentConfig(
111
+ model=config.model,
112
+ permission_mode="default",
113
+ allowed_tools=["Read", "Glob", "Grep"],
114
+ system_prompt_additions=profile.get_system_prompt({"repo": repo}),
115
+ )
116
+
117
+ agent = SugarAgent(agent_config)
118
+ await agent.start_session()
119
+
120
+ try:
121
+ response = await agent.execute(processed["prompt"])
122
+ output_data = {"content": response.content}
123
+ result = await profile.process_output(output_data)
124
+
125
+ response_data = result.get("response", {})
126
+ confidence = response_data.get("confidence", 0.0)
127
+ content = response_data.get("content", "")
128
+
129
+ # Check if should auto-post
130
+ should_post = confidence >= config.auto_post_threshold
131
+
132
+ if should_post:
133
+ # Post to GitHub
134
+ client = GitHubClient(repo=repo)
135
+ client.post_comment(issue_data["number"], content)
136
+
137
+ # Record response
138
+ manager = IssueResponseManager()
139
+ await manager.initialize()
140
+ await manager.record_response(
141
+ repo=repo,
142
+ issue_number=issue_data["number"],
143
+ response_type="initial",
144
+ confidence=confidence,
145
+ response_content=content,
146
+ labels_applied=response_data.get("suggested_labels", []),
147
+ was_auto_posted=True,
148
+ work_item_id=work_item.get("id"),
149
+ )
150
+
151
+ return {
152
+ "success": True,
153
+ "posted": should_post,
154
+ "confidence": confidence,
155
+ "content": content,
156
+ }
157
+ finally:
158
+ await agent.end_session()
159
+
86
160
  async def execute_work(self, work_item: Dict[str, Any]) -> Dict[str, Any]:
87
161
  """
88
162
  Execute a work item using the Claude Agent SDK.
@@ -93,6 +167,12 @@ class AgentSDKExecutor(BaseExecutor):
93
167
  Returns:
94
168
  Result dictionary compatible with Sugar's workflow
95
169
  """
170
+ # Check for specialized task types
171
+ task_type = work_item.get("type", "")
172
+
173
+ if task_type == "issue_response":
174
+ return await self._execute_issue_response(work_item)
175
+
96
176
  if self.dry_run:
97
177
  logger.info(f"DRY RUN: Simulating execution of {work_item.get('title')}")
98
178
  return await self._simulate_execution(work_item)