name: prism description: Spawn isolated agent sessions in their own git worktrees using the prism tool. Use when the user asks to spawn an agent, delegate work to another session, run something in parallel, or work on a PR or different repo.
Note: The prism source code and this skill file live in the
nixos-configrepository undermodules/programs/prism/. Changes to prism itself — the Go CLI, tmux config, agents, and skills — are made there.
Introspection layers
Prism provides three introspection layers, ordered from most to least human-readable:
--helptext — human-readable usage, intended for humans at a terminal.prism agent-context— machine-readable JSON describing the full CLI shape: every command, every flag (with type, enum values, defaults and default sources), available profiles, and cross-cutting precedence rules. This is the layer-2 surface agents should consult when they need to discover available flags, enum values, or profile names programmatically. It emits valid JSON to stdout and exits 0.- This SKILL.md — workflow prose, decision trees, and context that neither
--helpnoragent-contextcapture.
Using prism agent-context
# Full CLI shape as JSON (excludes hidden/internal commands by default)
prism agent-context
# Include hidden/internal commands (e.g. agent-run, sidecar)
prism agent-context --include-hidden
# Discover available profiles
prism agent-context | jq '.available_profiles'
# Inspect spawn's --isolation flag (type, valid values, default source)
prism agent-context | jq '.commands.spawn.flags["--isolation"]'
# See precedence rules for profile and isolation resolution
prism agent-context | jq '.precedence'
# List all top-level commands
prism agent-context | jq '.commands | keys'
The document shape (top-level keys):
| Key | Description |
|---|---|
schema_version |
Schema version string. Currently "1". Bump on breaking changes. |
prism_version |
Git SHA of the binary ("" in dev builds). |
commands |
Map of command name → CommandMeta (recursive, includes subcommands). |
available_profiles |
Array of profile names from ~/.config/prism/profiles.json. [] if missing. |
precedence |
Map of cross-cutting precedence chains (e.g. profile, isolation). |
Each CommandMeta contains:
description— short descriptionflags— map of"--flag-name"→{type, values?, default?, default_source?, required?, description}subcommands— recursive map (same shape)positional_args— array of{name, required?}aliases— optional array
Flag type is one of: bool, string, int, duration, stringArray, enum. When type == "enum", the values array lists every valid value — use it instead of parsing help text.
Spawning Agents with prism
prism spawn creates a new git worktree, starts a tmux session in it, and launches the agent harness (pi). Use it when work should be isolated, long-running, or in a different repo — rather than a subagent, which shares the current session context.
When to use prism spawn vs a subagent
| Situation | Use |
|---|---|
| User says "spawn an agent" or "use prism" | prism spawn |
| Work is on a PR branch | prism spawn --pr or prism pr |
| Task is long-running / should outlive this session | prism spawn |
| Quick research or analysis within this repo | subagent (@explore, @general) |
Converting a repo to bare+worktree layout
# Convert the current directory
prism convert
# Convert a specific repo
prism convert ~/code/nixos-config
This converts a regular git clone to the prism bare+worktree layout in-place. The working tree moves to <repo>/<branch>/, a bare clone is created at <repo>/.bare, and the index is populated immediately so git status is clean. This is the same operation as selecting [convert to bare+worktree layout] in the C-f picker.
Basic usage
# Spawn in the current repo with a timestamped branch
prism spawn --prompt "go implement feature X and open a PR"
# Spawn on a named branch — use a short descriptive kebab-case name, not an issue number
prism spawn --branch update-plex-image --prompt "..."
# To work in a different repo, delegate via its coordinator (see "Delegating work to another repo")
prism prompt home-ops@main --prompt 'update the plex image to the latest tag and open a PR'
# Check out a PR branch and spawn a session on it (--repo is supported on prism pr)
prism pr 268 --prompt "review this PR"
prism pr 268 --repo nixos-config --prompt "review this PR"
Passing prompts safely — shell escaping
The --prompt value passes through the caller's shell before prism receives it.
Shell metacharacters such as backticks, $(), $VAR, and double-quote contents
are interpreted by the shell and silently corrupted if you are not careful.
Preferred approaches (safest first):
--prompt-file <path>— Write the prompt to a temp file; the shell never touches its contents:printf 'run `gh pr view 42` and review the diff' > /tmp/prompt.txt prism spawn --prompt-file /tmp/prompt.txt--prompt -(read from stdin) — The literal value-is a reserved sentinel that tells prism to read the prompt from stdin. Pipe or heredoc the prompt in. Use a quoted heredoc delimiter (<<'EOF') to prevent expansion inside the body:prism spawn --prompt - <<'EOF' run `gh pr view 42` and review the diff EOFNote: because
-is reserved, you cannot pass the literal string-as a prompt via--prompt. Use--prompt-fileor single quotes with a different phrasing if your prompt content is literally a dash.Single quotes — Wrap the value in single quotes. Single quotes prevent all shell interpolation in bash/zsh:
prism spawn --prompt 'run `gh pr view 42` and review the diff'Single quotes cannot contain a literal
'. For prompts with apostrophes, prefer option 1 or 2 instead.
Do not use double quotes around prompts containing backticks or $:
# BAD — the shell executes `gh pr view 42` and splices its output in
prism spawn --prompt "run `gh pr view 42` and summarise"
All flags
| Flag | Description |
|---|---|
--branch <name> |
Branch name for the new worktree. Defaults to a timestamp. Use a short, descriptive kebab-case name derived from the task (e.g. update-plex-image, fix-login-redirect) — never an issue number, PR number, or Jira ID. The branch name becomes the session name (e.g. nixos-config@update-plex-image), so it should be immediately readable in prism sessions list and the tmux picker without looking anything up. |
--pr <number> |
Fetch and check out the branch for this PR number. |
--prompt <text> |
Instruction passed to the agent on launch. Wrap values containing shell metacharacters in single quotes. The value - is reserved and reads from stdin (cannot pass a literal -). |
--prompt-file <path> |
Read the prompt from a file instead of passing it as an argument. Mutually exclusive with --prompt. A single trailing newline is stripped. |
--agent <name> |
Agent to use (worker or plan). Defaults to worker. |
--attach |
Switch the current tmux client to the new session instead of spawning headlessly. |
--reuse |
If an active session already exists on the requested branch, return its name/port/agent and exit 0 instead of failing. Without --reuse, spawning onto an existing branch exits non-zero and tells you to run prism cleanup or pass --reuse. |
Behaviour
- Headless by default: the session is created in the background. The user can switch to it via the prism dashboard (
C-w) or picker (C-f). --attach: use when the user wants to be taken directly to the new session.- The prompt is sent to the agent automatically after startup — no manual input needed.
- Each spawned session gets its own worktree, so changes are isolated from other branches.
Delegating work to another repo
When you need to delegate work to a repo you are not the coordinator for, route it through that repo's @main coordinator session. The coordinator has full context about that repo's conventions, open work, and branch state.
Flow:
- Run
prism sessions listand look for<repo>@main. - Found, not in
waitingstate: send the work request withprism prompt <repo>@main --prompt '...'. - Found, in
waitingstate: escalate to the user — the coordinator is blocked and expecting human input. The user needs to switch to that session and unblock it directly. Do not attempt to work around the waiting state guard. - Not found: there is no coordinator to delegate to. Escalate to the user and ask them to start a
<repo>@maincoordinator session. Note: you also cannot work around this by spawning ontomainyourself — in the bare+worktree layout prism uses,mainalready has a worktree, soprism spawn --branch mainwill fail with a git error.
Spawning directly into a feature branch in another repo (bypassing the coordinator) should only happen when you are the coordinator for that repo, or when the user explicitly instructs you to.
# Check if the target repo has a coordinator session
prism sessions list
# If home-ops@main exists and is not waiting:
prism prompt home-ops@main --prompt 'Please update the plex image to the latest tag and open a PR'
# If home-ops@main exists but IS in waiting state:
# escalate to the user — they need to switch to that session and unblock it
# If home-ops@main does not exist:
# escalate to the user — ask them to start a coordinator session for home-ops
Example: spawning directly as coordinator
If you are the coordinator for the target repo (or the user has explicitly instructed you to spawn directly), use prism spawn:
prism spawn \
--branch update-plex-image \
--prompt "find the plex container image in this repo and update it to the latest tag from dockerhub, then open a PR"
Running code review
Scope:
prism reviewis for worker agents and spawned sessions only. Coordinator agents must never callprism reviewdirectly. When a user asks a coordinator to review a PR, the coordinator should useprism pr <number> --prompt 'review this PR'to spawn a session on the PR branch — that spawned session then runsprism reviewand reports back.
Code review is done with prism review <pr>, which is async: it spawns 5
review agents, registers a group, and returns immediately with an
acknowledgement. Results are delivered to you via a follow-up prism prompt
when all agents complete.
prism review <pr-number>
Do NOT commit, merge, or announce completion until the review-complete prompt arrives. When it does, handle PASS/FAIL per the worker agent instructions.
For a synchronous flow (one-shot script, no other work to do meanwhile) pass
--wait:
prism review <pr-number> --wait
prism review <pr-number> --wait --json # script-friendly
--wait blocks until the review group reaches a terminal state and exits 0
on all-PASS, non-zero on any FAIL / no-start / timeout. See the --wait
section above for the full contract (exit codes, Ctrl-C semantic, idempotent
observation).
Pre-flight rebase gate (prism review refuses when behind origin/main)
Before spawning any review agents, prism review runs a one-shot pre-flight
check:
git fetch origin main(one network round-trip).Strict ancestor check:
git merge-base --is-ancestor origin/main HEAD.If
origin/mainis an ancestor ofHEAD: proceed unchanged.If not: refuse, exit non-zero, no agents spawn, and no cycle counter increment. The error message names the number of commits behind and the recommended fix:
prism review: branch is N commits behind origin/main git fetch origin main git rebase origin/main git push --force-with-lease Or rerun with --rebase to do this inline.
The --rebase flag is the inline opt-in fix:
prism review <pr> --rebase
It performs the fetch + rebase + force-push inline and then proceeds to the
review against the rebased HEAD. If the rebase produces conflicts, the rebase
is aborted, the worktree is restored to the original HEAD, and the command
exits non-zero — never leaves the worktree mid-rebase. Resolve conflicts
manually and re-run.
Why this gate exists. Reviewers regularly produce noisy findings of the
form "you should also update X" when X landed on main after the branch was
cut. A simple rebase makes the diff smaller and the finding disappear, but
discovering this from a FAIL verdict burns a full 5-agent cycle. The gate
catches drift in one fetch, before any agent spawns.
Cycle-counter contract. Gate failures (behind-main refusal, fetch failure,
missing origin/main, rebase conflict abort) do not increment the
review-cycle counter. They are the same category as "round already in
progress" / pure-infrastructure failures / ran-but-no-parseable-verdict
rounds (#1995): no full set of parseable verdicts was produced, so the
round does not count. A worker that hits the gate three times in a row
and then runs three real reviews still has all three real cycles
available before the LOOP-LIMIT footer fires.
Rounds that do not count toward the 3-cycle limit:
- Pre-flight gate refusals — behind-main, fetch failure, missing
origin/main, rebase conflict abort. No agents spawned. - Round-already-in-progress refusals — a prior review is still active for the same parent. No agents spawned.
- Pure-infrastructure failures — every agent failed to start (no frames received — e.g. the container never bound its port) and/or stalled mid-run (the agent ran, then stopped producing frames — #2239). Header mentions "infrastructure failure".
- Ran-but-no-parseable-verdict rounds (#1995) — one or more agents
reached
finishedstate without emitting a parseable<verdict>PASS</verdict>/<verdict>FAIL</verdict>tag (e.g. truncated mid-analysis or ended on a tool-only turn). Header says "One or more review agents ran but produced no parseable verdict" and explicitly tells the worker to re-run. This is NOT a code-quality FAIL — re-run, do not escalate.
In each of these cases the correct action is to re-run prism review
(after fixing any orthogonal blocking issues another agent surfaced),
not to escalate.
Design notes:
- Strict ancestor check, not loose. A "files-touched-in-common" variant
sounds clever but breaks on renames, deletes, and cross-cutting helper
introductions. Strict
is-ancestoris unambiguous and cheap. - Refuse-by-default + opt-in
--rebase, not auto-rebase. Areviewverb that silently mutates the branch is a footgun if the worker has uncommitted work or local-only commits. Default refusal forces a deliberate choice. - One-shot at the start, not continuous.
maincan advance during a review run; we do not chase that. The gate is a snapshot at review-spawn time, consistent with how CI works. - Same gate in host-direct and container-routed paths. A container worker
routes through
/reviewon the host sidecar; the gate runs on the host side, and the refusal streams back to the container worker as a non-zero review exit — same UX as a host-direct refusal.
Handling no-start errors in review-complete prompts
When a review-complete prompt says "One or more review agents failed to start
(infrastructure failure)", treat it as a failed review run — not a
code-quality FAIL verdict. The agents never ran, so no conclusions about the PR
quality can be drawn. Re-run prism review <pr> to retry the infrastructure
that failed. Do not treat a no-start error the same as a FAIL verdict from a
review agent that ran and found issues.
Signs of a no-start error in the per-agent findings:
**Verdict:** ERROR- Output contains
ERROR: agent failed to start (no-start): - The delivery message header mentions "infrastructure failure" and instructs you to re-run
Handling mid-run stalls in review-complete prompts
A mid-run stall is the sibling failure class to a no-start (#2239): the agent started and did real work — inbound frames flowed — then went silent long enough to trip the inactivity watchdog. The report distinguishes it from a no-start so you can tell "never ran" from "ran, then wedged".
Treat a stall the same way you treat a no-start: it is an infrastructure failure to re-run, not a code-quality FAIL, and the round does not count toward the 3-cycle limit. One caveat: repeated stalls under concurrent load can indicate provider rate/subscription limits — if the same agent stalls across multiple consecutive rounds, escalate to the coordinator instead of burning further rounds on blind re-runs.
Signs of a mid-run stall:
**Verdict:** ERROR- Output contains
ERROR: agent stalled mid-run after <elapsed> (<n> frame(s) received, last at <t>) - The delivery message header says "stalled mid-run" and mentions "infrastructure failure"
Handling ran-but-no-parseable-verdict in review-complete prompts
When a review-complete prompt says "One or more review agents ran but
produced no parseable verdict" (#1995), treat it the same way you treat a
no-start error: re-run prism review <pr> rather than escalate. The agents
did run — the problem is that one or more of them terminated in finished
state without emitting a parseable <verdict>PASS</verdict> /
<verdict>FAIL</verdict> tag (e.g. truncated mid-analysis, ended on a
tool-only turn). This is not a code-quality FAIL and the round does
not count toward the 3-cycle limit.
Signs of a ran-but-no-parseable-verdict round:
- The per-agent output for at least one agent contains
ERROR: no verdict found in agent outputorERROR: no output produced - The delivery message header explicitly says the agent(s) ran but produced no parseable verdict and instructs you to re-run
If any other agent in the same round surfaced genuine blocking issues, fix those before re-running.
If no review-complete prompt arrives within 30 minutes, check progress with:
prism checkin <session>~review-<N>-review-goal
Investigator agents
Use prism investigate to spawn a read-only research session from within a prism session. Investigators are well-suited to tasks like tracing call chains, mapping symptoms to a file:line, or surveying scope before spawning a worker. They are denied in the worker deny list — only coordinators can use them.
Spawning
prism investigate --prompt "question"
prism investigate --prompt-file /tmp/question.txt
prism investigate --name my-analysis --prompt "question"
Flags
| Flag | Description |
|---|---|
--prompt <text> |
Research question. Mutually exclusive with --prompt-file. |
--prompt-file <path> |
Read the question from a file. Mutually exclusive with --prompt. |
--name <slug> |
Human-readable slug for the session name. When provided, the session is named <invoker>~investigate-<slug> exactly. Only [a-z0-9-] allowed, max 40 chars, no leading/trailing dash. When omitted, the slug is derived automatically from the prompt text. |
One of --prompt or --prompt-file is required. The command returns a session name within ~2 seconds and exits 0. There is no --wait flag — prism investigate is inherently async.
Session-naming convention
Spawned sessions are named <invoker>~investigate-<slug>. When --name is provided, <slug> is the supplied name exactly. When --name is omitted, <slug> is a short kebab-case token derived automatically from the prompt text (word-boundary truncated at 30 chars).
Per-turn notification contract
After each investigator turn that produces output, the sidecar delivers a body-bearing notification to the invoking session. Each notification includes:
- Header:
From investigator session: <name>— use this to route the notification to the correct open question when multiple investigators are running concurrently. - Body: the investigator's findings for that turn.
- Steering hint:
Reply with: prism prompt <name> --prompt '...'— follow-up steering is done viaprism prompt.
Cleanup responsibility
Investigators do not self-terminate. When the investigation is complete and the findings consumed, the coordinator must run:
prism cleanup --yes --session <inv-session>
Constraint
prism investigate must be run from within a prism session (errors if no invoker is detectable). Workers have prism investigate in their deny list; only coordinators may use it.
Merge queue (coordinators only)
Coordinators only. Worker agents, container worker agents, bwrap worker agents, and review agents all have
prism mergeandprism merge *denied in their bash deny lists. If you are not a coordinator agent, skip this section.
The merge queue is a local serial FIFO queue running in the coordinator's sidecar process. The sidecar polls the head of the queue every 45 seconds; only one PR is in flight at a time. The watcher's lifetime equals the coordinator session's lifetime — there is no persistent daemon.
A queued PR moves through states keyed off GitHub's mergeStateStatus: watching → merged / failed / cancelled / abandoned. See issue #783 for the full state machine.
Command surface
| Command | Description |
|---|---|
prism merge <pr> |
Enqueue a PR. Returns within ~2 seconds. Idempotent — safe to call on an already-queued PR. |
prism merges |
Show the queue scoped to the current coordinator session (alias for prism merges list). |
prism merges list |
Same as above. |
prism merges list --failed |
Show only failed entries. |
prism merges list --abandoned |
Show entries left behind by a previous coordinator incarnation. |
prism merges list --all |
Include terminal-state history (last 7 days). |
prism merges cancel <pr> |
Remove a watching entry from the queue. |
Add --json to any prism merges / prism merges list invocation (including with --failed, --abandoned, or --all) to get a JSON array of merge-queue entries instead of the table — use this when scripting or polling.
--wait for synchronous flows (#1500)
prism merge, prism review, and prism spawn accept --wait for cases where the agent's workflow is genuinely synchronous — for example, a one-shot script that wants to merge a PR and then immediately deploy. With --wait, the command blocks until the underlying job reaches a terminal state and exits non-zero on any non-success terminal.
| Command | Terminal definition | Default --timeout |
|---|---|---|
prism merge <pr> --wait |
merged / failed / cancelled / abandoned (in pending_merges) |
30m |
prism review <pr> --wait |
All review agents reached finished/error/deleted (group complete) |
20m |
prism spawn ... --wait |
Spawned agent reached finished / error / interrupted / deleted for its first turn |
10m |
When to prefer --wait vs the notification path:
- Prefer
--waitwhen you have no other useful work to do until the job lands — a one-shot script, a deploy that depends on the merge, or a review that gates further commits. - Prefer the notification path (no
--wait) when you have other tasks in flight. Coordinators with several PRs in flight should never--wait—prism merge <pr>returns immediately and the merge-queue watcher delivers a notification when each PR lands. Same shape forprism reviewandprism spawn. - Add
--jsonwhen scripting:prism merge <pr> --wait --jsonemits a single JSON object on stdout (no human-readable chatter) so consumers canjqthe status.
Idempotent observation. prism merge <pr> --wait on an already-merged PR returns immediately with the merged status — safe to call any number of times.
Ctrl-C semantic. Killing a --wait invocation (Ctrl-C, SIGTERM) interrupts the local wait only — the underlying merge / review / spawn keeps running. To recover the result later, re-run the same command (or prism merges list / prism reviews list / prism checkin <session>).
Inside a sandbox. --wait works identically inside and outside a bwrap / sandbox-exec sandbox. The CLI auto-detects PRISM_HOST_API and routes its terminal-state probes through the sidecar's read-only wait endpoints, so the host's prism.db is the source of truth in either case.
Exit codes. --wait paths use exit codes that distinguish failure modes:
| Exit | Meaning |
|---|---|
0 |
terminal success (merged / all-PASS / finished) |
2 |
terminal failure (failed CI, any-FAIL, error state) |
3 |
local --timeout elapsed (the underlying job continues) |
4 |
user interrupted with Ctrl-C (the underlying job continues) |
Reviews ledger: prism reviews list
Reviews now have a dedicated ledger surface analogous to prism merges. Use it instead of prism sessions list | grep '~review-N-' (fragile, missing group metadata).
prism reviews # alias for `prism reviews list`
prism reviews list # all review groups, newest first
prism reviews list --json
Each row carries: PR number (when derivable from the parent branch), parent (worker) session, agent sessions, group state (in-progress / completed / empty), round number, and started_at timestamp.
Notification contract
When a queued PR reaches a terminal outcome, the watcher delivers a bus notification to the coordinator session. The text is:
| Outcome | Notification text |
|---|---|
| Merged (with archive path) | PR #N merged. Archive: <archive_path>. Run `git pull` in @main and `prism cleanup` the worker session. |
| Merged (no archive path yet) | PR #N merged. Run `git pull` in @main and `prism cleanup` the worker session. |
| Merge conflicts | PR #N has merge conflicts — worker rebase needed |
| CI failure | PR #N CI failed — needs worker fix |
| Closed without merging | PR #N was closed without merging — removed from queue |
| Review required | PR #N is blocked — human reviewer approval required before merge |
| Changes requested | PR #N is blocked — reviewer requested changes — fix and re-request review |
| Other merge failure | PR #N merge failed: <error> |
| Coordinator session ended while watching | Row transitions to abandoned — surfaces via prism merges list --abandoned only; no live notification. |
Action table
When a merge-queue notification arrives, treat it as high-priority (same as a worker finished-notification):
| Notification | Action |
|---|---|
merged |
git pull in @main, then prism cleanup --yes --session <worker-session> |
merge conflicts |
prism prompt <worker-session> asking it to rebase onto main and push; re-enqueue with prism merge <pr> after the worker finishes |
CI failed |
prism prompt <worker-session> asking it to investigate the failed check, fix, and push; re-enqueue with prism merge <pr> |
closed without merging |
Usually nothing — the PR was closed deliberately. Investigate if unexpected. |
blocked — human reviewer approval required |
Request a human review on the PR (e.g. via gh pr review --request <user>). Once approved, re-enqueue with prism merge <pr>. |
blocked — reviewer requested changes |
prism prompt <worker-session> asking it to address the reviewer's requested changes and re-request review on the PR. Once the reviewer re-approves, re-enqueue with prism merge <pr>. |
merge failed: <error> |
Read the error, decide whether to retry (prism merge <pr>) or escalate to the user. |
abandoned (via --abandoned listing) |
A new coordinator decides whether to re-enqueue with prism merge <pr>. |
Why workers cannot invoke it
Worker agents, container worker agents, bwrap worker agents, and review agents all have prism merge and prism merge * in their bash deny lists. Only coordinator agents have it allowed. This is by security design: only coordinators arbitrate merge order. Do not attempt to work around the deny list.
Example: reviewing a PR (manual spawn)
# PR in the current repo
prism pr 268 --prompt "review this PR and summarise the changes"
# PR in a different repo (--repo is supported on prism pr)
prism pr 268 --repo nixos-config --prompt "review this PR and summarise the changes"
Example: create a ticket then spawn an agent to action it
When the user asks you to create a ticket (e.g. Jira) and then spawn an agent to work on it:
- Create the ticket using the appropriate MCP tool and capture the ticket ID (e.g.
PROJ-123) - Derive a short, descriptive kebab-case branch name from the ticket title — not the ticket ID
- Spawn the agent — pass the ticket ID in the prompt so the agent can look it up; the Atlassian MCP is available in the spawned session
# After creating ticket PROJ-123 ("Update plex image to latest tag"):
prism spawn \
--branch update-plex-image \
--prompt "Please take a look at PROJ-123, cover off the work required, and open a pull request."
Lifecycle: cleaning up after a merge
When you spawn a session and later merge its PR yourself, you are responsible for cleaning up the worktree and session. The spawned agent cannot do this — it would be tearing down its own environment.
prism spawn prints the session name when running headlessly:
session "nixos-config@update-plex" created
Note down the session name from that output. Two commands tear down a session, with different defaults:
| Command | Default behaviour | When to use |
|---|---|---|
prism close --yes --session <name> |
Smart-decide: soft-close if an open PR exists for the branch, otherwise hard-cleanup. | Default. Safe for parked WIP branches and merged work alike. Bound to prefix+q. |
prism cleanup --yes --session <name> |
Always destructive (remove worktree + force-delete branch). | Scripted coordinator workflows that know the work is finished and want a guaranteed hard cleanup. |
prism close — smart-decide
prism close --yes --session "nixos-config@update-plex"
Decision tree (issue #2179):
- Coordinator session (
root_agent_name == "coordinator"or@main) → soft close. - Non-worktree session (no
@) → soft close. - Worker worktree session: probe
gh pr list --head <branch>:- any PR is OPEN → soft close (preserve worktree + branch)
- all PRs MERGED/CLOSED → hard cleanup
- no PR found → hard cleanup
- probe error / timeout /
ghmissing / unauthenticated → soft close (fail-safe)
The fail-safe is deliberate: a spurious soft close costs one extra prism close --remove-worktree later; a spurious hard cleanup destroys uncommitted work. The probe is bounded by a 5-second context timeout so a hung GitHub API cannot wedge the tmux popup.
Force flags override the decision (mutually exclusive):
--keep-worktree— always soft close (paranoid mode for long-lived WIP branches).--remove-worktree— always hard cleanup ("I'm done with this branch").
prism close --yes writes nothing to stdout/stderr on the happy path, making it safe to bind to a tmux popup. The --json envelope is identical to prism cleanup's and is still emitted when --json is passed.
prism cleanup — always destructive
prism cleanup --yes --session "nixos-config@update-plex"
prism cleanup --yes --session <name> will:
- Remove the git worktree
- Force-delete the local branch (relies on the orchestrator-trust contract: call this only after confirming the PR is merged)
- Kill the tmux session, redirecting any attached client to
scratchpad - Mark the
agent_statusrow as ended (stampsended_at, releases the harness port, and clears the piharness_session_idso the next spawn starts a fresh conversation)
For parity with prism close, prism cleanup also accepts --keep-worktree, which downgrades it to a soft close even on a worker session. Without that flag the command keeps its pre-#2179 always-destructive default, so scripted coordinator workflows that call prism cleanup --yes --session <X> after a merge are unaffected.
The agent_status row itself is preserved — it is not deleted. Re-spawning on the same branch name reuses the row: tmux-session-start re-seeds it to idle, which the state machine accepts from any non-deleted terminal state (error, finished, interrupted). Long-term retention is handled by the 90-day Prune job.
The --json envelope reports per-resource outcomes so you can verify the bookkeeping ran:
{
"session": "nixos-config@update-plex",
"worktree_removed": "/code/nixos-config/update-plex",
"branch_deleted": "update-plex",
"session_killed": true,
"ended_at_stamped": true,
"harness_port_released": true,
"harness_session_id_cleared": true
}
Each of ended_at_stamped, harness_port_released, harness_session_id_cleared is true on success (or idempotent no-op — the row is in the cleaned-up state), or a string describing the failure on error. harness_session_id_cleared has one extra value class: "skipped: archive failed: <err>" when the resume-linkage sever did not run because the session-archive step failed — the sever runs after the archive (it deletes the same transcript JSONL the archive copies) and is skipped on archive failure so the transcript is not lost (#2219). If the DB cannot be opened, all three carry the same failure description so the operator can tell the bookkeeping was NOT attempted. Re-running cleanup on an already-ended session is idempotent: ended_at_stamped and harness_port_released still report true, the row stays ended, and the command exits 0 — though when the first run already wrote the archive, the re-run reports harness_session_id_cleared as "skipped: archive failed: …" (the archive directory already exists); nothing is lost or re-deleted, since the first run already archived and severed.
Only call this after you have confirmed the PR is merged. The --yes path always force-deletes the branch — it does not check whether the branch is reachable from main, because squash-merges produce a different SHA on main than the branch tip.
Pi sessions block git worktree prune and git worktree remove at the extension layer. When recovering from a failed spawn, use prism cleanup — do not reach for git plumbing.
A/B-test workflow: prism spawn --abtest, prism stats compare, merge decision, cleanup
An A/B test spawns two sibling sessions on the same prompt with different model profiles (or other configuration), lets both run to completion, then compares the two outcomes to decide which one to merge.
The workflow is:
Spawn the pair.
prism spawn --abtest <profile-a>,<profile-b> --prompt '<the prompt>'creates two sessions that share a singleabtest_pair_idinspawn_inputs. Each leg runs in its own worktree on its own branch and opens its own PR when it finishes.Wait for both workers to finish. Each session lands in a terminal state (
finished,error, orinterrupted). You can watch them inprism sessions list; both legs will surface terminal-state notifications via the usual coordinator-notification surface (see Worker terminal-state notifications below).Run
prism stats comparefor the merge-decision data. Once both legs have transitioned to terminal state — before you merge either PR — compare them:prism stats compare <instance-id-A> <instance-id-B> # or, if you minted the pair via --abtest, by the shared pair id: prism stats abtest <pair-id> # machine-readable when scripting the winner decision: prism stats compare <instance-id-A> <instance-id-B> --json | jq .The output carries the
Spawn Inputsblock (profile, harness, isolation, agent role, branch, abtest_pair_id) plus per-axis aggregates (tokens, cost, tool calls, durations, end_state). The aggregates are available between terminal-state transition andprism cleanup— they no longer require cleanup to materialise (issue #2102). Use the cost / duration / msg_assistant axes alongside the quality of the produced PRs to pick a winner.Merge the winner, close the loser. Standard merge / close flow on the two PRs.
Cleanup both sessions.
prism cleanup --yes --session <winner>andprism cleanup --yes --session <loser>. Cleanup persists thespawn_outcomerow for long-term querying viaprism stats --group-by profile|model|...; until then the row is computed on the fly fromagent_eventswheneverprism stats compareis run.
Notes:
prism stats compareshows—for aggregate axes while a session is still in progress (stateactive,idle, orreviewing). The aggregates only stabilise at terminal transition.- The
Spawn Inputsblock surfaces whatever the writer captured at spawn time. Pre-#2087 sessions may have a partial row — missing columns render as—rather than collapsing the whole block. - Use
--json(preferred) or the equivalent--format jsonfor machine-readable output (e.g. when scripting the winner decision); thespawn_inputsobject carries the same fields shown in the table. On error, both surfaces emit a single-line{"error":"..."}JSON envelope to stderr (no cobra usage dump) — script the failure path against the JSON contract too, not by parsing human-readable text (issue #2099).
Querying prism state — prefer --json for scripting
Every list-style and lookup-style prism subcommand supports a --json flag that emits a single JSON document to stdout — keys are snake_case, timestamps are RFC 3339, empty lists are [] (never null, never absent), and any informational/progress text routes to stderr. When you need to parse prism output programmatically, always pass --json. Screen-scraping tabular human-readable output is fragile and burns tokens.
| Command | --json shape |
|---|---|
prism sessions list --json |
array of session-status objects |
prism sessions list --all --json |
array of session-status objects across all repos |
prism checkin <session> --json |
session + events object |
prism stats --json (and prism stats <id> --json) |
rows mirroring the host-API |
prism stats compare --json <runs...> (alias for --format json) |
{runs:[...], diffs:{spawn_inputs:[...], spawn_outcome:[...]}} |
prism stats abtest <group_id> --json |
same shape as stats compare --json |
prism stats --abtest --json |
{pairs:[...]} — abtest pair listing |
prism merges --json (and prism merges list --json, --failed --json, --abandoned --json, --all --json) |
array of merge-queue entries |
prism audit --json |
object with events array, truncated bool, optional hint |
prism sessions status --json |
object keyed by state (active, waiting, idle, finished, error) with integer counts (mutually exclusive with --tmux-format) |
prism profile list --json |
array of profile objects |
prism profile show [name] --json |
single profile object describing the slot table |
prism archive <session> --all --json |
array of archive-entry objects (instance_id, started_at, archive_path) |
Checking in on a running session
Use prism sessions list to see all active agent sessions with their state and current task title:
prism sessions list # human-readable table
prism sessions list --json # JSON array (use this when scripting)
Use prism checkin <session> to read the recent conversation history for a session, sourced from the prism DB. The default output is a rich narrative view: assistant messages, state changes, and tool call one-liners interleaved chronologically. Pass --json when you need to parse the events programmatically.
prism checkin nixos-config@update-plex
checkin: nixos-config@update-plex
state: finished
[18:48:35] assistant
I'll fix both issues from the review.
→ edit: container.go ✓
→ edit: sidecar.go ✓
→ bash: go build ./... ✓
→ bash: go test ./internal/... ok (9.2s)
[18:49:16] assistant
Tests pass. Committing and pushing.
→ bash: git commit -m "fix: capture elapsed..." ✓
→ bash: git push ✓
[18:50:25] ● finished
── end of event log ──
Tool one-liner format: → <tool>: <key_arg> <result_summary>
- bash — key arg is the command (first ~80 chars); result is first meaningful output line,
✓for empty stdout,✗ <message>on error - read — key arg is the file path; result is
✓ (N lines) - edit / write — key arg is the file path; result is
✓or✗ - task — key arg is the description; result is
✓or✗ - glob / grep — key arg is the pattern; result is
N matchesorno matches - todowrite — result is
✓(key arg omitted)
State changes appear inline as ● <state> (e.g. ● finished) with a timestamp.
User messages (▶ user) are visually distinct from assistant turns so coordinator prompts and bus notifications are easy to spot.
With no argument, prism checkin lists available sessions and exits with a hint.
See Debugging a running or stuck session for prism checkin flag reference and a full decision tree for diagnosing issues.
Sending a follow-up prompt to a running session
Use prism prompt <session> --prompt <text> to send a follow-up message to the agent in a session that is already running (or has finished and is waiting for input):
# Simple prompt — single quotes prevent shell interpolation
prism prompt nixos-config@update-plex --prompt 'looks good, go ahead and open a PR'
# Prompt with backticks or complex content — use --prompt-file
printf 'run `make test` and fix any failures' > /tmp/p.txt
prism prompt nixos-config@update-plex --prompt-file /tmp/p.txt
# Or via stdin
prism prompt nixos-config@update-plex --prompt - <<'EOF'
run `make test` and fix any failures
EOF
The same shell-escaping conventions that apply to prism spawn apply here —
see Passing prompts safely above.
prism prompt flags
| Flag | Description |
|---|---|
--prompt <text> |
Prompt text to send. Supports - to read from stdin. |
--prompt-file <path> |
Read prompt from a file. Mutually exclusive with --prompt. |
--deliver-as <mode> |
Delivery mode for socket-pipe (PI) sessions: steer (default), followUp, or nextTurn. Has no effect for HTTP-transport sessions. |
Delivery modes (relevant for PI / socket-pipe sessions):
| Mode | Behaviour |
|---|---|
steer (default) |
Injects the prompt mid-turn so the agent sees it immediately, even during a long tool-call sequence. Use for coordinator mid-flight corrections and scope changes. |
followUp |
Queues the prompt as the next user turn, delivered after the current turn completes. Use when the message is informational and does not need to interrupt the current turn. |
nextTurn |
Alias for followUp; the sidecar's own default when deliver_as is absent from the request body. |
The CLI validates the mode client-side before making any network call. An invalid value exits non-zero with a message listing the accepted values.
The prompt is delivered directly via HTTP to the session. The session must exist and have an active harness port — use prism sessions list to check first.
Waiting state guard
prism prompt will refuse to send a prompt if the target session is in waiting state. A waiting agent has paused and is expecting direct input from the user — injecting a programmatic prompt would corrupt the input field.
If you encounter this error, escalate to the user. Do not attempt to work around the guard. The user must switch to the session themselves (via C-f or C-w) and respond directly.
session "nixos-config@update-plex" is waiting for user input
The agent has paused and is expecting a direct response from the user.
Please switch to that session and respond there, or escalate to the user
so they can address it directly.
prism checkin nixos-config@update-plex — inspect the current state
(C-f or C-w) — switch to the session in tmux
Escalating to your coordinator with prism escalate
Workers that hit a decision they cannot make alone (3-cycle review-limit reached, AC contradiction, scope ambiguity, infrastructure block) should use prism escalate rather than crafting a prism prompt to the coordinator by hand and stopping. The command resolves the right coordinator, delivers the message, transitions the calling session into a new escalated state, and emits a session.escalated bus event — in one step, with no redundant "has finished" notification.
Surface
# Auto-discover the same-repo coordinator and deliver the prompt.
prism escalate --prompt "3-cycle review limit reached on PR #1234. Agent failing: review-security. Decision needed: option A (relax) or option B (rework)."
# Read the body from stdin for multi-line prompts:
prism escalate --prompt - <<'EOF'
PR #1234 cycle 3:
- review-goal, review-code, review-qa, review-context: PASS
- review-security: FAIL on the same blocker as cycle 2
Proposed resolution: relax permission check to coordinator-only.
Decision needed: yes / no.
EOF
# Override discovery and target a specific session by name:
prism escalate --to nixos-config@main --prompt "..."
State machine
active ──prism escalate──▶ escalated
escalated ──turn_start (after the escalating turn ends)──▶ active
escalatedis a new value alongsideactive/idle/finished/reviewing. It surfaces inprism sessions listso a glance shows which workers are paused awaiting guidance.- Same-turn frames do not clear it (issue #2255):
prism escalateruns as a bash tool call mid-turn, and the agent loop emits furtherturn_startframes before the escalating turn ends. The sidecar arms a same-turn guard when the escalate succeeds and suppresses those frames' state writes, so the rest of the escalating turn cannot clobber theescalatedstate. The guard releases when the turn fully ends (finished debounce), when a prompt is delivered to the session, or on a terminal exit. - After the escalating turn has ended, the transition out is triggered by any incoming
turn_start, not specificallyprism prompt. A human who pokes at the worker via tmux clears the flag too. - While in
escalated, the sidecar suppresses the "has finished" notification — including for asession_shutdownterminal exit. Thesession.escalatedbus event is the notification.
Discovery rules
| Situation | Behaviour |
|---|---|
| Exactly one same-repo coordinator candidate | Auto-discover, send to it. |
| Multiple same-repo coordinator candidates | Refuse without --to; list candidates and exit non-zero. State does NOT transition. |
| No coordinator candidate found | Still transition into escalated; record no coordinator found, please wait for a human to come check on you in the worker's own log. The worker stays paused. |
--to <session> set but session unknown |
Exit non-zero. State does NOT transition. |
A same-repo coordinator candidate is any active (ended_at IS NULL) row in the same repo whose root_agent_name = 'coordinator', OR a legacy row literally named <repo>@main with NULL root_agent_name.
Bus event
- New event type
session.escalated, distinct fromsession.finished. Existing handlers that subscribe only tosession.finishedcontinue to receive nothing for escalations. - Payload carries:
source(calling worker),target(coordinator session, empty when none),prompt(body),pr_numbers(open PRs whose head matches the worker's branch),branch,head_sha,verdicts(last review-cycle verdicts when discoverable),occurred_at(RFC3339). - The same payload is also written into the calling session's own event log as type
escalationsoprism checkin <self>shows the escalation context inline.
Success signal — the OK line is the verification
On successful delivery prism escalate prints exactly one line to stdout, mirrored to stderr so combined-stream capture (e.g. a bash tool that merges both) always surfaces it:
prism escalate: OK delivered to <target> (delivery_id=<uuid>)
The OK token is the first whitespace-delimited word after escalate: so callers can grep for it as the unambiguous success signal. Do not re-run prism escalate to verify delivery — the OK line is the verification. Re-running is the bug pattern issue #2018 fixed: a worker that interpreted a (no output) capture as failure and re-ran produced two distinct deliveries to the coordinator before the sender-side guard was added.
The --json flag emits a single line to stdout instead:
{"delivered_to": "<target>", "delivery_id": "<uuid>", "replayed": false}
In --json mode the human-readable line is NOT emitted on stdout (mutual exclusion); it may still be mirrored to stderr for log capture. On error, --json emits {"error": "<message>"} to stderr and exits non-zero.
The success signal reaches the caller identically from a direct-host invocation and from inside a bwrap / sandbox-exec sandbox: the sandbox path's sidecar proxy captures the host-side child's stdout and stderr separately and re-emits them on the matching local streams, so the OK line lands on the container's stdout (and the mirror on stderr) byte-for-byte the same as a host invocation. --json is forwarded to the host child via the proxy request body, so the JSON envelope is also surfaced end-to-end.
Sender-side idempotency — re-runs within 5 minutes are a no-op
Running prism escalate a second time within 5 minutes with the same (calling session, target, prompt text) triple as a previously-delivered escalation is a no-op that exits 0. The replay invocation:
- does NOT write a new
bus_messagesrow - does NOT write new
escalationorsession.escalatedrows toagent_events - does NOT re-deliver the prompt to the coordinator's sidecar
- does NOT re-transition
agent_status.state(it staysescalated)
The replay emits a distinct success line so the operator/agent can tell it was deduped:
prism escalate: OK already delivered to <target> (delivery_id=<prior>, age=<duration>)
The <duration> is the time since the prior sent_at, formatted as Ns / Nm / Nh. The --json form is {"delivered_to": "<target>", "delivery_id": "<prior>", "replayed": true, "age_seconds": <int>}.
The dedup query is scoped to from_session = self exactly. A different worker in the same repo sending the same prompt to the same coordinator is a distinct escalation and lands as normal.
The dedup guard additionally requires the calling session's current_state to still be escalated. If an incoming turn_start unstuck the worker between the two invocations, the second call is a genuine re-escalation: it transitions back to escalated and re-delivers.
Delivery guarantee — exactly-once with optional replay marker
The escalation prompt is delivered to the coordinator's harness exactly once per prism escalate invocation. The sidecar's /prompt handler is idempotent: each delivery carries a delivery_id (UUID minted by the sender), and repeats whose ID has been seen recently are dropped before they reach the harness pipe — the dedup set is bounded (LRU, capacity 256, in-memory per sidecar). Senders that retry with the same delivery_id see {"replayed":true} in the response so the retry is observable, not silent.
The one path that produces a second copy is the reconnect-replay case for AC #7: if the coordinator's PI extension is disconnected from its sidecar when an escalation arrives, the sidecar buffers the delivery and flushes it on the next successful handshake. The replayed prompt frame carries replay: true so the coordinator can distinguish it from a fresh signal. The buffer is bounded (capacity 16); under a long partition the oldest entries are dropped FIFO with a log line.
Coordinator-side handling. Coordinators receiving prism prompt-style frames do not need to deduplicate — the sidecar guarantees exactly-once for the same delivery_id. If you see replay: true on a prompt frame (visible in the assistant-side prompt body once the PI runtime exposes it; for now, observable only in raw frame archives), the delivery is a buffered resume of a partition-window escalation. Treat it informationally: the original was already accepted, this is the post-reconnect notification of that earlier acceptance.
This contract supersedes the pre-fix behaviour where prism escalate could deliver the same prompt body multiple times under load (issue #1685). Sender-side double-invocation (a worker re-running prism escalate because the success signal was unclear) is covered by the idempotency guard above; see issue #2018.
When to use prism escalate vs prism prompt
prism escalate— you are a worker handing a question or decision to the coordinator and pausing your turn until you hear back. Use this whenever you would have otherwise stopped after sending a hand-craftedprism prompt.prism prompt— you are sending an informational follow-up to a running session and either continuing your work (sender keeps going) or expect no response (e.g. delivering a review-complete prompt). Workers prompting their own coordinator should usually be usingprism escalateinstead.
Out of scope (v1)
- Cross-repo escalation — v1 is single-repo; auto-discovery is repo-scoped.
- Escalation receipts back to the worker — the worker discovers the coordinator's response by being prompted.
- Re-escalation timeouts.
- Dashboard panel for
escalatedsessions — surfaced viaprism sessions listand the bus only.
Worker terminal-state notifications
When a worker session reaches a terminal state, the coordinator that spawned it receives a body-bearing prompt notification. This is the signal the coordinator uses to know that a delegated task has finished and is ready for review / cleanup / merge.
Wording (verbatim)
| Terminal state | Notification body |
|---|---|
finished (clean exit) |
Agent <name> has finished its current task |
error (crash / restart-exhausted) |
Agent <name> has errored its current task |
The wording is fixed so coordinators can pattern-match on either string.
Delivery contract
- Exactly-once with replay marker (issue #1695). Each notification carries a
delivery_id(UUID minted by the sender). The receiving sidecar dedups repeats by ID before they reach the harness pipe. Retried deliveries with the same ID see{"replayed":true}in the response so retries are observable, not silent. - Delivery mode is
followUp. The notification queues behind any in-flight turn on the coordinator side so it doesn't interleave with an active assistant turn. - Suppressed while escalated. A worker in the
escalatedstate has already informed the coordinator viasession.escalated; a subsequent "has finished" notification would be a false signal (the worker is paused awaiting guidance, not done). The state clears on any incoming turn_start, after which a normal finish notifies as usual.
Debugging a running or stuck session
Use this decision tree when a session appears stuck, produces no output, or fails unexpectedly.
Step 1 — Session state check:
prism sessions list # human-readable table
prism sessions list --json # parseable when scripting
Examine the state column (active, waiting, idle, finished, error), the port, and the last_seen timestamp. If a session has a DB row but no live tmux session, it may be a zombie (DB row without a live process). Proceed to step 2.
Step 2 — Recent activity:
prism checkin <session>
Reads the last 10 turns from the prism DB as a rich narrative view. Use --verbose for full tool args/results when something looks off. See prism checkin flags below for all options.
Step 3 — Sidecar logs:
prism logs <session>
The raw sidecar log is where sandbox startup errors, timing traces, and stderr from failed launch commands land. This is the most informative diagnostic for infrastructure failures. See prism logs below for full flag documentation.
Step 4 — Source cross-reference:
When a log or event message includes a file:line reference, read that location directly in the Go source under modules/programs/prism/prism/ to understand the exact code path.
prism checkin flags
| Flag | Description |
|---|---|
--last <N> |
Number of message turns to show (default 10) |
--from <id> |
Show N events forward from this event ID |
--before <id> |
Show N events backward from this event ID |
--verbose / -v |
Full forensic output: full tool args and full results with no truncation |
--types <list> |
Orthogonal event-type filter — routes to the raw-event path (e.g. --types audit, --types state_change). Not needed for the narrative view; tool calls are included by default. |
--all |
(no-arg mode only) List all sessions across all repos |
prism logs
The prism logs command streams the raw sidecar log for a session to stdout.
prism logs <session> # full sidecar log to stdout
prism logs <session> --tail N # last N lines only
prism logs <session> --follow # stream new lines as they arrive (ends ~5s after terminal state)
prism logs <session> -f # alias for --follow
prism logs <session> --harness-events # raw PI JSONL frames (P5.LOGS / #1218)
Works identically from a host shell and from inside a coordinator container. When PRISM_HOST_API is set (container mode), prism logs proxies through the host-API Unix socket — no special handling required. The output is the raw log and can be piped to grep / rg.
--deliver=<sink> — route the artifact directly
The --deliver flag short-circuits the usual "pipe stdout into something" two-step. Three sinks are supported:
prism logs <session> # stdout (default)
prism logs <session> --deliver=stdout # explicit stdout
prism logs <session> --deliver=file:/tmp/sidecar.log # atomic write to a file
prism logs <session> --deliver=webhook:https://example.com/triage
prism logs <session> --harness-events --deliver=webhook:https://example.com/frames
file:<path>writes via tempfile + rename so a failed deliver cannot leave a half-written file. On success it prints{"delivered_to":"file:<path>","bytes":N}to stdout.webhook:<url>POSTs the content withContent-Type: text/plain(orapplication/x-ndjsonfor--harness-events). On success it prints{"delivered_to":"webhook:<url>","status":<code>}. A 4xx or 5xx response is surfaced as a non-zero exit with a JSON object containingstatusand a truncatedbody. The local log file is read on demand so a failed delivery never modifies the source.- Unknown schemes (
s3:...,mailto:..., etc.) are refused with the valid set listed (principle 3 of #1497). --deliverand--followare mutually exclusive: delivery captures a snapshot.
Common failure signatures
A lookup table of log patterns, their causes, and remediation hints:
statfs <path>: no such file or directory— the sandbox was told to bind-mount a path that does not exist on the host. Check the preceding log line for the full launch command. See incident #751 for a historical example.startup-connect timeout fired— the session started but the sidecar never received the first SSE event from the agent. Usual causes: a misconfiguredharness-config.json(agent not declared, malformed JSON), a missing bind-mount, or a missing--agentflag value. Check the sidecar log for the launch command line and any JSON parse errors.Session rows present in
prism sessions listbut no events inprism checkin— the agent process either never started or died immediately after creation. Runprism logs <session>to see the full launch command line and its stderr output.Session name doesn't match expected shape (e.g.
~reviewwhere~review-1-review-codeis expected) — the agent-list construction produced the wrong agent names, or the--agentflag value is incorrect. Check the session'sharness-config.jsonfor theagentblock contents and the sidecar log for the--agentflag value used in the command line.Zombie DB rows (session in
prism sessions listbut no live tmux session) — a previous session's process died without cleaning up DB state. Useprism cleanup --yes --session <name>to end the row (stampsended_at, releases any dangling port, clears the pi resume linkage) so it drops out of the active-session view. The row itself is preserved; re-spawning on the same branch name reuses it by re-seedingstateback toidle.
Escalation
If two diagnostic cycles (prism checkin + prism logs) do not clarify the issue, escalate to the user rather than continuing to probe in circles. Document what you observed in each cycle and what remains unclear. Do not run a third diagnostic cycle on your own.
prism feedback — record CLI friction
Use prism feedback to record short notes about CLI rough edges — flags rejected for the wrong reason, error messages that don't enumerate, race conditions in async paths. Each entry is appended as one JSON object per line to $XDG_STATE_HOME/prism/feedback.jsonl (defaults to ~/.local/state/prism/feedback.jsonl).
prism feedback "the --tier flag rejects 'enterprise' but the docs list it as valid"
echo "feedback from a script" | prism feedback - # read from stdin
prism feedback list # human-readable list
prism feedback list --json # JSON array of all entries
prism feedback list --days 7 # only entries from the last 7 days
prism feedback prune --days 90 --yes # drop entries older than 90 days
Each entry includes timestamp (RFC 3339), text, session (the value of PRISM_SESSION_NAME if set), and prism_version. Optional fields cover cwd and other contextual hints.
Upstream POST (opt-in). When the PRISM_FEEDBACK_ENDPOINT environment variable is set (or a feedback_endpoint field is present in ~/.config/prism/config.json), each new entry is POSTed to the configured URL after being written locally. The local record is the source of truth: if the upstream POST fails, the local entry is unaffected and the failure is reported in the success message. Env var wins over config.
Pruning safety (principle 1). prism feedback prune requires --yes to confirm — omitting it errors instead of prompting. This matches the rest of the prism CLI's no-implicit-confirmations stance.