gitvow 0.13.0__tar.gz → 0.13.2__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.13.0/src/gitvow.egg-info → gitvow-0.13.2}/PKG-INFO +1 -1
- {gitvow-0.13.0 → gitvow-0.13.2}/pyproject.toml +1 -1
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/cli.py +51 -11
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/install.py +104 -7
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/status.py +39 -38
- {gitvow-0.13.0 → gitvow-0.13.2/src/gitvow.egg-info}/PKG-INFO +1 -1
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_adapters.py +0 -1
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_status.py +38 -3
- {gitvow-0.13.0 → gitvow-0.13.2}/LICENSE +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/README.md +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/setup.cfg +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/__init__.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/__main__.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/adapters.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/collect.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/decisions.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/default_policy.json +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/digest.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/hooks/__init__.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/policy.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/pricing.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/providers.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/recall.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/redact.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/report.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/rules.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/selftest.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/snapshots.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/state.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow/transcript.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow.egg-info/SOURCES.txt +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow.egg-info/dependency_links.txt +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow.egg-info/entry_points.txt +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow.egg-info/requires.txt +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/src/gitvow.egg-info/top_level.txt +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_collect.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_cost.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_decisions.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_digest.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_external_adapter.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_hooks.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_install_cli.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_packaging.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_policy.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_providers.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_recall.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_redact.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_report.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_rules.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_snapshots.py +0 -0
- {gitvow-0.13.0 → gitvow-0.13.2}/tests/test_transcript_formats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
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
5
|
Author-email: Nikhil Bora <nikhil@wirevow.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitvow"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.2"
|
|
8
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
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -97,18 +97,57 @@ def _check_agent(agent: str) -> int:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def cmd_install(a: argparse.Namespace) -> int:
|
|
100
|
-
agent
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
"""Without --agent, configure every agent found on this machine: not passing a flag should not
|
|
101
|
+
silently leave an agent ungated."""
|
|
102
|
+
from .install import detect_agents
|
|
103
|
+
|
|
104
|
+
home = os.path.expanduser("~")
|
|
105
|
+
repo = os.path.abspath(a.repo)
|
|
106
|
+
if a.agent:
|
|
107
|
+
agents, detected = [a.agent], False
|
|
105
108
|
else:
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
agents, detected = detect_agents(home), True
|
|
110
|
+
if not agents:
|
|
111
|
+
agents = ["claude"]
|
|
112
|
+
for agent in agents:
|
|
113
|
+
if _check_agent(agent):
|
|
114
|
+
return 1
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
if detected:
|
|
117
|
+
lines.append(f"agents found here: {', '.join(agents)} (use --agent to pick one)")
|
|
118
|
+
for agent in agents:
|
|
119
|
+
lines += install_user(home, agent=agent) if a.user else install_repo(repo, agent=agent)
|
|
120
|
+
seen: set[str] = set()
|
|
121
|
+
for ln in lines:
|
|
122
|
+
if ln not in seen:
|
|
123
|
+
seen.add(ln)
|
|
124
|
+
print(ln)
|
|
125
|
+
print("next: run `gitvow status` inside a repository to check the record is actually being written.")
|
|
126
|
+
if a.check:
|
|
127
|
+
from .selftest import run, run_agent
|
|
128
|
+
|
|
129
|
+
print()
|
|
130
|
+
worst = 0
|
|
131
|
+
for agent in agents:
|
|
132
|
+
worst = max(worst, run() if agent == "claude" else run_agent(agent))
|
|
133
|
+
if worst:
|
|
134
|
+
print("the self-check failed; `gitvow status` says what is missing", file=sys.stderr)
|
|
108
135
|
return 0
|
|
109
136
|
|
|
110
137
|
|
|
111
138
|
def cmd_uninstall(a: argparse.Namespace) -> int:
|
|
139
|
+
from .install import installed_agents
|
|
140
|
+
|
|
141
|
+
if not a.agent:
|
|
142
|
+
home = os.path.expanduser("~")
|
|
143
|
+
repo = os.path.abspath(a.repo)
|
|
144
|
+
found = [ag for ag, scope, _ in installed_agents(home, repo) if (scope == "user") == bool(a.user)]
|
|
145
|
+
if len(found) > 1:
|
|
146
|
+
worst = 0
|
|
147
|
+
for ag in dict.fromkeys(found):
|
|
148
|
+
worst = max(worst, cmd_uninstall(argparse.Namespace(**{**vars(a), "agent": ag})))
|
|
149
|
+
return worst
|
|
150
|
+
a.agent = found[0] if found else None
|
|
112
151
|
agent = a.agent or "claude"
|
|
113
152
|
if _check_agent(agent):
|
|
114
153
|
return 1
|
|
@@ -464,9 +503,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
464
503
|
s.add_argument("--user", action="store_true")
|
|
465
504
|
s.add_argument(
|
|
466
505
|
"--agent",
|
|
467
|
-
|
|
468
|
-
|
|
506
|
+
help="claude|codex|gemini|cursor|copilot|factory or an external gitvow-agent-<name>; "
|
|
507
|
+
"omit to configure every agent found on this machine",
|
|
469
508
|
)
|
|
509
|
+
s.add_argument("--check", action="store_true", help="run the self-check afterwards and show it working")
|
|
470
510
|
s.set_defaults(f=cmd_install)
|
|
471
511
|
s = sub.add_parser("uninstall", help="remove what install added")
|
|
472
512
|
s.add_argument("repo", nargs="?", default=".")
|
|
@@ -477,8 +517,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
477
517
|
s.add_argument("--purge-snapshots", action="store_true")
|
|
478
518
|
s.add_argument(
|
|
479
519
|
"--agent",
|
|
480
|
-
|
|
481
|
-
|
|
520
|
+
help="claude|codex|gemini|cursor|copilot|factory or an external gitvow-agent-<name>; "
|
|
521
|
+
"omit to remove gitvow from every agent configured here",
|
|
482
522
|
)
|
|
483
523
|
s.set_defaults(f=cmd_uninstall)
|
|
484
524
|
s = sub.add_parser("check", help="dry-run the policy: gitvow check -- git push --force")
|
|
@@ -374,18 +374,115 @@ def agent_next_steps(agent: str, home: str, repo: str | None = None) -> list[str
|
|
|
374
374
|
"codex: its sandbox refuses writes inside .git, so the agent cannot commit at all. Add to "
|
|
375
375
|
f'~/.codex/config.toml: [sandbox_workspace_write] writable_roots = ["{gd}"]'
|
|
376
376
|
)
|
|
377
|
-
out
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
GIT_HOOKS_EXPECTED = ("prepare-commit-msg", "pre-push", "pre-commit", "post-commit")
|
|
381
|
+
GIT_HOOK_BODIES = {
|
|
382
|
+
"prepare-commit-msg": GIT_HOOK,
|
|
383
|
+
"pre-push": PRE_PUSH_HOOK,
|
|
384
|
+
"pre-commit": PRE_COMMIT_HOOK,
|
|
385
|
+
"post-commit": POST_COMMIT_HOOK,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# How to tell an agent is on this machine: a configuration directory under $HOME, an absolute path, or a
|
|
389
|
+
# command on the PATH. Detection only decides what install offers; it never changes what a hook does.
|
|
390
|
+
AGENT_PROBES: dict[str, tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]] = {
|
|
391
|
+
"claude": ((".claude",), (), ("claude",)),
|
|
392
|
+
"codex": ((".codex",), (), ("codex",)),
|
|
393
|
+
"gemini": ((".gemini",), (), ("gemini",)),
|
|
394
|
+
"cursor": ((".cursor",), ("/Applications/Cursor.app",), ("cursor-agent", "cursor")),
|
|
395
|
+
"copilot": ((".copilot",), (), ("copilot",)),
|
|
396
|
+
"factory": ((".factory",), (), ("droid",)),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def detect_agents(home: str) -> list[str]:
|
|
401
|
+
"""Agents that look installed here, built-in first, then any external gitvow-agent-<name> found."""
|
|
402
|
+
found = [
|
|
403
|
+
agent
|
|
404
|
+
for agent, (dirs, paths, bins) in AGENT_PROBES.items()
|
|
405
|
+
if any(os.path.isdir(os.path.join(home, d)) for d in dirs)
|
|
406
|
+
or any(os.path.exists(p) for p in paths)
|
|
407
|
+
or any(shutil.which(b) for b in bins)
|
|
408
|
+
]
|
|
409
|
+
seen = set(found)
|
|
410
|
+
for d in [os.path.join(home, ".gitvow", "agents"), *os.environ.get("PATH", "").split(os.pathsep)]:
|
|
411
|
+
try:
|
|
412
|
+
names = os.listdir(d)
|
|
413
|
+
except OSError:
|
|
414
|
+
continue
|
|
415
|
+
for n in names:
|
|
416
|
+
if n.startswith("gitvow-agent-") and os.access(os.path.join(d, n), os.X_OK):
|
|
417
|
+
name = n[len("gitvow-agent-") :]
|
|
418
|
+
if name not in seen:
|
|
419
|
+
seen.add(name)
|
|
420
|
+
found.append(name)
|
|
421
|
+
return found
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def hook_commands(path: str) -> list[str]:
|
|
425
|
+
"""Every gitvow hook command in an agent settings file, whatever the schema around it."""
|
|
426
|
+
if not os.path.exists(path):
|
|
427
|
+
return []
|
|
428
|
+
try:
|
|
429
|
+
with open(path) as fh:
|
|
430
|
+
data = json.load(fh)
|
|
431
|
+
except (OSError, json.JSONDecodeError):
|
|
432
|
+
return []
|
|
433
|
+
out: list[str] = []
|
|
434
|
+
|
|
435
|
+
def walk(node: Any) -> None:
|
|
436
|
+
if isinstance(node, dict):
|
|
437
|
+
cmd = node.get("command")
|
|
438
|
+
if isinstance(cmd, str) and MARKER in cmd:
|
|
439
|
+
out.append(cmd)
|
|
440
|
+
for v in node.values():
|
|
441
|
+
walk(v)
|
|
442
|
+
elif isinstance(node, list):
|
|
443
|
+
for v in node:
|
|
444
|
+
walk(v)
|
|
445
|
+
|
|
446
|
+
walk(data)
|
|
447
|
+
return out
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def installed_agents(home: str, repo: str | None = None) -> list[tuple[str, str, str]]:
|
|
451
|
+
"""(agent, scope, path) for every agent settings file that already carries gitvow hooks."""
|
|
452
|
+
out: list[tuple[str, str, str]] = []
|
|
453
|
+
for agent, (user_rel, repo_rel) in AGENT_FILES.items():
|
|
454
|
+
for scope, path in (("user", os.path.join(home, user_rel)), ("repo", os.path.join(repo or "", repo_rel))):
|
|
455
|
+
if scope == "repo" and not repo:
|
|
456
|
+
continue
|
|
457
|
+
if hook_commands(path):
|
|
458
|
+
out.append((agent, scope, path))
|
|
459
|
+
return out
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def expected_events(agent: str) -> set[str]:
|
|
463
|
+
"""The hook events this version of gitvow installs for an agent."""
|
|
464
|
+
if agent not in AGENT_FILES:
|
|
465
|
+
return set()
|
|
466
|
+
return set(_agent_entries(agent, "gitvow"))
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def stale_git_hooks(dirpath: str) -> list[str]:
|
|
470
|
+
"""Hooks whose contents are not what this version writes: an install left behind by an older gitvow."""
|
|
471
|
+
out = []
|
|
472
|
+
for name, body in GIT_HOOK_BODIES.items():
|
|
473
|
+
p = os.path.join(dirpath, name)
|
|
474
|
+
try:
|
|
475
|
+
with open(p) as fh:
|
|
476
|
+
if fh.read() != body:
|
|
477
|
+
out.append(name)
|
|
478
|
+
except OSError:
|
|
479
|
+
continue # missing hooks are reported separately
|
|
378
480
|
return out
|
|
379
481
|
|
|
380
482
|
|
|
381
483
|
def _write_git_hook(dirpath: str) -> str:
|
|
382
484
|
os.makedirs(dirpath, exist_ok=True)
|
|
383
|
-
for name, body in (
|
|
384
|
-
("prepare-commit-msg", GIT_HOOK),
|
|
385
|
-
("pre-push", PRE_PUSH_HOOK),
|
|
386
|
-
("pre-commit", PRE_COMMIT_HOOK),
|
|
387
|
-
("post-commit", POST_COMMIT_HOOK),
|
|
388
|
-
):
|
|
485
|
+
for name, body in GIT_HOOK_BODIES.items():
|
|
389
486
|
p = os.path.join(dirpath, name)
|
|
390
487
|
with open(p, "w") as fh:
|
|
391
488
|
fh.write(body)
|
|
@@ -7,46 +7,25 @@ worst outcome, so this exists to make that state loud.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import json
|
|
11
10
|
import os
|
|
12
11
|
import shutil
|
|
13
|
-
from typing import Any
|
|
14
12
|
|
|
15
|
-
from .install import
|
|
13
|
+
from .install import (
|
|
14
|
+
AGENT_FILES,
|
|
15
|
+
GIT_HOOKS_EXPECTED,
|
|
16
|
+
NOTES_GLOB,
|
|
17
|
+
agent_next_steps,
|
|
18
|
+
expected_events,
|
|
19
|
+
hook_commands,
|
|
20
|
+
stale_git_hooks,
|
|
21
|
+
)
|
|
16
22
|
from .policy import PolicyError, load_policy
|
|
17
23
|
from .redact import RedactionError, load_rules
|
|
18
24
|
from .state import git, git_dir, toplevel
|
|
19
25
|
|
|
20
|
-
GIT_HOOKS = ("prepare-commit-msg", "pre-commit", "post-commit", "pre-push")
|
|
21
26
|
Check = tuple[str, str, str] # (state: ok|fail|note, what, fix or detail)
|
|
22
27
|
|
|
23
28
|
|
|
24
|
-
def _hook_commands(path: str) -> list[str]:
|
|
25
|
-
"""Every gitvow hook command in an agent settings file, whatever the schema around it."""
|
|
26
|
-
if not os.path.exists(path):
|
|
27
|
-
return []
|
|
28
|
-
try:
|
|
29
|
-
with open(path) as fh:
|
|
30
|
-
data = json.load(fh)
|
|
31
|
-
except (OSError, json.JSONDecodeError):
|
|
32
|
-
return []
|
|
33
|
-
out: list[str] = []
|
|
34
|
-
|
|
35
|
-
def walk(node: Any) -> None:
|
|
36
|
-
if isinstance(node, dict):
|
|
37
|
-
cmd = node.get("command")
|
|
38
|
-
if isinstance(cmd, str) and MARKER in cmd:
|
|
39
|
-
out.append(cmd)
|
|
40
|
-
for v in node.values():
|
|
41
|
-
walk(v)
|
|
42
|
-
elif isinstance(node, list):
|
|
43
|
-
for v in node:
|
|
44
|
-
walk(v)
|
|
45
|
-
|
|
46
|
-
walk(data)
|
|
47
|
-
return out
|
|
48
|
-
|
|
49
|
-
|
|
50
29
|
def _executable(cmd: str) -> tuple[bool, str, bool]:
|
|
51
30
|
"""(runnable, resolved path, found on PATH rather than recorded absolutely)."""
|
|
52
31
|
parts = cmd.split()
|
|
@@ -65,7 +44,7 @@ def _agent_checks(home: str, cwd: str) -> list[Check]:
|
|
|
65
44
|
for scope, path in (("user", os.path.join(home, user_rel)), ("repo", os.path.join(top or cwd, repo_rel))):
|
|
66
45
|
if scope == "repo" and not top:
|
|
67
46
|
continue
|
|
68
|
-
cmds =
|
|
47
|
+
cmds = hook_commands(path)
|
|
69
48
|
if not cmds:
|
|
70
49
|
continue
|
|
71
50
|
found_any = True
|
|
@@ -91,6 +70,15 @@ def _agent_checks(home: str, cwd: str) -> list[Check]:
|
|
|
91
70
|
)
|
|
92
71
|
else:
|
|
93
72
|
checks.append(("ok", f"{agent} ({scope}): {len(cmds)} hooks in {where} → {resolved}", ""))
|
|
73
|
+
missing = sorted(e for e in expected_events(agent) if not any(f" {e}" in c for c in cmds))
|
|
74
|
+
if missing:
|
|
75
|
+
checks.append(
|
|
76
|
+
(
|
|
77
|
+
"fail",
|
|
78
|
+
f"{agent} ({scope}): installed by an older gitvow, missing {', '.join(missing)}",
|
|
79
|
+
f"re-run `gitvow install {'--user ' if scope == 'user' else ''}--agent {agent}`",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
94
82
|
checks += _agent_notes(agent, home, top)
|
|
95
83
|
if not found_any:
|
|
96
84
|
checks.append(
|
|
@@ -121,7 +109,7 @@ def _git_checks(cwd: str, home: str) -> list[Check]:
|
|
|
121
109
|
checks.append(("fail", "core.hooksPath is not set, so no commit gets a trailer", "run `gitvow install --user`"))
|
|
122
110
|
return checks
|
|
123
111
|
hooks_dir = hp if os.path.isabs(hp) else os.path.join(top, hp)
|
|
124
|
-
missing = [h for h in
|
|
112
|
+
missing = [h for h in GIT_HOOKS_EXPECTED if not os.access(os.path.join(hooks_dir, h), os.X_OK)]
|
|
125
113
|
if missing:
|
|
126
114
|
checks.append(
|
|
127
115
|
(
|
|
@@ -131,7 +119,17 @@ def _git_checks(cwd: str, home: str) -> list[Check]:
|
|
|
131
119
|
)
|
|
132
120
|
)
|
|
133
121
|
else:
|
|
134
|
-
|
|
122
|
+
stale = stale_git_hooks(hooks_dir)
|
|
123
|
+
if stale:
|
|
124
|
+
checks.append(
|
|
125
|
+
(
|
|
126
|
+
"fail",
|
|
127
|
+
f"git hooks written by an older gitvow: {', '.join(stale)}",
|
|
128
|
+
"re-run `gitvow install` here; behaviour added since that install is not active",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
checks.append(("ok", f"git hooks in {hp}: {', '.join(GIT_HOOKS_EXPECTED)}", ""))
|
|
135
133
|
rc, refs, _ = git(["config", "--get-all", "notes.displayRef"], top)
|
|
136
134
|
if NOTES_GLOB not in (refs or ""):
|
|
137
135
|
checks.append(
|
|
@@ -179,9 +177,12 @@ def render(checks: list[Check]) -> str:
|
|
|
179
177
|
if fix:
|
|
180
178
|
lines.append(" " + fix)
|
|
181
179
|
fails = sum(1 for s, _, _ in checks if s == "fail")
|
|
182
|
-
notes = sum(1 for s, _,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
f"{fails} failing
|
|
186
|
-
|
|
180
|
+
notes = sum(1 for s, _, fix in checks if s == "note" and fix) # only notes that ask something of you
|
|
181
|
+
tail = f" {notes} step{'s' if notes != 1 else ''} left to you above." if notes else ""
|
|
182
|
+
if fails:
|
|
183
|
+
lines += ["", f"{fails} failing: the record is not being written until those are fixed.{tail}"]
|
|
184
|
+
elif any(s == "note" and "no hook log" in w for s, w, _ in checks):
|
|
185
|
+
lines += ["", f"Ready: run a session and commit, or `gitvow selftest` to see it work now.{tail}"]
|
|
186
|
+
else:
|
|
187
|
+
lines += ["", f"The record is live here.{tail}"]
|
|
187
188
|
return "\n".join(lines) + "\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitvow
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
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
5
|
Author-email: Nikhil Bora <nikhil@wirevow.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -227,7 +227,6 @@ def test_install_per_agent_idempotent_and_reversible(home, repo):
|
|
|
227
227
|
# hooks are on by default; what install must say is that they are skipped until trusted
|
|
228
228
|
assert any("/hooks" in x and "trust" in x for x in out)
|
|
229
229
|
assert any("writable_roots" in x for x in out)
|
|
230
|
-
assert any("gitvow status" in x for x in out)
|
|
231
230
|
if agent == "cursor":
|
|
232
231
|
assert s["version"] == 1 and s["hooks"][key][0]["failClosed"] is True
|
|
233
232
|
if agent == "gemini":
|
|
@@ -19,11 +19,11 @@ def test_a_clean_install_reports_nothing_failing(repo, home, monkeypatch):
|
|
|
19
19
|
assert not [c for c in checks if c[0] == "fail"], checks
|
|
20
20
|
assert _first(checks, "policy loads")[0] == "ok"
|
|
21
21
|
assert _first(checks, "redaction rules load")[0] == "ok"
|
|
22
|
-
assert "prepare-commit-msg, pre-
|
|
22
|
+
assert "prepare-commit-msg, pre-push, pre-commit, post-commit" in _first(checks, "git hooks in")[1]
|
|
23
23
|
# no session has run in this fixture repo yet, and status says so rather than implying it works
|
|
24
24
|
assert _first(checks, "no hook log")[0] == "note"
|
|
25
25
|
text = render(checks)
|
|
26
|
-
assert text.startswith("gitvow ") and "
|
|
26
|
+
assert text.startswith("gitvow ") and "Ready: run a session and commit" in text
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def test_an_unreachable_hook_command_fails_loudly(repo, home, monkeypatch):
|
|
@@ -88,6 +88,41 @@ def test_cli_status_exit_code_and_json(repo, home, monkeypatch, capsys):
|
|
|
88
88
|
def test_install_prints_the_codex_steps(repo, home):
|
|
89
89
|
out = install_user(str(home), agent="codex")
|
|
90
90
|
assert any("/hooks" in x and "trust" in x for x in out)
|
|
91
|
-
assert any("gitvow status" in x for x in out)
|
|
92
91
|
assert not [x for x in install_user(str(home)) if "/hooks" in x] # claude has no extra step
|
|
93
92
|
assert Path(home, ".claude", "settings.json").exists()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_install_without_an_agent_configures_everything_it_finds(repo, home, monkeypatch, capsys):
|
|
96
|
+
"""Forgetting --agent must not silently leave an agent ungated."""
|
|
97
|
+
from gitvow.install import detect_agents
|
|
98
|
+
|
|
99
|
+
monkeypatch.chdir(repo)
|
|
100
|
+
for d in (".codex", ".cursor"):
|
|
101
|
+
os.makedirs(home / d, exist_ok=True)
|
|
102
|
+
assert set(detect_agents(str(home))) >= {"codex", "cursor"}
|
|
103
|
+
assert cli.main(["install", str(repo)]) == 0
|
|
104
|
+
out = capsys.readouterr().out
|
|
105
|
+
assert "agents found here:" in out and "codex" in out and "cursor" in out
|
|
106
|
+
assert (repo / ".codex" / "hooks.json").exists() and (repo / ".cursor" / "hooks.json").exists()
|
|
107
|
+
assert "next: run `gitvow status`" in out
|
|
108
|
+
# and uninstall with no --agent clears every one of them, leaving any hooks of your own in place
|
|
109
|
+
from gitvow.install import hook_commands
|
|
110
|
+
|
|
111
|
+
assert cli.main(["uninstall", str(repo)]) == 0
|
|
112
|
+
for rel in (".codex/hooks.json", ".cursor/hooks.json", ".claude/settings.json"):
|
|
113
|
+
assert hook_commands(str(repo / rel)) == [], rel
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_an_old_install_is_reported_as_stale(repo, home, monkeypatch):
|
|
117
|
+
monkeypatch.chdir(repo)
|
|
118
|
+
install_repo(str(repo))
|
|
119
|
+
hooks = repo / ".gitvow" / "git-hooks"
|
|
120
|
+
(hooks / "pre-commit").write_text("#!/bin/sh\n# an older gitvow wrote this\nexit 0\n")
|
|
121
|
+
c = _first(build(str(repo), str(home)), "written by an older gitvow")
|
|
122
|
+
assert c and c[0] == "fail" and "pre-commit" in c[1] and "re-run" in c[2]
|
|
123
|
+
# an agent settings file from before an event existed
|
|
124
|
+
settings = json.loads((repo / ".claude" / "settings.json").read_text())
|
|
125
|
+
settings["hooks"].pop("Stop", None)
|
|
126
|
+
(repo / ".claude" / "settings.json").write_text(json.dumps(settings))
|
|
127
|
+
c = _first(build(str(repo), str(home)), "installed by an older gitvow")
|
|
128
|
+
assert c and c[0] == "fail" and "Stop" in c[1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|