# TOML Help System Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace hardcoded help text with TOML-driven help topics in `content/help/`, and add `@help` admin commands for creating, editing, and removing help topics in-game.

**Architecture:** Help topics are TOML files in `content/help/` loaded at startup into a `_help_topics` dict. The `help` command checks topics before falling back to command detail. `@help` admin commands provide a guided creation/editing flow, using the existing Editor for body text only (not raw TOML). Metadata fields are prompted inline.

**Tech Stack:** Python 3.12+, tomllib for reading, tomli_w for writing TOML, existing Editor class for body editing.

---

## Design

### TOML format (`content/help/<name>.toml`)

```toml
name = "zones"
title = "zone building guide"
admin = false
body = """
zones are spatial containers - rooms, dungeons, overworld areas.
each zone has a terrain grid, spawn point, and optional portals.

  listing zones
    @zones              list all registered zones

  navigating
    @goto <zone>        teleport to a zone's spawn point
    enter <portal>      step through a portal to another zone

  building
    @dig <name> <w> <h> create a new blank zone
    @paint              toggle paint mode for terrain editing
    @save               save current zone to file

  see: @zones, @goto, @dig, @paint, @save
"""
```

Fields:
- `name` (required) — topic name, matches filename stem
- `title` (optional) — short description shown in listings, defaults to name
- `admin` (optional) — if true, only admins can view. defaults to false
- `body` (required) — the help text, multiline string

### How `help <topic>` resolution works

1. Check `_help_topics` dict for exact match
2. If found and `admin=True`, check player.is_admin
3. If found, display title + body
4. If not found, fall through to existing command detail logic

### `@help` subcommands

- `@help` — list all help topics (name + title, admin-only topics marked)
- `@help create` — guided creation: prompts for name, title, admin flag, then opens editor for body
- `@help edit <topic>` — shows current metadata (enter to keep), opens editor for body
- `@help remove <topic>` — deletes topic file + removes from dict (with confirmation)

### Editor integration

The editor opens with ONLY the body text. When saved, the `@help` command
reassembles the full TOML and writes it. The player never sees TOML syntax.

---

## Task 1: Help topic dataclass and loader

**Files:**
- Modify: `src/mudlib/content.py` — add `HelpTopic` dataclass and `load_help_topics()`

**Step 1: Write the failing test**

Create `tests/test_help_topics.py`:

```python
"""Tests for TOML help topic loading."""

import textwrap
from pathlib import Path

import pytest

from mudlib.content import HelpTopic, load_help_topics


@pytest.fixture
def help_dir(tmp_path):
    """Create a temp directory with sample help TOML files."""
    topic = tmp_path / "combat.toml"
    topic.write_text(textwrap.dedent("""\
        name = "combat"
        title = "combat primer"
        body = \"\"\"
        combat is initiated when you attack another entity.
        use skills to learn your available moves.
        \"\"\"
    """))

    admin_topic = tmp_path / "building.toml"
    admin_topic.write_text(textwrap.dedent("""\
        name = "building"
        title = "builder's guide"
        admin = true
        body = \"\"\"
        use @dig to create zones and @paint to edit terrain.
        \"\"\"
    """))
    return tmp_path


def test_load_help_topics(help_dir):
    topics = load_help_topics(help_dir)
    assert "combat" in topics
    assert "building" in topics


def test_help_topic_fields(help_dir):
    topics = load_help_topics(help_dir)
    combat = topics["combat"]
    assert combat.name == "combat"
    assert combat.title == "combat primer"
    assert combat.admin is False
    assert "combat is initiated" in combat.body


def test_help_topic_admin_flag(help_dir):
    topics = load_help_topics(help_dir)
    building = topics["building"]
    assert building.admin is True


def test_help_topic_title_defaults_to_name(tmp_path):
    topic = tmp_path / "simple.toml"
    topic.write_text('name = "simple"\nbody = "just a test"\n')
    topics = load_help_topics(tmp_path)
    assert topics["simple"].title == "simple"


def test_load_help_topics_empty_dir(tmp_path):
    topics = load_help_topics(tmp_path)
    assert topics == {}


def test_load_help_topics_skips_bad_files(tmp_path):
    bad = tmp_path / "broken.toml"
    bad.write_text("not valid toml [[[")
    good = tmp_path / "good.toml"
    good.write_text('name = "good"\nbody = "works"\n')
    topics = load_help_topics(tmp_path)
    assert "good" in topics
    assert "broken" not in topics
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_help_topics.py -v`
Expected: ImportError — `HelpTopic` and `load_help_topics` don't exist yet.

