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.
- agent_experience/__init__.py +3 -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 +170 -0
- agent_experience/commands/__init__.py +0 -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 +35 -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 +63 -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 +196 -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 +38 -0
- agent_experience/commands/hook/scripts/write.py +24 -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/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 +72 -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/core/__init__.py +0 -0
- agent_experience/core/backend.py +16 -0
- agent_experience/core/capabilities.py +44 -0
- agent_experience/core/config.py +42 -0
- agent_experience/core/hook_io.py +95 -0
- agent_experience/core/paths.py +26 -0
- agent_experience/core/render.py +27 -0
- agent_experience/core/skill_loader.py +36 -0
- agex_cli-0.11.0.dist-info/METADATA +56 -0
- agex_cli-0.11.0.dist-info/RECORD +73 -0
- agex_cli-0.11.0.dist-info/WHEEL +4 -0
- agex_cli-0.11.0.dist-info/entry_points.txt +2 -0
- 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.
|
agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md
ADDED
|
@@ -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.
|
agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md
ADDED
|
@@ -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).
|
agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md
ADDED
|
@@ -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.
|
agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md
ADDED
|
@@ -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, "")
|