pi-jar 0.1.3 → 0.1.5

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
- | **Tasks & questions** | A Claude-style `jar_todo` checklist the agent maintains itself, and `jar_ask` structured questions with options, multi-select and free-form answers. |
26
- | **Change review** | Every file the agent edits is remembered as it was; `/diff` (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>) shows changed files next to a colored diff, with accept or revert per file or all at once. |
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
+ | **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 (current tool, tool count, elapsed); click it to open its transcript and report, or stop it. |
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,6 +217,26 @@ 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
 
220
+ ### Role fallback models
221
+
222
+ Every role can have an ordered fallback chain. The primary remains configured with `/roles set ROLE`:
223
+
224
+ ```text
225
+ /roles fallback advisor openai/gpt-5:high @smol
226
+ /roles fallback advisor
227
+ /roles fallback advisor clear
228
+ ```
229
+
230
+ Add `--project` when saving or clearing to override the global chain for this project. Configuration is stored in `pi-jar-roles.json` alongside `roles`:
231
+
232
+ ```json
233
+ "fallbacks": { "advisor": ["openai/gpt-5:high", "@smol"] }
234
+ ```
235
+
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.
237
+
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.
239
+
211
240
  ## Usage and context
212
241
 
213
242
  `/usage` and `/context` open one tabbed panel (<kbd>Tab</kbd> switches, <kbd>Esc</kbd> closes):
@@ -217,12 +246,22 @@ The advisor is a second model (the `advisor` role; the current model if unassign
217
246
 
218
247
  ## Tasks, questions and history
219
248
 
220
- - **`jar_todo`** — a Claude-style task list the agent keeps for any multi-step request. It writes the full list at once; 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. The live checklist above the composer shows `✔` struck-through done tasks, a bold `◼` current task and `☐` pending ones; 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.
250
+ ```json
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" }] }
254
+ ```
255
+ Use `parent` with `append` to add a genuinely new subtask to active work; `start`, `done`, `open`, `edit`, `list`, `add` and `delete` remain available.
221
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.
222
- - **`/diff`** — review what the agent changed since its 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. Only `edit`/`write` changes inside the project are tracked; shell-made changes are not.
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.
223
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.
224
- - **`jar_delegate`** — up to four subagents run in parallel as separate one-shot Pi processes with a fresh context, on a role (default `task`, then `default`, then the current model). They are read-only (`read`, `grep`, `find`, `ls`) unless `write: true`; subagents cannot delegate further; aborting the turn stops them. Each has a live footer row; the activity view (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>A</kbd>, `/jar activity`, a click on the row) shows its task, live transcript and report, and `x` stops just that one. The tool result lists every report.
225
- - **Activity view** — one split view 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). Finished runs stay listed for review.
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 high-level task/checklist progress, current activity, changed files, remaining work, workspace and last report; `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.
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.
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). Agents use it for narrow Q/A, not as another transcript.
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.
226
265
  - **`/jar history`** — separate, read-only timeline of the active branch (paging, search, expandable details). Pi's native transcript is untouched.
227
266
  - 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.
228
267
 
@@ -232,7 +271,7 @@ The advisor is a second model (the `advisor` role; the current model if unassign
232
271
 
233
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).
234
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).
235
- - **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.
236
275
 
237
276
  pi-jar preferences are saved in `pi-jar-settings.json` in Pi's agent directory; the Pi tab writes Pi's own settings.
238
277
 
@@ -261,7 +300,10 @@ pi-jar/
261
300
  │ ├── split-view.ts shared two-pane frame
262
301
  │ ├── changes.ts change tracker and line diff; diff-view.ts is /diff
263
302
  │ ├── shells.ts background shells and jar_shell
264
- │ ├── 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
265
307
  │ ├── activity-view.ts subagent/shell details view
266
308
  │ ├── icons.ts unicode / nerd / ascii glyph sets
267
309
  │ ├── attachments.ts image chips; prompt-search.ts is prompt history
@@ -58,7 +58,7 @@ Then check:
58
58
  - the composer: typing `/pl` shows autocomplete below the frame; long drafts grow before scrolling; Ember changes mood while dialogs are open;
59
59
  - `/jar settings`: Tab through Appearance → Footer → Pi; cycle Unicode/Nerd Font/ASCII icons and toggle mouse mode in the temporary agent dir;
60
60
  - `/jar sessions`: search, move between rows, inspect the selected prompt/goal/plan, then resume; the footer session name or `sessions` indicator opens the same picker;
61
- - `/jar activity`: select a subagent or background shell, scroll its live transcript/output, pause and resume follow, and stop/kill only the selected run;
61
+ - `/jar activity`: select a subagent or background shell, scroll its live transcript/output, pause and resume follow, `Tab` into a subagent's transcript and expand/collapse entries, steer it with `s`, and stop/kill only the selected run;
62
62
  - `/roles`: the split manager, `n` new role, `a` alias, `s` scope;
63
63
  - `/plan`: the status shows `◆ PLAN · read-only` and a directory appears under `$TMPDIR/pi-jar/plans/`.
64
64
 
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,12 +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.
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.
117
127
 
118
128
  ## Background shells
119
129
 
@@ -129,11 +139,26 @@ watch matches or process exits → pi-jar.shell message → agent wakes (o
129
139
 
130
140
  ## Subagents
131
141
 
132
- - `jar_delegate` takes 1–4 tasks. Each runs `pi --mode json -p --no-session --model <role model> [--thinking <effort>] [--tools read,grep,find,ls] "<task>"` in the project directory, with `PI_JAR_CHILD=1`.
133
- - The model comes from the task's role (default `task`), falling back to `default` and then the current model.
134
- - Progress (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.
135
- - The activity view reads a live transcript per subagent (each tool call with its command, path or pattern, then the assistant's text; last 200 lines). It lives only in memory with the 8 most recent finished runs; the tool result saved in the session carries the reports, not the transcript.
136
- - 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`). The result lists every report with its role, model and outcome. 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: task, state, model/role/mode, checklist progress, current activity, changed files, remaining checklist leaves, workspace and last report. `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
+ - 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.
147
+ - `jar_discuss` gives moderator and children a session-scoped structured paper. `ask` appends a question with stable id/from/to fields, `answer` references the question id, and `list` reads recent entries. Text is capped at 1600 characters per entry, at most 64 entries are retained, and the serialized UTF-8 JSON paper is strictly bounded to 64 KiB with oldest-entry eviction and atomic rewrites. It is intentionally for terse inter-agent Q/A rather than another transcript.
148
+ - 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.
149
+ - 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.
150
+ - The deprecated `write: true` mode remains a one-shot shared-workspace compatibility path. New delegated implementation should use `mode: "worktree"`.
151
+
152
+ ### Exceptional agentic democracy
153
+
154
+ `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.
155
+
156
+ - 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.
157
+ - 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. Voters are retained for follow-up.
158
+ - Each voter investigates independently and returns one nonce-bound option ID with a short evidence-based rationale. 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.
159
+ - 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.
160
+ - 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.
161
+ - 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.
137
162
 
