gentle-pi 4.1.0 → 4.2.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 (81) hide show
  1. package/README.md +39 -14
  2. package/bin/engram-guard +1 -1
  3. package/bin/gentle-shell.mjs +16 -11
  4. package/docs/auto-permission-answers.md +25 -0
  5. package/docs/auto-permission-evidence.md +39 -0
  6. package/docs/bundled-install.md +8 -6
  7. package/docs/gentle-shell.md +17 -10
  8. package/docs/install-wizard.md +9 -9
  9. package/docs/permission-modes.md +50 -5
  10. package/docs/readme-reference.md +48 -12
  11. package/docs/session-profile-format.md +39 -0
  12. package/docs/yolo-mode.md +2 -1
  13. package/extensions/ask-user-choice.ts +4 -0
  14. package/extensions/ask-user-question.ts +6 -1
  15. package/extensions/gentle-agents.ts +54 -23
  16. package/extensions/gentle-ai.ts +105 -25
  17. package/extensions/gentle-shell.ts +26 -31
  18. package/extensions/gentle-todo.ts +26 -16
  19. package/lib/auto-permission-answers.ts +133 -0
  20. package/lib/auto-permission-classifier.ts +215 -36
  21. package/lib/auto-permission-evidence.ts +88 -0
  22. package/lib/auto-permission-memory.ts +69 -0
  23. package/lib/auto-permission-runtime.ts +251 -35
  24. package/lib/auto-permission-tool-context.ts +61 -0
  25. package/lib/destructive-command-guard.ts +104 -28
  26. package/lib/gentle-shell-bundled.ts +23 -5
  27. package/lib/gentle-shell-launcher.ts +1 -1
  28. package/lib/model-routing-authority.ts +6 -1
  29. package/lib/selection-engine.ts +4 -1
  30. package/lib/session-profile-freeze.ts +10 -15
  31. package/lib/session-profile-persistence.ts +187 -0
  32. package/lib/shell-changes.ts +2 -0
  33. package/lib/shell-prompt.ts +6 -4
  34. package/lib/shell-todo.ts +20 -8
  35. package/lib/vim-editor-adapter.ts +22 -4
  36. package/package.json +5 -5
  37. package/runtime/gentle-shell-bundled.mjs +23 -5
  38. package/runtime/gentle-shell-launcher.mjs +1 -1
  39. package/scripts/install-wizard-preview.mjs +2 -2
  40. package/scripts/installer-downloads.mjs +7 -7
  41. package/scripts/installer-preflight.mjs +1 -1
  42. package/scripts/pi-update.mjs +223 -0
  43. package/tests/append-system-prompt-route.test.ts +18 -1
  44. package/tests/auto-mode-human-answers.test.ts +68 -0
  45. package/tests/auto-mode-invariants.test.ts +23 -5
  46. package/tests/auto-mode-sdk.test.ts +29 -2
  47. package/tests/auto-permission-answers.test.ts +170 -0
  48. package/tests/auto-permission-classifier.test.ts +353 -2
  49. package/tests/auto-permission-compound.test.ts +24 -0
  50. package/tests/auto-permission-evidence.test.ts +88 -0
  51. package/tests/auto-permission-memory.test.ts +46 -0
  52. package/tests/auto-permission-public-regressions.test.ts +111 -0
  53. package/tests/auto-permission-runtime.test.ts +270 -12
  54. package/tests/autonomous-guard.test.ts +86 -16
  55. package/tests/bundled-install.test.ts +3 -3
  56. package/tests/engram-safety.test.ts +34 -6
  57. package/tests/gentle-agents.test.ts +162 -6
  58. package/tests/gentle-shell-bin.test.ts +17 -17
  59. package/tests/gentle-shell-bundled.test.ts +67 -33
  60. package/tests/gentle-shell-launcher.test.ts +18 -13
  61. package/tests/gentle-shell.test.ts +71 -5
  62. package/tests/gentle-todo.test.ts +79 -27
  63. package/tests/install-wizard.test.ts +2 -2
  64. package/tests/installer-posix-bootstrap.test.ts +8 -8
  65. package/tests/installer-preflight.test.ts +3 -3
  66. package/tests/installer-probes.test.ts +8 -8
  67. package/tests/installer-runner.test.ts +14 -13
  68. package/tests/installer-server.test.ts +5 -5
  69. package/tests/main-channel-upgrade.test.ts +7 -1
  70. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  71. package/tests/package-manifest.test.ts +6 -6
  72. package/tests/pi-update-launcher.test.ts +87 -0
  73. package/tests/pi-update.test.ts +186 -0
  74. package/tests/rdd-status-line.test.ts +52 -0
  75. package/tests/selection-engine.test.ts +35 -5
  76. package/tests/session-profile-freeze.test.ts +14 -1
  77. package/tests/session-profile-persistence.test.ts +334 -0
  78. package/tests/shell-changes.test.ts +18 -0
  79. package/tests/shell-todo.test.ts +15 -12
  80. package/tests/vim-editor-adapter.test.ts +41 -15
  81. package/tests/yolo-mode.test.ts +6 -1
@@ -2,6 +2,32 @@
2
2
 
3
3
  Use **Normal**, **Auto**, or **YOLO** for the current primary session. Optionally save any of them as the user-global default for future sessions. These modes do not redesign Gentle Shell delegation, expand the task you authorized, or replace independent safety and consent.
4
4
 
