gentle-pi 3.2.1 → 3.3.0

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.
Files changed (71) hide show
  1. package/docs/gentle-shell.md +27 -14
  2. package/docs/readme-reference.md +41 -7
  3. package/docs/review-integration.md +22 -17
  4. package/extensions/gentle-agents.ts +85 -17
  5. package/extensions/gentle-ai.ts +123 -21
  6. package/extensions/gentle-shell.ts +379 -31
  7. package/extensions/gentle-todo.ts +19 -1
  8. package/lib/agents-view.ts +41 -14
  9. package/lib/agents-widget.ts +84 -13
  10. package/lib/command-palette-catalog.ts +1 -0
  11. package/lib/double-esc-cancel-policy.ts +138 -0
  12. package/lib/inprocess-reviewer.ts +260 -0
  13. package/lib/native-review-cli.ts +14 -0
  14. package/lib/odd-runtime-delegation-gate.ts +88 -0
  15. package/lib/review-host-relay.ts +246 -171
  16. package/lib/review-integration-v2.ts +110 -26
  17. package/lib/shell-bar.ts +150 -75
  18. package/lib/shell-card.ts +19 -9
  19. package/lib/shell-changes-view.ts +43 -5
  20. package/lib/shell-changes.ts +92 -5
  21. package/lib/shell-hover.ts +39 -0
  22. package/lib/shell-prompt.ts +10 -1
  23. package/lib/shell-sidebar-layout.ts +111 -15
  24. package/lib/shell-sidebar.ts +16 -0
  25. package/lib/shell-todo.ts +7 -1
  26. package/lib/shell-usage-view.ts +98 -10
  27. package/package.json +1 -1
  28. package/runtime/native-review-cli.mjs +14 -0
  29. package/runtime/review-integration-v2.mjs +110 -26
  30. package/scripts/gentle-ai-installer.mjs +10 -10
  31. package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
  32. package/scripts/verify-package-files.mjs +3 -3
  33. package/tests/agents-grouping.test.ts +75 -18
  34. package/tests/agents-view.test.ts +28 -18
  35. package/tests/agents-widget.test.ts +100 -12
  36. package/tests/command-palette.test.ts +1 -0
  37. package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
  38. package/tests/double-esc-cancel-policy.test.ts +194 -0
  39. package/tests/gentle-agents.test.ts +528 -5
  40. package/tests/gentle-ai-binary.test.ts +1 -1
  41. package/tests/gentle-ai-installer.test.ts +47 -47
  42. package/tests/gentle-ai.test.ts +69 -5
  43. package/tests/gentle-shell.test.ts +795 -23
  44. package/tests/gentle-todo.test.ts +17 -4
  45. package/tests/inprocess-reviewer.test.ts +368 -0
  46. package/tests/maintainer/provider-relay.maintest.ts +101 -143
  47. package/tests/native-review-capability-contract.test.ts +19 -1
  48. package/tests/odd-runtime-delegation-gate.test.ts +212 -0
  49. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  50. package/tests/package-manifest.test.ts +6 -6
  51. package/tests/review-host-relay-routing.test.ts +77 -0
  52. package/tests/review-host-relay.test.ts +277 -300
  53. package/tests/review-integration-v2-forward.test.ts +61 -0
  54. package/tests/review-integration-v2.test.ts +116 -1
  55. package/tests/review-relay-transport-agent.test.ts +22 -24
  56. package/tests/runtime-harness.mjs +11 -0
  57. package/tests/session-changes-shell.test.ts +27 -0
  58. package/tests/session-worktree-registry.test.ts +41 -0
  59. package/tests/shell-bar.test.ts +192 -124
  60. package/tests/shell-card.test.ts +5 -3
  61. package/tests/shell-changes-view.test.ts +47 -0
  62. package/tests/shell-changes.test.ts +177 -0
  63. package/tests/shell-hover.test.ts +19 -0
  64. package/tests/shell-prompt.test.ts +20 -0
  65. package/tests/shell-sidebar-fullscreen.test.ts +59 -0
  66. package/tests/shell-sidebar-layout.test.ts +243 -5
  67. package/tests/shell-sidebar.test.ts +25 -1
  68. package/tests/shell-todo.test.ts +36 -0
  69. package/tests/shell-usage-view.test.ts +120 -1
  70. package/lib/opaque-pi-reviewer-adapter.ts +0 -404
  71. package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
