gentle-pi 3.2.0 → 3.2.1

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.
@@ -123,14 +123,19 @@ Keep one writer and a short synthesized handoff. Delegation is mandatory at the
123
123
 
124
124
  #### Mandatory Delegation Triggers
125
125
 
126
- These are parent-orchestrator routing boundaries. Use the smallest useful topology and keep the safety machinery behind the outcome-first interaction. Do not pass these rules to child agents as permission to orchestrate.
127
-
128
- 1. **Bounded read rule**: read 1–3 files inline to decide or verify.
129
- 2. **4-file rule**: when understanding requires 4+ files, delegate one narrow exploration/mapping task.
130
- 3. **Write rule**: keep one mechanical, already-understood file inline only when it needs no research or unresolved design work; delegate one writer for 2+ non-trivial files.
131
- 4. **Context rule**: delegate reading that prepares a write and broad research/context compression.
132
- 5. **Per-action rule**: tests, builds, and installs may use fresh workers without changing the implementation route or creating SDD state.
133
- 6. **Optional SDD rule**: retain SDD only after an explicit request or accepted proposal. Resolve organic uncertainty with optional research and a concise proposal only for a real decision; risk alone never forces SDD.
126
+ These are parent-orchestrator routing boundaries; do not pass these rules to child agents as permission to orchestrate. These triggers are mandatory, not advisory. When one fires, stop and delegate through the runtime's subagent mechanism before continuing; executing past a fired trigger inline is a routing defect even if the work succeeds. Delegation keeps the parent context thin enough to orchestrate; it does not slow the work down.
127
+
128
+ 1. **Mapping trigger (4-file rule):** when understanding the work requires 4 or more files, delegate one narrow exploration or mapping task before deciding or writing anything.
129
+ 2. **Writer trigger (Multi-file write rule):** when implementation touches 2 or more non-trivial files, delegate one bounded writer instead of editing them inline.
130
+ 3. **Incident rule:** after wrong `cwd`, accidental repository/worktree mutation, failed merge recovery, confusing test command, or environment workaround, stop and diagnose the incident separately before resuming.
131
+ 4. **Long-session backstop (Long-session rule):** after about 20 tool calls, 5 exploratory reads, or 2 non-mechanical edits without any delegation, pause and delegate the next bounded unit of work.
132
+ 5. **Verification rule** (gentle-pi#661/#662, RDD-aware): executing or delegating verification commands goes to `gentle-ai-verify`; only the 1–3-file read-only check stays inline. The normative on/off/unknown routing is stated once under Pi Trigger Runtime Bindings below; reference it, do not restate it.
133
+
134
+ **Preparation trigger:** reading that prepares a write, and broad research or context compression, delegate together with or ahead of the write instead of filling the parent context.
135
+
136
+ **Route declaration:** for substantial work, record the chosen route per task (inline or delegated) and the trigger evidence in the feature document, so skipped delegation is observable instead of silent.
137
+
138
+ These triggers never select SDD and never create SDD artifacts; they only choose between direct inline and delegated direct inside the organic flow.
134
139
 
135
140
  For bounded multi-file writes, prefer the installed package-owned `gentle-ai-worker`, then a user-configured `worker`. If neither worker definition exists, fall back to the native `Agent` even when `subagent_*` tools are available. If no delegation mechanism is available, stop and explain the blocker. Judgment Day phase roles are never generic fallbacks. If the generic writer chain is unavailable, use the documented native generic fallback or stop.
136
141
 
@@ -56,8 +56,8 @@ Mandatory Delegation Triggers — once fired, delegate through the best availabl
56
56
  1. **4-file rule** — 4+ files to understand → delegate a scout/mapping task.
57
57
  2. **Multi-file write rule** — 2+ non-trivial files touched → delegate one writer.
58
58
  3. **Incident rule** — diagnose wrong cwd/worktree/git/tooling incidents separately before resuming work.
59
- 4. **Verification rule** — executing/delegating verification commands `gentle-ai-verify`; only the 1-3-file read-only check stays inline.
60
- 5. **Long-session rule** — ~20 tool calls, 5 exploratory reads, or 2 non-mechanical edits without delegation → pause and delegate.
59
+ 4. **Long-session rule** — ~20 tool calls, 5 exploratory reads, or 2 non-mechanical edits without delegation → pause and delegate.
60
+ 5. **Verification rule** — executing/delegating verification commands `gentle-ai-verify`; only the 1-3-file read-only check stays inline.
61
61
 
62
62
  {{GENTLE_PI_BACKGROUND_POLICY}}; rules: the background-subagents block in the delegation contract.
63
63
 
@@ -15,7 +15,7 @@ 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.0` with a package-local Gentle AI `v3.1.0` pin; this is not a claim that `3.2.0` is published.
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.
19
19
 
20
20
  ## Shell interactions and runtime behavior
21
21
 
@@ -30,7 +30,7 @@ The sidebar Status card also shows `Profile` in its Model section when the profi
30
30
  The status bar replaces pi's three-line footer with a single line of segments:
31
31
 
32
32
  ```text
33
- ✿ gentle-pi ⟡ ~/work/gentle-pi main ⟡ gpt-5.5 · medium ⟡ ctx ▰▰▰▰▱▱▱▱ 45% ⟡ $9.49 sub ⟡ MCP: 3 servers enabled Release notes
33
+ ✿ gentle shell ⟡ ~/work/gentle-pi main ⟡ gpt-5.5 · medium ⟡ ctx ▰▰▰▰▱▱▱▱ 45% ⟡ $9.49 sub ⟡ MCP: 3 servers enabled Release notes
34
34
  ```
35
35
 
36
36
  - Context is a gauge, not a number. It turns amber at 80% and red at 95%; after compaction it shows `?%` until the next response.
@@ -90,15 +90,25 @@ The separate `session_worktree_register` tool still registers canonical same-clo
90
90
 
91
91
  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
92
 
93
- Subscription usage shows in the bar after the cost, and `/gentle:usage` opens a panel with every window per provider:
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:
94
94
 
95
95
  ```text
96
- ✿ gentle-pi ⟡ … ⟡ $9.49 sub ⟡ codex 5h ▰▰▰▰▰▱▱▱ 62% · week 31%
96
+ ✿ gentle shell ⟡ … ⟡ $9.49 sub ⟡ codex 5h ▰▰▰▰▰▱▱▱ 62% · week 31%
97
+ ```
98
+
99
+ The panel rows a provider reports its windows with:
100
+
101
+ ```text
102
+ ✿ nan · updated just now
103
+ deepseek-v4-flash ▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱ 26% · resets in 12d 17h
104
+ glm5.3-flash ▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱ 11% · resets in 12d 17h
97
105
  ```
98
106
 
99
107
  - For Codex, usage comes from the same account usage endpoint the Codex CLI reads, using the OAuth token pi already holds. It is fetched at session start, at most every 5 minutes after a turn, and on `r` in the panel. Rate-limit headers on SSE responses are picked up too.
100
108
  - For Claude Pro/Max, usage arrives in the rate-limit headers of every response, so the 5h and weekly windows appear after the first turn.
101
- - The bar names the subscription it shows (`codex`, `claude`) and always follows the active model. The panel puts the active provider first, marked with the petal, and says why it has no data when it does not: API-key providers have no subscription windows, Claude reports after the first response, Codex waits for a fetch.
109
+ - For NaN Cloud, usage comes from the quota endpoint the official dashboard reads, with the same API key pi already holds. Each metered model reports one allowance for the billing period, and that window carries no label: the model id names it in the bar and the reset text says what it is in the panel. A model that also reports a rolling window shows that one labeled next to it (`4h`), which today's payload does not send; percentages are tokens used over the allowance, exactly as the dashboard draws them, and the allowance is the full-period cap (`fullCap`) whenever the model reports a positive one, because `cap` alone is the prorated allowance of the period in progress. It is fetched under the same 5-minute rule as Codex, counted per provider so a switch fetches the provider it switched to, refuses redirects so the bearer cannot be replayed to another origin, and keeps no cached copy. The endpoint sits outside NaN's published OpenAPI, so the parser reads it defensively: a model that reports no allowance is skipped, as the dashboard skips it, while a metered model whose usage cannot be read fails the whole read, so a partial payload never replaces a complete snapshot with a cheaper-looking one. A session that already has a snapshot keeps the last valid one through a malformed payload or a failed fetch, and the pending note appears only while there is nothing to draw.
110
+ - The bar names the subscription it shows (`codex`, `claude`, a NaN model) and always follows the active model. A provider with per-model allowances draws the session model's own meter, falling back to its family and then to the account total, never to whichever model the payload happens to list first — and that holds for a payload that reports a single metered model too, because one allowance is still per-model data rather than a reason to echo the first entry. The panel puts the active provider first, marked with the petal, and says why it has no data when it does not: API-key providers have no subscription windows, Claude reports after the first response, Codex and NaN wait for a fetch. A provider without per-model allowances keeps its single aggregate line in the sidebar, unchanged.
111
+ - A provider with per-model allowances is ordered by family on both surfaces: a family stays together, the family that consumes most comes first, and the models inside it follow the same rule, most used first. There are no `total` rows anywhere — an aggregate nobody can act on only costs space — so the account and family totals survive only as the bar's fallback name when the session model holds no allowance of its own (`nan total`). An allowance row leaves the window label empty and prints `name meter percent`, while a labeled sub-window (`4h`) keeps its column, and the reset a window reports rides that same line after a `·`; a window without one ends at its percentage, never on a dangling separator. The sidebar's Usage group prints those same rows in that same order, so the breakdown does not require opening the panel, and stops at the percentage: the reset dates stay in the panel. A row whose windows all round to `0%` is dropped from that group — an allowance nobody has touched yet tells the reader nothing the missing row does not — and the same rule retires the aggregate line of a provider without raw allowances once every window it shows sits at `0%`; the bar and the panel keep printing it, so a zeroed subscription is still verifiable there.
102
112
  - Only the plan name and the windows are kept; account details in the payload are discarded.
103
113
  - Gauges turn amber at 80% and red at 95%, like the context gauge.
104
114
 
@@ -105,7 +105,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
105
105
  | **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
106
  | **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
107
107
  | **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.1.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. |
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
109
  | **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
110
110
 
111
111
  ## Native pointer regions
@@ -142,7 +142,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
142
142
 
143
143
  ### Source checkout
144
144
 
145
- This checkout prepares `gentle-pi` `3.2.0`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.1.0`, distinct from the published `v2.6.0` pairing.
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
146
 
147
147
  The native SDD status consumer accepts both the pinned producer's legacy
148
148
  `apply`/`verify`/`remediate`/`archive` instruction record and the classical
@@ -153,7 +153,7 @@ Unknown or incomplete instruction records still fail closed.
153
153
  The Pi runtime now uses native status exclusively for SDD and retires standalone
154
154
  sync. The full chain follows completed apply to archive, where applicable delta
155
155
  specs are composed; verification remains explicitly invokable. With the current
156
- 3.1.0 pin, native still requires verification and its emitted evidence requirements;
156
+ 3.2.1 pin, native still requires verification and its emitted evidence requirements;
157
157
  a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
158
158
  those exact instructions without overriding readiness or inventing legacy evidence.
159
159
  Classical direct-archive behavior is compatibility-tested with an identified
@@ -187,7 +187,7 @@ pi install npm:gentle-pi@2.6.0
187
187
 
188
188
  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
189
 
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.1.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.1.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.
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
191
 
192
192
  Recommended companion packages:
193
193
 
@@ -361,13 +361,13 @@ flowchart TD
361
361
 
362
362
  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
363
 
364
- For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.1.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.
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
365
 
366
366
  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
367
 
368
368
  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
369
 
370
- Once the source checkout's pinned gentle-ai runtime (currently v3.1.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.
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
371
 
372
372
  ### FINALIZE wrapper input
373
373
 
@@ -8,8 +8,8 @@ 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, and returns raw final bytes or a typed transport error. |
12
- | Host coordinator | Executes the exact Go-issued materialize/submission tokens, launches the adapter, and submits its untouched result only through the supplied token. |
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). |
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
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.
@@ -18,10 +18,17 @@ The adapter does not parse bindings, select work, rebuild prompts, inspect repos
18
18
 
