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.
Files changed (115) hide show
  1. agent_experience/__init__.py +24 -0
  2. agent_experience/__main__.py +4 -0
  3. agent_experience/backends/__init__.py +0 -0
  4. agent_experience/backends/acp/__init__.py +0 -0
  5. agent_experience/backends/acp/probe.py +9 -0
  6. agent_experience/backends/capabilities/acp.yaml +7 -0
  7. agent_experience/backends/capabilities/claude-code.yaml +4 -0
  8. agent_experience/backends/capabilities/codex.yaml +7 -0
  9. agent_experience/backends/capabilities/copilot.yaml +7 -0
  10. agent_experience/backends/claude_code/__init__.py +0 -0
  11. agent_experience/backends/claude_code/probe.py +97 -0
  12. agent_experience/backends/codex/__init__.py +0 -0
  13. agent_experience/backends/codex/probe.py +16 -0
  14. agent_experience/backends/copilot/__init__.py +0 -0
  15. agent_experience/backends/copilot/probe.py +9 -0
  16. agent_experience/cli.py +485 -0
  17. agent_experience/commands/__init__.py +0 -0
  18. agent_experience/commands/doctor/SKILL.md +41 -0
  19. agent_experience/commands/doctor/__init__.py +0 -0
  20. agent_experience/commands/doctor/assets/report.md.j2 +39 -0
  21. agent_experience/commands/doctor/references/design.md +36 -0
  22. agent_experience/commands/doctor/scripts/__init__.py +0 -0
  23. agent_experience/commands/doctor/scripts/doctor.py +394 -0
  24. agent_experience/commands/explain/SKILL.md +26 -0
  25. agent_experience/commands/explain/__init__.py +0 -0
  26. agent_experience/commands/explain/assets/topics/agex.md +37 -0
  27. agent_experience/commands/explain/references/.gitkeep +0 -0
  28. agent_experience/commands/explain/scripts/__init__.py +0 -0
  29. agent_experience/commands/explain/scripts/explain.py +64 -0
  30. agent_experience/commands/gamify/SKILL.md +31 -0
  31. agent_experience/commands/gamify/__init__.py +0 -0
  32. agent_experience/commands/gamify/assets/hooks/claude-code.json +28 -0
  33. agent_experience/commands/gamify/references/.gitkeep +0 -0
  34. agent_experience/commands/gamify/scripts/__init__.py +0 -0
  35. agent_experience/commands/gamify/scripts/install.py +203 -0
  36. agent_experience/commands/hook/SKILL.md +31 -0
  37. agent_experience/commands/hook/__init__.py +0 -0
  38. agent_experience/commands/hook/assets/table.md.j2 +17 -0
  39. agent_experience/commands/hook/references/.gitkeep +0 -0
  40. agent_experience/commands/hook/scripts/__init__.py +0 -0
  41. agent_experience/commands/hook/scripts/read.py +53 -0
  42. agent_experience/commands/hook/scripts/write.py +25 -0
  43. agent_experience/commands/learn/SKILL.md +21 -0
  44. agent_experience/commands/learn/__init__.py +0 -0
  45. agent_experience/commands/learn/assets/menu.md.j2 +7 -0
  46. agent_experience/commands/learn/assets/topics/cicd/SKILL.md +103 -0
  47. agent_experience/commands/learn/assets/topics/gamify/SKILL.md +35 -0
  48. agent_experience/commands/learn/assets/topics/gamify/assets/skill-template/claude-code/SKILL.md +22 -0
  49. agent_experience/commands/learn/assets/topics/introspect/SKILL.md +41 -0
  50. agent_experience/commands/learn/assets/topics/introspect/assets/skill-template/claude-code/SKILL.md +22 -0
  51. agent_experience/commands/learn/assets/topics/levelup/SKILL.md +31 -0
  52. agent_experience/commands/learn/assets/topics/levelup/assets/skill-template/claude-code/SKILL.md +22 -0
  53. agent_experience/commands/learn/assets/topics/visualize/SKILL.md +27 -0
  54. agent_experience/commands/learn/assets/topics/visualize/assets/skill-template/claude-code/SKILL.md +19 -0
  55. agent_experience/commands/learn/references/.gitkeep +0 -0
  56. agent_experience/commands/learn/scripts/__init__.py +0 -0
  57. agent_experience/commands/learn/scripts/learn.py +73 -0
  58. agent_experience/commands/overview/SKILL.md +31 -0
  59. agent_experience/commands/overview/__init__.py +0 -0
  60. agent_experience/commands/overview/assets/backends/acp.yaml +7 -0
  61. agent_experience/commands/overview/assets/backends/claude-code.yaml +7 -0
  62. agent_experience/commands/overview/assets/backends/codex.yaml +7 -0
  63. agent_experience/commands/overview/assets/backends/copilot.yaml +7 -0
  64. agent_experience/commands/overview/assets/sections.md.j2 +52 -0
  65. agent_experience/commands/overview/references/.gitkeep +0 -0
  66. agent_experience/commands/overview/scripts/__init__.py +0 -0
  67. agent_experience/commands/overview/scripts/overview.py +40 -0
  68. agent_experience/commands/pr/SKILL.md +90 -0
  69. agent_experience/commands/pr/__init__.py +0 -0
  70. agent_experience/commands/pr/assets/__init__.py +0 -0
  71. agent_experience/commands/pr/assets/backends/__init__.py +0 -0
  72. agent_experience/commands/pr/assets/backends/acp.yaml +21 -0
  73. agent_experience/commands/pr/assets/backends/claude-code.yaml +21 -0
  74. agent_experience/commands/pr/assets/backends/codex.yaml +21 -0
  75. agent_experience/commands/pr/assets/backends/copilot.yaml +21 -0
  76. agent_experience/commands/pr/assets/rules/__init__.py +0 -0
  77. agent_experience/commands/pr/assets/rules/lint_rules.py +79 -0
  78. agent_experience/commands/pr/assets/rules/next_step_rules.py +78 -0
  79. agent_experience/commands/pr/assets/templates/__init__.py +0 -0
  80. agent_experience/commands/pr/assets/templates/delta.md.j2 +32 -0
  81. agent_experience/commands/pr/assets/templates/footer.md.j2 +2 -0
  82. agent_experience/commands/pr/assets/templates/lint_result.md.j2 +19 -0
  83. agent_experience/commands/pr/assets/templates/pr_briefing.md.j2 +69 -0
  84. agent_experience/commands/pr/assets/templates/pr_open_result.md.j2 +17 -0
  85. agent_experience/commands/pr/assets/templates/pr_reply_result.md.j2 +15 -0
  86. agent_experience/commands/pr/assets/templates/pr_review_result.md.j2 +5 -0
  87. agent_experience/commands/pr/scripts/__init__.py +0 -0
  88. agent_experience/commands/pr/scripts/_footer.py +32 -0
  89. agent_experience/commands/pr/scripts/_journal.py +21 -0
  90. agent_experience/commands/pr/scripts/_qodo.py +147 -0
  91. agent_experience/commands/pr/scripts/_readiness.py +76 -0
  92. agent_experience/commands/pr/scripts/_sonar.py +29 -0
  93. agent_experience/commands/pr/scripts/await_.py +156 -0
  94. agent_experience/commands/pr/scripts/delta.py +84 -0
  95. agent_experience/commands/pr/scripts/lint.py +72 -0
  96. agent_experience/commands/pr/scripts/open_.py +104 -0
  97. agent_experience/commands/pr/scripts/read.py +151 -0
  98. agent_experience/commands/pr/scripts/reply.py +160 -0
  99. agent_experience/commands/pr/scripts/review.py +59 -0
  100. agent_experience/core/__init__.py +0 -0
  101. agent_experience/core/backend.py +80 -0
  102. agent_experience/core/capabilities.py +44 -0
  103. agent_experience/core/config.py +46 -0
  104. agent_experience/core/github.py +355 -0
  105. agent_experience/core/hook_io.py +95 -0
  106. agent_experience/core/journal.py +90 -0
  107. agent_experience/core/paths.py +26 -0
  108. agent_experience/core/prog.py +44 -0
  109. agent_experience/core/render.py +42 -0
  110. agent_experience/core/skill_loader.py +36 -0
  111. devex_cli-0.24.0.dist-info/METADATA +55 -0
  112. devex_cli-0.24.0.dist-info/RECORD +115 -0
  113. devex_cli-0.24.0.dist-info/WHEEL +4 -0
  114. devex_cli-0.24.0.dist-info/entry_points.txt +3 -0
  115. 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.
@@ -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,7 @@
1
+ sections:
2
+ - project_memory
3
+ - skills
4
+ - hooks
5
+ - mcp
6
+ - settings
7
+ probe: acp
@@ -0,0 +1,7 @@
1
+ sections:
2
+ - project_memory
3
+ - skills
4
+ - hooks
5
+ - mcp
6
+ - settings
7
+ probe: claude_code
@@ -0,0 +1,7 @@
1
+ sections:
2
+ - project_memory
3
+ - skills
4
+ - hooks
5
+ - mcp
6
+ - settings
7
+ probe: codex
@@ -0,0 +1,7 @@
1
+ sections:
2
+ - project_memory
3
+ - skills
4
+ - hooks
5
+ - mcp
6
+ - settings
7
+ probe: copilot
@@ -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
@@ -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", {}