tapps-agents 3.5.39__py3-none-any.whl → 3.5.41__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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/enhancer/agent.py +2728 -2728
- tapps_agents/agents/implementer/agent.py +35 -13
- tapps_agents/agents/reviewer/agent.py +43 -10
- tapps_agents/agents/reviewer/scoring.py +59 -68
- tapps_agents/agents/reviewer/tools/__init__.py +24 -0
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -0
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -0
- tapps_agents/beads/__init__.py +11 -0
- tapps_agents/beads/hydration.py +213 -0
- tapps_agents/beads/specs.py +206 -0
- tapps_agents/cli/commands/health.py +19 -3
- tapps_agents/cli/commands/simple_mode.py +842 -676
- tapps_agents/cli/commands/task.py +227 -0
- tapps_agents/cli/commands/top_level.py +13 -0
- tapps_agents/cli/main.py +658 -651
- tapps_agents/cli/parsers/top_level.py +1978 -1881
- tapps_agents/core/config.py +1622 -1622
- tapps_agents/core/init_project.py +3012 -2897
- tapps_agents/epic/markdown_sync.py +105 -0
- tapps_agents/epic/orchestrator.py +1 -2
- tapps_agents/epic/parser.py +427 -423
- tapps_agents/experts/adaptive_domain_detector.py +0 -2
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +15 -15
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +19 -44
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +324 -0
- tapps_agents/health/checks/outcomes.py +134 -46
- tapps_agents/health/orchestrator.py +12 -4
- tapps_agents/hooks/__init__.py +33 -0
- tapps_agents/hooks/config.py +140 -0
- tapps_agents/hooks/events.py +135 -0
- tapps_agents/hooks/executor.py +128 -0
- tapps_agents/hooks/manager.py +143 -0
- tapps_agents/session/__init__.py +19 -0
- tapps_agents/session/manager.py +256 -0
- tapps_agents/simple_mode/code_snippet_handler.py +382 -0
- tapps_agents/simple_mode/intent_parser.py +29 -4
- tapps_agents/simple_mode/orchestrators/base.py +185 -59
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2667 -2642
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +2 -2
- tapps_agents/simple_mode/workflow_suggester.py +37 -3
- tapps_agents/workflow/agent_handlers/implementer_handler.py +18 -3
- tapps_agents/workflow/cursor_executor.py +2337 -2118
- tapps_agents/workflow/direct_execution_fallback.py +16 -3
- tapps_agents/workflow/message_formatter.py +2 -1
- tapps_agents/workflow/models.py +38 -1
- tapps_agents/workflow/parallel_executor.py +43 -4
- tapps_agents/workflow/parser.py +375 -357
- tapps_agents/workflow/rules_generator.py +337 -337
- tapps_agents/workflow/skill_invoker.py +9 -3
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/METADATA +5 -1
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/RECORD +58 -54
- tapps_agents/agents/analyst/SKILL.md +0 -85
- tapps_agents/agents/architect/SKILL.md +0 -80
- tapps_agents/agents/debugger/SKILL.md +0 -66
- tapps_agents/agents/designer/SKILL.md +0 -78
- tapps_agents/agents/documenter/SKILL.md +0 -95
- tapps_agents/agents/enhancer/SKILL.md +0 -189
- tapps_agents/agents/implementer/SKILL.md +0 -117
- tapps_agents/agents/improver/SKILL.md +0 -55
- tapps_agents/agents/ops/SKILL.md +0 -64
- tapps_agents/agents/orchestrator/SKILL.md +0 -238
- tapps_agents/agents/planner/story_template.md +0 -37
- tapps_agents/agents/reviewer/templates/quality-dashboard.html.j2 +0 -150
- tapps_agents/agents/tester/SKILL.md +0 -71
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/licenses/LICENSE +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.41.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Snippet Handler - Detect and handle pasted code in user input.
|
|
3
|
+
|
|
4
|
+
Detects markdown code blocks, creates temporary files in scratchpad directory,
|
|
5
|
+
and integrates with workflow suggester for automatic *fix workflow invocation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Language to file extension mapping
|
|
22
|
+
LANGUAGE_EXTENSIONS: dict[str, str] = {
|
|
23
|
+
"python": ".py",
|
|
24
|
+
"py": ".py",
|
|
25
|
+
"javascript": ".js",
|
|
26
|
+
"js": ".js",
|
|
27
|
+
"typescript": ".ts",
|
|
28
|
+
"ts": ".ts",
|
|
29
|
+
"java": ".java",
|
|
30
|
+
"go": ".go",
|
|
31
|
+
"rust": ".rs",
|
|
32
|
+
"rs": ".rs",
|
|
33
|
+
"c": ".c",
|
|
34
|
+
"cpp": ".cpp",
|
|
35
|
+
"c++": ".cpp",
|
|
36
|
+
"csharp": ".cs",
|
|
37
|
+
"cs": ".cs",
|
|
38
|
+
"ruby": ".rb",
|
|
39
|
+
"rb": ".rb",
|
|
40
|
+
"php": ".php",
|
|
41
|
+
"swift": ".swift",
|
|
42
|
+
"kotlin": ".kt",
|
|
43
|
+
"scala": ".scala",
|
|
44
|
+
"shell": ".sh",
|
|
45
|
+
"bash": ".sh",
|
|
46
|
+
"sh": ".sh",
|
|
47
|
+
"sql": ".sql",
|
|
48
|
+
"html": ".html",
|
|
49
|
+
"css": ".css",
|
|
50
|
+
"json": ".json",
|
|
51
|
+
"yaml": ".yaml",
|
|
52
|
+
"yml": ".yml",
|
|
53
|
+
"xml": ".xml",
|
|
54
|
+
"markdown": ".md",
|
|
55
|
+
"md": ".md",
|
|
56
|
+
"text": ".txt",
|
|
57
|
+
"txt": ".txt",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Markdown code fence pattern
|
|
62
|
+
MARKDOWN_CODE_FENCE_PATTERN = r"```(?P<lang>\w+)?\s*\n(?P<code>.*?)```"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class CodeSnippet:
|
|
67
|
+
"""
|
|
68
|
+
Detected code snippet with metadata.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
code: The extracted code content
|
|
72
|
+
language: Detected language (or 'txt' if unknown)
|
|
73
|
+
extension: File extension for the language
|
|
74
|
+
confidence: Detection confidence (0.0-1.0)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
code: str
|
|
78
|
+
language: str
|
|
79
|
+
extension: str
|
|
80
|
+
confidence: float
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class TempFile:
|
|
85
|
+
"""
|
|
86
|
+
Temporary file information.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
path: Full path to the temporary file
|
|
90
|
+
filename: Just the filename
|
|
91
|
+
language: Detected language
|
|
92
|
+
created_at: Timestamp when file was created
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
path: Path
|
|
96
|
+
filename: str
|
|
97
|
+
language: str
|
|
98
|
+
created_at: float
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class CodeSnippetHandler:
|
|
102
|
+
"""
|
|
103
|
+
Handler for detecting and processing pasted code snippets.
|
|
104
|
+
|
|
105
|
+
Detects markdown code blocks in user input, creates temporary files
|
|
106
|
+
in the scratchpad directory, and prepares for workflow integration.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, scratchpad_dir: Optional[Path] = None):
|
|
110
|
+
"""
|
|
111
|
+
Initialize code snippet handler.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
scratchpad_dir: Path to scratchpad directory for temp files.
|
|
115
|
+
If None, uses default Claude Code scratchpad location.
|
|
116
|
+
"""
|
|
117
|
+
self._logger = logging.getLogger(__name__)
|
|
118
|
+
|
|
119
|
+
# Use provided scratchpad or default location
|
|
120
|
+
if scratchpad_dir is None:
|
|
121
|
+
# Default Claude Code scratchpad location
|
|
122
|
+
import tempfile
|
|
123
|
+
import os
|
|
124
|
+
|
|
125
|
+
base_temp = Path(tempfile.gettempdir())
|
|
126
|
+
claude_dir = base_temp / "claude"
|
|
127
|
+
|
|
128
|
+
# Try to find existing Claude directory with session ID
|
|
129
|
+
if claude_dir.exists():
|
|
130
|
+
# Look for subdirectories (session IDs)
|
|
131
|
+
session_dirs = [d for d in claude_dir.iterdir() if d.is_dir()]
|
|
132
|
+
if session_dirs:
|
|
133
|
+
# Use first session directory found
|
|
134
|
+
scratchpad_dir = session_dirs[0] / "scratchpad"
|
|
135
|
+
else:
|
|
136
|
+
# Create default session directory
|
|
137
|
+
scratchpad_dir = claude_dir / "default" / "scratchpad"
|
|
138
|
+
else:
|
|
139
|
+
# Create default Claude directory structure
|
|
140
|
+
scratchpad_dir = claude_dir / "default" / "scratchpad"
|
|
141
|
+
|
|
142
|
+
self.scratchpad_dir = Path(scratchpad_dir)
|
|
143
|
+
self._logger.debug(f"Scratchpad directory: {self.scratchpad_dir}")
|
|
144
|
+
|
|
145
|
+
# Ensure scratchpad directory exists
|
|
146
|
+
try:
|
|
147
|
+
self.scratchpad_dir.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
self._logger.warning(
|
|
150
|
+
f"Could not create scratchpad directory {self.scratchpad_dir}: {e}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def detect_code_snippet(self, user_input: str) -> Optional[CodeSnippet]:
|
|
154
|
+
"""
|
|
155
|
+
Detect code snippet in user input.
|
|
156
|
+
|
|
157
|
+
Searches for markdown code fences (```lang...```) and extracts
|
|
158
|
+
the code content and language.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
user_input: User's natural language input
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
CodeSnippet if code block detected, None otherwise
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> handler = CodeSnippetHandler()
|
|
168
|
+
>>> result = handler.detect_code_snippet('''
|
|
169
|
+
... Fix this code:
|
|
170
|
+
... ```python
|
|
171
|
+
... def add(a, b):
|
|
172
|
+
... return a / b
|
|
173
|
+
... ```
|
|
174
|
+
... ''')
|
|
175
|
+
>>> result.language
|
|
176
|
+
'python'
|
|
177
|
+
>>> result.code
|
|
178
|
+
'def add(a, b):\\n return a / b\\n'
|
|
179
|
+
"""
|
|
180
|
+
if not user_input or not isinstance(user_input, str):
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
# Search for markdown code fence
|
|
185
|
+
match = re.search(
|
|
186
|
+
MARKDOWN_CODE_FENCE_PATTERN,
|
|
187
|
+
user_input,
|
|
188
|
+
re.DOTALL | re.IGNORECASE
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not match:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
# Extract language and code
|
|
195
|
+
language = match.group("lang")
|
|
196
|
+
code = match.group("code")
|
|
197
|
+
|
|
198
|
+
# Normalize language
|
|
199
|
+
if language:
|
|
200
|
+
language = language.lower().strip()
|
|
201
|
+
else:
|
|
202
|
+
language = "txt" # Default if no language specified
|
|
203
|
+
|
|
204
|
+
# Get file extension
|
|
205
|
+
extension = LANGUAGE_EXTENSIONS.get(language, ".txt")
|
|
206
|
+
|
|
207
|
+
# Validate code is not empty
|
|
208
|
+
if not code or not code.strip():
|
|
209
|
+
self._logger.debug("Code block detected but empty")
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# High confidence if language specified and code non-empty
|
|
213
|
+
confidence = 0.95 if match.group("lang") else 0.80
|
|
214
|
+
|
|
215
|
+
return CodeSnippet(
|
|
216
|
+
code=code.strip(),
|
|
217
|
+
language=language,
|
|
218
|
+
extension=extension,
|
|
219
|
+
confidence=confidence
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self._logger.error(f"Error detecting code snippet: {e}", exc_info=True)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def generate_temp_filename(
|
|
227
|
+
self,
|
|
228
|
+
language: str,
|
|
229
|
+
extension: str,
|
|
230
|
+
code_content: str
|
|
231
|
+
) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Generate unique temporary filename.
|
|
234
|
+
|
|
235
|
+
Uses timestamp and hash of code content to ensure uniqueness.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
language: Detected language
|
|
239
|
+
extension: File extension
|
|
240
|
+
code_content: The code content (used for hash)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Unique filename like "pasted_code_1234567890_abc123.py"
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> handler = CodeSnippetHandler()
|
|
247
|
+
>>> filename = handler.generate_temp_filename("python", ".py", "code")
|
|
248
|
+
>>> filename.startswith("pasted_code_")
|
|
249
|
+
True
|
|
250
|
+
>>> filename.endswith(".py")
|
|
251
|
+
True
|
|
252
|
+
"""
|
|
253
|
+
# Generate timestamp
|
|
254
|
+
timestamp = int(time.time())
|
|
255
|
+
|
|
256
|
+
# Generate hash of code content
|
|
257
|
+
code_hash = hashlib.md5(code_content.encode('utf-8')).hexdigest()[:8]
|
|
258
|
+
|
|
259
|
+
# Construct filename
|
|
260
|
+
filename = f"pasted_code_{timestamp}_{code_hash}{extension}"
|
|
261
|
+
|
|
262
|
+
return filename
|
|
263
|
+
|
|
264
|
+
def create_temp_file(self, snippet: CodeSnippet) -> Optional[TempFile]:
|
|
265
|
+
"""
|
|
266
|
+
Create temporary file with code snippet content.
|
|
267
|
+
|
|
268
|
+
Writes the code snippet to a uniquely named file in the
|
|
269
|
+
scratchpad directory.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
snippet: CodeSnippet to write to file
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
TempFile with file information, or None if creation failed
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> handler = CodeSnippetHandler()
|
|
279
|
+
>>> snippet = CodeSnippet(
|
|
280
|
+
... code="print('hello')",
|
|
281
|
+
... language="python",
|
|
282
|
+
... extension=".py",
|
|
283
|
+
... confidence=0.95
|
|
284
|
+
... )
|
|
285
|
+
>>> temp_file = handler.create_temp_file(snippet)
|
|
286
|
+
>>> temp_file.path.exists()
|
|
287
|
+
True
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
# Generate unique filename
|
|
291
|
+
filename = self.generate_temp_filename(
|
|
292
|
+
snippet.language,
|
|
293
|
+
snippet.extension,
|
|
294
|
+
snippet.code
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Construct full path
|
|
298
|
+
file_path = self.scratchpad_dir / filename
|
|
299
|
+
|
|
300
|
+
# Write code to file
|
|
301
|
+
file_path.write_text(snippet.code, encoding='utf-8')
|
|
302
|
+
|
|
303
|
+
self._logger.info(f"Created temp file: {file_path}")
|
|
304
|
+
|
|
305
|
+
return TempFile(
|
|
306
|
+
path=file_path,
|
|
307
|
+
filename=filename,
|
|
308
|
+
language=snippet.language,
|
|
309
|
+
created_at=time.time()
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
self._logger.error(
|
|
314
|
+
f"Failed to create temp file: {e}",
|
|
315
|
+
exc_info=True
|
|
316
|
+
)
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def detect_and_create_temp_file(
|
|
320
|
+
self,
|
|
321
|
+
user_input: str
|
|
322
|
+
) -> Optional[TempFile]:
|
|
323
|
+
"""
|
|
324
|
+
Detect code snippet and create temporary file in one step.
|
|
325
|
+
|
|
326
|
+
Convenience method that combines detection and file creation.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
user_input: User's natural language input
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
TempFile if code detected and file created, None otherwise
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> handler = CodeSnippetHandler()
|
|
336
|
+
>>> temp_file = handler.detect_and_create_temp_file('''
|
|
337
|
+
... ```python
|
|
338
|
+
... def hello():
|
|
339
|
+
... print("world")
|
|
340
|
+
... ```
|
|
341
|
+
... ''')
|
|
342
|
+
>>> temp_file is not None
|
|
343
|
+
True
|
|
344
|
+
"""
|
|
345
|
+
# Detect code snippet
|
|
346
|
+
snippet = self.detect_code_snippet(user_input)
|
|
347
|
+
if snippet is None:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
# Create temp file
|
|
351
|
+
temp_file = self.create_temp_file(snippet)
|
|
352
|
+
return temp_file
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Convenience function for workflow integration
|
|
356
|
+
def detect_pasted_code(user_input: str) -> Optional[TempFile]:
|
|
357
|
+
"""
|
|
358
|
+
Detect pasted code and create temporary file.
|
|
359
|
+
|
|
360
|
+
Convenience function for use in workflow suggester and other modules.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
user_input: User's natural language input
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
TempFile if code detected and file created, None otherwise
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> temp_file = detect_pasted_code('''
|
|
370
|
+
... Fix this code:
|
|
371
|
+
... ```python
|
|
372
|
+
... def bad_code():
|
|
373
|
+
... return 1 / 0
|
|
374
|
+
... ```
|
|
375
|
+
... ''')
|
|
376
|
+
>>> temp_file is not None
|
|
377
|
+
True
|
|
378
|
+
>>> temp_file.language
|
|
379
|
+
'python'
|
|
380
|
+
"""
|
|
381
|
+
handler = CodeSnippetHandler()
|
|
382
|
+
return handler.detect_and_create_temp_file(user_input)
|
|
@@ -379,17 +379,42 @@ class IntentParser:
|
|
|
379
379
|
intent_type = IntentType.UNKNOWN
|
|
380
380
|
confidence = 0.0
|
|
381
381
|
|
|
382
|
-
# Detect "compare to codebase" intent
|
|
382
|
+
# Detect "compare to codebase" intent with enhanced semantics
|
|
383
383
|
compare_to_codebase = False
|
|
384
384
|
compare_phrases = [
|
|
385
|
+
# Direct compare phrases
|
|
385
386
|
"compare to",
|
|
386
387
|
"compare with",
|
|
388
|
+
"compare against",
|
|
389
|
+
"compare this to",
|
|
390
|
+
"compare this with",
|
|
391
|
+
"compare to codebase",
|
|
392
|
+
"compare to our",
|
|
393
|
+
"compare to project",
|
|
394
|
+
# Match/align phrases
|
|
387
395
|
"match our",
|
|
396
|
+
"match the",
|
|
397
|
+
"match patterns",
|
|
398
|
+
"match codebase",
|
|
388
399
|
"align with",
|
|
400
|
+
"align to",
|
|
389
401
|
"follow patterns",
|
|
390
|
-
"
|
|
391
|
-
"
|
|
392
|
-
|
|
402
|
+
"follow our patterns",
|
|
403
|
+
"follow project patterns",
|
|
404
|
+
# Make match phrases (implicit compare)
|
|
405
|
+
"make match",
|
|
406
|
+
"make this match",
|
|
407
|
+
"make it match",
|
|
408
|
+
# Consistency phrases
|
|
409
|
+
"consistent with",
|
|
410
|
+
"consistency with",
|
|
411
|
+
"conform to",
|
|
412
|
+
"conform with",
|
|
413
|
+
# Standard phrases
|
|
414
|
+
"match standards",
|
|
415
|
+
"follow standards",
|
|
416
|
+
"meet standards",
|
|
417
|
+
"adhere to",
|
|
393
418
|
]
|
|
394
419
|
if any(phrase in input_lower for phrase in compare_phrases):
|
|
395
420
|
compare_to_codebase = True
|
|
@@ -1,59 +1,185 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Base orchestrator interface for Simple Mode.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
"""
|
|
2
|
+
Base orchestrator interface for Simple Mode.
|
|
3
|
+
|
|
4
|
+
Integrates optional hook system: UserPromptSubmit before workflow,
|
|
5
|
+
PostToolUse after implementer tool use, WorkflowComplete after workflow ends.
|
|
6
|
+
Hooks are opt-in (no behavior change when hooks disabled).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from tapps_agents.core.config import ProjectConfig
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..intent_parser import Intent
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_hook_manager(project_root: Path) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Load HookManager for project if hooks are configured.
|
|
27
|
+
|
|
28
|
+
Returns None when hooks.yaml is missing or has no enabled hooks (opt-in).
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
from tapps_agents.hooks.config import load_hooks_config
|
|
32
|
+
from tapps_agents.hooks.manager import HookManager
|
|
33
|
+
|
|
34
|
+
config = load_hooks_config(project_root=project_root)
|
|
35
|
+
has_any = any(
|
|
36
|
+
any(h.enabled for h in hooks)
|
|
37
|
+
for hooks in config.hooks.values()
|
|
38
|
+
)
|
|
39
|
+
if not has_any:
|
|
40
|
+
return None
|
|
41
|
+
return HookManager(project_root=project_root)
|
|
42
|
+
except Exception as e: # pylint: disable=broad-except
|
|
43
|
+
logger.debug("Hooks not loaded (opt-in): %s", e)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SimpleModeOrchestrator(ABC):
|
|
48
|
+
"""Base class for Simple Mode orchestrators."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
project_root: Path | None = None,
|
|
53
|
+
config: ProjectConfig | None = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize orchestrator.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
project_root: Project root directory
|
|
60
|
+
config: Optional project configuration
|
|
61
|
+
"""
|
|
62
|
+
self.project_root = project_root or Path.cwd()
|
|
63
|
+
self.config = config
|
|
64
|
+
self._hook_manager = None # Lazy-loaded when needed
|
|
65
|
+
|
|
66
|
+
def _get_hook_manager(self) -> Any:
|
|
67
|
+
"""Return HookManager if hooks are enabled for this project; else None."""
|
|
68
|
+
if self._hook_manager is None:
|
|
69
|
+
self._hook_manager = _get_hook_manager(self.project_root)
|
|
70
|
+
return self._hook_manager
|
|
71
|
+
|
|
72
|
+
def _trigger_user_prompt_submit(
|
|
73
|
+
self,
|
|
74
|
+
prompt: str,
|
|
75
|
+
workflow_type: str | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Fire UserPromptSubmit hook before workflow execution.
|
|
79
|
+
|
|
80
|
+
No-op when hooks disabled or no hooks configured for this event.
|
|
81
|
+
"""
|
|
82
|
+
mgr = self._get_hook_manager()
|
|
83
|
+
if not mgr:
|
|
84
|
+
return
|
|
85
|
+
try:
|
|
86
|
+
from tapps_agents.hooks.events import UserPromptSubmitEvent
|
|
87
|
+
|
|
88
|
+
payload = UserPromptSubmitEvent(
|
|
89
|
+
prompt=prompt,
|
|
90
|
+
project_root=str(self.project_root),
|
|
91
|
+
workflow_type=workflow_type,
|
|
92
|
+
)
|
|
93
|
+
mgr.trigger("UserPromptSubmit", payload)
|
|
94
|
+
except Exception as e: # pylint: disable=broad-except
|
|
95
|
+
logger.debug("UserPromptSubmit hook error (non-fatal): %s", e)
|
|
96
|
+
|
|
97
|
+
def _trigger_post_tool_use(
|
|
98
|
+
self,
|
|
99
|
+
tool_name: str,
|
|
100
|
+
file_path: str | None,
|
|
101
|
+
file_paths: list[str] | None = None,
|
|
102
|
+
workflow_id: str | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Fire PostToolUse hook after implementer Write/Edit.
|
|
106
|
+
|
|
107
|
+
No-op when hooks disabled or no matching hooks.
|
|
108
|
+
"""
|
|
109
|
+
mgr = self._get_hook_manager()
|
|
110
|
+
if not mgr:
|
|
111
|
+
return
|
|
112
|
+
try:
|
|
113
|
+
from tapps_agents.hooks.events import PostToolUseEvent
|
|
114
|
+
|
|
115
|
+
payload = PostToolUseEvent(
|
|
116
|
+
file_path=file_path,
|
|
117
|
+
file_paths=file_paths or ([file_path] if file_path else []),
|
|
118
|
+
tool_name=tool_name,
|
|
119
|
+
project_root=str(self.project_root),
|
|
120
|
+
workflow_id=workflow_id,
|
|
121
|
+
)
|
|
122
|
+
mgr.trigger(
|
|
123
|
+
"PostToolUse",
|
|
124
|
+
payload,
|
|
125
|
+
tool_name=tool_name,
|
|
126
|
+
file_path=file_path,
|
|
127
|
+
)
|
|
128
|
+
except Exception as e: # pylint: disable=broad-except
|
|
129
|
+
logger.debug("PostToolUse hook error (non-fatal): %s", e)
|
|
130
|
+
|
|
131
|
+
def _trigger_workflow_complete(
|
|
132
|
+
self,
|
|
133
|
+
workflow_type: str,
|
|
134
|
+
workflow_id: str,
|
|
135
|
+
status: str,
|
|
136
|
+
beads_issue_id: str | None = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Fire WorkflowComplete hook after workflow ends.
|
|
140
|
+
|
|
141
|
+
status: 'completed', 'failed', or 'cancelled'.
|
|
142
|
+
No-op when hooks disabled.
|
|
143
|
+
"""
|
|
144
|
+
mgr = self._get_hook_manager()
|
|
145
|
+
if not mgr:
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
from tapps_agents.hooks.events import WorkflowCompleteEvent
|
|
149
|
+
|
|
150
|
+
payload = WorkflowCompleteEvent(
|
|
151
|
+
workflow_type=workflow_type,
|
|
152
|
+
workflow_id=workflow_id,
|
|
153
|
+
status=status,
|
|
154
|
+
project_root=str(self.project_root),
|
|
155
|
+
beads_issue_id=beads_issue_id,
|
|
156
|
+
)
|
|
157
|
+
mgr.trigger("WorkflowComplete", payload)
|
|
158
|
+
except Exception as e: # pylint: disable=broad-except
|
|
159
|
+
logger.debug("WorkflowComplete hook error (non-fatal): %s", e)
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
async def execute(
|
|
163
|
+
self, intent: Intent, parameters: dict[str, Any] | None = None
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Execute the orchestrator's workflow.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
intent: Parsed user intent
|
|
170
|
+
parameters: Additional parameters from user input
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary with execution results
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def get_agent_sequence(self) -> list[str]:
|
|
178
|
+
"""
|
|
179
|
+
Get the sequence of agents this orchestrator coordinates.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of agent names in execution order
|
|
183
|
+
"""
|
|
184
|
+
return []
|
|
185
|
+
|