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.
Files changed (88) hide show
  1. package/README.md +88 -59
  2. package/assets/orchestrator-delegation.md +1 -1
  3. package/bin/gentle-shell.mjs +198 -0
  4. package/docs/assets/brand/gentle-shell-banner.gif +0 -0
  5. package/docs/assets/diagrams/odd-workflow.svg +74 -0
  6. package/docs/assets/features/agents-view.png +0 -0
  7. package/docs/assets/features/changes-view.png +0 -0
  8. package/docs/assets/features/command-palette.png +0 -0
  9. package/docs/assets/features/profiles-routing.png +0 -0
  10. package/docs/gentle-agents-activity.md +95 -0
  11. package/docs/gentle-shell.md +26 -2
  12. package/docs/readme-reference.md +99 -6
  13. package/extensions/ask-user-choice.ts +70 -22
  14. package/extensions/ask-user-question.ts +338 -0
  15. package/extensions/gentle-agents.ts +41 -1
  16. package/extensions/gentle-ai.ts +59 -18
  17. package/extensions/gentle-shell.ts +99 -10
  18. package/extensions/quiet-tools.ts +28 -5
  19. package/extensions/startup-banner.ts +25 -10
  20. package/lib/agents-rpc-publisher.ts +342 -0
  21. package/lib/agents-runner.ts +7 -2
  22. package/lib/animation-policy.ts +52 -0
  23. package/lib/background-cache-warming.ts +38 -0
  24. package/lib/command-palette-catalog.ts +1 -0
  25. package/lib/gentle-shell-launcher.ts +482 -0
  26. package/lib/inprocess-reviewer.ts +38 -1
  27. package/lib/native-review-cli.ts +36 -10
  28. package/lib/questionnaire/questionnaire-view.ts +603 -0
  29. package/lib/questionnaire/schema.ts +82 -0
  30. package/lib/questionnaire/validate.ts +141 -0
  31. package/lib/review-candidate-view-owner.ts +20 -5
  32. package/lib/review-candidate-view.ts +9 -2
  33. package/lib/review-host-relay.ts +10 -0
  34. package/lib/review-integration-v2.ts +4 -1
  35. package/lib/rpc-host.ts +36 -0
  36. package/lib/shell-bar.ts +13 -0
  37. package/lib/shell-sidebar-layout.ts +10 -4
  38. package/lib/shell-usage-view.ts +5 -2
  39. package/lib/shell-usage.ts +120 -6
  40. package/package.json +5 -1
  41. package/runtime/gentle-shell-launcher.mjs +483 -0
  42. package/runtime/native-review-cli.mjs +35 -9
  43. package/runtime/review-integration-v2.mjs +4 -1
  44. package/scripts/build-runtime-modules.mjs +1 -0
  45. package/scripts/gentle-ai-installer.mjs +10 -10
  46. package/scripts/install-gentle-ai.mjs +14 -7
  47. package/scripts/install-tui-mode-setting.mjs +78 -1
  48. package/scripts/verify-package-files.mjs +6 -3
  49. package/tests/agents-rpc-publisher.test.ts +407 -0
  50. package/tests/agents-runner.test.ts +10 -0
  51. package/tests/animation-policy.test.ts +42 -0
  52. package/tests/ask-user-choice.test.ts +129 -0
  53. package/tests/ask-user-question.test.ts +661 -0
  54. package/tests/background-cache-warming.test.ts +60 -0
  55. package/tests/background-subagents.test.ts +68 -0
  56. package/tests/command-palette.test.ts +9 -0
  57. package/tests/gentle-agents.test.ts +161 -2
  58. package/tests/gentle-ai-binary.test.ts +1 -1
  59. package/tests/gentle-ai-installer.test.ts +47 -47
  60. package/tests/gentle-ai.test.ts +56 -4
  61. package/tests/gentle-shell-bin.test.ts +188 -0
  62. package/tests/gentle-shell-launcher.test.ts +718 -0
  63. package/tests/gentle-shell.test.ts +355 -2
  64. package/tests/inprocess-reviewer.test.ts +92 -0
  65. package/tests/install-tui-mode-guard.test.ts +99 -0
  66. package/tests/install-tui-mode-setting.test.ts +39 -1
  67. package/tests/native-review-capability-contract.test.ts +16 -1
  68. package/tests/native-review-parity.test.ts +19 -0
  69. package/tests/package-manifest.test.ts +6 -17
  70. package/tests/questionnaire-schema.test.ts +274 -0
  71. package/tests/questionnaire-view.test.ts +446 -0
  72. package/tests/rdd-status-line.test.ts +21 -4
  73. package/tests/review-candidate-owner-retry.test.ts +63 -0
  74. package/tests/review-candidate-view.test.ts +15 -0
  75. package/tests/review-controller-native-routing.test.ts +86 -0
  76. package/tests/review-host-relay.test.ts +21 -0
  77. package/tests/review-integration-v2.test.ts +30 -0
  78. package/tests/review-ledger-contract.test.ts +1 -2
  79. package/tests/review-relay-transport-agent.test.ts +107 -2
  80. package/tests/review-risk-assessment.test.ts +104 -0
  81. package/tests/rpc-host.test.ts +77 -0
  82. package/tests/shell-bar.test.ts +8 -0
  83. package/tests/shell-sidebar-layout.test.ts +60 -5
  84. package/tests/shell-usage.test.ts +129 -0
  85. package/tests/skill-collision-prefixes.test.ts +1 -1
  86. package/tests/startup-banner.test.ts +93 -2
  87. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  88. package/skills/release/SKILL.md +0 -137
