agentgov-cli 0.1.9__tar.gz → 0.1.12__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 (28) hide show
  1. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/.gitignore +3 -0
  2. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/PKG-INFO +2 -2
  3. agentgov_cli-0.1.12/agentgov_cli/asset_manifest.py +205 -0
  4. agentgov_cli-0.1.12/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +792 -0
  5. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +43 -0
  6. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/doctor.py +71 -2
  7. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/install.py +76 -48
  8. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/workitem.py +86 -0
  9. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/pyproject.toml +4 -4
  10. agentgov_cli-0.1.9/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +0 -206
  11. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/README.md +0 -0
  12. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/__init__.py +0 -0
  13. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/commands/workitem.md +0 -0
  14. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py +0 -0
  15. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/hooks/sessionstart_register.py +0 -0
  16. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/client_assets/managed-settings.json.tmpl +0 -0
  17. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/__init__.py +0 -0
  18. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/gateway.py +0 -0
  19. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/login.py +0 -0
  20. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/register_device.py +0 -0
  21. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/status.py +0 -0
  22. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/uninstall.py +0 -0
  23. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/commands/wrap.py +0 -0
  24. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/gateway_process.py +0 -0
  25. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/loopback.py +0 -0
  26. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/main.py +0 -0
  27. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/agentgov_cli/port_resolver.py +0 -0
  28. {agentgov_cli-0.1.9 → agentgov_cli-0.1.12}/hatch_build.py +0 -0
@@ -82,3 +82,6 @@ CLAUDE.local.md
82
82
 
83
83
  # Build artifact: vendored by cli/hatch_build.py from the repo-root copy.
84
84
  cli/agentgov_cli/client_assets/
85
+
86
+ # Scratch dir used by tooling when the system temp dir is unavailable.
87
+ .wf-tmp/
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentgov-cli
3
- Version: 0.1.9
3
+ Version: 0.1.12
4
4
  Summary: AgentGov CLI — wrap Claude Code, bind work items, check gateway health.
5
5
  Author: AgentGov Contributors
6
6
  License: Apache-2.0
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Requires-Dist: httpx>=0.27.2
9
9
  Requires-Dist: pydantic>=2.9.0
10
10
  Requires-Dist: pynacl>=1.5.0
