xone-cli 0.1.0__py3-none-any.whl

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.
xone_cli/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.1.0"
4
+
xone_cli/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from xone_cli.cli import entrypoint
4
+
5
+
6
+ if __name__ == "__main__":
7
+ entrypoint()
8
+
xone_cli/cli.py ADDED
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from collections.abc import Sequence
8
+
9
+ from xone_cli import __version__
10
+ from xone_cli.evidence import build_failure_packet, collect_evidence, collect_for_runbook, gate_evidence
11
+ from xone_cli.lab import render_evidence_loop_lab
12
+ from xone_cli.risk import render_risk_context
13
+ from xone_cli.runbook import write_runbook
14
+ from xone_cli.release import print_release_report, verify_release
15
+ from xone_cli.tooling import doctor_status
16
+
17
+
18
+ def build_parser() -> argparse.ArgumentParser:
19
+ parser = argparse.ArgumentParser(prog="xone")
20
+ parser.add_argument("--version", action="store_true", help="show version and exit")
21
+ subparsers = parser.add_subparsers(dest="command")
22
+
23
+ doctor = subparsers.add_parser("doctor", help="check local X-One tool availability")
24
+ doctor.add_argument("--json", action="store_true", help="write machine-readable status")
25
+ doctor.add_argument("--dry-run", action="store_true", help="show checks without running them")
26
+
27
+ evidence = subparsers.add_parser("evidence", help="work with PR evidence and failure packets")
28
+ evidence_subparsers = evidence.add_subparsers(dest="evidence_command")
29
+ collect = evidence_subparsers.add_parser("collect", help="collect PR evidence")
30
+ _add_evidence_collection_args(collect)
31
+ collect.add_argument("--output", type=Path, required=True, help="JSON evidence output path")
32
+ collect.add_argument("--dry-run", action="store_true")
33
+
34
+ gate = evidence_subparsers.add_parser("gate", help="run baseline gate")
35
+ _add_evidence_collection_args(gate)
36
+ gate.add_argument("--baseline", type=Path, required=True)
37
+ gate.add_argument("--dry-run", action="store_true")
38
+
39
+ packet = evidence_subparsers.add_parser("packet", help="build a redacted failure packet")
40
+ packet.add_argument("--input", type=Path, required=True)
41
+ packet.add_argument("--output", type=Path, required=True)
42
+ packet.add_argument("--profile", choices=("incident", "issue"), default="issue")
43
+ packet.add_argument("--dry-run", action="store_true")
44
+
45
+ risk = subparsers.add_parser("risk", help="work with MCP risk context")
46
+ risk_subparsers = risk.add_subparsers(dest="risk_command")
47
+ context = risk_subparsers.add_parser("context", help="render risk context")
48
+ context.add_argument("--catalog", type=Path, required=True)
49
+ context.add_argument("--output", type=Path, required=True)
50
+ context.add_argument("--dry-run", action="store_true")
51
+
52
+ lab = subparsers.add_parser("lab", help="work with safe-local lab scenarios")
53
+ lab_subparsers = lab.add_subparsers(dest="lab_command")
54
+ evidence_loop = lab_subparsers.add_parser("evidence-loop", help="render Agent Evidence Loop scenario")
55
+ evidence_loop.add_argument("--output", type=Path, required=True)
56
+ evidence_loop.add_argument("--dry-run", action="store_true")
57
+
58
+ release = subparsers.add_parser("release", help="verify local release readiness")
59
+ release_subparsers = release.add_subparsers(dest="release_command")
60
+ verify = release_subparsers.add_parser("verify", help="run release/package checks")
61
+ verify.add_argument("--project-root", type=Path, default=Path("."))
62
+ verify.add_argument("--build", action="store_true")
63
+ verify.add_argument("--install", action="store_true")
64
+ verify.add_argument("--smoke", action="store_true")
65
+ verify.add_argument("--json", action="store_true")
66
+
67
+ runbook = subparsers.add_parser("runbook", help="assemble a local X-One evidence runbook")
68
+ _add_evidence_collection_args(runbook)
69
+ runbook.add_argument("--output", type=Path)
70
+ runbook.add_argument("--dry-run", action="store_true", help="show underlying commands without running them")
71
+ runbook.add_argument("--json", action="store_true", help="write machine-readable summary")
72
+
73
+ return parser
74
+
75
+
76
+ def main(argv: Sequence[str] | None = None) -> int:
77
+ parser = build_parser()
78
+ args = parser.parse_args(argv)
79
+
80
+ if args.version:
81
+ print(f"xone {__version__}")
82
+ return 0
83
+
84
+ if args.command == "doctor":
85
+ report = doctor_status()
86
+ payload = report.to_dict()
87
+ payload["dry_run"] = args.dry_run
88
+ if args.json:
89
+ print(json.dumps(payload, indent=2, sort_keys=True))
90
+ else:
91
+ _print_doctor(report)
92
+ return 0
93
+
94
+ if args.command == "evidence":
95
+ if args.evidence_command == "collect":
96
+ return collect_evidence(
97
+ repo=args.repo,
98
+ base=args.base,
99
+ head=args.head,
100
+ output=args.output,
101
+ test_logs=args.test_log,
102
+ profile=args.profile,
103
+ dry_run=args.dry_run,
104
+ )
105
+ if args.evidence_command == "gate":
106
+ return gate_evidence(
107
+ repo=args.repo,
108
+ base=args.base,
109
+ head=args.head,
110
+ baseline=args.baseline,
111
+ profile=args.profile,
112
+ dry_run=args.dry_run,
113
+ )
114
+ if args.evidence_command == "packet":
115
+ return build_failure_packet(
116
+ input_path=args.input,
117
+ output=args.output,
118
+ profile=args.profile,
119
+ dry_run=args.dry_run,
120
+ )
121
+
122
+ if args.command == "risk" and args.risk_command == "context":
123
+ return render_risk_context(catalog=args.catalog, output=args.output, dry_run=args.dry_run)
124
+
125
+ if args.command == "lab" and args.lab_command == "evidence-loop":
126
+ return render_evidence_loop_lab(output=args.output, dry_run=args.dry_run)
127
+
128
+ if args.command == "release" and args.release_command == "verify":
129
+ report = verify_release(
130
+ project_root=args.project_root,
131
+ build=args.build,
132
+ install=args.install,
133
+ smoke=args.smoke,
134
+ )
135
+ print_release_report(report, as_json=args.json)
136
+ return 0 if report["ok"] else 1
137
+
138
+ if args.command == "runbook":
139
+ code, report, message = collect_for_runbook(
140
+ repo=args.repo,
141
+ base=args.base,
142
+ head=args.head,
143
+ test_logs=args.test_log,
144
+ profile=args.profile,
145
+ dry_run=args.dry_run,
146
+ )
147
+ if code != 0:
148
+ print(message)
149
+ return code
150
+ if args.dry_run:
151
+ print(message)
152
+ return 0
153
+ payload = {
154
+ "schema_version": "xone.runbook.v1",
155
+ "handoff_decision": (report or {}).get("handoff_decision"),
156
+ }
157
+ if args.json:
158
+ print(json.dumps(payload, indent=2, sort_keys=True))
159
+ if args.output and report is not None:
160
+ write_runbook(report, args.output)
161
+ elif report is not None:
162
+ print(json.dumps(report, indent=2, sort_keys=True))
163
+ return 0
164
+
165
+ parser.print_help(sys.stderr)
166
+ return 2
167
+
168
+
169
+ def entrypoint() -> None:
170
+ raise SystemExit(main())
171
+
172
+
173
+ def _print_doctor(report) -> None:
174
+ print("X-One toolchain")
175
+ for tool in report.tools:
176
+ status = "ok" if tool.available else "missing"
177
+ print(f"- {tool.name}: {status}")
178
+ if tool.version:
179
+ print(f" version: {tool.version}")
180
+ if not tool.available:
181
+ print(f" install: {tool.install_hint}")
182
+
183
+
184
+ def _add_evidence_collection_args(parser: argparse.ArgumentParser) -> None:
185
+ parser.add_argument("--repo", default=".", help="git repository path")
186
+ parser.add_argument("--base", required=True, help="base git ref")
187
+ parser.add_argument("--head", required=True, help="head git ref")
188
+ parser.add_argument("--test-log", action="append", default=[], help="test log path; repeatable")
189
+ parser.add_argument("--profile", choices=("default", "strict"), default="strict")
xone_cli/evidence.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from xone_cli.tooling import run_command
8
+
9
+
10
+ def collect_evidence(
11
+ *,
12
+ repo: str,
13
+ base: str,
14
+ head: str,
15
+ output: Path | None,
16
+ test_logs: list[str],
17
+ profile: str,
18
+ dry_run: bool = False,
19
+ ) -> int:
20
+ command = [
21
+ "agent-pr-evidence",
22
+ "collect",
23
+ "--repo",
24
+ repo,
25
+ "--base",
26
+ base,
27
+ "--head",
28
+ head,
29
+ "--profile",
30
+ profile,
31
+ "--format",
32
+ "json",
33
+ ]
34
+ for test_log in test_logs:
35
+ command.extend(["--test-log", test_log])
36
+ if output:
37
+ command.extend(["--output", str(output)])
38
+ result = run_command(command, dry_run=dry_run)
39
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
40
+ if result.stderr:
41
+ print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
42
+ return result.returncode
43
+
44
+
45
+ def gate_evidence(
46
+ *,
47
+ repo: str,
48
+ base: str,
49
+ head: str,
50
+ baseline: Path,
51
+ profile: str,
52
+ dry_run: bool = False,
53
+ ) -> int:
54
+ command = [
55
+ "agent-pr-evidence",
56
+ "gate",
57
+ "--repo",
58
+ repo,
59
+ "--base",
60
+ base,
61
+ "--head",
62
+ head,
63
+ "--profile",
64
+ profile,
65
+ "--baseline",
66
+ str(baseline),
67
+ "--format",
68
+ "json",
69
+ ]
70
+ result = run_command(command, dry_run=dry_run)
71
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
72
+ if result.stderr:
73
+ print(result.stderr, end="" if result.stderr.endswith("\n") else "\n")
74
+ return result.returncode
75
+
76
+
77
+ def build_failure_packet(*, input_path: Path, output: Path, profile: str, dry_run: bool = False) -> int:
78
+ validate = run_command(["agent-failure-packet", "validate", "--input", str(input_path)], dry_run=dry_run)
79
+ print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
80
+ if validate.stderr:
81
+ print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
82
+ if validate.returncode != 0:
83
+ return validate.returncode
84
+
85
+ build = run_command(
86
+ [
87
+ "agent-failure-packet",
88
+ "build",
89
+ "--input",
90
+ str(input_path),
91
+ "--profile",
92
+ profile,
93
+ "--output",
94
+ str(output),
95
+ ],
96
+ dry_run=dry_run,
97
+ )
98
+ print(build.stdout, end="" if build.stdout.endswith("\n") else "\n")
99
+ if build.stderr:
100
+ print(build.stderr, end="" if build.stderr.endswith("\n") else "\n")
101
+ return build.returncode
102
+
103
+
104
+ def collect_for_runbook(
105
+ *,
106
+ repo: str,
107
+ base: str,
108
+ head: str,
109
+ test_logs: list[str],
110
+ profile: str,
111
+ dry_run: bool,
112
+ ) -> tuple[int, dict | None, str]:
113
+ command = [
114
+ "agent-pr-evidence",
115
+ "collect",
116
+ "--repo",
117
+ repo,
118
+ "--base",
119
+ base,
120
+ "--head",
121
+ head,
122
+ "--profile",
123
+ profile,
124
+ "--format",
125
+ "json",
126
+ ]
127
+ for test_log in test_logs:
128
+ command.extend(["--test-log", test_log])
129
+ if dry_run:
130
+ result = run_command(command, dry_run=True)
131
+ return result.returncode, None, result.stdout
132
+ with tempfile.TemporaryDirectory(prefix="xone-runbook-") as tmp:
133
+ output = Path(tmp) / "evidence.json"
134
+ result = run_command([*command, "--output", str(output)])
135
+ if result.returncode != 0:
136
+ return result.returncode, None, result.stderr or result.stdout
137
+ return 0, json.loads(output.read_text(encoding="utf-8")), ""
138
+
xone_cli/lab.py ADDED
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from xone_cli.tooling import run_command
7
+
8
+
9
+ def render_evidence_loop_lab(*, output: Path, dry_run: bool = False) -> int:
10
+ with tempfile.TemporaryDirectory(prefix="xone-lab-") as tmp:
11
+ scenario_dir = Path(tmp) / "scenarios"
12
+ init = run_command(["ai-incident-lab", "init", "--output", str(scenario_dir)], dry_run=dry_run)
13
+ print(init.stdout, end="" if init.stdout.endswith("\n") else "\n")
14
+ if init.stderr:
15
+ print(init.stderr, end="" if init.stderr.endswith("\n") else "\n")
16
+ if init.returncode != 0:
17
+ return init.returncode
18
+
19
+ scenario = scenario_dir / "agent-evidence-loop.yml"
20
+ validate = run_command(["ai-incident-lab", "validate", "--scenarios", str(scenario_dir)], dry_run=dry_run)
21
+ print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
22
+ if validate.stderr:
23
+ print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
24
+ if validate.returncode != 0:
25
+ return validate.returncode
26
+
27
+ render = run_command(
28
+ ["ai-incident-lab", "render", "--scenarios", str(scenario), "--format", "markdown"],
29
+ dry_run=dry_run,
30
+ )
31
+ if render.returncode == 0 and not dry_run:
32
+ output.write_text(render.stdout, encoding="utf-8")
33
+ else:
34
+ print(render.stdout, end="" if render.stdout.endswith("\n") else "\n")
35
+ if render.stderr:
36
+ print(render.stderr, end="" if render.stderr.endswith("\n") else "\n")
37
+ return render.returncode
38
+
xone_cli/model.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class CommandResult:
8
+ command: list[str]
9
+ returncode: int
10
+ stdout: str
11
+ stderr: str
12
+ dry_run: bool = False
13
+
14
+ def to_dict(self) -> dict:
15
+ return asdict(self)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ToolStatus:
20
+ name: str
21
+ executable: str | None
22
+ available: bool
23
+ version: str | None
24
+ install_hint: str
25
+
26
+ def to_dict(self) -> dict:
27
+ return asdict(self)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class DoctorReport:
32
+ schema_version: str
33
+ tools: list[ToolStatus]
34
+
35
+ @property
36
+ def ok(self) -> bool:
37
+ return all(tool.available for tool in self.tools)
38
+
39
+ def to_dict(self) -> dict:
40
+ data = asdict(self)
41
+ data["ok"] = self.ok
42
+ return data
43
+
xone_cli/release.py ADDED
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from xone_cli.tooling import run_command
9
+
10
+ RELEASE_VERIFY_SCHEMA_VERSION = "xone.release.verify.v1"
11
+
12
+ REQUIRED_RELEASE_FILES = (
13
+ "pyproject.toml",
14
+ "README.md",
15
+ "README.zh-CN.md",
16
+ "CHANGELOG.md",
17
+ "LICENSE",
18
+ ".github/workflows/ci.yml",
19
+ ".github/workflows/publish.yml",
20
+ )
21
+
22
+
23
+ def required_files_status(project_root: Path) -> dict[str, bool]:
24
+ return {name: (project_root / name).exists() for name in REQUIRED_RELEASE_FILES}
25
+
26
+
27
+ def verify_release(
28
+ *,
29
+ project_root: Path,
30
+ build: bool,
31
+ install: bool,
32
+ smoke: bool,
33
+ ) -> dict:
34
+ project_root = project_root.resolve()
35
+ required_files = required_files_status(project_root)
36
+ checks: list[dict] = []
37
+ release_env = _release_subprocess_env()
38
+ smoke_commands: list[list[str]] = [
39
+ ["python", "-m", "xone_cli", "--version"],
40
+ ["python", "-m", "xone_cli", "doctor", "--json"],
41
+ ]
42
+
43
+ if build:
44
+ result = run_command(["python", "-m", "build"], dry_run=False, cwd=project_root, env=release_env)
45
+ checks.append(_check("build", result.returncode, result.stdout, result.stderr))
46
+
47
+ if install:
48
+ with tempfile.TemporaryDirectory(prefix="xone-release-venv-") as tmp:
49
+ venv_dir = Path(tmp) / "venv"
50
+ venv = run_command(["python", "-m", "venv", str(venv_dir)])
51
+ checks.append(_check("venv", venv.returncode, venv.stdout, venv.stderr))
52
+ if venv.returncode == 0:
53
+ wheel = _latest_wheel(project_root)
54
+ if wheel:
55
+ pip = venv_dir / "bin" / "pip"
56
+ installed = run_command([str(pip), "install", str(wheel)], env=release_env)
57
+ checks.append(_check("wheel-install", installed.returncode, installed.stdout, installed.stderr))
58
+ xone = venv_dir / "bin" / "xone"
59
+ smoke_commands = [
60
+ [str(xone), "--version"],
61
+ [str(xone), "doctor", "--json"],
62
+ ]
63
+ else:
64
+ checks.append({"name": "wheel-install", "returncode": 1, "stdout": "", "stderr": "No wheel found in dist/"})
65
+
66
+ if smoke:
67
+ for name, command in (("cli-version", smoke_commands[0]), ("doctor-json", smoke_commands[1])):
68
+ result = run_command(command, cwd=project_root, env=release_env)
69
+ checks.append(_check(name, result.returncode, result.stdout, result.stderr))
70
+ elif smoke:
71
+ for name, command in (("cli-version", smoke_commands[0]), ("doctor-json", smoke_commands[1])):
72
+ result = run_command(command, cwd=project_root, env=release_env)
73
+ checks.append(_check(name, result.returncode, result.stdout, result.stderr))
74
+
75
+ ok = all(required_files.values()) and all(check["returncode"] == 0 for check in checks)
76
+ return {
77
+ "schema_version": RELEASE_VERIFY_SCHEMA_VERSION,
78
+ "ok": ok,
79
+ "required_files": required_files,
80
+ "checks": checks,
81
+ }
82
+
83
+
84
+ def print_release_report(report: dict, *, as_json: bool) -> None:
85
+ if as_json:
86
+ print(json.dumps(report, indent=2, sort_keys=True))
87
+ return
88
+ print("X-One release verification")
89
+ print(f"ok: {str(report['ok']).lower()}")
90
+ print("required files:")
91
+ for name, exists in report["required_files"].items():
92
+ print(f"- {name}: {'ok' if exists else 'missing'}")
93
+ if report["checks"]:
94
+ print("checks:")
95
+ for check in report["checks"]:
96
+ print(f"- {check['name']}: {'ok' if check['returncode'] == 0 else 'failed'}")
97
+
98
+
99
+ def _check(name: str, returncode: int, stdout: str, stderr: str) -> dict:
100
+ return {"name": name, "returncode": returncode, "stdout": stdout[-2000:], "stderr": stderr[-2000:]}
101
+
102
+
103
+ def _latest_wheel(project_root: Path) -> Path | None:
104
+ wheels = sorted((project_root / "dist").glob("*.whl"), key=lambda path: path.stat().st_mtime, reverse=True)
105
+ return wheels[0] if wheels else None
106
+
107
+
108
+ def _release_subprocess_env() -> dict[str, str]:
109
+ env = dict(os.environ)
110
+ env.pop("PYTHONPATH", None)
111
+ return env
xone_cli/risk.py ADDED
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from xone_cli.tooling import run_command
6
+
7
+ RISK_CONTEXT_BOUNDARY = "This is review context, not an allow/deny decision."
8
+
9
+
10
+ def render_risk_context(*, catalog: Path, output: Path, dry_run: bool = False) -> int:
11
+ validate = run_command(["mcp-risk-index", "validate", "--catalog", str(catalog), "--strict"], dry_run=dry_run)
12
+ print(validate.stdout, end="" if validate.stdout.endswith("\n") else "\n")
13
+ if validate.stderr:
14
+ print(validate.stderr, end="" if validate.stderr.endswith("\n") else "\n")
15
+ if validate.returncode != 0:
16
+ return validate.returncode
17
+
18
+ rendered = run_command(["mcp-risk-index", "render", "--catalog", str(catalog), "--format", "markdown"], dry_run=dry_run)
19
+ if rendered.returncode == 0 and not dry_run:
20
+ output.write_text(f"> {RISK_CONTEXT_BOUNDARY}\n\n{rendered.stdout}", encoding="utf-8")
21
+ else:
22
+ print(rendered.stdout, end="" if rendered.stdout.endswith("\n") else "\n")
23
+ if rendered.stderr:
24
+ print(rendered.stderr, end="" if rendered.stderr.endswith("\n") else "\n")
25
+ return rendered.returncode
26
+
xone_cli/runbook.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def render_runbook(report: dict) -> str:
8
+ decision = report.get("handoff_decision") or {}
9
+ lines = [
10
+ "# X-One Evidence Runbook",
11
+ "",
12
+ "## Handoff Decision",
13
+ "",
14
+ f"- Decision: `{decision.get('decision', 'unknown')}`",
15
+ f"- Reason: {decision.get('reason', 'not provided')}",
16
+ f"- Evidence source: {decision.get('evidence_source', 'not provided')}",
17
+ f"- Handoff target: {decision.get('handoff_target', 'not provided')}",
18
+ f"- Revisit trigger: {decision.get('revisit_trigger', 'not provided')}",
19
+ "",
20
+ "## Next Action",
21
+ "",
22
+ _next_action(decision.get("decision")),
23
+ "",
24
+ "## Raw Summary",
25
+ "",
26
+ "```json",
27
+ json.dumps(report.get("summary", {}), indent=2, sort_keys=True),
28
+ "```",
29
+ "",
30
+ ]
31
+ return "\n".join(lines)
32
+
33
+
34
+ def write_runbook(report: dict, output: Path) -> None:
35
+ output.write_text(render_runbook(report), encoding="utf-8")
36
+
37
+
38
+ def _next_action(decision: str | None) -> str:
39
+ if decision == "create-failure-packet":
40
+ return "- Build a redacted failure packet with `xone evidence packet`."
41
+ if decision == "block-before-merge":
42
+ return "- Add passing tests, an accountable owner, or a remediation plan before merge."
43
+ if decision == "baseline-review":
44
+ return "- Review the baseline before accepting new or existing risk."
45
+ if decision == "request-test-evidence":
46
+ return "- Provide test logs or explain why tests are not required."
47
+ return "- Continue normal review."
48
+
xone_cli/tooling.py ADDED
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ import shutil
5
+ import subprocess
6
+ from collections.abc import Mapping, Sequence
7
+ from pathlib import Path
8
+
9
+ from xone_cli.model import CommandResult, DoctorReport, ToolStatus
10
+
11
+ DOCTOR_SCHEMA_VERSION = "xone.doctor.v1"
12
+
13
+ REQUIRED_TOOLS = (
14
+ "agent-pr-evidence",
15
+ "agent-failure-packet",
16
+ "mcp-risk-index",
17
+ "ai-incident-lab",
18
+ )
19
+
20
+ PACKAGE_BY_TOOL = {
21
+ "agent-pr-evidence": "xone-agent-pr-evidence",
22
+ "agent-failure-packet": "xone-agent-failure-packet",
23
+ "mcp-risk-index": "xone-mcp-risk-index",
24
+ "ai-incident-lab": "xone-ai-incident-lab",
25
+ }
26
+
27
+
28
+ def find_tool(name: str) -> str | None:
29
+ return shutil.which(name)
30
+
31
+
32
+ def install_hint(name: str) -> str:
33
+ package = PACKAGE_BY_TOOL.get(name, name)
34
+ return f"python -m pip install {package}"
35
+
36
+
37
+ def run_command(
38
+ command: Sequence[str],
39
+ *,
40
+ dry_run: bool = False,
41
+ cwd: Path | None = None,
42
+ env: Mapping[str, str] | None = None,
43
+ ) -> CommandResult:
44
+ command_list = [str(part) for part in command]
45
+ if dry_run:
46
+ return CommandResult(
47
+ command=command_list,
48
+ returncode=0,
49
+ stdout=f"DRY RUN: {_format_command(command_list)}",
50
+ stderr="",
51
+ dry_run=True,
52
+ )
53
+
54
+ try:
55
+ completed = subprocess.run(
56
+ command_list,
57
+ check=False,
58
+ text=True,
59
+ stdout=subprocess.PIPE,
60
+ stderr=subprocess.PIPE,
61
+ cwd=cwd,
62
+ env=dict(env) if env is not None else None,
63
+ )
64
+ except FileNotFoundError as error:
65
+ return CommandResult(
66
+ command=command_list,
67
+ returncode=127,
68
+ stdout="",
69
+ stderr=str(error),
70
+ )
71
+ return CommandResult(
72
+ command=command_list,
73
+ returncode=completed.returncode,
74
+ stdout=completed.stdout,
75
+ stderr=completed.stderr,
76
+ )
77
+
78
+
79
+ def doctor_status() -> DoctorReport:
80
+ tools = [_tool_status(name) for name in REQUIRED_TOOLS]
81
+ return DoctorReport(schema_version=DOCTOR_SCHEMA_VERSION, tools=tools)
82
+
83
+
84
+ def _tool_status(name: str) -> ToolStatus:
85
+ executable = find_tool(name)
86
+ if not executable:
87
+ return ToolStatus(
88
+ name=name,
89
+ executable=None,
90
+ available=False,
91
+ version=None,
92
+ install_hint=install_hint(name),
93
+ )
94
+ version_result = run_command([name, "--version"])
95
+ version = version_result.stdout.strip() if version_result.returncode == 0 else None
96
+ return ToolStatus(
97
+ name=name,
98
+ executable=executable,
99
+ available=True,
100
+ version=version,
101
+ install_hint=install_hint(name),
102
+ )
103
+
104
+
105
+ def _format_command(command: Sequence[str]) -> str:
106
+ return " ".join(shlex.quote(part) for part in command)
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: xone-cli
3
+ Version: 0.1.0
4
+ Summary: Unified CLI entry point for X-One Agent Evidence Loop workflows.
5
+ Author: X-One-AI
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/X-One-AI/xone-cli
8
+ Project-URL: Repository, https://github.com/X-One-AI/xone-cli
9
+ Project-URL: Issues, https://github.com/X-One-AI/xone-cli/issues
10
+ Keywords: ai,agents,devsecops,mcp,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2.2; extra == "dev"
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # xone-cli
29
+
30
+ Languages: English | [中文](./README.zh-CN.md)
31
+
32
+ `xone-cli` is the unified local entry point for X-One Agent Evidence Loop workflows.
33
+
34
+ It helps developers move from scattered tools to one clear path:
35
+
36
+ ```text
37
+ collect PR evidence
38
+ -> decide handoff
39
+ -> create failure packet when needed
40
+ -> attach MCP risk context when useful
41
+ -> run safe-local training scenarios
42
+ ```
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ python -m pip install xone-cli
48
+ xone --version
49
+ xone doctor
50
+ ```
51
+
52
+ ## First Run
53
+
54
+ ```bash
55
+ xone doctor
56
+ xone runbook --base main --head HEAD --dry-run
57
+ ```
58
+
59
+ `xone-cli` orchestrates these X-One tools:
60
+
61
+ - `agent-pr-evidence`
62
+ - `agent-failure-packet`
63
+ - `mcp-risk-index`
64
+ - `ai-incident-lab`
65
+
66
+ ## Boundary
67
+
68
+ - It does not replace the underlying tools.
69
+ - It does not post GitHub comments.
70
+ - It does not modify repositories automatically.
71
+ - It does not make allow/deny runtime enforcement decisions.
72
+ - It does not make hidden network calls.
73
+
74
+ ## Docs
75
+
76
+ - [Product Foundation](./docs/product-foundation.md)
77
+ - [Install](./docs/install.md)
78
+ - [Release](./docs/release.md)
79
+ - [Open-source Feedback Ledger](./docs/feedback/open-source-feedback-ledger.md)
@@ -0,0 +1,16 @@
1
+ xone_cli/__init__.py,sha256=YosWCpCE15wFKb9PYPbECNR0HI0P7moX8U0yX319veg,59
2
+ xone_cli/__main__.py,sha256=J4Wq8cwjCVitF0nyuoNwPmGAthKhoaPiVobP6l3_e-w,119
3
+ xone_cli/cli.py,sha256=gek02J0PgTRYdnHOSdhCf5veIHzBTP4j73VFnFS1q1w,7760
4
+ xone_cli/evidence.py,sha256=9Dbzb-GTVxaf2RVrKgLwvlI8S9qeEaXJoW2LfmB4bIA,3571
5
+ xone_cli/lab.py,sha256=8KU7Pti_1Co9NY5wmR1zHcEgzOGMalA2zWu_3EWTtrk,1631
6
+ xone_cli/model.py,sha256=XRm1-8n8HXCkfj8Nx7vDDpKnCGDF11MlMO1_Zem2J9o,804
7
+ xone_cli/release.py,sha256=RcbjD-ydgOhzQfwagU1A3MqAtBOb9hFooEmMeJuBViY,4186
8
+ xone_cli/risk.py,sha256=-XV6drlXUwrolpfU5lxxsJTpu3y5BbcebWNRpgoERXI,1142
9
+ xone_cli/runbook.py,sha256=twUO0yjF4K8vpJA_dBMSa1HvF9-_vuzXDuInjtTpbBY,1649
10
+ xone_cli/tooling.py,sha256=c2BRssDHRWFQaX2jvHd5yDLXNcrsj4nbEShtEtV5Les,2790
11
+ xone_cli-0.1.0.dist-info/licenses/LICENSE,sha256=2AV3_k1rPe0sVkPEDXEeDKTvt3wS9rQm0Yu7wcRBCVw,1066
12
+ xone_cli-0.1.0.dist-info/METADATA,sha256=5YVycYbFK3AVZh8OoMIaEx1i2UHZO788W7mdEDdCoFs,2157
13
+ xone_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ xone_cli-0.1.0.dist-info/entry_points.txt,sha256=hH0QkGff99qEymxv5xd8lJE9zoXjVf7qsixgX4yMPMA,49
15
+ xone_cli-0.1.0.dist-info/top_level.txt,sha256=Q8zVoHnDEJIJHvSd8tvff5HWUXeQgqfq7o3SA7Ir-lk,9
16
+ xone_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ xone = xone_cli.cli:entrypoint
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 X-One-AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1 @@
1
+ xone_cli