devex-cli 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_experience/__init__.py +24 -0
- agent_experience/__main__.py +4 -0
- agent_experience/backends/__init__.py +0 -0
- agent_experience/backends/acp/__init__.py +0 -0
- agent_experience/backends/acp/probe.py +9 -0
- agent_experience/backends/capabilities/acp.yaml +7 -0
- agent_experience/backends/capabilities/claude-code.yaml +4 -0
- agent_experience/backends/capabilities/codex.yaml +7 -0
- agent_experience/backends/capabilities/copilot.yaml +7 -0
- agent_experience/backends/claude_code/__init__.py +0 -0
- agent_experience/backends/claude_code/probe.py +97 -0
- agent_experience/backends/codex/__init__.py +0 -0
- agent_experience/backends/codex/probe.py +16 -0
- agent_experience/backends/copilot/__init__.py +0 -0
- agent_experience/backends/copilot/probe.py +9 -0
- agent_experience/cli.py +485 -0
- agent_experience/commands/__init__.py +0 -0
- agent_experience/commands/doctor/SKILL.md +41 -0
- agent_experience/commands/doctor/__init__.py +0 -0
- agent_experience/commands/doctor/assets/report.md.j2 +39 -0
- agent_experience/commands/doctor/references/design.md +36 -0
- agent_experience/commands/doctor/scripts/__init__.py +0 -0
- agent_experience/commands/doctor/scripts/doctor.py +394 -0
- agent_experience/commands/explain/SKILL.md +26 -0
- agent_experience/commands/explain/__init__.py +0 -0
- agent_experience/commands/explain/assets/topics/agex.md +37 -0
- agent_experience/commands/explain/references/.gitkeep +0 -0
- agent_experience/commands/explain/scripts/__init__.py +0 -0
- agent_experience/commands/explain/scripts/explain.py +64 -0
- agent_experience/commands/gamify/SKILL.md +31 -0
- agent_experience/commands/gamify/__init__.py +0 -0
- agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
- agent_experience/commands/gamify/references/.gitkeep +0 -0
- agent_experience/commands/gamify/scripts/__init__.py +0 -0
- agent_experience/commands/gamify/scripts/install.py +203 -0
- agent_experience/commands/hook/SKILL.md +31 -0
- agent_experience/commands/hook/__init__.py +0 -0
- agent_experience/commands/hook/assets/table.md.j2 +17 -0
- agent_experience/commands/hook/references/.gitkeep +0 -0
- agent_experience/commands/hook/scripts/__init__.py +0 -0
- agent_experience/commands/hook/scripts/read.py +53 -0
- agent_experience/commands/hook/scripts/write.py +25 -0
- agent_experience/commands/learn/SKILL.md +21 -0
- agent_experience/commands/learn/__init__.py +0 -0
- agent_experience/commands/learn/assets/menu.md.j2 +7 -0
- agent_experience/commands/learn/assets/topics/cicd/SKILL.md +103 -0
- agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
- agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
- agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
- agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
- agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
- agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
- agent_experience/commands/learn/references/.gitkeep +0 -0
- agent_experience/commands/learn/scripts/__init__.py +0 -0
- agent_experience/commands/learn/scripts/learn.py +73 -0
- agent_experience/commands/overview/SKILL.md +31 -0
- agent_experience/commands/overview/__init__.py +0 -0
- agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
- agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
- agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
- agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
- agent_experience/commands/overview/assets/sections.md.j2 +52 -0
- agent_experience/commands/overview/references/.gitkeep +0 -0
- agent_experience/commands/overview/scripts/__init__.py +0 -0
- agent_experience/commands/overview/scripts/overview.py +40 -0
- agent_experience/commands/pr/SKILL.md +90 -0
- agent_experience/commands/pr/__init__.py +0 -0
- agent_experience/commands/pr/assets/__init__.py +0 -0
- agent_experience/commands/pr/assets/backends/__init__.py +0 -0
- agent_experience/commands/pr/assets/backends/acp.yaml +21 -0
- agent_experience/commands/pr/assets/backends/claude-code.yaml +21 -0
- agent_experience/commands/pr/assets/backends/codex.yaml +21 -0
- agent_experience/commands/pr/assets/backends/copilot.yaml +21 -0
- agent_experience/commands/pr/assets/rules/__init__.py +0 -0
- agent_experience/commands/pr/assets/rules/lint_rules.py +79 -0
- agent_experience/commands/pr/assets/rules/next_step_rules.py +78 -0
- agent_experience/commands/pr/assets/templates/__init__.py +0 -0
- agent_experience/commands/pr/assets/templates/delta.md.j2 +32 -0
- agent_experience/commands/pr/assets/templates/footer.md.j2 +2 -0
- agent_experience/commands/pr/assets/templates/lint_result.md.j2 +19 -0
- agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +69 -0
- agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +17 -0
- agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +15 -0
- agent_experience/commands/pr/assets/templates/pr_review_result.md.j2 +5 -0
- agent_experience/commands/pr/scripts/__init__.py +0 -0
- agent_experience/commands/pr/scripts/_footer.py +32 -0
- agent_experience/commands/pr/scripts/_journal.py +21 -0
- agent_experience/commands/pr/scripts/_qodo.py +147 -0
- agent_experience/commands/pr/scripts/_readiness.py +76 -0
- agent_experience/commands/pr/scripts/_sonar.py +29 -0
- agent_experience/commands/pr/scripts/await_.py +156 -0
- agent_experience/commands/pr/scripts/delta.py +84 -0
- agent_experience/commands/pr/scripts/lint.py +72 -0
- agent_experience/commands/pr/scripts/open_.py +104 -0
- agent_experience/commands/pr/scripts/read.py +151 -0
- agent_experience/commands/pr/scripts/reply.py +160 -0
- agent_experience/commands/pr/scripts/review.py +59 -0
- agent_experience/core/__init__.py +0 -0
- agent_experience/core/backend.py +80 -0
- agent_experience/core/capabilities.py +44 -0
- agent_experience/core/config.py +46 -0
- agent_experience/core/github.py +355 -0
- agent_experience/core/hook_io.py +95 -0
- agent_experience/core/journal.py +90 -0
- agent_experience/core/paths.py +26 -0
- agent_experience/core/prog.py +44 -0
- agent_experience/core/render.py +42 -0
- agent_experience/core/skill_loader.py +36 -0
- devex_cli-0.24.0.dist-info/METADATA +55 -0
- devex_cli-0.24.0.dist-info/RECORD +115 -0
- devex_cli-0.24.0.dist-info/WHEEL +4 -0
- devex_cli-0.24.0.dist-info/entry_points.txt +3 -0
- devex_cli-0.24.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
from agent_experience.core.prog import error_prefix, prog_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _fragments_file() -> Traversable:
|
|
15
|
+
return files("agent_experience.commands.gamify").joinpath("assets", "hooks", "claude-code.json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fragments_for(backend: Backend) -> list[dict]:
|
|
19
|
+
if backend != Backend.CLAUDE_CODE:
|
|
20
|
+
return []
|
|
21
|
+
data = json.loads(_fragments_file().read_text(encoding="utf-8"))
|
|
22
|
+
return data["fragments"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _hooks_file_for(backend: Backend, project_dir: Path) -> Path | None:
|
|
26
|
+
if backend == Backend.CLAUDE_CODE:
|
|
27
|
+
return project_dir / ".claude" / "hooks.json"
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _refuse(path: Path, reason: str) -> ValueError:
|
|
32
|
+
return ValueError(
|
|
33
|
+
f"{path} {reason}; refusing to overwrite. " "Fix or remove the file before re-running."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_hooks_file(path: Path) -> dict:
|
|
38
|
+
# Malformed or unexpectedly-shaped files are NEVER silently overwritten —
|
|
39
|
+
# the caller gets a ValueError, surfaces it as exit 2, and the user's file
|
|
40
|
+
# stays on disk untouched.
|
|
41
|
+
if not path.exists():
|
|
42
|
+
return {}
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
45
|
+
except json.JSONDecodeError as e:
|
|
46
|
+
raise _refuse(path, f"is not valid JSON ({e})") from e
|
|
47
|
+
if not isinstance(data, dict):
|
|
48
|
+
raise _refuse(path, "is not a JSON object at the top level")
|
|
49
|
+
for entries in data.values():
|
|
50
|
+
if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
|
|
51
|
+
raise _refuse(path, "is not a mapping of event → list of hook objects")
|
|
52
|
+
return data
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _write_hooks_file(path: Path, data: dict) -> None:
|
|
56
|
+
# Sonar pythonsecurity:S2083: `path` derives from
|
|
57
|
+
# Path.cwd()/".claude/hooks.json"; the backend is enum-validated by
|
|
58
|
+
# parse_backend() before reaching here. Full rationale lives in
|
|
59
|
+
# sonar-project.properties. The suppression tag below is the
|
|
60
|
+
# load-bearing one under SonarCloud Automatic Analysis.
|
|
61
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") # NOSONAR
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _merge_fragments(hooks: dict, fragments: list[dict]) -> tuple[list[str], int]:
|
|
66
|
+
"""Append each fragment's {id, hook} under its event if the id isn't already
|
|
67
|
+
there. Returns (all_fragment_ids_in_insertion_order, added_count)."""
|
|
68
|
+
written_ids: list[str] = []
|
|
69
|
+
added = 0
|
|
70
|
+
for frag in fragments:
|
|
71
|
+
event = frag["event"]
|
|
72
|
+
entry = {"id": frag["id"], "hook": frag["hook"]}
|
|
73
|
+
hooks.setdefault(event, [])
|
|
74
|
+
if not any(e.get("id") == frag["id"] for e in hooks[event]):
|
|
75
|
+
hooks[event].append(entry)
|
|
76
|
+
added += 1
|
|
77
|
+
written_ids.append(frag["id"])
|
|
78
|
+
return written_ids, added
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _remove_ids_from_hooks(hooks: dict, ids_to_remove: set[str]) -> int:
|
|
82
|
+
"""Filter entries matching ids_to_remove out of each event. Event keys whose
|
|
83
|
+
arrays become empty as a result of removal are deleted. Pre-existing empty
|
|
84
|
+
event arrays are left intact. Returns total entries removed."""
|
|
85
|
+
removed = 0
|
|
86
|
+
for event in list(hooks.keys()):
|
|
87
|
+
original = hooks[event]
|
|
88
|
+
filtered = [e for e in original if e.get("id") not in ids_to_remove]
|
|
89
|
+
event_removed = len(original) - len(filtered)
|
|
90
|
+
if event_removed == 0:
|
|
91
|
+
continue
|
|
92
|
+
removed += event_removed
|
|
93
|
+
if filtered:
|
|
94
|
+
hooks[event] = filtered
|
|
95
|
+
else:
|
|
96
|
+
del hooks[event]
|
|
97
|
+
return removed
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def install(backend: Backend) -> tuple[str, int, str]:
|
|
101
|
+
ensure_init()
|
|
102
|
+
project_dir = Path.cwd()
|
|
103
|
+
hooks_file = _hooks_file_for(backend, project_dir)
|
|
104
|
+
if hooks_file is None:
|
|
105
|
+
return (_unsupported_notice(backend), 0, "")
|
|
106
|
+
|
|
107
|
+
fragments = _fragments_for(backend)
|
|
108
|
+
if not fragments:
|
|
109
|
+
return (_unsupported_notice(backend), 0, "")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
hooks = _load_hooks_file(hooks_file)
|
|
113
|
+
except ValueError as e:
|
|
114
|
+
return ("", 2, error_prefix(str(e)))
|
|
115
|
+
|
|
116
|
+
written_ids, added_count = _merge_fragments(hooks, fragments)
|
|
117
|
+
if added_count:
|
|
118
|
+
_write_hooks_file(hooks_file, hooks)
|
|
119
|
+
|
|
120
|
+
cfg = load_config()
|
|
121
|
+
previous = cfg.installed.get("gamify", {})
|
|
122
|
+
if previous.get("hook_fragment_ids") != written_ids:
|
|
123
|
+
cfg.installed["gamify"] = {
|
|
124
|
+
"at": datetime.now(tz=timezone.utc).isoformat(),
|
|
125
|
+
"hook_fragment_ids": written_ids,
|
|
126
|
+
}
|
|
127
|
+
save_config(cfg)
|
|
128
|
+
|
|
129
|
+
rel = hooks_file.relative_to(project_dir)
|
|
130
|
+
if added_count:
|
|
131
|
+
status_line = (
|
|
132
|
+
f"- Added {added_count} hook fragment(s); "
|
|
133
|
+
f"ensured {len(written_ids)} present in `{rel}`."
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
status_line = (
|
|
137
|
+
f"- Ensured {len(written_ids)} hook fragment(s) already present "
|
|
138
|
+
f"in `{rel}` (no changes)."
|
|
139
|
+
)
|
|
140
|
+
lines = [
|
|
141
|
+
f"# Gamify installed — {backend.value}",
|
|
142
|
+
"",
|
|
143
|
+
status_line,
|
|
144
|
+
"- Fragment IDs: " + ", ".join(f"`{i}`" for i in written_ids),
|
|
145
|
+
"",
|
|
146
|
+
f"Next: run `{prog_name()} learn gamify --agent {backend.value}`"
|
|
147
|
+
" to set up the levelup skill.",
|
|
148
|
+
"",
|
|
149
|
+
]
|
|
150
|
+
return ("\n".join(lines), 0, "")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def uninstall(backend: Backend) -> tuple[str, int, str]:
|
|
154
|
+
ensure_init()
|
|
155
|
+
project_dir = Path.cwd()
|
|
156
|
+
hooks_file = _hooks_file_for(backend, project_dir)
|
|
157
|
+
if hooks_file is None:
|
|
158
|
+
return (_unsupported_notice(backend), 0, "")
|
|
159
|
+
|
|
160
|
+
cfg = load_config()
|
|
161
|
+
installed = cfg.installed.get("gamify", {})
|
|
162
|
+
ids_to_remove = set(installed.get("hook_fragment_ids", []))
|
|
163
|
+
if not ids_to_remove:
|
|
164
|
+
return (f"# Gamify uninstalled — nothing to remove on {backend.value}.\n", 0, "")
|
|
165
|
+
|
|
166
|
+
rel = hooks_file.relative_to(project_dir)
|
|
167
|
+
# If the user already removed the hooks file, just drop the config record.
|
|
168
|
+
# Don't re-create the file with an empty object.
|
|
169
|
+
if not hooks_file.exists():
|
|
170
|
+
cfg.installed.pop("gamify", None)
|
|
171
|
+
save_config(cfg)
|
|
172
|
+
return (
|
|
173
|
+
f"# Gamify uninstalled — `{rel}` was already gone; " "cleared config record.\n",
|
|
174
|
+
0,
|
|
175
|
+
"",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
hooks = _load_hooks_file(hooks_file)
|
|
180
|
+
except ValueError as e:
|
|
181
|
+
return ("", 2, error_prefix(str(e)))
|
|
182
|
+
|
|
183
|
+
removed_count = _remove_ids_from_hooks(hooks, ids_to_remove)
|
|
184
|
+
if removed_count:
|
|
185
|
+
_write_hooks_file(hooks_file, hooks)
|
|
186
|
+
|
|
187
|
+
cfg.installed.pop("gamify", None)
|
|
188
|
+
save_config(cfg)
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
f"# Gamify uninstalled — removed {removed_count} fragment(s) from `{rel}`.\n",
|
|
192
|
+
0,
|
|
193
|
+
"",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _unsupported_notice(backend: Backend) -> str:
|
|
198
|
+
return (
|
|
199
|
+
f"## `gamify` is not supported on {backend.value}\n\n"
|
|
200
|
+
f"Hooks are required to track usage events, and {backend.value} does not expose "
|
|
201
|
+
f"a hook interface {prog_name()} can write to.\n\n"
|
|
202
|
+
"Want this supported? Open an issue: <https://github.com/agentculture/devex/issues>\n"
|
|
203
|
+
)
|
|
@@ -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,53 @@
|
|
|
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 import journal as _journal
|
|
7
|
+
from agent_experience.core.paths import data_dir, ensure_init
|
|
8
|
+
from agent_experience.core.render import render_string
|
|
9
|
+
|
|
10
|
+
KNOWN_STREAMS = ["post-tool-use", "user-prompt", "stop", "sessions"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _assets_root() -> 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 overview.py / learn.py pattern.
|
|
17
|
+
return files("agent_experience.commands").joinpath("hook", "assets")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _summarize(events):
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
"ts": e.get("ts", ""),
|
|
24
|
+
"details": ", ".join(f"{k}={v}" for k, v in e.items() if k not in ("ts", "event")),
|
|
25
|
+
}
|
|
26
|
+
for e in events
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(backend: Backend) -> tuple[str, int, str]:
|
|
31
|
+
ensure_init()
|
|
32
|
+
streams = []
|
|
33
|
+
|
|
34
|
+
# Flat streams: data/<name>.json
|
|
35
|
+
for name in KNOWN_STREAMS:
|
|
36
|
+
events = load_events(name)
|
|
37
|
+
streams.append({"name": name, "events": _summarize(events)})
|
|
38
|
+
|
|
39
|
+
# Nested streams: data/<subdir>/<name>.jsonl
|
|
40
|
+
root = data_dir()
|
|
41
|
+
if root.exists():
|
|
42
|
+
for subdir in sorted(p for p in root.iterdir() if p.is_dir()):
|
|
43
|
+
for jsonl_file in sorted(subdir.glob("*.jsonl")):
|
|
44
|
+
stream_name = f"{subdir.name}/{jsonl_file.stem}"
|
|
45
|
+
events = _journal.load_events(stream_name)
|
|
46
|
+
streams.append({"name": stream_name, "events": _summarize(events)})
|
|
47
|
+
|
|
48
|
+
template_text = _assets_root().joinpath("table.md.j2").read_text(encoding="utf-8")
|
|
49
|
+
out = render_string(
|
|
50
|
+
template_text,
|
|
51
|
+
{"backend": backend.value, "source": str(root), "streams": streams},
|
|
52
|
+
)
|
|
53
|
+
return (out, 0, "")
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
from agent_experience.core.prog import error_prefix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(event: str, args: list[str]) -> tuple[str, int, str]:
|
|
9
|
+
ensure_init()
|
|
10
|
+
payload: dict = {"ts": datetime.now(tz=timezone.utc).isoformat()}
|
|
11
|
+
for arg in args:
|
|
12
|
+
if "=" in arg:
|
|
13
|
+
k, v = arg.split("=", 1)
|
|
14
|
+
if k:
|
|
15
|
+
payload[k] = v
|
|
16
|
+
# Positional event name is authoritative — it overrides any `event=...`
|
|
17
|
+
# pair in args so hook scripts can't misattribute events.
|
|
18
|
+
payload["event"] = event
|
|
19
|
+
try:
|
|
20
|
+
append_event(event, payload)
|
|
21
|
+
except ValueError as e:
|
|
22
|
+
# append_event → _stream_path rejects names that don't match the
|
|
23
|
+
# `^[a-z][a-z0-9-]*$` slug whitelist (path-traversal guard).
|
|
24
|
+
return ("", 2, error_prefix(str(e)))
|
|
25
|
+
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 `{{ prog }} 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,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cicd
|
|
3
|
+
description: How to use `agex pr` to ship a PR end-to-end — lint, open with auto-signature, fetch the unified briefing (status + comments + readiness), batch-reply with thread resolution, and run alignment-delta when needed.
|
|
4
|
+
type: lesson
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Lesson — CI/CD with `agex pr` for {{ backend }}
|
|
8
|
+
|
|
9
|
+
The full PR loop boils down to six commands. Each one ends with a
|
|
10
|
+
**Next step:** footer that names the right next command — chain it.
|
|
11
|
+
|
|
12
|
+
## Standard happy path
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git checkout -b feat/<desc>
|
|
16
|
+
# ... edit ...
|
|
17
|
+
agex pr lint --agent {{ backend }} # portability + alignment
|
|
18
|
+
git commit -am "..." && git push -u origin <branch>
|
|
19
|
+
|
|
20
|
+
agex pr open --agent {{ backend }} \
|
|
21
|
+
--title "..." --body-file ./body.md \
|
|
22
|
+
--delayed-read # creates PR + waits 180s + briefing
|
|
23
|
+
|
|
24
|
+
# briefing arrived; triage and prepare replies.jsonl, then:
|
|
25
|
+
agex pr reply <PR> --agent {{ backend }} < replies.jsonl
|
|
26
|
+
|
|
27
|
+
# fix anything, push, then:
|
|
28
|
+
agex pr read <PR> --agent {{ backend }} --wait 180
|
|
29
|
+
# repeat until reviewers quiet + CI green
|
|
30
|
+
# wait for human merge — never merge yourself
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## `read --wait` vs. `await`
|
|
34
|
+
|
|
35
|
+
Both poll the same readiness loop. They differ on what they do with
|
|
36
|
+
the result:
|
|
37
|
+
|
|
38
|
+
- `agex pr read <PR> --wait 180` — always exits 0. Renders the briefing
|
|
39
|
+
and lets the agent decide. Use when you want the unified view.
|
|
40
|
+
- `agex pr await <PR>` — exits **1** on SonarCloud gate `ERROR`,
|
|
41
|
+
unresolved review threads, or failing CI checks; **0** otherwise
|
|
42
|
+
(clean state or timeout). Use when you want to gate the next command
|
|
43
|
+
on PR health (e.g., in a shell loop that should fail if Sonar or CI
|
|
44
|
+
is red).
|
|
45
|
+
|
|
46
|
+
## When CLAUDE.md / culture.yaml / .claude/skills change
|
|
47
|
+
|
|
48
|
+
`agex pr lint` flags this and points you at:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
agex pr delta --agent {{ backend }}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Read each sibling's CLAUDE.md head + culture.yaml, decide whether each
|
|
55
|
+
needs a follow-up PR, and mention any drift in your reply.
|
|
56
|
+
|
|
57
|
+
## JSONL reply shape
|
|
58
|
+
|
|
59
|
+
Each line of stdin to `agex pr reply <PR>`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{"in_reply_to": 123456, "thread_id": "T_kw...", "body": "Fixed in <commit>."}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `in_reply_to` is the inline review-comment id. Omit for top-level conversation comments.
|
|
66
|
+
- `thread_id` triggers `resolveReviewThread` after the post.
|
|
67
|
+
- `body` is auto-signed with `- <nick> (Claude)` if the signature is missing. `<nick>` comes from the first agent's `suffix` in `culture.yaml`, falling back to the repo basename.
|
|
68
|
+
|
|
69
|
+
## Side effects
|
|
70
|
+
|
|
71
|
+
Network: every command except `lint` and `delta` talks to GitHub via `gh`.
|
|
72
|
+
Disk: `pr open`, `pr read`, and `pr reply` append events to
|
|
73
|
+
`.agex/data/pr/events.jsonl` for retrospective tooling.
|
|
74
|
+
|
|
75
|
+
## When something goes wrong
|
|
76
|
+
|
|
77
|
+
- `gh` not installed → `agex: install gh — https://cli.github.com/ — then rerun`
|
|
78
|
+
- `gh` not authenticated → `agex: run 'gh auth login' then rerun`
|
|
79
|
+
- `pr reply` partial failure → stderr names the line slice to resubmit; the
|
|
80
|
+
command stops at the first failure to keep recovery surgical.
|
|
81
|
+
- `pr read --wait` / `pr await` timeout → exit 0 with a "Still waiting on:
|
|
82
|
+
<reviewers>" banner; rerun the same command to keep waiting.
|
|
83
|
+
|
|
84
|
+
## Long waits (5-minute cache TTL)
|
|
85
|
+
|
|
86
|
+
Anthropic's prompt cache has a 5-minute TTL. If you expect to wait
|
|
87
|
+
longer than that — for example, polling a slow CI gate with
|
|
88
|
+
`agex pr read <PR> --wait 600` or `agex pr await <PR> --max-wait 1800`
|
|
89
|
+
— run the wait inside a subagent and triage the result when it fires.
|
|
90
|
+
The parent session keeps its cache warm; only the subagent pays the
|
|
91
|
+
per-iteration cost.
|
|
92
|
+
|
|
93
|
+
Pattern (Claude Code): spawn an `Agent(..., run_in_background=true)`
|
|
94
|
+
that runs the command and echoes the final headline + exit code.
|
|
95
|
+
Continue with unrelated work; act on the notification when it
|
|
96
|
+
arrives. This is exactly how steward's `cicd` workflow handles its
|
|
97
|
+
`workflow.sh await <PR>` chains.
|
|
98
|
+
|
|
99
|
+
## Reply etiquette
|
|
100
|
+
|
|
101
|
+
Every comment gets a reply — no silent fixes. Always include a
|
|
102
|
+
`thread_id` so the thread closes automatically. Reference the fix-up
|
|
103
|
+
commit SHA in the reply body where relevant.
|
|
@@ -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.
|