**Step 3: Write minimal implementation**

Add to `src/mudlib/content.py`:

```python
@dataclass
class HelpTopic:
    """A help topic loaded from a TOML file."""

    name: str
    body: str
    title: str = ""
    admin: bool = False

    def __post_init__(self):
        if not self.title:
            self.title = self.name


def load_help_topic(path: Path) -> HelpTopic:
    """Load a single help topic from a TOML file."""
    with open(path, "rb") as f:
        data = tomllib.load(f)

    return HelpTopic(
        name=data["name"],
        body=data["body"].strip(),
        title=data.get("title", ""),
        admin=data.get("admin", False),
    )


def load_help_topics(directory: Path) -> dict[str, HelpTopic]:
    """Load all help topics from a directory of TOML files."""
    topics: dict[str, HelpTopic] = {}
    for path in directory.glob("*.toml"):
        try:
            topic = load_help_topic(path)
            topics[topic.name] = topic
        except Exception as e:
            log.warning("failed to load help topic from %s: %s", path, e)
    return topics
```

Also add `from dataclasses import dataclass` to imports in content.py.

**Step 4: Run test to verify it passes**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add HelpTopic dataclass and TOML loader`

---

## Task 2: Wire help topics into the help command

**Files:**
- Modify: `src/mudlib/commands/help.py` — add topic lookup, topic listing
- Modify: `src/mudlib/server.py` — load help topics at startup

**Step 1: Write the failing tests**

Add to `tests/test_help_topics.py`:

```python
from unittest.mock import AsyncMock, MagicMock

from mudlib import commands
from mudlib.commands import help as help_mod  # noqa: F401
from mudlib.commands.help import _help_topics


@pytest.fixture
def mock_writer():
    writer = MagicMock()
    writer.write = MagicMock()
    writer.drain = AsyncMock()
    return writer


@pytest.fixture
def player(mock_writer):
    from mudlib.player import Player

    return Player(name="Tester", x=0, y=0, reader=MagicMock(), writer=mock_writer)


@pytest.fixture
def admin_player(mock_writer):
    from mudlib.player import Player

    p = Player(name="Admin", x=0, y=0, reader=MagicMock(), writer=mock_writer)
    p.is_admin = True
    return p


@pytest.fixture(autouse=True)
def _clear_topics():
    _help_topics.clear()
    yield
    _help_topics.clear()


@pytest.mark.asyncio
async def test_help_shows_toml_topic(player):
    from mudlib.content import HelpTopic

    _help_topics["combat"] = HelpTopic(
        name="combat", body="fight stuff", title="combat primer"
    )
    await commands.dispatch(player, "help combat")
    output = "".join(c[0][0] for c in player.writer.write.call_args_list)
    assert "combat primer" in output
    assert "fight stuff" in output


@pytest.mark.asyncio
async def test_help_admin_topic_hidden_from_players(player):
    from mudlib.content import HelpTopic

    _help_topics["secret"] = HelpTopic(
        name="secret", body="hidden", title="secret stuff", admin=True
    )
    await commands.dispatch(player, "help secret")
    output = "".join(c[0][0] for c in player.writer.write.call_args_list)
    assert "hidden" not in output
    assert "unknown" in output.lower()


@pytest.mark.asyncio
async def test_help_admin_topic_visible_to_admins(admin_player):
    from mudlib.content import HelpTopic

    _help_topics["secret"] = HelpTopic(
        name="secret", body="hidden", title="secret stuff", admin=True
    )
    await commands.dispatch(admin_player, "help secret")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "hidden" in output
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_help_topics.py -v -k "test_help_shows or test_help_admin"`
Expected: ImportError on `_help_topics`.

**Step 3: Write implementation**

In `src/mudlib/commands/help.py`, add a module-level dict and modify `cmd_help`:

```python
# At module level, after imports
from mudlib.content import HelpTopic

