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,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,73 @@
|
|
|
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.prog import error_prefix
|
|
7
|
+
from agent_experience.core.render import render_string
|
|
8
|
+
from agent_experience.core.skill_loader import Skill, load_skill
|
|
9
|
+
|
|
10
|
+
_TOPIC_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
11
|
+
_SKILL_FILENAME = "SKILL.md"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _learn_assets() -> Traversable:
|
|
15
|
+
# Anchor on the `commands` package (which has __init__.py) and navigate in.
|
|
16
|
+
# Avoids relying on namespace-package semantics for `assets/`, which is a
|
|
17
|
+
# data directory, not a package. Matches explain.py / overview.py pattern.
|
|
18
|
+
return files("agent_experience.commands").joinpath("learn", "assets")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_skill_from_traversable(trav: Traversable) -> Skill:
|
|
22
|
+
# load_skill expects a pathlib.Path; resolve via as_file when needed.
|
|
23
|
+
with as_file(trav) as path:
|
|
24
|
+
return load_skill(path)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _list_topics() -> list[dict]:
|
|
28
|
+
topics: list[dict] = []
|
|
29
|
+
topics_root = _learn_assets().joinpath("topics")
|
|
30
|
+
for topic_dir in sorted(topics_root.iterdir(), key=lambda p: p.name):
|
|
31
|
+
if topic_dir.is_file():
|
|
32
|
+
continue
|
|
33
|
+
skill_md = topic_dir.joinpath(_SKILL_FILENAME)
|
|
34
|
+
if not skill_md.is_file():
|
|
35
|
+
continue
|
|
36
|
+
skill = _load_skill_from_traversable(skill_md)
|
|
37
|
+
# Use the directory name as the canonical slug — `run_topic` looks up
|
|
38
|
+
# by directory, so the menu's `agex learn <name>` invocation must
|
|
39
|
+
# match. Frontmatter `skill.name` is still available for drift checks.
|
|
40
|
+
topics.append(
|
|
41
|
+
{"name": topic_dir.name, "description": skill.description, "unsupported": None}
|
|
42
|
+
)
|
|
43
|
+
return topics
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_menu(backend: Backend) -> tuple[str, int, str]:
|
|
47
|
+
"""Return (stdout, exit_code, stderr) for the topic menu."""
|
|
48
|
+
topics = _list_topics()
|
|
49
|
+
template_text = _learn_assets().joinpath("menu.md.j2").read_text(encoding="utf-8")
|
|
50
|
+
out = render_string(template_text, {"backend": backend.value, "topics": topics})
|
|
51
|
+
return (out, 0, "")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_topic(topic: str, backend: Backend) -> tuple[str, int, str]:
|
|
55
|
+
"""Return (stdout, exit_code, stderr) for a specific lesson topic."""
|
|
56
|
+
if not _TOPIC_RE.match(topic):
|
|
57
|
+
menu_out, _, _ = run_menu(backend)
|
|
58
|
+
return (menu_out, 2, error_prefix(f"unknown topic '{topic}'"))
|
|
59
|
+
|
|
60
|
+
topic_dir = _learn_assets().joinpath("topics", topic)
|
|
61
|
+
skill_md = topic_dir.joinpath(_SKILL_FILENAME)
|
|
62
|
+
if not skill_md.is_file():
|
|
63
|
+
menu_out, _, _ = run_menu(backend)
|
|
64
|
+
return (menu_out, 2, error_prefix(f"unknown topic '{topic}'"))
|
|
65
|
+
|
|
66
|
+
skill = _load_skill_from_traversable(skill_md)
|
|
67
|
+
template_path = topic_dir.joinpath("assets", "skill-template", backend.value, _SKILL_FILENAME)
|
|
68
|
+
template_body = template_path.read_text(encoding="utf-8") if template_path.is_file() else ""
|
|
69
|
+
rendered = render_string(
|
|
70
|
+
skill.body,
|
|
71
|
+
{"backend": backend.value, "skill_template_body": template_body},
|
|
72
|
+
)
|
|
73
|
+
return (rendered, 0, "")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overview
|
|
3
|
+
description: Render a descriptive markdown snapshot of the project's current agent setup.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex overview --agent <backend>`
|
|
8
|
+
|
|
9
|
+
Call this to get a read-only markdown snapshot of what's configured in the
|
|
10
|
+
current project for a given backend — skills, hooks, agents, MCP servers,
|
|
11
|
+
and relevant config files. Descriptive, not diagnostic. Read it before you
|
|
12
|
+
act.
|
|
13
|
+
|
|
14
|
+
## From your shell tool
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
agex overview --agent claude-code
|
|
18
|
+
agex overview --agent codex
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What you get
|
|
22
|
+
|
|
23
|
+
Markdown sections: Project root, `CLAUDE.md`/`AGENTS.md` presence, Skills,
|
|
24
|
+
Hooks, MCP servers, Settings.
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- Malformed files are skipped with a `> ⚠️` inline warning.
|
|
29
|
+
- Read-only except first-run `.agex/` init.
|
|
30
|
+
- Build diagnostic logic (gaps, recommendations) into an agent-authored skill:
|
|
31
|
+
`agex learn introspect --agent <backend>`.
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Overview — {{ backend }}
|
|
2
|
+
|
|
3
|
+
**Project:** `{{ project_dir }}`
|
|
4
|
+
{% if probe.warnings %}
|
|
5
|
+
{% for w in probe.warnings -%}
|
|
6
|
+
> ⚠️ {{ w }}
|
|
7
|
+
{% endfor %}
|
|
8
|
+
{%- endif %}
|
|
9
|
+
## Project memory
|
|
10
|
+
{% if probe.claude_md -%}
|
|
11
|
+
- `{{ probe.claude_md.name }}` present at `{{ probe.claude_md }}`
|
|
12
|
+
{%- else -%}
|
|
13
|
+
- _no CLAUDE.md / AGENTS.md equivalent found_
|
|
14
|
+
{%- endif %}
|
|
15
|
+
|
|
16
|
+
## Skills ({{ probe.skills|length }})
|
|
17
|
+
{% if probe.skills -%}
|
|
18
|
+
| name | description |
|
|
19
|
+
|---|---|
|
|
20
|
+
{% for s in probe.skills -%}
|
|
21
|
+
| `{{ s.name }}` | {{ s.description }} |
|
|
22
|
+
{% endfor %}
|
|
23
|
+
{%- else -%}
|
|
24
|
+
_none_
|
|
25
|
+
{%- endif %}
|
|
26
|
+
|
|
27
|
+
## Hooks ({{ probe.hooks|length }})
|
|
28
|
+
{% if probe.hooks -%}
|
|
29
|
+
{% for h in probe.hooks -%}
|
|
30
|
+
- `{{ h.event }}` — {{ h.entries|length }} entry(s)
|
|
31
|
+
{% endfor %}
|
|
32
|
+
{%- else -%}
|
|
33
|
+
_none_
|
|
34
|
+
{%- endif %}
|
|
35
|
+
|
|
36
|
+
## MCP servers ({{ probe.mcp_servers|length }})
|
|
37
|
+
{% if probe.mcp_servers -%}
|
|
38
|
+
{% for m in probe.mcp_servers -%}
|
|
39
|
+
- `{{ m.name }}`
|
|
40
|
+
{% endfor %}
|
|
41
|
+
{%- else -%}
|
|
42
|
+
_none_
|
|
43
|
+
{%- endif %}
|
|
44
|
+
|
|
45
|
+
## Settings
|
|
46
|
+
{% if probe.settings -%}
|
|
47
|
+
```json
|
|
48
|
+
{{ probe.settings | tojson(indent=2) }}
|
|
49
|
+
```
|
|
50
|
+
{%- else -%}
|
|
51
|
+
_no settings file found_
|
|
52
|
+
{%- endif %}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
from importlib.resources.abc import Traversable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from agent_experience.backends.acp.probe import probe as acp_probe
|
|
6
|
+
from agent_experience.backends.claude_code.probe import probe as claude_code_probe
|
|
7
|
+
from agent_experience.backends.codex.probe import probe as codex_probe
|
|
8
|
+
from agent_experience.backends.copilot.probe import probe as copilot_probe
|
|
9
|
+
from agent_experience.core.backend import Backend
|
|
10
|
+
from agent_experience.core.paths import ensure_init
|
|
11
|
+
from agent_experience.core.render import render_string
|
|
12
|
+
|
|
13
|
+
_PROBES = {
|
|
14
|
+
Backend.CLAUDE_CODE: claude_code_probe,
|
|
15
|
+
Backend.CODEX: codex_probe,
|
|
16
|
+
Backend.COPILOT: copilot_probe,
|
|
17
|
+
Backend.ACP: acp_probe,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _assets_root() -> Traversable:
|
|
22
|
+
# Anchor on the `commands` package (which has __init__.py) and navigate in,
|
|
23
|
+
# matching explain.py's pattern. Avoids relying on namespace-package
|
|
24
|
+
# semantics for `assets/`, which is a data directory, not a package.
|
|
25
|
+
return files("agent_experience.commands").joinpath("overview", "assets")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(backend: Backend) -> tuple[str, int, str]:
|
|
29
|
+
"""Return (stdout, exit_code, stderr)."""
|
|
30
|
+
ensure_init()
|
|
31
|
+
project_dir = Path.cwd()
|
|
32
|
+
|
|
33
|
+
probe_result = _PROBES[backend](project_dir)
|
|
34
|
+
|
|
35
|
+
template_text = _assets_root().joinpath("sections.md.j2").read_text(encoding="utf-8")
|
|
36
|
+
out = render_string(
|
|
37
|
+
template_text,
|
|
38
|
+
{"backend": backend.value, "project_dir": project_dir, "probe": probe_result},
|
|
39
|
+
)
|
|
40
|
+
return (out, 0, "")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr
|
|
3
|
+
description: GitHub PR lifecycle commands for agents — lint, open, read, reply, review, await, delta. Each command ends with a deterministic "Next step:" footer so the agent never has to guess what to chain.
|
|
4
|
+
type: command
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# `agex pr` — PR lifecycle for agents
|
|
8
|
+
|
|
9
|
+
Seven verbs, in roughly the order an agent uses them on a PR:
|
|
10
|
+
|
|
11
|
+
| Verb | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `agex pr lint` | Portability + alignment-trigger lint on the working diff. |
|
|
14
|
+
| `agex pr open --title T [--body-file F] [--draft] [--delayed-read]` | `gh pr create` with auto-signed body; with `--delayed-read` chains to `read --wait 180`. |
|
|
15
|
+
| `agex pr read [<PR>] [--wait SECS]` | Unified briefing: CI checks, SonarCloud quality gate, all comments, reviewer readiness. With `--wait`, polls until required reviewers are ready or timeout. Always exits 0. |
|
|
16
|
+
| `agex pr reply <PR>` | Read JSONL replies on stdin, post each, resolve threads. |
|
|
17
|
+
| `agex pr review [<PR>]` | Post the Qodo agentic-review trigger (`/agentic_review`) on a PR. Re-triggers on an already-open PR; `pr open` posts it automatically for a new non-draft PR. |
|
|
18
|
+
| `agex pr await [<PR>] [--max-wait SECS]` | "Wake me when this is triage-able" — polls readiness, runs CI + Sonar gate, dumps briefing. **Exits 1 on quality-gate `ERROR`, unresolved threads, or failing CI checks**, 0 on clean state or timeout. Default `--max-wait 1800` (30 min). |
|
|
19
|
+
| `agex pr delta` | Dump sibling-project `CLAUDE.md` heads + `culture.yaml` for alignment review. |
|
|
20
|
+
|
|
21
|
+
`pr read --wait` and `pr await` share the same readiness-polling loop; the
|
|
22
|
+
difference is the exit code. Use `read` when you want the briefing
|
|
23
|
+
unconditionally; use `await` when you want to **gate** the next step on
|
|
24
|
+
PR health (e.g., in scripts that should fail if Sonar errors).
|
|
25
|
+
|
|
26
|
+
## Readiness and `--wait` semantics
|
|
27
|
+
|
|
28
|
+
- **`--wait` / `--max-wait` is an upper bound, not a minimum sleep.** It's the
|
|
29
|
+
*maximum* time to poll (60s interval). The loop returns as soon as the
|
|
30
|
+
readiness predicate holds — including **immediately** (`waited=0s`) if it
|
|
31
|
+
already held on entry. When that happens the stderr heartbeat appends
|
|
32
|
+
`(readiness already satisfied on entry; not polling)`, so a fast return on a
|
|
33
|
+
large `--wait` is expected, not a bug.
|
|
34
|
+
- **What "ready" means.** `ready=True` when every reviewer in
|
|
35
|
+
`[pr].required_reviewers` (default `["qodo"]`) has posted a non-empty comment.
|
|
36
|
+
This is **review-feedback-present**, *not* merge-ready: it says nothing about
|
|
37
|
+
unresolved review threads, CI checks, or the SonarCloud quality gate. Widen
|
|
38
|
+
the predicate by adding reviewers to `[pr].required_reviewers` in
|
|
39
|
+
`.agex/config.toml`.
|
|
40
|
+
- **Need merge-gating?** Use `agex pr await`, which adds the CI + Sonar + thread
|
|
41
|
+
gates on top of the same readiness loop and **exits non-zero** when the PR
|
|
42
|
+
isn't clean.
|
|
43
|
+
|
|
44
|
+
## Qodo agentic review
|
|
45
|
+
|
|
46
|
+
When a PR is opened, Qodo runs an agentic code review on demand when it sees a
|
|
47
|
+
trigger comment. The current command is **`/agentic_review`**. The legacy
|
|
48
|
+
`/improve` command is **deprecated** (Qodo replies with a deprecation banner and
|
|
49
|
+
no committed removal date), so agex never posts `/improve`.
|
|
50
|
+
|
|
51
|
+
- `agex pr open` posts `/agentic_review` **automatically** for a freshly created,
|
|
52
|
+
non-draft PR — out of the box, no flag. Drafts are skipped (not review-ready)
|
|
53
|
+
and already-open PRs are skipped (idempotent re-opens shouldn't spam the
|
|
54
|
+
thread).
|
|
55
|
+
- `agex pr review [<PR>]` posts the same trigger on demand — use it to re-trigger
|
|
56
|
+
a review on an existing PR (e.g. after pushing fixes) or to trigger a draft
|
|
57
|
+
once it's ready.
|
|
58
|
+
|
|
59
|
+
The command string lives in one place (`scripts/review.QODO_REVIEW_TRIGGER`), so
|
|
60
|
+
a future Qodo rename is a one-line change for every consumer of `agex pr`.
|
|
61
|
+
|
|
62
|
+
## SonarCloud project key
|
|
63
|
+
|
|
64
|
+
`pr read` and `pr await` query the SonarCloud quality gate for the current
|
|
65
|
+
PR. The project key is resolved in order:
|
|
66
|
+
|
|
67
|
+
1. `SONAR_PROJECT_KEY` env var (override for non-standard naming).
|
|
68
|
+
2. `[pr] sonar_project_key` in `.agex/config.toml`.
|
|
69
|
+
3. `<owner>_<repo>` (SonarCloud GitHub-import default).
|
|
70
|
+
|
|
71
|
+
When the project isn't on SonarCloud the API 404s and agex silently
|
|
72
|
+
skips the section, so the verbs stay safe for non-Sonar repos.
|
|
73
|
+
|
|
74
|
+
Every command ends with a `**Next step:**` footer — chase the chain without guessing.
|
|
75
|
+
|
|
76
|
+
## Side effects
|
|
77
|
+
|
|
78
|
+
Network: every command except `lint` and `delta` talks to GitHub via `gh`.
|
|
79
|
+
`pr open` (non-draft, new PR) and `pr review` post the `/agentic_review` trigger
|
|
80
|
+
comment.
|
|
81
|
+
Disk: `pr open`, `pr read`, `pr reply`, and `pr review` append events to
|
|
82
|
+
`.agex/data/pr/events.jsonl`.
|
|
83
|
+
|
|
84
|
+
## Prerequisites
|
|
85
|
+
|
|
86
|
+
- `gh` (GitHub CLI) on PATH and authenticated (`gh auth login`).
|
|
87
|
+
- `--agent` flag, or first agent's `backend:` set in `culture.yaml`.
|
|
88
|
+
|
|
89
|
+
For each verb's full behavior, error modes, and exit codes, see
|
|
90
|
+
`docs/superpowers/specs/2026-05-10-agex-pr-design.md`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
hints:
|
|
2
|
+
lint_clean: "Commit, push, then `{{ prog }} pr open --title \"...\"`."
|
|
3
|
+
lint_clean_with_alignment: "Run `{{ prog }} pr delta` (this PR touches alignment files), then commit, push, and `{{ prog }} pr open --title \"...\"`."
|
|
4
|
+
lint_violations: "Fix the {{ violation_count }} violation(s) above and rerun `{{ prog }} pr lint`."
|
|
5
|
+
open_recommend_read: "`{{ prog }} pr read {{ pr }} --wait 180` (recommended) or `{{ prog }} pr read {{ pr }}` in ~3 min."
|
|
6
|
+
open_already_exists: "PR already open as #{{ pr }} — `{{ prog }} pr read {{ pr }} --wait 180` to fetch the briefing."
|
|
7
|
+
read_unresolved_after_commits: "Push fixes, then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
8
|
+
read_unresolved_no_commits: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
9
|
+
read_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
10
|
+
read_clean: "Wait for human merge."
|
|
11
|
+
read_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr read {{ pr }} --wait 180` to continue."
|
|
12
|
+
reply_with_failures: "Resubmit the failed lines (see table above) to `{{ prog }} pr reply {{ pr }}`."
|
|
13
|
+
reply_clean: "Push fixes (if any), then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
14
|
+
delta_done: "Triage each sibling — open follow-up PRs where alignment drifted; mention them in your reply."
|
|
15
|
+
review_posted: "Posted `/agentic_review` — `{{ prog }} pr read {{ pr }} --wait 180` once Qodo responds."
|
|
16
|
+
await_clean: "PR #{{ pr }} is ready — wait for human merge."
|
|
17
|
+
await_gate_error: "SonarCloud quality gate is ERROR — fix the new issues above, push, then `{{ prog }} pr await {{ pr }}` again."
|
|
18
|
+
await_gate_unknown: "SonarCloud quality gate is UNKNOWN — analysis is pending or the project isn't analyzed yet; wait and re-run `{{ prog }} pr await {{ pr }}`."
|
|
19
|
+
await_unresolved_threads: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
20
|
+
await_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
21
|
+
await_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr await {{ pr }}` to keep waiting."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
hints:
|
|
2
|
+
lint_clean: "Commit, push, then `{{ prog }} pr open --title \"...\"`."
|
|
3
|
+
lint_clean_with_alignment: "Run `{{ prog }} pr delta` (this PR touches alignment files), then commit, push, and `{{ prog }} pr open --title \"...\"`."
|
|
4
|
+
lint_violations: "Fix the {{ violation_count }} violation(s) above and rerun `{{ prog }} pr lint`."
|
|
5
|
+
open_recommend_read: "`{{ prog }} pr read {{ pr }} --wait 180` (recommended) or `{{ prog }} pr read {{ pr }}` in ~3 min."
|
|
6
|
+
open_already_exists: "PR already open as #{{ pr }} — `{{ prog }} pr read {{ pr }} --wait 180` to fetch the briefing."
|
|
7
|
+
read_unresolved_after_commits: "Push fixes, then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
8
|
+
read_unresolved_no_commits: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
9
|
+
read_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
10
|
+
read_clean: "Wait for human merge."
|
|
11
|
+
read_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr read {{ pr }} --wait 180` to continue."
|
|
12
|
+
reply_with_failures: "Resubmit the failed lines (see table above) to `{{ prog }} pr reply {{ pr }}`."
|
|
13
|
+
reply_clean: "Push fixes (if any), then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
14
|
+
delta_done: "Triage each sibling — open follow-up PRs where alignment drifted; mention them in your reply."
|
|
15
|
+
review_posted: "Posted `/agentic_review` — `{{ prog }} pr read {{ pr }} --wait 180` once Qodo responds."
|
|
16
|
+
await_clean: "PR #{{ pr }} is ready — wait for human merge."
|
|
17
|
+
await_gate_error: "SonarCloud quality gate is ERROR — fix the new issues above, push, then `{{ prog }} pr await {{ pr }}` again."
|
|
18
|
+
await_gate_unknown: "SonarCloud quality gate is UNKNOWN — analysis is pending or the project isn't analyzed yet; wait and re-run `{{ prog }} pr await {{ pr }}`."
|
|
19
|
+
await_unresolved_threads: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
20
|
+
await_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
21
|
+
await_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr await {{ pr }}` to keep waiting."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
hints:
|
|
2
|
+
lint_clean: "Commit, push, then `{{ prog }} pr open --title \"...\"`."
|
|
3
|
+
lint_clean_with_alignment: "Run `{{ prog }} pr delta` (this PR touches alignment files), then commit, push, and `{{ prog }} pr open --title \"...\"`."
|
|
4
|
+
lint_violations: "Fix the {{ violation_count }} violation(s) above and rerun `{{ prog }} pr lint`."
|
|
5
|
+
open_recommend_read: "`{{ prog }} pr read {{ pr }} --wait 180` (recommended) or `{{ prog }} pr read {{ pr }}` in ~3 min."
|
|
6
|
+
open_already_exists: "PR already open as #{{ pr }} — `{{ prog }} pr read {{ pr }} --wait 180` to fetch the briefing."
|
|
7
|
+
read_unresolved_after_commits: "Push fixes, then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
8
|
+
read_unresolved_no_commits: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
9
|
+
read_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
10
|
+
read_clean: "Wait for human merge."
|
|
11
|
+
read_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr read {{ pr }} --wait 180` to continue."
|
|
12
|
+
reply_with_failures: "Resubmit the failed lines (see table above) to `{{ prog }} pr reply {{ pr }}`."
|
|
13
|
+
reply_clean: "Push fixes (if any), then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
14
|
+
delta_done: "Triage each sibling — open follow-up PRs where alignment drifted; mention them in your reply."
|
|
15
|
+
review_posted: "Posted `/agentic_review` — `{{ prog }} pr read {{ pr }} --wait 180` once Qodo responds."
|
|
16
|
+
await_clean: "PR #{{ pr }} is ready — wait for human merge."
|
|
17
|
+
await_gate_error: "SonarCloud quality gate is ERROR — fix the new issues above, push, then `{{ prog }} pr await {{ pr }}` again."
|
|
18
|
+
await_gate_unknown: "SonarCloud quality gate is UNKNOWN — analysis is pending or the project isn't analyzed yet; wait and re-run `{{ prog }} pr await {{ pr }}`."
|
|
19
|
+
await_unresolved_threads: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
20
|
+
await_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
21
|
+
await_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr await {{ pr }}` to keep waiting."
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
hints:
|
|
2
|
+
lint_clean: "Commit, push, then `{{ prog }} pr open --title \"...\"`."
|
|
3
|
+
lint_clean_with_alignment: "Run `{{ prog }} pr delta` (this PR touches alignment files), then commit, push, and `{{ prog }} pr open --title \"...\"`."
|
|
4
|
+
lint_violations: "Fix the {{ violation_count }} violation(s) above and rerun `{{ prog }} pr lint`."
|
|
5
|
+
open_recommend_read: "`{{ prog }} pr read {{ pr }} --wait 180` (recommended) or `{{ prog }} pr read {{ pr }}` in ~3 min."
|
|
6
|
+
open_already_exists: "PR already open as #{{ pr }} — `{{ prog }} pr read {{ pr }} --wait 180` to fetch the briefing."
|
|
7
|
+
read_unresolved_after_commits: "Push fixes, then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
8
|
+
read_unresolved_no_commits: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
9
|
+
read_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
10
|
+
read_clean: "Wait for human merge."
|
|
11
|
+
read_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr read {{ pr }} --wait 180` to continue."
|
|
12
|
+
reply_with_failures: "Resubmit the failed lines (see table above) to `{{ prog }} pr reply {{ pr }}`."
|
|
13
|
+
reply_clean: "Push fixes (if any), then `{{ prog }} pr read {{ pr }} --wait 180`."
|
|
14
|
+
delta_done: "Triage each sibling — open follow-up PRs where alignment drifted; mention them in your reply."
|
|
15
|
+
review_posted: "Posted `/agentic_review` — `{{ prog }} pr read {{ pr }} --wait 180` once Qodo responds."
|
|
16
|
+
await_clean: "PR #{{ pr }} is ready — wait for human merge."
|
|
17
|
+
await_gate_error: "SonarCloud quality gate is ERROR — fix the new issues above, push, then `{{ prog }} pr await {{ pr }}` again."
|
|
18
|
+
await_gate_unknown: "SonarCloud quality gate is UNKNOWN — analysis is pending or the project isn't analyzed yet; wait and re-run `{{ prog }} pr await {{ pr }}`."
|
|
19
|
+
await_unresolved_threads: "Triage and prepare `replies.jsonl`, then `{{ prog }} pr reply {{ pr }} < replies.jsonl`."
|
|
20
|
+
await_ci_red: "Fix CI before requesting re-review (see failing checks above)."
|
|
21
|
+
await_wait_timeout: "Still waiting on: {{ reviewers }}. Rerun `{{ prog }} pr await {{ pr }}` to keep waiting."
|
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Portability and alignment rules for `agex pr lint`.
|
|
2
|
+
|
|
3
|
+
Pure functions over (file_path, file_content) tuples so they can be
|
|
4
|
+
unit-tested without git. The CLI driver in `commands/pr/scripts/lint.py`
|
|
5
|
+
collects the diff and feeds it in.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
_HOME_PATH_RE = re.compile(r"/home/[a-zA-Z0-9_-]+/")
|
|
14
|
+
_DOTFILE_RE = re.compile(r"~/\.[a-zA-Z][a-zA-Z0-9_-]*/")
|
|
15
|
+
_DOC_EXTS = (".md", ".rst", ".txt")
|
|
16
|
+
_ALIGNMENT_PATHS = ("CLAUDE.md", "culture.yaml")
|
|
17
|
+
_ALIGNMENT_PREFIXES = (".claude/skills/",)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Violation:
|
|
22
|
+
rule: str
|
|
23
|
+
file: str
|
|
24
|
+
line: int
|
|
25
|
+
evidence: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_absolute_home(path: str, content: str) -> list[Violation]:
|
|
29
|
+
out: list[Violation] = []
|
|
30
|
+
for lineno, line in enumerate(content.splitlines(), start=1):
|
|
31
|
+
m = _HOME_PATH_RE.search(line)
|
|
32
|
+
if m:
|
|
33
|
+
out.append(
|
|
34
|
+
Violation(
|
|
35
|
+
rule="absolute-home-path",
|
|
36
|
+
file=path,
|
|
37
|
+
line=lineno,
|
|
38
|
+
evidence=line.strip(),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _check_dotfile_in_doc(path: str, content: str) -> list[Violation]:
|
|
45
|
+
if not path.lower().endswith(_DOC_EXTS):
|
|
46
|
+
return []
|
|
47
|
+
out: list[Violation] = []
|
|
48
|
+
for lineno, line in enumerate(content.splitlines(), start=1):
|
|
49
|
+
if _DOTFILE_RE.search(line):
|
|
50
|
+
out.append(
|
|
51
|
+
Violation(
|
|
52
|
+
rule="user-dotfile-reference",
|
|
53
|
+
file=path,
|
|
54
|
+
line=lineno,
|
|
55
|
+
evidence=line.strip(),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_files(files: list[tuple[str, str]]) -> list[Violation]:
|
|
62
|
+
"""Run all portability rules over (path, content) tuples."""
|
|
63
|
+
out: list[Violation] = []
|
|
64
|
+
for path, content in files:
|
|
65
|
+
out.extend(_check_absolute_home(path, content))
|
|
66
|
+
out.extend(_check_dotfile_in_doc(path, content))
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_alignment_trigger(file_paths: list[str]) -> bool:
|
|
71
|
+
"""Return True if the changed file list touches any alignment-relevant
|
|
72
|
+
file (CLAUDE.md, culture.yaml, .claude/skills/**)."""
|
|
73
|
+
for p in file_paths:
|
|
74
|
+
if p in _ALIGNMENT_PATHS:
|
|
75
|
+
return True
|
|
76
|
+
for prefix in _ALIGNMENT_PREFIXES:
|
|
77
|
+
if p.startswith(prefix):
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Prioritized 'Next step:' rule keys for `agex pr` commands.
|
|
2
|
+
|
|
3
|
+
Each function takes the data the command already gathered and returns the
|
|
4
|
+
footer rule key + a context dict for variable substitution. First match
|
|
5
|
+
wins. Per-backend phrasing lives in `assets/backends/*.yaml`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def lint_next_step(violations: list[Any], alignment_triggered: bool) -> tuple[str, dict[str, Any]]:
|
|
14
|
+
if violations:
|
|
15
|
+
return "lint_violations", {"violation_count": len(violations)}
|
|
16
|
+
if alignment_triggered:
|
|
17
|
+
return "lint_clean_with_alignment", {}
|
|
18
|
+
return "lint_clean", {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def open_next_step(pr: int, was_already_open: bool) -> tuple[str, dict[str, Any]]:
|
|
22
|
+
key = "open_already_exists" if was_already_open else "open_recommend_read"
|
|
23
|
+
return key, {"pr": pr}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_next_step(
|
|
27
|
+
pr: int,
|
|
28
|
+
threads_unresolved: int,
|
|
29
|
+
has_recent_local_commits: bool,
|
|
30
|
+
ci_red: bool,
|
|
31
|
+
) -> tuple[str, dict[str, Any]]:
|
|
32
|
+
if ci_red:
|
|
33
|
+
return "read_ci_red", {"pr": pr}
|
|
34
|
+
if threads_unresolved > 0:
|
|
35
|
+
if has_recent_local_commits:
|
|
36
|
+
return "read_unresolved_after_commits", {"pr": pr}
|
|
37
|
+
return "read_unresolved_no_commits", {"pr": pr}
|
|
38
|
+
return "read_clean", {"pr": pr}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_wait_timeout_step(pr: int, reviewers: list[str]) -> tuple[str, dict[str, Any]]:
|
|
42
|
+
return "read_wait_timeout", {"pr": pr, "reviewers": ", ".join(reviewers)}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def await_next_step(
|
|
46
|
+
pr: int,
|
|
47
|
+
gate_error: bool,
|
|
48
|
+
threads_unresolved: int,
|
|
49
|
+
ci_red: bool,
|
|
50
|
+
gate_unknown: bool = False,
|
|
51
|
+
) -> tuple[str, dict[str, Any]]:
|
|
52
|
+
if gate_error:
|
|
53
|
+
return "await_gate_error", {"pr": pr}
|
|
54
|
+
if gate_unknown:
|
|
55
|
+
return "await_gate_unknown", {"pr": pr}
|
|
56
|
+
if threads_unresolved > 0:
|
|
57
|
+
return "await_unresolved_threads", {"pr": pr}
|
|
58
|
+
if ci_red:
|
|
59
|
+
return "await_ci_red", {"pr": pr}
|
|
60
|
+
return "await_clean", {"pr": pr}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def await_wait_timeout_step(pr: int, reviewers: list[str]) -> tuple[str, dict[str, Any]]:
|
|
64
|
+
return "await_wait_timeout", {"pr": pr, "reviewers": ", ".join(reviewers)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def reply_next_step(pr: int, failure_count: int) -> tuple[str, dict[str, Any]]:
|
|
68
|
+
if failure_count > 0:
|
|
69
|
+
return "reply_with_failures", {"pr": pr}
|
|
70
|
+
return "reply_clean", {"pr": pr}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def review_next_step(pr: int) -> tuple[str, dict[str, Any]]:
|
|
74
|
+
return "review_posted", {"pr": pr}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def delta_next_step() -> tuple[str, dict[str, Any]]:
|
|
78
|
+
return "delta_done", {}
|
|
File without changes
|