feat(loop): headless sbx save/publish + hh-loop VM-library skill

Adds the autonomous /loop foundation: a headless `hack-house sbx save|publish`
subcommand so a non-TUI operator can persist a built VM to the host library
through the same canonical registry path the room UI uses (no schema drift).

- snapshot.rs: hoist register_saved_snapshot/publish_snapshot/oci_image_size out
  of app.rs into a shared module used by both the TUI and the new CLI.
- registry.rs: advisory cross-process lockfile (~/.hh/registry.lock, O_EXCL spin
  + stale-reclaim) around every read-modify-write, plus atomic temp+rename store,
  so concurrent /loop wave members can't clobber each other.
- main.rs: `Sbx { Save, Publish }` subcommand wired to the shared snapshot logic.
- skills/hh-loop: the loop doctrine — value rubric, adaptive 1-3 operator
  topology, visible-tmux-by-default run flow, --record logs/film, wave scaling,
  and safe `tmux -L hh-loop` teardown.

Proven end-to-end: built+verified a stdlib VM in a sandbox, headless
save+publish -> registry entry shareable:true with a portable tar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-30 14:06:19 -07:00
parent 354205cc16
commit e5fb820e63
6 changed files with 429 additions and 94 deletions
+67
View File
@@ -11,6 +11,7 @@ mod layout;
mod net;
mod registry;
mod sbx;
mod snapshot;
mod theme;
mod ui;
@@ -76,6 +77,44 @@ enum Cmd {
#[arg(long, default_value_t = false)]
insecure: bool,
},
/// Headless snapshot ops on the VM library — the save/publish surface the
/// interactive `/sbx` grammar exposes, usable by an autonomous `/loop` runner
/// without a TUI. Writes byte-identical registry entries to the in-room path.
Sbx {
#[command(subcommand)]
action: SbxCmd,
},
}
#[derive(Subcommand)]
enum SbxCmd {
/// Commit the running sandbox to a snapshot and index it in the VM registry,
/// caching the container's `.hh-agent` manifest summary. Mirrors `/sbx save`.
Save {
/// Snapshot label (image tag / registry key).
label: String,
/// Also export a portable `hh-snapshots/hh-snap-<label>.tar` immediately.
#[arg(long, default_value_t = false)]
local: bool,
/// Running container name to commit + read the manifest from.
#[arg(long, default_value = "hack-house")]
name: String,
/// Sandbox backend: docker | podman | multipass | local.
#[arg(long, default_value = "podman")]
backend: String,
/// Origin tag recorded on the entry's `created_by`.
#[arg(long, default_value = "operator")]
created_by: String,
},
/// Mark a saved snapshot shareable, exporting a portable artifact if needed,
/// so peers can `/sbx pull` it from the library. Mirrors `/sbx publish`.
Publish {
/// Label of an already-saved snapshot (see `Sbx Save`).
label: String,
/// Skill tags for catalog filtering (repeatable: `--tag recon --tag kali`).
#[arg(long = "tag")]
tags: Vec<String>,
},
}
fn main() -> Result<()> {
@@ -164,6 +203,34 @@ fn main() -> Result<()> {
no_tls,
insecure,
} => handshake(&ip, port, &user, &password, no_tls, insecure),
Cmd::Sbx { action } => sbx_cmd(action),
}
}
/// Headless `hack-house sbx …` — the save/publish library surface for the
/// autonomous `/loop` runner. Delegates to the same `snapshot`/`sbx`/`registry`
/// code the interactive TUI uses, so there's exactly one canonical entry shape.
fn sbx_cmd(action: SbxCmd) -> Result<()> {
match action {
SbxCmd::Save {
label,
local,
name,
backend,
created_by,
} => {
let be = sbx::Backend::parse(&backend)
.with_context(|| format!("unknown backend '{backend}'"))?;
let desc = sbx::save_state(be, &name, &label, local)?;
snapshot::register_saved_snapshot(be, &name, &label, &created_by);
println!("{desc}");
Ok(())
}
SbxCmd::Publish { label, tags } => {
let line = snapshot::publish_snapshot(&label, &tags)?;
println!("{line}");
Ok(())
}
}
}