feat(operator): portable CAPABILITIES.md as the single Layer-1 prompt (P4)
The operator's capabilities contract was Claude-only — delivered via the hh-operator skill, which non-Claude runners cannot load. Extract it into a portable CAPABILITIES.md and make compose_directive runner-aware: the claude runner still loads its skill, every other runner gets the capabilities prompt inlined. Same contract, no skill machinery required. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <verb>`, aliased `HH`)
|
||||||
|
- `up <host> <port> <name> --password <pw> [--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 "<text>"` — 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 "<text>" [enter]` — inject keystrokes into the shared PTY (drive).
|
||||||
|
- `exec "<cmd>"` — run a shell command in the sandbox out-of-band (does not
|
||||||
|
disturb the live terminal).
|
||||||
|
- `write <path>` (content on stdin) / `get <path>` — write/read a file in the
|
||||||
|
sandbox.
|
||||||
|
- `watch --for '<regex>' --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 <dir> …` — 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 "<objective incl. the join command>" --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 <yourname>`. 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.
|
||||||
@@ -265,17 +265,37 @@ def plan_creds(dest_home: str | os.PathLike, *, allow: bool,
|
|||||||
|
|
||||||
|
|
||||||
# ── directive + run argv ────────────────────────────────────────────────────
|
# ── 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,
|
def compose_directive(objective: str, room: dict, budget: Budget,
|
||||||
stop: list[str] | None = None) -> str:
|
stop: list[str] | None = None,
|
||||||
"""The prompt the nested Claude runs under `claude -p`. Tells it to use the
|
runner: "Runner | str | None" = None) -> str:
|
||||||
hh-operator skill, where to join, what to achieve, when to stop, and the
|
"""The prompt the nested operator runs headlessly. Tells it where to join,
|
||||||
recursion budget it inherits (so it self-limits its own spawns)."""
|
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", "?")
|
host, port = room.get("host", "?"), room.get("port", "?")
|
||||||
name = room.get("name", "operator")
|
name = room.get("name", "operator")
|
||||||
stop = stop or ["objective met", "owner says stop/leave", "idle past remit"]
|
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 = [
|
lines = [
|
||||||
"Use the hh-operator skill. You are an autonomous operator joining a "
|
preamble,
|
||||||
"hack-house room.",
|
|
||||||
f"Join: `hh-bridge up {host} {port} {name}` "
|
f"Join: `hh-bridge up {host} {port} {name}` "
|
||||||
f"(password via room config; add --no-tls for plain ws).",
|
f"(password via room config; add --no-tls for plain ws).",
|
||||||
f"Objective: {objective}",
|
f"Objective: {objective}",
|
||||||
|
|||||||
@@ -612,7 +612,8 @@ class OperatorBridge(Client):
|
|||||||
return {"ok": False, "error": str(e)}
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
stop = req.get("stop") or None
|
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,
|
argv = boot.build_run_argv(directive, skip_permissions=skip_perms,
|
||||||
config_dir=config_dir, runner=runner)
|
config_dir=config_dir, runner=runner)
|
||||||
creds = boot.plan_creds(config_dir or "~", allow=allow_creds, runner=runner)
|
creds = boot.plan_creds(config_dir or "~", allow=allow_creds, runner=runner)
|
||||||
|
|||||||
@@ -323,6 +323,19 @@ def test_compose_directive_carries_objective_stop_budget():
|
|||||||
assert "depth=1" in d and "fanout=2" in d
|
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():
|
def test_plan_creds_gated_off_by_default():
|
||||||
off = boot.plan_creds("/tmp/child", allow=False)
|
off = boot.plan_creds("/tmp/child", allow=False)
|
||||||
assert off["staged"] is False and "gating on" in off["reason"]
|
assert off["staged"] is False and "gating on" in off["reason"]
|
||||||
|
|||||||
Reference in New Issue
Block a user