monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.9__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 (58) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +51 -0
  3. monoco/core/hooks/__init__.py +19 -0
  4. monoco/core/hooks/base.py +104 -0
  5. monoco/core/hooks/builtin/__init__.py +11 -0
  6. monoco/core/hooks/builtin/git_cleanup.py +266 -0
  7. monoco/core/hooks/builtin/logging_hook.py +78 -0
  8. monoco/core/hooks/context.py +131 -0
  9. monoco/core/hooks/registry.py +222 -0
  10. monoco/core/integrations.py +6 -0
  11. monoco/core/registry.py +2 -0
  12. monoco/core/setup.py +1 -1
  13. monoco/core/skills.py +226 -42
  14. monoco/features/{scheduler → agent}/__init__.py +4 -2
  15. monoco/features/{scheduler → agent}/cli.py +134 -80
  16. monoco/features/{scheduler → agent}/config.py +17 -3
  17. monoco/features/agent/defaults.py +55 -0
  18. monoco/features/agent/flow_skills.py +281 -0
  19. monoco/features/{scheduler → agent}/manager.py +39 -2
  20. monoco/features/{scheduler → agent}/models.py +6 -3
  21. monoco/features/{scheduler → agent}/reliability.py +1 -1
  22. monoco/features/agent/resources/skills/flow_engineer/SKILL.md +94 -0
  23. monoco/features/agent/resources/skills/flow_manager/SKILL.md +88 -0
  24. monoco/features/agent/resources/skills/flow_reviewer/SKILL.md +114 -0
  25. monoco/features/{scheduler → agent}/session.py +36 -1
  26. monoco/features/{scheduler → agent}/worker.py +2 -2
  27. monoco/features/i18n/resources/skills/i18n_scan_workflow/SKILL.md +105 -0
  28. monoco/features/issue/commands.py +427 -21
  29. monoco/features/issue/core.py +100 -0
  30. monoco/features/issue/criticality.py +553 -0
  31. monoco/features/issue/domain/models.py +28 -2
  32. monoco/features/issue/engine/machine.py +70 -13
  33. monoco/features/issue/git_service.py +185 -0
  34. monoco/features/issue/linter.py +291 -62
  35. monoco/features/issue/models.py +49 -2
  36. monoco/features/issue/resources/en/SKILL.md +48 -0
  37. monoco/features/issue/resources/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  38. monoco/features/issue/resources/zh/SKILL.md +50 -0
  39. monoco/features/issue/validator.py +185 -65
  40. monoco/features/memo/__init__.py +2 -1
  41. monoco/features/memo/adapter.py +32 -0
  42. monoco/features/memo/cli.py +36 -14
  43. monoco/features/memo/core.py +59 -0
  44. monoco/features/memo/resources/skills/note_processing_workflow/SKILL.md +140 -0
  45. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  46. monoco/features/memo/resources/zh/SKILL.md +75 -0
  47. monoco/features/spike/resources/skills/research_workflow/SKILL.md +121 -0
  48. monoco/main.py +2 -3
  49. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/METADATA +1 -1
  50. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/RECORD +55 -37
  51. monoco/features/scheduler/defaults.py +0 -54
  52. monoco/features/skills/__init__.py +0 -0
  53. monoco/features/skills/core.py +0 -102
  54. /monoco/core/{hooks.py → githooks.py} +0 -0
  55. /monoco/features/{scheduler → agent}/engines.py +0 -0
  56. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/WHEEL +0 -0
  57. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/entry_points.txt +0 -0
  58. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.9.dist-info}/licenses/LICENSE +0 -0
monoco/cli/workspace.py CHANGED
@@ -4,7 +4,7 @@ from rich.console import Console
4
4
  import yaml
5
5
 
6
6
  from monoco.core.output import AgentOutput, OutputManager
7
- from monoco.core.hooks import install_hooks
7
+ from monoco.core.githooks import install_hooks
8
8
 
9
9
  app = typer.Typer(help="Manage Monoco Workspace")
10
10
  console = Console()