_help_topics: dict[str, HelpTopic] = {}
```

Modify `cmd_help` to check topics BEFORE the existing hidden-command check:

```python
async def cmd_help(player: Player, args: str) -> None:
    args = args.strip()
    if not args:
        await player.send(
            "type help <command> for details. see also: commands, skills, client\r\n"
        )
        return

    # Check help topics first
    topic = _help_topics.get(args)
    if topic is not None:
        if topic.admin and not player.is_admin:
            await player.send(f"Unknown command: {args}\r\n")
            return
        await player.send(f"{topic.title}\r\n{topic.body}\r\n")
        return

    # Existing hidden command check...
    defn = _registry.get(args)
    if defn is not None and defn.hidden:
        if defn.admin and not player.is_admin:
            await player.send(f"Unknown command: {args}\r\n")
            return
        await defn.handler(player, "")
        return

    await _show_command_detail(player, args)
```

In `src/mudlib/server.py`, add loading after the content commands block:

```python
from mudlib.content import load_help_topics
from mudlib.commands.help import _help_topics

# Load help topics from content/help/
help_dir = pathlib.Path(__file__).resolve().parents[2] / "content" / "help"
if help_dir.exists():
    loaded_topics = load_help_topics(help_dir)
    _help_topics.update(loaded_topics)
    log.info("loaded %d help topics from %s", len(loaded_topics), help_dir)
```

**Step 4: Run test to verify it passes**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Wire TOML help topics into help command`

---

## Task 3: Migrate hardcoded zones help to TOML

**Files:**
- Create: `content/help/zones.toml`
- Modify: `src/mudlib/commands/help.py` — remove `cmd_zones_help` function and its registration
- Modify: `tests/test_help_command.py` — adjust zones tests

**Step 1: Create the TOML file**

```toml
name = "zones"
title = "zones"
admin = true
body = """
  zones are spatial containers - rooms, dungeons, overworld areas.
  each zone has a terrain grid, spawn point, and optional portals.

  listing zones
    @zones              list all registered zones

  navigating
    @goto <zone>        teleport to a zone's spawn point
    enter <portal>      step through a portal to another zone

  building
    @dig <name> <w> <h> create a new blank zone
    @paint              toggle paint mode for terrain editing
    @save               save current zone to file

  see: @zones, @goto, @dig, @paint, @save
"""
```

**Step 2: Remove `cmd_zones_help` and its registration from help.py**

Delete the `cmd_zones_help` function (lines 357-378) and its `register()` call (lines 415-424).

**Step 3: Update tests**

The existing `test_help_zones_*` tests in `test_help_command.py` need to load the topic into `_help_topics` instead of relying on the hidden command. Update them to populate `_help_topics` with the zones topic, and remove the build import that was only needed for the old hidden command.

Also update `test_help_topics.py` to add a test that verifies the zones TOML file loads correctly from `content/help/`.

**Step 4: Run all tests**

Run: `pytest tests/test_help_command.py tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Migrate zones help from hardcoded to TOML`

---

## Task 4: `@help` list command

**Files:**
- Create: `src/mudlib/commands/helpadmin.py`
- Modify: `src/mudlib/server.py` — import the module

**Step 1: Write the failing test**

Add to `tests/test_help_topics.py`:

```python
from mudlib.commands import helpadmin  # noqa: F401


@pytest.mark.asyncio
async def test_at_help_lists_topics(admin_player):
    from mudlib.content import HelpTopic

    _help_topics["combat"] = HelpTopic(
        name="combat", body="fight", title="combat primer"
    )
    _help_topics["zones"] = HelpTopic(
        name="zones", body="build", title="zone guide", admin=True
    )
    await commands.dispatch(admin_player, "@help")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "combat" in output
    assert "combat primer" in output
    assert "zones" in output
    assert "[admin]" in output


@pytest.mark.asyncio
async def test_at_help_requires_admin(player):
    await commands.dispatch(player, "@help")
    output = "".join(c[0][0] for c in player.writer.write.call_args_list)
    # Should be rejected by dispatch (admin=True check)
    assert "unknown" in output.lower() or output == ""
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_help_topics.py -v -k "at_help"`
Expected: ImportError on `helpadmin`.

**Step 3: Write implementation**

Create `src/mudlib/commands/helpadmin.py`:

```python
"""Admin help topic management commands (@help)."""

from mudlib.commands import CommandDefinition, register
from mudlib.commands.help import _help_topics
from mudlib.player import Player


async def cmd_at_help(player: Player, args: str) -> None:
    """List all help topics or dispatch to subcommands."""
    args = args.strip()

    if args.startswith("create"):
        await _cmd_create(player, args[6:].strip())
        return
    if args.startswith("edit"):
        await _cmd_edit(player, args[4:].strip())
        return
    if args.startswith("remove"):
        await _cmd_remove(player, args[6:].strip())
        return

    # Default: list all topics
    if not _help_topics:
        await player.send("No help topics defined.\r\n")
        return

    lines = ["help topics"]
    for name in sorted(_help_topics):
        topic = _help_topics[name]
        admin_tag = " [admin]" if topic.admin else ""
        lines.append(f"  {name:<20} {topic.title}{admin_tag}")
    lines.append("")
    lines.append("  @help create          create a new topic")
    lines.append("  @help edit <topic>    edit an existing topic")
    lines.append("  @help remove <topic>  remove a topic")
    await player.send("\r\n".join(lines) + "\r\n")


async def _cmd_create(player: Player, args: str) -> None:
    """Placeholder for guided topic creation."""
    await player.send("Not yet implemented.\r\n")


async def _cmd_edit(player: Player, args: str) -> None:
    """Placeholder for topic editing."""
    await player.send("Not yet implemented.\r\n")


async def _cmd_remove(player: Player, args: str) -> None:
    """Placeholder for topic removal."""
    await player.send("Not yet implemented.\r\n")


register(
    CommandDefinition(
        "@help", cmd_at_help, admin=True, mode="*", help="manage help topics"
    )
)
```

Add `import mudlib.commands.helpadmin` to server.py with the other command imports.

**Step 4: Run test to verify it passes**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add @help list command for admin topic management`

---

## Task 5: `@help create` with guided prompts and editor

**Files:**
- Modify: `src/mudlib/commands/helpadmin.py` — implement `_cmd_create`

This is the most involved task. The guided flow needs to:
1. Prompt for topic name (if not provided as arg)
2. Prompt for title
3. Prompt for admin flag (y/n)
4. Open the editor for body text
5. On editor save, assemble TOML and write to `content/help/<name>.toml`

**Step 1: Write the failing tests**

Add to `tests/test_help_topics.py`:

```python
@pytest.mark.asyncio
async def test_at_help_create_with_all_args_opens_editor(admin_player):
    """@help create <name> should set up prompting state."""
    await commands.dispatch(admin_player, "@help create combat")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "title" in output.lower()


@pytest.mark.asyncio
async def test_at_help_create_no_args_prompts_name(admin_player):
    """@help create with no args prompts for the topic name."""
    await commands.dispatch(admin_player, "@help create")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "name" in output.lower()
```

**Step 2: Run test to verify it fails**

Expected: "Not yet implemented" in output, assertions fail.

**Step 3: Write implementation**

The create flow uses `player.pending_input` — a callback that intercepts the
next line of input. This is the same pattern MUD systems use for multi-step
prompts. We'll need to add this to the Player class if it doesn't exist, or
use an equivalent mechanism.

Implementation approach for prompted input:

```python
async def _cmd_create(player: Player, args: str) -> None:
    """Guided topic creation."""
    if args:
        # Name provided, skip to title prompt
        await _prompt_title(player, name=args)
    else:
        await player.send("topic name: ")
        player.pending_input = _on_name_input


async def _on_name_input(player: Player, line: str) -> None:
    """Handle name input."""
    name = line.strip()
    if not name:
        await player.send("Cancelled.\r\n")
        return
    if name in _help_topics:
        await player.send(f"Topic '{name}' already exists. Use @help edit.\r\n")
        return
    await _prompt_title(player, name=name)


async def _prompt_title(player: Player, name: str) -> None:
    await player.send(f"title [{name}]: ")
    player.pending_input = lambda p, line: _on_title_input(p, line, name)


async def _on_title_input(player: Player, line: str, name: str) -> None:
    title = line.strip() or name
    await player.send("admin only? [y/N]: ")
    player.pending_input = lambda p, line: _on_admin_input(p, line, name, title)


async def _on_admin_input(
    player: Player, line: str, name: str, title: str
) -> None:
    admin = line.strip().lower() in ("y", "yes")
    # Open editor for body
    await _open_body_editor(player, name=name, title=title, admin=admin, body="")


async def _open_body_editor(
    player: Player, name: str, title: str, admin: bool, body: str
) -> None:
    from mudlib.editor import Editor

    save_cb = _make_help_save_callback(player, name, title, admin)
    player.editor = Editor(
        save_callback=save_cb,
        content_type="text",
        color_depth=player.color_depth or "16",
        initial_content=body,
    )
    player.mode_stack.append("editor")
    await player.send("Entering editor for body text. Type :h for help.\r\n")
