4 Commits

Author SHA1 Message Date
leetcrypt fcb3cb8684 chore(attribution): commit Cargo.lock for ed25519-dalek dep
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:16:46 -07:00
Claude (hack-house) f4180edf61 feat(attribution): pseudonymous file attribution (ESA-style)
Adapt Princess_Pi's Encrypt-Share-Attribution scheme so shared files are
anonymous but provably attributable.

A. In-session: a persistent Ed25519 persona key (~/.config/hack-house/
   persona_ed25519) signs every /send and /sendroom offer over the content
   hash; receivers verify it and see the persona fingerprint. Optional
   --attest <passphrase> attaches a revealable SHA-512(pass||sha256)
   commitment. Additive JSON — wire-compatible with the Python client.

B. Portable: /export-signed <dir> packages a directory into Princess_Pi's
   exact ESA 7z (fresh per-round Ed25519 sig over an inner 7z, SHA-512
   checksums, bundled verify scripts). Builder embedded from
   hh/tools/esa/esa_build.sh; verifiable with just bash+7z+ssh-keygen.

Tests: 45 pass (3 new persona). ESA archive build + verify-everything.sh +
passphrase reveal verified end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 12:28:07 -07:00
leetcrypt 7fb3911550 feat(bench): multi-language capability benchmark + model picker
Add bench-lang.py + bench/ package: a third benchmark axis answering
"which open-source model is best for my workflow?" across Python,
JavaScript, Go, Rust and Bash.

- MultiPL-E (Go/Rust/JS/Bash) + original HumanEval (Python), loaded via
  the HF datasets-server REST API with on-disk cache — no datasets/
  pyarrow dependency.
- Completions go straight to Ollama /api/generate with raw=True so
  instruct models continue the code instead of replying with prose.
- Code runs in rootless, network-less podman (safe default) with a
  host-toolchain fallback; pass@1/pass@k via the HumanEval estimator.
- run/score separation: results persist to a scorecard JSON, then
  `pick --workflow ops` re-ranks without re-running any model.
- Extensible: a new language is one Lang entry; a new workflow is one
  block in workflows.json.

Also fix a --runs grant-persistence bug in bench-sandbox.py: the grant
leaked across runs, invalidating the L0-nogrant refusal test on runs 2+.
Each run now revokes the ACL and starts ungranted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-17 09:39:27 -07:00
leetcrypt c5715ba2e3 test(scripts): add E2E AI chat + sandbox code-path benchmarks
bench-ai.py drives the /ai chat path end-to-end (SRP -> Fernet -> WebSocket
-> provider), measuring TTFT/total/tok-s per model, with a --direct provider
mode that isolates raw model throughput from event-loop contention.