monoco/core/config.py CHANGED
@@ -93,12 +93,55 @@ class TransitionConfig(BaseModel):
93
93
  command_template: Optional[str] = None
94
94
 
95
95
 
96
+ class CriticalityRuleConfig(BaseModel):
97
+ """Configuration for auto-escalation rules."""
98
+
99
+ name: str
100
+ description: str = ""
101
+ path_patterns: List[str] = Field(default_factory=list)
102
+ tag_patterns: List[str] = Field(default_factory=list)
103
+ target_level: str = "medium" # low, medium, high, critical
104
+
105
+
106
+ class CriticalityConfig(BaseModel):
107
+ """Configuration for issue criticality system."""
108
+
109
+ enabled: bool = Field(default=True)
110
+ # Type to criticality default mapping
111
+ type_defaults: Dict[str, str] = Field(
112
+ default_factory=lambda: {
113
+ "epic": "high",
114
+ "feature": "medium",
115
+ "chore": "low",
116
+ "fix": "high",
117
+ }
118
+ )
119
+ # Auto-escalation rules
120
+ auto_rules: List[CriticalityRuleConfig] = Field(default_factory=list)
121
+
122
+ def merge(self, other: "CriticalityConfig") -> "CriticalityConfig":
123
+ if not other:
124
+ return self
125
+ if other.enabled is not None:
126
+ self.enabled = other.enabled
127
+ if other.type_defaults:
128
+ self.type_defaults.update(other.type_defaults)
129
+ if other.auto_rules:
130
+ # Merge by name
131
+ existing = {r.name: r for r in self.auto_rules}
132
+ for rule in other.auto_rules:
133
+ existing[rule.name] = rule
134
+ self.auto_rules = list(existing.values())
135
+ return self
136
+
137
+
96
138
  class IssueSchemaConfig(BaseModel):
97
139
  types: List[IssueTypeConfig] = Field(default_factory=list)
98
140
  statuses: List[str] = Field(default_factory=list)
99
141
  stages: List[str] = Field(default_factory=list)
100
142
  solutions: List[str] = Field(default_factory=list)
101
143
  workflows: List[TransitionConfig] = Field(default_factory=list)
144
+ criticality: CriticalityConfig = Field(default_factory=CriticalityConfig)
102
145
 
103
146
  def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
104
147
  if not other:
@@ -130,6 +173,10 @@ class IssueSchemaConfig(BaseModel):
130
173
  wf_map[ow.name] = ow
131
174
  self.workflows = list(wf_map.values())
132
175
 
176
+ # Criticality config
177
+ if other.criticality:
178
+ self.criticality = self.criticality.merge(other.criticality)
179
+
133
180
  return self
134
181
 
135
182
 
@@ -187,6 +234,10 @@ class MonocoConfig(BaseModel):
187
234
  default_factory=dict,
188
235
  description="Git hooks configuration (hook_name -> command)",
189
236
  )
237
+ session_hooks: Dict[str, Any] = Field(
238
+ default_factory=dict,
239
+ description="Session lifecycle hooks configuration (hook_name -> config)",
240
+ )
190
241
 
191
242
  issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
192
243
  domains: DomainConfig = Field(default_factory=DomainConfig)
