Skip to content

Latest commit

 

History

History
162 lines (138 loc) · 7.25 KB

File metadata and controls

162 lines (138 loc) · 7.25 KB

AGENTS.md — orientation for LLM agents working in this tree

You are working on mac-control: remote control for classic Mac OS (System 7+) from a modern host or LLM. Guest-side C runs under Retro68; host-side Rust exposes an MCP surface so any MCP client can drive the guest. Read README.md for the human-facing overview.

Ground rules (hard-won; don't relearn)

  • Never yank media from a running guest. Ejecting a mounted disk / quitting an app / hard-resetting a MAME VM without asking will bomb the guest. Ask before vmctl unmount, vmctl quit, vmctl reset, unmounting a shared host volume, or killing a guest process. See the user memory feedback_never_unmount_disks for context.
  • Never auto-install the jGNE filter at startup. agent/src/jgne_filter.c installs a system-wide hook at low-mem $29A. On-demand install via diag.jgne install works (validated 2026-08-02, cross-app keystroke injection to Finder confirmed via A5 self-skip). Auto-install during main() was re-attempted 2026-08-03 and immediately wedged the agent post-restart — root cause unknown. Callers install per-session when they need cross-app input. See project_jgne_cross_app_working in memory for the working technique.
  • Never route input synth through MAME / vmctl. mc-control has to work on real classic Mac hardware, not just the emulator. See feedback_no_mame_input_injection in memory.
  • Never rename apps or types after vendors. Generic, capability- describing names only. This is a global user rule.
  • Never skip pre-commit hooks or bypass signing. If a hook fails, fix the underlying issue.

Size + memory budgets

  • 68k agent binary target: ≤ ~80 KB. Currently ~78 KB. Every feature added is size + guest RAM. If a change pushes past this, either explain why or find something to drop.
  • Guest is a Mac II with 8 MB RAM. MacTCP takes a chunk; usable free memory is ~200-300 KB. Handles must be purgeable when idle; the arena grows dynamically but caps at 128 KB. Watch FreeMem in logs.
  • Wire framing caps: 64 KB JSON per frame, 128 KB binary per frame. Bigger transfers (like update.stage) are chunked at 16 KB.

Build workflows (exact commands)

Guest:

./agent/build-68k.sh
# Always via this script — it re-runs CMake configure to refresh the
# version stamp (YYYY.M.D.H.M.S). Raw `cmake --build agent/build-68k`
# uses a stale timestamp and breaks the update-verify step.

Host:

cargo build --release          # all crates + binaries
cargo test                     # includes mock-agent round-trip

Push a rebuilt agent to a running guest (no reboot):

./target/release/mc-cli --host <guest-ip> update \
    --binary agent/dist/mc-agent-68k.bin \
    --version <stamp>
# --compress packbits saves ~1% on the compiled binary but is meaningful
# for text / resource payloads.

Restart the MCP server after rebuilding mac-controld:

# No `claude mcp restart` command exists — remove + re-add, or start
# a new session. MCP clients don't hot-reload the server process.
claude mcp remove mac-control
claude mcp add --scope project mac-control \
    ./target/release/mac-controld -- --host <guest-ip>

Architecture map (one line each)

Agent:

  • agent/src/main.c — event loop + per-conn state machine + heartbeat.
  • agent/src/transport_mactcp.c — MacTCP async I/O + notifier.
  • agent/src/frame.[ch] — 5-byte framing codec.
  • agent/src/json.c — arena-backed parser (see pitfalls).
  • agent/src/arena.c — Handle-backed bump allocator; mc_arena_trim matters (see pitfalls).
  • agent/src/dispatch.c — method table + envelope builders.
  • agent/src/methods_*.c — one file per method group.
  • agent/src/packbits.c — streaming decoder for wire compression.
  • agent/src/ae_desc.c — Apple Event JSON ↔ AEDesc.
  • agent/src/screen_input.c — Time Manager tape for click/drag/type.
  • agent/src/jgne_filter.c — research; do not auto-install.
  • agent/src/update.c — stage / apply / commit self-update.

Host:

  • crates/mc-agent-client/ — pure library, protocol types + sync client.
  • crates/mc-pict/ — classic PICT → PNG (headerless variant included).
  • crates/mc-ocr/ — macOS Vision OCR helper.
  • crates/mc-mac-roman/ — MacRoman ↔ UTF-8 codec.
  • bin/mc-cli/ — developer CLI; one verb per method plus composers (aete, aete-file, click-text, menu).
  • bin/mac-controld/ — MCP server; src/tools.rs is the tool registry.

Wire is defined by the code, not a spec doc: agent/src/methods_*.c (guest) and crates/mc-agent-client/src/ (host types) are the source of truth. Keep them in sync when reshaping a method.

Pitfalls we've already hit

  • JSON parser arena bloat. parse_string_raw pre-allocated remaining bytes per string and used to leak the tail. Requests over ~170 bytes with many string fields silently corrupted into bad_params: missing 'method'. Fixed with mc_arena_trim in agent/src/json.c — don't reintroduce.
  • PICT emitted by the agent has no 512-byte header. sips on macOS misrenders it as all-white; use mc_pict::decode_to_png.
  • PostEvent is per-process. Under Process Manager (System 7+) each process has its own event queue. PostEvent from mc-agent reaches ONLY mc-agent's queue. Cross-app input works via jGNE filter with A5 self-skip (see the jGNE ground rule above) — keystroke injection to Finder is verified. Menu-hold-open is not: mouseDown is delivered but MenuSelect reads button state from a source we haven't identified. See project_menu_screenshot_status in memory. For menu-driven work, prefer Apple Events.
  • AEDesc data via Handle, not AEGetDescData. Multiversal headers predate the modern API. Read desc->dataHandle with HLock + GetHandleSize directly.
  • AETE format varies by era. Our parser (crates/mc-agent-client/ src/aete.rs) handles System 7 / Retro68 output. Carbon-era AETEs (iTunes 4.9) have variant fields; parse fails partway through.
  • Time Manager tasks + code segments. Anything installed via InsTime MUST be RmvTime'd before mc-agent exits, else the next fire jumps into freed code and bombs whichever process is frontmost. See mc_screen_input_shutdown.
  • Cursor / MBState low-mem restore. Same shutdown path resets MBState and clears modifier bits in KeyMap. Without this, an aborted tape can leave the OS thinking the button is held down system-wide.
  • AETE pstrings are packed, no padding. Earlier assumption of word-alignment was wrong for the AETEs we've tested.

Where things live

  • Design north-star: plan.md
  • Backlog + gotchas: todos.md
  • Wire: agent/src/methods_*.c (guest) + crates/mc-agent-client/src/ (host types). No standalone spec.
  • Per-project memory (accumulated context, load-bearing corrections): ~/.claude/projects/-Users-nick-code-mac-control/memory/MEMORY.md

When you're stuck

  • Guest unreachable? Try mc-cli hello; check mc-cli logs -n 40.
  • Made a bad push? The old binary is in Startup Items; a reboot recovers unless commit succeeded.
  • Weird MacTCP -23012 (invalidBufPtr)? Usually async send buffer moved before completion; retry.
  • VM frozen after jgne install? Hard reset; verify diag-only path before ever retrying.