# AgentIRC Extraction — Track A3 (Final Cutover) Implementation Plan

> **Plan revised 2026-05-03.** A2's approach changed (cross-repo split filed as agentculture/agentirc#22): culture's `culture/cli/server.py:_run_server` now embeds `agentirc.ircd.IRCd` directly in-process (no subprocess), and `culture/bots/virtual_client.py` is a thin wrapper over the public `agentirc.virtual_client.VirtualClient`. A3 inherits that — Task 1.3's old "in-process dispatch vs subprocess run" decision is settled (A2 already chose in-process); the partial-passthrough shim still preserves culture-owned verbs (`default`/`rename`/`archive`/`unarchive`).

> **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.
>
> **This plan supersedes** `docs/superpowers/plans/2026-04-30-agentirc-extraction-track-a.md`. The original drafted A1+A2+A3 as a single PR; phasing forced a split. A1 shipped in culture#309 (8.8.0); A2 ships in `feat/bots-embedded-agentirc` (8.9.0, see `docs/superpowers/plans/2026-05-02-agentirc-extraction-track-a2.md`); A3 is this plan.

**Goal:** Delete the bundled IRCd in `culture/agentirc/`. A2 already swapped `culture/cli/server.py:_run_server` to embed `agentirc.ircd.IRCd` directly, so the bundled files are dead weight by this point. A3 removes them, finishes the partial-passthrough shim for non-`start` lifecycle verbs (forwarding to `agentirc.cli.dispatch`), and bumps major. After A3, the only Python files remaining under `culture/agentirc/` are `config.py` (the A1 re-export shim) and `__init__.py`. Major version bump 8.x → 9.0.0.