```

The save callback writes the TOML file:

```python
def _make_help_save_callback(
    player: Player, name: str, title: str, admin: bool
):
    async def save_callback(content: str) -> None:
        _write_help_toml(name, title, admin, content)
        # Update in-memory dict
        from mudlib.content import HelpTopic
        _help_topics[name] = HelpTopic(
            name=name, body=content.strip(), title=title, admin=admin
        )
        await player.send(f"Help topic '{name}' saved.\r\n")

    return save_callback


def _write_help_toml(name: str, title: str, admin: bool, body: str) -> None:
    """Write a help topic TOML file."""
    from pathlib import Path

    help_dir = Path(__file__).resolve().parents[2] / "content" / "help"
    help_dir.mkdir(parents=True, exist_ok=True)
    path = help_dir / f"{name}.toml"

    lines = [f'name = "{name}"']
    lines.append(f'title = "{title}"')
    if admin:
        lines.append("admin = true")
    lines.append(f'body = """\n{body.strip()}\n"""')
    path.write_text("\n".join(lines) + "\n")
```

**Important:** Check whether `player.pending_input` exists. If not, this
needs to be added to the Player class and the server's input loop. Look for
existing prompt patterns — if there's already a confirmation prompt somewhere,
follow that pattern. If not, `pending_input` is a `Callable | None` on
Player, and the server input loop checks it before dispatch:

```python
# In server.py input loop, before command dispatch:
if player.pending_input is not None:
    callback = player.pending_input
    player.pending_input = None
    await callback(player, inp)
    continue
```

**Step 4: Run tests**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add @help create with guided prompts and editor`

---

## Task 6: `@help edit <topic>`

**Files:**
- Modify: `src/mudlib/commands/helpadmin.py`

**Step 1: Write the failing tests**

```python
@pytest.mark.asyncio
async def test_at_help_edit_unknown_topic(admin_player):
    await commands.dispatch(admin_player, "@help edit bogus")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "not found" in output.lower()


@pytest.mark.asyncio
async def test_at_help_edit_existing_opens_editor(admin_player):
    from mudlib.content import HelpTopic

    _help_topics["combat"] = HelpTopic(
        name="combat", body="old body", title="combat primer"
    )
    await commands.dispatch(admin_player, "@help edit combat")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    # Should prompt for title with current value as default
    assert "combat primer" in output
    # Should eventually open editor
    assert admin_player.editor is not None
    assert "old body" in "\n".join(admin_player.editor.buffer)
```

**Step 2: Run test to verify it fails**

Expected: "Not yet implemented" output.

**Step 3: Write implementation**

```python
async def _cmd_edit(player: Player, args: str) -> None:
    """Edit an existing help topic."""
    name = args.strip()
    if not name:
        await player.send("Usage: @help edit <topic>\r\n")
        return

    topic = _help_topics.get(name)
    if topic is None:
        await player.send(f"Topic '{name}' not found.\r\n")
        return

    await player.send(f"title [{topic.title}]: ")
    player.pending_input = lambda p, line: _on_edit_title(
        p, line, name, topic
    )


async def _on_edit_title(player, line, name, topic):
    title = line.strip() or topic.title
    await player.send(f"admin only? [{'Y' if topic.admin else 'y'}/{'N' if not topic.admin else 'n'}]: ")
    player.pending_input = lambda p, line: _on_edit_admin(
        p, line, name, title, topic
    )


async def _on_edit_admin(player, line, name, title, topic):
    if line.strip():
        admin = line.strip().lower() in ("y", "yes")
    else:
        admin = topic.admin
    await _open_body_editor(player, name=name, title=title, admin=admin, body=topic.body)
```

**Step 4: Run tests**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add @help edit for modifying existing topics`

---

## Task 7: `@help remove <topic>`

**Files:**
- Modify: `src/mudlib/commands/helpadmin.py`

**Step 1: Write the failing tests**

```python
@pytest.mark.asyncio
async def test_at_help_remove_unknown_topic(admin_player):
    await commands.dispatch(admin_player, "@help remove bogus")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "not found" in output.lower()


@pytest.mark.asyncio
async def test_at_help_remove_prompts_confirmation(admin_player):
    from mudlib.content import HelpTopic

    _help_topics["combat"] = HelpTopic(
        name="combat", body="fight", title="combat primer"
    )
    await commands.dispatch(admin_player, "@help remove combat")
    output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list)
    assert "confirm" in output.lower() or "y/n" in output.lower()
