# Agex Integration Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add `agex-cli` as a PyPI library dependency, expose a `culture agex` passthrough subcommand, and introduce three universal introspection verbs (`explain`/`overview`/`learn`) at culture's CLI root.

**Architecture:** Culture imports `agent_experience` (the agex-cli package) as a normal library dependency — no vendoring or git subtree. A tiny internal dispatcher (`culture/cli/introspect.py`) owns the universal verbs with a topic→handler registry. The `culture agex` group passes argv straight through to agex's typer app and also registers handlers for `agex` topic on the dispatcher. The existing `culture/cli/` group protocol is extended by one optional attribute (`NAMES`) so the introspect module can own three top-level verbs instead of one noun-group.

**Tech Stack:** Python 3.12, `uv`, argparse (culture), typer (agex), pytest + pytest-xdist.

**Spec:** `docs/superpowers/specs/2026-04-22-agex-integration-design.md`

**Branch:** `agex-integration` (already created from `origin/main`, commit `f4a6324` carries the spec).

---

## File Structure

**Create:**

- `culture/cli/introspect.py` — dispatcher module. Exposes `NAMES`, `register(subparsers)`, `dispatch(args)` (the culture CLI group protocol), plus an internal registry API `register_topic(topic, *, explain=, overview=, learn=)` and resolver functions `explain(topic)`, `overview(topic)`, `learn(topic)`.
- `culture/cli/agex.py` — passthrough group. Exposes `NAME`, `register(subparsers)`, `dispatch(args)`. On import, calls `introspect.register_topic("agex", ...)` so `culture explain agex`, `culture overview agex`, `culture learn agex` work through the universal-verb path too.
- `tests/test_cli_introspect.py` — tests for the dispatcher and root verbs.
- `tests/test_cli_agex.py` — tests for the `culture agex` passthrough.
- `docs/reference/cli/agex.md` — user-facing docs for the passthrough + universal verb contract.

**Modify:**

- `pyproject.toml` — add `agex-cli>=0.13,<1.0` to `[project].dependencies`.
- `uv.lock` — regenerated by `uv sync`.
- `culture/cli/__init__.py` — import `agex` and `introspect`; add them to `GROUPS`; extend the dispatch loop to use `NAMES` (set-typed) with `{group.NAME}` as fallback; add a line in the `GROUPS` docstring listing the three universal verbs.
- `docs/reference/cli/index.md` — add a short subsection linking to `agex.md` and listing the three root verbs.
- `CHANGELOG.md` + `culture/__init__.py` — bumped by `/version-bump minor` at the end.

---

## Task 1: Add `agex-cli` dependency

**Files:**

- Modify: `pyproject.toml:17-24`
- Regenerated: `uv.lock`

**Context:** Agex is published to PyPI as `agex-cli`; its import path is `agent_experience`. Pin `>=0.13,<1.0` per the spec. Locking is what actually reproduces the exact version; the floor/ceiling in `pyproject.toml` is just policy.

- [ ] **Step 1: Add the dependency line**

Open `pyproject.toml` and replace the `dependencies` list:

```toml
dependencies = [
    "pyyaml>=6.0",
    "anthropic>=0.40",
    "claude-agent-sdk>=0.1",
    "mistune>=3.0",
    "aiohttp>=3.9",
    "textual>=1.0",
    "agex-cli>=0.13,<1.0",
]
```

- [ ] **Step 2: Resolve and lock**

```bash
cd /home/spark/git/culture
uv sync
```

Expected: `uv.lock` updates; several new transitive packages appear (`typer`, `jinja2`, `tomlkit`, `portalocker`). No resolver errors.

- [ ] **Step 3: Verify the import path works**

```bash
uv run python -c "from agent_experience.cli import app; print(type(app).__name__)"
```

Expected stdout: `Typer`

- [ ] **Step 4: Commit**

```bash
git add pyproject.toml uv.lock
git commit -m "deps: add agex-cli>=0.13,<1.0"
```

---

## Task 2: Dispatcher module — core registry and resolvers

**Files:**

- Create: `culture/cli/introspect.py`
- Create: `tests/test_cli_introspect.py`

**Context:** The dispatcher owns the universal verb registry. It is also a culture CLI "group" — it extends the group protocol by exporting `NAMES: frozenset[str]` (plural) instead of a single `NAME`. Task 4 teaches `culture/cli/__init__.py` to handle `NAMES`.