@@ -0,0 +1,483 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ import { join } from "node:path";
3
+
4
+ // The gentle-shell launcher: pure, side-effect-free functions over injected
5
+ // env/fs/exec. `bin/gentle-shell.mjs` (T2) wires these into the real process,
6
+ // filesystem and child process so this module stays fully unit-testable.
7
+
8
+
9
+
10
+ // pi's own package-management subcommands (see pi's cli/args.ts printHelp
11
+ // "Commands" list): each is dispatched by pi itself, before pi's own flag
12
+ // parsing, purely on argv[0]. `uninstall` is pi's alias for `remove`.
13
+ export const PI_SUBCOMMANDS = ["install", "remove", "uninstall", "update", "list", "config", "auth"] ;
14
+
15
+
16
+
17
+ function isPiSubcommand(token ) {
18
+ return (PI_SUBCOMMANDS ).includes(token);
19
+ }
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+  
32
+
33
+
34
+
35
+
36
+
37
+
38
+ // Home-subcommand parsing is deliberately shallow: `home` only counts as the
39
+ // subcommand when it is argv[0], and everything after it is handed over
40
+ // untouched as commandArgs — T2 owns interpreting `home link|isolated|<path>`.
41
+ export function parseLauncherArgs(argv ) {
42
+ if (argv[0] === "home") {
43
+ return {
44
+ link: false,
45
+ isolated: false,
46
+ home: undefined,
47
+ help: false,
48
+ version: false,
49
+ command: "home",
50
+ commandArgs: argv.slice(1),
51
+ passthrough: [],
52
+ piSubcommand: undefined,
53
+ error: undefined,
54
+ };
55
+ }
56
+
57
+ let link = false;
58
+ let isolated = false;
59
+ let home ;
60
+ let help = false;
61
+ let version = false;
62
+ let error ;
63
+ let piSubcommand ;
64
+ const passthrough = [];
65
+
66
+ for (let i = 0; i < argv.length; i += 1) {
67
+ const arg = argv[i];
68
+ if (arg === "--") {
69
+ const rest = argv.slice(i + 1);
70
+ if (passthrough.length === 0 && rest.length > 0 && isPiSubcommand(rest[0])) {
71
+ piSubcommand = rest[0];
72
+ }
73
+ passthrough.push(...rest);
74
+ break;
75
+ }
76
+ if (arg === "--link") {
77
+ link = true;
78
+ continue;
79
+ }
80
+ if (arg === "--isolated") {
81
+ isolated = true;
82
+ continue;
83
+ }
84
+ if (arg === "--help" || arg === "-h") {
85
+ help = true;
86
+ continue;
87
+ }
88
+ if (arg === "--version") {
89
+ version = true;
90
+ continue;
91
+ }
92
+ if (arg.startsWith("--home=")) {
93
+ const value = arg.slice("--home=".length);
94
+ if (value.length === 0) {
95
+ error = "--home requires a non-empty path argument";
96
+ continue;
97
+ }
98
+ home = value;
99
+ continue;
100
+ }
101
+ if (arg === "--home") {
102
+ const value = argv[i + 1];
103
+ if (value === undefined || value.length === 0) {
104
+ error = "--home requires a non-empty path argument";
105
+ if (value !== undefined) i += 1;
106
+ continue;
107
+ }
108
+ home = value;
109
+ i += 1;
110
+ continue;
111
+ }
112
+ if (passthrough.length === 0 && isPiSubcommand(arg)) {
113
+ piSubcommand = arg;
114
+ }
115
+ passthrough.push(arg);
116
+ }
117
+
118
+ if (error === undefined) {
119
+ if (link && isolated) {
120
+ error = "--link cannot be combined with --isolated";
121
+ } else if (link && home !== undefined) {
122
+ error = "--link cannot be combined with --home";
123
+ } else if (isolated && home !== undefined) {
124
+ error = "--isolated cannot be combined with --home";
125
+ }
126
+ }
127
+
128
+ return { link, isolated, home, help, version, command: undefined, commandArgs: [], passthrough, piSubcommand, error };
129
+ }
130
+
131
+ // --- home resolution -------------------------------------------------------
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+ // A discriminated union instead of a plain `home: string` field: `resolveHome`
143
+ // switches on `mode` rather than re-parsing the raw on-disk string, and the
144
+ // `path` case carries its `dir` explicitly so a "link"/"isolated" string can
145
+ // never be mistaken for a filesystem path at the call site.
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+ // Pi Subagents resolves `PI_CODING_AGENT_DIR || ~/.pi/agent`; `--link` reuses
156
+ // that exact home so gentle-shell never diverges from the user's own pi.
157
+ function linkDir(env , homedir ) {
158
+ return env.PI_CODING_AGENT_DIR || join(homedir, ".pi", "agent");
159
+ }
160
+
161
+ function isolatedDir(env , homedir ) {
162
+ return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
163
+ }
164
+
165
+ export function resolveHome(input ) {
166
+ const { args, env, homedir, config } = input;
167
+
168
+ if (args.link) return { mode: "link", dir: linkDir(env, homedir), source: "flag" };
169
+ if (args.isolated) return { mode: "isolated", dir: isolatedDir(env, homedir), source: "flag" };
170
+ if (args.home !== undefined) return { mode: "path", dir: args.home, source: "flag" };
171
+
172
+ if (config !== undefined) {
173
+ if (config.mode === "link") return { mode: "link", dir: linkDir(env, homedir), source: "config" };
174
+ if (config.mode === "isolated") return { mode: "isolated", dir: isolatedDir(env, homedir), source: "config" };
175
+ return { mode: "path", dir: config.dir, source: "config" };
176
+ }
177
+
178
+ return { mode: "isolated", dir: isolatedDir(env, homedir), source: "default" };
179
+ }
180
+
181
+ export function launcherConfigPath(homedir ) {
182
+ return join(homedir, ".gentle-shell", "config.json");
183
+ }
184
+
185
+ // Tolerant on purpose: a malformed or foreign config.json must never crash
186
+ // the launcher, it just falls through to the default isolated home.
187
+ //
188
+ // The on-disk shape stays the flat `{ "home": "link" | "isolated" | "<path>" }`
189
+ // documented in the feature scope; only the parsed, in-memory `LauncherConfig`
190
+ // is a discriminated union. Any non-empty string other than the exact literals
191
+ // "link" or "isolated" is treated as a path, including a near-miss like
192
+ // "linked" — this is deliberate: there is no separate "unrecognised mode"
193
+ // error, a typo just resolves to a (probably nonexistent) path instead.
194
+ export function parseLauncherConfig(text ) {
195
+ let parsed ;
196
+ try {
197
+ parsed = JSON.parse(text);
198
+ } catch {
199
+ return undefined;
200
+ }
201
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
202
+ const home = (parsed ).home;
203
+ if (typeof home !== "string" || home.length === 0) return undefined;
204
+ if (home === "link") return { mode: "link" };
205
+ if (home === "isolated") return { mode: "isolated" };
206
+ return { mode: "path", dir: home };
207
+ }
208
+
209
+ // --- pi runtime resolution ---------------------------------------------------
210
+
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
+
225
+
226
+ export function resolvePiRuntime(deps ) {
227
+ const envOverride = deps.env.GENTLE_SHELL_PI;
228
+ if (envOverride !== undefined && envOverride.length > 0) return { kind: "env", command: envOverride, args: [] };
229
+
230
+ const bundledCliPath = deps.resolveBundledCli();
231
+ if (bundledCliPath !== undefined) return { kind: "bundled", command: deps.nodeExecPath, args: [bundledCliPath] };
232
+
233
+ const onPath = deps.findOnPath("pi");
234
+ if (onPath !== undefined) return { kind: "path", command: onPath, args: [] };
235
+
236
+ return undefined;
237
+ }
238
+
239
+ export function missingPiMessage() {
240
+ return [
241
+ "No pi runtime could be found. Pick one of:",
242
+ " - Set GENTLE_SHELL_PI to the path of a pi executable.",
243
+ " - Install @earendil-works/pi-coding-agent next to gentle-pi (it ships as an optional peer dependency).",
244
+ " - Install pi and make sure it is on your PATH.",
245
+ ].join("\n");
246
+ }
247
+
248
+ // --- pi version gate ---------------------------------------------------------
249
+
250
+ export const MIN_PI_VERSION = "0.85.1";
251
+
252
+
253
+
254
+ const VERSION_PATTERN = /v?(\d+)\.(\d+)\.(\d+)/;
255
+
256
+ function compareVersions(a , b ) {
257
+ for (let i = 0; i < 3; i += 1) {
258
+ if (a[i] !== b[i]) return a[i] - b[i];
259
+ }
260
+ return 0;
261
+ }
262
+
263
+ export function checkPiVersion(output , minimum = MIN_PI_VERSION) {
264
+ const match = VERSION_PATTERN.exec(output);
265
+ if (!match) {
266
+ return { ok: false, message: `Could not determine the pi version from "${output.trim()}" (need at least ${minimum}).` };
267
+ }
268
+ const version = `${match[1]}.${match[2]}.${match[3]}`;
269
+ const minimumMatch = VERSION_PATTERN.exec(minimum);
270
+ if (!minimumMatch) throw new Error(`invalid minimum version "${minimum}"`);
271
+ const found = [Number(match[1]), Number(match[2]), Number(match[3])];
272
+ const wanted = [Number(minimumMatch[1]), Number(minimumMatch[2]), Number(minimumMatch[3])];
273
+ if (compareVersions(found, wanted) < 0) {
274
+ return { ok: false, version, message: `pi version ${version} is older than the required minimum ${minimum}.` };
275
+ }
276
+ return { ok: true, version };
277
+ }
278
+
279
+ // --- packaging drift guard -----------------------------------------------------
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+ // Keeps the MIN_PI_VERSION drift-guard test's failure readable: a missing
288
+ // peerDependencies block, a missing peer entry, or a malformed range must
289
+ // fail with a clear assertion message, not a raw TypeError from indexing an
290
+ // undefined value the way a direct `packageJson.peerDependencies[peerName]`
291
+ // lookup would.
292
+ export function checkPeerVersionPin(packageJson , peerName , minVersion ) {
293
+ const peerDependencies = packageJson.peerDependencies;
294
+ if (peerDependencies === undefined) {
295
+ return { ok: false, message: "package.json is missing a peerDependencies block" };
296
+ }
297
+ const pinned = peerDependencies[peerName];
298
+ if (typeof pinned !== "string") {
299
+ return { ok: false, message: `package.json peerDependencies is missing "${peerName}"` };
300
+ }
301
+ if (!/^>=\d+\.\d+\.\d+$/.test(pinned)) {
302
+ return { ok: false, message: `package.json peerDependencies["${peerName}"] ("${pinned}") is not a simple >=x.y.z range` };
303
+ }
304
+ const version = pinned.replace(/^>=/, "");
305
+ if (version !== minVersion) {
306
+ return { ok: false, message: `MIN_PI_VERSION ("${minVersion}") does not match the pinned peer range ("${pinned}")` };
307
+ }
308
+ return { ok: true, pinned };
309
+ }
310
+
311
+ // --- settings.json detection ---------------------------------------------------
312
+
313
+ function packageEntryDeclaresGentlePi(entry ) {
314
+ const declares = (value ) => typeof value === "string" && (value === "npm:gentle-pi" || value.startsWith("npm:gentle-pi@"));
315
+ if (declares(entry)) return true;
316
+ if (entry !== null && typeof entry === "object") return declares((entry ).source);
317
+ return false;
318
+ }
319
+
320
+ export function settingsDeclareGentlePi(settingsText ) {
321
+ if (settingsText === undefined) return false;
322
+ let parsed ;
323
+ try {
324
+ parsed = JSON.parse(settingsText);
325
+ } catch {
326
+ return false;
327
+ }
328
+ if (typeof parsed !== "object" || parsed === null) return false;
329
+ const packages = (parsed ).packages;
330
+ if (!Array.isArray(packages)) return false;
331
+ return packages.some(packageEntryDeclaresGentlePi);
332
+ }
333
+
334
+ // --- pi invocation builder ---------------------------------------------------
335
+
336
+
337
+
338
+
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+
354
+
355
+
356
+ // The `-e/--theme/--skill/--prompt-template` injection is skipped when the
357
+ // caller already confirmed the target settings.json declares the package
358
+ // (the `--link` case with a pi-managed install), or when passthrough[0] is
359
+ // one of pi's own subcommands: pi dispatches install/remove/uninstall/
360
+ // update/list/config/auth on argv[0] before it even parses flags, so any
361
+ // injected flag ahead of it stops pi from recognising its subcommand at
362
+ // all — this is exactly the observed 2026-09-22 bug where `gentle-shell
363
+ // install npm:x` opened an interactive pi session instead of running the
364
+ // package manager. Isolated and path homes never declare the package, so
365
+ // callers pass `settingsDeclareGentlePi: false` for those and the
366
+ // injection always happens there, unless a pi subcommand is set.
367
+ export function buildPiInvocation(input ) {
368
+ const args = [...input.runtime.args];
369
+ if (input.piSubcommand === undefined && !input.settingsDeclareGentlePi) {
370
+ args.push(
371
+ "-e",
372
+ input.packageRoot,
373
+ "--theme",
374
+ join(input.packageRoot, "themes"),
375
+ "--skill",
376
+ join(input.packageRoot, "skills"),
377
+ "--prompt-template",
378
+ join(input.packageRoot, "prompts"),
379
+ );
380
+ }
381
+ args.push(...input.passthrough);
382
+
383
+ return {
384
+ command: input.runtime.command,
385
+ args,
386
+ env: { ...input.baseEnv, PI_CODING_AGENT_DIR: input.home.dir, GENTLE_PI_AGENT_HOME: input.home.dir },
387
+ };
388
+ }
389
+
390
+ // --- spawn planning ------------------------------------------------------------
391
+
392
+ // R3-001: `findOnPath` can resolve a PATHEXT candidate such as a .CMD or .BAT
393
+ // shim on win32 (exactly how an npm-installed `pi` lands on PATH), and a
394
+ // GENTLE_SHELL_PI override can point at one too. Current Node releases refuse
395
+ // to spawn a batch file directly without `shell: true` (EINVAL), so both the
396
+ // version probe and the real launch route a batch shim through cmd.exe as one
397
+ // quoted command line instead of spawning it directly.
398
+ const CMD_EXE_SPECIAL_CHARS = /[\s"&|<>^%()]/;
399
+
400
+ // cmd.exe quoting is deliberately simple, not a full cmd.exe parser: wrap a
401
+ // token in double quotes when it is empty or contains whitespace or any of
402
+ // `"&|<>^%()`, and escape an inner `"` as `\"` — doubling inner quotes is not
403
+ // reliable in cmd.exe, unlike the `\"` convention Node's own Windows spawn
404
+ // helpers use.
405
+ export function quoteForCmdExe(token ) {
406
+ if (token.length > 0 && !CMD_EXE_SPECIAL_CHARS.test(token)) return token;
407
+ return `"${token.replace(/"/g, '\\"')}"`;
408
+ }
409
+
410
+
411
+
412
+
413
+
414
+
415
+
416
+
417
+
418
+
419
+
420
+
421
+
422
+ export function planSpawn(input ) {
423
+ const { command, args, platform } = input;
424
+ if (platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
425
+ return { command: [command, ...args].map(quoteForCmdExe).join(" "), args: [], shell: true };
426
+ }
427
+ return { command, args, shell: false };
428
+ }
429
+
430
+ // --- reporting ---------------------------------------------------------------
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
+ export function describeVersion(input ) {
439
+ return [
440
+ `gentle-shell ${input.gentlePiVersion}`,
441
+ `pi ${input.piVersion ?? "not found"}`,
442
+ `home ${input.home.mode} ${input.home.dir}`,
443
+ ].join("\n");
444
+ }
445
+
446
+ export function helpText() {
447
+ return [
448
+ "Usage: gentle-shell [options] [-- pi-args...]",
449
+ " gentle-shell home [link|isolated|<path>]",
450
+ "",
451
+ "Opens pi with the Gentle Shell package loaded, without touching your",
452
+ "vanilla pi installation.",
453
+ "",
454
+ "Options:",
455
+ " --link Use your existing pi agent home (never edits its settings.json).",
456
+ " --isolated Use the dedicated ~/.gentle-shell/agent home (default).",
457
+ " --home <path> Use a custom agent home directory.",
458
+ " --help, -h Show this help text.",
459
+ " --version Show gentle-shell, pi, and home version information.",
460
+ "",
461
+ "Commands:",
462
+ " home Print or persist the effective home mode (link, isolated, or a path).",
463
+ "",
464
+ "Managing packages:",
465
+ " gentle-shell install npm:<pkg> Run pi's own 'install' against the resolved home.",
466
+ " gentle-shell remove <source> Run pi's own 'remove' against the resolved home.",
467
+ " gentle-shell list Run pi's own 'list' against the resolved home.",
468
+ " gentle-shell update [target] Run pi's own 'update' against the resolved home.",
469
+ " gentle-shell config Run pi's own 'config' against the resolved home.",
470
+ " gentle-shell auth <command> Run pi's own 'auth' against the resolved home.",
471
+ " These run pi's own commands, forwarded verbatim, against the --isolated home",
472
+ " (or your own pi home with --link). Running 'gentle-shell install npm:gentle-pi'",
473
+ " inside the isolated home is unnecessary: gentle-shell already loads the",
474
+ " package itself.",
475
+ "",
476
+ "Environment variables:",
477
+ " GENTLE_SHELL_PI Path to the pi executable to run.",
478
+ " GENTLE_SHELL_HOME Directory for the isolated home (default: ~/.gentle-shell/agent).",
479
+ " PI_CODING_AGENT_DIR Directory for the --link home, shared with pi itself.",
480
+ "",
481
+ "Every other argument is forwarded to pi unchanged.",
482
+ ].join("\n");
483
+ }
@@ -706,7 +706,11 @@ function isNativeUntrackedPath(value ) {
706
706
  && value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
707
707
  }
708
708
 
709
- function nativeUntrackedSelection(request ) {
709
+ export function nativeUntrackedSelection(request
710
+
711
+
712
+
713
+ ) {
710
714
  const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
711
715
  const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
712
716
  if (!declared) return {};
@@ -717,16 +721,19 @@ function nativeUntrackedSelection(request )
717
721
  ) {
718
722
  throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
719
723
  }
720
- if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (intendedUntracked?.length ?? 0) > 0) {
724
+ // The guard above establishes the array and element types for both typed
725
+ // native requests and untyped facade input.
726
+ const paths = intendedUntracked ;
727
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (paths?.length ?? 0) > 0) {
721
728
  throw new TypeError("Native exclude untracked selection cannot include paths");
722
729
  }
723
- if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (intendedUntracked?.length ?? 0) === 0) {
730
+ if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (paths?.length ?? 0) === 0) {
724
731
  throw new TypeError("Native select untracked selection requires at least one path");
725
732
  }