19
19
  1. Gentle AI emits an opaque materialization or submission token for the selected Pi runtime.
20
20
  2. The host coordinator executes that exact token and gives only the materialized bytes to the adapter.
21
- 3. The adapter returns raw output bytes to the coordinator.
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
22
  4. The coordinator sends those bytes only through the exact Go-issued submission token.
23
23
 
24
- A typed Pi transport refusal fails closed. The coordinator reports the refusal without an agentless lifecycle fallback, local retry policy, synthetic result, or alternate approval path.
24
+ ## Reviewer launch selection (user-owned)
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:
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.
30
+
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.
25
32
 
26
33
  ## Dynamic contract delivery
27
34
 
@@ -29,16 +36,18 @@ Package static assets intentionally omit lifecycle instructions, candidate routi
29
36
 
30
37
  ## Integration constraints
31
38
 
32
- - Keep Pi transport opaque: raw prompt bytes in, raw result bytes or a typed error out.
39
+ - Keep Pi transport opaque: raw prompt bytes in, the event stream's assistant text or a typed, evidenced error out.
33
40
  - 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.
34
42
  - Treat a transport failure as unavailable evidence, never as an approval, completion, or permission to substitute a local workflow.
35
43
  - Keep command safety and user interaction in the host, without interpreting provider authority state.
