skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
fellowship-dev/dogfooded-skills109 installs

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-cli
view source ↗

Is 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:

LifetimeWhat it's forHow
Ephemeral scratchA draft you're still iterating on in this turnnothing — stays in-turn
Resumable checkpointA private draft/report that must survive a turn endingpresign --conversation <id> → PUT → finalize (finalize takes only --sha256/--size, no scope flag)
Published artifactEvidence 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-runner needs improve-code-quality and test-in-staging in 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 runner is 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, view shows queued → idle, last_exit_code: -1, session_id: null, frozen heartbeat_at, and /ecs/pylot-workers has 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's job_id and 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:

RouteSinceAuth boundary
POST /conversations/:id/slack-post#2622handler: org member
POST /conversations/:id/slack-pickup#2622handler: org member
POST /conversations/:id/admin-action#2891handler: org-admin check via turn-trigger
POST /conversations/:id/admin-action/confirm#2891handler: 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.

CallMission operatorDevbox worker · local pylot auth login
workers spawn/list/view/prompt/output/stop/resume/logs with --missionyesyes
the same verbs without --mission (unscoped /workers/:wid)403yes
workers spawn/list --conversation403yes
devboxes projects, devboxes project <org/repo>yesyes
devboxes spawn/view/connect/delete/list403yes
deploy build-worker, deploy promote403yes
deploy status <build_id>yesyes

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):

  1. An open issue matching --search (the root cause: symbol, file, error text or fingerprint) gets a comment instead of a new issue.
  2. 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-Www issue (label digest) as a comment, once per title. A new week's digest closes the previous one.
  3. At most 3 new issues per filer run across all repos (FINDING_CYCLE, default the mission id). P0 and --incident are 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-action must be present in the worker image before workers are directed here. Verify: pylot convo admin-action --help must 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).

optiereffect
setup-statusread (any conversation member)onboarding state: App install, teams, repo bindings + worker-image presence, goals, operators/skills, provider chain, budget
team-createimmediate (org admin requester)create a team in this org
team-renameimmediate (org admin requester)rename a team
repos-addimmediate (org admin requester)bind a repo to a team; response includes worker-image presence
goals-setimmediate (org admin requester)set team goals (the store auto-pylot stage 00 reads)
operator-addimmediate (org admin requester)create an operator on a team
skill-assignimmediate (org admin requester)assign a skill to an operator
instructions-setimmediate (org admin requester)set operator instructions
org-updateconfirmorg settings (Slack default team, max_concurrent, …)
budget-setconfirm (spend)team daily budget cap
provider-assignconfirm (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.

  1. pylot convo admin-action <confirm-op> [args] → gateway returns 202 {code, summary}
  2. Worker relays to the user: "An org admin must reply confirm <CODE> within 10 minutes." (summary describes exactly what will change — relay it verbatim.)
  3. The org admin sends a reply; the approval turn's own message sender proves their identity.
  4. pylot convo admin-confirm <CODE> (called in the approval turn) → 200, executed.
  5. Rejected or expired: pylot convo admin-confirm <CODE> --reject kills the pending action; an expired code (>10 min) returns 410 and never executes.

Errors

errormeaninguser-facing guidance
no_human_triggerTurn has no human sender (scheduled wake-up, automation)Retry from a conversation turn triggered by a human message
unlinked_slackSender's Slack account is not linked to a GitHub accountAsk the user to link their GitHub account in the Slack app settings
not_org_adminLinked GitHub account is not an admin of this orgImmediate ops need an org admin in the thread; confirm-tier: anyone can stage, an org admin must confirm
terminal_onlyOp would require a credential or secret valueUse the terminal CLI — credentials cannot transit the conversation
code_expiredConfirm code is older than 10 minutesRe-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.

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>