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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {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,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
|