monoco-toolkit 0.3.6__py3-none-any.whl → 0.3.10__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 (113) hide show
  1. monoco/cli/workspace.py +1 -1
  2. monoco/core/config.py +58 -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/injection.py +63 -29
  11. monoco/core/integrations.py +8 -2
  12. monoco/core/output.py +5 -5
  13. monoco/core/registry.py +9 -1
  14. monoco/core/resource/__init__.py +5 -0
  15. monoco/core/resource/finder.py +98 -0
  16. monoco/core/resource/manager.py +91 -0
  17. monoco/core/resource/models.py +35 -0
  18. monoco/core/resources/en/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  19. monoco/core/resources/zh/{SKILL.md → skills/monoco_core/SKILL.md} +2 -0
  20. monoco/core/setup.py +1 -1
  21. monoco/core/skill_framework.py +292 -0
  22. monoco/core/skills.py +538 -254
  23. monoco/core/sync.py +73 -1
  24. monoco/core/workflow_converter.py +420 -0
  25. monoco/features/{scheduler → agent}/__init__.py +5 -3
  26. monoco/features/agent/adapter.py +31 -0
  27. monoco/features/agent/apoptosis.py +44 -0
  28. monoco/features/agent/cli.py +296 -0
  29. monoco/features/agent/config.py +96 -0
  30. monoco/features/agent/defaults.py +12 -0
  31. monoco/features/{scheduler → agent}/engines.py +32 -6
  32. monoco/features/agent/flow_skills.py +281 -0
  33. monoco/features/agent/manager.py +91 -0
  34. monoco/features/{scheduler → agent}/models.py +6 -3
  35. monoco/features/agent/resources/atoms/atom-code-dev.yaml +61 -0
  36. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +73 -0
  37. monoco/features/agent/resources/atoms/atom-knowledge.yaml +55 -0
  38. monoco/features/agent/resources/atoms/atom-review.yaml +60 -0
  39. monoco/features/agent/resources/en/skills/flow_engineer/SKILL.md +94 -0
  40. monoco/features/agent/resources/en/skills/flow_manager/SKILL.md +93 -0
  41. monoco/features/agent/resources/en/skills/flow_planner/SKILL.md +85 -0
  42. monoco/features/agent/resources/en/skills/flow_reviewer/SKILL.md +114 -0
  43. monoco/features/agent/resources/roles/role-engineer.yaml +49 -0
  44. monoco/features/agent/resources/roles/role-manager.yaml +46 -0
  45. monoco/features/agent/resources/roles/role-planner.yaml +46 -0
  46. monoco/features/agent/resources/roles/role-reviewer.yaml +47 -0
  47. monoco/features/agent/resources/workflows/workflow-dev.yaml +83 -0
  48. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +72 -0
  49. monoco/features/agent/resources/workflows/workflow-review.yaml +94 -0
  50. monoco/features/agent/resources/zh/skills/flow_engineer/SKILL.md +94 -0
  51. monoco/features/agent/resources/zh/skills/flow_manager/SKILL.md +88 -0
  52. monoco/features/agent/resources/zh/skills/flow_planner/SKILL.md +259 -0
  53. monoco/features/agent/resources/zh/skills/flow_reviewer/SKILL.md +137 -0
  54. monoco/features/{scheduler → agent}/session.py +36 -1
  55. monoco/features/{scheduler → agent}/worker.py +40 -4
  56. monoco/features/glossary/adapter.py +31 -0
  57. monoco/features/glossary/config.py +5 -0
  58. monoco/features/glossary/resources/en/AGENTS.md +29 -0
  59. monoco/features/glossary/resources/en/skills/monoco_glossary/SKILL.md +35 -0
  60. monoco/features/glossary/resources/zh/AGENTS.md +29 -0
  61. monoco/features/glossary/resources/zh/skills/monoco_glossary/SKILL.md +35 -0
  62. monoco/features/i18n/resources/en/skills/i18n_scan_workflow/SKILL.md +105 -0
  63. monoco/features/i18n/resources/en/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  64. monoco/features/i18n/resources/zh/skills/i18n_scan_workflow/SKILL.md +105 -0
  65. monoco/features/i18n/resources/zh/{SKILL.md → skills/monoco_i18n/SKILL.md} +2 -0
  66. monoco/features/issue/commands.py +427 -21
  67. monoco/features/issue/core.py +140 -1
  68. monoco/features/issue/criticality.py +553 -0
  69. monoco/features/issue/domain/models.py +28 -2
  70. monoco/features/issue/engine/machine.py +75 -15
  71. monoco/features/issue/git_service.py +185 -0
  72. monoco/features/issue/linter.py +291 -62
  73. monoco/features/issue/models.py +50 -2
  74. monoco/features/issue/resources/en/skills/issue_create_workflow/SKILL.md +167 -0
  75. monoco/features/issue/resources/en/skills/issue_develop_workflow/SKILL.md +224 -0
  76. monoco/features/issue/resources/en/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  77. monoco/features/issue/resources/en/skills/issue_refine_workflow/SKILL.md +203 -0
  78. monoco/features/issue/resources/en/{SKILL.md → skills/monoco_issue/SKILL.md} +50 -0
  79. monoco/features/issue/resources/zh/skills/issue_create_workflow/SKILL.md +167 -0
  80. monoco/features/issue/resources/zh/skills/issue_develop_workflow/SKILL.md +224 -0
  81. monoco/features/issue/resources/zh/skills/issue_lifecycle_workflow/SKILL.md +159 -0
  82. monoco/features/issue/resources/zh/skills/issue_refine_workflow/SKILL.md +203 -0
  83. monoco/features/issue/resources/zh/{SKILL.md → skills/monoco_issue/SKILL.md} +52 -0
  84. monoco/features/issue/validator.py +185 -65
  85. monoco/features/memo/__init__.py +2 -1
  86. monoco/features/memo/adapter.py +32 -0
  87. monoco/features/memo/cli.py +36 -14
  88. monoco/features/memo/core.py +59 -0
  89. monoco/features/memo/resources/en/skills/monoco_memo/SKILL.md +77 -0
  90. monoco/features/memo/resources/en/skills/note_processing_workflow/SKILL.md +140 -0
  91. monoco/features/memo/resources/zh/AGENTS.md +8 -0
  92. monoco/features/memo/resources/zh/skills/monoco_memo/SKILL.md +77 -0
  93. monoco/features/memo/resources/zh/skills/note_processing_workflow/SKILL.md +140 -0
  94. monoco/features/spike/resources/en/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  95. monoco/features/spike/resources/en/skills/research_workflow/SKILL.md +121 -0
  96. monoco/features/spike/resources/zh/{SKILL.md → skills/monoco_spike/SKILL.md} +2 -0
  97. monoco/features/spike/resources/zh/skills/research_workflow/SKILL.md +121 -0
  98. monoco/main.py +2 -3
  99. monoco_toolkit-0.3.10.dist-info/METADATA +124 -0
  100. monoco_toolkit-0.3.10.dist-info/RECORD +156 -0
  101. monoco/features/scheduler/cli.py +0 -285
  102. monoco/features/scheduler/config.py +0 -68
  103. monoco/features/scheduler/defaults.py +0 -54
  104. monoco/features/scheduler/manager.py +0 -49
  105. monoco/features/scheduler/reliability.py +0 -106
  106. monoco/features/skills/core.py +0 -102
  107. monoco_toolkit-0.3.6.dist-info/METADATA +0 -127
  108. monoco_toolkit-0.3.6.dist-info/RECORD +0 -97
  109. /monoco/core/{hooks.py → githooks.py} +0 -0
  110. /monoco/features/{skills → glossary}/__init__.py +0 -0
  111. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/WHEEL +0 -0
  112. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.dist-info}/entry_points.txt +0 -0
  113. {monoco_toolkit-0.3.6.dist-info → monoco_toolkit-0.3.10.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
@@ -91,6 +91,54 @@ class TransitionConfig(BaseModel):
91
91
  required_solution: Optional[str] = None
92
92
  description: str = ""
93
93
  command_template: Optional[str] = None
94
+ post_actions: List[str] = Field(default_factory=list)
95
+
96
+
97
+ class CriticalityRuleConfig(BaseModel):
98
+ """Configuration for auto-escalation rules."""
99
+
100
+ name: str
101
+ description: str = ""
102
+ path_patterns: List[str] = Field(default_factory=list)
103
+ tag_patterns: List[str] = Field(default_factory=list)
104
+ target_level: str = "medium" # low, medium, high, critical
105
+
106
+
107
+ class CriticalityConfig(BaseModel):
108
+ """Configuration for issue criticality system."""
109
+
110
+ enabled: bool = Field(default=True)
111
+ # Type to criticality default mapping
112
+ type_defaults: Dict[str, str] = Field(
113
+ default_factory=lambda: {
114
+ "epic": "high",
115
+ "feature": "medium",
116
+ "chore": "low",
117
+ "fix": "high",
118
+ }
119
+ )
120
+ # Auto-escalation rules
121
+ auto_rules: List[CriticalityRuleConfig] = Field(default_factory=list)
122
+
123
+ def merge(self, other: "CriticalityConfig") -> "CriticalityConfig":
124
+ if not other:
125
+ return self
126
+ if other.enabled is not None:
127
+ self.enabled = other.enabled
128
+ if other.type_defaults:
129
+ self.type_defaults.update(other.type_defaults)
130
+ if other.auto_rules:
131
+ # Merge by name
132
+ existing = {r.name: r for r in self.auto_rules}
133
+ for rule in other.auto_rules:
134
+ existing[rule.name] = rule
135
+ self.auto_rules = list(existing.values())
136
+ return self
137
+
138
+
139
+ class AgentConfig(BaseModel):
140
+ """Configuration for AI Agents."""
141
+ timeout_seconds: int = Field(default=900, description="Global timeout for agent sessions")
94
142
 
95
143
 
96
144
  class IssueSchemaConfig(BaseModel):
@@ -99,6 +147,7 @@ class IssueSchemaConfig(BaseModel):
99
147
  stages: List[str] = Field(default_factory=list)
100
148
  solutions: List[str] = Field(default_factory=list)
101
149
  workflows: List[TransitionConfig] = Field(default_factory=list)
150
+ criticality: CriticalityConfig = Field(default_factory=CriticalityConfig)
102
151
 
103
152
  def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
104
153
  if not other:
@@ -130,6 +179,10 @@ class IssueSchemaConfig(BaseModel):
130
179
  wf_map[ow.name] = ow
131
180
  self.workflows = list(wf_map.values())
132
181
 
182
+ # Criticality config
183
+ if other.criticality:
184
+ self.criticality = self.criticality.merge(other.criticality)
185
+
133
186
  return self
134
187
 
135
188
 
@@ -187,9 +240,14 @@ class MonocoConfig(BaseModel):
187
240
  default_factory=dict,
188
241
  description="Git hooks configuration (hook_name -> command)",
189
242
  )
243
+ session_hooks: Dict[str, Any] = Field(
244
+ default_factory=dict,
245
+ description="Session lifecycle hooks configuration (hook_name -> config)",
246
+ )
190
247
 
191
248
  issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
192
249
  domains: DomainConfig = Field(default_factory=DomainConfig)
250
+ agent: AgentConfig = Field(default_factory=AgentConfig)
193
251
 
194
252
  @staticmethod
195
253
  def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
@@ -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", False)
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
+ )