docs: add operator guide + model-agnostic operator plan

CLAUDE.md operating guide (room/operator/manifest/VM-registry map +
demo-toolkit pointers) and docs/model-agnostic-operator-plan.md — the
phased spec to let any function-calling model operate a room, not just
Claude (hoist Provider core, harness-mode operator, runner registry,
portable CAPABILITIES, role presets, MCP).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-28 22:53:47 -07:00
parent 12ae553ca1
commit 7fbae3ca84
2 changed files with 409 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# 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
```
## 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.
+350
View File
@@ -0,0 +1,350 @@
# Model-Agnostic Operator — Implementation Plan
**Status:** Draft / proposed
**Author:** drafted with Claude (operator session)
**Date:** 2026-06-28
**Scope:** Make *any* AI model able to connect as a hack-house operator (drive a
room + shared sandbox, hand off work, spawn nested operators) — not just Claude
Code. Unify the two divergent AI integration paths under one Provider core, and
lift role/coordination context out of bespoke shell scripts into reusable presets.
---
## 1. Motivation — the asymmetry we are closing
hack-house has **two independent AI integration paths** with opposite degrees of
model-agnosticism:
| | `cmd_chat/agent/` — the `/ai` chat agent | `cmd_chat/operator/` — the operator bridge / spawn |
|---|---|---|
| Model coupling | **Fully pluggable** | **Hardcoded to Claude Code** |
| Mechanism | `providers.py` `Provider` Protocol + `module:Class` plugins; `profiles.py` `models.toml` presets | `bootstrap.build_run_argv``["claude","-p",directive]` |
| Tool calling | `complete_with_tools` + capability probe + native harness loop (`agent/bridge.py` `NATIVE_TOOLS`) | delegated to Claude Code's own loop |
| Token accounting | real `prompt_eval_count`/`eval_count`, budgeted against true tokens | none (the operator *is* the model's own session) |
| Install / creds | n/a (HTTP) | installs `@anthropic-ai/claude-code`, carries `~/.claude/.credentials.json` |
The brain that **drives the room + sandbox and spawns nested operators is
Claude-only**, while the **in-room chat brain is already any-model**. We already
own every primitive needed to close the gap: `providers.py`, `profiles.py`,
`bootstrap.Budget`, the native harness loop, the `.hh-agent` manifest, and the
`watch` stop-condition engine. This plan wires them together.
### Goals
- Any function-calling model (Ollama-local, Groq, GPT-4o, vLLM, Anthropic API)
can act as a first-class operator with **no CLI install and no creds carry**.
- `spawn` is generalized beyond `claude -p` to a small **runner registry**
(claude / codex / gemini / generic) while keeping the existing budget + gated
creds machinery.
- One **portable capabilities prompt** (Layer-1) is the single source of truth,
delivered by-skill to Claude and by-injection to everyone else.
- Role behaviours (planner/builder/tester) become **declarative presets**, not
copy-pasted shell scripts.
### Non-goals
- Not replacing Claude Code subprocess operators — they stay as one runner kind.
- Not building a new agent framework; we borrow patterns (LiteLLM/Continue for
providers, Swarm/CrewAI for handoffs+roles) but keep the room-as-bus +
shared-VM-as-workspace model that already differentiates hack-house.
- No secrets in any committed file (presets name `api_key_env`, never the key).
---
## 2. Target architecture
An operator = **(a brain) + (the hack-house toolset) + (a read→think→act loop)**.
The brain is supplied two ways; both are first-class:
```
┌──────────────────────────────┐
│ cmd_chat/ai/ (Provider core)│ ← hoisted, shared
│ providers.py · profiles.py │
└───────────────┬──────────────┘
┌────────────────────┼─────────────────────┐
▼ ▼ ▼
/ai chat agent HARNESS-MODE OPERATOR CLI-SUBPROCESS OPERATOR
(agent/bridge.py) (native loop + operator (bootstrap runner
toolset; any Provider) registry: claude|codex|
gemini|cmd)
└──────────── operator bridge daemon (operator/bridge.py) ──────┘
verbs: say keys write exec get screen watch manifest spawn
```
Context is layered (one minimal prompt, behaviour overlaid):
- **Layer 1 — Capabilities** (`CAPABILITIES.md` + tool schema): room model,
read→think→act loop, verb table, `/grant` drive model, stop-vocabulary,
`.hh-agent` manifest. ~1 screen. Single source; two delivery channels.
- **Layer 2 — Role presets** (`roles/*.toml`): objective frame, tool bias,
budget, stop conditions, review-loop gates. planner / builder / tester / …
- **Layer 3 — Coordination protocol**: blackboard = manifest + room events;
gates = `watch --for … --in events` (never sleeps); recursion = `Budget` +
(new) per-agent token ceiling.
---
## 3. Phased plan
Phases are ordered by dependency and sized to be independently reviewable.
Each lists **what**, **files**, **design notes**, **acceptance**, **risk**.
### Phase 1 — Hoist the Provider core to `cmd_chat/ai/`
**What:** Move `agent/providers.py` + `agent/profiles.py` to a shared
`cmd_chat/ai/` so operator and `/ai` agent consume one core.
**Files:**
- New: `cmd_chat/ai/__init__.py`, `cmd_chat/ai/providers.py`, `cmd_chat/ai/profiles.py`
- Shim: `cmd_chat/agent/providers.py`, `cmd_chat/agent/profiles.py` → re-export
from `cmd_chat.ai` (`from cmd_chat.ai.providers import *`) so existing imports
and the native-harness bench keep working unchanged.
- Update `agent/bridge.py`, `agent/__main__.py` imports to `cmd_chat.ai`.
**Design notes:** Pure refactor — **no behaviour change**. The re-export shims
make it backward-compatible and let the move land before any consumer is
rewritten. `OllamaEmbedder` moves too (it is provider-adjacent).
**Acceptance:** `pytest` green; native-harness bench unchanged; `/ai start
qwen2.5:3b` still answers in a room.
**Risk:** Low. Import churn only. Mitigated by shims.
---
### Phase 2 — Harness-mode operator (the real agnostic win)
**What:** A new operator brain that runs the native tool-calling loop (from
`agent/bridge.py`) but with the **operator toolset** instead of the chat
`NATIVE_TOOLS`, driving the operator bridge daemon.
**Files:**
- New: `cmd_chat/operator/harness.py` — the loop adapter.
- Reuse: the loop body around `agent/bridge.py:1094-1209` (`complete_with_tools`,
token estimate, transcript trim) — extract the model-agnostic loop into
`cmd_chat/ai/harness.py` so both chat and operator share it; chat passes its
`NATIVE_TOOLS`, operator passes `OPERATOR_TOOLS`.
- New tool schema `OPERATOR_TOOLS`: `say`, `keys`, `write_file`, `exec`,
`get_file`, `screen`, `watch`, `manifest`, `spawn` — each mapping to the
operator bridge verb it already implements (`operator/bridge.py`).
- Wire `operator/__main__.py`: a new verb (e.g. `operate --provider … --model …
--role …`) that joins via the bridge and runs the harness loop.
**Design notes:**
- Tool handlers call the existing operator bridge unix-socket verbs — no new
side-effect surface, just a new *driver*.
- `ToolsUnsupported` already exists; if a model can't function-call, degrade to
a constrained text-protocol or refuse with a clear message (don't silently
fall through to chat-style prose).
- Per-agent **token ceiling** is enforceable here (we own the loop and
`complete_with_tools` returns real usage) — add it as a stop condition.
- Grant/drive: harness operator must respect `/grant` exactly like the CLI path
(no driving the PTY before granted).
**Acceptance:** an Ollama model (qwen2.5:3b) joins a room as `operate`, is
granted drive, writes a file + runs a command + reports in-room — verified by a
tmux-demo scenario mirroring the vmhub flow but with a local model as the builder.
**Risk:** Medium. Weak local models leak tool calls as text — the existing
`_extract_text_tool_calls` recovery in `providers.py` already mitigates this and
should be reused verbatim. CPU latency is real; keep `num_ctx` modest.
---
### Phase 3 — Runner registry in `bootstrap`
**What:** Generalize the three Claude-hardcoded spots into a registry keyed by
runner kind.
**Files:** `cmd_chat/operator/bootstrap.py`
- `build_run_argv` (currently `["claude","-p",directive]` ~L203) → look up an
argv template by runner.
- `install_plan` (`CLAUDE_NPM_PACKAGE` ~L35/128) → per-runner install spec.
- `creds_source` (`~/.claude/.credentials.json` ~L148) + `child_env`
(`CLAUDE_CONFIG_DIR`) → per-runner creds path + config-dir env var.
**Design (sketch):**
```python
RUNNERS = {
"claude": Runner(argv=lambda d: ["claude","-p",d,"--dangerously-skip-permissions"],
install=["npm","i","-g","@anthropic-ai/claude-code"],
creds="~/.claude/.credentials.json", config_env="CLAUDE_CONFIG_DIR"),
"codex": Runner(argv=lambda d: ["codex","exec",d], creds="~/.codex/auth.json",
config_env="CODEX_HOME", install=[...]),
"gemini": Runner(argv=lambda d: ["gemini","-p",d,"--yolo"], ...),
"cmd": Runner(argv=template_from("$HH_OPERATOR_CMD"), ...), # generic
}
```
- `spawn` gains `--runner {claude|codex|gemini|cmd}` (default `claude`,
back-compat).
- Keep `Budget`, `plan_creds` (gated), `detect_system`, `install_plan` structure
intact — they are already model-neutral.
**Design notes:** Runner argv templates are the ONLY model-specific knowledge;
everything else (budget, gating, detection) stays shared. Generic `cmd` runner
covers aider/cursor-agent/opencode without first-class entries.
**Acceptance:** `spawn --runner cmd` with a stub script launches and joins;
`--runner claude` is byte-identical to today. Pure unit tests (no side effects)
cover argv/install/creds selection per runner.
**Risk:** LowMedium. Non-Claude CLIs vary in flags; the generic `cmd` runner is
the safety valve. Only `claude` is validated end-to-end initially.
---
### Phase 4 — Portable `CAPABILITIES.md` + tool schema (Layer-1)
**What:** Extract the minimal-but-complete capabilities prompt into one portable
artifact; deliver it two ways.
**Files:**
- New: `cmd_chat/operator/CAPABILITIES.md` (prose, ~1 screen) + a machine
schema (reuse `OPERATOR_TOOLS` from Phase 2 as the canonical tool list).
- `~/.claude/skills/hh-operator/SKILL.md` → reference/include CAPABILITIES.md
(single source of truth) rather than duplicating doctrine.
- Phase-2 harness injects CAPABILITIES.md as the system prompt for non-Claude
operators; `compose_directive` (for CLI runners) points at it too.
**Design notes:** Keep it minimal — verbs, the read→think→act loop, grant/drive,
stop-vocabulary, manifest. No Claude-isms (no "use the skill" for injected
models; that line stays only in the Claude delivery path).
**Acceptance:** Claude operator behaviour unchanged (skill still works); a
non-Claude harness operator behaves correctly with only the injected
CAPABILITIES.md as system context.
**Risk:** Low. Mostly docs + a delivery switch.
---
### Phase 5 — `roles/` preset library (Layer-2)
**What:** Replace the demo's bespoke `vmhub-{builder,tester}-task.sh` behaviour
with declarative role presets.
**Files:**
- New: `cmd_chat/operator/roles/{planner,builder,tester,reviewer}.toml`
- Loader mirroring `profiles.py` lookup order (`$HH_ROLES_DIR`, `./roles/`,
`~/.config/hh/roles/`).
- `spawn`/`operate` gain `--role <name>` → compose Layer-1 + role overlay + join.
**Role schema (sketch):**
```toml
[tester]
objective_frame = "verify the work; report GREEN/RED in-room; add regressions on review"
tool_bias = ["write_file","keys","watch","manifest"]
budget = { depth = 0, fanout = 0 }
stop = ["suite green + planner signed off","owner says stop"]
review_gate = { watch_for = "pull-count regression", channel = "events" }
```
**Design notes:** Roles are thin overlays on Layer-1, not full prompts. The
review-loop **event-gating** (watch-for-event, never fixed sleeps) is encoded
here — the hard-won lesson from the vmhub reel becomes a reusable primitive.
**Acceptance:** the vmhub three-Claude flow is reproducible from
`spawn --role builder` / `--role tester` with no task-specific shell scripts;
demo scenario updated to use roles.
**Risk:** LowMedium. Behaviour parity with the existing scripts must be proven
by re-running the vmhub capture.
---
### Phase 6 — MCP server exposing the verbs (stretch, biggest reach)
**What:** Publish the operator toolset as an MCP server so any MCP-capable client
(Claude, Cursor, Cline, Continue, Agents-SDK app) is an operator with zero
hack-house-specific glue.
**Files:** New `cmd_chat/operator/mcp_server.py` exposing `hh.say`, `hh.keys`,
`hh.write`, `hh.exec`, `hh.get`, `hh.screen`, `hh.watch`, `hh.manifest`,
`hh.spawn` over the MCP stdio/SSE transport, backed by the operator bridge.
**Design notes:** Complementary to Phases 23, not a replacement: harness-mode
serves raw-HTTP models that aren't MCP clients; MCP serves the rest for free.
Same bridge verbs underneath, so no new side-effect surface.
**Acceptance:** an MCP client lists + calls the `hh.*` tools and drives a room.
**Risk:** Medium. New transport + protocol surface; gate behind explicit enable.
Defer until 15 land.
---
## 4. Dependency graph & sequencing
```
P1 (hoist) ──► P2 (harness operator) ──► P5 (roles)
│ ▲ ▲
│ │ │
└──► P4 (CAPABILITIES + schema) ─────────┘
P3 (runner registry) ── independent of P1/P2; pairs with P5 for spawn --role --runner
P6 (MCP) ── independent stretch; after P1P5 are stable
```
Recommended merge order: **P1 → P3 → P4 → P2 → P5 → P6.** P3 and P4 are small,
low-risk, independently valuable, and can land early to de-risk the big P2.
---
## 5. Git considerations
**Branching.** Cut a dedicated feature branch from `main`:
`feat/model-agnostic-operator`. The current working branch
(`feat/native-harness-working-memory`) carries unrelated in-flight work; do not
pile this on top of it.
> ⚠️ **Concurrent-session caveat (known):** multiple Claude Code sessions run
> against this single checkout, so commits land on whatever branch is *currently
> checked out*, regardless of which session authored them. Before starting work
> on this plan, confirm the intended branch is checked out, and avoid running
> parallel sessions that commit to different logical features at the same time.
**One PR per phase.** Each phase is sized to be independently reviewable and
(except P2→P5) independently mergeable:
- **P1** is a pure refactor — land it first behind re-export shims so nothing
else breaks; review is "imports moved, tests green, zero behaviour delta."
- **P3** and **P4** are small and additive; safe to land in parallel with P1.
- **P2** is the large one; keep it focused (loop extraction + operator toolset +
one `operate` verb), defer roles to P5.
- **P6** behind an explicit feature gate; do not enable by default.
**Backward compatibility.**
- Keep `cmd_chat/agent/providers.py` / `profiles.py` as thin re-export shims for
at least one release after P1; add a deprecation note, remove later.
- `spawn` default runner stays `claude` so existing demos/scripts are unaffected.
- The Claude operator path (skill + `compose_directive`) must remain
byte-identical in behaviour after P4 — verify with the existing vmhub capture.
**Commit hygiene.** Conventional-commit style matching the repo
(`feat(operator): …`, `refactor(ai): …`, `docs(operator): …`). Reference this
spec in the body. Do **not** squash the refactor (P1) into a feature commit —
keep the no-behaviour-change move isolated so a bisect can trust it.
**Testing gates (every PR must keep green).**
1. `pytest` unit suite (bootstrap logic is already pure/side-effect-free — keep
it that way; add runner-registry + role-loader unit tests).
2. The native-harness benchmark (regression guard for the loop extraction in P2).
3. The **vmhub tmux-demo capture** as the integration smoke for P2/P5
(Claude-spawns-Claude still works; then the local-model variant).
**Secrets.** No keys in `roles/*.toml`, `models.toml`, `CAPABILITIES.md`, or
runner specs — only `api_key_env` names. Add a CI/pre-commit grep for accidental
key patterns if one isn't already present.
**Docs.** Update `CLAUDE.md` (key-components table + operator section) and the
`hh-operator` skill to reference `cmd_chat/ai/` and the new `operate` verb /
runner registry / roles once P1P5 land. Add a memory note on the new module
layout.
---
## 6. Open questions
- **Loop extraction boundary (P2):** how much of `agent/bridge.py`'s loop is
genuinely model-agnostic vs chat-specific (typing/stream frames, transcript
semantics)? Spike before committing the extraction shape.
- **Non-Claude CLI auth:** codex/gemini creds/login flows differ from Claude's
`~/.claude/.credentials.json` — first-class entries need per-runner validation;
until then route them through the generic `cmd` runner.
- **Degrade policy when `ToolsUnsupported`:** refuse vs constrained text-protocol
fallback for the harness operator. Leaning refuse-with-clear-message to avoid a
weak model "operating" by prose.
- **Per-agent token ceiling:** enforce as a hard stop, or as a compaction
trigger first then stop? Likely both, configurable per role.
- **MCP transport (P6):** stdio for local clients vs SSE for remote — pick based
on the first target client.
```