36
44
  - Keep durable review state, admissions, correction accounting, and approvals in Gentle AI. Keep delivery decisions in ordinary repository policy.
37
45
 
38
46
  ## Review checklist
39
47
 
40
- - [ ] The adapter surface is still `Buffer → Buffer/error`.
48
+ - [ ] The adapter surface is still `Buffer → Buffer/error` (the output is the pi event stream's assistant text; failures carry typed evidence).
41
49
  - [ ] 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.
42
51
  - [ ] Typed transport refusal remains fail-closed.
43
52
  - [ ] No package code or static prompt uses review authority to decide, authorize, rewrite, or block delivery commands.
44
53
 
@@ -136,6 +136,7 @@ import {
136
136
  reviewHostRelayUnachievableDetail,
137
137
  reviewHostRelayUnachievableReason,
138
138
  reviewProviderRoleVectorSlots,
139
+ resolveReviewHostRelayExtensionPaths,
139
140
  resolveReviewHostRelaySubmission,
140
141
  runReviewHostRelayReviewerGroup,
141
142
  runReviewHostRelaySlot,
@@ -1274,7 +1275,7 @@ Organic Driven Development (ODD) is the predefined workflow of this orchestrator
1274
1275
  3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
1275
1276
  4. **Classify.** The work is substantial when exploration yields two or more meaningful implementation steps, or progress worth recovering after an interruption. Small, understood work stays small and creates no durable task artifacts.
1276
1277
  5. **Track before the first write.** For substantial authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds.
1277
- 6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, with the configured TDD mode and applicable checks. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized substantial ODD implementation; push, pull request creation, and merge remain the user's decisions.
1278
+ 6. **Implement task by task.** Route each task through the orchestrator's Work Routing Ladder, honoring its mandatory delegation triggers, with the configured TDD mode and applicable checks. These triggers are mandatory, not advisory: executing past a fired trigger inline is a routing defect even if the work succeeds. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized substantial ODD implementation; push, pull request creation, and merge remain the user's decisions.
1278
1279
  7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
1279
1280
  Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
1280
1281
 
@@ -5770,6 +5771,19 @@ function nativeStatusInputRejection(reason: string, field?: string): Record<stri
5770
5771
  };
5771
5772
  }
