alma-memory 0.3.0__py3-none-any.whl → 0.5.0__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 (77) hide show
  1. alma/__init__.py +99 -29
  2. alma/confidence/__init__.py +47 -0
  3. alma/confidence/engine.py +540 -0
  4. alma/confidence/types.py +351 -0
  5. alma/config/loader.py +3 -2
  6. alma/consolidation/__init__.py +23 -0
  7. alma/consolidation/engine.py +678 -0
  8. alma/consolidation/prompts.py +84 -0
  9. alma/core.py +15 -15
  10. alma/domains/__init__.py +6 -6
  11. alma/domains/factory.py +12 -9
  12. alma/domains/schemas.py +17 -3
  13. alma/domains/types.py +8 -4
  14. alma/events/__init__.py +75 -0
  15. alma/events/emitter.py +284 -0
  16. alma/events/storage_mixin.py +246 -0
  17. alma/events/types.py +126 -0
  18. alma/events/webhook.py +425 -0
  19. alma/exceptions.py +49 -0
  20. alma/extraction/__init__.py +31 -0
  21. alma/extraction/auto_learner.py +264 -0
  22. alma/extraction/extractor.py +420 -0
  23. alma/graph/__init__.py +81 -0
  24. alma/graph/backends/__init__.py +18 -0
  25. alma/graph/backends/memory.py +236 -0
  26. alma/graph/backends/neo4j.py +417 -0
  27. alma/graph/base.py +159 -0
  28. alma/graph/extraction.py +198 -0
  29. alma/graph/store.py +860 -0
  30. alma/harness/__init__.py +4 -4
  31. alma/harness/base.py +18 -9
  32. alma/harness/domains.py +27 -11
  33. alma/initializer/__init__.py +37 -0
  34. alma/initializer/initializer.py +418 -0
  35. alma/initializer/types.py +250 -0
  36. alma/integration/__init__.py +9 -9
  37. alma/integration/claude_agents.py +10 -10
  38. alma/integration/helena.py +32 -22
  39. alma/integration/victor.py +57 -33
  40. alma/learning/__init__.py +27 -27
  41. alma/learning/forgetting.py +198 -148
  42. alma/learning/heuristic_extractor.py +40 -24
  43. alma/learning/protocols.py +62 -14
  44. alma/learning/validation.py +7 -2
  45. alma/mcp/__init__.py +4 -4
  46. alma/mcp/__main__.py +2 -1
  47. alma/mcp/resources.py +17 -16
  48. alma/mcp/server.py +102 -44
  49. alma/mcp/tools.py +174 -37
  50. alma/progress/__init__.py +3 -3
  51. alma/progress/tracker.py +26 -20
  52. alma/progress/types.py +8 -12
  53. alma/py.typed +0 -0
  54. alma/retrieval/__init__.py +11 -11
  55. alma/retrieval/cache.py +20 -21
  56. alma/retrieval/embeddings.py +4 -4
  57. alma/retrieval/engine.py +114 -35
  58. alma/retrieval/scoring.py +73 -63
  59. alma/session/__init__.py +2 -2
  60. alma/session/manager.py +5 -5
  61. alma/session/types.py +5 -4
  62. alma/storage/__init__.py +41 -0
  63. alma/storage/azure_cosmos.py +107 -31
  64. alma/storage/base.py +157 -4
  65. alma/storage/chroma.py +1443 -0
  66. alma/storage/file_based.py +56 -20
  67. alma/storage/pinecone.py +1080 -0
  68. alma/storage/postgresql.py +1452 -0
  69. alma/storage/qdrant.py +1306 -0
  70. alma/storage/sqlite_local.py +376 -31
  71. alma/types.py +62 -14
  72. alma_memory-0.5.0.dist-info/METADATA +905 -0
  73. alma_memory-0.5.0.dist-info/RECORD +76 -0
  74. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/WHEEL +1 -1
  75. alma_memory-0.3.0.dist-info/METADATA +0 -438
  76. alma_memory-0.3.0.dist-info/RECORD +0 -46
  77. {alma_memory-0.3.0.dist-info → alma_memory-0.5.0.dist-info}/top_level.txt +0 -0