**Architecture:** Single PR off `main`, after A2 has merged. `git rm` the IRCd source files (`ircd.py`, `server_link.py`, `channel.py`, `events.py`, the four stores, `rooms_util.py`, `skill.py`, `skills/`). `git mv` `client.py` and `remote_client.py` to `culture/transport/` (preserves blame; they were never IRCd code). `culture/cli/server.py` already constructs `agentirc.ircd.IRCd(config)` for `start` (per A2); A3 layers on a partial passthrough — culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) keep their existing handlers, lifecycle verbs other than `start` (`stop`/`status`/`restart`/`link`/`logs`/`version`/`serve`) forward to `agentirc.cli.dispatch`. Delete `protocol/extensions/` (lives in agentirc's repo). Bump major.

**Tech Stack:** Python 3.x, uv, argparse, pytest + pytest-asyncio + pytest-xdist, pre-commit (black + isort + flake8 + pylint + bandit + markdownlint).

**Companion spec:** `docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md` (sections "Boundary", "Architecture", "CLI surface", "Import contract").

---

## Preconditions (do not start until all are true)

- A2 has merged and a culture release has been cut. `culture/bots/*` no longer imports anything from `culture.agentirc.{ircd, server_link, channel, events, room_store, thread_store, history_store, rooms_util, skill}`. Verify:

  ```bash
  grep -rn 'from culture\.agentirc\.\(ircd\|server_link\|channel\|events\|room_store\|thread_store\|history_store\|rooms_util\|skill\)' culture/bots/ tests/
  ```

  Expected: no matches under `culture/bots/`. Test files may still hit IRCd internals — Task 7 cleans those up.

- `agentirc-cli>=9.6,<10` exposes the full CLI surface culture is shimming to:

  ```bash
  cd /tmp && uv run --with "agentirc-cli>=9.6,<10" agentirc --help
  ```

  Expected: subcommands `serve start stop restart status link logs version` (or a superset). If anything's missing, **stop** — the shim parity test (Task 5) will fail.

- `agentirc.cli.dispatch(argv) -> int` is callable in-process (verifies the shim path works without forking a subprocess):

  ```bash
  cd /tmp && uv run --with "agentirc-cli>=9.6,<10" python -c "
  from agentirc.cli import dispatch
  rc = dispatch(['version'])
  print('exit code:', rc)
  "
  ```

  Expected: agentirc prints its version and `exit code: 0`. If `dispatch` is not exposed (only `main()`), Task 4 must use the subprocess path.

- Working tree on `main` is clean. `git status` shows no staged/unstaged changes inside `culture/agentirc/`, `culture/cli/server.py`, `culture/cli/shared/mesh.py`, `culture/transport/`, `protocol/extensions/`, or related tests.

---

## File Structure (what changes in this PR)

| Path | Action | Notes |
|---|---|---|
| `pyproject.toml` | Modify | Drop `culture/agentirc/` from package data / wheel includes if explicitly listed. No dep change (A2 already pinned `>=9.6,<10`). |
| `culture/agentirc/{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}.py` | Delete | The IRCd core. |
| `culture/agentirc/skills/` | Delete (whole dir) | Server-side skill plugins. |
| `culture/agentirc/__main__.py` | Delete | `python -m culture.agentirc` retired (no known callers). |
| `culture/agentirc/client.py` | `git mv` → `culture/transport/client.py` | Preserves blame. |
| `culture/agentirc/remote_client.py` | `git mv` → `culture/transport/remote_client.py` | Preserves blame. |
| `culture/agentirc/config.py` | Keep | A1 re-export shim over `agentirc.config`. Stays through 9.x; remove in 10.0.0. |
| `culture/agentirc/__init__.py` | Modify | Trim to a single re-export of `agentirc.config` symbols. |
| `culture/transport/__init__.py` | Create | Re-exports the public class names from `client.py` and `remote_client.py`. |
| `culture/cli/server.py` | Modify | ~30 lines. Partial passthrough — culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) dispatch to `culture/cli/server_local.py`; `start` continues to use the in-process `agentirc.ircd.IRCd(config)` path A2 introduced; other lifecycle verbs (`stop`/`status`/`restart`/`link`/`logs`/`version`/`serve`) forward to `agentirc.cli.dispatch`. |
| `culture/cli/server_local.py` | Create | Houses the moved handler bodies for `default`/`rename`/`archive`/`unarchive` (extracted from today's `culture/cli/server.py`). Helpers (`read_default_server`, rename/archive utilities) stay in `culture/cli/shared/`. |
| `culture/bots/`, `culture/clients/*/daemon.py` | Modify (imports only) | `culture.agentirc.{client,remote_client}` → `culture.transport.{client,remote_client}`. |
| `protocol/extensions/` | Delete (whole dir) | Lives in agentirc now. |
| `tests/test_*` for IRCd internals | Delete | They live in agentirc's repo now. |
| `tests/test_server_shim.py` | Create | Asserts every verb in `agentirc --help` is reachable via `culture server <verb> --help`. |
| `tests/test_agentirc_smoke.py` | Create | Boots `agentirc serve` as a subprocess, connects a culture transport client, exchanges one message. |
| `culture/__init__.py`, `pyproject.toml`, `CHANGELOG.md` | Modify | `/version-bump major` (8.9.x → 9.0.0). |

---

## Task 1 — Verify preconditions

**Files:** none (verification only).

- [ ] **Step 1.1:** Confirm A2 already cleaned up bot-side imports (run the grep from "Preconditions"). If any matches remain in `culture/bots/`, **stop** — A2 wasn't fully done.

- [ ] **Step 1.2:** Confirm the agentirc CLI surface (run the `agentirc --help` snippet from "Preconditions"). Save the output — Task 5's shim parity test reads from this.

- [ ] **Step 1.3:** Inherit A2's decision: **in-process embedding via `agentirc.ircd.IRCd`** for `culture server start`, plus `agentirc.cli.dispatch(argv)` for the other lifecycle verbs (`stop`, `status`, `link`, `logs`, `version`, `serve`). A2 already wired `_run_server` to construct `agentirc.ircd.IRCd(config)` directly; A3 just reuses that path. The "subprocess for `start`" alternative from the original A3 draft is no longer relevant — A2 confirmed the public class boots cleanly in culture's CLI process. Confirm by reading `culture/cli/server.py` post-A2 and verifying `_run_server` already imports from `agentirc.ircd`.

---

## Task 2 — Branch and audit remaining culture-side imports of `culture.agentirc.*`

**Files:** none in this task (audit only).

- [ ] **Step 2.1:** Branch out.

  ```bash
  git checkout main && git pull
  git checkout -b feat/agentirc-extraction-cutover
  ```

- [ ] **Step 2.2:** Audit:

  ```bash
  grep -rn 'culture\.agentirc' culture/ tests/
  ```

  Tabulate. Expected categories:
  1. `from culture.agentirc.config import …` — keep (config.py stays as the A1 shim).
  2. `from culture.agentirc.client import Client` — Task 3 retargets to `culture.transport.client`.
  3. `from culture.agentirc.remote_client import RemoteClient` — Task 3 retargets to `culture.transport.remote_client`.
  4. `from culture.agentirc.{ircd,server_link,channel,events,…} import …` — Task 7 deletes / rewrites.
  5. `python -m culture.agentirc` invocations in scripts/CI — replace with `agentirc` (binary) or `python -m agentirc`.

  Anything outside these five categories is a surprise; investigate before deleting.

---

## Task 3 — Move `client.py` + `remote_client.py` to `culture/transport/`

**Files:** `culture/agentirc/client.py`, `culture/agentirc/remote_client.py`, `culture/transport/__init__.py`.

- [ ] **Step 3.1:** Create the destination directory:

  ```bash
  mkdir -p culture/transport
  ```

- [ ] **Step 3.2:** `git mv` (preserves blame):

  ```bash
  git mv culture/agentirc/client.py culture/transport/client.py
  git mv culture/agentirc/remote_client.py culture/transport/remote_client.py
  ```

- [ ] **Step 3.3:** Create `culture/transport/__init__.py` re-exporting the public class names:

  ```python
  """Culture's IRC transport — TCP client + remote-server-link client."""

  from culture.transport.client import Client
  from culture.transport.remote_client import RemoteClient

  __all__ = ["Client", "RemoteClient"]
  ```

  Names to re-export are whatever the moved files publicly defined; `grep -E '^class |^def ' culture/transport/{client,remote_client}.py` to enumerate. Don't speculate — match what was already there.

- [ ] **Step 3.4:** Inside `culture/transport/client.py`, replace any IRC-verb / numeric / extension-tag string literals with `agentirc.protocol.*` constants. Survey first:

  ```bash
  grep -nE '"(JOIN|PART|PRIVMSG|NOTICE|NAMES|WHO|MODE|CAP|EVENT|EVENTSUB|EVENTPUB|EVENTUNSUB|EVENTERR|SEVENT|SMSG|THREAD|ROOM|HISTORY|DEFAULT|RENAME|ARCHIVE|UNARCHIVE|[0-9]{3})"' culture/transport/client.py
  ```

  Replace each match with the named constant from `agentirc.protocol`. This is the same retargeting A1 did for `culture/cli/shared/mesh.py` against `agentirc.config` — the pattern is mechanical. If `agentirc.protocol` doesn't expose a constant for a verb culture uses, file an issue against agentirc and keep the string literal for now (mark with a `# TODO(A3)` comment).

- [ ] **Step 3.5:** Update every importer in culture:

  ```bash
  grep -rln 'from culture\.agentirc\.client import\|from culture\.agentirc\.remote_client import' culture/ tests/ | xargs sed -i 's|from culture\.agentirc\.client import|from culture.transport.client import|g; s|from culture\.agentirc\.remote_client import|from culture.transport.remote_client import|g'
  ```

  Spot-check the diff before staging.

- [ ] **Step 3.6:** Run a focused test pass:

  ```bash
  bash .claude/skills/run-tests/scripts/test.sh -p -q tests/test_*client*.py
  ```

  Expected: green. Transport behavior is unchanged; only the import path moved.

---

## Task 4 — Replace `culture/cli/server.py:_run_server` with a passthrough shim

**Files:** `culture/cli/server.py`.

- [ ] **Step 4.1:** Read the current `culture/cli/server.py` and enumerate **every** subcommand it owns. Today's `culture server` parser carries (per `culture/cli/server.py:1` docstring + the subparser block):

  | Verb | Owner after A3 |
  |---|---|
  | `start`, `stop`, `status` | agentirc (lifecycle) |
  | `link`, `logs`, `version`, `serve` | agentirc (lifecycle / introspection) |
  | `default` | **culture** (writes the local default-server pointer; reads `read_default_server()` from `culture/cli/shared/server.py`) |
  | `rename` | **culture** (renames the server in the local config and updates agent nicks; touches culture's own state) |
  | `archive` | **culture** (archives the local server config; culture-side state) |
  | `unarchive` | **culture** (unarchives the local server config; culture-side state) |

  Run `grep -n 'add_parser\|server_sub' culture/cli/server.py` to confirm the live list before continuing — do not rely on this table if it disagrees with the file.

- [ ] **Step 4.2:** Decide the routing rule. The shim is a **partial** passthrough — culture-owned verbs (`default`, `rename`, `archive`, `unarchive`) stay in culture; everything else forwards to agentirc. Implement as a dispatch dict, not as a hard-coded "if argv[0] in {…}" + drop-through, so the verb list is reviewable in one place:

  ```python
  """culture server <verb> — partial passthrough.

  - culture-owned verbs (default/rename/archive/unarchive) stay local
  - `start` keeps using A2's in-process agentirc.ircd.IRCd path
  - other lifecycle verbs forward to agentirc.cli.dispatch
  """
  from agentirc.cli import dispatch as _agentirc_dispatch

  from culture.cli.server_local import (
      cmd_default,
      cmd_rename,
      cmd_archive,
      cmd_unarchive,
      cmd_start,  # the A2-introduced in-process path; still owned by culture
  )

  _CULTURE_OWNED = {
      "default": cmd_default,
      "rename": cmd_rename,
      "archive": cmd_archive,
      "unarchive": cmd_unarchive,
      "start": cmd_start,
  }

  def server(argv: list[str]) -> int:
      """culture server <verb> <args> → culture-local handler if culture-owned, else agentirc."""
      if argv and argv[0] in _CULTURE_OWNED:
          return _CULTURE_OWNED[argv[0]](argv[1:])
      return _agentirc_dispatch(argv)
  ```

  Move the existing handler bodies for `default`/`rename`/`archive`/`unarchive` and the A2-introduced in-process `start` (whichever name `_run_server` uses today, exposed as `cmd_start`) from `culture/cli/server.py` into a new `culture/cli/server_local.py` so the shim file stays small and the culture-owned logic has its own home. Keep the helpers (`read_default_server`, the rename/archive utilities) where they live today (`culture/cli/shared/`).

  Why `start` is culture-owned, not forwarded: A2 wired it to construct `agentirc.ircd.IRCd(config)` directly and register culture's bots against it. Forwarding `start` to `agentirc.cli.dispatch(["start", ...])` would lose the bot registration — bots would never load. Keep `start` local; A3 doesn't need to change A2's choice here.

- [ ] **Step 4.3:** Properties to preserve:
  - **Pure forwarding for non-culture verbs.** Culture does not parse, validate, or rename any flag agentirc owns. New verbs added in agentirc are reachable via `culture server <new-verb>` automatically.
  - **Single source of truth for help / error / exit codes** of agentirc-owned verbs — all from agentirc.
  - **Culture-owned verbs stay culture-owned.** `default`/`rename`/`archive`/`unarchive` keep their current signatures, flags, output, and exit codes. They must continue to operate on culture's state — agentirc has no notion of them.

- [ ] **Step 4.4:** Wire the shim into culture's CLI dispatcher. The dispatcher (in `culture/cli/__init__.py` or `culture/cli/main.py`) probably calls `_run_server(args)` today; switch it to call `server(argv)`.

- [ ] **Step 4.5:** Manual smoke — covers both branches of the dispatch:

  ```bash
  # agentirc branch:
  uv run python -m culture server --help
  uv run python -m culture server status
  uv run python -m culture server version

  # culture-owned branch:
  uv run python -m culture server default --help
  uv run python -m culture server rename --help
  uv run python -m culture server archive --help
  uv run python -m culture server unarchive --help
  ```

  Agentirc verbs should match `agentirc <verb> --help` exactly. Culture-owned verbs should print culture's own help text, identical to today's `culture server <verb> --help` before the shim landed.

---

## Task 5 — Add the shim parity test

**Files:** `tests/test_server_shim.py`.

- [ ] **Step 5.1:** Create the test. Note: the shim is a **partial** passthrough (per Task 4.2), so the parity test only covers agentirc-owned verbs. Culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) get their own regression tests that assert their pre-A3 behavior is unchanged.

  ```python
  """Asserts culture server <verb> is byte-identical to agentirc <verb> for agentirc-owned verbs."""
  import subprocess
  import sys

  import pytest

  CULTURE = [sys.executable, "-m", "culture", "server"]
  AGENTIRC = ["agentirc"]

  # Agentirc-owned verbs only; culture-owned verbs (start/default/rename/archive/unarchive)
  # are tested separately because they either have no agentirc counterpart (default/rename/archive/unarchive)
  # or culture deliberately keeps them local (start — A2 hosts an in-process agentirc.ircd.IRCd plus culture's bots).
  AGENTIRC_VERBS = ["status", "version", "logs", "stop", "restart", "link", "serve"]


  def _run(cmd: list[str]) -> tuple[int, str, str]:
      proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
      return proc.returncode, proc.stdout, proc.stderr


  def _strip_argv0(text: str) -> str:
      """The argv[0] in usage lines will differ ("culture server" vs "agentirc"); normalize it."""
      return "\n".join(
          line.replace("culture server", "agentirc")
          for line in text.splitlines()
      )


  def test_top_level_help_lists_all_agentirc_verbs() -> None:
      """`culture server --help` must list every verb agentirc lists, plus the culture-owned ones."""
      _, culture_out, _ = _run([*CULTURE, "--help"])
      _, agentirc_out, _ = _run([*AGENTIRC, "--help"])
      for verb in AGENTIRC_VERBS:
          assert verb in culture_out, f"agentirc-owned verb {verb!r} missing from culture's help"
          assert verb in agentirc_out
      for verb in ("default", "rename", "archive", "unarchive"):
          assert verb in culture_out, f"culture-owned verb {verb!r} missing from culture's help"


  @pytest.mark.parametrize("verb", AGENTIRC_VERBS)
  def test_agentirc_verb_help_matches(verb: str) -> None:
      """Help text for agentirc-owned verbs must be byte-identical between culture and agentirc."""
      culture_rc, culture_out, _ = _run([*CULTURE, verb, "--help"])
      agentirc_rc, agentirc_out, _ = _run([*AGENTIRC, verb, "--help"])
      assert culture_rc == agentirc_rc
      assert _strip_argv0(culture_out) == agentirc_out


  @pytest.mark.parametrize("verb", ["start", "default", "rename", "archive", "unarchive"])
  def test_culture_owned_verb_still_dispatches_locally(verb: str) -> None:
      """Culture-owned verbs must NOT be forwarded to agentirc. `--help` must succeed without an agentirc binary on PATH."""
      rc, out, err = _run([*CULTURE, verb, "--help"])
      assert rc == 0, f"{verb} --help failed: {err}"
      # Sanity: this is culture's argparse output, not agentirc's. argparse usage line includes the parser path.
      assert verb in out
  ```

  Pattern for the agentirc-side comparison lifted from PR #309's `tests/test_agentirc_config_shim.py::test_shim_is_identity` — same identity-invariant style. The culture-owned verb tests are new and are A3's regression guardrail against accidentally over-shimming.

- [ ] **Step 5.2:** Run it:

  ```bash
  bash .claude/skills/run-tests/scripts/test.sh -p -q tests/test_server_shim.py
  ```

---

## Task 6 — Add the cross-repo smoke test

**Files:** `tests/test_agentirc_smoke.py`.

- [ ] **Step 6.1:** Create a test that boots a real `agentirc serve` subprocess on a random port, connects a `culture.transport.Client`, and exchanges a `PRIVMSG`:

  ```python
  """End-to-end smoke: culture transport client talks to a real agentirc subprocess."""
  import asyncio
  import socket
  import subprocess
  import sys
  from pathlib import Path

  import pytest

  from culture.transport import Client


  def _free_port() -> int:
      with socket.socket() as s:
          s.bind(("127.0.0.1", 0))
          return s.getsockname()[1]


  @pytest.mark.asyncio
  async def test_agentirc_subprocess_smoke(tmp_path: Path) -> None:
      port = _free_port()
      cfg = tmp_path / "server.yaml"
      cfg.write_text(f"name: smoke\nirc_port: {port}\nwebhook_port: 0\n")
      proc = subprocess.Popen(
          ["agentirc", "serve", "--config", str(cfg)],
          stdout=subprocess.PIPE,
          stderr=subprocess.PIPE,
      )
      try:
          await asyncio.sleep(1.0)  # give the daemon a beat to bind
          client = Client(host="127.0.0.1", port=port, nick="smoke")
          await client.connect()
          await client.privmsg("smoke", "hello")
          await client.disconnect()
      finally:
          proc.terminate()
          proc.wait(timeout=5)
  ```

  This is the *only* cross-repo integration test in culture — it imports nothing from agentirc internals; it just confirms the wire works.

- [ ] **Step 6.2:** Run it:

  ```bash
  bash .claude/skills/run-tests/scripts/test.sh -p -q tests/test_agentirc_smoke.py
  ```

---

## Task 7 — Delete the bundled IRCd

**Files:** `culture/agentirc/{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}.py`, `culture/agentirc/skills/`, `culture/agentirc/__main__.py`, `culture/agentirc/__init__.py`, `protocol/extensions/`, IRCd-internal tests under `tests/`.

- [ ] **Step 7.1:** `git rm` the IRCd source:

  ```bash
  git rm culture/agentirc/ircd.py
  git rm culture/agentirc/server_link.py
  git rm culture/agentirc/channel.py
  git rm culture/agentirc/events.py
  git rm culture/agentirc/room_store.py
  git rm culture/agentirc/thread_store.py
  git rm culture/agentirc/history_store.py
  git rm culture/agentirc/rooms_util.py
  git rm culture/agentirc/skill.py
  git rm -r culture/agentirc/skills/
  git rm culture/agentirc/__main__.py
  ```

- [ ] **Step 7.2:** Trim `culture/agentirc/__init__.py` to just the A1 re-export shim:

  ```python
  """A1-introduced re-export shim — kept through the 9.x line, removed in 10.0.0.

  All new code should import from `agentirc.config` directly.
  """
  from culture.agentirc.config import LinkConfig, ServerConfig, TelemetryConfig

  __all__ = ["LinkConfig", "ServerConfig", "TelemetryConfig"]
  ```

  This matches the actual A1 shim shipped in culture#309 (`culture/agentirc/config.py` re-exports exactly `LinkConfig`, `ServerConfig`, `TelemetryConfig` from `agentirc.config` — no `PeerSpec`; `agentirc.config` does not export a `PeerSpec` class as of 9.5, peer entries are represented as `LinkConfig` instances). Verify against the file before committing — don't speculate.

- [ ] **Step 7.3:** `git rm` `protocol/extensions/`:

  ```bash
  git rm -r protocol/extensions/
  ```

  These were already moved to agentirc's repo as part of Track B (see spec). The culture-side directory is dead weight.

- [ ] **Step 7.4:** Delete IRCd-internal tests. Survey:

  ```bash
  grep -rln 'from culture\.agentirc\.\(ircd\|server_link\|channel\|events\|room_store\|thread_store\|history_store\|rooms_util\|skill\)' tests/
  ```

  For each match, `git rm` the test file. They live in agentirc's repo now (per Track B's test-suite migration). Examples likely to appear: `tests/test_ircd_*.py`, `tests/test_server_link_*.py`, `tests/test_channel_*.py`, `tests/test_room_store_*.py`, etc.

  **Caveat:** preserve any test that survives because it covers culture's *use* of these modules, not the modules themselves. Such tests are rare after A2; if you find one, list it for review before deleting.

