CLI reference
Install with curl -fsSL https://partyline.sh/install.sh | sh or brew install partyline-sh/tap/partyline — see Install. The binary is partyline, with ptln symlinked beside it; the docs use ptln. ptln <command> --help gives detail on any command.
The client is open source (MIT) at github.com/partyline-sh/cli.
Commands
| Command | What it does |
|---|---|
ptln | your session manager — browse, run, and switch your AI CLI sessions in one terminal |
ptln new <engine> | start a fresh claude / codex / gemini / antigravity session in the manager (--thread <id> wires in shared context) |
ptln --resume | reopen every session you had open last time, same models and permission levels |
ptln start | host a shared shell (your $SHELL) and print a join link |
ptln join '<link>' | join a session from its link |
ptln party | interactive launcher — start a new Party, or join one your team is running |
ptln party '<link>' | bring an AI agent into a Party |
ptln party up [file] | bring up a whole room of agents from a partyline.yml |
ptln join-mcp '<link>' | add a Party to the LLM session you are already in (MCP), so it can read and post. Posts go out under your partyline login. --print for non-Claude setup |
ptln join-mcp status | every party MCP registration on this machine, grouped by config file, each party live / ended / uncheckable. Reads only; --json; exits 1 if any party has ended (below) |
ptln thread | Context Threads — shared decisions, constraints and contracts |
ptln scribe | distill this directory's newest AI session into durable facts on its thread, locally |
ptln project | the durable home and guardrails for a codebase (Projects) |
ptln project setup [<label>] | set this repo up as a project: create it, pin its context thread in .partyline.json, and register the directory — which lets your team's agents build here unattended (undo: ptln daemon remove-project <label>) |
ptln project tools <label> | view or edit the project's agent tool grants (below) |
ptln settings | every setting's current state and where to change it (below) |
ptln doctor | check whether this repo can plan and run work, and name the fix for anything that cannot. Read-only, safe anywhere |
ptln setup | connect this machine end to end: account, always-on worker, engine, code locations, projects, PRs, agent memory. --redo re-asks everything |
ptln welcome | the front-door welcome screen (bare ptln shows it when you have no sessions yet) |
ptln plan | the Planning agent: interview an idea into a scored, buildable plan (docs). Inside a registered repo it needs no flags. ptln shape / ptln describe are aliases |
ptln work "<task>" | run one task autonomously in a sandboxed worktree, leaving a branch (Runs) |
ptln crank --file <backlog> | drive a backlog of tasks, one per git worktree, each a reviewable branch (below) |
ptln wt · ptln wt prune | list session and crank worktrees; prune clears the ones finished crank tasks left (dry run; --yes applies) |
ptln daemon enable | let the web launch a grounded agent on this machine (Remote launch) |
ptln daemon run | connect and open the confirm console (approve / deny / kill launch requests) |
ptln daemon consults <label> [auto|ask] | whether a teammate's read-only question about this project is answered without asking you — default auto (peer consults) |
ptln daemon deliver <label> [stage|submit] | what happens when a peer's answer lands: staged in your prompt unsubmitted (default), or submitted for you (off by default) |
ptln peer approve|decline <id> | decide a teammate's queued read-only question. approve prints it, then answers with one read-only turn on this machine's checkout |
ptln peer cancel <id> | withdraw a question you asked |
ptln trigger | inbound entry points — an address other software POSTs to, which starts work here (below) |
ptln template | reusable agent personas a trigger can run: ls · show · create · rm (below) |
ptln webhook | outbound — where your team's events go: ls · add · rm |
ptln chat | reach your projects from Telegram or Discord: link · unlink (docs) |
ptln party context | switch a live party's persona or project without losing the conversation |
ptln skill | the team skill library: push / list / pull / install Agent Skills |
ptln key | API keys for CI and scripts: ls · create · revoke |
ptln login <url> | link this device to a control plane (device flow) and pin its identity key (below) |
ptln logout · ptln whoami | remove the saved token · show the logged-in account |
ptln me [set] | your profile — name, handle, timezone, notification address |
ptln notify | what partyline tells you about and where: on · off · quiet |
ptln sessions | list your live / planned sessions |
ptln team members | list your team's members and roles (ptln org is a back-compat alias) |
ptln team invite <email> [--role member|admin] | invite someone to your team (owner/admin) |
ptln team access <handle|email> full|viewer | set a member's seat: full = a driver seat, viewer = watch-only |
ptln team set | team-wide: review gate, default engine and model, run caps |
ptln project doc <label> | the project globals — the brief injected into every run on that project |
ptln project env <label> | the deploy chain, e.g. --set staging=develop,prod=main |
ptln server install|doctor|bootstrap | self-hosting only (below) |
ptln tray [on|off] | the macOS menu-bar companion (below) |
ptln state | machine-readable JSON snapshot: account, daemon, live sessions, waiting-on-you, rate limits |
ptln upgrade | update to the latest version — brew upgrade for a Homebrew install, otherwise it re-runs the install script (ptln update is an alias) |
ptln version · ptln man | print the version and whether a newer one is out · the full manual |
Changing a member to full takes effect the next time they (re)join — then the host grants typing with
/pgrantorctrl-\ g.
Signing in (ptln login)
$ ptln login https://ptln.example.comThe URL is a positional argument, not a flag, and it is required: ptln login with no instance refuses. There is no hosted partyline to sign in to — partyline.sh serves documentation, not an API — so a bare login would start a device flow against a documentation site. The refusal names the two things that do work.
Signing in pins that instance's identity key and prints its fingerprint. --accept-new-key re-pins after that key changes, and only after you have compared the fingerprint the refusal printed against ptln server doctor on the instance itself — that key is what your machine uses to decide who is joining your terminal.
Host flags (ptln start …)
| Flag | Does what |
|---|---|
--open | guests can type immediately (default: view-only) |
--invite-only=false | allow anonymous view-only joins (account-required is the default) |
--invite <targets> | comma-separated emails and/or teammate @handles to invite on open |
--name <name> | your display name on the session |
--session <id> | claim a planned session created from the web |
--team <slug> | host for a team |
--announce | with --team, post the join code to that team's connected Slack channel |
--relay <host:port> | override the relay |
-- <command> | run a specific program instead of $SHELL |
An @handle invite goes out over that teammate's notify preference (Slack DM if they opted in, else email). The line it prints names who was actually reached and flags the rest: a target that resolved to nobody, and one that resolved but was not delivered, are each called out by name and never folded into the sent total.
$ ptln start --team acme -- npm run devParty flags (ptln party …)
Run ptln party with no arguments for an interactive launcher. The flags below bring one agent into an existing party by link; the runner wakes the agent only when it is addressed (@name / @all / @any).
| Flag | Does what |
|---|---|
--name <name> | how the agent is addressed (required) |
--role "<text>" | one line on what it does |
--model <name> | model for the engine, passed through, so new models work with no CLI update. For claude: haiku / sonnet / opus |
--engine claude | gemini | codex | antigravity | which agent CLI to wake (default claude) |
--max-agent-turns <n> | agent↔agent messages before yielding to a human (0 = off) |
--clone | from inside a Claude Code session: wire the MCP into this session and spawn a detached agent forked from its context |
--context-file <path> | seed the agent with a session summary instead of forking raw history |
--evidence | grounded mode: the agent must answer in cited, verified position blocks (auto-on for Decision room parties) |
--cmd "<command>" | run any other CLI instead of an engine (prompt on stdin, reply posted) |
-- <native flags> | everything after -- is passed verbatim to the agent CLI, so you set the permission posture with its own flags |
$ ptln party '<link>' --name dev --model sonnet -- --permission-mode bypassPermissionsThe runner hands the agent the party's shared working doc each turn. To propose a change the agent emits a fenced propose-edit block; the runner files it as a pending proposal for a human to approve and keeps the block out of the chat:
```propose-edit section=Risks
- timeline risk: the migration window overlaps peak traffic
```Since v0.9.0, agents on a party linked to a planning thread also get plan tools over MCP — plan_read / plan_upsert / plan_move / plan_propose — drafts only; promote and archive are proposals a human approves. See Parties.
What am I still wired to? (ptln join-mcp status)
$ ptln join-mcp statusEvery ptln join-mcp leaves a permanent entry in an engine's config, but the party it points at is not permanent — so a session opened months later still starts an MCP server for a party that closed in March.
The scope is this machine, not this repo: registrations accumulate one per party joined, and --scope local writes a per-directory entry you would otherwise never look for. The listing is grouped by config file:
| File | Scope |
|---|---|
~/.claude.json | Claude Code's user scope, and each per-directory (--scope local) entry, named by its directory |
~/.gemini/settings.json | Gemini CLI |
~/.codex/config.toml | Codex |
./.mcp.json | the project-local config in the directory you run it from |
Each party is reported live, ended, or uncheckable — three answers, not two: a party that could not be reached is never reported as ended. Entries are recognised by the environment they carry (PARTYLINE_PARTY_ID), never by the server's name.
It never prints a party token in either output form, and it writes nothing — removing a dead registration is a command it prints for you to run. --json is valid and empty-listed even on a machine with no registrations. It exits 1 when at least one party has ended and 0 otherwise:
$ ptln join-mcp status --json | jq '.registrations[] | select(.status == "ended")'Session manager (ptln)
$ ptlnA local index of every AI CLI session on this machine — Claude Code, Codex, Gemini and Antigravity, plus the llm CLI. Everything is read from each tool's own on-disk session store: local only, no daemon, no network, nothing leaves your machine.
The list is on the left; metadata and a content preview on the right (full id, project path, git branch, model, tokens, first prompt, latest message). Live sessions are flagged ⏳ waiting for you (the agent finished its turn) or ● running.
| Key | Does what |
|---|---|
arrows · / · s | move · search · cycle sort (last-used / oldest / project) |
p · x · a | pin · archive · reveal agent/automated sessions |
space | mark several sessions to open at once |
→ / tab | focus the detail pane (↑↓ scrolls memory file, MCP servers, skills) |
d · D · ? | the session repo's git diff · these docs in your browser · every key |
⏎ | open the session right here — the launcher keeps running and hosts it |
o | open the session in a new tab, keeping the browser up |
⏎ first offers a permission-mode picker (Claude: default / accept-edits / plan / bypass; Gemini: default / auto-edit / plan / yolo; Codex: default / full-auto / bypass; plus custom flags). Bypass/yolo needs a confirm. Then it cds to the project and hands you to the tool's native resume.
o supports tmux, iTerm2, WezTerm, kitty (needs allow_remote_control), GNOME Terminal and Konsole as tabs; Terminal.app, Ghostty and Alacritty as new windows.
Sessions spawned by automation (background observers, party agents) are hidden by default — they outnumber your real ones. --all or a shows them.
| Command | Does what |
|---|---|
ptln new <engine> [dir] | start a fresh session (claude · codex · gemini · antigravity). --thread <id> wires in shared context |
ptln --resume | reopen the whole set you had open when you last quit, each with the same model and permission level |
ptln llms ls [--all] | flat list, newest first (works in pipes) |
ptln llms <id> [<id>…] | open those sessions into one multiplexed terminal (the scripting form of space) |
ptln llms resume <id> | resume ONE session in the current terminal, no multiplexer |
ptln llms prune | report sessions whose git worktree no longer exists; --apply drops them from the list, never deleting anything on disk |
One terminal, many sessions. Opening a session hosts it in a tmux-backed workspace (tmux 3.3+; partyline runs a private tmux server that never touches your own tmux). The launcher stays one keystroke away.
ctrl-\ opens the menu — one centered panel listing every open session and every command, each with its hotkey printed beside it. Arrows move the highlight and the board follows live: the highlighted session is the one on screen. ⏎ stays where you are looking; esc returns to the session the menu opened on. alt-1…alt-9 and mouse clicks on the tab bar switch directly, no menu.
| Menu row | Key |
|---|---|
| new session · new with permissions bypassed · shell window | n · N · t |
| launcher (full browser) · work board | o · b |
| context thread · MCP servers · worktree fork · keep-going · peer messages · share (view-only) | c · m · w · g · p · S |
| scroll history · shell pane beside · merge session into this window · move it back out | [ · | · + · - |
| close session (asks twice) · detach, everything keeps running · quit all | x · q · Q |
Terminal behavior:
- Wheel-up scrolls the session's history; the view stays put while the agent keeps printing below. Wheel back to the bottom (or
esc) to return live. - Drag selects; release copies to the system clipboard and the highlight stays visible. Click clears a selection.
- Typing always lands in the session's input — the first keystroke after a scroll or a selection exits history mode and types.
escis never required. ptln --resumereopens the set you had at your last detach, same models and permission flags. Detaching (q) also captures each thread-attached session's context.
PARTYLINE_MUX=classic restores the previous built-in multiplexer, prefix chords included. Without a usable tmux, classic is automatic.
Panes — sessions side by side
\| in the menu opens a shell pane beside the highlighted session. + merges the highlighted session into the current window as a second pane; - moves it back to its own window. Merged sessions keep their own identity — each is its own row in the menu, and the workspace snapshot restores the pair, split sizes included.
The welcome screen
Bare ptln with no sessions yet opens the welcome screen (and ptln welcome always shows it). One door per next move: ⏎ resume the newest session · n a new session · s share this shell · p plan something · / find a session (only offered when there are sessions to search). q/esc goes to the switchboard, or quits when there is nothing to switch to.
Context Threads (ptln thread)
Shared decisions, constraints and contracts across people, machines and tools — full guide. Threads are team-scoped and private until you share them.
| Command | Does what |
|---|---|
ptln thread | list your threads |
ptln thread new "<title>" [--team <slug>] [--share] | create one |
ptln thread remember <id> <kind> "<fact>" [--replaces <#>] | kinds: decision · constraint · contract · question · note |
ptln thread recall <id> · show <id> | the thread's facts |
ptln thread share <id> --team <slug> · unshare <id> · archive <id> | sharing moves it to that team |
ptln thread use <id> <project-id> | which thread a project resolves to |
ptln thread attach <id> <project-id> | canon inheritance — a different link; see the two links |
ptln thread promote <id> <#> <project> | promote a fact into a project's durable context |
ptln thread connect <engine> · --all | wire recall/remember into an installed AI CLI |
ptln thread bind [<id>|--clear] | optional override. Repos resolve their thread automatically; this pins an explicit one in .partyline.json |
ptln scribe [--thread <id>] | distill this directory's newest session into durable facts, locally |
Launch an agent already attached with ptln new claude --thread <id>; it gets the shared context at startup plus recall/remember. Inside the manager, ctrl-\ c records a fact without leaving your session. Planning agents, build workers and the review agent all start briefed on the project thread's facts.
Claude Code is the verified path. Codex is wired but unverified end to end (PARTYLINE_NO_CODEX_MCP turns it off); gemini has no per-invocation MCP config and gets no thread tools.
In-session controls (shared shell)
These control a shared shell session (ptln start). The mux ctrl-\ keys above are separate — they switch between your own AI sessions.
- Typed at a shell prompt:
/pwho·/phud [on|off]·/pgrant [name]·/plock·/pinvite <email|@handle>·/pexit·/phelp - Prefix
ctrl-\then:wwho ·ggrant/open (host) ·rrequest control ·hHUD ·llock (host) ·dleave ·qend (host) ·?help
The ctrl-\ prefix works inside full-screen apps (vim, claude); typed /p commands only work at a shell prompt. Ctrl-C does not quit (raw mode) — use /pexit or exit.
Reconnects: if the relay connection drops, the host and every joiner reconnect automatically — same session, same encryption key — within a few seconds. After ~3 minutes of failed retries it gives up with a message to start a fresh session.
Staying up to date
When you start a session, partyline checks at most once a day, in the background and never blocking, whether a newer version is out, and prints a one-line notice. The check is a plain version lookup: no account token, nothing about you is sent. ptln upgrade updates; ptln version checks on demand. PARTYLINE_NO_UPDATE_CHECK=1 disables the check entirely (also skipped in CI and when output is not a terminal).
Slack
Connect Slack on your team page, then from any channel: /partyline list · /partyline start · /partyline party [mode] · /partyline party end · /partyline who. The host claims a session with ptln start --session <id>; live-start and session-end also post to the connected channel.
Orchestrator (ptln crank)
Drive a whole backlog autonomously — one task per git worktree, each landing on its own reviewable branch. Run from inside a git repo; each non-blank, non-# line of the file is one task.
| Flag | Does what |
|---|---|
--file <backlog.txt> | the worklist, one task per line (blank and # lines skipped) |
--literal | take every non-blank line as a task, # and all. Use it when task titles start with an issue ref (#570: fix the loop). Runs dispatched from the web always use it |
--thread <id> | the context thread every task shares (defaults to the repo's bound thread) |
--allow-bash | let the worker run shell commands (off by default: read/edit only) |
--engine <e> · --model <m> | what every task's worker runs — default claude; codex and goose require --allow-bash, antigravity cannot run headless (which engine builds) |
--max N · --max-tokens N | stop after N tasks · stop once the worklist's tokens hit N |
--halt-on-fail K · --timeout <dur> | stop after K consecutive failures (default 2) · per-task wall-clock budget |
--max-repairs N | how many times a task rejected by the verify gate goes back to the builder before it quarantines (0–5, default 2). Projects set it in Project settings |
--resume · --restart | continue a run, skipping tasks already done · discard prior attempts and rebuild from the base branch |
--claim --run <uuid> --workers N | fleet mode: claim tasks from the run store instead of a file |
--base <name> · --merge-policy <p> · --draft | fork point and PR target · manual | pr | auto · open PRs as drafts |
Tasks run sequentially, not in parallel, so item n can build on the context item n−1 left on the shared thread. The run stops on any brake: list exhausted, --max reached, or K consecutive failures. When --max-tokens is hit, an interactive run pauses to approve more budget or remove the limit; unattended runs stop. It prepares branches and never pushes or merges (ptln wt lists them).
The task line is the whole spec. Write each one to be self-verifying: an exact target (file/function), explicit cases and behavior, and an executable acceptance command (go test …, cd web && npx tsc --noEmit, gofmt -l) the worker checks against. Crank prints a non-blocking ⚠ task N has no acceptance check advisory for any task missing one.
$ ptln crank --file backlog.txt --thread t_abc --allow-bash --max 5 --max-tokens 500000Agent tool grants (ptln project tools)
An agent partyline launches gets read and edit tools only. A tool grant widens that for one project and one role — a shell command prefix, or an MCP server from the running machine's own catalog. Editable in the web (a project's Agent tools panel) and from the terminal:
$ ptln project tools checkout-web # what's granted today
ptln project tools checkout-web --role build \
--allow-shell "gh *" --allow-mcp linear # grant a prefix + an MCP server
ptln project tools checkout-web --role planning --revoke-shell "gh *"- Roles are
planningandbuild. The review role is deliberately not grantable — the verifier stays independent of the producer. - Grants are pure data: names and prefixes, never commands. An MCP name selects an entry from the running machine's catalog (
~/.partyline/mcp.json); a name with no local entry is skipped with a note, so the server can only ever point at something the machine's owner already configured. A shell prefix becomes an entry inside the engine's own allowlist ("gh *"→Bash(gh:*)), not a shell string. - Fail-closed. Anything invalid, unknown, or over the 20-entry cap is skipped or truncated, never widened. Both the server and the daemon validate, and the daemon does not trust the server's validation. A rejected save names the offending entry and how to write it. Claude's own rule syntax is accepted as sugar:
Bash(gh:*)andgh:*both normalize togh *. - A change applies to the next launch or run; live agents keep the posture they started with.
- Non-claude build workers log "not yet applied" rather than silently running ungranted, and
ptln workstays ungranted by design.
Configuring partyline from a shell
A setting you can only change by clicking is a setting your agent cannot configure, so every setting has a command.
$ ptln settings # every surface + the command that changes it
ptln me set --handle ada --timezone Europe/London
ptln notify off work email # stop emailing me about runs
ptln notify quiet 22:00 07:00
ptln team set --review on # finished work waits for a human
ptln project doc my-app --set @BRIEF.md
ptln project env my-app --set staging=develop,prod=mainSecrets pipe; they do not get pasted
Anything that mints a secret takes --key-only, which puts only the key on stdout and everything else on stderr:
$ ptln key create ci --key-only | gh secret set PARTYLINE_API_KEY
ptln trigger create "Deploy triage" --slug deploy-prod \
--project my-app --on failed --key-only | gh secret set PARTYLINE_DEPLOY_KEYThe value goes process to process; it never lands in a transcript, a model's context, or a clipboard.
This grants no new power — an agent with your shell could always call the same API with the same token — but it makes the operations validated, consistent, and visible in ptln trigger ls and ptln webhook ls. Still web-only: the Slack and GitHub integrations, and members and roles. ptln settings says so.
Send work to the backlog from any session (send_to_partyline)
Say "send this to partyline" in a session with your own LLM and it files the work — one task, or a whole epic ▸ feature ▸ task decomposition promoted as a chain, each task landing its own reviewable branch. That is also how a tracker epic gets built: "take PROJ-42 from Linear and send it over."
It may ask you questions instead of filing. Work about to be built by an agent with nobody watching is checked against four things the server can verify, and anything missing comes back as the exact question to put to you:
| a real project | the label resolves, or it does not |
| an executable check | a criterion naming a command that proves it is done |
| it fits one session | arithmetic |
| a spec beyond the title | a floor, not a score |
Readiness is derived from those facts, not asserted by whatever wrote the item — an author scoring its own work is not a gate. The checks are structural on purpose: a gate that can be satisfied by writing more is one a language model defeats by writing more, so word count and tone are not part of it. The executable check is the biggest lever: in dogfooding, a task carrying one came back clean and mergeable; a fuzzy one produced a silent no-op that looked fine — green run, plausible diff, nothing working.
import_work_item opens a planning session seeded with the ticket rather than filing a task, because a raw ticket is a statement of a problem, not a spec. Say so and your LLM will shape it with you and file it directly, through the same checks. Imported ticket text is sanitized and fenced as untrusted input before it reaches an agent.
Write the persona once (ptln template)
ptln template holds a trigger's persona — "triage a failed deploy the way a good engineer would" — separately from the payload of any one call. Without it, the same instruction gets retyped into every webhook and drifts the moment one copy is edited.
$ ptln template create "Deploy triage" \
--body @brief.md \
--stop-when "Stop and say so if you cannot reach the build log." \
--approve
ptln trigger set deploy-prod --template "Deploy triage"
ptln trigger set deploy-prod --template "" # detach--body and --stop-when both take @file; shell-quoting paragraphs is how they arrive mangled. --refuse-when is the same flag as --stop-when. ptln template show "Deploy triage" (--json for the whole record) prints the instruction a trigger will actually run; the list does not carry it.
ptln template rm does not delete the triggers pointing at the removed template — they fall back to their own inline task, and rm names the ones that just did. Nothing stops firing; what it asks changed.
Persona and payload compose; neither replaces the other. The persona goes first, framing how to read what follows, then the rendered event. The payload keeps its fence: your template is team-authored and trusted, the payload is written by whoever fired the webhook and is not.
--task is optional once a template is attached. The persona then runs against the caller's whole payload, rendered field by field and fenced as untrusted exactly as a templated task would be:
$ ptln trigger create "Ticket intake" --slug tickets \
--project acme --template "Ticket triage"More of the payload is shown this way; none of it is trusted any more than before. A value still cannot close the fence, log-shaped fields keep their tail, and anything dropped by the field cap is named rather than silently lost.
One of --task or --template is required — a trigger with neither would wake an agent and tell it nothing — so it is refused at creation, and you cannot blank the last one out later.
A template is a draft until you approve it. A trigger pointing at a draft falls back to its own inline task rather than running an unfinished instruction.
--stop-when is its own field rather than a paragraph in the body: it is the one thing an unattended agent most needs and most often lacks. Given no instruction to stop, an agent improvises around a missing precondition instead of saying so.
Run a trigger on a clock (--cron)
$ ptln trigger create "Nightly audit" --slug nightly \
--project my-app --machine mini --preset report \
--cron '0 3 * * *' --task @audit.md
ptln trigger set nightly --cron '0 9 * * mon-fri' # change it
ptln trigger set nightly --schedule pause # stop the clock, keep the address
ptln trigger set nightly --schedule resume
ptln trigger set nightly --cron '' # back to webhook-onlyFive-field cron — minute hour day-of-month month day-of-week — plus @daily and @hourly. It is read in your team's time zone, not the server's, so "9am" stays 9am across a daylight-saving change. A team with no zone set reads as UTC.
The schedule supplies the when and nothing else. A scheduled fire goes down the same path an inbound call does: same project, machine, preset, merge policy and gate, all chosen by the admin who created the trigger. Putting a clock on a trigger grants it nothing a webhook did not already have.
| a missed window | fires once on recovery, then rejoins the future. Eight hours of downtime on a half-hourly schedule does not queue sixteen runs |
| an overlapping run | is skipped, not stacked. While the previous run is still open the window is passed over and the reason recorded — ptln trigger log <slug> shows it |
An expression the server cannot parse is refused, not stored. A schedule nobody can read is one that silently never fires, and it looks configured from every angle you can check. ptln trigger ls carries the schedule and the next run for anything on a clock.
When an agent wakes without being asked
A run a trigger started notifies on its own preference key, agent_report, separate from work. work means "the run you started finished"; this means "something broke, an agent investigated, here's what it found." A single key would make the mute too coarse.
$ ptln notify ls
ptln notify off agent_report slack # email only
ptln notify quiet 22:00 07:00Quiet hours apply, and any channel added later carries it with no further work.
The message leads with the agent's own summary and carries an "ask about it" link, which opens a room seeded with the whole incident: what woke the run, what each task concluded, which branches it left. The room is created when a human clicks, never on the agent's initiative — one party per failed deploy would spawn a runner and spend tokens on every incident nobody chose to discuss. Clicking twice lands in the same room.
Watching what your triggers and webhooks do
Your dashboard carries two activity panels, separate on purpose:
- Triggers (inbound) — things outside partyline that start work here. A failure means we chose not to act: a duplicate, a disabled trigger, an outcome not in
--on. - Webhooks (outbound) — where your events go. A failure means their endpoint is down.
Each panel is a stacked timeline, one layer per trigger or endpoint on a shared axis. Nothing needs registering, and something that has never fired stays in the list rather than being hidden.
Inside a trigger's layer the shading is the outcome: solid where a call started a run, progressively fainter for calls where nothing ran (accepted, not acted on, duplicate, disabled). Hue says which trigger; opacity says what the call did. A trigger that fired forty times and started nothing looks healthy on a count alone, and is not.
Click a trigger's row for its event log: every call, newest first, the reason nothing ran when nothing did, and a link straight to the run when one started.
$ ptln trigger activity # every trigger, broken down by outcome
ptln trigger activity --days 30 --json # a wider window, parseable
ptln trigger log deploy-prod # one trigger's event log, by slug
ptln trigger log deploy-prod --limit 20ptln trigger log takes the slug you chose, not an id you would have to look up, and prints the run id for every call that started one.
Self-hosting (ptln server)
You run your own instance, and the CLI is the installer.
ptln server install # stand the instance up on this machine
ptln server doctor # which features this box's environment configures
ptln server bootstrap # print the install plan for doing it by handptln server install
$ ptln server install --site https://partyline.example.comFive phases, each allowed strictly less than the next: preflight (reads the machine, writes nothing) → plan (prints what will happen, in order) → apply (does it, asserting after every step that the step took effect) → verify (checks the box actually serves, not that the steps ran) → report. It is idempotent: re-running reconciles, rewrites stack files only when their content differs, only ever adds to .env, and never rewrites the secrets already in it. Verification polls /api/health.
| Flag | Does what | Default |
|---|---|---|
--site URL | public URL this box will serve | prompted if omitted |
--dir PATH | where the stack lives | /opt/partyline |
--bind ADDR | host interface to publish on | 0.0.0.0 |
--http-port N | host port for HTTP | 80 |
--https-port N | host port for HTTPS | 443 |
--relay-port N | host port for the relay | 2222 |
--tls MODE | auto, acme, internal, or off | auto |
--no-caddy | do not run the edge — something else terminates TLS | off |
--dry-run | print the plan and stop, writing nothing | off |
--yes / -y | do not prompt; take the flags and defaults as given | off |
That is the whole flag set. No public domain is the normal case and it works: with --tls auto, a name no public CA can issue for — an IP, a .local, a Tailscale name, a bare hostname — gets a certificate from Caddy's own CA, so HTTPS works offline and browsers warn until you trust that CA. --tls off serves plain HTTP. Moving HTTP/HTTPS off 80/443 costs you a Let's Encrypt certificate, validated on the public 80 or 443 — but not --tls internal, which needs nothing from the internet.
Nothing on the web can trigger this: it writes to a system directory and starts containers, which is exactly the authority kept out of the control plane's hands.
Then point a machine at it — the URL is a positional argument; there is no --api flag:
$ ptln login https://partyline.example.comptln server doctor and ptln server bootstrap
doctor runs on the box running the stack and reports every feature as configured or not, naming the variables a not-configured one is missing. Names and set/unset only — a value is never printed, so the output is safe to paste into an issue. It also prints this instance's identity fingerprint. --json for the machine-readable form.
bootstrap checks a fresh box (docker, the compose v2 plugin, the stack's ports, free disk, whether the stack's files are present, which required variables are unset) and prints the exact ordered install commands, naming each missing variable and only paths it confirmed exist. It only prints: it never writes /opt/partyline/.env, generates a secret, or runs docker compose. Exit 1 if a prerequisite is missing.
Full walkthrough, including every environment variable the stack reads: Self-host.
Every setting in one place (ptln settings)
$ ptln settingsA read-only master index: your account, this repo's thread bind, the MCP catalog, agent tool grants, the daemon's registered projects, the effective peer-consult and peer-delivery policies, engine wiring, and the surfaces that live only on the web — each printed with the exact command or page that changes it.
The macOS tray (ptln tray)
A single menu-bar icon showing what this machine is doing. It ships with the CLI and starts automatically with the session manager or the daemon; ptln tray on adds it as a login item, ptln tray off opts out, bare prints status.
- Live sessions — which agents are running, and which are waiting on you (that count, and only that count, is the badge).
- Peer rows — a teammate's queued question with Approve / Decline, what this machine answered read-only today, and replies waiting for you.
- Daemon control — start/stop, auto-update, and the provider rate-limit line.
- Notifications fire only when a count rises, never on a steady state, and their bodies carry names and counts only — never question, answer, or transcript text, because banners land on lock screens and in system logs. Toggle them off with Desktop notifications in the menu.
The tray holds no token, opens no socket, and makes no HTTP call: it reads ptln state and every action shells ptln.
Remote launch (ptln daemon)
Early access. Let the web launch a grounded agent on your own machine, in the right project directory, with one click.
ptln daemon enable— enrol this machine (needsptln login <url>). It mints a device-scoped token, separate from your login token and individually revocable.ptln daemon add-project <label> [dir] [--preset spec|chat] [--engine <e>]— register a project the web may launch into.--enginesets the machine's default engine for this project's launches and runs. The absolute path stays on this machine — only the label is mirrored to the server, to populate an "Add agent" picker.ptln daemon run— connect and listen, opening an interactive confirm console:approve <id>,deny <id>,kill <id>,list.- On the party page, anyone in the team clicks Add agent → pick a project, creating a pending request. Nothing runs until the machine's owner approves it — then a grounded agent (read-only
Read/Grep/Glob; thespecpreset answers in cited, verified positions) spawns in that project dir and joins the party. A Stop button, orptln daemon kill <id>, terminates it. ptln daemon disable— revoke the device token and remove it locally.
ptln daemon install runs it always-on as a background service (launchd/systemd). ptln daemon autoupdate on opts that machine into fleet auto-update, which updates only when nothing is running and the published version is strictly newer.
The daemon's heartbeat also mirrors the names of the MCP servers in this machine's catalog (~/.partyline/mcp.json) — names only, never a command, an argument, an env var, or a key. That list is the inventory the web's Agent tools picker offers, and it is the other half of reference, not command.
The same daemon answers peer consults. Three settings there, whose defaults point in opposite directions:
ptln daemon consults <label> [auto|ask]— defaultauto. Questions are answered automatically within a daily budget; a human is asked only once that budget is spent, or when auto-answer is off — not question by question. So a teammate on your team can cause this machine to run a read-only engine turn on your checkout, on your tokens, without asking you. The turn cannot write a file or run a command.askturns it off for that project. The per-project allowance (default 24/day, plus 48/day for the whole machine) is a project setting in the web that every machine in the project honours;PARTYLINE_CONSULT_AUTO_DAILYonly clamps it downward on this box.ptln daemon consults --all ask— the machine-wide off switch. Persisted local state the daemon re-reads on every question, so it applies immediately to the always-on service too, with no restart or reinstall — and it outranks every per-project setting, including projects registered later.--all autohands the decision back to each project.ptln daemon deliver <label> [stage|submit]— defaultstage. A landed answer is pasted into the asking session's prompt unsubmitted; your Enter makes it a turn.submitskips that, and the read-only guarantee does not help you here — it protects the machine answering, not yours, and your session has whatever tools it was launched with.
The security model is "reference, not command": the control plane only ever sends a project label, never a path or a command. A label becomes a runnable command only inside the daemon, matched against this machine's own local registry, and only under the run mode you set for that project on this machine — auto (the default: registering the directory was the consent) launches without asking, ask holds it for approval in the console. This is an MVP; a formal security review gates auto-launch and broader rollout.
$ ptln daemon enable
ptln daemon add-project myapp ~/dev/myapp --preset spec
ptln daemon run