- [ ] **Step 1: Write the failing registry test**

Create `tests/test_cli_introspect.py` with:

```python
"""Tests for the universal introspection verb dispatcher."""

from culture.cli import introspect


def test_register_and_resolve_explain():
    introspect.register_topic("demo", explain=lambda _t: ("demo-explain", 0))
    try:
        stdout, code = introspect.explain("demo")
        assert stdout == "demo-explain"
        assert code == 0
    finally:
        introspect._clear_registry()  # test-only helper


def test_unknown_topic_exits_1_with_available_list():
    introspect.register_topic("alpha", explain=lambda _t: ("a", 0))
    try:
        stdout, code = introspect.explain("bogus")
        assert code == 1
        assert "bogus" in stdout
        assert "alpha" in stdout
    finally:
        introspect._clear_registry()


def test_default_topic_is_culture_when_registered():
    introspect.register_topic("culture", explain=lambda _t: ("root-ok", 0))
    try:
        stdout, code = introspect.explain(None)
        assert code == 0
        assert stdout == "root-ok"
    finally:
        introspect._clear_registry()


def test_verbs_have_independent_registries():
    introspect.register_topic("x", explain=lambda _t: ("e", 0))
    try:
        _, code = introspect.overview("x")
        assert code == 1  # no overview handler for x
    finally:
        introspect._clear_registry()
```

- [ ] **Step 2: Run tests to verify they fail**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: all four fail with `ModuleNotFoundError` or `AttributeError`.

- [ ] **Step 3: Implement the dispatcher**

Create `culture/cli/introspect.py`:

```python
"""Universal introspection verb dispatcher.

Registers three top-level verbs (``explain``, ``overview``, ``learn``)
on the culture CLI and dispatches each to a per-topic handler.

The module conforms to the extended culture CLI group protocol:
exports ``NAMES`` (frozenset) instead of the singular ``NAME``.
"""

from __future__ import annotations

import argparse
from typing import Callable

Handler = Callable[[str | None], tuple[str, int]]

NAMES = frozenset({"explain", "overview", "learn"})

_explain: dict[str, Handler] = {}
_overview: dict[str, Handler] = {}
_learn: dict[str, Handler] = {}

_REGISTRIES: dict[str, dict[str, Handler]] = {
    "explain": _explain,
    "overview": _overview,
    "learn": _learn,
}


def register_topic(
    topic: str,
    *,
    explain: Handler | None = None,
    overview: Handler | None = None,
    learn: Handler | None = None,
) -> None:
    """Register handlers for a topic. Any verb may be omitted."""
    if explain is not None:
        _explain[topic] = explain
    if overview is not None:
        _overview[topic] = overview
    if learn is not None:
        _learn[topic] = learn


def _clear_registry() -> None:
    """Test-only: wipe all registries."""
    _explain.clear()
    _overview.clear()
    _learn.clear()


def _resolve(verb: str, topic: str | None) -> tuple[str, int]:
    registry = _REGISTRIES[verb]
    effective = topic if topic is not None else "culture"
    handler = registry.get(effective)
    if handler is None:
        available = sorted(registry.keys())
        msg = f"unknown topic '{effective}' for {verb}; available: {', '.join(available) or '(none)'}"
        return msg, 1
    return handler(topic)


def explain(topic: str | None) -> tuple[str, int]:
    return _resolve("explain", topic)


def overview(topic: str | None) -> tuple[str, int]:
    return _resolve("overview", topic)


def learn(topic: str | None) -> tuple[str, int]:
    return _resolve("learn", topic)


# --- CLI group protocol ---------------------------------------------------

def register(subparsers: "argparse._SubParsersAction") -> None:
    for verb in ("explain", "overview", "learn"):
        p = subparsers.add_parser(verb, help=f"{verb.capitalize()} a topic (culture by default)")
        p.add_argument("topic", nargs="?", default=None, help="Topic to inspect")


def dispatch(args: argparse.Namespace) -> None:
    import sys

    verb = args.command
    topic = getattr(args, "topic", None)
    stdout, code = _resolve(verb, topic)
    if stdout:
        stream = sys.stdout if code == 0 else sys.stderr
        end = "" if stdout.endswith("\n") else "\n"
        print(stdout, end=end, file=stream)
    if code != 0:
        sys.exit(code)
```