- [ ] **Step 7.5:** Run the full suite:

  ```bash
  bash .claude/skills/run-tests/scripts/test.sh -p -q
  ```

  Expected: green. The remaining tests cover transport, bots (now CAP clients), CLI, telemetry, and the new shim/smoke tests.

---

## Task 8 — Doc-test alignment + pre-push reviewer + version bump

**Files:** `culture/__init__.py`, `pyproject.toml`, `CHANGELOG.md`, plus any docs surfaced by the audit.

- [ ] **Step 8.1:** Run `doc-test-alignment`:

  ```bash
  Agent(subagent_type="doc-test-alignment", prompt="Audit the staged diff on feat/agentirc-extraction-cutover for new public surface and report missing docs/ or protocol/extensions/ coverage. Note that protocol/extensions/ was deleted in this PR — references to it in docs/ should be removed or pointed at agentirc's repo.")
  ```

  Address findings: update `docs/` references, point at agentirc's docs where appropriate, remove dead links.

- [ ] **Step 8.2:** Stage, then run `superpowers:code-reviewer`:

  ```bash
  git add -A
  ```

  Then invoke `superpowers:code-reviewer` per culture/CLAUDE.md's "Pre-push review for library/protocol code" rule. A3 is structural — deleting code, moving call sites, swapping a process boundary. The reviewer catches anything that breaks an exception-handling chain or leaves dangling imports.

