pylot-cli
Use when operating the Pylot gateway through its CLI — dispatch, monitor, secrets, assets, workers, and automations.
How do I install this agent skill?
npx skills add https://github.com/fellowship-dev/dogfooded-skills --skill pylot-cliIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides instructions for the Pylot CLI to manage remote missions, assets, and secrets. It handles credentials using environment variables and short-lived tokens, which is a standard security practice. The primary security surface identified is the potential for indirect prompt injection via the task and prompt interfaces used to control remote workers.
- Socketwarn
1 alert: gptSecurity
- Snykwarn
Risk: MEDIUM · 1 issue
What does this agent skill do?
Choose the execution mode
Work with a human in the loop belongs on a devbox, not a mission. Missions are autonomous operator runs (cron, auto-pylot, skill-routed tasks).
- Use an autonomous mission when the task contract is complete enough for a worker to execute, review, and report without live steering. Dispatch it and let the factory own the implementation loop.
- Use an interactive devbox when the work genuinely requires live choices, iterative diagnosis, cross-worker relays, or frequent user direction. The active session owns prompts, verification, snapshots, and shutdown.
- Interactive does not imply continuous steering. Prefer one self-contained prompt that lets the selected skill or runner own its normal phases, then wait for a question, failure, or result before adding direction.
- Do not turn an autonomous mission into an interactive one merely because its progress can be watched. If a recurring mission needs steering, improve the factory skill that should have handled the case.
Dispatch a Mission
CONV_ID=$(ls ~/.claude/session-env/ | head -1)
pylot dispatch "<task>" --agent <team>.<role> --repo <org/repo> --context "conversation_id=$CONV_ID"
Always pass --context conversation_id=… for auto-wake. Prompt limit: 4 KB — put full specs in issue comments.
The task text must start with an explicit /skill. The operator harness routes
deterministically on that prefix; free text fails at boot with
routing failed: task has no explicit /skill (exit 1, zero tokens). The only
keyword fallbacks are auto-pylot and investigate … report findings. Team names
and roles drift — pylot teams list is truth, and pylot route validates a
<team>.<role> before you spend a dispatch.
Cross-org: the repository owner and the selected credential are separate
inputs. Outside the CLI's default org, select the credential explicitly with the
global option first: pylot --org <Org> context <Org>/<repo> …. A plain
403 forbidden from an unscoped cross-org call does not prove the repo, playbook
or capability is unavailable — retry with the repo's org before diagnosing
authorization. pylot auth status shows which orgs hold a credential.
Monitor Missions
pylot missions list # recent missions
pylot missions view <job-id> # full detail
pylot missions logs <job-id> --follow # tail CloudWatch logs
Terminal statuses: done, failed, cancelled, timeout.
Secrets Discovery
pylot secrets tree # full secrets tree
pylot secrets get <path> # bundle keys + fingerprints (no values)
Load into conversation: pylot conversations resources-add <conv-id> type=secret ref=pylot/<path>
Assets
You do not have S3; you have the assets API. Anything durable — a screenshot, a
long report, a recording — has to go through pylot assets, never a direct
write and never a link to somewhere else. Never raw.githubusercontent.com on
a feature branch, never a static S3 key: both rot once the branch or retention
window is gone — see the Hosting and durability
rule
for the receipts.
Three lifetimes — pick the one that matches what you're making, before you make it:
| Lifetime | What it's for | How |
|---|---|---|
| Ephemeral scratch | A draft you're still iterating on in this turn | nothing — stays in-turn |
| Resumable checkpoint | A private draft/report that must survive a turn ending | presign --conversation <id> → PUT → finalize (finalize takes only --sha256/--size, no scope flag) |
| Published artifact | Evidence meant for a human or another repo (screenshot, PR proof) | the full presign → finalize → publish recipe below |
Do not reach for publish --conversation <id> as a shortcut or retry path for a
checkpoint — it sets visibility: public, which is a privacy regression for
what is meant to be a private draft. Ownership is already fixed at presign
time via --conversation <id>; there is no separate "attach to conversation"
step for a checkpoint.
A checkpoint's retention is not tied to evidence_class — there is no special
TTL exemption for it. retention_policy already defaults to indefinite, so
passing it explicitly changes nothing. That said, in staging every asset (any
class, any retention_policy value) still auto-expires after 30 days — an
S3 object-lifecycle rule outside the API, with no per-asset override. Treat a
staging checkpoint as staging-ephemeral: good for surviving a turn boundary,
not a substitute for promoting the finished artifact once the report is done.
See Retention
for the full policy.
Ephemeral scratch is not durable: a turn can end more abruptly than a normal
function return drops it (see the Lambda Freeze
Convention
for that failure shape in miniature). Concrete trigger, don't wait for a
vaguer sense of "running low": if a context capacity system message shows up
in the conversation (fires at ~90% of context, per issue #944), that turn is
your last chance — checkpoint what you have as a private conversation asset
before you do anything else. If no such message has fired yet but you're
about to end a turn with a report still incomplete, checkpoint anyway; the
warning is a backstop, not a permission slip to wait for it.
Use the CLI for the complete Pylot asset lifecycle. The only operation outside
the CLI is the direct PUT to the short-lived presigned object URL; never call a
Pylot gateway asset endpoint with curl.
FILE=/path/to/evidence.png
CONTENT_TYPE=image/png
SIZE=$(wc -c < "$FILE" | tr -d ' ')
if command -v sha256sum >/dev/null 2>&1; then
SHA256=$(sha256sum "$FILE" | awk '{print $1}')
else
SHA256=$(shasum -a 256 "$FILE" | awk '{print $1}')
fi
# Choose the ownership scope required by the destination: --repo, --job, or --conversation.
PRESIGN=$(pylot assets presign \
--content-type "$CONTENT_TYPE" --size "$SIZE" \
--repo <org/repo> --filename "$(basename "$FILE")" \
--evidence-class visual --retention-policy <repo-policy>) # use the retention policy required by the owning repo (e.g. indefinite, 90d)
ASSET_ID=$(printf '%s' "$PRESIGN" | jq -r '.asset_id')
UPLOAD_URL=$(printf '%s' "$PRESIGN" | jq -r '.upload_url')
# The presigned URL carries its own credentials; do not add a Pylot auth header.
curl -fsS -X PUT "$UPLOAD_URL" -H "Content-Type: $CONTENT_TYPE" --data-binary @"$FILE" && \
pylot assets finalize "$ASSET_ID" --sha256 "$SHA256" --size "$SIZE"
visual is the evidence class for screenshots and recordings intended as
durable review evidence. Use the retention and evidence policy required by the
owning repo; do not class a transient conversation image as permanent evidence
unless that is intentional.
After finalization, use the verb that matches the destination:
pylot assets view "$ASSET_ID" # metadata + temporary GET URL
pylot assets attach "$ASSET_ID" --mission "$PYLOT_JOB_ID" # private mission evidence
pylot assets publish "$ASSET_ID" # public capability URL
pylot assets publish "$ASSET_ID" --conversation <id> --alt "description"
pylot assets unpublish "$ASSET_ID" # revoke public capability URL
Publishing returns public_url. Start with ordinary publish. Add
--override-policy only when the owning policy explicitly calls for an audited
override or the server returns the corresponding policy restriction; never use
it preemptively to bypass policy. Conversation publishing
both publishes and attaches the asset; attach --mission records private mission
evidence without publishing it. All six lifecycle actions — presign, finalize,
view, attach, publish, and unpublish — remain gateway operations and must
go through pylot assets.
Team Settings
One command, one round-trip, visible proof — prints the value read back from the server after the PATCH (#3094):
pylot teams config <team> deploy.release_mode=ship # dotted-path set + read-back
pylot teams config <team> budget_daily_usd=600
pylot teams get <team> --fields deploy,cron # scoped read of stored config
pylot teams update <team> key=value [...] # multi-field PATCH (no read-back print)
Mutable fields (server-validated; a 400 lists the live set as valid_fields):
budget_daily_usd enabled org chains provider_chain cron fargate
fargate_size deploy operators worker_images repos distill_enabled
context_warn_pct context_hard_pct slack_channels.
Read-back guarantee: a field the server cannot persist is rejected with 400 —
never accepted-and-dropped (the round-trip corpus test enforces this; team-level
skills was removed from the set for exactly that reason — operator skills live
under operators.<role>.skills). If a write "succeeds" but reads back null,
that is a bug — file it; do not retry with creative payloads.
Runner contract
A skill whose name ends in -runner is run from outside a devbox and requires
one. That is the whole definition. It applies wherever the caller sits: a
Pylot operator on a cron, or this session on this Mac spawning a devbox through
the gateway. Consequences:
- A runner never executes the engine itself. It spawns or targets a devbox, sends the prompt, polls, verifies, and reports. If you catch yourself running the engine's steps inline, you are in the wrong skill.
- A runner depends on engine skills being installed inside the devbox (for
example
improve-code-quality-runnerneedsimprove-code-qualityandtest-in-stagingin the worker). Today that dependency is a prose line in the runner's Prerequisites and nothing installs it; the worker only has the engine if the target repo vendors it. Until the Pylot frontmatter contract and worker-boot install land (fellowship-dev/pylot#3540), check the target repo's.claude/skills/before dispatching a runner at it. - Engines carry no suffix and must work inside any checkout with no gateway
access. Wrappers that only dispatch a worker on a schedule are runners too,
and stay private in
pylot-skills. pylot skills list --kind runneris the filter contract for runners.
Preflight and dispatch
Before dispatch, confirm:
- the authenticated CLI can reach Pylot;
- the live team, role, skill route, repository, worker image, and budget support the selected mode;
- relevant automations will not duplicate or conflict with the work;
- the task contract is self-contained, contains no secrets, and names the intended skill explicitly.
Dispatch through the CLI and retain the returned mission or worker identifier.
Do not substitute raw gateway calls or undocumented local state. For evidence
and assets, follow pylot-cli; do not reproduce its lifecycle here.
For substantial remote work, settle branch/base, Git identity and evidence
visibility up front, and prove a small authorized durable checkpoint early.
Do not accumulate hours of work before discovering that publication is blocked.
Preserve prior scoped authorization; distinguish an actual tool rejection from
an agent's interpretation, and investigate the latter using current evidence.
After recovery or compaction, verify claimed blockers against actual tool receipts
and the accepted task. A missing named model tool does not establish that an
installed CLI is unavailable; check its help through the available shell. Resume
with concrete bounded actions, preserved authority and explicit artifact destinations
rather than an elaborate handoff narrative. Never override an actual access denial.
Workers — Spawn, Drive, Stop
A worker is a Fargate devbox running the target repo. Ownership is by scope:
spawn and list need exactly one of --mission / --conversation; the
per-worker verbs take --mission or fall back to the unscoped /workers/:wid
route. Verify flags with --help — image CLIs vary in age.
1. Preflight the repo
pylot devboxes project <org/repo> # → task_def{family,revision}, required_env_ok
No task_def means no image was ever built and the spawn boots into
CannotPullContainerError. Build first: pylot deploy build-worker <org/repo> --wait
(admin credential — an operator gets 403; see §6). Spawn failures are typed:
404 no_project/no_repo (repo not in any team's devbox config), 409 mission
already terminal, 422 provider_required, 502 ECS/secrets.
2. Spawn
# inside a mission — your own job
pylot workers spawn --mission "$PYLOT_JOB_ID" repo=<org/repo>
# from a conversation — name= is the human-readable pretty name (tag pylot:name)
pylot workers spawn --conversation "$CONV_ID" repo=<org/repo> name=<short-purpose>
# from a local session (a laptop running `pylot auth login`, no mission context):
pylot devboxes spawn <org/repo> --idle-ttl 3600 name=<short-purpose> # standalone box, the default
pylot devboxes view <task-arn> # wait for RUNNING before the first prompt
# persistent-conversation variant — capture only the id so the session credential is never printed
CONV_ID=$(pylot conversations create --org <org> --team <team> --repos <repo-name> --title "<task>" | jq -r .id)
pylot devboxes connect <task-arn> # → ssh_command; drive by SSH when direct command control beats prompting
201 → {worker_id, task_arn, last_status}. Boot runs PROVISIONING → RUNNING
(~1–2 min). Prompts are queued server-side and claimed once the in-container
daemon boots, so an early prompt is not lost; if you need RUNNING confirmed,
pylot workers list --mission "$PYLOT_JOB_ID" carries live ecs_status (the
single-worker view does not). name= is honoured on the conversation path only.
3. Drive
pylot workers prompt <wid> --mission "$PYLOT_JOB_ID" "<text>" # 202 {turn_seq} | 409 busy|stopped
pylot workers prompt <wid> --mission "$PYLOT_JOB_ID" --wait --timeout 3600 "<text>"
pylot workers view <wid> --mission "$PYLOT_JOB_ID" # turn_state, turn_seq, last_result, last_exit_code
pylot workers output <wid> --mission "$PYLOT_JOB_ID" # result text only
pylot workers logs <wid> --mission "$PYLOT_JOB_ID" # CloudWatch tail (--mission required)
The turn is done when turn_state is idle/error/reaped and turn_seq ≥
the seq the prompt returned. Only trust last_result then. --wait polls every
5 s up to 900 s (override with --timeout) and exits non-zero on a non-idle
terminal state; --follow streams container logs to stderr and requires --wait.
A devbox that dies mid-turn is reaped with last_exit_code: -1, so the loop
cannot hang forever. Between phases, read the output before sending the next
prompt — a failed phase should not be built on.
Before invoking a remote slash command, send a plain-text discovery prompt to list
the worker's installed commands and skills and the repo's instructions — local
skills are not installed remotely. An Unknown command result is a failed turn
even when the harness reports exit code 0.
Two boot failures, both fixable without archaeology:
- Prompt queues forever,
viewshowsqueued → idle,last_exit_code: -1,session_id: null, frozenheartbeat_at, and/ecs/pylot-workershas no[worker-prompt-daemon]boot line → the worker image predates the prompt daemon.pylot deploy build-worker <org/repo>and respawn. - A turn fails
403 unknown job/unknown devbox worker→ proxy-principal regression; check the worker row'sjob_idand file it against the gateway.
If --wait / --follow / output are absent from pylot workers prompt --help,
this container's CLI predates them: poll view on a sleep loop, or use §7.
In a chat/Lambda runtime there is no budget to block on --wait — never busy-poll.
Prompt, then schedule a wake (see Async Wake Pattern) and re-check view next turn.
4. Stop (destructive) and resume
pylot workers stop <wid> --mission "$PYLOT_JOB_ID" --force
pylot workers resume <wid> --mission "$PYLOT_JOB_ID"
--force is mandatory — the CLI refuses the stop without it. Ask a human before
stopping a box someone is working in. Stop is idempotent; always stop a mission
worker when the skill finishes (harvest-on-complete is the backstop, not the plan).
Conversation-owned devboxes snapshot on stop and resume restores that snapshot
with session_id preserved; mission-worker snapshots are opt-in and OFF by default
(cost control), so treat a mission worker's stop as final unless you know the flag
is on.
Harvest the completed turn's full output before stopping: stop can replace
last_output with a truncated snapshot transcript. stopped: true does not mean
recovery is available — require snapshot_status: verified before relying on
resume. Deleting a conversation is destructive; never do it without an explicit ask.
5. Multi-box work — you are the message bus
Boxes never talk to each other. Preflight and spawn one per repo with distinct
purpose names, then relay by hand: workers view A → extract the load-bearing
evidence (request id, stack trace, payload shape) → workers prompt B with it.
Keep relayed context small and factual; never pass secrets between boxes — each
box already carries its own bundle as env. Two boxes burn budget twice as fast,
so only spawn a second when both repos genuinely need code or log access.
6. What your credential can call
The capability gate fires only on the operator JWT a mission container runs on
(PYLOT_OPERATOR_TOKEN, aliased to PYLOT_DISPATCH_TOKEN). Operators hold every
operator capability, so the gate reduces to one question: is the route mapped at
all? Mission-scoped worker routes are; unscoped ones are not.
Exception — session JWT ($PYLOT_API_TOKEN): the following conversation-scoped routes
ARE mapped at org:member tier (route-capability.mts:224-229) and ARE reachable
with a session JWT — the capability gate passes them through, and each handler enforces
its own authorization server-side:
| Route | Since | Auth boundary |
|---|---|---|
POST /conversations/:id/slack-post | #2622 | handler: org member |
POST /conversations/:id/slack-pickup | #2622 | handler: org member |
POST /conversations/:id/admin-action | #2891 | handler: org-admin check via turn-trigger |
POST /conversations/:id/admin-action/confirm | #2891 | handler: org-admin check via turn-trigger |
These are the only conversation-scoped routes that accept a session JWT. All other
conversation-scoped routes and all /admin/* routes remain 403 for session JWTs — by design.
| Call | Mission operator | Devbox worker · local pylot auth login |
|---|---|---|
workers spawn/list/view/prompt/output/stop/resume/logs with --mission | yes | yes |
the same verbs without --mission (unscoped /workers/:wid) | 403 | yes |
workers spawn/list --conversation | 403 | yes |
devboxes projects, devboxes project <org/repo> | yes | yes |
devboxes spawn/view/connect/delete/list | 403 | yes |
deploy build-worker, deploy promote | 403 | yes |
deploy status <build_id> | yes | yes |
A gate 403 reads {"error":"forbidden","reason":"capability_required","capability":"unknown"}.
Nothing outside the operator JWT is capability-gated, but handler-level org fences
still apply to org-scoped tokens. Inside a mission, always pass
--mission "$PYLOT_JOB_ID" — that one habit avoids every operator 403 above.
7. Fallback when there is no usable CLI
Only for containers whose image carries no pylot or one too old for the verb you
need. PYLOT_JOB_ID, PYLOT_API/PYLOT_GATEWAY_URL and PYLOT_DISPATCH_TOKEN are
set in both operator and mission-worker containers.
AUTH=(-H "Authorization: Bearer $PYLOT_DISPATCH_TOKEN" -H "Content-Type: application/json")
BASE="${PYLOT_API:-$PYLOT_GATEWAY_URL}/missions/${PYLOT_JOB_ID}/workers"
WID=$(curl -s --max-time 90 -X POST "${AUTH[@]}" -d "{\"repo\":\"$REPO\"}" "$BASE" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("worker_id",""))')
SEQ=$(curl -s --max-time 30 -X POST "${AUTH[@]}" -d "{\"prompt\":\"$PROMPT\"}" "$BASE/$WID/prompt" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("turn_seq",""))')
# poll GET "$BASE/$WID" until turn_state=idle AND turn_seq=$SEQ, then read last_output
curl -s --max-time 30 -X POST "${AUTH[@]}" "$BASE/$WID/stop" >/dev/null 2>&1 || true
Same routes, same semantics as §2–4. Prefer the CLI wherever it exists — one transport, one source of truth.
Supervise a run
After starting a worker or prompt, make one immediate compact status check to confirm it was accepted and is starting; queued/provisioning is not yet running. Prefer a completion event or a detached status monitor that wakes the session only on a terminal state. When scheduled checks are necessary, use adaptive 10–30 minute intervals: about 10 minutes near an expected result or a known failure, 20–30 minutes for healthy hour-long implementation or test work. An unchanged healthy check should lengthen the next wait, not trigger another prompt. Use shorter checks only for a concrete startup or failure diagnosis. Keep at most one follow-up owner per task; pause the old follow-up when another session takes over, and pause on completion or a decision that blocks safe work. An automation error or safety rejection is a reason to inspect and stop repeated retries, not to keep waking the same blocked action or change routes to evade it.
Intervene only when evidence shows the factory cannot continue safely: a materially ambiguous task contract, an unrecoverable execution condition, or a result that would violate scope or safety. Prefer resuming from durable remote state. Do not coach routine implementation, rewrite merely imperfect work, or build session-local workarounds for a recurring factory defect.
Verify the terminal result
A successful process exit is not the deliverable. Independently verify the requested outcome against the task scope:
- terminal mission state, report, and cost are coherent;
- the expected branch or PR exists with the correct target and scope;
- PR claims match the actual diff and recorded checks;
- review suggestions and any remaining risks are visible;
- for delivery tasks, review findings are resolved and authorized release work proceeds through target-environment workflow acceptance, with deployed revision and runtime evidence; a PR or worker completion is an intermediate result;
- attached evidence is accessible through the intended Pylot asset flow;
- the execution resource is stopped or otherwise left in its intended terminal state.
The coordinator owns the remaining delivery tail after worker completion. Explicit PR-only tasks can finish at a verified PR. When verification exposes a reusable defect, correct the owning skill rather than working around it here.
Filing findings — never a raw gh issue create
Every automated finding (a bug, gap or follow-up noticed during any mission, stage or review) goes through this helper, which ships with this skill on every operator:
FF="${PYLOT_WORKSPACE:-$HOME/.claude}/skills/pylot-cli/scripts/file-finding.sh"
bash "$FF" --repo <org/repo> --title "<title>" --search "<root-cause terms>" \
--body-file <evidence.md> [--severity P0|P1|P2|P3] [--incident] [--blocking] [--label <l>]...
It enforces the owner rule (2026-10-06, cut issue inflow):
- An open issue matching
--search(the root cause: symbol, file, error text or fingerprint) gets a comment instead of a new issue. - A finding that is not P0/P1, not an incident and not blocking a PR or
release goes to the repo's one rolling
Weekly findings digest — YYYY-Wwwissue (labeldigest) as a comment, once per title. A new week's digest closes the previous one. - At most 3 new issues per filer run across all repos (
FINDING_CYCLE, default the mission id). P0 and--incidentare exempt; over-cap findings go to the digest.
It prints commented N, digest N <reason> or created N; cite that number.
--dry-run writes nothing. Without the helper, follow the same three rules by
hand. Exempt: tracking issues a stage maintains by a fixed label (one per
label), and closed-issue reopens.
GitHub Auth Through the CLI
pylot auth login stores per-org credentials in ~/.pylot/credentials;
pylot auth git-token --repo <org>/<repo> mints a one-off short-lived App
installation token. The App has org-wide access, but each minted token is scoped
to the single repo you asked for — a narrow gh repo list under that token is
not an App limit; mint another token for another repo. A repo "lacking pylot
support" means its devbox config is missing (not in a team, no worker image),
never that the App cannot reach it. Never export a session-wide GH_TOKEN.
App tokens cannot read user-specific surfaces (GitHub notifications); those need
a logged-in gh identity or event-ledger routing. Secrets: never in prompts or
payloads — pylot secrets, then reference env var names.
Org Setup From a Conversation (admin-action)
Requires pylot#2978 —
pylot convo admin-actionmust be present in the worker image before workers are directed here. Verify:pylot convo admin-action --helpmust succeed in the container.
Session JWTs carry scopes [dispatch, missions:read, heartbeat] — /admin/* routes
always 403 by design. /conversations/:id/admin-action is a separate authorization
boundary (see §6 exception table) that accepts session JWTs; the endpoint enforces
org-admin authorization server-side via the turn's triggering message sender.
Primary path
# auto-detects $CONVERSATION_ID from environment
pylot convo admin-action <op> [key=value ...]
# explicit conversation id override
pylot convo admin-action <op> --conversation <id> [key=value ...]
# confirm a staged (confirm-tier) operation after org admin replies
pylot convo admin-confirm <code>
# reject a pending confirm-tier operation
pylot convo admin-confirm <code> --reject
Operations
Op list is closed — any value not in this table returns 400. Never pass credentials or API keys in op args — the gateway returns 400 and nothing is stored (hard boundary: credential-looking args are explicitly rejected).
| op | tier | effect |
|---|---|---|
setup-status | read (any conversation member) | onboarding state: App install, teams, repo bindings + worker-image presence, goals, operators/skills, provider chain, budget |
team-create | immediate (org admin requester) | create a team in this org |
team-rename | immediate (org admin requester) | rename a team |
repos-add | immediate (org admin requester) | bind a repo to a team; response includes worker-image presence |
goals-set | immediate (org admin requester) | set team goals (the store auto-pylot stage 00 reads) |
operator-add | immediate (org admin requester) | create an operator on a team |
skill-assign | immediate (org admin requester) | assign a skill to an operator |
instructions-set | immediate (org admin requester) | set operator instructions |
org-update | confirm | org settings (Slack default team, max_concurrent, …) |
budget-set | confirm (spend) | team daily budget cap |
provider-assign | confirm (model/spend routing) | attach an existing platform provider chain to org/team |
Confirm-tier flow
Confirm-tier ops (org-update, budget-set, provider-assign) are staged first,
then approved by an org admin's reply — the approver's identity is proven by their
own conversation turn, not supplied by the worker.
pylot convo admin-action <confirm-op> [args]→ gateway returns202 {code, summary}- Worker relays to the user: "An org admin must reply
confirm <CODE>within 10 minutes." (summarydescribes exactly what will change — relay it verbatim.) - The org admin sends a reply; the approval turn's own message sender proves their identity.
pylot convo admin-confirm <CODE>(called in the approval turn) →200, executed.- Rejected or expired:
pylot convo admin-confirm <CODE> --rejectkills the pending action; an expired code (>10 min) returns410and never executes.
Errors
| error | meaning | user-facing guidance |
|---|---|---|
no_human_trigger | Turn has no human sender (scheduled wake-up, automation) | Retry from a conversation turn triggered by a human message |
unlinked_slack | Sender's Slack account is not linked to a GitHub account | Ask the user to link their GitHub account in the Slack app settings |
not_org_admin | Linked GitHub account is not an admin of this org | Immediate ops need an org admin in the thread; confirm-tier: anyone can stage, an org admin must confirm |
terminal_only | Op would require a credential or secret value | Use the terminal CLI — credentials cannot transit the conversation |
code_expired | Confirm code is older than 10 minutes | Re-run the original op to get a fresh code |
Transition-window curl fallback (pre-#2978 images only)
Use this only if the container image predates pylot convo admin-action.
$CONVERSATION_ID is set in chat-worker containers. Use $PYLOT_API_TOKEN
(session JWT) — NOT $PYLOT_DISPATCH_TOKEN (operator token 403s this endpoint
by design; using it here is the exact mis-behavior pylot#2979 corrects).
# immediate op
curl -s -X POST \
-H "Authorization: Bearer $PYLOT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"op\":\"$OP\"}" \
"${PYLOT_API:-$PYLOT_GATEWAY_URL}/conversations/$CONVERSATION_ID/admin-action"
# confirm a staged op (after org admin replies with the code)
curl -s -X POST \
-H "Authorization: Bearer $PYLOT_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"code\":\"$CODE\"}" \
"${PYLOT_API:-$PYLOT_GATEWAY_URL}/conversations/$CONVERSATION_ID/admin-action/confirm"
Automations
pylot automations list # inventory with run stats
pylot automations get <name> # single rule detail
Per-repo coverage: filter only_repos/skip_repos from list output.
Org-specific admin runbooks that automations dispatch live in that org's repo
docs and team playbook (pylot context <org/repo>), not in this skill.
Slack Channel Routing
Bind Slack channels to teams for message routing. Many channels can bind to the same team (many-to-one). CLI is the primary interface — no UI equivalent.
# Bind a channel to a team (additive; channel must already be known to the bot)
pylot teams channels bind <channel-id> --team <team> [--org <org>]
# List all Slack channels with their bound team (null = unbound)
pylot teams channels list [--org <org>]
# Set the org-level fallback team for unbound channels
pylot orgs set-slack-default <team> --org <org>
# Clear the org-level fallback team
pylot orgs clear-slack-default --org <org>
Default-team fallback: when a Slack message arrives on a channel with no explicit binding, it is routed to the org-level default team (if set). Unbound channels with no org default are dropped.
Async Wake Pattern
Dispatch with --context conversation_id=… → auto-wake on mission terminal + PR lifecycle.
Self-wake fallback: pylot conversations wakes-add <conv-id> in_seconds=300 content="check X"
One wake at a time; re-schedule rather than stack.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/fellowship-dev/dogfooded-skills/pylot-cli">View pylot-cli on skillZs</a>