Repository navigation
Conversation
5 tasks done
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical defects can leave undone harvest work unrestored with --again and allow stop to report success before a concurrent start finishes launching agents.
4 open issues
What changed in this PR
Implements the retirement and cleanup stage of #115 and #125, preserving agent work before removing engagement resources.
Changes:
- Adds persistent workspace volumes and explicit retirement and cleanup with verified recovery refs.
- Strengthens engagement identity checks, harvest bookkeeping, and protection for unshipped commits.
- Updates lifecycle documentation, dashboard status, and unit and Docker runtime coverage.
| File | Description |
|---|---|
| USAGE.md | Documents lifecycle commands, safeguards, and recovery procedures. |
| tests/test.sh | Extends runtime coverage and rejects incomplete unit runs. |
| tests/test_session_end_push.sh | Tests unshipped commits and scratch-push recovery. |
| tests/test_runtime.sh | Updates legacy-engagement refusal checks. |
| tests/test_harvest.sh | Tests base-aware merging and harvest recovery bookkeeping. |
| tests/test_dashboard.sh | Tests engagement-bound actions and retained-volume status. |
| tests/test_ci_workflows.sh | Verifies unit tests must reach their summaries. |
| tests/runtime_signal_trap.sh | Adapts stop checks to engagement state snapshots. |
| tests/runtime_retire.sh | Adds real-Docker retirement and interruption coverage. |
| tests/runtime_pi.sh | Fixes fixture ownership and runtime resource cleanup. |
| tests/runtime_cleanup.sh | Adds real-Docker preservation and repeated-engagement coverage. |
| tests/_test_env.sh | Adds a shared SIGINT capability check. |
| README.md | Explains workspace persistence and the cleanup lifecycle. |
| lib/preserve.sh | Adds containment checks, recovery imports, and verification. |
| lib/harness.sh | Protects unshipped commits before workspace resets. |
| harvest.sh | Adds base-aware merges, recorded progress, and --again. |
| dashboard.sh | Shows retained volumes and binds stop/post-process actions to engagements. |
| CHANGELOG.md | Records preservation features and lifecycle compatibility changes. |
Review effort: Balanced
…checkout before deleting it
…nly on a clean runtime
…pying the object store
orsissimo
force-pushed
the
feat/safe-retirement
branch
from
November 6, 2026 20:07
d14369a to
0d5e973
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Step 2 of the split proposed in #115, "safe replacement, retirement, cleanup, preservation refs", based on
0782f33. This implements invariants 6 and 7 from #125 and the cleanup part of invariant 3.launch.sh retire [--post-process] [--discard-uncommitted]removes stopped containers by ID after checking them; their volumes stay.launch.sh cleanup [--discard-uncommitted] [--allow-unharvested]take the engagement lock and refuse while the bare repo holds agent work thatHEAD's history lacks. Harvest first, or use--allow-unharvestedto keep work left by a rewritten upstream as recovery refs only.git fsck --full. This includes refs, reflogs, objects, stashes, unfinished operations and git-dir files; work reported as uncommitted requires--discard-uncommittedto be dropped.startwhile an engine container, workspace volume or engagement runtime remains. Removedocker rm -ffrom engine container replacement, thealpinehelper fromlaunch.sh, andrm -rfrecipes from the docs.tests/test_launch.sh,tests/test_retire.shandtests/test_preserve.sh.Choices for step 2
@moodmosaic, I chose option B for step 2. Previously, a stopped container kept the workspace in its own layer, so removing it could lose commits that never reached the bare repo. Option A was to copy each stopped container's Git metadata out with
docker cpfirst. With option B, each engine container's/workspaceis a named Docker volume labeled with the project, checkout, engagement and a per-engagement token.retireleaves that work in place;cleanupinspects every workspace with real Git in a helper container before deleting anything.state.jsonrecordsbase_commiton the host, and harvest uses it instead of the bare repo'srefs/swarm/base, which agents can write. Recovery refs live in the checkout underrefs/swarm-recovery/...; the engine never deletes them. What do you think of this approach, and does it work for you? Would you prefer the three small fixes for temporary files, Bash 3.2 exit status andruntime_pi.shon UID != 1000 in a separate PR?Changes
af7958d: addlib/preserve.shchecks thatHEADcontains the bare repo'sagent-work,swarm/*andagent-parked/*tips, plusrefs/swarm-runs/reserved for Multi-Agent Post-Process Proposal #112.4ce87d8: start the bare repo from the checkout'sHEAD, remove old agent branch refs and recordbase_commitin the state file.cab56ba: makelib/harness.shpush unshipped commits before the loop resets the workspace; if commits remain unshipped, exit without resetting.b6baa96: record interactive sessions before releasing the lock, keep the bare repo and mirrors while a session is open, and add only missing mirrors when joining.ef7e8ff: add recovery bundles inlib/preserve-scan.shandlib/preserve.sh, with object inventories and checks that imported refs retain every object.2fd92fe: addlaunch.sh retire. Check all selected containers in snapshots before removing any; require stopped containers with their own workspace volumes and leave containers that changed since inspection.d044c23: createlaunch.shtemporary files inTMPDIRor/tmpand remove them on exit, including failures and interrupts.2cb1328: addlaunch.sh cleanupandlib/cleanup.sh. Scan and preserve repository locations before deletion; checkpoint interrupted cleanup and recheck its recovery refs on retry.--discard-uncommitteddrops exactly the work listed.b1b76e1: fixtests/runtime_pi.shfor Linux host UIDs other than 1000: read its auth fixture before changing ownership and remove agent-owned bare-repo files through the test image.b55fb82: create labeled workspace volumes, record the Docker daemon and engagement token, and require a clean runtime for new engagements. Bind lifecycle commands to that engagement; addstop --numberedandSWARM_WAIT_POLL(default 10 seconds betweenwaitchecks).2b05163: return failure when Bash 3.2 stopslaunch.sh, retire or cleanup on an unset variable; fail unit test files that exit without their summary.3341839: letharvest.shmerge work based on the recorded engagement base afterHEADmoves on. Record completed and pending merges; add--againfor an undone or abandoned harvest, still requiring the base inHEAD's history.30ed5a0: showvolume onlyand unfinished cleanup indashboard.sh.5b660de: wrap long lines inUSAGE.mdandCHANGELOG.md.c68cb53: host reads of the bare repo and the mirrors never fetch and never check signatures. Preservation scans read no global or system Git config, and stop before reading a work tree when a repository's config sets a filter driver or includes other files. Cap what a retire or cleanup helper writes to any one host file withSWARM_HELPER_OUTPUT_LIMIT_MB(default 16384 MB), keep no container log, and check scan archives before extraction.1073247: preserve Git checkouts outside workspace volumes as recovery refs before retiring containers. Addretire --discard-uncommittedandSWARM_RETIRE_LAYER_LIMIT_MB(default 100 MB of bundles per container); retire still refuses bare repositories and separate git dirs outside volumes.c1d5f69: clone bare repos and mirrors withgit clone --bare --no-localso objects held only by earlier recovery refs are not copied into each new engagement. Each mirror keeps the base's recorded submodule commit underrefs/swarm/gitlink, fetching it by ID when needed; warn if the source lacks it.2fc3b60: after the Git 2.56.0 CI failure, make the swapped loose files test expect a hash-mismatch stop or both files stored, based on the copy Git reads.5c55c16: also unsetSWARM_ENGAGEMENT_TOKENintests/_test_env.shso a token in the caller's environment does not reach the tests.76fa1be: let cleanup accept an ignored.claude/in the workspace's own checkout when it holds only the engine'ssettings.local.json. Anything else there, and any other ignored directory, is still reported.bf49b79: makeharvest.sh --againalso merge recorded commits by ID whenHEADlacks them and no branch of the bare repo brings them back, including after an interactive branch is deleted oragent-workis moved back. Each commit must exist locally and be built on the engagement's base; it stays recorded until merged, including across a conflict or an interrupted run.0d5e973:stopfirst stops what runs without the engagement lock, so a post-process holding it stays stoppable, then waits up toSWARM_STOP_TIMEOUTseconds for the lock and stops what a start launched meanwhile; it fails when the lock stays held.waitwaits up to a minute for the lock once the agents are done, andSWARM_STOP_TIMEOUTmust be whole seconds.Breaking changes for embedders
/workspacevolume. The workspace mount, engagement labels, container name,--restart noand--rm=falsecome afterdocker_args.post-processandinteractivejoining an engagement,stop,wait,retireandcleanuprefuse another daemon; commands keep the Docker context they started with.SWARM_ENGAGEMENT_TOKENbindsstop,post-process,interactiveandharvest.shto one engagement. A mismatch refuses; with that token supplied,post-processandinteractivecannot create a new engagement.harvest.sh, then./launch.sh cleanupbefore starting another. While cleanup has a checkpoint,start,post-process,interactive,stopandharvest.shrefuse until it finishes.stopstops containers by ID only after checking the engagement token, first without the engagement lock, then under it after waiting up toSWARM_STOP_TIMEOUTseconds for a start still launching agents; it fails when the lock stays held. An interactive session being started, and awaitthat already runs and goes on to its post-process, are not waited for.--numberedselects numbered agents only; the default includes post-process and interactive containers.post-processrefuses while numbered agents run or an earlier post-process container exists. Stop it if needed, then use./launch.sh retire --post-processbefore running it again.launch.shcommands other thanstatusandlogs, andharvest.sh, refuse them; finish them with the release that started them.HEADreach; mirrors also keep the base's recorded submodule commits when available. Other objects held only by remote-tracking refs, stashes, reflogs or recovery refs are no longer copied.SWARM_STOP_TIMEOUTmust be a whole number of seconds below 1000000; other values makestoprefuse before it stops anything.Self-review checklist
Test plan
Results on
0d5e973unless noted../tests/test.sh --unit: 3658 tests in 18 files passed on macOS 26.5.2 with/bin/bash3.2.57 and Git 2.50.1; 3663 passed on Ubuntu 24.04 arm64 with Bash 5.2.21 and Git 2.43.0, as UID 501 and UID 1000; 3654 passed on Ubuntu 24.04 with Git 2.56.0../tests/test.sh --runtime: signal trap 16/16,runtime_retire.sh83/83 andruntime_cleanup.sh33/33 on OrbStack with Docker 29.8.2 and on Ubuntu 24.04 with Docker 29.1.3, as UID 501 and UID 1000.shellcheck -s bash --severity=warningis clean. Every commit was reviewed and tested on its own before it was committed.retire --post-process, post-process again, harvest and refusal to start before cleanup.retirenames and refuses an untracked file;retire --discard-uncommittedpreserves both scratch repositories as recovery refs. Cleanup leaves no container, volume or bare repo;git fsck --fullpasses. Recovery refs per engagement: 515 and 527 on every host.retirenames an untracked log in a test fixture repository under/tmp;--discard-uncommittedpreserves its history without the log. On macOS,cleanupalso refuses an uncommitted edit left in a volume;--discard-uncommitteddrops only that file.git fsck --fullpasses; recovery refs grow by 1,100 to 1,300 per engagement (on3a40a98).statusworked;start,post-process,stop,harvest.sh,retireandcleanuprefused with an explanation and left its containers, state file and checkout unchanged (on3a40a98).git clone --bare --no-localtakes the same 0.8 s as the old copy and creates a 21 MB bare repo instead of 905 MB (on3a40a98).stopas soon as its first agent runs, three times per host on macOS/OrbStack and on Ubuntu as UID 501:stopreturned only afterstarthad finished launching, with no container of the engagement left running. With the engagement lock held by another process,stopwithSWARM_STOP_TIMEOUT=5stopped what ran, failed and said why, then succeeded once the lock was free../tests/test.sh --all.