5
+ ## Tool context in Auto
6
+
7
+ For each pending tool, Auto can include the effective registry's name, description and argument schema as **untrusted descriptive evidence**. The same mechanism applies to ordinary tools and plugins; no tool name or description grants permission. These definitions are not added to the reviewer's callable tools, and embedded instructions cannot replace the live user request or restrictions.
8
+
9
+ The snapshot is limited to 8 KiB, 512 JSON nodes and 12 levels of nesting. It omits metadata whole if it is missing, malformed, contains detectable credential material, or would crowd out the live request/action. Accessors and custom serializers are not invoked; non-JSON symbol and non-enumerable annotations are excluded. This does not change the protection of secrets in actual action arguments. A changed projected definition revokes a pending verdict. When metadata is unavailable, the existing classifier path remains active without inventing tool semantics or an automatic allow.
10
+
11
+ Definitions describe intended behavior; they do not prove implementation safety. This context improvement does not by itself explain timeouts or unsupported reviewer tool calls. Deterministic tests verify projection, bounds and admission behavior, not a live model's interpretation.
12
+
13
+ ## Long sessions in Auto
14
+
15
+ Auto sends the classifier your live messages from this session, up to 24 KB. Beyond that it no longer blocks every action. It always keeps your **first** message (the task) and your **latest** message, fills the rest with the most recent messages, and shortens any message over 6 KB in the middle with a `[N bytes omitted by host]` marker. Sizes are measured as the classifier receives them (JSON-escaped), with room reserved for the omission note. The classifier is told how many earlier messages were left out; that extra policy is sent only when something was omitted.
16
+
17
+ A restriction stated only in an omitted message, or in the shortened middle of a long message, no longer reaches the classifier. A very large action can still exceed the classifier input budget on its own. Silence is not permission, so the classifier still requires clear authorization in the kept messages for destructive, irreversible, external or out-of-scope actions. Restate any restriction that must hold for the whole session. No extra model call is made, and history held in memory still resets on reload.
18
+
19
+ ## Task-linked project memory
20
+
21
+ Auto still classifies every memory call. It can treat a non-sensitive Engram finding or session summary as auxiliary work within the live authorized task, rather than requiring a separate request for each save. Explicit no-save/no-persistence restrictions always win; this does not permit secrets, deletion, maintenance, personal/global or cross-project writes, or unrelated `topic_key` overwrites.
22
+
23
+ This contextual treatment requires live host evidence, not a tool name or an assistant claim:
24
+
25
+ - The effective `mem_current_project` and write registrations must come from the same native `npm:gentle-engram` package source. MCP adapters, namesakes, ambiguous effective registry entries and unavailable provenance do not qualify. Pi resolves duplicate tool names before exposing this registry; the check follows the effective winning registration, not historical registrations.
26
+ - An already-authorized `mem_current_project({cwd})` must complete successfully in this session with exactly the current workspace cwd, a nonempty canonical project, `git_remote` or `dir_basename` resolution, and no ambiguity, alternatives or warning/error fields.
27
+ - The subsequent native `mem_save` or `mem_session_summary` must explicitly supply that same `cwd` and `project`; saves must use project scope (or its default). Self-asserted project names alone do not qualify.
28
+
29
+ The evidence is never restored from transcript text. Session/workspace replacement, permission-mode cancellation, removed branch anchors, provider changes or an unsuccessful new resolution invalidate its applicability. No extra resolution call is launched automatically by the admission layer. Without this evidence, calls remain on ordinary contextual classification; they are not automatically allowed or denied by this adapter. Fixture tests prove these boundaries and plumbing, not a live model's interpretation of the policy.
30
+
5
31
  ## Quick path
6
32
 
7
33
  ```text
@@ -25,7 +51,7 @@ Changing the default does **not** change the current session. To return this ses
25
51
  | Mode | Behavior |
26
52
  | --- | --- |
27
53
  | **Normal** | Existing permission behavior; the default when no valid global preference exists. |
28
- | **Auto** | A separate, tool-free inference using the current session model checks eligible actions against live user intent before execution. Local reads are classified too; locality does not exempt an action from explicit restrictions. |
54
+ | **Auto** | A separate reviewer using the current session model checks eligible actions against live user intent before execution. Local reads are classified too; locality does not exempt an action from explicit restrictions. |
29
55
  | **YOLO** | Standing permission for ordinary already-authorized work and delivery; no Auto classifier. Destructive and independent consent boundaries remain. [Full YOLO details](yolo-mode.md). |
30
56
 
31
57
  ## Commands and menu
@@ -45,10 +71,16 @@ Current-session selection takes precedence over the global default. Repository s
45
71
 
46
72
  Auto intercepts eligible primary tool calls before their executors run, including `write`/`edit`, shell tools (`bash`, `bash_background`, `monitor`), custom/MCP tools, and nested calls dispatched through Pi's tool-event pipeline, such as codemode's `executeTool` calls. Local `read`, `grep`, `find`, and `ls` calls are classified, as are local edits. Workspace locality is not a safety exemption: explicit read prohibitions and read-only instructions remain part of the decision.
47
73
 
48
- The classifier receives a bounded projection of captured live interactive requests, the proposed action and relevant earlier actions. Tool output and arbitrary `role: user` transcript entries are not authority. A resumed transcript or RPC/extension message alone cannot establish the live interactive intent Auto requires. If context cannot be safely represented, the action is not run.
74
+ The classifier receives a bounded projection of captured live interactive requests, the proposed action and relevant earlier actions. For simple local script commands it can request bounded read-only inspection of exact host-selected files before deciding. This is evidence gathering, not permission to run a script. Tool output and arbitrary `role: user` transcript entries are not authority. A resumed transcript or RPC/extension message alone cannot establish the live interactive intent Auto requires. If context cannot be safely represented, the action is not run.
49
75
 
50
76
  A live request with an attached image still counts; its typed text is what the classifier receives. When a live request answers the assistant, the classifier also receives a bounded tail of the assistant message immediately before it, paired with the reply. That prompt is model-authored, untrusted context: it never authorizes anything by itself and only resolves what a short reply such as "yes" refers to. A reply authorizes only what its prompt concretely named, so authorize writes outside the project by naming the location (for example, "yes, write to `/path/.cache`"). Prompts that look secret-bearing are dropped, leaving the bare reply.
51
77
 
78
+ Earlier actions are optional evidence, not permission precedent. Auto retains at most 12 complete recent records within 4 KB, omits oversized records rather than truncating script contents, and clears this window on fresh human input. It still retains live human restrictions: a new instruction does not silently erase an earlier prohibition. A large previous write therefore does not consume every later action's classification budget.
79
+
80
+ Auto deliberately does not cache blanket approval for reads or command names. Files and script effects can change, and an explicit prohibition such as “do not read private-notes.txt” still applies to otherwise routine local work.
81
+
82
+ Answers submitted through the first-party choice and questionnaire UI are captured directly by the host, not reconstructed from tool results. Auto keeps the actual selected label or free text alongside the complete displayed question/options as **untrusted context**, so a generic “Yes” retains its original scope. Valid answers can recover a paused Auto check. Opaque choice tokens never become permission text. Cancelled, stale, noninteractive, or unrelated-session responses grant nothing; mode/session changes revoke captured UI answers. An answer always stays trusted; its question is kept while it fits the 4 KB question budget, newest first. See [host-answer provenance](auto-permission-answers.md).
83
+
52
84
  An allow verdict only returns control to the original executor and remaining guards. A denial blocks execution and asks the agent to find a materially safer action within the request—not hide the same effect behind a different tool, script, or delegate. If safe progress needs a real human decision, the agent must ask.
53
85
 
54
86
  ### Boundaries that never become Auto approval
@@ -61,9 +93,22 @@ An allow verdict only returns control to the original executor and remaining gua
61
93
 
62
94
  ## Failure behavior and cost
63
95
 
64
- Auto uses the **current session model and existing model registry/authentication** in an independent tool-free request. It does not ask the acting conversation to approve itself. Enabling or saving Auto discloses this additional inference; individual actions do not each ask for inference consent. Depending on the provider, these checks add token cost and latency.
96
+ Auto uses the **current session model and existing model registry/authentication** in an independent request. Ordinary checks remain tool-free; an eligible script check can inspect at most two complete 8 KB files, with at most three provider calls and a 60-second overall limit. Each provider call has a 20-second deadline. Only explicit script operands in simple shell commands, or `package.json` for supported package-script commands, are eligible. Compound commands, cwd-changing options (such as `bun --cwd nested check.ts`), substitutions and globs are not expanded; Auto does not crawl directories or execute a script to assess it. See [inspection limits and accounting](auto-permission-evidence.md). It does not ask the acting conversation to approve itself. Enabling or saving Auto discloses this additional inference; individual actions do not each ask for inference consent. Depending on the provider, these checks add token cost and latency.
97
+
98
+ Invalid or unavailable verdicts, errors, timeouts, cancellation and stale decisions do not authorize execution. A provider error (for example a transient `503`) is retried twice with a short backoff while the action, mode, model and session stay current; if every attempt fails, the action is not run and the block names the classifier provider as the cause. Changing the action, model, mode or session invalidates a pending decision. Three consecutive denials, twenty total denials since the latest live input, or three counted unavailable checks without a usable verdict pause further eligible actions pending a safer human instruction. Contention (`busy`), cancellation and expired decisions do not count toward the unavailable-check limit. Long sessions keep a bounded window of your messages instead of pausing; see [Long sessions in Auto](#long-sessions-in-auto).
99
+
100
+ Technical failures are not safety verdicts:
101
+
102
+ | Failure | What to do |
103
+ | --- | --- |
104
+ | Timeout | The action did not run; a timeout alone does not mean it is unsafe. Wait for the pending provider request to settle, then retry or use manual permissions. |
105
+ | Classifier still settling | Do not launch more checks to route around the pending request. This does not consume the unavailable-check limit. |
106
+ | Context over budget | Narrow the current action or use manual permissions. Repeating the same oversized input does not help. |
107
+ | Potentially sensitive evidence | Use non-sensitive evidence or manual review; do not reveal credentials to secure approval. |
108
+ | No live human request | Provide a specific instruction in the current branch. A transcript alone is not restored authorization. |
109
+ | Invalid/truncated verdict | Retry later or use manual permissions. No action is authorized by a malformed reply. |
65
110
 
66
- Invalid or unavailable verdicts, errors, timeouts, cancellation and stale decisions do not authorize execution. A provider error (for example a transient `503`) is retried twice with a short backoff while the action, mode, model and session stay current; if every attempt fails, the action is not run and the block names the classifier provider as the cause. Changing the action, model, mode or session invalidates a pending decision. Three consecutive denials, twenty total denials since the latest live input, or three consecutive unavailable checks pause further eligible actions pending a safer human instruction. Oversized retained intent requires manual permissions or a fresh scoped session rather than silently dropping restrictions.
111
+ Changing to manual permissions is always a human decision; Auto never changes mode to recover itself.
67
112
 
68
113
  The **Auto checks** status and `gentle-auto-permission` session entries account for classifier attempts separately, including denied calls that never produce a normal tool result. Available late usage is recorded when the same live session still owns it. These records are **not included in Pi's native conversation totals**; unavailable usage is not evidence that an attempt was free.
69
114
 
@@ -75,7 +120,7 @@ Live mode selection is bound to the SessionManager, session ID and canonical Git
75
120
 
76
121
  ## Limits
77
122
 
78
- Auto is contextual permission checking, **not an operating-system sandbox or a guarantee against prompt injection**. The model can misjudge an action; lexical guards and secret projection are not comprehensive inspection of scripts or tool implementations. Use constrained credentials and process/container isolation for containment.
123
+ Auto is contextual permission checking, **not an operating-system sandbox or a guarantee against prompt injection**. The model can misjudge an action; bounded file evidence, lexical guards and secret projection are not comprehensive inspection of scripts, their dependencies or tool implementations. Use constrained credentials and process/container isolation for containment.
79
124
 
80
125
  The design follows Claude Code's general Auto approach, not a claim of identical proprietary classifier behavior. Offline tests establish gating, cancellation, precedence and SDK integration mechanics—not real-provider classification accuracy.
81
126
 
@@ -215,7 +215,7 @@ This checkout declares `gentle-pi` `4.0.0` with a package-local Gentle AI `v4.1.
215
215
 
