agex-cli 0.11.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 (73) hide show
  1. agent_experience/__init__.py +3 -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 +170 -0
  17. agent_experience/commands/__init__.py +0 -0
  18. agent_experience/commands/explain/SKILL.md +26 -0
  19. agent_experience/commands/explain/__init__.py +0 -0
  20. agent_experience/commands/explain/assets/topics/agex.md +35 -0
  21. agent_experience/commands/explain/references/.gitkeep +0 -0
  22. agent_experience/commands/explain/scripts/__init__.py +0 -0
  23. agent_experience/commands/explain/scripts/explain.py +63 -0
  24. agent_experience/commands/gamify/SKILL.md +31 -0
  25. agent_experience/commands/gamify/__init__.py +0 -0
  26. agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
  27. agent_experience/commands/gamify/references/.gitkeep +0 -0
  28. agent_experience/commands/gamify/scripts/__init__.py +0 -0
  29. agent_experience/commands/gamify/scripts/install.py +196 -0
  30. agent_experience/commands/hook/SKILL.md +31 -0
  31. agent_experience/commands/hook/__init__.py +0 -0
  32. agent_experience/commands/hook/assets/table.md.j2 +17 -0
  33. agent_experience/commands/hook/references/.gitkeep +0 -0
  34. agent_experience/commands/hook/scripts/__init__.py +0 -0
  35. agent_experience/commands/hook/scripts/read.py +38 -0
  36. agent_experience/commands/hook/scripts/write.py +24 -0
  37. agent_experience/commands/learn/SKILL.md +21 -0
  38. agent_experience/commands/learn/__init__.py +0 -0
  39. agent_experience/commands/learn/assets/menu.md.j2 +7 -0
  40. agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
  41. agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
  42. agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
  43. agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
  44. agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
  45. agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
  46. agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
  47. agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
  48. agent_experience/commands/learn/references/.gitkeep +0 -0
  49. agent_experience/commands/learn/scripts/__init__.py +0 -0
  50. agent_experience/commands/learn/scripts/learn.py +72 -0
  51. agent_experience/commands/overview/SKILL.md +31 -0
  52. agent_experience/commands/overview/__init__.py +0 -0
  53. agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
  54. agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
  55. agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
  56. agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
  57. agent_experience/commands/overview/assets/sections.md.j2 +52 -0
  58. agent_experience/commands/overview/references/.gitkeep +0 -0
  59. agent_experience/commands/overview/scripts/__init__.py +0 -0
  60. agent_experience/commands/overview/scripts/overview.py +40 -0
  61. agent_experience/core/__init__.py +0 -0
  62. agent_experience/core/backend.py +16 -0
  63. agent_experience/core/capabilities.py +44 -0
  64. agent_experience/core/config.py +42 -0
  65. agent_experience/core/hook_io.py +95 -0
  66. agent_experience/core/paths.py +26 -0
  67. agent_experience/core/render.py +27 -0
  68. agent_experience/core/skill_loader.py +36 -0
  69. agex_cli-0.11.0.dist-info/METADATA +56 -0
  70. agex_cli-0.11.0.dist-info/RECORD +73 -0
  71. agex_cli-0.11.0.dist-info/WHEEL +4 -0
  72. agex_cli-0.11.0.dist-info/entry_points.txt +2 -0
  73. agex_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("agex-cli")
