devex-cli 0.24.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.
- agent_experience/__init__.py +24 -0
- agent_experience/__main__.py +4 -0
- agent_experience/backends/__init__.py +0 -0
- agent_experience/backends/acp/__init__.py +0 -0
- agent_experience/backends/acp/probe.py +9 -0
- agent_experience/backends/capabilities/acp.yaml +7 -0
- agent_experience/backends/capabilities/claude-code.yaml +4 -0
- agent_experience/backends/capabilities/codex.yaml +7 -0
- agent_experience/backends/capabilities/copilot.yaml +7 -0
- agent_experience/backends/claude_code/__init__.py +0 -0
- agent_experience/backends/claude_code/probe.py +97 -0
- agent_experience/backends/codex/__init__.py +0 -0
- agent_experience/backends/codex/probe.py +16 -0
- agent_experience/backends/copilot/__init__.py +0 -0
- agent_experience/backends/copilot/probe.py +9 -0
- agent_experience/cli.py +485 -0
- agent_experience/commands/__init__.py +0 -0
- agent_experience/commands/doctor/SKILL.md +41 -0
- agent_experience/commands/doctor/__init__.py +0 -0
- agent_experience/commands/doctor/assets/report.md.j2 +39 -0
- agent_experience/commands/doctor/references/design.md +36 -0
- agent_experience/commands/doctor/scripts/__init__.py +0 -0
- agent_experience/commands/doctor/scripts/doctor.py +394 -0
- agent_experience/commands/explain/SKILL.md +26 -0
- agent_experience/commands/explain/__init__.py +0 -0
- agent_experience/commands/explain/assets/topics/agex.md +37 -0
- agent_experience/commands/explain/references/.gitkeep +0 -0
- agent_experience/commands/explain/scripts/__init__.py +0 -0
- agent_experience/commands/explain/scripts/explain.py +64 -0
- agent_experience/commands/gamify/SKILL.md +31 -0
- agent_experience/commands/gamify/__init__.py +0 -0
- agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
- agent_experience/commands/gamify/references/.gitkeep +0 -0
- agent_experience/commands/gamify/scripts/__init__.py +0 -0
- agent_experience/commands/gamify/scripts/install.py +203 -0
- agent_experience/commands/hook/SKILL.md +31 -0
- agent_experience/commands/hook/__init__.py +0 -0
- agent_experience/commands/hook/assets/table.md.j2 +17 -0
- agent_experience/commands/hook/references/.gitkeep +0 -0
- agent_experience/commands/hook/scripts/__init__.py +0 -0
- agent_experience/commands/hook/scripts/read.py +53 -0
- agent_experience/commands/hook/scripts/write.py +25 -0
- agent_experience/commands/learn/SKILL.md +21 -0
- agent_experience/commands/learn/__init__.py +0 -0
- agent_experience/commands/learn/assets/menu.md.j2 +7 -0
- agent_experience/commands/learn/assets/topics/cicd/SKILL.md +103 -0
- agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
- agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
- agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
- agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
- agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
- agent_experience/commands/learn/references/.gitkeep +0 -0
- agent_experience/commands/learn/scripts/__init__.py +0 -0
- agent_experience/commands/learn/scripts/learn.py +73 -0
- agent_experience/commands/overview/SKILL.md +31 -0
- agent_experience/commands/overview/__init__.py +0 -0
- agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
- agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
- agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
- agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
- agent_experience/commands/overview/assets/sections.md.j2 +52 -0
- agent_experience/commands/overview/references/.gitkeep +0 -0
- agent_experience/commands/overview/scripts/__init__.py +0 -0
- agent_experience/commands/overview/scripts/overview.py +40 -0
- agent_experience/commands/pr/SKILL.md +90 -0
- agent_experience/commands/pr/__init__.py +0 -0
- agent_experience/commands/pr/assets/__init__.py +0 -0
- agent_experience/commands/pr/assets/backends/__init__.py +0 -0
- agent_experience/commands/pr/assets/backends/acp.yaml +21 -0
- agent_experience/commands/pr/assets/backends/claude-code.yaml +21 -0
- agent_experience/commands/pr/assets/backends/codex.yaml +21 -0
- agent_experience/commands/pr/assets/backends/copilot.yaml +21 -0
- agent_experience/commands/pr/assets/rules/__init__.py +0 -0
- agent_experience/commands/pr/assets/rules/lint_rules.py +79 -0
- agent_experience/commands/pr/assets/rules/next_step_rules.py +78 -0
- agent_experience/commands/pr/assets/templates/__init__.py +0 -0
- agent_experience/commands/pr/assets/templates/delta.md.j2 +32 -0
- agent_experience/commands/pr/assets/templates/footer.md.j2 +2 -0
- agent_experience/commands/pr/assets/templates/lint_result.md.j2 +19 -0
- agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +69 -0
- agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +17 -0
- agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +15 -0
- agent_experience/commands/pr/assets/templates/pr_review_result.md.j2 +5 -0
- agent_experience/commands/pr/scripts/__init__.py +0 -0
- agent_experience/commands/pr/scripts/_footer.py +32 -0
- agent_experience/commands/pr/scripts/_journal.py +21 -0
- agent_experience/commands/pr/scripts/_qodo.py +147 -0
- agent_experience/commands/pr/scripts/_readiness.py +76 -0
- agent_experience/commands/pr/scripts/_sonar.py +29 -0
- agent_experience/commands/pr/scripts/await_.py +156 -0
- agent_experience/commands/pr/scripts/delta.py +84 -0
- agent_experience/commands/pr/scripts/lint.py +72 -0
- agent_experience/commands/pr/scripts/open_.py +104 -0
- agent_experience/commands/pr/scripts/read.py +151 -0
- agent_experience/commands/pr/scripts/reply.py +160 -0
- agent_experience/commands/pr/scripts/review.py +59 -0
- agent_experience/core/__init__.py +0 -0
- agent_experience/core/backend.py +80 -0
- agent_experience/core/capabilities.py +44 -0
- agent_experience/core/config.py +46 -0
- agent_experience/core/github.py +355 -0
- agent_experience/core/hook_io.py +95 -0
- agent_experience/core/journal.py +90 -0
- agent_experience/core/paths.py +26 -0
- agent_experience/core/prog.py +44 -0
- agent_experience/core/render.py +42 -0
- agent_experience/core/skill_loader.py +36 -0
- devex_cli-0.24.0.dist-info/METADATA +55 -0
- devex_cli-0.24.0.dist-info/RECORD +115 -0
- devex_cli-0.24.0.dist-info/WHEEL +4 -0
- devex_cli-0.24.0.dist-info/entry_points.txt +3 -0
- devex_cli-0.24.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""`agex doctor` — read-only health check.
|
|
2
|
+
|
|
3
|
+
Composes a list of per-check `CheckResult` rows grouped into categories, then
|
|
4
|
+
renders them through the Jinja report template. No side effects: never calls
|
|
5
|
+
``ensure_init`` or touches the filesystem outside of read attempts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from importlib.resources import as_file, files
|
|
14
|
+
from importlib.resources.abc import Traversable
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
from agent_experience import __version__
|
|
21
|
+
from agent_experience.core.paths import (
|
|
22
|
+
GITIGNORE_CONTENT,
|
|
23
|
+
agex_dir,
|
|
24
|
+
config_path,
|
|
25
|
+
data_dir,
|
|
26
|
+
)
|
|
27
|
+
from agent_experience.core.prog import error_prefix
|
|
28
|
+
from agent_experience.core.render import render_string
|
|
29
|
+
from agent_experience.core.skill_loader import load_skill
|
|
30
|
+
|
|
31
|
+
Status = Literal["ok", "warn", "fail", "info"]
|
|
32
|
+
_ROLE_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
33
|
+
_MIN_PYTHON = (3, 10)
|
|
34
|
+
|
|
35
|
+
# Check-row names — kept as constants so each appears once in source
|
|
36
|
+
# (sonar python:S1192).
|
|
37
|
+
_NAME_AGEX_VERSION = "agex version"
|
|
38
|
+
_NAME_PYTHON = "Python"
|
|
39
|
+
_NAME_PACKAGE_RESOURCES = "Package resources"
|
|
40
|
+
_NAME_AGEX_DIR = "`.agex/` directory"
|
|
41
|
+
_NAME_CONFIG_TOML = "`.agex/config.toml`"
|
|
42
|
+
_NAME_GITIGNORE = "`.agex/.gitignore`"
|
|
43
|
+
_NAME_DATA_DIR = "`.agex/data/`"
|
|
44
|
+
_NAME_SKILL_MD = "Shipped SKILL.md frontmatter"
|
|
45
|
+
_NAME_CAPABILITY_YAML = "Backend capability YAML"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CheckResult:
|
|
50
|
+
name: str
|
|
51
|
+
status: Status
|
|
52
|
+
detail: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Category:
|
|
57
|
+
title: str
|
|
58
|
+
results: list[CheckResult] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _commands_root() -> Traversable:
|
|
62
|
+
return files("agent_experience.commands")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _doctor_assets() -> Traversable:
|
|
66
|
+
return files("agent_experience.commands").joinpath("doctor", "assets")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --- Install checks --------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_version() -> CheckResult:
|
|
73
|
+
if not __version__:
|
|
74
|
+
return CheckResult(
|
|
75
|
+
_NAME_AGEX_VERSION,
|
|
76
|
+
"fail",
|
|
77
|
+
"Could not resolve `agent_experience.__version__`. Reinstall with "
|
|
78
|
+
"`uv pip install -e .[dev]`, `pipx install agex-cli`, "
|
|
79
|
+
"`pipx install agent-devex`, or `pipx install devex-cli`.",
|
|
80
|
+
)
|
|
81
|
+
return CheckResult(_NAME_AGEX_VERSION, "ok", __version__)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check_python() -> CheckResult:
|
|
85
|
+
cur = sys.version_info[:3]
|
|
86
|
+
detail = ".".join(str(p) for p in cur)
|
|
87
|
+
if cur[:2] < _MIN_PYTHON:
|
|
88
|
+
return CheckResult(
|
|
89
|
+
_NAME_PYTHON,
|
|
90
|
+
"fail",
|
|
91
|
+
f"{detail} (need >= {_MIN_PYTHON[0]}.{_MIN_PYTHON[1]})",
|
|
92
|
+
)
|
|
93
|
+
return CheckResult(_NAME_PYTHON, "ok", detail)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _check_resources() -> CheckResult:
|
|
97
|
+
# Probe assets that doctor itself depends on at runtime, plus a sentinel
|
|
98
|
+
# from a sibling command — broken packaging surfaces here rather than
|
|
99
|
+
# later when a render call raises.
|
|
100
|
+
required = [
|
|
101
|
+
("explain", "assets", "topics", "agex.md"),
|
|
102
|
+
("doctor", "assets", "report.md.j2"),
|
|
103
|
+
]
|
|
104
|
+
try:
|
|
105
|
+
cmds = _commands_root()
|
|
106
|
+
for parts in required:
|
|
107
|
+
if not cmds.joinpath(*parts).is_file():
|
|
108
|
+
return CheckResult(
|
|
109
|
+
_NAME_PACKAGE_RESOURCES,
|
|
110
|
+
"fail",
|
|
111
|
+
f"missing shipped asset `commands/{'/'.join(parts)}`. "
|
|
112
|
+
"Reinstall the package.",
|
|
113
|
+
)
|
|
114
|
+
except Exception as exc: # pragma: no cover - defensive only
|
|
115
|
+
return CheckResult(_NAME_PACKAGE_RESOURCES, "fail", f"resource lookup raised: {exc}")
|
|
116
|
+
return CheckResult(_NAME_PACKAGE_RESOURCES, "ok", "all shipped assets reachable")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- Project state checks --------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _check_agex_dir() -> CheckResult:
|
|
123
|
+
root = agex_dir()
|
|
124
|
+
if not root.exists():
|
|
125
|
+
return CheckResult(
|
|
126
|
+
_NAME_AGEX_DIR,
|
|
127
|
+
"info",
|
|
128
|
+
f"not initialized at `{root}` — run any backend-aware command "
|
|
129
|
+
"(e.g. `agex overview --agent claude-code`) to bootstrap.",
|
|
130
|
+
)
|
|
131
|
+
if not root.is_dir():
|
|
132
|
+
return CheckResult(
|
|
133
|
+
_NAME_AGEX_DIR,
|
|
134
|
+
"fail",
|
|
135
|
+
f"`{root}` exists but is not a directory.",
|
|
136
|
+
)
|
|
137
|
+
return CheckResult(_NAME_AGEX_DIR, "ok", str(root))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_config_toml() -> CheckResult:
|
|
141
|
+
path = config_path()
|
|
142
|
+
if not path.exists():
|
|
143
|
+
return CheckResult(
|
|
144
|
+
_NAME_CONFIG_TOML,
|
|
145
|
+
"info",
|
|
146
|
+
"not present (expected when `.agex/` is uninitialized).",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Parse defensively — config.load() raises on malformed TOML, which we
|
|
150
|
+
# catch and surface as a fail row rather than letting bubble up.
|
|
151
|
+
try:
|
|
152
|
+
from agent_experience.core import config as config_module
|
|
153
|
+
|
|
154
|
+
cfg = config_module.load()
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
return CheckResult(
|
|
157
|
+
_NAME_CONFIG_TOML,
|
|
158
|
+
"fail",
|
|
159
|
+
f"failed to parse: {exc}. Edit or delete the file.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if cfg.agex_version and cfg.agex_version != __version__:
|
|
163
|
+
return CheckResult(
|
|
164
|
+
_NAME_CONFIG_TOML,
|
|
165
|
+
"warn",
|
|
166
|
+
(
|
|
167
|
+
f'`agex_version = "{cfg.agex_version}"` does not match installed '
|
|
168
|
+
f"`{__version__}`. Will reconcile on next write."
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
return CheckResult(_NAME_CONFIG_TOML, "ok", f"version {cfg.agex_version}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_gitignore() -> CheckResult:
|
|
175
|
+
root = agex_dir()
|
|
176
|
+
gi = root / ".gitignore"
|
|
177
|
+
if not root.exists():
|
|
178
|
+
return CheckResult(_NAME_GITIGNORE, "info", "skipped (no `.agex/`).")
|
|
179
|
+
if not gi.exists():
|
|
180
|
+
return CheckResult(
|
|
181
|
+
_NAME_GITIGNORE,
|
|
182
|
+
"warn",
|
|
183
|
+
"missing — `data/` may end up tracked. Re-run any agex command to restore.",
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
actual = gi.read_text(encoding="utf-8")
|
|
187
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
188
|
+
return CheckResult(
|
|
189
|
+
_NAME_GITIGNORE,
|
|
190
|
+
"fail",
|
|
191
|
+
f"could not read `{gi}`: {exc}. Check permissions and encoding.",
|
|
192
|
+
)
|
|
193
|
+
if actual != GITIGNORE_CONTENT:
|
|
194
|
+
return CheckResult(
|
|
195
|
+
_NAME_GITIGNORE,
|
|
196
|
+
"warn",
|
|
197
|
+
"content drifted from the managed default — `data/` may not be ignored.",
|
|
198
|
+
)
|
|
199
|
+
return CheckResult(_NAME_GITIGNORE, "ok", "matches managed content")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_data_dir() -> CheckResult:
|
|
203
|
+
if not agex_dir().exists():
|
|
204
|
+
return CheckResult(_NAME_DATA_DIR, "info", "skipped (no `.agex/`).")
|
|
205
|
+
d = data_dir()
|
|
206
|
+
if not d.exists():
|
|
207
|
+
return CheckResult(
|
|
208
|
+
_NAME_DATA_DIR,
|
|
209
|
+
"warn",
|
|
210
|
+
f"`{d}` is missing — re-run any agex command to recreate it.",
|
|
211
|
+
)
|
|
212
|
+
if not d.is_dir():
|
|
213
|
+
return CheckResult(_NAME_DATA_DIR, "fail", f"`{d}` is not a directory.")
|
|
214
|
+
# Read-only contract — no probe write. Just check perms.
|
|
215
|
+
import os
|
|
216
|
+
|
|
217
|
+
if not os.access(d, os.W_OK):
|
|
218
|
+
return CheckResult(_NAME_DATA_DIR, "fail", f"`{d}` is not writable.")
|
|
219
|
+
return CheckResult(_NAME_DATA_DIR, "ok", str(d))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# --- Internal consistency checks -------------------------------------------
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _iter_skill_relpaths() -> list[str]:
|
|
226
|
+
with as_file(_commands_root()) as root:
|
|
227
|
+
return sorted("/".join(p.relative_to(root).parts) for p in root.glob("**/SKILL.md"))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _check_skill_md_consistency() -> CheckResult:
|
|
231
|
+
relpaths = _iter_skill_relpaths()
|
|
232
|
+
if not relpaths:
|
|
233
|
+
return CheckResult(
|
|
234
|
+
_NAME_SKILL_MD,
|
|
235
|
+
"fail",
|
|
236
|
+
"No SKILL.md files discovered — package data is missing.",
|
|
237
|
+
)
|
|
238
|
+
failures: list[str] = []
|
|
239
|
+
with as_file(_commands_root()) as root:
|
|
240
|
+
for rel in relpaths:
|
|
241
|
+
try:
|
|
242
|
+
load_skill(root / rel)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
failures.append(f"{rel}: {exc}")
|
|
245
|
+
if failures:
|
|
246
|
+
return CheckResult(
|
|
247
|
+
_NAME_SKILL_MD,
|
|
248
|
+
"fail",
|
|
249
|
+
f"{len(failures)} of {len(relpaths)} failed: {'; '.join(failures)}",
|
|
250
|
+
)
|
|
251
|
+
return CheckResult(
|
|
252
|
+
_NAME_SKILL_MD,
|
|
253
|
+
"ok",
|
|
254
|
+
f"{len(relpaths)} files parse cleanly",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _check_capability_yaml() -> CheckResult:
|
|
259
|
+
failures: list[str] = []
|
|
260
|
+
count = 0
|
|
261
|
+
with as_file(_commands_root()) as root:
|
|
262
|
+
for path in root.glob("**/assets/backends/*.yaml"):
|
|
263
|
+
count += 1
|
|
264
|
+
try:
|
|
265
|
+
yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
failures.append(f"{path.relative_to(root)}: {exc}")
|
|
268
|
+
if failures:
|
|
269
|
+
return CheckResult(
|
|
270
|
+
_NAME_CAPABILITY_YAML,
|
|
271
|
+
"fail",
|
|
272
|
+
f"{len(failures)} of {count} failed: {'; '.join(failures)}",
|
|
273
|
+
)
|
|
274
|
+
if count == 0:
|
|
275
|
+
# `overview` already ships per-backend YAMLs, so finding zero at
|
|
276
|
+
# runtime indicates a broken wheel rather than expected state.
|
|
277
|
+
return CheckResult(
|
|
278
|
+
_NAME_CAPABILITY_YAML,
|
|
279
|
+
"fail",
|
|
280
|
+
"no per-backend YAML files found — package data is missing. Reinstall.",
|
|
281
|
+
)
|
|
282
|
+
return CheckResult(_NAME_CAPABILITY_YAML, "ok", f"{count} files parse cleanly")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# --- Operator verification (markdown-only, no programmatic check) ----------
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
_OPERATOR_CHECKLIST = [
|
|
289
|
+
"Confirm `.agex/config.toml` is committed and `.agex/data/` is gitignored.",
|
|
290
|
+
"Confirm your shell tool can invoke `agex --version` and `agex doctor`.",
|
|
291
|
+
"If you installed hooks via `agex gamify`, confirm the backend hook file "
|
|
292
|
+
"still contains the `agex:` fragment IDs recorded in `.agex/config.toml`.",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# --- Role section ----------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _resolve_role(role: str) -> Traversable | None:
|
|
300
|
+
if not _ROLE_RE.match(role):
|
|
301
|
+
return None
|
|
302
|
+
candidate = _doctor_assets().joinpath("roles", f"{role}.md.j2")
|
|
303
|
+
return candidate if candidate.is_file() else None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# --- Aggregation -----------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _build_categories() -> list[Category]:
|
|
310
|
+
return [
|
|
311
|
+
Category(
|
|
312
|
+
"Install",
|
|
313
|
+
[_check_version(), _check_python(), _check_resources()],
|
|
314
|
+
),
|
|
315
|
+
Category(
|
|
316
|
+
"Project state",
|
|
317
|
+
[
|
|
318
|
+
_check_agex_dir(),
|
|
319
|
+
_check_config_toml(),
|
|
320
|
+
_check_gitignore(),
|
|
321
|
+
_check_data_dir(),
|
|
322
|
+
],
|
|
323
|
+
),
|
|
324
|
+
Category(
|
|
325
|
+
"Internal consistency",
|
|
326
|
+
[_check_skill_md_consistency(), _check_capability_yaml()],
|
|
327
|
+
),
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _summarize(categories: list[Category]) -> dict[str, int]:
|
|
332
|
+
summary = {"ok": 0, "warn": 0, "fail": 0, "info": 0}
|
|
333
|
+
for cat in categories:
|
|
334
|
+
for r in cat.results:
|
|
335
|
+
summary[r.status] += 1
|
|
336
|
+
return summary
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def run(role: str | None = None) -> tuple[str, int, str]:
|
|
340
|
+
"""Return ``(stdout, exit_code, stderr)``.
|
|
341
|
+
|
|
342
|
+
Read-only. Never initializes ``.agex/``.
|
|
343
|
+
"""
|
|
344
|
+
if role is not None and _ROLE_RE.match(role) is None:
|
|
345
|
+
return ("", 2, error_prefix(f"invalid role slug '{role}'"))
|
|
346
|
+
|
|
347
|
+
role_section: str | None = None
|
|
348
|
+
if role is not None:
|
|
349
|
+
trav = _resolve_role(role)
|
|
350
|
+
if trav is None:
|
|
351
|
+
return ("", 2, error_prefix(f"unknown role '{role}'"))
|
|
352
|
+
try:
|
|
353
|
+
role_text = trav.read_text(encoding="utf-8")
|
|
354
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
355
|
+
return ("", 1, error_prefix(f"could not read role asset for '{role}': {exc}"))
|
|
356
|
+
# Role assets are Jinja templates per the addendum spec; v0.1 passes
|
|
357
|
+
# an empty context but rendering still resolves any `{% raw %}` /
|
|
358
|
+
# `{{ }}` markup the role file may use, rather than dumping it raw.
|
|
359
|
+
try:
|
|
360
|
+
role_section = render_string(role_text, {})
|
|
361
|
+
except Exception as exc:
|
|
362
|
+
return ("", 1, error_prefix(f"failed to render role '{role}': {exc}"))
|
|
363
|
+
|
|
364
|
+
categories = _build_categories()
|
|
365
|
+
summary = _summarize(categories)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
template_text = _doctor_assets().joinpath("report.md.j2").read_text(encoding="utf-8")
|
|
369
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
370
|
+
return (
|
|
371
|
+
"",
|
|
372
|
+
1,
|
|
373
|
+
error_prefix(
|
|
374
|
+
f"could not read `doctor/assets/report.md.j2`: {exc}. "
|
|
375
|
+
"Reinstall the package."
|
|
376
|
+
),
|
|
377
|
+
)
|
|
378
|
+
out = render_string(
|
|
379
|
+
template_text,
|
|
380
|
+
{
|
|
381
|
+
"version": __version__,
|
|
382
|
+
"project_dir": str(Path.cwd()),
|
|
383
|
+
"categories": categories,
|
|
384
|
+
"operator_checklist": _OPERATOR_CHECKLIST,
|
|
385
|
+
"role": role,
|
|
386
|
+
"role_section": role_section,
|
|
387
|
+
"summary": summary,
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if summary["fail"] > 0:
|
|
392
|
+
stderr = error_prefix(f"{summary['fail']} health check(s) failed")
|
|
393
|
+
return (out, 1, stderr)
|
|
394
|
+
return (out, 0, "")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explain
|
|
3
|
+
description: Emit markdown documentation for any agex command, lesson, or concept.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex explain <topic>`
|
|
8
|
+
|
|
9
|
+
Use this to get authoritative, deterministic documentation on an agex command, lesson, or concept without invoking a lesson or running a probe.
|
|
10
|
+
|
|
11
|
+
## How it resolves
|
|
12
|
+
|
|
13
|
+
1. `commands/<topic>/SKILL.md` (command-level, wins if present)
|
|
14
|
+
2. `commands/learn/assets/topics/<topic>/SKILL.md` (lesson-level)
|
|
15
|
+
3. `commands/explain/assets/topics/<topic>.md` (concept-level override)
|
|
16
|
+
|
|
17
|
+
First match wins.
|
|
18
|
+
|
|
19
|
+
## From your shell tool
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agex explain overview
|
|
23
|
+
agex explain gamify
|
|
24
|
+
agex explain levelup
|
|
25
|
+
agex explain agex # self-describing page
|
|
26
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# `agex` — agent-operated developer-experience CLI
|
|
2
|
+
|
|
3
|
+
`agex` is a non-agentic Python CLI that emits deterministic per-backend markdown for autonomous agents. You (the agent) invoke it from your shell tool to learn about and configure your own runtime.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
| Command | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `agex overview --agent X` | Snapshot of the project's current setup for backend X. |
|
|
10
|
+
| `agex learn --agent X` | Menu of lesson topics available for backend X. |
|
|
11
|
+
| `agex learn <topic> --agent X` | Teach a lesson (e.g., introspect, visualize, gamify, levelup). |
|
|
12
|
+
| `agex gamify --agent X` | Install usage-tracking hooks (or unsupported notice). |
|
|
13
|
+
| `agex gamify --uninstall --agent X` | Reverse `gamify`. |
|
|
14
|
+
| `agex hook write <event> [...]` | Append a tracking event. Called by installed hooks. |
|
|
15
|
+
| `agex hook read --agent X` | Show tracked events as markdown + source path. |
|
|
16
|
+
| `agex doctor` | Diagnose agex install + repo setup. |
|
|
17
|
+
| `agex explain <topic>` | You're reading this. |
|
|
18
|
+
|
|
19
|
+
## First steps
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
agex explain agex # this page
|
|
23
|
+
agex doctor # is the install + repo healthy?
|
|
24
|
+
agex learn --agent claude-code # what can I learn for my backend?
|
|
25
|
+
agex overview --agent claude-code # what's in this project?
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Design invariants
|
|
29
|
+
|
|
30
|
+
- **Non-agentic.** Zero LLM calls inside agex. All output is deterministic.
|
|
31
|
+
- **Markdown is the universal format.** No `--json` flag.
|
|
32
|
+
- **`--agent` is required** on backend-sensitive commands.
|
|
33
|
+
- **Unsupported is success.** If your backend lacks a feature, you get a markdown notice + link to file an issue — exit code 0.
|
|
34
|
+
|
|
35
|
+
## Repo
|
|
36
|
+
|
|
37
|
+
<https://github.com/agentculture/devex>
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from importlib.resources.abc import Traversable
|
|
4
|
+
|
|
5
|
+
from agent_experience.core.prog import error_prefix
|
|
6
|
+
from agent_experience.core.skill_loader import Skill, load_skill
|
|
7
|
+
|
|
8
|
+
_TOPIC_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _commands_root() -> Traversable:
|
|
12
|
+
return files("agent_experience.commands")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_topic(topic: str) -> tuple[str, Traversable] | None:
|
|
16
|
+
"""Resolve topic per spec precedence. Returns (kind, traversable) or None.
|
|
17
|
+
|
|
18
|
+
Rejects any topic that isn't a simple slug to prevent path traversal.
|
|
19
|
+
"""
|
|
20
|
+
if not _TOPIC_RE.match(topic):
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
cmds = _commands_root()
|
|
24
|
+
|
|
25
|
+
cmd_skill = cmds.joinpath(topic, "SKILL.md")
|
|
26
|
+
if cmd_skill.is_file():
|
|
27
|
+
return ("command", cmd_skill)
|
|
28
|
+
|
|
29
|
+
lesson_skill = cmds.joinpath("learn", "assets", "topics", topic, "SKILL.md")
|
|
30
|
+
if lesson_skill.is_file():
|
|
31
|
+
return ("lesson", lesson_skill)
|
|
32
|
+
|
|
33
|
+
concept = cmds.joinpath("explain", "assets", "topics", f"{topic}.md")
|
|
34
|
+
if concept.is_file():
|
|
35
|
+
return ("concept", concept)
|
|
36
|
+
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_skill_from_traversable(trav: Traversable) -> Skill:
|
|
41
|
+
# load_skill expects a pathlib.Path; resolve via as_file when needed. Since
|
|
42
|
+
# our package resources are on a real filesystem (hatch force-include), the
|
|
43
|
+
# Traversable is a MultiplexedPath / PosixPath wrapper whose .read_text()
|
|
44
|
+
# works directly. We rebuild a Skill by parsing the body in-line to avoid
|
|
45
|
+
# Path coupling.
|
|
46
|
+
from importlib.resources import as_file
|
|
47
|
+
|
|
48
|
+
with as_file(trav) as path:
|
|
49
|
+
return load_skill(path)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(topic: str) -> tuple[str, int, str]:
|
|
53
|
+
"""Return (stdout, exit_code, stderr)."""
|
|
54
|
+
resolved = resolve_topic(topic)
|
|
55
|
+
if resolved is None:
|
|
56
|
+
agex_page = _commands_root().joinpath("explain", "assets", "topics", "agex.md")
|
|
57
|
+
body = agex_page.read_text(encoding="utf-8") if agex_page.is_file() else ""
|
|
58
|
+
return (body, 2, error_prefix(f"unknown topic '{topic}'"))
|
|
59
|
+
|
|
60
|
+
kind, trav = resolved
|
|
61
|
+
if kind == "concept":
|
|
62
|
+
return (trav.read_text(encoding="utf-8"), 0, "")
|
|
63
|
+
skill = _load_skill_from_traversable(trav)
|
|
64
|
+
return (skill.body, 0, "")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gamify
|
|
3
|
+
description: Install or uninstall backend-native hooks that track usage via agex hook write.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex gamify --agent <backend>` / `agex gamify --uninstall --agent <backend>`
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Writes backend-native hook fragments (each tagged with a stable `agex:*` ID) that call `agex hook write <event>` on PostToolUse, UserPromptSubmit, and Stop events. Agent-authored skills (e.g., `levelup`) read the accumulated data via `agex hook read`.
|
|
12
|
+
|
|
13
|
+
## Why it's safe
|
|
14
|
+
|
|
15
|
+
- Idempotent: re-running is a no-op.
|
|
16
|
+
- Reversible: `--uninstall` removes exactly the `agex:*` fragments; user-authored hooks are untouched.
|
|
17
|
+
- Calling `agex gamify` explicitly is the confirmation — no separate prompt.
|
|
18
|
+
|
|
19
|
+
## Unsupported backends
|
|
20
|
+
|
|
21
|
+
If your backend doesn't support hooks, you get a markdown notice + issue link instead.
|
|
22
|
+
|
|
23
|
+
## From your shell tool
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
agex gamify --agent claude-code
|
|
27
|
+
# ... use your runtime for a while ...
|
|
28
|
+
agex hook read --agent claude-code
|
|
29
|
+
# ... later, to undo:
|
|
30
|
+
agex gamify --uninstall --agent claude-code
|
|
31
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fragments": [
|
|
3
|
+
{
|
|
4
|
+
"id": "agex:post-tool-use",
|
|
5
|
+
"event": "PostToolUse",
|
|
6
|
+
"hook": {
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "agex hook write post-tool-use tool=\"$CLAUDE_TOOL_NAME\""
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "agex:user-prompt",
|
|
13
|
+
"event": "UserPromptSubmit",
|
|
14
|
+
"hook": {
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "agex hook write user-prompt"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "agex:stop",
|
|
21
|
+
"event": "Stop",
|
|
22
|
+
"hook": {
|
|
23
|
+
"type": "command",
|
|
24
|
+
"command": "agex hook write stop"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
File without changes
|
|
File without changes
|