agentgov-cli 0.1.8__tar.gz → 0.1.11__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 (32) hide show
  1. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/PKG-INFO +2 -2
  2. agentgov_cli-0.1.11/agentgov_cli/asset_manifest.py +205 -0
  3. agentgov_cli-0.1.11/agentgov_cli/client_assets/commands/workitem.md +58 -0
  4. agentgov_cli-0.1.11/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +759 -0
  5. agentgov_cli-0.1.11/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +194 -0
  6. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/doctor.py +71 -2
  7. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/install.py +137 -50
  8. agentgov_cli-0.1.11/agentgov_cli/commands/status.py +72 -0
  9. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/uninstall.py +56 -2
  10. agentgov_cli-0.1.11/agentgov_cli/commands/workitem.py +377 -0
  11. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/pyproject.toml +4 -4
  12. agentgov_cli-0.1.8/agentgov_cli/client_assets/commands/workitem.md +0 -18
  13. agentgov_cli-0.1.8/agentgov_cli/client_assets/hooks/userpromptsubmit_workitem.py +0 -199
  14. agentgov_cli-0.1.8/agentgov_cli/client_assets/statusline/agentgov_statusline.sh +0 -74
  15. agentgov_cli-0.1.8/agentgov_cli/commands/status.py +0 -46
  16. agentgov_cli-0.1.8/agentgov_cli/commands/workitem.py +0 -72
  17. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/.gitignore +0 -0
  18. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/README.md +0 -0
  19. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/__init__.py +0 -0
  20. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/hooks/pretooluse_pathguard.py +0 -0
  21. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/hooks/sessionstart_register.py +0 -0
  22. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/client_assets/managed-settings.json.tmpl +0 -0
  23. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/__init__.py +0 -0
  24. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/gateway.py +0 -0
  25. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/login.py +0 -0
  26. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/register_device.py +0 -0
  27. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/commands/wrap.py +0 -0
  28. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/gateway_process.py +0 -0
  29. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/loopback.py +0 -0
  30. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/main.py +0 -0
  31. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/agentgov_cli/port_resolver.py +0 -0
  32. {agentgov_cli-0.1.8 → agentgov_cli-0.1.11}/hatch_build.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentgov-cli
3
- Version: 0.1.8
3
+ Version: 0.1.11
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
@@ -0,0 +1,58 @@
1
+ ---
2
+ description: Bind this session to a work item — ticket id, URL, or any short label
3
+ allowed-tools: Bash(agentgov workitem:*)
4
+ argument-hint: <PROJ-1234 | https://tracker/issue/42 | any short label>
5
+ ---
6
+
7
+ !`agentgov workitem --session "${CLAUDE_SESSION_ID}" -- '$ARGUMENTS'`
8
+
9
+ The command above has already run and its output is shown. Relay that output to
10
+ the user verbatim — do not restate, reformat, or interpret it — and then stop.
11
+
12
+ If it reported a successful bind, simply acknowledge it and carry on with
13
+ whatever the user was doing. If it reported a failure, show the message as-is;
14
+ it already contains the remediation step.
15
+
16
+ <!--
17
+ WHY THIS FILE LOOKS THE WAY IT DOES
18
+ ===================================
19
+
20
+ 1. WHY A `!` SHELL BLOCK AND NOT A MODEL-DRIVEN TOOL CALL
21
+ An unbound session is DENIED by the gateway (WorkItemMiddleware →
22
+ NO_WORK_ITEM), so no model turn is possible until the bind succeeds. Asking
23
+ the model to run the bind would deadlock: the request that would fix the
24
+ problem is the request the problem blocks. The bind must therefore complete
25
+ entirely in the shell, before anything is sent upstream.
26
+
27
+ 2. WHY `${CLAUDE_SESSION_ID}` IS PASSED EXPLICITLY
28
+ Claude Code substitutes it into slash-command shell lines, and it is the same
29
+ id sent as `x-claude-code-session-id` on every API request — i.e. exactly the
30
+ key the gateway stores sessions under. Passing it is what lets this work in
31
+ the VS Code extension, with no `agentgov wrap` and no loopback listener on
32
+ :8788. Requiring that listener is what made `/workitem` fail for every editor
33
+ user.
34
+
35
+ 3. WHY `agentgov workitem` NEVER EXITS NON-ZERO
36
+ Claude Code aborts the entire invocation when a `!` command exits non-zero and
37
+ reports `Shell command failed for pattern "..."`. That message is Claude Code
38
+ quoting the shell line it tried to run — it is NOT AgentGov matching the
39
+ user's text against a pattern, and nothing here inspects the format of what
40
+ the user typed. Our real error text was being swallowed by it, which is why
41
+ the failure was unreadable. The CLI now reports every failure as ordinary
42
+ output with exit code 0.
43
+
44
+ 4. WHY `'$ARGUMENTS'` IS SINGLE-QUOTED
45
+ `$ARGUMENTS` is substituted as text into a shell line, so unquoted user input
46
+ would let `$(...)`, backticks, `;`, `|`, `&` and `>` be interpreted by the
47
+ shell. Single quotes make every one of those literal.
48
+
49
+ KNOWN GAP: a literal apostrophe in the user's text (e.g. `don't`) closes the
50
+ quote early and breaks the line. Anthropic's documentation does not state
51
+ whether Claude Code escapes `$ARGUMENTS` before substitution, so this could
52
+ not be settled from the docs and is deliberately not guessed at. The CLI
53
+ defends on its side by stripping a stray surrounding quote pair and by
54
+ joining all argv entries into one reference. The clean fix is a
55
+ `UserPromptExpansion` hook, which receives the typed text as structured JSON
56
+ and never involves a shell — revisit once that hook's schema and exit-code
57
+ semantics are documented.
58
+ -->