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.
Files changed (115) hide show
  1. agent_experience/__init__.py +24 -0
  2. agent_experience/__main__.py +4 -0
  3. agent_experience/backends/__init__.py +0 -0
  4. agent_experience/backends/acp/__init__.py +0 -0
  5. agent_experience/backends/acp/probe.py +9 -0
  6. agent_experience/backends/capabilities/acp.yaml +7 -0
  7. agent_experience/backends/capabilities/claude-code.yaml +4 -0
  8. agent_experience/backends/capabilities/codex.yaml +7 -0
  9. agent_experience/backends/capabilities/copilot.yaml +7 -0
  10. agent_experience/backends/claude_code/__init__.py +0 -0
  11. agent_experience/backends/claude_code/probe.py +97 -0
  12. agent_experience/backends/codex/__init__.py +0 -0
  13. agent_experience/backends/codex/probe.py +16 -0
  14. agent_experience/backends/copilot/__init__.py +0 -0
  15. agent_experience/backends/copilot/probe.py +9 -0
  16. agent_experience/cli.py +485 -0
  17. agent_experience/commands/__init__.py +0 -0
  18. agent_experience/commands/doctor/SKILL.md +41 -0
  19. agent_experience/commands/doctor/__init__.py +0 -0
  20. agent_experience/commands/doctor/assets/report.md.j2 +39 -0
  21. agent_experience/commands/doctor/references/design.md +36 -0
  22. agent_experience/commands/doctor/scripts/__init__.py +0 -0
  23. agent_experience/commands/doctor/scripts/doctor.py +394 -0
  24. agent_experience/commands/explain/SKILL.md +26 -0
  25. agent_experience/commands/explain/__init__.py +0 -0
  26. agent_experience/commands/explain/assets/topics/agex.md +37 -0
  27. agent_experience/commands/explain/references/.gitkeep +0 -0
  28. agent_experience/commands/explain/scripts/__init__.py +0 -0
  29. agent_experience/commands/explain/scripts/explain.py +64 -0
  30. agent_experience/commands/gamify/SKILL.md +31 -0
  31. agent_experience/commands/gamify/__init__.py +0 -0
  32. agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
  33. agent_experience/commands/gamify/references/.gitkeep +0 -0
  34. agent_experience/commands/gamify/scripts/__init__.py +0 -0
  35. agent_experience/commands/gamify/scripts/install.py +203 -0
  36. agent_experience/commands/hook/SKILL.md +31 -0
  37. agent_experience/commands/hook/__init__.py +0 -0
  38. agent_experience/commands/hook/assets/table.md.j2 +17 -0
  39. agent_experience/commands/hook/references/.gitkeep +0 -0
  40. agent_experience/commands/hook/scripts/__init__.py +0 -0
  41. agent_experience/commands/hook/scripts/read.py +53 -0
  42. agent_experience/commands/hook/scripts/write.py +25 -0
  43. agent_experience/commands/learn/SKILL.md +21 -0
  44. agent_experience/commands/learn/__init__.py +0 -0
  45. agent_experience/commands/learn/assets/menu.md.j2 +7 -0
  46. agent_experience/commands/learn/assets/topics/cicd/SKILL.md +103 -0
  47. agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
  48. agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
  49. agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
  50. agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
  51. agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
  52. agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
  53. agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
  54. agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
  55. agent_experience/commands/learn/references/.gitkeep +0 -0
  56. agent_experience/commands/learn/scripts/__init__.py +0 -0
  57. agent_experience/commands/learn/scripts/learn.py +73 -0
  58. agent_experience/commands/overview/SKILL.md +31 -0
  59. agent_experience/commands/overview/__init__.py +0 -0
  60. agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
  61. agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
  62. agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
  63. agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
  64. agent_experience/commands/overview/assets/sections.md.j2 +52 -0
  65. agent_experience/commands/overview/references/.gitkeep +0 -0
  66. agent_experience/commands/overview/scripts/__init__.py +0 -0
  67. agent_experience/commands/overview/scripts/overview.py +40 -0
  68. agent_experience/commands/pr/SKILL.md +90 -0
  69. agent_experience/commands/pr/__init__.py +0 -0
  70. agent_experience/commands/pr/assets/__init__.py +0 -0
  71. agent_experience/commands/pr/assets/backends/__init__.py +0 -0
  72. agent_experience/commands/pr/assets/backends/acp.yaml +21 -0
  73. agent_experience/commands/pr/assets/backends/claude-code.yaml +21 -0
  74. agent_experience/commands/pr/assets/backends/codex.yaml +21 -0
  75. agent_experience/commands/pr/assets/backends/copilot.yaml +21 -0
  76. agent_experience/commands/pr/assets/rules/__init__.py +0 -0
  77. agent_experience/commands/pr/assets/rules/lint_rules.py +79 -0
  78. agent_experience/commands/pr/assets/rules/next_step_rules.py +78 -0
  79. agent_experience/commands/pr/assets/templates/__init__.py +0 -0
  80. agent_experience/commands/pr/assets/templates/delta.md.j2 +32 -0
  81. agent_experience/commands/pr/assets/templates/footer.md.j2 +2 -0
  82. agent_experience/commands/pr/assets/templates/lint_result.md.j2 +19 -0
  83. agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +69 -0
  84. agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +17 -0
  85. agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +15 -0
  86. agent_experience/commands/pr/assets/templates/pr_review_result.md.j2 +5 -0
  87. agent_experience/commands/pr/scripts/__init__.py +0 -0
  88. agent_experience/commands/pr/scripts/_footer.py +32 -0
  89. agent_experience/commands/pr/scripts/_journal.py +21 -0
  90. agent_experience/commands/pr/scripts/_qodo.py +147 -0
  91. agent_experience/commands/pr/scripts/_readiness.py +76 -0
  92. agent_experience/commands/pr/scripts/_sonar.py +29 -0
  93. agent_experience/commands/pr/scripts/await_.py +156 -0
  94. agent_experience/commands/pr/scripts/delta.py +84 -0
  95. agent_experience/commands/pr/scripts/lint.py +72 -0
  96. agent_experience/commands/pr/scripts/open_.py +104 -0
  97. agent_experience/commands/pr/scripts/read.py +151 -0
  98. agent_experience/commands/pr/scripts/reply.py +160 -0
  99. agent_experience/commands/pr/scripts/review.py +59 -0
  100. agent_experience/core/__init__.py +0 -0
  101. agent_experience/core/backend.py +80 -0
  102. agent_experience/core/capabilities.py +44 -0
  103. agent_experience/core/config.py +46 -0
  104. agent_experience/core/github.py +355 -0
  105. agent_experience/core/hook_io.py +95 -0
  106. agent_experience/core/journal.py +90 -0
  107. agent_experience/core/paths.py +26 -0
  108. agent_experience/core/prog.py +44 -0
  109. agent_experience/core/render.py +42 -0
  110. agent_experience/core/skill_loader.py +36 -0
  111. devex_cli-0.24.0.dist-info/METADATA +55 -0
  112. devex_cli-0.24.0.dist-info/RECORD +115 -0
  113. devex_cli-0.24.0.dist-info/WHEEL +4 -0
  114. devex_cli-0.24.0.dist-info/entry_points.txt +3 -0
  115. 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