conduct-cli 0.7.4__tar.gz → 0.7.6__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.
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/PKG-INFO +1 -1
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/pyproject.toml +1 -1
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/guard.py +8 -2
- conduct_cli-0.7.6/src/conduct_cli/hooks/session_report_push.py +137 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/stop.py +17 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/README.md +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/setup.cfg +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/setup.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_log_util.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.7.4 → conduct_cli-0.7.6}/tests/test_switch.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.6"
|
|
8
8
|
description = "CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -33,8 +33,14 @@ def active_policy_path() -> Path:
|
|
|
33
33
|
_THIN_LAUNCHERS = {
|
|
34
34
|
"pretooluse": (
|
|
35
35
|
"#!/usr/bin/env python3\n"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"import sys\n"
|
|
37
|
+
"_cmd = sys.argv[1] if len(sys.argv) > 1 else ''\n"
|
|
38
|
+
"if _cmd == 'post':\n"
|
|
39
|
+
" try:\n from conduct_cli.hooks.posttooluse import main; main()\n"
|
|
40
|
+
" except (ImportError, ModuleNotFoundError): sys.exit(0)\n"
|
|
41
|
+
"else:\n"
|
|
42
|
+
" try:\n from conduct_cli.hooks.pretooluse import main; main()\n"
|
|
43
|
+
" except (ImportError, ModuleNotFoundError): sys.exit(0)\n"
|
|
38
44
|
),
|
|
39
45
|
"posttooluse": (
|
|
40
46
|
"#!/usr/bin/env python3\n"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Standalone background job: run paxel on the current session and push results
|
|
2
|
+
to /guard/session-reports. Invoked as a detached subprocess from the Stop hook
|
|
3
|
+
so it never blocks session close.
|
|
4
|
+
|
|
5
|
+
Usage (internal):
|
|
6
|
+
python -m conduct_cli.hooks.session_report_push <session_id>
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import getpass
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_config() -> dict:
|
|
21
|
+
for p in [
|
|
22
|
+
Path.home() / ".conductguard" / "config.json",
|
|
23
|
+
Path.home() / ".conduct" / "config.json",
|
|
24
|
+
]:
|
|
25
|
+
if p.exists():
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(p.read_text())
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(session_id: str = "") -> None:
|
|
34
|
+
cfg = _load_config()
|
|
35
|
+
server = (cfg.get("api_url") or cfg.get("server") or "https://api.conductai.ai").rstrip("/")
|
|
36
|
+
api_key = cfg.get("api_key", "")
|
|
37
|
+
member_token = cfg.get("member_token", "")
|
|
38
|
+
workspace_id = cfg.get("workspace_id", "")
|
|
39
|
+
|
|
40
|
+
if not server or not workspace_id:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
bundled = Path(__file__).parent.parent / "paxel.py"
|
|
44
|
+
if not bundled.exists():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
tmpdir = Path(tempfile.mkdtemp(prefix="conduct-paxel-bg-"))
|
|
48
|
+
try:
|
|
49
|
+
paxel_script = tmpdir / "paxel.py"
|
|
50
|
+
shutil.copy(bundled, paxel_script)
|
|
51
|
+
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[sys.executable, str(paxel_script), "--no-open"],
|
|
54
|
+
cwd=str(tmpdir),
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=120,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
stats_path = tmpdir / "stats.json"
|
|
61
|
+
report_path = tmpdir / "report.md"
|
|
62
|
+
|
|
63
|
+
if not stats_path.exists():
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
stats = json.loads(stats_path.read_text())
|
|
67
|
+
report_md = report_path.read_text() if report_path.exists() else ""
|
|
68
|
+
|
|
69
|
+
volume = stats.get("volume", {})
|
|
70
|
+
behavior = stats.get("behavior", {})
|
|
71
|
+
autonomy = stats.get("autonomy", {})
|
|
72
|
+
tools = stats.get("tools", {})
|
|
73
|
+
velocity = stats.get("velocity", {})
|
|
74
|
+
|
|
75
|
+
sessions = volume.get("total_sessions", 0)
|
|
76
|
+
prompts = volume.get("total_prompts", 0)
|
|
77
|
+
active_days = volume.get("active_days", 0)
|
|
78
|
+
autonomy_score = autonomy.get("autonomy_score_0_100")
|
|
79
|
+
planning_ratio = behavior.get("planning_ratio_explore_to_doing", 0)
|
|
80
|
+
commits = velocity.get("git_commits_real") if isinstance(velocity, dict) else None
|
|
81
|
+
lines_per_hr = velocity.get("git_lines_per_active_hour") if isinstance(velocity, dict) else None
|
|
82
|
+
top_tools = dict(tools.get("top_tools") or [])
|
|
83
|
+
|
|
84
|
+
# Skip push for trivial sessions (< 3 prompts — not worth reporting)
|
|
85
|
+
if int(prompts or 0) < 3:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if autonomy_score is not None and float(autonomy_score) >= 70:
|
|
89
|
+
archetype = "Autonomous Builder"
|
|
90
|
+
elif planning_ratio and float(planning_ratio) > 1.0:
|
|
91
|
+
archetype = "Strategic Planner"
|
|
92
|
+
else:
|
|
93
|
+
archetype = "Execution-Focused Builder"
|
|
94
|
+
|
|
95
|
+
email = cfg.get("user_email") or getpass.getuser()
|
|
96
|
+
|
|
97
|
+
payload = json.dumps({
|
|
98
|
+
"developer_email": email,
|
|
99
|
+
"archetype": archetype,
|
|
100
|
+
"autonomy_score": float(autonomy_score) if autonomy_score is not None else None,
|
|
101
|
+
"planning_ratio": float(planning_ratio) if planning_ratio else None,
|
|
102
|
+
"sessions": int(sessions or 0),
|
|
103
|
+
"prompts": int(prompts or 0),
|
|
104
|
+
"commits": int(commits) if commits is not None else 0,
|
|
105
|
+
"lines_per_hour": float(lines_per_hr) if lines_per_hr is not None else None,
|
|
106
|
+
"active_days": int(active_days or 0),
|
|
107
|
+
"tools_json": top_tools,
|
|
108
|
+
"report_md": report_md[:50000] if report_md else None,
|
|
109
|
+
"source": "auto",
|
|
110
|
+
}).encode()
|
|
111
|
+
|
|
112
|
+
headers: dict[str, str] = {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
"X-Workspace-ID": workspace_id,
|
|
115
|
+
}
|
|
116
|
+
if member_token:
|
|
117
|
+
headers["Authorization"] = f"Bearer {member_token}"
|
|
118
|
+
elif api_key:
|
|
119
|
+
headers["X-API-Key"] = api_key
|
|
120
|
+
|
|
121
|
+
req = urllib.request.Request(
|
|
122
|
+
f"{server}/guard/session-reports",
|
|
123
|
+
data=payload,
|
|
124
|
+
headers=headers,
|
|
125
|
+
method="POST",
|
|
126
|
+
)
|
|
127
|
+
urllib.request.urlopen(req, timeout=15)
|
|
128
|
+
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
finally:
|
|
132
|
+
shutil.rmtree(str(tmpdir), ignore_errors=True)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
session_id = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
137
|
+
run(session_id)
|
|
@@ -58,6 +58,20 @@ def _post_intent(session_id: str, ai_tool: str, cwd: str | None) -> None:
|
|
|
58
58
|
print("Guard: session parse failed", file=sys.stderr)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
def _spawn_session_report(session_id: str) -> None:
|
|
62
|
+
"""Fire paxel analysis + Guard push in a detached subprocess. Never blocks."""
|
|
63
|
+
try:
|
|
64
|
+
import subprocess
|
|
65
|
+
subprocess.Popen(
|
|
66
|
+
[sys.executable, "-m", "conduct_cli.hooks.session_report_push", session_id],
|
|
67
|
+
start_new_session=True,
|
|
68
|
+
stdout=subprocess.DEVNULL,
|
|
69
|
+
stderr=subprocess.DEVNULL,
|
|
70
|
+
)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
61
75
|
def main() -> None:
|
|
62
76
|
try:
|
|
63
77
|
raw = sys.stdin.read()
|
|
@@ -81,6 +95,9 @@ def main() -> None:
|
|
|
81
95
|
except Exception:
|
|
82
96
|
pass
|
|
83
97
|
|
|
98
|
+
# Fire paxel analysis in background — non-blocking, posts to /guard/session-reports
|
|
99
|
+
_spawn_session_report(session_id)
|
|
100
|
+
|
|
84
101
|
sys.exit(0)
|
|
85
102
|
|
|
86
103
|
|
|
@@ -22,6 +22,7 @@ src/conduct_cli/hooks/posttooluse.py
|
|
|
22
22
|
src/conduct_cli/hooks/precompact.py
|
|
23
23
|
src/conduct_cli/hooks/pretooluse.py
|
|
24
24
|
src/conduct_cli/hooks/session_parser.py
|
|
25
|
+
src/conduct_cli/hooks/session_report_push.py
|
|
25
26
|
src/conduct_cli/hooks/session_start.py
|
|
26
27
|
src/conduct_cli/hooks/stop.py
|
|
27
28
|
tests/test_advisory_and_hook.py
|
|
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
|