Skip to content

Instantly share code, notes, and snippets.

@knowsuchagency
Last active August 21, 2026 23:34
Show Gist options
  • Select an option

  • Save knowsuchagency/ea8ca2c024e9c5f14aff5e280b442c07 to your computer and use it in GitHub Desktop.

Select an option

Save knowsuchagency/ea8ca2c024e9c5f14aff5e280b442c07 to your computer and use it in GitHub Desktop.
SSH Wars visible TUI play through Herdr
name ssh-wars
description Play SSH Wars when a user asks to connect to sshwars.com, trade products, take turns, inspect the market, travel, or manage an SSH Wars account. Requires a Herdr-managed session so the user can continuously watch the actual in-game TUI.
compatibility HERDR_ENV=1, herdr CLI, SSH access to sshwars.com.

SSH Wars — visible TUI play through Herdr

Use this skill for all SSH Wars gameplay. The user must be able to follow the actual game state in a right-hand Herdr pane throughout play.

Non-negotiable interaction contract

  • MUST play in the actual SSH Wars terminal UI, in a visible sibling pane on the right of the caller's current Herdr pane.
  • MUST NOT replace the game screen with JSON, a shell dashboard, a text summary, or a wrapper while the session is active.
  • MUST NOT use SSH Wars non-PTY/--json gameplay commands to buy, sell, travel, or otherwise alter game state. They are machine interfaces and are not readable gameplay for the user.
  • MUST leave the game TUI open in the right pane after every action.
  • MUST narrate decisions briefly in chat, but narration supplements the TUI; it never replaces it.
  • MUST NOT create a second account, claim a city, choose a handle, or complete another irreversible onboarding step unless the user expressly asks to do so or has already directed gameplay that necessarily requires it.

Recommended control architecture (validated 2026-08-21)

The best loop is NOT raw keystroke/send-read pairs typed by hand, and NOT a pexpect process owning the game. It is:

  1. The real game TUI runs in the visible right pane via herdr pane run <pane-id> "ssh -tt -i <identity-key> sshwars.com".
  2. A persistent Python kernel (or any script) drives it by shelling out to the herdr CLI. This keeps the game user-visible while giving the agent programmatic control, settle detection, and pattern waits.

Core helpers (define once in the kernel):

import subprocess, time, re

PANE = "wDA:p3"  # the visible game pane id

def _run(args):
    return subprocess.run(["herdr", "pane", *args],
                          capture_output=True, text=True, timeout=10).stdout

def read_pane(lines=60):
    return _run(["read", PANE, "--source", "visible", "--lines", str(lines)])

def send_keys(k):   # named keys ONLY: enter, esc, up, down, left, right, ...
    subprocess.run(["herdr", "pane", "send-keys", PANE, k], capture_output=True)

def send_text(t):   # literal text into text fields (handles, quantities, search)
    subprocess.run(["herdr", "pane", "send-text", PANE, t], capture_output=True)

def frame_stable(timeout=8.0, interval=0.35):
    """Poll the visible pane until two consecutive reads are identical."""
    prev, t0 = None, time.time()
    while time.time() - t0 < timeout:
        cur = read_pane()
        if cur == prev and cur.strip():
            return cur
        prev, _ = cur, time.sleep(interval)
    return prev or read_pane()

def act(*keys, settle=6.0):
    """Send named keys, wait for the frame to settle, return it."""
    for k in keys:
        send_keys(k); time.sleep(0.15)
    return frame_stable(settle)

def wait_for(pat, timeout=10.0):
    """Poll the pane until regex pat appears in the visible frame."""
    t0 = time.time()
    while time.time() - t0 < timeout:
        f = read_pane()
        if re.search(pat, f):
            return f
        time.sleep(0.3)
    return None

Why this beats alternatives:

  • herdr pane send-keys accepts key NAMES only (enter, esc, up, …); arbitrary text fails with invalid_key. Use herdr pane send-text for text fields. Wrap both.
  • The server redraws asynchronously (news ticker, animations). Sleeping a fixed time between send and read is flaky; two-consecutive-identical-reads settle detection is reliable.
  • pexpect only helps when the game runs outside a visible pane, which this skill forbids. Driving herdr from Python gives the same precision with the TUI still user-visible.

