agentleFS
colonyopscolonyops/hive/.agents/skills/desktop-api/SKILL.md

desktop-api

Skill41 starsChanged 7 days agoMITView on GitHub

Observe the Hive Desktop notification pipeline and force a re-poll through its loopback MCP server, instead of reading desktop-pipeline.db. Use to assert what an event produced in the inbox, force a refresh after a devserver overlay/action/scenario, discover the webhook port for a push, or confirm an actions.yml/flow edit actually loaded.

  • Reads credentials

1 other project uses a version of this, 1 of them edited.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If /api/status refuses the connection, the app is not running, was not built from this worktree, or the HTTP server is disabled …

    line 28
  2. Very specific:

    A failing tool answers with a result, not an error: Look for .result.isError and read .result.content[].text, which opens with a stable …

    line 171

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill 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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Observe the pipeline through the MCP server

The desktop exposes its capabilities as MCP tools over app.App (ADR mcp-replaces-the-agent-facing-http-api), so a test asserts against a supported surface instead of desktop-pipeline.db. One loopback http server (on by default) serves webhook push (/hooks/…), the MCP server (/mcp), and the liveness probe (/api/status) on one port.

Pair this with the devserver skill: devserver acts (rewrites GitHub, pushes webhooks), these tools observe and reload.

1. Find the port and confirm the app is up

The HTTP port is written to the worktree's launch.env. Liveness stays a plain GET, because a JSON-RPC handshake is the wrong shape for "is it running?":

API=127.0.0.1:$(grep HIVE_DESKTOP_HTTP_PORT launch.env | cut -d'"' -f2)
curl -s $API/api/status  | jq .   # {webhook:{running,host,port,pathPrefix}, api:{pathPrefix}}
curl -s $API/api/version | jq .   # revision + dirty flag of the running build

If /api/status refuses the connection, the app is not running, was not built from this worktree, or the HTTP server is disabled (http.enabled: false). A fresh mise run desktop:dev:prepare turns it on.

2. Connect

The endpoint is http://$API/mcp, Streamable HTTP, no authentication (loopback bind, and no tool here spawns a process). Prefer a real MCP client — add it once and the tools appear as ordinary tools:

claude mcp add --transport http hive-desktop "http://$API/mcp"

The server is stateless and answers application/json rather than SSE, so a single curl also works when a client is not set up. Ask it what it serves — the schemas are generated from the server's own Go types, so this never drifts:

mcp() { curl -s "http://$API/mcp" -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-\{\}}}"; }

mcp tools/list | jq -r '.result.tools[] | "\(.name)\t\(.title)"'

tools/call takes {"name": …, "arguments": {…}}, and a tool's answer is in .result.structuredContent:

call() { mcp tools/call "{\"name\":\"$1\",\"arguments\":${2:-\{\}}}" \
  | jq '.result.structuredContent // .result'; }

3. The loop: reload → read → retry

There is no server-side wait. source_refresh returns once the event log is appended; the flow engine commits a moment later on its own goroutine. So force the poll, then read and retry a few times:

call source_refresh
for i in $(seq 1 10); do
  hit=$(call inbox_list '{"profileId":"'"$P"'","detail":"full","limit":50}' \
    | jq --arg repo acme/widgets --argjson num 42 \
        '[.items[] | select(.payload.repo==$repo and .payload.num==$num)][0]')
  [ "$hit" != "null" ] && echo "$hit" | jq '{lifecycle, sourceState, unread, reason: .payload.reason}' && break
  sleep 1
done

Selecting on payload fields is what detail: "full" is for, and why this narrows with limit — see Read tools before widening it.

A webhook push appends directly (no refresh needed) — push via devserver, then read + retry the same way.

4. Read tools

call profile_list                                    # ids to scope everything else by
call flow_get '{"profileId":"'"$P"'"}'                # the graph: node ids, types, wires
call inbox_list '{"profileId":"'"$P"'"}'                # newest first; title/state/lifecycle, no payloads
call inbox_list '{"profileId":"'"$P"'","feed":"'"$FEED"'"}'                  # presence == routed there
call inbox_list '{"profileId":"'"$P"'","feed":"'"$FEED"'","archived":true}'
call inbox_list '{"externalId":"'"$EXT"'","detail":"full"}'  # a GitHub global node id, NOT repo#num
call feed_list '{"profileId":"'"$P"'"}'                # every declared feed, counts joined on
call inbox_event_list '{"itemId":'"$ID"'}'      # one item's history
call inbox_session_list '{"itemId":'"$ID"'}'          # hive sessions this item started

