Commands
New to the CLI? Start with Get started with Pidgen. This page is the reference for commands, flags, input files, and their public API routes.
pidgen <noun> <verb> [arguments] [flags], in the style of gh. Every command below is available
unless it is marked planned. auth status and account list ask the server who you are
(GET /me) when the credential in use is a CLI token; auth logout and account use work on the
local configuration only, and their server-backed parts are marked planned below.
Terminal stdout defaults to human-readable output; nonterminal stdout defaults to the JSON envelope.
--format json and --format table override this, including errors. JSON fields and exit codes stay
unchanged. Help/version print text; foreground pidgen worker start inherits the worker's streams
and status. See Output and exit codes. Commands with side effects follow
Preview and apply.
Resource reads in human output identify collections and displayed counts, label detail sections,
and retain multiline content. Tables adapt to terminal width with complete actionable values.
Use --format json for every field in the response; see
resource read output.
Mutation responses label unapplied previews, settled applications, pending operations and timed-out
waits, retaining requirements, created keys and follow-up details. Direct writes retain their
existing mutation policy. See human-readable mutation results
for presentation and application examples; select --format json for structured results.
Global flags
Section titled “Global flags”| Flag | Meaning |
|---|---|
--profile <name> |
Use a named profile instead of the default (Authentication) |
--account <ACC-…> |
Use the stored credential for another Account for this command |
--format json|table |
Output format; automatic default is table for terminal stdout, json otherwise |
--timeout <seconds> |
Network, --wait, and worker stop timeout |
-h, --help |
Help for any command |
-V, --version |
CLI version and supported API version range |
Input files
Section titled “Input files”-f <file> takes a request body as JSON or YAML: .json files are JSON, .yaml and .yml files
are YAML, and anything else, including standard input (-f -), is JSON when it starts with { or
[ and YAML otherwise. YAML uses the 1.2 core schema, so a YAML file and a JSON file with the same
content give the same body. Duplicate keys are refused.
Before sending, the CLI validates the body against the operation's request schema in the committed
public API document, resolving local $refs. Any failure exits 2 with INPUT_INVALID:
- a syntax error has
error.details{source, format, line, column, reason}, and the message names the line and column; - a body that does not match the schema has
error.details.issues, one{path, message}per problem, wherepathlocates it in the body ($is the whole body, e.g.$.acceptance_criteria[1].statement).
The CLI sends the body as written; it does not add defaults or drop fields the schema does not name.
The three additions are pidgen ticket edit's, which sends the ticket's current version with the
file's fields, pidgen ticket clarify's, which sends the open clarification's gate_generation
when the file names none, and pidgen request submit's, which sends a generated submission_key
when the file and flags name none.
Command tree
Section titled “Command tree”auth login | logout | status | list | revokeaccount list | useproject list | showdomain list | show | create | edit | archive | unarchive | epics · project add|rm · parent add|rm member add|rm · maintainer add|rmcontract list | show | create | publish | propose | revise | acceptticket list | show | create | edit | cancel | actions | act | retry | replay | reopen | clarify template | types | owner | follow | unfollow | instructions | history · dep add|rm|list · move --epic contract implements|builds-against|rmintent submit | respond | list | show | withdrawrequest submit | list | show | reply | acknowledge | accept | close | promotesession steer | instructions | edit-instruction | withdraw-instructionepic list | show | create | edit · dag show · edge add|rm (aliases of ticket commands)plan show | apply | promote | template | cohortsmanifest template | applyreview list | attentionrun list | show | node-outputworker install | verify | update | uninstall | enroll | start | stop | status | doctor | subscriptions · subscriptions useskills install | statusfeedback (one command: pidgen feedback --kind … <message>)setup (one command: pidgen setup; the one interactive command)doctor (one command: pidgen doctor)api get | post | put | patch | deleteRoutes below are paths on the public API under /api/v1. {ref} is a ticket key such as
PIDGEN-12; the Account comes from the credential.
| Command | Does | Route |
|---|---|---|
pidgen auth login [<url>] [--device-label <name>] |
Device-code login for one Account; verifies the CLI token and stores it in the profile | GET /health, POST /auth/device, POST /auth/device/token, GET /me |
pidgen auth login [<url>] --with-token --account <ACC-…> |
Verifies a Personal API Token read from standard input and stores it as the profile's credential for that Account | GET /health, GET /projects |
pidgen auth logout [--account <ACC-…>] |
Forgets the profile's credential for the current or named Account. Planned: revoking it on the server | — (planned: DELETE /me/sessions/{session_id}) |
pidgen auth status [--check] |
Who you are (the User, the Account and your role, with a CLI token), the profile, server, Account, credential kind and expiry, and the cached server check; --check re-checks the server |
GET /me with a CLI token, GET /health with --check |
pidgen auth list |
Your browser sessions and CLI tokens, marking the credential in use. Refuses a Personal API Token | GET /me/sessions |
pidgen auth revoke <id> |
Revokes one session or CLI token; revoking the credential in use also removes it from the profile. Refuses a Personal API Token | DELETE /me/sessions/{session_id} |
pidgen login is an alias for pidgen auth login (planned).
GET /me, GET /me/sessions and DELETE /me/sessions/{session_id} are in the committed public API
document. auth login, auth status and account list use GET /me; auth list uses
GET /me/sessions and auth revoke uses DELETE /me/sessions/{session_id}. These routes are not Account-scoped: they ignore the selected Account and act for the
signed-in User. GET /me answers the User, every Account they are an active member of, and — for a
CLI token — that token's token_id and Account (cli_token, null otherwise). GET /me/sessions
lists browser sessions and CLI tokens (kind session or cli_token, current marking the
credential in use) and never a token secret. DELETE /me/sessions/{session_id} revokes one at once;
a CLI token may revoke itself to sign out. A Personal API Token is refused on all three with
TOKEN_NOT_ALLOWED (exit code 3).
auth login
Section titled “auth login”$ pidgen auth login https://pidgen.example.com [--device-label laptop]Open https://pidgen.example.com/device and enter the code WDJB-MJHTWaiting for approval…<url>is the server to sign in to. Without it, the login usesPIDGEN_API_URL, else the profile's server, else the hosted server,https://api.pidgen.ai.- Before anything else, checks
GET /healthwithout a credential, as--with-tokendoes:NOT_PIDGEN(exit code 1) orAPI_VERSION_UNSUPPORTED(exit code 7), and nothing is written. - Then starts a login with
POST /auth/device(sending--device-label, 1–200 characters, when given) and writes the verification address and user code to standard error. The code is written there even when standard error is not a terminal, because a person has to read it; it is never on standard output.Waiting for approval…is written only on a terminal. The device code is never shown. - Polls
POST /auth/device/tokeneveryintervalseconds from the server.authorization_pendingkeeps polling;slow_downgrows the interval to the server'serror.details.interval, or by five seconds when it gives none. access_denied(the login was denied in the web app),expired_token(the code expired, including whenexpires_inpasses without an answer) andinvalid_grantfail with thaterror.codeand exit code 3. Nothing is written.- On approval, verifies the new CLI token before storing it:
GET /mewith the token (and no Account, since/meis not Account-scoped) must answer200. Any refusal fails the login with the server'serror.code—TOKEN_INVALID(exit code 3) for a token the server does not accept — and a message saying nothing was stored;error.details.storedisfalse. When the answer'scli_token.account_idnames a different Account from the one approved, the login fails withACCOUNT_MISMATCH(exit code 3). Nothing is written in either case. - Then stores the CLI token as a
clicredential for the Account chosen in the web app, with itstoken_idandexpires_at(the sooner of its idle and absolute expiry), in the profile (--profile,PIDGEN_PROFILE, or the default profile), as--with-tokendoes: the file is written with mode0600, the Account becomes the profile's default if it had none, and credentials for a different server are dropped. --accountis a usage error (CLI_USAGE, exit code 2): you choose the Account when you approve.data:profile,url,account,credential_kind("cli"),token_id,device_label,expires_at,absolute_expires_at,verified(true), andserver(serviceandapi_version). The token is never printed.
auth login --with-token
Section titled “auth login --with-token”$ pidgen auth login https://pidgen.example.com --with-token --account ACC-EXAMPLE < token.txt-
Reads the token from standard input and trims surrounding whitespace.
--accountis required, because a token is bound to one Account. A missing--account, an empty token, or a URL that is nothttp(s)is a usage error (CLI_USAGE, exit code 2). -
Before storing anything, checks
GET /healthwithout sending the token: a server that is not Pidgen is refused withNOT_PIDGEN(exit code 1), and one whose API version range does not include the CLI's withAPI_VERSION_UNSUPPORTED(exit code 7). Nothing is written in either case. -
Then verifies the token before storing it. A Personal API Token is refused on
/me*by design, so the CLI callsGET /projectswith the token andX-PIDGEN-Accountset to--account:200— the token is valid for the Account;403TOKEN_SCOPE_NOT_GRANTED— the token is valid but lackstickets:read; it is still stored, anddata.scope_limitationreports it;401TOKEN_INVALID(the token is wrong, revoked or expired) or403ACCOUNT_MISMATCH(it is bound to another Account) — the login is refused with thaterror.codeand exit code 3;- any other refusal or failure keeps the server's
error.codeand its exit code (Output and exit codes).
A refused login writes nothing: the message says so and
error.details.storedisfalse. -
Then stores the token as a
patcredential for the Account in the profile (--profile,PIDGEN_PROFILE, or the default profile), sets the profile's URL and server check, and makes the Account the profile's default if it had none. The file is written with mode0600. If the profile pointed at a different server, its credentials for that server are dropped, so they are never sent to the new one. -
data:profile,url,account,credential_kind("pat"),verified(true),scope_limitation, andserver(serviceandapi_version).scope_limitationisnullwhenGET /projectsanswered200, and otherwise{code, missing_scope, message}:codeisTOKEN_SCOPE_NOT_GRANTED, andmissing_scopeis the scope the server names in its error details (missing_scopeorrequired_scope), ortickets:readwhen it names none. Other scopes are not checked. The token is never printed.
auth status
Section titled “auth status”$ pidgen auth status [--check]- Reads the selected profile (
--profile,PIDGEN_PROFILE, or the default profile) and Account (--account,PIDGEN_ACCOUNT, or the profile's default), honouringPIDGEN_API_URLandPIDGEN_TOKEN. - With a CLI token (a stored
clicredential, or aPIDGEN_TOKENstartingpidgen_cli_orawes_cli_), asks the server who you are:GET /mewith the token and no Account, since/meis not Account-scoped. A refusal keeps the server'serror.codeand exit code —TOKEN_INVALID(exit code 3) for a revoked or expired token — and an answer that does not match the public API document isUNEXPECTED_RESPONSE(exit code 1).meta.request_idandmeta.api_versionare the answer's. - With a Personal API Token, which the server refuses on
/meby design, or anPIDGEN_TOKENof unrecognised kind, it reports the local profile only and makes no network call unless--checkis given;data.identity_notesays why. --checkfirst re-runs the server check (GET /health, without the credential), asauth logindoes:NOT_PIDGEN(exit code 1) orAPI_VERSION_UNSUPPORTED(exit code 7) on failure. The result is shown but not stored.- No credential for the Account:
AUTH_REQUIRED(exit code 3), witherror.details.hintnaming the login command. It makes no network call. data:profile,url,account,default_account,credential_kind("pat","cli", ornullfor an unrecognisedPIDGEN_TOKEN),credential_source("config"or"environment"),expires_at(nullwhen unknown),server(service,api_version,checked_at; the check stored at login, the fresh one with--check, ornull),identityandidentity_note. The token is never printed.identity, fromGET /mewith a CLI token:user(user_id,email,display_name),account(the Account the token is bound to,cli_token.account_id, or the selected Account when the answer names none),account_nameandrole(owner,administratorormember) from your membership of that Account (nullwhenGET /medoes not list it), andtoken_id.nullwhenGET /mewas not called.identity_note:nullwith a CLI token; otherwise why only the local profile is shown — for a Personal API Token, that the server does not describe Personal API Tokens onGET /me.
auth logout
Section titled “auth logout”$ pidgen auth logout [--account ACC-EXAMPLE]- Removes the stored credential for the Account named with
--account, or else the current Account (PIDGEN_ACCOUNT, then the profile's default), from the selected profile. If the profile's default Account was that Account, the default is cleared. No network call is made. - No credential stored for that Account:
CREDENTIAL_NOT_FOUND(exit code 4); nothing is written. data:profile,account,removed(true),revoked(false: the token stays valid on the server; revoke a Personal API Token in the web app), anddefault_account(after the change, ornull).- Planned: revoking a CLI token on the server (
DELETE /me/sessions/{session_id}, published). Until then,pidgen auth revoke <id>revokes it and removes it from the profile.
auth list
Section titled “auth list”$ pidgen auth list- Lists your live browser sessions and CLI tokens with
GET /me/sessions, sending the credential in use (PIDGEN_TOKEN, or the stored credential for--account,PIDGEN_ACCOUNTor the profile's default) and no Account, since the route is not Account-scoped: it lists your tokens for every Account. The order is the server's: sessions first, each most recently used first. - A Personal API Token (a stored
patcredential, or aPIDGEN_TOKENstartingpidgen_pat_orawes_pat_) is refused before any network call withTOKEN_NOT_ALLOWED(exit code 3);error.details.hintsays Personal API Tokens are listed and revoked in the web app. A token of unrecognised kind is sent, and the server'sTOKEN_NOT_ALLOWEDgains the same hint. - No credential:
AUTH_REQUIRED(exit code 3), with no network call. Other refusals keep the server'serror.codeand exit code —TOKEN_INVALID(exit code 3) for a revoked or expired token — and an answer that does not match the public API document isUNEXPECTED_RESPONSE(exit code 1). datais an array, one entry per session or token:id(whatpidgen auth revoketakes),kind(sessionorcli_token),account(the Account a CLI token is bound to;nullfor a session),device_label(the browser and platform, or the name given at login; may benull),created_at,last_used_at,expires_at(a CLI token's;nullfor a session),current(truefor the credential making the request),mfa_verified, andstored_account(the Account this profile stores that CLI token for, matched bytoken_id, ornull). No token secret is listed.
auth revoke
Section titled “auth revoke”$ pidgen auth revoke tok-example- Revokes one of your browser sessions or CLI tokens by the
idpidgen auth listshows, withDELETE /me/sessions/{session_id}(the id percent-encoded as one path segment, no Account sent). It stops working at once. The server has no preview for revocation, so the command acts directly (Preview and apply). - When the server answers that the revoked credential was the one in use (
current), every credential in the selected profile holding that token is removed, and so is any stored CLI token whosetoken_idis<id>, even when it was not in use; the profile's default Account is cleared if it pointed at a removed one. Nothing else in the profile changes, and a refusal writes nothing. - A Personal API Token is refused before any network call with
TOKEN_NOT_ALLOWED(exit code 3), as forauth list, with the same hint. A missing, blank,.or..id is a usage error (CLI_USAGE, exit code 2).SESSION_NOT_FOUND(exit code 4) means none of your sessions or tokens has that id. Other refusals keep the server's code and exit code. data:id,revoked(true),current(whether it was the credential in use),profile,removed_from_profile(the Accounts whose stored credential was removed, sorted; empty when none) anddefault_account(after the change, ornull).
account
Section titled “account”| Command | Does | Route |
|---|---|---|
pidgen account list |
With a CLI token, every Account you belong to merged with the Accounts that have a stored credential in the profile; otherwise the stored Accounts only. Marks the default | GET /me with a CLI token |
pidgen account use <ACC-…> |
Makes an Account with a stored credential the profile default. Planned: starting a login if none is stored | — |
account use reads and writes the local configuration only and makes no network call.
-
pidgen account list— when the credential in use (PIDGEN_TOKEN, or the stored credential for--account,PIDGEN_ACCOUNTor the profile's default) is a CLI token and a server is configured, callsGET /mewith it (no Account is sent) and merges the Accounts you are an active member of with the Accounts the selected profile holds a credential for. Without a CLI token — a Personal API Token, which/merefuses by design, or no credential — it lists the stored Accounts only and makes no network call. A refusal fromGET /mekeeps the server'serror.codeand exit code, as forauth status.datais an array, one entry per Account, sorted by Account id:{account, account_name, role, member, default, credential_stored, credential_kind, expires_at}.account_nameandrolecome fromGET /me(nullwhen it does not list the Account or was not called);memberistruewhenGET /melists the Account,falsewhen it does not (a stored credential for an Account you no longer belong to), andnullwhen the server was not asked;defaultistruefor the profile's default Account;credential_storedsays whether the profile holds a credential for it, whosecredential_kindandexpires_atfollow (nullotherwise). A profile with no credentials and no CLI token gives an empty array. -
pidgen account use <ACC-…>— sets the selected profile's default Account.data:profile,account,previous_default,credential_kind. When no credential is stored for the Account it fails withAUTH_REQUIRED(exit code 3), writes nothing, anderror.details.hintnamespidgen auth login <url> --with-token --account <ACC-…>. A missing Account id isCLI_USAGE(exit code 2).
project
Section titled “project”| Command | Does | Route |
|---|---|---|
pidgen project list |
Projects you can see in the Account | GET /projects |
pidgen project show <key> |
One project, including its merge policy | GET /projects/{project_ref} |
Both are read commands: one GET with the selected profile's credential and Account. data is the
server's data unchanged, and meta carries the server's request_id and api_version with the
profile and Account. A server refusal keeps its own error.code and exits with the code for its
status (Output and exit codes).
pidgen project list—datais an array of projects, each with itsid(proj-…),name,prefix(the ticket-key namespace),merge_policy, delivery settings and ticketrollup. An Account with no projects gives an empty array.pidgen project show <key>—<key>is the project's id or its ticket-key prefix (e.g.Pidgen);datais that one project, with the same fields, including itsmerge_policy. An unknown project, or another Account's, exits 4 with the server'sPROJECT_NOT_FOUND. A missing, empty,.or..key, or more than one key, isCLI_USAGE(exit code 2) and sends nothing.
domain
Section titled “domain”A domain is a named group of projects in the Account. Domains nest: each has one or more parent domains, up to 8 levels deep, under the Account domain, which contains every other. Domain Members read everything in the projects the domain covers; Maintainers manage the domain, and a Maintainer is also a member. The routes are in the published document (API compatibility).
| Command | Does | Route |
|---|---|---|
pidgen domain list |
Domains you can read, the Account domain first | GET /domains |
pidgen domain show <domain> |
One domain | GET /domains/{domain_id} |
pidgen domain create --name <name> --key <KEY> [--parent <domain>]... |
Create a domain | POST /domains |
pidgen domain edit <domain> [--name <name>] [--key <KEY>] |
Rename or rekey a domain | PATCH /domains/{domain_id} |
pidgen domain archive <domain> |
Archive a domain | PATCH /domains/{domain_id} {archived: true} |
pidgen domain unarchive <domain> |
Unarchive a domain | PATCH /domains/{domain_id} {archived: false} |
pidgen domain project add <domain> <project> |
The domain contains the project | PUT /domains/{domain_id}/projects/{project_ref} |
pidgen domain project rm <domain> <project> |
The domain stops containing the project | DELETE /domains/{domain_id}/projects/{project_ref} |
pidgen domain parent add <domain> <parent-domain> |
Put the domain under another parent | PUT /domains/{domain_id}/parents/{parent_id} |
pidgen domain parent rm <domain> <parent-domain> |
Remove one of its parents | DELETE /domains/{domain_id}/parents/{parent_id} |
pidgen domain member add <domain> <user-id> |
Make the person a Domain Member | PUT /domains/{domain_id}/people/{user_id} {role: member} |
pidgen domain member rm <domain> <user-id> |
Remove the person from the domain | DELETE /domains/{domain_id}/people/{user_id} |
pidgen domain maintainer add <domain> <user-id> |
Make the person a Maintainer | PUT /domains/{domain_id}/people/{user_id} {role: maintainer} |
pidgen domain maintainer rm <domain> <user-id> |
The person stops maintaining and stays a member | PUT /domains/{domain_id}/people/{user_id} {role: member} |
pidgen domain epics <domain> [--state open|finished] [--page <n>] [--per-page <n>] [--no-subdomains] |
The domain's Epics, their work by project, and what you cannot see | GET /domains/{domain_id}/epics |
<domain> and <parent-domain> are a domain's key, such as PAY, or its id;
<project> is a project's id or ticket-key prefix; <user-id> is the person's user id (a UUID),
and they must be an active member of the Account. Each is sent percent-encoded as one path segment. Every
command sends one request with the selected profile's credential and Account; the writes act
directly, with no preview (Preview and apply). Reads need tickets:read,
writes tickets:write.
data is the server's domain (for list, the array of them) unchanged: its id, key, name,
is_account_root, archived and archived_at, the ids of its parents and of the projects and
domains it contains, its people counts under people (members, maintainers) and its Epic
counts under epics (open, total). show and every write answer the domain as it now stands,
with granted, the people granted directly on it (user_id, role). A request body is checked
against the published document before it is sent, and an answer that does not match it exits 1 with
UNEXPECTED_RESPONSE, error.details.route naming the route and error.details.issues what is
wrong.
pidgen domain create— sends{name, key}, withparent_idswhen--parentis given (repeat it for more parents); without it the server puts the domain under the Account domain. The key is 3-6 uppercase letters or digits; another key is the server'sDOMAIN_KEY_INVALID(422, exit 2). A key that is already a project prefix or domain key of the Account is the server'sDOMAIN_KEY_TAKEN(409, exit 5).pidgen domain edit— sends only the fields given; an edit with neither--namenor--keyisCLI_USAGE(exit 2) and sends nothing.pidgen domain maintainer rmkeeps the person as a Domain Member;pidgen domain member rmremoves them, Maintainer or not.
The server's refusals keep their codes and error.details:
| Code | When | Exit |
|---|---|---|
DOMAIN_NOT_FOUND |
No such domain in the Account (another Account's included) | 4 |
MEMBER_NOT_FOUND |
The person is not an active member of the Account | 4 |
DOMAIN_CYCLE |
The parent is already inside the domain | 5 |
DOMAIN_TOO_DEEP |
Nesting would pass 8 levels | 5 |
DOMAIN_IS_ACCOUNT_ROOT |
A parent link naming the Account domain either way, archiving it, or rekeying it once it holds an Epic | 5 |
DOMAIN_COVERAGE_IN_USE |
Removing a project or parent while open work under the domain's Epics sits in a project leaving its coverage; error.details.ticket_keys names the work you can see and hidden_count counts the rest |
5 |
DOMAIN_HAS_OPEN_EPICS |
Archiving a domain with an open Epic; error.details.ticket_keys names them |
5 |
DOMAIN_KEY_TAKEN (409) |
The key is already a project prefix or domain key of the Account | 5 |
DOMAIN_KEY_INVALID, DOMAIN_KEY_FIXED (422) |
The key is not 3-6 uppercase letters or digits, or the domain's home already holds a Ticket | 2 |
ENTITLEMENT_LIMIT_REACHED or FEATURE_NOT_IN_PLAN (403) |
The Account's domains feature is switched off; writes are refused, reads still answer | 3 |
The five graph refusals exit 5 whether the server answers them 409 or 422. A missing <domain>,
<project>, <parent-domain> or <user-id>, an empty, . or .. one, an extra argument, a
create without --name or --key, or an empty --name, --key or --parent, is CLI_USAGE
(exit 2) and sends nothing.
Domain Epics
Section titled “Domain Epics”A Domain Epic is an Epic filed in a domain rather than in one project: it holds work from
several of the domain's projects. It is a planning and tracking parent, not a workspace: it owns no
branch, and each project's work under it delivers to its own project as usual. The server files it
in the domain's home project, so its key is the domain's key and a number (COM-12), its
branch_mode is project_base, and it takes the Account's Unassigned Agent and the
epic-delivery workflow type. A Domain Epic may name a Domain Epic of an ancestor domain as its
parent_ticket_id.
- File one with
pidgen ticket create --kind epic --domain <domain> -f <file>orpidgen epic create --domain <domain> -f <file>(see ticket). - Work joins a Domain Epic as it joins any Epic: name the Domain Epic as the ticket's
parent_ticket_id, and a manifest may name one as itsepic_id. Work in a project outside the domain's coverage is refused with the server'sPARENT_OUTSIDE_DOMAIN. Nothing in the CLI changes for this. pidgen domain epics <domain>reads a domain's Epics (GET /domains/{domain_id}/epics).--state,--page,--per-pageand--no-subdomainsare sent asstate,page,per_pageandsubdomains=false; a flag not given is not sent, so the route's defaults apply: open Epics, the subdomains' Epics included, 10 finished Epics a page.datais the server's:epics, each with itsepic, thedomain_idit belongs to, its work grouped by project inprojects(each{project: {id, key, name}, ticket_ids}) andhidden, how many of its tickets you cannot see;tickets, every ticket the groups name; andrunning. An answer that does not match the publishedDomainEpicBoardexits 1 withUNEXPECTED_RESPONSE. An unknown--state, a--pagethat is not a whole number from 1, a--per-pageoutside 1 to 50 or a missing<domain>isCLI_USAGE(exit 2) and sends nothing; an unknown domain isDOMAIN_NOT_FOUND(exit 4). Needstickets:read.
contract
Section titled “contract”A contract is a project's or a domain's public interface: a REST, GraphQL, gRPC, events or
library interface, or another kind. It has a provider (a project or a domain), a title, an owner
and a status (draft, accepted, deprecated, retired), and is keyed per Account as CT-<n>.
Each revision has a number, a kind, Markdown, an optional native schema and its format, a
change (additive or breaking), its operations, and how it was accepted. Every contract and every
revision is readable by everyone in the Account; work in projects you cannot open appears only as a
count. An accepted revision never changes.
A contract enters Pidgen as a file: the provider publishes its spec file with
pidgen contract publish (by hand, or from its CI on every merge to its default branch), and an
addition another project needs is proposed from a fragment file with pidgen contract propose.
The CLI moves the files; it never reads a schema into a command. A contract can also be created
with pidgen contract create and changed by revising it. A published revision describes code
that exists; each operation of a contract is landed (in the newest published revision),
pending (proposed and accepted, not yet published) or removed (in an earlier published
revision and not in the newest). A new contract or
revision is accepted at once, unless the Account's Contract approval setting asks for a person
(before breaking revisions, or before any); then it waits, and work linked to the contract does not
start until someone accepts it. The routes are in the published document
(API compatibility).
| Command | Does | Route |
|---|---|---|
pidgen contract list --project <project> |
A project's contracts, then those its work builds against | GET /projects/{project_ref}/contracts |
pidgen contract list --domain <domain> |
A domain's contracts, its coverage's by provider, then those its work builds against | GET /domains/{domain_id}/contracts |
pidgen contract show <contract> |
One contract, its revisions, each operation's state and the work linked to it | GET /contracts/{contract_id} |
pidgen contract create (--project <project> | --domain <domain>) [-f <file>] [--title <title>] [--kind <kind>] [--schema-format <format>] [--schema <file>] [--markdown <file.md>] [--owner <user id>] [--existing] |
Create a contract a Project or a Domain provides, with its first revision | POST /projects/{project_ref}/contracts or POST /domains/{domain_id}/contracts |
pidgen contract publish <file> (--project <project> | --domain <domain>) [--path <source path>] [--commit <sha>] [--ref <ref>] [--include <glob>...] [--breaking] [--check] |
Publish a spec file, with the files beside it, as the contract at its source path: created the first time, revised after | POST /projects/{project_ref}/contracts/publish or POST /domains/{domain_id}/contracts/publish |
pidgen contract propose <contract> <fragment> [--include <glob>...] [--check] |
Propose a revision from a fragment file | POST /contracts/{contract_id}/proposals |
pidgen contract propose --project <project> --title <title> <fragment> [--include <glob>...] [--check] |
Propose a contract the project is to provide, before it exists | POST /projects/{project_ref}/contracts/proposals |
pidgen contract revise <contract> -f <file> [--markdown <file.md>] |
Add a revision | POST /contracts/{contract_id}/revisions |
pidgen contract accept <contract> [--revision <n>] |
Accept a waiting revision | POST /contracts/{contract_id}/revisions/{revision_number}/accept |
<contract> is a contract's key, such as CT-7, or its id; <project> is a project's key prefix
or id and <domain> a domain's key or id. Each is sent as one percent-encoded path segment. data
is the server's answer unchanged, and every answer is validated against the published document:
one that does not match exits 1 with UNEXPECTED_RESPONSE. The writes act directly, with
no preview (Preview and apply).
pidgen contract list— give exactly one of--projectand--domain; both, neither or an empty one isCLI_USAGE(exit 2) and sends nothing. For a projectdatais{provides, builds_against}: its own contracts and those its work builds against from outside it. For a domain it is{own, coverage, builds_against}: the contracts the domain itself provides, its covered projects' contracts grouped byprovider, and those its work builds against. Each entry is the contract without revisions: itsid,key,number,title,status,provider(project_id,kindofprojectordomain,domain_id— null for a project's —keyandname),owner_user_id,kind,implemented,newest_accepted_revision,waiting_revisionsand its replacement, if deprecated.pidgen contract show—datais the contract with itsrevisions, oldest first, each withnumber,kind,change,schema_format,operations(each{name, types?}),markdown,native_schemaandacceptance:waiting,automatic, orpersonwithaccepted_by(a user id) andaccepted_at. Itsimplemented_byandbuilt_against_byeach list the Tickets you can see inwork({ticket_id, ticket_key, title, stage, project_id, operations}) and count the rest inhidden_count.implementedis true once the implementing work completes;may_reviseandmay_acceptsay what you may do.operation_stateslists each operation bynamewith itsstate:landed(in the newest published revision, in its order), thenpending(in an accepted proposed revision, not yet published), thenremoved(in an earlier published revision and not the newest). A state outside those three exits 1 withUNEXPECTED_RESPONSE.pidgen contract create— give exactly one of--project(the provider Project) and--domain(the provider Domain's id; the provider is the Domain's home); both, neither or an empty one isCLI_USAGE(exit 2). The body is-f's file (JSON or YAML, or-f -for standard input):{title, kind, markdown, schema_format, native_schema?, operations?, owner_user_id?, describes_existing?}, each operation{name, types?}; there is nochange, a first revision is additive. The flags override the file:--title,--kind(rest,graphql,grpc,events,library,other),--schema-format(openapi,graphql,proto,asyncapi,typescript,python,prose; named so because the global--formatis the output format),--markdown <file.md>reads the document,--owner <user id>setsowner_user_id(by default you own it), and--existingsetsdescribes_existing: true: the interface already exists in the provider's code, so the contract reads implemented once accepted. Files are read relative to the working directory. With a native schema in the file and no markdown, the CLI writes the document:# <title>and a line saying the interface is described by its<format>schema.--schema <file>never reads the file intonative_schema: with--existingit publishes the file aspidgen contract publishdoes, and otherwise it proposes it, with the files beside it, as a new contract the project is to provide, which needs--projectand--title. With--schema,--titleand--schema-format(openapiorasyncapi, the file's format, sent asformat) apply, and-f,--kind,--markdownand--ownerareCLI_USAGE(exit 2);datais the publish's or the proposal's answer. For example,pidgen contract create --project ALPHA --title "Payments API" --schema-format openapi --schema openapi.yaml --existing. The body is checked against the publishedContractCreateRequestfirst: one that does not parse or match exits 2 withINPUT_INVALIDanderror.details.issues; a missing or empty--markdownfile isCLI_USAGE(exit 2). Nothing is sent then.datais the created contract, aspidgen contract showprints it, with revision 1:automatic, orwaitingwhen the Account asks a person to accept every new contract. Creating needs a Contributor or Manager role in the provider Project, Maintainer of the provider Domain, or Owner or Administrator; a Domain's contract also needs the Account's plan to include Domains.pidgen contract publish— sends files, never schema text:<file>as the entry and the files beside it, every.json,.yamland.ymlfile under the entry's directory (never undernode_modulesor a dot directory), or with--include <glob>(repeatable;*,**,?,{a,b}) only the files there that match. Each is named by its path relative to the entry's directory, and the server resolves relative$refs among them and nowhere else. A bundle holds at most 200 files and 8 MiB in all: over that it exits 5 withFILE_TOO_LARGEand sends nothing. The contract is identified by its provider and its source path:--path, or by default the entry's path relative to the root of its git repository (outside a repository give--path; an absolute one, or one that leaves the repository, isCLI_USAGE). It sendssource_kindciwhen the environment hasCI=true, elseupload, and--commitand--refwhen given.--breakingrecords the change as breaking whatever comparing the operations says.datais the server's answer:outcome(created,revisedorunchanged— the same interface again records nothing),dry_run,contract_idandcontract_key(null for a dry run of a path never published),revision,acceptance(automaticorwaiting; null for a dry run orunchanged),change(additiveorbreaking; null when unchanged) andchange_reason, the file'sformat,version,titleandsource_path, itsoperation_countandtype_count, the operationsaddedandremoved, the typestypes_addedandtypes_removed, the pending operations itlandedand thosestill_pending, and the earlier refused publishes of the path it resolves (resolved_refusals).--checkis the dry run for pull requests: it records nothing, exits 0 when the change is additive or the file is unchanged, and on a breaking change exits 5 withCONTRACT_CHANGE_BREAKING(thechange_reason, or else the removed operations, inerror.message, the dry run's answer inerror.details.result) unless--breakingis given. A provider's pipeline needs two lines:pidgen contract publish contracts/public-api/openapi.json --project ALPHA --commit "$GITHUB_SHA" --ref "$GITHUB_REF"on the default branch, and the same with--checkon pull requests.pidgen contract propose— sends a fragment file in the contract's format holding only the operations being added or changed, with the files beside it aspublishsends them (every.json,.yamland.ymlfile under the fragment's directory, or with--include <glob>only those matching, at most 200 files and 8 MiB in all), so its relative$refs resolve; the server takes the operations from the fragment.<contract> <fragment>proposes a revision of an existing contract;--project <project> --title <title> <fragment>proposes a contract that does not exist yet. The proposal is accepted under the Account's Contract approval, and its operations stay pending until a published revision contains them.--checkrecords nothing.dataisdry_run,contract_idandcontract_key(null for a dry run of a contract that does not exist yet),revision,acceptance, the fragment'sformat,versionandtitle, itsoperations, those it madependingand those alreadylanded.--projectwith a contract, without--title, or--titlewithout--projectisCLI_USAGE(exit 2) and sends nothing.- A refused file, from
publishorpropose, exits 5 with the refusal's code and prints every error with its location:error.messagenames the reason and lists each error as<location>: <message>((no location)when the server gives none), anderror.details.errorsholds them as{message, location}, the server's own message inerror.details.server_message. A publish refused when it is not a dry run is announced in the Inbox with the refused file, and names it inerror.details.refusal_id. pidgen contract revise— the file (JSON or YAML, or-f -for standard input) is the revision:{kind, markdown, schema_format, change, operations?, native_schema?}, each operation{name, types?}.--markdownreads a Markdown file, relative to the working directory, intomarkdown. The body is checked against the publishedContractRevisionRequestfirst: one that does not parse or match exits 2 withINPUT_INVALIDanderror.details.issues; a missing or empty--markdownfile, or a file that namesmarkdownas well as--markdown, isCLI_USAGE(exit 2). Nothing is sent then.datais the contract with its new revision last; the revision'sacceptanceisautomatic, orwaitingwhen a person must accept it. Revising needs a Project role in the provider project, or Maintainer of the provider domain.pidgen contract accept— with--revision <n>accepts that revision; without it reads the contract and accepts its one waiting revision. With none waiting it exits 5 withNO_REVISION_WAITING(error.details:contractand each revision'snumberandacceptanceinstatuses), and with more than one waitingREVISIONS_WAITING(error.details.waiting); no accept is sent then. With automatic acceptance nothing ever waits.datais the contract with the accepted revision. Accepting is allowed to the provider project's Project Managers, the provider domain's Maintainers, the owner, and the Account's Owners and Administrators; the server decides. A--revisionthat is not a whole number from 1 isCLI_USAGE(exit 2) and sends nothing.
The server's refusals keep their codes and error.details:
| Code | When | Exit |
|---|---|---|
PROJECT_NOT_FOUND, DOMAIN_NOT_FOUND |
No such project or domain in the Account | 4 |
CONTRACT_NOT_FOUND, CONTRACT_REVISION_NOT_FOUND |
No such contract in the Account, or no such revision of it | 4 |
CONTRACT_REVISION_NOT_WAITING, CONTRACT_REVISION_SUPERSEDED (409) |
The named revision is already accepted, or a newer one is | 5 |
CONTRACT_RETIRED (409) |
The contract is retired and changes no more | 5 |
CONTRACT_TITLE_INVALID, INPUT_INVALID (422) |
The server refuses the new contract's title or body | 2 |
CONTRACT_FILE_INVALID |
The file does not parse or fails validation | 5 |
CONTRACT_FORMAT_UNSUPPORTED |
The file is not OpenAPI (2.0, 3.0, 3.1) or AsyncAPI (2.x, 3.x) | 5 |
CONTRACT_EXTERNAL_REF |
A $ref is a URL, an absolute path, or a path outside the files sent |
5 |
CONTRACT_CONVERSION_TIMEOUT |
Converting the files took longer than 60 seconds | 5 |
FILE_TOO_LARGE |
The files are over 8 MiB or 200 files in all | 5 |
CONTRACT_CHANGE_BREAKING (the CLI's) |
publish --check found a breaking change and --breaking was not given |
5 |
FORBIDDEN (403) |
You may not create a contract for the provider, revise the contract, or accept its revision | 3 |
ENTITLEMENT_LIMIT_REACHED or FEATURE_NOT_IN_PLAN (403) |
A domain's contract, and the Account's domains feature is switched off; a project's contract needs no feature | 3 |
ticket
Section titled “ticket”| Command | Does | Route |
|---|---|---|
pidgen ticket list [--stage] [--project] [--query] [--intervention] [--kind] |
List tickets, every page; --kind epic lists Epics; --epic is planned |
GET /tickets |
pidgen ticket show <ref> [--kind] |
One ticket with delivery, findings and prerequisites | GET /tickets/{ref}, GET /tickets/{ref}/prerequisites |
pidgen ticket create -f <file> [--kind] [--domain] |
Create a structured ticket; --kind epic files an Epic, --domain a Domain Epic |
POST /tickets |
pidgen ticket edit <ref> -f <file> [--kind] |
Edit content (uses the ticket's current version) | GET /tickets/{ref}, PATCH /tickets/{ref} |
pidgen ticket owner <ref> (--user <user_id> | --clear) [--reason <text>] |
Set or clear the owner using the current version; requires a server publishing the owner route | PUT /tickets/{ref}/owner |
pidgen ticket follow <ref> |
Explicitly follow a ticket; requires a server publishing the following route | PUT /tickets/{ref}/following |
pidgen ticket unfollow <ref> |
Stop explicitly following a ticket; requires a server publishing the following route | PUT /tickets/{ref}/following |
pidgen ticket cancel <ref> [--disposition] [--superseded-by] [--discard-workspace] [--close-pr] |
Preview/apply cancelling a ticket and its descendants; an open pull request is left open unless --close-pr |
POST /tickets/{ref}/cancellation/preview, POST /tickets/{ref}/cancellation |
pidgen ticket actions <ref> [--family <f>] [--run <id>] [--node <id>] [--prerequisite <id>] [--decision <decision_key>] |
The actions the ticket, its runs, its dependencies and its decisions allow, why a blocked one is blocked, which one is recommended, and what each would do | GET /tickets/{ref}/actions |
pidgen ticket act <ref> <action> [--run <id>] [--node <id>] [--prerequisite <id>] [--criterion <index>] [--decision <decision_key>] [--until <date-time>] [--delegate-to <user_id>] [--exception-reason <text>] [--criterion-status <status> --override-reason <text>] [--answer <decision>=<text>] [--alternative <decision>=<option>] [--reason <text>] [--note <text>] |
Preview/apply one of those actions | GET /tickets/{ref}/actions, POST /tickets/{ref}/actions/preview, POST /tickets/{ref}/actions/execute |
pidgen ticket retry <ref> [--run <id>] |
Preview/apply a fresh attempt of a failed or stalled run (retry_run, or restart_refinement for a Refinery run) |
GET /tickets/{ref}/actions?family=recovery, POST /tickets/{ref}/actions/preview, POST /tickets/{ref}/actions/execute |
pidgen ticket replay <ref> --node <id> [--run <id>] |
Preview/apply a replay from one node (replay_from_node) |
same |
pidgen ticket reopen <ref> |
Preview/apply reopening a completed ticket whose delivery cannot be proven | POST /tickets/{ref}/reopen/preview, POST /tickets/{ref}/reopen |
pidgen ticket clarify <ref> -f <answers> |
Answer a parked clarification; abandoned is deprecated and now places a hold |
GET /tickets/{ref}/clarification, POST /tickets/{ref}/clarification |
pidgen ticket instructions <ref> [--kind steer|request_changes] |
List a ticket's instructions, across its sessions | GET /tickets/{ref}/instructions |
pidgen ticket history <ref> [--show everything|people|observations] [--after <n>] [--limit <n>] [--all] |
Read what happened to a ticket, oldest first; --all follows every page |
GET /tickets/{ref}/history |
pidgen ticket dep add <ref> --on <ref> [--type blocks] |
Make a ticket wait for another | POST /tickets/{ref}/dependencies |
pidgen ticket dep rm <ref> --on <ref> [--type blocks] |
Remove that edge | DELETE /tickets/{ref}/dependencies/{source} |
pidgen ticket dep list <ref> |
Edges in both directions | GET /tickets/{ref}/dependencies |
pidgen ticket contract implements <ref> <contract> [--operation <op>]... |
Link a delivery root to the contract it implements | POST /tickets/{ticket_id}/contracts |
pidgen ticket contract builds-against <ref> <contract> [--operation <op>]... |
Link work to a contract it builds against | POST /tickets/{ticket_id}/contracts |
pidgen ticket contract rm <ref> <contract> |
Remove a ticket's link to a contract | DELETE /tickets/{ticket_id}/contracts/{contract_id} |
pidgen ticket move <ref> --epic <key>|--no-epic |
Move a ticket into or out of an Epic by its parent_ticket_id |
GET /tickets/{ref}, PATCH /tickets/{ref} |
pidgen ticket template [-o <file>] [--force] |
Write an annotated ticket file to fill in | — |
pidgen ticket types |
The workflow types registered in the Account: the valid values of a ticket's workflow_type, each with its label, workflow and ticket count |
GET /workflow-types |
pidgen ticket list, pidgen ticket show and pidgen ticket types are read commands like
pidgen project list. pidgen ticket create, pidgen ticket edit, pidgen ticket template,
pidgen ticket dep add, pidgen ticket dep rm, pidgen ticket dep list,
pidgen ticket contract implements, pidgen ticket contract builds-against,
pidgen ticket contract rm, pidgen ticket move,
pidgen ticket cancel, pidgen ticket actions, pidgen ticket act, pidgen ticket retry,
pidgen ticket replay, pidgen ticket reopen, pidgen ticket clarify, pidgen ticket instructions and
pidgen ticket history change or read one ticket.
-
pidgen ticket list—datais one array of every ticket you can see, newest first. The CLI asks for pages of 100 (page,per_page) and follows them until a page holds fewer than 100, so the array is the whole list; a refusal on any page fails the command rather than returning part of it, andmetais the first page's. Each filter is sent as the route's query parameter and may be combined:Flag Query parameter Keeps --stage <stage>stageTickets in that stage: backlog,research_needed,specification,planning,implementation_ready,in_execution,awaiting_review,delivered,completed,failed,abandoned--project <id>project_idTickets of that project --query <text>qTickets whose key, id, title or objective contains the text, ignoring case; the server returns at most 20 --intervention <type>interventionTickets waiting on an open intervention of that type: clarification_needed,review_required,execution_failed,child_blocked,delivery_blocked,delivery_closed_unmerged,delivery_auto_merge_failed--kind <kind>kindTickets of that kind: epic,storyortask. Without--kindthe server leaves Epics out and lists Stories and Tasks;--kind epiclists only EpicsAn unknown stage, intervention type or kind, an empty
--projector--query, a--querylonger than 200 characters, or an argument isCLI_USAGE(exit code 2) and sends nothing.--epic(the members of one Epic) is planned and refused asCLI_USAGE; read an Epic's members withpidgen epic dag show. -
pidgen ticket show <ref>—<ref>is the ticket's key (e.g.PIDGEN-12) or id.datais the server's ticket, with its delivery, effective merge policy and open findings, plusprerequisites: whether the ticket isreadyto run, itsreadiness_blockersandblockersexplained, and itsblocking_prerequisites(the unfinished tickets holding it, each byticket_keyandstage). When the server sends them,dataalso carries the ticket'soutcome_pause(statepausingorpaused,requested_by,requested_at, ornull) and itscancellation(cancellation_id,disposition,superseded_by,reason,requested_by,requested_at,state,pull_request_closeand what it isawaiting:execution_stop,pull_request_close, ornull), printed as the server sends them.metais the ticket's response. An unknown ticket, or another Account's, exits 4 with the server'sTICKET_NOT_FOUND, and the prerequisites are not requested.--kind <kind>(e.g.--kind epic) refuses a ticket of another kind withTICKET_KIND_MISMATCH(exit code 4, with theticket, itskindand theexpected_kindinerror.details), and the prerequisites are not requested. A missing, empty,.or..ref, or more than one, or an unknown--kind, isCLI_USAGE(exit code 2) and sends nothing. -
pidgen ticket create -f <file>— creates a ticket in Backlog from a JSON or YAML file (-f -reads standard input; see Input files). The body is checked against the create request schema and sent as written:titleandobjectiveare required,project_idnames the project by id or ticket-key prefix, andworkflow_typeis one ofpidgen ticket types. Optionalteam_member_idselects an Account Agent by ID (not a model name), a non-empty string of at most 80 characters. Omission ornulllets the server choose the Project default, else the Account's reserved Unassigned Agent. The CLI sends the selection unchanged and keeps an omitted field omitted. The Agent is chosen when the ticket is created; an edit namingteam_member_idreassigns it later (pidgen ticket edit).datais the server's created ticket, with itsid,ticket_key,stageand, when the file asked forstart_refinement, therefinement_command_idor thecommand_errorsaying why refinement did not start. A body that does not parse or match the schema exits 2 withINPUT_INVALIDand sends nothing. The server's refusals keep their codes:PROJECT_NOT_FOUND,EPIC_NOT_FOUNDandTEAM_MEMBER_NOT_FOUNDexit 4;PROJECT_REQUIRED,UNKNOWN_WORKFLOW_TYPEandEPIC_PROJECT_MISMATCHexit 2;FORBIDDENexits 3. No-f, or an argument, isCLI_USAGE(exit code 2).A Story or Task is filed in an Epic by naming the Epic (its key or id) as
parent_ticket_id.--kind <kind>sets the body'skind;--kind epicfiles an Epic, a ticket of kindepicthat holds Stories and Tasks and does no work of its own. An Epic is numbered like any ticket and may carrydescription(stored as its objective),branch_mode(project_base,create_newortarget),feature_branch,target_dateandmerge_policy; it refusesteam_member_id,workflow_type,approval_mode,acceptance_criteria,start_refinement,required_capabilities,estimated_complexityandparent_ticket_id(EPIC_FIELD_REFUSED,EPIC_HAS_NO_PARENT, exit 2). A file whosekinddiffers from--kindexits 2 withINPUT_INVALIDand sends nothing. An Epic's branch is checked on the project's forge (EPIC_BRANCH_REFUSED, exit 2; nothing is created).title: Sign-indescription: Everything people need to sign in.project_id: EXAMPLEbranch_mode: create_newmerge_policy: manual--domain <domain>(a domain's key or id) files a Domain Epic: the body is sent withkind: epicanddomain_idset to the domain instead ofproject_id, and is checked against the published create request, which carriesdomain_id. Without--domainnothing changes. With it,--kind storyor--kind task, or a file namingproject_id,branch_modeorfeature_branch(even asnull), anotherkindor anotherdomain_id, exits 2 withINPUT_INVALID,error.details.issuesnaming each field ($.project_id, …), and sends nothing; an empty--domainisCLI_USAGE. The server's refusals keep their codes:DOMAIN_HAS_NO_PROJECTS(the domain contains no project) exits 5;DOMAIN_PROJECT_CONFLICTandEPIC_FIELD_REFUSEDexit 2;DOMAIN_NOT_FOUNDexits 4; a 403 (you may not create a Domain Epic — Domain Maintainers, Owners and Administrators may — or the Account's domains feature is switched off) exits 3.title: Checkout across payments and cardsdescription: One checkout across the payment and card projects.target_date: 2026-12-01T00:00:00Z -
pidgen ticket edit <ref> -f <file>— changes a ticket's content. The file holds only the fields to change (title,objective,scope_boundaries,assumptions,open_questions,acceptance_criteria,success_metrics,context_artifacts,required_capabilities,priority,estimated_complexity,merge_policy,team_member_id,parent_ticket_id,add_artifact_upload_ids,remove_artifact_ids); a list replaces the ticket's list, andmerge_policy: nullmakes the ticket inherit again.parent_ticket_idmoves the ticket into an Epic, or out of it withnull(aspidgen ticket movedoes).--kind <kind>(e.g.--kind epic) refuses a ticket of another kind withTICKET_KIND_MISMATCH(exit code 4) after the read, and no edit is sent.success_metrics,context_artifactsandrequired_capabilitiesaccept arrays or null as whole-list replacements, with the same item shapes as creation. Metrics requirenameandtarget(optionalcurrent,unit); artifacts requiretypeandcontent(optionalreference_url,tier); capabilities are strings. Omitted fields stay unchanged. These edits let a hand-refined Backlog ticket add readiness content before promotion.titlerenames the ticket: it must not be empty once its surrounding spaces are trimmed, and it is sent as written. A file naming onlytitleis a complete edit. The CLI reads the ticket (GET /tickets/{ref}) and sends its currentversionwith the fields, so the file never namesversion. A file that does not parse or match the edit schema, names no field, namesversion, or has a blanktitleexits 2 withINPUT_INVALIDand sends nothing.datais the ticket as the edit left it, plusadmission: the server's report of what the edit did to the root's execution admission and planning (root_ticket_id,admission_invalidated,root_stage,plan_revised,review_requiredandplanning_restarted), which the server sends when the edit withdrew admission or restarted planning, andnullwhen it was untouched.metais the edit's response. A ticket that changed between the read and the edit is the server'sCONFLICT(exit code 5, withexpected_versionandactual_versioninerror.details): read it again and retry. A field the ticket's stage does not allow editing (EDIT_STAGE_NOT_ALLOWED), a merge policy that can no longer change because the pull request is merged (EDIT_MERGE_POLICY_LOCKED) and work execution already owns (EDIT_EXECUTION_ALREADY_ACCEPTED,EDIT_CAPACITY_LEASE_ACTIVE) also exit 5. The first two name what was refused inerror.details:details.fieldis the refused field and, forEDIT_STAGE_NOT_ALLOWED,details.stageis the ticket's stage. Which stages accept which field is the server's rule; the CLI does not check it and reports the refusal as the server gives it. An unknown ticket exits 4 withTICKET_NOT_FOUNDand nothing is changed. A missing, empty,.or..ref, more than one, or no-fisCLI_USAGE(exit code 2).team_member_idreassigns the ticket to that Account Agent (an Agent ID of 1 to 80 characters, not a model name) through the server's assignment, which also accepts an Agent-only edit in any stage the assignment allows; it is sent as written.null, an empty ID or one over 80 characters exits 2 withINPUT_INVALIDand sends nothing; to unassign, name the Account's reserved Unassigned Agent. Eligibility is the server's:TEAM_MEMBER_NOT_FOUNDexits 4,TEAM_MEMBER_ARCHIVED(422) exits 2 and an assignment refusal (409) exits 5, each with the server's code. Conflicts cause no automatic write retry or admin fallback. -
pidgen ticket owner <ref> --user <user_id>orpidgen ticket owner <ref> --clear, with optional--reason <text>— reads the ticket's currentversionand sends it withowner_user_id(the user id ornullto clear) and the reason when given.datais the server's result:ticket_id,owner_user_id,effective_owner(user_id,display_name, andsource:ticket,parent,submitterornone), and the resultingversion. A stale version (409) andOWNER_NOT_ELIGIBLE(422) exit 5; permission denied (403) exits 3. Give exactly one of--userand--clear: neither, both, or an empty user id exits 2 withCLI_USAGEbefore any request. This command requires a server publishing the owner route; an unavailable route reports the server's 404 (exit 4). -
pidgen ticket follow <ref>andpidgen ticket unfollow <ref>— sendfollowing: trueorfollowing: falserespectively. The operation is idempotent.datais the server's result:ticket_id, effectivefollowing, andsource(explicit,owner,administratorornull). Removing explicit following can leave effective following through ownership or administration; the CLI prints what the server reports. An unreadable ticket or unavailable route reports the server's 404 (exit 4). They require a server publishing the following route. These commands andticket owneract immediately: their public routes provide no preview operation. A missing, empty,.or..ref, an extra argument or an unknown flag exits 2 withCLI_USAGEand sends nothing. -
pidgen ticket template [-o <file>] [--force]— writes an annotated YAML ticket to fill in, toticket.yamlin the working directory unless-onames another file. Every value in it is valid as written, sopidgen ticket create -faccepts the file unchanged; comments say what each field means and which to replace. Its acceptance criterion has astatement(the only required field), apriorityand an optionalassertion— what a verifier would see when the criterion holds;test_assertionappears only in a comment, as an advanced pinned check for when one specific command must run. It also writesestimated_complexity(3),context_artifacts(an empty list, with a commented example) andrequired_capabilities(an empty list). Each scored field is annotated with the readiness points it earns: a ticket runs only with 60 of 100 points and anestimated_complexity. Criteria earn 10 points each (at most 30) and MUST criteria 5 more each (at most 20);scope_boundariesandsuccess_metricsearn 10 each; context artifacts earn 5 each (at most 15); each open question costs 5;estimated_complexityandrequired_capabilitiesearn none; and reachingimplementation_readyadds 15. It calls no route.datais{path, format}, the absolute path written andyaml. An existing file is kept and the command exits 2 withCLI_USAGEunless--forceis given; an empty-oor an argument is alsoCLI_USAGE. A file that cannot be written exits 1 withCLI_INTERNAL. -
pidgen ticket types—datais the server's array of workflow types, ordered bykey, each withkey(the value a ticket'sworkflow_typenames),label,workflow_name(the workflow it runs),ticket_count(tickets of that type you can see),builtin,defaultandversion. It takes no arguments; an extra one isCLI_USAGE(exit code 2). -
pidgen ticket dep add <ref> --on <ref> [--type <type>]— declares that the<ref>ticket depends on (waits for) the--onticket, each named by its key (e.g.PIDGEN-2) or id. It sends--onassource_id, and--type(blocks,informsorparallel_safe) asdep_typewhen given; the server's default isblocks. Both tickets must be in the same project.datais the stored edge (id,source_id,target_id,dep_type,project_id, and a retiredepic_idthat is alwaysnull: an edge carries no Epic label). Either ticket may be an Epic. Adding an edge that already exists returns it. The server's refusals keep their codes and exit 5: a cycle (DEPENDENCY_CYCLE, with the cycle inerror.details),DEPENDENCY_SELF_EDGE,DEPENDENCY_CROSS_PROJECT,DEPENDENCY_TARGET_FROZEN(<ref>is at or past execution),DEPENDENCY_CROSSES_COHORTandDEPENDENCY_INCOMPATIBLE_BASE.TICKET_NOT_FOUNDandDEPENDENCY_ENDPOINT_NOT_FOUND(no such--onticket) exit 4. No--onor an empty one, an unknown--type, or a missing, empty,.or..ref, or more than one, isCLI_USAGE(exit code 2) and sends nothing. -
pidgen ticket dep rm <ref> --on <ref> [--type <type>]— removes the edge by which<ref>depends on--on:--onis the last path segment, and--typeis sent as thedep_typequery parameter when given (the server's default isblocks).datais the edge removed.TICKET_NOT_FOUND,DEPENDENCY_ENDPOINT_NOT_FOUNDandDEPENDENCY_NOT_FOUND(no such edge) exit 4;DEPENDENCY_SELF_EDGEandDEPENDENCY_TARGET_FROZENexit 5. Its usage errors are those ofdep add, and a.or..--onisCLI_USAGEtoo. -
pidgen ticket dep list <ref>—datais the server's answer unchanged: the ticket'sticket_idandticket_key;blocked_by, the tickets it waits for;blocks, the tickets waiting for it, each with itsid,ticket_key,title,stage,dep_typeandedge_id, oldest first; andinherited_blocked_by, theblocksedges declared on its ancestors, which hold it just as hard, each also naming the ancestor it is declared on (declared_on_id,declared_on_key,declared_on_title). An unknown ticket exits 4 withTICKET_NOT_FOUND. A missing, empty,.or..ref, or more than one, isCLI_USAGE(exit code 2) and sends nothing. -
pidgen ticket move <ref> --epic <key>|--no-epic— moves a ticket into the Epic--epicnames (its key such asPIDGEN-40, or its id), or out of its Epic with--no-epic, by setting the ticket'sparent_ticket_id: the CLI reads the ticket (GET /tickets/{ref}) for itsversionand sendsPATCH /tickets/{ref}withversionandparent_ticket_id(the Epic, ornull). The Epic must be in the ticket's own project, and only a ticket without a parent or a member of an Epic moves, before its execution starts.datais the ticket as the move left it, withadmission(aspidgen ticket editreports it) andmove:moved(false when the ticket was already there),previous_parent_ticket_idandparent_ticket_id.TICKET_NOT_FOUNDexits 4,FORBIDDENexits 3,CONFLICT(the ticket changed between the read and the move) exits 5, andPARENT_PROJECT_MISMATCH(an Epic of another project),PARENT_NOT_EPIC,EPIC_HAS_NO_PARENTandPARENT_CHANGE_NOT_DELIVERY_ROOTexit 2. Exactly one of--epicand--no-epicis required: both, neither, an empty--epic, or a missing, empty,.or..ref, or more than one, isCLI_USAGE(exit code 2) and sends nothing.
ticket contract
Section titled “ticket contract”Work links to a contract, never to other work. Only a delivery root links: in the
contract's provider project (or, for a domain's contract, a project the domain covers) it
implements the contract and gains the acceptance criterion "implements {contract}"; in any other
project a delivery root or an Epic builds against it. A link names the operations it uses; none
named means all of them. A link never blocks: it is not a dependency edge (use
ticket dep add for that), and linked work waits only while the contract has a revision
waiting for a person, when the Account's Contract approval setting asks for one. The server
attaches the contract's accepted revision to the linked ticket's runs itself; the CLI uploads
nothing. The routes are in the published document
(API compatibility).
| Command | Route | Body |
|---|---|---|
pidgen ticket contract implements <ref> <contract> [--operation <op>]... |
POST /tickets/{ticket_id}/contracts |
{contract_id, link: "implements", operations} |
pidgen ticket contract builds-against <ref> <contract> [--operation <op>]... |
POST /tickets/{ticket_id}/contracts |
{contract_id, link: "builds_against", operations} |
pidgen ticket contract rm <ref> <contract> |
DELETE /tickets/{ticket_id}/contracts/{contract_id} |
— |
<ref> is the ticket's key or id and <contract> the contract's key, such as CT-7, or its id;
both are percent-encoded path segments where they appear in the route. --operation is repeatable,
each an operation the contract's newest accepted revision names, e.g. --operation 'GET /payments';
with none, operations is []. data is the ticket's contracts after the change, validated
against the published document (UNEXPECTED_RESPONSE, exit 1, when it does not match):
contracts, each link's contract_id, key, title, status, provider_project_id, link,
operations, criterion_id (the criterion an implements link adds; null for builds-against),
linked_at and linked_by. rm removes whichever link the ticket has to the contract and prints
the ticket's remaining contracts. The writes act directly, with no preview. A missing ticket or contract, an
empty, . or .. ref, an empty contract or an empty --operation is CLI_USAGE (exit 2) and
sends nothing.
The server's refusals write nothing and keep their codes, messages and error.details:
| Code | When | Exit |
|---|---|---|
CONTRACT_LINK_NOT_DELIVERY_ROOT |
The ticket is not a delivery root | 5 |
CONTRACT_LINK_OUTSIDE_PROVIDER |
Implementing from outside the provider (or the domain's coverage) | 5 |
CONTRACT_LINK_OWN_PROVIDER |
Building against your own project's contract | 5 |
CONTRACT_LINK_INSIDE_COHORT |
The ticket is inside another cohort: link the cohort's root | 5 |
CONTRACT_DEPRECATED |
The contract is deprecated; error.details names its replacement |
5 |
CONTRACT_RETIRED |
The contract is retired | 5 |
CONTRACT_UNKNOWN_OPERATION |
An operation the newest accepted revision lacks; with none accepted only no --operation is allowed |
5 |
TICKET_NOT_FOUND, CONTRACT_NOT_FOUND, CONTRACT_LINK_NOT_FOUND |
No such ticket, contract, or link to remove | 4 |
FORBIDDEN (403) |
You may not change the ticket's links | 3 |
The seven link refusals exit 5 whatever status the server answers them with.
ticket actions and act
Section titled “ticket actions and act”$ pidgen ticket actions PIDGEN-4 [--family recovery|run_control|ticket_control|decision_followup|decision_answers] [--run <id>] [--node <id>] [--prerequisite <id>] [--decision <decision_key>]$ pidgen ticket act PIDGEN-4 retry_run [--run <id>] [--node <id>] [--fan-out-key <key>]$ pidgen ticket act PIDGEN-4 retry_run --apply --acknowledge ambiguous_effects [--reason "…"] [--note "…"] [--no-wait]$ pidgen ticket act PIDGEN-4 remove_dependency --prerequisite TKT-EXAMPLE [--apply]$ pidgen ticket act PIDGEN-4 defer_decision --decision <decision_key> --until 2026-10-12T09:00:00Z [--apply]$ pidgen ticket act PIDGEN-4 delegate_decision --decision <decision_key> --delegate-to <user_id> [--apply]$ pidgen ticket act PIDGEN-4 hold_decision --decision <decision_key> --reason "…" [--apply]$ pidgen ticket act PIDGEN-4 plan_work|start_work|ask_before_starting|approve_delivery [--apply]$ pidgen ticket act PIDGEN-4 approve_with_exception --exception-reason "…" [--apply]$ pidgen ticket act PIDGEN-4 request_delivery_changes --note "…" [--apply]$ pidgen ticket act PIDGEN-4 override_acceptance_criterion --criterion <index> --criterion-status passing|failing|pending --override-reason "…" [--apply]$ pidgen ticket act PIDGEN-4 correct_plan_decision --answer <decision>=<text> --alternative <decision>=<option> [--apply]The App Service publishes one interface for what a ticket, its runs, its declared dependencies and
the decisions waiting on a person can do next. Each action decision names an action — any
kind the public API document's ActionKind lists. The CLI reads the kinds and the families from the
document it bundles, not from a list of its own, so pidgen ticket act accepts every one the
document publishes: restart_refinement, replay_from_node, retry_run,
replace_with_current_contract, recover_parked_node, redrive_run, reopen_for_delivery,
resolve_target_conflict, pause_run, resume_run, cancel_ticket, terminate_run,
remove_dependency, retry_admission, the six decision follow-up kinds snooze_reminders,
defer_decision, hold_decision, delegate_decision, proceed_on_assumption and keep_holding
(see Decision follow-up), and the eight plan, start, approve and override
kinds plan_work, start_work, ask_before_starting, correct_plan_decision,
approve_delivery, approve_with_exception, request_delivery_changes and
override_acceptance_criterion (see Plan, start, approve and override).
The server may offer further kinds, such as relocate_run, wait_for_worker, retry_provisioning,
start_from_remote_branch, start_step_fresh, reapply_delivery, return_to_planning,
close_completed, answer_request and sync_comments: pidgen ticket actions lists whatever the
server offers and pidgen ticket act sends any of them. A decision also names its family (recovery, run_control, ticket_control for
remove_dependency, decision_followup, or decision_answers for the plan, start, approve and
override kinds), a person-facing label, its target (kind ticket, run, node,
dependency, decision or criterion, with ticket_id and, when they apply, run_id,
node_id, fan_out_key, for a dependency prerequisite_id, for a decision decision_key and, for
a criterion, criterion_index, its position in the ticket's acceptance criteria counting from 0: a
target may omit those it does not have), two gates, admissible
(the ticket's state allows it) and authorized (you may take it), each {allowed, refusal} with
the refusal's code, message and the action to take instead; recommended ({reason} on the
recommended action, else null); its consequence (run: continue or fresh, and for each input
class — configuration, contract, ticket_instructions, rework_brief — whether it is
preserved or adopted); and requires_preview.
Some actions, and when each is the one to take:
-
restart_refinement("Start again with the current configuration") starts a fresh Refinery run for the ticket, adopting the current configuration, workflow contract and ticket instructions; the old run and its events are kept. It is the refinement re-dispatch, offered only while the ticket is still in refinement — atresearch_needed,specificationorplanning— with no live Refinery run and no open clarification (otherwiseREFINEMENT_RESUME_STAGE_REQUIRED,LIVE_REFINERY_RUN_EXISTSorCLARIFICATION_PENDING). -
replay_from_nodere-runs one node of a finished run and what depends on it, reusing the rest of the run's frozen work. Choose it overrestart_refinementwhen one step went wrong and the rest of the run is worth keeping; a Refinery run with no replayable step answersinstead: restart_refinement. -
resolve_target_conflict("Resolve merge conflicts") returns a delivered pull request that the forge reports as conflicting with its target branch to implementation, to merge the target and deliver again. It is refused withPULL_REQUEST_MERGEABLEwhen there is no conflict,NO_PULL_REQUEST,PULL_REQUEST_MERGEDorPULL_REQUEST_CLOSED. -
remove_dependency(familyticket_control) removes the ticket's declared dependency on the prerequisite its target names; name it with--prerequisite <id>. Once work has started the dependency can no longer change (DEPENDENCY_TARGET_FROZEN). -
pidgen ticket actions <ref>readsGET /tickets/{ref}/actions, passing--family,--run,--nodeand--prerequisiteas thefamily,run_id,node_idandprerequisite_idfilters.--decision <decision_key>keeps the actions whosetarget.decision_keyit names; the route publishes no filter for it, so the CLI applies it to the answer and sends none.datais{ticket_id, actions}; each action is the server's decision unchanged plusstate(allowed, orblockedwhen either gate refuses),blocked_by(admissibleorauthorized, else null),refusal(the blocking gate'scode,messageandinstead, else null) andrecommended_reason. It changes nothing. An unknown ticket exits 4 with the server'sTICKET_NOT_FOUND; a caller withoutticket.readexits 3. -
pidgen ticket act <ref> <action>(the action may be writtenretry-run) reads the decisions, narrowed by--family,--run,--node,--fan-out-key,--prerequisite,--criterion(passed as thecriterion_indexfilter and matched against each target'scriterion_index) and--decision(matched against each target'sdecision_key), and takes the one for that action. Then, with Preview and apply and the shared flags:- Without
--applyit previews withPOST /tickets/{ref}/actions/preview, sending{action, target}(andfollowup,exception,criterion_overrideorplan_correction, when the flags for them are given), and changes nothing.datais the preview —preview_digest,required_acknowledgements,action,target,consequenceand the action'seffects— and thedecisionit previewed. --applypreviews again and executes that preview withPOST /tickets/{ref}/actions/execute, sending the sameaction,targetand choices, itspreview_digest, theacknowledgementsgiven with--acknowledge <code>...(or--ack, repeatable),--reason,--note(sent asnoteonly when given) and an idempotency key the CLI derives from the command, target, digest and acknowledgements (--idempotency-keysets it). The body is checked against the published request first:reasonat most 10240 characters,noteat most 4000,idempotency_key1–255 characters and at most 20 acknowledgements; anything longer exits 2 withINPUT_INVALIDand nothing is executed. A code the preview requires but you did not pass exits 6 withACKNOWLEDGEMENT_REQUIRED,error.details.missingand the preview, and nothing is executed; the CLI never acknowledges on your behalf.--apply --digest <d>executes exactly that earlier preview without previewing again.- A
200is settled. A202is followed on itsstatus_urluntil it settles (--wait, the default;--timeoutbounds it and a timeout exits 8);--no-waitreturns at once withhandle, the status route.dataafter an apply:settled,result,idempotency_keyand, while unsettled,handle.resultis the server's execute result:command_id(the durable command),ticket_id,status(appliedorapplying),disposition_code(null unless the server recorded one),workflow_run_id(the run the action started or acts on, else null) andstatus_url(the command's status route), beside theactionandtarget. It has noresultobject.
- Without
-
A blocked decision is refused before anything is previewed:
error.codeanderror.messageare its refusal's,error.detailscarriesaction,label,target,blocked_byandinstead(andnext, the command for the alternative), and it exits 5 when not admissible or 3 when not authorized. A refused preview or execute keeps the server's refusal code and message and exits non-zero: 5 for a refusal (409), 3 when not permitted (403), 4 when the target is gone (404), and 6 forPREVIEW_STALE(preview again) orACKNOWLEDGEMENT_REQUIRED. -
An action the ticket does not offer for the target exits 5 with
ACTION_NOT_OFFERED,error.details.offeredlisting what it offers. An action offered on several targets without--run,--node,--fan-out-key,--prerequisite,--criterionor--decisionto choose one exits 2 withACTION_TARGET_AMBIGUOUSanderror.details.targets. Neither previews anything. With--decision,ACTION_NOT_OFFEREDnames it inerror.details.decision_key, and with--criterioninerror.details.criterion_index. A server that does not publish a kind does not offer it, so the new kinds reportACTION_NOT_OFFEREDthere, as any kind a ticket does not offer. -
A missing or unknown action, a missing or malformed ref, an unknown
--family, an empty or overlong--run(over 128 characters),--node(256),--prerequisite(128),--fan-out-key(200),--decision(256) or--delegate-to(128), a--criterionthat is not a whole number from 0,--digestwithout--apply, a malformed acknowledgement code, a follow-up choice, or a plan, start, approve or override choice, that the action does not take, that the action requires and was not given, or that is not valid (see below), or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
Decision follow-up
Section titled “Decision follow-up”A decision that waits on a person — a clarification, a delivery review or a hold review — is
followed up: it is reminded, escalated and, if it stays unanswered, held, and a hold comes back
for review. The decision_followup family lets a person act on that. Its actions target a
decision, named with --decision <decision_key> (the decision_key an attention finding
carries).
| Action | Takes | Does |
|---|---|---|
snooze_reminders |
--until <date-time> |
Stops your reminders until that time |
defer_decision |
--until <date-time> |
Defers the decision to that time, no later than the decision's defer_latest |
hold_decision |
— | Holds the decision; --reason is recorded with the hold |
delegate_decision |
--delegate-to <user_id> |
Hands the decision to that user |
proceed_on_assumption |
— | Proceeds on the decision's recommended answer |
keep_holding |
--until <date-time> |
Keeps a hold that is due for review, to be reviewed again at that time |
The decision's label and consequence say exactly what the server will do.
-
--until <date-time>is an ISO 8601 date-time with an offset —2026-10-12T09:00:00Zor2026-10-12T09:00:00+02:00; seconds are optional. It is accepted only withsnooze_reminders,defer_decisionandkeep_holding.--delegate-to <user_id>is accepted only withdelegate_decision. Either one with another action, a--untilthat is not a date-time with an offset (a date alone, a time with no offset, a 30th of February), or an empty value isCLI_USAGE(exit code 2), and nothing is sent. The CLI does not judge whether the time is in the future, how far away it is, or whether the user may take the decision: the server does. -
The choices travel as
followup: {until, delegate_to}on both the preview and the execute, withnullfor the one not given and nofollowupat all when neither is given. The preview's digest covers them, so execute with the choices you previewed: a different choice at the execute isPREVIEW_STALE(exit 6; preview again), and so is--apply --digest <d>with other choices than the digest's preview.--untilis sent normalised to RFC 3339 (upper-caseTandZ, seconds present); its instant is the one you gave. -
A decision the server blocks — in
pidgen ticket actionsitsrefusal, and inpidgen ticket actthe error — or refuses at the preview or the execute reports the server's code and sentence and exits 5, as every other action does. The refusals the follow-up actions add:Code Sentence FOLLOW_UP_NOT_FOUNDThis decision has no follow-up to change. FOLLOW_UP_TIME_PASTThat time has already passed. Choose a later one. DEFER_TOO_LONGThat is later than this decision may be deferred. error.details.latestis the latest time allowed.DECISION_HELDThis decision is held, so this follow-up does not apply. HOLD_ALREADY_REQUESTEDA hold has already been requested for this decision. HOLD_NOT_APPLICABLEThis decision cannot be held. DELEGATE_NOBODYName the person to delegate this decision to. DELEGATE_NOT_ELIGIBLEThat person cannot take this decision. NO_RECOMMENDED_ANSWERThis decision has no recommended answer to proceed on. HOLD_REVIEW_NOT_DUEThis hold is not due for review yet. HOLD_REVIEWER_REQUIREDOnly a reviewer of this hold may decide it. pidgen review attention --addressee on_holdlists the holds due for review. The published follow-up read,GET /tickets/{ref}/follow-up(the decisions waiting on a person and the holds concerning a ticket), has no command yet;pidgen api get /tickets/PIDGEN-4/follow-upreads it.
Plan, start, approve and override
Section titled “Plan, start, approve and override”These kinds belong to the decision_answers family: the answers a person gives while a ticket is
planned, started and reviewed. Except override_acceptance_criterion, which acts on one acceptance criterion, they act on the
ticket.
| Action | Takes | Does |
|---|---|---|
plan_work |
— | Plans the ticket's work |
start_work |
— | Starts the ticket's work; the preview shows its four checks |
ask_before_starting |
— | Asks before starting; the preview lists the plan's decisions |
correct_plan_decision |
--answer <decision>=<text> or --alternative <decision>=<option>, repeatable, at least one |
Corrects the answers of plan decisions |
approve_delivery |
— | Approves the delivery |
approve_with_exception |
--exception-reason <text> |
Approves the delivery although acceptance criteria are not proven, with the reason on record |
request_delivery_changes |
--note <text> |
Sends the delivery back, --note saying what should change |
override_acceptance_criterion |
--criterion <index>, --criterion-status passing|failing|pending and --override-reason <text> |
Sets one acceptance criterion's status, with the reason on record |
The decision's label and consequence say exactly what the server will do.
-
--criterion <index>names an acceptance criterion by its position in the ticket's list, counting from 0, as a target filter like--prerequisite: it is passed as the actions route'scriterion_indexfilter and matched against each decision'starget.criterion_index.override_acceptance_criterionis offered once for each criterion, so name the one to override; without--criterionand with several offered it exits 2 withACTION_TARGET_AMBIGUOUS. It is accepted with any action; one that is offered for no such criterion isACTION_NOT_OFFERED. -
--exception-reason <text>is sent asexception: {reason};--criterion-statuswith--override-reasonascriterion_override: {status, reason};--answerand--alternativeasplan_correction: {answers}. Each--answer <decision>=<text>answers plan decision<decision>(its number, counting from 0) in your own words (free_text), and each--alternative <decision>=<option>with one of the options it offers (alternative): an answer carries exactly one of the two, the text after the first=is taken whole, answers are sent in the order of their decisions, and a decision answered twice, or more than 50 answers, is a usage error. The choices go to both the preview and the execute, as the follow-up choices do: the preview's digest covers them, and another choice at the execute isPREVIEW_STALE(exit 6; preview again). A reason is trimmed, must not be blank and is at most 2000 characters. -
request_delivery_changesrequires--note: the execute's existingnote(at most 4000 characters) is what should change.--noteis required with or without--apply, so that what is previewed is what is applied; the preview does not send it. -
A choice flag given for a kind that does not take it —
--exception-reasonwith anything butapprove_with_exception,--criterion-statusor--override-reasonwith anything butoverride_acceptance_criterion,--answeror--alternativewith anything butcorrect_plan_decision— and a kind that requires a choice given without it (approve_with_exceptionwithout--exception-reason,override_acceptance_criterionwithout--criterion-statusand--override-reason,correct_plan_decisionwithout--answeror--alternative,request_delivery_changeswithout--note) isCLI_USAGE(exit code 2), and nothing is sent, not even the listing of actions. So is an unknown--criterion-status(onlypassing,failingandpending), a blank value, and an answer that is not<decision>=<text>with a whole decision number from 0. -
The preview prints what the action would do, as the server reports it, in
data.effects: forstart_work,effects.checks, the four checks that must pass before work starts; forask_before_startingandcorrect_plan_decision,effects.decisions, the plan's decisions with their options. The CLI passes them through unchanged.pidgen ticket actionswith--family decision_answerslists these decisions, each blocked one with itsrefusal. -
A decision the server blocks, or a preview or execute it refuses, reports the server's code and sentence and exits 5, as every other action does, naming in
error.details.insteadthe action to take instead when there is one. The refusals these kinds add:Code Sentence MUST_CRITERIA_NOT_PASSINGNot every MUST acceptance criterion is passing. Approve with an exception, or ask for changes. EXCEPTION_REASON_REQUIREDSay why you approve although criteria are not proven. INDEPENDENT_APPROVAL_REQUIREDSomeone other than the author of this work must approve its delivery. REVIEW_NOT_OPENThis ticket's delivery is not open for review. FEEDBACK_REQUIREDSay what should change in the delivery. OVERRIDE_REASON_REQUIREDSay why you are changing this criterion's status. CRITERION_NOT_FOUNDThis ticket has no acceptance criterion at that position. START_CHECKS_FAILEDA check that must pass before work starts did not pass. PLAN_DECISION_NOT_OPENThat plan decision is not open for an answer. WORK_ALREADY_STARTEDWork on this ticket has already started. PLANNING_NOT_AVAILABLEPlanning is not available for this ticket. BACKLOG_STAGE_REQUIREDThis ticket must be in the backlog for this action. The CLI never judges these: whether the criteria pass, who is independent and whether the checks hold are the server's to say, and the sentence is the server's, which may differ from the one published in the contract.
-
A delivery review in
pidgen review attentioncarriesunproven_criteria(the acceptance criteria not yet proven passing, or null) andreviewed_revision(the revision under review, or null) on its finding, passed through unchanged. The published review decision route,POST /tickets/{ref}/review/decisions, takes an optionalexception_reasonand answers withreviewed_revision,exception_reasonandunproven_criteriaon its decision; it has no command, andpidgen api post /tickets/PIDGEN-4/review/decisionsreaches it.
ticket retry and replay
Section titled “ticket retry and replay”$ pidgen ticket retry PIDGEN-4 [--run <id>]$ pidgen ticket retry PIDGEN-4 --apply --digest sha256:9f… [--ack ambiguous-effects] [--reason "…"]$ pidgen ticket replay PIDGEN-4 --node implement [--run <id>]$ pidgen ticket replay PIDGEN-4 --node implement --apply --digest sha256:9f… [--no-wait]- Both recover one workflow run of the ticket through the actions routes, keeping their flags.
They read the ticket's recovery decisions (
GET /tickets/{ref}/actions?family=recovery, withrun_idfrom--runand, for a replay,node_idfrom--node) and take one:pidgen ticket retrytakesretry_run, a fresh attempt of the whole run. For a Refinery run it takesrestart_refinementinstead when the decision says so: theretry_rundecision is blocked withinstead: restart_refinement, or the server offers onlyrestart_refinement.pidgen ticket replaytakesreplay_from_nodefor the node named by--node: it re-runs that node and what depends on it, reusing the rest.
--run <id>names the run. Without it the command takes the first matching decision, the newest run's.- A blocked decision exits with its refusal's code and message before anything is previewed — 5
when not admissible (for example
REFINERY_SELECTIVE_REPLAY_REQUIRED, orREPLAY_REQUIRES_PRODUCER, witherror.details.instead), 3 when not authorized. No matching decision exits 5 withACTION_NOT_OFFERED; an unknown ticket exits 4 with the server'sTICKET_NOT_FOUND. - Without
--applyit previews the decision withPOST /tickets/{ref}/actions/preview({action, target}) and changes nothing;datais the preview, as forpidgen ticket act. --apply --digest <d>executes withPOST /tickets/{ref}/actions/execute, sending the action, target, digest, the--ackacknowledgements,--reasonand the idempotency key; a changed decision is the server'sPREVIEW_STALEand a missing acknowledgement itsACKNOWLEDGEMENT_REQUIRED, both exit 6 witherror.details.nextsaying what to do.--applywithout--digestexecutes a fresh preview only when it requires no acknowledgement; otherwise it exits 6 withDIGEST_REQUIREDand the preview.- A
202is followed on itsstatus_urluntil it settles (--wait, the default);--timeoutbounds it and a timeout exits 8.--no-waitreturns at once.dataafter an apply is as forpidgen ticket act. The server's other refusals keep their codes and exit 5 (or 4 for a missing target), also when reported by the status route. - A missing or malformed ref,
pidgen ticket replaywithout--node, an empty--nodeor--run(or one longer than the server accepts),--digestwithout--apply, a malformed--ackcode, or an extra argument isCLI_USAGE(exit code 2) and sends nothing. - The CLI does not call the older recovery routes (
POST /tickets/{ref}/recovery/preview,POST /tickets/{ref}/recovery, whose request names itsactionandsource_run_id); they stay in the published contract until the App Service retires them.
ticket reopen
Section titled “ticket reopen”$ pidgen ticket reopen PIDGEN-4$ pidgen ticket reopen PIDGEN-4 --apply --digest sha256:9f… [--reason "…"]- Reopens a completed ticket whose completion cannot be proven — it has no delivery evidence, or
none of its execution runs ever succeeded — returning it to
awaiting_reviewand superseding its delivery record. It follows Preview and apply with the shared flags. - Without
--applyit previews withPOST /tickets/{ref}/reopen/previewand changes nothing.datais the preview:plan(ticket_id,from_stagecompleted,to_stageawaiting_review,violated_invariantssuch ascompleted_without_deliveryorcompleted_without_successful_run, aninvariant_detailssentence for each, thedeliveryit supersedes ornull, andexecution_run_statuses),preview_digestandrequired_acknowledgements(empty when none). - With
--apply --digest <d>it sendsPOST /tickets/{ref}/reopenwith the digest, the acknowledgements,--reasonand the idempotency key.--applywithout--digestapplies a fresh preview only when it requires no acknowledgement; otherwise it exits 6 withDIGEST_REQUIREDand the preview. A ticket that changed since the preview exits 6 with the server'sPREVIEW_STALE, and a missing acknowledgement exits 6 withACKNOWLEDGEMENT_REQUIRED;error.details.nextsays what to do. - A reopen settles in the request.
dataafter an apply:settled(true),result(the server's result:command_id,statusapplied,ticket_id,from_stage,to_stage,violated_invariants,deliveries_superseded,findings_resolved,preview_digestandreason) andidempotency_key. - A ticket that cannot be reopened is refused, by the preview or the apply, with the server's code
and exits 5:
REOPEN_NOT_JUSTIFIED(its completion invariants hold, so there is nothing to repair),REOPEN_NOT_COMPLETED(it is not completed),ACTIVE_ATTEMPT_EXISTS(a run is still active on it) orREOPEN_REQUIRES_HUMAN;COMMAND_IDEMPOTENCY_CONFLICTalso exits 5. An unknown ticket exits 4 withTICKET_NOT_FOUND. - A missing, empty,
.or..ref,--digestwithout--apply, a malformed--ackcode, or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
ticket cancel
Section titled “ticket cancel”$ pidgen ticket cancel PIDGEN-4 [--discard-workspace] [--close-pr]$ pidgen ticket cancel PIDGEN-4 --apply --digest sha256:9f… --ack cancels-descendants [--reason "…"]$ pidgen ticket cancel PIDGEN-4 --apply --digest sha256:9f… --close-pr --ack cancels-descendants$ pidgen ticket cancel PIDGEN-4 --apply --digest sha256:9f… --superseded-by PIDGEN-9 [--no-wait]- Cancels a ticket and every descendant: from the apply on no work in its subtree starts again, its
runs are halted and its tickets move to
abandonedas the cancellation converges. It follows Preview and apply with the shared flags (--apply,--digest,--ack,--reason,--idempotency-key,--wait/--no-wait). - Without
--applyit previews withPOST /tickets/{ref}/cancellation/preview, sendingretain_workspace(true, orfalsewith--discard-workspace) andclose_pull_request(false, ortruewith--close-pr), and sends nothing else.datais the preview:plan(ticket_id,ticket_key,cancelled_tickets— the ticket and every descendant it cancels, deepest first, each withticket_key,titleandstage—descendant_count,blocked_dependentsleft blocked outside the subtree (they are not cancelled), theworkspaceit governs ornull, withbranch,is_ownerandreleased,retain_workspace, thepull_requestof the ticket's delivery ornull— itsurl,numberandstate(open,closedormerged) — and the ticket's latestdelivery_operationornull),preview_digestandrequired_acknowledgements:cancels_descendantswhen descendants are cancelled,discards_workspacewhen the workspace branch is deleted. Adelivery_operationis a push, a pull-request update or an apply a worker is running or ran for the ticket: itsoperation_id,kind(push,pr_updateorapply),ownership_generation,worker(host_idandname),started_at,observed_atandstate(in_flight,settledorunknown). The CLI prints both as the server answers them. - The pull request.
--close-prcloses the ticket's open, unmerged pull request as part of the cancellation. Without it the default isclose_pull_request: false, and an open pull request is left open: cancelling a ticket does not touch its pull request unless you ask. A merged pull request is never touched. The choice goes to the preview and to the apply, and the preview's digest covers it, so an apply whose choice differs from its preview's —--close-pron one and not the other — exits 6 with the server'sPREVIEW_STALE: preview again with the choice you want. Preview with--close-prand apply with--close-pr. - With
--apply --digest <d>it sendsPOST /tickets/{ref}/cancellationwith the sameretain_workspaceandclose_pull_request, thedisposition, the digest, the acknowledgements given with--ack(never added on your behalf),--reasonand the idempotency key.--dispositioniscancelled(the default: stop now; it may be wanted later),wont_do(orwont-do: deliberately not doing it) orsuperseded;--superseded-by <ref>names the replacing ticket, is sent assuperseded_by, and impliessuperseded.--applywithout--digestapplies a fresh preview only when it requires no acknowledgement; otherwise it exits 6 withDIGEST_REQUIREDand the preview. - A subtree, blocked dependents or workspace that changed since the preview — or a
--discard-workspacethat differs from the preview's — exits 6 with the server'sPREVIEW_STALE: preview again. A missing acknowledgement exits 6 withACKNOWLEDGEMENT_REQUIRED,error.details.missinglisting the codes anderror.details.nextthe--ackflags that accept them. - The published apply answers
200.dataafter an apply:settled(true),result(the server's cancellation:cancellation_id,statusapplied,state—accepted,haltingorconverged—ticket_id,disposition,superseded_by,retain_workspace,close_pull_request,pull_request_close,reason,cancelled_ticket_idsandrequested_at) andidempotency_key.pull_request_closesays what became of the pull request:left_open(the default: an open pull request stays open),pending(closing it is still to happen),closed,failed(it could not be closed),merged(it was already merged, so it is left as it is) ornot_required(there was none to close). Repeating the apply answers the same cancellation and starts nothing new. Should the server answer202,--wait(the default) follows the result'sstatus_urluntil it settles, bounded by--timeout(a timeout exits 8), and--no-waitreturns at once withsettled: falseand the status route ashandle. - A ticket that cannot be cancelled is refused, by the preview or the apply, with the server's code
and exits 5:
ALREADY_TERMINAL(it is completed or abandoned; a failed ticket can be cancelled),ALREADY_DELIVERED(its change was merged; undo it with new work),DELIVERY_IN_FLIGHT(a delivery operation is in flight; retry once it settles) orPUBLICATION_RESULT_UNKNOWN(a delivery operation's result is unknown; it must be settled first). The last two keep their code and exit status and print the operation inerror.details.operation, with the same fields as the preview'sdelivery_operation, so you can see which operation, on which worker, blocks the cancellation. Nothing is cancelled on a refusal. An unknown ticket exits 4 withTICKET_NOT_FOUND; an unknown--superseded-byticket is the server'sSUPERSEDING_TICKET_NOT_FOUND. - A missing, empty,
.or..ref,--digestwithout--apply, a malformed--ackcode, an unknown--disposition,--disposition supersededwithout--superseded-by,--superseded-bywith another disposition or a malformed ref, or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
ticket clarify
Section titled “ticket clarify”$ pidgen review list$ pidgen ticket clarify PIDGEN-7 -f answers.yaml-
Answers, or abandons, the questions a ticket's refine gate is parked on.
pidgen review listlists the tickets waiting on one, andGET /tickets/{ref}/clarification(for example throughpidgen api get) shows its questions, each with thekeyto answer it under, itsanswer_type(text,boolean,single_selectfromchoices, ormulti_select) and whether it isrequired. -
The file is the request body, as JSON or YAML (
-f -reads standard input):answers, each question'skeyto its answer; optionallyoutcome—answered(the default) orabandoned, which is deprecated: against a server that publishes decision follow-up it now places a hold on the decision and leaves the question open, where it used to close the clarification — anotesaying why,gate_generationandidempotency_key. The--helpsays so too.answers:login_method: passwordremember_me: truenote: Agreed with the product owner.It is checked against the answer schema before anything is sent; a file that does not parse or match, or an
answeredfile with no answer, exits 2 withINPUT_INVALID. -
The CLI first reads the open clarification with
GET /tickets/{ref}/clarification, then sends the file withPOST /tickets/{ref}/clarification. When the file names nogate_generation, the CLI adds the one that read reported (when a run holds a live gate), so the answers settle the question you saw; this is the one field it adds to the file. -
datais the server's result:outcome,assumptions_written(each write the answers made to the ticket, in question order),gate_generation,intervention_id,resolved_by,gate_signalled,decision, and therefinement_command_idandrefine_workflow_idof the fresh refinement run the ticket resumes in, ordispatch_errorsaying why resuming failed (the answers are kept and the server retries), andhold: the hold anabandonedoutcome placed, else null. -
A ticket with no open question is a refusal, not a missing ticket: the server's
CLARIFICATION_NOT_FOUND, from the read or the answer, exits 5 with that code. An open record that addresses an earlier gate generation (the read'sstale) exits 5 withINTERVENTION_STALEand sends no answers, as does the server's ownINTERVENTION_STALE; read the ticket again. The server's answer refusals keep their codes and exit 2:CLARIFICATION_ANSWER_REQUIRED,CLARIFICATION_GENERATION_STALE(the gate moved past the file'sgate_generation),CLARIFICATION_UNKNOWN_QUESTION,CLARIFICATION_ANSWER_TYPE_INVALID,CLARIFICATION_CHOICE_INVALIDandCLARIFICATION_QUESTIONS_REQUIRED. An unknown ticket, or another Account's, exits 4 withTICKET_NOT_FOUND. -
No
-f, a missing, empty,.or..ref, or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
ticket instructions
Section titled “ticket instructions”$ pidgen ticket instructions PIDGEN-7$ pidgen ticket instructions PIDGEN-7 --kind steer- Lists the instructions of one ticket, across its sessions and in every state
(
GET /tickets/{ref}/instructions).datais the server's array unchanged, each instruction aspidgen sessiondescribes it: itsstate, with the live path'sdispatchedandaccepted, itsmode, and, for an instruction a running session returned, itsreturn_code. The help (--help) describes each state.--kind steeror--kind request_changesis sent askindand limits the list to steers or torequest_changesinstructions; without it the CLI sends nokindand the server lists every kind. - An unknown ticket, or another Account's, exits 4 with
TICKET_NOT_FOUND. An unknown--kind, a--kindwith no value, a missing, empty,.or..ref, or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
ticket history
Section titled “ticket history”$ pidgen ticket history PIDGEN-7$ pidgen ticket history PIDGEN-7 --show people$ pidgen ticket history PIDGEN-7 --after 40 --limit 20$ pidgen ticket history PIDGEN-7 --all- Reads what happened to one ticket, oldest first (
GET /tickets/{ref}/history). In JSON mode, it prints one envelope.datais the server's page:ticket_id,entriesandnext_after. Each entry hassequence,occurred_at,kind(stage_change,command,review_decision,cancellation,intervention,owner_change,epic_changeoredit),category(peopleorobservations),actor(kind,idanddisplay_name, ornull),summary,reason,statusandcode; the last three arenullwhen the entry has none.metais the response's. --showis sent asshowand keepspeople(what a person did or decided),observations(what the system recorded) oreverything, the route's default.sequenceis the entry's position in the ticket's whole history and stays the same under a filter, so a filtered list can skip numbers.--after <n>is sent asafterand keeps the entries whosesequenceis greater thann.--limit <n>is sent aslimitand caps a page atnentries, 1 to 200; the server's default is 100. A flag not given is not sent.- Without
--allthe command reads one page. When more follow,next_afteris thesequenceto pass as--afterto read the next page; on the last page it isnull. - With
--allthe command followsnext_afteruntil it isnull, asking each page with the same--showand--limit(--limitis the page size), and starting after--afterwhen given.data.entriesthen holds every entry in order anddata.next_afterisnull;metais the first page's. A refusal on any page fails the command rather than printing part of the history. Anext_afterthat does not move on, or pages still coming after 1000, isUNEXPECTED_RESPONSE(exit code 1); a larger--limitneeds fewer pages. - An unknown ticket, or another Account's, or one you cannot read, is the server's
404TICKET_NOT_FOUND(exit code 4). An unknown--show, an--afterthat is not a whole number from 0, a--limitthat is not a whole number from 1 to 200, an option with no value, a missing, empty,.or..ref, or an extra argument isCLI_USAGE(exit code 2) and sends nothing.
intent
Section titled “intent”| Command | Does | Route |
|---|---|---|
pidgen intent submit -f <file> [--submission-key <key>] |
Submit prose work; returns a ticket or clarification questions | POST /intents |
pidgen intent respond <conversation> -f <answers> |
Answer, assume, hand on or leave open an intake request's questions | POST /intents/{conversation_id}/respond |
pidgen intent list [--project <ref>] [--state clarifying|created|withdrawn] |
List intake requests | GET /intents |
pidgen intent show <conversation> |
Show one intake request | GET /intents/{conversation_id} |
pidgen intent withdraw <conversation> [--reason <text>] |
Withdraw a request still waiting on answers | POST /intents/{conversation_id}/withdraw |
A request that needs answers is an intake request: a persisted record that never expires, keeps
the id the intent routes return (conversation_id, also the request's request_id), and has
structured questions. These routes and fields are published in the public API. In JSON mode,
each command prints one envelope; terminal stdout defaults to human-readable output.
-
pidgen intent submit -f <file> [--submission-key <key>]— submits prose work from a JSON or YAML file (-f -reads standard input; see Input files). The body is checked against the submit request schema and sent as written:titleandobjectiveare required,project_idnames the project by id or ticket-key prefix, andapproval_mode,merge_policy,target_lane,epic_id,team_member_id,references,artifact_upload_idsandsubmission_keyare optional; never sendsource_user, since the creator is the caller.--submission-key <key>is sent assubmission_key(1–200 characters): submitting again with the same key and the same body returns the same outcome and creates nothing new, so a retry after a lost response is safe, and the same key with a different body isSUBMISSION_KEY_REUSED(exit code 5). A body file that already has asubmission_keykeeps it unless the option is given, which replaces it; an empty or over-long key isCLI_USAGE(exit code 2) and sends nothing.datais the server's result; switch ondata.outcome:created— the intent became a ticket in Backlog:ticket_id,ticket_key,stage,ticket_url,target_lane,auto_promotion, and therefinement_command_idor thecommand_errorsaying why refinement did not start; a server that publishes durable requests adds therequest_idandassumptions_recorded, how many questions were settled by assuming their recommendation;needs_clarification— nothing was created:conversation_idandquestions, the questions to answer withpidgen intent respond; a server that publishes durable requests addsrequest_idandrequest, the request with its structured questions (seeintent show). The request never expires.
A body that does not parse or match the schema exits 2 with
INPUT_INVALIDand sends nothing. The server's refusals keep their codes:PROJECT_NOT_FOUND,EPIC_NOT_FOUNDandTEAM_MEMBER_NOT_FOUNDexit 4;PROJECT_REQUIRED,READY_LANE_REQUIRES_TEAM_MEMBER,EPIC_PROJECT_MISMATCHandTEAM_MEMBER_ARCHIVEDexit 2;FORBIDDENexits 3;SUBMISSION_KEY_REUSEDexits 5. No-f, or an argument, isCLI_USAGE(exit code 2). -
pidgen intent respond <conversation> -f <answers>— settles questions of the requestpidgen intent submitreturned, by theconversation_id. The file is the request body, as JSON or YAML (-f -reads standard input):answers, each naming its question byquestion_id(from the request'squestions) or, as before, by its 0-basedquestion_index, with anoutcome:outcomeDoes answer(the default)records answer, which is required for this outcomeuse_recommendationassumes the question's recommendation; no answerask_teammateasks the teammate named by ask_user_id; noanswerleave_openleaves the question open; no answerand optionally
delegate: trueto create the ticket now even if questions remain; withdelegate: true,answersmay be empty. The file is checked against the respond request schema before anything is sent: an answer that names no question, anansweroutcome with no text, an unknown outcome, or no answers withoutdelegateisINPUT_INVALID(exit code 2).answers:- question_id: q-example-1outcome: use_recommendation- question_id: q-example-2answer: Email and password.- question_index: 2answer: Yes, for 30 days.delegate: falsedatais the server's result; switch ondata.outcome:createdis the ticket, as forintent submit, withrequest_idandassumptions_recorded, andstill_needs_clarificationmeans the answers were recorded and questions remain (messagesays so, andrequestis the updated request): answer them, or respond withdelegate: true. The request never expires, so there is no deadline to answer by; withdraw it withpidgen intent withdrawif the work is no longer wanted. A conversation that is unknown or another Account's exits 4 with the server'sCONVERSATION_NOT_FOUND.REQUEST_WITHDRAWN(the request was withdrawn) exits 5.QUESTION_NOT_FOUND(no suchquestion_id),ANSWER_EMPTY,NO_RECOMMENDATION(use_recommendationon a question with none),ASKEE_NOT_ELIGIBLEandQUESTION_INDEX_INVALID(an answer names a question the request does not have; nothing is recorded) exit 2, as doREADY_LANE_REQUIRES_TEAM_MEMBER,EPIC_PROJECT_MISMATCHandTEAM_MEMBER_ARCHIVED;PROJECT_NOT_FOUND,EPIC_NOT_FOUNDandTEAM_MEMBER_NOT_FOUNDexit 4;FORBIDDENexits 3. A missing, empty,.or..conversation, more than one, or no-fisCLI_USAGE(exit code 2). -
pidgen intent list [--project <ref>] [--state <state>]—datais one array of the intake requests the caller may see, newest first, each asintent showreturns it; the CLI follows every page aspidgen ticket listdoes.--projectis sent asproject_id, and--stateasstate:clarifying(waiting on answers),created(became a ticket) orwithdrawn. Without--statethe CLI sends none, and the server lists theclarifyingrequests. An unknown state (creatingtoo, which the list does not take), an empty--project, a missing option value, or an argument isCLI_USAGE(exit code 2) and sends nothing. -
pidgen intent show <conversation>—datais the request, unchanged:request_id,project_id,title,text,sourceandsource_location,submitted_byandsubmitted_at, the Agent (team_member_id, andagent_source:chosen,project_defaultorunassigned), itsstate(clarifying,creating,createdorwithdrawn), itsquestionswithquestion_count,answered_countandassumption_count, the ticket it became (ticket_id,ticket_key,created_by, else null), thewithdrawnrecord (by,at,reason, else null),decision_key,followup,version,addressed_to— who it waits on, each with areasonofsubmitter,askedorfallback—waiting_since, andactions: what can be done with it (answer_request,create_request_anyway,withdraw_request,snooze_remindersordefer_decision), each with itsadmissibleandauthorizedverdicts and itsrecommendedreason, or null. Each question has itsquestion_id,text,recommendation(answerandconsequence, or null),choices(or null), whether it isrequired, itsanswer(text,kindofanswerorassumption,byandat, or null), the teammates it wasaskedof and itsleft_openrecord (or null). An unknown or another Account's request exits 4 with the server'sCONVERSATION_NOT_FOUND. A missing, empty,.or..conversation, or more than one, isCLI_USAGE(exit code 2). -
pidgen intent withdraw <conversation> [--reason <text>]— withdraws a request that is still waiting on answers and sends{reason}when--reasonis given, else an empty body.datais the withdrawn request, withstatewithdrawnandwithdrawnsaying who withdrew it, when and why. Two refusals exit 5, as refusals rather than failures:REQUEST_WITHDRAW_FORBIDDEN(a 403: you may not withdraw this request) andREQUEST_ALREADY_CREATED(it already became a ticket;error.details.ticket_idanderror.details.ticket_keyname it). An unknown or another Account's request exits 4 withCONVERSATION_NOT_FOUND; a plainFORBIDDENstill exits 3. An empty--reason, a missing value, or a missing, empty,.or..conversation isCLI_USAGE(exit code 2) and sends nothing.
request
Section titled “request”A Request is work someone asks for: a bug, feature, task or question, with a title,
the requester's own text (body, never rewritten) and, for a bug, its details (steps, expected
and actual results, and the ones that are unknown). It is submitted through a source (a token
submits through the source it is bound to; a web session through the Account's web source) and
triaged: acknowledged, asked about, replied to, accepted, closed, or promoted to a Ticket. Its
state is new, acknowledged, accepted, promoted, resolved or closed, and its id is
conv-…; its page is /requests/{id} on the server. Submitting needs request.submit; every
other write needs request.triage.
The routes are in the published document: every body is validated against it before it is sent
(INPUT_INVALID, exit 2) and every answer after (UNEXPECTED_RESPONSE, exit 1). The writes act
directly, with no preview (Preview and apply).
| Command | Does | Route |
|---|---|---|
pidgen request submit [-f <file>] [--kind <kind>] [--title <title>] [--body <text>] [--steps <text>] [--expected <text>] [--actual <text>] [--urgency <urgency>] [--target <kind>:<id>] [--fingerprint <text>] [--submission-key <key>] [--evidence <file>...] |
Submit a Request, with a bug's details and evidence files | POST /requests, then POST /requests/{request_id}/attachments per file |
pidgen request list [--state <state>] [--kind <kind>] [--target <kind>:<id>] [--source <id>] [--query <text>] [--page <n>] [--per-page <n>] |
List Requests | GET /requests |
pidgen request show <request> |
A Request, its requester and its timeline with attachments | GET /requests/{request_id}, GET /requests/{request_id}/timeline |
pidgen request reply <request> --text <text> [--ask] |
Reply to the requester, or ask them a question | POST /requests/{request_id}/reply or /ask |
pidgen request acknowledge <request> |
Tell the requester it was seen | POST /requests/{request_id}/acknowledge |
pidgen request accept <request> [--priority P1|P2|P3|P4] [--unknown steps|expected|actual ...] |
Accept it as work to do | POST /requests/{request_id}/accept |
pidgen request close <request> --reason <reason> [--note <text>] [--duplicate-of <request>] |
Close it with a reason | POST /requests/{request_id}/close |
pidgen request promote <request> [--project <project>] [--workflow-type <type>] [--no-refine] |
Promote it to a Ticket in the Project it targets | POST /requests/{request_id}/promote |
pidgen request submit— builds the body{kind, title, body, details?, urgency?, target?, external_ref?, external_url?, requester?, fingerprint?, submission_key?}.-fwith a.json,.yamlor.ymlfile (or standard input starting with{) is that body, and the flags override it; any other file (e.g.report.md) is thebodytext. Give the body with--bodyor-f, not both.--steps,--expectedand--actualare a bug'sdetails.--kindisbug,feature,taskorquestion;--urgencylow,normal,highorurgent;--targetproject:<id>ordomain:<id>(a bare id is a Project; without it the Request is aimed at the Account's domain).--fingerprintnames the same problem across submissions, so a match is counted as an occurrence of the open Request, or a regression of a resolved one. Without--submission-keyone is generated, so a retry after a network failure is the same submission. Each--evidencefile is read before anything is sent (a missing one isCLI_USAGE, exit 2) and uploaded after, as multipart, to the returnedoccurrence_id, or elseentry_id.datais{outcome, url, request, entry_id, occurrence_id, attachments}:outcomeiscreated,appended,occurrenceorregression,urlthe Request's page, andattachmentseach uploaded file's{id, name, media_type, size}. A refused upload (FILE_TOO_LARGE,REQUEST_ATTACHMENT_TYPE_REFUSED,REQUEST_ATTACHMENTS_LIMIT) keeps its code and exit, and itserror.detailsname the Request already filed (request,outcome,url,file,uploaded).pidgen request list—--state,--kind,--target,--sourceand--queryare sent asstate,kind,target_kindandtarget_id,source_idandq. Without--pageevery page is followed to the last; with--page(and--per-page, 1–100) only that page is read.datais the list of Requests, newest first.pidgen request show—datais{request, timeline}. The Request carries every field the server states:state,waiting_on_requester,kind,title,body,details,urgency,priority,labels,target,source,external_ref,external_url,requester,fingerprint,occurrence_count,close_reason,close_note,duplicate_of,promoted,version,created_atandupdated_at;urgency,target,sourceandrequestermay be null. Itsrequesteris{name, contact}, andcontactis left out when the server withholds it (a caller withoutrequest.triage). Each timeline entry is{id, kind, author, text, channel, attachments, files, created_at},kindbeingrequester,reply,note,systemoroccurrence, oldest first:authoris{actor, name, contact},attachmentsthe ids of the files attached to it, andfileseach one's{id, name, media_type, size}, in the same order.reply,acknowledge,accept,closeandpromoteread the Request first (GET /requests/{request_id}) and send itsversionwith the write, so a Request changed in between is refused withREQUEST_VERSION_CONFLICTrather than overwritten.datais the Request in its new state;replyanswers{state, request, entry_id}andpromote{state, request, ticket_id},statebeing the Request's new state.reply --askasks a question, after which the Request waits on the requester (waiting_on_requester).accept --unknownnames a bug's details accepted as unknown.closerequires--reason, one ofcannot_reproduce,wont_do,duplicate,invalid,answered,no_response,withdrawnandother;duplicaterequires--duplicate-ofandotherrequires--note, checked before anything is sent (CLI_USAGE, exit 2).promotemakes an accepted Request a Ticket in the Project it targets;--projectconfirms that Project, and another, or a Request that targets a Domain or nothing, is refused withREQUEST_TARGET_NEEDS_PROJECT. It carries every attachment to the Ticket and starts refinement unless--no-refine; a retry answers the same Ticket.
The Request refusals print the CLI's own sentence in error.message (the server's message is kept
in error.details.server_message) and exit with the same code whatever the HTTP status:
error.code |
Exit | Sentence |
|---|---|---|
REQUEST_NOT_FOUND |
4 | No such request in this Account, or one you may not see. |
REQUEST_VERSION_CONFLICT |
5 | The request changed since it was read; show it again and retry. |
REQUEST_TRANSITION_REFUSED |
5 | The request's state does not allow that; a promoted request no longer changes. |
REQUEST_RATE_LIMITED |
5 | Too many requests came through this source; wait and submit again. |
REQUEST_SOURCE_PAUSED |
5 | The source this credential submits through is paused; nothing was filed. |
REQUEST_BUG_DETAILS_REQUIRED |
5 | A bug is accepted only with its steps, expected and actual results; name the missing ones with --unknown. |
REQUEST_TARGET_NEEDS_PROJECT |
5 | The request does not target that Project, or targets a Domain or nothing; move it to a Project first. |
REQUEST_REPLIES_OFF |
5 | The request's source does not carry replies to the requester; add a note instead. |
session
Section titled “session”| Command | Does | Route |
|---|---|---|
pidgen session steer <session> (--text <text> | -f <file>) [--queue] [--attach <artifact>]… [--quote-message <id>]… [--quote-file <path>:<start>-<end>@<snapshot>]… |
Give an agent session an instruction, delivered live to the running session or to the step's next attempt, with its mode, attachments and quotes | POST /sessions/{session_id}/instructions |
pidgen session instructions <session> |
List a session's instructions | GET /sessions/{session_id}/instructions |
pidgen session edit-instruction <session> <instruction> (--text <text> | -f <file>) |
Change the text of an instruction that has not been delivered | GET /sessions/{session_id}/instructions, PATCH /sessions/{session_id}/instructions/{instruction_id} |
pidgen session withdraw-instruction <session> <instruction> |
Withdraw an instruction that has not been delivered | POST /sessions/{session_id}/instructions/{instruction_id}/withdraw |
Steering an instruction is giving one agent session something to act on, with a receipt. The instruction is queued for that session, then reaches the agent one of two ways:
- Live. While the step's attempt is running on a worker that supports live delivery, the
instruction is dispatched to that running session and accepted into the agent's running
conversation, then acknowledged when that attempt settles.
consumed_atis set when the provider shows the agent read it. - Next attempt. Otherwise, or when the running session cannot take it, it is delivered once
to that step's next prompt (the session's next turn or, if the session ended, the step's next
attempt), and then acknowledged. An instruction a running session returned is
queuedagain with areturn_codesaying why (for exampleHARNESS_UNSUPPORTEDorATTEMPT_SETTLING): the running session did not receive it.
It is reported unapplied when the step never runs again. Only a queued instruction can be
edited or withdrawn. acknowledged means the agent received the instruction; it does not mean
the instruction was implemented. Read the work itself to see what the agent did. Each instruction
has a mode: steer reaches the agent at its next safe point, after the current tool calls;
queue reaches it when it would otherwise finish its current work. pidgen session steer sends
queue with --queue, and otherwise leaves the mode to the server's default, steer. The state
queued is not the mode queue.
<session> is the agent session's id and the Account comes from the credential.
In JSON mode, each command prints one envelope, and data is the server's instruction (or array of
instructions) unchanged. Terminal stdout defaults to human-readable output.
An instruction has:
-
instruction_id,kind(steer, orrequest_changes, an instruction made by requesting changes rather than by steering; the CLI creates only steers),ticket_id, and the step it targets:target_run_id,target_node_id,target_fan_out_key,target_session_idandtarget_label(each null when the target does not name one); -
author, a person:kind(user,agentorsystem),user_id(null for an Agent or the system) anddisplay_name; -
text,items(eachitem_idandtext) andartifact_refs, the artifacts pinned to it: eachartifact_id,name,media_type,size_bytes, theversionpinned, itsauthorand whether it is stillavailable;source_message_ids,presented_message_ids,reviewed_revisionandrequest_key; -
state:queued(not yet given to the agent),dispatched(sent to the step's running session),accepted(in the agent's running conversation),delivered(a prompt carried it, indelivered_session_id),acknowledged,unapplied(the step never ran again;unapplied_reasonisstep_not_run_again) orwithdrawn, withqueued_at,dispatched_at,accepted_at,delivered_at,acknowledged_at,unapplied_atandwithdrawn_at, each null until it happens; -
the live path's evidence:
consumed_at(when the provider showed the agent read it; not every provider shows it),provider_message_ref(the provider's identifier for the input, where it returns one) andreturn_code(why it last returned from a running session toqueued; null if it never did); -
mode,steerorqueue, anddeadline_seconds, reserved and null in this release; -
quoted_event_refs(eachsession_id,event_idanddigest) andquoted_file_refs(eachpath,side,start_line,end_line,source,snapshot_commit_sha,turn_id,captured_atanddigest): what the author quoted, as provenance only; -
editable, whether it can still be edited or withdrawn, andversion, which changes whenever the instruction does. -
pidgen session steer <session> (--text <text> | -f <file>)— gives the session the text as an instruction and prints it in the state the server reports (queued, ordispatchedwhen the server sent it straight to the running session), with itsmode. The help (--help) describes live delivery and each state. Give the text with--text, or with-f <file>(-f -reads standard input), exactly one of them: it is plain text, not a JSON or YAML body, and surrounding whitespace, such as a file's final newline, is removed. It must be 1–8000 characters. The command sends one generatedidempotency_keywith the text, and sends it again unchanged if a response is lost, so a retried command does not queue the instruction twice; running the command again by hand queues another. The server refuses with a409—STEP_SETTLED(the step already finished),RUN_PAUSED,RUN_HELD,RUN_TERMINAL(the run has ended) orSTEER_TARGET_NOT_PROMPT(the step takes no prompt) — and each is a refusal, exit code 5. An unknown session exits 4 withSESSION_NOT_FOUND.STEER_EMPTY,STEER_TOO_LONGandARTIFACT_REFERENCE_UNAVAILABLEandMODE_UNSUPPORTED(a422) exit 2, and a403exits 3. Giving both--textand-f, or neither, empty text, text over 8000 characters, an unreadable file (INPUT_INVALID), a missing, empty,.or..session, or an extra argument is a usage error (exit code 2) and sends nothing.The composer's flags are each sent only when given, and each repeatable flag keeps the order given:
--queuesendsmode: queue: the agent gets the instruction when it would otherwise finish.--attach <artifact>(repeatable) pins an existing artifact by id, sent asartifact_refs.- Planned:
--upload <file>, staging a local file and pinning it (the request'supload_ids). It waits for the public API to publish an upload route (API compatibility). --quote-message <id>(repeatable) records a conversation message the text quotes, sent assource_message_ids.--quote-file <path>:<start>-<end>@<snapshot>(repeatable) records linesstarttoendofpath, from the new side of the snapshot commitsnapshot(7 to 64 hexadecimal characters), sent as aquoted_file_refsentry withsource: snapshot. The CLI readspathfrom the working directory, which should hold that snapshot, and sends thesha256:digest of the quoted lines (joined with a newline, without a final one).
The quotes are provenance only: the text is the instruction, so put the quoted words in it. At most 10 artifacts in all. Every flag is checked and every file read before anything is sent: an empty
--attachor--quote-message, more than 10 attachments, a--quote-filefile that cannot be read (INPUT_INVALID), or a--quote-filethat is not in that form, counts from line 0, ends before it starts, ends past the file's last line or names a snapshot that is not a commit is a usage error (exit code 2). -
pidgen session instructions <session>— lists the instructions steered to the session, newest first, in every state, as one array. An unknown session exits 4 withSESSION_NOT_FOUND. A missing, empty,.or..session, or an extra argument, isCLI_USAGE(exit code 2). -
pidgen session edit-instruction <session> <instruction> (--text <text> | -f <file>)— changes an instruction's text and prints the edited instruction. The text is given as forsteer. The CLI first reads the session's instructions (the list route) and sends the named instruction's currentversionwith the new text, so the edit is made to the instruction you saw; an instruction the session does not list exits 4 with the CLI's ownINSTRUCTION_NOT_FOUND, and nothing is changed. Whether the instruction can still be edited is the server's decision, so the CLI sends the change even when the list shows it delivered.INSTRUCTION_ALREADY_DELIVERED(a prompt already carried it) andINSTRUCTION_VERSION_STALE(it changed after the read) are409refusals, exit code 5.INSTRUCTION_NOT_AUTHORis a403: only the author may edit, and like every403it exits 3. Usage errors are as forsteer, plus a missing instruction. -
pidgen session withdraw-instruction <session> <instruction>— withdraws the instruction and prints it, withstatewithdrawn. The request has no body and is not retried after a lost response, because a second attempt would be answered as a refusal.INSTRUCTION_ALREADY_DELIVEREDis a refusal (exit code 5),INSTRUCTION_NOT_AUTHORexits 3, and an unknown session or instruction is the server's404(exit code 4). A missing, empty,.or..session or instruction, or an extra argument, isCLI_USAGE(exit code 2) and sends nothing.
An Epic is a ticket of kind epic. It is filed, listed, read and edited with the ticket commands
and --kind epic, a ticket sits in it through its parent_ticket_id, and edges between Epics and
tickets are ordinary dependencies. The pidgen epic commands are aliases onto those ticket
commands, with the flags they always had, so existing scripts keep working:
| Command | Alias of | Route |
|---|---|---|
pidgen epic list [--project] |
pidgen ticket list --kind epic [--project] |
GET /tickets?kind=epic |
pidgen epic show <key> |
pidgen ticket show <key> --kind epic |
GET /tickets/{key}, GET /tickets/{key}/prerequisites |
pidgen epic create -f <file> [--domain <domain>] |
pidgen ticket create --kind epic -f <file> [--domain <domain>] |
POST /tickets |
pidgen epic edit <key> -f <file> |
pidgen ticket edit <key> --kind epic -f <file> |
GET /tickets/{key}, PATCH /tickets/{key} |
pidgen epic dag show <key> |
— (the Epic's dependency graph) | GET /tickets/{key}/dependencies/graph |
pidgen epic edge add <key> --from <ref> --to <ref> [--type] |
pidgen ticket dep add <--to> --on <--from> [--type] |
POST /tickets/{--to}/dependencies |
pidgen epic edge rm <key> --from <ref> --to <ref> [--type] |
pidgen ticket dep rm <--to> --on <--from> [--type] |
DELETE /tickets/{--to}/dependencies/{--from} |
{key} is the Epic's ticket key, such as PIDGEN-40, or its id (TKT-…). Each alias behaves, and
answers, exactly as the command it stands for (see ticket); a server refusal keeps its
own error.code and exits with the code for its status
(Output and exit codes).
pidgen epic list [--project <id>]—datais every Epic you can see, every page followed.--projectis sent asproject_id. An empty--project, or an argument, isCLI_USAGE(exit code 2) and sends nothing.pidgen epic show <key>—datais the Epic with itsprerequisites. A ticket that is not an Epic isTICKET_KIND_MISMATCHand an unknown one the server'sTICKET_NOT_FOUND; both exit 4.pidgen epic create -f <file>— the file is a ticket create body, sent withkind: epic(seepidgen ticket create). The retired epic fieldspriority_weight,target_completionandcreate_feature_branchare not part of it: a file naming them exits 2 withINPUT_INVALID; usepriority,target_dateandbranch_mode.--domain <domain>files a Domain Epic, aspidgen ticket create --domaindoes.pidgen epic edit <key> -f <file>— the file is a ticket edit body (seepidgen ticket edit):title,objective,priority,merge_policyand the other content fields. A ticket that is not an Epic isTICKET_KIND_MISMATCH(exit 4) and nothing is changed.pidgen epic dag show <key>—datais the server's graph centred on the Epic unchanged: itsticket_idandticket_key; the tickets it waits for and that wait for it asnodes(each withid,ticket_key,title,stage,status_label,situation,depthandon_critical_path) and theblocksedgesbetween them; and its members aspieceswith thepiece_edgesbetween them. An unknown ticket exits 4 withTICKET_NOT_FOUND. A missing, empty,.or..key, or more than one, isCLI_USAGE(exit code 2) and sends nothing.pidgen epic edge add <key> --from <ref> --to <ref> [--type <type>]— declares that--todepends on--from, either of which may be an Epic or a ticket of the same project.<key>names the Epic for the caller and is not sent. The refusals and usage errors are those ofpidgen ticket dep add; a missing, empty,.or..<key>,--fromor--toisCLI_USAGE(exit code 2) and sends nothing.pidgen epic edge rm <key> --from <ref> --to <ref> [--type <type>]— removes that edge, aspidgen ticket dep rmdoes.
Child plans: a root ticket and the children that share its workspace.
| Command | Does | Route |
|---|---|---|
pidgen plan show <root> |
The current child plan, its version and digest | GET /tickets/{ref}/child-plan |
pidgen plan apply <root> -f <plan> |
Preview/apply a complete child plan and its dependencies | POST /tickets/{ref}/child-plan/preview, PUT /tickets/{ref}/child-plan |
pidgen plan promote <root> |
Preview/apply promotion; the preview lists failing gates | POST /tickets/{ref}/promotion/preview, POST /tickets/{ref}/promotion |
pidgen plan template |
Write an annotated plan file to fill in | — |
pidgen plan cohorts -f <slices> [--max-children <n>] |
Group planned slices into delivery cohorts by the files they edit; local, sends nothing | — |
plan show
Section titled “plan show”$ pidgen plan show PIDGEN-12- Reads
GET /tickets/{ref}/child-planfor the high-level ticket<root>. datais the server's child plan:root_ticket_id,ticket_version(the root's version),materialized_plan_digest(the current plan's digest,nullbefore any plan was applied),items(each current child with itsplan_item_id),dependenciesbetween them byplan_item_id, andvalidation_findings. A response withoutticket_versionandmaterialized_plan_digestisUNEXPECTED_RESPONSE(exit code 1).- A ticket with a parent is refused with
HIGH_LEVEL_TICKET_REQUIRED(exit code 5); an unknown ticket isTICKET_NOT_FOUND(exit code 4).
plan apply
Section titled “plan apply”$ pidgen plan apply PIDGEN-12 -f child-plan.yaml$ pidgen plan apply PIDGEN-12 -f child-plan.yaml --apply --digest sha256:9f… [--ack supersedes-children]-f <plan>is required; without it the command exits 2 (CLI_USAGE) and sends nothing. The file (Input files) is the complete plan —children,dependenciesandsummary, aspidgen plan templatewrites it — and is validated against the child-plan schema before anything is sent; issue paths are relative to the file ($.children[0].objective).- Follows Preview and apply with the shared flags. Without
--applyit previews withPOST /tickets/{ref}/child-plan/previewand changes nothing;datais the preview:plan,preview_digest,required_acknowledgements, theticket_versionandmaterialized_plan_digestit read, and itseffects(created,retained,superseded). - A child may select its Agent with
team_member_id, an Account Agent ID (not a model name) of 1 to 80 characters;nullor another type exits 2 withINPUT_INVALIDbefore anything is sent. Omitted, a kept child keeps its Agent and a new child takes the Project default, else the Account's reserved Unassigned Agent. Eligibility is the server's: an ID that is no Agent of the Account isTEAM_MEMBER_NOT_FOUND(exit 4), and a kept child that has already started keeps its Agent, so selecting another isPLAN_ITEM_AGENT_FIXED(exit 5). - Each child may carry starting briefs — the files to change, the pattern to copy, the contract
rows and the narrowest test — in one of two forms, never both (see Child briefs
below). Both are checked, and a problem exits 2 with
INPUT_INVALIDbefore anything is sent. - With
--applyit sends the same plan withPUT /tickets/{ref}/child-plan, together with the preview's digest, which binds the version and plan digest the preview read, so a plan or ticket changed since is refused withPREVIEW_STALE(exit code 6). Leaving out a current child supersedes it, which the preview lists as thesupersedes_childrenacknowledgement. dataafter an apply:settled,result(the server's result:status,items,superseded_ticket_ids, the newmaterialized_plan_digest),idempotency_key, and, when not settled with--no-wait,handle(the status route).- A refused plan exits 5 with the server's code, for example
PLAN_ALREADY_ADMITTED,BACKLOG_STAGE_REQUIREDorSUBJECT_EXECUTION_READINESS_FAILED— also when the server answers the refusal with200: itsdisposition_codebecomeserror.code, anderror.detailscarries thedisposition_code, anydisposition_detail, and the server'sresult.
Child briefs
Section titled “Child briefs”A child's objective says what it delivers; a brief gives it a starting point at filing time, so the planner does not edit objectives afterwards. A child carries at most one of these two fields in the plan file (JSON or YAML):
| Field | Meaning |
|---|---|
context_artifacts |
A list of at most 3 briefs, each {type: brief, tier: stage, content, reference_url?}: type is brief, tier is stage, content has 1 to 8192 characters, and reference_url is an optional string. Sent unchanged, in the preview and in the apply. |
brief_file |
A path to a text file, relative to the directory of the plan file (to the working directory when the plan is read from standard input; an absolute path is used as it is). The CLI reads it and sends its text, as written, as one {type: brief, tier: stage, content} item in context_artifacts; brief_file itself is not sent. |
children: - key: first title: First slice objective: Deliver the first observable behavior. workflow_type: implementation brief_file: briefs/first.md # read relative to this plan file - plan_item_id: ITEM-EXAMPLE-KEPT title: Follow-up slice objective: Extend the first behavior. workflow_type: implementation context_artifacts: - type: brief tier: stage content: "Files to change: src/follow-up.ts. Narrowest test: tests/follow-up.test.ts."- Omitting both means the child has no brief. For a child kept with
plan_item_id, the stored briefs are replaced only when the child namescontext_artifactsorbrief_file; a child that names neither keeps what it has. - A
brief_filethat is missing or unreadable, is not a file, is empty, or has more than 8192 characters (counted as characters, not bytes), abrief_filethat is not a non-empty string, and a child naming bothbrief_fileandcontext_artifactsare usage errors: exit 2,INPUT_INVALID, witherror.details.issueslocating each at$.children[<n>].brief_file. Acontext_artifactsthat is not a list of at most 3 briefs of typebriefand tierstagewith 1 to 8192 characters of content is refused the same way, at$.children[<n>].context_artifacts. Every problem in the file is listed together, and no request is sent. - The App Service publishes this contract (see API compatibility).
Agent assignment eligibility and active-run effects are a server contract. The server owns
authorization, same-Account membership, archived-Agent eligibility and any effect on accepted or
active execution. The CLI validates only the ID's shape on ticket creation, the one published
place an Agent is selected, permits reserved Unassigned IDs, and returns public server results and
refusals. HTTP 403 exits 3, 404 exits 4, 409 exits 5; PREVIEW_STALE exits 6. Other statuses
follow Output and exit codes.
plan promote
Section titled “plan promote”$ pidgen plan promote PIDGEN-12$ pidgen plan promote PIDGEN-12 --apply --digest sha256:9f… [--reason "Start the cohort"]- Follows Preview and apply with the shared flags. Without
--applyit previews withPOST /tickets/{ref}/promotion/previewand changes nothing;datais the preview:plan(the root, itsticket_version, thecovered_childrenpromotion releases with the root, thedependenciestouching them, thematerialized_plan_digestandreleases_count),preview_digest,required_acknowledgements,passed, every gate ingate_results, and each failing gate infailing_gates— itsgate(for examplematerialized_plan_currentorticket:{id}:execution_readiness),reasonandrepair_target, the ticket to repair. Failing gates are part of the preview, not a refusal: the preview exits 0. On a terminal, standard error also lists the failing gates. A preview withoutpassed,gate_resultsandfailing_gatesisUNEXPECTED_RESPONSE(exit code 1). - With
--apply --digest <d>it sends that digest withPOST /tickets/{ref}/promotion; the server admits the ticket and its cohort only when every gate passed and nothing changed since the preview. A202is followed on itsstatus_urluntil it settles (--no-waitreturns at once). --applywithout--digestpreviews first; when that preview has a failing gate it applies nothing and exits 5 withPROMOTION_GATE_FAILED,error.details.failing_gateslisting the gates to repair.dataafter an apply:settled,result(the server's result:status,command_id,root_ticket_id, the newticket_version,execution_admission_digest,materialized_plan_digest,releases_countandcovered_ticket_ids),idempotency_key, and, when not settled with--no-wait,handle(the status route).- A refused promotion exits 5 with the server's code:
PROMOTION_GATE_FAILED(a gate failed since the preview;error.details.gate_results),ACCOUNT_NOT_ACTIVE,HIGH_LEVEL_TICKET_REQUIRED,BACKLOG_STAGE_REQUIRED,TICKET_PROJECT_REQUIRED,PROMOTION_PREVIEW_LIMIT_EXCEEDEDorCOMMAND_IDEMPOTENCY_CONFLICT. A promotion changed since the preview isPREVIEW_STALE(exit code 6); an unknown ticket isTICKET_NOT_FOUND(exit code 4). - Without
<root>, or with--digestbut no--apply, it exits 2 (CLI_USAGE) and sends nothing.
plan template
Section titled “plan template”$ pidgen plan template [-o child-plan.yaml] [--force]- Writes an annotated child plan to
-o <file>(defaultchild-plan.yamlin the current directory) and sends nothing. The placeholders are valid, so the file as written is a planpidgen plan applyaccepts; replace them first. A.jsonoutput gets the same plan as JSON, without the comments. - Each child's acceptance criteria need only a
statement; the template shows an optionalassertionon one and leavestest_assertion(a pinned check) to a comment. - A comment shows both forms of a child's brief,
context_artifactsandbrief_file, without setting either: the file as written has nobrief_file, which would need a file beside it. See Child briefs. - Comments describe a child's optional
team_member_idwithout setting it; the ticket template shows the optionalteam_member_idat creation and the automatic edit-version handling. - An existing file is not overwritten:
FILE_EXISTS(exit code 5) unless--force. data:pathandformat(yamlorjson).
plan cohorts
Section titled “plan cohorts”pidgen plan cohorts reads a YAML or JSON slice list — each slice's id, the existing files it
will edit, the slices it needs, and optionally its cohort — and proposes delivery cohorts, or
checks the ones you gave. It prints the cohorts, the root-to-root edges they need, the number of
pull requests and the longest chain, and sends nothing to the server. See
Planning work in cohorts.
manifest
Section titled “manifest”Ticket manifests: a new high-level ticket, its children, the edges among them and where they are filed, previewed with every finding at once and committed in one transaction, optionally promoting it. The routes are published by the server (API compatibility).
| Command | Does | Route |
|---|---|---|
pidgen manifest template [-o <file>] [--children <n>] |
Write an annotated manifest to fill in; local, sends nothing | — |
pidgen manifest apply -f <manifest> |
Preview, then with --apply commit, a whole manifest |
POST /ticket-manifests/preview, POST /ticket-manifests |
manifest template
Section titled “manifest template”$ pidgen manifest template [-o ticket-manifest.yaml] [--children 2] [--force]- Writes an annotated manifest to
-o <file>(defaultticket-manifest.yamlin the current directory) and sends nothing. The placeholders are valid, so the file as written is a manifestpidgen manifest applyaccepts; replace them first. A.jsonoutput gets the same manifest as JSON, without the comments. - Every field has a comment, and each content field says the readiness points it earns: a ticket
runs only with 60 of 100 points and an
estimated_complexity. Criteria earn 10 points each (at most 30) and MUST criteria 5 more (at most 20);scope_boundaries10; at least one success metric 10; context artifacts 5 each (at most 15); each open question costs 5; a child, which lands atimplementation_ready, earns 15 more. --children <n>writesnchildren (0 to 50, default 2),child-1tochild-n, chained byblocksedges. Anything else isCLI_USAGE(exit code 2) and writes nothing.- An existing file is not overwritten:
FILE_EXISTS(exit code 5) unless--force. data:path,format(yamlorjson) andchildren.
manifest apply
Section titled “manifest apply”$ pidgen manifest apply -f ticket-manifest.yaml$ pidgen manifest apply -f ticket-manifest.yaml --apply [--digest sha256:9f…] [--idempotency-key <k>] [--reason "…"] [--no-wait]-f <manifest>is required (Input files); the file is the manifest, checked against the manifest schema before anything is sent. An invalid file exits 2 withINPUT_INVALID, every issue located in the file ($.children[0].ref), and sends nothing.- Without
--applyit previews withPOST /ticket-manifests/previewand changes nothing.data:committable,promotable,preview_digest,required_acknowledgements,finding_count,findingsgrouped by ref —manifestfor the manifest as a whole, then the root's ref, then each child's, in manifest order; a criterion's finding is under its ticket — and the server's resolvedplan. A committable preview exits 0, whatever advisory or promotion findings it has. - A preview that is not committable exits 5 with
MANIFEST_NOT_COMMITTABLE, also with--apply, and no commit is sent.error.detailscarries the same report, andblockinglists the codes that block the commit. Fix every finding the preview reports, then preview again. - With
--applyit previews, then commits the same manifest withPOST /ticket-manifests, sending the preview's digest (or--digest), an idempotency key (--idempotency-key, or one derived from the command, the Project and root ref, the digest and the acknowledgements, so a repeated commit is safe), the--ackcodes and--reason. A202is followed on itsstatus_urluntil it settles (--no-waitreturns at once withhandle). dataafter a commit:settled,preview_digest,idempotency_key,tickets(each ref's ticket key, the root first),promoted, the server'sresult, andhandlewhen unsettled.- A manifest that changed since the preview is
PREVIEW_STALE(exit code 6). Other refusals exit 5 with the server's code, e.g.MANIFEST_NOT_COMMITTABLE,PROMOTION_GATE_FAILEDorMANIFEST_IDEMPOTENCY_CONFLICT; an unknown Project isPROJECT_NOT_FOUND(exit code 4). --digestwithout--applyisCLI_USAGE(exit code 2) and sends nothing.promote: truein the manifest is an explicit promotion: commit it only when the person asked for the work to run.
review
Section titled “review”| Command | Does | Route |
|---|---|---|
pidgen review list [--type clarification_needed] |
Tickets waiting on a person | GET /tickets?intervention=… |
pidgen review attention [--addressee me|claimable|following|on_hold|all] |
Open findings that need a person's decision — those with a recommended action first, then the oldest — each with its target, its action decisions and its recommended action, and the intake requests waiting on a person; on_hold lists the holds due for review |
GET /attention |
pidgen review list and pidgen review attention are available read commands. A server refusal keeps
its own error.code and exits with the code for its status (Output and exit
codes).
pidgen review list [--type <type>]—datais one array of every ticket with an open intervention of the type, newest first, each aspidgen ticket listreturns it; the CLI follows every page aspidgen ticket list --intervention <type>does.--typedefaults toclarification_needed, the tickets to answer withpidgen ticket clarify, and acceptsreview_required,execution_failed,child_blocked,delivery_blocked,delivery_closed_unmergedanddelivery_auto_merge_failedtoo. An unknown type, a--typewithout a value, or an argument isCLI_USAGE(exit code 2) and sends nothing.pidgen review attention—datais the server's queue:findingsin the server's order — those with a recommended action first, then the oldest — each with its ticket (ticket_id,ticket_key,title,stage,project_id,parent_ticket_id), what is wrong (code,severity,detail, and therun_id,node_id,fan_out_key,session_idandprerequisite_idconcerned), when it wasfirst_detected_atandlast_seen_at, itstarget(the step, run, dependency or Ticket the decision is about, shaped as an action'starget, or null) andlabel, and the server's further facts:cause_code,effect_safety,recovery_reason,workflow_attention,review_item,related_records,delivery_count,revision,hold_state,hold_until,question,command_id,live_run_hold,decision_key, anaddressee(kind:owner,fallback,worker_owner,delegateorreviewer, and nullableuser_id) and a nullableclaim. A finding has noactionfield: what can be done is in itsactions, its action decisions (aspidgen ticket actionslists them), unchanged. The CLI addsrecommended_action: the recommended decision'saction,label,reason,target,state(allowedorblocked) and blockingrefusal(else null), or null when the finding recommends none — take it withpidgen ticket act. Thenopen_count,ticket_countandswept_at, when the audit sweep last ran: an empty queue is clean only since then;addressee,countsforme,claimable,following,on_holdandall; andfollowingtickets (empty unless the filter isfollowing), each withticket_id,ticket_key,title,project_id,stage,situation, nullablewaiting_on(user_id,display_name) andsource(explicit,owneroradministrator).--addressee me|claimable|following|on_hold|allpasses that filter to the server; without it the CLI sends no filter (the server defaults toall).on_holdlists the holds due for review in your review scope; the queue'sreview_scope(kindaccountorprojects, withproject_ids, or null) says what that scope is, andcountsgainson_hold. Each finding carriesfollowup— where its decision stands in reminders, escalation and holds — andhold, the hold concerning it, or null. A decision due for review offers the follow-up actions, for examplekeep_holding, in itsactions. The CLI preserves every field the server returns. A server that publishes durable intake requests addsrequests— the intake requests waiting on a person, each{decision_key, request}with the request aspidgen intent showreturns it — andrequest_counts(me, the requests waiting on you,all, anditems, the request items that need you); the CLI prints both exactly as the server returns them, as it does the queue'srequest_itemsandcontract_refusals, adds nothing to them, and sends no new option. Against a server that does not publish them the queue has neither field and is printed as before. An unknown addressee, missing option value, argument or unknown flag isCLI_USAGE(exit code 2) and sends nothing.
| Command | Does | Route |
|---|---|---|
pidgen run list <ref> |
Workflow runs of a ticket, newest first | GET /tickets/{ref}/runs |
pidgen run show <run> |
One run, its nodes, terminal reason and stranded state | GET /runs/{run_id} |
pidgen run node-output <run> <node> |
One node's output | GET /runs/{run_id}/nodes/{node_id} |
<run> is a run's id (WFR-…) as pidgen run list reports it in workflow_run_id; <node> is a
node's id in the workflow, such as implement. pidgen run list, pidgen run show and
pidgen run node-output are available. A server refusal keeps its own error.code and exits with the
code for its status (Output and exit codes).
pidgen run list <ref>—datais the server's array of the ticket's workflow runs, newest first (highestattempt_number, ties by the latest start), in the server's order. Each run has itsworkflow_run_id,logical_run_id,attempt_number,workflow_name,status,started_at,ended_at,terminal_reason_code(why it ended;nullwhile it runs),liveness(state—live,suspected,repairingorstranded— withrecovery_count,recovery_limitandcause),parent_attempt_id,pass_countandadmission_block_reason. A ticket with no runs is an empty array. The route is not paged, so the array is the whole list. An unknown ticket, or another Account's, exits 4 with the server'sTICKET_NOT_FOUND.pidgen run show <run>—datais that one run: the same fields,stranded_cause(the cause the runtime recorded when it last found the run making no progress, ornull) andnodes, one entry per node that has an attempt with itsstatus,attempt_count,latest_attempt,started_atandended_at. An unknown run, or another Account's, exits 4 with the server'sRUN_NOT_FOUND.pidgen run node-output <run> <node>—datais that node of the run:status,attempts(every attempt oldest first, each with its ownoutputorerror) andoutput, the latest attempt'svalue,artifacts,evidenceandoutput_digest, ornulluntil the node produces one. An unknown run exits 4 withRUN_NOT_FOUND, and a node with no attempt in the run exits 4 withNODE_NOT_FOUND.- A missing, empty,
.or..ref, run or node, or an extra argument, isCLI_USAGE(exit code 2) and sends nothing;pidgen run node-outputneeds both<run>and<node>.
worker
Section titled “worker”See Workers.
| Command | Does |
|---|---|
pidgen worker install [--runtime wheel|container] [--version <version>] [--force] [--test] |
Read the series to install from the server's worker release list with no credential (GET /workers/releases), take that series' newest release and this machine's wheel from PyPI (TestPyPI with --test), verify the wheel's PEP 740 attestation (or the image's cosign signature over the digest its tag names) against the worker's release identity, then install it with uv tool install --python 3.12 or pull <repository>@<digest>, and write the install record |
pidgen worker update [--version <version>] [--restart] [--test] |
Install the server's current release, or --version, in the recorded form with the same verification as worker install, and update the install record; --restart restarts a detached worker or the container on it, rolling back if it does not start |
pidgen worker uninstall [--purge] |
Stop a detached worker or the container and remove the installed worker (uv tool uninstall, or the container and image) and its install record; --purge also removes the worker home (and the container's volume) |
pidgen worker verify |
Re-check the installed wheel's or image's digest and provenance against the install record |
pidgen worker enroll [--label <label>] [--install|--no-install] [-- args] |
Install the worker when it is missing and --install is given (the verified install of worker install), then create an enrollment (POST /workers/enrollments) and run pidgen-worker enroll --server <server> --no-start; it never asks and never starts the worker (pidgen worker start --detach does) |
pidgen worker start [--detach] [-- args] |
Run pidgen-worker in the foreground, or manage it in the background with --detach; a container install runs the named container pidgen-worker |
pidgen worker stop [--force] |
Stop the detached worker or container, killing it after --timeout only with --force |
pidgen worker status [--host <host_id>] |
Show the detached worker record and local liveness and, with a credential, the App Service's record of this worker (GET /workers) |
pidgen worker doctor [-- args] |
Run pidgen-worker doctor --json (with docker exec in the container) and return its report |
pidgen worker subscriptions <worker> |
Show a worker's provider subscriptions per harness: primary, active, login, entitlement, quota and pause (GET /workers/{host_id}/subscriptions) |
pidgen worker subscriptions use <worker> <harness> <label> [--move-continuing] |
Make one of a worker's subscriptions primary for a harness (GET /workers/{host_id}/subscriptions, then PUT /workers/{host_id}/subscriptions/{harness}/primary) |
worker enroll's data is enrollment (the enrollment's id, Account, label, fingerprint, creator,
creation and expiry times, and state), install (each install option the server returned, its
form, artifact and install step, reported and never run), eula, installed (the install
record of the worker the CLI installed before creating the enrollment, as worker install reports
it, or null when the worker was already present), worker (the pidgen-worker it ran) and
enrolled: true. The one-time token is never in it. A missing worker is installed first, so a failed or declined install spends no
enrollment (Workers).
worker install's data is the install record — form, version, artifact, digest,
identity, verified_at, installed_at, location, file (null for a container) and, for a
container, volume — plus installed (false when the recorded version was already installed
and --force was not given). It prints the licence notice and every command it runs to standard
error first, and refuses with RUNTIME_UNAVAILABLE (exit 2, no uv, or no docker and cosign
for --runtime container), RELEASE_UNSUPPORTED (exit 5) or PROVENANCE_INVALID (exit 5,
nothing pulled, installed or run). worker verify's data is checks (digest and provenance, in pidgen doctor's
shape) and record; a failed check exits 5 with PROVENANCE_INVALID, and no install record exits
4 with WORKER_NOT_INSTALLED (Workers). The release route is
agreed with the App Service and not yet published, so install and update wait on the server
publishing it.
worker update's data is the new install record plus updated (false when the record already
names the release; nothing is installed or restarted), previous_version, running (whether a
detached worker or the container runs), restarted and, after a restart, worker (the new
detached worker or container record). Without --restart a running worker keeps the old version
until it is restarted. If the restarted worker exits during the startup check, the previous
version is reinstalled and restarted and the command exits 1 with UPDATE_ROLLED_BACK
(Workers). No install record exits 4 with WORKER_NOT_INSTALLED.
worker uninstall's data is uninstalled (the removed install record), stopped (the detached
worker or container it stopped, as worker stop reports it, or null), removed (what was
removed), purged and worker_home. No install record exits 4 with WORKER_NOT_INSTALLED
(Workers).
worker status's data is record (the detached worker record, the container's record for a
container install, or null), running and
update (installed, current, needed and reason: protocol_unsupported,
protocol_outdated or newer_available, or null; Workers). With a
credential it adds server (host_id, enrolled, worker — the matching GET /workers entry
with its last-seen time, protocol state and git credential coverage, or null — and the service's
service_protocol and accepted_protocols) and warnings. An unreachable or refusing server
leaves server: null, adds a warning with the error's code, and still exits 0
(Workers).
worker subscriptions
Section titled “worker subscriptions”$ pidgen worker subscriptions worker-host.example.com$ pidgen worker subscriptions use worker-host.example.com claude claude-fallback-1 [--move-continuing]A worker owns its provider subscriptions: which exist, their fallback order and the primary live
in its node.yaml, and the App Service reads what the worker advertises and relays commands to it.
<worker> is the worker's host id, as pidgen worker status and GET /workers report it.
-
pidgen worker subscriptions <worker>—datais the server'sdataunchanged (--format json, the default):host_id,connected,advertised(whether the worker has reported its subscriptions) andharnesses, each with itsharness,primary,activeandsubscriptionsin fallback order. Each subscription has itslabel, whether it is theprimaryand theactiveone,login(ready,not_ready,unknownorduplicate),entitled,last_error,quota(each window'swindow,used_percent,resets_atandobserved_at, ornullwhen unknown),paused, itspauses(each with itscapacity_key,windowandresets_at) andresets_at, when the pause lifts. Quota is as ofobserved_at, never live. Human output labels connection and advertisement state, each harness's primary and active subscriptions, login errors, entitlement, complete quota/pause windows and observation times. Summaries retain each quota window's age (five_hour 42% (12m ago)) and pause (paused until …). -
pidgen worker subscriptions use <worker> <harness> <label>reads the worker's subscriptions first and sendsPUT /workers/{host_id}/subscriptions/{harness}/primarywith{label, expected_primary, move_continuing_steps}, whereexpected_primaryis the primary that fresh read reports, so a change someone made meanwhile is refused rather than overwritten.--move-continuingsendsmove_continuing_steps: true: the worker's continuing steps of that harness move to the new primary and carry their conversation at their next attempt; without it they keep their subscription and only new steps use the new primary.datais the change's result unchanged:request_id,harness,outcome(applied, orunknownwhen the worker did not answer within the App Service's reply timeout — the next read shows what happened),primary,activeanddetail. It exits 0 for either outcome. There is no preview: the route has none, and the expected primary guards the change. -
Each refusal exits 5 with the server's code, a one-line
error.messagefrom the CLI, and the server's own wording inerror.details.server_message:Code Meaning ENTITLEMENT_LIMIT_REACHEDThe Account's plan does not include multiple_subscriptions, so the App Service does not switch a worker's subscriptions. Change it on the worker withpidgen-worker subscriptions use, which is always allowed.WORKER_OFFLINEThe worker is not connected; the App Service queues no command. SUBSCRIPTION_STALEThe worker's primary changed after the read; nothing was changed. Read again and retry. SUBSCRIPTION_UNAVAILABLEThe label is not declared for that harness, not logged in, or a duplicate login. SUBSCRIPTIONS_NOT_ENTITLEDThe worker has not been told the Account holds multiple_subscriptions. -
A harness the worker does not advertise exits 4 with
HARNESS_NOT_ADVERTISED(error.details.advertisedlists the harnesses it does), and noPUTis sent. An unknown worker is the server'sWORKER_NOT_FOUND(exit 4); a caller who is not an Account Owner or Administrator isFORBIDDEN(exit 3). -
A missing or empty worker, harness or label is a usage error (exit 2), and a label the request schema refuses (1–64 letters, digits,
.,_or-, starting with a letter or digit) isINPUT_INVALID(exit 2); nothing is sent.useis a subcommand, so a worker whose host id isusecannot be read with this command.
skills
Section titled “skills”See Skills.
| Command | Does |
|---|---|
pidgen skills install [--agent claude|codex|pi|all] [--scope user|project] [--force] |
Copy the bundled skills into an agent's skill directory |
pidgen skills status [--agent claude|codex|pi|all] [--scope user|project] |
Report installed skills that are stale or edited, and whether the CLI's API contract differs from the server's |
feedback
Section titled “feedback”| Command | Does | Route |
|---|---|---|
pidgen feedback --kind not-working|improvement|other <message> |
Send product feedback about Pidgen itself | POST /feedback |
pidgen feedback is one command with no verb. It tells the people who build Pidgen that something
is not working, could be better, or anything else; it is not about the work in a ticket.
| Flag | Meaning |
|---|---|
--kind <kind> |
Required. not-working, improvement or other, sent as kind not_working, improvement or other; any other value is CLI_USAGE (exit 2) and sends nothing |
<message> |
The feedback, 1–4000 characters |
--message-file <file|-> |
Read the message from a file instead of the argument; - reads standard input. Trailing line breaks are dropped. Giving both, or neither, is CLI_USAGE (exit 2) |
--no-contact |
Send contact_ok: false: Pidgen may not contact you about it. Without it contact_ok is true |
--after <command> <exit> |
The pidgen command the feedback is about and its exit code, sent as last_command and exit_code. Both values are required and the exit code is an integer; a message may follow them |
The body is {kind, message, contact_ok, context}, where context says where the feedback came
from: {source: "cli", cli_version, os, arch, node_version, api_base} (os and arch as Node
reports them, api_base the selected profile's server), plus last_command and exit_code only
when --after is given. The body is validated against FeedbackSubmission in the public API
document before anything is sent: an empty or over-long message is INPUT_INVALID (exit 2) and
sends nothing. data is the server's report: id, reference (8 hex characters to quote when
following up), kind and created_at.
pidgen feedback --kind not-working --after "pidgen ticket show PIDGEN-12" 4 "It said not found, but the ticket exists."pidgen feedback --kind improvement --message-file idea.md --no-contactTwo refusals keep their code and exit 5, whatever the status: FEEDBACK_RATE_LIMITED (a 429: too
much feedback recently; try again later) and FEEDBACK_MESSAGE_TOO_LONG (a 422: the server takes
less than was sent). Other errors exit as their status does (Output and exit
codes).
| Command | Does | Route |
|---|---|---|
pidgen setup [--runtime wheel] [--version <version>] [--label <label>] [--url <url>] [--test] |
The guided path from a fresh machine to a verified worker connected to the App Service | The routes of the commands it runs, and GET /workers |
$ pidgen setup [--runtime wheel] [--version <version>] [--label <label>] [--url <url>]pidgen setup is the primary entry point and the CLI's only interactive command: every other
command never asks. It is composed of the scriptable commands below and prints each one to
standard error, as $ pidgen …, before it runs it, so the whole path can be scripted afterwards.
It runs six steps in order:
Step (id) |
Runs | Does |
|---|---|---|
check |
pidgen doctor |
Runs the doctor checks against the server it will sign in to and stops on a failing check a later step cannot fix, with each check's fix. A failing credential (fixed by signing in) or worker (fixed by installing) does not stop it |
sign-in |
pidgen auth login <url> |
Device-code login. When the User belongs to more than one Account it asks which one the worker serves (pidgen account use <ACC-…> makes it the default; an Account with no stored credential is signed in again and approved for it). Then it says whether the credential may enroll workers, from the role and token scopes GET /me publishes: it may when the scopes include workers:enroll, which an Account Owner or Administrator holds. When it may not, it names those roles and offers the Personal API Token path, pidgen auth login <url> --with-token --account <ACC-…> with a token that has workers:enroll |
runtime |
— | The one question: the worker runtime, defaulting to what the machine has (uv means wheel), shown with the worker's licence notice. Installing accepts the licence. The container runtime is planned, so wheel is the only answer today |
install |
pidgen worker install --runtime <runtime>, then pidgen worker verify |
Installs the release and prints each verification as it passes |
enroll |
pidgen worker enroll --label <label>, then pidgen worker start --detach |
Enrolls this host and starts the worker detached, then reads GET /workers every two seconds until the App Service reports this host connected, for up to the global --timeout (120 seconds by default) |
prove |
pidgen worker doctor |
Runs the worker's own checks, then prints the one next thing to do as a command: pidgen skills install, or, with the skills installed, pidgen ticket template to file a first ticket |
--runtime,--versionand--labelanswer in advance:--runtimeskips the question,--versionpicks the release (default: the server'scurrent), and--labelnames the worker (default: this host's name).--urlis the server (default: the configured one, else the hosted server,https://api.pidgen.ai); naming another server thanPIDGEN_API_URLisCLI_USAGE.- Resuming. Run it again and it resumes: a signed-in profile for the server (a credential
GET /meaccepts) skipssign-in; an installed worker (an install record for the requested version, or, without--version, one the server still accepts) skipsruntimeandinstall; an enrolled host (GET /workerslists this host's worker unrevoked) skips the enrollment, and a running detached worker skips the start.enrollisskippedwhen both are. A credential that cannot readGET /workers(a Personal API Token) counts a running worker as enrolled. - Waiting. A credential that cannot read
GET /workerscannot see the worker connect: theenrollstep reportsconnected: nulland namespidgen worker statusto check with an Owner's or Administrator's sign-in. Otherwise a worker not reportedconnectedbefore the timeout exits 8 withWORKER_NOT_CONNECTED;error.detailshashost_id, the server'sworkerrecord, whether the worker isrunning, and itslog. - Failures. Setup never hides a failure: a failed step stops it, and nothing after it runs. The
error keeps the failing command's own
codeand exit code; its message names the command, the error and the fix, anderror.detailsaddsstep,command(the command that failed),error({code, message, details}),fix, andstepsso far. A credential that may not enroll stops atenrollwithENROLL_NOT_PERMITTED(exit 3) before any enrollment is created. - No terminal. Without a terminal on standard input and standard error it asks nothing, runs
nothing, and exits 2 with
TERMINAL_REQUIRED;error.details.commandslists the equivalent commands in order:pidgen doctor,pidgen auth login <url>,pidgen worker install,pidgen worker verify,pidgen worker enroll --label <label>,pidgen worker start --detach,pidgen worker status(to see the worker connect) andpidgen worker doctor, with--runtime,--versionand the global--profileand--accountas given. - Setup installs nothing the worker needs besides the worker: when a harness,
uvor Docker is missing, thecheckstep's fix says how to install it.
data is steps, one entry per step that ran, in order: id, status (done, skipped or
failed), command (the commands the step runs, joined with &&, whether or not it was skipped;
null for runtime) and summary. sign-in adds account and enroll (allowed, account,
kind, role, scopes, roles that may enroll, the scope that may, and token_command);
runtime adds runtime; install adds version; enroll adds host_id and connected;
prove adds the worker's report and next, the next command.
doctor
Section titled “doctor”| Command | Does | Route |
|---|---|---|
pidgen doctor |
Diagnose this machine and its connection to Pidgen before anything is installed | GET /health, GET /me, GET /workers/releases (pending) |
$ pidgen doctorpidgen doctor is one command with no verb and no flags beyond the global ones. It changes nothing
and works with nothing configured and no worker installed. It differs from pidgen worker doctor, which runs the installed worker's own checks (Workers).
data.checks is a list of {id, status, summary, fix}, always in this order. status is ok,
warn, fail or skip; summary is one sentence saying what was found; fix is the command or
address that resolves the problem, or null when there is nothing to fix. pidgen worker verify
reports its checks in the same shape.
| Check | Looks at | Fails, with its fix, when |
|---|---|---|
node |
The Node.js version this CLI runs on | It is older than 22.12 (https://nodejs.org/en/download) |
server |
GET /health, sent without the credential: the server answers as Pidgen and its API version window includes this CLI's |
The configuration cannot be read (chmod 600 <file> for loose permissions); the server is not Pidgen; its window excludes this CLI's API version (npm install -g pidgen@<channel> when the CLI is too old, the channel being the one this CLI was released on: alpha for an alpha, else latest); or it cannot be reached (the health address) |
credential |
GET /me: the profile's credential is valid, the current Account's role, the token kind (cli or pat) and scopes the server reports for it, and whether it may enroll workers |
No credential is stored for the Account, or the server refuses it (pidgen auth login <url> --account <ACC-…>) |
runtime |
uv on PATH for the wheel runtime; docker and cosign on PATH for the container runtime |
Neither runtime is complete (the uv or cosign installation page), or the platform is not Linux or macOS |
releases |
GET /workers/releases, the server's worker release list, read with no credential and no Account (published) |
The server does not answer it, or its answer does not match the published document (the route's address) |
harness:<kind> |
One check per harness the release list names: its command on PATH. The CLI carries no harness list of its own |
No harness the list names is on PATH (install the harness command); one missing beside another that is found is warn |
worker |
The install record of pidgen worker install against the releases the list accepts |
The installed release is one the server no longer accepts or does not list (pidgen worker install); a supported release that is not current, or no install yet, is warn with the same fix |
credentialisskipwhen theservercheck fails: the credential is never sent to a server that did not pass. With no server configured, the checks run against the hosted server,https://api.pidgen.ai.credentialmay enroll workers when the token's scopes includeworkers:enroll. For a CLI token the server reports the scopes the User's current role grants, so an Account Owner or Administrator may and a Member may not. A credential that is valid but may not enroll iswarn, with what to do.runtimepasses with one complete runtime. A wheel runtime besidedockerwithoutcosigniswarn, with the cosign installation page.releases, theharness:<kind>checks andworkercome from the release list. When theservercheck fails the list is not read:releases,harness(standing for the harness checks) andworkerareskip. When the list cannot be read,releasesfails andharnessandworkerareskip. The release list needs no credential, so it is read even whencredentialfails. Until the App Service publishes the route,releasesfails against a real server.
A failing check exits 1 with DOCTOR_FAILED; the message names the failing checks and
error.details is the full report ({checks}). Warnings and skips alone exit 0 with ok: true.
Arguments and unknown options are CLI_USAGE (exit 2) and send nothing.
pidgen api <get|post|put|patch|delete> <path> [--query key=value]... [-f <file|->] sends one
request to a public-API route with the selected profile's credential. In JSON mode, it prints one
envelope; terminal stdout defaults to human-readable output. It exists so a skill never needs
curl; prefer a named command when one exists, and use --format json to consume structured results.
| Flag | Meaning |
|---|---|
<path> |
A path such as /tickets/PIDGEN-12, with or without the /api/v1 prefix |
--query <key=value> |
Add a query parameter; repeat for more. A key given twice is refused |
-f, --file <file|-> |
Request body as JSON or YAML (Input files); - reads standard input |
- The path and method must match a path template and method in the public API document bundled
with the CLI (the committed
contracts/openapi.json):/tickets/PIDGEN-12matches/api/v1/tickets/{ticket_id}, and a template parameter matches exactly one non-empty path segment. When several templates match, the one with the most literal segments wins. Otherwise the command exits 2 withPATH_NOT_PUBLICand sends nothing;error.detailshasmethod,path, and, when the path is documented for other methods,allowed_methods. Paths with.or..segments, or with an encoded/or\in a segment, are never public. Put query parameters in--query, not in the path. - A
-fbody is read and validated against the operation's request schema before anything is sent; an invalid body, a body for an operation that takes none, or a missing body the operation requires exits 2 withINPUT_INVALID. - On success,
datais the server'sdataandmetacarries the server'srequest_idandapi_versionwith the profile and Account. A server error keeps the server'serror.code, message, and details, and exits with the code for its status (Output and exit codes).
pidgen api get /projects --query limit=10pidgen api post /tickets/PIDGEN-4/retry/apply -f apply.yaml