Account and key management

  • SSH Wars identifies accounts by SSH public key. Keep ONE dedicated key per account: ~/.ssh/sshwars_<handle> (generate with ssh-keygen -t ed25519), connect with -i ~/.ssh/sshwars_<handle> -o IdentitiesOnly=yes.
  • Reconnecting with the same key resumes the account (cash, cargo, Heat, Actions, city all persist).
  • If reconnect opens onboarding despite an existing account, the wrong key is in use — stop before claiming anything and tell the user.

Connect sequence

  1. Confirm Herdr: test "${HERDR_ENV:-}" = 1.
  2. herdr pane split --current --direction right --cwd "$PWD" --no-focus (or reuse an existing idle game pane).
  3. herdr pane run <pane-id> "ssh -tt -i <key> -o IdentitiesOnly=yes sshwars.com"
  4. Each connection begins with a ~4s NIGHTWIRE animation; send any key to skip it. The first post-login frame can show a blank/loading state (WORKING..., empty player fields) — poll with frame_stable() until the real status bar appears (regex ACTIONS \d).
  5. Fetch the server's machine-readable mechanics doc once per account (this is read-only and sanctioned): ssh -i <key> sshwars.com skill
  6. The server asks agents to identify honestly: set the profile identity to AGENT (Profile → identity metadata).

Playing visibly

  • Read the current visible frame before every consequential move.
  • One decision narrated in chat, one visible keystroke, then re-read.
  • Report human outcomes: product, quantity, price, cash, cargo, Actions, Heat, city.
  • For "play N turns" requests, perform N distinct completed in-game actions with the TUI visible for all of them.
  • Prefer sensible play unless the user explicitly requests otherwise (e.g. "play unhinged"): preserve travel Actions, account for seller tax, avoid unnecessary Heat.

Key mechanics quick reference (from ssh sshwars.com skill)

  • Actions: max 120, +5/hour recovery (offline included). Buy/sell = 1, travel = 3, deploy cops = 2, tax change = 2. Views are free.
  • Travel incident chance: 12% + Heat/2, cap 55%. Deterministic per journey.
  • Heat: +1 per trade (+up to 5 more for size), −2/hour decay.
  • City controller taxes visitor sales (0–100%); 70% of tax to controller, 30% to job treasury. Controllers untaxed at home.
  • Crooked Cops tiers at 10/50/150/500 deployed: Heat −1/−2/−3/−5 and safehouse bonuses. Deploy only in your controlled home city.
  • Safehouse: entering removes 10 Heat; 35% faster Action recovery.
  • Chicago has blackjack (C then B); Vegas/NYC paid venues.
  • Rename costs $1,000 / $2,500 / $250,000 (max 3).
  • Invite codes: invitee +$5,000, inviter +$10,000 per signup.

Pane lifecycle

  • Leave the TUI open while the user may be watching or directing play.
  • Never close a user-owned pane. A pane created for SSH Wars may be closed only at the user's request or after the user says the session is finished.
  • If the game pane is unrecoverably stuck, explain, close only that pane, create a new right sibling, and reconnect visibly.
Join my trashtalk room and talk to whoever else is in there.
Room: https://trashtalk.to/r/r_b1c2cb0ed12f0fa5?k=244b8e991c06c38b
How: https://trashtalk.to/SKILL.md
Read the SKILL first, pick yourself a name, join, then keep polling
and replying until I tell you to stop. Don't reply to your own messages.

Herdr 3-column layout: chat | game | agent

How to arrange a Herdr tab into three columns — a live chat feed on the left, the widest column in the middle for a TUI (here: SSH Wars), and the agent's own terminal on the right — while the agent keeps driving the game programmatically.

Final geometry (144-col window):

+------------+----------------------+------------+
| chat feed  |  game TUI (widest)   | agent term |
|   32 cols  |       72 cols        |   40 cols  |
+------------+----------------------+------------+

Starting point

A tab with two panes: the agent session on the left, the game TUI on the right (created earlier with herdr pane split --current --direction right). Pane ids below: agent = A, game = G.

herdr pane layout --current   # read real pane ids from JSON, never guess

Recipe

1. Create the third column

Split the game pane to the right. The new empty shell becomes the chat pane:

herdr pane split G --direction right --ratio 0.34 --no-focus
# -> returns the new pane id, call it C.  Layout: [A | G | C]

2. Put the agent on the right

Swap contents of the agent pane and the new right-most shell. pane swap exchanges pane contents (processes keep running) without restarting anything:

herdr pane swap --source-pane A --target-pane C
# -> [C(empty) | G(game) | A(agent)]

3. Resize — amounts are FRACTIONS, not columns

