# pi-queue-steer-factory

[![CI](https://github.com/monotykamary/pi-queue-steer-factory/actions/workflows/ci.yml/badge.svg)](https://github.com/monotykamary/pi-queue-steer-factory/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A visible steering, follow-up, and session-control timeline for [Pi](https://github.com/earendil-works/pi-mono), with acknowledged [`/fabric prewalk`](https://github.com/monotykamary/pi-fabric) barriers.

Queue instructions while the agent works. Every row keeps its Pi delivery timing — blue steering at a turn boundary, yellow follow-up after a run — inside one first-in, first-out execution outline. Follow-ups are root rows that start runs; steering is indented beneath the run it will join.

Move into any row to edit it. The selected row becomes the live Pi editor, with its cursor, wrapping, paste handling, autocomplete and custom-editor behaviour intact.

For Rakazo, Pi RPC, or other headless owners, see the [versioned controller/control bridge](docs/headless.md). Import `QueueController` from `pi-queue-steer-factory/headless` without loading TUI. Async durable write-ahead persistence is supported.

## Demo

![Looping demonstration of steering and follow-up queues while Pi continues working](assets/pi-queue-steer-demo.gif)

## Install

Install the queue and Pi Fabric from npm:

```bash
pi install npm:pi-queue-steer-factory
pi install npm:pi-fabric
```

Pin the current releases when you want reproducible installs:

```bash
pi install npm:pi-queue-steer-factory@0.16.0
pi install npm:pi-fabric@0.83.0
```

The GitHub package is also installable directly:

```bash
pi install git:github.com/monotykamary/pi-queue-steer-factory@v0.17.0
```

Then start a new Pi session or run `/reload`.

## Controls

The extension follows your configured Pi action bindings. These are the default keys on macOS terminals:

| Context | Key | Action |
|---|---|---|
| Agent working | `Enter` | Steer the current run; insert after its existing steers, ahead of future queued runs |
| Agent working | `Option+Enter` | Append a visible follow-up; it runs after earlier rows and the active run |
| Queue visible | `Option+Up` | Select the most recently queued row |
| Editing a row | `Option+Up` | Keep the current draft and move to the previous visual row |
| Editing a row | `Option+Down` | Keep the current draft and move to the next visual row; mirrors the configured `Option+Up` action |
| Editing a row | Type normally | Edit directly inside the selected row |
| Editing a row | `Option+X` | Mark the selected row for removal; save deletes it, a second press restores it |
| Editing a row | `Option+Right` | Indent the row into steering for the run above; its timeline position does not change |
| Editing a row | `Option+Left` | Outdent the row into a follow-up that starts the next run; its timeline position does not change |
| Editing a row | `Option+T` | Toggle the same depth as a fallback when a terminal cannot distinguish Option+Arrow |
| Editing a row | `Option+P` | Pause or resume the selected row where it sits; a paused row stops dispatch at its position until resumed |
| Editing a row | `Option+Shift+Up` / `Option+Shift+Down` | Move the selected row one position in the timeline, across lanes too; works during unsaved depth edits |
| Editing a row | `Enter` or `Option+Enter` | Save all row edits, including explicit indent/outdent drafts |
| Editing a row | `Escape` | Cancel the session and roll back all unsaved row edits |
| Empty composer, follow-up at the timeline head | `Enter` | Promote that next follow-up to steering now |
| Queue paused after an abort | `Enter` | Resume from the next timeline row |
| Queue paused after a run error | `Enter` | Resume manually; a recovered run (built-in retry, auto-compact, pi-retry) releases the queue first |
| Queue restored after resume | `Enter` | Send the next queued row; `Option+Up` edits it first |
| Agent stopped | `Option+Enter` | Queue a message, skill/template, or control command (`/compact`, `/reload`, `/new`, `/model`, `/thinking`, `/fabric prewalk`) visibly and paused |
| Agent working, queue visible | `Escape` | Abort the run and pause the entire delivery plan |
| Agent working | `/pause` | Pause the run once every in-flight tool call finishes; tool work is never killed mid-execution |
| Any state | `Option+W` | Toggle a peer settle gate row (pick a peer or all peers) |
| Peer gate waiting | `Escape` | Cancel the wait and pause the gate row |

`Option+Left/Right`, `Option+W`, `Option+X`, `Option+T`, `Option+P` and `Option+Shift+Up/Down` are fixed shortcuts. The other controls use Pi’s configured action bindings, and the next-row key mirrors whatever `app.message.dequeue` is bound to — rebind that action to `ctrl+up` and next-row becomes `ctrl+down`. A dequeue binding with no `up` twin keeps `Option+Down`. The physical arrow sequences change depth only while editing queued rows; `Option+B/F` remain editor word navigation. A terminal that encodes Option+Arrow as those indistinguishable word-navigation bytes can use `Option+T` instead. Terminals outside macOS may label `Option` as `Alt`.

## Execution outline

The indentation is deliberately one-way:

```text
• current run
  ↳ steer current work
○ queued run A
  ↳ steer inside A
○ queued run B
```

A yellow follow-up is a root because it starts a run. Any blue steering rows immediately after it are children because they enter that run at turn boundaries. Leading steering rows sit under an implicit **current run** (or **next run** while idle). A follow-up after steering always outdents to start another run; queues are therefore never shown as children of steers. There are exactly two depths, so `Option+Right` on steering and `Option+Left` on a follow-up are no-ops. The keys preview real depth changes in place, and saving changes only delivery timing—not FIFO position, text, images, or row identity.

## Pausing at a tool boundary

Interrupting a run mid-tool (`Escape`) kills the executing tool outright. `/pause` is the graceful alternative: with tool calls in flight it holds fire until every one of them finishes and then stops the run at that tool boundary, so long bash jobs and edits always complete and their results stay in the transcript. With no tool call executing it stops the LLM call immediately, and with the agent idle it simply parks the visible queues. A paused queue stays put until an explicit `Enter` on the empty composer, and `/pause` during compaction reports back instead of cutting summarization.

## Pausing a single row

The whole-queue pause stops everything; sometimes the agent should keep working until it reaches one specific row. While editing the queue (`Option+Up`), press `Option+P` to pause or resume the selected row where it sits. A paused row is a dispatch barrier: earlier rows still send on their normal boundaries, and once the paused row reaches the timeline head delivery stops there — rows behind it never jump ahead, regardless of lane — until you select it again and press `Option+P` to resume. The pause belongs to the row like its lane does: it survives a save, persists across restart and resume, and an unsaved toggle rolls back with the rest of the editing session on `Escape`. A drain skips paused rows and leaves them parked.

## Peer settle gates

When another Pi Fabric session is running in the same project, queue the edit work and hold it behind a gate until that peer settles. Press `Option+W` (or queue `/fabric await [LABEL]` by hand) to add a gate row: with one live peer it targets that peer directly, with several it offers an all-peers default plus per-peer cards (`● PQS-1 · gpt-5.4 · running · started 6m ago`, with Linear-style project labels that are never reused). Rows queued behind the gate dispatch as soon as every watched peer has settled and stayed quiet briefly; peers that leave the mesh count as settled. `Escape` cancels an active wait and pauses the gate row; `Option+W` again removes a queued gate. Requires pi-fabric 0.64.0 or newer.

## Delivery semantics

The extension keeps Pi’s 2 delivery classes inside one ordered timeline:

- steering at the timeline head reaches the current run at Pi’s next safe turn boundary
- a follow-up at the head waits until the current run finishes
- later rows never overtake the head because they use the other delivery class
- yellow follow-ups stay at the root and blue steering rows indent beneath the run they will join
- reordered rows keep their stable IDs, text drafts and attachments
- change depth, edit text, reorder up/down and switch rows in one editing session, with no intermediate saves; one final `Enter` saves everything and `Escape` rolls it all back
- new interactive steers join the leading current-run segment before the first future follow-up root; existing rows keep their relative order
- Pi’s `one-at-a-time` and `all` settings still apply per lane, but an `all` batch stops at the next lane switch, command, or paused row

To steer a future queued run instead, queue its child with `Option+Enter`, select it with `Option+Up`, then indent with `Option+Right`. Reorder it beneath the desired run without saving first. A plan of `follow-up A → steer A → follow-up B` starts A after the current run, steers A at its next turn boundary, then starts B after A settles. Plain-Enter `/compact` and `/new` retain their safe tail placement behind earlier rows; explicit headless enqueue also remains append-only.

The extension hands the timeline head back to Pi only when that row's delivery boundary arrives. Rows remain visible and editable before that point, and Pi records delivered rows as normal user messages. Queue ownership is TUI-only; RPC, JSON and print-mode input pass through unchanged.

## Run errors and retries

A run that ends in an error (including context overflow) pauses the queue instead of de-queueing the next row into the failed session. Recovery gets the queue first: Pi's built-in retry and overflow auto-compaction, or an external retry loop such as pi-retry that re-prompts once the agent goes idle. The pause lifts automatically at the first healthy assistant tail — or when an overflow compact-and-retry cycle concludes — and the parked rows then flow in order. A threshold compaction triggered merely by context size after a failed run is housekeeping, not recovery, and leaves the queue parked. If nothing recovers the run (no retry installed, retries exhausted, or compact-and-retry itself failed), the rows stay parked until `Enter` sends the next one. Aborting during recovery keeps the pause; an aborted tail never counts as recovery.

## Queueing while stopped

With the agent stopped, `Enter` keeps Pi's normal immediate send. `Option+Enter` instead places the submission as a yellow follow-up root, paused — including skill and prompt-template invocations and the supported `/compact [instructions]`, `/reload`, `/new`, `/model [target]`, `/thinking [level]`, exact `/fabric prewalk`, and `/fabric await [label]` controls. Press `Enter` on the empty composer to execute the next row, or `Option+Up` to edit it first.

A plain `Enter` still runs every command immediately. With `Option+Enter`, other Pi built-ins, other extension commands, unknown slash input and `!` bash keep passing straight through.

## Prompt templates and Agent Skills

Queued `/do-less this code`, `/skill:bro` and `/bro` rows stay short and editable, then expand when delivered — while the agent works they queue through steering or follow-up input, and while stopped `Option+Enter` parks them paused like any message. `/bro` is shorthand for `/skill:bro` unless a built-in, prompt or extension already uses that name. Template arguments and images are preserved; unknown slash input remains ordinary text.

Arbitrary commands are intentionally not replayed. The supported command rows have explicit completion signals; an unsupported queued extension command pauses until you edit or remove it.

## Command rows

Text-only rows matching `/compact [instructions]`, `/reload`, `/new`, `/model [target]`, `/thinking [level]`, exact `/fabric prewalk`, or `/fabric await [label]` are command rows. A row with image attachments remains a normal message even if its text matches a command, so attachments are never discarded. Command rows execute the control operation instead of becoming LLM messages:

- `Option+Enter` while the agent works queues a command in normal follow-up order; while stopped it parks every command row paused, `/compact` and `/reload` included
- lane timing is uniform: a steered command row executes at the next turn boundary — mid-run, exactly as if typed there — and a queued (follow-up) command row runs when the run settles; rows behind an executing command wait for it
- `/model provider/model` resolves an exact available model; bare or non-exact `/model` opens a filtered picker, and cancellation or authentication failure restores and pauses the row
- `/thinking level` sets Pi's thinking level through the clamped public API; bare `/thinking` opens the level picker, and an unknown level or a cancelled picker restores and pauses the row
- exact `/fabric prewalk` waits for Pi Fabric to acknowledge that prewalk is armed before the next row can run; it requires Pi Fabric 0.62.7 or newer
- `/fabric await [label]` holds the tail until every watched peer session on the project mesh settles (a quiet window after its last observed run) or leaves the mesh; it requires Pi Fabric 0.64.0 or newer. Controls and watch state show on the row (`waiting for PQS-1 (running)`)
- `/new` starts a fresh session and transfers its committed tail to that replacement runtime without adding rows to either transcript; the tail continues automatically, while reopening a persisted queue still starts paused. Pi resolves a fresh session's model from the shared saved default (the last model any session persisted) or the first scoped model, so the handoff pins the outgoing session's model back onto the replacement session before the tail runs — or warns and continues if that model is no longer available
- `/reload` runs Pi’s built-in reload; committed trailing rows retain their IDs, lanes, attachments and pause state across the runtime swap
- a queued `/compact` runs at settle via Pi’s public compaction API and trailing rows resume when compaction finishes; steered, it fires at the next turn boundary and aborts the in-flight run on purpose — a start failure restores and pauses the command row
- `/reload` queued as a follow-up runs at settle, sidestepping Pi's built-in busy wait warning; steered, it fires at the next turn boundary and Pi's own busy handling applies
- plain `Enter` on `/compact` or `/new` while the agent works parks it as a steer row instead of firing instantly: it runs at the next turn boundary — after the turn's in-flight tool results land — and the extension owns the abort tail; from idle, both still start immediately and a started compaction holds visible rows until it settles
- ordinary messages submitted during compaction remain in Pi's native queue and can run before extension-owned command rows after compaction finishes
- stopped `Option+Enter` parks `/compact` and `/reload` paused like every other control row; they run only on an explicit empty-composer `Enter`
- unsupported command forms, including `/fabric prewalk <task>`, are not control rows; queue exact `/fabric prewalk` and the task as separate rows
- command rows show a `⚙` marker and keep the same pause, edit, reorder and snapshot semantics as messages

## Factory pipelines

A linear Factory run is just an observed queue of controls followed by work:

```text
/new
/model openai/gpt-5.4
/fabric prewalk
Implement the queued task
```

Queue each line with `Option+Enter`, then press `Enter` on the empty composer. The dispatcher waits for session replacement, model selection and prewalk arming before advancing; later controls wait for Pi's `agent_settled` idle boundary. A failed or cancelled control remains at the front and pauses the whole tail, so it can be edited or retried without reordering.

Fabric remains the execution plane inside the task: it can launch durable or recursive agents, steer them, and create isolated worktrees. This extension owns only the visible deterministic queue and its observation boundaries; it does not introduce another agent loop.

## Draining the queue

`/queue-drain` empties both lanes into the run as a single combined message. Row texts join in exact global timeline order, expanding prompt templates and skills as they go, with every row's image attachments appended in the same order.

- during a run, the combined message reaches Pi as one steering message
- while stopped, the combined message starts a new run directly
- a mid-turn drain lands inside the in-flight call's context when the turn has not responded yet, or as the next steering turn once it has — either way the transcript records the combined message exactly once
- command rows are not messages: `/compact`, `/reload`, `/new`, `/model`, `/thinking`, and `/fabric prewalk` stay queued and execute at their lane's dispatch boundary
- an active row-editing session refuses the drain, so rows are never pulled away mid-draft
- a synchronous hand-off failure restores every row, in order, and pauses the queue

## Editing semantics

- `Option+Up` starts at the row you queued most recently
- `Option+Up` and `Option+Down` then move through the visible timeline
- saving never changes a row’s lane implicitly; `Option+Right` indents to steering and `Option+Left` outdents to follow-up
- depth changes preview in place and commit without changing the row’s global timeline position; `Option+T` remains a toggle fallback
- `Option+X` marks the selected row for removal; save deletes it, and `Escape` or a second `Option+X` restores it
- a selected row becomes the real editor without a nested composer frame
- one editing session can hold drafts for several rows
- `Escape` restores every row from the session snapshot, including removal marks and indent/outdent drafts
- saving an empty text-only row removes it
- image-only rows survive text clearing; `Option+X` removes them
- an unrelated composer draft is stashed and restored when editing ends

A touched head row is pinned until you save or cancel. In `one-at-a-time` mode, later rows do not block the head. In `all` mode, editing any row in the contiguous dispatchable head segment holds that batch at active-run delivery boundaries; rows beyond a lane switch, command, or paused row are not part of it.

## Abort and recovery

Aborting a run pauses the entire visible delivery plan. This prevents a follow-up from starting immediately after the abort.

Press `Enter` on the empty composer to resume; the same keypress sends rows queued while stopped. A synchronous handoff or preflight failure returns the affected batch to the global timeline head.

Committed rows also survive quitting and resuming Pi. On shutdown the extension records the queue in the session file as an invisible custom entry that stays out of the transcript and out of the model context. Committed row saves and accepted deliveries immediately supersede that entry with the remaining queue—or an empty tombstone—so edited-away or already-sent rows cannot return after a later restart. Reopening that session restores the remaining rows **paused**: nothing sends until you press `Enter` on the empty composer. A `/reload` runtime swap still carries committed rows and pause state through a short in-process handoff. Edit drafts stay session-local and never persist; ordinary `/new` and forks start clean, while queued `/new` intentionally transfers its committed tail.

## Public API limits

Pi’s public `sendUserMessage` API is fire-and-forget. The extension restores synchronous message-dispatch failures and preflight/expansion failures without reordering; once the call returns, it retires the accepted row and checkpoints the remaining queue. Pi does not expose later asynchronous input rejection. Inferring rejection from queue timing could duplicate a delayed successful handoff, so the extension does not do that.

Queued `/model` uses Pi's awaited model API. Queued `/new` runs through an internal extension-command adapter because `newSession()` is intentionally available only in command contexts. Queued `/fabric prewalk` uses Pi Fabric's versioned host-local request/ack protocol. `/reload` remains the one supported control exposed only through the TUI editor's `void` submit callback, so Pi cannot acknowledge or reject that submit back to the extension.

If an `all`-mode head segment stays pinned until the agent settles, saving from idle starts the new run with the timeline head, then delivers the remaining rows in that segment at the next native boundary. The public API has no atomic idle-to-native-queue batch operation, so this restart cannot be one native batch. Draining sidesteps that limit by composing its combined message client-side, so one send carries every row.

## Resume persistence

Queuing a row does not send it. When Pi shuts down cleanly — `/quit`, Ctrl+C, Ctrl+D, or a session switch — the extension records the committed queue as a custom session entry (`pi-queue-steer:queue`), invisible in the transcript and excluded from the model context. Successful deliveries and committed row saves append a superseding snapshot immediately; if they empty the queue, they append a tombstone instead. When the same session is reopened (`pi -c`, `pi -r`, `pi --session`, `/resume`), only the rows in the newest snapshot come back, in FIFO order with their IDs, lanes, image attachments and command rows intact — and the queue is parked paused. Press `Enter` on the empty composer to send the next row, or `Option+Up` to edit it first.

Rows normally belong to the session they were queued in. `/fork` and an ordinary immediate `/new` start with an empty queue. A **queued** `/new` is the explicit exception: it retires the old session snapshot, transfers its tail in process, and continues that tail in the replacement session. Older snapshots superseded by later ones stay in session files but are never restored, and a session can only be resumed at all if Pi wrote it: sessions without an assistant response are not persisted by Pi, and a hard kill skips the shutdown hook.

## Editor composition

pi-queue-steer-factory wraps the active Pi editor. It does not replace Pi’s input model.

For display, it extracts the live editor’s text and cursor from the editor frame. It then places that content inside the selected queue row. Autocomplete remains below the edited text.

The extension composes with custom editors including raw-paste and pi-session-hud.

## Extension interop

The queue publishes its state for peer extensions on the shared `pi.events` bus:

- **Event** — `queue-steer:state`, emitted on every change to `{ pending, paused, blocked }`. `pending` counts all rows still held by the queue (both lanes, including paused and edit-held rows), `paused` means dispatch is paused, `blocked` means a control row (`/compact`, `/model`, `/thinking`, `/new`, `/reload`, `/fabric prewalk`) is executing.
- **Mirror** — the same snapshot lives on `globalThis.__tmustierPiQueueSteerState` for synchronous reads, immune to extension load order, and survives `/reload` runtime swaps.

Consumers: [pi-ledger](https://github.com/inloopstudio-team/pi-ledger) ≥ 0.6.0 holds back its no-credit engagement wizard while `pending > 0`, so a parked backlog no longer triggers the billing prompt, and re-offers it once the backlog drains without starting a run. [pi-fovea](https://github.com/monotykamary/pi-fovea) yields post-turn `deliverAs: "steer"` / `triggerTurn` while `pending > 0`, so a queued user row stays ahead of a Fovea sync notice in Pi's native lane (the notice rides `nextTurn` instead).

### Fabric conversation previews

The loaded extension also serves `queue-steer:conversation-queue:request:v1` on `pi.events`. Compatible Fabric conversation views acquire an isolated queue per participant, using this extension's actual `DeliveryQueue`, `QueueEditSession`, inline editor extraction, and shared execution-outline widget. No installation-path probing or second copy of the renderer is needed. Releases without this bridge fall back to Fabric's native Pi pending-message display.

Fabric owns routing and delivery acknowledgement for these child views, not this extension's Main-session dispatcher. Sent rows stay visible until child delivery is observed and cannot be edited locally; parked rows support in-place editing. Closing a preview retains its session-owned queue, while a session reset releases it. Child queues never modify Main's queue, persist model-visible drafts, or execute Main-session command rows.

## Development

```bash
npm install
npm run ci
./test/tui-evidence.sh /tmp/pi-queue-tui-evidence
pi -e ./index.ts
```

The automated suite covers delivery, editing, command rows, resource expansion, recovery, images, editor composition, repeated reloads, real retry ordering, real manual compaction success/failure and real automatic overflow compaction. The tmux harness exercises the same paths through Pi's real TUI, including actual runtime reloads and native post-compaction input.

The Pi package ranges are intentionally unpinned. The full suite and real-TUI harness are verified against the current resolved Pi release; see [the validation record](docs/validation.md) for exact commands and evidence.

## Security

Pi extensions run with the same system permissions as Pi. Review extension source before installing a third-party package.

## Licence

MIT. See [LICENSE](LICENSE).

This project draws on Cursor’s queue interaction. It is not affiliated with Cursor or Anysphere.