5772
5773
 
5774
+ function nativeInspectInputRejection(reason: string, field?: string): Record<string, unknown> {
5775
+ return {
5776
+ operation: REVIEW_CONTROLLER_OPERATION.INSPECT,
5777
+ status: "blocked",
5778
+ outcome: "native-inspect-input-invalid",
5779
+ reason,
5780
+ ...(field === undefined ? {} : { field }),
5781
+ ...baseRefRejectionHint(reason),
5782
+ mutation_performed: false,
5783
+ mutation_outcome: "none",
5784
+ };
5785
+ }
5786
+
5773
5787
  const PENDING_REVIEW_CONSENT_TTL_MS = 10 * 60 * 1000;
5774
5788
  const REVIEW_SESSION_PERMISSION_STATUS_KEY = "gentle-review-session-permission";
5775
5789
  const REVIEW_SESSION_PERMISSION_STATUS_TEXT = "reviews allowed for this session";
@@ -6575,6 +6589,23 @@ function reviewHostRelayFailureReport(error: ReviewHostRelayError): Record<strin
6575
6589
  // refusal reason lives (gentle-pi#524); dropping it hid every admission
6576
6590
  // refusal behind "submission-refused".
6577
6591
  ...(error.stderr.length === 0 ? {} : { stderr: error.stderr }),
