Agent CLI: Feedback Triage
Read and triage feedback/crash tickets from the command line with @botiverse/hands-cli.
A task-focused walkthrough for agents that need to read and triage feedback and crash tickets from the command line. For the broader auth model and release/share operations, see the Agent Guide; for the full command surface, the CLI Reference.
Choose the right auth surface
Hands supports both Raft manifest actions and the local CLI, but their sessions are intentionally separate:
raft integration invokeuses the session held by Raft Agent Login and is the preferred surface for interactive agent/admin actions.@botiverse/hands-clirequires an explicit Hands bearer/deploy token and is the preferred surface for CI and repeatable release scripts.
An integration login is not exported to the CLI through environment variables. Do not scrape cookies or callback handoffs to bridge the two.
1. Get a CLI token
For app-scoped automation, an admin can create a deploy token through the Hands console or the Raft integration:
raft integration login --service hands-4cc7a2
raft integration invoke --service hands-4cc7a2 \
--action create-deploy-token \
--param app_id=<app-uuid> \
--data-json '{"name":"agent-cli","app_role":"publisher"}' \
--json
Store the one-time raw token in a private secret manager, then export it for the CLI:
export HANDS_BEARER_TOKEN=<deploy-token>
Use a viewer token for read-only triage and publisher only when the script must upload or mutate releases. Revoke and replace any token whose initial secret-store write fails.
2. Run the CLI
No global install needed:
npm exec --package @botiverse/hands-cli -- hands whoami
(Pin a version with @botiverse/hands-cli@0.3.2 if you want reproducibility.) Every command below takes --json for scripting.
Triage commands
The first argument is the app slug (or id), e.g. raft-android.
# List tickets, newest first (filterable)
hands feedback list raft-android --status open --kind crash
hands feedback list raft-android --kind bug
# Show one ticket: description, device context, attachments, comments.
# Use the full UUID from the submitted ticket response/reference.
hands feedback show raft-android 389d855b-0000-0000-0000-000000000000
# Triage: change status and/or (re)assign
hands feedback update raft-android 389d855b-0000-0000-0000-000000000000 --status in_progress --assignee me
hands feedback update raft-android 389d855b-0000-0000-0000-000000000000 --status resolved
hands feedback update raft-android 389d855b-0000-0000-0000-000000000000 --assignee none # unassign
# Leave an internal comment (also where auto-retrace/symbolication land)
hands feedback comment raft-android 389d855b-0000-0000-0000-000000000000 "reproduced on SGT-AL10; fix in progress"
Status flow: open → in_progress → resolved → closed. Assignee is independent of status.
Quick lookup by feedback id
When a user pastes a Hands feedback id, first identify the app slug. For Raft Android/mobile, that slug is normally raft-android.
New Hands feedback responses include the full UUID in both id and the copyable reference field, for example:
raft-android · 1.0.4 (1000400) · ticket 389d855b-0000-0000-0000-000000000000
Use that UUID directly:
TICKET_ID=389d855b-0000-0000-0000-000000000000
APP=raft-android
hands feedback show "$APP" "$TICKET_ID"
The CLI now prints full UUIDs in human output. You can also pass a short id or any unique prefix — the feedback read/detail/attachment endpoints resolve it to the full ticket automatically (an ambiguous prefix returns 409; use a longer prefix or the full UUID):
hands feedback show raft-android 389d855b # 8-char short id works
The show output is the first place to check for diagnostics: it includes the user message, version/channel, device context, comments, and all attachment ids. Crash deobfuscation/symbolication output is appended as an internal comment when mapping/symbol assets exist.
For scripts, keep the raw JSON:
hands feedback show "$APP" "$TICKET_ID" --json > ticket.json
Crashes
Crash tickets (--kind crash) carry a grouping signature (exception class + top frame). When the build's mapping.txt (Java/R8) or native-symbols archive was uploaded, an auto-deobfuscated / symbolicated stack is appended as an internal comment — visible in feedback show. To aggregate by signature across a fleet, call the API directly:
curl -s -H "Authorization: Bearer $HANDS_BEARER_TOKEN" \
https://hands.build/api/apps/<appId>/feedback/crash-groups
Attachments (diagnostics zips, logs)
feedback show lists each attachment's id, filename, and size. Download one with the CLI (accepts a slug and a short/prefix ticket id):
hands feedback download-attachment raft-android 389d855b <attachmentId>
# → Saved 445539 bytes to raft-feedback-....zip (use -o to choose the path)
Hands saves the raw bytes as-is — it does not unzip or interpret the contents. Hands does not define the files inside an app's diagnostics archive; it stores and serves the attachment, and the producing app owns the log format and archive layout (how to open a given app's zip is documented in that app's repo, not here).
The same downloads and ticket detail can also be fetched directly via REST (the API path uses the app UUID, not the slug):
APP_ID="$(hands apps get "$APP" --json \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
curl -s -H "Authorization: Bearer $HANDS_BEARER_TOKEN" \
"https://hands.build/api/apps/$APP_ID/feedback/$TICKET_ID" # ticket detail
curl -s -H "Authorization: Bearer $HANDS_BEARER_TOKEN" \
"https://hands.build/api/apps/$APP_ID/feedback/$TICKET_ID/attachments/<attachmentId>" \
-o diagnostics.zip # raw attachment
Other environment knobs
HANDS_API— point the CLI at a non-production Worker (defaults to the production origin).--json— machine-readable output on every command, for agents that parse results.