gentle-pi 3.3.0 → 3.5.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.
- package/README.md +88 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/bin/gentle-shell.mjs +198 -0
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-agents-activity.md +95 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +99 -6
- package/extensions/ask-user-choice.ts +70 -22
- package/extensions/ask-user-question.ts +338 -0
- package/extensions/gentle-agents.ts +41 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-rpc-publisher.ts +342 -0
- package/lib/agents-runner.ts +7 -2
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/gentle-shell-launcher.ts +482 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/rpc-host.ts +36 -0
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +5 -1
- package/runtime/gentle-shell-launcher.mjs +483 -0
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +14 -7
- package/scripts/install-tui-mode-setting.mjs +78 -1
- package/scripts/verify-package-files.mjs +6 -3
- package/tests/agents-rpc-publisher.test.ts +407 -0
- package/tests/agents-runner.test.ts +10 -0
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-choice.test.ts +129 -0
- package/tests/ask-user-question.test.ts +661 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +161 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell-bin.test.ts +188 -0
- package/tests/gentle-shell-launcher.test.ts +718 -0
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/install-tui-mode-guard.test.ts +99 -0
- package/tests/install-tui-mode-setting.test.ts +39 -1
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/rpc-host.test.ts +77 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
// The gentle-shell launcher: pure, side-effect-free functions over injected
|
|
4
|
+
// env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
|
|
5
|
+
// filesystem and child process so this module stays fully unit-testable.
|
|
6
|
+
|
|
7
|
+
export type LauncherCommand = "home";
|
|
8
|
+
|
|
9
|
+
// pi's own package-management subcommands (see pi's cli/args.ts printHelp
|
|
10
|
+
// "Commands" list): each is dispatched by pi itself, before pi's own flag
|
|
11
|
+
// parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
|
|
12
|
+
export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] as const;
|
|
13
|
+
|
|
14
|
+
export type PiSubcommand = (typeof PI_SUBCOMMANDS)[number];
|
|
15
|
+
|
|
16
|
+
function isPiSubcommand(token: string): token is PiSubcommand {
|
|
17
|
+
return (PI_SUBCOMMANDS as readonly string[]).includes(token);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ParsedLauncherArgs {
|
|
21
|
+
link: boolean;
|
|
22
|
+
isolated: boolean;
|
|
23
|
+
home?: string;
|
|
24
|
+
help: boolean;
|
|
25
|
+
version: boolean;
|
|
26
|
+
command?: LauncherCommand;
|
|
27
|
+
commandArgs: string[];
|
|
28
|
+
passthrough: string[];
|
|
29
|
+
// Set when the first passthrough token is one of PI_SUBCOMMANDS (e.g.
|
|
30
|
+
// `gentle-shell install npm:x`). It stays part of `passthrough` — this
|
|
31
|
+
// field only tells buildPiInvocation to skip its extension injection, so
|
|
32
|
+
// pi sees the bare subcommand it expects as argv[0].
|
|
33
|
+
piSubcommand?: PiSubcommand;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Home-subcommand parsing is deliberately shallow: `home` only counts as the
|
|
38
|
+
// subcommand when it is argv[0], and everything after it is handed over
|
|
39
|
+
// untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
|
|
40
|
+
export function parseLauncherArgs(argv: string[]): ParsedLauncherArgs {
|
|
41
|
+
if (argv[0] === "home") {
|
|
42
|
+
return {
|
|
43
|
+
link: false,
|
|
44
|
+
isolated: false,
|
|
45
|
+
home: undefined,
|
|
46
|
+
help: false,
|
|
47
|
+
version: false,
|
|
48
|
+
command: "home",
|
|
49
|
+
commandArgs: argv.slice(1),
|
|
50
|
+
passthrough: [],
|
|
51
|
+
piSubcommand: undefined,
|
|
52
|
+
error: undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let link = false;
|
|
57
|
+
let isolated = false;
|
|
58
|
+
let home: string | undefined;
|
|
59
|
+
let help = false;
|
|
60
|
+
let version = false;
|
|
61
|
+
let error: string | undefined;
|
|
62
|
+
let piSubcommand: PiSubcommand | undefined;
|
|
63
|
+
const passthrough: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
66
|
+
const arg = argv[i];
|
|
67
|
+
if (arg === "--") {
|
|
68
|
+
const rest = argv.slice(i + 1);
|
|
69
|
+
if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
|
|
70
|
+
piSubcommand = rest[0];
|
|
71
|
+
}
|
|
72
|
+
passthrough.push(...rest);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--link") {
|
|
76
|
+
link = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg === "--isolated") {
|
|
80
|
+
isolated = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg === "--help" || arg === "-h") {
|
|
84
|
+
help = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === "--version") {
|
|
88
|
+
version = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("--home=")) {
|
|
92
|
+
const value = arg.slice("--home=".length);
|
|
93
|
+
if (value.length === 0) {
|
|
94
|
+
error = "--home requires a non-empty path argument";
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
home = value;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--home") {
|
|
101
|
+
const value = argv[i + 1];
|
|
102
|
+
if (value === undefined || value.length === 0) {
|
|
103
|
+
error = "--home requires a non-empty path argument";
|
|
104
|
+
if (value !== undefined) i += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
home = value;
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (passthrough.length === 0 && isPiSubcommand(arg)) {
|
|
112
|
+
piSubcommand = arg;
|
|
113
|
+
}
|
|
114
|
+
passthrough.push(arg);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (error === undefined) {
|
|
118
|
+
if (link && isolated) {
|
|
119
|
+
error = "--link cannot be combined with --isolated";
|
|
120
|
+
} else if (link && home !== undefined) {
|
|
121
|
+
error = "--link cannot be combined with --home";
|
|
122
|
+
} else if (isolated && home !== undefined) {
|
|
123
|
+
error = "--isolated cannot be combined with --home";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { link, isolated, home, help, version, command: undefined, commandArgs: [], passthrough, piSubcommand, error };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- home resolution -------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
export type HomeMode = "link" | "isolated" | "path";
|
|
133
|
+
export type HomeSource = "flag" | "config" | "default";
|
|
134
|
+
|
|
135
|
+
export interface ResolvedHome {
|
|
136
|
+
mode: HomeMode;
|
|
137
|
+
dir: string;
|
|
138
|
+
source: HomeSource;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A discriminated union instead of a plain `home: string` field: `resolveHome`
|
|
142
|
+
// switches on `mode` rather than re-parsing the raw on-disk string, and the
|
|
143
|
+
// `path` case carries its `dir` explicitly so a "link"/"isolated" string can
|
|
144
|
+
// never be mistaken for a filesystem path at the call site.
|
|
145
|
+
export type LauncherConfig = { mode: "link" } | { mode: "isolated" } | { mode: "path"; dir: string };
|
|
146
|
+
|
|
147
|
+
export interface ResolveHomeInput {
|
|
148
|
+
args: ParsedLauncherArgs;
|
|
149
|
+
env: Record<string, string | undefined>;
|
|
150
|
+
homedir: string;
|
|
151
|
+
config: LauncherConfig | undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pi Subagents resolves `PI_CODING_AGENT_DIR || ~/.pi/agent`; `--link` reuses
|
|
155
|
+
// that exact home so gentle-shell never diverges from the user's own pi.
|
|
156
|
+
function linkDir(env: Record<string, string | undefined>, homedir: string): string {
|
|
157
|
+
return env.PI_CODING_AGENT_DIR || join(homedir, ".pi", "agent");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isolatedDir(env: Record<string, string | undefined>, homedir: string): string {
|
|
161
|
+
return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function resolveHome(input: ResolveHomeInput): ResolvedHome {
|
|
165
|
+
const { args, env, homedir, config } = input;
|
|
166
|
+
|
|
167
|
+
if (args.link) return { mode: "link", dir: linkDir(env, homedir), source: "flag" };
|
|
168
|
+
if (args.isolated) return { mode: "isolated", dir: isolatedDir(env, homedir), source: "flag" };
|
|
169
|
+
if (args.home !== undefined) return { mode: "path", dir: args.home, source: "flag" };
|
|
170
|
+
|
|
171
|
+
if (config !== undefined) {
|
|
172
|
+
if (config.mode === "link") return { mode: "link", dir: linkDir(env, homedir), source: "config" };
|
|
173
|
+
if (config.mode === "isolated") return { mode: "isolated", dir: isolatedDir(env, homedir), source: "config" };
|
|
174
|
+
return { mode: "path", dir: config.dir, source: "config" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { mode: "isolated", dir: isolatedDir(env, homedir), source: "default" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function launcherConfigPath(homedir: string): string {
|
|
181
|
+
return join(homedir, ".gentle-shell", "config.json");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Tolerant on purpose: a malformed or foreign config.json must never crash
|
|
185
|
+
// the launcher, it just falls through to the default isolated home.
|
|
186
|
+
//
|
|
187
|
+
// The on-disk shape stays the flat `{ "home": "link" | "isolated" | "<path>" }`
|
|
188
|
+
// documented in the feature scope; only the parsed, in-memory `LauncherConfig`
|
|
189
|
+
// is a discriminated union. Any non-empty string other than the exact literals
|
|
190
|
+
// "link" or "isolated" is treated as a path, including a near-miss like
|
|
191
|
+
// "linked" — this is deliberate: there is no separate "unrecognised mode"
|
|
192
|
+
// error, a typo just resolves to a (probably nonexistent) path instead.
|
|
193
|
+
export function parseLauncherConfig(text: string): LauncherConfig | undefined {
|
|
194
|
+
let parsed: unknown;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(text);
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
201
|
+
const home = (parsed as Record<string, unknown>).home;
|
|
202
|
+
if (typeof home !== "string" || home.length === 0) return undefined;
|
|
203
|
+
if (home === "link") return { mode: "link" };
|
|
204
|
+
if (home === "isolated") return { mode: "isolated" };
|
|
205
|
+
return { mode: "path", dir: home };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- pi runtime resolution ---------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export type PiRuntimeKind = "env" | "bundled" | "path";
|
|
211
|
+
|
|
212
|
+
export interface PiRuntime {
|
|
213
|
+
kind: PiRuntimeKind;
|
|
214
|
+
command: string;
|
|
215
|
+
args: string[];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface PiRuntimeDeps {
|
|
219
|
+
env: Record<string, string | undefined>;
|
|
220
|
+
resolveBundledCli: () => string | undefined;
|
|
221
|
+
findOnPath: (name: string) => string | undefined;
|
|
222
|
+
nodeExecPath: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function resolvePiRuntime(deps: PiRuntimeDeps): PiRuntime | undefined {
|
|
226
|
+
const envOverride = deps.env.GENTLE_SHELL_PI;
|
|
227
|
+
if (envOverride !== undefined && envOverride.length > 0) return { kind: "env", command: envOverride, args: [] };
|
|
228
|
+
|
|
229
|
+
const bundledCliPath = deps.resolveBundledCli();
|
|
230
|
+
if (bundledCliPath !== undefined) return { kind: "bundled", command: deps.nodeExecPath, args: [bundledCliPath] };
|
|
231
|
+
|
|
232
|
+
const onPath = deps.findOnPath("pi");
|
|
233
|
+
if (onPath !== undefined) return { kind: "path", command: onPath, args: [] };
|
|
234
|
+
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function missingPiMessage(): string {
|
|
239
|
+
return [
|
|
240
|
+
"No pi runtime could be found. Pick one of:",
|
|
241
|
+
" - Set GENTLE_SHELL_PI to the path of a pi executable.",
|
|
242
|
+
" - Install @earendil-works/pi-coding-agent next to gentle-pi (it ships as an optional peer dependency).",
|
|
243
|
+
" - Install pi and make sure it is on your PATH.",
|
|
244
|
+
].join("\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- pi version gate ---------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
export const MIN_PI_VERSION = "0.85.1";
|
|
250
|
+
|
|
251
|
+
export type PiVersionCheck = { ok: true; version: string } | { ok: false; message: string; version?: string };
|
|
252
|
+
|
|
253
|
+
const VERSION_PATTERN = /v?(\d+)\.(\d+)\.(\d+)/;
|
|
254
|
+
|
|
255
|
+
function compareVersions(a: readonly [number, number, number], b: readonly [number, number, number]): number {
|
|
256
|
+
for (let i = 0; i < 3; i += 1) {
|
|
257
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
258
|
+
}
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function checkPiVersion(output: string, minimum: string = MIN_PI_VERSION): PiVersionCheck {
|
|
263
|
+
const match = VERSION_PATTERN.exec(output);
|
|
264
|
+
if (!match) {
|
|
265
|
+
return { ok: false, message: `Could not determine the pi version from "${output.trim()}" (need at least ${minimum}).` };
|
|
266
|
+
}
|
|
267
|
+
const version = `${match[1]}.${match[2]}.${match[3]}`;
|
|
268
|
+
const minimumMatch = VERSION_PATTERN.exec(minimum);
|
|
269
|
+
if (!minimumMatch) throw new Error(`invalid minimum version "${minimum}"`);
|
|
270
|
+
const found: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
271
|
+
const wanted: [number, number, number] = [Number(minimumMatch[1]), Number(minimumMatch[2]), Number(minimumMatch[3])];
|
|
272
|
+
if (compareVersions(found, wanted) < 0) {
|
|
273
|
+
return { ok: false, version, message: `pi version ${version} is older than the required minimum ${minimum}.` };
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, version };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- packaging drift guard -----------------------------------------------------
|
|
279
|
+
|
|
280
|
+
export interface PackageJsonPeerShape {
|
|
281
|
+
peerDependencies?: Record<string, string>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type PeerVersionPinCheck = { ok: true; pinned: string } | { ok: false; message: string };
|
|
285
|
+
|
|
286
|
+
// Keeps the MIN_PI_VERSION drift-guard test's failure readable: a missing
|
|
287
|
+
// peerDependencies block, a missing peer entry, or a malformed range must
|
|
288
|
+
// fail with a clear assertion message, not a raw TypeError from indexing an
|
|
289
|
+
// undefined value the way a direct `packageJson.peerDependencies[peerName]`
|
|
290
|
+
// lookup would.
|
|
291
|
+
export function checkPeerVersionPin(packageJson: PackageJsonPeerShape, peerName: string, minVersion: string): PeerVersionPinCheck {
|
|
292
|
+
const peerDependencies = packageJson.peerDependencies;
|
|
293
|
+
if (peerDependencies === undefined) {
|
|
294
|
+
return { ok: false, message: "package.json is missing a peerDependencies block" };
|
|
295
|
+
}
|
|
296
|
+
const pinned = peerDependencies[peerName];
|
|
297
|
+
if (typeof pinned !== "string") {
|
|
298
|
+
return { ok: false, message: `package.json peerDependencies is missing "${peerName}"` };
|
|
299
|
+
}
|
|
300
|
+
if (!/^>=\d+\.\d+\.\d+$/.test(pinned)) {
|
|
301
|
+
return { ok: false, message: `package.json peerDependencies["${peerName}"] ("${pinned}") is not a simple >=x.y.z range` };
|
|
302
|
+
}
|
|
303
|
+
const version = pinned.replace(/^>=/, "");
|
|
304
|
+
if (version !== minVersion) {
|
|
305
|
+
return { ok: false, message: `MIN_PI_VERSION ("${minVersion}") does not match the pinned peer range ("${pinned}")` };
|
|
306
|
+
}
|
|
307
|
+
return { ok: true, pinned };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- settings.json detection ---------------------------------------------------
|
|
311
|
+
|
|
312
|
+
function packageEntryDeclaresGentlePi(entry: unknown): boolean {
|
|
313
|
+
const declares = (value: unknown): boolean => typeof value === "string" && (value === "npm:gentle-pi" || value.startsWith("npm:gentle-pi@"));
|
|
314
|
+
if (declares(entry)) return true;
|
|
315
|
+
if (entry !== null && typeof entry === "object") return declares((entry as Record<string, unknown>).source);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function settingsDeclareGentlePi(settingsText: string | undefined): boolean {
|
|
320
|
+
if (settingsText === undefined) return false;
|
|
321
|
+
let parsed: unknown;
|
|
322
|
+
try {
|
|
323
|
+
parsed = JSON.parse(settingsText);
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (typeof parsed !== "object" || parsed === null) return false;
|
|
328
|
+
const packages = (parsed as Record<string, unknown>).packages;
|
|
329
|
+
if (!Array.isArray(packages)) return false;
|
|
330
|
+
return packages.some(packageEntryDeclaresGentlePi);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- pi invocation builder ---------------------------------------------------
|
|
334
|
+
|
|
335
|
+
export interface BuildPiInvocationInput {
|
|
336
|
+
runtime: PiRuntime;
|
|
337
|
+
home: ResolvedHome;
|
|
338
|
+
packageRoot: string;
|
|
339
|
+
settingsDeclareGentlePi: boolean;
|
|
340
|
+
passthrough: string[];
|
|
341
|
+
// Set when parseLauncherArgs recognised passthrough[0] as one of
|
|
342
|
+
// PI_SUBCOMMANDS. pi dispatches install/remove/uninstall/update/list/
|
|
343
|
+
// config/auth on argv[0] before its own flag parsing, so none of the
|
|
344
|
+
// gentle-pi extension injection below may precede it.
|
|
345
|
+
piSubcommand?: PiSubcommand;
|
|
346
|
+
baseEnv: Record<string, string | undefined>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface PiInvocation {
|
|
350
|
+
command: string;
|
|
351
|
+
args: string[];
|
|
352
|
+
env: Record<string, string | undefined>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// The `-e/--theme/--skill/--prompt-template` injection is skipped when the
|
|
356
|
+
// caller already confirmed the target settings.json declares the package
|
|
357
|
+
// (the `--link` case with a pi-managed install), or when passthrough[0] is
|
|
358
|
+
// one of pi's own subcommands: pi dispatches install/remove/uninstall/
|
|
359
|
+
// update/list/config/auth on argv[0] before it even parses flags, so any
|
|
360
|
+
// injected flag ahead of it stops pi from recognising its subcommand at
|
|
361
|
+
// all — this is exactly the observed 2026-09-22 bug where `gentle-shell
|
|
362
|
+
// install npm:x` opened an interactive pi session instead of running the
|
|
363
|
+
// package manager. Isolated and path homes never declare the package, so
|
|
364
|
+
// callers pass `settingsDeclareGentlePi: false` for those and the
|
|
365
|
+
// injection always happens there, unless a pi subcommand is set.
|
|
366
|
+
export function buildPiInvocation(input: BuildPiInvocationInput): PiInvocation {
|
|
367
|
+
const args = [...input.runtime.args];
|
|
368
|
+
if (input.piSubcommand === undefined && !input.settingsDeclareGentlePi) {
|
|
369
|
+
args.push(
|
|
370
|
+
"-e",
|
|
371
|
+
input.packageRoot,
|
|
372
|
+
"--theme",
|
|
373
|
+
join(input.packageRoot, "themes"),
|
|
374
|
+
"--skill",
|
|
375
|
+
join(input.packageRoot, "skills"),
|
|
376
|
+
"--prompt-template",
|
|
377
|
+
join(input.packageRoot, "prompts"),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
args.push(...input.passthrough);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
command: input.runtime.command,
|
|
384
|
+
args,
|
|
385
|
+
env: { ...input.baseEnv, PI_CODING_AGENT_DIR: input.home.dir, GENTLE_PI_AGENT_HOME: input.home.dir },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- spawn planning ------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
// R3-001: `findOnPath` can resolve a PATHEXT candidate such as a .CMD or .BAT
|
|
392
|
+
// shim on win32 (exactly how an npm-installed `pi` lands on PATH), and a
|
|
393
|
+
// GENTLE_SHELL_PI override can point at one too. Current Node releases refuse
|
|
394
|
+
// to spawn a batch file directly without `shell: true` (EINVAL), so both the
|
|
395
|
+
// version probe and the real launch route a batch shim through cmd.exe as one
|
|
396
|
+
// quoted command line instead of spawning it directly.
|
|
397
|
+
const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
|
|
398
|
+
|
|
399
|
+
// cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
|
|
400
|
+
// token in double quotes when it is empty or contains whitespace or any of
|
|
401
|
+
// `"&|<>^%()`, and escape an inner `"` as `\"` — doubling inner quotes is not
|
|
402
|
+
// reliable in cmd.exe, unlike the `\"` convention Node's own Windows spawn
|
|
403
|
+
// helpers use.
|
|
404
|
+
export function quoteForCmdExe(token: string): string {
|
|
405
|
+
if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
|
|
406
|
+
return `"${token.replace(/"/g, '\\"')}"`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface PlanSpawnInput {
|
|
410
|
+
command: string;
|
|
411
|
+
args: string[];
|
|
412
|
+
platform: NodeJS.Platform;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface SpawnPlan {
|
|
416
|
+
command: string;
|
|
417
|
+
args: string[];
|
|
418
|
+
shell: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function planSpawn(input: PlanSpawnInput): SpawnPlan {
|
|
422
|
+
const { command, args, platform } = input;
|
|
423
|
+
if (platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
|
|
424
|
+
return { command: [command, ...args].map(quoteForCmdExe).join(" "), args: [], shell: true };
|
|
425
|
+
}
|
|
426
|
+
return { command, args, shell: false };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- reporting ---------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
export interface DescribeVersionInput {
|
|
432
|
+
gentlePiVersion: string;
|
|
433
|
+
piVersion: string | undefined;
|
|
434
|
+
home: ResolvedHome;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function describeVersion(input: DescribeVersionInput): string {
|
|
438
|
+
return [
|
|
439
|
+
`gentle-shell ${input.gentlePiVersion}`,
|
|
440
|
+
`pi ${input.piVersion ?? "not found"}`,
|
|
441
|
+
`home ${input.home.mode} ${input.home.dir}`,
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function helpText(): string {
|
|
446
|
+
return [
|
|
447
|
+
"Usage: gentle-shell [options] [-- pi-args...]",
|
|
448
|
+
" gentle-shell home [link|isolated|<path>]",
|
|
449
|
+
"",
|
|
450
|
+
"Opens pi with the Gentle Shell package loaded, without touching your",
|
|
451
|
+
"vanilla pi installation.",
|
|
452
|
+
"",
|
|
453
|
+
"Options:",
|
|
454
|
+
" --link Use your existing pi agent home (never edits its settings.json).",
|
|
455
|
+
" --isolated Use the dedicated ~/.gentle-shell/agent home (default).",
|
|
456
|
+
" --home <path> Use a custom agent home directory.",
|
|
457
|
+
" --help, -h Show this help text.",
|
|
458
|
+
" --version Show gentle-shell, pi, and home version information.",
|
|
459
|
+
"",
|
|
460
|
+
"Commands:",
|
|
461
|
+
" home Print or persist the effective home mode (link, isolated, or a path).",
|
|
462
|
+
"",
|
|
463
|
+
"Managing packages:",
|
|
464
|
+
" gentle-shell install npm:<pkg> Run pi's own 'install' against the resolved home.",
|
|
465
|
+
" gentle-shell remove <source> Run pi's own 'remove' against the resolved home.",
|
|
466
|
+
" gentle-shell list Run pi's own 'list' against the resolved home.",
|
|
467
|
+
" gentle-shell update [target] Run pi's own 'update' against the resolved home.",
|
|
468
|
+
" gentle-shell config Run pi's own 'config' against the resolved home.",
|
|
469
|
+
" gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
|
|
470
|
+
" These run pi's own commands, forwarded verbatim, against the --isolated home",
|
|
471
|
+
" (or your own pi home with --link). Running 'gentle-shell install npm:gentle-pi'",
|
|
472
|
+
" inside the isolated home is unnecessary: gentle-shell already loads the",
|
|
473
|
+
" package itself.",
|
|
474
|
+
"",
|
|
475
|
+
"Environment variables:",
|
|
476
|
+
" GENTLE_SHELL_PI Path to the pi executable to run.",
|
|
477
|
+
" GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent).",
|
|
478
|
+
" PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
|
|
479
|
+
"",
|
|
480
|
+
"Every other argument is forwarded to pi unchanged.",
|
|
481
|
+
].join("\n");
|
|
482
|
+
}
|
|
@@ -59,6 +59,13 @@ export interface InProcessReviewerRequest {
|
|
|
59
59
|
readonly prompt: Buffer;
|
|
60
60
|
readonly timeoutMs: number;
|
|
61
61
|
readonly signal?: AbortSignal;
|
|
62
|
+
/**
|
|
63
|
+
* The live pi session id, threaded from the extension context. Pi's main
|
|
64
|
+
* agent loop adds OpenCode attribution headers itself; this side-call must
|
|
65
|
+
* carry them itself instead. Absent (or empty) means no attribution header,
|
|
66
|
+
* never an invented one and never an error.
|
|
67
|
+
*/
|
|
68
|
+
readonly sessionId?: string;
|
|
62
69
|
/** e.g. "review-risk" — only used to name the routing config key in refusal messages. */
|
|
63
70
|
readonly routingKey: string;
|
|
64
71
|
}
|
|
@@ -126,6 +133,30 @@ function isTextContent(part: { type?: unknown }): part is TextContent {
|
|
|
126
133
|
return part.type === "text";
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Mirrors pi's main-loop OpenCode attribution condition exactly
|
|
138
|
+
* (core/provider-attribution.js#getSessionHeaders): the model's provider is
|
|
139
|
+
* `opencode` or `opencode-go`, or its baseUrl host is `opencode.ai`. Returns
|
|
140
|
+
* the `{ x-opencode-session, x-opencode-client }` attribution pair, or
|
|
141
|
+
* undefined when the model is not OpenCode-routed or there is no live session
|
|
142
|
+
* id — a missing session id is never an error and never invents a header.
|
|
143
|
+
* The URL parse is guarded: an unparseable baseUrl follows the provider
|
|
144
|
+
* condition alone.
|
|
145
|
+
*/
|
|
146
|
+
export function openCodeSessionAttributionHeaders(model: Model<Api>, sessionId: string | undefined): ProviderHeaders | undefined {
|
|
147
|
+
const isOpenCode = model.provider === "opencode"
|
|
148
|
+
|| model.provider === "opencode-go"
|
|
149
|
+
|| (() => {
|
|
150
|
+
try {
|
|
151
|
+
return new URL(String(model.baseUrl ?? "")).hostname === "opencode.ai";
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
if (!isOpenCode || typeof sessionId !== "string" || sessionId.length === 0) return undefined;
|
|
157
|
+
return { "x-opencode-session": sessionId, "x-opencode-client": "pi" };
|
|
158
|
+
}
|
|
159
|
+
|
|
129
160
|
/**
|
|
130
161
|
* Runs one reviewer completion in-process: resolve the model, authenticate,
|
|
131
162
|
* map the routing thinking label, and complete exactly one frozen prompt as
|
|
@@ -168,6 +199,12 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
|
|
|
168
199
|
);
|
|
169
200
|
}
|
|
170
201
|
|
|
202
|
+
// Extension side-calls bypass pi's main agent loop, which is where OpenCode
|
|
203
|
+
// attribution headers are otherwise added, so this completion carries them
|
|
204
|
+
// itself — as a default beneath the registry's own auth headers, the same
|
|
205
|
+
// merge order pi's core uses for explicit header sources.
|
|
206
|
+
const attributionHeaders = openCodeSessionAttributionHeaders(model, request.sessionId);
|
|
207
|
+
|
|
171
208
|
// The caller's own signal (if any) and a floor timeout race together:
|
|
172
209
|
// whichever fires first aborts the completion. The catch branch below
|
|
173
210
|
// tells them apart by which underlying signal actually fired, never by
|
|
@@ -188,7 +225,7 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
|
|
|
188
225
|
signal: combinedSignal,
|
|
189
226
|
timeoutMs: request.timeoutMs,
|
|
190
227
|
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
191
|
-
...(auth.headers === undefined ? {} : { headers: auth.headers }),
|
|
228
|
+
...(auth.headers === undefined && attributionHeaders === undefined ? {} : { headers: attributionHeaders === undefined ? auth.headers : { ...attributionHeaders, ...auth.headers } }),
|
|
192
229
|
...(reasoning.reasoning === undefined ? {} : { reasoning: reasoning.reasoning }),
|
|
193
230
|
};
|
|
194
231
|
|
package/lib/native-review-cli.ts
CHANGED
|
@@ -201,7 +201,7 @@ export interface NativeReviewModeRequest {
|
|
|
201
201
|
// explicit `committedOnly` acknowledgement, exactly like Native START's
|
|
202
202
|
// baseRef/committedOnly pairing, because both select a committed range
|
|
203
203
|
// instead of the ambient working tree.
|
|
204
|
-
export interface NativeReviewAssessRequest {
|
|
204
|
+
export interface NativeReviewAssessRequest extends NativeUntrackedSelection {
|
|
205
205
|
cwd: string;
|
|
206
206
|
baseRef?: string;
|
|
207
207
|
committedOnly?: boolean;
|
|
@@ -705,7 +705,11 @@ function isNativeUntrackedPath(value: unknown): value is string {
|
|
|
705
705
|
&& value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
-
function nativeUntrackedSelection(request:
|
|
708
|
+
export function nativeUntrackedSelection(request: {
|
|
709
|
+
untrackedScope?: unknown;
|
|
710
|
+
expectedUntrackedInventory?: unknown;
|
|
711
|
+
intendedUntracked?: unknown;
|
|
712
|
+
}): NativeUntrackedSelection {
|
|
709
713
|
const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
|
|
710
714
|
const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
|
|
711
715
|
if (!declared) return {};
|
|
@@ -716,16 +720,19 @@ function nativeUntrackedSelection(request: NativeUntrackedSelectionRequest): Nat
|
|
|
716
720
|
) {
|
|
717
721
|
throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
|
|
718
722
|
}
|
|
719
|
-
|
|
723
|
+
// The guard above establishes the array and element types for both typed
|
|
724
|
+
// native requests and untyped facade input.
|
|
725
|
+
const paths = intendedUntracked as readonly string[] | undefined;
|
|
726
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (paths?.length ?? 0) > 0) {
|
|
720
727
|
throw new TypeError("Native exclude untracked selection cannot include paths");
|
|
721
728
|
}
|
|
722
|
-
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (
|
|
729
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (paths?.length ?? 0) === 0) {
|
|
723
730
|
throw new TypeError("Native select untracked selection requires at least one path");
|
|
724
731
|
}
|
|
725
732
|
return {
|
|
726
733
|
untrackedScope,
|
|
727
734
|
expectedUntrackedInventory,
|
|
728
|
-
intendedUntracked:
|
|
735
|
+
intendedUntracked: paths === undefined ? undefined : [...paths],
|
|
729
736
|
};
|
|
730
737
|
}
|
|
731
738
|
|
|
@@ -1018,6 +1025,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1018
1025
|
// and hint remain dark because neither is proven to reach the negotiated
|
|
1019
1026
|
// START path Pi consumes.
|
|
1020
1027
|
"3.4.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 }),
|
|
1028
|
+
// v3.5.0 repeats 3.4.0: the published provider contract bundle is
|
|
1029
|
+
// byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
|
|
1030
|
+
// with only build-identity differences, and no review-integration schema
|
|
1031
|
+
// changed. The v2 preflight failure identity fix does not change the
|
|
1032
|
+
// closed START/STATUS fields this row negotiates. riskEvidence and hint
|
|
1033
|
+
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1034
|
+
"3.5.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 }),
|
|
1021
1035
|
});
|
|
1022
1036
|
|
|
1023
1037
|
export interface NativeReviewProcessDiagnostics {
|
|
@@ -1102,8 +1116,19 @@ function decodeSelectedLenses(value: unknown, riskLevel: string, lensesRequired:
|
|
|
1102
1116
|
function enumString(value: unknown, allowed: readonly string[]): string { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
|
|
1103
1117
|
const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
|
|
1104
1118
|
|
|
1105
|
-
function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT): string {
|
|
1106
|
-
|
|
1119
|
+
function sanitizeNativeDiagnosticText(value: string, limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation?: NativeReviewOperation): string {
|
|
1120
|
+
// ASSESS diagnostics are projected into a public verification plan. Retain
|
|
1121
|
+
// native guidance, not local paths or environment assignment values.
|
|
1122
|
+
const input = operation === NATIVE_REVIEW_OPERATION.ASSESS
|
|
1123
|
+
? value
|
|
1124
|
+
.replace(/(?<![\w-])[a-z_][a-z0-9_]*=(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s]+)/gi, "[REDACTED ENV]")
|
|
1125
|
+
.replace(/--(?:password|token|secret|authorization|cookie|private[_-]key|access[_-]token|[a-z0-9_-]+[_-]token|api[_-]?key)[ \t]+(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s]+)/gi, "[REDACTED CREDENTIAL]")
|
|
1126
|
+
// Quoted paths have a clear boundary. For an unquoted path, the
|
|
1127
|
+
// remaining line is ambiguous (spaces may belong to the filename).
|
|
1128
|
+
// Redact that suffix rather than leak trailing path components.
|
|
1129
|
+
.replace(/"(?:[A-Za-z]:[\\/]|\/)[^"\r\n]*"|'(?:[A-Za-z]:[\\/]|\/)[^'\r\n]*'|(?:[A-Za-z]:[\\/]|\/)[^\r\n]*/g, "[REDACTED PATH]")
|
|
1130
|
+
: value;
|
|
1131
|
+
const normalized = input
|
|
1107
1132
|
.replace(/\x1b](?:[^\x07\x1b]|\x1b(?!\\))*?(?:\x07|\x1b\\)/g, "[REDACTED CONTROL]")
|
|
1108
1133
|
.replace(/\x1b[PX^_][\s\S]*?\x1b\\/g, "[REDACTED CONTROL]")
|
|
1109
1134
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "[REDACTED CONTROL]")
|
|
@@ -1137,7 +1162,7 @@ export function sanitizeForeignNativeReviewDiagnostics(value: unknown): NativeRe
|
|
|
1137
1162
|
timed_out: booleanValue(raw.timed_out),
|
|
1138
1163
|
output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
|
|
1139
1164
|
...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint! }),
|
|
1140
|
-
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
|
|
1165
|
+
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr), NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) }),
|
|
1141
1166
|
};
|
|
1142
1167
|
} catch { return undefined; }
|
|
1143
1168
|
}
|
|
@@ -1154,7 +1179,7 @@ function nativeProcessDiagnostics(operation: NativeReviewOperation, code: Native
|
|
|
1154
1179
|
...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
|
|
1155
1180
|
? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
|
|
1156
1181
|
: {}),
|
|
1157
|
-
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
|
|
1182
|
+
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr, NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) } : {}),
|
|
1158
1183
|
};
|
|
1159
1184
|
}
|
|
1160
1185
|
|
|
@@ -2535,6 +2560,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2535
2560
|
// rejects -- callers (the `gentle_review` tool's `assess` operation) fail
|
|
2536
2561
|
// closed to `high`.
|
|
2537
2562
|
async assess(request: NativeReviewAssessRequest): Promise<ReviewAssessmentV1> {
|
|
2563
|
+
const selection = nativeUntrackedSelection(request);
|
|
2538
2564
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
|
|
2539
2565
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
|
|
2540
2566
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
|
|
@@ -2542,7 +2568,7 @@ export class NativeReviewCliV216 implements NativeReviewCli {
|
|
|
2542
2568
|
const execution = await this.invoke(
|
|
2543
2569
|
NATIVE_REVIEW_OPERATION.ASSESS,
|
|
2544
2570
|
cwd,
|
|
2545
|
-
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
|
|
2571
|
+
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...nativeUntrackedSelectionArguments(selection), "--json"],
|
|
2546
2572
|
false,
|
|
2547
2573
|
request.signal,
|
|
2548
2574
|
this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
|