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.
- package/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +15 -5
- package/docs/readme-reference.md +6 -6
- package/docs/review-integration.md +15 -6
- package/extensions/gentle-ai.ts +72 -7
- package/extensions/gentle-shell.ts +29 -7
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +9 -0
- package/lib/opaque-pi-reviewer-adapter.ts +130 -10
- package/lib/review-host-relay.ts +95 -2
- package/lib/shell-bar.ts +63 -9
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +9 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-shell.test.ts +109 -3
- package/tests/native-review-capability-contract.test.ts +14 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +153 -9
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay.test.ts +83 -14
- package/tests/review-relay-transport-agent.test.ts +86 -1
- package/tests/shell-bar.test.ts +153 -3
- package/tests/shell-usage-view.test.ts +3 -2
- package/tests/shell-usage.test.ts +254 -6
|
@@ -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.
|
|
127
|
-
|
|
128
|
-
1. **
|
|
129
|
-
2. **
|
|
130
|
-
3. **
|
|
131
|
-
4. **
|
|
132
|
-
5. **
|
|
133
|
-
|
|
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
|
|
package/assets/orchestrator.md
CHANGED
|
@@ -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. **
|
|
60
|
-
5. **
|
|
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
|
|
package/docs/gentle-shell.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
package/docs/readme-reference.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12
|
-
| Host coordinator | Executes the exact Go-issued materialize/submission tokens, launches the adapter, and submits its
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -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
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
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
|
-
|
|
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
|
|
498
|
-
const
|
|
499
|
-
const fetched = await fetchCodexUsage(
|
|
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> {
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -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 {
|