@@ -0,0 +1,205 @@
1
+ """Provenance for the client assets AgentGov installs into ~/.claude/.
2
+
3
+ WHY THIS EXISTS
4
+ ---------------
5
+ Nothing in the product could tell "installed" from "current", and five separate
6
+ layers all needed that one fact:
7
+
8
+ * `agentgov install` skipped the hooks directory whenever it already existed,
9
+ so the documented upgrade (`pip install -U` + `agentgov install`) refreshed
10
+ the CLI and left the OLD hooks in place. Every hook fix we shipped stopped
11
+ at the machines that already had AgentGov.
12
+ * The setup page's Step 0 preflight printed `READY -> nothing to do` on the
13
+ strength of three hook REGISTRATIONS, never asking whether the registered
14
+ files were current — so it actively confirmed a broken machine as healthy.
15
+ * `agentgov doctor` checked presence, executability and registration. Never
16
+ content.
17
+ * `smoke-package` only ever installed into a pristine HOME, so the upgrade
18
+ path that every existing user takes was untested.
19
+
20
+ That combination shipped a `UserPromptSubmit` hook whose block message went
21
+ only to stderr — which the VS Code extension does not surface — so a governed
22
+ session with no work item bound erased the prompt and displayed nothing at all.
23
+ The fix existed in the repo for a while and could not reach anyone.
24
+
25
+ A recorded hash per installed file answers the question all five were asking.
26
+
27
+ WHY THE HASHES AND NOT JUST A VERSION STRING
28
+ --------------------------------------------
29
+ A version alone cannot separate the two reasons an installed file differs from
30
+ the shipped one:
31
+
32
+ STALE we wrote it, and a newer version has since shipped -> overwrite,
33
+ silently, because it is ours and it is out of date.
34
+ MODIFIED it matches neither what we shipped nor what we last wrote -> the
35
+ developer edited it. Back it up before overwriting.
36
+
37
+ Getting that distinction wrong in either direction is bad: silently eating a
38
+ local edit, or burying every upgrade in `.bak` files and scary warnings.
39
+
40
+ WHERE IT LIVES
41
+ --------------
42
+ Inside `~/.claude/agentgov-hooks/`, alongside the assets it describes, so that
43
+ `agentgov uninstall` (which rmtrees that directory) disposes of it without
44
+ needing to know it exists. The leading dot keeps it out of the `*.py` globs
45
+ that install and doctor use to chmod and enumerate hooks.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import hashlib
51
+ import json
52
+ import sys
53
+ from datetime import UTC, datetime
54
+ from pathlib import Path
55
+
56
+ MANIFEST_NAME = ".agentgov-manifest.json"
57
+
58
+ # Asset source is discovered relative to the installed package (client_assets is
59
+ # a sibling of the CLI package). In wheel installs we ship it as package data;
60
+ # in a source checkout we look up-tree.
61
+ ASSET_ROOTS = [
62
+ # Vendored into the package at build time by cli/hatch_build.py. This is the
63
+ # path that works for pip/pipx installs: it is relative to __file__, so it
64
+ # survives relocated venvs and `pip install --user`, unlike sys.prefix.
65
+ Path(__file__).resolve().parent / "client_assets",
66
+ # Source checkout: client_assets/ sits at the repo root (CLAUDE.md §4).
67
+ Path(__file__).resolve().parent.parent.parent / "client_assets",
68
+ # Legacy shared-data location, kept so older installs keep working.
69
+ Path(sys.prefix) / "share" / "agentgov" / "client_assets",
70
+ ]
71
+
72
+ # Which source subdirectory lands in which directory under ~/.claude, and the
73
+ # glob that selects the files install is responsible for. `managed-settings.json.tmpl`
74
+ # sits at the source root and is deliberately absent: it is rendered into
75
+ # ~/.agentgov/ for an admin to place, never copied into ~/.claude.
76
+ _ASSET_DIRS: tuple[tuple[str, str, str], ...] = (
77
+ ("hooks", "agentgov-hooks", "*.py"),
78
+ ("statusline", "agentgov-statusline", "*.sh"),
79
+ ("commands", "commands", "*.md"),
80
+ )
81
+
82
+
83
+ def managed_assets(src: Path) -> list[tuple[str, str]]:
84
+ """(source-relative path, destination dir) for everything install owns.
85
+
86
+ Enumerated from the source tree rather than hardcoded so that adding an
87
+ asset makes it managed automatically. A hardcoded list is precisely what
88
+ let the statusline drift out of date without anything noticing — it was
89
+ never in whatever each layer happened to believe the asset set was.
90
+ """
91
+ out: list[tuple[str, str]] = []
92
+ for sub, dest_dir, pattern in _ASSET_DIRS:
93
+ for path in sorted((src / sub).glob(pattern)):
94
+ out.append((f"{sub}/{path.name}", dest_dir))
95
+ return out
96
+
97
+
98
+ def find_assets() -> Path:
99
+ for root in ASSET_ROOTS:
100
+ if (root / "hooks").is_dir():
101
+ return root
102
+ raise FileNotFoundError(f"client_assets/ not found in any of: {ASSET_ROOTS}")
103
+
104
+
105
+ def cli_version() -> str:
106
+ try:
107
+ from importlib.metadata import version
108
+
109
+ return version("agentgov-cli")
110
+ except Exception:
111
+ return "unknown"
112
+
113
+
114
+ def sha256_file(path: Path) -> str:
115
+ h = hashlib.sha256()
116
+ with path.open("rb") as fh:
117
+ for chunk in iter(lambda: fh.read(65536), b""):
118
+ h.update(chunk)
119
+ return h.hexdigest()
120
+
121
+
122
+ def hooks_dir(home: Path | None = None) -> Path:
123
+ return (home or Path.home()) / ".claude" / "agentgov-hooks"
124
+
125
+
126
+ def manifest_path(home: Path | None = None) -> Path:
127
+ return hooks_dir(home) / MANIFEST_NAME
128
+
129
+
130
+ def installed_path(rel: str, dest_dir: str, home: Path | None = None) -> Path:
131
+ """Where a shipped asset lands on disk."""
132
+ return (home or Path.home()) / ".claude" / dest_dir / Path(rel).name
133
+
134
+
135
+ def load(home: Path | None = None) -> dict[str, str]:
136
+ """Recorded hash per asset from the last install. Empty when absent.
137
+
138
+ Absent is the normal state for every install predating this file, so it
139
+ must degrade quietly rather than raise.
140
+ """
141
+ path = manifest_path(home)
142
+ try:
143
+ data = json.loads(path.read_text())
144
+ except Exception:
145
+ return {}
146
+ assets = data.get("assets")
147
+ return assets if isinstance(assets, dict) else {}
148
+
149
+
150
+ def load_version(home: Path | None = None) -> str:
151
+ try:
152
+ data = json.loads(manifest_path(home).read_text())
153
+ except Exception:
154
+ return ""
155
+ return str(data.get("version") or "")
156
+
157
+
158
+ def save(assets: dict[str, str], home: Path | None = None) -> None:
159
+ path = manifest_path(home)
160
+ path.parent.mkdir(parents=True, exist_ok=True)
161
+ path.write_text(
162
+ json.dumps(
163
+ {
164
+ "version": cli_version(),
165
+ "installed_at": datetime.now(UTC).isoformat(),
166
+ "assets": assets,
167
+ },
168
+ indent=2,
169
+ )
170
+ + "\n"
171
+ )
172
+
173
+
174
+ def classify(rel: str, dest_dir: str, src: Path, home: Path | None = None) -> str:
175
+ """One of MISSING | OK | STALE | MODIFIED for a single installed asset."""
176
+ shipped = src / rel
177
+ target = installed_path(rel, dest_dir, home)
178
+ if not target.exists():
179
+ return "MISSING"
180
+ if not shipped.exists():
181
+ # Nothing to compare against — treat as fine rather than invent a fault.
182
+ return "OK"
183
+ current = sha256_file(target)
184
+ if current == sha256_file(shipped):
185
+ return "OK"
186
+ recorded = load(home).get(rel)
187
+ # No manifest (pre-manifest install) means we cannot prove we wrote it, so
188
+ # it is reported as MODIFIED and install will back it up. That is the safe
189
+ # direction: it happens at most once per machine, and it never destroys an
190
+ # edit we cannot account for.
191
+ return "STALE" if recorded and recorded == current else "MODIFIED"
192
+
193
+
194
+ def drift(home: Path | None = None) -> list[tuple[str, str]]:
195
+ """(relative path, state) for every managed asset that is not OK."""
196
+ try:
197
+ src = find_assets()
198
+ except FileNotFoundError:
199
+ return []
200
+ out: list[tuple[str, str]] = []
201
+ for rel, dest_dir in managed_assets(src):
202
+ state = classify(rel, dest_dir, src, home)
203
+ if state != "OK":
204
+ out.append((rel, state))
205
+ return out