Repository navigation
feat(tmux): restore sessions, programs and conversations after a reboot - #112
Conversation
A reboot lost window names, panes and programs. Two causes stacked. The client-detached save ran while shutdown was killing the server and wrote a snapshot of bare shells, and resurrect pointed `last` at it. Separately, resurrect replays only the commands it is told to, and the Claude hooks resumed every conversation with one fixed set of flags. tmux-restore now rewrites each pane's saved command into what restarts it: an agent's resume command with that pane's own flags, a named program, or the command typed back on the prompt. A user unit saves on logind's PrepareForShutdown under a delay lock, before anything is stopped. tmux-snapshots points `last` at the newest whole snapshot before every restore, thins old snapshots by hour and day, and lists and restores them. continuum restores at server start, so a bare `tmux` is the cold start. The resurrect fork runs one restore at a time. pluginsync now clones a plugin again when its checkout came from another repository or branch than tmux.conf names, since TPM counts a plugin installed by directory alone. sesh goes. It named sessions after directories and held the restore wrapper, and tmux's own choose-tree and switch-client -l take its keys. claude-resurrect keeps only `resume`, which reads the warehouse's copy. Agent-Name: claude-code Agent-Session: 378edfa1-ac93-4f22-94c0-1946cef0b677 Worklog: 3328.1
datapointchris
left a comment
There was a problem hiding this comment.
PR review — system-design
Used: Cloned PR 112 at head. Read the unit file, install/system.yml, tmux.conf plugin lines, tmux-snapshots, the pluginsync diff and the docs. Fetched the far-side shutdown-save.sh from andrewboldi/tmux-restore. Did not run the unit or any apply.
Standards: No standards searched; the findings rest on the far-side script and systemd behaviour.
Findings: 2 correctness · 1 design · 0 prose
Correctness
system-design.1 configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:12 · install/system.yml:103
Reach: A fresh provision or a machine where TPM has not yet cloned tmux-restore. system.yml enables and starts the unit (enabled: true, active: true). ExecStart points at ~/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh, which only TPM creates. If systemd runs the unit before the clone exists, it fails with 203/EXEC. Restart=on-failure with RestartSec=5 retries. After the default StartLimitBurst the unit sits in 'failed'. Nothing restarts it, and the next reboot is saved by nothing but continuum. A reboot with no attached client then loses the sessions, which is the bug this PR fixes. The far-side script also ends in exit 1 when gdbus monitor dies, which feeds the same loop.
The unit depends on a path that a different step creates, and no ordering ties the two together. Fix: add ConditionPathExists=%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh to [Unit], so the unit is skipped, not failed. Run systemctl --user start for it after the TPM sync step. Add StartLimitIntervalSec=0, or a longer RestartSec, so a transient failure cannot park it as failed.
system-design.2 configs/common/.config/tmux/tmux.conf:403 · configs/common/.config/tmux/tmux.conf:404 · src/dotfiles/providers/pluginsync.py:289 · configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:12
Reach: A new machine, or any machine where sync_tmux's new replaced() rmtree's a checkout, runs TPM install_plugins. TPM clones whatever andrewboldi/tmux-resurrect#fix-concurrent-restore and the default branch of andrewboldi/tmux-restore point to that day. tmux-restore's shutdown-save.sh runs as a long-lived user service and calls the save script. Both plugins also run as the user in every tmux server. The account owner can push to those branches. A force-push, or a compromise of that account, becomes code running as the user on every login and reboot. Nothing pins a commit.
The PR moves the whole save and restore path from tmux-plugins/* (unchanged since 2023/2024) to two single-maintainer forks. TPM tracks a branch name only, and the tmux-restore line has no ref at all. Docs say a box keeps its first commit, but replaced() now deletes and reclones on any origin or branch mismatch, so a re-provision picks up the branch tip. The unit also puts that code in a systemd service, which is a new, always-on process. Fix: pin both lines to a full commit SHA, since TPM accepts #<sha>. Alternatively vendor the shutdown script into this repo and have the unit run that copy.
Design and architecture
system-design.3 docs/architecture/tmux-sessions.md:216 · configs/common/.config/tmux/tmux.conf:420 · configs/common/.config/tmux/tmux.conf:427
Reach: A shutdown on a slow disk or a heavy session count. logind's InhibitDelayMaxSec defaults to 5 s, and the PR leaves it unchanged. The inhibitor is released only when the far-side script exits, after save.sh returns. save.sh runs the post-save-layout hook (forget-boards) and the post-save-all hook (tmux-snapshots prune, which awk-scans the snapshots) before it returns. With pane-contents capture on, a save that runs past 5 s is cut off. That leaves the cut-off snapshot the PR describes, and repair-last then falls back to an older whole one, so the user loses the last minutes of state without any signal.
The only measurement is one save of 14 panes taking about 1 s, and it excludes prune. Fix: raise InhibitDelayMaxSec in a logind.conf.d drop-in through install/system.yml. Alternatively run prune only from continuum's periodic saves, not on the shutdown save, so the lock window holds just the save.
Findings as JSON
{
"used": "Cloned PR 112 at head. Read the unit file, install/system.yml, tmux.conf plugin lines, tmux-snapshots, the pluginsync diff and the docs. Fetched the far-side shutdown-save.sh from andrewboldi/tmux-restore. Did not run the unit or any apply.",
"standards": "No standards searched; the findings rest on the far-side script and systemd behaviour.",
"findings": [
{
"number": "system-design.1",
"section": "correctness",
"sites": [
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:12",
"install/system.yml:103"
],
"reach": "A fresh provision or a machine where TPM has not yet cloned tmux-restore. system.yml enables and starts the unit (enabled: true, active: true). ExecStart points at ~/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh, which only TPM creates. If systemd runs the unit before the clone exists, it fails with 203/EXEC. Restart=on-failure with RestartSec=5 retries. After the default StartLimitBurst the unit sits in 'failed'. Nothing restarts it, and the next reboot is saved by nothing but continuum. A reboot with no attached client then loses the sessions, which is the bug this PR fixes. The far-side script also ends in `exit 1` when gdbus monitor dies, which feeds the same loop.",
"body": "The unit depends on a path that a different step creates, and no ordering ties the two together. Fix: add `ConditionPathExists=%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh` to `[Unit]`, so the unit is skipped, not failed. Run `systemctl --user start` for it after the TPM sync step. Add `StartLimitIntervalSec=0`, or a longer `RestartSec`, so a transient failure cannot park it as failed."
},
{
"number": "system-design.2",
"section": "correctness",
"sites": [
"configs/common/.config/tmux/tmux.conf:403",
"configs/common/.config/tmux/tmux.conf:404",
"src/dotfiles/providers/pluginsync.py:289",
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:12"
],
"reach": "A new machine, or any machine where sync_tmux's new `replaced()` rmtree's a checkout, runs TPM install_plugins. TPM clones whatever `andrewboldi/tmux-resurrect#fix-concurrent-restore` and the default branch of `andrewboldi/tmux-restore` point to that day. tmux-restore's shutdown-save.sh runs as a long-lived user service and calls the save script. Both plugins also run as the user in every tmux server. The account owner can push to those branches. A force-push, or a compromise of that account, becomes code running as the user on every login and reboot. Nothing pins a commit.",
"body": "The PR moves the whole save and restore path from tmux-plugins/* (unchanged since 2023/2024) to two single-maintainer forks. TPM tracks a branch name only, and the tmux-restore line has no ref at all. Docs say a box keeps its first commit, but `replaced()` now deletes and reclones on any origin or branch mismatch, so a re-provision picks up the branch tip. The unit also puts that code in a systemd service, which is a new, always-on process. Fix: pin both lines to a full commit SHA, since TPM accepts `#<sha>`. Alternatively vendor the shutdown script into this repo and have the unit run that copy."
},
{
"number": "system-design.3",
"section": "design",
"sites": [
"docs/architecture/tmux-sessions.md:216",
"configs/common/.config/tmux/tmux.conf:420",
"configs/common/.config/tmux/tmux.conf:427"
],
"reach": "A shutdown on a slow disk or a heavy session count. logind's InhibitDelayMaxSec defaults to 5 s, and the PR leaves it unchanged. The inhibitor is released only when the far-side script exits, after save.sh returns. save.sh runs the post-save-layout hook (forget-boards) and the post-save-all hook (`tmux-snapshots prune`, which awk-scans the snapshots) before it returns. With pane-contents capture on, a save that runs past 5 s is cut off. That leaves the cut-off snapshot the PR describes, and repair-last then falls back to an older whole one, so the user loses the last minutes of state without any signal.",
"body": "The only measurement is one save of 14 panes taking about 1 s, and it excludes prune. Fix: raise `InhibitDelayMaxSec` in a logind.conf.d drop-in through install/system.yml. Alternatively run prune only from continuum's periodic saves, not on the shutdown save, so the lock window holds just the save."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — failure-paths
Used: Checked out the PR head. Read the diff, TPM's install_plugins.sh and tmux-restore's shutdown-save.sh. Ran tmux-snapshots repair-last in my own HOME against cut-off, dangling-last and read-only fixtures. I did not run sync_tmux, because it needs a live TPM and network.
Standards: fleet standards search "destructive removal before replacement clone fails" returned code-quality.md "A duplicated implementation copies the originals failure order... Validate before removing" and infrastructure.md "A downloaded artifact is verified before it replaces the installed one". The second is the closest match to the finding.
Findings: 3 correctness · 0 design · 0 prose
Correctness
failure-paths.1 src/dotfiles/providers/pluginsync.py:289 · src/dotfiles/providers/pluginsync.py:290
Reach: A box that deploys this PR and holds the old tmux-plugins/tmux-resurrect or tmux-continuum checkouts runs sync_tmux. replaced() flags the resurrect checkout, because the new tmux.conf names the andrewboldi fork. sync_tmux then calls shutil.rmtree on it before TPM runs. A network outage, a deleted fork branch or a GitHub rate limit makes TPM's clone fail. TPM exits nonzero then; I read install_plugins.sh, whose exit_value_helper carries the failure. I did not run it. sync_tmux returns COMMAND_FAILED, but the working checkout is already gone. The same happens if rmtree raises on a permission error. The exception is uncaught and leaves the tree half deleted. tmux then starts with no resurrect, and the new continuum and tmux-restore setup has nothing to save or restore. Local commits or edits in the removed checkout are lost without a prompt.
The sync deletes the working plugin before it knows the replacement can be cloned.
The removal runs first. The clone is TPM's, and it runs later and over the network. A failed clone leaves the box with no plugin.
Fix: move the old checkout aside, for example to <name>.replaced, instead of deleting it. Run TPM. On success, remove the moved copy. On failure, or on a missing directory afterwards, move it back and say so in the Result. Catch OSError from the move, and return a failed Result that names the plugin.
This follows code-quality.md § "A duplicated implementation copies the originals failure order, not only its result": validate before removing. infrastructure.md § "A downloaded artifact is verified before it replaces the installed one" is the closest match.
failure-paths.2 configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:5 · install/system.yml:99
Reach: unproven — run systemctl --user start tmux-shutdown-save.service on a box where ~/.config/tmux/plugins/tmux-restore is absent. The cause is the unit being enabled and active, and its ExecStart pointing into a TPM-cloned directory that exists only after TPM has run. The replaced() rmtree above creates the same state. The unit's Restart=on-failure with RestartSec=5 would then retry a missing binary every 5 s, which stays under systemd's default start-rate limit of 5 in 10 s.
The shutdown-save unit has no condition on its script and retries a missing one forever.
The unit is enabled and started by the installer. Its ExecStart is a file TPM clones later. When the file is absent, the unit fails with a 203 exec error and restarts every 5 s without end. The journal fills, and nothing says the shutdown save is not in place.
Fix: add ConditionPathExists=%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh to [Unit]. A box without the plugin then skips the unit instead of looping. Keep Restart=on-failure, which the script's own exit 1 needs when its gdbus monitor dies.
failure-paths.3 apps/common/tmux-snapshots:150 · apps/common/tmux-snapshots:152
Reach: Ran tmux-snapshots repair-last in my own HOME with every snapshot cut off. It printed "no snapshot ... is whole" on stderr and exited 0. It also printed an awk fatal error whenever last was dangling. The hook runs from tmux-resurrect's pre-restore-all. Its stderr is not shown to the user and the exit code is ignored. The restore goes on to read the cut-off last, and the user sees panes with no windows.
repair-last reports "no whole snapshot exists" as success, to a place nobody reads.
The only signal is a stderr line from a hook. Add tmux display-message on this path, as the repaired path already does. Make is_whole silent on a missing file, for example by testing [[ -f ]] first, so the dangling-last case does not print an awk fatal.
Findings as JSON
{
"used": "Checked out the PR head. Read the diff, TPM's install_plugins.sh and tmux-restore's shutdown-save.sh. Ran tmux-snapshots repair-last in my own HOME against cut-off, dangling-last and read-only fixtures. I did not run sync_tmux, because it needs a live TPM and network.",
"standards": "fleet standards search \"destructive removal before replacement clone fails\" returned code-quality.md \"A duplicated implementation copies the originals failure order... Validate before removing\" and infrastructure.md \"A downloaded artifact is verified before it replaces the installed one\". The second is the closest match to the finding.",
"findings": [
{
"number": "failure-paths.1",
"section": "correctness",
"sites": [
"src/dotfiles/providers/pluginsync.py:289",
"src/dotfiles/providers/pluginsync.py:290"
],
"reach": "A box that deploys this PR and holds the old `tmux-plugins/tmux-resurrect` or `tmux-continuum` checkouts runs `sync_tmux`. `replaced()` flags the resurrect checkout, because the new tmux.conf names the `andrewboldi` fork. `sync_tmux` then calls `shutil.rmtree` on it before TPM runs. A network outage, a deleted fork branch or a GitHub rate limit makes TPM's clone fail. TPM exits nonzero then; I read `install_plugins.sh`, whose `exit_value_helper` carries the failure. I did not run it. `sync_tmux` returns COMMAND_FAILED, but the working checkout is already gone. The same happens if `rmtree` raises on a permission error. The exception is uncaught and leaves the tree half deleted. tmux then starts with no resurrect, and the new continuum and tmux-restore setup has nothing to save or restore. Local commits or edits in the removed checkout are lost without a prompt.",
"body": "**The sync deletes the working plugin before it knows the replacement can be cloned.**\n\nThe removal runs first. The clone is TPM's, and it runs later and over the network. A failed clone leaves the box with no plugin.\n\n**Fix:** move the old checkout aside, for example to `<name>.replaced`, instead of deleting it. Run TPM. On success, remove the moved copy. On failure, or on a missing directory afterwards, move it back and say so in the Result. Catch `OSError` from the move, and return a failed Result that names the plugin.\n\nThis follows code-quality.md § \"A duplicated implementation copies the originals failure order, not only its result\": validate before removing. infrastructure.md § \"A downloaded artifact is verified before it replaces the installed one\" is the closest match."
},
{
"number": "failure-paths.2",
"section": "correctness",
"sites": [
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:5",
"install/system.yml:99"
],
"reach": "unproven — run `systemctl --user start tmux-shutdown-save.service` on a box where `~/.config/tmux/plugins/tmux-restore` is absent. The cause is the unit being `enabled` and `active`, and its `ExecStart` pointing into a TPM-cloned directory that exists only after TPM has run. The `replaced()` rmtree above creates the same state. The unit's `Restart=on-failure` with `RestartSec=5` would then retry a missing binary every 5 s, which stays under systemd's default start-rate limit of 5 in 10 s.",
"body": "**The shutdown-save unit has no condition on its script and retries a missing one forever.**\n\nThe unit is enabled and started by the installer. Its `ExecStart` is a file TPM clones later. When the file is absent, the unit fails with a 203 exec error and restarts every 5 s without end. The journal fills, and nothing says the shutdown save is not in place.\n\n**Fix:** add `ConditionPathExists=%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh` to `[Unit]`. A box without the plugin then skips the unit instead of looping. Keep `Restart=on-failure`, which the script's own `exit 1` needs when its gdbus monitor dies."
},
{
"number": "failure-paths.3",
"section": "correctness",
"sites": [
"apps/common/tmux-snapshots:150",
"apps/common/tmux-snapshots:152"
],
"reach": "Ran `tmux-snapshots repair-last` in my own HOME with every snapshot cut off. It printed \"no snapshot ... is whole\" on stderr and exited 0. It also printed an awk fatal error whenever `last` was dangling. The hook runs from tmux-resurrect's pre-restore-all. Its stderr is not shown to the user and the exit code is ignored. The restore goes on to read the cut-off `last`, and the user sees panes with no windows.",
"body": "**`repair-last` reports \"no whole snapshot exists\" as success, to a place nobody reads.**\n\nThe only signal is a stderr line from a hook. Add `tmux display-message` on this path, as the repaired path already does. Make `is_whole` silent on a missing file, for example by testing `[[ -f ]]` first, so the dangling-`last` case does not print an awk fatal."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — useless-test
Used: Cloned datapointchris/dotfiles, checked out PR 112, read the diff of src/ and tests/ beside pluginsync.py and registry.py, read the existing sync_tmux tests in tests/install/test_pluginsync.py and the tmux-sync rows in tests/matrix/test_toolchains_plugins.py, searched tests/ for every changed script and config, and ran the four added or edited tests (4 passed).
Standards: Applied testing.md/Test behavior the user cares about, not what this commit happened to change; pull-requests.md/Tests the diff adds are checked for whether they test the program or the change; testing.md/Never assert on rendered output — assert the value it was built from. No test the diff adds or edits, and no existing test over the changed code, takes a shape they name for deletion.
Findings: 0 correctness · 0 design · 0 prose
No findings.
Findings as JSON
{
"used": "Cloned datapointchris/dotfiles, checked out PR 112, read the diff of src/ and tests/ beside pluginsync.py and registry.py, read the existing sync_tmux tests in tests/install/test_pluginsync.py and the tmux-sync rows in tests/matrix/test_toolchains_plugins.py, searched tests/ for every changed script and config, and ran the four added or edited tests (4 passed).",
"standards": "Applied testing.md/Test behavior the user cares about, not what this commit happened to change; pull-requests.md/Tests the diff adds are checked for whether they test the program or the change; testing.md/Never assert on rendered output — assert the value it was built from. No test the diff adds or edits, and no existing test over the changed code, takes a shape they name for deletion.",
"findings": []
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — code-architecture
Used: I cloned the repo and checked out PR 112 at head 4b0c870. I read the whole diff, pluginsync.py, registry.py's tmux_plugins_dir, tmux-sessions and tmux.conf at head. I also cloned the two plugins the change pins, andrewboldi/tmux-resurrect#fix-concurrent-restore and andrewboldi/tmux-restore, and compared their helpers.sh, save.sh, tmux-restore.tmux, systemd/tmux-restore.service and install.sh with the copies this PR writes. I ran nothing against a live tmux, systemd or keyring.
Standards: I cited code-quality.md § "Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site", python.md § "Ask the library where it wrote; never rebuild the path" and the ruling known-names-declared-once. A search for a rule on vendored unit files with hard-coded install paths found nothing closer.
Findings: 0 correctness · 2 design · 0 prose
Design and architecture
code-architecture.1 configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:10 · configs/common/.config/tmux/tmux.conf:377 · install/packages.yml:1507 · src/dotfiles/registry.py:919
Reach: There are two next changes. The first is moving TPM's install_dir in packages.yml, which the repo already spells two ways: ~/.tmux/plugins/tpm appears in tests/resolver/test_catalog.py:328. The second is any tmux-restore release that changes its shipped systemd/tmux-restore.service. In both cases the hand-copied unit has to be edited in step, and nothing flags it when it is not. The PR is already the second instance of this copy: the sesh() wrapper it deletes hard-coded $HOME/.config/tmux/plugins/tmux-resurrect/scripts/restore.sh in the same way.
The shutdown unit hard-codes the TPM plugins directory, a fourth owner of that path.
The new user unit hard-codes %h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh. Three sites already decide the plugins directory:
packages.yml's TPMinstall_dir.registry.tmux_plugins_dir, whose docstring derives the directory from that clone because "a constant here would be the same fact written twice, free to disagree".- tmux.conf's
XDG_CONFIG_HOME_TMUX_PLUGINS_DIR.
The unit is also a contract copy. It reproduces the unit tmux-restore ships at systemd/tmux-restore.service under a different name. Upstream's own unit says install.sh points this at the plugin's real location, and upstream renders it from $PLUGINS. Here a static file does that job.
A moved plugins directory, or a change to upstream's unit, leaves the declared unit pointing at a script that is not there. The Restart=on-failure loop then hides the failure.
This is code-quality.md § "Ask whatever owns a fact…", and it repeats the trap tmux_plugins_dir was written to close.
Fix: render the unit from the one owner. Let the systemd resource write ExecStart from tmux_plugins_dir(session) / 'tmux-restore/scripts/shutdown-save.sh', or from tmux-restore's own systemd/tmux-restore.service with that path substituted. Do not deploy a static copy from configs/.
code-architecture.2 apps/common/tmux-snapshots:36 · apps/common/tmux-snapshots:49 · apps/common/tmux-snapshots:92 · apps/common/tmux-snapshots:104 · apps/common/tmux-snapshots:129 · apps/common/tmux-snapshots:154 · apps/common/tmux-snapshots:172 · apps/common/tmux-snapshots:177 · apps/common/tmux-snapshots:191
Reach: The next change is any edit to @resurrect-dir. One case is spelling it with $HOSTNAME to keep per-host snapshots on a shared home, which resurrect documents. The other is a fork change to the file prefix or stamp. Either one silently splits what resurrect writes from what repair-last and prune read. This PR swaps upstream resurrect for a fork, so the owner of these facts is already moving. The copy also recurs: the sidecar_path() this PR deletes from claude-resurrect was an earlier copy of the same resolution, and it disagreed with resurrect by skipping ~/.tmux/resurrect.
tmux-snapshots re-derives resurrect's snapshot directory, file naming and last link, and its copy already disagrees with resurrect.
snapshot_dir() restates resurrect's resurrect_dir(). tmux-snapshots also rebuilds the tmux_resurrect_<stamp>.txt name at six sites and assumes resurrect's %Y%m%dT%H%M%S stamp for its age cutoffs.
The copy has already drifted. Resurrect's helpers.sh:99-106 expands $HOSTNAME in @resurrect-dir, and snapshot_dir() does not. With such a setting, repair-last and prune run against a directory resurrect never writes.
The owner is already within reach. restore_snapshot reads @resurrect-restore-script-path at line 143, and helpers.sh sits beside that script. It exports resurrect_dir, last_resurrect_file, RESURRECT_FILE_PREFIX and RESURRECT_FILE_EXTENSION.
This is code-quality.md § "Ask whatever owns a fact…" and python.md § "Ask the library where it wrote; never rebuild the path".
Fix: take the directory from the plugin rather than rebuilding it. Source $(dirname "$(tmux show-option -gqv @resurrect-restore-script-path)")/helpers.sh once. Then use resurrect_dir and the prefix and extension variables in place of snapshot_dir() and the literal tmux_resurrect_. Keep a fallback only for the case where resurrect is not loaded.
Findings as JSON
{
"used": "I cloned the repo and checked out PR 112 at head 4b0c8706. I read the whole diff, `pluginsync.py`, `registry.py`'s `tmux_plugins_dir`, `tmux-sessions` and tmux.conf at head. I also cloned the two plugins the change pins, `andrewboldi/tmux-resurrect#fix-concurrent-restore` and `andrewboldi/tmux-restore`, and compared their `helpers.sh`, `save.sh`, `tmux-restore.tmux`, `systemd/tmux-restore.service` and `install.sh` with the copies this PR writes. I ran nothing against a live tmux, systemd or keyring.",
"standards": "I cited `code-quality.md` § \"Ask whatever owns a fact, the function or enum that computes it, never duplicate its logic at a second site\", `python.md` § \"Ask the library where it wrote; never rebuild the path\" and the ruling `known-names-declared-once`. A search for a rule on vendored unit files with hard-coded install paths found nothing closer.",
"findings": [
{
"number": "code-architecture.1",
"section": "design",
"sites": [
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:10",
"configs/common/.config/tmux/tmux.conf:377",
"install/packages.yml:1507",
"src/dotfiles/registry.py:919"
],
"reach": "There are two next changes. The first is moving TPM's `install_dir` in `packages.yml`, which the repo already spells two ways: `~/.tmux/plugins/tpm` appears in `tests/resolver/test_catalog.py:328`. The second is any tmux-restore release that changes its shipped `systemd/tmux-restore.service`. In both cases the hand-copied unit has to be edited in step, and nothing flags it when it is not. The PR is already the second instance of this copy: the `sesh()` wrapper it deletes hard-coded `$HOME/.config/tmux/plugins/tmux-resurrect/scripts/restore.sh` in the same way.",
"body": "**The shutdown unit hard-codes the TPM plugins directory, a fourth owner of that path.**\n\nThe new user unit hard-codes `%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh`. Three sites already decide the plugins directory:\n- `packages.yml`'s TPM `install_dir`.\n- `registry.tmux_plugins_dir`, whose docstring derives the directory from that clone because \"a constant here would be the same fact written twice, free to disagree\".\n- tmux.conf's `XDG_CONFIG_HOME_TMUX_PLUGINS_DIR`.\n\nThe unit is also a contract copy. It reproduces the unit tmux-restore ships at `systemd/tmux-restore.service` under a different name. Upstream's own unit says `install.sh points this at the plugin's real location`, and upstream renders it from `$PLUGINS`. Here a static file does that job.\n\nA moved plugins directory, or a change to upstream's unit, leaves the declared unit pointing at a script that is not there. The `Restart=on-failure` loop then hides the failure.\n\nThis is `code-quality.md` § \"Ask whatever owns a fact…\", and it repeats the trap `tmux_plugins_dir` was written to close.\n\n**Fix:** render the unit from the one owner. Let the systemd resource write `ExecStart` from `tmux_plugins_dir(session) / 'tmux-restore/scripts/shutdown-save.sh'`, or from tmux-restore's own `systemd/tmux-restore.service` with that path substituted. Do not deploy a static copy from `configs/`."
},
{
"number": "code-architecture.2",
"section": "design",
"sites": [
"apps/common/tmux-snapshots:36",
"apps/common/tmux-snapshots:49",
"apps/common/tmux-snapshots:92",
"apps/common/tmux-snapshots:104",
"apps/common/tmux-snapshots:129",
"apps/common/tmux-snapshots:154",
"apps/common/tmux-snapshots:172",
"apps/common/tmux-snapshots:177",
"apps/common/tmux-snapshots:191"
],
"reach": "The next change is any edit to `@resurrect-dir`. One case is spelling it with `$HOSTNAME` to keep per-host snapshots on a shared home, which resurrect documents. The other is a fork change to the file prefix or stamp. Either one silently splits what resurrect writes from what `repair-last` and `prune` read. This PR swaps upstream resurrect for a fork, so the owner of these facts is already moving. The copy also recurs: the `sidecar_path()` this PR deletes from `claude-resurrect` was an earlier copy of the same resolution, and it disagreed with resurrect by skipping `~/.tmux/resurrect`.",
"body": "**tmux-snapshots re-derives resurrect's snapshot directory, file naming and `last` link, and its copy already disagrees with resurrect.**\n\n`snapshot_dir()` restates resurrect's `resurrect_dir()`. `tmux-snapshots` also rebuilds the `tmux_resurrect_<stamp>.txt` name at six sites and assumes resurrect's `%Y%m%dT%H%M%S` stamp for its age cutoffs.\n\nThe copy has already drifted. Resurrect's `helpers.sh:99-106` expands `$HOSTNAME` in `@resurrect-dir`, and `snapshot_dir()` does not. With such a setting, `repair-last` and `prune` run against a directory resurrect never writes.\n\nThe owner is already within reach. `restore_snapshot` reads `@resurrect-restore-script-path` at line 143, and `helpers.sh` sits beside that script. It exports `resurrect_dir`, `last_resurrect_file`, `RESURRECT_FILE_PREFIX` and `RESURRECT_FILE_EXTENSION`.\n\nThis is `code-quality.md` § \"Ask whatever owns a fact…\" and `python.md` § \"Ask the library where it wrote; never rebuild the path\".\n\n**Fix:** take the directory from the plugin rather than rebuilding it. Source `$(dirname \"$(tmux show-option -gqv @resurrect-restore-script-path)\")/helpers.sh` once. Then use `resurrect_dir` and the prefix and extension variables in place of `snapshot_dir()` and the literal `tmux_resurrect_`. Keep a fallback only for the case where resurrect is not loaded."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — stale-prose
Used: I cloned PR 112 at its head (4b0c870). I read every added sentence in the diff next to the code at the head. To check the outside claims I cloned three plugins into my directory: andrewboldi/tmux-restore, tmux-plugins/tmux-continuum, and the andrewboldi/tmux-resurrect fork on fix-concurrent-restore. I ran tmux-snapshots list, prune and repair-last against 15 made-up snapshots. Those runs used a sandboxed HOME and TMUX_TMPDIR inside my directory with TMUX unset. prune left one whole snapshot per hour past a day and one per day past a week. repair-last moved last off a cut-off snapshot. The prose about pruning and repair says the same as what the script did.
Standards: pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite; documentation.md/A comment describes the code as it is, never the change or history that produced it; documentation.md/A doc points at the source instead of restating its content or constants, and the pointer is written with the source open; documentation.md/A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant
Findings: 0 correctness · 0 design · 3 prose
Prose
stale-prose.1 configs/common/.config/tmux/tmux.conf:425 · docs/architecture/tmux-sessions.md:227
The 90-day retention is written out in two places, but tmux.conf line 428 owns it. This is the shape a value the code holds. The setting is set -g @resurrect-delete-backup-after '90'. Change that number and both sentences below go wrong without anything flagging them.
configs/common/.config/tmux/tmux.conf:425: "prune thins the snapshots after each save, and resurrect deletes past 90 days." The option it describes sits three lines further down.docs/architecture/tmux-sessions.md:227: "and one a day after that, to 90 days."
The diff already gets this right in one place. apps/common/tmux-snapshots:13 says "resurrect deletes what is older than @resurrect-delete-backup-after". That points at the option without copying its value.
Rewrites:
- tmux.conf:425 → "prune thins the snapshots after each save, and resurrect deletes what is older than the option below."
- tmux-sessions.md:227 → "and one a day after that, until resurrect deletes it at
@resurrect-delete-backup-after."
stale-prose.2 docs/architecture/tmux-sessions.md:234 · docs/architecture/tmux-sessions.md:217 · docs/architecture/tmux-sessions.md:227 · docs/architecture/tmux-sessions.md:255 · configs/trust/fleet/.config/tmux/restored-programs.conf:2 · configs/common/.config/tmux/tmux.conf:409
Six sentences state measurements or another repo's current state, and nothing keeps them up to date. This is the shape will go stale. Each is true today. None names the command that produced it, and none would change when the outside thing changes.
docs/architecture/tmux-sessions.md:234: "upstream resurrect and continuum have had no commit since 2023 and 2024." I checked both. The last continuum commit is dated 2024-01-20. Upstream resurrect's newest commit on the fork's base is dated 2023-03-06. The sentence goes wrong the day either repo gets a commit. Rewrite: delete it. The fork is already justified by the PR it waits on,tmux-plugins/tmux-resurrect#600.docs/architecture/tmux-sessions.md:217: "A save of 14 panes, each holding 3,000 lines of scrollback, takes about a second." This was measured once, and no command sits beside it. Rewrite: "The save has to finish inside that window. With@resurrect-capture-pane-contentson, scrollback is most of its cost." Alternatively, keep the number and add the command that measures it.docs/architecture/tmux-sessions.md:227: "They are a few kilobytes each, so the thinning is for reading the list rather than for space." Rewrite: "The thinning is for reading the list rather than for space."docs/architecture/tmux-sessions.md:255: "and in a month of snapshots one long-lived session came from it." Rewrite: delete it. The sentence before it already gives the reason: "It names a session after a directory, where the model names it after the work."configs/trust/fleet/.config/tmux/restored-programs.conf:2: "Each ran in a pane for hours in a month of snapshots." This was counted once, and new programs will be added to the line without being counted. Rewrite: "Each is a long-running viewer that is safe to start again."configs/common/.config/tmux/tmux.conf:409: "beyond resurrect's defaults (nvim, less, tail, top, ...) and tmux-restore's (btop, log followers)." The lists copy two other repos. resurrect's list lives inscripts/variables.shasdefault_proc_list. tmux-restore's lives inscripts/save-hook.sh, and today it also has nvtop, iotop and nethogs. Rewrite: "Programs started again on restore, beyond the ones resurrect and tmux-restore already restart."
stale-prose.3 configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:6 · docs/architecture/tmux-sessions.md:254
Two sentences describe this repo's earlier setup rather than the files as they are. This is the shape the change narrated. The rejected-alternatives section of the architecture doc already records the history. These two clauses repeat it in places where the current sentence works without them.
configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:6-7: "A hook inside tmux fires only as the server is torn down, and saved a cut-off snapshot." The past-tense "saved" refers to theclient-detachedhook, which this diff deleted. Rewrite: "A hook inside tmux fires only as the server is torn down, too late to save it whole."docs/architecture/tmux-sessions.md:254: "Rejected: sesh, which this layout used for a while." The clause "which this layout used for a while" is the repo's own history. The rejection works without it. Rewrite: "Rejected: sesh." followed by the existing reasons.
Findings as JSON
{
"used": "I cloned PR 112 at its head (4b0c8706). I read every added sentence in the diff next to the code at the head. To check the outside claims I cloned three plugins into my directory: andrewboldi/tmux-restore, tmux-plugins/tmux-continuum, and the andrewboldi/tmux-resurrect fork on fix-concurrent-restore. I ran `tmux-snapshots list`, `prune` and `repair-last` against 15 made-up snapshots. Those runs used a sandboxed HOME and TMUX_TMPDIR inside my directory with TMUX unset. prune left one whole snapshot per hour past a day and one per day past a week. repair-last moved `last` off a cut-off snapshot. The prose about pruning and repair says the same as what the script did.",
"standards": "pull-requests.md/Sentences the diff adds are checked for what goes stale, and the finding carries the rewrite; documentation.md/A comment describes the code as it is, never the change or history that produced it; documentation.md/A doc points at the source instead of restating its content or constants, and the pointer is written with the source open; documentation.md/A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant",
"findings": [
{
"number": "stale-prose.1",
"section": "prose",
"sites": [
"configs/common/.config/tmux/tmux.conf:425",
"docs/architecture/tmux-sessions.md:227"
],
"reach": "",
"body": "**The 90-day retention is written out in two places, but tmux.conf line 428 owns it.** This is the shape **a value the code holds**. The setting is `set -g @resurrect-delete-backup-after '90'`. Change that number and both sentences below go wrong without anything flagging them.\n\n- `configs/common/.config/tmux/tmux.conf:425`: \"prune thins the snapshots after each save, and resurrect deletes past 90 days.\" The option it describes sits three lines further down.\n- `docs/architecture/tmux-sessions.md:227`: \"and one a day after that, to 90 days.\"\n\nThe diff already gets this right in one place. `apps/common/tmux-snapshots:13` says \"resurrect deletes what is older than @resurrect-delete-backup-after\". That points at the option without copying its value.\n\nRewrites:\n- tmux.conf:425 → \"prune thins the snapshots after each save, and resurrect deletes what is older than the option below.\"\n- tmux-sessions.md:227 → \"and one a day after that, until resurrect deletes it at `@resurrect-delete-backup-after`.\""
},
{
"number": "stale-prose.2",
"section": "prose",
"sites": [
"docs/architecture/tmux-sessions.md:234",
"docs/architecture/tmux-sessions.md:217",
"docs/architecture/tmux-sessions.md:227",
"docs/architecture/tmux-sessions.md:255",
"configs/trust/fleet/.config/tmux/restored-programs.conf:2",
"configs/common/.config/tmux/tmux.conf:409"
],
"reach": "",
"body": "**Six sentences state measurements or another repo's current state, and nothing keeps them up to date.** This is the shape **will go stale**. Each is true today. None names the command that produced it, and none would change when the outside thing changes.\n\n- `docs/architecture/tmux-sessions.md:234`: \"upstream resurrect and continuum have had no commit since 2023 and 2024.\" I checked both. The last continuum commit is dated 2024-01-20. Upstream resurrect's newest commit on the fork's base is dated 2023-03-06. The sentence goes wrong the day either repo gets a commit. Rewrite: delete it. The fork is already justified by the PR it waits on, `tmux-plugins/tmux-resurrect#600`.\n- `docs/architecture/tmux-sessions.md:217`: \"A save of 14 panes, each holding 3,000 lines of scrollback, takes about a second.\" This was measured once, and no command sits beside it. Rewrite: \"The save has to finish inside that window. With `@resurrect-capture-pane-contents` on, scrollback is most of its cost.\" Alternatively, keep the number and add the command that measures it.\n- `docs/architecture/tmux-sessions.md:227`: \"They are a few kilobytes each, so the thinning is for reading the list rather than for space.\" Rewrite: \"The thinning is for reading the list rather than for space.\"\n- `docs/architecture/tmux-sessions.md:255`: \"and in a month of snapshots one long-lived session came from it.\" Rewrite: delete it. The sentence before it already gives the reason: \"It names a session after a directory, where the model names it after the work.\"\n- `configs/trust/fleet/.config/tmux/restored-programs.conf:2`: \"Each ran in a pane for hours in a month of snapshots.\" This was counted once, and new programs will be added to the line without being counted. Rewrite: \"Each is a long-running viewer that is safe to start again.\"\n- `configs/common/.config/tmux/tmux.conf:409`: \"beyond resurrect's defaults (nvim, less, tail, top, ...) and tmux-restore's (btop, log followers).\" The lists copy two other repos. resurrect's list lives in `scripts/variables.sh` as `default_proc_list`. tmux-restore's lives in `scripts/save-hook.sh`, and today it also has nvtop, iotop and nethogs. Rewrite: \"Programs started again on restore, beyond the ones resurrect and tmux-restore already restart.\""
},
{
"number": "stale-prose.3",
"section": "prose",
"sites": [
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:6",
"docs/architecture/tmux-sessions.md:254"
],
"reach": "",
"body": "**Two sentences describe this repo's earlier setup rather than the files as they are.** This is the shape **the change narrated**. The rejected-alternatives section of the architecture doc already records the history. These two clauses repeat it in places where the current sentence works without them.\n\n- `configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:6-7`: \"A hook inside tmux fires only as the server is torn down, and saved a cut-off snapshot.\" The past-tense \"saved\" refers to the `client-detached` hook, which this diff deleted. Rewrite: \"A hook inside tmux fires only as the server is torn down, too late to save it whole.\"\n- `docs/architecture/tmux-sessions.md:254`: \"*Rejected:* sesh, which this layout used for a while.\" The clause \"which this layout used for a while\" is the repo's own history. The rejection works without it. Rewrite: \"*Rejected:* sesh.\" followed by the existing reasons."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — contracts
Used: I cloned the PR head and read the full diff. I searched all 115 checkouts in repos-registry for callers of every removed or renamed surface: claude-resurrect save|restore, sesh, tm, sesh.toml, prefix s/prefix L, client-detached and the resurrect hook options. I took the old and new hook lines from tmux.conf and loaded both in turn into a throwaway tmux server on a socket in my own directory. I ran claude-resurrect restore at head. I ran tmux-snapshots list, repair-last, list --json, restore with no stamp, and the bare command against a scratch HOME holding one whole snapshot and one cut-off snapshot. I cloned andrewboldi/tmux-restore and the andrewboldi/tmux-resurrect fix-concurrent-restore branch to check the hook chaining, the user agents directory, shutdown-save.sh, and the claude_session/claude_resume/proc_* functions that fleet.sh calls.
Standards: code-quality.md § "A pin, label, registration or tombstone a tool leaves behind ships the command that clears it"; documentation.md § "A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo"; cli-design.md § "--json on every read"; cli-design.md § "No args shows help, always"; cli-design.md § "Exit codes are 0 success, 2 usage error, other non-zero for failure"; no rule came up for pinning a TPM plugin (search: "third party plugin pinned commit clone default branch").
Findings: 1 correctness · 3 design · 0 prose
Correctness
contracts.1 configs/common/.config/tmux/tmux.conf:367 · configs/common/.config/tmux/tmux.conf:420 · configs/common/.config/tmux/tmux.conf:426 · apps/common/claude-resurrect:66
Reach: Any tmux server that is already running when this deploys and then reloads its config, through tmux-reload or source-file. Old hook values survive the reload: the client-detached save hook and @resurrect-hook-post-restore-all pointing at claude-resurrect restore. The PR describes the detach hook as the cause of the bug it fixes, and a reloaded server keeps it until the next shutdown. Reproduced on a throwaway server; output quoted in the body.
A config reload keeps the save-on-detach hook this PR removes, and calls a claude-resurrect subcommand that no longer exists.
The PR deletes set-hook -g client-detached '…save.sh quiet' and set -g @resurrect-hook-post-restore-all '…claude-resurrect restore'. Nothing unsets them. tmux keeps a hook or an option that a re-sourced file no longer mentions. The PR already handles this for keys: it binds s and L again "so a config reload on a running server drops whatever these keys held before". It does nothing for the hooks.
I sourced main's hook lines into a throwaway server, then head's lines:
== after reload
@resurrect-hook-post-restore-all "\$HOME/.local/bin/claude-resurrect restore"
@resurrect-hook-post-save-all "\$HOME/.local/bin/tmux-snapshots prune"
…
client-detached[0] run-shell "/tmux-resurrect/scripts/save.sh quiet"
At head, claude-resurrect restore prints claude-resurrect: unknown command restore and exits 1. That call fires on every in-server restore, such as tmux-snapshots restore <stamp> or prefix C-r. The surviving detach hook still writes a snapshot while shutdown kills the server, which is the cut-off save the PR sets out to stop. repair-last now hides that snapshot from the next restore, but the save still happens.
The code-quality.md rule "A pin, label, registration or tombstone a tool leaves behind ships the command that clears it" also reaches this, because these are hooks the old config registered on a live server.
Fix: add set-hook -gu client-detached and set -gu @resurrect-hook-post-restore-all next to the plugin block. Do it the same way the PR binds s and L again. Alternatively, keep claude-resurrect restore as a no-op that exits 0 and says it is retired, for one release.
Design and architecture
contracts.2 configs/trust/fleet/.config/sesh/sesh.toml:1 · shell/common/functions.sh:331 · configs/common/.config/tmux/tmux.conf:370 · apps/common/claude-resurrect:16 · terminal-library:cards/tmux-sessions.md:17 · terminal-library:cards/tmux-sessions.md:22 · terminal-library:cards/tmux-commands.md:77 · terminal-library:cards/session-management-with-sesh.md:5 · terminal-library:labs/tmux-window-moves.md:85 · terminal-library:labs/tmux-window-moves.md:118 · terminal-library:tools/registry.yml:1247 · terminal-library-fleet:fleet-built-tools.json:44
Reach: Anyone who reads the terminal-library cards through howto on any machine, the work box included, and every fleet tool that reads fleet-built-tools.json. Running rg sesh and rg claude-resurrect across all 115 registry checkouts found these files. They still describe the old surfaces as current.
Two other repos still teach sesh and the old claude-resurrect, which this PR removes.
The PR removes the sesh binary, the sesh() restore wrapper, tm(), sesh.toml, the sesh cheatsheet, and claude-resurrect save|restore. It gives prefix s and prefix L back to tmux's choose-tree -Zs and switch-client -l. These docs in other repos still describe the old behavior:
terminal-library/cards/tmux-sessions.md:17-29listssesh.tomlas the recipe that survives a reboot. It says "theseshshell wrapper replays the last save… So the firstseshafter a reboot brings everything back". After this PR,continuum-restoreon a baretmuxdoes that, and the wrapper is gone.terminal-library/cards/tmux-commands.md:77-78still listsprefix + sas "session picker (sesh)" andprefix + Las "last session (sesh)".terminal-library/cards/session-management-with-sesh.mdis a whole card for the removed tool.terminal-library/labs/tmux-window-moves.md:85-118tells the reader to expect the sesh picker withCtrl-t/Ctrl-g/Ctrl-xfilters. It also says sessions come back "the next time theseshwrapper restores on a cold start".terminal-library/tools/registry.yml:1247-1264listssesh connect dotfiles,sesh listandsesh lastas usage examples.terminal-library-fleet/fleet-built-tools.json:44-48still describesclaude-resurrectas "Carry Claude Code sessions across a tmux server restart." At head its own help says "Resume a Claude Code session here, from the warehouse's copy."
This matches documentation.md § "A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo".
Fix: change these alongside the PR. Rewrite tmux-sessions.md and the tmux-commands.md key rows around continuum-restore, choose-tree and tmux-snapshots. Retire session-management-with-sesh.md and the sesh entry in registry.yml. Update the lab's picker step. Change the claude-resurrect description in fleet-built-tools.json to the new help line.
contracts.3 apps/common/tmux-snapshots:96 · apps/common/tmux-snapshots:208 · apps/common/tmux-snapshots:211
Reach: The next script or skill that needs a snapshot's stamp, wholeness or counts, for example to choose what to pass to tmux-snapshots restore <stamp>. Today it can only parse a fixed-width table. A user typing the bare command also gets a usage error instead of help. I ran both against a scratch HOME; output quoted in the body.
tmux-snapshots, a new CLI, has no --json on its one read, and the bare command exits 2.
list is the only place that shows the stamp restore <stamp> takes and whether each snapshot is whole. Its only output is a padded table:
STAMP TAKEN SESSIONS WINDOWS PANES STATE
20261114T110000 2026-11-14 11:00:00 1 0 1 cut off <- last
20261114T100000 2026-11-14 10:00:00 1 1 1 whole
tmux-snapshots list --json returns tmux-snapshots: list takes a count, not --json with exit 2. That conflicts with cli-design.md § "--json on every read". It also conflicts with § "Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it". The STATE column reads cut off with a space, so a caller splitting on whitespace gets the wrong number of fields.
The bare tmux-snapshots prints usage to stderr and exits 2. That conflicts with cli-design.md § "No args shows help, always". tmux-snapshots is a namespace, so a bare call should print the help to stdout and exit 0.
Fix: add list --json, an array of {stamp, taken, whole, sessions, windows, panes, last} that measure already computes. Route an empty $1 to usage with exit 0. Keep exit 2 for an unknown command. Do both before a second caller starts parsing the table.
contracts.4 configs/common/.config/tmux/tmux.conf:401 · configs/common/.config/tmux/tmux.conf:402 · configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:11 · configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:26 · configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:10
Reach: Every box that clones tmux-restore for the first time, and every box where pluginsync's new replaced() deletes the checkout and TPM clones it again. Each gets that day's default branch of andrewboldi/tmux-restore. Its last three commits, de7f675, 45a4abb and e8b7e53, are all dated 2026-11-13. fleet.sh and the systemd unit depend on internals that its docs do not promise to keep.
fleet.sh and the shutdown unit depend on internals of an unpinned plugin that is still changing.
set -g @plugin 'andrewboldi/tmux-restore' has no #ref. The adapter fleet.sh calls proc_comm, proc_args, proc_children, the ARGV global, claude_session and claude_resume. These are functions in the plugin's scripts/lib.sh and agents/claude.sh. docs/agents.md documents only the ~/.config/tmux-restore/agents/ directory, not these functions. tmux-shutdown-save.service hard-codes %h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh. The plugin's own unit and installer point at a different path, %h/.tmux/plugins/….
The new tmux.conf comment says a box "keeps the commit it cloned". That is true for one box, but two boxes cloned on different days run different adapter APIs. A rename upstream breaks the fleet board's conversation restore silently: fleet_session fails, and the board comes back without its conversation.
In the same PR, pluginsync learns to honour #branch. A #<commit or tag> would therefore be honoured too.
Fix: pin both plugin lines to a ref, andrewboldi/tmux-restore#<tag>, and move it on purpose. Alternatively, have fleet.sh check that claude_session and claude_resume are defined and say so when they are not, instead of quietly saving the bare board.
Findings as JSON
{
"used": "I cloned the PR head and read the full diff. I searched all 115 checkouts in repos-registry for callers of every removed or renamed surface: `claude-resurrect save|restore`, `sesh`, `tm`, `sesh.toml`, `prefix s`/`prefix L`, `client-detached` and the resurrect hook options. I took the old and new hook lines from tmux.conf and loaded both in turn into a throwaway tmux server on a socket in my own directory. I ran `claude-resurrect restore` at head. I ran `tmux-snapshots list`, `repair-last`, `list --json`, `restore` with no stamp, and the bare command against a scratch HOME holding one whole snapshot and one cut-off snapshot. I cloned andrewboldi/tmux-restore and the andrewboldi/tmux-resurrect fix-concurrent-restore branch to check the hook chaining, the user agents directory, `shutdown-save.sh`, and the `claude_session`/`claude_resume`/`proc_*` functions that fleet.sh calls.",
"standards": "code-quality.md § \"A pin, label, registration or tombstone a tool leaves behind ships the command that clears it\"; documentation.md § \"A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo\"; cli-design.md § \"--json on every read\"; cli-design.md § \"No args shows help, always\"; cli-design.md § \"Exit codes are 0 success, 2 usage error, other non-zero for failure\"; no rule came up for pinning a TPM plugin (search: \"third party plugin pinned commit clone default branch\").",
"findings": [
{
"number": "contracts.1",
"section": "correctness",
"sites": [
"configs/common/.config/tmux/tmux.conf:367",
"configs/common/.config/tmux/tmux.conf:420",
"configs/common/.config/tmux/tmux.conf:426",
"apps/common/claude-resurrect:66"
],
"reach": "Any tmux server that is already running when this deploys and then reloads its config, through `tmux-reload` or `source-file`. Old hook values survive the reload: the `client-detached` save hook and `@resurrect-hook-post-restore-all` pointing at `claude-resurrect restore`. The PR describes the detach hook as the cause of the bug it fixes, and a reloaded server keeps it until the next shutdown. Reproduced on a throwaway server; output quoted in the body.",
"body": "**A config reload keeps the save-on-detach hook this PR removes, and calls a `claude-resurrect` subcommand that no longer exists.**\n\nThe PR deletes `set-hook -g client-detached '…save.sh quiet'` and `set -g @resurrect-hook-post-restore-all '…claude-resurrect restore'`. Nothing unsets them. tmux keeps a hook or an option that a re-sourced file no longer mentions. The PR already handles this for keys: it binds `s` and `L` again \"so a config reload on a running server drops whatever these keys held before\". It does nothing for the hooks.\n\nI sourced main's hook lines into a throwaway server, then head's lines:\n\n```text\n== after reload\n@resurrect-hook-post-restore-all \"\\$HOME/.local/bin/claude-resurrect restore\"\n@resurrect-hook-post-save-all \"\\$HOME/.local/bin/tmux-snapshots prune\"\n…\nclient-detached[0] run-shell \"/tmux-resurrect/scripts/save.sh quiet\"\n```\n\nAt head, `claude-resurrect restore` prints `claude-resurrect: unknown command restore` and exits 1. That call fires on every in-server restore, such as `tmux-snapshots restore <stamp>` or prefix C-r. The surviving detach hook still writes a snapshot while shutdown kills the server, which is the cut-off save the PR sets out to stop. `repair-last` now hides that snapshot from the next restore, but the save still happens.\n\nThe code-quality.md rule \"A pin, label, registration or tombstone a tool leaves behind ships the command that clears it\" also reaches this, because these are hooks the old config registered on a live server.\n\n**Fix**: add `set-hook -gu client-detached` and `set -gu @resurrect-hook-post-restore-all` next to the plugin block. Do it the same way the PR binds `s` and `L` again. Alternatively, keep `claude-resurrect restore` as a no-op that exits 0 and says it is retired, for one release."
},
{
"number": "contracts.2",
"section": "design",
"sites": [
"configs/trust/fleet/.config/sesh/sesh.toml:1",
"shell/common/functions.sh:331",
"configs/common/.config/tmux/tmux.conf:370",
"apps/common/claude-resurrect:16",
"terminal-library:cards/tmux-sessions.md:17",
"terminal-library:cards/tmux-sessions.md:22",
"terminal-library:cards/tmux-commands.md:77",
"terminal-library:cards/session-management-with-sesh.md:5",
"terminal-library:labs/tmux-window-moves.md:85",
"terminal-library:labs/tmux-window-moves.md:118",
"terminal-library:tools/registry.yml:1247",
"terminal-library-fleet:fleet-built-tools.json:44"
],
"reach": "Anyone who reads the terminal-library cards through howto on any machine, the work box included, and every fleet tool that reads fleet-built-tools.json. Running `rg sesh` and `rg claude-resurrect` across all 115 registry checkouts found these files. They still describe the old surfaces as current.",
"body": "**Two other repos still teach sesh and the old `claude-resurrect`, which this PR removes.**\n\nThe PR removes the `sesh` binary, the `sesh()` restore wrapper, `tm()`, `sesh.toml`, the sesh cheatsheet, and `claude-resurrect save|restore`. It gives `prefix s` and `prefix L` back to tmux's `choose-tree -Zs` and `switch-client -l`. These docs in other repos still describe the old behavior:\n\n- `terminal-library/cards/tmux-sessions.md:17-29` lists `sesh.toml` as the recipe that survives a reboot. It says \"the `sesh` shell wrapper replays the last save… So the first `sesh` after a reboot brings everything back\". After this PR, `continuum-restore` on a bare `tmux` does that, and the wrapper is gone.\n- `terminal-library/cards/tmux-commands.md:77-78` still lists `prefix + s` as \"session picker (sesh)\" and `prefix + L` as \"last session (sesh)\".\n- `terminal-library/cards/session-management-with-sesh.md` is a whole card for the removed tool.\n- `terminal-library/labs/tmux-window-moves.md:85-118` tells the reader to expect the sesh picker with `Ctrl-t`/`Ctrl-g`/`Ctrl-x` filters. It also says sessions come back \"the next time the `sesh` wrapper restores on a cold start\".\n- `terminal-library/tools/registry.yml:1247-1264` lists `sesh connect dotfiles`, `sesh list` and `sesh last` as usage examples.\n- `terminal-library-fleet/fleet-built-tools.json:44-48` still describes `claude-resurrect` as \"Carry Claude Code sessions across a tmux server restart.\" At head its own help says \"Resume a Claude Code session here, from the warehouse's copy.\"\n\nThis matches documentation.md § \"A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo\".\n\n**Fix**: change these alongside the PR. Rewrite `tmux-sessions.md` and the `tmux-commands.md` key rows around continuum-restore, `choose-tree` and `tmux-snapshots`. Retire `session-management-with-sesh.md` and the `sesh` entry in registry.yml. Update the lab's picker step. Change the `claude-resurrect` description in fleet-built-tools.json to the new help line."
},
{
"number": "contracts.3",
"section": "design",
"sites": [
"apps/common/tmux-snapshots:96",
"apps/common/tmux-snapshots:208",
"apps/common/tmux-snapshots:211"
],
"reach": "The next script or skill that needs a snapshot's stamp, wholeness or counts, for example to choose what to pass to `tmux-snapshots restore <stamp>`. Today it can only parse a fixed-width table. A user typing the bare command also gets a usage error instead of help. I ran both against a scratch HOME; output quoted in the body.",
"body": "**`tmux-snapshots`, a new CLI, has no `--json` on its one read, and the bare command exits 2.**\n\n`list` is the only place that shows the stamp `restore <stamp>` takes and whether each snapshot is whole. Its only output is a padded table:\n\n```text\nSTAMP TAKEN SESSIONS WINDOWS PANES STATE\n20261114T110000 2026-11-14 11:00:00 1 0 1 cut off <- last\n20261114T100000 2026-11-14 10:00:00 1 1 1 whole\n```\n\n`tmux-snapshots list --json` returns `tmux-snapshots: list takes a count, not --json` with exit 2. That conflicts with cli-design.md § \"--json on every read\". It also conflicts with § \"Anything a table or screen shows is reachable through --json or a named verb, never only by parsing it\". The STATE column reads `cut off` with a space, so a caller splitting on whitespace gets the wrong number of fields.\n\nThe bare `tmux-snapshots` prints usage to stderr and exits 2. That conflicts with cli-design.md § \"No args shows help, always\". `tmux-snapshots` is a namespace, so a bare call should print the help to stdout and exit 0.\n\n**Fix**: add `list --json`, an array of `{stamp, taken, whole, sessions, windows, panes, last}` that `measure` already computes. Route an empty `$1` to `usage` with exit 0. Keep exit 2 for an unknown command. Do both before a second caller starts parsing the table."
},
{
"number": "contracts.4",
"section": "design",
"sites": [
"configs/common/.config/tmux/tmux.conf:401",
"configs/common/.config/tmux/tmux.conf:402",
"configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:11",
"configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:26",
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:10"
],
"reach": "Every box that clones tmux-restore for the first time, and every box where pluginsync's new `replaced()` deletes the checkout and TPM clones it again. Each gets that day's default branch of andrewboldi/tmux-restore. Its last three commits, de7f675, 45a4abb and e8b7e53, are all dated 2026-11-13. fleet.sh and the systemd unit depend on internals that its docs do not promise to keep.",
"body": "**fleet.sh and the shutdown unit depend on internals of an unpinned plugin that is still changing.**\n\n`set -g @plugin 'andrewboldi/tmux-restore'` has no `#ref`. The adapter `fleet.sh` calls `proc_comm`, `proc_args`, `proc_children`, the `ARGV` global, `claude_session` and `claude_resume`. These are functions in the plugin's `scripts/lib.sh` and `agents/claude.sh`. `docs/agents.md` documents only the `~/.config/tmux-restore/agents/` directory, not these functions. `tmux-shutdown-save.service` hard-codes `%h/.config/tmux/plugins/tmux-restore/scripts/shutdown-save.sh`. The plugin's own unit and installer point at a different path, `%h/.tmux/plugins/…`.\n\nThe new tmux.conf comment says a box \"keeps the commit it cloned\". That is true for one box, but two boxes cloned on different days run different adapter APIs. A rename upstream breaks the fleet board's conversation restore silently: `fleet_session` fails, and the board comes back without its conversation.\n\nIn the same PR, pluginsync learns to honour `#branch`. A `#<commit or tag>` would therefore be honoured too.\n\n**Fix**: pin both plugin lines to a ref, `andrewboldi/tmux-restore#<tag>`, and move it on purpose. Alternatively, have fleet.sh check that `claude_session` and `claude_resume` are defined and say so when they are not, instead of quietly saving the bare board."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — fleet-patterns
Used: Checked out PR 112 at 4b0c870 and read the diff for tmux.conf, pluginsync.py, fleet.sh, restored-programs.conf, the shutdown unit and claude-resurrect. Ran indy search --owned on six phrasings: snapshot retention, newest whole snapshot, logind shutdown inhibitor, git remote parsing, plugin re-clone and tmux restore after reboot. Read fleet views/inbox/terminal.go, fleet views/conversation/session_keys.go, the homelab backup retention docs and the terminal-library cards.
Standards: documentation.md/A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo (fleet standards search "change to a tool's keys or commands updates the cheatsheet cards in another repo")
Findings: 0 correctness · 2 design · 0 prose
Design and architecture
fleet-patterns.1 configs/common/.config/tmux/tmux.conf:370 · configs/common/.config/tmux/tmux.conf:371 · shell/common/functions.sh · terminal-library:cards/tmux-sessions.md:17 · terminal-library:cards/tmux-sessions.md:22 · terminal-library:cards/tmux-sessions.md:29 · terminal-library:cards/tmux-commands.md:77 · terminal-library:cards/tmux-commands.md:78 · terminal-library:cards/session-management-with-sesh.md:1 · terminal-library:tools/registry.yml:1247 · terminal-library:labs/tmux-window-moves.md:85 · terminal-library:labs/tmux-window-moves.md:118
Reach: howto, through howto library sync, serves the terminal-library cards on every box. After this PR merges they still teach sesh, prefix s as the sesh picker, prefix L as sesh last, and a cold start that the sesh wrapper restores. The PR removes all four.
The terminal-library cards still describe the removed sesh setup
The PR removes sesh and the tm/sesh wrapper. It rebinds prefix s to choose-tree -Zs and prefix L to switch-client -l. It also turns on continuum's own restore.
The cards that document this keybinding and restore setup live in datapointchris/terminal-library. dotfiles' README names that repo as the place tools are documented. They were not changed:
cards/tmux-sessions.md:22says continuum's "own auto-restore is off — theseshshell wrapper replays the last save". The PR sets@continuum-restore 'on'and deletes the wrapper.cards/tmux-sessions.md:17listssesh.tomlas a component that survives a reboot. The PR deletesconfigs/trust/fleet/.config/sesh/sesh.toml.cards/tmux-sessions.md:29andcards/tmux-commands.md:77-78listprefix + sas "session picker (sesh)" andprefix + Las "last session (sesh)".cards/session-management-with-sesh.mdis a whole card on a tool this PR uninstalls.tools/registry.yml:1247keeps aseshentry withsesh connectandsesh lastexamples.labs/tmux-window-moves.md:85-118walks through the sesh picker and "theseshwrapper restores on a cold start".
None of the cards mention tmux-restore, tmux-snapshots, repair-last or the shutdown save unit.
documentation.md § "A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo" covers this case. The describer is in another repo, and a search inside dotfiles does not find it.
Fix: open a companion terminal-library PR with these changes:
- Delete
session-management-with-sesh.mdand theseshregistry entry. - Rewrite the
tmux-sessions.mdrestore paragraph to describe continuum restore,tmux-snapshots list/restore <stamp>and the shutdown save. - Change the
prefix sandprefix Lrows to tmux's session tree and last session.
Link the companion PR from this one.
fleet-patterns.2 configs/trust/fleet/.config/tmux/restored-programs.conf:9 · configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:10 · configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:15 · fleet:views/inbox/terminal.go:206 · fleet:views/inbox/terminal.go:210 · fleet:cmd/tui.go
Reach: The next fleet change to its viewer subcommands or to how a board launches claude. Renaming fleet ledger or fleet views, or running C's claude through muxctl pane open the way views/conversation/session_keys.go:187 already does for resume, makes the dotfiles copy silently stop restoring that pane. No test in fleet knows the copy exists.
dotfiles hand-copies two facts that fleet owns
The PR's restore configuration depends on two facts that only fleet's source defines.
- Which fleet subcommands are safe to rerun on restore.
restored-programs.conf:9lists"fleet tui" "fleet ledger" "fleet inbox browse" "fleet views" "slate tui". Its comment explains why it lists subcommands rather thanfleet:fleet agents dispatchmust never rerun. That is a statement about fleet's command surface, kept in a dotfiles string. - How a board hands its terminal to claude.
fleet.sh:10-21matchescomm == fleetwithargv[0] == tui. It then looks for claude as a direct child of the board. That holds today only becausefleet:views/inbox/terminal.go:206-210runs claude throughtea.Execwith aproc.TerminalChild. TheCkey in the conversation view (fleet:views/conversation/session_keys.go:187) already launches claude a different way, throughmuxctl pane open. So the process shape is not one fleet keeps across its own views.
The PR body explains why these files sit under configs/trust/fleet/: machines off that trust have neither program. It does not explain why fleet's restart contract is written outside fleet.
The evidence is weak. I found no other repo where a tool ships its own tmux-restore adapter or restart list. fleet ships no tmux integration today, and the copies do not yet disagree. The cost is the next fleet rename or launch change, which leaves this copy stale with no failing test.
Fix (preferred): have fleet own both facts. For example, fleet could print its restartable viewers and its adapter (fleet tmux-restore adapter, or a file fleet installs), and dotfiles would source what fleet emits.
Fix (minimum): add a comment in fleet:views/inbox/terminal.go and fleet:cmd/tui.go naming dotfiles:configs/trust/fleet/.config/tmux-restore/agents/fleet.sh as a dependent of the board's process shape and subcommand names.
Findings as JSON
{
"used": "Checked out PR 112 at 4b0c8706 and read the diff for tmux.conf, pluginsync.py, fleet.sh, restored-programs.conf, the shutdown unit and claude-resurrect. Ran indy search --owned on six phrasings: snapshot retention, newest whole snapshot, logind shutdown inhibitor, git remote parsing, plugin re-clone and tmux restore after reboot. Read fleet views/inbox/terminal.go, fleet views/conversation/session_keys.go, the homelab backup retention docs and the terminal-library cards.",
"standards": "documentation.md/A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo (fleet standards search \"change to a tool's keys or commands updates the cheatsheet cards in another repo\")",
"findings": [
{
"number": "fleet-patterns.1",
"section": "design",
"sites": [
"configs/common/.config/tmux/tmux.conf:370",
"configs/common/.config/tmux/tmux.conf:371",
"shell/common/functions.sh",
"terminal-library:cards/tmux-sessions.md:17",
"terminal-library:cards/tmux-sessions.md:22",
"terminal-library:cards/tmux-sessions.md:29",
"terminal-library:cards/tmux-commands.md:77",
"terminal-library:cards/tmux-commands.md:78",
"terminal-library:cards/session-management-with-sesh.md:1",
"terminal-library:tools/registry.yml:1247",
"terminal-library:labs/tmux-window-moves.md:85",
"terminal-library:labs/tmux-window-moves.md:118"
],
"reach": "`howto`, through `howto library sync`, serves the terminal-library cards on every box. After this PR merges they still teach sesh, `prefix s` as the sesh picker, `prefix L` as `sesh last`, and a cold start that the `sesh` wrapper restores. The PR removes all four.",
"body": "### The terminal-library cards still describe the removed sesh setup\n\nThe PR removes sesh and the `tm`/`sesh` wrapper. It rebinds `prefix s` to `choose-tree -Zs` and `prefix L` to `switch-client -l`. It also turns on continuum's own restore.\n\nThe cards that document this keybinding and restore setup live in datapointchris/terminal-library. dotfiles' README names that repo as the place tools are documented. They were not changed:\n\n- `cards/tmux-sessions.md:22` says continuum's \"own auto-restore is off — the `sesh` shell wrapper replays the last save\". The PR sets `@continuum-restore 'on'` and deletes the wrapper.\n- `cards/tmux-sessions.md:17` lists `sesh.toml` as a component that survives a reboot. The PR deletes `configs/trust/fleet/.config/sesh/sesh.toml`.\n- `cards/tmux-sessions.md:29` and `cards/tmux-commands.md:77-78` list `prefix + s` as \"session picker (sesh)\" and `prefix + L` as \"last session (sesh)\".\n- `cards/session-management-with-sesh.md` is a whole card on a tool this PR uninstalls.\n- `tools/registry.yml:1247` keeps a `sesh` entry with `sesh connect` and `sesh last` examples.\n- `labs/tmux-window-moves.md:85-118` walks through the sesh picker and \"the `sesh` wrapper restores on a cold start\".\n\nNone of the cards mention tmux-restore, `tmux-snapshots`, `repair-last` or the shutdown save unit.\n\n`documentation.md` § \"A rename, move or behavior change sweeps every doc and comment that describes it, from above every repo\" covers this case. The describer is in another repo, and a search inside dotfiles does not find it.\n\n**Fix:** open a companion terminal-library PR with these changes:\n\n- Delete `session-management-with-sesh.md` and the `sesh` registry entry.\n- Rewrite the `tmux-sessions.md` restore paragraph to describe continuum restore, `tmux-snapshots list`/`restore <stamp>` and the shutdown save.\n- Change the `prefix s` and `prefix L` rows to tmux's session tree and last session.\n\nLink the companion PR from this one."
},
{
"number": "fleet-patterns.2",
"section": "design",
"sites": [
"configs/trust/fleet/.config/tmux/restored-programs.conf:9",
"configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:10",
"configs/trust/fleet/.config/tmux-restore/agents/fleet.sh:15",
"fleet:views/inbox/terminal.go:206",
"fleet:views/inbox/terminal.go:210",
"fleet:cmd/tui.go"
],
"reach": "The next fleet change to its viewer subcommands or to how a board launches claude. Renaming `fleet ledger` or `fleet views`, or running C's claude through `muxctl pane open` the way `views/conversation/session_keys.go:187` already does for resume, makes the dotfiles copy silently stop restoring that pane. No test in fleet knows the copy exists.",
"body": "### dotfiles hand-copies two facts that fleet owns\n\nThe PR's restore configuration depends on two facts that only fleet's source defines.\n\n1. **Which fleet subcommands are safe to rerun on restore.** `restored-programs.conf:9` lists `\"fleet tui\" \"fleet ledger\" \"fleet inbox browse\" \"fleet views\" \"slate tui\"`. Its comment explains why it lists subcommands rather than `fleet`: `fleet agents dispatch` must never rerun. That is a statement about fleet's command surface, kept in a dotfiles string.\n2. **How a board hands its terminal to claude.** `fleet.sh:10-21` matches `comm == fleet` with `argv[0] == tui`. It then looks for claude as a direct child of the board. That holds today only because `fleet:views/inbox/terminal.go:206-210` runs claude through `tea.Exec` with a `proc.TerminalChild`. The `C` key in the conversation view (`fleet:views/conversation/session_keys.go:187`) already launches claude a different way, through `muxctl pane open`. So the process shape is not one fleet keeps across its own views.\n\nThe PR body explains why these files sit under `configs/trust/fleet/`: machines off that trust have neither program. It does not explain why fleet's restart contract is written outside fleet.\n\nThe evidence is weak. I found no other repo where a tool ships its own tmux-restore adapter or restart list. fleet ships no tmux integration today, and the copies do not yet disagree. The cost is the next fleet rename or launch change, which leaves this copy stale with no failing test.\n\n**Fix (preferred):** have fleet own both facts. For example, fleet could print its restartable viewers and its adapter (`fleet tmux-restore adapter`, or a file fleet installs), and dotfiles would source what fleet emits.\n\n**Fix (minimum):** add a comment in `fleet:views/inbox/terminal.go` and `fleet:cmd/tui.go` naming `dotfiles:configs/trust/fleet/.config/tmux-restore/agents/fleet.sh` as a dependent of the board's process shape and subcommand names."
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — G1
Used: Cloned the PR and installed the resurrect fork, tmux-restore and continuum into a HOME inside my review directory. The tmux socket ran in a private mount namespace. With tmux-snapshots and the fleet restored-programs.conf in place, I saved, killed the server and cold-started it with a bare tmux. I ran list, prune and repair-last on snapshots I generated, read resurrect's process_restore_helpers.sh, and ran tests/install/test_pluginsync.py (23 passed) and shellcheck (clean).
Standards: Cited dependencies.md § "A tiny, young project is not an option, however well it fits", documentation.md § "A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant" and documentation.md § "A comment describes the code as it is, never the change or history that produced it". The searches "pin third-party dependency to a commit" and "allowlist pattern matches substring of argument" found no rule on restore-list patterns.
Findings: 3 correctness · 1 design · 1 prose
Correctness
G1.1 configs/trust/fleet/.config/tmux/restored-programs.conf:9
Reach: On a fleet machine, any pane whose saved command contains the text watch-ci or ci-runs-watch. One example is nvim apps/trust/fleet/watch-ci or less apps/trust/fleet/watch-ci, a file this repo holds.
A pane open on the watch-ci script comes back running watch-ci. ~watch-ci->watch-ci * is a resurrect inline strategy. ~ matches the text anywhere in the saved command. resurrect's _get_inline_strategy then replaces the whole command with watch-ci plus whatever follows the match. It does this for every command on the restore list, including nvim, less and tail from resurrect's defaults and tmux-restore's TMUX_RESTORE=1 marker.
I ran this with the fork, tmux-restore, continuum and this restored-programs.conf loaded. A pane ran less apps/trust/fleet/watch-ci. The snapshot saved :less apps/trust/fleet/watch-ci. After a kill and a bare tmux cold start, the pane was running a stub watch-ci on PATH, which logged WATCH-CI RAN args=[]. The file was not open. An nvim pane also loses @resurrect-strategy-nvim 'session'. ~ci-runs-watch->ci-runs-watch * has the same shape.
The ~ is needed because watch-ci is a uv run --script script, so procfs records uv run --script /home/…/bin/watch-ci ….
Fix: anchor each pattern to the installed path, as resurrect's README example ~bin/my_program->my_program * does. Use "~bin/watch-ci->watch-ci *" and "~bin/ci-runs-watch->ci-runs-watch *". Do not use ^ or spaces in the pattern. resurrect's _get_command_arguments puts the match into a sed BRE, where a ^ that is not first is a literal.
G1.2 configs/common/.config/tmux/tmux.conf:414
Reach: unproven — a snapshot holding a pane that ran ssh <host> <command> would settle it. The PR body's month of snapshots is where to look.
ssh on the restore list runs every saved ssh host command again at the next cold start. A plain resurrect entry matches a command whose first word is ssh. _proc_matches_full_command checks ^ssh , and restore_pane_process sends the whole saved command line. So a pane running ssh nas 'sudo pacman -Syu' or ssh box rsync … reruns that remote command unattended after a reboot.
restored-programs.conf names this hazard itself: "Named by subcommand rather than fleet alone, because a pane running fleet agents dispatch would otherwise start another round on restore." The base list does not apply the same rule to ssh.
Fix: drop ssh and let tmux-restore type it back on the prompt, as it does for anything unnamed. Or restore only a bare login with an inline strategy that keeps just the host. For example, "~^ssh [^ ]*$" matches only an ssh <host> with nothing after it. Check that form against resurrect's sed before relying on it.
G1.3 configs/common/.config/tmux/tmux.conf:403 · configs/common/.config/tmux/tmux.conf:404 · src/dotfiles/providers/pluginsync.py:289
Reach: A fresh box's apply, where TPM runs git clone -b fix-concurrent-restore. Also every save and every shutdown on every box, which runs tmux-restore's save hook. The PR's own Decisions section names the next change: "If it goes unmaintained, its ideas move here."
The reboot restore now rests on a two-week-old project from one author, cloned unpinned. dependencies.md § "A tiny, young project is not an option, however well it fits" applies. Measured with gh api on 2026-11-14:
andrewboldi/tmux-restore: created 2026-10-31, 83 stars, 3 contributors, 25 commits, last push 2026-11-13. Five commits landed on 11-13.andrewboldi/tmux-resurrect: created 2026-10-31, 0 stars. It is a branch carrying one open, unmerged upstream PR, tmux-plugins/tmux-resurrect#600.
The PR body gives none of these numbers. Its reason for the choice is fit, and that rule says fit does not offset age.
Three costs follow:
- Boxes drift. TPM clones HEAD and dotfiles never updates it. Each box runs whatever master was the day it was set up, and the code that runs is sourced and
evaled on every minute's save. The PR verified one commit. - The branch is likely to vanish.
fix-concurrent-restoreexists until #600 merges, and a merged branch is usually deleted. After that, a fresh box's clone fails, and that box has no resurrect at all. - A failed clone deletes the old checkout.
sync_tmuxrunsshutil.rmtreeon the swapped checkout before TPM clones. If that clone fails, the running server's@resurrect-save-script-pathpoints at nothing, and continuum and the shutdown save both stop until the next good apply.
Fix: write the rewrite here, as the PR already plans if the project goes unmaintained, and name tmux-restore as prior art. Otherwise, fork both repos under datapointchris and pin each @plugin line to a tag, which git clone -b accepts. In sync_tmux, move a swapped checkout aside and put it back if TPM fails.
Design and architecture
G1.4 configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:7 · configs/common/.config/tmux/tmux.conf:368
Reach: documentation.md § "A comment describes the code as it is, never the change or history that produced it". It is a rule written for drift no test catches. Each sentence narrates the hook and bindings this PR removed, and reads as a puzzle once every server has restarted.
Two comments explain the migration rather than the code.
tmux-shutdown-save.service:7: "A hook inside tmux fires only as the server is torn down, and saved a cut-off snapshot." The past tense "saved" refers to the deletedclient-detachedhook. Rewrite: "A hook inside tmux fires only as the server is torn down, so a save from one is cut off."tmux.conf:368: "tmux's own defaults, bound again so a config reload on a running server drops whatever these keys held before." "Held before" means sesh, which this PR removes. Rewrite: "tmux's own defaults, bound here so this file owns the keys whatever a plugin binds."
Prose
G1.5 docs/architecture/tmux-sessions.md:217 · docs/architecture/tmux-sessions.md:234 · configs/trust/fleet/.config/tmux/restored-programs.conf:2
Three sentences pin a measurement or a date that the next change makes stale. documentation.md § "A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant" covers them.
tmux-sessions.md:217: "A save of 14 panes, each holding 3,000 lines of scrollback, takes about a second." Rewrite: "The save, its hooks and the prune after it have to finish inside that delay. One that does not leaves a cut-off snapshot, whichtmux-snapshots listshows."tmux-sessions.md:234: "upstream resurrect and continuum have had no commit since 2023 and 2024." This is false after the next upstream commit. Rewrite: "The fork carries one change, proposed upstream as #600."restored-programs.conf:2: "Each ran in a pane for hours in a month of snapshots." It names no snapshot set, and it goes stale with the next program added. Rewrite: drop the sentence. The next sentence stands alone: "The fleet's own viewers, restarted after a restore like the programs tmux.conf names."
Findings as JSON
{
"used": "Cloned the PR and installed the resurrect fork, tmux-restore and continuum into a HOME inside my review directory. The tmux socket ran in a private mount namespace. With tmux-snapshots and the fleet restored-programs.conf in place, I saved, killed the server and cold-started it with a bare tmux. I ran list, prune and repair-last on snapshots I generated, read resurrect's process_restore_helpers.sh, and ran tests/install/test_pluginsync.py (23 passed) and shellcheck (clean).",
"standards": "Cited dependencies.md § \"A tiny, young project is not an option, however well it fits\", documentation.md § \"A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant\" and documentation.md § \"A comment describes the code as it is, never the change or history that produced it\". The searches \"pin third-party dependency to a commit\" and \"allowlist pattern matches substring of argument\" found no rule on restore-list patterns.",
"findings": [
{
"number": "G1.1",
"section": "correctness",
"sites": [
"configs/trust/fleet/.config/tmux/restored-programs.conf:9"
],
"reach": "On a fleet machine, any pane whose saved command contains the text `watch-ci` or `ci-runs-watch`. One example is `nvim apps/trust/fleet/watch-ci` or `less apps/trust/fleet/watch-ci`, a file this repo holds.",
"body": "**A pane open on the `watch-ci` script comes back running `watch-ci`.** `~watch-ci->watch-ci *` is a resurrect inline strategy. `~` matches the text anywhere in the saved command. resurrect's `_get_inline_strategy` then replaces the whole command with `watch-ci` plus whatever follows the match. It does this for every command on the restore list, including `nvim`, `less` and `tail` from resurrect's defaults and tmux-restore's `TMUX_RESTORE=1` marker.\n\nI ran this with the fork, tmux-restore, continuum and this `restored-programs.conf` loaded. A pane ran `less apps/trust/fleet/watch-ci`. The snapshot saved `:less apps/trust/fleet/watch-ci`. After a kill and a bare `tmux` cold start, the pane was running a stub `watch-ci` on PATH, which logged `WATCH-CI RAN args=[]`. The file was not open. An nvim pane also loses `@resurrect-strategy-nvim 'session'`. `~ci-runs-watch->ci-runs-watch *` has the same shape.\n\nThe `~` is needed because `watch-ci` is a `uv run --script` script, so procfs records `uv run --script /home/…/bin/watch-ci …`.\n\nFix: anchor each pattern to the installed path, as resurrect's README example `~bin/my_program->my_program *` does. Use `\"~bin/watch-ci->watch-ci *\"` and `\"~bin/ci-runs-watch->ci-runs-watch *\"`. Do not use `^` or spaces in the pattern. resurrect's `_get_command_arguments` puts the match into a sed BRE, where a `^` that is not first is a literal."
},
{
"number": "G1.2",
"section": "correctness",
"sites": [
"configs/common/.config/tmux/tmux.conf:414"
],
"reach": "unproven — a snapshot holding a pane that ran `ssh <host> <command>` would settle it. The PR body's month of snapshots is where to look.",
"body": "**`ssh` on the restore list runs every saved `ssh host command` again at the next cold start.** A plain resurrect entry matches a command whose first word is `ssh`. `_proc_matches_full_command` checks `^ssh `, and `restore_pane_process` sends the whole saved command line. So a pane running `ssh nas 'sudo pacman -Syu'` or `ssh box rsync …` reruns that remote command unattended after a reboot.\n\n`restored-programs.conf` names this hazard itself: \"Named by subcommand rather than `fleet` alone, because a pane running `fleet agents dispatch` would otherwise start another round on restore.\" The base list does not apply the same rule to `ssh`.\n\nFix: drop `ssh` and let tmux-restore type it back on the prompt, as it does for anything unnamed. Or restore only a bare login with an inline strategy that keeps just the host. For example, `\"~^ssh [^ ]*$\"` matches only an `ssh <host>` with nothing after it. Check that form against resurrect's sed before relying on it."
},
{
"number": "G1.3",
"section": "correctness",
"sites": [
"configs/common/.config/tmux/tmux.conf:403",
"configs/common/.config/tmux/tmux.conf:404",
"src/dotfiles/providers/pluginsync.py:289"
],
"reach": "A fresh box's apply, where TPM runs `git clone -b fix-concurrent-restore`. Also every save and every shutdown on every box, which runs tmux-restore's save hook. The PR's own Decisions section names the next change: \"If it goes unmaintained, its ideas move here.\"",
"body": "**The reboot restore now rests on a two-week-old project from one author, cloned unpinned.** `dependencies.md` § \"A tiny, young project is not an option, however well it fits\" applies. Measured with `gh api` on 2026-11-14:\n\n- `andrewboldi/tmux-restore`: created 2026-10-31, 83 stars, 3 contributors, 25 commits, last push 2026-11-13. Five commits landed on 11-13.\n- `andrewboldi/tmux-resurrect`: created 2026-10-31, 0 stars. It is a branch carrying one open, unmerged upstream PR, tmux-plugins/tmux-resurrect#600.\n\nThe PR body gives none of these numbers. Its reason for the choice is fit, and that rule says fit does not offset age.\n\nThree costs follow:\n\n- **Boxes drift.** TPM clones HEAD and dotfiles never updates it. Each box runs whatever master was the day it was set up, and the code that runs is sourced and `eval`ed on every minute's save. The PR verified one commit.\n- **The branch is likely to vanish.** `fix-concurrent-restore` exists until #600 merges, and a merged branch is usually deleted. After that, a fresh box's clone fails, and that box has no resurrect at all.\n- **A failed clone deletes the old checkout.** `sync_tmux` runs `shutil.rmtree` on the swapped checkout before TPM clones. If that clone fails, the running server's `@resurrect-save-script-path` points at nothing, and continuum and the shutdown save both stop until the next good apply.\n\nFix: write the rewrite here, as the PR already plans if the project goes unmaintained, and name tmux-restore as prior art. Otherwise, fork both repos under datapointchris and pin each `@plugin` line to a tag, which `git clone -b` accepts. In `sync_tmux`, move a swapped checkout aside and put it back if TPM fails."
},
{
"number": "G1.4",
"section": "design",
"sites": [
"configs/display/wayland/.config/systemd/user/tmux-shutdown-save.service:7",
"configs/common/.config/tmux/tmux.conf:368"
],
"reach": "`documentation.md` § \"A comment describes the code as it is, never the change or history that produced it\". It is a rule written for drift no test catches. Each sentence narrates the hook and bindings this PR removed, and reads as a puzzle once every server has restarted.",
"body": "**Two comments explain the migration rather than the code.**\n\n- `tmux-shutdown-save.service:7`: \"A hook inside tmux fires only as the server is torn down, and saved a cut-off snapshot.\" The past tense \"saved\" refers to the deleted `client-detached` hook. Rewrite: \"A hook inside tmux fires only as the server is torn down, so a save from one is cut off.\"\n- `tmux.conf:368`: \"tmux's own defaults, bound again so a config reload on a running server drops whatever these keys held before.\" \"Held before\" means sesh, which this PR removes. Rewrite: \"tmux's own defaults, bound here so this file owns the keys whatever a plugin binds.\""
},
{
"number": "G1.5",
"section": "prose",
"sites": [
"docs/architecture/tmux-sessions.md:217",
"docs/architecture/tmux-sessions.md:234",
"configs/trust/fleet/.config/tmux/restored-programs.conf:2"
],
"reach": "",
"body": "**Three sentences pin a measurement or a date that the next change makes stale.** `documentation.md` § \"A count, version or measurement pinned in a comment or doc names what made it true and how it was measured, or goes stale as a constant\" covers them.\n\n- `tmux-sessions.md:217`: \"A save of 14 panes, each holding 3,000 lines of scrollback, takes about a second.\" Rewrite: \"The save, its hooks and the prune after it have to finish inside that delay. One that does not leaves a cut-off snapshot, which `tmux-snapshots list` shows.\"\n- `tmux-sessions.md:234`: \"upstream resurrect and continuum have had no commit since 2023 and 2024.\" This is false after the next upstream commit. Rewrite: \"The fork carries one change, proposed upstream as #600.\"\n- `restored-programs.conf:2`: \"Each ran in a pane for hours in a month of snapshots.\" It names no snapshot set, and it goes stale with the next program added. Rewrite: drop the sentence. The next sentence stands alone: \"The fleet's own viewers, restarted after a restore like the programs tmux.conf names.\""
}
]
}This review does not gate merge.
datapointchris
left a comment
There was a problem hiding this comment.
PR review — synthesis
Used: the 22 findings of round 1, from 8 review(s), and nothing else
Standards: none, because this step reads only the findings
Findings: 0 correctness · 3 design · 0 prose
Design and architecture
synthesis.1 explains G1.3 · system-design.2 · contracts.4
The tmux-restore and resurrect-fork plugins are cloned with no pinned commit or tag. This sits in tmux.conf lines 403-404. Each box runs whatever the branch held the day it cloned. The shutdown unit, the fleet.sh adapter and the save path all depend on that moving code.
synthesis.2 explains G1.5 · stale-prose.1 · stale-prose.2
Prose and comments copy measurements, dates, counts or another repo's current state. These are tmux-sessions.md, restored-programs.conf and tmux.conf. None names its source. Each goes stale when the value or the outside repo changes.
synthesis.3 explains code-architecture.1 · code-architecture.2 · fleet-patterns.2
Facts owned by another component are hand-copied into dotfiles. These are the TPM plugins directory, resurrect's snapshot directory and file naming, and fleet's restartable subcommands and process shape. The copies drift from the owner with no test to flag it.
One defect, raised more than once
failure-paths.2 · system-design.1: tmux-shutdown-save.service has no ConditionPathExists on its ExecStart script, which only TPM creates, so a missing script makes it fail and retry.
G1.3 · system-design.2: tmux.conf lines 403-404 clone the andrewboldi tmux-restore and resurrect forks unpinned, with no tag or commit.
G1.4 · stale-prose.3: The tmux-shutdown-save.service comment narrates the removed client-detached hook in the past tense instead of describing the code.
fleet-patterns.1 · contracts.2: The terminal-library cards, labs and registry still describe the removed sesh setup and wrapper, and the old restore flow.
Shapes as JSON
{
"shapes": [
{
"number": "synthesis.1",
"section": "design",
"explains": [
"G1.3",
"system-design.2",
"contracts.4"
],
"body": "The tmux-restore and resurrect-fork plugins are cloned with no pinned commit or tag. This sits in tmux.conf lines 403-404. Each box runs whatever the branch held the day it cloned. The shutdown unit, the fleet.sh adapter and the save path all depend on that moving code."
},
{
"number": "synthesis.2",
"section": "design",
"explains": [
"G1.5",
"stale-prose.1",
"stale-prose.2"
],
"body": "Prose and comments copy measurements, dates, counts or another repo's current state. These are tmux-sessions.md, restored-programs.conf and tmux.conf. None names its source. Each goes stale when the value or the outside repo changes."
},
{
"number": "synthesis.3",
"section": "design",
"explains": [
"code-architecture.1",
"code-architecture.2",
"fleet-patterns.2"
],
"body": "Facts owned by another component are hand-copied into dotfiles. These are the TPM plugins directory, resurrect's snapshot directory and file naming, and fleet's restartable subcommands and process shape. The copies drift from the owner with no test to flag it."
}
],
"duplicates": [
{
"defect": "tmux-shutdown-save.service has no ConditionPathExists on its ExecStart script, which only TPM creates, so a missing script makes it fail and retry.",
"findings": [
"failure-paths.2",
"system-design.1"
]
},
{
"defect": "tmux.conf lines 403-404 clone the andrewboldi tmux-restore and resurrect forks unpinned, with no tag or commit.",
"findings": [
"G1.3",
"system-design.2"
]
},
{
"defect": "The tmux-shutdown-save.service comment narrates the removed client-detached hook in the past tense instead of describing the code.",
"findings": [
"G1.4",
"stale-prose.3"
]
},
{
"defect": "The terminal-library cards, labs and registry still describe the removed sesh setup and wrapper, and the old restore flow.",
"findings": [
"fleet-patterns.1",
"contracts.2"
]
}
]
}This review does not gate merge.
PR review round 1Ran: G1, useless-test, code-architecture, fleet-patterns, system-design, stale-prose, failure-paths, contracts, synthesis
Share of the week: 0.19% of the largest plan's week, 0.18–0.24% across the fits kept Round record{
"grammar": 6,
"round": 1,
"repo": "datapointchris/dotfiles",
"pr": 112,
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"experiment": "none",
"dispatch": "20261115T043652Z-pr-review",
"roster": "c8249da3828c4bab6b2c8a0641cc9c231df7a881",
"harness": "6fd0e0ed6d0a769fe4763889f82bb2ba4571f208",
"machine": "archlinux",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:44:31Z",
"reviewers": [
{
"label": "G1",
"agent": "pr-reviewer",
"model": "opus",
"effort": "high",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/general.md",
"prompt_sha": "00271016dd7d",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 424,
"tokens": {
"input": 74,
"cache_write": 113128,
"cache_read": 2693993,
"output": 33756
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 74,
"cache_write": 113128,
"cache_read": 2693993,
"output": 33756
}
},
"session": "01a1dd7e-7531-762b-a604-00b7248e2e15",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909134825",
"review_id": 5909134825,
"findings": {
"correctness": 3,
"design": 1,
"prose": 1
},
"numbers": [
"G1.1",
"G1.2",
"G1.3",
"G1.4",
"G1.5"
],
"rules": {
"proposed": 1,
"keys": [
"rule/shell/a-pattern-that-picks-a-program-to-run-matches-the-program-never-text-an-argument-can-carry"
],
"filed": true,
"why": "",
"path": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records/G1.rules.json"
},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:44:05Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "useless-test",
"agent": "pr-useless-test-reviewer",
"model": "opus",
"effort": "high",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/useless-test.md",
"prompt_sha": "f0b52bbf8a48",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 75,
"tokens": {
"input": 22,
"cache_write": 31962,
"cache_read": 271307,
"output": 5765
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 22,
"cache_write": 31962,
"cache_read": 271307,
"output": 5765
}
},
"session": "01a1dd7e-7535-74ea-a54f-69461f7837fe",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909099399",
"review_id": 5909099399,
"findings": {
"correctness": 0,
"design": 0,
"prose": 0
},
"numbers": [],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:38:16Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "code-architecture",
"agent": "pr-code-architecture-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "283f4014efc3",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 109,
"tokens": {
"input": 34,
"cache_write": 53205,
"cache_read": 581131,
"output": 9253
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 34,
"cache_write": 53205,
"cache_read": 581131,
"output": 9253
}
},
"session": "01a1dd7e-7537-7df9-9ea5-4c47db71ad16",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909102930",
"review_id": 5909102930,
"findings": {
"correctness": 0,
"design": 2,
"prose": 0
},
"numbers": [
"code-architecture.1",
"code-architecture.2"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:38:50Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "fleet-patterns",
"agent": "pr-fleet-patterns-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "44a558c09eb3",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 198,
"tokens": {
"input": 36,
"cache_write": 32743,
"cache_read": 495907,
"output": 8413
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 36,
"cache_write": 32743,
"cache_read": 495907,
"output": 8413
}
},
"session": "01a1dd7e-7538-77b5-b072-b633173c5c17",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909112543",
"review_id": 5909112543,
"findings": {
"correctness": 0,
"design": 2,
"prose": 0
},
"numbers": [
"fleet-patterns.1",
"fleet-patterns.2"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:59Z",
"finished": "2026-11-15T04:40:19Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "system-design",
"agent": "pr-system-design-reviewer",
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "de4e3739f687",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 39,
"tokens": {
"input": 12,
"cache_write": 21420,
"cache_read": 73108,
"output": 3559
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 12,
"cache_write": 21420,
"cache_read": 73108,
"output": 3559
}
},
"session": "01a1dd7e-7536-7aa8-803c-d0a5f047dd76",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909095556",
"review_id": 5909095556,
"findings": {
"correctness": 2,
"design": 1,
"prose": 0
},
"numbers": [
"system-design.1",
"system-design.2",
"system-design.3"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:37:39Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "stale-prose",
"agent": "pr-stale-prose-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "4359b874a436",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 113,
"tokens": {
"input": 28,
"cache_write": 54300,
"cache_read": 505670,
"output": 10508
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 28,
"cache_write": 54300,
"cache_read": 505670,
"output": 10508
}
},
"session": "01a1dd7e-7534-75b5-8f97-3ac3867fae15",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909103378",
"review_id": 5909103378,
"findings": {
"correctness": 0,
"design": 0,
"prose": 3
},
"numbers": [
"stale-prose.1",
"stale-prose.2",
"stale-prose.3"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:38:53Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "failure-paths",
"agent": "pr-failure-paths-reviewer",
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "4239ed2be477",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 54,
"tokens": {
"input": 14,
"cache_write": 40052,
"cache_read": 174394,
"output": 5097
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 14,
"cache_write": 40052,
"cache_read": 174394,
"output": 5097
}
},
"session": "01a1dd7e-7533-7db8-b161-fa5272e08cd3",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909097092",
"review_id": 5909097092,
"findings": {
"correctness": 3,
"design": 0,
"prose": 0
},
"numbers": [
"failure-paths.1",
"failure-paths.2",
"failure-paths.3"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:37:54Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "contracts",
"agent": "pr-contracts-reviewer",
"model": "opus",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/persona.md",
"prompt_sha": "3cf737a7c111",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 168,
"tokens": {
"input": 50,
"cache_write": 62588,
"cache_read": 984322,
"output": 14500
},
"tokens_from": "result",
"tokens_by_model": {
"claude-opus-5-5": {
"input": 50,
"cache_write": 62588,
"cache_read": 984322,
"output": 14500
}
},
"session": "01a1dd7e-72d7-7036-8518-a8fa8a5c2814",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909109134",
"review_id": 5909109134,
"findings": {
"correctness": 1,
"design": 3,
"prose": 0
},
"numbers": [
"contracts.1",
"contracts.2",
"contracts.3",
"contracts.4"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:36:58Z",
"finished": "2026-11-15T04:39:48Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
},
{
"label": "synthesis",
"agent": null,
"model": "sonnet",
"effort": "medium",
"experiment": "none",
"arm": "default",
"reason": "STATIC",
"prompt_profile": "pr-reviewer-prompts/synthesis.md",
"prompt_sha": "8832c4b84e09",
"status": "ran",
"why": "",
"attempts": 1,
"seconds": 14,
"tokens": {
"input": 2,
"cache_write": 19100,
"cache_read": 0,
"output": 1805
},
"tokens_from": "result",
"tokens_by_model": {
"claude-sonnet-5-5": {
"input": 2,
"cache_write": 19100,
"cache_read": 0,
"output": 1805
}
},
"session": "01a1dd85-12d5-7baa-a25e-fa27dc984e51",
"worklog": "3328.1",
"worklog_refused": "",
"claude_code": "2.1.325",
"review_url": "https://github.com/datapointchris/dotfiles/pull/112#pullrequestreview-5909135940",
"review_id": 5909135940,
"findings": {
"correctness": 0,
"design": 3,
"prose": 0
},
"numbers": [
"synthesis.1",
"synthesis.2",
"synthesis.3"
],
"rules": {},
"head": "4b0c870697b9a092adbbf6b820a2a516db2fb9c6",
"kept": false,
"records": "/home/chris/.cache/run-pr-reviewers/datapointchris-dotfiles-pr112-round1-20261115T043652Z/records",
"started": "2026-11-15T04:44:14Z",
"finished": "2026-11-15T04:44:31Z",
"account": "live",
"api_key_source": "none",
"machine": "archlinux"
}
],
"rules": {
"proposed": 1,
"keys": [
"rule/shell/a-pattern-that-picks-a-program-to-run-matches-the-program-never-text-an-argument-can-carry"
],
"unfiled": []
},
"kept": [],
"share": {
"pct": 0.18514440544533328,
"least_pct": 0.17611610714011097,
"most_pct": 0.23901788653817174
}
} |
The unit ran tmux-restore's shutdown-save.sh out of the plugin checkout, so it named a path that TPM and pluginsync already own and broke whenever the plugin moved or failed to clone. tmux-shutdown-save holds a logind delay lock, waits for PrepareForShutdown, and runs resurrect's save script as the running server names it, so every save hook still fires. The unit restarts a failed watcher a few times, then stops and shows as failed. Agent-Name: claude-code Agent-Session: 378edfa1-ac93-4f22-94c0-1946cef0b677 Worklog: 3328.1
… checkout aside TPM hands what follows `#` in a plugin line to `git clone -b`, which takes a branch or a tag and no commit. So a line pinned to a full commit is cloned here at that commit before TPM runs, and fetched and moved when the pin moves. TPM then finds it installed and leaves it alone. `dotfiles check` reports a checkout that is not at its pin. A checkout cloned from another repository or branch than its line names was deleted before TPM cloned the new one, so a failed clone left tmux with no plugin. It is now moved beside the plugins directory and put back if the replacement does not arrive. Agent-Name: claude-code Agent-Session: 378edfa1-ac93-4f22-94c0-1946cef0b677 Worklog: 3328.1
…ay when nothing is whole The snapshot directory and file names come from the helpers.sh beside the save script the server names, so an @resurrect-dir setting or a change in resurrect's naming reaches this script too. repair-last with no whole snapshot left now says so on the status line and on stderr, and exits 1, rather than letting a partial restore go by unremarked. `list --json` gives each snapshot's stamp, counts, whether it is whole and whether `last` points at it. A bare call prints the usage, and an unknown command exits 2. Agent-Name: claude-code Agent-Session: 378edfa1-ac93-4f22-94c0-1946cef0b677 Worklog: 3328.1
… restart only safe programs Both andrewboldi plugins are pinned to the commits the restore was tested at, which pluginsync now holds. A config reload unsets the client-detached hook and the post-restore hook, so a running server stops saving on detach without a restart. ssh leaves the restart list, because a saved `ssh host command` would run its command again, so it comes back typed at the prompt. watch-ci and ci-runs-watch match on `bin/<name>`, because a bare name matched a pane reading a file of that name and restarted the watcher in it. Agent-Name: claude-code Agent-Session: 378edfa1-ac93-4f22-94c0-1946cef0b677 Worklog: 3328.1
A reboot lost window names, panes and the programs in them. Two causes stacked. The
client-detachedhook saved while shutdown was killing the server, and wrote a snapshot of panelines only, every pane a bare shell, with no window or state lines. resurrect then pointed
lastat it, so the restore brought back a fraction of the sessions while the whole snapshot from a
minute earlier sat unread on disk. Separately, resurrect replays only the commands it is told to,
and the Claude hooks resumed every conversation with one fixed set of flags.
This replaces that setup with tmux-restore on top of resurrect and continuum:
command with that pane's own flags. A program
@resurrect-processesnames restarts as it ran.Anything else comes back typed on the zsh prompt, waiting for Enter.
tmux-shutdown-save, run by its user unit, holds a logind delay lock and saves onPrepareForShutdown, before anything is stopped. It runs resurrect's save script as the servernames it, so every save hook fires.
tmux-snapshots repair-lastruns before every restore and pointslastat the newest wholesnapshot.
prunethins snapshots older than a day to the newest whole one an hour, and olderthan a week to one a day.
listandrestore <stamp>reach an older one.tmuxis the cold start.commit tested here. pluginsync clones a pinned plugin at its commit, since TPM's
#takes nocommit. It sets aside a checkout from another repository or branch than tmux.conf names, and puts
it back if the new clone fails.
fleet tuiboard opened in its own terminal comes back through anadapter in
configs/trust/fleet/.config/tmux-restore/agents/, ahead of the board.sesh goes: it named sessions after directories and held the restore wrapper. tmux's
choose-tree -Zsandswitch-client -ltakeprefix sandprefix Lback.claude-resurrectkeeps only
resume, which reads the warehouse's copy of a transcript.What to look at
apps/common/tmux-snapshots. Checkmeasure's test for a whole snapshot: a state line, atleast one pane, and a window line for every window a pane sits in. Check
prunenever deletesthe file
lastpoints at, and keeps the newest whole snapshot per bucket.configs/common/.config/tmux/tmux.conf, the plugin block. tmux-restore takespost-save-layoutandpre-restore-alland chains the values set before it, soforget-boardsandrepair-laststill run. continuum stays the last plugin line.src/dotfiles/providers/pluginsync.py:hold_pins, thenset_asideandsettlearoundTPM in
sync_tmux. Check that a failed clone puts the old checkout back, and that a commitafter
#is never read as a branch.configs/trust/fleet/.config/tmux-restore/agents/fleet.sh. It calls theclaude_sessionandclaude_resumefunctions of the adapter tmux-restore ships, at the pinned commit.apps/common/tmux-monitor-layout. abtop and btop now run under the shell.How it was verified
An isolated server, with its own HOME, XDG directories and socket, built a workspace, saved it,
was killed, and was started with a bare
tmux new-session -d:alpha:2ran btop as the window's own process, with no shell. resurrect records a pane's child, soit saved an empty command and the pane returned as a shell. The monitor window built by
tmux-monitor-layoutcame back whole.repair-laston a cold start, withlastpointing at a newer snapshot cut the way the shutdownsave cut it:
With no whole snapshot left, it says so and exits 1:
tmux-restore's save hook, with the new adapter, on a copy of a live snapshot against the live
server:
pluginsync on a home holding upstream resurrect, with both pins in tmux.conf:
resurrect's own matcher over saved commands, with the fleet list loaded:
survivors gave the same 448 names. The first run took 1.23 s and the next 0.05 s.
tmux-shutdown-save, started as a transient user unit, appeared insystemd-inhibit --listastmux-shutdown-save … shutdown Saving tmux sessions delay, andstopping the unit released it.
gdbus monitorprints a signal as<path>: <interface>.<member> (<args>), the shape the script matches.client-detachedhook and apost-restore hook left neither, and loaded without an error on a server holding neither.
What changes
tmux.seshand thetmfunction are gone.prefix sopens tmux's session tree, andprefix Lswitches to the last session.prs tui,abtop,yazi,oxkerandwatchrestart, and on fleet machinesfleet tui,fleet ledger,fleet inbox browse,fleet views,slate tui,watch-ciandci-runs-watchdo too.
the exception.
#changes anddotfiles plugins applyruns.dotfiles checkreports a checkout off its pin.Decisions, and what they rejected
tmux-restore rather than writing the rewriting here. Its tests take real agents through
save, kill and restore. Its edge cases, such as two panes on one conversation, a program that
renames itself and a Python venv, would be relearned. If it goes unmaintained, its ideas move
here. It is young, which is why both plugins are pinned:
The pins are held by pluginsync rather than by TPM. TPM passes the ref to
git clone -b,so only a branch or tag works there. A tag would need the upstream author to cut one.
The shutdown watcher is dotfiles' own script. A unit pointing into the plugin checkout
would be a fourth owner of the plugins path, and would break whenever the plugin moved.
The fleet programs and the adapter live under
configs/trust/fleet/. Machines off that trusthave neither program, and
source-file -qskips the list where it is absent.The shutdown save is a Wayland user unit only. A Mac relies on continuum's minute.
Smallest change that would work: dropping only the
client-detachedhook and addingrepair-last. It keeps the Claude hooks, which resumed every pane with one fixed set of flags,while 1,735 snapshots in 30 days held
--effort mediumand 420 held--model sonnet.The review
Round 1 posted 22 findings from eight reviewers and a synthesis. Each one's disposition:
apps/display/wayland/tmux-shutdown-saveStartLimitBurst=5stops a failing loopset_aside, thensettlerepair-lastreports no whole snapshot as successhelpers.sh--json, bare call exits 2set-hook -guandset -gu~watch-cimatches a pane reading that file~bin/<name>ssh host commandreruns its commanduseless-test posted no findings.
Risk and rollback
tmux-resurrectcheckout, clones the fork at its pin inits place, and clones tmux-restore at its pin. A failed clone puts upstream back.
~/go/bin/seshstays on a box until it is removed by hand, since nothing declares a Go tool'sremoval.
What this does not do
Opened by Claude Code session 378edfa1-ac93-4f22-94c0-1946cef0b677.