gator-command 1.2.2__tar.gz → 1.3.0__tar.gz

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 (142) hide show
  1. {gator_command-1.2.2/src/gator_command.egg-info → gator_command-1.3.0}/PKG-INFO +1 -1
  2. {gator_command-1.2.2 → gator_command-1.3.0}/pyproject.toml +1 -1
  3. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-deploy.py +1 -0
  4. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-session-aggregator.py +79 -26
  5. {gator_command-1.2.2/src/gator_command/templates/gator-starter → gator_command-1.3.0/src/gator_command}/scripts/gator-update.py +200 -103
  6. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gatorize.py +104 -20
  7. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-pre-commit.py +57 -7
  8. gator_command-1.3.0/src/gator_command/templates/gator-starter/scripts/gator-session-start.py +273 -0
  9. gator_command-1.3.0/src/gator_command/templates/gator-starter/vendor-hooks/claude-settings.json +15 -0
  10. gator_command-1.3.0/src/gator_command/templates/gator-starter/vendor-hooks/codex-hooks.json +15 -0
  11. gator_command-1.3.0/src/gator_command/templates/gator-starter/vendor-hooks/gemini-settings.json +15 -0
  12. {gator_command-1.2.2 → gator_command-1.3.0/src/gator_command.egg-info}/PKG-INFO +1 -1
  13. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command.egg-info/SOURCES.txt +7 -1
  14. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_pre_commit.py +135 -0
  15. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_session_aggregator.py +105 -0
  16. gator_command-1.3.0/tests/test_session_start.py +360 -0
  17. gator_command-1.3.0/tests/test_vendor_hooks.py +292 -0
  18. {gator_command-1.2.2 → gator_command-1.3.0}/LICENSE +0 -0
  19. {gator_command-1.2.2 → gator_command-1.3.0}/PYPI_README.md +0 -0
  20. {gator_command-1.2.2 → gator_command-1.3.0}/README.md +0 -0
  21. {gator_command-1.2.2 → gator_command-1.3.0}/setup.cfg +0 -0
  22. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/__init__.py +0 -0
  23. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/cli.py +0 -0
  24. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/crawler.py +0 -0
  25. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/dashboard.css +0 -0
  26. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/dashboard.html +0 -0
  27. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/dashboard.js +0 -0
  28. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/views/audit.js +0 -0
  29. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/views/fleet.js +0 -0
  30. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/views/repo.js +0 -0
  31. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/views/settings.js +0 -0
  32. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/dashboard/views/updates.js +0 -0
  33. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/enforcer-prompt.md +0 -0
  34. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/extract-claude-sessions.py +0 -0
  35. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/extract-codex-sessions.py +0 -0
  36. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/extract-gemini-sessions.py +0 -0
  37. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-audit-renderers.py +0 -0
  38. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-audit.py +0 -0
  39. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-charter-draft.py +0 -0
  40. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-charter-lint.py +0 -0
  41. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-charter-verify.py +0 -0
  42. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-dashboard.py +0 -0
  43. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-drift.py +0 -0
  44. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-enforce.py +0 -0
  45. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-fleet-intel.py +0 -0
  46. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-fleet-report.py +0 -0
  47. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-init-command-post.py +0 -0
  48. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-init.py +0 -0
  49. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-machine-id.py +0 -0
  50. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-policy-status.py +0 -0
  51. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-pulse.py +0 -0
  52. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-repo-status.py +0 -0
  53. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-session-common.py +0 -0
  54. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-session-sink.py +0 -0
  55. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-sessions.py +0 -0
  56. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator-version.py +0 -0
  57. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator_core.py +0 -0
  58. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator_remote.py +0 -0
  59. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gator_runtime.py +0 -0
  60. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gatorize-actions.sh +0 -0
  61. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gatorize-lib.sh +0 -0
  62. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gatorize-post.sh +0 -0
  63. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/gatorize.sh +0 -0
  64. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/generate_markdown.py +0 -0
  65. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/generate_wiki.py +0 -0
  66. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/graph_health.py +0 -0
  67. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/memex-lint.py +0 -0
  68. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/memex-lint.sh +0 -0
  69. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/memex.py +0 -0
  70. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/memex_formatters.py +0 -0
  71. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/memex_state.py +0 -0
  72. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/scripts/spawn.py +0 -0
  73. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/blueprints/README.md +0 -0
  74. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/charterignore +0 -0
  75. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/charters/README.md +0 -0
  76. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/charters/_template.md +0 -0
  77. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/commands/commit.md +0 -0
  78. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/commands/init.md +0 -0
  79. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/commands/update.md +0 -0
  80. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/constitution.md +0 -0
  81. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/field-guides/README.md +0 -0
  82. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/gator-start-up.md +0 -0
  83. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/procedures/charter-alignment.md +0 -0
  84. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/procedures/enforcer-review.md +0 -0
  85. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/procedures/field-guide-generation.md +0 -0
  86. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/procedures/knowledge-capture.md +0 -0
  87. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/procedures/significance-check.md +0 -0
  88. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/concierge-responses.md +0 -0
  89. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +0 -0
  90. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +0 -0
  91. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +0 -0
  92. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/example-project.md +0 -0
  93. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +0 -0
  94. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/git-workflow.md +0 -0
  95. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +0 -0
  96. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/refactor-approach.md +0 -0
  97. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +0 -0
  98. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +0 -0
  99. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +0 -0
  100. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  101. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  102. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  103. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  104. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  105. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/enforcer-prompt.md +0 -0
  106. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/enforcer-review.py +0 -0
  107. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-approve.py +0 -0
  108. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-enforce.py +0 -0
  109. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-init.py +0 -0
  110. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-pulse.py +0 -0
  111. {gator_command-1.2.2/src/gator_command → gator_command-1.3.0/src/gator_command/templates/gator-starter}/scripts/gator-update.py +0 -0
  112. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator-version.py +0 -0
  113. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/gator_core.py +0 -0
  114. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  115. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  116. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  117. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/commit-msg +0 -0
  118. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/post-commit +0 -0
  119. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/scripts/hooks/pre-commit +0 -0
  120. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/sessions/.gitignore +0 -0
  121. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  122. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command/templates/gator-starter/whiteboard.md +0 -0
  123. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command.egg-info/dependency_links.txt +0 -0
  124. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command.egg-info/entry_points.txt +0 -0
  125. {gator_command-1.2.2 → gator_command-1.3.0}/src/gator_command.egg-info/top_level.txt +0 -0
  126. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_audit.py +0 -0
  127. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_audit_integration.py +0 -0
  128. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_charter_lint.py +0 -0
  129. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_dashboard_sessions.py +0 -0
  130. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_deploy.py +0 -0
  131. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_drift.py +0 -0
  132. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_fleet_intel.py +0 -0
  133. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_fleet_report.py +0 -0
  134. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_gator_core.py +0 -0
  135. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_gatorize.py +0 -0
  136. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_hooks.py +0 -0
  137. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_init.py +0 -0
  138. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_memex_state.py +0 -0
  139. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_packaging.py +0 -0
  140. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_remote.py +0 -0
  141. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_session_sink.py +0 -0
  142. {gator_command-1.2.2 → gator_command-1.3.0}/tests/test_sessions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gator-command
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Git-native governance for AI-assisted engineering
5
5
  Author-email: Cumberland Laboratories <support@cumberlandlaboratories.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gator-command"