alma/harness/__init__.py CHANGED
@@ -9,17 +9,17 @@ A structured framework for creating learning agents across any domain:
9
9
  """
10
10
 
11
11
  from alma.harness.base import (
12
- Setting,
13
- Context,
14
12
  Agent,
15
- MemorySchema,
13
+ Context,
16
14
  Harness,
15
+ MemorySchema,
16
+ Setting,
17
17
  )
18
18
  from alma.harness.domains import (
19
19
  CodingDomain,
20
- ResearchDomain,
21
20
  ContentDomain,
22
21
  OperationsDomain,
22
+ ResearchDomain,
23
23
  )
24
24
 
25
25
  __all__ = [
alma/harness/base.py CHANGED
@@ -17,23 +17,23 @@ Flow:
17
17
  Repeat -> Agent appears to "learn" without weight changes
18
18
  """
19
19
 
20
- from abc import ABC, abstractmethod
21
20
  from dataclasses import dataclass, field
22
21
  from datetime import datetime, timezone
23
- from typing import Optional, List, Dict, Any, Callable
24
22
  from enum import Enum
23
+ from typing import Any, Callable, Dict, List, Optional
25
24
 
26
- from alma.types import MemorySlice, MemoryScope
25
+ from alma.types import MemoryScope, MemorySlice
27
26
 
28
27
 
29
28
  class ToolType(Enum):
30
29
  """Categories of tools available to agents."""
31
- SEARCH = "search" # Web search, semantic search
32
- DATA_ACCESS = "data_access" # APIs, databases
33
- EXECUTION = "execution" # Code execution, automation
30
+
31
+ SEARCH = "search" # Web search, semantic search
32
+ DATA_ACCESS = "data_access" # APIs, databases
33
+ EXECUTION = "execution" # Code execution, automation
34
34
  COMMUNICATION = "communication" # Email, messaging
35
- ANALYSIS = "analysis" # Data processing, synthesis
36
- CREATION = "creation" # Content generation, design
35
+ ANALYSIS = "analysis" # Data processing, synthesis
36
+ CREATION = "creation" # Content generation, design
37
37
 
38
38
 
39
39
  @dataclass
@@ -43,6 +43,7 @@ class Tool:
43
43
 
44
44
  Tools are the building blocks agents use to accomplish tasks.
45
45
  """
46
+
46
47
  name: str
47
48
  description: str
48
49
  tool_type: ToolType
@@ -66,6 +67,7 @@ class Setting:
66
67
  Includes available tools and immutable constraints that don't change
67
68
  between runs. The setting defines WHAT the agent CAN do.
68
69
  """
70
+
69
71
  name: str
70
72
  description: str
71
73
  tools: List[Tool] = field(default_factory=list)
@@ -100,6 +102,7 @@ class Context:
100
102
  This is injected fresh each time and contains task-specific information.
101
103
  The context defines WHAT the agent should do THIS run.
102
104
  """
105
+
103
106
  task: str
104
107
  user_id: Optional[str] = None
105
108
  project_id: Optional[str] = None
@@ -137,6 +140,7 @@ class MemorySchema:
137
140
  This defines WHAT gets remembered and HOW, ensuring relevance
138
141
  and preventing scope creep. Each domain has its own schema.
139
142
  """
143
+
140
144
  domain: str
141
145
  description: str
142
146
 
@@ -195,6 +199,7 @@ class Agent:
195
199
  Agents start "dumb" but get smarter via memory injections.
196
200
  They use tools to accomplish tasks and log reflections post-run.