@@ -15,19 +15,28 @@ The [v2.6.0 release](https://github.com/Gentleman-Programming/gentle-pi/releases
15
15
  - The Agents List and Details views preserve the orchestrator/session hierarchy and completion, abort, and lost-exit history. Parent-child queries and notifications have an explicit handoff path, while model, effort, and usage stay observable per task.
16
16
  - Named `/gentle:profiles` atomically route the orchestrator separately from packaged and review roles; see the [technical reference](readme-reference.md#agent-model-profiles) for the profile model.
17
17
 
18
- The source checkout currently prepares `gentle-pi` `3.2.1` with a package-local Gentle AI `v3.2.1` pin; this is not a claim that `3.2.1` is published.
18
+ The source checkout currently prepares `gentle-pi` `3.3.0` with a package-local Gentle AI `v3.4.0` pin; this is not a claim that `3.3.0` is published.
19
19
 
20
20
  ## Shell interactions and runtime behavior
21
21
 
22
- Gentle Shell is the Pi workspace experience provided by the `gentle-pi` package. It follows the Gentle themes: one border language, champagne titles, rose for whatever is alive.
22
+ Gentle Shell is the Pi workspace experience provided by the `gentle-pi` package. It follows the Gentle themes: one border language, rose for whatever is alive.
23
23
 
24
- In fullscreen at 140 columns or wider, the right sidebar scrolls **✿ Gentle-Pi ✿ → Status → Changes → Agents → TODO** together. The one-line heading is horizontally centered within the usable rail width, with pink flowers and normal white text in the Gentleman themes. Colors follow the active theme; no artwork scaling or custom fonts are used. Narrow/mobile terminals and regular mode retain bottom widgets without the sidebar heading. The original rose and text logo remain in the main chat startup intro.
24
+ ### Fullscreen layout
25
25
 
26
- The rail reuses its last frame until something it paints changes, so silent frames stay cheap and live session state still lands on the next frame: a model switch, a new thinking level, context growth, session cost, session name and extension statuses all refresh the Status card without a redraw of the rest of the sidebar.
26
+ At 140 columns or wider, fullscreen splits into a live header row over a transcript-and-rail split, both driven by [`lib/shell-sidebar-layout.ts`](../lib/shell-sidebar-layout.ts):
27
27
 
28
- The sidebar Status card also shows `Profile` in its Model section when the profiles store has a valid active marker. It follows profile changes on the next render. Missing, unreadable, or invalid stores leave the line hidden. The compact bottom bar is unchanged.
28
+ ```text
29
+ ✿ Gentle Shell ⟡ ~/work/gentle-pi main ⟡ gpt-5.5 · medium · team ctx ▰▰▰▰▱▱▱▱ 45% ⟡ $9.49 sub
30
+ ```
29
31
 
30
- The status bar replaces pi's three-line footer with a single line of segments:
32
+ - The header is one row, always visible, and carries only what changes every frame: session identity on the left (brand, cwd, branch, dirty count, model · effort · profile) and the two live counters right-aligned (the context gauge and session cost). It never shows the working/thinking state or extension statuses — those stay in the prompt title and the compact bar. When the terminal is too narrow for everything, segments give way in a fixed order profile, then effort, then the whole cwd/branch/dirty group — before the counters are touched; below that, only the brand survives, and below that the header renders nothing.
33
+ - The right rail scrolls **Status → Changes → TODO**, each an event-driven card that only repaints when its own state changes: a model switch or a cost tick refreshes the header, not the rail. Every card (sidebar or not) paints the same rose frame — the rounded border in the theme's plain border role, the title in the accent role — the look every `CARD_TONE.INFO` card in Gentle Shell uses (warning/error/success cards keep their own tone colors).
34
+ - The Status card carries only what an explicit event refreshes: Project (cwd, branch, session name, active profile), Changes, and Integrations (other extensions' statuses). Model, effort, context, cost, and the per-model usage table live in the header instead — the header ticks every frame, so duplicating them in a card would just make that card repaint every frame too.
35
+ - Gentle Agents is not part of the rail in any mode: its one card stays above the editor, where it already lived, with fixed right-aligned columns for `model · effort`, tokens, cost, and elapsed, each sized to the widest value among the shown tasks — so the numbers line up vertically even when one row's values are much shorter than another's. A queued task fills only the elapsed column with the word `queued`, leaving the other columns blank rather than overwriting the row.
36
+ - The sidebar reuses its last frame until something it paints changes, so silent frames stay cheap; a per-section cache means one card's changing digest (or the header's) never forces an unrelated card to redraw.
37
+ - Narrow terminals and regular mode keep the compact bottom bar and the above-editor Agents widget, with no header row and no sidebar.
38
+
39
+ Below 140 columns, or in regular mode, the compact bottom bar replaces pi's three-line footer with a single line of segments instead:
31
40
 
32
41
  ```text
33
42
  ✿ gentle shell ⟡ ~/work/gentle-pi main ⟡ gpt-5.5 · medium ⟡ ctx ▰▰▰▰▱▱▱▱ 45% ⟡ $9.49 sub ⟡ MCP: 3 servers enabled Release notes
@@ -63,6 +72,8 @@ Changes shows **captured write/edit operations from this agent session and its o
63
72
  - Diffs compare the content observed before the agent's first captured operation with its latest captured result, not with HEAD. Consecutive agent edits combine; an agent revert removes its net change.
64
73
  - Edits from your editor or other sessions do not update these captured diffs. If an external or unobserved edit breaks continuity before the next agent operation on the same file, the file is marked **diff unavailable**, rather than mixing ownership.
65
74
  - Only worktrees in the coordinating session's Git clone are accepted. Child evidence is accepted only from an owned task with paired successful write/edit events and a matching target.
75
+ - A changed file's worktree is resolved from its own directory upward (`git rev-parse --show-toplevel` starting there, never from an ancestor's cwd), so a repository nested inside another — a project scaffolded inside a personal workspace clone, say — is always attributed to its own, inner repository, never the outer one.
76
+ - When changes span more than one worktree, each tree header shows that root's own branch name, `no commits yet` for an unborn branch, or `detached` only for a real detached HEAD. The label is read from Git's HEAD once per root while the overlay is open (`symbolic-ref` and `rev-parse --verify`); the overlay still never runs `status`, `diff` or a worktree scan on your behalf.
66
77
  - **Coverage is deliberately limited to write/edit tools.** Shell commands, custom mutation tools, failed/interrupted outcomes and children without the capture extension provide no attributed diff. A missing row does not mean the repository is clean or that no other changes occurred.
67
78
 
68
79
  ### Bounds and session lifetime
@@ -78,7 +89,7 @@ The separate `session_worktree_register` tool still registers canonical same-clo
78
89
  `/gentle:changes` or `alt+g` opens the two-pane viewer. Worktrees are accordion groups on the left; selecting a file displays its captured diff on the right.
79
90
 
80
91
  - `j`/`k` or arrows navigate. On a group, Enter, Space or Right expands it; Left returns to its parent or collapses it. `ctrl+j/k` or Page Up/Down scroll the diff; Escape or `q` closes.
81
- - Fullscreen left-click selects files; mouse wheels scroll the file list and diff independently. Hover does not open files.
92
+ - Fullscreen left-click selects files; mouse wheels scroll the file list and diff independently. Hovering an unselected row (worktree or file, in either pane's list) paints it in the same shared hover role every clickable surface in the shell uses; it never opens or selects the file, and never overrides the already-selected row's own role.
82
93
  - Opening, pressing `r`, and the overlay's refresh cadence consult only the captured session model. They never rescan Git or load the current file contents. Same-line-count edits invalidate the diff preview by content revision.
83
94
  - On a file, `o` or Enter opens the actual current file in `$VISUAL` or `$EDITOR`, with its worktree as cwd. Edits made there are external and are not attributed to the agent.
84
95
  - `GENTLE_PI_SHELL_CHANGES_KEY` rebinds the shortcut; `off` disables it. `GENTLE_PI_SHELL_CHANGES_POLL_MS` controls only the open overlay's in-memory refresh. `GENTLE_PI_SHELL_CHANGES_WATCH_MS` no longer enables filesystem polling.
@@ -90,7 +101,7 @@ The separate `session_worktree_register` tool still registers canonical same-clo
90
101
 
91
102
  To use `ctrl+p` like OpenCode, rebind Pi's `app.model.cycleForward` in `~/.pi/agent/keybindings.json` (Pi reserves that action, so an extension cannot take `ctrl+p` while it holds it) and set `GENTLE_PI_COMMANDS_KEY=ctrl+p`.
92
103
 
93
- Subscription usage shows in the bar after the cost, and `/gentle:usage` opens a panel with one row per window of every provider: the limit name, its meter, its percentage and, when that window reports one, its reset, all on one line. Codex, Claude and NaN all read the same way:
104
+ Subscription usage shows in the bar after the cost, and `/gentle:usage` opens a panel with one row per window of every provider: the limit name, its meter, its percentage and, when that window reports one, its reset, all on one line. Codex, Claude and NaN all read the same way. In fullscreen mode, clicking the header's `usage` segment opens this same panel. The panel's own footer hints (`r refresh`, `esc close`) are clickable too, not just keyboard shortcuts, and hovering either one paints it in the shell's shared hover role while a refresh already in flight ignores a repeated click.
94
105
 
95
106
  ```text
96
107
  ✿ gentle shell ⟡ … ⟡ $9.49 sub ⟡ codex 5h ▰▰▰▰▰▱▱▱ 62% · week 31%
@@ -112,7 +123,7 @@ The panel rows a provider reports its windows with:
112
123
  - Only the plan name and the windows are kept; account details in the payload are discarded.
113
124
  - Gauges turn amber at 80% and red at 95%, like the context gauge.
114
125
 
115
- Gentle notices are drawn as cards: the same rounded frame as the prompt, with the left rail and the title in the tone of the notice and the rest of the frame in the theme's border color.
126
+ Gentle notices are drawn as cards: the same rounded frame as the prompt. An informational card paints the rounded frame in the theme's plain border role and its title in the accent role the rose look every sidebar card, the review preflight reminder, and a quiet Agents card share. A warning, error, or success card paints its frame and title in its own tone color instead.
116
127
 
117
128
  ```text
118
129
  ╭─ ✿ Gentle AI · review preflight ─────────────────────────────────────╮
@@ -135,11 +146,13 @@ Agent paths follow `GENTLE_PI_AGENT_HOME`, then `PI_CODING_AGENT_DIR`, then `~/.
135
146
 
136
147
  ```text
137
148
  ╭─ ❀ Agents · 1 active · 1 done ─────────────────────────────── 1m24s ╮
138
- │ ✓ sdd-explore map footer data sources gpt-5.6-terra · 34k · $0.27 · 25s │
139
- │ ◐ sdd-apply write gentle-shell footer gpt-5.6-terra · 12k · $0.09 · 41s │
140
- ╰──────────────────────────────────────────────────────────────────────────────╯
149
+ │ ✓ sdd-explore map footer data sources gpt-5.6-terra · 34k · $0.27 · 25s │
150
+ │ ◐ sdd-apply write gentle-shell footer gpt-5.6-terra · 120k · $12.50 · 41s │
151
+ ╰──────────────────────────────────────────────────────────────────────────────────╯
141
152
  ```
142
153
 
154
+ The card is above the editor in every mode, including fullscreen — it is not one of the sidebar's cards. Each metadata field (`model · effort`, tokens, cost, elapsed) gets its own fixed, right-aligned column sized to the widest value among the shown tasks, so the numbers line up vertically even when one row's values are much shorter than another's; a queued task fills only the elapsed column with the word `queued`, leaving the rest of the row blank rather than overwriting it. When the card is too narrow for every column, it degrades one column at a time and the same way for every row: the task text goes first, then the `model · effort` label, then tokens, then cost; elapsed is the last column standing, since it is the one value the reader cannot rebuild from anything else on screen.
155
+
143
156
  Every subagent is its own `pi --mode rpc` child process, so the terminal never runs subagent work: the host reads JSON lines, applies each one as a small delta to a bounded per-task thread, and notifies only the listeners of that task. A task-mode child's question (`ctx.ui.select`, `confirm`, `input`, `editor`) reaches you as an ordinary pi dialog; a background child's question is dismissed. Subagents have no automatic total execution timeout: a long-running child remains live while it continues emitting RPC events. A silent child still times out through the configurable `stall_timeout_ms` watchdog (default four minutes). An announced tool call that is still running is live work, not silence, so it is bounded by `tool_stall_timeout_ms` instead (default 30 minutes, never below `stall_timeout_ms`). Closing pi stops the children that are still running.
144
157
 
145
158
  - `subagent_list_agents`, `subagent_run` (`agent`, `task`, `label?`, `context?`, `workspace_root?`, `mode?` task or background), `subagent_status`, `subagent_result`, `subagent_list_tasks`, `subagent_reply` (one current-session reply to a live child query), `subagent_cancel`, `subagent_send_message` (steer a running child), `subagent_continue` (resume a finished task in its own session).
@@ -153,10 +166,10 @@ Every subagent is its own `pi --mode rpc` child process, so the terminal never r
153
166
  - Mouse controls take priority over keyboard hints: **Follow** (`f`), **Open session** (`o`), **Stop** (`s`, legacy `c`, owned active tasks only), and **Scope** (`a`). A compact footer's `>` cycles through actions. Scope switches between this session's direct active children and all open orchestrators, including idle ones. Open writes a markdown transcript for `$EDITOR`, not a resumed child session. `j`/`k` move through lists or scroll an expanded thread; `ctrl+j`/`ctrl+k` and Page Down/Up page the thread. In Pi fullscreen mode, the wheel scrolls the viewport under the pointer; regular terminal mode does not capture mouse input. Below 12 columns or three rows, only a bounded Close cell remains; zero-sized terminals render nothing.
154
167
  - The thread displays all retained Text, Thinking, Note, and Tool content without an additional presentation cap; existing store limits and truncation markers still apply. Only the selected task is subscribed while the overlay is open.
155
168
  - Thread entries are presented as labeled Text, Thinking, Note, or Tool blocks; tool blocks show their status and nonempty output.
156
- - Current scope has no orchestrator wrapper and excludes every terminal task. All sessions discovers open Pi instances sharing the same agent profile, even across repositories; it does not infer open sessions from retained tasks. Directory headings support left/right and mouse expansion, and cannot stop or open a task. Peer children and their retained threads are read-only: no local stop, editor-open, or continuation routing, and no import into the local task store.
169
+ - Current scope has no orchestrator wrapper. Its own session's finished subagents stay listed as history after the active ones — newest ended first — with their terminal glyph, elapsed frozen at completion, and their thread inspectable; a finished row is never cancellable. The header reads `N active · M finished`. History is capped at 200 finished tasks per session (oldest dropped); resuming a session (`session_start` with reason `resume`) restores that session's own finished tasks from disk automatically, a brand-new session starts empty, and All sessions discovers open Pi instances sharing the same agent profile, even across repositories it does not infer open sessions from retained tasks and stays presence-only (no cross-session history browsing). Directory headings support left/right and mouse expansion, and cannot stop or open a task. Peer children and their retained threads are read-only: no local stop, editor-open, or continuation routing, and no import into the local task store.
157
170
  - Presence refresh is paged while the overlay is open. Graceful shutdown withdraws an instance; after abrupt closure its last heartbeat may remain visible for up to 15 seconds plus the time to complete the next directory refresh. A recent heartbeat is a heuristic, not proof that a process is alive. Same-profile, same-user processes share retained activity text; this is not an authorization channel.
158
171
  - `alt+s` confirms stopping the current active or queued subagents owned by the current process. `GENTLE_PI_AGENTS_STOP_KEY` rebinds it; `off` disables it.
159
- - Finished tasks are written to `~/.pi/agent/gentle-agents/tasks/` (one JSON per task, newest `history_max_tasks` kept, default 200) and come back on demand for `subagent_result` and `subagent_continue`, never as overlay history. Child sessions live under `~/.pi/agent/gentle-agents/sessions/`.
172
+ - Finished tasks are written to `~/.pi/agent/gentle-agents/tasks/` (one JSON per task, newest `history_max_tasks` kept, default 200) and come back on demand for `subagent_result` and `subagent_continue`; an id looked up this way from an unrelated session never enters the overlay or becomes cancellable. Child sessions live under `~/.pi/agent/gentle-agents/sessions/`.
160
173
  - `ctrl+shift+a` collapses the card to its first row (`GENTLE_PI_AGENTS_KEY`), `GENTLE_PI_AGENTS_VIEW_KEY` rebinds the overlay, `GENTLE_PI_AGENTS_PI` overrides the pi command used for children, and `GENTLE_PI_AGENTS=0` disables the tools and the card.
161
174
 
162
175
  ### Gentle Todo
@@ -24,6 +24,7 @@ ODD is the predefined workflow: it runs by default on every request, without the
24
24
  - **One feature document:** `odd/tasks/<feature-name>.md` holds objective, problem, why, scope, constraints, actionable checklist with stable IDs and acceptance criteria, verification evidence, progress, and next step. Project-scoped Engram topic `odd/<feature-name>/tasks` mirrors the full document and repository-relative locator. Keep concise rationale for meaningful accepted changes here, not a separate plan or exhaustive journal. Accepted user, review, or verification changes update intent and tasks together; preserve valid completed work, add new tasks or reopen invalidated items with reasons. Findings alone do not authorize expansion or acceptance. Routine corrections stay with their tasks; checkoffs require observed proof.
25
25
  - **Recovery:** write local progress first and read back both copies; writes are not atomic. Unavailable Engram leaves an explicit pending mirror, not invented success or a block on unrelated safe work. Before implementation or resume, the parent reads full feature memory and the actual task file, reconciles code and evidence, and preserves conflicting versions. Pass the locator and relevant context; workers read the document before edits. The existing Todo UI is a projection, not another authority.
26
26
  - **Task size:** about 400 authored changed lines (additions plus deletions) is advisory only, not a cap, acceptance criterion, automatic stop, forced split, or RDD trigger. Keep coherent behavior with tests and docs, explain natural overages, and continue under existing PR policy. Forward this instruction to workers; never remove whitespace, comments, or tests, minify, invent abstractions, or split artificially for cosmetic savings.
27
+ - **Runtime boundary:** in a primary turn, direct `edit`/`write` calls may successfully mutate one repository file and repeat that path. A second distinct file is refused before mutation and must go through `subagent_run`; `odd/tasks/**` bookkeeping and delegated child actors are exempt, failed calls consume nothing, and the next primary start resets the boundary.
27
28
  - **Research:** optional research addresses a named uncertainty. Establish problem, intended outcome, constraints, and current evidence; inspect code and adapt depth to consequence, not fixed questionnaires or rounds. The parent asks one focused product question only when needed, then waits; workers return gaps. Use available authorized documentation/web tools, prefer primary sources, and attribute claims to URLs/code locations. Distinguish facts, assumptions, contradictions, freshness, and gaps; return a recommendation, tradeoffs, open questions, and implementation implications. Forward these instructions to an existing fresh general worker, not a specialized agent or `sdd-research`. Unavailable evidence pauses only unsafe dependent decisions. Research stays read-only with no new persistence/readiness machinery; a brief proposal is needed only for a real decision.
28
29
  - **Assumptions:** at most one scoped independent read-only challenge for a high-consequence unproven premise, including a small security-critical change. Deterministic failures need fixes, not debate. Native RDD claims stay with its refuter.
29
30
  - **TDD:** resolve on/off from existing project/session configuration or explicit user choice; retain source and exact runner in the feature document when present and forward all three on every implementation delegation, refreshing on resume. Test presence does not enable TDD. Enabled requires observed RED before implementation → GREEN → REFACTOR; disabled still requires ordinary functional checks. Unknown/conflicting mode or a missing runner needs only the clarification affecting the next action, never invented precedence, commands, or `sdd-init`.
@@ -105,7 +106,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
105
106
  | **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
106
107
  | **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
107
108
  | **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
108
- | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.2.1 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
109
+ | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.4.0 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
109
110
  | **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
110
111
 
111
112
  ## Native pointer regions
@@ -142,7 +143,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
142
143
 
143
144
  ### Source checkout
144
145
 
145
- This checkout prepares `gentle-pi` `3.2.1`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.2.1`, distinct from the published `v2.6.0` pairing.
146
+ This checkout prepares `gentle-pi` `3.3.0`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.4.0`, distinct from the published `v2.6.0` pairing.
146
147
 
147
148
  The native SDD status consumer accepts both the pinned producer's legacy
148
149
  `apply`/`verify`/`remediate`/`archive` instruction record and the classical
@@ -153,7 +154,7 @@ Unknown or incomplete instruction records still fail closed.
153
154
  The Pi runtime now uses native status exclusively for SDD and retires standalone
154
155
  sync. The full chain follows completed apply to archive, where applicable delta
155
156
  specs are composed; verification remains explicitly invokable. With the current
156
- 3.2.1 pin, native still requires verification and its emitted evidence requirements;
157
+ 3.4.0 pin, native still requires verification and its emitted evidence requirements;
157
158
  a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
158
159
  those exact instructions without overriding readiness or inventing legacy evidence.
159
160
  Classical direct-archive behavior is compatibility-tested with an identified
@@ -187,7 +188,7 @@ pi install npm:gentle-pi@2.6.0
187
188
 
188
189
  RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
189
190
 
190
- The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.2.1`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.2.1` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
191
+ The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.4.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.4.0` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
191
192
 
192
193
  Recommended companion packages:
193
194
 
@@ -361,13 +362,13 @@ flowchart TD
361
362
 
362
363
  VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
363
364
 
364
- For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.2.1 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
365
+ For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.4.0 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
365
366
 
366
367
  Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
367
368
 
368
369
  Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
369
370
 
370
- Once the source checkout's pinned gentle-ai runtime (currently v3.2.1) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
371
+ Once the source checkout's pinned gentle-ai runtime (currently v3.4.0) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
371
372
 
372
373
  ### FINALIZE wrapper input
373
374
 
@@ -730,7 +731,7 @@ Profiles are named, switchable snapshots of the global agent-model routing from
730
731
 
731
732
  Applying a profile writes `~/.pi/gentle-ai/models.json`, then reconciles agent frontmatter and `subagents.json` the same way `/gentle:models` does. A profile is a complete snapshot: every discoverable agent it omits returns to inherit, so routing materialized by a previous profile, by `/gentle:models`, or by a migration never survives a switch silently. The reconciliation happens on the next subagent launch, and that launch still routes with the previous routing — expect one launch of lag after switching. The active profile is persisted so `/gentle:profiles` reopens with the applied profile marked.
732
733
 
733
- A profile also carries the orchestrator under the reserved routing key `orchestrator`. Applying a profile that defines it writes `defaultProvider`, `defaultModel`, and `defaultThinkingLevel` to Pi's global `settings.json` (preserving every other key; an unreadable `settings.json` aborts that part and is reported instead of being overwritten). Applying a profile without an `orchestrator` entry never moves the orchestrator, and `s` snapshots the currently effective orchestrator together with the routing. `orchestrator` is reserved: it is not a subagent name, is never written to `subagents.json`, and is not counted as a role.
734
+ A profile also carries the orchestrator under the reserved routing key `orchestrator`. Applying a profile that defines it writes `defaultProvider`, `defaultModel`, and `defaultThinkingLevel` to Pi's global `settings.json` (preserving every other key; an unreadable `settings.json` aborts that part and is reported instead of being overwritten) and switches the session you are in to that model and thinking level right away, so the orchestrator answers with the profile's model from the next turn. When the model is not in Pi's catalog or its provider has no authentication, the default for new sessions is still recorded and the apply note says this session kept its current model. Applying a profile without an `orchestrator` entry never moves the orchestrator, and `s` snapshots the currently effective orchestrator together with the routing. `orchestrator` is reserved: it is not a subagent name, is never written to `subagents.json`, and is not counted as a role.
734
735
 
735
736
  The panel's current routing, the `current` seed, and `s` all read the routing in effect: `models.json` where it has an entry, and otherwise the `subagents.json` model profile or frontmatter routing the runtime actually resolves for that agent. A sparse `models.json` therefore never hides routing that is still live. When `profiles.json` is missing, the command seeds one profile named `current` captured from that effective routing, marked active only when it has routing entries. Profiles or routing entries dropped by normalization are named in a warning instead of being lost silently.
736
737
 
@@ -822,6 +823,7 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen
822
823
  | `/gentle:commands` | Opens the command palette (default `alt+k`): a curated, grouped menu (Configuration, Session, Diagnostics, SDD, Skills) of registered Gentle commands; search and run by label. |
823
824
  | `/gentle:persona` | Switches global persona mode, with project override support. |
824
825
  | `/gentle:background-subagents` | Shows or sets the managed background-subagents policy (`status\|enable\|disable`), naming the source that decided it. |
826
+ | `/gentle:double-esc-cancel` | Shows or sets the double-esc-cancel preference (`status\|enable\|disable`); no argument toggles it. |
825
827
  | `/gentle:telemetry` | Shows or changes the local Gentle AI telemetry trigger (`status\|enable\|disable\|preview`). |
826
828
  | `/gentle:review-mode` | Shows or sets the receipt-driven development mode (`status\|enable\|disable`); user-initiated only, Pi automation never toggles it. |
827
829
  | `/gentle:banner` | Configures startup banner rose, text logo, and color preset. |
@@ -865,6 +867,38 @@ Both files use the strict shape `{"schema":"gentle-pi.background-subagents/v1","
865
867
 
866
868
  Because the project file outranks the global one, `enable` still writes the global file but reports plainly when a project file keeps the effective policy unchanged. The resolved capability (`ready` or `absent`) reports whether `subagent_run` is actually callable in this session; a policy of `on` with capability `absent` means Gentle Agents is disabled or the retired subagents package is still installed.
867
869
 
870
+ ### Esc behavior
871
+
872
+ The Gentle prompt matches Claude Code's Esc model on top of Pi's own. Four flows share the frame's single hint slot on the bottom rule, each decided by its own state:
873
+
874
+ 1. **Working: cancel keeps the queue moving.** Esc aborts the running turn (a single Esc by default, or the confirming second Esc when double-esc-cancel below is enabled). Any steer or follow-up messages queued while the turn ran are sent as the next turn once the abort settles, instead of being dumped back into the editor for the user to notice and resend by hand; if another turn starts first (for example the user sends the restored draft before the abort has fully settled), the queued text simply waits and is sent once that turn settles, so it is never lost and never injected mid-turn. The user's own unsent draft stays in the editor untouched. Completed work before the abort is preserved, as Pi already does. Images inside a queued message are dropped, because Pi's own restore already drops them before this code ever sees the text. If Pi ever restores a shape this code does not recognize, Pi's own text is left exactly as written and nothing is dispatched, rather than guessing.
875
+ 2. **Working: double-esc-cancel (opt-in, off by default).** While the prompt is working (autocomplete hidden), a single Esc still aborts the turn immediately by default, exactly like Pi's own escape. Once enabled, the first Esc is swallowed and the prompt frame shows `esc again to cancel`; a second Esc within 1000ms falls through so Pi's own `onEscape` performs the abort (and flow 1 above still applies to that second Esc). Letting the window expire treats the next Esc as a first press again.
876
+ 3. **Idle with a draft: double Esc clears it.** With the prompt idle, autocomplete hidden, and non-empty editor text, the first Esc shows `esc again to clear` instead of doing nothing; a second Esc within 500ms, on the exact same text, adds the draft to history (recoverable with the Up arrow) and clears it. Editing the draft between the two presses starts a fresh first press on the new text instead of clearing the edit away. Letting the window expire treats the next Esc as a first press again. A bash-mode draft (starting with `!`) is never touched by this gate; Pi's own bash-mode Esc keeps deciding it.
877
+ 4. **Idle, empty editor: unchanged.** Pi's own idle double-Esc (`/tree` or `/fork`, 500ms) keeps deciding this case entirely; the prompt never intercepts it.
878
+
879
+ Overlays, autocomplete cancel, and bash mode all consume the first Esc locally and are unaffected by any of the four flows above.
880
+
881
+ Double-esc-cancel's policy is user-owned: only an explicit `/gentle:double-esc-cancel enable` or `disable` writes it, and Pi automation never toggles it.
882
+
883
+ ```text
884
+ /gentle:double-esc-cancel Toggle the effective policy (on -> off, off -> on).
885
+ /gentle:double-esc-cancel status Report the effective policy and the deciding source.
886
+ /gentle:double-esc-cancel enable Write "on" to the global file.
887
+ /gentle:double-esc-cancel disable Write "off" to the global file.
888
+ ```
889
+
890
+ Unlike `/gentle:background-subagents`, no argument here reports status; it toggles the effective policy instead, since this preference has only one file layer and nothing else can outrank a write.
891
+
892
+ Three sources can decide the policy, and the first hit wins:
893
+
894
+ | Priority | Source | Notes |
895
+ | -------- | ------------------------------------------- | ------------------------------------------------------------ |
896
+ | 1 | `<configHome>/double-esc-cancel.json` | Global file, written by `enable`/`disable`. `configHome` honors `GENTLE_PI_CONFIG_HOME` and defaults to `~/.pi/gentle-ai`. There is no project-level override: this preference changes what a keypress does, and per-project overrides would make the same key do two different things depending on which repo is open. |
897
+ | 2 | `GENTLE_PI_DOUBLE_ESC_CANCEL` | Exactly `on` or `off`. Any other value is ignored, and it decides only when the global file does not exist. |
898
+ | 3 | Built-in default | `off`. |
899
+
900
+ The file uses the strict shape `{"schema":"gentle-pi.double-esc-cancel/v1","policy":"on"}`. A file that is present but malformed fails closed to `off` instead of falling through to the environment variable, and the command reports that case as a warning instead of an ordinary `off`. The extension resolves the policy once at startup and updates it in memory when the command runs; the prompt never re-reads the file on every keypress.
901
+
868
902
  Startup banner settings remain global in `banner.json` under `GENTLE_PI_CONFIG_HOME` (default `~/.pi/gentle-ai`). Existing `showRose` and `showTextLogo` opt-outs independently control the main startup artwork; both default to enabled. Changes apply on the next session or `/reload`. Color presets are `pink` (default), `cyan`, `yellow`, and `green`. The static sidebar heading is independent of these preferences and follows the active theme.
869
903
 
870
904
  Startup flag:
@@ -8,27 +8,32 @@ Gentle Pi is a transport consumer, not a review authority. Gentle AI generates t
8
8
 
9
9
  | Component | Responsibility |
10
10
  | --- | --- |
11
- | Pi reviewer adapter | A pure opaque adapter: `Buffer Buffer/error`. It accepts a Go-materialized prompt as bytes, invokes Pi in JSON event mode, and returns the event stream's final assistant text or a typed transport error carrying what the child's own stream revealed. |
12
- | Host coordinator | Executes the exact Go-issued materialize/submission tokens, launches the adapter, and submits its result only through the supplied token. It validates and forwards two optional caller-owned launch selections: the lens's reviewer model (`--model`) and an explicit extension allowlist (`-e` paths). |
11
+ | In-process reviewer completion | A pure completion (`lib/inprocess-reviewer.ts`): resolves the lens's "provider/id" selection through the live model registry, authenticates through the registry's own resolver, and completes the Go-materialized prompt as one frozen user message no systemPrompt, no tools, no session, no extension hooks, no child process. |
12
+ | Host coordinator | Executes the exact Go-issued materialize/submission tokens, runs the completion through the caller-supplied model registry, and submits its result only through the supplied token. |
13
13
  | Gentle AI (Go) | Go owns worktree, lineage, candidate freeze, lens selection, correction, validator, approval burn, and review semantics. Delivery commands remain ordinary repository-policy operations. |
14
14
 
15
- The adapter does not parse bindings, select work, rebuild prompts, inspect repository state, retry, classify results, or create authority. The coordinator does not infer a command or replace a provider-issued token. The package has no durable receipt or policy authority.
15
+ The completion does not parse bindings, select work, rebuild prompts, inspect repository state, retry, classify results, or create authority. The coordinator does not infer a command or replace a provider-issued token. The package has no durable receipt or policy authority.
16
16
 
17
17
  ## Transport behavior
18
18
 
19
19
  1. Gentle AI emits an opaque materialization or submission token for the selected Pi runtime.
20
- 2. The host coordinator executes that exact token and gives only the materialized bytes to the adapter.
21
- 3. The adapter runs the child with `--mode json`, extracts the final assistant text from the pi event stream, and returns those bytes to the coordinator. A run that produced no assistant text fails typed with evidence: the stream kind, the reviewer selection the child itself reported, and whether a tool call was attempted (a text-mode run used to exit 0 with zero bytes and no diagnosable cause).
22
- 4. The coordinator sends those bytes only through the exact Go-issued submission token.
20
+ 2. The host coordinator executes that exact token and gives only the materialized bytes to the completion, as the single user message of one frozen prompt.
21
+ 3. The completion resolves its "provider/id" selection through the live model registry, authenticates, and returns the completion's text to the coordinator. A run that produced no text, attempted a tool call, or exceeded its bound fails typed with evidence (the completion's stop reason, its aborted/timed-out signal, or its reviewer model, depending on the code); a run over the fixed output bound also fails typed.
22
+ 4. The coordinator sends that text only through the exact Go-issued submission token.
23
23
 
24
- ## Reviewer launch selection (user-owned)
24
+ ## Reviewer completion selection (user-owned)
25
25
 
26
- The default reviewer launch is selection-free: no model flag, no extensions, no ambient inheritance of the session's model. Two optional, user-owned selections ride the request and are validated before any process launches; a broken value is refused typed as `reviewer-config-invalid`, never a mid-review transport failure:
26
+ There is no model flag, no extension allowlist, and no ambient default model: the in-process completion resolves entirely through the caller-supplied model registry (`ctx.modelRegistry`), and a missing registry or an unconfigured lens is refused typed as `reviewer-config-invalid` before materialize ever runs, never a mid-review transport failure.
27
27
 
28
- - **Lens model** — the capture path reads the lens's entry from the agent model routing config (`review-risk`, `review-resilience`, `review-readability`, `review-reliability`) and forwards it as `--model <provider/id>`.
29
- - **Extension allowlist** — `GENTLE_PI_REVIEW_RELAY_EXTENSIONS` holds absolute extension file paths separated by the platform path delimiter. They are loaded through explicit `-e` paths, which pi honors even under `--no-extensions`; this is how a subscription provider's OAuth billing adapter rides along without re-enabling extension discovery.
28
+ - **Lens model and thinking level** — the capture path reads the lens's entry from the agent model routing config (`review-risk`, `review-resilience`, `review-readability`, `review-reliability`) and forwards its `model` and `thinking` fields verbatim to the completion. A routing entry with no configured model is refused typed, naming the routing key — the completion never falls back to another provider or an ambient default.
30
29
 
31
- A typed Pi transport refusal fails closed. The coordinator reports the refusal — including the reviewer evidence and a bounded stderr excerpt on an empty-output failure — without an agentless lifecycle fallback, local retry policy, synthetic result, or alternate approval path.
30
+ A typed reviewer refusal fails closed. The coordinator reports the refusal — including the completion's evidence — without an agentless lifecycle fallback, local retry policy, synthetic result, or alternate approval path.
31
+
32
+ ## Refuter and targeted validator
33
+
34
+ The refuter and targeted-validator roles are host-mediated in-process completions too, on a provider that advertises the v9 role contract: the collect input carries `--materialize=true` and a provider-owned submission descriptor, exactly like a lens capture-result materialize slot — materialize, complete in-process, then submit through the exact `--input` form. Their entries in the agent model routing config, `review-refuter` and `review-validator`, select the model and thinking level the same way `review-<lens>` does for a lens; a missing entry is refused typed, naming that key, before materialize ever runs.
35
+
36
+ An older provider that has not advertised the v9 role contract still renders each role as a self-contained vector (binding tokens plus `--agent=pi --execute=true`, no submission): executing that exact vector makes Go materialize the role prompt, run its own locked-down pi subprocess, and admit the verdict itself. The host still accepts this compatibility form unchanged.
32
37
 
33
38
  ## Dynamic contract delivery
34
39
 
@@ -36,19 +41,19 @@ Package static assets intentionally omit lifecycle instructions, candidate routi
36
41
 
37
42
  ## Integration constraints
38
43
 
39
- - Keep Pi transport opaque: raw prompt bytes in, the event stream's assistant text or a typed, evidenced error out.
44
+ - Keep the completion frozen: raw prompt bytes in as one user message, the completion's text or a typed, evidenced error out.
40
45
  - Preserve Go-issued materialize and submission tokens exactly; they are the only authority-bearing inputs the host may execute.
41
- - Keep the reviewer launch selection user-owned: the relay never invents a model and never enables extension discovery; it only forwards the validated caller-owned selections.
42
- - Treat a transport failure as unavailable evidence, never as an approval, completion, or permission to substitute a local workflow.
46
+ - Keep the reviewer completion selection user-owned: it never invents a model or falls back to another provider or an ambient default; it only forwards the validated caller-owned selection and thinking level, or refuses typed.
47
+ - Treat a reviewer refusal as unavailable evidence, never as an approval, completion, or permission to substitute a local workflow.
43
48
  - Keep command safety and user interaction in the host, without interpreting provider authority state.
44
49
  - Keep durable review state, admissions, correction accounting, and approvals in Gentle AI. Keep delivery decisions in ordinary repository policy.
45
50
 
46
51
  ## Review checklist
47
52
 
48
- - [ ] The adapter surface is still `Buffer Buffer/error` (the output is the pi event stream's assistant text; failures carry typed evidence).
53
+ - [ ] The completion still takes raw prompt bytes as one frozen user message and returns text or a typed, evidenced error — no child process, no extension allowlist.
49
54
  - [ ] The coordinator executes only exact Go-issued materialize/submission tokens.
50
- - [ ] The reviewer launch stays selection-free unless the caller-owned selection and extension allowlist validate.
51
- - [ ] Typed transport refusal remains fail-closed.
55
+ - [ ] The reviewer completion selection stays refused typed unless the caller-owned model registry and a configured lens selection validate.
56
+ - [ ] Typed reviewer refusal remains fail-closed.
52
57
  - [ ] No package code or static prompt uses review authority to decide, authorize, rewrite, or block delivery commands.
53
58
 
54
59
  ← [Back to README](../README.md)
@@ -13,12 +13,11 @@ import os from "node:os";
13
13
  import { join, resolve, isAbsolute, sep } from "node:path";
14
14
  import { createBashToolDefinition, createLocalBashOperations, type BashOperations, keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
15
15
  import { Text, type TUI } from "@earendil-works/pi-tui";
16
- import { sidebarPart } from "../lib/shell-sidebar.ts";
17
16
  import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
18
17
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
19
18
  import { AGENT_MODE, discoverAgents, parseAgentDefinition, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
20
19
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
21
- import { isFinished, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
20
+ import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
22
21
  import { AgentRunner, piCommand, abortReasonText, plannedCommands, type RemediationPlan, type RemediationScope, REMEDIATION_PLAN_ENV, parseRemediationPlan, type AskAnswer, type RunnerDeps, type SddChangeSelection, type TaskRequest } from "../lib/agents-runner.ts";
23
22
  import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
24
23
  import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
@@ -499,6 +498,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
499
498
  let renderQueued = false;
500
499
  let cancelClock: (() => void) | undefined;
501
500
  const ownedTaskIds = new Set<string>();
501
+ // One diagnostic note per (task, guard): a dropped mutation says why once,
502
+ // not once per file, so a chatty child cannot flood its own thread.
503
+ const droppedAttributionGuards = new Set<string>();
502
504
  const stoppingTaskIds = new Set<string>();
503
505
  const yieldedTaskIds = new Set<string>();
504
506
  const metricsNow = deps.metricsNow ?? (() => performance.now());
@@ -744,16 +746,43 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
744
746
  }
745
747
  },
746
748
  onSuccessfulMutation: (task, tool) => {
747
- if (!sessions || !worktrees || task.parentSessionId !== activeSessionId() || !ownedTaskIds.has(task.id)) return;
748
- const root = deps.resolveWorktree(tool.path, task.cwd)?.root;
749
- const childRoot = deps.resolveWorktree(task.cwd, task.cwd)?.root;
750
- if (!root || root !== childRoot || !worktrees.roots().includes(root)) return;
751
- if (tool.evidence?.root === root) {
749
+ // Guard chain and posture are unchanged: only owned tasks of the
750
+ // active parent, inside registered roots, ever relay. The notes only
751
+ // explain a drop -- they never widen or narrow what gets attributed.
752
+ // The cheap session/ownership guards decide before any worktree
753
+ // resolution, so a foreign or stale mutation never reaches git.
754
+ let root: string | undefined;
755
+ let childRoot: string | undefined;
756
+ const noteDrop = (guard: string) => {
757
+ const key = `${task.id}:${guard}`;
758
+ if (droppedAttributionGuards.has(key)) return;
759
+ droppedAttributionGuards.add(key);
760
+ try { store.apply(task.id, { type: TASK_EVENT.NOTE, text: `changes not attributed: ${guard} (root=${root ?? "unknown"}, child=${childRoot ?? "unknown"})` }, deps.now()); }
761
+ catch { /* A note is best-effort explanation; it must never block the guard it is explaining. */ }
762
+ };
763
+ if (!sessions || !worktrees) return noteDrop("session-inactive");
764
+ if (task.parentSessionId !== activeSessionId()) return noteDrop("parent-session-mismatch");
765
+ if (!ownedTaskIds.has(task.id)) return noteDrop("not-owned");
766
+ root = deps.resolveWorktree(tool.path, task.cwd)?.root;
767
+ childRoot = deps.resolveWorktree(task.cwd, task.cwd)?.root;
768
+ if (!root) return noteDrop("root-unresolved");
769
+ if (root !== childRoot) return noteDrop("root-mismatch");
770
+ if (!worktrees.roots().includes(root)) return noteDrop("root-not-registered");
771
+ if (!tool.evidence) {
772
+ noteDrop("evidence-missing");
773
+ } else if (tool.evidence.root !== root) {
774
+ noteDrop("evidence-root-mismatch");
775
+ } else {
776
+ let path = tool.path.replace(/^@/, "");
777
+ if (path === "~" || path.startsWith("~/")) path = os.homedir() + path.slice(1);
778
+ let resolvedPath: string | undefined;
752
779
  try {
753
- let path = tool.path.replace(/^@/, "");
754
- if (path === "~" || path.startsWith("~/")) path = os.homedir() + path.slice(1);
755
- if (realpathSync(resolve(task.cwd, path)) === resolve(root, tool.evidence.path)) pi.events.emit(SESSION_CHANGE_RELAY, { sessionId: task.parentSessionId, evidence: { ...tool.evidence, id: `${task.id}:${tool.toolCallId}` } });
756
- } catch { /* Missing or mismatched targets cannot supply session diffs. */ }
780
+ resolvedPath = realpathSync(resolve(task.cwd, path));
781
+ } catch {
782
+ noteDrop("evidence-path-unreadable");
783
+ }
784
+ if (resolvedPath === resolve(root, tool.evidence.path)) pi.events.emit(SESSION_CHANGE_RELAY, { sessionId: task.parentSessionId, evidence: { ...tool.evidence, id: `${task.id}:${tool.toolCallId}` } });
785
+ else if (resolvedPath !== undefined) noteDrop("evidence-path-mismatch");
757
786
  }
758
787
  recordReviewMutation(pi, sessions, root, { source: "subagent", taskId: task.id, toolName: tool.toolName, toolCallId: tool.toolCallId });
759
788
  },
@@ -895,6 +924,33 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
895
924
  return stored?.task;
896
925
  };
897
926
 
927
+ // Restores this exact session's own finished subagents as visible history
928
+ // on an explicit resume, or on startup into a session that already has
929
+ // entries -- never on new, fork, or reload (see the session_start handler's
930
+ // preexisting check below, which is the actual gate). Marked in
931
+ // restoredTaskIds like any other disk restoration, so it stays
932
+ // non-cancellable (ownedTaskIds never gained the id either way) and is
933
+ // skipped if the id is somehow already live.
934
+ const restoreSessionHistory = async (ctx: ExtensionContext, sessionId: string): Promise<void> => {
935
+ let history: Awaited<ReturnType<typeof loadHistory>>;
936
+ try {
937
+ history = await loadHistory(tasksDir);
938
+ } catch {
939
+ return;
940
+ }
941
+ // The session may have moved on while disk was read; a stale restore
942
+ // must never land in the wrong session's store.
943
+ if (ctx.sessionManager.getSessionId() !== sessionId) return;
944
+ // Fire-and-forget from session_start: a throwing summary subscriber must
945
+ // never surface as an unhandled rejection. History is best-effort.
946
+ try {
947
+ for (const { task, thread } of history) {
948
+ if (task.parentSessionId !== sessionId) continue;
949
+ if (store.restore(task, thread)) restoredTaskIds.add(task.id);
950
+ }
951
+ } catch { /* Partial history is acceptable; the live session keeps running. */ }
952
+ };
953
+
898
954
  const openOverlay = async (ctx: ExtensionContext) => {
899
955
  if (!ctx.hasUI) return;
900
956
  if (ctx.mode !== "tui") {
@@ -974,19 +1030,19 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
974
1030
  ui = ctx.hasUI ? ctx.ui : undefined;
975
1031
  sessions = ctx.sessionManager;
976
1032
  tickClock();
1033
+ // Agents is not a rail part: the fullscreen sidebar only suppresses
1034
+ // bottom components registered through sidebarPart, and this widget is
1035
+ // the only Agents surface in every mode, so it stays a plain component.
977
1036
  ui?.setWidget(AGENTS_WIDGET_KEY, (tui, theme) => {
978
1037
  host = tui;
979
1038
  sidebarTui = tui;
980
- return sidebarPart(tui, "agents", {
1039
+ return {
981
1040
  render(width: number) {
982
1041
  const lines = renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, maxRows: widgetRows(tui.terminal?.rows), viewKey });
983
1042
  return lines.length === 0 ? [] : [...lines, ""];
984
1043
  },
985
1044
  invalidate() {},
986
- }, {
987
- render: (width) => renderAgentsCard(visibleTasks(), theme, width, deps.now(), { collapsed, collapseKey, viewKey }),
988
- invalidate() {},
989
- });
1045
+ };
990
1046
  });
991
1047
  };
992
1048
 
@@ -1362,13 +1418,25 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1362
1418
  });