216
216
  ### Pi compatibility
217
217
 
218
- The current package requires Pi 0.99.1 or newer and Node >=22.19.0. Development tests resolve Pi through the open `>=1.0.0` development range. The private Vim editor adapter admits only the audited Pi `0.99.1`, `0.99.2`, `1.0.0`, and `1.1.0` releases; any other release keeps ordinary prompt editing until its editor is audited. Use the latest Pi release; gentle-pi does not update your installed Pi automatically. Children, including any `GENTLE_PI_AGENTS_PI` override, must emit `agent_settled`: `agent_end` records a run's output but is not completion because retries or queued continuations may follow.
218
+ The current package requires Pi 1.2.0 or newer and Node >=22.19.0. Development tests resolve Pi through the open `>=1.0.0` development range. The private Vim editor adapter admits only the audited Pi `0.99.1`, `0.99.2`, `1.0.0`, `1.1.0`, and `1.2.0` releases; any other release keeps ordinary prompt editing until its editor is audited. Use the latest Pi release; gentle-pi does not update your installed Pi automatically. Children, including any `GENTLE_PI_AGENTS_PI` override, must emit `agent_settled`: `agent_end` records a run's output but is not completion because retries or queued continuations may follow.
219
219
 
220
220
  The [`v2.6.0` release](https://github.com/Gentleman-Programming/gentle-shell/releases/tag/v2.6.0) added persistent registered worktrees and grouped `/gentle:changes` views; fuller workspace interaction details are in the [Gentle Shell reference](gentle-shell.md). It also adds named atomic `/gentle:profiles`, native review intended-untracked selection and provider continuations, and opt-in custom ask responses. Pi recognizes its global Git-managed package path; subsystems install with explicit recovery guidance when npm lifecycle work was skipped. Windows keeps child consoles hidden and fixes ownership mode; Gentle Todo keeps the next pending task visible when collapsed.
221
221
 
@@ -322,11 +322,17 @@ gentle-shell self-uninstall [--dry-run] [--yes] [--include-shared]
322
322
 
323
323
  `gentle-shell install npm:<pkg>`, `gentle-shell remove ...`, `gentle-shell list`, `gentle-shell update ...`, `gentle-shell config`, and `gentle-shell auth ...` run pi's own commands against the resolved home — the `--isolated` home by default, or your own pi home with `--link`. A launcher flag before the subcommand (`--link`, `--isolated`, `--home <path>`) still selects which home the subcommand runs against. Running `gentle-shell install npm:gentle-pi` inside the isolated home is unnecessary: the launcher already loads the Gentle Shell package itself (see "Loading the package" below).
324
324
 
325
- `gentle-shell update` and `gentle-shell list` follow that same home selection, so they inspect and update packages in whichever home the effective flag or persisted `home` config points to.
325
+ `gentle-shell list` and extension updates follow that same home selection. `gentle-shell update --extensions-only` preserves Pi's extension-only command; `--extensions`, package targets, and model updates also stay with Pi.
326
+
327
+ `gentle-shell update` updates the selected Pi through its verified npm or pnpm global owner, avoiding Pi's self-update refusal for pnpm v11 linked stores. Ownership requires successful global listing JSON, an exact canonical listed package-root/CLI match, and binding to the selected runtime. An adjacent peer (the launcher's `bundled` runtime) is supplied by the exact listed `gentle-pi` package: that Shell package is reinstalled at its current version so the manager re-resolves the peer, and a separate global Pi is never the target, because updating it leaves the peer a Shell install runs unchanged. A `PATH` or `GENTLE_SHELL_PI` runtime is supplied only by its own listed global Pi package; when that executable is a package-manager shim (pnpm's POSIX shim, a `.cmd`), Shell follows the single `dist/bundle/cli.js` it names, relative to the shim's directory, and refuses shims naming none or several. Pi and Shell are never installed together in a comma group, and unrelated packages are never reinstalled. Merely residing under `PNPM_HOME` proves nothing. Adjacent peers resolve where the package manager places them: nested under the Shell package (npm), beside it in pnpm's linked store, or in the global group's own `node_modules`. npm source/link entries are rejected. pnpm updates additionally require that the actual old global group contains only the target top-level package: aggregated listings may contain other, separate groups, but shared groups are rejected because pnpm can remove the whole old group. Unknown, malformed, failed, or ambiguous ownership refuses before installation; update source checkouts and other unsupported layouts with their supplying package manager.
328
+
329
+ After a successful install, Shell reads the supplied Pi version back from a fresh global listing and reports it, naming the previous version when it changed. An unchanged pnpm result also names pnpm's `minimumReleaseAge` setting: pnpm installs only releases older than that age (pnpm 11 defaults to 4320 minutes), so a very recent Pi release is picked up once it is old enough; Shell reports the rule rather than overriding it. Both the listing and the install run from the system temporary directory, so a project's `packageManager` pin in the current directory never changes which pnpm answers. When the version cannot be read back, the output says so and points at `gentle-shell --version`.
330
+
331
+ Prefix-bundled installations keep their existing `gentle-shell upgrade` route for Pi. `update --all` updates the verified owner before forwarding extension updates; an ownership refusal stops without mutation. Shell's `upgrade` and `self-uninstall` ownership behavior is unchanged.
326
332
 
327
333
  ### `setup` subcommand
328
334
 
329
- **Bundled Engram safety:** a fresh state uses the checksum-pinned Engram 3.3.2: before Pi starts, Shell provisions the new store's identity with that core and records it, so later launches keep using it, and a store Shell did not provision is never adopted. A running server is reused only through Engram's exact `GET /compatibility` ready contract, or when it is Shell's own store with the recorded `/health` instance and the exact bundled core version. Anything else (foreign or unknown data, provenance mismatch, a newer schema in the database header, a token-protected or unknown server) disables memory with backup/update/restart/recheck guidance, and Shell points `ENGRAM_BIN` at its own guard (`bin/engram-guard`), which fails before any store access for every gentle-engram from 0.4.1 on, whichever Pi route loaded it. No compatibility is inferred from versions. An explicit `ENGRAM_BIN` or `ENGRAM_URL` is never replaced and bypasses the guard. Every launch also pins `npm:gentle-engram@0.4.2` through Pi's own `install` command and verifies `engram.startupPolicy: 1`; a failed check is reported and Shell still starts. See [bundled startup safety](bundled-install.md#engram-startup-safety) for the full rules and known limits.
335
+ **Bundled Engram safety:** a fresh state uses the checksum-pinned Engram 3.4.0: before Pi starts, Shell provisions the new store's identity with that core and records it, so later launches keep using it, and a store Shell did not provision is never adopted. A running server is reused only through Engram's exact `GET /compatibility` ready contract, or when it is Shell's own store with the recorded `/health` instance and the exact bundled core version. Anything else (foreign or unknown data, provenance mismatch, a newer schema in the database header, a token-protected or unknown server) disables memory with an explanation of the missing safety evidence and backup precautions (matching versions or updating alone do not prove compatibility), and Shell points `ENGRAM_BIN` at its own guard (`bin/engram-guard`), which fails before any store access for every gentle-engram from 0.4.1 on, whichever Pi route loaded it. No compatibility is inferred from versions. An explicit `ENGRAM_BIN` or `ENGRAM_URL` is never replaced and bypasses the guard. Every launch also pins `npm:gentle-engram@0.4.2` through Pi's own `install` command and verifies `engram.startupPolicy: 1`; a failed check is reported and Shell still starts. See [bundled startup safety](bundled-install.md#engram-startup-safety) for the full rules and known limits.
330
336
 
331
337
  The memory decision never skips or fails bundled setup: it always publishes the verified core, runs Gentle AI and pins the companion, then only selects the Engram environment (core or guard) for setup children and the launched Pi. `--dry-run` installs nothing. Non-bundled setup keeps the existing behavior described below.
332
338
 
@@ -360,7 +366,7 @@ gentle-ai's managed Pi stack always declares `npm:gentle-pi` itself in the home'
360
366
 
361
367
  Adjacent resolution uses Pi's public ESM entry, then verifies the canonical package root, package name and declared `pi` bin. Only a genuinely absent optional peer permits PATH fallback; an invalid installed package fails rather than silently selecting another runtime. Pi AI and TUI are optional `"*"` host peers, not runtime dependencies, so managed extensions use the host's classes and registries. The launcher still gates the actual runtime version before home bootstrap. This is a one-time baseline upgrade, not an auto-updater.
362
368
 
363
- If none resolve, `gentle-shell` exits 1 naming all three options. Once a runtime is found, its `pi --version` must be at least `0.99.1` (the coding-agent peer minimum): an older version exits 1 naming the found and required versions, and unparsable `--version` output exits 1 naming the required minimum.
369
+ If none resolve, `gentle-shell` exits 1 naming all three options. Once a runtime is found, its `pi --version` must be at least `1.2.0` (the coding-agent peer minimum): an older version exits 1 naming the found and required versions, and unparsable `--version` output exits 1 naming the required minimum.
364
370
 
365
371
  ### Environment variables
366
372
 
@@ -432,8 +438,8 @@ without it. If the installer cannot attribute the existing installation to npm
432
438
  or pnpm (for example, an `npm link` checkout), it leaves that installation
433
439
  untouched and explains why; see the [installation wizard](install-wizard.md).
434
440
 
435
- Do not substitute `gentle-shell update`: that is Pi's package update, not this
436
- Gentle Shell upgrade/bootstrap operation.
441
+ Do not substitute `gentle-shell update`: that updates the selected Pi owner,
442
+ not this Gentle Shell channel upgrade/bootstrap operation.
437
443
 
438
444
  #### Behavior on supporting builds
439
445
 
@@ -1055,12 +1061,14 @@ Both use the same shape, and both are a separate artifact from `profiles.json`:
1055
1061
  ```
1056
1062
 
1057
1063
  The fullscreen shell header and Status → Project → Profile show the effective profile for the session
1058
- repository: `name (local)` for a clone-local pin, `name (repo)` for a repository declaration, or the
1059
- global active name without a suffix. Invalid or stale pins fall through to the next valid layer.
1060
- Changes made inside or outside the profiles panel appear within about two seconds while the UI
1061
- session is active; the indicator is omitted if no valid profile remains.
1064
+ repository: `name (session)` for a profile selected in this session, `name (local)` for a clone-local
1065
+ pin, `name (repo)` for a repository declaration, or the global active name without a suffix. Invalid
1066
+ or stale pins fall through to the next valid layer. A session shows the profile it froze at startup
1067
+ (see **Session profile frozen at startup** below); a selection in this session, or any default change
1068
+ in `follow` mode, appears within about two seconds while the UI session is active. The indicator is
1069
+ omitted if no valid profile remains.
1062
1070
 
1063
- For a given working directory the winner is the local pin, then the repository declaration, then no pin. With no pin at all the repository keeps the behavior described above and follows the globally active profile. `p` and `P` are toggles: pressing one on the profile that already holds that layer removes it, and either key pressed outside a Git worktree writes nothing and says so.
1071
+ For a given working directory the winner is the local pin, then the repository declaration, then the globally active profile. A session resolves these layers once, when it starts, and keeps the result. `p` and `P` are toggles: pressing one on the profile that already holds that layer removes it, and either key pressed outside a Git worktree writes nothing and says so.
1064
1072
 
1065
1073
  In a pinned repository the pinned profile governs subagent launches: the agents it names take its model and effort, and the agents it omits return to inherit (their own definition, then the default model). The globally active profile and writes made through `/gentle:models` do not reach those launches, which `/gentle:models` reports when it runs inside a pinned repository. `a` follows the same boundary: inside a pinned repository it re-pins that repository instead of writing the global routing, so a global apply can never move another repository's routing. `enter` writes nothing at all: it binds the profile to the current session, which outranks the pin for this session's launches. The panel states which layer won, names the file that holds it, and marks the profile with `(pinned)`.
1066
1074
 
@@ -1082,6 +1090,34 @@ The orchestrator sits deliberately outside the pin. Its `defaultProvider`, `defa
1082
1090
 
1083
1091
  One limitation is worth stating. When a pinned profile omits an agent, that agent's own frontmatter still applies, so a model that an earlier global apply materialized into a user agent's frontmatter can still be inherited. Frontmatter cannot be told apart from content an author wrote, so a pin does not clear it.
1084
1092
 
1093
+ ### Session profile frozen at startup
1094
+
1095
+ Every parent session uses one profile for its subagent launches, the footer and Status profile, and the Usage provider scope. All three read the same rule:
1096
+
1097
+ 1. The profile selected in this session (`name (session)`).
1098
+ 2. Otherwise, the profile the session froze when it started.
1099
+ 3. Otherwise, only in `follow` mode, the current defaults.
1100
+
1101
+ A session that never selected a profile resolves the shared defaults once, at startup: the local pin (`p`), then the repository declaration (`P`), then the globally active profile in `profiles.json`. It keeps that profile's name, routing, and origin (`local`, `repo`, or `global`). Changing a pin, the repository declaration, the active profile, or the profile's content afterwards affects new sessions only, the same way Pi's "set as default" for the orchestrator model leaves open sessions alone. Outside a Git worktree the repository declaration is still read, as non-Git writer admission does.
1102
+
1103
+ **Behavior change for unpinned sessions.** Without a pin or declaration, launches now use the globally active profile from `profiles.json`, replacing the materialized `subagents.json` routing wholesale like a pin does. Before, an unpinned launch read the materialized stores. When nothing is pinned and no profile is active, the session freezes "no profile" and keeps routing through the materialized stores, as before; a pin added later does not change it.
1104
+
1105
+ **Drift notice.** On each subagent launch, a session with a frozen profile compares it with the current defaults of its own directory, without applying them. When they differ, it shows one line:
1106
+
1107
+ ```text
1108
+ el Gentleman: the default profile changed to "local" (local), this session keeps "frontier" (local). Press Enter on a profile in /gentle:profiles to adopt it.
1109
+ ```
1110
+
1111
+ The notice appears once per distinct change: further launches stay quiet until the defaults change again. The same profile reached through another layer is not a change; an edit of the profile's content is. Sessions with a profile selected in the session, `follow` sessions, and subagent children never show it.
1112
+
1113
+ **Foreign repositories.** A selected or frozen profile belongs to the session and also routes launches into a foreign `repository_root`. A session frozen without a profile leaves a foreign repository on its own local pin and repository declaration; the globally active profile does not apply there.
1114
+
1115
+ **`follow` mode.** Start Pi with `GENTLE_PI_PROFILE_FOLLOW=1` (CI and headless runs) to skip the freeze: every launch resolves the current defaults of its target, and no drift notice is shown. Any other value, or no value, freezes. Selecting a profile with Enter still makes it the session's profile. Limitation: the orchestrator model is chosen when the session starts and does not follow later default changes, so after one the orchestrator and the subagents can follow different profiles.
1116
+
1117
+ **Subagent children** never freeze, compare, or warn: their model comes from the parent's launch.
1118
+
1119
+ **Current limits.** The frozen profile is kept in memory for the life of the Pi process, so `/reload` keeps it. Resuming a session in a new process (`--resume`, `--continue`, `--session`), `/new`, and `/fork` freeze again from the defaults current at that moment, and the drift notice memory starts empty, so a pending change is announced once more. Persisting the frozen profile in the session file is planned.
1120
+
1085
1121
  ## Commands
1086
1122
 
1087
1123
  | Command | What it does |
@@ -1209,7 +1245,7 @@ The frame labels INSERT, NORMAL, VISUAL (characterwise), or VISUAL LINE (linewis
1209
1245
 
1210
1246
  **Deliberate `/` divergence from Claude Code:** NORMAL `/` hands off to **Pi's native slash commands and skills**, enters INSERT, and inserts `/` at the existing cursor. Pi offers slash completion only at the start of the first line; elsewhere it inserts a literal slash without moving or replacing the draft. There is **no reverse prompt-history search**. Pi's explicit history shortcuts still work, transferring to INSERT first. Unknown NORMAL printable input, encoded text and bracketed paste do not silently insert; application shortcuts can transfer to INSERT before acting.
1211
1247
 
1212
- This is a bounded command subset, not full Claude Code/Vim parity. The private editor adapter supports only the audited Pi coding-agent/TUI `0.99.1`, `0.99.2`, `1.0.0`, and `1.1.0` package pairs. The two 0.99 releases have byte-identical editor and undo-stack sources. Pi 1.0.0 and 1.1.0 are separately audited against the touched state, paste/history, cursor/layout, autocomplete and undo-snapshot contracts; current bundled and unbundled 1.1.0 tests prove identity, edit/undo, paste, selection, wrapping/scroll and autocomplete behavior, not old/new byte identity. Fabricated metadata tests preserve exact-version admission coverage for the older audited releases. Version metadata must come from a canonical candidate host package root whose actual `CustomEditor` and `Editor` classes match the loaded classes, never from the extension's local metadata or CLI path alone. Unknown versions, mismatched prototypes, or invalid layouts fail closed: a single compatibility warning is shown and the prompt continues with ordinary editing instead of silently entering inert NORMAL mode. Operations that would cross a registered collapsed paste marker, or encounter duplicate occurrences of a registered marker ID, are rejected without editing it. Visual highlighting relies on Pi's render layout and may be omitted if its geometry cannot be validated. No live-terminal proof of every layout or complete parity is claimed.
1248
+ This is a bounded command subset, not full Claude Code/Vim parity. The private editor adapter supports only the audited Pi coding-agent/TUI `0.99.1`, `0.99.2`, `1.0.0`, `1.1.0`, and `1.2.0` package pairs. The two 0.99 releases have byte-identical editor and undo-stack sources. Pi 1.0.0, 1.1.0 and 1.2.0 are separately audited against the touched state, paste/history, cursor/layout, autocomplete and undo-snapshot contracts. Pi 1.2.0's paired APC fake-cursor markers remain intact so the host can suppress software cursors when hardware cursors are visible; selection inversion and empty-prompt hints support both these markers and legacy SGR cursors. Current bundled and unbundled 1.2.0 tests prove identity, edit/undo, paste, selection, wrapping/scroll and autocomplete behavior, not old/new byte identity. Fabricated metadata tests preserve exact-version admission coverage for the older audited releases. Version metadata must come from a canonical candidate host package root whose actual `CustomEditor` and `Editor` classes match the loaded classes, never from the extension's local metadata or CLI path alone. Unknown versions, mismatched prototypes, or invalid layouts fail closed: a single compatibility warning is shown and the prompt continues with ordinary editing instead of silently entering inert NORMAL mode. Operations that would cross a registered collapsed paste marker, or encounter duplicate occurrences of a registered marker ID, are rejected without editing it. Visual highlighting relies on Pi's render layout and may be omitted if its geometry cannot be validated. No live-terminal proof of every layout or complete parity is claimed.
1213
1249
 
1214
1250
  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.
1215
1251
 
@@ -0,0 +1,39 @@
1
+ # Session profile record format
2
+
3
+ `lib/session-profile-persistence.ts` defines the optional session profile v1
4
+ codec and pure replay contract. Adding this module does not change Enter,
5
+ startup, routing, shared defaults, or live orchestrator behavior.
6
+
7
+ ## Payloads
8
+
9
+ A custom entry uses `customType: "gentle-pi.session-profile/v1"` and one payload:
10
+
11
+ ```json
12
+ {"kind":"bind","origin":"user","name":"work","modelProfiles":{"worker":{"model":"provider/model"},"orchestrator":{"thinking":"high"}}}
13
+ {"kind":"clear"}
14
+ ```
15
+
16
+ The encoder creates only explicit `user` selections. The decoder accepts the
17
+ closed origin set `user`, `local`, `repo`, and `global`; this does not implement
18
+ inherited startup persistence or follow mode. A bound empty snapshot is not a
19
+ clear. Snapshots are detached, known invalid fields reject the whole binding,
20
+ and unknown extra fields carry no routing meaning. Existing route normalization
21
+ supports legacy model strings and `effort`; valid `thinking` takes precedence.
22
+
23
+ ## Replay
24
+
25
+ `replaySessionProfileBranch` requires entries already corroborated on disk,
26
+ ordered oldest to newest on the active branch. Supplying `getBranch()` alone
27
+ does not establish persistence. The newest profile-family entry is terminal:
28
+
29
+ | Result | Meaning |
30
+ | --- | --- |
31
+ | `absent` | No profile-family entry on the supplied branch. |
32
+ | `bound` | A detached, validated binding, including an empty snapshot. |
33
+ | `cleared` | An explicit clear; fallback is a later consumer's decision. |
34
+ | `invalid` | Invalid v1 data; never resurrect an earlier binding. |
35
+ | `unsupported` | Unknown profile-family identifier, regardless of payload. |
36
+
37
+ Replay neither appends nor publishes bindings and never applies a model or
38
+ thinking level. A subsequent disk-reader slice supplies corroboration; this
39
+ codec alone cannot establish it.
package/docs/yolo-mode.md CHANGED
@@ -29,7 +29,7 @@ The live YOLO toggle does not save preferences. The separate **Default permissio
29
29
 
30
30
  YOLO supplies standing human permission for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation. Its active-only system instruction qualifies the default commit/push/PR confirmation clauses; it also tells the primary agent to make ordinary reversible implementation choices without needless interviews.
31
31
 
32
- Only a single plain `git push` can skip the shell guard's routine default confirmation. Simple remote/ref arguments and `-u`/`--set-upstream` are eligible. Compound commands, wrappers, repository-changing options and other push options conservatively retain normal guard behavior. Full command evaluation, including the recognized data-loss guards, runs first. Explicit push confirmations or blocks in either configuration layer remain restrictions, including when legacy persistent autonomy or its environment override would ignore them. YOLO never changes persistent `autonomousMode`.
32
+ The shell guard's routine default confirmation is skipped for reversible delivery only. Every guarded segment of the command is judged on its own, so `git commit -m "..." && git push -u origin feature`, `git push && gh pr create --fill`, `git -C <dir> push origin main` and `git fetch && git rebase origin/main && git push` run without a prompt, while the other segments keep their ordinary guard (`git push && rm -rf data` still asks for the `rm`). A push segment is eligible with plain remote/ref arguments, `src:dst` refspecs with both sides present, and the options `-u`/`--set-upstream`, `-q`/`--quiet`, `-v`/`--verbose`, `--tags`, `--follow-tags`, `--no-verify` and `--porcelain`. A rebase segment is eligible with plain refs, `--onto`, `--continue`, `--abort`, `--skip` and `--autostash`; it is recoverable from the reflog. Anything that deletes or rewrites a remote ref, publishes, or runs arbitrary commands never qualifies: `--delete`/`-d`, `:branch` and `+branch` refspecs, `--mirror`, `--prune`, `--all`, every force spelling (hard-blocked), `--receive-pack`/`--exec`, `--repo`, `-o`, `sudo`, shell wrappers such as `sh -c`, interactive or `--exec` rebases, `git branch -D`, `pi remove` and `npm publish`. Full command evaluation, including the recognized data-loss guards, runs first. Explicit push or rebase confirmations or blocks in either configuration layer remain restrictions, including when legacy persistent autonomy or its environment override would ignore them. YOLO never changes persistent `autonomousMode`.
33
33
 
34
34
  ## What still requires a decision
35
35
 
@@ -79,6 +79,7 @@ Children receive the lightweight `child-safety.ts` entry alongside `child-contex
79
79
  ### Detection limitations
80
80
 
81
81
  - This is deterministic lexical recognition, **not** shell or SQL evaluation. Aliases, functions, dynamically assembled commands, variable/command expansion inside quoted payloads, unusual wrapper flags, complex heredocs and deeply nested wrappers can evade recognition. Simple recursive removal of a variable target is guarded, but its expanded target cannot reliably be identified as a hard-deny path.
82
+ - Guarded-command confirmations and hard-denied patterns ignore text that only appears as an argument of a known data command (`grep`, `rg`, `cat`, `echo`, `printf`, `head`, `tail`, `jq`, `sort`, `uniq`, `wc` and similar), including the body of a real heredoc fed to one. Searching a file for a push or printing a reset command does not open a dialog. The exemption applies only when every command in the line is such a data command or an inert one (`cd`, `pwd`, `test`, `mkdir`, `touch`), optionally after plain `VAR=value` assignments or a flagless `env`. Anything else anywhere in the line keeps the full lexical match: any other command or wrapper (`sed`, `sh`, `python`, `awk`, `xargs`, `sudo`, `exec`, `at`, `./script`), command or process substitution, a `<<` that is not an unquoted heredoc, and quoting the tokenizer cannot account for. `sed` is excluded because GNU sed can execute its script.
82
83
  - Scripts, migration files, redirected SQL files, arbitrary interpreter programs (Python/Node/etc.), encoded payloads and remote execution are not inspected. Non-recursive wildcard deletion, file truncation, other database clients and nontrivial tautological SQL predicates are not comprehensively classified. A `WHERE` clause is not proof of bounded impact.
83
84
  - Only `bash` tool calls reach this data-loss boundary, including nested calls dispatched through Pi's tool-event pipeline. Direct process execution, user shell commands, MCP tools and other tools are not covered by these hooks.
84
85
  - The older primary regex safeguards remain authoritative and may be more conservative about text than the shared recognizer. Missing or explicitly overridden child extension paths remove the lightweight boundary; a valid package installation must include the safety entry.
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
3
  import { Container, Input, isKeyRelease, matchesKey, Text, type KeybindingsManager, type TuiMouseEvent } from "@earendil-works/pi-tui";
4
4
  import { type Static, Type } from "typebox";
5
+ import { captureChoiceAnswer } from "../lib/auto-permission-answers.ts";
5
6
  import { NativeChoiceList } from "../lib/native-choice-list.ts";
6
7
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
7
8
  import { isInteractiveMode, isInteractiveRpcHost } from "../lib/rpc-host.ts";
@@ -226,6 +227,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
226
227
  parameters: ChoiceParamsSchema,
227
228
  executionMode: "sequential",
228
229
  async execute(_toolCallId, params: ChoiceParams, _signal, _onUpdate, ctx) {
230
+ const captureAnswer = captureChoiceAnswer(ctx, _toolCallId, params, _signal);
229
231
  if (ctx.mode !== "tui") {
230
232
  if (!isInteractiveRpcHost(ctx.mode, process.env)) {
231
233
  throw new Error("ask_user_choice is unavailable outside the interactive TUI");
@@ -238,6 +240,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
238
240
  finally {
239
241
  pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
240
242
  }
243
+ captureAnswer(rpcSelection);
241
244
  return choiceToolResult(params, rpcSelection);
242
245
  }
243
246
 
@@ -302,6 +305,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
302
305
  pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
303
306
  }
304
307
 
308
+ captureAnswer(selection);
305
309
  return choiceToolResult(params, selection);
306
310
  },
307
311
  renderCall(args, theme) {
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
+ import { captureQuestionnaireAnswers } from "../lib/auto-permission-answers.ts";
4
5
  import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
5
6
  import { type QuestionData, type QuestionParams, QuestionParamsSchema } from "../lib/questionnaire/schema.ts";
6
7
  import {
@@ -264,11 +265,14 @@ export default function askUserQuestion(pi: ExtensionAPI): void {
264
265
  ): Promise<QuestionnaireToolResult> {
265
266
  const error = validateQuestionnaire(params);
266
267
  if (error) return invalidQuestionnaireResult(error);
268
+ const captureAnswers = captureQuestionnaireAnswers(ctx, _toolCallId, params, _signal);
267
269
  if (ctx.mode !== "tui") {
268
270
  if (!isInteractiveRpcHost(ctx.mode, process.env)) return unavailableResult();
269
271
  try {
270
272
  pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: true });
271
- return await askThroughDialogs(ctx, params);
273
+ const result = await askThroughDialogs(ctx, params);
274
+ if (!result.details.cancelled) captureAnswers(result.details.answers);
275
+ return result;
272
276
  }
273
277
  finally {
274
278
  pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: false });
@@ -307,6 +311,7 @@ export default function askUserQuestion(pi: ExtensionAPI): void {
307
311
  details: { cancelled: true },
308
312
  };
309
313
  }
314
+ captureAnswers(selection.answers);
310
315
  return {
311
316
  content: [{ type: "text", text: answersText(selection.answers) }],
312
317
  details: { answers: selection.answers },
@@ -27,7 +27,7 @@ import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
27
27
  import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
28
28
  import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
29
29
  import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
30
- import { readSessionProfileBinding, sessionOrPinModelProfiles } from "../lib/session-profile-binding.ts";
30
+ import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt } from "../lib/session-profile-freeze.ts";
31
31
  import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
32
32
  import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
33
33
  import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
@@ -61,7 +61,6 @@ import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
61
61
  import { openInExternalEditor } from "./gentle-shell.ts";
62
62
  import { gentlePiConfigHome } from "../lib/agent-home.ts";
63
63
  import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
64
- import { resolveProfilePin, resolveUnversionedProjectProfile } from "../lib/agent-profile-pin.ts";
65
64
  import { allowedEditSurfaces, inheritAllowedEditSurfaces, isBoundedWriter, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
66
65
  import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
67
66
  import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
@@ -1570,21 +1569,26 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1570
1569
  if (isGenericBoundedWriter(agent.name) && !current()) throw new Error("Writer session Git authority changed before admission.");
1571
1570
  let admittedModel: string | undefined;
1572
1571
  const surfaces = allowedEditSurfaces(prompt, context);
1573
- // gentle-shell#1064 slice 2: the binding is read once per task request,
1574
- // before admission, so the admitted model and the launch routing resolve
1575
- // the same session layer and can never disagree about it (#1558: a bound
1576
- // session used to kill its own non-git writer mid-preparation because
1577
- // admission still read only the pin/global layers).
1578
- const sessionBinding = readSessionProfileBinding(ctx.sessionManager.getSessionId());
1572
+ // gentle-shell#1064 slice 2: the session profile is resolved once per task
1573
+ // request, before admission, so the admitted model and the launch routing
1574
+ // resolve the same session layer and can never disagree about it (#1558: a
1575
+ // bound session used to kill its own non-git writer mid-preparation because
1576
+ // admission still read only the pin/global layers). Slice 3b-ii: the same
1577
+ // read covers the frozen inherited profile and `follow` mode.
1578
+ const profileConfigHome = gentlePiConfigHome(deps.env);
1579
+ const sessionProfile = resolveSessionProfile({
1580
+ sessionId: ctx.sessionManager.getSessionId(),
1581
+ cwd: originalCwd,
1582
+ configHome: profileConfigHome,
1583
+ resolveWorktree: deps.resolveWorktree,
1584
+ env: deps.env,
1585
+ });
1579
1586
  if (!resume && isGenericBoundedWriter(agent.name) && surfaces?.some(isDevelopmentSurface) && !deps.resolveWorktree(originalCwd, originalCwd) && repositoryRoot === undefined) {
1580
1587
  const root = safeBootstrapDirectory(originalCwd);
1581
1588
  if (!root || (workspaceRoot !== undefined && (!isAbsolute(workspaceRoot) || safeBootstrapDirectory(workspaceRoot) !== root))) throw new Error("Writer bootstrap requires the original safe project root.");
1582
1589
  const config = withPinnedModelProfiles(
1583
1590
  loadAgentsConfig(roots(ctx)),
1584
- sessionOrPinModelProfiles(
1585
- sessionBinding?.modelProfiles,
1586
- resolveUnversionedProjectProfile(root, gentlePiConfigHome(deps.env))?.modelProfiles,
1587
- ),
1591
+ sessionProfileRoutingAt(sessionProfile, { cwd: root, configHome: profileConfigHome, resolveWorktree: deps.resolveWorktree }),
1588
1592
  );
1589
1593
  const model = resolveAgentProfile(agent, config).model ?? ctx.model;
1590
1594
  const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
@@ -1630,22 +1634,34 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
1630
1634
  const pinIdentity: WorktreeResolver = foreign ? resolveSessionWorktree : target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
1631
1635
  ? () => parentIdentity
1632
1636
  : deps.resolveWorktree;
1633
- // gentle-shell#1064 slice 1: a parent-session profile binding outranks the
1634
- // pin layers for launches from that session (`session → p → P → global`),
1635
- // with the same wholesale-replacement contract as the pin. The binding is
1637
+ // gentle-shell#1064: the session profile outranks the shared defaults for
1638
+ // launches from that session (`session → frozen p → P → G`), with the same
1639
+ // wholesale-replacement contract as the pin. Explicit and frozen profiles
1640
+ // belong to the session and apply to every target, a foreign repository
1641
+ // included; only `follow` reads the defaults of the target. The profile is
1636
1642
  // resolved here, at task-request creation, so queued and running children
1637
1643
  // keep the routing frozen into their requests even if the session rebinds.
1638
1644
  const config = withPinnedModelProfiles(
1639
1645
  loadAgentsConfig(roots(ctx)),
1640
- sessionOrPinModelProfiles(
1641
- sessionBinding?.modelProfiles,
1642
- resolveProfilePin({
1643
- cwd: target ?? parentCwd,
1644
- configHome: gentlePiConfigHome(deps.env),
1645
- resolveWorktree: pinIdentity,
1646
- })?.modelProfiles,
1647
- ),
1646
+ sessionProfileRoutingAt(sessionProfile, {
1647
+ cwd: target ?? parentCwd,
1648
+ configHome: profileConfigHome,
1649
+ resolveWorktree: pinIdentity,
1650
+ foreignRepository: foreign,
1651
+ }),
1648
1652
  );
1653
+ // Drift is measured against the defaults of the session's own directory,
1654
+ // where the profile was frozen, never against a delegated target.
1655
+ if (ctx.hasUI) {
1656
+ const drift = inheritedProfileDriftNotice({
1657
+ sessionId: ctx.sessionManager.getSessionId(),
1658
+ cwd: parentCwd,
1659
+ configHome: profileConfigHome,
1660
+ resolveWorktree: deps.resolveWorktree,
1661
+ env: deps.env,
1662
+ });
1663
+ if (drift !== undefined) ctx.ui.notify(drift, "info");
1664
+ }
1649
1665
  const profile = resolveAgentProfile(agent, config);
1650
1666
  if (admittedModel !== undefined) {
1651
1667
  const model = profile.model ?? ctx.model;
@@ -2243,6 +2259,21 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
2243
2259
  publishActivity();
2244
2260
  });
2245
2261
  pi.on("session_start", async (event, ctx) => {
2262
+ // gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
2263
+ // profile binding freezes `p → P → G` now. This handler is parent-only (a
2264
+ // child returns before registering it) and runs for every reason Pi
2265
+ // reports, including `startup` for --resume/--continue/--session. The
2266
+ // freeze is in-memory and idempotent per session id: /reload keeps it, a
2267
+ // new process freezes again until slice 3b-i persists it as an entry.
2268
+ try {
2269
+ freezeSessionProfileAtStartup({
2270
+ sessionId: ctx.sessionManager.getSessionId(),
2271
+ cwd: ctx.sessionManager.getCwd(),
2272
+ configHome: gentlePiConfigHome(deps.env),
2273
+ resolveWorktree: deps.resolveWorktree,
2274
+ env: deps.env,
2275
+ });
2276
+ } catch { /* A launch freezes lazily through the same resolver. */ }
2246
2277
  stateCache.load(ctx.sessionManager);
2247
2278
  // A resumed, reloaded, or replaced session starts with an empty completion
2248
2279
  // queue so nothing pending from another session can replay here.