197
201
  """
202
+
198
203
  name: str
199
204
  role: str
200
205
  description: str
@@ -227,6 +232,7 @@ class Agent:
227
232
  @dataclass
228
233
  class RunResult:
229
234
  """Result of a harness run."""
235
+
230
236
  success: bool
231
237
  output: Any
232
238
  reflections: List[str] = field(default_factory=list)
@@ -319,7 +325,9 @@ class Harness:
319
325
  agent=self.agent.name,
320
326
  task=context.task,
321
327
  outcome="success" if result.success else "failure",
322
- strategy_used=", ".join(result.tools_used) if result.tools_used else "direct",
328
+ strategy_used=(
329
+ ", ".join(result.tools_used) if result.tools_used else "direct"
330
+ ),
323
331
  duration_ms=result.duration_ms,
324
332
  error_message=result.error,
325
333
  feedback="; ".join(result.reflections) if result.reflections else None,
@@ -347,6 +355,7 @@ class Harness:
347
355
  RunResult with output or prompt
348
356
  """
349
357
  import time
358
+
350
359
  start_time = time.time()
351
360
 
352
361
  # 1. Pre-run: Get relevant memories
alma/harness/domains.py CHANGED
@@ -13,23 +13,22 @@ Each domain includes:
13
13
  - Agent templates
