@justjoshn/pi-pstack
pstack 0.15.36 for Pi with its soft-dependency team-kit skills vendored in: rigorous agent workflows and principles as Pi skills, with the host features they use: subagents, todos, modes, plan mode, the model rule, /loop, subscriptions and routines, AskQu
Package details
Install @justjoshn/pi-pstack from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@justjoshn/pi-pstack- Package
@justjoshn/pi-pstack- Version
0.15.36-pi.14- Published
- Nov 4, 2026
- Downloads
- 758/mo · 758/wk
- Author
- justjoshn
- License
- MIT
- Types
- extension, skill
- Size
- 6.5 MB
- Dependencies
- 2 dependencies · 5 peers
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pstack for Pi (pi-pstack)
pi-pstack is pstack 0.15.36 for Pi 1.5.0: rigorous agent workflows (/poteto-mode and its playbooks), the principle skills, and the review, research, and verification skills, plus the full soft-dependency team-kit set (deslop, control-cli, control-ui, CI/PR helpers, thermo-nuclear review, and the rest) vendored in so you do not install a separate team-kit package. A small extension provides the host features the skills call by name: /name skill commands, /mode, /plan, the Task subagent tool with local worktree workers, forks, and agent_status, background shells (bash_background and AwaitShell), TodoWrite, AskQuestion, GenerateImage, SwitchMode, the model rule, /loop, subscriptions and webhook routines, and visualize.
Get set up
Clone the repository and install the package from the checkout. Pi loads a local package in place, so keep the checkout where it is.
git clone https://github.com/just-joshn/pi-customizations.git cd pi-customizations bun install pi install ./extensions/pi-pstackPi does not install dependencies for a local package, so
bun install(ornpm installinextensions/pi-pstack) fetchesyamlfor plan metadata and, for the macOS read-only shell (readonly subagents and/plan),@anthropic-ai/sandbox-runtime. On Linux the read-only shell runsbubblewrapfrom the system instead (sudo apt install bubblewrap), with no user-namespace change on Ubuntu 24.04 and later. Where no sandbox can start, a read-only shell command fails instead of running unsandboxed. macOS uses the built-insandbox-exec. pi-pstack needs Node 22.19 or newer. To try it for one session without installing, runpi -e ./extensions/pi-pstack. To install the published package instead, runpi install npm:@justjoshn/pi-pstack. Installing loads pi-pstack's tools and system-prompt sections at once, but no skill runs until you invoke it. 61 workflow skills stay out of the model's automatic skill list. Setup, create-skill, and the team-kit shipping and review skills (deslop,control-cli,control-ui, CI and PR helpers, and the rest of that set) can load from your words alone.Run
/setup-pstack. It lists the models theTasktool can use, asks for a reasoning budget, maps a model to each role, and writes the model rule. A new rule applies from the next request.Start real work with
/poteto-mode, a goal, and a check that can pass or fail.
The guide in docs/guide/ walks through setup, /poteto-mode, understanding code, building, verifying, overnight runs, and making pstack yours.
Remove the package with pi remove ./extensions/pi-pstack. Its state lives in ~/.pi/agent/pstack/, or $PI_CODING_AGENT_DIR/pstack/ when that variable is set (written <pstack dir> below): the model rule, routine credential files under routines/, and one folder per workspace under workspaces/ with subagent transcripts, agent stores, projects, artifacts, and isolated-worker worktrees. Delete that directory to remove the state too.
Use it
| You type | What happens |
|---|---|
/poteto-mode <task> |
The skill attaches to that one message. Every skill Pi loaded works this way: pstack's (/how, /why, /arena, /interrogate, /principle-prove-it-works, /deslop) and your own, such as a /<handle>-mode skill from /automate-me. /skill:<name> works too. |
/mode poteto-mode [task] |
Keeps the skill in context on every turn until you leave the mode: the skill rides every request in its own system-prompt section, which Pi re-sends only when it changes. The first request after entry says the user entered the mode, later requests carry a reminder that names the skill file, and the request after /mode off or a switch to another mode says the user left it. The footer shows a badge with the skill's icon and color metadata, such as a yellow ♛ mode: poteto-mode (other skills get ⚡). /mode off exits and /mode reports. Any loaded skill can back a mode. |
/plan [task] |
Plan mode: the agent researches with read-only tools (read, grep, find, ls, the sandboxed bash_readonly, AskQuestion, TodoWrite, agent_status, Task, whose launches run readonly: a fresh one runs here with no worktree, and a resume or fork stays where its agent ran, and stop_agent), asks its clarifying questions early, and presents the plan with CreatePlan (plan, name, overview, todos, is_project, phases). CreatePlan writes the plan file under <pstack dir>/plans/ (<name>-<8 hex>.plan.md: the chat id in a comment, the todos as front matter, then the plan), one file per plan name in a chat. Todo dependencies and project phases stay in the file. Building imports the file's unfinished root todos into the session, including edits made during review. Worktree builders import their own lists before the first model request and retain the associated plan on resume. Later writable TodoWrite status updates synchronize those root todos without rewriting other metadata or the plan body. An omitted status imports as pending and is inserted when the todo is updated. A readonly resume keeps its todo changes inside the session. edit and write work on markdown files only, so the agent refines the plan in its file. After a run that saved a plan, an interactive session asks Yes, build locally, Yes, build in cloud (in a git repository: a background isolated worker builds it on its own branch), or No, propose changes. /plan build builds the saved plan too, and /plan off restores your tools. A bare /plan enters plan mode. The footer shows plan. |
/loop 30m <prompt> |
Runs the prompt now, then again every 30 minutes from each fire, so a long run does not push the next tick out. A tick that comes due while a run is still going fires once when that run settles; several missed ticks still fire once. The interval can come first or last, written as 30m, 10 minutes, or every 10 minutes. Without an interval the agent picks each wake with the Loop tool, and the loop ends when it picks none and no background subagent or background shell is still running. A run that waits on an event (CI finishing, a ref moving, a log line) starts a watcher with bash_background, whose notification wakes the agent, and keeps a long wake as the fallback. Different prompts create independent loops. The same prompt replaces its schedule. /loop lists them. /loop stop ends the only loop, or /loop stop <id> selects one when several are running. Escape during a run ends the session's loops. Loop results include the loop ID in text and details; pass loop_id to wake, stop, or replace one of several loops. The prompt can be a skill command, such as /loop 1h /poteto-mode run the audit tick. When a playbook says to arm /loop, the agent runs the prompt once in that turn and starts the loop with the Loop tool, whose first scheduled run comes after the interval or wake. Intervals go up to 24 days. |
/routines |
Lists webhook routines with their URL, credential file, and status. |
/setup-pstack |
Configures the per-role models. |
/poteto-help <question> |
Answers a question about pstack and hands you a prompt to send. |
/mode, /plan, /loop, and subscriptions need an interactive or RPC session. In the interactive / menu, skills appear under their own names next to Pi's /skill:<name> entries. Aliases work even with enableSkillCommands off. If you have a prompt template or extension command with a skill's name, yours keeps the name; Pi's built-in commands are handled before any alias.
Host features
| Feature | Behavior |
|---|---|
Task (description, prompt, subagent_type, model, run_in_background, resume, fork, fork_turns, readonly, file_attachments, environment, cloud_base_branch) |
Starts a separate pi --mode rpc process per subagent, so this Pi process can steer and abort it. Foreground calls return the response and an agent ID. Background calls return at once, and a <system_notification> with a <task> block arrives after your turn ends, titled by the first line of the prompt (80 characters at most) and followed by a request to do any follow-up work; the notification card shows the call's description. Print and JSON runs wait for pending notifications before exiting, unless aborted. Several calls in one message run in parallel. A subagent's own subagents cannot spawn more, and every subagent is told to spawn none unless asked. A run lock per agent ID keeps out a second writer, even from another Pi process. A token-directory lease serializes stale-lock recovery while the PID file identifies the transcript writer. Children inherit the parent's project-trust decision. Foreground usage counts toward the session totals. The Task card leads with the call's description, then the agent type and the model that ran, and while a foreground child works it shows its last few tool steps and latest line instead of a blank wait. |
resume |
Sends a follow-up to an idle agent with its context, model, type, and directory. The root chat can resume or fork a descendant that a worktree worker started inside that worktree, and the descendant still runs where it first ran. On an agent running in this Pi process, it delivers the prompt as a steer and returns at once. A readonly or plan-mode call cannot steer a writable agent. An agent running in another Pi process is refused. This is how a playbook "messages" a worker. Pi's own /resume command reopens a chat session instead. |
fork |
Starts a new agent whose transcript begins as a copy of an idle agent's transcript, in that agent's directory (its worktree for an isolated worker), so it sees the built checkout and uncommitted changes. Type, model, and readonly default from the source; readonly can be added, never lifted. fork_turns: N (only with fork) copies just the first N user turns. fork excludes resume, environment, and cloud_base_branch. Pi's own /fork command branches the chat session instead. |
stop_agent |
agent_id. Stops a running subagent. One this Pi process owns gets an RPC abort, then a kill after a grace period. One another of your Pi processes owns gets a signal to the live pid in its run lock. An idle or finished agent is left alone, and the result says so. |
agent_status |
Reads a subagent's state (running while its run lock names a live pid, idle, or missing), type, model, directory, branch, times, and last reply, without waking it. It also shows whether the agent is readonly, the agent it was forked from, its pid while running, and its transcript. Without agent_id it lists the workspace's 20 newest agents. In the root chat that includes every descendant, isolated workers too. |
| Subagent types | The built-in generalPurpose (default) and explore (a file-search specialist the caller tells how thorough to be, with grep, find, and ls on and no Task), then poteto-agent (a background delegate that reads poteto-mode first), ci-watcher (a background PR check watcher), comment-sicko, and thermo-nuclear-code-quality-review, the last four read from agents/*.md. Unknown types fail with the list. |
| Models | A Pi model spec such as claude-opus-5-5:xhigh or xai/grok-4.7:high, model parameters such as claude-opus-5-5[effort=high] or grok-4.7[fast=true] (picks a listed -fast id of that model), fast for the parent model's listed -fast variant when it has one, else the parent model, or inherit, inherit-parent, auto, or nothing for the parent model. A model Pi does not list, or a thinking level the model does not support, fails with the allowed list. A bare model id that several providers offer resolves to the parent chat's provider when it offers that id, and otherwise fails with the choices. Older rules that wrote claude-opus-5-5-xhigh still resolve. The system prompt's pstack_subagents section lists the types, the parent model, and the models with their levels. |
Isolated workers (environment: "cloud") |
The subagent runs in a new git worktree at <pstack dir>/workspaces/<workspace folder>/worktrees/<agent id> on branch pstack/<agent id>, from cloud_base_branch (origin/<branch> when that remote-tracking ref exists, else a local branch or other ref) or your HEAD commit, so uncommitted changes do not reach it. The result and the card name the branch to merge, and resume and fork run in the same worktree. It fails clearly outside a git repository, in a repository with no commits, or when the base does not resolve. The worker shares the root chat's workspace state, so it sees the same projects, stores, and subagent transcripts. It also shares this machine's ports, processes, writable caches, simulators, and credentials. Assign separate ports, output directories, and writable caches to concurrent runtime lanes. swarm and orchestrate spawn worktree workers by default. |
readonly: true |
The child gets read, grep, find, ls, TodoWrite, and a read-only bash, and no MCP or background shell. That shell runs each command in an OS sandbox that blocks every file write, all network access, and Unix sockets, so git log works and touch or curl fail. On Linux it is bubblewrap with every namespace unshared, the filesystem bound read-only, and a seccomp filter that refuses socket(AF_UNIX) and io_uring_setup; on macOS it is @anthropic-ai/sandbox-runtime's sandbox-exec policy. Where no sandbox can start, the command fails rather than running unsandboxed. Readonly is not credential isolation. The child still inherits environment variables and can read credential files through its read tools. |
TodoWrite |
todos with id, content, status, and merge. Its description says when to keep a list, the four states, and one task in progress at a time. The list follows the session branch, shows above the editor, and rides every request's system prompt. Plan-imported lists persist as branch entries. Their dependencies survive status-only updates, and matching plan-file statuses are synchronized. |
AskQuestion |
title, and per question id, prompt, options (at least two, each id, label, optional description), and allow_multiple, through Pi's select dialog. It is for decisions only the user can make, and a recommended option goes first with (Recommended). Every question ends with Other: (type to answer); a typed answer comes back as other with its text. In print and JSON runs each question comes back skipped. Subagents have no AskQuestion. |
| Always-applied rules | The two team-kit rules are in pstack_host as their shipped text on every request, alongside the same rules as hidden skills. |
| The model rule | <pstack dir>/pstack-models.md, plain Markdown with one line per role, added to every request's system prompt. A pstack-models.mdc from an earlier version is moved there once, without its frontmatter, and kept as pstack-models.mdc.migrated. |
| Subscriptions | subscribe_github_pr (scope pr with prUrl, repo with repo, or author with author and repo) polls with gh every 60 seconds and wakes the chat when a PR's state, head, mergeability, review decision, reviews, or comments change, or when a check concludes or its conclusion changes; the first poll is a baseline. subscribe_timer takes a name, a prompt, and a 5-field cron or delaySeconds, either with once; sessionStrategy wake_self queues a wake for this owning session. With new_session, a live owner starts a background Task and receives its notification. An offline owner permits a detached run with a recorded outcome. Both reuse the model, thinking level, and project-trust decision the timer was created with. Re-subscribing an existing timer name returns created: false. list_subscriptions and unsubscribe manage them. The authoritative registry is <pstack dir>/durable/<workspace>/subscriptions.json. Any session in that workspace can list or cancel subscriptions. Session history does not recreate cancelled or completed subscriptions. Owner-directed events wait for the creating session rather than waking another workspace session. A detached run records its own final message, shown by list_subscriptions. A timer does not expire unless expiresInHours says so (at most 30 days); 0 means no expiry. A GitHub watch keeps a 7-day default because it is this port's own surface. |
| Webhook routines | subscribe_webhook (name, prompt, optional sessionStrategy) serves http://127.0.0.1:<port>/routines/<slug> (port PSTACK_ROUTINES_PORT, else the port this workspace last bound, else 47913, where 0 picks a free port) and keeps a random sender key in <pstack dir>/routines/<slug>/credentials.json (mode 0600). The result names the URL and the file, never the key. A JSON POST with X-Automation-Key or Authorization: Bearer returns 200 after durable acceptance, and 503 with Retry-After while that routine already holds 20 undelivered events. With the default wake_self, the owner receives a [routine] turn holding the prompt and a <webhook_event> block (headers, body_digest, body, timestamp_ms). With new_session, each accepted POST starts a fresh detached run, the way a hosted automation runs with no chat open, and records its outcome in list_subscriptions. HTTP 200 does not mean the run finished. Each authenticated POST is a separate event, including identical bodies. Storage failure returns 503 rather than acknowledging a lost event, and a full delivery queue refuses the new event instead of dropping an earlier accepted one. A wrong key gets 401, an unknown routine 404, a method other than POST 405, a body over 64 KiB 413, and a body that is not a JSON object 400. A routine does not expire unless expiresInHours says so. make-bot-ui builds a page on top of this. |
| Durable supervisor | Creating a subscription auto-starts pstack-routines-supervisor --workspace <cwd> when it is not already running. Creation waits for that subscription to arm, and webhook creation returns the bound listener's URL. The supervisor reads the durable registry, arms timers and GitHub polls, and owns the routines port. An online owner receives native Pi continuation messages. Owner-directed events accepted while the owner is offline wait for that same session to resume. A timer or webhook routine explicitly configured with sessionStrategy: "new_session" starts detached work while its owner is offline. You can still start the bin yourself. For reboot survival, run packaging/install-supervisor-unit.sh --workspace <cwd> (idempotent; installs the launchd or systemd unit from the examples under packaging/). Print and JSON runs still cannot create subscriptions. |
| Stores and projects | The system prompt names "the current agent's store" (per session), "the root agent's store" (the main chat's, named to every subagent however deep), and the workspace's projects/ folder, where a program's Project is <slug>/notes.md (its tracker) and <slug>/store/. |
visualize |
title, kind (bar or line), labels, series (name, values), and unit render a Unicode chart in the result and write a standalone SVG to the session's artifacts folder. |
| Transcripts | The system prompt names this workspace's Pi session directory, the current session file, and this workspace's subagent transcripts. |
GenerateImage |
Generates an image (description of at least six words, filename, reference_image_paths, aspect_ratio) on Pi's image models through ctx.modelRegistry.generateImages(). It is offered when an image model is available (for example an OpenRouter image model), not to readonly subagents or in plan mode, and saves each image to the session's artifacts folder. Without an image model, the host section tells skills to draw a Mermaid diagram instead. |
| Background shells | bash_background (command, description, block_until_ms, notify_on_output with pattern, reason, and debounce_ms, working_directory) runs a command in the background, or waits up to block_until_ms for it and then moves it there. Output goes to <pstack dir>/workspaces/<workspace folder>/terminals/<shell id>.txt, which starts with a header (pid, cwd, command, title, status, started_at, and running_for_ms, refreshed every 5 seconds) and ends with an exit footer. After the turn, a <system_notification> with a kind: shell task block arrives when the command finishes (status: success, error with exit_code=N, or aborted) and when a line matches notify_on_output (the first match at once, later ones counted and sent after each debounce_ms window, at least 5 seconds). AwaitShell (shell_id, block_until_ms, pattern) polls or waits on a job, or sleeps without one; seeing a job end there skips its notification. Print and JSON runs wait for their jobs before they exit. Ending the session stops running jobs. Readonly subagents and plan mode do not get these tools. |
SwitchMode |
target_mode_id (plan or agent) and explanation. The agent proposes entering or leaving plan mode, and the switch happens only after you confirm it. Offered in interactive and RPC sessions. |
Subagent transcripts are Pi sessions in <pstack dir>/workspaces/<workspace folder>/subagents/, with the agent ID in each file name, next to a small record that resume, fork, and agent_status read. Records belong to the main chat's shared workspace state, so the root chat can act on a descendant that ran in a worktree. Children inherit your installed extensions and MCP servers, and local ones run in your working directory. Give each local subagent that writes its own git worktree, or launch it with environment: "cloud", because a child process does not isolate the filesystem. A child exits on its own if its parent Pi process dies.
Limits
- Worktree workers run on this machine, not remote VMs. They start from committed code only and share your installed extensions, MCP servers, and network, except that a readonly worker gets no MCP servers and no network. A worktree is not a credential boundary. Concurrent runtime lanes need separate ports, output directories, and writable caches. pi-pstack never pushes their branches, though a worker can push its own. Worktrees and branches stay until you or the worktree-cleanup playbook remove them.
- The supervisor can accept events after chat exit, but owner-directed events wait until the owning session resumes. Explicit
new_sessiontimers and webhook routines can spend tokens with no human watching. Detached runs do not supply the live owner's Task notification flow. For reboot survival, runpackaging/install-supervisor-unit.sh --workspace <cwd>. If the supervisor cannot start, an explicitly reported in-process fallback stops when that Pi process exits. A GitHub subscription restarted after downtime takes a new baseline, so changes made while nothing was polling do not wake it. Each workspace binds its own routines port. With no explicit port, a busy default falls back to an ephemeral port that the workspace then reuses and reports, so two workspaces serve routines at once. An explicitPSTACK_ROUTINES_PORTis a configuration: if it is taken, the routine reports that error instead of moving. The listener binds127.0.0.1, so a page served to other machines forwards through a local server, asmake-bot-uidescribes. - Event delivery is at-least-once. A process crash after Pi queues a message but before its durable acknowledgement can repeat a wake or a fresh run. Automation prompts must make external actions idempotent. Up to 20 undelivered events per subscription stay queued until delivery or cancellation, and a new event beyond that is refused rather than replacing an older accepted one. Expiry stops new occurrences rather than discarding accepted work. Failed detached executions retain their event for retry and expose the error in
list_subscriptions. Cancelling a subscription drops pending undelivered events, but cannot undo work already handed to Pi. - The Benny automation pack under
automations/benny/runs on pi-pstack subscriptions. Keep its owning Pi session running for immediate handling, or resume that session to handle parked events. It needs a Slack MCP server for its Slack steps. /planusesbash_readonlyfor its shell rather than replacingbash, so your shell settings stay as they are outside plan mode.- Background subagent token usage is recorded in the child's transcript and in the
usagefield of the notification's details, but does not enter the parent session's totals: Pi 1.5.0 has no public API for adding a background completion's usage to the parent totals. Native tool-result usage covers synchronous nested work, not later notifications. /reload,/new, and Pi's/resumeand/forkcommands stop running background subagents, with a warning that names how many. Continue one later withTaskresumeand its agent ID.- A print or JSON run waits for its background subagents before it exits, with no timeout of its own, unless it is aborted.
- Subagents start the same Pi script that runs the parent (
process.argv[1]), orpifromPATH. Embedding the package through the SDK in another host is not supported. - Depth limits apply to
Task. A writable subagent can still startpithroughbash, and that process is outside the depth limit and parent watchdog. - A child loads your installed extensions plus this package. If you run one checkout with
-ewhile a different checkout of pi-pstack is installed, the child loads both and fails on duplicate tools; use one copy, or a separatePI_CODING_AGENT_DIR, aslive:e2edoes. - Subagents run headless in RPC mode, so an extension that asks for permission through a dialog cannot ask there.
- The read-only shell sandboxes shell subprocesses only. On Linux it needs
bubblewrapthat can create its user namespace (Ubuntu's defaultbwrapAppArmor profile allows that) on x86-64 or arm64; bubblewrap loads the seccomp filter itself, so no nested namespace is needed. A missingbwrap, another architecture, or a namespace refusal blocks the shell command. Unix sockets stay blocked because a host socket service can mutate files or access the network on a command's behalf, and the shell never falls back to permitting them. Do not treat a readonly child as a credential boundary. Environment variables and readable credential files are still available to its read tools. - A subagent's background shells stop when the subagent finishes, and its launch result says so, because a subagent is its own process. A print or JSON run waits for its background shells, with no timeout of its own, so a job that never ends keeps the run open.
- Whether a model follows a playbook is the model's call.
- Workspace folders follow Pi's session-folder naming, so two paths that differ only by
/versus-share a folder. Resume and fork refuse an agent whose record names the other directory, except that a record created inside this workspace's own worktrees belongs to this workspace. Subscription reads and mutations also reject a registry owned by the other workspace. - The run lock that keeps one writer per agent ID names the process writing the transcript. A lock file without a readable pid counts as held; the error names the file to delete.
agent_statusshows such an agent asidle. A token-directory lease beside the PID file serializes stale-lock recovery. A dead process or an unreaped Linux zombie does not count as a live writer. Older installed versions without the lease do not participate in this protection. skills/poteto-mode/scripts/watch-prneeds Bun and installs its one dependency on first run.check-plan.mjsneeds only Node.worktree-audit.shneedsrgandjq, and works with BSD (macOS) or GNUstatanddate.statechartrunsskills/statechart/scripts/statecharton Node 22.18 to 22.x, Node 23.6 or later, Bun 1.1.5 or later, or Deno 2.0 or later. The first run installs TLC and Java through mise and needs the network. Without a SPEC it printsGive one SPEC.tla.and its usage, then exits 2.
Maintain it
The skills, agents, guide, and automation pack are this package's own files; edit them in place. They track pstack 0.15.36 in behavior. docs/parity-status.md records what was reconstructed, what each host translation uses, and which differences remain host-bound. The record of the move to Pi-native text and the host features added for it is at the repository root, in parity/pi-pstack-0.15.36/ (design and decisions); earlier records are in the root's parity/pi-pstack-*. Four mechanisms Pi 1.5.0 does not export keep a hand-written implementation here: docs/native-gaps.md names each one, the evidence, and the public API that would replace it.
bun run typecheck
bun run test
bun run test:statechart # full compiler suite; set TLA2TOOLS and JAVA for an existing verified toolchain
bun run live:e2e -- --model deepseek/deepseek-flash # real-model scenarios; needs credentials and gives the model under test your real auth.json (see the script header)
Licenses
The port's own code is MIT (LICENSE). pstack's files keep their MIT notice in licenses/pstack.LICENSE (© 2026 Lauren Tan), and the vendored team-kit skills and agents keep theirs in licenses/cursor-team-kit.LICENSE.