diff --git a/cmd_chat/operator/CAPABILITIES.md b/cmd_chat/operator/CAPABILITIES.md new file mode 100644 index 0000000..9bda358 --- /dev/null +++ b/cmd_chat/operator/CAPABILITIES.md @@ -0,0 +1,58 @@ +# hack-house operator — capabilities + +You are an **operator**: a first-class member of an encrypted hack-house room. +You can read and answer humans and other agents, and — once granted — drive a +**shared sandbox** (a container/VM the room can all see) with full shell and +keystroke control. This is the minimal, complete contract; it is the single +source of truth whether you run as Claude (via the `hh-operator` skill) or as any +other model (this text is injected as your system context). + +## The loop: read → think → act +Operate in a tight loop. **Read** room activity, **think** about the objective, +take **one action**, then read again. Never act blind; never spin on fixed +sleeps — gate on events. + +## Verbs (the operator CLI — `hh-bridge `, aliased `HH`) +- `up --password [--no-tls]` — join the room (starts + your daemon). If a spawn directive already joined you, **reuse** it: wait for + `status` to report `"connected": true` rather than calling `up` again (a second + `up` races the first and restarts the bridge). +- `read [--wait --timeout N]` — long-poll new room events (chat, joins, grants). +- `say ""` — speak into the room. Report what you are doing and what you + found; the room is the coordination bus. +- `screen [--tail N]` — print the relayed shared-sandbox terminal buffer. +- `keys "" [enter]` — inject keystrokes into the shared PTY (drive). +- `exec ""` — run a shell command in the sandbox out-of-band (does not + disturb the live terminal). +- `write ` (content on stdin) / `get ` — write/read a file in the + sandbox. +- `watch --for '' --in events|screen --timeout N` — **block** until a + condition fires. This is how you coordinate: wait for a teammate's report or a + build result instead of sleeping. +- `manifest push|update --root …` — stamp the sandbox's `.hh-agent` + handoff record (purpose / objective / status / done / todo) so work and memory + hand off to the next agent or human. +- `spawn "" --room-host … --go` — start a + nested operator (budgeted; see below). +- `down` — leave the room and stop your daemon. + +## Drive is granted, not assumed +You may read and chat immediately, but you may **only drive the sandbox** (keys/ +exec/write) after the room owner runs `/grant `. Wait for +`status` to show `"granted": true` before driving. Never touch a system you were +not asked to. + +## Coordination is event-gated +When you depend on another agent (e.g. a tester waiting for a planner's review), +**wait on `watch --for … --in events`**, never a fixed timer — agent boot and +work timing vary widely. Arm your `watch` before the event you expect can fire. + +## Recursion budget +If you may spawn children you inherit a budget: `depth` (how much deeper you may +nest, 0 = leaf), `fanout` (how many children you may start), and an advisory +`cost`. Spawn only while `depth > 0` and `fanout > 0`, and pass each child a +decremented budget. This is the hard stop against a runaway tree. + +## Stop conditions +Leave with `down` when any holds: the objective is met; the owner says +stop/leave; or you have gone idle past your remit. Do not linger. diff --git a/cmd_chat/operator/bootstrap.py b/cmd_chat/operator/bootstrap.py index 8ce1014..effc111 100644 --- a/cmd_chat/operator/bootstrap.py +++ b/cmd_chat/operator/bootstrap.py @@ -265,17 +265,37 @@ def plan_creds(dest_home: str | os.PathLike, *, allow: bool, # ── directive + run argv ──────────────────────────────────────────────────── +def load_capabilities() -> str: + """The portable Layer-1 capabilities prompt (`CAPABILITIES.md`) — the + minimal-but-complete operator contract injected for runners that have no + Claude-style skill to load.""" + return Path(__file__).with_name("CAPABILITIES.md").read_text() + + def compose_directive(objective: str, room: dict, budget: Budget, - stop: list[str] | None = None) -> str: - """The prompt the nested Claude runs under `claude -p`. Tells it to use the - hh-operator skill, where to join, what to achieve, when to stop, and the - recursion budget it inherits (so it self-limits its own spawns).""" + stop: list[str] | None = None, + runner: "Runner | str | None" = None) -> str: + """The prompt the nested operator runs headlessly. Tells it where to join, + what to achieve, when to stop, and the recursion budget it inherits (so it + self-limits its own spawns). + + Layer-1 capabilities are delivered per runner: the ``claude`` runner loads + the ``hh-operator`` skill (its own packaging), while every other runner gets + the portable ``CAPABILITIES.md`` prepended inline — same contract, no skill + machinery required.""" + r = get_runner(runner) host, port = room.get("host", "?"), room.get("port", "?") name = room.get("name", "operator") stop = stop or ["objective met", "owner says stop/leave", "idle past remit"] + if r.name == "claude": + preamble = ("Use the hh-operator skill. You are an autonomous operator " + "joining a hack-house room.") + else: + preamble = (load_capabilities() + + "\n\nYou are an autonomous operator joining a hack-house " + "room. Operate per the capabilities above.") lines = [ - "Use the hh-operator skill. You are an autonomous operator joining a " - "hack-house room.", + preamble, f"Join: `hh-bridge up {host} {port} {name}` " f"(password via room config; add --no-tls for plain ws).", f"Objective: {objective}", diff --git a/cmd_chat/operator/bridge.py b/cmd_chat/operator/bridge.py index 1ab2fe1..4992006 100644 --- a/cmd_chat/operator/bridge.py +++ b/cmd_chat/operator/bridge.py @@ -612,7 +612,8 @@ class OperatorBridge(Client): return {"ok": False, "error": str(e)} stop = req.get("stop") or None - directive = boot.compose_directive(objective, room, child_budget, stop=stop) + directive = boot.compose_directive(objective, room, child_budget, + stop=stop, runner=runner) argv = boot.build_run_argv(directive, skip_permissions=skip_perms, config_dir=config_dir, runner=runner) creds = boot.plan_creds(config_dir or "~", allow=allow_creds, runner=runner) diff --git a/tests/test_operator_bridge.py b/tests/test_operator_bridge.py index 7f0f682..a85c134 100644 --- a/tests/test_operator_bridge.py +++ b/tests/test_operator_bridge.py @@ -323,6 +323,19 @@ def test_compose_directive_carries_objective_stop_budget(): assert "depth=1" in d and "fanout=2" in d +def test_compose_directive_injects_capabilities_for_non_claude(): + d = boot.compose_directive( + "map the room and report", {"host": "h", "port": 9, "name": "scout"}, + boot.Budget(depth=1, fanout=2, cost_usd=3.0), stop=["task done"], + runner="codex") + # Portable capabilities are inlined; the Claude-only skill line is absent. + assert "hh-operator skill" not in d + assert boot.load_capabilities().split("\n", 1)[0] in d + assert "read" in d and "think" in d and "act" in d + assert "map the room and report" in d + assert "depth=1" in d and "fanout=2" in d + + def test_plan_creds_gated_off_by_default(): off = boot.plan_creds("/tmp/child", allow=False) assert off["staged"] is False and "gating on" in off["reason"]