1363
1419
  }
1364
1420
 
1365
- pi.on("session_start", async (_event, ctx) => {
1421
+ pi.on("session_start", async (event, ctx) => {
1366
1422
  // A resumed, reloaded, or replaced session starts with an empty completion
1367
1423
  // queue so nothing pending from another session can replay here.
1368
1424
  completions.dropAll();
1369
1425
  presence?.dispose();
1370
1426
  registryFor(ctx);
1371
1427
  showWidget(ctx);
1428
+ // An explicit in-session /resume always brings this session's own
1429
+ // finished subagents back from disk. Pi also reports "startup" (not
1430
+ // "resume") when the CLI is launched directly into an existing session
1431
+ // file, e.g. --continue or the --resume picker (agent-session.js:152
1432
+ // defaults to "startup"); that case restores too, but only when the
1433
+ // session actually has prior entries -- a brand-new session can also be
1434
+ // announced as "startup", and a fresh session has none. "new", "fork",
1435
+ // and "reload" never restore here, matching the existing on-demand
1436
+ // resolveTask path for anything else.
1437
+ const sessionId = ctx.sessionManager.getSessionId();
1438
+ const preexisting = event.reason === "resume" || (event.reason === "startup" && ctx.sessionManager.getEntries().length > 0);
1439
+ if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
1372
1440
  try {
1373
1441
  presence = PresencePublisher.start({ profile: agentHome, sessionId: activeSessionId() ?? "",
1374
1442
  label: ctx.sessionManager.getSessionName?.() || ctx.sessionManager.getCwd().split(/[\\/]/).pop() || "Orchestrator", activity: [] });