Running an @bot line
SkillDev toolsHow to run an @bot line: delegate it to its own session and wait for the result.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Running an @bot line skill
What this skill tells your AI
The instructions your AI receives, as published by surething-io/cockpit in skills/bot-run/SKILL.md and read by ahel’s review.
Every [subagent·@name] line in the message is that Bot's work, not yours. Hand it to a session of
its own and wait for the answer — you are the dispatcher for these lines, not the executor.
Do not read the Bot's BOT.md or bot-turn yourself. They are listed at the end of the
message under "pass the path along; do not open them yourself" — that is the only reason you have
those paths. The child reads them and works from them; keeping them out of this session is the
point.
1. Write a self-contained brief
The child sees none of this conversation. Everything it needs goes in the brief: the user's intent in their own words, the context you have accumulated here (which repository, which files are in scope, what was already decided, what it should NOT re-litigate), and what "done" looks like. This is the whole value of delegating from here rather than letting the user type it — the brief you write is the part the user did not have to.
Start it with both paths, in this order — the general contract, then the particular Bot:
Read
<the bot-turn path>for how a Bot turn works, then<the Bot's BOT.md path>for who this Bot is and which of its files to read. Follow its "Before working" section, then do the task in the voice its identity files define.
Both paths are in the list at the end of the message. Pass them verbatim; a child that is missing
bot-turn has no write-lock protocol and will invent one.
Write only what you learned here; never assert what the Bot remembers. You have not read its files — it has. A brief that says "this is already settled in its memory" is a guess dressed as a citation, and the child will take it as a premise it may not re-open. Say where a constraint actually came from ("the user said so in this conversation"), and leave its memory to it.
2. Delegate — fire every @bot line before waiting on any of them
BRIEF=$(mktemp /tmp/cockpit-brief.XXXXXX)
cat > "$BRIEF" <<'EOF_BRIEF'
<the brief>
EOF_BRIEF
curl -sS --fail-with-body -X POST "{{BASE_URL}}/api/sessions/delegate" \
-H "Content-Type: application/json" -H "x-cockpit-run-id: $COCKPIT_RUN_ID" \
--data-binary @- <<EOF_JSON
{"cwd": "${COCKPIT_CWD:-$PWD}", "title": "@<name>: <short label>", "briefPath": "$BRIEF"}
EOF_JSON
The body is { cwd, engine?, model?, title?, prompt? | briefPath? }:
-
Do not delete the brief file. The POST returns before the child has read it: the endpoint only checks that the file exists, and the child is handed the path. Tidying up
$BRIEFafter the call leaves it with a task it cannot read. -
cwdhas two right answers, decided by what the line asks for:The @nameline iscwdordinary work ${COCKPIT_CWD:-$PWD}— exactly as written above, a variable you never resolve yourself"导出 / export", "复盘 / review", "装上 / attach" the Bot's own directory: dirnameof the BOT.md path listed at the end of the messageOrdinary work happens in the project, and the Bot reaches its own files by absolute path. The three maintenance verbs are work on the Bot: its files are the subject, its
.locks/is there, and a review writes.reviews/into it — a child sitting in some unrelated project would reach all of that by absolute path for no reason, andgitinside the Bot's own repository would not work at all. Takingdirnameof that path is not opening it; the rule against reading it stands.COCKPIT_CWDis this session's own working directory, exported by Cockpit. Leave it as a variable in the JSON — the heredoc expands it. Never substitute a path you worked out instead: not the git root, not the nearest directory with a CLAUDE.md, not any parent that looks more like a project than where you are. Doing that silently moves the child one or more levels above where the user is working, and its transcript then lands under a different project in the session list — which is what happened when a session working inside a subdirectory delegated a child at the repository root instead. It is also notpwd:pwdfollows anycdthis turn has made, which is why the fallback is only there for a Cockpit too old to export the variable. -
Leave
engineandmodelunset. Bot manifests expose onlynameanddescription, and the dispatcher must not openBOT.mdto invent an engine preference. -
titleis optional but always worth setting — it is how the session is listed later. -
For a one-line task you may send
"prompt": "..."instead ofbriefPath(escape"and\). A Bot task rarely is one: the brief carries the accumulated context, which is the whole point. -
The receipt comes back at once:
{ cwd, engine, model?, sessionId, link, title, createdAt, parent? }. Post every Bot's brief first, then wait. -
A 400 names the problem — cwd missing, relative, or not a directory; a brief file that does not exist; too many delegated sessions already running. Report what it says; do not retry blindly.
3. Wait
A Bot turn routinely outlasts a single bash call's tool timeout — a working-tree code review measured 11 minutes against a 10-minute ceiling — so the wait has to survive that. Two ways, in order of preference:
Run it in the background if your Bash tool can (run_in_background, a job that notifies you
on exit, or equivalent). Set BUDGET high (3600) and launch once: the tool timeout stops applying,
you get woken when it finishes, and you are free in the meantime. This is what to reach for first.
Otherwise wait in chunks. Foreground, BUDGET about 10% under your Bash timeout (600s max →
BUDGET=540; can't raise it → assume 120s and use BUDGET=100). Each call prints where things
stand and exits; if anything is still running, run the same block again with the remaining ids.
That is the normal path, not a failure.
CWD='<the receipt's cwd, verbatim>'
SIDS='<sessionId>
<sessionId>' # still-running ids, ONE PER LINE
BUDGET=540 # seconds THIS call may spend
started=$(date +%s)
deadline=$(( started + BUDGET ))
while :; do
pending=''
for sid in $(printf '%s\n' "$SIDS"); do
body=$(curl -sS --fail-with-body --get "{{BASE_URL}}/api/sessions/status" \
--data-urlencode "cwd=$CWD" --data-urlencode "sessionId=$sid") || body='{}'
state=$(printf '%s' "$body" | python3 -c 'import json,sys
try: print(json.load(sys.stdin).get("status","?"))
except Exception: print("?")')
echo "+$(( $(date +%s) - started ))s $sid -> $state"
# `?` = the status call failed, NOT a terminal state. Keep it pending so a
# blip retries and a real mistake shows up as a stall, never as "finished".
if [ "$state" = running ] || [ "$state" = '?' ]; then pending="$pending
$sid"; fi
done
SIDS=$(printf '%s\n' "$pending" | grep -v '^$')
[ -z "$SIDS" ] && break
[ "$(date +%s)" -ge "$deadline" ] && break
if [ "$(( $(date +%s) - started ))" -lt 30 ]; then sleep 5; else sleep 15; fi
done
echo "still running: ${SIDS:-none}"
still running: <ids>→ the budget ran out first. Run the block again withSIDSset to exactly those ids. Nothing is wrong.still running: none→ every Bot reached a terminal status. The last+Ns <sid> -> <state>line for each is that status; read it before writing the report.- Any
-> ?line → that status call failed and the id stays pending. If it repeats, the call is wrong, not the session — see below. - One loop covers every Bot, so they run in parallel and each drops out as it finishes.
- Keep the ids newline-separated and keep
$(printf '%s\n' "$SIDS")— a plainfor sid in $SIDSreads as a single id under zsh, which does not word-split parameters.
There is no fixed cap. Keep waiting while the user is waiting on the answer. For scale: two measured working-tree code reviews took 7 and 11 minutes; a Bot review sweep reads a whole directory and takes longer. Once it has run past roughly half an hour, stop waiting silently — say it is still going, hand over the links, and let the user decide.
Never state an outcome you have not seen a non-running status for. Hand-rolled variants of
this check go wrong in one direction only: they exit immediately and you report a result the child
never produced.
done = the last turn finished normally; failed = the engine recorded an error;
incomplete = stopped or interrupted, and the user can open it and carry on.
? is not a status. It means the status call itself failed — wrong cwd, wrong id, server
gone. The loop keeps such an id pending rather than counting it finished, so a transient blip
just retries; an id stuck on ? for a whole budget means the call is wrong. Re-run the bare curl
without || body='{}', read the error body, and fix it. Never report ? as an outcome.
4. Follow up in the same session — do not delegate twice
A Bot's answer often needs one more exchange: it asks which of two directories you meant, or the user reads the result and wants a change. That is a message to the session that already exists, not a new delegation.
curl -sS --fail-with-body -X POST "{{BASE_URL}}/api/chat" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF_JSON
{"cwd": "<the receipt's cwd, verbatim>", "sessionId": "<the receipt's sessionId>", "prompt": "<the follow-up>"}
EOF_JSON
- No
x-cockpit-run-idhere, unlike §2: only the delegate endpoint reads that header, to record which session started the child. This one continues a session that already has a parent. - Match the route to the engine the receipt reported:
/api/chatis claude; codex, ollama, kimi, deepseek and glm each have/api/chat/<engine>. Sending to the wrong one starts a different engine on that transcript. - It returns
{ runKey, sessionId }immediately, exactly like a delegation. Wait for it the same way — §3, with the samecwdand thatsessionId. 409 session is already runningmeans the child has not finished. That is not an error to route around; wait for a non-runningstatus first and send it then.- Long or multi-part follow-ups go in a brief file exactly as in §1 — this body takes
promptonly, so write the file and reference it in the text if it is too big to inline comfortably.
Delegating again instead is the tempting mistake, and it is expensive in a way that does not look expensive: the new session starts from nothing, re-reads the Bot's files, and knows only what your brief happens to restate. Everything the last turn established — what it checked, what it ruled out, what the user already answered — is in that transcript, and a summary of it is not the same thing. It has happened: a dispatcher decided Cockpit had no way to continue a session, re-delegated with a paragraph of context, and paid for the whole task twice.
5. Report
One short section per Bot: its conclusion in a few lines, then [open session](<link>). The full
answer lives in that session — do not paste it wholesale, and do not restate it as your own.
lastReply is cut at 4000 characters; when lastReplyTruncated is true and you need the rest,
repeat the status call with --data-urlencode "full=1". Use the receipt's link exactly as
returned — it is the only record of the delegation, and tool output is not searchable later.
A numbered list is a special case. Some Bot tasks (a review sweep, for instance) end in a list of findings the user picks from. Relay it whole with its numbering intact, and keep whatever path the child saved it to — the user answers either in that session or by naming that file in a new one, and dropping either detail strands the work.
Pressing stop here does not stop a delegated session — it is a separate run. Say so if the user expects otherwise.
When the user answers your report — a correction, a pick from a numbered list, an answer to a question the Bot asked — that reply belongs in the session it came from. Go back to §4; do not start a second one.
Signals
- GitHub stars
- 38
- Forks
- 10
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
bot-run- Source
- github.com/surething-io/cockpit