6592
+ // gentle-shell#1156: what the reviewer child's own event stream revealed.
6593
+ ...(error.reviewerEvidence === undefined ? {} : { reviewer: error.reviewerEvidence }),
6594
+ };
6595
+ }
6596
+
6597
+ // gentle-shell#1136 / #1158: the only two user-owned launch selections the
6598
+ // relay accepts. The lens's model comes from the agent model routing config
6599
+ // under the lens's agent name; the extension allowlist comes from the
6600
+ // environment. Both are optional, and neither is ever invented here.
6601
+ function reviewHostRelayLaunchSelection(lens: string | undefined, config: AgentModelConfig, environment: NodeJS.ProcessEnv): { reviewerModel?: string; reviewerExtensionPaths?: readonly string[] } {
6602
+ const agentName = lens === undefined || lens.length === 0 ? undefined : lens.startsWith("review-") ? lens : `review-${lens}`;
6603
+ const entry = agentName === undefined ? undefined : config[agentName];
6604
+ const model = typeof entry === "object" && entry !== null && typeof (entry as AgentRoutingEntry).model === "string" && (entry as AgentRoutingEntry).model!.length > 0 ? (entry as AgentRoutingEntry).model : undefined;
6605
+ const extensionPaths = resolveReviewHostRelayExtensionPaths(environment);
6606
+ return {
6607
+ ...(model === undefined ? {} : { reviewerModel: model }),
6608
+ ...(extensionPaths.length === 0 ? {} : { reviewerExtensionPaths: extensionPaths }),
6578
6609
  };
6579
6610
  }
6580
6611
 
@@ -6686,12 +6717,19 @@ async function executeReviewHostRelayCapture(
6686
6717
  REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE,
6687
6718
  );
6688
6719
  }
6689
- const result = await activeReviewHostRelayRunner({
6690
- captureArgumentTokens: slot.captureArgumentTokens,
6691
- targetCwd: cwd,
6692
- submission: slot.submission,
6693
- ...(signal === undefined ? {} : { signal }),
6694
- });
6720
+ const result = await activeReviewHostRelayRunner((() => {
6721
+ // gentle-shell#1136 / #1158: the lens's user-owned reviewer selection and
6722
+ // the extension allowlist ride the request; the relay validates and
6723
+ // refuses broken configurations typed before anything launches.
6724
+ const launch = reviewHostRelayLaunchSelection(slot.lens, readModelConfig(cwd), process.env);
6725
+ return {
6726
+ captureArgumentTokens: slot.captureArgumentTokens,
6727
+ targetCwd: cwd,
6728
+ submission: slot.submission,
6729
+ ...launch,
6730
+ ...(signal === undefined ? {} : { signal }),
6731
+ };
6732
+ })());
6695
6733
  const closure = decodeRelayLastEventClosure(result.submission);
6696
6734
  if (closure !== undefined) return mapAndClearLastEventClosure(closure, binding, selections, cwd);
6697
6735
  return {
@@ -7463,6 +7501,7 @@ async function executeReviewCaptureGroupOperation(
7463
7501
  captureArgumentTokens: slot.captureArgumentTokens,
7464
7502
  targetCwd: cwd,
7465
7503
  submission: slot.submission!,
7504
+ ...reviewHostRelayLaunchSelection(slot.lens, readModelConfig(cwd), process.env),
7466
7505
  ...(signal === undefined ? {} : { signal }),
7467
7506
  }));
7468
7507
  let prepared: readonly ReviewHostRelayPreparedResult[];
