28 KiB
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)
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 stemtitle(optional) — short description shown in listings, defaults to nameadmin(optional) — if true, only admins can view. defaults to falsebody(required) — the help text, multiline string
How help <topic> resolution works
- Check
_help_topicsdict for exact match - If found and
admin=True, check player.is_admin - If found, display title + body
- 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— addHelpTopicdataclass andload_help_topics()
Step 1: Write the failing test
Create tests/test_help_topics.py:
"""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:
@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:
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:
# 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:
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:
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— removecmd_zones_helpfunction and its registration - Modify:
tests/test_help_command.py— adjust zones tests
Step 1: Create the TOML file
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:
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:
"""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:
- Prompt for topic name (if not provided as arg)
- Prompt for title
- Prompt for admin flag (y/n)
- Open the editor for body text
- 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:
@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:
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:
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:
# 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
@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
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
@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
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— addpending_inputfield - Modify:
src/mudlib/server.py— checkpending_inputbefore dispatch
Step 1: Write the failing test
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:
pending_input: Callable | None = None
In server.py input loop (find where dispatch is called), add before it:
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
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
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).