# wc-agent CLI Redesign — Project System + Two-Level Subcommands

**Date**: 2026-03-26
**Scope**: Restructure wc-agent CLI with multi-project support, Typer-based `project`/`irc`/`agent` subgroups, interactive config generation, manual test guide.

## Overview

Redesign the wc-agent CLI to support a clean manual testing workflow. Key changes:
- **Project system**: `~/.wc-agent/projects/<name>/` — each project is an independent IRC environment with its own config and agent state
- **Two-level subcommands**: `project`, `irc`, `agent` groups using Typer
- **Interactive config**: `project create` prompts for IRC server/nick/channels, generates config
- **No static config in project root**: `weechat-claude.toml` is replaced by per-project config in `~/.wc-agent/`

## Command Structure

```
# Project management
wc-agent project create <name>         # Interactive setup: IRC server, nick, channels
wc-agent project list                  # List all projects
wc-agent project use <name>            # Set default project
wc-agent project remove <name>         # Remove project config + state
wc-agent project show [name]           # Show project config

# IRC infrastructure
wc-agent irc daemon start              # Start ergo IRC server
wc-agent irc daemon stop               # Stop ergo
wc-agent irc start [--nick <n>]        # Start WeeChat in tmux + auto-connect IRC
wc-agent irc stop                      # Stop WeeChat (/quit)
wc-agent irc status                    # Show IRC server + client status

# Agent lifecycle
wc-agent agent create <name>           # Create agent (claude + channel-server)
  [--workspace <path>] [--channels <c>]
wc-agent agent stop <name>             # Stop agent
wc-agent agent list                    # List all agents with status
wc-agent agent status <name>           # Single agent details
wc-agent agent send <name> <text>      # Send text to agent's tmux pane
wc-agent agent restart <name>          # Restart agent

# Convenience
wc-agent shutdown                      # Stop all agents + WeeChat + ergo

# Global option (all commands)
wc-agent --project <name> <cmd>        # Specify project (overrides auto-detection)
```

## Project System

### Directory Structure

```
~/.wc-agent/
  default                              # Contains default project name (set by `project use`)
  projects/
    my-team/
      config.toml                      # Generated by `project create`
      state.json                       # Runtime state (agents, panes)
    local-dev/
      config.toml
      state.json
```

### Project Resolution Order

1. `--project my-team` — explicit CLI argument
2. `.wc-agent` file in current directory or parent directories — contains project name (like `.git`)
3. `~/.wc-agent/default` file — set by `wc-agent project use`
4. None found → error: `No project selected. Run 'wc-agent project create <name>' or use '--project <name>'.`

### `project create` Interactive Flow

```
$ wc-agent project create my-team
Creating project 'my-team':
  IRC server [127.0.0.1]: irc.libera.chat
  IRC port [6667]: 6697
  TLS [false]: true
  Password []:
  Nickname [alice]: alice
  Default channels [#general]: #my-team,#general

Project 'my-team' created at ~/.wc-agent/projects/my-team/
Config saved to ~/.wc-agent/projects/my-team/config.toml
```

### Generated config.toml

```toml
[irc]
server = "irc.libera.chat"
port = 6697
tls = true
password = ""

[agents]
default_channels = ["#my-team", "#general"]
username = "alice"
```

### CLI Parameter Priority

For parameters that exist in both config and CLI:
1. CLI argument (e.g., `--nick bob`) — highest priority, does not modify config
2. Config value (e.g., `username = "alice"`) — default
3. System fallback (e.g., `$USER`) — if config value is empty

## Pane Lifecycle

All tmux panes (WeeChat, agents) are managed passively:

- **Create**: `irc start` / `agent create` spawn panes, record `pane_id` in `state.json`
- **Detect**: `agent list/status/send` check if pane exists (`tmux list-panes`)
  - If pane gone → mark `offline` in state, report to user
- **Recover**: User must manually `agent create` again — no auto-restart
- **Cleanup**: `agent stop` sends `/exit` to pane, cleans workspace, updates state. If pane already gone, just cleans state.