726
733
  return {
727
734
  untrackedScope,
728
735
  expectedUntrackedInventory,
729
- intendedUntracked: intendedUntracked === undefined ? undefined : [...intendedUntracked],
736
+ intendedUntracked: paths === undefined ? undefined : [...paths],
730
737
  };
731
738
  }
732
739
 
@@ -1019,6 +1026,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
1019
1026
  // and hint remain dark because neither is proven to reach the negotiated
1020
1027
  // START path Pi consumes.
1021
1028
  "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 }),
1029
+ // v3.5.0 repeats 3.4.0: the published provider contract bundle is
1030
+ // byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
1031
+ // with only build-identity differences, and no review-integration schema
1032
+ // changed. The v2 preflight failure identity fix does not change the
1033
+ // closed START/STATUS fields this row negotiates. riskEvidence and hint
1034
+ // remain dark; neither is proven to reach Pi's negotiated START path.
1035
+ "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 }),
1022
1036
  });
1023
1037
 
1024
1038
 
@@ -1103,8 +1117,19 @@ function decodeSelectedLenses(value , riskLevel , lensesRequired
1103
1117
  function enumString(value , allowed ) { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
1104
1118
  const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
1105
1119
 
1106
- function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT) {
1107
- const normalized = value
1120
+ function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation ) {
1121
+ // ASSESS diagnostics are projected into a public verification plan. Retain
1122
+ // native guidance, not local paths or environment assignment values.
1123
+ const input = operation === NATIVE_REVIEW_OPERATION.ASSESS
1124
+ ? value
1125
+ .replace(/(?<![\w-])[a-z_][a-z0-9_]*=(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s]+)/gi, "[REDACTED ENV]")
1126
+ .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]")
1127
+ // Quoted paths have a clear boundary. For an unquoted path, the
1128
+ // remaining line is ambiguous (spaces may belong to the filename).
1129
+ // Redact that suffix rather than leak trailing path components.
1130
+ .replace(/"(?:[A-Za-z]:[\\/]|\/)[^"\r\n]*"|'(?:[A-Za-z]:[\\/]|\/)[^'\r\n]*'|(?:[A-Za-z]:[\\/]|\/)[^\r\n]*/g, "[REDACTED PATH]")
1131
+ : value;
1132
+ const normalized = input
1108
1133
  .replace(/\x1b](?:[^\x07\x1b]|\x1b(?!\\))*?(?:\x07|\x1b\\)/g, "[REDACTED CONTROL]")