14
14
  """
15
15
 
16
- from dataclasses import dataclass, field
17
- from typing import List, Dict, Any, Optional
16
+ from typing import Any
18
17
 
19
18
  from alma.harness.base import (
19
+ Agent,
20
+ Harness,
21
+ MemorySchema,
20
22
  Setting,
21
23
  Tool,
22
24
  ToolType,
23
- Agent,
24
- MemorySchema,
25
- Harness,
26
25
  )
27
26
 
28
-
29
27
  # =============================================================================
30
28
  # CODING DOMAIN
31
29
  # =============================================================================
32
30
 
31
+
33
32
  class CodingDomain:
34
33
  """Pre-built configurations for coding/development agents."""
35
34
 
@@ -114,13 +113,19 @@ class CodingDomain:
114
113
  name="playwright",
115
114
  description="Browser automation for UI testing",
116
115
  tool_type=ToolType.EXECUTION,
117
- constraints=["Use explicit waits, not sleep()", "Prefer role-based selectors"],
116
+ constraints=[
117
+ "Use explicit waits, not sleep()",
118
+ "Prefer role-based selectors",
119
+ ],
118
120
  ),
119
121
  Tool(
120
122
  name="api_client",
121
123
  description="HTTP client for API testing",
122
124
  tool_type=ToolType.DATA_ACCESS,
123
- constraints=["Log all requests/responses", "Handle timeouts gracefully"],
125
+ constraints=[
126
+ "Log all requests/responses",
127
+ "Handle timeouts gracefully",
128
+ ],
124
129
  ),
125
130
  Tool(
126
131
  name="database_query",
@@ -201,6 +206,7 @@ class CodingDomain:
201
206
  # RESEARCH DOMAIN
202
207
  # =============================================================================
203
208
 
209
+
204
210
  class ResearchDomain:
205
211
  """Pre-built configurations for research and analysis agents."""
206
212
 
@@ -268,7 +274,10 @@ class ResearchDomain:
268
274
  name="synthesis",
269
275
  description="Combine multiple sources into insights",
270
276
  tool_type=ToolType.ANALYSIS,
271
- constraints=["Note conflicting information", "Confidence levels required"],
277
+ constraints=[
278
+ "Note conflicting information",
279
+ "Confidence levels required",
280
+ ],
272
281
  ),
273
282
  ],
274
283
  global_constraints=[
@@ -314,6 +323,7 @@ class ResearchDomain:
314
323
  # CONTENT DOMAIN
315
324
  # =============================================================================
316
325
 
326
+
317
327
  class ContentDomain:
318
328
  """Pre-built configurations for content creation agents."""
319
329
 
@@ -490,6 +500,7 @@ class ContentDomain:
490
500
  # OPERATIONS DOMAIN
491
501
  # =============================================================================
492
502
 
503
+
493
504
  class OperationsDomain:
494
505
  """Pre-built configurations for operations and support agents."""
495
506
 
@@ -636,6 +647,7 @@ class OperationsDomain:
636
647
  # FACTORY FUNCTION
637
648
  # =============================================================================
638
649
 
650
+
639
651
  def create_harness(
640
652
  domain: str,
641
653
  agent_type: str,
@@ -666,7 +678,9 @@ def create_harness(
666
678
  "victor": CodingDomain.create_victor,
667
679
  },
668
680
  "research": {
669
- "researcher": lambda a: ResearchDomain.create_researcher(a, kwargs.get("focus", "general")),
681
+ "researcher": lambda a: ResearchDomain.create_researcher(
682
+ a, kwargs.get("focus", "general")
683
+ ),
670
684
  },
671
685
  "content": {
672
686
  "copywriter": ContentDomain.create_copywriter,
@@ -678,7 +692,9 @@ def create_harness(
678
692
  }
679
693
 
680
694
  if domain not in factories:
681
- raise ValueError(f"Unknown domain: {domain}. Available: {list(factories.keys())}")
695
+ raise ValueError(
696
+ f"Unknown domain: {domain}. Available: {list(factories.keys())}"
697
+ )
682
698
 
683
699
  if agent_type not in factories[domain]:
684
700
  raise ValueError(
@@ -0,0 +1,37 @@
1
+ """
2
+ ALMA Initializer Module.
3
+
4
+ Bootstrap pattern that orients the agent before work begins.
5
+
6
+ Usage:
7
+ from alma.initializer import SessionInitializer, InitializationResult
8
+
9
+ initializer = SessionInitializer(alma)
10
+ result = initializer.initialize(
11
+ project_id="my-project",
12
+ agent="Helena",
13
+ user_prompt="Test the login flow",
14
+ project_path="/path/to/project",
15
+ )
16
+
17
+ # Inject into agent prompt
18
+ prompt = f'''
19
+ {result.to_prompt()}
20
+
21
+ Now proceed with the first work item.
22
+ '''
23
+ """
24
+
25
+ from alma.initializer.initializer import SessionInitializer
26
+ from alma.initializer.types import (
27
+ CodebaseOrientation,
28
+ InitializationResult,
29
+ RulesOfEngagement,
30
+ )
31
+
32
+ __all__ = [
33
+ "CodebaseOrientation",
34
+ "InitializationResult",
35
+ "RulesOfEngagement",
36
+ "SessionInitializer",
37
+ ]
@@ -0,0 +1,418 @@
1
+ """
2
+ Session Initializer.
3
+
4
+ Bootstrap pattern that orients the agent before work begins.
5
+ "Stage manager sets the stage, actor performs."
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Any, List, Optional
13
+
14
+ from alma.initializer.types import (
15
+ CodebaseOrientation,
16
+ InitializationResult,
17
+ RulesOfEngagement,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class SessionInitializer:
24
+ """
25
+ Bootstrap domain memory from user prompt.
26
+
27
+ The Initializer Pattern:
28
+ 1. Expand user prompt to structured work items
29
+ 2. Orient to current codebase state (git, files)
30
+ 3. Retrieve relevant memories from past sessions
31
+ 4. Set rules of engagement from agent scope
32
+ 5. Suggest optimal starting point
33
+
34
+ Usage:
35
+ initializer = SessionInitializer(alma)
36
+
37
+ result = initializer.initialize(
38
+ project_id="my-project",
39
+ agent="Helena",
40
+ user_prompt="Test the login flow",
41
+ )
42
+
43
+ # Inject into agent prompt
44
+ prompt = f'''
45
+ {result.to_prompt()}
46
+
47
+ Now proceed with the first work item.
48
+ '''
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ alma: Optional[Any] = None,
54
+ progress_tracker: Optional[Any] = None,
55
+ session_manager: Optional[Any] = None,
56
+ ):
57
+ """
58
+ Initialize the SessionInitializer.
59
+
60
+ Args:
61
+ alma: ALMA instance for memory retrieval
62
+ progress_tracker: ProgressTracker for work item management
63
+ session_manager: SessionManager for session context
64
+ """
65
+ self.alma = alma
66
+ self.progress_tracker = progress_tracker
67
+ self.session_manager = session_manager
68
+
69
+ def initialize(
70
+ self,
71
+ project_id: str,
72
+ agent: str,
73
+ user_prompt: str,
74
+ project_path: Optional[str] = None,
75
+ auto_expand: bool = True,
76
+ memory_top_k: int = 5,
77
+ ) -> InitializationResult:
78
+ """
79
+ Full session initialization.
80
+
81
+ Args:
82
+ project_id: Project identifier
83
+ agent: Agent name (e.g., "Helena", "Victor")
84
+ user_prompt: Raw user prompt/task
85
+ project_path: Optional path to project root (for git orientation)
86
+ auto_expand: Whether to expand prompt to work items
87
+ memory_top_k: How many memories to retrieve
88
+
89
+ Returns:
90
+ InitializationResult with everything agent needs
91
+ """
92
+ logger.info(f"Initializing session for {agent} on {project_id}")
93
+
94
+ # Create result
95
+ result = InitializationResult.create(
96
+ project_id=project_id,
97
+ agent=agent,
98
+ original_prompt=user_prompt,
99
+ )
100
+
101
+ # 1. Expand prompt to work items
102
+ if auto_expand:
103
+ work_items = self.expand_prompt(user_prompt)
104
+ result.work_items = work_items
105
+ if work_items:
106
+ result.goal = self._summarize_goal(user_prompt, work_items)
107
+
108
+ # 2. Orient to codebase
109
+ if project_path:
110
+ result.orientation = self.orient_to_codebase(project_path)
111
+
112
+ # 3. Retrieve relevant memories
113
+ if self.alma:
114
+ try:
115
+ memories = self.alma.retrieve(
116
+ task=user_prompt,
117
+ agent=agent,
118
+ top_k=memory_top_k,
119
+ )
120
+ result.relevant_memories = memories
121
+ except Exception as e:
122
+ logger.warning(f"Failed to retrieve memories: {e}")
123
+
124
+ # 4. Get rules of engagement
125
+ if self.alma:
126
+ result.rules = self.get_rules_of_engagement(agent)
127
+
128
+ # 5. Suggest starting point
129
+ if result.work_items:
130
+ result.recommended_start = self._select_starting_point(result.work_items)
131
+
132
+ # 6. Get recent activity from session manager
133
+ if self.session_manager:
134
+ try:
135
+ context = self.session_manager.start_session(
136
+ agent=agent,
137
+ goal=result.goal,
138
+ )
139
+ result.session_id = context.session_id
140
+ if context.previous_handoff:
141
+ result.recent_activity = context.previous_handoff.next_steps or []
142
+ except Exception as e:
143
+ logger.warning(f"Failed to get session context: {e}")
144
+
145
+ logger.info(
146
+ f"Initialization complete: {len(result.work_items)} work items, "
147
+ f"orientation: {'yes' if result.orientation else 'no'}, "
148
+ f"memories: {'yes' if result.relevant_memories else 'no'}"
149
+ )
150
+
151
+ return result
152
+
153
+ def expand_prompt(
154
+ self,
155
+ user_prompt: str,
156
+ use_ai: bool = False,
157
+ ) -> List[Any]:
158
+ """
159
+ Expand user prompt into structured work items.
160
+
161
+ Simple implementation: extract bullet points and numbered items.
162
+ AI implementation: use LLM to break down complex tasks.
163
+
164
+ Args:
165
+ user_prompt: Raw user prompt
166
+ use_ai: Whether to use AI for expansion (requires LLM)
167
+
168
+ Returns:
169
+ List of WorkItem objects
170
+ """
171
+ from alma.progress import WorkItem
172
+
173
+ work_items = []
174
+
175
+ # Simple extraction: look for bullet points and numbered items
176
+ lines = user_prompt.strip().split("\n")
177
+
178
+ for line in lines:
179
+ line = line.strip()
180
+ if not line:
181
+ continue
182
+
183
+ # Match bullet points: -, *, •
184
+ bullet_match = re.match(r"^[-*•]\s+(.+)$", line)
185
+ if bullet_match:
186
+ title = bullet_match.group(1).strip()
187
+ work_items.append(
188
+ WorkItem.create(
189
+ project_id="", # Will be set by caller
190
+ title=title,
191
+ description=title,
192
+ )
193
+ )
194
+ continue
195
+
196
+ # Match numbered items: 1., 2., etc.
197
+ number_match = re.match(r"^\d+\.\s+(.+)$", line)
198
+ if number_match:
199
+ title = number_match.group(1).strip()
200
+ work_items.append(
201
+ WorkItem.create(
202
+ project_id="",
203
+ title=title,
204
+ description=title,
205
+ )
206
+ )
207
+ continue
208
+
209
+ # If no structured items found, create single item from prompt
210
+ if not work_items:
211
+ # Truncate long prompts for title
212
+ title = user_prompt[:100].strip()
213
+ if len(user_prompt) > 100:
214
+ title += "..."
215
+
216
+ work_items.append(
217
+ WorkItem.create(
218
+ project_id="",
219
+ title=title,
220
+ description=user_prompt,
221
+ )
222
+ )
223
+
224
+ return work_items
225
+
226
+ def orient_to_codebase(
227
+ self,
228
+ project_path: str,
229
+ max_commits: int = 5,
230
+ ) -> CodebaseOrientation:
231
+ """
232
+ Orient to current codebase state.
233
+
234
+ Reads git status, recent commits, and file structure.
235
+
236
+ Args:
237
+ project_path: Path to project root
238
+ max_commits: Max number of recent commits to include
239
+
240
+ Returns:
241
+ CodebaseOrientation with codebase state
242
+ """
243
+ path = Path(project_path)
244
+
245
+ # Default orientation
246
+ orientation = CodebaseOrientation(
247
+ current_branch="unknown",
248
+ has_uncommitted_changes=False,
249
+ recent_commits=[],
250
+ root_path=str(path),
251
+ key_directories=[],
252
+ config_files=[],
253
+ )
254
+
255
+ # Check if it's a git repo
256
+ git_dir = path / ".git"
257
+ is_git_repo = git_dir.exists()
258
+
259
+ if is_git_repo:
260
+ try:
261
+ # Get current branch
262
+ result = subprocess.run(
263
+ ["git", "branch", "--show-current"],
264
+ cwd=path,
265
+ capture_output=True,
266
+ text=True,
267
+ timeout=5,
268
+ )
269
+ if result.returncode == 0:
270
+ orientation.current_branch = (
271
+ result.stdout.strip() or "HEAD detached"
272
+ )
273
+
274
+ # Check for uncommitted changes
275
+ result = subprocess.run(
276
+ ["git", "status", "--porcelain"],
277
+ cwd=path,
278
+ capture_output=True,
279
+ text=True,
280
+ timeout=5,
281
+ )
282
+ if result.returncode == 0:
283
+ orientation.has_uncommitted_changes = bool(result.stdout.strip())
284
+
285
+ # Get recent commits
286
+ result = subprocess.run(
287
+ ["git", "log", "--oneline", f"-{max_commits}"],
288
+ cwd=path,
289
+ capture_output=True,
290
+ text=True,
291
+ timeout=5,
292
+ )
293
+ if result.returncode == 0:
294
+ commits = result.stdout.strip().split("\n")
295
+ orientation.recent_commits = [c for c in commits if c]
296
+
297
+ except subprocess.TimeoutExpired:
298
+ logger.warning("Git commands timed out")
299
+ except Exception as e:
300
+ logger.warning(f"Git orientation failed: {e}")
301
+
302
+ # Find key directories
303
+ key_dirs = ["src", "lib", "tests", "test", "app", "api", "core"]
304
+ orientation.key_directories = [d for d in key_dirs if (path / d).is_dir()]
305
+
306
+ # Find config files
307
+ config_files = [
308
+ "package.json",
309
+ "pyproject.toml",
310
+ "setup.py",
311
+ "Cargo.toml",
312
+ "go.mod",
313
+ "pom.xml",
314
+ "build.gradle",
315
+ "Makefile",
316
+ "CMakeLists.txt",
317
+ ]
318
+ orientation.config_files = [f for f in config_files if (path / f).exists()]
319
+
320
+ # Generate summary
321
+ orientation.summary = self._generate_orientation_summary(orientation)
322
+
323
+ return orientation
324
+
325
+ def get_rules_of_engagement(
326
+ self,
327
+ agent: str,
328
+ ) -> RulesOfEngagement:
329
+ """
330
+ Get rules of engagement from agent scope.
331
+
332
+ Args:
333
+ agent: Agent name
334
+
335
+ Returns:
336
+ RulesOfEngagement with scope rules, constraints, quality gates
337
+ """
338
+ rules = RulesOfEngagement()
339
+
340
+ if not self.alma:
341
+ return rules
342
+
343
+ # Get scope from ALMA
344
+ scope = self.alma.scopes.get(agent)
345
+ if not scope:
346
+ return rules
347
+
348
+ # Convert scope to rules
349
+ if scope.can_learn:
350
+ rules.scope_rules = [f"Learn from: {', '.join(scope.can_learn)}"]
351
+
352
+ if scope.cannot_learn:
353
+ rules.constraints = [f"Do not learn from: {', '.join(scope.cannot_learn)}"]
354
+
355
+ # Default quality gates
356
+ rules.quality_gates = [
357
+ "All tests pass",
358
+ "No regressions introduced",
359
+ "Changes documented if significant",
360
+ ]
361
+
362
+ return rules
363
+
364
+ def _summarize_goal(self, prompt: str, work_items: List[Any]) -> str:
365
+ """Summarize goal from prompt and work items."""
366
+ if len(work_items) == 1:
367
+ return prompt
368
+
369
+ item_titles = [getattr(item, "title", str(item)) for item in work_items]
370
+ return f"{prompt}\n\nBroken down into {len(work_items)} items: {', '.join(item_titles[:3])}{'...' if len(item_titles) > 3 else ''}"
371
+
372
+ def _select_starting_point(self, work_items: List[Any]) -> Optional[Any]:
373
+ """Select the best starting point from work items."""
374
+ if not work_items:
375
+ return None
376
+
377
+ # Find highest priority unblocked item
378
+ actionable = [
379
+ item
380
+ for item in work_items
381
+ if getattr(item, "status", "pending") == "pending"
382
+ and not getattr(item, "blocked_by", [])
383
+ ]
384
+
385
+ if actionable:
386
+ # Sort by priority (higher = more important)
387
+ actionable.sort(key=lambda x: getattr(x, "priority", 50), reverse=True)
388
+ return actionable[0]
389
+
390
+ return work_items[0]
391
+
392
+ def _generate_orientation_summary(self, orientation: CodebaseOrientation) -> str:
393
+ """Generate a one-line summary of codebase orientation."""
394
+ parts = []
395
+
396
+ parts.append(f"Branch: {orientation.current_branch}")
397
+
398
+ if orientation.has_uncommitted_changes:
399
+ parts.append("has uncommitted changes")
400
+
401
+ if orientation.key_directories:
402
+ parts.append(f"key dirs: {', '.join(orientation.key_directories[:3])}")
403
+
404
+ if orientation.config_files:
405
+ # Infer project type from config files
406
+ if "package.json" in orientation.config_files:
407
+ parts.append("Node.js project")
408
+ elif (
409
+ "pyproject.toml" in orientation.config_files
410
+ or "setup.py" in orientation.config_files
411
+ ):
412
+ parts.append("Python project")
413
+ elif "Cargo.toml" in orientation.config_files:
414
+ parts.append("Rust project")
415
+ elif "go.mod" in orientation.config_files:
416
+ parts.append("Go project")
417
+
418
+ return "; ".join(parts)