## Deletion of `weechat-claude.toml`

The project-root `weechat-claude.toml` is no longer needed. Config lives in `~/.wc-agent/projects/<name>/config.toml`. The old file will be deleted from the repo.

## Migration from Current CLI

| Current | New | Change |
|---------|-----|--------|
| `weechat-claude.toml` (static file) | `wc-agent project create` → `~/.wc-agent/projects/<name>/config.toml` | Config moved to project dir, generated interactively |
| `wc-agent --config <path>` | `wc-agent --project <name>` | Config path → project name |
| `wc-agent start` | `wc-agent irc daemon start` + `wc-agent irc start` + `wc-agent agent create agent0` | Split into 3 independent commands |
| `wc-agent create <n>` | `wc-agent agent create <n>` | Move to `agent` group |
| `wc-agent stop <n>` | `wc-agent agent stop <n>` | Move to `agent` group |
| `wc-agent list` | `wc-agent agent list` | Move to `agent` group |
| `wc-agent status <n>` | `wc-agent agent status <n>` | Move to `agent` group |
| `wc-agent restart <n>` | `wc-agent agent restart <n>` | Move to `agent` group |
| `wc-agent shutdown` | `wc-agent shutdown` | Unchanged |
| — | `wc-agent project create/list/use/remove/show` | **New** |
| — | `wc-agent irc daemon start/stop` | **New** |
| — | `wc-agent irc start/stop/status` | **New** |
| — | `wc-agent agent send <name> <text>` | **New** |

## Cross-File Impact

### `server.py` `_handle_create_agent` MCP tool

**Current**: Calls `wc-agent/cli.py` as subprocess: `[sys.executable, cli_path, "create", name]`
**Change**: Replace with direct import of `AgentManager`. No CLI subprocess — use the Python library directly since channel-server can import it.

```python
async def _handle_create_agent(arguments: dict) -> list[TextContent]:
    from wc_agent.agent_manager import AgentManager
    # Build manager from env vars (IRC_SERVER, etc. are already available)
    # Call manager.create(scoped_name) directly
```

This eliminates: CLI path resolution, subprocess overhead, Typer dependency in channel-server.

### `start.sh`

**Current**: `python3 wc-agent/cli.py --config $CONFIG start --workspace $WORKSPACE`
**Change**: Three separate commands with project context:
```bash
wc-agent --project local irc daemon start
wc-agent --project local irc start
wc-agent --project local agent create agent0 --workspace "$WORKSPACE"
```

Or simpler if default project is set: `wc-agent irc daemon start && wc-agent irc start && wc-agent agent create agent0`

### `stop.sh`

**Current**: `python3 wc-agent/cli.py --config $CONFIG shutdown`
**Change**: `wc-agent shutdown` (uses project resolution to find config).

### `weechat-claude.toml`

**Delete** from project root. Config now lives in `~/.wc-agent/projects/<name>/config.toml`.

### E2E tests (`e2e-test.sh`, `e2e-test-manual.sh`, `helpers.sh`)

All `wc-agent` invocations need `irc`/`agent` subgroup prefix:
- `create agent1` → `agent create agent1`
- `stop agent1` → `agent stop agent1`
- `list` → `agent list`
- `start` → `irc daemon start` + `irc start` + `agent create agent0`

### Unit tests

- `test_channel_server_irc.py`: Remove `test_create_agent_tool_cli_path` (no longer uses CLI path)
- `test_agent_manager.py`: Tests remain valid — AgentManager API unchanged

## Implementation: Typer

```python
import typer

app = typer.Typer(name="wc-agent", help="Claude Code agent lifecycle management")
project_app = typer.Typer(name="project", help="Project configuration management")
irc_app = typer.Typer(name="irc", help="IRC server and client management")
irc_daemon_app = typer.Typer(name="daemon", help="Local ergo IRC server")
agent_app = typer.Typer(name="agent", help="Claude Code agent lifecycle")

app.add_typer(project_app, name="project")
app.add_typer(irc_app, name="irc")
irc_app.add_typer(irc_daemon_app, name="daemon")
app.add_typer(agent_app, name="agent")
```