1109
1134
  .replace(/\x1b[PX^_][\s\S]*?\x1b\\/g, "[REDACTED CONTROL]")
1110
1135
  .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "[REDACTED CONTROL]")
@@ -1138,7 +1163,7 @@ export function sanitizeForeignNativeReviewDiagnostics(value )
1138
1163
  timed_out: booleanValue(raw.timed_out),
1139
1164
  output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
1140
1165
  ...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
1141
- ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
1166
+ ...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr), NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) }),
1142
1167
  };
1143
1168
  } catch { return undefined; }
1144
1169
  }
@@ -1155,7 +1180,7 @@ function nativeProcessDiagnostics(operation , code
1155
1180
  ...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
1156
1181
  ? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
1157
1182
  : {}),
1158
- ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
1183
+ ...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr, NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) } : {}),
1159
1184
  };
1160
1185
  }
1161
1186
 
@@ -2536,6 +2561,7 @@ export class NativeReviewCliV216 {
2536
2561
  // rejects -- callers (the `gentle_review` tool's `assess` operation) fail
2537
2562
  // closed to `high`.
2538
2563
  async assess(request ) {
2564
+ const selection = nativeUntrackedSelection(request);
2539
2565
  if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
2540
2566
  if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
2541
2567
  if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
@@ -2543,7 +2569,7 @@ export class NativeReviewCliV216 {
2543
2569
  const execution = await this.invoke(
2544
2570
  NATIVE_REVIEW_OPERATION.ASSESS,
2545
2571
  cwd,
2546
- ["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
2572
+ ["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...nativeUntrackedSelectionArguments(selection), "--json"],
2547
2573
  false,
2548
2574
  request.signal,
2549
2575
  this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
@@ -340,6 +340,7 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
340
340
 
341
341
 
342
342
 
343
+
343
344
 
344
345
 
345
346
  const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] ;
@@ -1151,7 +1152,8 @@ export function decodeReviewArtifactSubjectV2(value )
1151
1152
  }
1152
1153
 
1153
1154
  function decodeChangedPathEntry(value , label ) {
1154
- const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"]);
1155
+ const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"], ["generated"]);
1156
+ if (body.generated !== undefined && body.generated !== true) throw new TypeError(`${label}.generated must be true when present`);
1155
1157
  return {
1156
1158
  path: nonempty(body.path, `${label}.path`),
1157
1159
  status: enumeration(body.status, ["A", "D", "M", "T"] , `${label}.status`),
@@ -1161,6 +1163,7 @@ function decodeChangedPathEntry(value , label )
1161
1163
  typeChanged: boolean(body.type_changed, `${label}.type_changed`),
1162
1164
  modeOnly: boolean(body.mode_only, `${label}.mode_only`),
1163
1165
  intendedUntracked: boolean(body.intended_untracked, `${label}.intended_untracked`),
1166
+ ...(body.generated === undefined ? {} : { generated: true }),
1164
1167
  };
1165
1168
  }
1166
1169
 
@@ -13,6 +13,7 @@ const sources = [
13
13
  "review-risk-assessment",
14
14
  "native-review-cli",
15
15
  "telemetry-trigger",
16
+ "gentle-shell-launcher",
16
17
  ];
17
18
  const header = "// Generated by scripts/build-runtime-modules.mjs. Do not edit.\n";
18
19
 
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
36
36
  // version check below) derives from this constant instead of repeating the
37
37
  // literal, so a pin bump cannot leave a stale copy behind. See
38
38
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
39
- export const INSTALLER_VERSION = "3.4.0";
39
+ export const INSTALLER_VERSION = "3.5.0";
40
40
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
41
41
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
42
42
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
45
45
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.4.0`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.5.0`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit 82a6de96, the published v3.4.0 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:bQOM+Xa3WN5+idTagEk15rgEEQQWVhZvq8BPF/FHtSs=";
50
+ // tag resolves to commit 0b335e43, the published v3.5.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:3y+Nb7CtgGB3ne3bSNxkzV6j19etkOnGDXIiuv7tMp8=";
52
52
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
53
53
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
54
54
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
67
67
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
68
68
  // sentinel digest can never match a real SHA-256, so installation fails closed,
69
69
  // and verify-package-files.mjs refuses to pack/publish while any digest below
70
- // still holds it. The v3.4.0 digests are pinned from the published release:
70
+ // still holds it. The v3.5.0 digests are pinned from the published release:
71
71
  // archive sha256 values verified against the minisign-signed checksums.txt and
72
72
  // freshly computed hashes; binary sha256 values computed from the extracted
73
73
  // executables.
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
109
109
  }
110
110
 
111
111
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
112
- // distributing unsigned Windows builds in c4b764d0, so v3.4.0 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v3.5.0 publishes signed
113
113
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
114
114
  // exact-tag Go SumDB source-build path below; restore archive rows only when
115
115
  // upstream ships signed Windows assets.
116
116
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
117
- "darwin/amd64": asset("gentle-ai_3.4.0_darwin_amd64.tar.gz", "7d13fa45489098ff9e82dee2232204f72bf1c04ef998ceadfe1ea972697f2e56", "d9096b4bb13e56aae1251dfacd2487821a15c40b7c9397555ad615ede47e658a", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_3.4.0_darwin_arm64.tar.gz", "b9052d8dc02923663d4451a4b031c4ef7328cd866c0cb6729e8ff2b9853603f0", "0aca1239cafd87ce63b194ab69da90e762801d82a96418fd6b7b12ea3806dc07", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_3.4.0_linux_amd64.tar.gz", "c287289a514420381e890991bb3fbea4a2c36d7b4b1774fcf6bc4deb83378915", "309d9aafb48de5ef90ba0a212e8a98e8fdbb82d0852e24015e36a5ce0fe04c06", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_3.4.0_linux_arm64.tar.gz", "dfd1fe70acfe577d2a36e3a658895f481467518a2923b7200403dbcca0181399", "83ba7d27d250019115690be51664bd356e09085da8858761e362e6053c6290d3", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_3.5.0_darwin_amd64.tar.gz", "483b8363cc2b717d224ee7a15c834ed518854c31a1d79d84b6ea0551fd7905cf", "06e5ce2936f0b0579fa382b5cd59cf42c23b07404b162d9ff9a036dd107093a4", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_3.5.0_darwin_arm64.tar.gz", "d7a5c233ce23e8fc2ce917ed3e6a2e643655e6e8322fddb3c522324d4cf8ae52", "ec7757fe13f6cd5c9dffa6466d75c5cd68992eac44e684d330c838282c5e2e7e", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_3.5.0_linux_amd64.tar.gz", "3ea016622981f5e4f4a80632fb9c4673149c98f4e5cecc4b979b41bda758ba4a", "7a4202f75c8b90056f0ecd8fceaaef9efe407abc6f98bb42a3ee1e5ad69eb197", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_3.5.0_linux_arm64.tar.gz", "aca1997b3ae8439c17883cde39c3ba577ae38352ad6be1d1c4b2c6bc428c4505", "4d81f356f6e221bc5730a280bae049df8315232a2470089993200577e36e97cc", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,