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.
- 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 +219 -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 +2196 -2118
- tapps_agents/workflow/direct_execution_fallback.py +16 -3
- tapps_agents/workflow/message_formatter.py +2 -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.40.dist-info}/METADATA +5 -1
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/RECORD +57 -53
- 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.40.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/licenses/LICENSE +0 -0
- {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)
|