htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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 (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,175 @@
1
+ """
2
+ Shared git command classification for hooks.
3
+
4
+ Provides consistent rules for which git operations are allowed vs require delegation.
5
+ Used by both validator.py and orchestrator.py to ensure consistent behavior.
6
+ """
7
+
8
+ from typing import Literal
9
+
10
+ GitCommandType = Literal["read", "write", "unknown"]
11
+
12
+ # Read-only git commands (safe to allow)
13
+ GIT_READ_ONLY = {
14
+ "status",
15
+ "log",
16
+ "diff",
17
+ "show",
18
+ "branch", # When used with -l or --list or no args
19
+ "reflog",
20
+ "ls-files",
21
+ "ls-remote",
22
+ "rev-parse",
23
+ "describe",
24
+ "tag", # When used without -a/-d or with -l
25
+ "remote", # When used with -v or show
26
+ }
27
+
28
+ # Write operations (require delegation)
29
+ GIT_WRITE_OPS = {
30
+ "add",
31
+ "commit",
32
+ "push",
33
+ "pull",
34
+ "fetch",
35
+ "merge",
36
+ "rebase",
37
+ "cherry-pick",
38
+ "reset",
39
+ "checkout", # Can modify working tree
40
+ "switch",
41
+ "restore",
42
+ "rm",
43
+ "mv",
44
+ "clean",
45
+ "stash",
46
+ }
47
+
48
+
49
+ def classify_git_command(command: str) -> GitCommandType:
50
+ """
51
+ Classify a git command as read, write, or unknown.
52
+
53
+ Args:
54
+ command: Full command string (e.g., "git status" or "git add .")
55
+
56
+ Returns:
57
+ "read", "write", or "unknown"
58
+
59
+ Examples:
60
+ >>> classify_git_command("git status")
61
+ "read"
62
+ >>> classify_git_command("git commit -m 'msg'")
63
+ "write"
64
+ >>> classify_git_command("git log --oneline")
65
+ "read"
66
+ >>> classify_git_command("git add .")
67
+ "write"
68
+ """
69
+ # Strip "git" prefix and get subcommand
70
+ parts = command.strip().split()
71
+ if not parts or parts[0] != "git":
72
+ return "unknown"
73
+
74
+ if len(parts) < 2:
75
+ return "unknown"
76
+
77
+ subcommand = parts[1]
78
+
79
+ # Check write operations first (more critical)
80
+ if subcommand in GIT_WRITE_OPS:
81
+ return "write"
82
+
83
+ # Special handling for branch (flag-based classification)
84
+ if subcommand == "branch":
85
+ # branch with -d or -D flags is write, otherwise read
86
+ if len(parts) > 2:
87
+ flags = " ".join(parts[2:])
88
+ if (
89
+ " -d " in flags
90
+ or " -D " in flags
91
+ or flags.startswith("-d ")
92
+ or flags.startswith("-D ")
93
+ ):
94
+ return "write"
95
+ return "read"
96
+
97
+ # Special handling for tag (flag-based classification)
98
+ if subcommand == "tag":
99
+ # tag with -a (annotated) or -d (delete) flags is write, otherwise read
100
+ if len(parts) > 2:
101
+ flags = " ".join(parts[2:])
102
+ if (
103
+ " -a " in flags
104
+ or " -d " in flags
105
+ or flags.startswith("-a ")
106
+ or flags.startswith("-d ")
107
+ ):
108
+ return "write"
109
+ return "read"
110
+
111
+ # Then check read-only
112
+ if subcommand in GIT_READ_ONLY:
113
+ return "read"
114
+
115
+ # Unknown git command
116
+ return "unknown"
117
+
118
+
119
+ def should_allow_git_command(command: str) -> bool:
120
+ """
121
+ Check if a git command should be allowed without delegation.
122
+
123
+ Returns:
124
+ True if command is read-only (safe), False if write (delegate)
125
+
126
+ Examples:
127
+ >>> should_allow_git_command("git status")
128
+ True
129
+ >>> should_allow_git_command("git commit -m 'msg'")
130
+ False
131
+ >>> should_allow_git_command("git diff HEAD~1")
132
+ True
133
+ >>> should_allow_git_command("git push origin main")
134
+ False
135
+ """
136
+ cmd_type = classify_git_command(command)
137
+ return cmd_type == "read"
138
+
139
+
140
+ def get_git_delegation_reason(command: str) -> str:
141
+ """
142
+ Get delegation reason for git write operations.
143
+
144
+ Args:
145
+ command: Git command that requires delegation
146
+
147
+ Returns:
148
+ Human-readable reason explaining why delegation is required
149
+ """
150
+ parts = command.strip().split()
151
+ if len(parts) < 2:
152
+ return "Git write operations should be delegated to Skill('.claude-plugin:copilot')"
153
+
154
+ subcommand = parts[1]
155
+
156
+ if subcommand in ["commit", "add", "push"]:
157
+ return (
158
+ f"Git {subcommand} is a write operation and should be delegated to "
159
+ f"Skill('.claude-plugin:copilot') for proper Git workflow management"
160
+ )
161
+ elif subcommand in ["merge", "rebase", "cherry-pick"]:
162
+ return (
163
+ f"Git {subcommand} is a complex merge operation and should be delegated to "
164
+ f"Skill('.claude-plugin:copilot') for safe execution"
165
+ )
166
+ elif subcommand in ["reset", "checkout", "restore"]:
167
+ return (
168
+ f"Git {subcommand} can modify working tree and should be delegated to "
169
+ f"Skill('.claude-plugin:copilot') for safe execution"
170
+ )
171
+ else:
172
+ return (
173
+ f"Git {subcommand} is a write operation and should be delegated to "
174
+ f"Skill('.claude-plugin:copilot')"
175
+ )
@@ -0,0 +1,12 @@
1
+ {
2
+ "enabled_hooks": [
3
+ "pre-commit",
4
+ "post-commit",
5
+ "post-checkout",
6
+ "post-merge",
7
+ "pre-push"
8
+ ],
9
+ "use_symlinks": true,
10
+ "backup_existing": true,
11
+ "chain_existing": true
12
+ }
@@ -0,0 +1,343 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ """
6
+ Git hooks installation and configuration management.
7
+ """
8
+
9
+ import json
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ class HookConfig:
16
+ """Configuration for git hooks installation."""
17
+
18
+ DEFAULT_CONFIG: dict[str, Any] = {
19
+ "enabled_hooks": [
20
+ "post-commit",
21
+ "post-checkout",
22
+ "post-merge",
23
+ "pre-push",
24
+ ],
25
+ "use_symlinks": True,
26
+ "backup_existing": True,
27
+ "chain_existing": True,
28
+ }
29
+
30
+ def __init__(self, config_path: Path | None = None):
31
+ """
32
+ Initialize hook configuration.
33
+
34
+ Args:
35
+ config_path: Path to hooks-config.json (defaults to .htmlgraph/hooks-config.json)
36
+ """
37
+ self.config_path = config_path
38
+ # Deep copy to avoid mutating DEFAULT_CONFIG
39
+ self.config = {
40
+ "enabled_hooks": self.DEFAULT_CONFIG["enabled_hooks"].copy(),
41
+ "use_symlinks": self.DEFAULT_CONFIG["use_symlinks"],
42
+ "backup_existing": self.DEFAULT_CONFIG["backup_existing"],
43
+ "chain_existing": self.DEFAULT_CONFIG["chain_existing"],
44
+ }
45
+
46
+ if config_path and config_path.exists():
47
+ self.load()
48
+
49
+ def load(self) -> None:
50
+ """Load configuration from file."""
51
+ if not self.config_path or not self.config_path.exists():
52
+ return
53
+
54
+ try:
55
+ with open(self.config_path, encoding="utf-8") as f:
56
+ user_config = json.load(f)
57
+ self.config.update(user_config)
58
+ except Exception as e:
59
+ logger.info(f"Warning: Failed to load hook config: {e}")
60
+
61
+ def save(self) -> None:
62
+ """Save configuration to file."""
63
+ if not self.config_path:
64
+ return
65
+
66
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
67
+ with open(self.config_path, "w", encoding="utf-8") as f:
68
+ json.dump(self.config, f, indent=2)
69
+
70
+ def is_hook_enabled(self, hook_name: str) -> bool:
71
+ """Check if a hook is enabled."""
72
+ return hook_name in self.config.get("enabled_hooks", [])
73
+
74
+ def enable_hook(self, hook_name: str) -> None:
75
+ """Enable a specific hook."""
76
+ enabled = self.config.get("enabled_hooks", [])
77
+ if hook_name not in enabled:
78
+ enabled.append(hook_name)
79
+ self.config["enabled_hooks"] = enabled
80
+
81
+ def disable_hook(self, hook_name: str) -> None:
82
+ """Disable a specific hook."""
83
+ enabled = self.config.get("enabled_hooks", [])
84
+ if hook_name in enabled:
85
+ enabled.remove(hook_name)
86
+ self.config["enabled_hooks"] = enabled
87
+
88
+
89
+ class HookInstaller:
90
+ """Handles installation of git hooks."""
91
+
92
+ def __init__(self, project_dir: Path, config: HookConfig | None = None):
93
+ """
94
+ Initialize hook installer.
95
+
96
+ Args:
97
+ project_dir: Project root directory
98
+ config: Hook configuration (creates default if not provided)
99
+ """
100
+ self.project_dir = Path(project_dir).resolve()
101
+ self.git_dir = self.project_dir / ".git"
102
+ self.htmlgraph_dir = self.project_dir / ".htmlgraph"
103
+ self.hooks_source_dir = Path(__file__).parent
104
+
105
+ # Load or create config
106
+ config_path = self.htmlgraph_dir / "hooks-config.json"
107
+ self.config = config or HookConfig(config_path)
108
+
109
+ def validate_environment(self) -> tuple[bool, str]:
110
+ """
111
+ Validate that the environment is ready for hook installation.
112
+
113
+ Returns:
114
+ Tuple of (is_valid, error_message)
115
+ """
116
+ if not self.git_dir.exists():
117
+ return False, "Not a git repository (no .git directory found)"
118
+
119
+ if not self.htmlgraph_dir.exists():
120
+ return False, "HtmlGraph not initialized (run 'htmlgraph init' first)"
121
+
122
+ git_hooks_dir = self.git_dir / "hooks"
123
+ if not git_hooks_dir.exists():
124
+ try:
125
+ git_hooks_dir.mkdir(parents=True)
126
+ except Exception as e:
127
+ return False, f"Cannot create .git/hooks directory: {e}"
128
+
129
+ return True, ""
130
+
131
+ def install_hook(
132
+ self, hook_name: str, force: bool = False, dry_run: bool = False
133
+ ) -> tuple[bool, str]:
134
+ """
135
+ Install a single git hook.
136
+
137
+ Args:
138
+ hook_name: Name of the hook (e.g., "pre-commit")
139
+ force: Force installation even if hook exists
140
+ dry_run: Show what would be done without doing it
141
+
142
+ Returns:
143
+ Tuple of (success, message)
144
+ """
145
+ # Check if hook is enabled
146
+ if not self.config.is_hook_enabled(hook_name):
147
+ return False, f"Hook '{hook_name}' is disabled in configuration"
148
+
149
+ # Source hook file
150
+ hook_source = self.hooks_source_dir / f"{hook_name}.sh"
151
+ if not hook_source.exists():
152
+ return False, f"Hook template not found: {hook_source}"
153
+
154
+ # Destination in .htmlgraph/hooks/
155
+ versioned_hooks_dir = self.htmlgraph_dir / "hooks"
156
+ versioned_hooks_dir.mkdir(exist_ok=True)
157
+ hook_dest = versioned_hooks_dir / f"{hook_name}.sh"
158
+
159
+ # Git hooks directory
160
+ git_hooks_dir = self.git_dir / "hooks"
161
+ git_hook_path = git_hooks_dir / hook_name
162
+
163
+ if dry_run:
164
+ msg = f"[DRY RUN] Would install {hook_name}:\n"
165
+ msg += f" Source: {hook_source}\n"
166
+ msg += f" Versioned: {hook_dest}\n"
167
+ msg += f" Git hook: {git_hook_path}"
168
+ return True, msg
169
+
170
+ # Copy hook to .htmlgraph/hooks/ (versioned)
171
+ try:
172
+ shutil.copy(hook_source, hook_dest)
173
+ hook_dest.chmod(0o755)
174
+ except Exception as e:
175
+ return False, f"Failed to copy hook to {hook_dest}: {e}"
176
+
177
+ # Handle existing git hook
178
+ if git_hook_path.exists() and not force:
179
+ if self.config.config.get("backup_existing", True):
180
+ backup_path = git_hook_path.with_suffix(".backup")
181
+ if not backup_path.exists():
182
+ shutil.copy(git_hook_path, backup_path)
183
+
184
+ if self.config.config.get("chain_existing", True):
185
+ # Create chained hook
186
+ return self._create_chained_hook(
187
+ hook_name, hook_dest, git_hook_path, backup_path
188
+ )
189
+ else:
190
+ return False, (
191
+ f"Hook {hook_name} already exists. "
192
+ f"Backed up to {backup_path}. "
193
+ f"Use --force to overwrite."
194
+ )
195
+
196
+ # Install hook (symlink or copy)
197
+ try:
198
+ if self.config.config.get("use_symlinks", True):
199
+ # Remove existing symlink if present
200
+ if git_hook_path.is_symlink():
201
+ git_hook_path.unlink()
202
+
203
+ git_hook_path.symlink_to(hook_dest.resolve())
204
+ return (
205
+ True,
206
+ f"Installed {hook_name} (symlink): {git_hook_path} -> {hook_dest}",
207
+ )
208
+ else:
209
+ shutil.copy(hook_dest, git_hook_path)
210
+ git_hook_path.chmod(0o755)
211
+ return True, f"Installed {hook_name} (copy): {git_hook_path}"
212
+ except Exception as e:
213
+ return False, f"Failed to install {hook_name}: {e}"
214
+
215
+ def _create_chained_hook(
216
+ self, hook_name: str, htmlgraph_hook: Path, git_hook: Path, backup_hook: Path
217
+ ) -> tuple[bool, str]:
218
+ """Create a chained hook that runs both existing and HtmlGraph hooks."""
219
+ chain_content = f'''#!/bin/bash
220
+ # Chained hook - runs existing hook then HtmlGraph hook
221
+
222
+ # Run existing hook
223
+ if [ -f "{backup_hook}" ]; then
224
+ "{backup_hook}" || exit $?
225
+ fi
226
+
227
+ # Run HtmlGraph hook
228
+ if [ -f "{htmlgraph_hook}" ]; then
229
+ "{htmlgraph_hook}" || true
230
+ fi
231
+ '''
232
+
233
+ try:
234
+ git_hook.write_text(chain_content, encoding="utf-8")
235
+ git_hook.chmod(0o755)
236
+ return True, (
237
+ f"Installed {hook_name} (chained):\n"
238
+ f" Backed up existing: {backup_hook}\n"
239
+ f" Installed wrapper: {git_hook}"
240
+ )
241
+ except Exception as e:
242
+ return False, f"Failed to create chained hook: {e}"
243
+
244
+ def install_all_hooks(
245
+ self, force: bool = False, dry_run: bool = False
246
+ ) -> dict[str, tuple[bool, str]]:
247
+ """
248
+ Install all enabled hooks.
249
+
250
+ Args:
251
+ force: Force installation even if hooks exist
252
+ dry_run: Show what would be done without doing it
253
+
254
+ Returns:
255
+ Dictionary mapping hook names to (success, message) tuples
256
+ """
257
+ results = {}
258
+
259
+ for hook_name in self.config.config.get("enabled_hooks", []):
260
+ success, message = self.install_hook(
261
+ hook_name, force=force, dry_run=dry_run
262
+ )
263
+ results[hook_name] = (success, message)
264
+
265
+ return results
266
+
267
+ def uninstall_hook(self, hook_name: str) -> tuple[bool, str]:
268
+ """
269
+ Uninstall a git hook.
270
+
271
+ Args:
272
+ hook_name: Name of the hook to uninstall
273
+
274
+ Returns:
275
+ Tuple of (success, message)
276
+ """
277
+ git_hook_path = self.git_dir / "hooks" / hook_name
278
+
279
+ if not git_hook_path.exists():
280
+ return False, f"Hook {hook_name} is not installed"
281
+
282
+ # Check if it's a symlink to our hook
283
+ versioned_hook = self.htmlgraph_dir / "hooks" / f"{hook_name}.sh"
284
+
285
+ try:
286
+ if git_hook_path.is_symlink():
287
+ target = git_hook_path.resolve()
288
+ if target == versioned_hook.resolve():
289
+ git_hook_path.unlink()
290
+ return True, f"Uninstalled {hook_name} (symlink removed)"
291
+ else:
292
+ return False, f"Hook {hook_name} points to {target}, not our hook"
293
+ else:
294
+ # Not a symlink - check for backup
295
+ backup_path = git_hook_path.with_suffix(".backup")
296
+ if backup_path.exists():
297
+ git_hook_path.unlink()
298
+ shutil.move(backup_path, git_hook_path)
299
+ return True, f"Uninstalled {hook_name} (restored backup)"
300
+ else:
301
+ return False, (
302
+ f"Hook {hook_name} exists but no backup found. "
303
+ f"Manual removal required."
304
+ )
305
+ except Exception as e:
306
+ return False, f"Failed to uninstall {hook_name}: {e}"
307
+
308
+ def list_hooks(self) -> dict[str, dict[str, Any]]:
309
+ """
310
+ List all hooks and their installation status.
311
+
312
+ Returns:
313
+ Dictionary mapping hook names to status info
314
+ """
315
+ from . import AVAILABLE_HOOKS
316
+
317
+ status = {}
318
+
319
+ for hook_name in AVAILABLE_HOOKS:
320
+ git_hook_path = self.git_dir / "hooks" / hook_name
321
+ versioned_hook = self.htmlgraph_dir / "hooks" / f"{hook_name}.sh"
322
+
323
+ info: dict[str, Any] = {
324
+ "enabled": self.config.is_hook_enabled(hook_name),
325
+ "installed": git_hook_path.exists(),
326
+ "versioned": versioned_hook.exists(),
327
+ "is_symlink": git_hook_path.is_symlink()
328
+ if git_hook_path.exists()
329
+ else False,
330
+ }
331
+
332
+ if info["is_symlink"]:
333
+ try:
334
+ target = git_hook_path.resolve()
335
+ info["symlink_target"] = str(target)
336
+ info["our_hook"] = target == versioned_hook.resolve()
337
+ except Exception:
338
+ info["symlink_target"] = "unknown"
339
+ info["our_hook"] = False
340
+
341
+ status[hook_name] = info
342
+
343
+ return status