- [ ] **Step 8.3:** `/version-bump major` (8.9.x → 9.0.0). A3 deletes in-tree code (~3,600 LOC of bundled IRCd) and finalizes the partial-passthrough shim — structural, even though user-visible CLI is unchanged.

- [ ] **Step 8.4:** Edit the new `[9.0.0]` section of `CHANGELOG.md`:

  ```markdown
  ### Changed (breaking)
  - The bundled IRCd in `culture/agentirc/{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}.py` and `culture/agentirc/skills/` is gone. `culture server <verb>` is now a partial passthrough into the installed `agentirc-cli` binary for lifecycle verbs (`start`/`stop`/`restart`/`status`/`serve`/`link`/`logs`/`version`); culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) continue to dispatch to culture's own handlers. User-visible behavior is unchanged for every existing verb.
  - `python -m culture.agentirc` is removed (no known callers). Use `agentirc` (CLI) or `python -m agentirc`.
  - `protocol/extensions/` moved to agentirc's repo.
  - `culture/agentirc/{client,remote_client}.py` moved to `culture/transport/`. Code that imported them under the old path needs to update imports — `culture/bots/*` and `culture/clients/*/daemon.py` are already updated.

  ### Kept (transitional)
  - `culture/agentirc/config.py` remains as a re-export shim over `agentirc.config` through the 9.x line; remove in 10.0.0.

  ### Notes
  - Final phase of the agentirc extraction. A1 (config dataclasses, 8.8.0, #309) and A2 (bot framework rewrite, 8.9.0) preceded this. Spec: `docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md`.
  ```

