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,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
|
-
|
|
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 && (
|
|
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:
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
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.
|
|
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
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
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.
|
|
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.
|
|
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.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
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,
|