```

**Step 2: Run test to verify it fails**

Expected: "Not yet implemented" output.

**Step 3: Write implementation**

```python
async def _cmd_remove(player: Player, args: str) -> None:
    """Remove a help topic."""
    name = args.strip()
    if not name:
        await player.send("Usage: @help remove <topic>\r\n")
        return

    if name not in _help_topics:
        await player.send(f"Topic '{name}' not found.\r\n")
        return

    await player.send(f"Remove topic '{name}'? [y/N]: ")
    player.pending_input = lambda p, line: _on_remove_confirm(p, line, name)


async def _on_remove_confirm(player: Player, line: str, name: str) -> None:
    if line.strip().lower() not in ("y", "yes"):
        await player.send("Cancelled.\r\n")
        return

    # Remove from memory
    _help_topics.pop(name, None)

    # Remove file
    from pathlib import Path
    path = Path(__file__).resolve().parents[2] / "content" / "help" / f"{name}.toml"
    if path.exists():
        path.unlink()

    await player.send(f"Topic '{name}' removed.\r\n")
```

**Step 4: Run tests**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add @help remove with confirmation prompt`

---

## Task 8: Add `pending_input` to Player and server loop

This task may need to be done BEFORE task 5 if `pending_input` doesn't exist
on Player yet. Check first — if there's already a prompt/callback mechanism,
use that instead.

**Files:**
- Modify: `src/mudlib/player.py` — add `pending_input` field
- Modify: `src/mudlib/server.py` — check `pending_input` before dispatch

**Step 1: Write the failing test**

```python
def test_player_has_pending_input():
    from mudlib.player import Player
    from unittest.mock import MagicMock

    p = Player(name="Test", x=0, y=0, reader=MagicMock(), writer=MagicMock())
    assert p.pending_input is None
```

**Step 2: Run test to verify it fails**

Expected: AttributeError.

**Step 3: Write implementation**

In Player dataclass, add:
```python
pending_input: Callable | None = None
```

In server.py input loop (find where `dispatch` is called), add before it:
```python
if player.pending_input is not None:
    callback = player.pending_input
    player.pending_input = None
    await callback(player, inp)
    continue
```

**Step 4: Run tests**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 5: Commit**

Message: `Add pending_input callback to Player for prompted input`

---

## Task 9: Create `help @help` topic

**Files:**
- Create: `content/help/@help.toml`

**Step 1: Create the file**

```toml
name = "@help"
title = "help system administration"
admin = true
body = """
  the help system stores topics as TOML files in content/help/.
  each topic has a name, title, optional admin flag, and body text.

  commands
    @help               list all help topics
    @help create        create a new topic (guided)
    @help create <name> create a new topic with the given name
    @help edit <topic>  edit an existing topic
    @help remove <topic> remove a topic

  creating topics
    @help create walks you through each field:
      name        the topic name (what players type after 'help')
      title       short description shown in listings
      admin only  whether only admins can view it
      body        the help text (opens in the editor)

  editing topics
    @help edit shows current values for each field.
    press enter to keep the current value.
    the editor opens with the existing body text.

  files
    topics are stored in content/help/<name>.toml
    they can also be edited directly with any text editor.
    changes are loaded at server startup.
"""
```

**Step 2: Write test**

```python
def test_at_help_toml_loads_from_content():
    from pathlib import Path
    from mudlib.content import load_help_topics

    help_dir = Path(__file__).resolve().parents[1] / "content" / "help"
    if help_dir.exists():
        topics = load_help_topics(help_dir)
        assert "@help" in topics
        assert topics["@help"].admin is True
```

**Step 3: Run tests**

Run: `pytest tests/test_help_topics.py -v`
Expected: all PASS.

**Step 4: Commit**

Message: `Add help @help topic for admin documentation`

---

## Task execution order

Tasks 1-3 are sequential (each builds on the last). Task 8 (pending_input)
must be done before Task 5. Task 4 can happen after Task 2. Tasks 5-7 are
sequential. Task 9 can happen any time after Task 2.

Suggested order: **1 → 2 → 3 → 8 → 4 → 5 → 6 → 7 → 9**

Parallelizable after Task 2: Tasks 3 and 8 can be done in parallel.
After Task 8: Tasks 4 and 5 can start (4 is simpler, do first).