### project commands

```python
WC_AGENT_DIR = os.path.expanduser("~/.wc-agent")

@project_app.command("create")
def project_create(name: str):
    """Create a new project with interactive config setup."""
    project_dir = os.path.join(WC_AGENT_DIR, "projects", name)
    if os.path.exists(project_dir):
        typer.echo(f"Project '{name}' already exists.")
        raise typer.Exit(1)
    os.makedirs(project_dir, exist_ok=True)
    # Interactive prompts
    server = typer.prompt("IRC server", default="127.0.0.1")
    port = typer.prompt("IRC port", default=6667, type=int)
    tls = typer.confirm("TLS", default=False)
    password = typer.prompt("Password", default="", hide_input=True)
    nick = typer.prompt("Nickname", default=os.environ.get("USER", "user"))
    channels = typer.prompt("Default channels", default="#general")
    # Write config.toml
    # ...

@project_app.command("list")
def project_list():
    """List all projects."""

@project_app.command("use")
def project_use(name: str):
    """Set default project."""
    # Write name to ~/.wc-agent/default

@project_app.command("remove")
def project_remove(name: str):
    """Remove a project and its state. Refuses if agents are running."""
    # Check state.json for running agents → error if any
    # Otherwise: shutil.rmtree(project_dir)

@project_app.command("show")
def project_show(name: Optional[str] = typer.Argument(None)):
    """Show project config."""
```

### irc daemon commands

```python
@irc_daemon_app.command("start")
def irc_daemon_start():
    """Start local ergo IRC server."""
    # Read config → check if local
    # mkdir -p $ERGO_DATA_DIR
    # cd $ERGO_DATA_DIR && ergo run --conf <ergo.yaml> &
    # Save PID to state file

@irc_daemon_app.command("stop")
def irc_daemon_stop():
    """Stop local ergo IRC server."""
    # pkill -x ergo
```

### irc client commands

```python
@irc_app.command("start")
def irc_start():
    """Start WeeChat in tmux, auto-connect to IRC."""
    # Create/reuse tmux session
    # tls_flag = "" if config tls else "-notls"
    # tmux send-keys: weechat -r '/server add wc-local <server>/<port> {tls_flag} -nicks=<nick>; /connect; /join #general'
    # Save weechat pane_id to state

@irc_app.command("stop")
def irc_stop():
    """Stop WeeChat."""
    # tmux send-keys -t <pane> /quit Enter

@irc_app.command("status")
def irc_status():
    """Show IRC server and client status."""
    # Check ergo process → running/stopped
    # Check weechat pane → running/stopped
    # Print summary
```

### agent commands

```python
@agent_app.command("create")
def agent_create(name: str, workspace: str = None, channels: str = typer.Option(None, help="Comma-separated channels to join")):
    """Create and launch a new agent."""
    # AgentManager.create(name, workspace)

@agent_app.command("stop")
def agent_stop(name: str):
    """Stop a running agent."""
    # AgentManager.stop(name)

@agent_app.command("list")
def agent_list():
    """List all agents with status."""
    # AgentManager.list_agents()

@agent_app.command("status")
def agent_status(name: str):
    """Show detailed info for a single agent."""
    # AgentManager.get_status(name)

@agent_app.command("send")
def agent_send(name: str, text: str):
    """Send text to agent's tmux pane (tmux send-keys)."""
    # Look up pane_id from state
    # subprocess: tmux send-keys -t <pane> <text> Enter

@agent_app.command("restart")
def agent_restart(name: str):
    """Restart an agent (stop + create with same config)."""
    # AgentManager.restart(name)
```

### shutdown command

```python
# Typer >=0.9 supports mixing @app.command() with add_typer()
@app.command("shutdown")
def shutdown(ctx: typer.Context):
    """Stop all agents + WeeChat + ergo."""
    # agent_manager: stop all agents
    # irc_stop(): send /quit to WeeChat pane
    # irc_daemon_stop(): pkill ergo
```

### Global options

