Skip to content

pstack ​

pstack-claude is a Claude Code plugin (pstack@pstack-claude): skills such as how, why, interrogate and arena, effort agents, and a SessionStart hook. Through the gateway it behaves the way it does in the Claude Code CLI. The gateway only decides which agents load it and with which models per role.

Install once, through Claude Code ​

On the gateway host, as the user the gateway runs as:

text
/plugin marketplace add michael-denyer/pstack-claude
/plugin install pstack@pstack-claude

Update it with /plugin update and check the version with /plugin. The gateway has no install command, keeps no copy and pins no version: every agent uses the one install Claude Code manages. It only reads installPath from ~/.claude/plugins/installed_plugins.json (or $CLAUDE_CONFIG_DIR), and only a user-scope install counts: one made for a single project (--scope project or local) does not apply to the projects workers run in.

Running Claude Code directly on the host is unaffected: the plugin behaves as any installed plugin does there.

Set up an agent ​

An agent that is not set up loads nothing from pstack, even when the plugin is enabled globally: every host worker gets enabledPlugins["pstack@pstack-claude"] in its --settings overlay, false unless the agent has an agents[].pstack block. A session of an agent without orchestration gets the same false key; it is merged into a --settings you set in claude.extraFlags rather than added beside it. If that --settings value is not a readable JSON object (inline or a file), such a session does not start and fails with INVALID_EXTRA_FLAGS, since pstack could not be kept off; Claude Code could not read it either. Claude Code merges that key over your own settings, so other plugins load as before. Claude workers of a set-up agent get the plugin and the agent's own model sheet. Codex workers never get pstack: no plugin, no sheet and no pstack skills in their catalog. An app agent's workers run in its container and cannot load it, so an app agent cannot be set up (PSTACK_UNSUPPORTED_AGENT). The agent's own skill catalog lists pstack's skills only once it is set up, together with a short note telling the agent that pstack means this plugin (not the Unix pstack tool), that its skills run in workers, that pstack:poteto-mode picks the playbook, and that pstack is set up here with /pstack-setup rather than pstack's own setup-pstack skill; an agent that is not set up gets neither, and /pstack:<skill> sent to an agent that is not set up is answered with a pointer to the setup command instead of reaching the agent. These /pstack:* replies come from orchestration mode (for web chat, orchestration.channels must include api); without it, /pstack:* reaches the agent as an ordinary message. On a set-up agent, /pstack:<skill> for a skill that is not user-invocable (the principle-* skills) is answered at once that it is not a command, as in Claude Code; workers load those skills by themselves. A name the plugin has no skill for is answered at once too, with up to three close user-invocable command names when there are any, including misspellings (/pstack:reflct suggests /pstack:reflect). /pstack:reflect runs in a new worker session, so its worker is given the transcripts of the chat session it reflects on: the agent's turns with the user and each earlier task's Claude worker. The gate decides what Claude Code loads, not what a host worker can read: the plugin's files stay on the host's disk. The gateway's own one-shot claude -p helpers (session titles, summaries, skill reviews) always run with pstack off.

On Telegram, send /pstack_setup (or /pstack-setup) alone in a direct message with the bot. It opens a menu that edits itself in place:

  • Main screen: whether pstack is on, the default effort, and how many roles there are and how many you changed, plus a ⚠️ line naming any role model (a skill default too) that is not in this gateway's model list, since those subagents would fail. Five buttons, one per row: Turn on / Turn off, Effort, Roles ▸, Reset all (to the skill defaults, after a confirmation; while pstack is off there is nothing to reset and it stays off) and Close. Turn on and Turn off end the menu like a /voice choice: the message becomes the result, such as ✅ pstack is on for alfred. The next worker loads it., with no buttons left.
  • Roles screen: Single-model ▸ and Panels ▸, each with its number of roles in the installed plugin.
  • Role list: one button per role with its current model (a panel role shows how many models it runs; ✏️ marks a role you changed), 8 a page.
  • Role screen: the role's model and effort. Pick a model from the Claude or GPT list (paged; a [1m] ID is listed only when the model has no base ID), set an effort level or go back to the default effort, or drop your change with Use skill default. ◀ Back returns to the list page the role is on.
  • Panel role screen (interrogate reviewers, arena runners…): one ➖ button per entry (it removes that entry only, even beside another entry of the same model) and ➕ buttons that add one Claude or GPT model at the default effort (an entry the panel already has is refused). The last entry cannot be removed; use Use skill default. A ➖ button from before the panel changed elsewhere is refused as out of date.
  • Default effort screen: session or a level.

