monoco-toolkit 0.3.12__py3-none-any.whl → 0.4.0__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 (120) hide show
  1. monoco/core/automation/__init__.py +0 -11
  2. monoco/core/automation/handlers.py +108 -26
  3. monoco/core/config.py +28 -10
  4. monoco/core/daemon/__init__.py +5 -0
  5. monoco/core/daemon/pid.py +290 -0
  6. monoco/core/injection.py +86 -8
  7. monoco/core/integrations.py +0 -24
  8. monoco/core/router/__init__.py +1 -39
  9. monoco/core/router/action.py +3 -142
  10. monoco/core/scheduler/events.py +28 -2
  11. monoco/core/setup.py +9 -0
  12. monoco/core/sync.py +199 -4
  13. monoco/core/watcher/__init__.py +6 -0
  14. monoco/core/watcher/base.py +18 -1
  15. monoco/core/watcher/im.py +460 -0
  16. monoco/core/watcher/memo.py +40 -48
  17. monoco/daemon/app.py +3 -60
  18. monoco/daemon/commands.py +459 -25
  19. monoco/daemon/scheduler.py +1 -16
  20. monoco/daemon/services.py +15 -0
  21. monoco/features/agent/resources/en/AGENTS.md +14 -14
  22. monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
  23. monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
  24. monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
  25. monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
  26. monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
  27. monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
  28. monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
  29. monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
  30. monoco/features/hooks/__init__.py +61 -6
  31. monoco/features/hooks/commands.py +281 -271
  32. monoco/features/hooks/dispatchers/__init__.py +23 -0
  33. monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
  34. monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
  35. monoco/features/hooks/manager.py +357 -0
  36. monoco/features/hooks/models.py +262 -0
  37. monoco/features/hooks/parser.py +322 -0
  38. monoco/features/hooks/universal_interceptor.py +503 -0
  39. monoco/features/im/__init__.py +67 -0
  40. monoco/features/im/core.py +782 -0
  41. monoco/features/im/models.py +311 -0
  42. monoco/features/issue/commands.py +65 -50
  43. monoco/features/issue/core.py +199 -99
  44. monoco/features/issue/domain_commands.py +0 -19
  45. monoco/features/issue/resources/en/AGENTS.md +17 -122
  46. monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
  47. monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
  48. monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
  49. monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
  50. monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
  51. monoco/features/issue/resources/zh/AGENTS.md +18 -123
  52. monoco/features/memo/cli.py +15 -64
  53. monoco/features/memo/core.py +6 -34
  54. monoco/features/memo/models.py +24 -15
  55. monoco/features/memo/resources/en/AGENTS.md +31 -0
  56. monoco/features/memo/resources/zh/AGENTS.md +28 -5
  57. monoco/main.py +5 -3
  58. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
  60. monoco/core/automation/config.py +0 -338
  61. monoco/core/execution.py +0 -67
  62. monoco/core/executor/__init__.py +0 -38
  63. monoco/core/executor/agent_action.py +0 -254
  64. monoco/core/executor/git_action.py +0 -303
  65. monoco/core/executor/im_action.py +0 -309
  66. monoco/core/executor/pytest_action.py +0 -218
  67. monoco/core/router/router.py +0 -392
  68. monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
  69. monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
  70. monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
  71. monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
  72. monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
  73. monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  74. monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
  75. monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
  76. monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
  77. monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
  78. monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
  79. monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
  80. monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
  81. monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
  82. monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
  83. monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
  84. monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
  85. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
  86. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
  87. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
  88. monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
  89. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
  90. monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
  91. monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
  92. monoco/features/hooks/adapter.py +0 -67
  93. monoco/features/hooks/core.py +0 -441
  94. monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
  95. monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  96. monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
  97. monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
  98. monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
  99. monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  100. monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  101. monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  102. monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  103. monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
  104. monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
  105. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
  106. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
  107. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
  108. monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
  109. monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
  110. monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  111. monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
  112. monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
  113. monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
  114. monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
  115. monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
  116. monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
  117. monoco_toolkit-0.3.12.dist-info/RECORD +0 -202
  118. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
  119. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
  120. {monoco_toolkit-0.3.12.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,478 @@
1
+ """
2
+ Git Hooks Dispatcher for Universal Hooks system.
3
+
4
+ Manages installation, execution, and uninstallation of Git hooks
5
+ to the `.git/hooks/` directory with non-destructive installation
6
+ and Glob matcher support for staged files.
7
+ """
8
+
9
+ import fnmatch
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ from ..manager import HookDispatcher
17
+ from ..models import GitEvent, HookType, ParsedHook
18
+
19
+
20
+ class GitHookDispatcher(HookDispatcher):
21
+ """
22
+ Dispatcher for Git lifecycle hooks.
23
+
24
+ Responsible for:
25
+ - Installing hook proxy scripts to `.git/hooks/`
26
+ - Executing hooks with staged file filtering
27
+ - Non-destructive installation (coexisting with Husky/pre-commit)
28
+ - Uninstalling and restoring original hooks
29
+
30
+ Supported events:
31
+ - pre-commit
32
+ - prepare-commit-msg
33
+ - commit-msg
34
+ - post-merge
35
+ - pre-push
36
+ - post-checkout
37
+ - pre-rebase
38
+ """
39
+
40
+ # Marker used to identify Monoco-managed hooks
41
+ HOOK_MARKER = "MONOCO_HOOK_MARKER"
42
+ BACKUP_SUFFIX = ".monoco.backup"
43
+
44
+ def __init__(self):
45
+ """Initialize the Git hook dispatcher."""
46
+ super().__init__(HookType.GIT, provider=None)
47
+ self._git_dir: Optional[Path] = None
48
+ self._hooks_dir: Optional[Path] = None
49
+
50
+ def _ensure_git_repo(self, project_root: Path) -> bool:
51
+ """
52
+ Check if project_root is a git repository and set up paths.
53
+
54
+ Args:
55
+ project_root: The project root directory
56
+
57
+ Returns:
58
+ True if valid git repository, False otherwise
59
+ """
60
+ git_dir = project_root / ".git"
61
+ if not git_dir.exists():
62
+ return False
63
+
64
+ self._git_dir = git_dir
65
+ self._hooks_dir = git_dir / "hooks"
66
+ self._hooks_dir.mkdir(exist_ok=True)
67
+ return True
68
+
69
+ def can_execute(self, hook: ParsedHook) -> bool:
70
+ """
71
+ Check if this dispatcher can execute the given hook.
72
+
73
+ Args:
74
+ hook: The parsed hook to check
75
+
76
+ Returns:
77
+ True if this is a git hook
78
+ """
79
+ return hook.metadata.type == HookType.GIT
80
+
81
+ def execute(self, hook: ParsedHook, context: Optional[dict] = None) -> bool:
82
+ """
83
+ Execute a git hook script.
84
+
85
+ Args:
86
+ hook: The parsed hook to execute
87
+ context: Optional execution context with 'event' and 'git_root'
88
+
89
+ Returns:
90
+ True if execution succeeded
91
+ """
92
+ if not hook.script_path.exists():
93
+ return False
94
+
95
+ # Check if we need to filter by staged files
96
+ if hook.metadata.matcher and context:
97
+ git_root = context.get("git_root")
98
+ if git_root and not self._should_trigger_for_staged_files(
99
+ git_root, hook.metadata.matcher
100
+ ):
101
+ # No matching staged files, skip silently
102
+ return True
103
+
104
+ # Execute the hook script
105
+ try:
106
+ env = os.environ.copy()
107
+ if context:
108
+ env["MONOCO_HOOK_EVENT"] = context.get("event", "")
109
+ env["MONOCO_HOOK_TYPE"] = "git"
110
+
111
+ result = subprocess.run(
112
+ [str(hook.script_path)],
113
+ cwd=hook.script_path.parent,
114
+ env=env,
115
+ capture_output=True,
116
+ text=True,
117
+ timeout=300, # 5 minute timeout
118
+ )
119
+ return result.returncode == 0
120
+ except Exception:
121
+ return False
122
+
123
+ def _should_trigger_for_staged_files(
124
+ self, git_root: Path, matchers: list[str]
125
+ ) -> bool:
126
+ """
127
+ Check if any staged files match the given glob patterns.
128
+
129
+ Args:
130
+ git_root: Path to the git repository root
131
+ matchers: List of glob patterns to match
132
+
133
+ Returns:
134
+ True if any staged file matches any pattern
135
+ """
136
+ try:
137
+ # Get staged files
138
+ result = subprocess.run(
139
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
140
+ cwd=git_root,
141
+ capture_output=True,
142
+ text=True,
143
+ timeout=30,
144
+ )
145
+
146
+ if result.returncode != 0:
147
+ return True # If we can't get staged files, allow execution
148
+
149
+ staged_files = result.stdout.strip().split("\n")
150
+ staged_files = [f for f in staged_files if f]
151
+
152
+ if not staged_files:
153
+ return False # No staged files, don't trigger
154
+
155
+ # Check if any staged file matches any pattern
156
+ for file_path in staged_files:
157
+ for pattern in matchers:
158
+ if fnmatch.fnmatch(file_path, pattern):
159
+ return True
160
+ # Also try matching against just the filename
161
+ if fnmatch.fnmatch(Path(file_path).name, pattern):
162
+ return True
163
+
164
+ return False
165
+
166
+ except Exception:
167
+ # If anything goes wrong, allow execution
168
+ return True
169
+
170
+ def install(
171
+ self,
172
+ hook: ParsedHook,
173
+ project_root: Path,
174
+ hook_id: Optional[str] = None,
175
+ ) -> bool:
176
+ """
177
+ Install a hook proxy script to `.git/hooks/`.
178
+
179
+ The proxy script calls `monoco hook run git <event>` to execute
180
+ the actual hook logic.
181
+
182
+ Args:
183
+ hook: The parsed hook to install
184
+ project_root: The project root directory
185
+ hook_id: Optional unique identifier for this hook
186
+
187
+ Returns:
188
+ True if installation succeeded
189
+ """
190
+ if not self._ensure_git_repo(project_root):
191
+ return False
192
+
193
+ event = hook.metadata.event
194
+ if not event:
195
+ return False
196
+
197
+ hook_path = self._hooks_dir / event
198
+ hook_id = hook_id or hook.script_path.stem
199
+
200
+ # Generate proxy script content
201
+ proxy_content = self._generate_proxy_script(event, hook_id, hook.metadata.matcher)
202
+
203
+ if hook_path.exists():
204
+ # Check if it's already managed by us
205
+ existing_content = hook_path.read_text(encoding="utf-8")
206
+ if self.HOOK_MARKER in existing_content:
207
+ # Already our hook, update it
208
+ hook_path.write_text(proxy_content, encoding="utf-8")
209
+ os.chmod(hook_path, 0o755)
210
+ return True
211
+ else:
212
+ # Existing hook not managed by us - backup and merge
213
+ return self._install_merged(hook_path, proxy_content, event, hook_id)
214
+ else:
215
+ # Fresh install
216
+ hook_path.write_text(proxy_content, encoding="utf-8")
217
+ os.chmod(hook_path, 0o755)
218
+ return True
219
+
220
+ def _generate_proxy_script(
221
+ self,
222
+ event: str,
223
+ hook_id: str,
224
+ matchers: Optional[list[str]] = None,
225
+ ) -> str:
226
+ """
227
+ Generate a proxy script for a git hook.
228
+
229
+ Args:
230
+ event: The git hook event (e.g., "pre-commit")
231
+ hook_id: Unique identifier for this hook
232
+ matchers: Optional list of glob patterns for file filtering
233
+
234
+ Returns:
235
+ The proxy script content
236
+ """
237
+ # Build staged files check if matchers are provided
238
+ staged_check = ""
239
+ if matchers:
240
+ patterns_str = " ".join(f'"{m}"' for m in matchers)
241
+ staged_check = f"""
242
+ # Check if staged files match patterns
243
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
244
+ if [ -z "$STAGED_FILES" ]; then
245
+ exit 0
246
+ fi
247
+
248
+ MATCHED=false
249
+ PATTERNS="{patterns_str}"
250
+ for file in $STAGED_FILES; do
251
+ for pattern in $PATTERNS; do
252
+ case "$file" in
253
+ $pattern | */$pattern)
254
+ MATCHED=true
255
+ break 2
256
+ ;;
257
+ esac
258
+ done
259
+ done
260
+
261
+ if [ "$MATCHED" = "false" ]; then
262
+ exit 0
263
+ fi
264
+ """
265
+
266
+ return f"""#!/bin/sh
267
+ # {self.HOOK_MARKER}: {hook_id}
268
+ # Auto-generated by Monoco Toolkit. Do not edit manually.
269
+
270
+ {staged_check}
271
+ # Execute Monoco hook
272
+ exec uv run python3 -m monoco hook run git {event} "$@"
273
+ """
274
+
275
+ def _install_merged(
276
+ self,
277
+ hook_path: Path,
278
+ proxy_content: str,
279
+ event: str,
280
+ hook_id: str,
281
+ ) -> bool:
282
+ """
283
+ Install by merging with an existing hook.
284
+
285
+ Creates a backup of the original and appends our proxy.
286
+
287
+ Args:
288
+ hook_path: Path to the existing hook
289
+ proxy_content: Our proxy script content
290
+ event: The git hook event
291
+ hook_id: Unique identifier for this hook
292
+
293
+ Returns:
294
+ True if installation succeeded
295
+ """
296
+ try:
297
+ # Backup original hook
298
+ backup_path = hook_path.with_suffix(self.BACKUP_SUFFIX)
299
+ shutil.copy2(hook_path, backup_path)
300
+
301
+ # Read original content
302
+ original_content = hook_path.read_text(encoding="utf-8")
303
+
304
+ # Create merged content - original runs first, then our proxy
305
+ # Extract just the execution part from our proxy (without shebang)
306
+ proxy_lines = proxy_content.split("\n")
307
+ exec_lines = []
308
+ in_staged_check = False
309
+ for line in proxy_lines:
310
+ if line.startswith("#!/"):
311
+ continue
312
+ if "exec monoco hook run" in line:
313
+ # Replace exec with direct call to allow continuation
314
+ line = line.replace("exec ", "")
315
+ exec_lines.append(line)
316
+
317
+ merged_content = f"""#!/bin/sh
318
+ # {self.HOOK_MARKER}: merged
319
+ # Original hook preserved by Monoco Toolkit
320
+
321
+ # Run original hook
322
+ ORIGINAL_EXIT=$?
323
+
324
+ # Run Monoco hook
325
+ {chr(10).join(exec_lines)}
326
+ MONOCO_EXIT=$?
327
+
328
+ # Return non-zero if either failed
329
+ if [ $ORIGINAL_EXIT -ne 0 ]; then
330
+ exit $ORIGINAL_EXIT
331
+ fi
332
+ exit $MONOCO_EXIT
333
+ """
334
+
335
+ # Insert original content before "Run original hook" comment
336
+ marker = "# Run original hook"
337
+ if marker in merged_content:
338
+ parts = merged_content.split(marker)
339
+ merged_content = parts[0] + original_content + "\n" + marker + parts[1]
340
+
341
+ hook_path.write_text(merged_content, encoding="utf-8")
342
+ os.chmod(hook_path, 0o755)
343
+ return True
344
+
345
+ except Exception:
346
+ return False
347
+
348
+ def uninstall(
349
+ self,
350
+ event: str,
351
+ project_root: Path,
352
+ hook_id: Optional[str] = None,
353
+ ) -> bool:
354
+ """
355
+ Uninstall a hook from `.git/hooks/`.
356
+
357
+ Restores the original hook if it was backed up.
358
+
359
+ Args:
360
+ event: The git hook event (e.g., "pre-commit")
361
+ project_root: The project root directory
362
+ hook_id: Optional hook identifier (for selective uninstall)
363
+
364
+ Returns:
365
+ True if uninstallation succeeded
366
+ """
367
+ if not self._ensure_git_repo(project_root):
368
+ return False
369
+
370
+ hook_path = self._hooks_dir / event
371
+ if not hook_path.exists():
372
+ return True # Already uninstalled
373
+
374
+ try:
375
+ content = hook_path.read_text(encoding="utf-8")
376
+
377
+ # Check if it's our marker
378
+ if self.HOOK_MARKER not in content:
379
+ # Not our hook, skip
380
+ return True
381
+
382
+ # Check for backup
383
+ backup_path = hook_path.with_suffix(self.BACKUP_SUFFIX)
384
+ if backup_path.exists():
385
+ # Restore original
386
+ shutil.move(backup_path, hook_path)
387
+ return True
388
+ else:
389
+ # No backup, just remove
390
+ hook_path.unlink()
391
+ return True
392
+
393
+ except Exception:
394
+ return False
395
+
396
+ def list_installed(self, project_root: Path) -> list[dict]:
397
+ """
398
+ List all Git hooks installed by Monoco.
399
+
400
+ Args:
401
+ project_root: The project root directory
402
+
403
+ Returns:
404
+ List of installed hook information
405
+ """
406
+ if not self._ensure_git_repo(project_root):
407
+ return []
408
+
409
+ installed = []
410
+
411
+ for event in GitEvent:
412
+ hook_path = self._hooks_dir / event.value
413
+ if hook_path.exists():
414
+ try:
415
+ content = hook_path.read_text(encoding="utf-8")
416
+ if self.HOOK_MARKER in content:
417
+ # Extract hook ID from marker
418
+ hook_id = None
419
+ for line in content.split("\n"):
420
+ if self.HOOK_MARKER in line:
421
+ parts = line.split(":")
422
+ if len(parts) >= 2:
423
+ hook_id = parts[1].strip()
424
+ break
425
+
426
+ installed.append({
427
+ "event": event.value,
428
+ "hook_id": hook_id,
429
+ "path": str(hook_path),
430
+ "is_merged": "merged" in content,
431
+ })
432
+ except Exception:
433
+ pass
434
+
435
+ return installed
436
+
437
+ def sync(
438
+ self,
439
+ hooks: list[ParsedHook],
440
+ project_root: Path,
441
+ ) -> dict[str, bool]:
442
+ """
443
+ Synchronize all git hooks with the repository.
444
+
445
+ Installs new hooks, updates existing ones, and removes
446
+ hooks that are no longer in the list.
447
+
448
+ Args:
449
+ hooks: List of parsed hooks to install
450
+ project_root: The project root directory
451
+
452
+ Returns:
453
+ Dictionary mapping hook events to success status
454
+ """
455
+ results = {}
456
+
457
+ if not self._ensure_git_repo(project_root):
458
+ return results
459
+
460
+ # Get current installed hooks
461
+ current = self.list_installed(project_root)
462
+ current_events = {h["event"] for h in current}
463
+
464
+ # Install/update hooks
465
+ new_events = set()
466
+ for hook in hooks:
467
+ if hook.metadata.type != HookType.GIT:
468
+ continue
469
+
470
+ event = hook.metadata.event
471
+ new_events.add(event)
472
+ results[event] = self.install(hook, project_root)
473
+
474
+ # Remove hooks that are no longer needed
475
+ for old_event in current_events - new_events:
476
+ results[old_event] = self.uninstall(old_event, project_root)
477
+
478
+ return results