govrail 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.
govrail-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: govrail
3
+ Version: 0.1.0
4
+ Summary: A language-agnostic governance plane for agent-driven development: gates and notes, delivered by the gov CLI.
5
+ Author: govrail
6
+ License: MIT
7
+ Keywords: agent,governance,gates,notes,development
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # govrail
15
+
16
+ English | [中文](README.zh.md)
17
+
18
+ A language-agnostic governance plane for agent-driven development: coding
19
+ agents work fast in parallel while machines — not vigilance — hold the quality
20
+ line. The only runtime dependency is Python 3.
21
+
22
+ The plane ships two mechanisms: **gates** (any promise a command can check
23
+ becomes a mechanical check) and **notes** (every non-trivial change records the
24
+ decision, what it beat, and the consequences). Bilingual pairing keeps the
25
+ external-presentation docs in sync.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pip install . # or: uv tool install . / pipx install .
31
+ ```
32
+
33
+ This puts the `gov` CLI on your PATH (stdlib only — no third-party
34
+ dependencies). It has one subcommand per action:
35
+
36
+ ```sh
37
+ gov init --project <path> # inject the plane into an existing project
38
+ gov uninstall --project <path> # reverse it exactly
39
+ gov run --mode all # run the project's gate DAG
40
+ gov self-test # prove every governance gate can reject
41
+ gov verify-pairing --write # re-confirm a bilingual pair after editing one side
42
+ gov change-scope --base <ref> # smallest sufficient check set for a diff
43
+ ```
44
+
45
+ `init` is non-invasive and idempotent: it creates `.gov/rules.md`, adds
46
+ `gates.json` and the notes README only when missing, appends one reference line
47
+ to AGENTS.md, and never overwrites the project's own files. `uninstall` reverses
48
+ it exactly.
49
+
50
+ ## What is inside
51
+
52
+ - `gov/` — the Python package: `gates` (the DAG runner over `gates.json`),
53
+ `verify_notes` (three required sections), `verify_translation_pairing`
54
+ (git blob hashes), `change_scope`, `self_test`, `archive_notes`.
55
+ - `gov/templates/` — the rules, default `gates.json`, and notes format that
56
+ `gov init` injects into a project.
57
+ - `.gov/rules.md` — the single source of truth for the rules.
58
+ - `.agents/notes/` — the decision-record format and lifecycle.
59
+
60
+ ## Origin
61
+
62
+ The mechanisms are distilled from the DeepSeek Harness repository, whose
63
+ gates-over-prose axiom shaped this template. Kept: the governance plane. Left to
64
+ you: the product plane. The locked design decisions live in
65
+ [docs/decisions.md](docs/decisions.md).
@@ -0,0 +1,52 @@
1
+ # govrail
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ A language-agnostic governance plane for agent-driven development: coding
6
+ agents work fast in parallel while machines — not vigilance — hold the quality
7
+ line. The only runtime dependency is Python 3.
8
+
9
+ The plane ships two mechanisms: **gates** (any promise a command can check
10
+ becomes a mechanical check) and **notes** (every non-trivial change records the
11
+ decision, what it beat, and the consequences). Bilingual pairing keeps the
12
+ external-presentation docs in sync.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pip install . # or: uv tool install . / pipx install .
18
+ ```
19
+
20
+ This puts the `gov` CLI on your PATH (stdlib only — no third-party
21
+ dependencies). It has one subcommand per action:
22
+
23
+ ```sh
24
+ gov init --project <path> # inject the plane into an existing project
25
+ gov uninstall --project <path> # reverse it exactly
26
+ gov run --mode all # run the project's gate DAG
27
+ gov self-test # prove every governance gate can reject
28
+ gov verify-pairing --write # re-confirm a bilingual pair after editing one side
29
+ gov change-scope --base <ref> # smallest sufficient check set for a diff
30
+ ```
31
+
32
+ `init` is non-invasive and idempotent: it creates `.gov/rules.md`, adds
33
+ `gates.json` and the notes README only when missing, appends one reference line
34
+ to AGENTS.md, and never overwrites the project's own files. `uninstall` reverses
35
+ it exactly.
36
+
37
+ ## What is inside
38
+
39
+ - `gov/` — the Python package: `gates` (the DAG runner over `gates.json`),
40
+ `verify_notes` (three required sections), `verify_translation_pairing`
41
+ (git blob hashes), `change_scope`, `self_test`, `archive_notes`.
42
+ - `gov/templates/` — the rules, default `gates.json`, and notes format that
43
+ `gov init` injects into a project.
44
+ - `.gov/rules.md` — the single source of truth for the rules.
45
+ - `.agents/notes/` — the decision-record format and lifecycle.
46
+
47
+ ## Origin
48
+
49
+ The mechanisms are distilled from the DeepSeek Harness repository, whose
50
+ gates-over-prose axiom shaped this template. Kept: the governance plane. Left to
51
+ you: the product plane. The locked design decisions live in
52
+ [docs/decisions.md](docs/decisions.md).
@@ -0,0 +1,7 @@
1
+ """govrail — a language-agnostic governance plane for agent-driven development.
2
+
3
+ The plane ships two mechanisms: gates (mechanical checks) and notes (decision
4
+ records), delivered by the ``gov`` CLI. The only runtime dependency is Python 3.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python3
2
+ """Seal the archived-notes manifest: recompute sha256 for every archived note.
3
+
4
+ Archived notes are frozen (D5); the manifest pins each file's content hash so
5
+ any later edit is detectable. Run this after moving a note into ``archived/``.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ from pathlib import Path
12
+
13
+ ARCHIVED = Path(".agents/notes/archived")
14
+
15
+
16
+ def main() -> int:
17
+ files: dict[str, dict[str, str]] = {}
18
+ for p in sorted(ARCHIVED.rglob("*.md")):
19
+ rel = str(p.relative_to(ARCHIVED))
20
+ files[rel] = {"sha256": hashlib.sha256(p.read_bytes()).hexdigest()}
21
+ (ARCHIVED / "manifest.json").write_text(
22
+ json.dumps({"files": files}, indent=2) + "\n", encoding="utf-8"
23
+ )
24
+ print(f"archive_notes: sealed {len(files)} file(s)")
25
+ return 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ raise SystemExit(main())
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Report what a change touches, and suggest the smallest gate set.
3
+
4
+ This is the "check only what changed" hint (rule 9). It does not run gates;
5
+ it maps the touched surfaces to the gates that cover them so a developer
6
+ picks the smallest sufficient set instead of reflexively running everything.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import subprocess
12
+ import sys
13
+
14
+ SURFACE_GATES = {
15
+ "governance": ["self-test"],
16
+ "notes": ["notes"],
17
+ "docs": ["pairing", "links"],
18
+ "config": ["self-test"],
19
+ }
20
+
21
+
22
+ def _classify(path: str) -> str:
23
+ if path.startswith(".agents/notes/"):
24
+ return "notes"
25
+ if path.endswith(".md"):
26
+ return "docs"
27
+ if path in ("gates.json",) or path.startswith(("gates.py", "verify_", "self_test", "gov.py", "change_scope")):
28
+ return "governance"
29
+ return "code"
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(description="Report touched surfaces since a base ref.")
34
+ parser.add_argument("--base", default="HEAD~1", help="git ref to diff against")
35
+ args = parser.parse_args(argv)
36
+ try:
37
+ proc = subprocess.run(
38
+ ["git", "diff", "--name-only", args.base],
39
+ capture_output=True,
40
+ text=True,
41
+ check=True,
42
+ )
43
+ except subprocess.CalledProcessError as e:
44
+ print(f"change_scope: git diff failed: {e.stderr.strip()}", file=sys.stderr)
45
+ return 2
46
+ files = [f for f in proc.stdout.splitlines() if f]
47
+ if not files:
48
+ print(f"change_scope: no changes since {args.base}")
49
+ return 0
50
+
51
+ surfaces = sorted({_classify(f) for f in files})
52
+ gates = sorted({g for s in surfaces for g in SURFACE_GATES.get(s, [])})
53
+ print(f"touched surfaces: {', '.join(surfaces)}")
54
+ for s in surfaces:
55
+ changed = [f for f in files if _classify(f) == s]
56
+ print(f" {s}: {len(changed)} file(s)")
57
+ print(f"suggested gates: {', '.join(gates) or 'code gates (project toolchain)'}")
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python3
2
+ """gov — install, uninstall, run, and self-test the governance plane.
3
+
4
+ Subcommands delegate to the modules in this package; ``init`` injects the
5
+ templates shipped as package data, and ``uninstall`` reverses it exactly via
6
+ the ``.gov/manifest.json`` it wrote.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import shutil
12
+ import sys
13
+ from importlib.resources import files
14
+ from pathlib import Path
15
+
16
+ from . import archive_notes, change_scope, gates, self_test
17
+ from . import verify_notes, verify_translation_pairing
18
+ from . import __version__
19
+
20
+ TEMPLATES = files("gov.templates")
21
+ REFERENCE_MARKER = "<!-- gov:rules -->"
22
+ REFERENCE_LINE = (
23
+ f"{REFERENCE_MARKER} Read .gov/rules.md and follow it before starting work."
24
+ )
25
+
26
+
27
+ def _copy(source, dest: Path) -> None:
28
+ dest.parent.mkdir(parents=True, exist_ok=True)
29
+ with source.open("rb") as f:
30
+ dest.write_bytes(f.read())
31
+
32
+
33
+ def _remove_empty_dirs(root: Path) -> None:
34
+ """Remove empty parent dirs, deepest first, stopping at the first non-empty."""
35
+ p = root
36
+ while p != p.parent:
37
+ try:
38
+ p.rmdir()
39
+ except OSError:
40
+ break
41
+ p = p.parent
42
+
43
+
44
+ def init(project: Path) -> int:
45
+ project = project.resolve()
46
+ if not project.is_dir():
47
+ print(f"init: {project} is not a directory", file=sys.stderr)
48
+ return 2
49
+ if (project / ".gov" / "manifest.json").exists():
50
+ print(f"init: {project} is already initialized")
51
+ return 0
52
+
53
+ gov_dir = project / ".gov"
54
+ created: list[str] = []
55
+
56
+ _copy(TEMPLATES.joinpath("rules.md"), gov_dir / "rules.md")
57
+
58
+ if not (project / "gates.json").exists():
59
+ _copy(TEMPLATES.joinpath("gates.json"), project / "gates.json")
60
+ created.append("gates.json")
61
+
62
+ notes_readme = project / ".agents" / "notes" / "README.md"
63
+ if not notes_readme.exists():
64
+ _copy(TEMPLATES.joinpath("notes-README.md"), notes_readme)
65
+ created.append(".agents/notes/README.md")
66
+
67
+ ag = project / "AGENTS.md"
68
+ if ag.exists():
69
+ text = ag.read_text(encoding="utf-8")
70
+ if REFERENCE_MARKER not in text:
71
+ if text and not text.endswith("\n"):
72
+ text += "\n"
73
+ ag.write_text(text + REFERENCE_LINE + "\n", encoding="utf-8")
74
+ else:
75
+ ag.write_text(REFERENCE_LINE + "\n", encoding="utf-8")
76
+
77
+ (gov_dir / "manifest.json").write_text(
78
+ json.dumps({"version": "0.1.0", "created": created}, indent=2) + "\n",
79
+ encoding="utf-8",
80
+ )
81
+
82
+ print(f"init: initialized {project}")
83
+ print(" .gov/rules.md (rules)")
84
+ if created:
85
+ print(" " + ", ".join(created) + " (created; project had none)")
86
+ print(" AGENTS.md reference line")
87
+ return 0
88
+
89
+
90
+ def uninstall(project: Path) -> int:
91
+ project = project.resolve()
92
+ manifest = project / ".gov" / "manifest.json"
93
+ if not manifest.exists():
94
+ print(f"uninstall: {project} is not initialized", file=sys.stderr)
95
+ return 2
96
+ try:
97
+ data = json.loads(manifest.read_text(encoding="utf-8"))
98
+ except (json.JSONDecodeError, OSError) as e:
99
+ print(f"uninstall: corrupt manifest {manifest}: {e}", file=sys.stderr)
100
+ return 2
101
+
102
+ ag = project / "AGENTS.md"
103
+ if ag.exists():
104
+ kept = [line for line in ag.read_text(encoding="utf-8").splitlines()
105
+ if REFERENCE_MARKER not in line]
106
+ while kept and kept[-1] == "":
107
+ kept.pop()
108
+ if kept:
109
+ ag.write_text("\n".join(kept) + "\n", encoding="utf-8")
110
+ else:
111
+ ag.unlink()
112
+
113
+ for rel in data.get("created", []):
114
+ p = project / rel
115
+ if p.exists():
116
+ p.unlink()
117
+ _remove_empty_dirs(p.parent)
118
+
119
+ shutil.rmtree(project / ".gov", ignore_errors=True)
120
+ print(f"uninstall: removed governance from {project}")
121
+ return 0
122
+
123
+
124
+ _COMMANDS = {
125
+ "init": "inject the plane into a project",
126
+ "uninstall": "reverse init",
127
+ "run": "run the project's gate DAG (args forwarded to gates.py)",
128
+ "self-test": "run governance rejection cases",
129
+ "verify-notes": "check note format",
130
+ "verify-pairing": "check bilingual pairing (e.g. --write)",
131
+ "change-scope": "report touched surfaces (e.g. --base <ref>)",
132
+ "archive-notes": "seal the archived-notes manifest",
133
+ }
134
+
135
+
136
+ def _usage() -> None:
137
+ print("usage: gov <command> [args]", file=sys.stderr)
138
+ print("commands:", file=sys.stderr)
139
+ for name, help_text in _COMMANDS.items():
140
+ print(f" {name:<16} {help_text}", file=sys.stderr)
141
+
142
+
143
+ _HELP_FLAGS = ("-h", "--help", "help")
144
+ _VERSION_FLAGS = ("-v", "--version", "version")
145
+ # Commands whose args are NOT forwarded to an argparse parser: they must
146
+ # intercept help/version themselves so a trailing flag never runs the action.
147
+ _NO_FORWARD = ("init", "uninstall", "self-test", "verify-notes", "archive-notes")
148
+
149
+
150
+ def _init_uninstall_args(args: list[str], what: str) -> Path | None:
151
+ """Parse the only supported flag ``--project <dir>``; reject the rest."""
152
+ project = "."
153
+ i = 0
154
+ while i < len(args):
155
+ a = args[i]
156
+ if a == "--project":
157
+ if i + 1 >= len(args):
158
+ print(f"gov {what}: --project requires a directory", file=sys.stderr)
159
+ return None
160
+ project = args[i + 1]
161
+ i += 2
162
+ else:
163
+ print(f"gov {what}: unexpected argument '{a}'", file=sys.stderr)
164
+ _usage()
165
+ return None
166
+ return Path(project)
167
+
168
+
169
+ def main(argv: list[str] | None = None) -> int:
170
+ argv = list(sys.argv[1:] if argv is None else argv)
171
+ if not argv:
172
+ _usage()
173
+ return 2
174
+ cmd, rest = argv[0], argv[1:]
175
+
176
+ if cmd in _HELP_FLAGS:
177
+ _usage()
178
+ return 0
179
+ if cmd in _VERSION_FLAGS:
180
+ print(f"gov {__version__}")
181
+ return 0
182
+ # Subcommand-level help/version: never execute the action as a side effect.
183
+ if cmd in _NO_FORWARD:
184
+ if any(a in _HELP_FLAGS for a in rest):
185
+ _usage()
186
+ return 0
187
+ if any(a in _VERSION_FLAGS for a in rest):
188
+ print(f"gov {__version__}")
189
+ return 0
190
+ if cmd == "init":
191
+ project = _init_uninstall_args(rest, "init")
192
+ return 2 if project is None else init(project)
193
+ if cmd == "uninstall":
194
+ project = _init_uninstall_args(rest, "uninstall")
195
+ return 2 if project is None else uninstall(project)
196
+ if cmd == "run":
197
+ return gates.main(rest)
198
+ if cmd == "self-test":
199
+ return self_test.main()
200
+ if cmd == "verify-notes":
201
+ return verify_notes.main()
202
+ if cmd == "verify-pairing":
203
+ return verify_translation_pairing.main(rest)
204
+ if cmd == "change-scope":
205
+ return change_scope.main(rest)
206
+ if cmd == "archive-notes":
207
+ return archive_notes.main()
208
+ print(f"gov: unknown command '{cmd}'", file=sys.stderr)
209
+ _usage()
210
+ return 2
211
+
212
+
213
+ if __name__ == "__main__":
214
+ raise SystemExit(main())