- [ ] **Step 4: Run tests to verify they pass**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: all four pass.

- [ ] **Step 5: Commit**

```bash
git add culture/cli/introspect.py tests/test_cli_introspect.py
git commit -m "feat(cli): add universal-verb dispatcher (explain/overview/learn)"
```

---

## Task 3: Register the `culture` root handler

**Files:**

- Modify: `culture/cli/introspect.py` — add a module-level registration for topic `"culture"`.
- Modify: `tests/test_cli_introspect.py` — add tests for root handler content.

**Context:** With no topic, `culture explain`/`overview`/`learn` should describe culture itself. `explain` and `overview` emit a short markdown block listing registered namespaces (with `(coming soon)` for any that aren't registered yet). `learn` reuses `culture.learn_prompt.generate_learn_prompt()` with defaults — this is the same engine powering `culture agent learn`.

- [ ] **Step 1: Write failing root-handler tests**

Append to `tests/test_cli_introspect.py`:

```python
def test_root_explain_mentions_culture_and_namespaces():
    # Module import registers the root handler as a side effect
    from culture.cli import introspect as intr
    stdout, code = intr.explain(None)
    assert code == 0
    assert "culture" in stdout.lower()
    # Expected namespaces named in the root handler
    for ns in ("agex", "server", "agent", "mesh", "bot", "channel", "skills"):
        assert ns in stdout


def test_root_overview_is_nonempty():
    from culture.cli import introspect as intr
    stdout, code = intr.overview(None)
    assert code == 0
    assert stdout.strip()


def test_root_learn_uses_generate_learn_prompt():
    from culture.cli import introspect as intr
    stdout, code = intr.learn(None)
    assert code == 0
    # Markers from generate_learn_prompt()'s template
    assert "Culture" in stdout
    assert "Install Skills" in stdout
```

- [ ] **Step 2: Run the new tests to confirm failure**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: the three new tests fail; prior tests may also fail if they rely on an empty registry. (The `_clear_registry` helper in Task 2 wipes state between tests — make sure the root handlers re-register themselves. Solution: put registration inside a function and call it from both module import and `_clear_registry()`.)

- [ ] **Step 3: Add the root handler and auto-register**

Edit `culture/cli/introspect.py`. Add at the top, below `_REGISTRIES`:

```python
_NAMESPACES = ("agex", "server", "agent", "mesh", "bot", "channel", "skills")


def _culture_explain(_topic: str | None) -> tuple[str, int]:
    lines = [
        "# Culture",
        "",
        "Culture is the framework of agreements that makes agent behavior",
        "portable, inspectable, and effective. It hosts a mesh of IRC servers",
        "where AI agents collaborate, share knowledge, and coordinate work.",
        "",
        "## Namespaces",
        "",
    ]
    for ns in _NAMESPACES:
        registered = ns in _explain
        marker = "" if registered else "  (coming soon)"
        lines.append(f"- `culture {ns}`{marker}")
    lines.append("")
    lines.append("## Universal verbs")
    lines.append("")
    lines.append("- `culture explain [topic]` — deep description")
    lines.append("- `culture overview [topic]` — shallow map")
    lines.append("- `culture learn [topic]` — agent onboarding prompt")
    return "\n".join(lines) + "\n", 0


def _culture_overview(_topic: str | None) -> tuple[str, int]:
    return (
        "Culture: agent IRC mesh. Namespaces: "
        + ", ".join(_NAMESPACES)
        + ". Universal verbs: explain, overview, learn.\n",
        0,
    )


def _culture_learn(_topic: str | None) -> tuple[str, int]:
    from culture.learn_prompt import generate_learn_prompt
    return generate_learn_prompt(), 0


def _register_root() -> None:
    register_topic(
        "culture",
        explain=_culture_explain,
        overview=_culture_overview,
        learn=_culture_learn,
    )


_register_root()
```

Then modify `_clear_registry` to re-register the root after wiping, so the module remains self-consistent for tests:

```python
def _clear_registry() -> None:
    """Test-only: wipe all registries, then re-register the root handlers."""
    _explain.clear()
    _overview.clear()
    _learn.clear()
    _register_root()
```

Finally, update the existing test `test_unknown_topic_exits_1_with_available_list` — it calls `_clear_registry()` then registers only `"alpha"`, but now `_clear_registry()` re-registers `"culture"`, so `alpha` plus `culture` will be listed. Adjust assertion:

```python
def test_unknown_topic_exits_1_with_available_list():
    introspect._clear_registry()  # starts fresh with root handlers
    introspect.register_topic("alpha", explain=lambda _t: ("a", 0))
    stdout, code = introspect.explain("bogus")
    assert code == 1
    assert "bogus" in stdout
    assert "alpha" in stdout
    assert "culture" in stdout
```

- [ ] **Step 4: Run all introspect tests**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: all pass (prior four + three new).

- [ ] **Step 5: Commit**

```bash
git add culture/cli/introspect.py tests/test_cli_introspect.py
git commit -m "feat(cli): register culture root explain/overview/learn handlers"
```

---

## Task 4: Wire root verbs into `culture/cli/__init__.py`

**Files:**

- Modify: `culture/cli/__init__.py`
- Modify: `tests/test_cli_introspect.py` — add end-to-end CLI test.

**Context:** The existing `GROUPS` dispatch loop matches `args.command == group.NAME`. To let `introspect` own three top-level verbs, teach the loop to prefer `group.NAMES` (set/frozenset) when present, falling back to `{group.NAME}` otherwise. Back-compatible with every existing group.

- [ ] **Step 1: Write failing end-to-end tests**

Append to `tests/test_cli_introspect.py`:

```python
import subprocess
import sys


def test_culture_explain_cli_lists_namespaces():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "explain"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert "culture" in result.stdout.lower()
    assert "agex" in result.stdout


def test_culture_overview_cli_runs():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "overview"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert result.stdout.strip()


def test_culture_learn_cli_runs():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "learn"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert "Culture" in result.stdout


def test_culture_explain_unknown_topic_exits_1():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "explain", "unknown-topic-xyz"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 1
    assert "unknown-topic-xyz" in result.stderr
```

- [ ] **Step 2: Run to confirm failure**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: four new tests fail (command unknown).

- [ ] **Step 3: Wire `introspect` into `__init__.py`**

Edit `culture/cli/__init__.py`. Full new contents:

```python
"""Unified CLI entry point for culture.

Commands are organized into noun-based groups:
    culture agent    {create,join,start,stop,status,rename,assign,sleep,wake,learn,message,read,archive,unarchive,delete}
    culture server   {start,stop,status,default,rename,archive,unarchive}
    culture mesh     {overview,setup,update,console}
    culture channel  {list,read,message,who}
    culture bot      {create,start,stop,list,inspect,archive,unarchive}
    culture skills   {install}
    culture agex     {...full passthrough to the standalone `agex` CLI...}

Universal verbs (available at the root):
    culture explain [topic]    full description of topic (default: culture)
    culture overview [topic]   shallow summary
    culture learn [topic]      agent-facing onboarding prompt
"""

from __future__ import annotations

import argparse
import logging
import sys

from culture import __version__
from culture.cli import agent, agex, bot, channel, introspect, mesh, server, skills

GROUPS = [agent, server, mesh, channel, bot, skills, agex, introspect]


def _names_of(group) -> set[str]:
    names = getattr(group, "NAMES", None)
    if names is not None:
        return set(names)
    return {group.NAME}


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="culture",
        description="culture — AI agent IRC mesh",
    )
    parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
    sub = parser.add_subparsers(dest="command")
    for group in GROUPS:
        group.register(sub)
    return parser


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(1)

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(name)s %(levelname)s %(message)s",
    )

    try:
        for group in GROUPS:
            if args.command in _names_of(group):
                group.dispatch(args)
                return
        parser.print_help()
        sys.exit(1)
    except KeyboardInterrupt:
        pass
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        sys.exit(1)
```

Note: `agex` must be created before this will import. If you're executing strictly in order, create a stub first and flesh it out in Task 5. The stub:

```python
# culture/cli/agex.py — minimal stub; replaced fully in Task 5
from __future__ import annotations
import argparse

NAME = "agex"


def register(subparsers: "argparse._SubParsersAction") -> None:
    subparsers.add_parser(NAME, help="Agex passthrough (see Task 5)")


def dispatch(_args: argparse.Namespace) -> None:  # pragma: no cover
    import sys
    print("agex passthrough not yet implemented", file=sys.stderr)
    sys.exit(1)
```

- [ ] **Step 4: Run introspect tests + existing CLI tests to confirm no regression**

```bash
/run-tests tests/test_cli_introspect.py tests/test_overview_cli.py tests/test_channel_cli.py tests/test_register_cli.py
```

Expected: all pass (new four plus existing).

- [ ] **Step 5: Commit**

```bash
git add culture/cli/__init__.py culture/cli/agex.py tests/test_cli_introspect.py
git commit -m "feat(cli): wire universal verbs into top-level CLI"
```

---

## Task 5: Real `culture agex` passthrough + agex topic handlers

**Files:**

- Replace: `culture/cli/agex.py` (stub from Task 4 becomes the real implementation).
- Create: `tests/test_cli_agex.py`

**Context:** The passthrough uses `argparse.REMAINDER` to collect all arguments after `culture agex` verbatim, then hands them to agex's typer `app` via `app(args=rest, standalone_mode=False)`. Typer returns the exit code instead of calling `sys.exit` when `standalone_mode=False`. We also register the `agex` topic with the universal-verb dispatcher so `culture explain agex` / `culture overview agex` / `culture learn agex` route through culture's universal path.

- [ ] **Step 1: Write failing passthrough tests**

Create `tests/test_cli_agex.py`:

```python
"""Tests for `culture agex` passthrough and `agex` universal topic."""

import subprocess
import sys


def test_culture_agex_version_runs():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "agex", "--version"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    # agex --version prints just the version string (e.g. "0.13.0")
    assert result.stdout.strip()
    assert all(c.isdigit() or c == "." for c in result.stdout.strip())


def test_culture_agex_explain_agex():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "agex", "explain", "agex"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert result.stdout.strip()


def test_culture_agex_help_shows_typer_help():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "agex", "--help"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0
    # typer help contains "Usage:" and lists top-level commands
    assert "Usage:" in result.stdout or "usage:" in result.stdout.lower()


def test_culture_explain_agex_via_universal_verb():
    result = subprocess.run(
        [sys.executable, "-m", "culture", "explain", "agex"],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, result.stderr
    assert result.stdout.strip()
```

- [ ] **Step 2: Run to confirm failure**

```bash
/run-tests tests/test_cli_agex.py
```

Expected: all four fail (agex stub is in place from Task 4 but doesn't do passthrough; universal verb doesn't know `agex`).

- [ ] **Step 3: Replace `culture/cli/agex.py` with the real implementation**

Overwrite `culture/cli/agex.py`:

```python
"""`culture agex` — passthrough to the standalone agex CLI.

Also registers universal-verb handlers for the ``agex`` topic so
``culture explain agex`` / ``culture overview agex`` / ``culture learn agex``
route through culture's universal-verb path.
"""

from __future__ import annotations

import argparse
import sys

from culture.cli import introspect

NAME = "agex"


def _run_agex(argv: list[str]) -> int:
    """Invoke agex's typer app in-process, returning its exit code.

    Uses standalone_mode=True (the typer default) so typer's own --help,
    --version, and Exit handling work unchanged. Typer calls sys.exit when
    done; we translate that SystemExit back into a return value.
    """
    try:
        from agent_experience.cli import app
    except ImportError as exc:  # pragma: no cover — declared dep
        print(f"agex-cli is not installed: {exc}", file=sys.stderr)
        return 2
    try:
        app(args=argv)
    except SystemExit as e:
        if e.code is None:
            return 0
        if isinstance(e.code, int):
            return e.code
        print(e.code, file=sys.stderr)
        return 1
    return 0


# --- universal-verb topic handlers for ``agex`` ---------------------------

def _agex_explain(_topic: str | None) -> tuple[str, int]:
    import io, contextlib
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        code = _run_agex(["explain", "agex"])
    return buf.getvalue(), code


def _agex_overview(_topic: str | None) -> tuple[str, int]:
    import io, contextlib
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        code = _run_agex(["overview", "--agent", "claude-code"])
    return buf.getvalue(), code


def _agex_learn(_topic: str | None) -> tuple[str, int]:
    import io, contextlib
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        code = _run_agex(["learn", "--agent", "claude-code"])
    return buf.getvalue(), code


introspect.register_topic(
    "agex",
    explain=_agex_explain,
    overview=_agex_overview,
    learn=_agex_learn,
)


# --- CLI group protocol ---------------------------------------------------

def register(subparsers: "argparse._SubParsersAction") -> None:
    p = subparsers.add_parser(
        NAME,
        help="Run agex (agent-experience CLI) via passthrough",
        add_help=False,  # let agex/typer own --help so `culture agex --help` shows agex's help
    )
    p.add_argument("agex_args", nargs=argparse.REMAINDER, help="Arguments passed to agex")


def dispatch(args: argparse.Namespace) -> None:
    rest = list(getattr(args, "agex_args", []) or [])
    sys.exit(_run_agex(rest))
```

- [ ] **Step 4: Run the agex tests**

```bash
/run-tests tests/test_cli_agex.py
```

Expected: all four pass.

- [ ] **Step 5: Run the introspect CLI tests too to confirm `agex` now shows as registered**

```bash
/run-tests tests/test_cli_introspect.py
```

Expected: still all pass — and `test_culture_explain_cli_lists_namespaces` no longer marks `agex` with `(coming soon)` because the `agex` module registers on import.

- [ ] **Step 6: Commit**

```bash
git add culture/cli/agex.py tests/test_cli_agex.py
git commit -m "feat(cli): add 'culture agex' passthrough + agex universal-verb handlers"
```

---

## Task 6: Documentation

**Files:**

- Create: `docs/reference/cli/agex.md`
- Modify: `docs/reference/cli/index.md`

**Context:** Culture's CLI docs live in `docs/reference/cli/` and use Jekyll front matter (see existing `index.md`). Add a new page for the agex passthrough + universal verb contract. Update `index.md` to link it and document the three root verbs.

- [ ] **Step 1: Create `docs/reference/cli/agex.md`**

```markdown
---
title: "culture agex"
parent: "CLI"
grand_parent: "Reference"
nav_order: 10
sites: [agentirc, culture]
description: "Agex passthrough and universal introspection verbs."
permalink: /reference/cli/agex/
---

# `culture agex` and universal verbs

Culture ships [agex](https://agex.culture.dev) as a first-class
citizen. Two affordances:

## `culture agex <anything>`

A full passthrough to the standalone [`agex`](https://agex.culture.dev) CLI.
Everything after `culture agex` is forwarded verbatim to agex's typer app.
Exit codes propagate.

```bash
culture agex --version
culture agex explain agex
culture agex overview --agent claude-code
culture agex learn --agent claude-code
```

`culture agex --help` shows agex's help, not culture's.

## Universal verbs: `explain` / `overview` / `learn`

Three verbs live at the root of the culture command tree. Each takes an
optional `topic`; when omitted, the topic defaults to `culture`.

| Verb | Meaning |
|------|---------|
| `explain X` | Full description of X and everything under X (deep) |
| `overview X` | Summary of X (shallow map view) |
| `learn X` | Agent-facing onboarding prompt for operating X |

```bash
culture explain           # describes culture + its namespaces
culture explain agex      # routes to agex explain
culture overview          # culture map
culture learn             # agent onboarding prompt for culture
culture learn agex        # agent onboarding for agex
```

`learn` produces an agent-facing self-teaching prompt so an agent doesn't
have to re-explore a tool every time. This matches agex's `agex learn`
verb semantically.

## Each namespace owns its own

Culture is pure plumbing: a tiny internal dispatcher
(`culture/cli/introspect.py`) maps topics to handlers. Each namespace that
wants to participate registers its own handlers on import. For `agex`,
the agex-cli library already implements the three verbs — culture just
routes. Native namespaces (`mesh`, `server`, `agent`, …) will add their
own handlers in a future release.
```

- [ ] **Step 2: Update `docs/reference/cli/index.md`**

Add this section near the end (before the file closes):

```markdown
## Universal verbs

Available at the root of the command tree. See
[`culture agex`](./agex.md) for detail.

- `culture explain [topic]` — deep description
- `culture overview [topic]` — shallow summary
- `culture learn [topic]` — agent onboarding prompt
- `culture agex <anything>` — passthrough to the agex CLI
```

- [ ] **Step 3: Lint markdown**

```bash
markdownlint-cli2 docs/reference/cli/agex.md docs/reference/cli/index.md
```

Expected: no errors. Fix any warnings.

- [ ] **Step 4: Run doc-test-alignment subagent**

Dispatch `Agent(subagent_type="doc-test-alignment", ...)` with the branch's staged+unstaged diff. Confirm no new public API is missing documentation.

- [ ] **Step 5: Commit**

```bash
git add docs/reference/cli/agex.md docs/reference/cli/index.md
git commit -m "docs: document 'culture agex' passthrough + universal verbs"
```

---

## Task 7: Version bump + final verification

**Files:**

- Modify: `pyproject.toml`, `culture/__init__.py`, `CHANGELOG.md` (all via `/version-bump`).

**Context:** New CLI surface → minor bump per culture's convention.

- [ ] **Step 1: Run version bump**

```bash
# In this conversation, invoke the skill:
# /version-bump minor
```

Expected: `pyproject.toml` version, `culture/__init__.py` `__version__`, and `CHANGELOG.md` all updated atomically; `/version-bump` commits the change itself.

- [ ] **Step 2: Run the full test suite**

```bash
/run-tests
```

Expected: all existing tests plus the new introspect and agex tests pass. No new failures.

- [ ] **Step 3: Manual verification**

```bash
uv run culture agex --version            # prints agex version
uv run culture agex explain agex         # agex self-description
uv run culture explain                   # culture overview + namespaces
uv run culture overview                  # one-line culture map
uv run culture learn | head -20          # generate_learn_prompt output
uv run culture explain unknown-topic-xyz # exit 1, helpful stderr
```

Confirm each behaves per the spec's Verification section.

- [ ] **Step 4: Pre-push review for shared/CLI code**

Per `CLAUDE.md`: this touches the CLI dispatcher and a new transport boundary (agex typer app embedding). Run the code-reviewer subagent on the staged diff before first push:

```bash
# Agent(subagent_type="superpowers:code-reviewer", ...)
```

Fix any findings. Re-commit.

- [ ] **Step 5: Push and open PR**

```bash
git push -u origin agex-integration
gh pr create --title "feat: culture agex passthrough + universal verbs (explain/overview/learn)" --body "$(cat <<'EOF'
## Summary

- Adds `agex-cli>=0.13,<1.0` as a PyPI library dependency.
- Introduces `culture agex <anything>` as a full passthrough to the standalone agex CLI.
- Adds three universal introspection verbs at the root: `culture explain`, `culture overview`, `culture learn`.
- Each namespace owns its own handlers via a tiny dispatcher (`culture/cli/introspect.py`); `agex` is the only namespace wired this pass — natives land in a follow-up.

Spec: `docs/superpowers/specs/2026-04-22-agex-integration-design.md`

## Test plan

- [x] `/run-tests` — new introspect + agex tests pass; no regressions.
- [x] `uv run culture agex --version`
- [x] `uv run culture agex explain agex`
- [x] `uv run culture explain` lists namespaces
- [x] `uv run culture learn` produces onboarding prompt
- [x] `uv run culture explain unknown-topic-xyz` exits 1 with helpful message

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```

---

## Follow-up (not this PR)

After merge, file these as GitHub issues on `OriNachum/culture`:

1. **Native-namespace universal verbs** — each of `mesh`/`server`/`agent`/`bot`/`channel`/`skills` should register its own `explain`/`overview`/`learn` handlers via `introspect.register_topic`. `culture agent learn` is already correct under the contract and becomes the reference.
2. **Afi integration** — apply this same recipe once `afi-cli` stabilizes: PyPI dep + passthrough module + dispatcher registration.

## Self-review checklist

- **Spec coverage:** Task 1 ↔ dependency; Task 2 ↔ dispatcher contract; Task 3 ↔ root handler + `learn_prompt` reuse; Task 4 ↔ `GROUPS` protocol extension + CLI wiring; Task 5 ↔ passthrough + `agex` topic handlers; Task 6 ↔ docs (`docs/reference/cli/agex.md` and index); Task 7 ↔ version bump + manual verification + doc-test-alignment + code-reviewer.
- **Types consistent:** `Handler = Callable[[str | None], tuple[str, int]]` used uniformly; `register_topic(topic, *, explain=, overview=, learn=)` signature stable; `NAMES` vs `NAME` divergence handled in `_names_of` helper.
- **No placeholders:** every code block shows real code; every command shows exact invocation.