@@ -7585,6 +7624,30 @@ async function executeReviewControllerOperation(
7585
7624
  parameters.operation === REVIEW_CONTROLLER_OPERATION.INSPECT &&
7586
7625
  nativeReviewCli !== null
7587
7626
  ) {
7627
+ const rawInspect = parameters.input === undefined
7628
+ ? undefined
7629
+ : parseControllerJson(parameters.input, REVIEW_CONTROLLER_OPERATION.INSPECT);
7630
+ const unknownField = rawInspect === undefined
7631
+ ? undefined
7632
+ : Object.keys(rawInspect).find((field) => !["baseRef", "committedOnly"].includes(field));
7633
+ if (unknownField !== undefined) return nativeInspectInputRejection("unknown-field", unknownField);
7634
+ const baseRef = rawInspect?.baseRef;
7635
+ if (baseRef !== undefined && !isCanonicalProcessString(baseRef)) return nativeInspectInputRejection("base-ref-invalid");
7636
+ if (baseRef !== undefined && rawInspect?.committedOnly !== true) return nativeInspectInputRejection("committed-only-required");
7637
+ if (rawInspect !== undefined && baseRef === undefined) return nativeInspectInputRejection("committed-only-invalid");
7638
+ let canonicalBaseRef: string | undefined;
7639
+ if (typeof baseRef === "string") {
7640
+ try {
7641
+ canonicalBaseRef = resolveCanonicalCandidateBase(defaultCwd, baseRef).commit;
7642
+ } catch (error) {
7643
+ if (error instanceof CandidateViewError && error.diagnostics !== undefined) return nativeOperationFailure(parameters.operation, Object.assign(error, { candidateViewPreNative: true }));
7644
+ if (error instanceof CandidateViewError && (error.reason === "base-ref-ambiguous" || error.reason === "base-ref-unresolvable" || error.reason === "base-ref-moved")) return nativeInspectInputRejection(error.reason);
7645
+ return nativeInspectInputRejection("base-ref-unresolvable");
7646
+ }
7647
+ }
7648
+ const inspectSelector = canonicalBaseRef === undefined
7649
+ ? {}
7650
+ : { baseRef: canonicalBaseRef, committedOnly: true as const };
7588
7651
  // A new inspect supersedes every pre-lineage selection before its first
7589
7652
  // STATUS attempt. A failed or changed-candidate inspect cannot leave an
7590
7653
  // older selection available for a later START.
@@ -7595,6 +7658,7 @@ async function executeReviewControllerOperation(
7595
7658
  nativeReviewCli,
7596
7659
  {
7597
7660
  cwd: defaultCwd,
7661
+ ...inspectSelector,
7598
7662
  ...(signal === undefined ? {} : { signal }),
7599
7663
  },
7600
7664
  retainedUntrackedSelections,
@@ -7670,6 +7734,7 @@ async function executeReviewControllerOperation(
7670
7734
  nativeReviewCli,
7671
7735
  {
7672
7736
  cwd: defaultCwd,
7737
+ ...inspectSelector,
7673
7738
  untrackedScope: parameters.untrackedScope,
7674
7739
  expectedUntrackedInventory: inventory,
7675
7740
  intendedUntracked: selected.intendedUntracked,
@@ -15,7 +15,7 @@ import { buildCommandPaletteGroups } from "../lib/command-palette-catalog.ts";
15
15
  import { agentsViewKey } from "../lib/agents-keys.ts";
16
16
  import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
17
17
  import { framePromptLines, PROMPT_HINT, PROMPT_STATE, SHELL_PULSE_MS, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
18
- import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, parseCodexUsage, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
18
+ import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, NAN_PROVIDER, NAN_QUOTA_URL, parseCodexUsage, parseNanQuota, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
19
19
  import { UsageView } from "../lib/shell-usage-view.ts";
20
20
  import { sidebarPart } from "../lib/shell-sidebar.ts";
21
21
  import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
@@ -482,21 +482,43 @@ export async function fetchCodexUsage(token: string | undefined, fetchFn: typeof
482
482
  }
483
483
  }
484
484
 
485
+ // The NaN Cloud quota endpoint is the one the official dashboard reads with the
486
+ // same API key pi already holds. The key travels in the header only: the request
487
+ // refuses redirects so it cannot be replayed to another origin, asks for no
488
+ // stored copy, and nothing here logs, renders, or persists it.
489
+ export async function fetchNanUsage(apiKey: string | undefined, fetchFn: typeof fetch, now: number): Promise<ProviderUsage | undefined> {
490
+ if (!apiKey) return undefined;
491
+ try {
492
+ const response = await fetchFn(NAN_QUOTA_URL, {
493
+ redirect: "error",
494
+ cache: "no-store",
495
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "User-Agent": "gentle-pi" },
496
+ });
497
+ if (!response.ok) return undefined;
498
+ const parsed = parseNanQuota(await response.json(), now);
499
+ return parsed.limits.length > 0 ? parsed : undefined;
500
+ } catch {
501
+ return undefined;
502
+ }
503
+ }
504
+
485
505
  export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, overrides: Partial<ShellDeps> = {}): void {
486
506
  installSessionChangeCapture(pi, env, overrides.resolveWorktree ?? resolveSessionWorktree);
487
507
  if (!shellEnabled(env)) return;
488
508
  const deps: ShellDeps = { ...defaultShellDeps, activeProfile: createActiveProfileReader(env), ...overrides };
489
509
  const usage = new UsageStore();
490
510
  let renderHost: ShellRenderHost | undefined;
491
- let usageFetchedAt = 0;
511
+ // The 5-minute rule is per provider: one provider's fetch cannot leave the
512
+ // next one waiting for an interval it never used.
513
+ const usageFetchedAt = new Map<string, number>();
492
514
  const refreshUsage = async (ctx: ExtensionContext, force: boolean) => {
493
515
  const provider = ctx.model?.provider;
494
- if (provider !== CODEX_PROVIDER) return;
516
+ if (provider !== CODEX_PROVIDER && provider !== NAN_PROVIDER) return;
495
517
  const now = deps.now();
496
- if (!force && now - usageFetchedAt < USAGE_REFRESH_MS) return;
497
- usageFetchedAt = now;
498
- const token = await ctx.modelRegistry.getApiKeyForProvider(CODEX_PROVIDER).catch(() => undefined);
499
- const fetched = await fetchCodexUsage(token, deps.fetch, deps.now());
518
+ if (!force && now - (usageFetchedAt.get(provider) ?? 0) < USAGE_REFRESH_MS) return;
519
+ usageFetchedAt.set(provider, now);
520
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider).catch(() => undefined);
521
+ const fetched = provider === NAN_PROVIDER ? await fetchNanUsage(apiKey, deps.fetch, deps.now()) : await fetchCodexUsage(apiKey, deps.fetch, deps.now());
500
522
  if (!fetched) return;
501
523
  usage.record(fetched);
502
524
  renderHost?.invalidateSidebar?.();
@@ -25,7 +25,7 @@ export type ModelConfigFileResult =
25
25
  | { status: "invalid"; path: string }
26
26
  | { status: "valid"; config: AgentModelConfig };
27
27
 
28
- const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
28
+ export const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
29
29
  const SAFE_AGENT_NAME_PATTERN = /^[A-Za-z0-9._:@/+%-]+$/;
30
30
 
31
31
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -995,6 +995,15 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
995
995
  // remain dark because neither is proven to reach the negotiated START
996
996
  // path Pi consumes.
997
997
  "3.1.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
998
+ // v3.2.1 changed the ODD orchestrator contract only (gentle-ai #4714
999
+ // follow-up). Ground-truthed by diffing contracts/review-integration/v2 and
1000
+ // contracts/review-provider-contract between the v3.1.0 and v3.2.1 tags
1001
+ // in the gentle-ai source tree: zero bytes changed (provider contract
1002
+ // stays 1.2.0). Neither change touches the closed START/STATUS fields
1003
+ // this row negotiates, so it repeats 3.1.0 exactly. riskEvidence and hint
1004
+ // remain dark because neither is proven to reach the negotiated START
1005
+ // path Pi consumes.
1006
+ "3.2.1": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
998
1007
  });
999
1008
 
1000
1009
  export interface NativeReviewProcessDiagnostics {