pi-jar 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,14 +18,14 @@ The showcase is captured from a real pi-jar terminal session: the torch-style `
18
18
  | **Composer** | Rounded input that grows with your draft (up to ~60% of the terminal), click-to-place cursor in fullscreen, dim **next-prompt suggestions** you accept with <kbd>Tab</kbd>, and **Ember** — a tiny flame mascot that blinks, cheers, focuses, dozes and reacts. |
19
19
  | **Plan mode** | Read-only exploration; the agent must write a structured plan file and submit it. You review it in a split view (headings on the left, section on the right) and approve, compact-and-approve, refine or stop. |
20
20
  | **Goal mode** | Set an outcome; the agent must break it into tracked tasks and keeps working until they are done, then an **auditor** pass verifies the goal before it can be marked complete. |
21
- | **Roles** | Named model roles (`default`, `smol`, `slow`, `plan`, `implement`, `advisor`, `task`, `commit`, plus your own) with `@alias` chains, `:effort` suffixes and project overrides; switched automatically as you move between plan, execution, goal rounds and audits, and never over a model you picked yourself. |
21
+ | **Roles** | Named model roles (`default`, `smol`, `slow`, `plan`, `implement`, `advisor`, `moderator`, `scout`, `worker`, `reviewer`, `task`, `commit`, plus your own) with aliases, effort, per-role fallback models and project overrides. |
22
22
  | **Advisor** | A second-opinion model: the agent calls `jar_advisor` for risky decisions or when stuck, `/advisor [focus]` asks on demand, and automatic gates consult it when the agent repeats the same tool call or keeps failing. |
23
23
  | **Usage & context** | `/usage` shows session cost and tokens per model (advisor and commit calls included) and plan-limit bars with reset times; `/context` draws a grid of what fills the context window, with a per-file, per-skill and per-tool breakdown. |
24
24
  | **Commit** | `/jar commit [note]` drafts a message for the staged changes with the `commit` role, lets you edit it, then commits (never pushes). |
25
25
  | **Tasks & questions** | A Claude/omp-style `jar_todo` checklist with subtasks and per-task progress that the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
26
26
  | **Change review** | Every file the agent or its editing subagents change is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files (each counted once) next to a colored diff, with accept or revert per file or all at once. |
27
27
  | **Background shells** | `jar_shell` runs dev servers, watchers and long tests in the background; the agent is woken when a watch pattern matches or the process exits, so it never polls. Running shells get a footer row; click it (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>) to tail or kill them. |
28
- | **Subagents** | `jar_delegate` fans out up to four read-only (or opt-in editing) subagents in parallel on your model roles. Each gets a live footer row; click it to open its checklist and an omp-style collapsible transcript, steer it with a message, or stop it. The parent gets a detailed report (tools, files, tasks, failures) from each. |
28
+ | **Subagents** | The main agent becomes a moderator over a configurable retained pool (2, 4, 6, 8 or 16; default 4). `jar_delegate` spawns `scout`, `fork` or sandboxed `worktree` agents; `jar_subagent` peeks, steers, asks BTW questions, pauses, resumes and stops them. Workers can exchange bounded structured Q/A through `jar_discuss`; worktree changes stay isolated until stop, then safely enter `/diff`. |
29
29
  | **Sessions & history** | Recent sessions (with their goal and plan) on the welcome card, one click from resuming; <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>H</kbd> searches earlier prompts; pasted images show as chips under the composer. |
30
30
  | **Footer & themes** | Responsive footer (model, effort, session, cwd, context, RAM, cost, quota, goal, roles, branch, live subagents and shells) with unicode, [Nerd Font](https://www.nerdfonts.com) or ascii icons, and seven dark themes. |
31
31
 
@@ -175,10 +175,15 @@ Roles map a purpose to a model. They live in `~/.pi/agent/pi-jar-roles.json` (Pi
175
175
  "plan": "@slow",
176
176
  "advisor": "@slow:xhigh",
177
177
  "smol": "anthropic/claude-haiku-4-5-20251001",
178
- "review": "openai/gpt-5:medium"
178
+ "scout": "@smol:minimal",
179
+ "worker": "anthropic/claude-sonnet-5:medium",
180
+ "reviewer": "@default:medium"
179
181
  },
180
- "cycleOrder": ["smol", "default", "slow"],
181
- "tags": { "review": { "name": "Reviewer" } }
182
+ "fallbacks": {
183
+ "scout": ["openai/gpt-5-mini:low"],
184
+ "worker": ["@default"]
185
+ },
186
+ "cycleOrder": ["smol", "default", "slow"]
182
187
  }
183
188
  ```
184
189
 