- [ ] **Step 8.5:** Final test pass + format check:

  ```bash
  bash .claude/skills/run-tests/scripts/test.sh -p -q
  uv run black culture/ tests/
  uv run isort culture/ tests/
  ```

- [ ] **Step 8.6:** Commit and push:

  ```bash
  git commit -m "$(cat <<'EOF'
  feat!: delete bundled IRCd; shim culture server to agentirc-cli (Track A3)

  Final phase of the agentirc extraction. Deletes culture/agentirc/{ircd,
  server_link,channel,events,...} and protocol/extensions/. Moves
  client.py + remote_client.py to culture/transport/. Replaces
  culture/cli/server.py:_run_server with a passthrough into
  agentirc.cli.dispatch. culture/agentirc/config.py stays as the A1
  re-export shim through 9.x.

  BREAKING CHANGE: python -m culture.agentirc is gone. User-visible
  culture server <verb> behavior is unchanged.

  - Claude

  Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  EOF
  )"
  git push -u origin feat/agentirc-extraction-cutover
  ```

- [ ] **Step 8.7:** Open the PR:

  ```bash
  gh pr create --title "Phase A3: delete bundled IRCd; shim culture server to agentirc-cli" --body "$(cat <<'EOF'
  ## Summary

  - Delete `culture/agentirc/{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}.py` and `culture/agentirc/skills/` — the bundled IRCd is gone. Final phase of the agentirc extraction.
  - `git mv culture/agentirc/{client,remote_client}.py → culture/transport/` (preserves blame). Update importers across `culture/bots/*` and `culture/clients/*/daemon.py`.
  - Replace `culture/cli/server.py:_run_server` with a partial-passthrough shim: lifecycle verbs (`start`/`stop`/`restart`/`status`/`serve`/`link`/`logs`/`version`) forward into `agentirc.cli.dispatch`, while culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) keep dispatching to culture's own handlers (now in `culture/cli/server_local.py`). New agentirc verbs reach `culture server <verb>` automatically; culture's own verbs are not affected.
  - Delete `protocol/extensions/` (lives in agentirc's repo now).
  - Keep `culture/agentirc/config.py` as the A1 re-export shim through 9.x; removed in 10.0.0.
  - `8.9.x → 9.0.0` (major — structural change, even though user-visible CLI is unchanged).

  ## Why

  Final phase of the agentirc extraction (spec: `docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md`). A1 (config dataclasses, #309) and A2 (bot framework rewrite, see Track A2 plan) preceded this; A3 completes the cutover.

  ## Test plan

  - [x] `bash .claude/skills/run-tests/scripts/test.sh -p -q` — full suite green
  - [x] New `tests/test_server_shim.py` asserts every verb in `agentirc --help` is reachable via `culture server <verb> --help` with byte-identical output
  - [x] New `tests/test_agentirc_smoke.py` boots `agentirc serve` as a subprocess and TCP-connects via `culture.transport.Client`
  - [x] Pre-commit hooks clean
  - [x] `superpowers:code-reviewer` clean
  - [x] `doc-test-alignment` clean

  ## Migration notes for users

  - `python -m culture.agentirc` is gone. Run `agentirc` (CLI) or `python -m agentirc` instead.
  - Code that imports from `culture.agentirc.{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}` is broken. There is no replacement in culture; depend on `agentirc-cli` and import from `agentirc.*`.
  - `culture.agentirc.{client,remote_client}` imports — update to `culture.transport.{client,remote_client}`.
  - `culture.agentirc.config` imports — still work; remove in 10.0.0.

  - Claude

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

- [ ] **Step 8.8:** Wait for CI:

  ```bash
  gh pr checks
  ```

  If anything fails, fix inline, push, run `/sonarclaude` before declaring ready (per culture/CLAUDE.md). Use the `cicd` skill for automated reviewer comments.

---

## Summary

- Deletes `culture/agentirc/{ircd,server_link,channel,events,room_store,thread_store,history_store,rooms_util,skill}.py` + `skills/` (~3,600 LOC). Final structural change of the agentirc extraction.
- Moves `client.py` + `remote_client.py` to `culture/transport/` (`git mv` preserves blame).
- Replaces `culture/cli/server.py:_run_server` with a passthrough into `agentirc.cli.dispatch`.
- Deletes `protocol/extensions/` (lives in agentirc's repo).
- Keeps `culture/agentirc/config.py` as the A1 re-export shim through 9.x.
- Major version bump 8.9.x → 9.0.0.

User-visible behavior unchanged: every `culture server <verb> <args>` invocation continues to work. Lifecycle verbs (`start`/`stop`/`restart`/`status`/`serve`/`link`/`logs`/`version`) get their flags / output / exit codes from `agentirc-cli` via the shim; culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) keep their existing culture-side implementations. On-disk footprint (config path, sockets, systemd units, logs) unchanged.

Spec: `docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md`.

## Test plan

- [ ] Existing test suite passes (`bash .claude/skills/run-tests/scripts/test.sh -p -q`).
- [ ] New `tests/test_server_shim.py` asserts every agentirc-owned verb is reachable with byte-identical help via `culture server <verb> --help`, and that culture-owned verbs (`default`/`rename`/`archive`/`unarchive`) continue to dispatch locally rather than being shimmed to agentirc.
- [ ] New `tests/test_agentirc_smoke.py` boots `agentirc serve` as a subprocess and TCP-connects to it.
- [ ] Post-merge: verify on the spark host that `culture-agent-spark-culture.service` restarts cleanly with the new release; `culture server status` and `agentirc status` produce identical output (Track C verification).

---

## Post-merge (Track C — out of scope for this plan, but on the to-do list)

After this PR merges and a culture release is published:

- `pip install -U culture` on the spark host.
- Confirm `culture-agent-spark-culture.service` restarts cleanly.
- Confirm `culture server status` and `agentirc status` produce byte-identical output.
- Confirm an existing peer link still establishes (`culture server link` → other peer responds).
- Optionally: schedule a follow-up agent in 6 months to drop `culture/agentirc/config.py` (and the surrounding shim machinery) when culture cuts a 10.0.0 release.
