gitvow 0.1.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.
gitvow-0.1.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 gitvow contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
gitvow-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitvow
3
+ Version: 0.1.0
4
+ Summary: Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies.
5
+ Author-email: Nikhil Bora <nikhil@wirevow.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://wirevow.dev/gitvow
8
+ Project-URL: Documentation, https://wirevow.dev/gitvow
9
+ Project-URL: Repository, https://github.com/wirevow/gitvow
10
+ Project-URL: Security, https://github.com/wirevow/gitvow/blob/main/SECURITY.md
11
+ Keywords: git,provenance,ai-agents,claude-code,hooks,policy
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Version Control :: Git
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8; extra == "dev"
22
+ Requires-Dist: pytest-cov>=5; extra == "dev"
23
+ Requires-Dist: ruff>=0.6; extra == "dev"
24
+ Requires-Dist: bandit>=1.7; extra == "dev"
25
+ Requires-Dist: pip-audit>=2.7; extra == "dev"
26
+ Requires-Dist: mypy>=1.10; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # gitvow
30
+
31
+ Provenance and a policy gate for AI-agent coding sessions, stored in the git you already have.
32
+
33
+ [![ci](https://github.com/wirevow/gitvow/actions/workflows/ci.yml/badge.svg)](https://github.com/wirevow/gitvow/actions/workflows/ci.yml)
34
+ [![codeql](https://github.com/wirevow/gitvow/actions/workflows/codeql.yml/badge.svg)](https://github.com/wirevow/gitvow/actions/workflows/codeql.yml)
35
+ ![python](https://img.shields.io/badge/python-3.9%E2%80%933.12-blue) ![deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen) ![license](https://img.shields.io/badge/license-Apache--2.0-blue)
36
+
37
+ **Who made this change, what were they trying to do, and was it allowed?** For code written with AI agents, git alone cannot answer. gitvow makes it answer.
38
+
39
+ - Commits made during an agent session carry the session id and a step number as **trailers**.
40
+ - Each such commit gets a **session note**: the agent's stated plan, the tools it used, the files it touched, how much of the commit it wrote. Redacted, stored as a git note, never in the tree.
41
+ - Every tool call passes a **gate** first: destructive commands are refused, risky ones require asking you, edits to gate-bearing files need a human. The rules are a JSON file you own. Missing policy fails closed.
42
+ - A **ledger** of the whole session stays in your home directory. Nothing leaves the machine unless you push it.
43
+
44
+ Standard-library Python and git. No runtime dependencies, no network calls, no telemetry.
45
+
46
+ ## Quick start
47
+
48
+ ```sh
49
+ pip install gitvow
50
+ gitvow install --user
51
+ gitvow selftest
52
+ ```
53
+
54
+ Work in Claude Code as usual. When the agent commits:
55
+
56
+ ```sh
57
+ $ git log -1 --format=%B
58
+ Fix week-start cache key
59
+
60
+ Gitvow-Session: 8f3d5c71-574a-4eec-8903-9425e3a8335b
61
+ Gitvow-Step: 4
62
+
63
+ $ gitvow show HEAD
64
+ ...
65
+ gitvow-session
66
+ {
67
+ "step": 4,
68
+ "tools_used": ["Bash", "Edit", "Read"],
69
+ "last_stated_plan": "Change the cache key to include week start so per-org settings do not collide...",
70
+ "files_in_commit": ["query-engine/.../QueryCacheHelper.java | 4 +++-"],
71
+ "files_written_by_agent_this_session": ["query-engine/.../QueryCacheHelper.java"],
72
+ "attribution": {"files_in_commit": 1, "touched_by_agent": 1}
73
+ }
74
+ ```
75
+
76
+ Try the gate by hand:
77
+
78
+ ```sh
79
+ gitvow check -- git push --force # DENY: force push
80
+ gitvow check -- kubectl apply -f x.yaml # CONFIRM: cluster apply
81
+ gitvow check --path core/authz_rules.go # CONFIRM: edits an authorization or gate file
82
+ ```
83
+
84
+ Remove everything:
85
+
86
+ ```sh
87
+ gitvow uninstall --user
88
+ ```
89
+
90
+ ## Documentation
91
+
92
+ The docs site is the source of truth: **https://wirevow.dev/gitvow** (built from `docs/`).
93
+
94
+ - [Quick start](docs/quickstart.md)
95
+ - Concepts: [Sessions, steps and notes](docs/concepts/sessions.md) · [The gate](docs/concepts/gate.md) · [What stays out of git](docs/concepts/storage.md)
96
+ - Guides: [Install per user or per repo](docs/guides/install.md) · [Write a policy](docs/guides/policy.md) · [Read a commit's session](docs/guides/reading.md) · [Run a trial](docs/guides/trial.md) · [Redaction](docs/guides/redaction.md)
97
+ - Reference: [CLI](docs/reference/cli.md) · [Hook payloads](docs/reference/hooks.md) · [Note schema](docs/reference/note.md) · [Policy schema](docs/reference/policy.md)
98
+ - [Security](docs/security.md) · [Roadmap](docs/roadmap.md) · [FAQ](docs/faq.md)
99
+
100
+ ## How it works
101
+
102
+ ```
103
+ Claude Code ──hook──▶ gitvow hook PreToolUse ──▶ policy ──▶ allow / confirm / deny (exit 0 / 2 / 2)
104
+ ──hook──▶ gitvow hook PostToolUse ─▶ on `git commit`: read transcript → redact → git notes add
105
+ git commit ──prepare-commit-msg──▶ Gitvow-Session / Gitvow-Step trailers (from .git/gitvow-session.json)
106
+ Claude Code ──hook──▶ gitvow hook Stop ─────────▶ ~/.gitvow/ledger/<session>.json
107
+ ```
108
+
109
+ | Data | Where | Enters git? |
110
+ |---|---|---|
111
+ | session id, step | commit trailers | yes |
112
+ | session note (structure, redacted plan, attribution) | `refs/notes/sessions` | as a note; local until pushed |
113
+ | ledger, hook log, session state | `~/.gitvow/`, `<repo>/.git/` | no |
114
+ | transcript | untouched | never |
115
+
116
+ ## Status
117
+
118
+ 0.1.0. Used in a small internal trial; the [roadmap](docs/roadmap.md) lists what comes next and what is deliberately not planned. Claude Code is the only agent supported today; the hook payload is [documented](docs/reference/hooks.md) so adapters are straightforward.
119
+
120
+ ## Contributing and security
121
+
122
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Reproductions of redaction gaps must use synthetic secrets.
123
+
124
+ Apache-2.0.
gitvow-0.1.0/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # gitvow
2
+
3
+ Provenance and a policy gate for AI-agent coding sessions, stored in the git you already have.
4
+
5
+ [![ci](https://github.com/wirevow/gitvow/actions/workflows/ci.yml/badge.svg)](https://github.com/wirevow/gitvow/actions/workflows/ci.yml)
6
+ [![codeql](https://github.com/wirevow/gitvow/actions/workflows/codeql.yml/badge.svg)](https://github.com/wirevow/gitvow/actions/workflows/codeql.yml)
7
+ ![python](https://img.shields.io/badge/python-3.9%E2%80%933.12-blue) ![deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen) ![license](https://img.shields.io/badge/license-Apache--2.0-blue)
8
+
9
+ **Who made this change, what were they trying to do, and was it allowed?** For code written with AI agents, git alone cannot answer. gitvow makes it answer.
10
+
11
+ - Commits made during an agent session carry the session id and a step number as **trailers**.
12
+ - Each such commit gets a **session note**: the agent's stated plan, the tools it used, the files it touched, how much of the commit it wrote. Redacted, stored as a git note, never in the tree.
13
+ - Every tool call passes a **gate** first: destructive commands are refused, risky ones require asking you, edits to gate-bearing files need a human. The rules are a JSON file you own. Missing policy fails closed.
14
+ - A **ledger** of the whole session stays in your home directory. Nothing leaves the machine unless you push it.
15
+
16
+ Standard-library Python and git. No runtime dependencies, no network calls, no telemetry.
17
+
18
+ ## Quick start
19
+
20
+ ```sh
21
+ pip install gitvow
22
+ gitvow install --user
23
+ gitvow selftest
24
+ ```
25
+
26
+ Work in Claude Code as usual. When the agent commits:
27
+
28
+ ```sh
29
+ $ git log -1 --format=%B
30
+ Fix week-start cache key
31
+
32
+ Gitvow-Session: 8f3d5c71-574a-4eec-8903-9425e3a8335b
33
+ Gitvow-Step: 4
34
+
35
+ $ gitvow show HEAD
36
+ ...
37
+ gitvow-session
38
+ {
39
+ "step": 4,
40
+ "tools_used": ["Bash", "Edit", "Read"],
41
+ "last_stated_plan": "Change the cache key to include week start so per-org settings do not collide...",
42
+ "files_in_commit": ["query-engine/.../QueryCacheHelper.java | 4 +++-"],
43
+ "files_written_by_agent_this_session": ["query-engine/.../QueryCacheHelper.java"],
44
+ "attribution": {"files_in_commit": 1, "touched_by_agent": 1}
45
+ }
46
+ ```
47
+
48
+ Try the gate by hand:
49
+
50
+ ```sh
51
+ gitvow check -- git push --force # DENY: force push
52
+ gitvow check -- kubectl apply -f x.yaml # CONFIRM: cluster apply
53
+ gitvow check --path core/authz_rules.go # CONFIRM: edits an authorization or gate file
54
+ ```
55
+
56
+ Remove everything:
57
+
58
+ ```sh
59
+ gitvow uninstall --user
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ The docs site is the source of truth: **https://wirevow.dev/gitvow** (built from `docs/`).
65
+
66
+ - [Quick start](docs/quickstart.md)
67
+ - Concepts: [Sessions, steps and notes](docs/concepts/sessions.md) · [The gate](docs/concepts/gate.md) · [What stays out of git](docs/concepts/storage.md)
68
+ - Guides: [Install per user or per repo](docs/guides/install.md) · [Write a policy](docs/guides/policy.md) · [Read a commit's session](docs/guides/reading.md) · [Run a trial](docs/guides/trial.md) · [Redaction](docs/guides/redaction.md)
69
+ - Reference: [CLI](docs/reference/cli.md) · [Hook payloads](docs/reference/hooks.md) · [Note schema](docs/reference/note.md) · [Policy schema](docs/reference/policy.md)
70
+ - [Security](docs/security.md) · [Roadmap](docs/roadmap.md) · [FAQ](docs/faq.md)
71
+
72
+ ## How it works
73
+
74
+ ```
75
+ Claude Code ──hook──▶ gitvow hook PreToolUse ──▶ policy ──▶ allow / confirm / deny (exit 0 / 2 / 2)
76
+ ──hook──▶ gitvow hook PostToolUse ─▶ on `git commit`: read transcript → redact → git notes add
77
+ git commit ──prepare-commit-msg──▶ Gitvow-Session / Gitvow-Step trailers (from .git/gitvow-session.json)
78
+ Claude Code ──hook──▶ gitvow hook Stop ─────────▶ ~/.gitvow/ledger/<session>.json
79
+ ```
80
+
81
+ | Data | Where | Enters git? |
82
+ |---|---|---|
83
+ | session id, step | commit trailers | yes |
84
+ | session note (structure, redacted plan, attribution) | `refs/notes/sessions` | as a note; local until pushed |
85
+ | ledger, hook log, session state | `~/.gitvow/`, `<repo>/.git/` | no |
86
+ | transcript | untouched | never |
87
+
88
+ ## Status
89
+
90
+ 0.1.0. Used in a small internal trial; the [roadmap](docs/roadmap.md) lists what comes next and what is deliberately not planned. Claude Code is the only agent supported today; the hook payload is [documented](docs/reference/hooks.md) so adapters are straightforward.
91
+
92
+ ## Contributing and security
93
+
94
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Reproductions of redaction gaps must use synthetic secrets.
95
+
96
+ Apache-2.0.
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gitvow"
7
+ version = "0.1.0"
8
+ description = "Provenance and policy gate for AI-agent coding sessions: session trailers on commits, redacted session notes in git, a tool-call gate, and a local ledger. No runtime dependencies."
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ requires-python = ">=3.9"
12
+ authors = [{name = "Nikhil Bora", email = "nikhil@wirevow.com"}]
13
+ keywords = ["git", "provenance", "ai-agents", "claude-code", "hooks", "policy"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Version Control :: Git",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "bandit>=1.7", "pip-audit>=2.7", "mypy>=1.10"]
25
+
26
+ [project.scripts]
27
+ gitvow = "gitvow.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://wirevow.dev/gitvow"
31
+ Documentation = "https://wirevow.dev/gitvow"
32
+ Repository = "https://github.com/wirevow/gitvow"
33
+ Security = "https://github.com/wirevow/gitvow/blob/main/SECURITY.md"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.ruff]
39
+ line-length = 120
40
+ target-version = "py39"
41
+ src = ["src", "tests"]
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "W", "I", "B", "UP", "S", "N", "C4", "SIM", "RUF"]
45
+ ignore = ["S101", "S603", "S607", "E501"]
46
+
47
+ [tool.ruff.lint.per-file-ignores]
48
+ "tests/*" = ["S105", "S106", "S108"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ addopts = "-q --cov=gitvow --cov-report=term-missing --cov-fail-under=85"
53
+
54
+ [tool.mypy]
55
+ python_version = "3.10"
56
+ strict = false
57
+ ignore_missing_imports = true
58
+
59
+ [tool.bandit]
60
+ exclude_dirs = ["tests"]
61
+ skips = ["B404", "B603", "B607"]
gitvow-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """gitvow: provenance and policy gate for AI-agent coding sessions."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,135 @@
1
+ """gitvow command line."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ from . import __version__
11
+ from .collect import collect, summarize_text
12
+ from .hooks import HANDLERS
13
+ from .install import install_repo, install_user, uninstall_repo, uninstall_user
14
+ from .policy import PolicyError, evaluate, load_policy
15
+ from .state import git
16
+
17
+
18
+ def cmd_hook(a: argparse.Namespace) -> int:
19
+ try:
20
+ payload = json.load(sys.stdin)
21
+ except json.JSONDecodeError:
22
+ payload = {}
23
+ handler = HANDLERS.get(a.event)
24
+ if not handler:
25
+ print(f"unknown hook event {a.event}", file=sys.stderr)
26
+ return 1
27
+ code, msg = handler(payload)
28
+ if msg:
29
+ print(msg, file=sys.stderr)
30
+ return code
31
+
32
+
33
+ def cmd_install(a: argparse.Namespace) -> int:
34
+ done = install_user(os.path.expanduser("~")) if a.user else install_repo(os.path.abspath(a.repo))
35
+ print("\n".join(done))
36
+ return 0
37
+
38
+
39
+ def cmd_uninstall(a: argparse.Namespace) -> int:
40
+ done = (
41
+ uninstall_user(os.path.expanduser("~"), a.purge_policy, a.purge_ledger)
42
+ if a.user
43
+ else uninstall_repo(os.path.abspath(a.repo), a.purge_notes)
44
+ )
45
+ print("\n".join(done))
46
+ return 0
47
+
48
+
49
+ def cmd_check(a: argparse.Namespace) -> int:
50
+ """Dry-run the policy against a command, path or MCP tool name."""
51
+ try:
52
+ pol = load_policy(os.getcwd())
53
+ except PolicyError as e:
54
+ print(f"policy error: {e}", file=sys.stderr)
55
+ return 2
56
+ if a.path:
57
+ d = evaluate(pol, "Edit", {"file_path": a.path})
58
+ elif a.mcp:
59
+ d = evaluate(pol, a.mcp, {})
60
+ else:
61
+ d = evaluate(pol, "Bash", {"command": " ".join(a.command)})
62
+ print(d.outcome.upper() + (f": {d.reason}" if d.reason else ""))
63
+ return 2 if d.blocks else 0
64
+
65
+
66
+ def cmd_show(a: argparse.Namespace) -> int:
67
+ """Print a commit's trailers and session note."""
68
+ rc, msg, _ = git(["log", "-1", "--format=%H%n%s%n%b", a.commit], os.getcwd())
69
+ if rc != 0:
70
+ print(f"no such commit: {a.commit}", file=sys.stderr)
71
+ return 1
72
+ print(msg)
73
+ rc, note, _ = git(["notes", "--ref=sessions", "show", a.commit], os.getcwd())
74
+ print(note if rc == 0 else "(no session note)")
75
+ return 0
76
+
77
+
78
+ def cmd_collect(a: argparse.Namespace) -> int:
79
+ w = collect(os.path.expanduser("~"), a.out)
80
+ print(summarize_text(w))
81
+ print(f"collected into {w} (redacted at write time; review before sending)")
82
+ return 0
83
+
84
+
85
+ def cmd_summarize(a: argparse.Namespace) -> int:
86
+ print(summarize_text(a.dir))
87
+ return 0
88
+
89
+
90
+ def cmd_selftest(a: argparse.Namespace) -> int:
91
+ from .selftest import run
92
+
93
+ return run()
94
+
95
+
96
+ def main(argv: list[str] | None = None) -> int:
97
+ p = argparse.ArgumentParser(prog="gitvow", description="Provenance and policy gate for agent coding sessions.")
98
+ p.add_argument("--version", action="version", version=f"gitvow {__version__}")
99
+ sub = p.add_subparsers(dest="cmd", required=True)
100
+ s = sub.add_parser("hook", help="run as a Claude Code hook (reads JSON on stdin)")
101
+ s.add_argument("event")
102
+ s.set_defaults(f=cmd_hook)
103
+ s = sub.add_parser("install", help="install per user (--user) or into a repo")
104
+ s.add_argument("repo", nargs="?", default=".")
105
+ s.add_argument("--user", action="store_true")
106
+ s.set_defaults(f=cmd_install)
107
+ s = sub.add_parser("uninstall", help="remove what install added")
108
+ s.add_argument("repo", nargs="?", default=".")
109
+ s.add_argument("--user", action="store_true")
110
+ s.add_argument("--purge-notes", action="store_true")
111
+ s.add_argument("--purge-policy", action="store_true")
112
+ s.add_argument("--purge-ledger", action="store_true")
113
+ s.set_defaults(f=cmd_uninstall)
114
+ s = sub.add_parser("check", help="dry-run the policy: gitvow check -- git push --force")
115
+ s.add_argument("command", nargs="*")
116
+ s.add_argument("--path")
117
+ s.add_argument("--mcp")
118
+ s.set_defaults(f=cmd_check)
119
+ s = sub.add_parser("show", help="print a commit's trailers and session note")
120
+ s.add_argument("commit", nargs="?", default="HEAD")
121
+ s.set_defaults(f=cmd_show)
122
+ s = sub.add_parser("collect", help="gather ledger, logs, trailers and notes into one directory")
123
+ s.add_argument("--out", default=os.path.expanduser("~/Desktop"))
124
+ s.set_defaults(f=cmd_collect)
125
+ s = sub.add_parser("summarize", help="metrics from a collected directory")
126
+ s.add_argument("dir")
127
+ s.set_defaults(f=cmd_summarize)
128
+ s = sub.add_parser("selftest", help="prove the hooks work here without touching a real repo")
129
+ s.set_defaults(f=cmd_selftest)
130
+ a = p.parse_args(argv)
131
+ return a.f(a)
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,103 @@
1
+ """Gather what a trial produced on this machine into one redacted directory, and compute trial metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import collections
6
+ import glob
7
+ import json
8
+ import os
9
+ import shutil
10
+ import time
11
+ from typing import Any
12
+
13
+ from .state import git
14
+
15
+
16
+ def collect(home: str, out_dir: str) -> str:
17
+ stamp = time.strftime("%Y%m%d-%H%M%S")
18
+ w = os.path.join(out_dir, f"gitvow-{stamp}")
19
+ os.makedirs(os.path.join(w, "ledger"), exist_ok=True)
20
+ os.makedirs(os.path.join(w, "repos"), exist_ok=True)
21
+ led = os.path.join(home, ".gitvow", "ledger")
22
+ repos: set[str] = set()
23
+ for f in glob.glob(os.path.join(led, "*.json")):
24
+ shutil.copy(f, os.path.join(w, "ledger"))
25
+ try:
26
+ with open(f) as fh:
27
+ r = json.load(fh).get("repo")
28
+ if r:
29
+ repos.add(r)
30
+ except (OSError, json.JSONDecodeError):
31
+ pass
32
+ for r in sorted(repos):
33
+ if not os.path.isdir(r):
34
+ continue
35
+ d = os.path.join(w, "repos", os.path.basename(r.rstrip("/")))
36
+ os.makedirs(d, exist_ok=True)
37
+ rc, gd, _ = git(["rev-parse", "--git-dir"], r)
38
+ if rc != 0:
39
+ continue
40
+ gd = gd if os.path.isabs(gd) else os.path.join(r, gd)
41
+ lp = os.path.join(gd, "gitvow-hooks.log")
42
+ if os.path.exists(lp):
43
+ shutil.copy(lp, os.path.join(d, "gitvow-hooks.log"))
44
+ for name, args in (
45
+ ("commits-with-trailers.txt", ["log", "--format=%H %ad %s", "--date=short", "--grep=Gitvow-Session:"]),
46
+ ("notes.txt", ["log", "--show-notes=sessions", "--format=%H%n%N%n----", "--grep=Gitvow-Session:"]),
47
+ ("remote.txt", ["remote", "get-url", "origin"]),
48
+ ):
49
+ _, out, _ = git(args, r)
50
+ with open(os.path.join(d, name), "w") as fh:
51
+ fh.write(out)
52
+ with open(os.path.join(w, "SUMMARY.txt"), "w") as fh:
53
+ fh.write(summarize_text(w))
54
+ return w
55
+
56
+
57
+ def summarize(w: str) -> dict[str, Any]:
58
+ led = [json.load(open(f)) for f in glob.glob(os.path.join(w, "ledger", "*.json"))] # noqa: SIM115
59
+ tools: collections.Counter[str] = collections.Counter(t.get("tool") for s in led for t in s.get("tool_calls", []))
60
+ kinds: collections.Counter[str] = collections.Counter()
61
+ reasons: collections.Counter[tuple[str, str]] = collections.Counter()
62
+ trailered = notes = 0
63
+ for lp in glob.glob(os.path.join(w, "repos", "*", "gitvow-hooks.log")):
64
+ with open(lp) as fh:
65
+ for line in fh:
66
+ try:
67
+ e = json.loads(line)
68
+ except json.JSONDecodeError:
69
+ continue
70
+ kinds[e.get("kind", "?")] += 1
71
+ if e.get("kind") in ("blocked", "confirm_required"):
72
+ reasons[(e.get("kind", "?"), e.get("reason", "?"))] += 1
73
+ for f in glob.glob(os.path.join(w, "repos", "*", "commits-with-trailers.txt")):
74
+ with open(f) as fh:
75
+ trailered += sum(1 for ln in fh if ln.strip())
76
+ for f in glob.glob(os.path.join(w, "repos", "*", "notes.txt")):
77
+ with open(f) as fh:
78
+ notes += fh.read().count("gitvow-session")
79
+ return {
80
+ "sessions": len(led),
81
+ "repos": len({s.get("repo") for s in led}),
82
+ "tool_calls": sum(tools.values()),
83
+ "by_tool": dict(tools.most_common(8)),
84
+ "commits_during_sessions": sum(len(s.get("commits_during_session", [])) for s in led),
85
+ "hook_decisions": dict(kinds),
86
+ "commits_with_trailers": trailered,
87
+ "notes_attached": notes,
88
+ "gate_fired_on": [{"kind": k, "reason": r, "n": n} for (k, r), n in reasons.most_common(20)],
89
+ }
90
+
91
+
92
+ def summarize_text(w: str) -> str:
93
+ s = summarize(w)
94
+ lines = [
95
+ f"sessions: {s['sessions']} repos touched: {s['repos']}",
96
+ f"tool calls: {s['tool_calls']} | by tool: {s['by_tool']}",
97
+ f"commits during sessions: {s['commits_during_sessions']}",
98
+ f"hook decisions: {s['hook_decisions']}",
99
+ f"commits with trailers: {s['commits_with_trailers']} | notes attached: {s['notes_attached']}",
100
+ "gate fired on:",
101
+ ]
102
+ lines += [f" {g['kind']:<17} {g['n']:>4} {g['reason']}" for g in s["gate_fired_on"]] or [" (never)"]
103
+ return "\n".join(lines) + "\n"