@@ -190,13 +195,17 @@ Roles map a purpose to a model. They live in `~/.pi/agent/pi-jar-roles.json` (Pi
190
195
  | `plan` | plan mode (restored when you leave it) |
191
196
  | `implement` | executing an approved plan and goal implement rounds (falls back to the current model) |
192
197
  | `advisor` | `jar_advisor`, `/advisor`, stuck-work gates and the goal audit |
193
- | `task` | `jar_delegate` subagents |
198
+ | `moderator` | optional model assignment for the coordinating main agent |
199
+ | `scout` | default for fresh read-only delegated discovery; ideal for a cheap model |
200
+ | `worker` | default for sandboxed worktree implementation |
201
+ | `reviewer` | default for context-forked read-only review |
202
+ | `task` | legacy/generic explicit subagent role |
194
203
  | `commit` | `/jar commit` messages |
195
204
  | `smol` / `slow` | cycling with <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>M</kbd> |
196
205
 
197
206
  Any other valid name (`a-z`, `0-9`, `-`) is a custom role.
198
207
  - **Switching is automatic and scoped.** Entering plan mode applies `plan`; approving applies your chosen role or `implement` for the run; goal rounds alternate `implement` and `advisor`. Each temporary switch restores the previous model afterwards — unless you picked a model or effort yourself in the meantime, which always wins.
199
- - `/roles` opens a split manager: role list on the left; resolved model, alias chain, effort, scope and usage on the right. Keys: `m` model, `a` alias, `t` effort, `s` move between global/project scope, <kbd>Enter</kbd> activate, `c` clear, `n` new role, `d` delete custom role.
208
+ - `/roles` opens a split manager: role list on the left; resolved model, alias chain, effort, **fallback model**, scope and usage on the right. Keys: `m` primary model, `f` fallback model, `a` alias, `t` effort, `s` move between global/project scope, <kbd>Enter</kbd> activate, `c` clear, `n` new role, `d` delete custom role. The CLI still supports an ordered chain with `/roles fallback ROLE MODEL...`.
200
209
  - Older v1 files are read and upgraded in memory; they are rewritten as v2 only when you change a role.
201
210
 
202
211
  ## Advisor
@@ -208,9 +217,9 @@ The advisor is a second model (the `advisor` role; the current model if unassign
208
217
  - **Gates** — when the agent makes the same tool call three times within its last eight calls, the call is blocked and the advisor's review is returned instead; after three failing tool results in a row, the advice is steered into the running turn. At most two automatic consultations per prompt.
209
218
  - Settings → Pi toggles the advisor and the gates. Advisor calls are counted in `/usage`. The footer and welcome show it as a working teammate while it thinks.
210
219
 
211
- ### Advisor fallback models
220
+ ### Role fallback models
212
221
 
213
- Configure an ordered fallback chain (the primary advisor remains configured with `/roles set advisor`):
222
+ Every role can have an ordered fallback chain. The primary remains configured with `/roles set ROLE`:
214
223
 
215
224
  ```text
216
225
  /roles fallback advisor openai/gpt-5:high @smol
@@ -224,7 +233,7 @@ Add `--project` when saving or clearing to override the global chain for this pr
224
233
  "fallbacks": { "advisor": ["openai/gpt-5:high", "@smol"] }
225
234
  ```
226
235
 
227
- One-shot advisor calls (`jar_advisor`, `/advisor`, and stuck-work gates) try the primary first, then up to eight configured fallbacks on missing models, authentication/provider errors (including rate limits), or empty answers. Cancellation never starts another attempt. Aliases and per-model thinking levels are supported; duplicate model/effort pairs are skipped. Answers identify the successful model, returned attempts are counted in `/usage`, and exhaustion reports every failure. An empty project list disables inherited global fallbacks. No fallback is used unless explicitly configured; context is sent only to the models you choose. The same configuration works for other one-shot roles such as `commit`, but not interactive role activation or goal audit model switches.
236
+ Role activation and delegated model selection try the primary first and then configured fallbacks in order; aliases and per-model effort are supported and duplicate model/effort pairs are skipped. Side-model workflows such as advisor and commit keep their own retry/error reporting while using the same fallback configuration. An empty project fallback list disables inherited global fallbacks. This makes roles such as `scout` practical to pin to a cheap model with a more capable fallback instead of silently escalating every task.
228
237
 
229
238
  This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-advisor/) currently resolves one configured advisor model rather than a fallback chain.
230
239
 
@@ -237,20 +246,24 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
237
246
 
238
247
  ## Tasks, questions and history
239
248
 
240
- - **`jar_todo`** — a Claude/omp-style task list the agent keeps for any multi-step request. Prefer incremental `append` (new tasks only), `replace` (update one stable `id`), and `remove` (delete one task and its subtasks); IDs are returned for every task. Omitted replacement fields and subtasks stay intact. Legacy `write` (or `todos` without an action) still replaces the entire list, removing omitted tasks. Invalid updates leave the list unchanged. Each task is `pending`, `in_progress` (exactly one at a time) or `completed`, with an `activeForm` ("Running tests") that replaces the working spinner text while it runs. Tasks can have one level of **subtasks**: a parent's status follows its subtasks, its row shows `(done/total)`, and progress counts every subtask (and every task without subtasks). The live checklist above the composer shows `✔` struck-through done tasks, a bold `◼` current task and `☐` pending ones, with subtasks indented; a finished list stays until your next prompt. `/jar tasks` is your view/editor: `a` add, <kbd>Space</kbd>/<kbd>Enter</kbd> check, `e` edit, `d` delete, `f` filter.
249
+ - **`jar_todo`** — a Claude/omp-style task list the agent keeps for multi-step work. A new request starts with `write`: the complete fresh plan, replacing old completed work. `update` changes one stable `id` without disturbing omitted fields/subtasks, and `remove` deletes one task tree. `append` is reserved mainly for new requirements introduced while an existing checklist is already active, such as user steering; if the previous list is fully completed, the agent writes a new list instead of extending the fossil record. IDs are returned for every task. Each task is `pending`, `in_progress` (exactly one leaf at a time) or `completed`, with optional `activeForm`. `start` and `done` deliberately return only the task they changed, while the internal details still carry the checklist state needed by the UI and parent/subagent mirroring. Tasks can have one level of **subtasks**; parent status and progress roll up automatically. The live checklist above the composer shows `✔`, `◼` and `☐`; `/jar tasks` remains the human editor.
241
250
  ```json
242
- { "action": "append", "todos": [{ "content": "Run tests", "status": "pending", "activeForm": "Running tests" }] }
243
- { "action": "replace", "id": "<returned-id>", "status": "in_progress" }
244
- { "action": "remove", "id": "<returned-id>" }
251
+ { "action": "write", "todos": [{ "content": "Implement feature", "status": "in_progress" }, { "content": "Run tests", "status": "pending" }] }
252
+ { "action": "update", "id": "<returned-id>", "status": "completed" }
253
+ { "action": "append", "todos": [{ "content": "Handle newly requested edge case", "status": "pending" }] }
245
254
  ```
246
- Use `parent` with `append` to add subtasks; `start`, `done`, `open`, `edit`, and `list` remain available (`add`/`delete` also remain supported).
255
+ Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
247
256
  - **`jar_ask`** — structured questions: numbered options with descriptions, single or multi-select, *Type your own answer* (multi-line, paste-friendly) and *Chat about this* to discuss before choosing.
248
257
  - **`/diff`** — review what the agent (and its editing subagents) changed since the first edit to each file: files with `+/−` counts on the left, a numbered, colored diff on the right. `a` accept (keep, stop tracking), `r` then `y` revert (restore the original, or remove a file it created), `A`/`R` for all. The footer shows `± N files · /diff` while anything is unreviewed; each file counts once no matter who edited it. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
249
258
  - **`jar_shell`** — `start` (with optional `name`, `watch` regex and `notify`), `list`, `output`, `kill`. Output is ANSI-stripped and bounded (2000 lines). When a watch matches or the process ends, a visible message wakes the agent (queued if it is busy). Each running shell gets a footer row; the activity view (`/jar shells`, a click on the row) tails it live (`x` kill, `f` follow). All shells stop when the session ends.
250
- - **`jar_delegate`** — up to four subagents run in parallel as separate Pi processes (RPC mode) with a fresh context, on a role (default `task`, then `default`, then the current model). They are read-only (`read`, `grep`, `find`, `ls`, plus `jar_todo`) unless `write: true`; subagents cannot delegate further; aborting the turn stops them. Each has a live footer row. When they finish, the parent gets a detailed report per subagent: outcome, duration, tool calls by name, files read and edited, its task checklist, failed calls, then its own Summary / Details / Verification / Open issues.
259
+ - **`jar_delegate`** — spawns **retained session-scoped** subagents up to **Max subagents** in `/jar settings` (2, 4, 6, 8 or 16; default 4). Idle/paused children and live legacy workers count toward the shared limit, not the parent; over-cap batches are rejected before launch. Lowering the limit does not terminate existing children, but blocks new launches until there is room. The parent is given moderator context and should coordinate rather than duplicate their work. `mode: "scout"` is fresh/read-only and defaults to the `scout` role; `fork` is a true Pi session fork, read-only, and defaults to `reviewer`; `worktree` forks context and defaults to `worker`, editing inside a path-guarded disposable Git worktree. A settled retained agent becomes `idle`, keeping its process, model context and workspace for reuse. The old `write: true` direct shared-workspace mode remains one-shot for compatibility.
260
+ - **`jar_subagent`** — moderator control plane: `peek` returns only task progress (done/total, the in-progress task, activity, the next open tasks), read from memory; `steer` redirects a running worker; `ask` injects a terse BTW question and waits for its answer; `pause` aborts the current operation but keeps context/workspace; `resume` wakes that same agent with an optional instruction; `stop` retires it and returns a structured handoff with progress, work directory, changed/applied files and what remains. When a resumed turn ends, a visible `pi-jar.subagent` message wakes the moderator (or queues for its current run) with that progress and the turn's report, so it never polls `peek`. Every action returns on <kbd>Esc</kbd>, `pause` gives up waiting after 15 s, and a working child with no RPC activity for 10 minutes is stopped as hung.
261
+ - **Worktree finalization** — worktree edits do **not** touch the parent merely because a worker becomes idle. They remain private across pause/resume cycles. On `jar_subagent stop`, pi-jar verifies parent drift and reviewability, applies safe changes, and adds them to `/diff`; conflicts remain isolated and the workspace path is reported. Resolve parent drift and retry `stop` to reconcile; failed workspaces are retained rather than trimmed out of report history. Process closure is awaited before finalization. All replacements/rollback backups are prepared before parent mutation; failed multi-file application rolls back rather than silently leaving a partial result.
262
+ - **`jar_democracy`** — **exceptional only**, for a persistent **super-complex** issue with several viable options and evidence of at least two distinct failed approaches. The moderator opens 2–8 evidence-backed options, resumes explicitly relevant idle/paused **fresh-context read-only scouts** or spawns fresh scouts within the same pool limit, and collects one private ballot per voter before publishing the tally. A **strict majority of the invited electorate** wins; ties, plurality without majority, and missing/invalid ballots require the user to choose among leading options, with evidence/tradeoffs rather than invented probabilities. TUI uses the structured question picker; RPC returns `needs-user` for the moderator to resolve with `jar_ask`. Voting returns a recommendation and **never implements it** or bypasses safety/approval. Prefer new scouts if past discussions could bias reused agents. Do not use democracy for ordinary complexity or routine decisions. A round is bounded: a 10-minute ballot deadline records unfinished voters as failures, rationales are capped at 600 characters, and scouts spawned for the round are retired when it ends.
263
+ - **`jar_discuss`** — a bounded shared discussion paper for virtual agent-to-agent discussion. Questions and answers have stable IDs plus author/target metadata; entries are capped at 1600 characters, the newest 64 are retained, and serialized UTF-8 is capped at 64 KiB (older entries are evicted until it fits). `list` replies are capped at 4000 characters; `since` rereads only newer entries and `questionId` reads one thread in full. Agents use it for narrow Q/A, not as another transcript.
251
264
  - **Activity view** — one split view (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>, `/jar activity`, or a click on a footer row) for subagents, background shells and other extensions' teammates: list on the left, live details on the right (auto-follows; scroll up to pause, `f` to follow again). For a subagent the details show its task, its own checklist with subtasks, and a transcript like omp's: each tool call and message is one collapsed line that expands on demand (`Tab` into it, `↑↓`, `Enter`, or click) to show arguments and output, while streaming text shows live. `s` opens a steering input: type a message and press `Enter` to redirect the running subagent. `x` stops just that one. Finished runs stay listed for review.
252
265
  - **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
253
- - Pi's built-in read/shell/edit/write tool cards render compactly; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away.
266
+ - Pi's built-in read/shell/edit/write tool cards render compactly: collapsed, each is one padded, state-colored card with its call line and a one-line summary (edit included), and partially streamed arguments never flicker to a fallback; the full output or diff stays one click or <kbd>Ctrl</kbd>+<kbd>O</kbd> away. Background sources (subagent streams, shell output) repaint the footer at most every 250 ms and the activity view every 100 ms, because every requested frame re-renders the whole transcript. The sliding bar some terminals show in the tab while the agent works is Pi's own `OSC 9;4` progress (`terminal.showTerminalProgress` in Pi settings), not pi-jar.
254
267
 
255
268
  ## Settings
256
269
 
@@ -258,7 +271,7 @@ This is a pi-jar enhancement: [pi-advisor](https://github.com/philipbrembeck/pi-
258
271
 
259
272
  - **Appearance** — accent, motion, rounded composer, Ember mascot, next-prompt suggestions, pi-jar UI, icons (`unicode` default, `nerd` for a [Nerd Font](https://www.nerdfonts.com) terminal like omp's nerd preset, `ascii` for plain labels).
260
273
  - **Footer** — field visibility. In fullscreen mode the session name (or `sessions` for an unnamed session) opens the session picker and each subagent/shell row opens the activity view. SoL-Pi's savings status is not repeated in the footer (it already notifies).
261
- - **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, advisor, advisor gates.
274
+ - **Pi** — mouse clicks (Pi fullscreen mode), copy on select, goal auto rounds, **Max subagents** (explicit choice then Save; default 4), advisor, advisor gates.
262
275
 
263
276
  pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
264
277
 
@@ -287,7 +300,10 @@ pi-jar/
287
300
  │ ├── split-view.ts shared two-pane frame
288
301
  │ ├── changes.ts change tracker and line diff; diff-view.ts is /diff
289
302
  │ ├── shells.ts background shells and jar_shell
290
- │ ├── delegate.ts jar_delegate subagents and their live registry
303
+ │ ├── delegate.ts retained subagents, moderator controls and RPC lifecycle
304
+ │ ├── delegate-worktree.ts isolated Git worktrees, path guard and safe reconciliation
305
+ │ ├── discussion.ts bounded structured cross-agent Q/A paper
306
+ │ ├── democracy.ts exceptional scout ballots, strict majority and user tie-break
291
307
  │ ├── activity-view.ts subagent/shell details view
292
308
  │ ├── icons.ts unicode / nerd / ascii glyph sets
293
309
  │ ├── attachments.ts image chips; prompt-search.ts is prompt history
@@ -50,7 +50,7 @@ ctx.ui.setStatus("pi-jar.quota.openai-codex", JSON.stringify({
50
50
  | `pi-jar.goal` | goal events (`set`, `status`, `round`; v1 `set`/`clear` accepted) | active branch |
51
51
  | `pi-jar.plan` | plan state (`enabled`, `steps`, `text`, `path`, `title`) | active branch |
52
52
 
53
- Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.goal-context`, `pi-jar.goal-continuation`, `pi-jar.suggest-reminder`) are filtered or deduplicated in the `context` hook, so they never accumulate.
53
+ Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.goal-context`, `pi-jar.goal-continuation`, `pi-jar.moderator-context`, `pi-jar.suggest-reminder`) are filtered or deduplicated in the `context` hook, so they never accumulate. Pi applies a `context` rewrite to one LLM call only and re-sends the full persisted history before the next, so the pruning runs on every call.
54
54
 
55
55
  ## Tools pi-jar registers
56
56
 
@@ -65,7 +65,7 @@ Hidden context messages (`pi-jar.plan-context`, `pi-jar.plan-reminder`, `pi-jar.
65
65
  | `jar_delegate` | up to four parallel subagents (`tasks: [{task, name?, role?}]`, `write?`) |
66
66
  | `jar_advisor` | second opinion from the `advisor` role (`question?`, `draft?`) |
67
67
 
68
- `jar_shell` events arrive as a visible `pi-jar.shell` custom message that triggers (or queues) an agent turn. `jar_delegate` runs child processes with `PI_JAR_CHILD=1`, which keeps pi-jar in the child from registering `jar_delegate` again, and publishes each running subagent through the role contract above as `pi-jar.role.delegate-<batch>-<n>`. The advisor publishes `pi-jar.role.advisor` while it is consulting, and its `/advisor` and gate answers arrive as visible `pi-jar.advisor` custom messages.
68
+ `jar_shell` events arrive as a visible `pi-jar.shell` custom message that triggers (or queues) an agent turn; when a subagent turn started by `jar_subagent resume` ends, a visible `pi-jar.subagent` message does the same. `jar_delegate` runs child processes with `PI_JAR_CHILD=1`, which keeps pi-jar in the child from registering `jar_delegate` again, and publishes each running subagent through the role contract above as `pi-jar.role.delegate-<batch>-<n>`. The advisor publishes `pi-jar.role.advisor` while it is consulting, and its `/advisor` and gate answers arrive as visible `pi-jar.advisor` custom messages.
69
69
 
70
70
  ## Commands that overlap other packages
71
71
 
package/docs/WORKFLOWS.md CHANGED
@@ -78,8 +78,9 @@ Resolution for a role name:
78
78
 
79
79
  1. Merge global (`<agent dir>/pi-jar-roles.json`) and project (`<cwd>/.pi/pi-jar-roles.json`) roles; project wins per role.
80
80
  2. If the spec is `@other[:effort]`, remember the effort (first one wins) and follow `other`; stop after five hops or on a cycle.
81
- 3. A `provider/model[:effort]` spec resolves through Pi's model registry; activation fails with a notice if the model is unknown or unauthenticated.
82
- 4. An unassigned role "follows the current model" (activating it only labels the footer).
81
+ 3. A `provider/model[:effort]` spec resolves through Pi's model registry.
82
+ 4. For an assigned role, activation tries the primary then that role's configured fallback specs in order. Delegated agents also choose the first currently available candidate. An explicit empty project fallback list disables the inherited global list.
83
+ 5. An unassigned role "follows the current model" (activating it only labels the footer).
83
84
 
84
85
  `activateTemporary(role)` returns a restore function that puts back the previous model, effort and active role — used by plan mode (`plan`), approved-plan execution (`implement`), goal rounds (`implement` / `advisor`).
85
86
 
@@ -108,13 +109,21 @@ Manual choices win: a model or effort you select yourself (model picker, cycling
108
109
  - If a completed turn did not suggest, a single hidden reminder asks for one (never while plan or goal automation owns the next step, and never after an interrupt).
109
110
  - The suggestion is sanitized to one line (≤ 160 characters) and kept in memory only. It is cleared by your next prompt, a new run, typing, or branch navigation.
110
111
 
112
+ ## Task tracking discipline
113
+
114
+ - `jar_todo write` is the normal way to establish the checklist for a new multi-step request. It represents the complete current plan, so completed work from an earlier request is naturally dropped.
115
+ - `update` changes one stable task id while preserving omitted fields and subtasks. `remove` deletes one task tree.
116
+ - `append` is for genuinely new work introduced while an existing checklist is still active, especially user steering or an added requirement. When the current checklist is already fully completed, start a fresh list with `write` instead of appending.
117
+ - `start` and `done` deliberately return/render only the single task they changed. Their structured result still carries the bounded checklist (the full list inside subagents) so parent/subagent progress mirroring remains accurate.
118
+ - Exactly one leaf task may be `in_progress`; starting another leaf parks the previous one. Parent status is derived from its subtasks.
119
+
111
120
  ## Change review
112
121
 
113
122
  - A `tool_call` hook records a file's content right before the agent's **first** `edit` or `write` to it (project files only, up to 200 files, 1 MB each, text only). Later edits keep that baseline, so the review always shows everything since the agent started on the file.
114
123
  - `/diff` (or <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) compares the baseline with the file on disk using a line diff with 3 lines of context. Files that are back to their baseline drop out.
115
124
  - **Accept** forgets the baseline. **Revert** writes the baseline back (or removes a file the agent created) after a second key press to confirm. Errors are reported and the file stays listed.
116
125
  - Baselines live in memory for the session; changes made through `bash` or by you are not tracked.
117
- - **Subagent edits** join the same review. An editing `jar_delegate` batch gets a private temporary directory (`PI_JAR_BASELINE_DIR`); pi-jar inside each subagent writes a file's baseline there right before the subagent's first edit to it. After each successful subagent `edit`/`write` the parent adopts that baseline, unless it already tracks the file (the older baseline wins), so every file is counted once in the footer and `/diff`. The directory is removed when the batch ends. A subagent without pi-jar loaded cannot record baselines, so its edits are listed in its report but not in `/diff`.
126
+ - **Subagent edits** join the same review in two ways. Retained `worktree` writers remain isolated while working, idle or paused. Only `jar_subagent stop` finalizes them: pi-jar verifies that each parent target still equals the private baseline snapshot, copies reviewable safe results back, then captures/marks those parent files in the normal `ChangeTracker`. The deprecated one-shot `write: true` shared-workspace mode keeps its older `PI_JAR_BASELINE_DIR` bridge for compatibility.
118
127
 
119
128
  ## Background shells
120
129
 
@@ -130,13 +139,28 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
130
139
 
131
140
  ## Subagents
132
141
 
133
- - `jar_delegate` takes 1–4 tasks. Each runs `pi --mode rpc --no-session [parent's -e/--extension/-ne flags] --model <role model> [--thinking <effort>] [--tools read,grep,find,ls,jar_todo]` in the project directory, with `PI_JAR_CHILD=1`, and receives the task as an RPC `prompt` (with instructions to track work in `jar_todo` and to report in Summary / Details / Verification / Open issues sections). Once the child emits `agent_settled`, stdin is closed and Pi exits in order. Dialogs a child opens are answered as cancelled so it never blocks.
134
- - The model comes from the task's role (default `task`), falling back to `default` and then the current model.
135
- - Progress (current tool, streaming text, tools used, turns, cost) streams into the tool card; each running subagent is published as a teammate for the welcome TEAM row and footer, and cleared when it ends.
136
- - The activity view keeps a structured transcript per subagent: every tool call (arguments, streamed and final output, duration, success) and each assistant message, plus steering messages; the newest 200 entries, each clipped to 4000 characters, and the text still streaming. Entries are collapsed to one line and rendered only when expanded (`Tab` into the transcript, `↑↓`, `Enter`, or a click on the entry). The subagent's own `jar_todo` checklist (with subtasks) is mirrored from its tool results. It all lives only in memory with the 8 most recent finished runs; the tool result saved in the session carries the reports, not the transcript.
137
- - **Steering:** `s` in the activity view (or a click on the input row) opens an input; `Enter` sends the text as an RPC `steer` message, delivered after the subagent's current tool calls. Steering works while the subagent is running.
138
- - **Report back:** for each subagent the parent reads its outcome and duration, turns, tool calls by name, files read and edited, its checklist with per-task progress, failed tool calls, and then the subagent's own report.
139
- - Subagents time out after 20 minutes and stop when the turn is aborted; `x` in the activity view stops one without touching the rest of its batch (its report reads `failed: stopped`). In goal mode, `write: true` needs an open task.
142
+ - The main agent is the **moderator** while a retained fleet exists. A hidden bounded context lists the fleet and tells the parent to decompose, route, peek, steer, resolve disagreements and synthesize instead of duplicating a worker's task. The optional `moderator` role gives this purpose a configurable model without forcing a model switch.
143
+ - `jar_delegate` shares a session-wide **Max subagents** setting in `/jar settings`: exactly 2, 4, 6, 8 or 16, default 4. All retained live children (including idle/paused and live legacy one-shots) count; the moderator does not. Concurrent/reentrant batches reserve capacity atomically and reject over-cap requests before any launch. Lowering the limit preserves the fleet and prevents further spawning until room is available. `scout` runs `pi --mode rpc --no-session`, is read-only and defaults to the `scout` role. `fork` uses `--fork <parent-session>`, is read-only and defaults to `reviewer`. `worktree` uses the same real context fork, defaults to `worker`, and runs in a detached disposable Git worktree. Explicit `task.role` overrides those defaults; each role uses its primary/fallback candidates before falling back to the parent's current model.
144
+ - Retained children do not exit on `agent_settled`. They become `idle`, keeping the same RPC process and model context. `jar_subagent pause` sends `clear_queue` + `abort` and leaves the agent `paused`; `resume` sends a new prompt to that same child. `steer` redirects a working child. `ask` pauses an active task, crosses a settled boundary, then starts a compact BTW-only prompt, so the old task report cannot be mistaken for the answer. It preserves the prior paused state and assigned scope for later resume.
145
+ - `jar_subagent peek` is the moderator's cheap status surface and reads only task progress from memory: key, name, state, done/total checklist leaves, the in-progress task, current activity and the next three open tasks (no git, transcript or report reads). `stop` is destructive/final: abort an active operation if necessary, close the child, return a handoff (workspace, changed/applied files, remaining work, last report), and finalize safe worktree changes.
146
+ - The moderator never has to poll: when a turn started by `jar_subagent resume` ends, pi-jar coalesces the settled agents into one visible `pi-jar.subagent` message (their `peek` progress plus the turn's report, clipped to 1200 characters) that triggers or queues a moderator turn. `pause`, `ask` and `stop` cancel that wake-up because the moderator is handling the agent directly.
147
+ - Nothing in the moderator path waits unbounded. `jar_subagent` returns as soon as the tool call is aborted (the child keeps its state); `pause` reports "not settled yet" after 15 s if the child ignores abort and still pauses when it settles; a working child that sends no RPC event for 10 minutes is killed as hung. After a child exits, its stdio gets 2 s to drain: a grandchild (MCP server, shell) that inherited the pipes cannot keep `stop` or session shutdown waiting for `close`.
148
+ - Worktree setup snapshots the exact starting workspace: `HEAD` plus tracked dirty changes and non-ignored untracked files are committed only inside the detached worktree. The worktree persists across idle/pause/resume. On stop, the parent target must still byte/mode-match the snapshot and the change must be trackable by `/diff`; otherwise nothing is overwritten and the workspace/error is reported. Conflicted workspaces survive report-history trimming; after resolving parent drift, retry `stop`. Finalization waits for RPC stdio closure. Snapshot blobs preserve filesystem bytes without executing clean/smudge filters or hooks. The path guard handles Pi path normalization, rejects lexical/symlink/dangling-link escapes and Git control metadata, checks read-tool filename fallbacks (NFD, curly apostrophes, AM/PM spaces), and rechecks both source and destination ancestors during reconciliation. All replacements/rollback backups are prepared before parent mutation; failed multi-file application restores earlier files. Exceptional rollback failures explicitly report actual applied paths for recovery. Worktree children receive only `read,edit,write,grep,find,ls,jar_todo,jar_discuss`; shell tools are absent. This is filesystem-write isolation, not a network/process sandbox.
149
+ - `jar_discuss` gives moderator and children a session-scoped structured paper. `ask` appends a question with stable id/from/to fields and `answer` references the question id (validated under the same lock as the write). `list` returns the newest entries within a 4000-character reply, each clipped to 400 characters, and says how many older entries it omitted; `since: "d12"` returns only newer entries (a no-change reread costs one line) and `questionId` reads one thread in full. Text is capped at 1600 characters per entry, at most 64 entries are retained, and the compact UTF-8 JSON paper is strictly bounded to 64 KiB with oldest-entry eviction and atomic rewrites. The file lock waits by yielding to the event loop (at most ~1 s), never blocking the UI. It is intentionally for terse inter-agent Q/A rather than another transcript; agents are told not to poll it.
150
+ - Fork/worktree children inherit the parent model conversation but reset pi-jar goal/plan/todo automation. All child modes inherit explicit extension flags, set `PI_JAR_CHILD=1` to prevent recursive delegation, and share the discussion-paper path/name through environment variables.
151
+ - Progress (current tool, streaming text, tools used, turns, cost) stays in the activity view. Retained `idle` and `paused` agents remain visible in the footer/activity fleet; the newest eight ordinary retired reports remain for review, while unresolved private workspaces remain recoverable until explicit finalization/session cleanup. `s` in the activity view still steers a working subagent and `x` stops the selected agent through the same registry.
152
+ - The deprecated `write: true` mode remains a one-shot shared-workspace compatibility path. New delegated implementation should use `mode: "worktree"`.
153
+
154
+ ### Exceptional agentic democracy
155
+
156
+ `jar_democracy` is **not** a default decomposition or decision tool. The moderator must supply `complexity: "super-complex"`, a concrete complexity justification, the persistent issue, at least two distinct failed approaches with evidence, and 2–8 uniquely identified options with tradeoffs. These gates are runtime-validated in addition to prompt guidance; the moderator is responsible for truthful evidence.
157
+
158
+ - Nominate relevant idle/paused **scouts** with `agents`, and set the total electorate with `voters` (default 4, or 2 at a cap of 2; always bounded by the configured cap). Forks, writable workers, busy agents and duplicate nominations are rejected. Reuse preserves context; prefer fresh scouts if discussion history could bias a ballot.
159
+ - Unfilled seats spawn fresh read-only scouts through the normal delegate controller. The same session-wide capacity applies; the moderator must explicitly stop unneeded agents if there is no room. Voting never silently kills existing workers. Scouts spawned for the round are retired once it ends, so they never keep holding pool slots; nominated scouts stay retained.
160
+ - Each voter checks the evidence briefly and independently and returns one nonce-bound option ID with a rationale (at most 600 characters; failure reasons 200). Voters are instructed not to use shared discussion. Results are collected before any tally is exposed; malformed/stale/missing/error ballots are recorded, never inferred. A round has a 10-minute ballot deadline: unfinished voters are cancelled and recorded as failures (a nominated scout cancelled this way is retired), so a hung voter can never hold the round open.
161
+ - A **strict majority** means more than half of **all invited voters**, with at least two valid ballots. The largest plurality is not a majority. Missing ballots do not reduce this denominator or allow a single response to decide.
162
+ - Ties, plurality without majority, and insufficient ballots return `needs-user`. The leading options are presented to the user with their evidence/tradeoffs in the TUI question picker, including custom/chat/cancel paths. In RPC, the moderator must use `jar_ask` before proceeding. Recommend the most plausible option from actual evidence; do not fabricate numerical probabilities or silently break a tie.
163
+ - A majority is a recommendation, not permission to execute dangerous work. The tool never implements the selected option, and ordinary safety/user-approval requirements remain in force. Only one round may run at a time; abort invalidates incomplete results.
140
164
 
141
165
  ## Sessions and prompt history
142
166
 
@@ -5,6 +5,8 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { Key, truncateToWidth, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
6
6
  import { ACCENT_NAMES, loadedAccents, selectAccent } from "../src/accent.ts";
7
7
  import { registerAskTool } from "../src/ask-tool.ts";
8
+ import { registerDemocracy } from "../src/democracy.ts";
9
+ import type { DelegateController } from "../src/delegate.ts";
8
10
  import { ComposerStyle } from "../src/composer.ts";
9
11
  import { installCompactBuiltinTools } from "../src/compact-tools.ts";
10
12
  import { WELCOME_INTERVAL_MS } from "../src/animations.ts";
@@ -34,7 +36,9 @@ import { TASK_ENTRY, TodoStore, todoProgress, todoTotals } from "../src/tasks.ts
34
36
  import { formatCost, sessionCost } from "../src/usage.ts";
35
37
  import { WorkingState } from "../src/working.ts";
36
38
  import { ChangeTracker } from "../src/changes.ts";
37
- import { DelegateRegistry, registerDelegate, type DelegateRun } from "../src/delegate.ts";
39
+ import { CHILD_ENV, DelegateRegistry, registerDelegate, SUBAGENT_MESSAGE, type DelegateRun } from "../src/delegate.ts";
40
+ import { CHILD_WORKTREE_ENV, workspacePathAllowed } from "../src/delegate-worktree.ts";
41
+ import { createDiscussionPaper, DISCUSSION_FILE_ENV, disposeDiscussionPaper, registerDiscussionTool, SUBAGENT_NAME_ENV } from "../src/discussion.ts";
38
42
  import { openActivityView, type ActivityTarget } from "../src/activity-view.ts";
39
43
  import { ICON_SETS, setIconSet, withIcon } from "../src/icons.ts";
40
44
  import { collectPrompts, openPromptSearch } from "../src/prompt-search.ts";
@@ -55,6 +59,40 @@ const LARGE_SESSION_ENTRIES = 800;
55
59
  /** Entries Pi writes when a fresh session starts; a branch holding only these has no transcript yet. */
56
60
  const SESSION_SETUP_ENTRIES = new Set(["thinking_level_change", "model_change", "session_info"]);
57
61
  export default function piJar(pi: ExtensionAPI): void {
62
+ const delegatedChild = process.env[CHILD_ENV] === "1";
63
+ const delegatedWorktree = delegatedChild ? process.env[CHILD_WORKTREE_ENV] : undefined;
64
+ if (delegatedChild) {
65
+ // Forked transcripts retain custom prompt messages even though their automation stores
66
+ // and handlers are disabled. Remove only parent automation, not task/user context.
67
+ const parentAutomation = new Set([
68
+ "pi-jar.goal-context", "pi-jar.goal-continuation",
69
+ "pi-jar.plan-context", "pi-jar.plan-reminder", "pi-jar.moderator-context", SUBAGENT_MESSAGE
70
+ ]);
71
+ pi.on("context", (event) => {
72
+ const messages = event.messages.filter((message) => !parentAutomation.has((message as { customType?: string }).customType ?? ""));
73
+ if (messages.length !== event.messages.length) return { messages };
74
+ });
75
+ }
76
+ if (delegatedWorktree) {
77
+ const pathTools = new Set(["read", "edit", "write", "grep", "find", "ls"]);
78
+ const shellTools = new Set(["bash", "powershell", "jar_shell"]);
79
+ pi.on("tool_call", (event, ctx) => {
80
+ if (shellTools.has(event.toolName)) {
81
+ return { block: true, reason: "Sandboxed worktree subagents cannot run shell tools." };
82
+ }
83
+ if (!pathTools.has(event.toolName)) return;
84
+ const input = event.input && typeof event.input === "object" ? event.input as Record<string, unknown> : {};
85
+ if (!workspacePathAllowed(delegatedWorktree, ctx.cwd, ".")) {
86
+ return { block: true, reason: "Sandboxed worktree subagent working directory is outside its workspace." };
87
+ }
88
+ for (const key of ["path", "file_path", "cwd"]) {
89
+ const value = input[key];
90
+ if (typeof value === "string" && value && !workspacePathAllowed(delegatedWorktree, ctx.cwd, value, event.toolName === "read")) {
91
+ return { block: true, reason: `Sandboxed worktree subagent cannot access outside its workspace: ${value}` };
92
+ }
93
+ }
94
+ });
95
+ }
58
96
  installCompactBuiltinTools(pi);
59
97
  let demo = false;
60
98
  let visualSettings = defaultVisualSettings();
@@ -94,6 +132,8 @@ export default function piJar(pi: ExtensionAPI): void {
94
132
  let workingIndicatorKey = "";
95
133
  let openSettings: (ctx: ExtensionContext) => Promise<void> = async () => {};
96
134
  let settingsOpen = false;
135
+ let discussionFile: string | undefined = process.env[DISCUSSION_FILE_ENV];
136
+ registerDiscussionTool(pi, () => discussionFile, () => process.env[SUBAGENT_NAME_ENV] ?? "moderator");
97
137
 
98
138
  // A finished list stays visible (all struck through) until the user's next prompt, like Claude.
99
139
  let todosAcknowledged = false;
@@ -158,19 +198,31 @@ export default function piJar(pi: ExtensionAPI): void {
158
198
  };
159
199
  /** Footer indicators owned by pi-jar; live subagents and shells get their own rows. */
160
200
  const footerChips = () => changeCount ? [withIcon("changes", `${changeCount} file${changeCount === 1 ? "" : "s"} · /diff`)] : [];
201
+ /**
202
+ * Subagent streams and shell output change the footer many times a second; every requested frame
203
+ * re-renders the whole transcript, so background sources repaint it at most this often.
204
+ */
205
+ const BACKGROUND_REPAINT_MS = 250;
206
+ let backgroundRepaint: NodeJS.Timeout | undefined;
207
+ const repaintFooterSoon = () => {
208
+ if (backgroundRepaint) return;
209
+ backgroundRepaint = setTimeout(() => { backgroundRepaint = undefined; footerTui?.requestRender(); }, BACKGROUND_REPAINT_MS);
210
+ backgroundRepaint.unref?.();
211
+ };
161
212
  const subagents = new DelegateRegistry();
162
- subagents.subscribe(() => footerTui?.requestRender());
213
+ subagents.subscribe(repaintFooterSoon);
163
214
  /** Finished work stays visible briefly so a quick run is not a flicker. */
164
215
  const ACTIVITY_LINGER_MS = 5000;
165
216
  const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
166
- : run.state === "done" ? "done" : run.error === "stopped" ? "stopped" : "failed";
217
+ : run.state === "idle" ? "idle" : run.state === "paused" ? "paused" : run.state === "stopped" ? "stopped"
218
+ : run.state === "done" ? "done" : "failed";
167
219
  const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
168
220
  : job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
169
221
  const seconds = (ms: number) => { const s = Math.max(0, Math.round(ms / 1000)); return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`; };
170
222
  const footerActivity = (now: number): FooterActivity[] => {
171
223
  const rows: FooterActivity[] = [];
172
224
  for (const { key, run } of subagents.records()) {
173
- if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
225
+ if ((run.state === "done" || run.state === "failed" || run.state === "stopped") && run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
174
226
  rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
175
227
  detail: run.error ?? run.activity ?? cleanText(run.task, 120),
176
228
  stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
@@ -227,17 +279,21 @@ export default function piJar(pi: ExtensionAPI): void {
227
279
  registerAdvisor(pi, modelRoles, { enabled: () => visualSettings.advisor, gates: () => visualSettings.advisorGates, usage: sideUsage });
228
280
  registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
229
281
  quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
230
- modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
231
- registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges });
282
+ modelRoles.register((ctx) => openRolesUi(ctx, modelRoles), { activateDefault: () => !delegatedChild });
283
+ let delegateController: DelegateController | undefined;
284
+ registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges, discussionFile: () => discussionFile,
285
+ getMaxSubagents: () => visualSettings.maxSubagents, onController: (controller) => { delegateController = controller; } });
286
+ if (!delegatedChild) registerDemocracy(pi, () => delegateController, () => visualSettings.maxSubagents);
232
287
  const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
233
- planMode.register();
288
+ if (!delegatedChild) planMode.register();
234
289
  const goalLoop = new GoalLoop(pi, {
235
290
  goals: () => goals, todos: () => todos, roles: modelRoles, planActive: () => planMode.isEnabled(),
236
291
  maxRounds: () => visualSettings.goalRounds,
292
+ subagentMode: (agent) => subagents.resolve(agent)?.run.mode,
237
293
  changed: (ctx) => { footerTui?.requestRender(); welcomeTui?.requestRender(); updateTaskWidget(ctx); },
238
294
  completed: () => composer.flash("complete", 4000)
239
295
  });
240
- goalLoop.register();
296
+ if (!delegatedChild) goalLoop.register();
241
297
  planMode.setOnEnter((ctx) => goalLoop.pause(ctx, "plan mode is on"));
242
298
  const suggestions = new SuggestionState();
243
299
  composer.attachSuggestions(suggestions);
@@ -603,13 +659,22 @@ export default function piJar(pi: ExtensionAPI): void {
603
659
  changeCount = 0;
604
660
  shells?.dispose();
605
661
  subagents.clear();
662
+ if (!delegatedChild) {
663
+ disposeDiscussionPaper(discussionFile);
664
+ discussionFile = createDiscussionPaper();
665
+ }
606
666
  shells = new ShellManager(onShellEvent);
607
- shells.onChange = () => footerTui?.requestRender();
667
+ shells.onChange = repaintFooterSoon;
608
668
  goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
609
669
  let branch: readonly unknown[] = [];
610
670
  try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
611
- restoreTodos(ctx, branch);
612
- restoreGoal(ctx, branch);
671
+ if (delegatedChild) {
672
+ todos.restore([]);
673
+ goals.restore([]);
674
+ } else {
675
+ restoreTodos(ctx, branch);
676
+ restoreGoal(ctx, branch);
677
+ }
613
678
  // Quota is enabled per session; no credentials or consent are persisted.
614
679
  quotaCache = new QuotaCache(
615
680
  (provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
@@ -641,7 +706,7 @@ export default function piJar(pi: ExtensionAPI): void {
641
706
  // A fresh session is not empty: Pi records the startup thinking level/model as entries.
642
707
  const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
643
708
  && typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
644
- if (!resumed) showWelcome(ctx);
709
+ if (!delegatedChild && !resumed) showWelcome(ctx);
645
710
  });
646
711
  // Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
647
712
  const dismissWelcome = (ctx: ExtensionContext) => {
@@ -684,13 +749,19 @@ export default function piJar(pi: ExtensionAPI): void {
684
749
  pi.on("session_tree", (_event, ctx) => {
685
750
  let branch: readonly unknown[] = [];
686
751
  try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
687
- restoreTodos(ctx, branch);
688
- restoreGoal(ctx, branch);
752
+ if (!delegatedChild) {
753
+ restoreTodos(ctx, branch);
754
+ restoreGoal(ctx, branch);
755
+ }
689
756
  updateCost(ctx);
690
757
  composer.refreshSession(ctx);
691
758
  sampleFooter(ctx);
692
759
  });
693
- pi.on("session_compact", (_event, ctx) => { restoreTodos(ctx); restoreGoal(ctx); updateCost(ctx); sampleFooter(ctx); });
760
+ pi.on("session_compact", (_event, ctx) => {
761
+ if (!delegatedChild) { restoreTodos(ctx); restoreGoal(ctx); }
762
+ updateCost(ctx);
763
+ sampleFooter(ctx);
764
+ });
694
765
  pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
695
766
  pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
696
767
  pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
@@ -703,6 +774,7 @@ export default function piJar(pi: ExtensionAPI): void {
703
774
  try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
704
775
  quotaCache?.stop();
705
776
  quotaCache = undefined;
777
+ if (!delegatedChild) { disposeDiscussionPaper(discussionFile); discussionFile = undefined; }
706
778
  todos = undefined;
707
779
  changes = undefined;
708
780
  changeCount = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jar",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "A warm, role-aware Pi terminal workspace with animated UI, plan and goal workflows, tasks, change review, model roles, background shells and subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,7 +46,8 @@ function collect(sources: ActivitySources): Item[] {
46
46
  if (records.length) items.push({ kind: "header", label: withIcon("subagents", "SUBAGENTS") });
47
47
  for (const record of records) {
48
48
  const { run } = record;
49
- const status: Status = run.state === "working" ? "running" : run.state === "queued" ? "pending" : run.state === "done" ? "success" : run.error === "stopped" ? "stopped" : "error";
49
+ const status: Status = run.state === "working" ? "running" : run.state === "queued" || run.state === "idle" ? "pending"
50
+ : run.state === "paused" || run.state === "stopped" ? "stopped" : run.state === "done" ? "success" : "error";
50
51
  items.push({ kind: "subagent", id: record.key, status, label: run.name, record });
51
52
  }
52
53
  const jobs = sources.shells?.summaries() ?? [];
@@ -215,7 +216,7 @@ function subagentHead(run: DelegateRun, status: Status, width: number, fg: Fg):
215
216
  ].filter(Boolean);
216
217
  const head = [
217
218
  fg("accent", [run.name, run.role, run.model].filter(Boolean).join(" · ")),
218
- fg(COLOR[status], withIcon(status, status === "stopped" ? "stopped" : run.state)) + fg("dim", " · " + facts.join(" · "))
219
+ fg(COLOR[status], withIcon(status, run.state === "paused" ? "paused" : status === "stopped" ? "stopped" : run.state)) + fg("dim", " · " + facts.join(" · "))
219
220
  ];
220
221
  // A turning glyph on the 1 s tick: a long tool call or a slow model still looks alive.
221
222
  if (run.activity) head.push(fg("muted", (run.state === "working" ? SPINNER[Math.floor(Date.now() / 1000) % SPINNER.length] + " " : "") + run.activity));
@@ -274,13 +275,22 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
274
275
  const fg: Fg = (color, text) => theme.fg(color as never, text);
275
276
  const shells = sources.shells;
276
277
  const previous = shells?.onChange;
277
- const unsubscribe = sources.subagents.subscribe(() => tui.requestRender());
278
- if (shells) shells.onChange = () => { previous?.(); tui.requestRender(); };
278
+ // Live streams notify far faster than a transcript view needs to repaint; ~10 frames a second reads as live.
279
+ let repaint: NodeJS.Timeout | undefined;
280
+ const repaintSoon = () => {
281
+ if (repaint || closed) return;
282
+ repaint = setTimeout(() => { repaint = undefined; if (!closed) tui.requestRender(); }, 100);
283
+ repaint.unref?.();
284
+ };
285
+ const unsubscribe = sources.subagents.subscribe(repaintSoon);
286
+ if (shells) shells.onChange = () => { previous?.(); repaintSoon(); };
279
287
  const cleanup = () => {
280
288
  if (closed) return;
281
289
  closed = true;
282
290
  clearInterval(timer);
283
291
  timer = undefined;
292
+ clearTimeout(repaint);
293
+ repaint = undefined;
284
294
  unsubscribe();
285
295
  if (shells) shells.onChange = previous;
286
296
  };
@@ -348,9 +358,14 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
348
358
  };
349
359
  const stopSelected = () => {
350
360
  const item = selected();
351
- if (!item || item.kind === "role" || (item.status !== "running" && item.status !== "pending")) return;
361
+ if (!item || item.kind === "role") return;
362
+ const subagentLive = item.kind === "subagent" && !["done", "failed", "stopped"].includes(item.record.run.state);
363
+ const shellLive = item.kind === "shell" && item.status === "running";
364
+ if (!subagentLive && !shellLive) return;
352
365
  try {
353
- if (item.kind === "subagent") sources.subagents.stop(item.id);
366
+ if (item.kind === "subagent") void sources.subagents.stop(item.id).catch((error: unknown) => {
367
+ ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error");
368
+ });
354
369
  else shells?.kill(item.id);
355
370
  } catch (error) { ctx.ui.notify("pi-jar: " + (error instanceof Error ? error.message : String(error)), "error"); }
356
371
  };
@@ -445,7 +460,8 @@ export async function openActivityView(ctx: ExtensionContext, sources: ActivityS
445
460
  // Footer: the two actions, the steering input for subagents, then key hints.
446
461
  const actions = optionList(theme, [item?.kind === "shell" ? "Kill the selected shell" : item?.kind === "role" ? "Stop (teammates from other extensions are read-only)" : "Stop the selected subagent",
447
462
  "Follow the latest output"], -1, ACTIONS.map((action) => action.key));
448
- const stoppable = item?.kind === "subagent" ? item.status === "running" || item.status === "pending" : item?.kind === "shell" && item.status === "running";
463
+ const stoppable = item?.kind === "subagent" ? !["done", "failed", "stopped"].includes(item.record.run.state)
464
+ : item?.kind === "shell" && item.status === "running";
449
465
  if (!stoppable) actions[0] = fg("dim", stripTerminalSequences(actions[0]!));
450
466
  const footer = [...actions];
451
467
  if (subagent) {