@@ -0,0 +1,19 @@
1
+ """
2
+ Monoco Native Hook System
3
+
4
+ Provides lifecycle hooks for Agent Sessions, independent of specific CLI tools.
5
+ """
6
+
7
+ from .base import SessionLifecycleHook, HookResult, HookStatus
8
+ from .context import HookContext
9
+ from .registry import HookRegistry, get_registry, reset_registry
10
+
11
+ __all__ = [
12
+ "SessionLifecycleHook",
13
+ "HookResult",
14
+ "HookStatus",
15
+ "HookContext",
16
+ "HookRegistry",
17
+ "get_registry",
18
+ "reset_registry",
19
+ ]
@@ -0,0 +1,104 @@
1
+ """
2
+ Base classes for Session Lifecycle Hooks.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from typing import Optional
8
+ from enum import Enum
9
+
10
+ from .context import HookContext
11
+
12
+
13
+ class HookStatus(str, Enum):
14
+ """Status of hook execution."""
15
+ SUCCESS = "success"
16
+ FAILURE = "failure"
17
+ SKIPPED = "skipped"
18
+ WARNING = "warning"
19
+
20
+
21
+ @dataclass
22
+ class HookResult:
23
+ """Result of a hook execution."""
24
+ status: HookStatus
25
+ message: str = ""
26
+ details: Optional[dict] = None
27
+
28
+ @classmethod
29
+ def success(cls, message: str = "", details: Optional[dict] = None) -> "HookResult":
30
+ return cls(status=HookStatus.SUCCESS, message=message, details=details)
31
+
32
+ @classmethod
33
+ def failure(cls, message: str = "", details: Optional[dict] = None) -> "HookResult":
34
+ return cls(status=HookStatus.FAILURE, message=message, details=details)
35
+
36
+ @classmethod
37
+ def skipped(cls, message: str = "", details: Optional[dict] = None) -> "HookResult":
38
+ return cls(status=HookStatus.SKIPPED, message=message, details=details)
39
+
40
+ @classmethod
41
+ def warning(cls, message: str = "", details: Optional[dict] = None) -> "HookResult":
42
+ return cls(status=HookStatus.WARNING, message=message, details=details)
43
+
44
+
45
+ class SessionLifecycleHook(ABC):
46
+ """
47
+ Abstract base class for session lifecycle hooks.
48
+
49
+ Hooks can be registered to execute at specific points in a session's lifecycle:
50
+ - on_session_start: Called when a session starts
51
+ - on_session_end: Called when a session ends (terminate)
52
+
53
+ Example:
54
+ class MyHook(SessionLifecycleHook):
55
+ def on_session_start(self, context: HookContext) -> HookResult:
56
+ print(f"Session {context.session_id} started")
57
+ return HookResult.success()
58
+
59
+ def on_session_end(self, context: HookContext) -> HookResult:
60
+ print(f"Session {context.session_id} ended")
61
+ return HookResult.success()
62
+ """
63
+
64
+ def __init__(self, name: Optional[str] = None, config: Optional[dict] = None):
65
+ """
66
+ Initialize the hook.
67
+
68
+ Args:
69
+ name: Optional name for the hook. If not provided, uses class name.
70
+ config: Optional configuration dictionary for the hook.
71
+ """
72
+ self.name = name or self.__class__.__name__
73
+ self.config = config or {}
74
+ self.enabled = self.config.get("enabled", True)
75
+
76
+ @abstractmethod
77
+ def on_session_start(self, context: HookContext) -> HookResult:
78
+ """
79
+ Called when a session starts.
80
+
81
+ Args:
82
+ context: The hook context containing session information.
83
+
84
+ Returns:
85
+ HookResult indicating the outcome of the hook execution.
86
+ """
87
+ pass
88
+
89
+ @abstractmethod
90
+ def on_session_end(self, context: HookContext) -> HookResult:
91
+ """
92
+ Called when a session ends.
93
+
94
+ Args:
95
+ context: The hook context containing session information.
96
+
97
+ Returns:
98
+ HookResult indicating the outcome of the hook execution.
99
+ """
100
+ pass
101
+
102
+ def is_enabled(self) -> bool:
103
+ """Check if this hook is enabled."""
104
+ return self.enabled
@@ -0,0 +1,11 @@
1
+ """
2
+ Built-in session lifecycle hooks.
3
+ """
4
+
5
+ from .git_cleanup import GitCleanupHook
6
+ from .logging_hook import LoggingHook
7
+
8
+ __all__ = [
9
+ "GitCleanupHook",
10
+ "LoggingHook",
11
+ ]
@@ -0,0 +1,266 @@
1
+ """
2
+ GitCleanupHook - Performs git cleanup operations when a session ends.
3
+
4
+ This hook ensures that:
5
+ 1. Current branch is switched back to main (if safe)
6
+ 2. Feature branches are deleted if the associated issue is completed/merged
7
+ """
8
+
9
+ import logging
10
+ from typing import Optional
11
+
12
+ from ..base import SessionLifecycleHook, HookResult, HookStatus
13
+ from ..context import HookContext
14
+
15
+ logger = logging.getLogger("monoco.core.hooks.git_cleanup")
16
+
17
+
18
+ class GitCleanupHook(SessionLifecycleHook):
19
+ """
20
+ Hook for cleaning up git state when a session ends.
21
+
22
+ Configuration options:
23
+ - auto_switch_to_main: Whether to automatically switch to main branch (default: True)
24
+ - auto_delete_merged_branches: Whether to delete merged feature branches (default: True)
25
+ - main_branch: Name of the main branch (default: "main", fallback: "master")
26
+ - require_clean_worktree: Whether to require clean worktree before operations (default: True)
27
+ """
28
+
29
+ def __init__(self, name: Optional[str] = None, config: Optional[dict] = None):
30
+ super().__init__(name=name or "git_cleanup", config=config)
31
+
32
+ # Configuration with defaults
33
+ self.auto_switch_to_main = self.config.get("auto_switch_to_main", True)
34
+ self.auto_delete_merged_branches = self.config.get("auto_delete_merged_branches", True)
35
+ self.main_branch = self.config.get("main_branch", "main")
36
+ self.require_clean_worktree = self.config.get("require_clean_worktree", True)
37
+
38
+ def on_session_start(self, context: HookContext) -> HookResult:
39
+ """
40
+ Called when session starts.
41
+
42
+ No action needed at session start for git cleanup.
43
+ """
44
+ return HookResult.success("Git cleanup hook initialized")
45
+
46
+ def on_session_end(self, context: HookContext) -> HookResult:
47
+ """
48
+ Called when session ends.
49
+
50
+ Performs cleanup operations:
51
+ 1. Check current git state
52
+ 2. Switch to main branch if needed and safe
53
+ 3. Delete feature branch if issue is completed
54
+ """
55
+ if not context.git:
56
+ return HookResult.skipped("No git context available")
57
+
58
+ project_root = context.git.project_root
59
+
60
+ try:
61
+ from monoco.core import git
62
+
63
+ # Check if we're in a git repo
64
+ if not git.is_git_repo(project_root):
65
+ return HookResult.skipped("Not a git repository")
66
+
67
+ # Get current state
68
+ current_branch = git.get_current_branch(project_root)
69
+ default_branch = self._get_default_branch(project_root)
70
+
71
+ # Check for uncommitted changes
72
+ has_changes = len(git.get_git_status(project_root)) > 0
73
+
74
+ results = []
75
+
76
+ # Step 1: Switch to main branch if needed
77
+ if self.auto_switch_to_main and current_branch != default_branch:
78
+ switch_result = self._switch_to_main(
79
+ project_root, current_branch, default_branch, has_changes
80
+ )
81
+ results.append(switch_result)
82
+
83
+ if switch_result.status == HookStatus.SUCCESS:
84
+ current_branch = default_branch
85
+
86
+ # Step 2: Delete feature branch if issue is completed
87
+ if self.auto_delete_merged_branches and context.issue:
88
+ delete_result = self._cleanup_feature_branch(
89
+ project_root, context.issue, default_branch, current_branch
90
+ )
91
+ if delete_result:
92
+ results.append(delete_result)
93
+
94
+ # Combine results
95
+ failures = [r for r in results if r.status == HookStatus.FAILURE]
96
+ warnings = [r for r in results if r.status == HookStatus.WARNING]
97
+
98
+ if failures:
99
+ return HookResult.failure(
100
+ f"Git cleanup completed with {len(failures)} failures",
101
+ {"results": [r.message for r in results]}
102
+ )
103
+ elif warnings:
104
+ return HookResult.warning(
105
+ f"Git cleanup completed with {len(warnings)} warnings",
106
+ {"results": [r.message for r in results]}
107
+ )
108
+ else:
109
+ return HookResult.success(
110
+ "Git cleanup completed successfully",
111
+ {"results": [r.message for r in results]}
112
+ )
113
+
114
+ except Exception as e:
115
+ logger.error(f"Git cleanup failed: {e}")
116
+ return HookResult.failure(f"Git cleanup failed: {e}")
117
+
118
+ def _get_default_branch(self, project_root) -> str:
119
+ """Determine the default branch (main or master)."""
120
+ from monoco.core import git
121
+
122
+ if git.branch_exists(project_root, self.main_branch):
123
+ return self.main_branch
124
+ elif git.branch_exists(project_root, "master"):
125
+ return "master"
126
+ return self.main_branch
127
+
128
+ def _switch_to_main(
129
+ self,
130
+ project_root,
131
+ current_branch: str,
132
+ default_branch: str,
133
+ has_changes: bool
134
+ ) -> HookResult:
135
+ """
136
+ Switch to the main branch if safe to do so.
137
+
138
+ Args:
139
+ project_root: The project root path
140
+ current_branch: Current branch name
141
+ default_branch: Target branch name (main/master)
142
+ has_changes: Whether there are uncommitted changes
143
+
144
+ Returns:
145
+ HookResult indicating success/failure
146
+ """
147
+ from monoco.core import git
148
+
149
+ # Safety check: uncommitted changes
150
+ if has_changes and self.require_clean_worktree:
151
+ return HookResult.warning(
152
+ f"Cannot switch from '{current_branch}' to '{default_branch}': "
153
+ f"uncommitted changes exist. Please commit or stash changes."
154
+ )
155
+
156
+ # Check if default branch exists
157
+ if not git.branch_exists(project_root, default_branch):
158
+ return HookResult.warning(
159
+ f"Cannot switch to '{default_branch}': branch does not exist"
160
+ )
161
+
162
+ try:
163
+ git.checkout_branch(project_root, default_branch)
164
+ return HookResult.success(
165
+ f"Switched from '{current_branch}' to '{default_branch}'"
166
+ )
167
+ except Exception as e:
168
+ return HookResult.failure(
169
+ f"Failed to switch to '{default_branch}': {e}"
170
+ )
171
+
172
+ def _cleanup_feature_branch(
173
+ self,
174
+ project_root,
175
+ issue,
176
+ default_branch: str,
177
+ current_branch: str
178
+ ) -> Optional[HookResult]:
179
+ """
180
+ Clean up the feature branch associated with an issue.
181
+
182
+ Args:
183
+ project_root: The project root path
184
+ issue: The IssueInfo object
185
+ default_branch: The default branch name
186
+ current_branch: Current branch name
187
+
188
+ Returns:
189
+ HookResult or None if no action needed
190
+ """
191
+ from monoco.core import git
192
+
193
+ # Get the branch name from issue
194
+ branch_name = issue.branch_name
195
+ if not branch_name:
196
+ # Try to infer from convention: feat/<issue_id>-*
197
+ # This is a fallback if isolation metadata is not set
198
+ return None
199
+
200
+ # Check if branch exists
201
+ if not git.branch_exists(project_root, branch_name):
202
+ return None
203
+
204
+ # Safety: don't delete the branch we're currently on
205
+ if current_branch == branch_name:
206
+ return HookResult.warning(
207
+ f"Cannot delete branch '{branch_name}': currently checked out"
208
+ )
209
+
210
+ # Check if issue is completed/closed
211
+ is_completed = issue.status in ("closed", "done", "merged")
212
+
213
+ if not is_completed:
214
+ return HookResult.skipped(
215
+ f"Branch '{branch_name}' not deleted: issue status is '{issue.status}'"
216
+ )
217
+
218
+ # Check if branch is merged into default branch
219
+ try:
220
+ is_merged = self._is_branch_merged(project_root, branch_name, default_branch)
221
+ except Exception:
222
+ is_merged = False
223
+
224
+ if not is_merged:
225
+ return HookResult.warning(
226
+ f"Branch '{branch_name}' not deleted: not merged into '{default_branch}'"
227
+ )
228
+
229
+ # Safe to delete
230
+ try:
231
+ git.delete_branch(project_root, branch_name, force=False)
232
+ return HookResult.success(
233
+ f"Deleted merged branch '{branch_name}'"
234
+ )
235
+ except Exception as e:
236
+ return HookResult.failure(
237
+ f"Failed to delete branch '{branch_name}': {e}"
238
+ )
239
+
240
+ def _is_branch_merged(
241
+ self,
242
+ project_root,
243
+ branch: str,
244
+ target: str
245
+ ) -> bool:
246
+ """
247
+ Check if a branch is merged into the target branch.
248
+
249
+ Args:
250
+ project_root: The project root path
251
+ branch: The branch to check
252
+ target: The target branch to check against
253
+
254
+ Returns:
255
+ True if branch is merged into target
256
+ """
257
+ from monoco.core import git
258
+
259
+ # Use git merge-base to check if branch is ancestor of target
260
+ code, stdout, _ = git._run_git(
261
+ ["merge-base", "--is-ancestor", branch, target],
262
+ project_root
263
+ )
264
+
265
+ # Exit code 0 means branch is ancestor of target (merged)
266
+ return code == 0
@@ -0,0 +1,78 @@
1
+ """
2
+ LoggingHook - Logs session lifecycle events.
3
+
4
+ Simple hook that logs when sessions start and end for auditing/debugging.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import Optional
10
+
11
+ from ..base import SessionLifecycleHook, HookResult
12
+ from ..context import HookContext
13
+
14
+ logger = logging.getLogger("monoco.core.hooks.logging")
15
+
16
+
17
+ class LoggingHook(SessionLifecycleHook):
18
+ """
19
+ Hook for logging session lifecycle events.
20
+
21
+ Configuration options:
22
+ - log_level: The logging level to use (default: INFO)
23
+ - log_start: Whether to log session start (default: True)
24
+ - log_end: Whether to log session end (default: True)
25
+ """
26
+
27
+ def __init__(self, name: Optional[str] = None, config: Optional[dict] = None):
28
+ super().__init__(name=name or "logging", config=config)
29
+
30
+ self.log_level = self._parse_log_level(self.config.get("log_level", "INFO"))
31
+ self.log_start = self.config.get("log_start", True)
32
+ self.log_end = self.config.get("log_end", True)
33
+
34
+ def _parse_log_level(self, level: str) -> int:
35
+ """Parse log level string to logging constant."""
36
+ levels = {
37
+ "DEBUG": logging.DEBUG,
38
+ "INFO": logging.INFO,
39
+ "WARNING": logging.WARNING,
40
+ "ERROR": logging.ERROR,
41
+ "CRITICAL": logging.CRITICAL,
42
+ }
43
+ return levels.get(level.upper(), logging.INFO)
44
+
45
+ def on_session_start(self, context: HookContext) -> HookResult:
46
+ """Log session start."""
47
+ if not self.log_start:
48
+ return HookResult.skipped("Session start logging disabled")
49
+
50
+ issue_info = f" for issue {context.issue.id}" if context.issue else ""
51
+ log_message = (
52
+ f"Session {context.session_id} started{issue_info} "
53
+ f"(role: {context.role_name})"
54
+ )
55
+
56
+ logger.log(self.log_level, log_message)
57
+
58
+ return HookResult.success(f"Logged session start: {context.session_id}")
59
+
60
+ def on_session_end(self, context: HookContext) -> HookResult:
61
+ """Log session end with duration."""
62
+ if not self.log_end:
63
+ return HookResult.skipped("Session end logging disabled")
64
+
65
+ duration = datetime.now() - context.created_at
66
+ duration_seconds = duration.total_seconds()
67
+
68
+ issue_info = f" for issue {context.issue.id}" if context.issue else ""
69
+ log_message = (
70
+ f"Session {context.session_id} ended{issue_info} "
71
+ f"(duration: {duration_seconds:.2f}s, status: {context.session_status})"
72
+ )
73
+
74
+ logger.log(self.log_level, log_message)
75
+
76
+ return HookResult.success(
77
+ f"Logged session end: {context.session_id} (duration: {duration_seconds:.2f}s)"
78
+ )
@@ -0,0 +1,131 @@
1
+ """
2
+ Hook Context - Data passed to hooks during session lifecycle events.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Optional, Dict, Any
9
+
10
+
11
+ @dataclass
12
+ class IssueInfo:
13
+ """Information about the issue associated with a session."""
14
+ id: str
15
+ status: Optional[str] = None
16
+ stage: Optional[str] = None
17
+ title: Optional[str] = None
18
+ branch_name: Optional[str] = None
19
+ is_merged: bool = False
20
+
21
+ @classmethod
22
+ def from_metadata(cls, metadata: Any) -> "IssueInfo":
23
+ """Create IssueInfo from IssueMetadata."""
24
+ return cls(
25
+ id=getattr(metadata, "id", ""),
26
+ status=getattr(metadata, "status", None),
27
+ stage=getattr(metadata, "stage", None),
28
+ title=getattr(metadata, "title", None),
29
+ branch_name=getattr(metadata, "isolation", {}).get("ref") if hasattr(metadata, "isolation") and metadata.isolation else None,
30
+ is_merged=False, # Will be determined by GitCleanupHook
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class GitInfo:
36
+ """Git repository information."""
37
+ project_root: Path
38
+ current_branch: Optional[str] = None
39
+ has_uncommitted_changes: bool = False
40
+ default_branch: str = "main"
41
+
42
+ def __post_init__(self):
43
+ if self.current_branch is None:
44
+ # Lazy load current branch
45
+ try:
46
+ from monoco.core import git
47
+ self.current_branch = git.get_current_branch(self.project_root)
48
+ except Exception:
49
+ self.current_branch = None
50
+
51
+
52
+ @dataclass
53
+ class HookContext:
54
+ """
55
+ Context object passed to lifecycle hooks.
56
+
57
+ Contains all relevant information about the session, issue, and environment
58
+ that hooks might need to perform their operations.
59
+ """
60
+
61
+ # Session Information
62
+ session_id: str
63
+ role_name: str
64
+ session_status: str
65
+ created_at: datetime
66
+
67
+ # Issue Information
68
+ issue: Optional[IssueInfo] = None
69
+
70
+ # Git Information
71
+ git: Optional[GitInfo] = None
72
+
73
+ # Additional Context
74
+ extra: Dict[str, Any] = field(default_factory=dict)
75
+
76
+ @classmethod
77
+ def from_runtime_session(
78
+ cls,
79
+ runtime_session: Any,
80
+ project_root: Optional[Path] = None,
81
+ ) -> "HookContext":
82
+ """
83
+ Create a HookContext from a RuntimeSession.
84
+
85
+ Args:
86
+ runtime_session: The RuntimeSession object
87
+ project_root: Optional project root path
88
+
89
+ Returns:
90
+ A populated HookContext
91
+ """
92
+ model = runtime_session.model
93
+
94
+ # Build IssueInfo if we have an issue_id
95
+ issue_info = None
96
+ if model.issue_id:
97
+ issue_info = IssueInfo(
98
+ id=model.issue_id,
99
+ branch_name=model.branch_name,
100
+ )
101
+
102
+ # Try to load full issue metadata
103
+ try:
104
+ from monoco.features.issue.core import find_issue_path, parse_issue
105
+ from monoco.core.config import find_monoco_root
106
+
107
+ if project_root is None:
108
+ project_root = find_monoco_root()
109
+
110
+ issues_root = project_root / "Issues"
111
+ issue_path = find_issue_path(issues_root, model.issue_id)
112
+ if issue_path:
113
+ metadata = parse_issue(issue_path)
114
+ if metadata:
115
+ issue_info = IssueInfo.from_metadata(metadata)
116
+ except Exception:
117
+ pass # Use basic issue info
118
+
119
+ # Build GitInfo
120
+ git_info = None
121
+ if project_root:
122
+ git_info = GitInfo(project_root=project_root)
123
+
124
+ return cls(
125
+ session_id=model.id,
126
+ role_name=model.role_name,
127
+ session_status=model.status,
128
+ created_at=model.created_at,
129
+ issue=issue_info,
130
+ git=git_info,
131
+ )