The port is deliberately separate from the reverse-engineering project: its own repository, its own clone, its own container. Two writers in one working tree means files change under whoever is mid-edit and a `git add -A` by one sweeps up the other's work -- which happened today in the Reborn tree, so this is set up not to repeat it. The wall: Godot never reads a disc format. An offline Rust exporter converts the user's disc into JSON + PNG + Ogg, and the Godot project reads only that. No GDExtension, no Rust in port/. Beyond the practical reason -- Godot cannot read IPFB, RATC, T8aD, XMA or WMV -- there is the design one: modding is a goal, and if the runtime reads the original formats then modding means reverse engineering, whereas if it reads JSON it means opening a file. The decoders come from sylpheed-formats PINNED BY REVISION (8b6dbcf), not vendored and not reimplemented. `sylpheed_formats::media` in particular already owns every case where one playable thing is not one archive entry: entries that span segment files, banks with several sub-waves, and the cutscene voices, which are one continuous XMA stream chunked into VOICE_*.slb entries whose boundaries do NOT match the cues. That last one is the easiest thing in this project to get subtly wrong, so the mission says outright not to re-derive it. docs/MISSION.md is the objective (P0-P7, each gated by an artifact rather than by compiling). docs/BLOCKED.md lists what cannot proceed until the RE agent answers Q1-Q10, and says plainly that none of it may be guessed -- this agent has no emulator and no oracle, so a value it invents is indistinguishable from a decoded one a month later. The container is deliberately small: 3 cpus / 4 GB against the RE container's 6 / 7, and an image with no C++ toolchain, no Vulkan stack and no emulator. Two full-size containers do not fit on this box beside a desktop. Its launcher sets the git identity through GIT_AUTHOR_*/GIT_COMMITTER_* rather than writing [user] into .git/config -- the config route captures every commit made in that tree, including a human's, which is how six of today's commits ended up attributed to the RE agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
161 lines
6.1 KiB
Bash
Executable File
161 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Launcher for the Godot port agent.
|
|
#
|
|
# ./sylph-port build build the image
|
|
# ./sylph-port shell interactive shell
|
|
# ./sylph-port loose [task] detached, self-running on a fixed interval
|
|
# ./sylph-port logs -f follow it
|
|
# ./sylph-port attach chat with it (Ctrl-P Ctrl-Q to leave it running)
|
|
# ./sylph-port remote a link to chat with it from anywhere
|
|
# ./sylph-port stop stop it
|
|
#
|
|
# Env:
|
|
# SYLPH_PORT_CPUS / SYLPH_PORT_MEM_GB override the cap (default 3 / 4)
|
|
# SYLPH_REBORN path to the Syplheed-Reborn checkout (read-only mount)
|
|
# SYLPH_DISC extracted disc root
|
|
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
|
|
# SYLPH_LOOP_INTERVAL fixed loop cadence (default 45m)
|
|
#
|
|
# ── Two hard-won constraints ────────────────────────────────────────────────
|
|
#
|
|
# 1. THIS REPO IS ITS OWN CLONE. It is deliberately NOT the tree the RE agent
|
|
# or a human is working in. Sharing a working tree between two writers means
|
|
# files change under whoever is mid-edit, and a `git add -A` by one sweeps up
|
|
# the other's work. That happened; do not re-create it.
|
|
#
|
|
# 2. IDENTITY GOES IN THE ENVIRONMENT, NOT `.git/config`. Writing `[user]` into
|
|
# a repo's config captures every commit made in that tree, including a
|
|
# human's. GIT_AUTHOR_*/GIT_COMMITTER_* apply to this container's commits and
|
|
# nobody else's.
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO="$(cd "$HERE/.." && pwd)"
|
|
IMAGE="${SYLPH_PORT_IMAGE:-sylpheed-port:latest}"
|
|
NAME="${SYLPH_PORT_NAME:-sylpheed-port}"
|
|
|
|
# Half of what the RE container takes. That container builds a C++ emulator and
|
|
# drives it; this one converts assets and runs Godot. Two full-size containers
|
|
# do not fit on a 12-core / 15 GB box beside a desktop -- memory is the binding
|
|
# constraint, and an over-committed build has crashed this machine before.
|
|
CPUS="${SYLPH_PORT_CPUS:-3}"
|
|
MEM_GB="${SYLPH_PORT_MEM_GB:-4}"
|
|
|
|
REBORN="${SYLPH_REBORN:-$(cd "$REPO/../Syplheed-Reborn" 2>/dev/null && pwd || true)}"
|
|
DISC="${SYLPH_DISC:-$(cd "$REPO/../sylph_extract" 2>/dev/null && pwd || true)}"
|
|
|
|
docker_args() {
|
|
local _out=(
|
|
--name "$NAME"
|
|
--hostname sylph-port
|
|
--cpus "$CPUS"
|
|
--memory "${MEM_GB}g"
|
|
--memory-swap "${MEM_GB}g" # no swap escape hatch: a swapping build
|
|
# thrashes the whole host
|
|
--pids-limit 2048
|
|
-v "$REPO:/work"
|
|
-v "sylpheed-port-target:/sylph-home/port/target-container"
|
|
-v "sylpheed-port-claude:/sylph-home/port/.claude"
|
|
-e "PROJECT_DIR=/work"
|
|
)
|
|
|
|
# The RE corpus, READ-ONLY. `docs/port/HANDOFF.md` is the contract, and the
|
|
# agent also builds sylpheed-cli from here for the reference renderer. Mounted
|
|
# ro so a port iteration cannot edit the other agent's repository.
|
|
if [ -n "$REBORN" ] && [ -d "$REBORN" ]; then
|
|
_out+=(-v "$REBORN:/reborn:ro")
|
|
else
|
|
echo "==> NOTE: no Syplheed-Reborn checkout found; the agent cannot read" >&2
|
|
echo " HANDOFF.md or build the reference renderer. Set SYLPH_REBORN." >&2
|
|
fi
|
|
|
|
if [ -n "$DISC" ] && [ -d "$DISC" ]; then
|
|
_out+=(-v "$DISC:/disc:ro" -e "SYLPHEED_DISC=/disc")
|
|
else
|
|
echo "==> NOTE: no extracted disc found; the exporter has nothing to read." >&2
|
|
echo " Set SYLPH_DISC to the directory holding dat/ and hidden/." >&2
|
|
fi
|
|
|
|
# Commits are attributed to the port agent, via the environment so that
|
|
# nothing is written into the repository's config. See constraint 2 above.
|
|
_out+=(
|
|
-e "GIT_AUTHOR_NAME=Sylpheed port agent"
|
|
-e "GIT_AUTHOR_EMAIL=port-agent@localhost"
|
|
-e "GIT_COMMITTER_NAME=Sylpheed port agent"
|
|
-e "GIT_COMMITTER_EMAIL=port-agent@localhost"
|
|
)
|
|
|
|
local gitcred="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
|
|
if [ -f "$gitcred" ]; then
|
|
_out+=(-v "$gitcred:/sylph-home/port/.git-credentials:ro")
|
|
else
|
|
echo "==> NOTE: no git credentials at $gitcred — the agent cannot push," >&2
|
|
echo " so its work dies with the container." >&2
|
|
fi
|
|
|
|
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
|
|
printf '%s\n' "${_out[@]}"
|
|
}
|
|
|
|
mapfile -t ARGS < <(docker_args)
|
|
|
|
case "${1:-}" in
|
|
build)
|
|
exec docker build -t "$IMAGE" \
|
|
--build-arg "AGENT_UID=$(id -u)" --build-arg "AGENT_GID=$(id -g)" "$HERE"
|
|
;;
|
|
|
|
shell)
|
|
TTY=(-i); [ -t 0 ] && TTY=(-it)
|
|
exec docker run --rm "${TTY[@]}" "${ARGS[@]}" "$IMAGE" bash
|
|
;;
|
|
|
|
loose)
|
|
shift
|
|
TASK="${1:-}"
|
|
if [ -z "$TASK" ]; then
|
|
if [ -f "$REPO/docs/loop-task.md" ]; then
|
|
TASK="$(cat "$REPO/docs/loop-task.md")"
|
|
else
|
|
TASK="Work the milestones in docs/MISSION.md."
|
|
fi
|
|
fi
|
|
# A FIXED interval, not self-pacing: the one thing an agent deep in a
|
|
# milestone reliably forgets is the bookkeeping after it, and a forgotten
|
|
# wake-up silently ends the loop.
|
|
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
|
|
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g pacing=${INTERVAL:-self}"
|
|
echo "==> repo: $REPO"
|
|
echo "==> reborn: ${REBORN:-<none>} (read-only)"
|
|
docker run -d -i -t "${ARGS[@]}" "$IMAGE" \
|
|
"/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null
|
|
echo
|
|
echo " running detached as '$NAME'."
|
|
echo " ./sylph-port remote link to chat with it from anywhere"
|
|
echo " ./sylph-port logs -f follow it"
|
|
echo " ./sylph-port attach chat with it locally"
|
|
echo " ./sylph-port stop stop it"
|
|
;;
|
|
|
|
logs) shift; exec docker logs "$@" "$NAME" ;;
|
|
attach) exec docker attach "$NAME" ;;
|
|
stop) exec docker rm -f "$NAME" ;;
|
|
|
|
remote)
|
|
echo "waiting for the session to register" >&2
|
|
for _ in $(seq 60); do
|
|
url=$(docker exec "$NAME" sh -c \
|
|
'grep -ho "https://claude.ai/code/session_[A-Za-z0-9]*" \
|
|
/sylph-home/port/.claude/**/*.jsonl 2>/dev/null | tail -1' 2>/dev/null || true)
|
|
[ -n "$url" ] && { echo "$url"; exit 0; }
|
|
sleep 2
|
|
done
|
|
echo "no session link yet — try ./sylph-port logs -f" >&2
|
|
exit 1
|
|
;;
|
|
|
|
*)
|
|
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
|
|
;;
|
|
esac
|