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,196 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from importlib.resources import files
4
+ from importlib.resources.abc import Traversable
5
+ from pathlib import Path
6
+
7
+ from agent_experience.core.backend import Backend
8
+ from agent_experience.core.config import load as load_config
9
+ from agent_experience.core.config import save as save_config
10
+ from agent_experience.core.paths import ensure_init
11
+
12
+
13
+ def _fragments_file() -> Traversable:
14
+ return files("agent_experience.commands.gamify").joinpath("assets", "hooks", "claude-code.json")
15
+
16
+
17
+ def _fragments_for(backend: Backend) -> list[dict]:
18
+ if backend != Backend.CLAUDE_CODE:
19
+ return []
20
+ data = json.loads(_fragments_file().read_text(encoding="utf-8"))
21
+ return data["fragments"]
22
+
23
+
24
+ def _hooks_file_for(backend: Backend, project_dir: Path) -> Path | None:
25
+ if backend == Backend.CLAUDE_CODE:
26
+ return project_dir / ".claude" / "hooks.json"
27
+ return None
28
+
29
+
30
+ def _refuse(path: Path, reason: str) -> ValueError:
31
+ return ValueError(
32
+ f"{path} {reason}; refusing to overwrite. " "Fix or remove the file before re-running."
33
+ )
34
+
35
+
36
+ def _load_hooks_file(path: Path) -> dict:
37
+ # Malformed or unexpectedly-shaped files are NEVER silently overwritten —
38
+ # the caller gets a ValueError, surfaces it as exit 2, and the user's file
39
+ # stays on disk untouched.
40
+ if not path.exists():
41
+ return {}
42
+ try:
43
+ data = json.loads(path.read_text(encoding="utf-8"))
44
+ except json.JSONDecodeError as e:
45
+ raise _refuse(path, f"is not valid JSON ({e})") from e
46
+ if not isinstance(data, dict):
47
+ raise _refuse(path, "is not a JSON object at the top level")
48
+ for entries in data.values():
49
+ if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
50
+ raise _refuse(path, "is not a mapping of event → list of hook objects")
51
+ return data
52
+
53
+
54
+ def _write_hooks_file(path: Path, data: dict) -> None:
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
57
+
58
+
59
+ def _merge_fragments(hooks: dict, fragments: list[dict]) -> tuple[list[str], int]:
60
+ """Append each fragment's {id, hook} under its event if the id isn't already
61
+ there. Returns (all_fragment_ids_in_insertion_order, added_count)."""
62
+ written_ids: list[str] = []
63
+ added = 0
64
+ for frag in fragments:
65
+ event = frag["event"]
66
+ entry = {"id": frag["id"], "hook": frag["hook"]}
67
+ hooks.setdefault(event, [])
68
+ if not any(e.get("id") == frag["id"] for e in hooks[event]):
69
+ hooks[event].append(entry)
70
+ added += 1
71
+ written_ids.append(frag["id"])
72
+ return written_ids, added
73
+
74
+
75
+ def _remove_ids_from_hooks(hooks: dict, ids_to_remove: set[str]) -> int:
76
+ """Filter entries matching ids_to_remove out of each event. Event keys whose
77
+ arrays become empty as a result of removal are deleted. Pre-existing empty
78
+ event arrays are left intact. Returns total entries removed."""
79
+ removed = 0
80
+ for event in list(hooks.keys()):
81
+ original = hooks[event]
82
+ filtered = [e for e in original if e.get("id") not in ids_to_remove]
83
+ event_removed = len(original) - len(filtered)
84
+ if event_removed == 0:
85
+ continue
86
+ removed += event_removed
87
+ if filtered:
88
+ hooks[event] = filtered
89
+ else:
90
+ del hooks[event]
91
+ return removed
92
+
93
+
94
+ def install(backend: Backend) -> tuple[str, int, str]:
95
+ ensure_init()
96
+ project_dir = Path.cwd()
97
+ hooks_file = _hooks_file_for(backend, project_dir)
98
+ if hooks_file is None:
99
+ return (_unsupported_notice(backend), 0, "")
100
+
101
+ fragments = _fragments_for(backend)
102
+ if not fragments:
103
+ return (_unsupported_notice(backend), 0, "")
104
+
105
+ try:
106
+ hooks = _load_hooks_file(hooks_file)
107
+ except ValueError as e:
108
+ return ("", 2, f"agex: error: {e}")
109
+
110
+ written_ids, added_count = _merge_fragments(hooks, fragments)
111
+ if added_count:
112
+ _write_hooks_file(hooks_file, hooks)
113
+
114
+ cfg = load_config()
115
+ previous = cfg.installed.get("gamify", {})
116
+ if previous.get("hook_fragment_ids") != written_ids:
117
+ cfg.installed["gamify"] = {
118
+ "at": datetime.now(tz=timezone.utc).isoformat(),
119
+ "hook_fragment_ids": written_ids,
120
+ }
121
+ save_config(cfg)
122
+
123
+ rel = hooks_file.relative_to(project_dir)
124
+ if added_count:
125
+ status_line = (
126
+ f"- Added {added_count} hook fragment(s); "
127
+ f"ensured {len(written_ids)} present in `{rel}`."
128
+ )
129
+ else:
130
+ status_line = (
131
+ f"- Ensured {len(written_ids)} hook fragment(s) already present "
132
+ f"in `{rel}` (no changes)."
133
+ )
134
+ lines = [
135
+ f"# Gamify installed — {backend.value}",
136
+ "",
137
+ status_line,
138
+ "- Fragment IDs: " + ", ".join(f"`{i}`" for i in written_ids),
139
+ "",
140
+ f"Next: run `agex learn gamify --agent {backend.value}` to set up the levelup skill.",
141
+ "",
142
+ ]
143
+ return ("\n".join(lines), 0, "")
144
+
145
+
146
+ def uninstall(backend: Backend) -> tuple[str, int, str]:
147
+ ensure_init()
148
+ project_dir = Path.cwd()
149
+ hooks_file = _hooks_file_for(backend, project_dir)
150
+ if hooks_file is None:
151
+ return (_unsupported_notice(backend), 0, "")
152
+
153
+ cfg = load_config()
154
+ installed = cfg.installed.get("gamify", {})
155
+ ids_to_remove = set(installed.get("hook_fragment_ids", []))
156
+ if not ids_to_remove:
157
+ return (f"# Gamify uninstalled — nothing to remove on {backend.value}.\n", 0, "")
158
+
159
+ rel = hooks_file.relative_to(project_dir)
160
+ # If the user already removed the hooks file, just drop the config record.
161
+ # Don't re-create the file with an empty object.
162
+ if not hooks_file.exists():
163
+ cfg.installed.pop("gamify", None)
164
+ save_config(cfg)
165
+ return (
166
+ f"# Gamify uninstalled — `{rel}` was already gone; " "cleared config record.\n",
167
+ 0,
168
+ "",
169
+ )
170
+
171
+ try:
172
+ hooks = _load_hooks_file(hooks_file)
173
+ except ValueError as e:
174
+ return ("", 2, f"agex: error: {e}")
175
+
176
+ removed_count = _remove_ids_from_hooks(hooks, ids_to_remove)
177
+ if removed_count:
178
+ _write_hooks_file(hooks_file, hooks)
179
+
180
+ cfg.installed.pop("gamify", None)
181
+ save_config(cfg)
182
+
183
+ return (
184
+ f"# Gamify uninstalled — removed {removed_count} fragment(s) from `{rel}`.\n",
185
+ 0,
186
+ "",
187
+ )
188
+
189
+
190
+ def _unsupported_notice(backend: Backend) -> str:
191
+ return (
192
+ f"## `gamify` is not supported on {backend.value}\n\n"
193
+ f"Hooks are required to track usage events, and {backend.value} does not expose "
194
+ f"a hook interface agex can write to.\n\n"
195
+ "Want this supported? Open an issue: <https://github.com/OriNachum/agex/issues>\n"
196
+ )
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: hook
3
+ description: Write and read agex tracking events.
4
+ type: command
5
+ ---
6
+
7
+ # `agex hook write <event> [key=value ...]` / `agex hook read --agent <backend>`
8
+
9
+ ## `write`
10
+
11
+ Called by installed hooks (see `agex gamify`, Phase 7). Appends a JSON line to `.agex/data/<event>.json`. Silent. Safe for concurrent invocation (file locking via `portalocker`).
12
+
13
+ ```bash
14
+ agex hook write post-tool-use tool=Read
15
+ ```
16
+
17
+ ## `read`
18
+
19
+ Renders tracked events as a markdown table. Prints the source JSON path for deeper inspection.
20
+
21
+ ```bash
22
+ agex hook read --agent claude-code
23
+ ```
24
+
25
+ ## Notes
26
+
27
+ - Event names are free-form; conventional names: `post-tool-use`, `user-prompt`, `stop`, `sessions`.
28
+ - Extra positional `key=value` pairs are captured into the payload. Empty keys (e.g., `=foo`) are dropped.
29
+ - Timestamp (`ts`) is attached automatically; a positional `ts=<value>` overrides it (useful for replays).
30
+ - The positional `<event>` name is authoritative — it always wins over any `event=...` pair in args.
31
+ - Malformed JSON lines in `.agex/data/*.json` (e.g., from a partial write) are skipped with a warning on `hook read`, not raised.
File without changes
@@ -0,0 +1,17 @@
1
+ # Hook events — {{ backend }}
2
+
3
+ **Source:** `{{ source }}`
4
+
5
+ {% for stream in streams %}
6
+ ## `{{ stream.name }}` ({{ stream.events|length }})
7
+
8
+ {% if stream.events -%}
9
+ | ts | event | details |
10
+ |---|---|---|
11
+ {% for e in stream.events -%}
12
+ | {{ e.ts }} | {{ stream.name }} | {{ e.details }} |
13
+ {% endfor %}
14
+ {%- else -%}
15
+ _no events_
16
+ {%- endif %}
17
+ {% endfor %}
File without changes
File without changes
@@ -0,0 +1,38 @@
1
+ from importlib.resources import files
2
+ from importlib.resources.abc import Traversable
3
+
4
+ from agent_experience.core.backend import Backend
5
+ from agent_experience.core.hook_io import load_events
6
+ from agent_experience.core.paths import data_dir, ensure_init
7
+ from agent_experience.core.render import render_string
8
+
9
+ KNOWN_STREAMS = ["post-tool-use", "user-prompt", "stop", "sessions"]
10
+
11
+
12
+ def _assets_root() -> Traversable:
13
+ # Anchor on the `commands` package (which has __init__.py) and navigate in.
14
+ # Avoids relying on namespace-package semantics for `assets/`, which is a
15
+ # data directory, not a package. Matches overview.py / learn.py pattern.
16
+ return files("agent_experience.commands").joinpath("hook", "assets")
17
+
18
+
19
+ def run(backend: Backend) -> tuple[str, int, str]:
20
+ ensure_init()
21
+ streams = []
22
+ for name in KNOWN_STREAMS:
23
+ events = load_events(name)
24
+ summarized = [
25
+ {
26
+ "ts": e.get("ts", ""),
27
+ "details": ", ".join(f"{k}={v}" for k, v in e.items() if k not in ("ts", "event")),
28
+ }
29
+ for e in events
30
+ ]
31
+ streams.append({"name": name, "events": summarized})
32
+
33
+ template_text = _assets_root().joinpath("table.md.j2").read_text(encoding="utf-8")
34
+ out = render_string(
35
+ template_text,
36
+ {"backend": backend.value, "source": str(data_dir()), "streams": streams},
37
+ )
38
+ return (out, 0, "")
@@ -0,0 +1,24 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from agent_experience.core.hook_io import append_event
4
+ from agent_experience.core.paths import ensure_init
5
+
6
+
7
+ def run(event: str, args: list[str]) -> tuple[str, int, str]:
8
+ ensure_init()
9
+ payload: dict = {"ts": datetime.now(tz=timezone.utc).isoformat()}
10
+ for arg in args:
11
+ if "=" in arg:
12
+ k, v = arg.split("=", 1)
13
+ if k:
14
+ payload[k] = v
15
+ # Positional event name is authoritative — it overrides any `event=...`
16
+ # pair in args so hook scripts can't misattribute events.
17
+ payload["event"] = event
18
+ try:
19
+ append_event(event, payload)
20
+ except ValueError as e:
21
+ # append_event → _stream_path rejects names that don't match the
22
+ # `^[a-z][a-z0-9-]*$` slug whitelist (path-traversal guard).
23
+ return ("", 2, f"agex: error: {e}")
24
+ return ("", 0, "")
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: learn
3
+ description: Show available lessons, or teach one.
4
+ type: command
5
+ ---
6
+
7
+ # `agex learn [topic] --agent <backend>`
8
+
9
+ Without a topic, lists the lessons available for your backend. With a topic, teaches it — emits a markdown lesson body plus inline skill-template code blocks you can write into your project.
10
+
11
+ ## From your shell tool
12
+
13
+ ```bash
14
+ agex learn --agent claude-code
15
+ agex learn introspect --agent claude-code
16
+ ```
17
+
18
+ ## Notes
19
+
20
+ - Lessons gated on a backend feature (e.g., `gamify` needs hooks) may still appear in the list, but they are not currently annotated as unsupported in the menu — Phase 8 adds that capability-based routing.
21
+ - v0.1 emits inline code blocks only. A future `--write` flag is tracked as an open issue.
File without changes
@@ -0,0 +1,7 @@
1
+ # Lessons — {{ backend }}
2
+
3
+ Run `agex learn <topic> --agent {{ backend }}` to learn one.
4
+
5
+ {% for topic in topics -%}
6
+ - **`{{ topic.name }}`** — {{ topic.description }}{% if topic.unsupported %} _(unsupported on {{ backend }}: {{ topic.unsupported }})_{% endif %}
7
+ {% endfor %}
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: gamify
3
+ description: Install usage tracking hooks and build a levelup skill to advise the user.
4
+ type: lesson
5
+ ---
6
+
7
+ # Lesson — set up gamification for {{ backend }}
8
+
9
+ Two parts:
10
+
11
+ ## Part 1 — install the tracking hooks
12
+
13
+ Run in your shell tool:
14
+
15
+ ```bash
16
+ agex gamify --agent {{ backend }}
17
+ ```
18
+
19
+ This writes backend-native hook fragments that call `agex hook write <event>` whenever you use a tool, submit a prompt, or stop. The events land in `.agex/data/`.
20
+
21
+ To uninstall: `agex gamify --uninstall --agent {{ backend }}`.
22
+
23
+ ## Part 2 — build the `levelup` skill
24
+
25
+ The hook data is inert without something to surface it. Build the levelup skill described in `agex learn levelup --agent {{ backend }}`, or copy its skill template directly:
26
+
27
+ ### `.claude/skills/levelup/SKILL.md`
28
+
29
+ ```markdown
30
+ {{ skill_template_body }}
31
+ ```
32
+
33
+ ## After both parts
34
+
35
+ Use your runtime normally for a few sessions. Then invoke `/levelup` — it will read the tracking data via `agex hook read` and advise the user.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: levelup
3
+ description: Read agex usage tracking data and suggest a next-feature-to-learn for the user.
4
+ type: command
5
+ ---
6
+
7
+ # Level up
8
+
9
+ > **Note:** Requires `agex hook read`, which ships in a future phase (Phase 6). Until it lands, this skill has no data source — treat it as a scaffold placeholder.
10
+
11
+ Invoke when the user asks for what's next, or opportunistically after a long session.
12
+
13
+ ## Process
14
+
15
+ 1. Run in your shell tool: `agex hook read --agent claude-code`
16
+ 2. Count occurrences per event type.
17
+ 3. Pick **one** feature area the user is under-using (e.g., they have MCP servers configured but `post-tool-use` shows zero MCP tool calls).
18
+ 4. Suggest one concrete next step in 3-4 sentences.
19
+
20
+ ## Rule
21
+
22
+ One suggestion per invocation. If nothing stands out, say so — don't invent.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: introspect
3
+ description: Build an agent-native introspect skill that audits your project setup and suggests next steps.
4
+ type: lesson
5
+ ---
6
+
7
+ # Lesson — build an `introspect` skill for {{ backend }}
8
+
9
+ ## What you'll end up with
10
+
11
+ A backend-native skill (for {{ backend }}) that:
12
+ 1. Calls `agex overview --agent {{ backend }}` to read the project's current state.
13
+ 2. Identifies gaps (missing CLAUDE.md, no skills, no hooks, etc.).
14
+ 3. Suggests the next one or two improvements you could apply.
15
+ 4. Is small enough that invoking it doesn't blow your context budget.
16
+
17
+ ## Why build it instead of shipping it
18
+
19
+ Two reasons: (1) each agent backend has its own native skill format, so no shipped skill fits perfectly; (2) you and the user stay in control of what gets installed into the project.
20
+
21
+ ## Step 1 — review the `agex overview` output
22
+
23
+ Run `agex overview --agent {{ backend }}` now. Note which sections are empty — those are your candidate gaps.
24
+
25
+ ## Step 2 — create the skill file
26
+
27
+ Write the file shown below to the path noted above its fence. Adjust the prose/tone to your project's voice.
28
+
29
+ ### `.claude/skills/introspect/SKILL.md`
30
+
31
+ ```markdown
32
+ {{ skill_template_body }}
33
+ ```
34
+
35
+ ## Step 3 — try it
36
+
37
+ Invoke `/introspect` (or equivalent) in your runtime. Read the output, apply one suggestion.
38
+
39
+ ## Why one suggestion at a time
40
+
41
+ Agents that shove 10 recommendations into one turn overwhelm the user. The skill you just built is explicitly capped at two — if you want more later, iterate.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: introspect
3
+ description: Audit the current project's agent setup and suggest 1-2 next improvements.
4
+ type: command
5
+ ---
6
+
7
+ # Introspect
8
+
9
+ When the user asks to audit the agent setup, improve it, or "what should I add", invoke this skill.
10
+
11
+ ## Process
12
+
13
+ 1. Run in your shell tool: `agex overview --agent claude-code`
14
+ 2. Read the output. Count what exists under each section (skills, hooks, MCP, settings).
15
+ 3. Identify the two weakest sections — the ones most likely to unblock a real workflow next.
16
+ 4. Emit a short markdown reply: what's missing, why it matters, what adding it costs.
17
+
18
+ ## Rules
19
+
20
+ - Cap suggestions at 2.
21
+ - No "nice-to-haves." Only suggestions that unblock something concrete.
22
+ - Don't install anything. Advise only.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: levelup
3
+ description: Build a skill that reads agex usage data and advises the user.
4
+ type: lesson
5
+ ---
6
+
7
+ # Lesson — build the `levelup` skill for {{ backend }}
8
+
9
+ > **Preview:** This lesson depends on `agex gamify` (Phase 7) and `agex hook read` (Phase 6); neither is available in agex 0.3.0. Treat the steps below as a design preview — the emitted skill template won't have real data to read until those commands ship.
10
+
11
+ Prerequisite: you've run `agex gamify --agent {{ backend }}` so there's data to read.
12
+
13
+ ## Step 1 — understand the data source
14
+
15
+ Run `agex hook read --agent {{ backend }}` now. You'll see a JSON list of events (tool calls, prompts submitted, stops). The levelup skill will parse this and suggest one area for improvement.
16
+
17
+ ## Step 2 — create the skill file
18
+
19
+ Write the file shown below to the path noted above its fence. This skill reads the tracking data and offers one concrete suggestion per invocation.
20
+
21
+ ### Skill template — `.claude/skills/levelup/SKILL.md`
22
+
23
+ ```markdown
24
+ {{ skill_template_body }}
25
+ ```
26
+
27
+ ## Step 3 — try it after a few sessions
28
+
29
+ Use your runtime normally for a few turns. Then invoke `/levelup` to see what the skill suggests.
30
+
31
+ See also: `agex learn gamify --agent {{ backend }}` (bundles the full setup).
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: levelup
3
+ description: Read agex usage tracking data and suggest a next-feature-to-learn for the user.
4
+ type: command
5
+ ---
6
+
7
+ # Level up
8
+
9
+ > **Note:** Requires `agex hook read`, which ships in a future phase (Phase 6). Until it lands, this skill has no data source — treat it as a scaffold placeholder.
10
+
11
+ Invoke when the user asks for what's next, or opportunistically after a long session.
12
+
13
+ ## Process
14
+
15
+ 1. Run in your shell tool: `agex hook read --agent claude-code`
16
+ 2. Count occurrences per event type.
17
+ 3. Pick **one** feature area the user is under-using (e.g., they have MCP servers configured but `post-tool-use` shows zero MCP tool calls).
18
+ 4. Suggest one concrete next step in 3-4 sentences.
19
+
20
+ ## Rule
21
+
22
+ One suggestion per invocation. If nothing stands out, say so — don't invent.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: visualize
3
+ description: Build a skill that renders a compact visual of your agent setup.
4
+ type: lesson
5
+ ---
6
+
7
+ # Lesson — build a `visualize` skill for {{ backend }}
8
+
9
+ Same shape as introspect, but the output optimizes for *at-a-glance* rather than recommendations. The skill runs `agex overview --agent {{ backend }}`, compresses it into a tight markdown block (counts only, with a token target of < 500 output tokens), and echoes it for situational awareness.
10
+
11
+ ## Step 1 — understand the goal
12
+
13
+ Run `agex overview --agent {{ backend }}` now. Note the counts of skills, hooks, MCP integrations, and CLAUDE.md status. The visualize skill will display these as a one-liner for quick reference.
14
+
15
+ ## Step 2 — create the skill file
16
+
17
+ Write the file shown below to the path noted above its fence. This skill is intentionally minimal — no prose, no recommendations.
18
+
19
+ ### Skill template — `.claude/skills/visualize/SKILL.md`
20
+
21
+ ```markdown
22
+ {{ skill_template_body }}
23
+ ```
24
+
25
+ ## Step 3 — invoke it
26
+
27
+ Say "what do I have" or "show the setup" and invoke `/visualize` (or equivalent). You'll get a compact status bar.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: visualize
3
+ description: At-a-glance snapshot of the project's agent setup — counts only, compact.
4
+ type: command
5
+ ---
6
+
7
+ # Visualize
8
+
9
+ When the user says "what do I have", "show the setup", or similar.
10
+
11
+ ## Process
12
+
13
+ 1. `agex overview --agent claude-code`
14
+ 2. Emit a compact block: `Skills: N · Hooks: M · MCP: K · CLAUDE.md: ✓/✗`
15
+ 3. Stop. No recommendations, no prose paragraphs.
16
+
17
+ ## Rule
18
+
19
+ Target < 500 output tokens. If you can't fit the summary, trim further.
File without changes
File without changes
@@ -0,0 +1,72 @@
1
+ import re
2
+ from importlib.resources import as_file, files
3
+ from importlib.resources.abc import Traversable
4
+
5
+ from agent_experience.core.backend import Backend
6
+ from agent_experience.core.render import render_string
7
+ from agent_experience.core.skill_loader import Skill, load_skill
8
+
9
+ _TOPIC_RE = re.compile(r"^[a-z][a-z0-9-]*$")
10
+ _SKILL_FILENAME = "SKILL.md"
11
+
12
+
13
+ def _learn_assets() -> Traversable:
14
+ # Anchor on the `commands` package (which has __init__.py) and navigate in.
15
+ # Avoids relying on namespace-package semantics for `assets/`, which is a
16
+ # data directory, not a package. Matches explain.py / overview.py pattern.
17
+ return files("agent_experience.commands").joinpath("learn", "assets")
18
+
19
+
20
+ def _load_skill_from_traversable(trav: Traversable) -> Skill:
21
+ # load_skill expects a pathlib.Path; resolve via as_file when needed.
22
+ with as_file(trav) as path:
23
+ return load_skill(path)
24
+
25
+
26
+ def _list_topics() -> list[dict]:
27
+ topics: list[dict] = []
28
+ topics_root = _learn_assets().joinpath("topics")
29
+ for topic_dir in sorted(topics_root.iterdir(), key=lambda p: p.name):
30
+ if topic_dir.is_file():
31
+ continue
32
+ skill_md = topic_dir.joinpath(_SKILL_FILENAME)
33
+ if not skill_md.is_file():
34
+ continue
35
+ skill = _load_skill_from_traversable(skill_md)
36
+ # Use the directory name as the canonical slug — `run_topic` looks up
37
+ # by directory, so the menu's `agex learn <name>` invocation must
38
+ # match. Frontmatter `skill.name` is still available for drift checks.
39
+ topics.append(
40
+ {"name": topic_dir.name, "description": skill.description, "unsupported": None}
41
+ )
42
+ return topics
43
+
44
+
45
+ def run_menu(backend: Backend) -> tuple[str, int, str]:
46
+ """Return (stdout, exit_code, stderr) for the topic menu."""
47
+ topics = _list_topics()
48
+ template_text = _learn_assets().joinpath("menu.md.j2").read_text(encoding="utf-8")
49
+ out = render_string(template_text, {"backend": backend.value, "topics": topics})
50
+ return (out, 0, "")
51
+
52
+
53
+ def run_topic(topic: str, backend: Backend) -> tuple[str, int, str]:
54
+ """Return (stdout, exit_code, stderr) for a specific lesson topic."""
55
+ if not _TOPIC_RE.match(topic):
56
+ menu_out, _, _ = run_menu(backend)
57
+ return (menu_out, 2, f"agex: error: unknown topic '{topic}'")
58
+
59
+ topic_dir = _learn_assets().joinpath("topics", topic)
60
+ skill_md = topic_dir.joinpath(_SKILL_FILENAME)
61
+ if not skill_md.is_file():
62
+ menu_out, _, _ = run_menu(backend)
63
+ return (menu_out, 2, f"agex: error: unknown topic '{topic}'")
64
+
65
+ skill = _load_skill_from_traversable(skill_md)
66
+ template_path = topic_dir.joinpath("assets", "skill-template", backend.value, _SKILL_FILENAME)
67
+ template_body = template_path.read_text(encoding="utf-8") if template_path.is_file() else ""
68
+ rendered = render_string(
69
+ skill.body,
70
+ {"backend": backend.value, "skill_template_body": template_body},
71
+ )
72
+ return (rendered, 0, "")