flow_get is where node ids come from — flow_preview's nodeId and the node-image tools take them, and nothing else on the surface reports one.

Payloads are opt-in. inbox_list and inbox_event_list default to detail: "summary", which omits the raw source payload — a PR body alone is kilobytes and a listing repeats it per item, so a whole feed at "full" runs to megabytes and will blow a tool-response limit. Narrow first (by feed, by externalId, or with limit), then ask for "full". Everything the app itself names — title, url, lifecycle, sourceState, unread, feedId, timestamps — is in the summary. Each answer echoes the detail it used, so an omitted payload is never mistaken for an absent one.

A missing thing is not_found, never an empty collection: a profile id, a feed id or an item id that resolves to nothing is an error, so an empty list always means "nothing has landed here". feed_list reports a declared feed at zero rather than omitting it, and marks declared: false on a feed only stale inbox rows still claim.

5. Test a flow without deploying it

flow_preview runs a flow against input you supply and returns what every node did, committing nothing (ADR flows-are-dry-run-against-supplied-input). Reach for it before the deploy → refresh → read loop above: that loop writes feed membership, kv and notifications to answer a question about a script, and it cannot tell a bug from "hasn't polled yet".

call flow_preview '{
  "profileId": "triage", "nodeId": "src-github",
  "messages": [{"Key": "acme/app#1", "Payload": {"state": "open"}}]
}' | jq '.nodes[] | {nodeId, in, out, dropped, ok,
                     emitted: [.emitted[] | {port, keys: [.messages[].Key]}],
                     console: [.console[].text], error}'
  • Isolate one node. nodeId is any node, not only a source — inject a captured payload straight at the function node under test and the source in front of it never runs.
  • Test an unsaved edit. Send flowYaml (the file's text) or flow (the same document as a JSON object) instead of profileId.
  • Seed kv ({"<nodeId>": {"<key>": <value>}}) to exercise notify-once against a known starting state. The real store is neither read nor written, so repeated calls give the same answer; what the run would have stored comes back in .kvMutations, and what it would have committed in .outputs.
  • Keep the response bounded with detail. A payload is reported once per node it reaches, so a real one through a deep graph comes back many times over. "emitted" (the default) drops each node's received — it is the upstream's emitted; "counts" drops message bodies entirely and is what a snapshot run wants; "full" restores both.
  • console.log in a function node is readable here and nowhere else — a live run discards it.
  • A script error's line and the positions inside its message and stack are all relative to the on_message body, not to the wrapper the engine compiles.

6. Did my config edit load?

Both config surfaces report their own load status, so an edit is verified by reading it back rather than by clicking through the UI. A file that fails to parse leaves the previous version in effect, so valid is the only signal that separates "accepted" from "rejected and ignored":

call action_list  | jq '{path, valid, error, ids: [.actions[].id]}'
call profile_list | jq '[.profiles[] | {id, valid}]'

action_list returns each action's type-specific config too, so a template edit is confirmed by the value that came back — .actions[] | select(.id=="…") | .clipboard.textTemplate.

Guardrails

  • A failing tool answers with a result, not an error. Look for .result.isError and read .result.content[].text, which opens with a stable kind — invalid, not_found, conflict, unavailable, internal. Branch on that, never on the prose.
  • Match on the payload, not the external id: external_id is the source's own id (a GitHub global node id), while the devserver and your test think in repo/num. Filter .items[] | select(.payload.repo==… and .payload.num==…) — which needs detail: "full", so scope the read before you widen it. The same external id can exist across profiles/scopes, so by-id reads return a slice.
  • Reload only helps pull sources. source_refresh re-polls sources.github; it does nothing for webhook-fed feeds (those append on delivery). It answers unavailable in mock modes (no producer).
  • The tools need the HTTP server on. They share that loopback server (on by default); with http.enabled: false there is nothing to mount on.
  • Reads, reloads and safe mutations only. Session control is deliberately absent — starting a session is command execution and lives behind the token-guarded /api/terminal/ prefix. Drive triage through the app or the devserver, and assert here. A dry run is part of that: it reports the outputs and KV writes a live run would have made instead of making them, so what it tells you about the commit is the graph's half. Two things the commit itself does are deliberately outside it — minting an inbox row for a key no source ingested, and dropping feed rows a snapshot no longer lists. Assert those through the inbox after a real refresh.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.