This is the part that costs the most time to figure out by trial:

  • --amount is a fraction of the split (0.0–1.0). Integer amounts get clamped or redistributed in confusing ways — --amount 40 does not mean 40 columns.
  • resize --direction X --pane P moves P's boundary in direction X.
  • The reliable pattern for making the middle pane widest: grow it from both sides.
# grow game leftward into the chat column
herdr pane resize --direction left  --amount 0.30 --pane G

# grow game rightward into the agent column
herdr pane resize --direction right --amount 0.30 --pane G

# widen the chat column a bit
herdr pane resize --direction right --amount 0.12 --pane C

herdr pane layout --current   # verify: chat ~32 | game ~72 | agent ~40

The game TUI reflows automatically on resize (SSH Wars handles anything down to 40×18; 60+ columns restores the full status bar).

4. Run the chat feed in the left pane

Any long-polling viewer works. This one tails a trashtalk.to room with timestamps, per-author colors, and proper word-wrap at the pane width:

herdr pane run C "uv run /tmp/tt_chat_viewer.py"
viewer script (stdlib only)
#!/usr/bin/env python3
"""Trashtalk room viewer — narrow-pane friendly TUI feed."""
import json, subprocess, sys, time, hashlib, shutil, textwrap
from datetime import datetime

ROOM = "r_b1c2cb0ed12f0fa5"
STATE = "/tmp/tt_join.json"          # {"token": "...", "participantId": "...", "cursor": N}
CURSOR_FILE = "/tmp/tt_chat_cursor"

def api(params=""):
    tok = json.load(open(STATE))["token"]
    r = subprocess.run(
        ["curl", "-s", "--max-time", "35",
         "-H", f"Authorization: Bearer {tok}",
         f"https://trashtalk.to/api/rooms/{ROOM}/messages?{params}"],
        capture_output=True, text=True)
    try:
        return json.loads(r.stdout)
    except Exception:
        return {}

def name_color(name):
    h = int(hashlib.md5(name.encode()).hexdigest(), 16)
    return f"\x1b[{31 + h % 6}m"

RESET, DIM, BOLD = "\x1b[0m", "\x1b[2m", "\x1b[1m"
me = json.load(open(STATE)).get("participantId", "")
cursor = open(CURSOR_FILE).read().strip() if __import__("os").path.exists(CURSOR_FILE) \
         else str(json.load(open(STATE))["cursor"])

print(f"{DIM}── trashtalk · {ROOM} ──{RESET}", flush=True)

hist = api("since=0&limit=200")
msgs = hist.get("messages", [])
for m in msgs[-6:]:
    ts = datetime.fromtimestamp(m["createdAt"]/1000).strftime("%H:%M")
    print(f"{DIM}{ts}{RESET} {BOLD}{name_color(m['author']['name'])}{m['author']['name']}{RESET}: "
          + textwrap.fill(m["body"], width=max(shutil.get_terminal_size().columns-2, 20),
                          subsequent_indent="  "), flush=True)
if msgs:
    cursor = hist["cursor"]
    open(CURSOR_FILE, "w").write(str(cursor))

while True:
    data = api(f"since={cursor}&wait=25000")
    if data.get("cursor"):
        cursor = data["cursor"]
        open(CURSOR_FILE, "w").write(str(cursor))
    w = max(shutil.get_terminal_size().columns - 2, 20)
    for m in data.get("messages", []):
        if m["author"]["participantId"] == me:
            continue
        ts = datetime.fromtimestamp(m["createdAt"]/1000).strftime("%H:%M")
        body = textwrap.fill(m["body"], width=w, subsequent_indent="  ")
        print(f"{DIM}{ts}{RESET} {BOLD}{name_color(m['author']['name'])}{m['author']['name']}{RESET}:\n"
              f"  {body}\n", flush=True)

Gotchas learned the hard way

  1. pane close kills the process in the pane. Verify which pane holds your agent session (herdr pane current --current) before closing anything. Closing your own pane mid-session is exactly as fun as it sounds.
  2. pane swap moves contents, not geometry — the safe way to reposition a running process (game, agent, viewer) without restarting it.
  3. Integer --amount values are a trap. If a resize "does nothing" or produces wild jumps, you passed columns instead of a 0-1 fraction.
  4. Read ids from JSON responses (pane layout --current, pane split output) — pane ids are opaque and shift as panes close.
  5. Zoom lies about geometry. If pane layout shows one pane filling the tab, the tab is zoomed: herdr pane zoom --pane <id> --off first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment