docker: turn the agent loose — detached /loop, with the first-run gates handled

`./sylph-agent loose [task]` starts Claude Code detached with
--dangerously-skip-permissions, running /loop on loop-task.md: work the RE
backlog one item at a time, commit to auto/* branches, never push, record
withdrawn results rather than deleting them. `logs`/`attach`/`stop` to watch and
end it. Runs -d WITHOUT --rm so the transcript survives the container exiting —
for an unattended run that is the only record of what happened.

Two things had to be fixed for an agent to survive being left alone.

MEMORY CONTINUITY. The project is now bind-mounted twice: at /work, and at its
own host path. Claude Code derives its per-project state key from the working
directory, so running at /work handed the agent an empty project instead of the
accumulated one. Verified: a loose run now reports MEMORY=yes and reads back the
same branch and backlog as the host.

FOUR INTERACTIVE GATES, each a silent permanent hang with nobody at the keyboard
-- no error, no log line, just a container that looks healthy and does nothing:

  theme picker      hasCompletedOnboarding + lastOnboardingVersion. Re-fires
                    whenever the container's Claude Code is a different version
                    to the host's, which is the normal case.
  folder trust      projects.<path>.hasTrustDialogAccepted
  bypass disclaimer answered in a pty by bin/claude-autonomous. It has no config
                    key by design -- it wants a person to accept once, and the
                    person did so by launching this.
  fullscreen upsell fullscreenUpsellSeenCount. This one fires MID-SESSION, after
                    the pty wrapper has already handed over, so it cannot be
                    answered the same way.

Config key names were read out of the shipped binary's own strings, not guessed.

The pty wrapper matches SINGLE WORDS. Claude Code draws its UI with
absolute-column escapes between words, so the prompt arrives as
`Yes,\x1b[13GI\x1b[15Gaccept` and a multi-word pattern never matches -- failing
in a way indistinguishable from the wrapper not running at all. It stops
matching once the session is live so nothing later is answered by accident.

~/.claude.json is now mounted read-only at a staging path and copied in, so the
container cannot rewrite the host config. Credentials stay shared read-write in
~/.claude, which is what token refresh and memory continuity need.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-08-18 17:10:09 +02:00
parent 23fdf12194
commit 82a80e4063
7 changed files with 343 additions and 3 deletions

View File

@@ -10,8 +10,49 @@ the emulator, reads its guest memory and photographs its screen.
./sylph-agent doctor # prove it can do the four things it exists for
./sylph-agent shell # poke around
./sylph-agent agent # Claude Code, --dangerously-skip-permissions
./sylph-agent loose # turn it loose: detached, /loop, self-paced
./sylph-agent logs -f # watch it
./sylph-agent stop # stop it
```
## On the loose
`./sylph-agent loose` starts Claude Code **detached**, with
`--dangerously-skip-permissions`, running `/loop` on the task in
[`loop-task.md`](loop-task.md) — work the RE backlog one item at a time, commit
to `auto/*` branches, never push, record withdrawn results rather than deleting
them. Pass your own task as an argument, or set `SYLPH_LOOP_INTERVAL=30m` for a
fixed cadence instead of letting it self-pace.
It runs `-d` **without** `--rm`, so the transcript survives the container
exiting — for an unattended run that is the only record of what happened.
`./sylph-agent attach` joins the live session (Ctrl-P Ctrl-Q to leave it
running).
**It cannot push.** No git credentials are mounted, deliberately: a human
reviews before anything leaves the box. Review with
`git -C <project>/Syplheed-Reborn log --oneline main..auto/<topic>`.
### The four gates
Claude Code has four one-time prompts, and each one is a silent, permanent hang
for an agent with nobody at the keyboard — no error, no log line, just a
container that looks healthy and does nothing. All four are handled:
| gate | how |
|---|---|
| theme picker | `hasCompletedOnboarding` + `lastOnboardingVersion` in `~/.claude.json` |
| "do you trust this folder?" | `projects.<path>.hasTrustDialogAccepted` |
| Bypass Permissions disclaimer | answered in a pty by [`bin/claude-autonomous`](bin/claude-autonomous) — it has no config key, by design |
| fullscreen-renderer upsell | `fullscreenUpsellSeenCount`, because it fires *mid-session*, after the pty wrapper has handed over |
Config keys were read out of the shipped binary's own strings rather than
guessed. The pty wrapper matches **single words**: Claude Code draws its UI with
absolute-column escapes between words, so `Yes, I accept` arrives as
`Yes,\x1b[13GI\x1b[15Gaccept` and a multi-word pattern never matches — which
looks exactly like the wrapper not running at all. It stops matching once the
session is live, so nothing later can be answered by accident.
## The resource cap
The container gets **half the machine**, computed at launch so it stays half on
@@ -137,6 +178,16 @@ needs to write). **That directory also holds your memory and project state**, so
the container agent and you share it. Point `SYLPH_CLAUDE_HOME` at a separate
directory to isolate it, or set `ANTHROPIC_API_KEY` instead.
`~/.claude.json` is different: mounted **read-only** at a staging path and
copied in, so the container cannot rewrite your host config — and so a version
skew between the container's Claude Code and yours cannot re-trigger onboarding.
The project is bind-mounted **twice**, at `/work` and at its own host path. The
host path is what makes memory carry over: Claude Code derives its per-project
state key from the working directory, so running at `/work` would hand the agent
an empty project instead of the accumulated one. Verified — a loose run reports
`MEMORY=yes` and reads back the same branch and backlog you see.
## Host prerequisites
* **A Vulkan SDK** (LunarG), for *building* only. Canary's shader step calls