agent: move the loop prompt's memory into the corpus, and let it push

The autonomous agent's loop prompt had grown to ~9 000 words of accumulated
findings, refutations and traps. That is a symptom, not a style: it was doing
the job the corpus should do, in the one place that dies with the container.
Three concrete failures followed from it, and each gets a structural fix rather
than a louder instruction.

1. IT REPEATED WORK IT HAD ALREADY DONE.
   The "do not revive" list and the method traps existed ONLY in the prompt, so
   nothing pointed a fresh iteration at them. Extracted verbatim into two
   tracked files:

     docs/re/REFUTED.md  105 claims tested and dead, grouped by subject so a
                         grep for your noun finds the neighbourhood
     docs/re/METHOD.md   the traps already paid for -- controls, inference,
                         searching, reading data, runtime

   Both are linked from INDEX.md, and the loop prompt now opens by requiring
   them to be read. This is the fix for "re-derived something already known":
   the knowledge is now where the next iteration looks, not in a context window.

2. IT FORGOT TO ARM THE NEXT WAKEUP.
   The prompt tried to solve this by shouting at itself in the first line. The
   real fix is to stop asking: `loose` now defaults to a FIXED interval (45m),
   so the harness owns the cadence and a forgotten ScheduleWakeup cannot end the
   run. SYLPH_LOOP_INTERVAL= (empty) restores self-pacing.

3. IT COULD NOT PUBLISH, SO THE WORK ONLY EXISTED IN THE CONTAINER.
   New `push-work`, plus a read-only credentials mount
   (SYLPH_GIT_CREDENTIALS, default ~/.sylph-git-credentials). It pushes the
   CURRENT branch only, refuses anything that is not auto/*, and never
   force-pushes -- so the consolidated line stays a human's decision and a
   confused iteration cannot rewrite history. The loop prompt now requires a
   push on every iteration that commits, rather than at the end of some longer
   arc, which is exactly when a container dies.

The prompt itself drops from ~9 000 words to 85 lines and, more importantly,
stops accumulating: findings go in the corpus, and the prompt points at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylpheed RE agent
2026-08-28 15:46:59 +02:00
parent 03ece95c06
commit e1dcc689bc
6 changed files with 453 additions and 31 deletions

67
docker/agent/bin/push-work Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Push the current topic branch to origin — the ONLY sanctioned way out of the
# container.
#
# Why a wrapper instead of plain `git push`:
#
# * **`main` and shared branches are refused.** The agent commits to
# `auto/<topic>`; a human merges. A token that can push anywhere is one
# confused iteration away from rewriting the consolidated line.
# * **Force-push is refused**, always. Nothing here needs it, and history
# rewriting is the one mistake that cannot be undone by merging.
# * It pushes the CURRENT branch only, by name, so a stray `--all` cannot
# publish another agent's worktree branch mid-experiment.
#
# Credentials come from a file mounted read-only at ~/.git-credentials (see
# `sylph-agent`). They are never printed, never logged, and never passed on a
# command line.
#
# push-work push the current branch
# push-work --dry-run say what it would do
set -euo pipefail
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
echo "push-work: not inside a git repository" >&2; exit 1; }
cd "$repo_root"
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "HEAD" ]; then
echo "push-work: detached HEAD — check out a branch first" >&2; exit 1
fi
case "$branch" in
auto/*) ;;
*)
echo "push-work: refusing to push '$branch'." >&2
echo " Only auto/* topic branches may leave the container; a human merges" >&2
echo " them into main. Move your work: git switch -c auto/<topic>" >&2
exit 1 ;;
esac
if [ ! -s "$HOME/.git-credentials" ]; then
echo "push-work: no credentials mounted at ~/.git-credentials." >&2
echo " The host must start the container with SYLPH_GIT_CREDENTIALS pointing" >&2
echo " at a file containing one line:" >&2
echo " https://<user>:<token>@git.mc02.dev" >&2
exit 1
fi
# `store` reads the file we mounted; nothing is written back (it is read-only).
git config --local credential.helper "store --file=$HOME/.git-credentials"
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
echo "push-work: $branch — $ahead commit(s) to publish"
if [ "$DRY" = 1 ]; then
echo "push-work: --dry-run, stopping here"
exit 0
fi
# --force-with-lease is deliberately NOT offered. If this is rejected as
# non-fast-forward, someone else moved the branch: fetch and merge, do not
# overwrite.
git push --set-upstream origin "$branch"
echo "push-work: pushed $branch"