Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ CI runs the same two checks on every push through one step,

## Ship

When it validates: tag `v1.0.0`, then open a PR to
When it validates: open a PR to
[Virtual-Protocol/butler-skills](https://github.com/Virtual-Protocol/butler-skills)
adding your repo as a submodule under `skills/<name>` at that tag. Maintainers
review the pinned commit; Butler containers clone exactly that commit. Names
starting with `butler-` are reserved for the Butler team; names starting with
adding one entry to `skills.json` — your `name`, your `repo` URL and a `ref`. The
registry keeps no copy of your code and takes no submodule: every build shallow-clones
your repo at that ref and publishes the commit it resolved to. A branch `ref` follows
you, so each merge reaches butlers on the next build; a tag `ref` holds a release.
Names starting with `butler-` are reserved for the Butler team; names starting with
`bevo-` are refused (that prefix is the container's own bundled-skill namespace).
11 changes: 9 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: _template
description: TODO one-sentence trigger description, start with the phrases an owner would say, <=160 chars
version: 0.1.0
metadata: {"openclaw":{"emoji":"TODO","requires":{"bins":["bevo-read"]}},"bevo":{"tier":"on-demand","modes":["one-off"],"moneyMoving":false,"keywords":["TODO"],"requires":{"routes":["GET /butler-read/TODO"],"features":[],"gates":[],"bins":["bevo-read"]},"params":[{"name":"TODO_PARAM","type":"string","required":true,"ask":"TODO ask phrase"}]}}
metadata: {"openclaw":{"emoji":"TODO","requires":{"bins":["bevo-read"]}},"butler":{"tier":"on-demand","modes":["one-off"],"moneyMoving":false,"keywords":["TODO"],"requires":{"routes":["GET /butler-read/TODO"],"features":[],"gates":[],"bins":["bevo-read"]},"params":[{"name":"TODO_PARAM","type":"string","required":true,"ask":"TODO ask phrase"}]}}
---

## When to use
Expand All @@ -21,14 +21,21 @@ TODO: reads/ids to resolve before doing anything (e.g. `bevo-read user <@handle>
TODO: walk through each `params` entry — what it changes, its default, its range, and which
numbered steps below are `[FIXED]` vs `[ADAPT]` because of it.

TODO: then close this section with the fork seam. Knobs cover the asks you anticipated; a
Butler will meet the ones you did not. Say what would call for a fork rather than a knob,
where in `duty.py` such a condition goes, and which read feeds it — `bevo-hub fork <name>`
makes the owner's own copy, the hub never overwrites it, and `bevo-automation create
--from-skill <the-fork>` files it like any other. A skill whose steps only make sense at its
own defaults is one nobody can extend.

## One-off procedure

1. [FIXED] TODO first fixed step (a read).
2. [ADAPT] TODO an adapt step (apply the owner's wording).

## Duty procedure

TODO: trigger JSON, env mapping from params, the exact `bevo-automation create --from-skill _template@0.1.0 '<json>'`
TODO: trigger JSON, env mapping from params, the exact `bevo-automation create --from-skill <name> '<json>'`
call, and what to say about pending -> arm -> pocket.

1. [FIXED] TODO
Expand Down
60 changes: 17 additions & 43 deletions duty.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,34 @@
declared defaults.
- Every bevo.trade(...)/bevo.execute(...) call passes idempotency_key=
derived from the source event id and bevo.SERVICE_ID.
- Keep a bounded state.json seen-set in cwd (belt: server ledger, braces:
local state).
- No bare `except: pass`, no subprocess/os.system/eval/exec.

Prefer the TYPED generators — bevo.trades(), bevo.messages(), bevo.transfers(),
bevo.ticks(), bevo.polls(), bevo.webhooks(), bevo.frames() — over the raw
bevo.events(), which is there for a kind they do not cover. `bevo.state` is a
dict that saves itself across restarts, so a duty needs no state file of its
own; the idempotency key is what stops a replayed event acting twice.
"""
import json
import os

import bevo

STATE_PATH = "state.json"
MAX_SEEN = 2000

# TODO: read your declared params with their defaults, e.g.:
# TODO_PARAM = os.environ.get("TODO_PARAM", "default-value")


def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {"handled": []}


def save_state(state: dict) -> None:
handled = state.get("handled", [])[-MAX_SEEN:]
state["handled"] = handled
tmp = STATE_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f)
os.replace(tmp, STATE_PATH)


def main() -> None:
state = load_state()
handled = set(state.get("handled", []))

for ev in bevo.events():
# TODO: filter to the event kind(s) this duty cares about.
kind = ev.get("kind")
if kind != "TODO":
continue

event = ev.get("event", {})
event_id = event.get("id")
if event_id is None or event_id in handled:
continue

# TODO: build the idempotency key and call bevo.trade / bevo.execute.
key = f"_template:{bevo.SERVICE_ID}:{event_id}"
bevo.log(f"TODO handling event {event_id} with key {key}")

handled.add(event_id)
state["handled"] = list(handled)
save_state(state)
# TODO: swap bevo.trades() for the generator matching your trigger kind.
for trade in bevo.trades():
# One key per SOURCE EVENT, never a timestamp: the pump can replay a
# row it already handed over after a restart, and the same key answers
# "already filed" instead of acting a second time.
key = f"TODO-skill:{bevo.SERVICE_ID}:{trade.id}"

# TODO: skip what the owner's knobs exclude, returning early — then
# act: bevo.buy / sell / long / short / close / stock_buy / stock_sell,
# or bevo.execute for a contract call. Each takes idempotency_key=key.
bevo.log(f"TODO handling {trade.id} with key {key}")


if __name__ == "__main__":
Expand Down
Loading