Each tap is saved at once, through the same checks as the text command below, and the menu says whether it worked (No change. when the tap left the setup as it was). A change made while pstack is off turns it on, and the menu says so. Each tap applies to the setup as last saved, so a change made meanwhile from the API or CLI is kept. A role whose saved value is not valid (for example, edited by hand in config.json) is marked ⚠️ and can be fixed by using the skill default, or for a single-model role by picking a model. Only allowlisted users can use the menu, and only in a direct message. Telegram command names cannot contain -, so /pstack_setup is the spelling in the bot's command list and in the gateway's replies; both spellings work everywhere.

Or type the change in a direct message with the bot:

MessageEffect
/pstack-setup onSet the agent up with every role at its skill default
/pstack-setup why investigators: gpt-6-sol[1m]Set one role
/pstack-setup interrogate reviewers: claude-opus-5-5 @xhigh, gpt-6-astra[1m]
default effort: high
Several pstack-models.md lines in one message
/pstack-setup bug-fix: defaultDrop an override, like deleting the line
/pstack-setup interrogate reviewers += gpt-6-sol[1m] @highAdd one entry to a panel role (an unset panel starts from its skill default; one model per line; an entry the panel already has is refused)
/pstack-setup interrogate reviewers -= claude-haiku-5-5Remove a model's entries from a panel role; -= claude-opus-5-5 @low removes that one entry (the last one cannot be removed; use <role>: default)
/pstack-setup offRemove pstack from the agent

Outside the Telegram menu, /pstack-setup alone, in a direct message, shows the agent's state, every role with its current model, the syntax and the Claude and GPT model IDs to choose from. Arguments are lines of pstack's own pstack-models.md. Changes apply to the agent's next worker. In a group or channel the command, bare or with arguments, only answers that it works in a direct message, so the agent's setup is not posted there.

In web (API) chat, /pstack-setup is a gateway command too: anyone in the chat can see the setup, and only an API key with write access to the agent can change it, as with the API below. Its reply carries the same menu as Telegram's buttons in actions (see command replies), so a web client can show them as buttons; Turn on and Turn off return their result with no actions, ending the menu. A pressed menu button is not stored in the chat history (neither the button nor the menu page it returns), so browsing the menu does not push messages out of the history a new context starts from.

Or through the API or CLI (an admin key may change any agent, a write key the agents it can access). A field you leave out keeps its saved value, so pstack enable on an agent already set up keeps its roles; send roles: {} to reset every role to the skill default:

bash
claude-gateway pstack enable alfred --roles '{"bug-fix":"claude-fable-5-1 @xhigh"}' --defaultEffort session
claude-gateway pstack disable alfred

See PUT /api/v1/agents/:agentId/pstack.

Configuration ​

agents[].pstack mirrors pstack-models.md, with full model IDs instead of slugs:

json
{
  "id": "alfred",
  "pstack": {
    "defaultEffort": "session",
    "roles": {
      "bug-fix": "claude-fable-5-1 @xhigh",
      "how explorer": "claude-sonnet-5-5 @medium",
      "why investigators": "gpt-6-sol[1m]",
      "interrogate reviewers": "claude-opus-5-5 @xhigh, gpt-6-astra[1m] @high, claude-sonnet-5-5, claude-haiku-5-5"
    }
  }
}
FieldValuesDefault
rolespstack role -> "<model-id>[ @<level>]"; a comma-separated list for the panel roles (arena runners, arena cross-judge pool, architect runners, interrogate reviewers), one subagent per entryevery role uses its skill default
defaultEffortsession (the parent's effort) or low, medium, high, xhigh, maxsession

Model values are IDs from /models. Aliases such as opus or gpt6sol are refused with MODEL_ALIAS_NOT_ALLOWED and the ID to use, so a role never changes model when an alias moves; inherit-parent and auto are pstack keywords (run on the parent's model) and are accepted. Saving checks every model, level and role against the live catalog and the installed plugin (UNKNOWN_MODEL, INVALID_EFFORT, UNKNOWN_PSTACK_ROLE, PSTACK_NOT_INSTALLED); any other field is refused.

The worker sees the agent's sheet in its context instead of ~/.claude/pstack-models.md. Every role is listed, with pstack's default slugs written as IDs (opus = claude-opus-5-5, fable = claude-fable-5-1, sonnet = claude-sonnet-5-5, haiku = claude-haiku-5-5). The gateway never writes the global sheet.

The plugin's SessionStart hook reads session hook: on|off only from the global ~/.claude/pstack-models.md, so that line applies to every set-up agent alike and has no per-agent setting.

Panels ​

A panel role (arena runners, arena cross-judge pool, architect runners, interrogate reviewers) is a list, exactly as in pstack: one subagent per entry (+= and -= edit panel roles only; any other role takes exactly one model, and a list there is refused), so the list length is the number of reviewers, and each entry may carry its own @level. Mix providers freely, e.g. claude-opus-5-5 @xhigh, gpt-6-sol[1m], claude-sonnet-5-5. The skill combines the results itself (in interrogate the parent is the lead), and a failed reviewer is reported to the skill like any subagent result. There is no budget: effort and list length are the only controls, and the provider's quota is the ceiling.

While a panel runs, Telegram's status message gains one line per task, prefixed with that task's title and counting its direct subagents, such as interrogate: 2/4 · ✅ Opus 5.5 · ⏳ GPT 6 Sol · ❌ Haiku 5.5 (worker timeout). Subagents never message the chat; the answer arrives once, from the task that ran the skill, and that task's task_status lists each subagent's model, harness, effort, state and duration. It also carries pstackEvidence, what the run actually did, so the agent can check the work before reporting it done: the pstack:* skills its task tree invoked (or was assigned), and how many subagents it started per model and effort, e.g. {"skills": ["pstack:how", "pstack:poteto-mode"], "subagents": 2, "subagentModels": {"claude-opus-5-5 high": 2}}. A task that used neither has no pstackEvidence. The dashboard's Tasks view shows subagents under their parent.

Subagents ​

A set-up agent's Claude workers that load the plugin do not use Claude Code's own Agent tool (it is disabled for them). pstack's fan-out goes through three worker tools with the same arguments instead, so every subagent is a gateway task that can run on another provider. /tasks, /stop and the task list API show only the task that ran the skill; its task_status lists the subagents, and stopping it stops them:

ToolDoes
subagent_spawndescription, prompt, subagent_type, model, readonly, run_in_background — like Agent. model is a model ID from the sheet or an alias such as sonnet, resolved when the subagent is spawned (omitted, inherit-parent or auto = the parent's model); a pstack:effort-<level> agent sets the subagent's effort, otherwise it inherits the parent's (Codex has no max, so a GPT subagent asked for max runs at xhigh). In the foreground it waits and returns the result (if that wait is interrupted, the error carries the subagent's task_id: it keeps running, so wait for it or cancel it rather than spawning it again); in the background it returns a task_id. Refused while the conversation is draining or the agent's queue is full, like task_spawn
subagent_waitWaits for the listed subagents and returns each result or failure, and the names of files a subagent staged
subagent_cancelCancels the caller's own subagents
  • When a pstack playbook routes a step to another pstack skill (how, why, interrogate...), the worker is told to invoke it with the Skill tool and to start the subagents it calls for, rather than doing that work itself.
  • Subagents nest at most three levels below the worker; the third level gets no subagent tools and works inline.
  • Subagents use the usual per-agent, per-conversation and gateway-wide worker limits, except that a subagent's own waiting ancestors are not counted against it, so a chain can always continue while siblings queue. A gateway-wide slot borrowed this way never takes one of the slots reserved for agents (processLimits.reservedAgent), so a decision turn can still start. A queued subagent whose parent is not running (for example, waiting for the user's answer) does not start, and does not keep Telegram's status message up.
  • When a task ends, is being cancelled, fails, or needs reconciliation, its unfinished subagents are cancelled with it (after a gateway restart too), and a read-only subagent's own subagents are read-only too.
  • A subagent cannot ask questions, as with Claude Code's own subagents: task_request_input is refused (SUBAGENT_INPUT_UNAVAILABLE) and the subagent is told to finish and put the open question in its result, which its parent reads. Nothing is ever asked in the chat.
  • A parent waiting in subagent_wait cannot report progress itself, so its subagents' progress reports (and finishes) count as its progress: it is not reminded or stopped as stalled while they report.
  • A subagent whose stop could not be confirmed (needs_reconciliation, its process may still be running) keeps its slot until its cleanup is retried. While its parent runs, subagent_wait reports it; once no running parent can see it, it is reported to the chat like a task and listed in /tasks and the task index with Retry cleanup. Cancelling its finished root task retries the cleanup too.
  • A subagent's prompt is its whole assignment, as with Claude Code's own subagents: it does not see the user's request or the conversation, only what its parent wrote.
  • Subagents report to their parent, never to the chat; files a subagent staged are delivered with the root task's report. The agent's task index lists the root task only (and any subagent awaiting reconciliation, as above); task_status with its task_id lists the subagents.
  • A GPT subagent runs on Codex without the plugin: it gets the agent definition, the prompt and the plugin's path to read files the prompt names. A read-only GPT subagent runs in Codex's read-only sandbox; a read-only Claude subagent gets read tools only and no MCP server but the gateway's (--strict-mcp-config), so neither loads the agent's connectors or the user's or a plugin's MCP servers, which can change things outside the host. Either way a read-only subagent, including one that inherited readonly, is told so (right after the agent definition, which may claim the full tool set), and its gateway tools exclude those that change something (cron changes, memory writes, file sharing, narration, image/video generation and the browser); the gateway refuses them if called.
  • isolation: "worktree" runs on the host as usual and answers isolation: "ignored". Resuming an earlier subagent is not supported; pstack spawns fresh subagents.

Claude Code inside.