crates/signaltty-cli/assets/SKILL.md
signaltty Agent Skill
You are running inside a signaltty-managed pane. Your pane id is in
$SIGNALTTY_PANE and the server socket in $SIGNALTTY_SOCKET
(defaults apply when unset). This file is the whole contract — read it
once, then ask the server for the rest.
Gate (read first)
- Act through this API only when
SIGNALTTY_PANEis set. Without it you are in a bare shell: plain terminal work still works, butnotify,hook-event,waitanddecision.answerwill misattribute or fail — say so instead of guessing a pane id. - Never invent pane ids, decision ids, or option ids. Use exactly what
the server returned (
pane.get,decision.created).
Learn the contract at runtime
| 1 | signaltty schema # methods, events, error codes, optional capabilities |
| 2 | signaltty pane get "$SIGNALTTY_PANE" |
--json on every command prints the raw result object. Unknown JSON
fields are ignored by the server; mistyped ones are BAD_PARAMS.
Things you can do
signaltty pane read ID [--mode screen|tail]— read scrollback.signaltty pane input ID (--data "…" | --stdin)— type into a pane.signaltty notify --pane ID --title T [--body B] [--severity info|warning|error]signaltty wait --pane ID --until blocked|done|idle|failed|exited|seen --timeout 300— matches current state immediately when already satisfied. Repeat--untilor use--until done,blocked,failedfor alternative outcomes. A primary agent fans out withpane.spawn,waits, then collects withpane read. That is the orchestration loop.signaltty decision answer --pane ID --decision DID --option OID— answer a structured approval (pending_decisioninpane get).signaltty hook-event --agent <kind> --event <Name> --payload-stdin— report lifecycle (shims do this; read the hook JSON from stdin).signaltty report-session --pane ID --session SID— pin a native session id when hooks cannot (resumed sessions often fire no start hook).
Rules
- Attention clears only on explicit per-pane interaction (
pane.mark_seen, focusing the pane, answering). Do not mark other panes seen. - Prose is not data: a
pending_decisionwith options is answerable; anything else gets answered inside the terminal, never via a guesseddecision.answer. - Prefer
wait+pane readover attaching to interactive streams. - Keep notifications short: title ≤ a few words, body ≤ a line.
Example: fan out, wait, collect
| 1 | A=$(signaltty --json pane spawn --workspace "$WS" --agent codex -- codex exec "write tests" | jq -r .pane.id) |
| 2 | B=$(signaltty --json pane spawn --workspace "$WS" --agent claude -- claude -p "review auth" | jq -r .pane.id) |
| 3 | signaltty wait --pane "$A" --until done --timeout 1800 |
| 4 | signaltty pane read "$A" --mode tail --lines 50 |
Orchestrating other agents
When you are the orchestrator in a pane (work that splits into independent, mergeable pieces, each in its own worktree), spawn worker tasks instead of doing it all yourself. Do one thing yourself when the change is small, needs your checkout, or the pieces cannot be reviewed separately.
The loop — exact commands (--json + jq to capture ids):
| 1 | # 1. Start N tasks back-to-back (each returns at `pending`; prompt is submitted for you). |
| 2 | A=$(signaltty --json task start --repo "$REPO" --context "$CTX" --label "api" \ |
| 3 | --agent claude --objective "Add the endpoint. Acceptance: cargo test passes." | jq -r .task.id) |
| 4 | PA=$(signaltty --json task get "$A" | jq -r .task.pane_id) # worker pane when needed |
| 5 | # 2. Wait until all settle (default: terminal OR input_required). |
| 6 | signaltty --json task wait --context "$CTX" --timeout 1800 |
| 7 | # 3. A task at input_required ended its turn without reporting (or went silent, |
| 8 | # or needs a permission): inspect, then follow up or answer. |
| 9 | signaltty --json task get "$A" | jq '{state, status_reason}' |
| 10 | signaltty --json pane read "$PA" --mode rendered --after-seq "$SEQ" # incremental; feed next_seq back |
| 11 | signaltty --json attention # who needs the user, ranked |
| 12 | signaltty --json pane submit "$PA" --text "please report your result now" # back to working |
| 13 | signaltty decision answer --pane "$PA" --decision DID --option OID # permissions only |
| 14 | # 4. Review each diff against its own base, then finish explicitly. |
| 15 | signaltty --json task diff "$A" |
| 16 | signaltty --json task diff "$A" | jq .merge_preview # {clean, conflicted}: order merges or re-run before finishing |
| 17 | signaltty --json task finish "$A" --merge # deletes the task branch (--keep-branch to keep); or --discard |
| 18 | # Finish outcome: .task.disposition.outcome (merged|discarded); .task.state keeps completed/failed. |
Pull requests
When a task produces work that should go through GitHub review rather than being merged directly:
| 1 | # Open a pull request for a completed task (pushes branch to origin, creates PR via gh): |
| 2 | signaltty --json task pr "$A" [--title "Title"] [--body "Description"] [--draft] |
| 3 | |
| 4 | # Refresh PR state, CI checks, and review decision: |
| 5 | signaltty --json task pr-refresh "$A" # single task |
| 6 | signaltty --json task pr-refresh # all open task PRs |
| 7 | |
| 8 | # While a PR is open, task finish --merge is refused: |
| 9 | # Once the PR is merged on GitHub, discard cleans up the worktree: |
| 10 | signaltty --json task finish "$A" --discard |
Rules: cap is 4 parallel tasks (over-cap → RATE_LIMITED, nothing
created). Never auto-merge: read task diff first. Workers report via
signaltty report --status completed|failed|rejected --summary … (it reads
$SIGNALTTY_TASK inside the worker pane). task.wait on a context ends at
the first input_required too — loop until everything is terminal.
signaltty schema is the exact contract when in doubt.
Workers must be agents whose hooks report readiness (--agent claude,
codex, …); a plain shell never gets ready and fails with ready_timeout.
A repo the agent has never trusted stops the worker on its folder-trust
prompt: the failed task's status_reason.screen_tail shows that screen.
Trust the repo once in the agent, then retry. A prompt typed but never
confirmed by activity parks at input_required with
reason: submit_unconfirmed; a nudge, or the worker starting late, resumes it.
Worked example (3 workers, one needs a nudge):
| 1 | CTX=tctx_demo; REPO=$PWD |
| 2 | for i in 1 2 3; do |
| 3 | signaltty --json task start --repo "$REPO" --context "$CTX" --label "worker-$i" \ |
| 4 | --agent claude --objective "task $i: <objective>. Acceptance: <criteria>." > "task$i.json" |
| 5 | done |
| 6 | signaltty --json task wait --context "$CTX" --until working --timeout 300 # background ready + submit |
| 7 | # ... workers work; one ends its turn silently ... |
| 8 | signaltty --json task wait --context "$CTX" --timeout 3600 # ends at input_required |
| 9 | T3=$(jq -r .task.id task3.json); P3=$(jq -r .pane.id task3.json) |
| 10 | signaltty --json pane submit "$P3" --text "please report your result now" |
| 11 | signaltty --json task wait --context "$CTX" --timeout 3600 # all terminal |
| 12 | for f in task1 task2 task3; do signaltty --json task diff "$(jq -r .task.id $f.json)"; done |
| 13 | signaltty --json task finish "$(jq -r .task.id task1.json)" --merge |
| 14 | signaltty --json task finish "$(jq -r .task.id task2.json)" --merge --keep-branch |
| 15 | signaltty --json task finish "$(jq -r .task.id task3.json)" --discard |
Reuse a pane for new work
Capture the server's baseline before input, then wait for a newer matching transition:
| 1 | baseline=$(signaltty --json pane get "$A" | jq -c .wait_baseline) |
| 2 | signaltty pane input "$A" --data "$prompt" |
| 3 | signaltty --json wait --pane "$A" --until done,blocked,failed \ |
| 4 | --after-baseline "$baseline" --timeout 1800 |
An earlier Done cannot satisfy this wait, and fast completion before wait arrival
still succeeds. Repeated same-state hooks do not prove new work. IDENTITY_CHANGED
means the process/session was replaced; inspect the pane and capture a new baseline
for future work. Never resubmit input automatically after a disconnected request.
Serialize concurrent submissions to one pane when you need exact turn attribution.
Event clients inspect subscribe.replay.status: incomplete history returns no replay
and closes the stream. Subscribe afresh before reading authoritative workspace state
and attaching panes. EOF/lag/byte gaps require that recovery; snapshots restore the
current screen. Prefer the bounded wait API over maintaining terminal streams.