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
agent_experience/cli.py
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""agex CLI — stdlib argparse front end.
|
|
2
|
+
|
|
3
|
+
No third-party CLI framework: this module routes `agex <command> [args]`
|
|
4
|
+
through `argparse` only, mirroring the skeleton used by the sibling Culture
|
|
5
|
+
repos (steward, devague). Business logic stays in `commands/<name>/scripts/`,
|
|
6
|
+
which return ``(stdout, exit_code, stderr)`` tuples; this module just parses
|
|
7
|
+
arguments and echoes those tuples. Adding a backend or command never touches
|
|
8
|
+
the parsing core beyond a `register_*` call.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from agent_experience import __version__
|
|
17
|
+
from agent_experience.commands.doctor.scripts import doctor as doctor_script
|
|
18
|
+
from agent_experience.commands.explain.scripts import explain as explain_script
|
|
19
|
+
from agent_experience.commands.gamify.scripts import install as gamify_script
|
|
20
|
+
from agent_experience.commands.hook.scripts import read as hook_read_script
|
|
21
|
+
from agent_experience.commands.hook.scripts import write as hook_write_script
|
|
22
|
+
from agent_experience.commands.learn.scripts import learn as learn_script
|
|
23
|
+
from agent_experience.commands.overview.scripts import overview as overview_script
|
|
24
|
+
from agent_experience.commands.pr.scripts import await_ as pr_await_script
|
|
25
|
+
from agent_experience.commands.pr.scripts import delta as pr_delta_script
|
|
26
|
+
from agent_experience.commands.pr.scripts import lint as pr_lint_script
|
|
27
|
+
from agent_experience.commands.pr.scripts import open_ as pr_open_script
|
|
28
|
+
from agent_experience.commands.pr.scripts import read as pr_read_script
|
|
29
|
+
from agent_experience.commands.pr.scripts import reply as pr_reply_script
|
|
30
|
+
from agent_experience.commands.pr.scripts import review as pr_review_script
|
|
31
|
+
from agent_experience.core.backend import parse_backend
|
|
32
|
+
from agent_experience.core.prog import prog_name
|
|
33
|
+
|
|
34
|
+
_AGENT_HELP = "Backend: claude-code, codex, copilot, or acp."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _gh_rerun_hint() -> str:
|
|
38
|
+
"""``<prog>: rerun once network is reachable`` — phrased with the invoked name."""
|
|
39
|
+
return f"{prog_name()}: rerun once network is reachable (gh failed)"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _AgexArgumentParser(argparse.ArgumentParser):
|
|
43
|
+
"""ArgumentParser used everywhere via ``parser_class=``.
|
|
44
|
+
|
|
45
|
+
argparse's native ``error()`` already prints usage to stderr and exits
|
|
46
|
+
with code 2 — which matches agex's existing bad-argument behavior — so no
|
|
47
|
+
override is required. The subclass exists only so nested subparsers inherit
|
|
48
|
+
it and to give a single place for any future tweak.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Output helpers — preserve the exact newline behavior of the old typer.echo
|
|
54
|
+
# calls. ``typer.echo(x, nl=False)`` wrote x verbatim; ``typer.echo(x)`` added
|
|
55
|
+
# a newline; ``err=True`` selected stderr.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _emit(stdout: str, stderr: str, *, stderr_newline: bool = True) -> None:
|
|
60
|
+
"""Write a command's stdout/stderr the way the old CLI did."""
|
|
61
|
+
if stdout:
|
|
62
|
+
sys.stdout.write(stdout)
|
|
63
|
+
if stderr:
|
|
64
|
+
if stderr_newline:
|
|
65
|
+
print(stderr, file=sys.stderr)
|
|
66
|
+
else:
|
|
67
|
+
sys.stderr.write(stderr)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_backend_or_report(agent: Optional[str]):
|
|
71
|
+
"""Parse ``--agent`` into a Backend.
|
|
72
|
+
|
|
73
|
+
Returns ``(backend, None)`` on success, or ``(None, 2)`` after printing the
|
|
74
|
+
canonical ``agex: error: <msg>`` to stderr — letting the caller ``return``
|
|
75
|
+
the exit code without exception gymnastics.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
return parse_backend(agent), None
|
|
79
|
+
except ValueError as exc:
|
|
80
|
+
print(f"{prog_name()}: error: {exc}", file=sys.stderr)
|
|
81
|
+
return None, 2
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Top-level command handlers
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cmd_explain(args: argparse.Namespace) -> int:
|
|
90
|
+
stdout, exit_code, stderr = explain_script.run(args.topic)
|
|
91
|
+
_emit(stdout, stderr)
|
|
92
|
+
return exit_code
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
96
|
+
stdout, exit_code, stderr = doctor_script.run(args.role)
|
|
97
|
+
_emit(stdout, stderr)
|
|
98
|
+
return exit_code
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_learn(args: argparse.Namespace) -> int:
|
|
102
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
103
|
+
if err is not None:
|
|
104
|
+
return err
|
|
105
|
+
if args.topic is None:
|
|
106
|
+
stdout, exit_code, stderr = learn_script.run_menu(backend)
|
|
107
|
+
else:
|
|
108
|
+
stdout, exit_code, stderr = learn_script.run_topic(args.topic, backend)
|
|
109
|
+
_emit(stdout, stderr)
|
|
110
|
+
return exit_code
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_gamify(args: argparse.Namespace) -> int:
|
|
114
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
115
|
+
if err is not None:
|
|
116
|
+
return err
|
|
117
|
+
if args.uninstall:
|
|
118
|
+
stdout, exit_code, stderr = gamify_script.uninstall(backend)
|
|
119
|
+
else:
|
|
120
|
+
stdout, exit_code, stderr = gamify_script.install(backend)
|
|
121
|
+
_emit(stdout, stderr)
|
|
122
|
+
return exit_code
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _cmd_overview(args: argparse.Namespace) -> int:
|
|
126
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
127
|
+
if err is not None:
|
|
128
|
+
return err
|
|
129
|
+
stdout, exit_code, stderr = overview_script.run(backend)
|
|
130
|
+
_emit(stdout, stderr)
|
|
131
|
+
return exit_code
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# hook subcommands
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _cmd_hook_write(args: argparse.Namespace) -> int:
|
|
140
|
+
_, exit_code, stderr = hook_write_script.run(args.event, args.args or [])
|
|
141
|
+
_emit("", stderr)
|
|
142
|
+
return exit_code
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cmd_hook_read(args: argparse.Namespace) -> int:
|
|
146
|
+
backend, err = _parse_backend_or_report(args.agent)
|
|
147
|
+
if err is not None:
|
|
148
|
+
return err
|
|
149
|
+
stdout, exit_code, stderr = hook_read_script.run(backend)
|
|
150
|
+
_emit(stdout, stderr)
|
|
151
|
+
return exit_code
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# pr subcommands
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cmd_pr_lint(args: argparse.Namespace) -> int:
|
|
160
|
+
try:
|
|
161
|
+
stdout, exit_code, stderr = pr_lint_script.run(
|
|
162
|
+
agent=args.agent, project_dir=Path.cwd(), exit_on_violation=args.exit_on_violation
|
|
163
|
+
)
|
|
164
|
+
except ValueError as exc:
|
|
165
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
166
|
+
return 2
|
|
167
|
+
_emit(stdout, stderr)
|
|
168
|
+
return exit_code
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_pr_open(args: argparse.Namespace) -> int:
|
|
172
|
+
try:
|
|
173
|
+
stdout, exit_code, stderr = pr_open_script.run(
|
|
174
|
+
agent=args.agent,
|
|
175
|
+
project_dir=Path.cwd(),
|
|
176
|
+
title=args.title,
|
|
177
|
+
body_file=args.body_file,
|
|
178
|
+
draft=args.draft,
|
|
179
|
+
delayed_read=args.delayed_read,
|
|
180
|
+
)
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
183
|
+
return 2
|
|
184
|
+
except RuntimeError as exc:
|
|
185
|
+
prog = prog_name()
|
|
186
|
+
print(str(exc), file=sys.stderr)
|
|
187
|
+
print(f"{prog}: rerun '{prog} pr open ...' once network is reachable", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
_emit(stdout, stderr)
|
|
190
|
+
return exit_code
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _cmd_pr_reply(args: argparse.Namespace) -> int:
|
|
194
|
+
try:
|
|
195
|
+
stdout, exit_code, stderr = pr_reply_script.run(
|
|
196
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr
|
|
197
|
+
)
|
|
198
|
+
except ValueError as exc:
|
|
199
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
200
|
+
return 2
|
|
201
|
+
except RuntimeError as exc:
|
|
202
|
+
print(str(exc), file=sys.stderr)
|
|
203
|
+
print(_gh_rerun_hint(), file=sys.stderr)
|
|
204
|
+
return 1
|
|
205
|
+
_emit(stdout, stderr, stderr_newline=False)
|
|
206
|
+
return exit_code
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _cmd_pr_read(args: argparse.Namespace) -> int:
|
|
210
|
+
try:
|
|
211
|
+
stdout, exit_code, stderr = pr_read_script.run(
|
|
212
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr, wait=args.wait
|
|
213
|
+
)
|
|
214
|
+
except ValueError as exc:
|
|
215
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
except RuntimeError as exc:
|
|
218
|
+
print(str(exc), file=sys.stderr)
|
|
219
|
+
print(_gh_rerun_hint(), file=sys.stderr)
|
|
220
|
+
return 1
|
|
221
|
+
_emit(stdout, stderr)
|
|
222
|
+
return exit_code
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _cmd_pr_await(args: argparse.Namespace) -> int:
|
|
226
|
+
try:
|
|
227
|
+
stdout, exit_code, stderr = pr_await_script.run(
|
|
228
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr, max_wait=args.max_wait
|
|
229
|
+
)
|
|
230
|
+
except ValueError as exc:
|
|
231
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
232
|
+
return 2
|
|
233
|
+
except RuntimeError as exc:
|
|
234
|
+
print(str(exc), file=sys.stderr)
|
|
235
|
+
print(_gh_rerun_hint(), file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
_emit(stdout, stderr)
|
|
238
|
+
return exit_code
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _cmd_pr_review(args: argparse.Namespace) -> int:
|
|
242
|
+
try:
|
|
243
|
+
stdout, exit_code, stderr = pr_review_script.run(
|
|
244
|
+
agent=args.agent, project_dir=Path.cwd(), pr=args.pr
|
|
245
|
+
)
|
|
246
|
+
except ValueError as exc:
|
|
247
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
248
|
+
return 2
|
|
249
|
+
except RuntimeError as exc:
|
|
250
|
+
print(str(exc), file=sys.stderr)
|
|
251
|
+
print(_gh_rerun_hint(), file=sys.stderr)
|
|
252
|
+
return 1
|
|
253
|
+
_emit(stdout, stderr)
|
|
254
|
+
return exit_code
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _cmd_pr_delta(args: argparse.Namespace) -> int:
|
|
258
|
+
try:
|
|
259
|
+
stdout, exit_code, stderr = pr_delta_script.run(agent=args.agent, project_dir=Path.cwd())
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
print(f"{prog_name()}: {exc}", file=sys.stderr)
|
|
262
|
+
return 2
|
|
263
|
+
except RuntimeError as exc:
|
|
264
|
+
print(str(exc), file=sys.stderr)
|
|
265
|
+
print(_gh_rerun_hint(), file=sys.stderr)
|
|
266
|
+
return 1
|
|
267
|
+
_emit(stdout, stderr, stderr_newline=False)
|
|
268
|
+
return exit_code
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# Parser construction
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _add_agent_option(parser: argparse.ArgumentParser, *, required: bool, help_text: str) -> None:
|
|
277
|
+
parser.add_argument("--agent", required=required, default=None, help=help_text)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _group_help(parser: argparse.ArgumentParser):
|
|
281
|
+
"""Return a handler that prints a group's help to stderr and exits 2.
|
|
282
|
+
|
|
283
|
+
Mirrors Typer's ``no_args_is_help`` for ``agex hook`` / ``agex pr`` invoked
|
|
284
|
+
with no subcommand.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def _handle(_args: argparse.Namespace) -> int:
|
|
288
|
+
parser.print_help(sys.stderr)
|
|
289
|
+
return 2
|
|
290
|
+
|
|
291
|
+
return _handle
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
295
|
+
parser = _AgexArgumentParser(
|
|
296
|
+
prog=prog_name(),
|
|
297
|
+
description="Agent-operated developer-experience CLI.",
|
|
298
|
+
)
|
|
299
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
300
|
+
sub = parser.add_subparsers(dest="command", parser_class=_AgexArgumentParser)
|
|
301
|
+
|
|
302
|
+
# explain
|
|
303
|
+
p_explain = sub.add_parser("explain", help="Describe a command or concept.")
|
|
304
|
+
p_explain.add_argument("topic", help="Topic to explain.")
|
|
305
|
+
p_explain.set_defaults(func=_cmd_explain)
|
|
306
|
+
|
|
307
|
+
# doctor
|
|
308
|
+
p_doctor = sub.add_parser("doctor", help="Diagnose the project's agex setup.")
|
|
309
|
+
p_doctor.add_argument(
|
|
310
|
+
"--role", default=None, help="Render a role-specific check section (e.g., pr-review)."
|
|
311
|
+
)
|
|
312
|
+
p_doctor.set_defaults(func=_cmd_doctor)
|
|
313
|
+
|
|
314
|
+
# learn
|
|
315
|
+
p_learn = sub.add_parser("learn", help="Teach a lesson topic (or show the menu).")
|
|
316
|
+
p_learn.add_argument("topic", nargs="?", default=None, help="Lesson topic (omit for menu).")
|
|
317
|
+
_add_agent_option(p_learn, required=True, help_text=_AGENT_HELP)
|
|
318
|
+
p_learn.set_defaults(func=_cmd_learn)
|
|
319
|
+
|
|
320
|
+
# gamify
|
|
321
|
+
p_gamify = sub.add_parser("gamify", help="Install (or uninstall) gamification.")
|
|
322
|
+
_add_agent_option(p_gamify, required=True, help_text=_AGENT_HELP)
|
|
323
|
+
p_gamify.add_argument("--uninstall", action="store_true", help="Reverse gamify.")
|
|
324
|
+
p_gamify.set_defaults(func=_cmd_gamify)
|
|
325
|
+
|
|
326
|
+
# overview
|
|
327
|
+
p_overview = sub.add_parser("overview", help="Render the per-backend overview briefing.")
|
|
328
|
+
_add_agent_option(p_overview, required=True, help_text=_AGENT_HELP)
|
|
329
|
+
p_overview.set_defaults(func=_cmd_overview)
|
|
330
|
+
|
|
331
|
+
_register_hook(sub)
|
|
332
|
+
_register_pr(sub)
|
|
333
|
+
|
|
334
|
+
return parser
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _register_hook(sub: argparse._SubParsersAction) -> None:
|
|
338
|
+
hook_p = sub.add_parser("hook", help="Write and read agex tracking events.")
|
|
339
|
+
hook_sub = hook_p.add_subparsers(dest="hook_command", parser_class=_AgexArgumentParser)
|
|
340
|
+
|
|
341
|
+
p_write = hook_sub.add_parser("write", help="Append a tracking event.")
|
|
342
|
+
p_write.add_argument("event", help="Event name (e.g., post-tool-use).")
|
|
343
|
+
p_write.add_argument("args", nargs="*", help="Additional key=value pairs.")
|
|
344
|
+
p_write.set_defaults(func=_cmd_hook_write)
|
|
345
|
+
|
|
346
|
+
p_read = hook_sub.add_parser("read", help="Render tracked events for a backend.")
|
|
347
|
+
_add_agent_option(p_read, required=True, help_text=_AGENT_HELP)
|
|
348
|
+
p_read.set_defaults(func=_cmd_hook_read)
|
|
349
|
+
|
|
350
|
+
hook_p.set_defaults(func=_group_help(hook_p))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _register_pr(sub: argparse._SubParsersAction) -> None:
|
|
354
|
+
pr_p = sub.add_parser("pr", help="GitHub PR lifecycle commands.")
|
|
355
|
+
pr_sub = pr_p.add_subparsers(dest="pr_command", parser_class=_AgexArgumentParser)
|
|
356
|
+
|
|
357
|
+
p_lint = pr_sub.add_parser("lint", help="Lint the PR branch state.")
|
|
358
|
+
_add_agent_option(
|
|
359
|
+
p_lint,
|
|
360
|
+
required=False,
|
|
361
|
+
help_text="Backend (claude-code|codex|copilot|acp); falls back to culture.yaml.",
|
|
362
|
+
)
|
|
363
|
+
p_lint.add_argument(
|
|
364
|
+
"--exit-on-violation",
|
|
365
|
+
action="store_true",
|
|
366
|
+
help="Exit 1 when violations are found (CI mode).",
|
|
367
|
+
)
|
|
368
|
+
p_lint.set_defaults(func=_cmd_pr_lint)
|
|
369
|
+
|
|
370
|
+
p_open = pr_sub.add_parser("open", help="Open a PR.")
|
|
371
|
+
p_open.add_argument("--title", required=True)
|
|
372
|
+
p_open.add_argument("--body-file", type=Path, default=None)
|
|
373
|
+
p_open.add_argument("--draft", action="store_true", default=False)
|
|
374
|
+
_add_agent_option(p_open, required=False, help_text=_AGENT_HELP)
|
|
375
|
+
p_open.add_argument(
|
|
376
|
+
"--delayed-read",
|
|
377
|
+
action="store_true",
|
|
378
|
+
default=False,
|
|
379
|
+
help="After create, immediately run `pr read --wait 180`.",
|
|
380
|
+
)
|
|
381
|
+
p_open.set_defaults(func=_cmd_pr_open)
|
|
382
|
+
|
|
383
|
+
p_reply = pr_sub.add_parser("reply", help="Reply to PR review threads.")
|
|
384
|
+
p_reply.add_argument("pr", type=int)
|
|
385
|
+
_add_agent_option(p_reply, required=False, help_text=_AGENT_HELP)
|
|
386
|
+
p_reply.set_defaults(func=_cmd_pr_reply)
|
|
387
|
+
|
|
388
|
+
p_read = pr_sub.add_parser("read", help="Read PR review state.")
|
|
389
|
+
p_read.add_argument("pr", type=int, nargs="?", default=None)
|
|
390
|
+
p_read.add_argument(
|
|
391
|
+
"--wait",
|
|
392
|
+
type=int,
|
|
393
|
+
default=None,
|
|
394
|
+
help=(
|
|
395
|
+
"Upper bound in seconds to poll for required-reviewer readiness; "
|
|
396
|
+
"returns early (down to waited=0s) once satisfied."
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
_add_agent_option(p_read, required=False, help_text=_AGENT_HELP)
|
|
400
|
+
p_read.set_defaults(func=_cmd_pr_read)
|
|
401
|
+
|
|
402
|
+
p_await = pr_sub.add_parser(
|
|
403
|
+
"await",
|
|
404
|
+
help="Wake-me-when-triage-able combo verb.",
|
|
405
|
+
description=(
|
|
406
|
+
"Polls readiness, runs CI + Sonar gate, renders briefing. Exits "
|
|
407
|
+
"non-zero on quality-gate ERROR or unresolved review threads."
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
p_await.add_argument("pr", type=int, nargs="?", default=None)
|
|
411
|
+
p_await.add_argument(
|
|
412
|
+
"--max-wait",
|
|
413
|
+
type=int,
|
|
414
|
+
default=1800,
|
|
415
|
+
help=(
|
|
416
|
+
"Upper bound in seconds to poll for required-reviewer readiness; "
|
|
417
|
+
"returns early (down to waited=0s) once satisfied (default 1800)."
|
|
418
|
+
),
|
|
419
|
+
)
|
|
420
|
+
_add_agent_option(p_await, required=False, help_text=_AGENT_HELP)
|
|
421
|
+
p_await.set_defaults(func=_cmd_pr_await)
|
|
422
|
+
|
|
423
|
+
p_review = pr_sub.add_parser(
|
|
424
|
+
"review",
|
|
425
|
+
help="Post the Qodo agentic-review trigger (/agentic_review) on a PR.",
|
|
426
|
+
)
|
|
427
|
+
p_review.add_argument("pr", type=int, nargs="?", default=None)
|
|
428
|
+
_add_agent_option(p_review, required=False, help_text=_AGENT_HELP)
|
|
429
|
+
p_review.set_defaults(func=_cmd_pr_review)
|
|
430
|
+
|
|
431
|
+
p_delta = pr_sub.add_parser("delta", help="Show the delta since the last PR read.")
|
|
432
|
+
_add_agent_option(p_delta, required=False, help_text=_AGENT_HELP)
|
|
433
|
+
p_delta.set_defaults(func=_cmd_pr_delta)
|
|
434
|
+
|
|
435
|
+
pr_p.set_defaults(func=_group_help(pr_p))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# Dispatch + entrypoint
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
444
|
+
rc = args.func(args)
|
|
445
|
+
return rc if rc is not None else 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
449
|
+
parser = _build_parser()
|
|
450
|
+
args = parser.parse_args(argv)
|
|
451
|
+
if getattr(args, "func", None) is None:
|
|
452
|
+
# No top-level command given — mirror Typer's no_args_is_help (exit 2).
|
|
453
|
+
parser.print_help(sys.stderr)
|
|
454
|
+
return 2
|
|
455
|
+
return _dispatch(args)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# Keep in sync with the sub.add_parser registrations above.
|
|
459
|
+
# If a new top-level command is added, extend this set so _main_entrypoint
|
|
460
|
+
# stops routing it to the unknown-command fallback page.
|
|
461
|
+
_KNOWN_COMMANDS = {"explain", "overview", "learn", "gamify", "hook", "doctor", "pr"}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _main_entrypoint() -> None:
|
|
465
|
+
"""CLI entry point that routes unknown subcommands to ``agex explain agex``.
|
|
466
|
+
|
|
467
|
+
When the first positional argument is not a known command (and is not a
|
|
468
|
+
flag), print the ``agex explain agex`` page to stdout and the canonical
|
|
469
|
+
error message to stderr, then exit with code 2. All other invocations —
|
|
470
|
+
known commands, ``--version``, ``--help``, zero-arg help — fall through to
|
|
471
|
+
the normal ``main()`` dispatch unchanged.
|
|
472
|
+
"""
|
|
473
|
+
argv = sys.argv[1:]
|
|
474
|
+
if argv and not argv[0].startswith("-") and argv[0] not in _KNOWN_COMMANDS:
|
|
475
|
+
print(f"{prog_name()}: error: unknown command '{argv[0]}'", file=sys.stderr)
|
|
476
|
+
# `agex` here is the explain-topic identifier (topics/agex.md), not the
|
|
477
|
+
# invoked command name — the canonical "what is this tool" page.
|
|
478
|
+
stdout, _, _ = explain_script.run("agex")
|
|
479
|
+
sys.stdout.write(stdout)
|
|
480
|
+
sys.exit(2)
|
|
481
|
+
sys.exit(main())
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if __name__ == "__main__":
|
|
485
|
+
_main_entrypoint()
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doctor
|
|
3
|
+
description: Diagnose the agex install and the current project's `.agex/` state with a deterministic markdown health report.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex doctor`
|
|
8
|
+
|
|
9
|
+
Run a zero-argument health check across:
|
|
10
|
+
|
|
11
|
+
1. **Install** — `agex` version, Python version, package resources are reachable.
|
|
12
|
+
2. **Project state** — whether `.agex/` exists in the current directory; if it does, that `config.toml` parses, `.gitignore` matches the managed content, and `data/` is writable.
|
|
13
|
+
3. **Internal consistency** — every shipped `commands/*/SKILL.md` parses with the required frontmatter, every per-backend capability YAML loads.
|
|
14
|
+
4. **Operator verification** — a short markdown checklist of things `doctor` cannot verify automatically (network reach, git-tracking of `.agex/config.toml`, agent shell-tool wiring).
|
|
15
|
+
|
|
16
|
+
`doctor` is strictly read-only. It will never create `.agex/` or write anywhere on disk — if the directory is missing, that is reported as info and the command keeps going.
|
|
17
|
+
|
|
18
|
+
## Exit codes
|
|
19
|
+
|
|
20
|
+
| Code | Meaning |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `0` | All checks `ok`, possibly with `warn` rows. |
|
|
23
|
+
| `1` | At least one `fail` row. Stderr carries a one-line summary. |
|
|
24
|
+
| `2` | CLI usage error (e.g., unknown role passed via `--role`). |
|
|
25
|
+
|
|
26
|
+
## From your shell tool
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
agex doctor # base health check
|
|
30
|
+
agex doctor --role pr-review # base + role-specific checks (when a role file ships)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Role-specific checks
|
|
34
|
+
|
|
35
|
+
`doctor` has an extension hook: a role file at `commands/doctor/assets/roles/<role>.md.j2` (slug-validated, `^[a-z][a-z0-9-]*$`) is rendered as an extra section when the user passes `--role <role>`. The current release ships zero role files — the contract exists so role-specific diagnostics can be added without touching `doctor` itself.
|
|
36
|
+
|
|
37
|
+
## What `doctor` does *not* do
|
|
38
|
+
|
|
39
|
+
- It does not run backend probes — that's `agex overview --agent X`.
|
|
40
|
+
- It does not perform any auto-fix. Recovery instructions are emitted as markdown for the operator (agent or human) to act on.
|
|
41
|
+
- It does not touch the network.
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% set icon = {"ok": "✓", "warn": "⚠️", "fail": "✗", "info": "·"} -%}
|
|
2
|
+
# {{ prog }} doctor
|
|
3
|
+
|
|
4
|
+
**{{ prog }}:** `{{ version }}`
|
|
5
|
+
**Project:** `{{ project_dir }}`
|
|
6
|
+
|
|
7
|
+
{% for cat in categories %}
|
|
8
|
+
## {{ cat.title }}
|
|
9
|
+
|
|
10
|
+
{% for r in cat.results -%}
|
|
11
|
+
- {{ icon[r.status] }} **{{ r.name }}** — {{ r.detail }}
|
|
12
|
+
{% endfor %}
|
|
13
|
+
{% endfor -%}
|
|
14
|
+
|
|
15
|
+
## Operator verification
|
|
16
|
+
|
|
17
|
+
These cannot be checked automatically — confirm by hand:
|
|
18
|
+
|
|
19
|
+
{% for item in operator_checklist -%}
|
|
20
|
+
- [ ] {{ item }}
|
|
21
|
+
{% endfor %}
|
|
22
|
+
{% if role_section %}
|
|
23
|
+
## Role: `{{ role }}`
|
|
24
|
+
|
|
25
|
+
{{ role_section }}
|
|
26
|
+
{% endif %}
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
| status | count |
|
|
30
|
+
|---|---|
|
|
31
|
+
| ✓ ok | {{ summary.ok }} |
|
|
32
|
+
| ⚠️ warn | {{ summary.warn }} |
|
|
33
|
+
| ✗ fail | {{ summary.fail }} |
|
|
34
|
+
| · info | {{ summary.info }} |
|
|
35
|
+
{% if summary.fail == 0 %}
|
|
36
|
+
No hard failures. Exit `0`.
|
|
37
|
+
{% else %}
|
|
38
|
+
{{ summary.fail }} hard failure(s) — exit `1`. Address `✗` rows above.
|
|
39
|
+
{% endif %}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `doctor` — design notes
|
|
2
|
+
|
|
3
|
+
Internal reference for maintainers. Not emitted at runtime.
|
|
4
|
+
|
|
5
|
+
## Categories
|
|
6
|
+
|
|
7
|
+
| Category | Checks |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Install | `agex` version resolves, Python ≥ 3.10, package resources reachable. |
|
|
10
|
+
| Project state | `.agex/` dir, `config.toml` parses, `.gitignore` matches `GITIGNORE_CONTENT`, `data/` writable. |
|
|
11
|
+
| Internal consistency | every shipped `SKILL.md` parses, every per-backend capability YAML loads. |
|
|
12
|
+
|
|
13
|
+
## Statuses
|
|
14
|
+
|
|
15
|
+
- `ok` — green; check passed.
|
|
16
|
+
- `warn` — non-fatal anomaly. Exit code stays `0`.
|
|
17
|
+
- `fail` — hard failure. Exit code `1`, plus a one-line stderr summary.
|
|
18
|
+
- `info` — neutral observation (e.g. `.agex/` not initialized — that's not a problem in a fresh project).
|
|
19
|
+
|
|
20
|
+
## Read-only contract
|
|
21
|
+
|
|
22
|
+
`doctor` must never write. It deliberately does **not** call `core.paths.ensure_init()`. New checks should use `os.access(..., os.W_OK)` or pure read paths — never probe writability with an actual write.
|
|
23
|
+
|
|
24
|
+
## Role-flag contract
|
|
25
|
+
|
|
26
|
+
`agex doctor --role <slug>` renders the contents of `assets/roles/<slug>.md.j2` (validated against `^[a-z][a-z0-9-]*$`) as an extra section after Operator verification.
|
|
27
|
+
|
|
28
|
+
The role asset is a Jinja template, but v0.1 passes no extra context — keep role files static markdown until a use case justifies the coupling. Unknown role → exit `2` with a stderr message.
|
|
29
|
+
|
|
30
|
+
## Adding a check
|
|
31
|
+
|
|
32
|
+
1. Add a `_check_<name>() -> CheckResult` function in `scripts/doctor.py`.
|
|
33
|
+
2. Append it to the appropriate `Category` in `_build_categories()`.
|
|
34
|
+
3. Cover it in `tests/commands/test_doctor.py`.
|
|
35
|
+
|
|
36
|
+
Keep each check small, side-effect-free, and tolerant of partial state — return `info` when the precondition (e.g. presence of `.agex/`) isn't met rather than `fail`.
|
|
File without changes
|