```python
@app.callback()
def main(
    ctx: typer.Context,
    project: str = typer.Option(None, help="Project name (overrides auto-detection)"),
    tmux_session: str = typer.Option("weechat-claude", help="tmux session name"),
):
    """Claude Code agent lifecycle management."""
    ctx.ensure_object(dict)
    # Resolve project: --project > .wc-agent file > ~/.wc-agent/default
    resolved = project or resolve_project_from_cwd() or resolve_default_project()
    if resolved and ctx.invoked_subcommand not in ("project",):
        # Load project config
        ctx.obj["project"] = resolved
        ctx.obj["config"] = load_project_config(resolved)
    ctx.obj["tmux_session"] = tmux_session


def resolve_project_from_cwd() -> str | None:
    """Walk up from cwd looking for .wc-agent file containing project name."""
    path = os.getcwd()
    while path != "/":
        marker = os.path.join(path, ".wc-agent")
        if os.path.isfile(marker):
            return open(marker).read().strip()
        path = os.path.dirname(path)
    return None


def resolve_default_project() -> str | None:
    """Read ~/.wc-agent/default."""
    default_file = os.path.join(WC_AGENT_DIR, "default")
    if os.path.isfile(default_file):
        return open(default_file).read().strip()
    return None
```

## Idempotency

All start commands are idempotent:
- `irc daemon start`: if ergo already running (`pgrep -x ergo`), print status and return
- `irc start`: if WeeChat pane already exists and alive, print status and return
- `agent create <name>`: if agent already running, error

All stop commands are idempotent:
- `irc daemon stop`: if ergo not running, print "not running" and return
- `irc stop`: if WeeChat pane not found, print "not running" and return
- `agent stop <name>`: if agent already offline, error

## Agent Name Scoping in server.py

When `server.py` directly imports `AgentManager`, it must pass the already-scoped name to avoid double-scoping:

```python
# In server.py _handle_create_agent:
from wc_agent.agent_manager import AgentManager

# Factory method builds from env vars available in channel-server
mgr = AgentManager.from_env()
# from_env() reads: IRC_SERVER, IRC_PORT, IRC_TLS, IRC_CHANNELS,
#   AGENT_NAME (for username extraction), WC_TMUX_SESSION
# channel_server_dir derived from __file__
# state_file derived from WC_PROJECT_DIR env var

scoped = scoped_name(name, username)
mgr.create(scoped, ...)
```

Note: `AgentManager._write_mcp_json()` must also set `WC_PROJECT_DIR` in the child agent's `.mcp.json` env, pointing to the project directory so child agents know where state lives.

## Entry Point

Add to `wc-agent/pyproject.toml` (new file):

```toml
[project]
name = "wc-agent"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["typer[all]>=0.9.0"]

[project.scripts]
wc-agent = "wc_agent.cli:app"
```

For development, run directly: `python3 wc-agent/cli.py` (Typer supports `if __name__ == "__main__": app()`)

## State File Migration

The current `~/.local/state/wc-agent/agents.json` will be replaced by `state.json` with the new schema. No migration — clean start required. First run of new CLI creates the file fresh. Document in README.

## State File

`~/.wc-agent/projects/<name>/state.json`:

```json
{
  "irc": {
    "daemon_pid": 12345,
    "weechat_pane_id": "%5"
  },
  "agents": {
    "alice-agent0": {
      "workspace": "/tmp/wc-agent-alice_agent0",
      "pane_id": "%7",
      "status": "running",
      "created_at": 1711276800,
      "channels": ["#general"]
    }
  }
}
```

Merged from current separate `agents.json` — now includes IRC state too.

### State File Schema Change

`AgentManager._load_state` must read from `data["agents"]`, `_save_state` must write `{"irc": {...}, "agents": {...}}`.
The `"irc"` key is written by `irc_manager.py` (daemon pid, weechat pane_id).
`AgentManager` only reads/writes the `"agents"` key.

## Command Output Examples

### `wc-agent irc status`

