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_PANE is set. Without it you are in a bare shell: plain terminal work still works, but notify, hook-event, wait and decision.answer will 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

sh
1signaltty schema # methods, events, error codes, optional capabilities
2signaltty 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 --until or use --until done,blocked,failed for alternative outcomes. A primary agent fans out with pane.spawn, waits, then collects with pane read. That is the orchestration loop.
  • signaltty decision answer --pane ID --decision DID --option OID — answer a structured approval (pending_decision in pane 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_decision with options is answerable; anything else gets answered inside the terminal, never via a guessed decision.answer.
  • Prefer wait + pane read over attaching to interactive streams.
  • Keep notifications short: title ≤ a few words, body ≤ a line.

Example: fan out, wait, collect

sh
1A=$(signaltty --json pane spawn --workspace "$WS" --agent codex -- codex exec "write tests" | jq -r .pane.id)
2B=$(signaltty --json pane spawn --workspace "$WS" --agent claude -- claude -p "review auth" | jq -r .pane.id)
3signaltty wait --pane "$A" --until done --timeout 1800
4signaltty 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):

sh
1# 1. Start N tasks back-to-back (each returns at `pending`; prompt is submitted for you).
2A=$(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)
4PA=$(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).
6signaltty --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.
9signaltty --json task get "$A" | jq '{state, status_reason}'
10signaltty --json pane read "$PA" --mode rendered --after-seq "$SEQ" # incremental; feed next_seq back
11signaltty --json attention # who needs the user, ranked
12signaltty --json pane submit "$PA" --text "please report your result now" # back to working
13signaltty decision answer --pane "$PA" --decision DID --option OID # permissions only
14# 4. Review each diff against its own base, then finish explicitly.
15signaltty --json task diff "$A"
16signaltty --json task diff "$A" | jq .merge_preview # {clean, conflicted}: order merges or re-run before finishing
17signaltty --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:

sh
1# Open a pull request for a completed task (pushes branch to origin, creates PR via gh):
2signaltty --json task pr "$A" [--title "Title"] [--body "Description"] [--draft]
3
4# Refresh PR state, CI checks, and review decision:
5signaltty --json task pr-refresh "$A" # single task
6signaltty --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:
10signaltty --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):

sh
1CTX=tctx_demo; REPO=$PWD
2for 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"
5done
6signaltty --json task wait --context "$CTX" --until working --timeout 300 # background ready + submit
7# ... workers work; one ends its turn silently ...
8signaltty --json task wait --context "$CTX" --timeout 3600 # ends at input_required
9T3=$(jq -r .task.id task3.json); P3=$(jq -r .pane.id task3.json)
10signaltty --json pane submit "$P3" --text "please report your result now"
11signaltty --json task wait --context "$CTX" --timeout 3600 # all terminal
12for f in task1 task2 task3; do signaltty --json task diff "$(jq -r .task.id $f.json)"; done
13signaltty --json task finish "$(jq -r .task.id task1.json)" --merge
14signaltty --json task finish "$(jq -r .task.id task2.json)" --merge --keep-branch
15signaltty --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:

sh
1baseline=$(signaltty --json pane get "$A" | jq -c .wait_baseline)
2signaltty pane input "$A" --data "$prompt"
3signaltty --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.