@@ -0,0 +1,4 @@
1
+ from agent_experience.cli import _main_entrypoint
2
+
3
+ if __name__ == "__main__":
4
+ _main_entrypoint()
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ from agent_experience.backends.claude_code.probe import ProbeResult
4
+
5
+
6
+ def probe(project_dir: Path) -> ProbeResult:
7
+ """Stub ACP probe — v0.1 returns empty. Full discovery tracked as open issue."""
8
+ del project_dir # accepted for signature parity with other probes; real discovery deferred
9
+ return ProbeResult()
@@ -0,0 +1,7 @@
1
+ hooks: false
2
+ hooks_alternative: |
3
+ ACP-speaking agents (KiroCLI, OpenCode) do not currently expose hooks.
4
+ Track usage via the ACP transport layer or a shell wrapper.
5
+ mcp: true
6
+ skills: false
7
+ agents: false
@@ -0,0 +1,4 @@
1
+ hooks: true
2
+ mcp: true
3
+ skills: true
4
+ agents: true
@@ -0,0 +1,7 @@
1
+ hooks: false
2
+ hooks_alternative: |
3
+ Codex does not expose a hook interface. Wrap your Codex invocation in a shell
4
+ script that calls `agex hook write` before/after each turn as a workaround.
5
+ mcp: true
6
+ skills: false
7
+ agents: false
@@ -0,0 +1,7 @@
1
+ hooks: false
2
+ hooks_alternative: |
3
+ GitHub Copilot CLI does not expose a hook interface in v0.1-era versions.
4
+ Track usage via a shell wrapper that calls `agex hook write`.
5
+ mcp: false
6
+ skills: false
7
+ agents: false
File without changes
@@ -0,0 +1,97 @@
1
+ import json
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from agent_experience.core.skill_loader import load_skill
9
+
10
+ _CLAUDE_DIR = ".claude"
11
+
12
+
13
+ @dataclass
14
+ class ProbeResult:
15
+ skills: list[dict[str, Any]] = field(default_factory=list)
16
+ hooks: list[dict[str, Any]] = field(default_factory=list)
17
+ agents: list[dict[str, Any]] = field(default_factory=list)
18
+ mcp_servers: list[dict[str, Any]] = field(default_factory=list)
19
+ claude_md: Path | None = None
20
+ settings: dict[str, Any] | None = None
21
+ warnings: list[str] = field(default_factory=list)
22
+
23
+
24
+ def _read_skill(path: Path) -> tuple[dict[str, Any] | None, str | None]:
25
+ try:
26
+ skill = load_skill(path)
27
+ except (ValueError, OSError, yaml.YAMLError) as e:
28
+ return None, str(e)
29
+ return (
30
+ {
31
+ "name": skill.name,
32
+ "description": skill.description,
33
+ "path": str(path),
34
+ },
35
+ None,
36
+ )
37
+
38
+
39
+ def _probe_settings(claude_dir: Path, result: ProbeResult) -> None:
40
+ settings = claude_dir / "settings.json"
41
+ if not settings.exists():
42
+ return
43
+ try:
44
+ result.settings = json.loads(settings.read_text(encoding="utf-8"))
45
+ except (json.JSONDecodeError, OSError) as e:
46
+ result.warnings.append(f"could not parse {settings}: {e}")
47
+
48
+
49
+ def _probe_skills(claude_dir: Path, result: ProbeResult) -> None:
50
+ skills_dir = claude_dir / "skills"
51
+ if not skills_dir.is_dir():
52
+ return
53
+ # Sort for deterministic snapshot ordering across platforms / filesystems.
54
+ for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
55
+ parsed, err = _read_skill(skill_md)
56
+ if parsed is not None:
57
+ result.skills.append(parsed)
58
+ else:
59
+ result.warnings.append(f"could not parse {skill_md}: {err}")
60
+
61
+
62
+ def _probe_hooks(claude_dir: Path, result: ProbeResult) -> None:
63
+ hooks_file = claude_dir / "hooks.json"
64
+ if not hooks_file.exists():
65
+ return
66
+ try:
67
+ data = json.loads(hooks_file.read_text(encoding="utf-8"))
68
+ except (json.JSONDecodeError, OSError) as e:
69
+ result.warnings.append(f"could not parse {hooks_file}: {e}")
70
+ return
71
+ if not isinstance(data, dict):
72
+ result.warnings.append(f"could not parse {hooks_file}: expected a JSON object")
73
+ return
74
+ for event, entries in data.items():
75
+ if not isinstance(entries, list):
76
+ result.warnings.append(
77
+ f"could not parse {hooks_file}: expected list for event '{event}'"
78
+ )
79
+ continue
80
+ result.hooks.append({"event": event, "entries": entries})
81
+
82
+
83
+ def probe(project_dir: Path) -> ProbeResult:
84
+ result = ProbeResult()
85
+ if not project_dir.exists():
86
+ return result
87
+
88
+ claude_md = project_dir / "CLAUDE.md"
89
+ if claude_md.exists():
90
+ result.claude_md = claude_md
91
+
92
+ claude_dir = project_dir / _CLAUDE_DIR
93
+ _probe_settings(claude_dir, result)
94
+ _probe_skills(claude_dir, result)
95
+ _probe_hooks(claude_dir, result)
96
+
97
+ return result
File without changes
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+
3
+ from agent_experience.backends.claude_code.probe import ProbeResult
4
+
5
+
6
+ def probe(project_dir: Path) -> ProbeResult:
7
+ """Minimal Codex probe — reads AGENTS.md if present. Other discovery deferred."""
8
+ result = ProbeResult()
9
+ if not project_dir.exists():
10
+ return result
11
+ agents_md = project_dir / "AGENTS.md"
12
+ if agents_md.exists():
13
+ result.claude_md = (
14
+ agents_md # reusing field — rename to `project_memory` in a future cleanup
15
+ )
16
+ return result
File without changes
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ from agent_experience.backends.claude_code.probe import ProbeResult
4
+
5
+
6
+ def probe(project_dir: Path) -> ProbeResult:
7
+ """Stub Copilot probe — v0.1 returns empty. Full discovery tracked as open issue."""
8
+ del project_dir # accepted for signature parity with other probes; real discovery deferred
9
+ return ProbeResult()
@@ -0,0 +1,170 @@
1
+ import sys
2
+ from typing import Any, Optional
3
+
4
+ import typer
5
+
6
+ from agent_experience import __version__
7
+ from agent_experience.commands.explain.scripts import explain as explain_script
8
+ from agent_experience.commands.gamify.scripts import install as gamify_script
9
+ from agent_experience.commands.hook.scripts import read as hook_read_script
10
+ from agent_experience.commands.hook.scripts import write as hook_write_script
11
+ from agent_experience.commands.learn.scripts import learn as learn_script
12
+ from agent_experience.commands.overview.scripts import overview as overview_script
13
+ from agent_experience.core.backend import parse_backend
14
+
15
+ app = typer.Typer(
16
+ name="agex",
17
+ help="Agent-operated developer-experience CLI.",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+
22
+ def _version_callback(value: bool) -> None:
23
+ if value:
24
+ typer.echo(__version__)
25
+ raise typer.Exit()
26
+
27
+
28
+ @app.callback()
29
+ def main(
30
+ version: Optional[bool] = typer.Option(
31
+ None, "--version", callback=_version_callback, is_eager=True
32
+ ),
33
+ ) -> None:
34
+ """Root callback — exists only to hold the --version option.
35
+
36
+ Typer invokes the eager _version_callback before any subcommand
37
+ dispatch; there is nothing else to do at the app level.
38
+ """
39
+
40
+
41
+ @app.command("explain")
42
+ def explain(topic: str = typer.Argument(..., help="Topic to explain.")) -> None:
43
+ stdout, exit_code, stderr = explain_script.run(topic)
44
+ if stdout:
45
+ typer.echo(stdout, nl=False)
46
+ if stderr:
47
+ typer.echo(stderr, err=True)
48
+ if exit_code != 0:
49
+ raise typer.Exit(code=exit_code)
50
+
51
+
52
+ def _agent_option() -> Any:
53
+ return typer.Option(..., "--agent", help="Backend: claude-code, codex, copilot, or acp.")
54
+
55
+
56
+ hook_app = typer.Typer(help="Write and read agex tracking events.", no_args_is_help=True)
57
+ app.add_typer(hook_app, name="hook")
58
+
59
+
60
+ @hook_app.command("write")
61
+ def hook_write(
62
+ event: str = typer.Argument(..., help="Event name (e.g., post-tool-use)."),
63
+ args: list[str] = typer.Argument(None, help="Additional key=value pairs."),
64
+ ) -> None:
65
+ args = args or []
66
+ _, exit_code, stderr = hook_write_script.run(event, args)
67
+ if stderr:
68
+ typer.echo(stderr, err=True)
69
+ if exit_code != 0:
70
+ raise typer.Exit(code=exit_code)
71
+
72
+
73
+ @hook_app.command("read")
74
+ def hook_read(agent: str = _agent_option()) -> None:
75
+ try:
76
+ backend = parse_backend(agent)
77
+ except ValueError as e:
78
+ typer.echo(f"agex: error: {e}", err=True)
79
+ raise typer.Exit(code=2)
80
+ stdout, exit_code, stderr = hook_read_script.run(backend)
81
+ if stdout:
82
+ typer.echo(stdout, nl=False)
83
+ if stderr:
84
+ typer.echo(stderr, err=True)
85
+ if exit_code != 0:
86
+ raise typer.Exit(code=exit_code)
87
+
88
+
89
+ @app.command("learn")
90
+ def learn(
91
+ topic: Optional[str] = typer.Argument(None, help="Lesson topic (omit for menu)."),
92
+ agent: str = _agent_option(),
93
+ ) -> None:
94
+ try:
95
+ backend = parse_backend(agent)
96
+ except ValueError as e:
97
+ typer.echo(f"agex: error: {e}", err=True)
98
+ raise typer.Exit(code=2)
99
+ if topic is None:
100
+ stdout, exit_code, stderr = learn_script.run_menu(backend)
101
+ else:
102
+ stdout, exit_code, stderr = learn_script.run_topic(topic, backend)
103
+ if stdout:
104
+ typer.echo(stdout, nl=False)
105
+ if stderr:
106
+ typer.echo(stderr, err=True)
107
+ if exit_code != 0:
108
+ raise typer.Exit(code=exit_code)
109
+
110
+
111
+ @app.command("gamify")
112
+ def gamify(
113
+ agent: str = _agent_option(),
114
+ uninstall: bool = typer.Option(False, "--uninstall", help="Reverse gamify."),
115
+ ) -> None:
116
+ try:
117
+ backend = parse_backend(agent)
118
+ except ValueError as e:
119
+ typer.echo(f"agex: error: {e}", err=True)
120
+ raise typer.Exit(code=2)
121
+ if uninstall:
122
+ stdout, exit_code, stderr = gamify_script.uninstall(backend)
123
+ else:
124
+ stdout, exit_code, stderr = gamify_script.install(backend)
125
+ if stdout:
126
+ typer.echo(stdout, nl=False)
127
+ if stderr:
128
+ typer.echo(stderr, err=True)
129
+ if exit_code != 0:
130
+ raise typer.Exit(code=exit_code)
131
+
132
+
133
+ @app.command("overview")
134
+ def overview(agent: str = _agent_option()) -> None:
135
+ try:
136
+ backend = parse_backend(agent)
137
+ except ValueError as e:
138
+ typer.echo(f"agex: error: {e}", err=True)
139
+ raise typer.Exit(code=2)
140
+ stdout, exit_code, stderr = overview_script.run(backend)
141
+ if stdout:
142
+ typer.echo(stdout, nl=False)
143
+ if stderr:
144
+ typer.echo(stderr, err=True)
145
+ if exit_code != 0:
146
+ raise typer.Exit(code=exit_code)
147
+
148
+
149
+ # Keep in sync with the @app.command / app.add_typer registrations above.
150
+ # If a new top-level command is added, extend this set so _main_entrypoint
151
+ # stops routing it to the unknown-command fallback page.
152
+ _KNOWN_COMMANDS = {"explain", "overview", "learn", "gamify", "hook"}
153
+
154
+
155
+ def _main_entrypoint() -> None:
156
+ """CLI entry point that routes unknown subcommands to ``agex explain agex``.
157
+
158
+ When the first positional argument is not a known command (and is not a
159
+ flag), this function prints the ``agex explain agex`` page to stdout and
160
+ the canonical error message to stderr, then exits with code 2. All other
161
+ invocations — known commands, ``--version``, ``--help``, zero-arg help —
162
+ fall through to the normal Typer ``app()`` dispatch unchanged.
163
+ """
164
+ argv = sys.argv[1:]
165
+ if argv and not argv[0].startswith("-") and argv[0] not in _KNOWN_COMMANDS:
166
+ typer.echo(f"agex: error: unknown command '{argv[0]}'", err=True)
167
+ stdout, _, _ = explain_script.run("agex")
168
+ typer.echo(stdout, nl=False)
169
+ sys.exit(2)
170
+ app()
File without changes
@@ -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,35 @@
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 explain <topic>` | You're reading this. |
17
+
18
+ ## First steps
19
+
20
+ ```bash
21
+ agex explain agex # this page
22
+ agex learn --agent claude-code # what can I learn for my backend?
23
+ agex overview --agent claude-code # what's in this project?
24
+ ```
25
+
26
+ ## Design invariants
27
+
28
+ - **Non-agentic.** Zero LLM calls inside agex. All output is deterministic.
29
+ - **Markdown is the universal format.** No `--json` flag.
30
+ - **`--agent` is required** on backend-sensitive commands.
31
+ - **Unsupported is success.** If your backend lacks a feature, you get a markdown notice + link to file an issue — exit code 0.
32
+
33
+ ## Repo
34
+
35
+ <https://github.com/OriNachum/agex>
File without changes
File without changes
@@ -0,0 +1,63 @@
1
+ import re
2
+ from importlib.resources import files
3
+ from importlib.resources.abc import Traversable
4
+
5
+ from agent_experience.core.skill_loader import Skill, load_skill
6
+
7
+ _TOPIC_RE = re.compile(r"^[a-z][a-z0-9-]*$")
8
+
9
+
10
+ def _commands_root() -> Traversable:
11
+ return files("agent_experience.commands")
12
+
13
+
14
+ def resolve_topic(topic: str) -> tuple[str, Traversable] | None:
15
+ """Resolve topic per spec precedence. Returns (kind, traversable) or None.
16
+
17
+ Rejects any topic that isn't a simple slug to prevent path traversal.
18
+ """
19
+ if not _TOPIC_RE.match(topic):
20
+ return None
21
+
22
+ cmds = _commands_root()
23
+
24
+ cmd_skill = cmds.joinpath(topic, "SKILL.md")
25
+ if cmd_skill.is_file():
26
+ return ("command", cmd_skill)
27
+
28
+ lesson_skill = cmds.joinpath("learn", "assets", "topics", topic, "SKILL.md")
29
+ if lesson_skill.is_file():
30
+ return ("lesson", lesson_skill)
31
+
32
+ concept = cmds.joinpath("explain", "assets", "topics", f"{topic}.md")
33
+ if concept.is_file():
34
+ return ("concept", concept)
35
+
36
+ return None
37
+
38
+
39
+ def _load_skill_from_traversable(trav: Traversable) -> Skill:
40
+ # load_skill expects a pathlib.Path; resolve via as_file when needed. Since
41
+ # our package resources are on a real filesystem (hatch force-include), the
42
+ # Traversable is a MultiplexedPath / PosixPath wrapper whose .read_text()
43
+ # works directly. We rebuild a Skill by parsing the body in-line to avoid
44
+ # Path coupling.
45
+ from importlib.resources import as_file
46
+
47
+ with as_file(trav) as path:
48
+ return load_skill(path)
49
+
50
+
51
+ def run(topic: str) -> tuple[str, int, str]:
52
+ """Return (stdout, exit_code, stderr)."""
53
+ resolved = resolve_topic(topic)
54
+ if resolved is None:
55
+ agex_page = _commands_root().joinpath("explain", "assets", "topics", "agex.md")
56
+ body = agex_page.read_text(encoding="utf-8") if agex_page.is_file() else ""
57
+ return (body, 2, f"agex: error: unknown topic '{topic}'")
58
+
59
+ kind, trav = resolved
60
+ if kind == "concept":
61
+ return (trav.read_text(encoding="utf-8"), 0, "")
62
+ skill = _load_skill_from_traversable(trav)
63
+ 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