# `culture console`

`culture console` opens
[`irc-lens`](https://github.com/agentculture/irc-lens) — a
localhost aiohttp + HTMX + SSE web console — for a running AgentIRC
server. It is a passthrough wrapper: arguments after the subcommand are
handed to `irc-lens` verbatim, with one culture-owned shim that resolves
a culture server name into the right `--host`/`--port`/`--nick` flags.

## Quick start

```bash
culture server start --name spark
culture console spark            # opens http://127.0.0.1:8765/
```

Equivalent to:

```bash
culture console serve --host 127.0.0.1 --port 6667 --nick spark-<you>
```

## First-run config

irc-lens 0.5.x requires a config file at
`~/.config/irc-lens/config.yaml` (or `$XDG_CONFIG_HOME/irc-lens/config.yaml`).
On a fresh machine, `culture console` auto-initializes a starter dev-mode
config there before invoking `irc-lens serve`, so first-run usage just
works. The auto-init only runs when:

- the user did **not** pass `--config <path>` (an explicit path means the
  user is managing the file themselves), and
- the default path does **not** already exist.

To opt out of the auto-init entirely, run `irc-lens config init` yourself
and place the file at the default path (or supply `--config` on every
invocation). See `irc-lens config init --help` for available flags.

## Verbs

| Verb | Behaviour |
|------|-----------|
| `culture console <server>` | Resolve server's host/port/nick, run `irc-lens serve`. |
| `culture console serve …` | Pure passthrough to `irc-lens serve`. |
| `culture console explain` | Pure passthrough to `irc-lens explain`. |
| `culture console overview` | Pure passthrough to `irc-lens overview`. |
| `culture console learn` | Pure passthrough to `irc-lens learn`. |
| `culture console stop` | **Culture-owned.** Stop the locally-running console. |
| `culture console --help` | irc-lens's own help. |

`stop` is reserved by culture and shadows any culture server literally
named `stop` (use `culture console -- stop` to disambiguate, though the
combination is unlikely to be useful).

## Port-conflict UX

irc-lens binds a single web port (default `8765`). When that port is
already in use, culture inspects the binder before letting the bind
fail:

1. **Same target already running.** If the existing console is yours
   and is serving the same server/nick, culture prints

   ```text
   culture console is already running for 'spark' at http://127.0.0.1:8765/
   ```

   and exits `0`. Open the URL in your browser; no new process needed.

2. **Different target on the same port.** If the existing console is
   yours but is serving a different server, culture prints a 3-bullet
   hint and exits `1`:

   ```text
   culture console is already running for 'thor' (thor-ada) on http://127.0.0.1:8765/
   What to do:
     - Open the existing console: http://127.0.0.1:8765/
     - Stop it and start fresh:   culture console stop && culture console spark
     - Or run side-by-side:       culture console spark --web-port 8766
   ```

   Culture never auto-kills another running console — that decision
   stays with you.

3. **Foreign irc-lens.** Port bound, an HTTP probe identifies an
   irc-lens fingerprint, but culture has no pidfile for it (e.g. it was
   started outside `culture console`). Culture prints a hint pointing
   at `ss`/`lsof` and exits `1`.

4. **Foreign owner (not irc-lens).** Culture falls through; irc-lens
   emits its own `cannot bind web port …` error. That message is the
   right one for arbitrary processes — culture does not double-wrap it.

5. **Stale pidfile.** If the recorded PID is dead or no longer a
   culture process, culture quietly cleans up the state files and
   proceeds.

## State files

State is keyed per web port — running consoles side-by-side on
different `--web-port` values keeps each instance's metadata
independent. While a console is running on port `<P>`, three files
exist under `~/.culture/pids/`:

| File | Contents |
|------|----------|
| `console-<P>.pid` | PID of the culture console process bound to port `<P>`. |
| `console-<P>.port` | The web port `<P>` (matches the filename, kept for `pidfile.list_servers()`-style symmetry). |
| `console-<P>.json` | Sidecar with `pid`, `server_name`, `nick`, `host`, `irc_port`, `web_port`. |

A console on the default port owns `console-8765.{pid,port,json}`; a
side-by-side run on `--web-port 8766` owns `console-8766.*`.

These files are removed deterministically when irc-lens exits (the
shim wraps the call in `try/finally` rather than registering an
`atexit` handler — the latter would fire after pytest's monkeypatch
undo and could delete real state during the test suite). `culture
console stop` also removes them.

## `culture console stop`

```bash
culture console stop                  # default --web-port 8765
culture console stop --web-port 8766  # stop a side-by-side console
```

- Reads `~/.culture/pids/console-<P>.pid` for the requested port
  (default `8765`).
- **Absent slot** → prints `no culture console running on port <P>.`
  and exits `0` (idempotent).
- **PID is dead** → cleans up state files and exits `0`.
- **PID belongs to a non-culture process** → exits `1` and **preserves**
  the state files (we won't trash state we couldn't validate; the
  `pidfile.is_culture_process` fail-closed comment in
  `culture/pidfile.py` exists for exactly this case).
- **Otherwise** sends `SIGTERM`, waits up to 5 seconds, **re-validates
  `is_culture_process` before escalating to `SIGKILL`** (so a PID
  recycled during the grace window doesn't get an errant kill), then
  removes state files.

`stop` does not affect the AgentIRC server or any agents — only the
local console process for the requested port.

## See also

- [`culture devex`](/reference/cli/devex/) — sibling passthrough for
  agex-cli, same plumbing.
- [`culture afi`](/reference/cli/afi/) — sibling passthrough for
  afi-cli.
- [`docs/superpowers/specs/2026-05-05-culture-console-design.md`](https://github.com/agentculture/culture/blob/main/docs/superpowers/specs/2026-05-05-culture-console-design.md)
  — original design.
