alma-memory 0.5.1__py3-none-any.whl → 0.7.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 (111) hide show
  1. alma/__init__.py +296 -226
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -430
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -265
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -106
  26. alma/graph/backends/__init__.py +32 -32
  27. alma/graph/backends/kuzu.py +624 -624
  28. alma/graph/backends/memgraph.py +432 -432
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -444
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -509
  55. alma/observability/__init__.py +91 -84
  56. alma/observability/config.py +302 -302
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -424
  59. alma/observability/metrics.py +583 -583
  60. alma/observability/tracing.py +440 -440
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -427
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -90
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1259
  80. alma/storage/base.py +1083 -583
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -103
  83. alma/storage/file_based.py +614 -614
  84. alma/storage/migrations/__init__.py +21 -21
  85. alma/storage/migrations/base.py +321 -321
  86. alma/storage/migrations/runner.py +323 -323
  87. alma/storage/migrations/version_stores.py +337 -337
  88. alma/storage/migrations/versions/__init__.py +11 -11
  89. alma/storage/migrations/versions/v1_0_0.py +373 -373
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1559
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1457
  95. alma/testing/__init__.py +46 -46
  96. alma/testing/factories.py +301 -301
  97. alma/testing/mocks.py +389 -389
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.1.dist-info/RECORD +0 -93
  110. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,418 +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)
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)