138
163
  ## Sessions and prompt history
139
164
 
@@ -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";
@@ -30,11 +32,13 @@ import { createDemoRoles } from "../src/roles.ts";
30
32
  import { ACTIVE_STATES, cleanText, collectStatuses, type JarRole } from "../src/status.ts";
31
33
  import { manageTasks } from "../src/tasks-ui.ts";
32
34
  import { registerTaskTool, todoRow } from "../src/task-tool.ts";
33
- import { TASK_ENTRY, TodoStore } from "../src/tasks.ts";
35
+ 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, 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"
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();
@@ -62,6 +100,17 @@ export default function piJar(pi: ExtensionAPI): void {
62
100
  let enabled = visualSettings.ui;
63
101
  let disposeFooter: (() => void) | undefined;
64
102
  let footerTui: { requestRender(): void } | undefined;
103
+ /** Footer facts that walk the session to compute; refreshed on the events that change them, never per frame. */
104
+ let footerContext = "ctx ?";
105
+ let footerSessionName: string | undefined;
106
+ const sampleFooter = (ctx: ExtensionContext) => {
107
+ try {
108
+ const usage = ctx.getContextUsage();
109
+ footerContext = usage?.percent == null || !Number.isFinite(usage.percent) ? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
110
+ } catch { footerContext = "ctx ?"; }
111
+ try { footerSessionName = ctx.sessionManager?.getSessionName?.(); } catch { footerSessionName = undefined; }
112
+ footerTui?.requestRender();
113
+ };
65
114
  let quotaCache: QuotaCache | undefined;
66
115
  let cost = 0;
67
116
  let welcomeInterval: ReturnType<typeof setInterval> | undefined;
@@ -83,6 +132,8 @@ export default function piJar(pi: ExtensionAPI): void {
83
132
  let workingIndicatorKey = "";
84
133
  let openSettings: (ctx: ExtensionContext) => Promise<void> = async () => {};
85
134
  let settingsOpen = false;
135
+ let discussionFile: string | undefined = process.env[DISCUSSION_FILE_ENV];
136
+ registerDiscussionTool(pi, () => discussionFile, () => process.env[SUBAGENT_NAME_ENV] ?? "moderator");
86
137
 
87
138
  // A finished list stays visible (all struck through) until the user's next prompt, like Claude.
88
139
  let todosAcknowledged = false;
@@ -93,27 +144,37 @@ export default function piJar(pi: ExtensionAPI): void {
93
144
  if (open.length) todosAcknowledged = false;
94
145
  const visible = enabled && items.length > 0 && (open.length > 0 || !todosAcknowledged);
95
146
  try {
96
- ctx.ui.setWidget("pi-jar.todos", !visible ? undefined : (_tui, theme) => ({
97
- invalidate() {},
98
- render(width: number) {
99
- const colors = ctx.ui.theme ?? theme;
100
- const all = todos?.all() ?? [];
101
- const done = all.filter((item) => item.done).length;
102
- // Show a window of up to 8 rows that keeps the running (or next open) task in view.
103
- const focus = Math.max(0, all.findIndex((item) => item.status === "in_progress") >= 0
104
- ? all.findIndex((item) => item.status === "in_progress") : all.findIndex((item) => !item.done));
105
- const start = Math.max(0, Math.min(focus - 2, all.length - 8));
106
- const shown = all.slice(start, start + 8);
107
- const rows = [
108
- colors.fg("accent", "Tasks") + colors.fg("dim", ` · ${done}/${all.length} done` + (done === all.length ? " · all complete" : "") + " · /jar tasks"),
109
- ...(start > 0 ? [colors.fg("dim", ` … ${start} earlier`)] : []),
110
- ...shown.map((item) => todoRow(item, (color, text) => colors.fg(color, text), (text) => colors.bold(text))),
111
- ...(start + shown.length < all.length ? [colors.fg("dim", ` … +${all.length - start - shown.length} more`)] : [])
112
- ];
113
- // Pi inserts a spacer before widgets, but not between widgets and the composer.
114
- return [...rows.map((line) => truncateToWidth(line, Math.max(0, width))), truncateToWidth(" ", Math.max(0, width))];
115
- }
116
- }));
147
+ // Rows are rebuilt only when the list changes (every change reinstalls the widget), the width or the theme.
148
+ ctx.ui.setWidget("pi-jar.todos", !visible ? undefined : (_tui, theme) => {
149
+ let memo: { width: number; sample: string; lines: string[] } | undefined;
150
+ return {
151
+ invalidate() { memo = undefined; },
152
+ render(width: number) {
153
+ const colors = ctx.ui.theme ?? theme;
154
+ const sample = colors.fg("accent", "·") + colors.fg("dim", "·");
155
+ if (memo && memo.width === width && memo.sample === sample) return memo.lines.slice();
156
+ const all = todos?.all() ?? [];
157
+ const { done, total } = todoTotals(all);
158
+ const parents = new Set<string>();
159
+ for (const item of all) if (item.parentId) parents.add(item.parentId);
160
+ // Show a window of up to 8 rows that keeps the running leaf (or next open one) in view.
161
+ const running = all.findIndex((item) => item.status === "in_progress" && !parents.has(item.id));
162
+ const focus = Math.max(0, running >= 0 ? running : all.findIndex((item) => !item.done && !parents.has(item.id)));
163
+ const start = Math.max(0, Math.min(focus - 2, all.length - 8));
164
+ const shown = all.slice(start, start + 8);
165
+ const rows = [
166
+ colors.fg("accent", "Tasks") + colors.fg("dim", ` · ${done}/${total} done` + (done === total ? " · all complete" : "") + " · /jar tasks"),
167
+ ...(start > 0 ? [colors.fg("dim", ` … ${start} earlier`)] : []),
168
+ ...shown.map((item) => todoRow(item, (color, text) => colors.fg(color, text), (text) => colors.bold(text), parents.has(item.id) ? todoProgress(all, item.id) : undefined)),
169
+ ...(start + shown.length < all.length ? [colors.fg("dim", ` … +${all.length - start - shown.length} more`)] : [])
170
+ ];
171
+ // Pi inserts a spacer before widgets, but not between widgets and the composer.
172
+ const lines = [...rows.map((line) => truncateToWidth(line, Math.max(0, width))), truncateToWidth(" ", Math.max(0, width))];
173
+ memo = { width, sample, lines };
174
+ return lines.slice();
175
+ }
176
+ };
177
+ });
117
178
  } catch { /* Optional widget; task data remains available via /jar tasks and jar_todo. */ }
118
179
  };
119
180
  registerTaskTool(pi, () => todos, (ctx) => updateTaskWidget(ctx));
@@ -142,14 +203,15 @@ export default function piJar(pi: ExtensionAPI): void {
142
203
  /** Finished work stays visible briefly so a quick run is not a flicker. */
143
204
  const ACTIVITY_LINGER_MS = 5000;
144
205
  const subagentState = (run: DelegateRun): ActivityState => run.state === "working" ? "running" : run.state === "queued" ? "queued"
145
- : run.state === "done" ? "done" : run.error === "stopped" ? "stopped" : "failed";
206
+ : run.state === "idle" ? "idle" : run.state === "paused" ? "paused" : run.state === "stopped" ? "stopped"
207
+ : run.state === "done" ? "done" : "failed";
146
208
  const shellActivityState = (job: Omit<ShellJob, "lines">): ActivityState => job.status === "running" ? "running"
147
209
  : job.status === "killed" ? "stopped" : job.status === "exited" && job.exitCode === 0 ? "done" : "failed";
148
210
  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`; };
149
211
  const footerActivity = (now: number): FooterActivity[] => {
150
212
  const rows: FooterActivity[] = [];
151
213
  for (const { key, run } of subagents.records()) {
152
- if (run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
214
+ if ((run.state === "done" || run.state === "failed" || run.state === "stopped") && run.endedAt && now - run.endedAt > ACTIVITY_LINGER_MS) continue;
153
215
  rows.push({ kind: "subagent", id: key, name: run.name, state: subagentState(run),
154
216
  detail: run.error ?? run.activity ?? cleanText(run.task, 120),
155
217
  stats: [run.tools ? `${run.tools} tools` : "", run.startedAt ? seconds((run.endedAt ?? now) - run.startedAt) : ""].filter(Boolean).join(" · ") });
@@ -206,17 +268,21 @@ export default function piJar(pi: ExtensionAPI): void {
206
268
  registerAdvisor(pi, modelRoles, { enabled: () => visualSettings.advisor, gates: () => visualSettings.advisorGates, usage: sideUsage });
207
269
  registerInfoPanels(pi, { side: sideUsage, quotaEnabled: () => quotaCache?.enabled ?? false,
208
270
  quota: (ctx) => quotaCache?.get(ctx.model?.provider, welcomeStatuses(), Date.now()) });
209
- modelRoles.register((ctx) => openRolesUi(ctx, modelRoles));
210
- registerDelegate(pi, modelRoles, subagents);
271
+ modelRoles.register((ctx) => openRolesUi(ctx, modelRoles), { activateDefault: () => !delegatedChild });
272
+ let delegateController: DelegateController | undefined;
273
+ registerDelegate(pi, modelRoles, subagents, { changes: () => changes, changed: refreshChanges, discussionFile: () => discussionFile,
274
+ getMaxSubagents: () => visualSettings.maxSubagents, onController: (controller) => { delegateController = controller; } });
275
+ if (!delegatedChild) registerDemocracy(pi, () => delegateController, () => visualSettings.maxSubagents);
211
276
  const planMode = new PlanMode(pi, () => todos, modelRoles, updateTaskWidget);
212
- planMode.register();
277
+ if (!delegatedChild) planMode.register();
213
278
  const goalLoop = new GoalLoop(pi, {
214
279
  goals: () => goals, todos: () => todos, roles: modelRoles, planActive: () => planMode.isEnabled(),
215
280
  maxRounds: () => visualSettings.goalRounds,
281
+ subagentMode: (agent) => subagents.resolve(agent)?.run.mode,
216
282
  changed: (ctx) => { footerTui?.requestRender(); welcomeTui?.requestRender(); updateTaskWidget(ctx); },
217
283
  completed: () => composer.flash("complete", 4000)
218
284
  });
219
- goalLoop.register();
285
+ if (!delegatedChild) goalLoop.register();
220
286
  planMode.setOnEnter((ctx) => goalLoop.pause(ctx, "plan mode is on"));
221
287
  const suggestions = new SuggestionState();
222
288
  composer.attachSuggestions(suggestions);
@@ -420,8 +486,6 @@ export default function piJar(pi: ExtensionAPI): void {
420
486
  let expiryAt: number | undefined;
421
487
  let memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
422
488
  let memorySampledAt = 0;
423
- let context = "ctx ?";
424
- let contextSampledAt = 0;
425
489
  let disposed = false;
426
490
  let hits: FooterHit[] = [];
427
491
  let pressed: { target: FooterTarget; at: number } | undefined;
@@ -473,13 +537,8 @@ export default function piJar(pi: ExtensionAPI): void {
473
537
  // Do not schedule cosmetic footer repaints. Role state still updates on Pi's native
474
538
  // stream/tool renders, which keeps long transcripts responsive.
475
539
  const active = animations && footerSettings.roles && roles.some((role) => ACTIVE_STATES.has(role.state));
476
- if (active) frame++;
477
- if (now - contextSampledAt >= 1000) {
478
- const usage = ctx.getContextUsage();
479
- context = usage?.percent == null || !Number.isFinite(usage.percent)
480
- ? "ctx ?" : `ctx ${Math.round(usage.percent)}%`;
481
- contextSampledAt = now;
482
- }
540
+ // Role glyphs advance with time, not with the render rate, so the footer memo still hits between steps.
541
+ if (active) frame = Math.floor(now / 250);
483
542
  if (now - memorySampledAt >= 5000) {
484
543
  memory = `ram ${Math.round(process.memoryUsage.rss() / 1048576)} MiB`;
485
544
  memorySampledAt = now;
@@ -487,9 +546,9 @@ export default function piJar(pi: ExtensionAPI): void {
487
546
  const quota = footerSettings.quota ? quotaCache?.get(ctx.model?.provider, statuses, now) : undefined;
488
547
  const layout = renderFooterLayout({
489
548
  model: ctx.model?.id ?? "no-model", effort: ctx.model?.reasoning === false ? "off" : (pi.getThinkingLevel?.() ?? "off"),
490
- sessionName: ctx.sessionManager?.getSessionName?.(),
549
+ sessionName: footerSessionName,
491
550
  cwd: ctx.cwd, settings: footerSettings, branch: footerData.getGitBranch(),
492
- context, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
551
+ context: footerContext, goal: goalLoop.progress(), chips: footerChips(), activity: footerActivity(now),
493
552
  memory, cost: formatCost(cost), quota, roles, extras: live.extras,
494
553
  demo, animations, frame, motionBudget: ctx.isIdle() ? 2 : 1
495
554
  }, width, ctx.ui.theme ?? theme);
@@ -589,13 +648,22 @@ export default function piJar(pi: ExtensionAPI): void {
589
648
  changeCount = 0;
590
649
  shells?.dispose();
591
650
  subagents.clear();
651
+ if (!delegatedChild) {
652
+ disposeDiscussionPaper(discussionFile);
653
+ discussionFile = createDiscussionPaper();
654
+ }
592
655
  shells = new ShellManager(onShellEvent);
593
656
  shells.onChange = () => footerTui?.requestRender();
594
657
  goals = new GoalStore((entry) => pi.appendEntry(GOAL_ENTRY, entry));
595
658
  let branch: readonly unknown[] = [];
596
659
  try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
597
- restoreTodos(ctx, branch);
598
- restoreGoal(ctx, branch);
660
+ if (delegatedChild) {
661
+ todos.restore([]);
662
+ goals.restore([]);
663
+ } else {
664
+ restoreTodos(ctx, branch);
665
+ restoreGoal(ctx, branch);
666
+ }
599
667
  // Quota is enabled per session; no credentials or consent are persisted.
600
668
  quotaCache = new QuotaCache(
601
669
  (provider: QuotaProvider, signal) => fetchQuota(provider, (id) => ctx.modelRegistry.getProviderAuth(id), signal),
@@ -609,6 +677,7 @@ export default function piJar(pi: ExtensionAPI): void {
609
677
  quotaDisabledByUser = false;
610
678
  updateCost(ctx);
611
679
  installUi(ctx);
680
+ sampleFooter(ctx);
612
681
  composer.setMascot(visualSettings.mascot);
613
682
  if (visualSettings.composer && enabled) composer.enable(ctx);
614
683
  suggest.sync();
@@ -626,7 +695,7 @@ export default function piJar(pi: ExtensionAPI): void {
626
695
  // A fresh session is not empty: Pi records the startup thinking level/model as entries.
627
696
  const resumed = branch.some((entry) => !(typeof entry === "object" && entry !== null && "type" in entry
628
697
  && typeof entry.type === "string" && SESSION_SETUP_ENTRIES.has(entry.type)));
629
- if (!resumed) showWelcome(ctx);
698
+ if (!delegatedChild && !resumed) showWelcome(ctx);
630
699
  });
631
700
  // Any real prompt dismisses the welcome. Slash commands bypass `input`, so `agent_start` covers them.
632
701
  const dismissWelcome = (ctx: ExtensionContext) => {
@@ -649,6 +718,8 @@ export default function piJar(pi: ExtensionAPI): void {
649
718
  pi.on("agent_start", (_event, ctx) => { dismissWelcome(ctx); working.start(); applyWorking(ctx); });
650
719
  pi.on("turn_start", (_event, ctx) => { working.start(); applyWorking(ctx); });
651
720
  pi.on("message_end", (event, ctx) => {
721
+ // Every message (user, assistant, tool result) moves the context window.
722
+ sampleFooter(ctx);
652
723
  if (event.message.role === "assistant") {
653
724
  working.reportOutputTokens(event.message.usage?.output ?? 0);
654
725
  addMessageCost(event.message);
@@ -667,15 +738,22 @@ export default function piJar(pi: ExtensionAPI): void {
667
738
  pi.on("session_tree", (_event, ctx) => {
668
739
  let branch: readonly unknown[] = [];
669
740
  try { branch = sessionBranch(ctx); } catch { /* keep empty */ }
670
- restoreTodos(ctx, branch);
671
- restoreGoal(ctx, branch);
741
+ if (!delegatedChild) {
742
+ restoreTodos(ctx, branch);
743
+ restoreGoal(ctx, branch);
744
+ }
672
745
  updateCost(ctx);
673
746
  composer.refreshSession(ctx);
747
+ sampleFooter(ctx);
748
+ });
749
+ pi.on("session_compact", (_event, ctx) => {
750
+ if (!delegatedChild) { restoreTodos(ctx); restoreGoal(ctx); }
751
+ updateCost(ctx);
752
+ sampleFooter(ctx);
674
753
  });
675
- pi.on("session_compact", (_event, ctx) => { restoreTodos(ctx); restoreGoal(ctx); updateCost(ctx); });
676
- pi.on("model_select", (_event, _ctx) => footerTui?.requestRender());
754
+ pi.on("model_select", (_event, ctx) => sampleFooter(ctx));
677
755
  pi.on("thinking_level_select", (_event, _ctx) => footerTui?.requestRender());
678
- pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); footerTui?.requestRender(); });
756
+ pi.on("session_info_changed", (_event, ctx) => { composer.refreshSession(ctx); sampleFooter(ctx); });
679
757
  pi.on("session_shutdown", (_event, ctx) => {
680
758
  clearSessionBranchCache(ctx);
681
759
  stopWelcome(ctx);
@@ -685,6 +763,7 @@ export default function piJar(pi: ExtensionAPI): void {
685
763
  try { if (ctx.hasUI && ctx.mode === "tui") { ctx.ui.setWorkingMessage?.(); ctx.ui.setWorkingIndicator(); ctx.ui.setWidget("pi-jar.todos", undefined); } } catch {}
686
764
  quotaCache?.stop();
687
765
  quotaCache = undefined;
766
+ if (!delegatedChild) { disposeDiscussionPaper(discussionFile); discussionFile = undefined; }
688
767
  todos = undefined;
689
768
  changes = undefined;
690
769
  changeCount = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jar",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",