muse-gadget-sdk / linux
AGENTS.md2.6k starsChanged 24 days agoApache-2.0View on GitHub
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> How…
- Installs packages
Trending
More talk lately.thin dataabout the whole repository
Who's talking about facebookincubator/muse-gadget-sdk
- @j4ck.xyz“I still wouldn't trust Meta with my personal context for agents, but what they're doing with Muse is pretty …”Bluesky · Oct 2, 2026 · 7 likes
- A Hacker News story“Open source SDK to build Muse gadgets”Hacker News · Oct 3, 2026 · 2 points
- ComBba/muse-gadget-sdk“CI preflight: wait for the installer serial close before SDK token setup”GitHub · Oct 17, 2026 · 1 reactions
- @sarubot.bsky.social“muse-gadget-sdk — Metaが放つ、ESP32やラズパイをAIガジェットへ変貌させるSDK。今週スター急増中の注目作! …”Bluesky · Oct 7, 2026 · 1 likes
- aegisfleet/tool-survey-report“feat: add survey report for Muse Gadget SDK”GitHub · Nov 9, 2026
The biggest 5 of 14 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 14
Talked about on GitHub, Bluesky and Hacker News (14 times).
On GitHub · 11
- Oct 17, 2026CI preflight: wait for the installer serial close before SDK token setup — ComBba/muse-gadget-sdk1 reaction · 2 comments
- Nov 9, 2026feat: add survey report for Muse Gadget SDK — aegisfleet/tool-survey-report0 reactions · 4 comments
- Oct 14, 2026Fix S3-2.06 ES7210 microphone and native USB serial console — polygoku/waveshare-amoled-206-debug0 reactions · 5 comments
- Oct 12, 2026Integration blocker: native Muse gadget command invocation — 1chris2many/muse-portal-kit0 reactions
- Oct 12, 2026Integration blocker: authoritative per-device reply correlation — 1chris2many/muse-portal-kit0 reactions · 2 comments
6 more on GitHub
- Oct 11, 2026[fork CI preflight] #181 candidate a6fd89c — ComBba/muse-gadget-sdk0 reactions · 1 comment
- Oct 11, 2026[fork CI] #179 Use the custom avatar in the UI simulator — ComBba/muse-gadget-sdk0 reactions · 1 comment
- Oct 11, 2026[fork CI] #181 Add validated avatar restore and host status — ComBba/muse-gadget-sdk0 reactions · 1 comment
- Oct 11, 2026[fork CI] #180 Validate avatar candidates before replacing the current renderer — ComBba/muse-gadget-sdk0 reactions · 1 comment
- Oct 11, 2026[fork CI] #174 Reject incomplete bench screenshots without padding pixels — ComBba/muse-gadget-sdk0 reactions · 1 comment
- Oct 11, 2026[fork CI] #177 Abort bench screenshots when USB console output stalls — ComBba/muse-gadget-sdk0 reactions · 1 comment
On Bluesky · 2
- Oct 2, 2026I still wouldn't trust Meta with my personal context for agents, but what they're doing with Muse is pretty cool & fun icl… — @j4ck.xyz7 likes and reposts
- Oct 7, 2026muse-gadget-sdk — Metaが放つ、ESP32やラズパイをAIガジェットへ変貌させるSDK。今週スター急増中の注目作! ・C言語製のESP32/Linux用SDK。センサーやオーディオをMuseエコシステムに即統合 ・AIエージェント「Muse… — @sarubot.bsky.social1 likes and repost
On Hacker News · 1
- Oct 3, 2026Open source SDK to build Muse gadgets2 points
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository48 other files it gives its agents.
- esp32/AGENTS.mdAGENTS.md
- gadget-apple-homepod-miniSkill
- gadget-apple-tv-4kSkill
- gadget-brother-printersSkill
- gadget-dyson-pure-hot-cool-hp04Skill
- gadget-ecowitt-gatewaysSkill
- gadget-elgato-key-lightSkill
- gadget-epson-printersSkill
- gadget-esphome-devicesSkill
- gadget-espsomfy-rtsSkill
- gadget-eufy-robovac-tuyaSkill
- gadget-freebox-player-popSkill
- gadget-google-castSkill
- gadget-google-home-maxSkill
- gadget-google-home-speakerSkill
- gadget-google-nest-audioSkill
- gadget-google-nest-hub-maxSkill
- gadget-google-nest-hubSkill
- gadget-google-nest-miniSkill
- gadget-google-nest-thermostatsSkill
- gadget-google-pixel-tabletSkill
- gadget-google-tv-streamerSkill
- gadget-hp-color-laserjet-pro-m254dwSkill
- gadget-irobot-roomba-braavaSkill
- gadget-lg-webos-tvsSkill
- gadget-logitech-squeezeboxSkill
- gadget-lutron-smart-bridgesSkill
- gadget-meross-smart-plugsSkill
- gadget-miele-g7566-dishwasherSkill
- gadget-moonraker-3d-printersSkill
- gadget-philips-hue-bridgesSkill
- gadget-ratgdo-v32-garage-doorSkill
- gadget-roborock-vacuumsSkill
- gadget-roku-tvs-and-playersSkill
- gadget-samsung-tizen-tvsSkill
- gadget-shelly-plugsSkill
- gadget-sonoff-rf-bridgeSkill
- gadget-sonos-speakersSkill
- gadget-teslaSkill
- gadget-tplink-kasa-ep10Skill
- gadget-tplink-kasa-ep25Skill
- gadget-tuya-wifi-devicesSkill
- gadget-unifi-network-consolesSkill
- gadget-velux-klf200Skill
- gadget-vizio-d40f-g9Skill
- gadget-wyze-rtsp-camerasSkill
- gadget-yi-hack-camerasSkill
- gadget-zigbee2mqtt-gatewaysSkill
The file
We split this AGENTS.md into the rules it gives an agent
9 rules. About 0 are common, 1 is distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 62
Run these commands from linux/.
- Very specific: line 155
After the Noise handshake, open POST /link-control and leave the body open.
- Distinctive: line 210
Only a matching settlement retires that input item's work through the matched submission, not newer aliases or unrelated inputs.
- Very specific: line 230
Add a spec to COMMAND_SPECS in executor.py: description, required and optional parameters (each with type and description), and timeout_ms …
- Very specific: line 236
Add a test in tests/test_executor.py.
- Very specific: line 356
Create it before you send the request.
- Very specific: line 396
Don't use hatch in a new file name or identifier.
- Very specific: line 409
If you changed install.sh, it passes ShellCheck.
- Very specific: line 410
If you deployed, the log shows registered with the Muse and no tracebacks.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<!--
Copyright (c) Meta Platforms, Inc. and affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->AGENTS.md
How to work on this Linux client, test it, and deploy it to a device. See
README.md for a shorter human overview.
What this is
A Python package (musegadget) that makes any Linux computer into a Muse
Home Link. It pairs with the Muse phone app over BLE, then holds an encrypted
Noise session to the user's Muse VM and runs the commands Muse sends it. It is
the Linux counterpart of the ESP32 firmware in ../esp32 and speaks the same
pairing and control protocols, so the same app pairs either.
| Module | Role |
cli.py | musegadget pair, run, send-user-msg, info, unpair |
pair.py | pair(): opens BLE setup, checks and saves the pairing, prints nothing |
pairing.py | Community pairing v5: P-256 ECDH, HKDF-SHA256, AES-256-GCM, confirm_app |
ble_framing.py | Chunked BLE framing (0xFE, index, total, payload) |
ble_setup.py | Setup commands behind the GATT characteristics (no BlueZ dependency) |
ble_server.py | BlueZ GATT peripheral and advertisement over D-Bus (GLib loop) |
identity.py | Persistent identity: homelink-xxxxxx node id, MuseGadgetXXXXXX BLE name |
muse_api.py | fetch_vms and device token refresh |
link_client.py | One session: /v1/noise upgrade, Noise XX, /link-control, /chat/stream, /chat/subscribe, TTS streams |
streaming.py | Bounded duplex request bodies, response chunks and NDJSON parsing |
voice_call.py, voice_audio.py | MCU realtime voice lifecycle, WAV input/output and optional live audio |
service.py | musegadget run: reconnect loop, token rotation, local socket |
muse_turn.py | Follows one request through Muse's chat events to its replies |
executor.py, fileops.py | The commands Muse can run, as the chosen account |
commands.py | The owner's own commands, from /etc/musegadget/commands.d/*.json |
noise/ | Noise XX handshake, framing and service envelopes |
data/ | The systemd unit and the hash-pinned requirements.lock, shipped in the package |
Prerequisites
- Python 3.9 or later. The package supports Debian 11 / Raspberry Pi OS Bullseye (Python 3.9, cryptography 3.3.2) and later, and Ubuntu 22.04 and later.
- uv for tests and builds.
- For a real device: BlueZ, and
python3-dbusandpython3-gifrom apt. These have no Linux wheels, so the venv must use the system interpreter with--system-site-packages. Don't point it at a uv-managed Python.
Tests
Run these commands from linux/. The tests need no Bluetooth, device or
external network:
uv run --no-project --isolated --with pytest --with . python -m pytest -qTo check the oldest supported versions, pin them. cryptography 3.3.2 has no Apple Silicon wheel, so run it under an x86-64 Python on a Mac:
uv run --no-project --isolated --python cpython-3.9-macos-x86_64 \
--with pytest --with cryptography==3.3.2 --with websockets==13.1 \
env PYTHONPATH=src python -m pytest -qThe pairing tests check against the published fixtures in
tests/vectors/link_pairing_v5.json. tests/test_link_client.py runs the
client against an in-process fake VM that does a real Noise handshake.
Deploy to a device
Copy this directory to the device and install from it:
bash install.sh --from . # asks before granting access
bash install.sh --from . --yes # doesn't askReinstalling keeps the pairing. It restarts the service, which kills any
command Muse is running at that moment, so check the log for recent invoke
lines first.
For a quicker loop, build a wheel and install it into the existing venv:
uv build --wheel
# on the device:
sudo /opt/musegadget/bin/uv pip install --python /opt/musegadget/venv/bin/python --no-deps --reinstall musegadget-*.whl
sudo systemctl restart musegadgetInstaller flags: --sdk-token TOKEN, --run-as USER, --no-pair, --yes,
--from SOURCE, --uninstall, --purge. The installer is ShellCheck-clean;
keep it that way.
Run and debug
sudo journalctl -u musegadget -f # service log
sudo musegadget -v pair # pairing, verbose
sudo musegadget info # identity and pairing stateState lives in /var/lib/musegadget (mode 0700): identity.json survives
unpairing, pairing.json holds the device tokens. The local socket is
/run/musegadget/musegadget.sock, owned by root and the run-as account's group.
A healthy start logs commands run as <user>, Noise session established,
sent link.register and registered with the Muse.
Each command the Muse runs logs invoke <command>, then how it ended, such as
system.run ok, exit 0 in 41 ms or file.read failed in 3 ms. The log
never has a command's parameters, output or error message.
Pairing
- Community mode only:
pairing_auth: "none", epoch 0, policyconfirm_app. A Pi has no button, so the app's own confirmation stands in for it, and BLE only advertises whilemusegadget pairruns. - The BLE name is
MuseGadgetplus the last six hex digits of the identity, with no hyphen: the apps compare the text after the prefix with the text afterhomelink-in the node id.get_device_infomust reportmodel: "hatch_link". - The apps always scan Wi-Fi and send
provision_v2with an SSID. When the device is online it offers one open network, "the current connection", and ignores the Wi-Fi fields it gets back. It never stores them. provision_v2carriesapi_url, which only older firmware reads (it adds/hatch/). This client ignores it and usesapi_url_v2when newer apps send it, orhttps://api.muse.ai, with bare API paths either way.- The Android app writes
negotiatedMtu - 3bytes with no cap, and Android rejects writes over 512 bytes. BlueZ must have[GATT] ExchangeMTU = 256in/etc/bluetooth/main.conf; the installer sets it. Symptom if it's missing: pairing works up to Wi-Fi, then "Couldn't connect" in the app. - Answer plaintext
get_device_infoat any time. Android re-sends it when it restarts a handshake on the same connection. - GATT status 133 on the phone is usually stale Bluetooth state on the phone. Toggling the phone's Bluetooth clears it.
Talking to the Muse
- Connect to
wss://<noise_host>/v1/noise?vm_id=<vm_id>with the per-VM bearer fromfetch_vmsin anAuthorizationheader. A 401 or 403 on the upgrade means fetch fresh VM credentials, not retry the same bearer. - After the Noise handshake, open
POST /link-controland leave the body open. Both directions carry JSON messages, each prefixed with a little-endian u32 length. The device sendslink.register, then alink.resultfor eachlink.invoke.link.unpairedmeans the Muse removed the device. - Register as
platform: "linux",device_family: "homehub". Never use familylinkor advertisedevice.ota: the server pushes ESP32 firmware updates to everylinkdevice. - Messages from the device to the Muse (
musegadget send-user-msg) go as separatePOST /chat/streamrequests on the same session, withdevice_idset to the node id and"output_modality": "text".session_idpicks the chat;chat_idis not an API field and is ignored. The response is only the ack (message_id); the reply appears in the Muse chat. Replies are text: to speak them, use a text-to-speech API of your choice. send-user-msg --wait(the local socket's"wait") opens/chat/subscribebefore sending and returns the answer's text: what follows the message's ownmessage.userevent, as on the ESP32, read throughMuseTurn. A new side chat is created by the message, so it is sent first. One waiting request per chat at a time; the whole wait, queueing included, is bounded by its seconds, plus at most 2 s of cleanup. On a new side chat, an answer finished before the follow-up subscription opens is missed and the wait times out.- The VM accepts at most 256 KB per
/link-controlmessage, so command output is cut at 96 KB per stream.
Realtime voice
musegadget voice-calluses the paired store and exact--vm-id; never add an owner-token fallback or change the account's default VM. ItsLinkSession(register_device=False)waits for transport readiness without/link-controlorlink.register, preserving the normal run service's live transport. Reuse the persistent identity for attribution only.- Each voice invocation admits a fresh call. Omit the realtime
session_idquery (it is for an already-admitted call); optionalthread_idbelongs in the admission record. Do not imply that this client implements resumption. - Set the configured voice app ID on the Noise upgrade query. Per-request app-ID headers are not a substitute. Keep ordinary chat ACKs buffered.
- Admit only
mcu-wearableaudio calls. Wait for HTTP success and matching call/session readiness before microphone upload. Explicitly end the call before upload EOS, including failure/cancellation paths, with bounded cleanup. - PCM is signed 16-bit LE, mono 24 kHz, up to 960 samples per 40 ms upload. Bound response chunks, lines, decoded audio and playback. Silence keepalives are not activity and must not hold the mic gate shut.
- Half duplex is the default, with playback flush on barge-in and server hold respected. Response completion/cancellation and turn settlement are not call termination or proof that all PCM has arrived.
- Right after the admission record, the client sends
{"type":"hatch.voice.capability.offer","end_call_stop":1}: it ends the call on asession_control/stop, so Muse may hang up that way when the caller says goodbye. A VM that doesn't know the key ignores it. The stop arrives on this call's realtime response stream. Itsitem.call_idis a control ID, not the admission call ID. Handle it once, drain queued playback within a bound, then explicitly end the call. Don't add other capabilities, invent server events or widen the device's permissions. - Post-readiness operation errors are advisory unless the peer explicitly
reports
session_dead; admission errors remain fatal. Never log error text. - Idle suspension follows identified active turns, including delayed aliases. Only a matching settlement retires that input item's work through the matched submission, not newer aliases or unrelated inputs. Keep identity storage bounded, restart idle after settlement, and never suspend the overall timer.
- A validated end-call ACK is not undone by a delayed media EOF if a bounded stream reset succeeds; report the incomplete drain separately.
- Tests use self-authored tones/zeros and synthetic identities only. Never put
real recordings, credentials, internal documents or captured events here.
test_streaming.py,test_voice_call.pyandtest_voice_cli.pyexercise real Noise against an in-process fake VM without hardware or external calls.
Adding a command
A device's owner adds commands without changing the package: one JSON file
per command in /etc/musegadget/commands.d/ (MUSEGADGET_COMMANDS_DIR
overrides it), read when the service starts. commands.py documents the
format and validates it, including the directory trust check;
Executor.run_drop_in runs them. Executor.specs takes the actual device
metadata to admit whole definitions under the serialized registration budget. See "Add your own
commands" in README.md. To add a built-in command to the package itself:
- Add a spec to
COMMAND_SPECSinexecutor.py:description,requiredandoptionalparameters (each withtypeanddescription), andtimeout_msif the default 30 seconds is too short. - Handle it in
Executor.run. Returnok(payload)orerror(message). - Anything that touches the machine should run in a child process with
self._child_options(), so it runs as the chosen account and not as root. - Add a test in
tests/test_executor.py.
Muse sees the new command after the service restarts and re-registers.
Building a gadget
A gadget is a program that uses musegadget to talk to Muse from its own
hardware, such as a robot or a speaker. It may use the parts of the package
listed here. Everything else is internal and can change.
Identity and state
identity.load_or_create()returns the deviceIdentity. It creates one on first use and keeps it across unpairing.Identity.node_idis thehomelink-xxxxxxID that Muse knows the device by.Identity.ble_nameis the name the Muse app shows during pairing.config.state_dir()is the directory for device state. SetMUSEGADGET_STATE_DIRto move it.config.load_json(name),config.save_json(name, data)andconfig.delete_json(name)read, write and remove one JSON file in the state directory.save_jsonwrites atomically and makes the file readable only by its owner.config.sdk_token()returns the SDK token fromMUSEGADGET_SDK_TOKENor the state directory, orNone. It raisesValueErrorfor a token that gadgets.muse.ai could not have issued.config.PAIRING_FILEis the file that holds the pairing.config.STATE_DIR_ENVandconfig.SDK_TOKEN_ENVhold the names of the two environment variables,MUSEGADGET_STATE_DIRandMUSEGADGET_SDK_TOKEN.
Pairing
pair.pair(on_open=..., force=False, timeout_s=600)opens Bluetooth setup so that the Muse app can pair the device. It prints nothing.- Just before setup opens, it calls
on_openwith aSetupWindow. Use it to tell the user what to do.ble_nameis the device to choose in the Muse app,timeout_sis how long setup stays open, andhas_sdk_tokenis false when neither the environment nor the state directory provides an SDK token. - It returns a
PairResult.PairResult.outcomeis aPairOutcome:PAIRED,ALREADY_PAIRED,INVALID_SDK_TOKENorWINDOW_CLOSED. ForINVALID_SDK_TOKEN,PairResult.sdk_token_problemsays what is wrong. - A paired device is left alone unless
forceis true.
Connecting
service.Servicekeeps a paired device connected to its Muse. It fetches the device's VMs, connects, reconnects with backoff and rotates the device token.Service(identity, executor, sdk_token=None, display_name=...)is a dataclass. A dataclass subclass can add fields with defaults.executoris anexecutor.Executorinstance.await Service.run()runs untilService.stop()is called orserve()returnsOutcome.STOPPED.- To describe your own device, or to run your own work while it is connected,
subclass
Serviceand override any of these three methods. describe()returns theDeviceDescriptionto register with Muse.Servicecalls it for each new session.run_command(name, params, timeout_ms=None)handles each command from Muse synchronously in a worker thread. Commands can run concurrently. By default it calls theExecutor. Return{"ok": True, "payload": {...}}on success or{"ok": False, "error": "..."}on failure.async serve(session, vm, stop)runs each newLinkSessionand returns itsOutcome.vmholds the selected VM's credentials, andstopis the service'sasyncio.Event. The default awaitssession.run(stop). An override must run the session, finish when it ends orstopis set, and clean up its background tasks before returning the outcome.Servicelogs exceptions fromserve()and reconnects with backoff. Cancellation propagates to the caller.await Service.serve_local(path)starts the optional local message socket at apathlib.Path. It returns anasyncio.Serverthat the caller must close. The socket forwards to the registered session whileserve()runs.Service.run()does not start this socket itself.link_client.DeviceDescription(node_id, display_name, version, commands)describes the device to Muse.commandsmaps each command name to its spec: adescription,requiredandoptionalparameters, and an optionaltimeout_ms.link_client.Outcomesays how a session ended:CLOSED,AUTH_REJECTED,FORBIDDEN,UNPAIREDorSTOPPED.
LinkSession
link_client.LinkSession(noise_host=..., vm_id=..., vm_auth_token=..., device=...,
run_command=...) is one encrypted session with a Muse VM.
await run(stop)connects, registers the device and serves Muse's commands untilstopis set or the link ends. It returns anOutcome.run_command(name, params, timeout_ms)is the synchronous callback, with the same threading and result contract asService.run_command.registeredis anasyncio.Eventthat is set once Muse accepts the device. It clears when the session ends.send_chat(message, session_id=None, *, output_modality="text")sends a user message as coming from this device. Withoutsession_idit goes to the main chat.send_voice(wav_bytes, session_id=None, *, message="", output_modality=None)sends a WAV voice note of at most 2 MiB.send_image(image_bytes, mime_type, session_id=None, *, message="", filename=None, output_modality="text")sends a picture of at mostlink_client.MAX_IMAGE_BYTES(2 MiB), withmessageas its text.mime_typeis one oflink_client.IMAGE_MIME_TYPES: JPEG, PNG, GIF or WebP. Anything else raisesValueError.link_client.acknowledgement(result)reads Muse's acknowledgement from the result ofsend_chat,send_voiceorsend_image. It raisesRequestRejected, with astatusattribute, when Muse refused the request, orValueErrorwhen the response is not a dict.await open_side_chat(session_id)looks up a side chat and returns aSideChat.SideChat.existsis false for an ID that Muse does not know yet. The firstsend_chatto that ID creates the chat. The lookup closes its subscription without reading events.subscribe_chat(session_id=None, *, opened=None)yields Muse's chat events.opened, anasyncio.Event, is set once Muse accepts that subscription.chat_subscribedis set while a subscription is open.stream_tts(message_id)yields MP3 audio for a reply that Muse gave in voice mode.send_chat,send_voiceandsend_imageare coroutines that return a dict withok,statusandresponse.subscribe_chatandstream_ttsare async iterators.open_side_chat,subscribe_chatandstream_ttsraiseHttpStreamErroron refused HTTP requests. It hasstatusandpathattributes.
Following one request
muse_turn.MuseTurnfollows one request through Muse's chat events.- Create it before you send the request. Pass every chat event to
feed(). Passacknowledgement(result)toacknowledge(). - Both methods return lists of events.
acknowledge()releases events held before the acknowledgement arrived. Each event is anAgentStatusfor public progress, aReplywhen an answer message changes, or aTaskFinishedwhen Muse's task ends.TurnEventis the union of these three types.AgentStatushasactivity_codeandactivity_text.Replyhasmessage_id,text,streamed,doneandbound, which is true when the reply is known to answer this request. Both texts have their control characters removed, except tab and newline. complete(now)says when Muse is done answering.settled(now)says when Muse finished the task, every reply is done and the grace period passed. Pass atime.monotonic()value asnow.MuseTurn(session_id=None, *, owns_chat=False, replay_scope=None, ignore_parentless_until=0.0)follows a request on the chatsession_id. Setowns_chatwhen the gadget owns that chat. Its acknowledgement must then confirm the samesession_idandis_thread: true.repliesmaps each answer's message ID to its latestReply.abandon_reply(message_id)stops waiting for one reply.acknowledge(response, follow_up=True)binds one more user message to the request. Callexpect_follow_up()before you send it. Until that acknowledgement arrives, the turn is neither complete nor settled.acknowledged,busy,task_finished,activity_codeandlast_activitydescribe the request's state.ReplayScopedrops events from earlier turns. Share one across the turns of a chat the gadget owns.retire()on a finished turn adds its IDs.ReplayScope.retire_request(response)retires a request that no turn follows.ReplayScope.observe_idle(event, session_id)retires an event seen while no turn is open.
Say Muse, never Hatch
Users never see the name Hatch.
- Anything a person reads says Muse, the Muse app, or the Muse's name:
- CLI output and help
- log lines
- errors
- docs
- Don't use
hatchin a new file name or identifier. Usemuseormusegadget. The device API client ismuse_api.py. hatchstays only where the server or the Muse app depends on it. Don't rename these:- the host
hatch.metaaivm.com - the
hatch_refresh:auth prefix - pairing labels and ids such as
hatch-link-pairing-v5, thehatch_linkmodel and thehatch-link:device id, and the test vectors
- the host
Before you hand back work
- The tests pass, on the newest Python and on Python 3.9 with cryptography 3.3.2 if you touched pairing or the Noise code.
- If you changed
install.sh, it passes ShellCheck. - If you deployed, the log shows
registered with the Museand no tracebacks.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