bench-sandbox.py benchmarks the /ai <agent> !<task> sandbox code path by
playing the room owner on the zero-knowledge relay: it grants drive, sends
graded tasks (L0 no-grant refusal, file/script/logic/multistep, destructive
gating+confirm, blast-radius cap), captures the agent's injected _sbx:input
frames, and grades correctness behind the same destructive guard. Includes
REPL-aware exec replay (folds python3 sessions into heredocs), prompt-prefix
stripping, model-fail vs replay-limit tagging, --runs N averaging with
per-step timing, and auto-bumped timeouts for reasoning models.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-16 21:41:18 -07:00
206 changed files with 3133 additions and 17980 deletions
-14
View File
@@ -21,13 +21,6 @@ secured_console_chat.egg-info/
*.log *.log
err.log err.log
# Sandbox save/load snapshots (large runtime tarballs, not source)
/hh/hh-snapshots/
# VM-library /loop artifacts — exported snapshot tars (repo-root, when `sbx`
# is run from here) + per-run logs. Generated; the VMs live in the host registry.
/hh-snapshots/
/.loop-runs/
# Out-of-tree experiments (not part of hack-house) # Out-of-tree experiments (not part of hack-house)
/experiments/ /experiments/
/headroom/ /headroom/
@@ -35,13 +28,6 @@ err.log
# Heavy / superseded demo-build kit (real demos live in video-toolkit) # Heavy / superseded demo-build kit (real demos live in video-toolkit)
/docs/demo/ /docs/demo/
# Native-harness benchmark harness + results — dev-internal test tooling, kept
# on disk but never pushed to origin (coupled to the local tmux+podman test rig)
/bench/
# Local-only planning/design docs (kept on disk, never pushed to origin)
/docs/plans/
# Project scratch # Project scratch
/i-try/ /i-try/
test_rsa.py test_rsa.py
-102
View File
@@ -1,102 +0,0 @@
# hack-house — Claude operating guide
Encrypted collaborative terminal sessions with a summoned, shareable sandbox.
**Rust TUI client** (`hh/`) + **Python server & operator bridge** (`cmd_chat/`).
A Claude Code session can join a room as a **first-class member**: read/answer
humans and other AIs, and — once granted — drive the shared sandbox with full
shell + keystroke control. VMs carry a self-describing `.hh-agent` manifest so
work + memory hand off between agents (and humans), and saved VM states are
tradeable via a host-global registry.
## Operating a room — use the `hh-operator` skill
To enter/run/watch a room, bridge a Claude session into one, or autonomously
monitor and respond, **use the `hh-operator` skill** (`~/.claude/skills/hh-operator/`).
It documents the full doctrine: the daemon + `hh-bridge` CLI, the read→think→act
loop, sandbox drive (exec/write/get), the keystroke relay + stop-vocabulary, the
`watch` stop-condition engine, and the `.hh-agent` manifest for handoffs.
```bash
# Run from this repo root; pin the interpreter to the venv.
HH=".venv/bin/python -m cmd_chat.operator" # = the hh-bridge CLI
$HH up <host> <port> <name> --password <pw> --no-tls # join (spawns the daemon)
$HH read --wait --timeout 30 # long-poll for activity
$HH say "…" # chat into the room
$HH keys "make build" enter ; $HH watch --for 'PASS|FAIL' --in screen # drive
$HH manifest push --root /root --name <vm> --purpose … --objective … # handoff
$HH spawn "<objective incl. the full join cmd>" --room-host … --go --skip-permissions
```
## Mobile operator on Android / Termux — the phone is REACHABLE, use it
Spec: `docs/termux-operator.md`. The operator side is pure Python and runs on
the **Fairphone-6** in Termux — it can both **join** and **host** rooms (the SRP
shim now covers the server side too, so no Rust / no `srp` C-ext). You can drive
the device directly over SSH — so **verify reachability before ever telling the
user "this can only run on-device, paste it back."** That framing is wrong when
the phone answers SSH.
One canonical, hardened invocation (use this prefix everywhere — it sidesteps
the wrong-alias trap and never hangs):
```bash
PHONE='ssh -i ~/.ssh/phone-deploy -p 8022 -o ConnectTimeout=8 -o BatchMode=yes -o StrictHostKeyChecking=accept-new u0_a203@100.95.202.68'
$PHONE 'uname -m; python --version' # aarch64 / Python 3.13.x
```
- **Reachability gate — probe the PORT, not `tailscale status`:**
`timeout 8 bash -c 'cat </dev/null >/dev/tcp/100.95.202.68/8022' && echo REACHABLE || echo OFFLINE`.
Tailscale reports `fairphone-6` **`active` even when sshd is dead** — Android
kills backgrounded Termux, taking `sshd` with it. The tailnet line is NOT
proof; the TCP probe is. If OFFLINE, the fix is **on-device** (wake the phone,
relaunch Termux, `termux-wake-lock` + start `sshd`) — say so, don't retry a
dead port.
- **Trap:** the `~/.ssh/config` aliases `phone` / `phone-deploy` point at
`100.95.180.14` (`trilluminati`), a *different, usually-offline* phone —
`ssh phone` will time out. Use the Fairphone IP `100.95.202.68` (the `$PHONE`
prefix), not the alias.
- Termux facts: Python 3.13 (≥3.11); **no `/tmp`**`$TMPDIR` is
`$HOME/.claude-temp`; `$PREFIX=/data/data/com.termux/files/usr`; `rustc`/
`openssl` not installed by default.
- Deps that work: `pkg install python-cryptography` (avoid a rust build),
`pip install requests rich websockets`; **skip `srp`** — both client and
server fall back to `cmd_chat/client/_srp_pure` (pure-Python SRP-6a shim).
- Push-restricted work: copy this tree to the phone with tar-over-ssh rather
than a stale gitea clone —
`tar czf - cmd_chat scripts requirements-operator.txt | $PHONE 'tar xzf - -C ~/hh-mobile'`.
- Phase-0 gate: `python scripts/termux-preflight.py` on the phone → expect
`RESULT: PASS`. Operator files staged at `~/hh-mobile` on-device.
- **Proven live (2026-07-07):** phone JOINS a dev-box room; the phone HOSTS a
room the laptop joins (cross-impl SRP: laptop C-ext ↔ phone pure Verifier);
and the stdlib mobile web console (`python -m cmd_chat.operator web`) mirrors a
real room (terminal + keystroke drive + chat) in a mobile browser.
## Key components
| What | Where |
|------|-------|
| Operator bridge (daemon + CLI) | `cmd_chat/operator/__main__.py`, `bridge.py` |
| Agent manifest (`hh-agent/v1`) | `cmd_chat/operator/manifest.py``.hh-agent/` bundle library + CLI |
| Recursive spawn (Claude-spawns-Claude) | `cmd_chat/operator/bootstrap.py` (budgeted: `--depth`/`--fanout`) |
| Saved-VM registry / discovery | `hh/src/registry.rs``~/.hh/registry.json`; reader: `$HH registry list\|show` |
| Sandbox grammar (TUI) | `/sbx podman\|launch`, `/grant <name>`, `/sbx save`, `/sbx browse`, `/sbx publish`, `/sbx catalog @user`, `/sbx pull @user <label>` |
| Server | `cmd_chat.py serve <host> <port> --password <pw> [--no-tls]` |
| Rust client | `hh/target/debug/hack-house connect <host> <port> <name> --password <pw>` |
## Demo videos — use the `video-toolkit`
hack-house feature reels are produced with `~/coding/video-toolkit` (headless
asciinema-in-tmux capture via `bin/tmux-demo.py`, then a `video-forge` cut).
- **Flagship reel — three Claudes, one shared VM:** `scenarios/hh-vmhub.json`
(capture) + `scenarios/hh-vmhub-cut.json` (forge cut). A planner, builder, and
tester (each a real nested `claude -p`) collaborate to build **VMHub** — a
GitHub-for-VMs web app — live inside one shared sandbox, then save it to the VM
library. Role task-scripts: `scenarios/vmhub-{builder,tester}-task.sh`; staged
app under `scenarios/assets/vmhub/`.
- A reel must **show the product**: the Rust TUI room (roster, encrypted chat, the
shared sandbox F2 view) with Claude visibly operating as a member and
Claude-spawning-Claude — not just a console of CLI calls.
> Provisioning note: the demo summons a pre-baked `localhost/hh-dev:slim` (python
> 3.11) podman sandbox so VMHub (stdlib, zero pip deps) runs with no installs.
+36 -54
View File
@@ -32,12 +32,11 @@ Encrypted chat that runs in your terminal. You host the server, you control the
- **SRP authentication** — the password is never sent over the network (zero-knowledge proof) - **SRP authentication** — the password is never sent over the network (zero-knowledge proof)
- **Zero-knowledge server** — relays only ciphertext; cannot read messages, files, or terminal output - **Zero-knowledge server** — relays only ciphertext; cannot read messages, files, or terminal output
- **RAM only** — nothing persisted on the server; close it and history is gone - **RAM only** — nothing persisted on the server; close it and history is gone
- **Shared sandbox** — summon a disposable `local` / `docker` / `podman` / `multipass` box the whole room can watch and drive. Docker defaults to **Parrot OS Security** (`parrotsec/core`) and Podman to **Kali** (`kalilinux/kali-rolling`) — pentest distros out of the box; Podman is rootless & daemonless, so **no sudo** to launch - **Shared sandbox** — summon a disposable `local` / `docker` / `multipass` box the whole room can watch and drive
- **Snapshot save/load** — freeze a sandbox to a named snapshot and restore it later (`/sbx save` · `/sbx load` · `/sbx snaps`) - **Snapshot save/load** — freeze a sandbox to a named snapshot and restore it later (`/sbx save` · `/sbx load` · `/sbx snaps`)
- **Local VirtualBox VMs** — `/sbx vms` detects VirtualBox and lists your VMs; `/sbx gui <vm>` opens a desktop VM locally for the room to gather around — per-user consent gate, with automatic resolution of VT-x conflicts (Docker Desktop / multipass) - **Local VirtualBox VMs** — `/sbx vms` detects VirtualBox and lists your VMs; `/sbx gui <vm>` opens a desktop VM locally for the room to gather around — per-user consent gate, with automatic resolution of VT-x conflicts (Docker Desktop / multipass)
- **Real permissions** — the host grants/revokes *drive* (keyboard) and *sudo* (VM superuser) per user; **stacking roster badges** show exactly who holds what, both in the clergy panel and inline on every chat message - **Real permissions** — the host grants/revokes *drive* (keyboard) and *sudo* (VM superuser) per user; **stacking roster badges** show exactly who holds what, both in the clergy panel and inline on every chat message
- **Local-first AI agent** — `/ai start` summons an in-room AI that runs against *your own* [Ollama](https://ollama.com) (no API key, nothing leaves your machine); replies **stream token-by-token** with **in-RAM semantic recall** of the conversation for context; model-agnostic, addressed-only, end-to-end encrypted like every other client - **Local-first AI agent** — `/ai start` summons an in-room AI that runs against *your own* [Ollama](https://ollama.com) (no API key, nothing leaves your machine); replies **stream token-by-token** with **in-RAM semantic recall** of the conversation for context; model-agnostic, addressed-only, end-to-end encrypted like every other client
- **AI that acts in the sandbox** — grant an agent *drive* and address it with `/ai <name> !<task>`; it works the shared box through a bounded, host-side **tool-calling loop** (run shell, write/read files, inspecting each result before the next step) and you watch its commands land live in the shared terminal. Ungranted, it stays **advisory-only** (tells you the commands, runs nothing); destructive commands are gated behind an explicit `/ai <name> confirm`
- **Encrypted file transfer** — `/send``/accept` with SHA-256 verification - **Encrypted file transfer** — `/send``/accept` with SHA-256 verification
- **TLS** — self-signed by default, or bring your own cert; `--no-tls` for local/Tailscale use - **TLS** — self-signed by default, or bring your own cert; `--no-tls` for local/Tailscale use
- **Themes** — seven switchable "vestments" (`crypt` default · `church` · `neon` · `blush` · `matrix` · `wraith` · `goldcrypt`), plus a live randomizer - **Themes** — seven switchable "vestments" (`crypt` default · `church` · `neon` · `blush` · `matrix` · `wraith` · `goldcrypt`), plus a live randomizer
@@ -61,6 +60,17 @@ git clone https://git.churchofmalware.org/trilltechnician/hack-house.git
cd hack-house cd hack-house
``` ```
> **Private beta.** The repo is private for now, so an anonymous clone returns
> *Not found*. Until it's public, clone with your Gitea credentials:
>
> ```bash
> # Personal access token (Gitea → Settings → Applications → Generate Token)
> git clone https://<user>:<token>@git.churchofmalware.org/trilltechnician/hack-house.git
>
> # …or SSH (add your key under Gitea → Settings → SSH Keys)
> git clone git@git.churchofmalware.org:trilltechnician/hack-house.git
> ```
### 1. One-shot setup (`hh/scripts/bootstrap.sh`) ### 1. One-shot setup (`hh/scripts/bootstrap.sh`)
Checks prerequisites, creates the Python venv, installs the server's Checks prerequisites, creates the Python venv, installs the server's
@@ -150,22 +160,18 @@ Type to chat. Slash commands and keys:
| `/help` · `F1` | Help overlay | | `/help` · `F1` | Help overlay |
| `/pw` | Show this room's password (local only — never broadcast) | | `/pw` | Show this room's password (local only — never broadcast) |
| `/theme [name]` | Switch vestments, or list them | | `/theme [name]` | Switch vestments, or list them |
| `/send <user> <path>` | Offer a file (or directory) directly to one member | | `/send <path>` | Offer a file (or directory) to the room |
| `/sendroom <path>` | Offer a file (or directory) to the whole room |
| `/accept` · `/reject` | Respond to a pending file offer | | `/accept` · `/reject` | Respond to a pending file offer |
| `/clear` | Wipe your chat scrollback (local only) | | `/ai start [model\|profile]` | Summon a local AI agent (default `ollama/qwen2.5:3b`; a bare name is a `models.toml` profile) |
| `/ai start [model\|profile] [allow]` | Summon a local AI agent (default `ollama/qwen2.5:3b`; a bare name is a `models.toml` profile). `allow` auto-grants it sandbox drive at spawn |
| `/ai stop` | Dismiss the agent you summoned | | `/ai stop` | Dismiss the agent you summoned |
| `/ai <question>` | Ask the agent (`/ai <name> <question>` if several present) | | `/ai <question>` | Ask the agent (`/ai <name> <question>` if several present) |
| `/ai <name> !<task>` | Have a *granted* agent act in the shared sandbox (advisory-only if it has no drive) |
| `/ai <name> confirm` | Approve a gated (destructive) command the agent proposed |
| `/ai list` | List the agents present (or hint to `/ai start` if none) | | `/ai list` | List the agents present (or hint to `/ai start` if none) |
| `/ai models` | Models the active agent can serve — or, with no agent, your local Ollama tags | | `/ai models` | Models the active agent can serve — or, with no agent, your local Ollama tags |
| `/sbx <local\|docker\|podman\|multipass> [image] [install]` | Summon the shared sandbox — the backend leads (`/sbx launch …` still works). Docker → **Parrot OS** (`parrotsec/core`), Podman → **Kali** (`kalilinux/kali-rolling`, rootless, no sudo). `install` fetches a missing backend; `docker --start` boots a stopped daemon | | `/sbx launch [local\|docker\|multipass] [image] [install]` | Summon the shared sandbox (`install` fetches a missing backend; `--start` boots a stopped Docker daemon) |
| `/sbx stop` | Tear down the sandbox you host | | `/sbx stop` | Tear down the sandbox you host |
| `/sbx save [label]` · `/sbx load <label>` · `/sbx snaps` | Snapshot the sandbox, restore one, or list snapshots | | `/sbx save [label]` · `/sbx load <label>` · `/sbx snaps` | Snapshot the sandbox, restore one, or list snapshots |
| `/sbx vms` | Detect VirtualBox and list local VMs | | `/sbx vms` | Detect VirtualBox and list local VMs |
| `/sbx vbox [new [name]]` | Open the local VirtualBox VM picker, or build a fresh VM via cloud-init | | `/sbx launch vbox [new [name]]` | Open the local VirtualBox VM picker, or build a fresh VM via cloud-init |
| `/sbx gui <vm> [--install]` | Open a local VirtualBox desktop VM for the room (consent-gated) | | `/sbx gui <vm> [--install]` | Open a local VirtualBox desktop VM for the room (consent-gated) |
| `/drive` · `F2` | Take the shared shell (`Esc` releases) | | `/drive` · `F2` | Take the shared shell (`Esc` releases) |
| `/grant <user>` · `/revoke <user>` | Owner: delegate/withdraw drive | | `/grant <user>` · `/revoke <user>` | Owner: delegate/withdraw drive |
@@ -179,7 +185,7 @@ Type to chat. Slash commands and keys:
### The shared sandbox ### The shared sandbox
Anyone in the room can summon a disposable Linux box with `/sbx <backend>`. The Anyone in the room can summon a disposable Linux box with `/sbx launch`. The
person who summons it is the **owner/host**: their client runs the real PTY person who summons it is the **owner/host**: their client runs the real PTY
locally and relays its output to everyone else as encrypted frames, so the locally and relays its output to everyone else as encrypted frames, so the
server only ever sees ciphertext (same trust model as chat). server only ever sees ciphertext (same trust model as chat).
@@ -187,15 +193,10 @@ server only ever sees ciphertext (same trust model as chat).
| Backend | Isolation | Notes | | Backend | Isolation | Notes |
|---|---|---| |---|---|---|
| `local` | none | a `bash` shell on the host — fast, for dev/testing only | | `local` | none | a `bash` shell on the host — fast, for dev/testing only |
| `docker` | container | **Parrot OS Security** (`parrotsec/core`) by default — swap `parrotsec/security` per-launch for the full pentest set; `/sbx docker --start` boots the daemon (or run `hh/scripts/ensure-docker.sh`) | | `docker` | container | `ubuntu:24.04` by default; `/sbx launch docker --start` boots the daemon (or run `hh/scripts/ensure-docker.sh`) |
| `podman` | container | **Kali rolling** (`kalilinux/kali-rolling`) by default — **rootless & daemonless, no sudo to launch** (add `kali-linux-headless` for the toolset); `hh/scripts/ensure-podman.sh` installs it |
| `multipass` | full VM | `24.04` by default; strongest isolation, ~30 s to boot, the choice for real use | | `multipass` | full VM | `24.04` by default; strongest isolation, ~30 s to boot, the choice for real use |
The backend leads the command — `/sbx podman`, `/sbx docker`, `/sbx multipass`, Tear it down with `/sbx stop` (purges the VM/container).
`/sbx local` (the older `/sbx launch <backend>` still works). Override the image
positionally, e.g. `/sbx docker parrotsec/security` or `/sbx podman ubuntu:24.04`.
Both container engines are Debian/apt-based, so the dev-toolchain bootstrap runs
unchanged. Tear it down with `/sbx stop` (purges the VM/container).
**Snapshots.** Freeze the current sandbox to a named checkpoint with `/sbx save **Snapshots.** Freeze the current sandbox to a named checkpoint with `/sbx save
[label]`, list what you've stored with `/sbx snaps`, and restore one later with [label]`, list what you've stored with `/sbx snaps`, and restore one later with
@@ -245,9 +246,8 @@ they can never advertise a power the room won't honour.
### Sharing files & directories ### Sharing files & directories
`/send <user> <path>` proposes a transfer to one member; `/sendroom <path>` `/send <path>` proposes a transfer; recipients `/accept` or `/reject`. A whole
offers it to everyone. Recipients `/accept` or `/reject`. A whole directory directory works too (it's packed before sending). Files are chunked (64 KB),
works too (it's packed into a `.tar` before sending). Files are chunked (64 KB),
encrypted with the room key, relayed as opaque ciphertext, and **SHA-256 encrypted with the room key, relayed as opaque ciphertext, and **SHA-256
verified** on arrival before landing in `./downloads/`. Max size is 50 MB. verified** on arrival before landing in `./downloads/`. Max size is 50 MB.
@@ -264,15 +264,6 @@ when you quit). Pick a model at summon time with `/ai start <model>`.
- **Addressed-only.** The agent reads room traffic like any client but forwards - **Addressed-only.** The agent reads room traffic like any client but forwards
to the model *only* the messages that trigger it (`/ai …`) — no passive to the model *only* the messages that trigger it (`/ai …`) — no passive
surveillance, no cost or noise when idle. surveillance, no cost or noise when idle.
- **Can drive the sandbox.** Grant an agent *drive* (`/grant <name>`, or summon it
pre-granted with `/ai start <name> allow`) and ask it to act with
`/ai <name> !<task>`. It works the shared box through a bounded **host-side
tool-calling loop** — run shell commands, write and read files — inspecting each
result before the next step, and you watch its commands appear live in the
shared terminal. Every command runs *inside the sandbox* (the container/VM is the
blast radius), capped in count and time. Without drive it stays **advisory-only**
(it spells out the commands, runs nothing). Destructive commands are blocked
pending an explicit `/ai <name> confirm`.
- **Model-agnostic.** Swap the backend without touching the client: bundled - **Model-agnostic.** Swap the backend without touching the client: bundled
adapters for `ollama` (default), `anthropic`, and any OpenAI-compatible adapters for `ollama` (default), `anthropic`, and any OpenAI-compatible
endpoint (OpenAI, Groq, Together, local vLLM…), plus a `module:Class` hook for endpoint (OpenAI, Groq, Together, local vLLM…), plus a `module:Class` hook for
@@ -307,32 +298,24 @@ directly:
Seven bundled themes — `crypt` (default, neutral monochrome, `✝` sigil), Seven bundled themes — `crypt` (default, neutral monochrome, `✝` sigil),
`church`, `neon`, `blush`, `matrix`, `wraith`, and `goldcrypt`. Switch live with `church`, `neon`, `blush`, `matrix`, `wraith`, and `goldcrypt`. Switch live with
`/theme <name>`, list them with bare `/theme`, roll a fresh randomized vestment `/theme <name>`, list them with bare `/theme`, roll a fresh randomized vestment
with `Ctrl+Alt+P` (keep one you like with `/theme save [name]`), or load your own with `Ctrl+Alt+P` (and save it to disk), or load your own TOML at launch with
TOML at launch with `--theme <path>` (see `hh/themes/`). Each theme defines its `--theme <path>` (see `hh/themes/`). Each theme defines its own sigil, colours,
own sigil, colours, and roster width. and roster width.
### Window layout ### Window layout
The chat, roster (clergy), sandbox-terminal, and message-input panes are The chat, roster (clergy), and sandbox-terminal panes are resizable and can be
resizable and can be fullscreened — live, with no restart. Resizing the terminal fullscreened — live, with no restart. Resizing the terminal also re-syncs the
also re-syncs the shared PTY grid so everyone in the room sees the same shared PTY grid so everyone in the room sees the same dimensions.
dimensions.
- **Fullscreen** — `F4` cycles the sandbox terminal fullscreen → chat - **Fullscreen** — `F4` cycles the sandbox terminal fullscreen → chat
fullscreen → back to the split. The input bar always stays visible, so fullscreen → back to the split. The 3-line input bar always stays visible, so
`F4` always brings you back. `F4` always brings you back.
- **Resize a pane** — click a pane (or press `F5` to cycle the selection: - **Resize a pane** — click a pane (or press `F5` to cycle the selection:
chat → terminal → roster → input). The selected pane gets a bold accent border terminal → chat → roster). The selected pane gets a bold accent border and an
and an `✎` marker in its title. The chat/terminal/roster panes form a binary `✎` marker in its title. Then:
split tree, so **every pane resizes on both axes** wherever a divider bounds it: - `↑` / `↓` grow / shrink the **terminal** or **chat** pane's height share
- `` / `` grow / shrink the selected pane's **height**. With a sandbox up, - `` / `` narrow / widen the **roster** column (down to hidden)
chat trades against the terminal directly below it. With no sandbox, chat and
the clergy instead **borrow from the message bar**`↑` grows the chat box
(shrinks the input), `↓` does the reverse — so vertical resize always moves
something. The **input bar** itself is also selectable and grows on `↑/↓`.
- `←` / `→` grow / shrink the selected pane's **width** (the chat/terminal
column trades with the roster, down to hidden). Resizing the terminal's
width now re-syncs the shared PTY too, not just its height.
- `Esc` or `Enter` finishes editing. - `Esc` or `Enter` finishes editing.
- **Presets** — save the current arrangement and recall it later: - **Presets** — save the current arrangement and recall it later:
@@ -377,11 +360,10 @@ supports `-h` / `--help`** for full usage.
| Script | What it does | | Script | What it does |
|---|---| |---|---|
| `ensure-docker.sh` | Install Docker and/or start its daemon (idempotent; `--check`/`--plan`/`--yes`). Invoked by `/sbx docker`. | | `ensure-docker.sh` | Install Docker and/or start its daemon (idempotent; `--check`/`--plan`/`--yes`). Invoked by `/sbx launch docker`. |
| `ensure-podman.sh` | Install Podman (rootless; sets up subuid/subgid). Daemonless — nothing to start. Invoked by `/sbx podman`. | | `ensure-multipass.sh` | Install Multipass. Invoked by `/sbx launch multipass`. |
| `ensure-multipass.sh` | Install Multipass. Invoked by `/sbx multipass`. | | `ensure-vbox.sh` | Install VirtualBox (warns on Secure Boot). Invoked by `/sbx launch virtualbox` and `/sbx gui`. |
| `ensure-vbox.sh` | Install VirtualBox (warns on Secure Boot). Invoked by `/sbx vbox` and `/sbx gui`. | | `vbox-new.sh` | Create + boot a fresh Ubuntu VirtualBox VM via cloud-init. Invoked by `/sbx launch virtualbox`. |
| `vbox-new.sh` | Create + boot a fresh Ubuntu VirtualBox VM via cloud-init. Invoked by `/sbx vbox new`. |
| `sandbox-bootstrap.sh` | Baseline dev-tool install piped into a Docker sandbox at provision time (package list from `sandbox-tools.json`). | | `sandbox-bootstrap.sh` | Baseline dev-tool install piped into a Docker sandbox at provision time (package list from `sandbox-tools.json`). |
| `sandbox-tools.json` | Editable package list consumed by `sandbox-bootstrap.sh`. | | `sandbox-tools.json` | Editable package list consumed by `sandbox-bootstrap.sh`. |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 851 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 KiB

Some files were not shown because too many files have changed in this diff Show More