Skip to content

Add hermod mvp - #1

Merged
zerlok merged 8 commits into
mainfrom
add-hermod-mvp
Jul 10, 2026
Merged

zerlok merged 8 commits into
mainfrom
add-hermod-mvp

Conversation

@zerlok

@zerlok zerlok commented Jul 10, 2026 •

Copy link
Copy Markdown
Owner

Hermod MVP: one-command bridge to a remote dev box

Establishes the Hermod MVP — a single command that mirrors your working directory to a
remote sandbox, drops you into a persistent session running there, and cleans up on the
way out. The repo is spec-driven: behavior was defined as OpenSpec specs before any code
was written, and this PR ships both the specs and their implementation.

Why

Working on a remote dev box means hand-wiring three tools every session — Mutagen to mirror
files, SSH to get a shell, tmux to survive drops — plus threading git identity through so
commits are attributed correctly, and tearing it all down cleanly on the way out. The
friction is high enough that people avoid remote sandboxes even when the sandbox is where
the work belongs. Hermod collapses that setup into one command.

What Changes

The core loop is sync → attach → pause/teardown:

0. you run `hermod prod-box` from your project directory
1. Mutagen session starts   →  working dir mirrors to the sandbox, live
2. SSH into the sandbox      →  attach to (or create) a persistent tmux session
3. git identity carried in   →  GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL exported, commits are yours
4. you work in tmux          →  the agent (or shell) runs remotely; edits sync back
5. you detach                →  session alive → sync PAUSES (resume later);
                              session gone  → sync TEARS DOWN cleanly
  • cli — parse one invocation into a resolved, immutable intent: sandbox host alias,
    session name (-s), working directory (-C), dry-run (-n), -- passthrough to the
    remote command, ssh-config path, log level.
  • file-sync — the Mutagen mirror lifecycle: open (create or resume), flush before
    work, pause, and terminate.
  • remote-session — the remote interactive session (a tmux window reached over SSH):
    attach-or-create a persistent window, run a remote command, probe liveness.
  • git-identity — read local user.name/user.email and carry them into the remote
    session's environment; a soft feature that never hard-fails when identity is unset.
  • session-orchestration — the end-to-end flow and its central decision: remote-session
    liveness after detach is the single source of truth for pause vs teardown.
    Guarantees mirror teardown even on interrupt/error.

Dry-run (-n) prints a copy-pasteable plan of side effects; read-only probes (git identity,
sync status) stay real even under --dry-run.

Also included: Makefile (build/test/lint + dev workflow), CI and release GitHub Actions
workflows, and bash completions installed via make install.

Implementation notes

  • Greenfield Go module — a single hermod binary with no runtime dependencies. It
    orchestrates tools already on the machine (Mutagen, ssh, tmux) and reads
    ~/.ssh/config; it does not bundle or provision them.
  • Domain packages: internal/cli, internal/mirror, internal/sandbox, internal/git,
    internal/shell (local/ssh/tmux/login-shell execution, quoting, exit codes).
  • Archives the completed add-hermod-mvp OpenSpec change and syncs its 5 delta specs into
    openspec/specs/ as the project's baseline.

zerlok and others added 6 commits July 10, 2026 22:39
Add the Go implementation of the single-command remote-sandbox bridge,
layered cli → control → git/mirror/sandbox → shell → execx:

- execx: how a command runs — real subprocess vs printed dry-run, behind New(opts)
- shell: where it runs — ssh/tmux decorators nested by the caller, over a leaf Factory
- git: read local author identity (from the mirrored dir) to carry into the remote
- mirror: the Mutagen file mirror — open (create-or-resume)/flush/pause/close/status
- sandbox: the tmux-over-ssh session — attach-or-create + read-only liveness probe
- control: apply the invocation options, run the flow, and settle the mirror by
  remote-session liveness alone (alive/unknown → pause; gone → flush then close)
- cli: cobra edge (-s/-C/-n/-q, -- passthrough, ssh-host completion) + entrypoint

Carry both GIT_AUTHOR_* and GIT_COMMITTER_*; default the session name to a path
slug. Reconcile docs/ARCHITECTURE.md, the MVP design, and the cli spec with the
implemented design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The execx package existed only to give shell its leaf executor, but its
Command/Result types leaked into git, mirror, and sandbox purely as the
Shell.Run signature. Fold execx into shell: Command/Result/ExitCode become
shell types, and the real-vs-dry-run choice collapses into the leaf as an
internal run func selected by NewLocal(dryRun bool) — no public Executor,
subprocess, or dry-run surface remains.

Also drop shell.Factory: control now builds the two leaves (real, effective)
directly and hands each collaborator only the shell it needs, matching what
ARCHITECTURE.md already described. Reconcile the docs (remote→sandbox,
git.Reader→git.Git, dropped Executor/Resolve references) and fix the
"sandbox sandbox" doubling.

Dependency graph simplifies to git/mirror/sandbox → shell; net -164 lines,
one package deleted. Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire signal.NotifyContext(SIGINT, SIGTERM) into the command context so an
interrupted attach is killed and unwinds through the normal teardown path
instead of leaking the mirror. Teardown itself runs on context.WithoutCancel
so cleanup survives the cancellation that triggered it — otherwise the probe
and pause/close calls would fail on the dead context, leaking the very mirror
the handler exists to settle.

Log the final pre-terminate flush failure instead of swallowing it: a failed
flush risks losing unsynced edits, so surface it (termination still proceeds,
since a live "gone" session is a leak). Cover the path with a teardown test.

Refine comments per the code's own principle (why, not what): drop the
WithWorkdir play-by-play and name-restating setter docs, and clarify NewLocal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a Makefile as the single entry point for development commands
(build, test, lint, install, release) and point the README there instead
of listing raw go commands.

Build and release targets are real file targets keyed on the source tree,
so Make skips binaries already newer than every source; release fans out
over PLATFORMS via $(foreach) to cross-compile static binaries. Ignore the
bin/ output directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a completion target so `make install` regenerates the bash
completion script from the freshly installed binary into the XDG
per-command directory, keeping it in lockstep with the installed
version. Modeled as real file targets keyed on sources, so an
unchanged tree is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI runs lint/test/build via the Makefile on PRs and pushes to main.
Release cross-compiles per-platform binaries and attaches them with
checksums to a published GitHub Release, using the tag as the version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zerlok zerlok self-assigned this Jul 10, 2026
@zerlok zerlok added the enhancement New feature or request label Jul 10, 2026
zerlok and others added 2 commits July 11, 2026 01:02
A passthrough like `hermod host -- claude` exited immediately: tmux runs
an explicit command in a bare non-login shell, so a PATH-installed tool
(claude) is not found. The empty-command case worked only because tmux's
default session is itself a login shell.

Wrap the command via `exec "$SHELL" -lc "$1"` so it inherits the sandbox
user's full PATH and profile. The wrapping is a new NewLoginShell shell
decorator the sandbox composes into its interactive stack — the tmux
transport stays generic; the login-shell policy lives with the session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the completed add-hermod-mvp change to the dated archive and sync
its 5 delta specs into openspec/specs/ as the project's baseline main
specs: cli, file-sync, git-identity, remote-session,
session-orchestration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zerlok
zerlok merged commit 6f6c10b into main Jul 10, 2026
1 check passed
@zerlok
zerlok deleted the add-hermod-mvp branch July 10, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant