tapps-agents 3.5.39__py3-none-any.whl → 3.5.40__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 (70) hide show
  1. tapps_agents/__init__.py +2 -2
  2. tapps_agents/agents/enhancer/agent.py +2728 -2728
  3. tapps_agents/agents/implementer/agent.py +35 -13
  4. tapps_agents/agents/reviewer/agent.py +43 -10
  5. tapps_agents/agents/reviewer/scoring.py +59 -68
  6. tapps_agents/agents/reviewer/tools/__init__.py +24 -0
  7. tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -0
  8. tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -0
  9. tapps_agents/beads/__init__.py +11 -0
  10. tapps_agents/beads/hydration.py +213 -0
  11. tapps_agents/beads/specs.py +206 -0
  12. tapps_agents/cli/commands/health.py +19 -3
  13. tapps_agents/cli/commands/simple_mode.py +842 -676
  14. tapps_agents/cli/commands/task.py +219 -0
  15. tapps_agents/cli/commands/top_level.py +13 -0
  16. tapps_agents/cli/main.py +658 -651
  17. tapps_agents/cli/parsers/top_level.py +1978 -1881
  18. tapps_agents/core/config.py +1622 -1622
  19. tapps_agents/core/init_project.py +3012 -2897
  20. tapps_agents/epic/markdown_sync.py +105 -0
  21. tapps_agents/epic/orchestrator.py +1 -2
  22. tapps_agents/epic/parser.py +427 -423
  23. tapps_agents/experts/adaptive_domain_detector.py +0 -2
  24. tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +15 -15
  25. tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +19 -44
  26. tapps_agents/health/checks/outcomes.backup_20260204_064058.py +324 -0
  27. tapps_agents/health/checks/outcomes.backup_20260204_064256.py +324 -0
  28. tapps_agents/health/checks/outcomes.backup_20260204_064600.py +324 -0
  29. tapps_agents/health/checks/outcomes.py +134 -46
  30. tapps_agents/health/orchestrator.py +12 -4
  31. tapps_agents/hooks/__init__.py +33 -0
  32. tapps_agents/hooks/config.py +140 -0
  33. tapps_agents/hooks/events.py +135 -0
  34. tapps_agents/hooks/executor.py +128 -0
  35. tapps_agents/hooks/manager.py +143 -0
  36. tapps_agents/session/__init__.py +19 -0
  37. tapps_agents/session/manager.py +256 -0
  38. tapps_agents/simple_mode/code_snippet_handler.py +382 -0
  39. tapps_agents/simple_mode/intent_parser.py +29 -4
  40. tapps_agents/simple_mode/orchestrators/base.py +185 -59
  41. tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2667 -2642
  42. tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +2 -2
  43. tapps_agents/simple_mode/workflow_suggester.py +37 -3
  44. tapps_agents/workflow/agent_handlers/implementer_handler.py +18 -3
  45. tapps_agents/workflow/cursor_executor.py +2196 -2118
  46. tapps_agents/workflow/direct_execution_fallback.py +16 -3
  47. tapps_agents/workflow/message_formatter.py +2 -1
  48. tapps_agents/workflow/parallel_executor.py +43 -4
  49. tapps_agents/workflow/parser.py +375 -357
  50. tapps_agents/workflow/rules_generator.py +337 -337
  51. tapps_agents/workflow/skill_invoker.py +9 -3
  52. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/METADATA +5 -1
  53. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/RECORD +57 -53
  54. tapps_agents/agents/analyst/SKILL.md +0 -85
  55. tapps_agents/agents/architect/SKILL.md +0 -80
  56. tapps_agents/agents/debugger/SKILL.md +0 -66
  57. tapps_agents/agents/designer/SKILL.md +0 -78
  58. tapps_agents/agents/documenter/SKILL.md +0 -95
  59. tapps_agents/agents/enhancer/SKILL.md +0 -189
  60. tapps_agents/agents/implementer/SKILL.md +0 -117
  61. tapps_agents/agents/improver/SKILL.md +0 -55
  62. tapps_agents/agents/ops/SKILL.md +0 -64
  63. tapps_agents/agents/orchestrator/SKILL.md +0 -238
  64. tapps_agents/agents/planner/story_template.md +0 -37
  65. tapps_agents/agents/reviewer/templates/quality-dashboard.html.j2 +0 -150
  66. tapps_agents/agents/tester/SKILL.md +0 -71
  67. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/WHEEL +0 -0
  68. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/entry_points.txt +0 -0
  69. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/licenses/LICENSE +0 -0
  70. {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,143 @@
1
+ """
2
+ Hook manager: lifecycle, registration by event, and triggering.
3
+
4
+ Loads hooks from config, runs them via the executor with payload-derived env,
5
+ and supports PostToolUse filtering by matcher and file_patterns.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import fnmatch
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Any, Protocol
14
+
15
+ from .config import HookDefinition, HooksConfig, load_hooks_config
16
+ from .executor import HookResult, run_hook
17
+
18
+
19
+ class _PayloadProtocol(Protocol):
20
+ """Payload must provide to_env() for hook execution."""
21
+
22
+ def to_env(self) -> dict[str, str]: ...
23
+ def to_dict(self) -> dict[str, Any]: ...
24
+
25
+
26
+ def _tool_matches(hook: HookDefinition, tool_name: str) -> bool:
27
+ """True if hook has no matcher or tool_name matches the matcher regex."""
28
+ if not hook.matcher:
29
+ return True
30
+ try:
31
+ return re.search(hook.matcher, tool_name) is not None
32
+ except re.error:
33
+ return False
34
+
35
+
36
+ def _file_matches(hook: HookDefinition, file_path: str | None) -> bool:
37
+ """True if hook has no file_patterns or file_path matches any pattern."""
38
+ if not hook.file_patterns or not file_path:
39
+ return True
40
+ name = Path(file_path).name
41
+ for pattern in hook.file_patterns:
42
+ if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(file_path, pattern):
43
+ return True
44
+ return False
45
+
46
+
47
+ def _filter_post_tool_use(
48
+ hooks: list[HookDefinition],
49
+ tool_name: str,
50
+ file_path: str | None,
51
+ ) -> list[HookDefinition]:
52
+ """Filter PostToolUse hooks by matcher and file_patterns."""
53
+ return [
54
+ h
55
+ for h in hooks
56
+ if _tool_matches(h, tool_name) and _file_matches(h, file_path)
57
+ ]
58
+
59
+
60
+ class HookManager:
61
+ """
62
+ Loads and triggers hooks by event type.
63
+
64
+ Hooks are loaded from .tapps-agents/hooks.yaml (or explicit path).
65
+ Trigger invokes all enabled hooks for the event with payload-derived env.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ config: HooksConfig | None = None,
71
+ *,
72
+ config_path: Path | str | None = None,
73
+ project_root: Path | None = None,
74
+ timeout_seconds: int | None = None,
75
+ ) -> None:
76
+ """
77
+ Initialize manager from config or load from path.
78
+
79
+ Args:
80
+ config: Pre-loaded config. If None, loads from config_path/project_root.
81
+ config_path: Path to hooks.yaml (used when config is None).
82
+ project_root: Project root for loading config and running hooks.
83
+ timeout_seconds: Override default hook timeout.
84
+ """
85
+ self._project_root = Path(project_root) if project_root else Path.cwd()
86
+ self._timeout_seconds = timeout_seconds
87
+ if config is not None:
88
+ self._config = config
89
+ else:
90
+ self._config = load_hooks_config(
91
+ config_path=config_path,
92
+ project_root=self._project_root,
93
+ )
94
+
95
+ def _get_hooks_for_event(self, event_name: str) -> list[HookDefinition]:
96
+ """Return enabled hooks for the event."""
97
+ hooks = self._config.hooks.get(event_name, [])
98
+ return [h for h in hooks if h.enabled]
99
+
100
+ def trigger(
101
+ self,
102
+ event_name: str,
103
+ payload: _PayloadProtocol,
104
+ *,
105
+ tool_name: str | None = None,
106
+ file_path: str | None = None,
107
+ ) -> list[HookResult]:
108
+ """
109
+ Run all hooks registered for the event with payload env.
110
+
111
+ For PostToolUse, only hooks matching tool_name and file_path (matcher
112
+ and file_patterns) are run.
113
+
114
+ Args:
115
+ event_name: One of UserPromptSubmit, PostToolUse, SessionStart,
116
+ SessionEnd, WorkflowComplete.
117
+ payload: Event payload with to_env() returning TAPPS_* dict.
118
+ tool_name: For PostToolUse, the tool that was used (e.g. Write, Edit).
119
+ file_path: For PostToolUse, the affected file path.
120
+
121
+ Returns:
122
+ List of HookResult in execution order. Caller can check fail_on_error
123
+ and result.success to decide whether to fail the workflow.
124
+ """
125
+ hooks = self._get_hooks_for_event(event_name)
126
+ if event_name == "PostToolUse" and (tool_name is not None or file_path is not None):
127
+ hooks = _filter_post_tool_use(hooks, tool_name or "", file_path)
128
+ env = payload.to_env()
129
+ results: list[HookResult] = []
130
+ for hook in hooks:
131
+ result = run_hook(
132
+ hook,
133
+ env,
134
+ timeout_seconds=self._timeout_seconds,
135
+ project_root=self._project_root,
136
+ )
137
+ results.append(result)
138
+ if hook.fail_on_error and not result.success:
139
+ raise RuntimeError(
140
+ f"Hook {hook.name} failed (fail_on_error=True): "
141
+ f"returncode={result.returncode}, stderr={result.stderr[:200]!r}"
142
+ )
143
+ return results
@@ -0,0 +1,19 @@
1
+ """
2
+ CLI session lifecycle for TappsCodingAgents.
3
+
4
+ Provides session tracking (session ID), SessionStart on first command,
5
+ SessionEnd on process exit via atexit, and optional session log/state
6
+ under .tapps-agents/sessions/.
7
+ """
8
+
9
+ from .manager import (
10
+ get_session_id,
11
+ ensure_session_started,
12
+ register_session_end_atexit,
13
+ )
14
+
15
+ __all__ = [
16
+ "get_session_id",
17
+ "ensure_session_started",
18
+ "register_session_end_atexit",
19
+ ]
@@ -0,0 +1,256 @@
1
+ """
2
+ CLI session lifecycle manager.
3
+
4
+ SessionStart on first tapps-agents command, SessionEnd on exit via atexit;
5
+ session tracking with session ID (TAPPS_SESSION_ID) and optional log/state
6
+ under .tapps-agents/sessions/ when configured.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import atexit
12
+ import json
13
+ import logging
14
+ import os
15
+ import uuid
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from tapps_agents.hooks.manager import HookManager
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Process-global: session id and whether we have already started/registered atexit
25
+ _session_id: str | None = None
26
+ _started: bool = False
27
+ _atexit_registered: bool = False
28
+ _project_root: Path | None = None
29
+
30
+
31
+ def _reset_state_for_testing() -> None:
32
+ """Reset process-global session state. For use in unit tests only."""
33
+ global _session_id, _started, _atexit_registered, _project_root
34
+ _session_id = None
35
+ _started = False
36
+ _atexit_registered = False
37
+ _project_root = None
38
+
39
+
40
+ def get_session_id() -> str | None:
41
+ """Return current session ID, or None if session not started."""
42
+ return _session_id
43
+
44
+
45
+ def _get_sessions_dir(project_root: Path) -> Path | None:
46
+ """
47
+ Return .tapps-agents/sessions path if session state is enabled.
48
+
49
+ Session log/state is written when:
50
+ - TAPPS_SESSION_STATE=1 or TAPPS_SESSION_LOG=1, or
51
+ - The directory .tapps-agents/sessions already exists (user-configured).
52
+ """
53
+ sessions_dir = project_root / ".tapps-agents" / "sessions"
54
+ if os.environ.get("TAPPS_SESSION_STATE", "") == "1":
55
+ return sessions_dir
56
+ if os.environ.get("TAPPS_SESSION_LOG", "") == "1":
57
+ return sessions_dir
58
+ if sessions_dir.exists():
59
+ return sessions_dir
60
+ return None
61
+
62
+
63
+ def _write_session_state(session_id: str, project_root: Path, started: bool) -> None:
64
+ """Write optional session state file under .tapps-agents/sessions/."""
65
+ sessions_dir = _get_sessions_dir(project_root)
66
+ if not sessions_dir:
67
+ return
68
+ try:
69
+ sessions_dir.mkdir(parents=True, exist_ok=True)
70
+ path = sessions_dir / f"{session_id}.json"
71
+ state = {
72
+ "session_id": session_id,
73
+ "project_root": str(project_root),
74
+ "started": started,
75
+ }
76
+ path.write_text(json.dumps(state, indent=2), encoding="utf-8")
77
+ except OSError as e:
78
+ logger.debug("Could not write session state: %s", e)
79
+
80
+
81
+ def _session_hydration_enabled(project_root: Path) -> bool:
82
+ """True if session lifecycle hydration is enabled (not disabled and task-specs configured)."""
83
+ if os.environ.get("TAPPS_SESSION_HYDRATION", "").strip().lower() in ("0", "false", "no"):
84
+ return False
85
+ task_specs_dir = project_root / ".tapps-agents" / "task-specs"
86
+ return task_specs_dir.exists()
87
+
88
+
89
+ def _run_session_start_hydration(project_root: Path, *, show_ready: bool = False) -> None:
90
+ """
91
+ On SessionStart: hydrate task specs to Beads; optionally run bd ready and log output.
92
+ No-op if hydration disabled or bd/task-specs not configured.
93
+ """
94
+ if not _session_hydration_enabled(project_root):
95
+ return
96
+ try:
97
+ from tapps_agents.beads.hydration import hydrate_to_beads
98
+ from tapps_agents.beads.client import is_available, run_bd
99
+
100
+ if not is_available(project_root):
101
+ return
102
+ report = hydrate_to_beads(project_root=project_root)
103
+ if report.bd_unavailable:
104
+ return
105
+ if report.created or report.deps_added:
106
+ logger.info(
107
+ "Session hydration: created=%d skipped=%d deps_added=%d",
108
+ report.created,
109
+ report.skipped,
110
+ report.deps_added,
111
+ )
112
+ if show_ready or os.environ.get("TAPPS_SESSION_SHOW_READY", "").strip() == "1":
113
+ r = run_bd(project_root, ["ready"])
114
+ if r.returncode == 0 and r.stdout:
115
+ logger.info("Beads ready: %s", r.stdout.strip()[:500])
116
+ except Exception as e:
117
+ logger.warning("SessionStart hydration error: %s", e)
118
+
119
+
120
+ def _run_session_end_dehydration(project_root: Path) -> None:
121
+ """
122
+ On SessionEnd: dehydrate from Beads and log short report (updated count).
123
+ No-op if hydration disabled or bd not available.
124
+ """
125
+ if not _session_hydration_enabled(project_root):
126
+ return
127
+ try:
128
+ from tapps_agents.beads.hydration import dehydrate_from_beads
129
+
130
+ updated = dehydrate_from_beads(project_root=project_root)
131
+ if updated > 0:
132
+ logger.info("Session hydration: %d spec(s) updated from Beads", updated)
133
+ except Exception as e:
134
+ logger.warning("SessionEnd dehydration error: %s", e)
135
+
136
+
137
+ def _fire_session_start(project_root: Path, hook_manager: HookManager) -> None:
138
+ """Fire SessionStart hook with current session ID and project root."""
139
+ sid = get_session_id()
140
+ if not sid:
141
+ return
142
+ from tapps_agents.hooks.events import SessionStartEvent
143
+
144
+ payload = SessionStartEvent(session_id=sid, project_root=str(project_root))
145
+ try:
146
+ hook_manager.trigger("SessionStart", payload)
147
+ except Exception as e:
148
+ logger.warning("SessionStart hook error: %s", e)
149
+ _run_session_start_hydration(project_root)
150
+
151
+
152
+ def _fire_session_end(project_root: Path, hook_manager: HookManager) -> None:
153
+ """Fire SessionEnd hook; run dehydrate and log hydration report."""
154
+ _run_session_end_dehydration(project_root)
155
+ sid = get_session_id()
156
+ if not sid:
157
+ return
158
+ from tapps_agents.hooks.events import SessionEndEvent
159
+
160
+ payload = SessionEndEvent(session_id=sid, project_root=str(project_root))
161
+ try:
162
+ hook_manager.trigger("SessionEnd", payload)
163
+ except Exception as e:
164
+ logger.warning("SessionEnd hook error: %s", e)
165
+
166
+
167
+ def ensure_session_started(
168
+ project_root: Path | str | None = None,
169
+ *,
170
+ hook_manager: HookManager | None = None,
171
+ ) -> str:
172
+ """
173
+ Start CLI session on first call: generate session ID, fire SessionStart, register atexit.
174
+
175
+ Idempotent: later calls in the same process return the same session ID and do not
176
+ fire SessionStart again.
177
+
178
+ Args:
179
+ project_root: Project root (default: cwd). Used for hooks and optional state.
180
+ hook_manager: HookManager instance. If None, one is created for project_root.
181
+
182
+ Returns:
183
+ Current session ID (UUID string).
184
+ """
185
+ global _session_id, _started, _atexit_registered, _project_root
186
+
187
+ root = Path(project_root) if project_root else Path.cwd()
188
+
189
+ if _started:
190
+ assert _session_id is not None
191
+ return _session_id
192
+
193
+ _session_id = str(uuid.uuid4())
194
+ _started = True
195
+ _project_root = root
196
+
197
+ if hook_manager is None:
198
+ from tapps_agents.hooks.manager import HookManager
199
+
200
+ hook_manager = HookManager(project_root=root)
201
+
202
+ _write_session_state(_session_id, root, started=True)
203
+ _fire_session_start(root, hook_manager)
204
+
205
+ if not _atexit_registered:
206
+ _atexit_registered = True
207
+ _hm = hook_manager
208
+
209
+ def _on_exit() -> None:
210
+ global _session_id, _project_root
211
+ root = _project_root or Path.cwd()
212
+ _fire_session_end(root, _hm)
213
+ if _session_id and _project_root:
214
+ _write_session_state(_session_id, _project_root, started=False)
215
+
216
+ atexit.register(_on_exit)
217
+
218
+ return _session_id
219
+
220
+
221
+ def register_session_end_atexit(
222
+ project_root: Path | str | None = None,
223
+ *,
224
+ hook_manager: HookManager | None = None,
225
+ ) -> None:
226
+ """
227
+ Register atexit handler for SessionEnd only (does not start session).
228
+
229
+ Use when you have already started the session via ensure_session_started
230
+ and want to ensure SessionEnd is registered. If ensure_session_started
231
+ was called, atexit is already registered; this is for edge cases or tests.
232
+ """
233
+ global _atexit_registered, _project_root
234
+
235
+ root = Path(project_root) if project_root else Path.cwd()
236
+ _project_root = _project_root or root
237
+
238
+ if hook_manager is None:
239
+ from tapps_agents.hooks.manager import HookManager
240
+
241
+ hook_manager = HookManager(project_root=root)
242
+
243
+ if _atexit_registered:
244
+ return
245
+
246
+ _atexit_registered = True
247
+ _hm = hook_manager
248
+
249
+ def _on_exit() -> None:
250
+ global _session_id, _project_root
251
+ root = _project_root or Path.cwd()
252
+ _fire_session_end(root, _hm)
253
+ if _session_id and _project_root:
254
+ _write_session_state(_session_id, _project_root, started=False)
255
+
256
+ atexit.register(_on_exit)