7
- version = "1.2.2"
7
+ version = "1.3.0"
8
8
  description = "Git-native governance for AI-assisted engineering"
9
9
  readme = "PYPI_README.md"
10
10
  license = {text = "MIT"}
@@ -160,6 +160,7 @@ ROOT_GITIGNORE = (
160
160
  "\n"
161
161
  "# Machine-local state (not portable)\n"
162
162
  ".gator/product-source.json\n"
163
+ ".gator/active-vendor-session.json\n"
163
164
  )
164
165
 
165
166
 
@@ -8,7 +8,7 @@ entry point. Dashboard and CLI scripts import this.
8
8
 
9
9
  @reads: .gator/session-snippets/*.json (per repo)
10
10
  @reads: ~/.gator/dashboard-repos.json (fleet queries)
11
- @writes: ~/.gator/sessions/<path-hash>/<session_id>.json (cached summaries)
11
+ @writes: ~/.gator/sessions/<path-hash>/<esk-hash>.json (cached summaries)
12
12
  @writes: ~/.gator/sessions/<path-hash>/_repo.json (repo metadata)
13
13
  """
14
14
 
@@ -33,6 +33,28 @@ SIGNIFICANCE_ORDER = {"routine": 0, "minor": 1, "high": 2}
33
33
  SKIP_CHANGE_TYPES_FOR_GOAL = {"release", "merge", "cleanup"}
34
34
 
35
35
 
36
+ # ---------------------------------------------------------------------------
37
+ # Session Identity
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def effective_session_key(snippet):
41
+ """Return the canonical grouping key for a snippet.
42
+
43
+ Uses vendor session identity when available, falls back to legacy
44
+ Gator session_id. The returned string is used for aggregation grouping,
45
+ cache filenames, and fingerprint lookups.
46
+
47
+ Returns:
48
+ "group:<repo>:<session_group_key>" when session_group_key is truthy
49
+ "legacy:<repo>:<session_id>" otherwise
50
+ """
51
+ repo = snippet.get("repo", "")
52
+ sgk = snippet.get("session_group_key")
53
+ if sgk:
54
+ return f"group:{repo}:{sgk}"
55
+ return f"legacy:{repo}:{snippet.get('session_id', '')}"
56
+
57
+
36
58
  # ---------------------------------------------------------------------------
37
59
  # 1a. Snippet Reader
38
60
  # ---------------------------------------------------------------------------
@@ -75,7 +97,11 @@ def read_snippets(repo_path):
75
97
  # ---------------------------------------------------------------------------
76
98
 
77
99
  def aggregate_sessions(snippets, repo_path):
78
- """Group snippets by (repo, session_id) and aggregate into summaries.
100
+ """Group snippets by effective_session_key() and aggregate into summaries.
101
+
102
+ Uses vendor session_group_key when present, falls back to legacy
103
+ Gator session_id. Snippets with different effective keys are never
104
+ merged, even if timestamps or transcript IDs look similar.
79
105
 
80
106
  Args:
81
107
  snippets: list of parsed snippet dicts (the "data" from SnippetRecords)
@@ -83,10 +109,10 @@ def aggregate_sessions(snippets, repo_path):
83
109
 
84
110
  Returns list of summary dicts matching gator-session-summary-v1 schema.
85
111
  """
86
- # Group by (repo, session_id)
112
+ # Group by effective_session_key
87
113
  groups = {}
88
114
  for s in snippets:
89
- key = (s.get("repo", ""), s.get("session_id", ""))
115
+ key = effective_session_key(s)
90
116
  if key not in groups:
91
117
  groups[key] = []
92
118
  groups[key].append(s)
@@ -94,7 +120,7 @@ def aggregate_sessions(snippets, repo_path):
94
120
  repo_key = session_cache_key(repo_path)
95
121
  summaries = []
96
122
 
97
- for (repo, session_id), group in groups.items():
123
+ for esk, group in groups.items():
98
124
  # Sort within group
99
125
  group.sort(key=lambda s: (
100
126
  s.get("started_at", ""),
@@ -110,6 +136,7 @@ def aggregate_sessions(snippets, repo_path):
110
136
  all_intents = []
111
137
  all_notes = []
112
138
  all_change_types = set()
139
+ all_models = set()
113
140
  max_sig = "routine"
114
141
  architect = ""
115
142
  transcript_session_id = None
@@ -150,6 +177,11 @@ def aggregate_sessions(snippets, repo_path):
150
177
  if ct:
151
178
  all_change_types.add(ct)
152
179
 
180
+ # Models — distinct
181
+ model = s.get("model_inferred", "")
182
+ if model:
183
+ all_models.add(model)
184
+
153
185
  # Max significance
154
186
  sig = s.get("significance", "routine")
155
187
  if SIGNIFICANCE_ORDER.get(sig, 0) > SIGNIFICANCE_ORDER.get(max_sig, 0):
@@ -165,14 +197,24 @@ def aggregate_sessions(snippets, repo_path):
165
197
  if not transcript_ref:
166
198
  transcript_ref = s.get("transcript_ref")
167
199
 
200
+ # session_group_key: first non-null from the group (all should agree)
201
+ session_group_key = None
202
+ for s in group:
203
+ sgk = s.get("session_group_key")
204
+ if sgk:
205
+ session_group_key = sgk
206
+ break
207
+
168
208
  summary = {
169
209
  "schema": "gator-session-summary-v1",
170
- "session_id": session_id,
171
- "repo": repo,
210
+ "session_id": first.get("session_id", ""),
211
+ "session_group_key": session_group_key,
212
+ "repo": first.get("repo", ""),
172
213
  "repo_key": repo_key,
173
214
  "branch": first.get("branch", ""),
174
215
  "vendor": first.get("vendor_inferred", ""),
175
216
  "model": first.get("model_inferred", ""),
217
+ "models": sorted(all_models),
176
218
  "agent": first.get("agent", ""),
177
219
  "architect": architect,
178
220
  "started_at": min(s.get("started_at", "") for s in group),
@@ -273,21 +315,32 @@ def write_repo_metadata(cache_path, repo_name, repo_path):
273
315
  _atomic_write(meta_path, json.dumps(meta, indent=2) + "\n")
274
316
 
275
317
 
276
- def _summary_cache_filename(repo, session_id):
277
- """Build a cache filename from the (repo, session_id) compound key.
318
+ def _esk_from_summary(summary):
319
+ """Reconstruct the effective session key from a summary dict.
320
+
321
+ Mirrors effective_session_key() but reads from summary fields
322
+ instead of snippet fields.
323
+ """
324
+ repo = summary.get("repo", "")
325
+ sgk = summary.get("session_group_key")
326
+ if sgk:
327
+ return f"group:{repo}:{sgk}"
328
+ return f"legacy:{repo}:{summary.get('session_id', '')}"
329
+
330
+
331
+ def _summary_cache_filename(esk):
332
+ """Build a cache filename from the effective session key.
278
333
 
279
- Uses a short hash prefix of the repo name to avoid filename collisions
280
- when two repos share a session_id in the same cache directory.
334
+ Hashes the full effective_session_key string to produce a stable,
335
+ filesystem-safe filename. The key already encodes repo + identity.
281
336
  """
282
- if repo:
283
- repo_prefix = hashlib.sha256(repo.encode()).hexdigest()[:6]
284
- return f"{repo_prefix}-{session_id}.json"
285
- return f"{session_id}.json"
337
+ h = hashlib.sha256(esk.encode()).hexdigest()[:16]
338
+ return f"{h}.json"
286
339
 
287
340
 
288
- def read_cached_summary(cache_path, repo, session_id):
341
+ def read_cached_summary(cache_path, esk):
289
342
  """Read a cached session summary, or return None."""
290
- summary_path = cache_path / _summary_cache_filename(repo, session_id)
343
+ summary_path = cache_path / _summary_cache_filename(esk)
291
344
  if not summary_path.exists():
292
345
  return None
293
346
  try:
@@ -296,10 +349,10 @@ def read_cached_summary(cache_path, repo, session_id):
296
349
  return None
297
350
 
298
351
 
299
- def write_cached_summary(cache_path, summary):
352
+ def write_cached_summary(cache_path, summary, esk):
300
353
  """Write a session summary to cache."""
301
354
  cache_path.mkdir(parents=True, exist_ok=True)
302
- filename = _summary_cache_filename(summary.get("repo", ""), summary["session_id"])
355
+ filename = _summary_cache_filename(esk)
303
356
  summary_path = cache_path / filename
304
357
  _atomic_write(summary_path, json.dumps(summary, indent=2) + "\n")
305
358
 
@@ -349,10 +402,10 @@ def get_session_summaries(repo_path, force_refresh=False):
349
402
  snippets_data = [r["data"] for r in records]
350
403
  sessions = aggregate_sessions(snippets_data, repo_path)
351
404
 
352
- # Build a lookup: (repo, session_id) -> list of records (for fingerprinting)
405
+ # Build a lookup: effective_session_key -> list of records (for fingerprinting)
353
406
  session_records = {}
354
407
  for r in records:
355
- key = (r["data"].get("repo", ""), r["data"].get("session_id", ""))
408
+ key = effective_session_key(r["data"])
356
409
  if key not in session_records:
357
410
  session_records[key] = []
358
411
  session_records[key].append(r)
@@ -362,13 +415,13 @@ def get_session_summaries(repo_path, force_refresh=False):
362
415
  wrote_any = False
363
416
 
364
417
  for summary in sessions:
365
- repo = summary["repo"]
366
- sid = summary["session_id"]
367
- recs = session_records.get((repo, sid), [])
418
+ # Reconstruct the effective key from the summary's first snippet identity
419
+ esk = _esk_from_summary(summary)
420
+ recs = session_records.get(esk, [])
368
421
  fp = snippet_fingerprint(recs)
369
422
 
370
423
  if not force_refresh:
371
- cached = read_cached_summary(cpath, repo, sid)
424
+ cached = read_cached_summary(cpath, esk)
372
425
  if cached and is_fresh(cached, fp):
373
426
  results.append(cached)
374
427
  continue
@@ -376,7 +429,7 @@ def get_session_summaries(repo_path, force_refresh=False):
376
429
  # Generate and cache
377
430
  summary["generated_at"] = datetime.now(timezone.utc).isoformat()
378
431
  summary["snippet_fingerprint"] = fp
379
- write_cached_summary(cpath, summary)
432
+ write_cached_summary(cpath, summary, esk)
380
433
  results.append(summary)
381
434
  wrote_any = True
382
435
 
@@ -237,6 +237,98 @@ def bump_policy_version(gator_dir, command_post):
237
237
  return policy_date, changed
238
238
 
239
239
 
240
+ def merge_hooks_into_settings(settings_path, hooks_template_path):
241
+ """Merge Gator hooks into a vendor settings JSON file without clobbering.
242
+
243
+ If the settings file doesn't exist, writes the hooks template as-is.
244
+ If it exists, deep-merges the hooks block: for each hook event,
245
+ appends Gator hook groups that aren't already present (identified by
246
+ the gator-session-start.py command string).
247
+
248
+ Returns: 'add', 'update', or 'unchanged'.
249
+ """
250
+ GATOR_HOOK_MARKER = "gator-session-start.py"
251
+
252
+ hooks_template = json.loads(hooks_template_path.read_text(encoding="utf-8"))
253
+ template_hooks = hooks_template.get("hooks", {})
254
+
255
+ if not settings_path.exists():
256
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
257
+ settings_path.write_text(
258
+ json.dumps(hooks_template, indent=2) + "\n", encoding="utf-8"
259
+ )
260
+ return "add"
261
+
262
+ # Read existing settings
263
+ try:
264
+ existing = json.loads(settings_path.read_text(encoding="utf-8"))
265
+ except (json.JSONDecodeError, ValueError):
266
+ # Corrupt file — don't clobber, skip
267
+ return "unchanged"
268
+
269
+ if not isinstance(existing, dict):
270
+ return "unchanged"
271
+
272
+ # Validate hooks shape — bail if it's not what we expect
273
+ existing_hooks = existing.get("hooks", {})
274
+ if not isinstance(existing_hooks, dict):
275
+ # hooks is present but wrong type (e.g., a list) — don't clobber
276
+ return "unchanged"
277
+
278
+ needs_update = False
279
+
280
+ for event_name, groups in template_hooks.items():
281
+ if event_name not in existing_hooks:
282
+ existing_hooks[event_name] = groups
283
+ needs_update = True
284
+ else:
285
+ event_value = existing_hooks[event_name]
286
+ if not isinstance(event_value, list):
287
+ # Event value is wrong shape — skip this event, don't corrupt
288
+ continue
289
+ existing_json = json.dumps(event_value)
290
+ if GATOR_HOOK_MARKER not in existing_json:
291
+ event_value.extend(groups)
292
+ needs_update = True
293
+
294
+ if not needs_update:
295
+ return "unchanged"
296
+
297
+ existing["hooks"] = existing_hooks
298
+ settings_path.write_text(
299
+ json.dumps(existing, indent=2) + "\n", encoding="utf-8"
300
+ )
301
+ return "update"
302
+
303
+
304
+ # Vendor hook config: (template_file, target_dir, target_filename)
305
+ VENDOR_HOOK_CONFIGS = [
306
+ ("vendor-hooks/claude-settings.json", ".claude", "settings.json"),
307
+ ("vendor-hooks/codex-hooks.json", ".codex", "hooks.json"),
308
+ ("vendor-hooks/gemini-settings.json", ".gemini", "settings.json"),
309
+ ]
310
+
311
+
312
+ def install_vendor_hooks(templates_dir, repo_root):
313
+ """Install vendor SessionStart hook configs into the repo.
314
+
315
+ Merge-safe: injects Gator hooks into existing settings files
316
+ without clobbering user content (permissions, env vars, other hooks).
317
+
318
+ Returns count of files added or updated.
319
+ """
320
+ changed = 0
321
+ for template_file, target_dir, target_name in VENDOR_HOOK_CONFIGS:
322
+ src = templates_dir / template_file
323
+ if not src.exists():
324
+ continue
325
+ dest = repo_root / target_dir / target_name
326
+ result = merge_hooks_into_settings(dest, src)
327
+ if result in ("add", "update"):
328
+ changed += 1
329
+ return changed
330
+
331
+
240
332
  def execute_updates(plan):
241
333
  """Execute the update plan — copy files that need adding or updating.
242
334
 
@@ -264,18 +356,18 @@ def execute_updates(plan):
264
356
  def plan_hook_updates(gator_dir, repo_root):
265
357
  """Plan git hook installations. Returns list of (hook_name, action).
266
358
 
267
- Compares the generated cross-platform Python wrappers to the current
268
- managed hook dir contents. This matches what install_git_hooks()
269
- actually writes, so the plan is accurate on both Unix and Windows.
270
- """
271
- git_hooks = get_managed_hook_dir(repo_root)
359
+ Compares the generated cross-platform Python wrappers to the current
360
+ managed hook dir contents. This matches what install_git_hooks()
361
+ actually writes, so the plan is accurate on both Unix and Windows.
362
+ """
363
+ git_hooks = get_managed_hook_dir(repo_root)
272
364
  gator_script = gator_dir / "scripts" / "gator-pre-commit.py"
273
365
 
274
366
  if not git_hooks.parent.is_dir() or not gator_script.exists():
275
367
  return []
276
368
 
277
- expected_hooks = build_git_hook_wrappers()
278
- config_needs_update = hooks_config_needs_update(repo_root)
369
+ expected_hooks = build_git_hook_wrappers()
370
+ config_needs_update = hooks_config_needs_update(repo_root)
279
371
  results = []
280
372
  for hook_name, expected_content in sorted(expected_hooks.items()):
281
373
  dest = git_hooks / hook_name
@@ -283,70 +375,70 @@ def plan_hook_updates(gator_dir, repo_root):
283
375
  results.append((hook_name, "add"))
284
376
  continue
285
377
  actual = dest.read_text(encoding="utf-8", errors="replace")
286
- if actual != expected_content or config_needs_update:
287
- results.append((hook_name, "update"))
288
- else:
289
- results.append((hook_name, "unchanged"))
378
+ if actual != expected_content or config_needs_update:
379
+ results.append((hook_name, "update"))
380
+ else:
381
+ results.append((hook_name, "unchanged"))
290
382
 
291
383
  return results
292
384
 
293
385
 
294
- def _hook_shebang():
295
- """Return the platform-correct shebang for hook wrappers."""
296
- return "#!C:/Windows/py.exe -3" if os.name == "nt" else "#!/usr/bin/env python3"
297
-
298
-
299
- def get_managed_hooks_path_value():
300
- """Return the core.hooksPath value Gator manages on this platform."""
301
- return ".git/gator-hooks" if os.name == "nt" else None
302
-
303
-
304
- def get_managed_hook_dir(repo_root):
305
- """Return the hook directory Gator actively manages."""
306
- hooks_path = get_managed_hooks_path_value()
307
- if hooks_path:
308
- return repo_root / Path(hooks_path)
309
- return repo_root / ".git" / "hooks"
310
-
311
-
312
- def get_hook_probe_dirs(repo_root):
313
- """Return installed-hook directories to probe, managed dir first."""
314
- probe_dirs = [get_managed_hook_dir(repo_root)]
315
- legacy = repo_root / ".git" / "hooks"
316
- if legacy not in probe_dirs:
317
- probe_dirs.append(legacy)
318
- return probe_dirs
319
-
320
-
321
- def hooks_config_needs_update(repo_root):
322
- """Return True when core.hooksPath is missing or wrong."""
323
- expected = get_managed_hooks_path_value()
324
- if not expected:
325
- return False
326
- actual, ok = git("config", "--local", "--get", "core.hooksPath", cwd=repo_root)
327
- return not ok or actual.strip() != expected
328
-
329
-
330
- def _configure_managed_hooks_path(repo_root):
331
- """Set core.hooksPath when this platform uses a non-default dir."""
332
- expected = get_managed_hooks_path_value()
333
- if not expected:
334
- return True
335
- _, ok = git("config", "--local", "core.hooksPath", expected, cwd=repo_root)
336
- return ok
337
-
338
-
339
- def get_managed_hook_display_path():
340
- """Return the human-readable relative hook path for output."""
341
- return get_managed_hooks_path_value() or ".git/hooks"
342
-
343
-
344
- def build_git_hook_wrappers():
345
- """Return the exact hook wrapper contents gator installs.
346
-
347
- The subprocess.call inside uses the exact interpreter path — spaces are
348
- safe there because it's a Python string argument, not a shebang.
349
- """
386
+ def _hook_shebang():
387
+ """Return the platform-correct shebang for hook wrappers."""
388
+ return "#!C:/Windows/py.exe -3" if os.name == "nt" else "#!/usr/bin/env python3"
389
+
390
+
391
+ def get_managed_hooks_path_value():
392
+ """Return the core.hooksPath value Gator manages on this platform."""
393
+ return ".git/gator-hooks" if os.name == "nt" else None
394
+
395
+
396
+ def get_managed_hook_dir(repo_root):
397
+ """Return the hook directory Gator actively manages."""
398
+ hooks_path = get_managed_hooks_path_value()
399
+ if hooks_path:
400
+ return repo_root / Path(hooks_path)
401
+ return repo_root / ".git" / "hooks"
402
+
403
+
404
+ def get_hook_probe_dirs(repo_root):
405
+ """Return installed-hook directories to probe, managed dir first."""
406
+ probe_dirs = [get_managed_hook_dir(repo_root)]
407
+ legacy = repo_root / ".git" / "hooks"
408
+ if legacy not in probe_dirs:
409
+ probe_dirs.append(legacy)
410
+ return probe_dirs
411
+
412
+
413
+ def hooks_config_needs_update(repo_root):
414
+ """Return True when core.hooksPath is missing or wrong."""
415
+ expected = get_managed_hooks_path_value()
416
+ if not expected:
417
+ return False
418
+ actual, ok = git("config", "--local", "--get", "core.hooksPath", cwd=repo_root)
419
+ return not ok or actual.strip() != expected
420
+
421
+
422
+ def _configure_managed_hooks_path(repo_root):
423
+ """Set core.hooksPath when this platform uses a non-default dir."""
424
+ expected = get_managed_hooks_path_value()
425
+ if not expected:
426
+ return True
427
+ _, ok = git("config", "--local", "core.hooksPath", expected, cwd=repo_root)
428
+ return ok
429
+
430
+
431
+ def get_managed_hook_display_path():
432
+ """Return the human-readable relative hook path for output."""
433
+ return get_managed_hooks_path_value() or ".git/hooks"
434
+
435
+
436
+ def build_git_hook_wrappers():
437
+ """Return the exact hook wrapper contents gator installs.
438
+
439
+ The subprocess.call inside uses the exact interpreter path — spaces are
440
+ safe there because it's a Python string argument, not a shebang.
441
+ """
350
442
  python_path = sys.executable.replace("\\", "/")
351
443
  gator_script = ".gator/scripts/gator-pre-commit.py"
352
444
  shebang = _hook_shebang()
@@ -370,19 +462,19 @@ def build_git_hook_wrappers():
370
462
  }
371
463
 
372
464
 
373
- def install_git_hooks(gator_dir, repo_root):
374
- """Install/refresh git hooks as cross-platform Python wrappers.
375
-
376
- Writes Python-shebang hooks that call gator-pre-commit.py directly.
377
- Windows installs into `.git/gator-hooks/` and sets `core.hooksPath`
378
- to bypass the Git-for-Windows/MSYS launcher path. Unix installs into
379
- the normal `.git/hooks/` directory.
380
-
381
- @writes: managed git hook dir, git config core.hooksPath (Windows only)
382
- """
383
- git_hooks = get_managed_hook_dir(repo_root)
384
- if not git_hooks.parent.is_dir():
385
- return 0
465
+ def install_git_hooks(gator_dir, repo_root):
466
+ """Install/refresh git hooks as cross-platform Python wrappers.
467
+
468
+ Writes Python-shebang hooks that call gator-pre-commit.py directly.
469
+ Windows installs into `.git/gator-hooks/` and sets `core.hooksPath`
470
+ to bypass the Git-for-Windows/MSYS launcher path. Unix installs into
471
+ the normal `.git/hooks/` directory.
472
+
473
+ @writes: managed git hook dir, git config core.hooksPath (Windows only)
474
+ """
475
+ git_hooks = get_managed_hook_dir(repo_root)
476
+ if not git_hooks.parent.is_dir():
477
+ return 0
386
478
 
387
479
  git_hooks.mkdir(parents=True, exist_ok=True)
388
480
  installed = 0
@@ -393,12 +485,12 @@ def install_git_hooks(gator_dir, repo_root):
393
485
  try:
394
486
  import stat
395
487
  dest.chmod(dest.stat().st_mode | stat.S_IEXEC)
396
- except (OSError, AttributeError):
397
- pass
398
- installed += 1
399
-
400
- _configure_managed_hooks_path(repo_root)
401
- return installed
488
+ except (OSError, AttributeError):
489
+ pass
490
+ installed += 1
491
+
492
+ _configure_managed_hooks_path(repo_root)
493
+ return installed
402
494
 
403
495
 
404
496
  # --- Output ---
@@ -429,15 +521,15 @@ def print_plan(plan, dry_run=False, hooks=None):
429
521
  print(f" ~ {dest.name}")
430
522
  print()
431
523
 
432
- if hook_adds or hook_updates:
433
- print(f" Git hooks ({len(hook_adds)} new, {len(hook_updates)} updated):")
434
- hook_dir = get_managed_hook_display_path()
435
- for name, action in (hooks or []):
436
- if action == "add":
437
- print(f" + {hook_dir}/{name}")
438
- elif action == "update":
439
- print(f" ~ {hook_dir}/{name}")
440
- print()
524
+ if hook_adds or hook_updates:
525
+ print(f" Git hooks ({len(hook_adds)} new, {len(hook_updates)} updated):")
526
+ hook_dir = get_managed_hook_display_path()
527
+ for name, action in (hooks or []):
528
+ if action == "add":
529
+ print(f" + {hook_dir}/{name}")
530
+ elif action == "update":
531
+ print(f" ~ {hook_dir}/{name}")
532
+ print()
441
533
 
442
534
  if not adds and not updates and not hook_adds and not hook_updates:
443
535
  print(f" Everything is current. No updates needed.")
@@ -466,14 +558,14 @@ def print_json_plan(plan, command_post, templates_dir, hooks=None):
466
558
  "filename": dest.name,
467
559
  })
468
560
 
469
- hook_items = []
470
- hook_dir = get_managed_hook_display_path()
471
- for name, action in (hooks or []):
472
- hook_items.append({
473
- "hook": name,
474
- "action": action,
475
- "destination": f"{hook_dir}/{name}",
476
- })
561
+ hook_items = []
562
+ hook_dir = get_managed_hook_display_path()
563
+ for name, action in (hooks or []):
564
+ hook_items.append({
565
+ "hook": name,
566
+ "action": action,
567
+ "destination": f"{hook_dir}/{name}",
568
+ })
477
569
 
478
570
  output = {
479
571
  "version": VERSION,
@@ -594,6 +686,11 @@ def main():
594
686
  if hooks_installed > 0:
595
687
  print(f" Git hooks: {hooks_installed} installed/refreshed")
596
688
 
689
+ # Install/merge vendor SessionStart hook configs
690
+ vendor_hooks_changed = install_vendor_hooks(templates_dir, repo_root)
691
+ if vendor_hooks_changed > 0:
692
+ print(f" Vendor hooks: {vendor_hooks_changed} config(s) installed/updated")
693
+
597
694
  # Channel 2: policy sync (only for policy-synced repos, unless --no-policy)
598
695
  if args.no_policy:
599
696
  pass # Skip channel 2 entirely