```
IRC Server:
  type:   local (ergo)
  status: running (pid 12345)
  listen: 127.0.0.1:6667

IRC Client (WeeChat):
  status: running
  pane:   %5
  nick:   alice
```

### `wc-agent agent list`

```
NAME             STATUS   UPTIME  PANE  CHANNELS    WORKSPACE
alice-agent0     running  12m     %7    #general    /tmp/wc-agent-alice_agent0
alice-helper     running  3m      %9    #general    /tmp/wc-agent-alice_helper
```

### `wc-agent agent send agent0 "hello"`

```
Sent to alice-agent0 (pane %7)
```

## Dependencies

Add to `wc-agent` (new pyproject.toml or inline script deps):
- `typer[all]>=0.9.0` — CLI framework

## Files to Create/Modify

### Create
- `docs/e2e-manual-test.md` — Manual test guide
- `wc-agent/project.py` — Project management (create, list, use, remove, show, resolve)
- `wc-agent/irc_manager.py` — IRC daemon/WeeChat pane management
- `wc-agent/pyproject.toml` — Package with Typer dependency + entry point

### Modify
- `wc-agent/cli.py` — Full rewrite: argparse → Typer two-level subcommands
- `wc-agent/agent_manager.py` — Add `send()` method, use project-relative state file
- `wc-agent/config.py` — Read from project dir instead of static path
- `weechat-channel-server/server.py` — `_handle_create_agent`: direct import AgentManager
- `tests/e2e/e2e-test.sh` — Update to new command format
- `tests/e2e/e2e-test-manual.sh` — Simplify using new CLI
- `tests/e2e/helpers.sh` — Update `wc_agent` helper
- `start.sh` / `stop.sh` — Update to new command format

### Delete
- `weechat-claude.toml` — Replaced by per-project config in `~/.wc-agent/`

## Manual Test Guide

`docs/e2e-manual-test.md`:

```markdown
# E2E Manual Test Guide

## Prerequisites

- ergo installed: `~/.local/bin/ergo`
- ergo languages: `~/.local/share/ergo/languages/`
- tmux installed
- wc-agent installed (or run via `python3 wc-agent/cli.py`)

## Step-by-Step

### 0. Create Project (first time only)

    $ wc-agent project create local-test
    Creating project 'local-test':
      IRC server [127.0.0.1]:
      IRC port [6667]:
      TLS [false]:
      Password []:
      Nickname [alice]: alice
      Default channels [#general]:
    Project 'local-test' created.

    $ wc-agent project use local-test

### 1. Start IRC Server

    $ wc-agent irc daemon start
    Starting ergo IRC server...
    ergo running (pid 12345).

### 2. Start WeeChat

    $ wc-agent irc start
    Starting WeeChat in tmux...
    WeeChat started in pane %5.

Attach to observe (iTerm2):

    $ tmux -CC attach -t weechat-claude

Or standard tmux:

    $ tmux attach -t weechat-claude

### 3. Verify IRC Status

    $ wc-agent irc status

Should show both server and client running.

### 4. Create Primary Agent

    $ wc-agent agent create agent0

In WeeChat, you should see `alice-agent0` join #general.

### 5. Test Agent Reply

    $ wc-agent agent send agent0 'Use the reply MCP tool to send "Hello everyone, agent0 here!" to #general'

Watch WeeChat #general for the message.

### 6. Test @mention

In WeeChat #general, type:

    @alice-agent0 what is the capital of France?

Agent should auto-respond in the channel.

### 7. Check Agent Status

    $ wc-agent agent list
    $ wc-agent agent status agent0

### 8. Create Second Agent

    $ wc-agent agent create helper
    $ wc-agent agent list

Note: `agent create helper` produces `alice-helper` on IRC (username prefix from config).

### 9. Agent-to-Agent Communication

    $ wc-agent agent send agent0 'Use the reply tool to send "hello helper, please respond with PONG" to "alice-helper"'

### 10. Stop Agents

    $ wc-agent agent stop helper
    $ wc-agent agent list

### 11. Shutdown Everything

    $ wc-agent shutdown

Verify: no ergo process, no tmux session, no agent panes.
```
