gentle-pi 3.2.0 → 3.3.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/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +40 -17
- package/docs/readme-reference.md +41 -7
- package/docs/review-integration.md +25 -11
- package/extensions/gentle-agents.ts +85 -17
- package/extensions/gentle-ai.ts +179 -12
- package/extensions/gentle-shell.ts +408 -38
- package/extensions/gentle-todo.ts +19 -1
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +260 -0
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +23 -0
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/review-host-relay.ts +262 -94
- package/lib/review-integration-v2.ts +110 -26
- package/lib/shell-bar.ts +158 -29
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +111 -15
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +98 -10
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +23 -0
- package/runtime/review-integration-v2.mjs +110 -26
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/command-palette.test.ts +1 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +528 -5
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +69 -5
- package/tests/gentle-shell.test.ts +903 -25
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +368 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +32 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +285 -239
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +116 -1
- package/tests/review-relay-transport-agent.test.ts +83 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +224 -6
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +243 -5
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +123 -3
- package/tests/shell-usage.test.ts +254 -6
- package/lib/opaque-pi-reviewer-adapter.ts +0 -284
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -266
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
8
|
-
OPAQUE_PI_REVIEWER_ARGV,
|
|
9
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE,
|
|
10
|
-
OpaquePiReviewerTransportError,
|
|
11
|
-
resolvePiLaunch,
|
|
12
|
-
runOpaquePiReviewer,
|
|
13
|
-
} from "../lib/opaque-pi-reviewer-adapter.ts";
|
|
14
|
-
|
|
15
|
-
const FAKE_PI = `#!/usr/bin/env node
|
|
16
|
-
const fs = require("node:fs");
|
|
17
|
-
const path = require("node:path");
|
|
18
|
-
const argv = process.argv.slice(2);
|
|
19
|
-
const chunks = [];
|
|
20
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
21
|
-
process.stdin.on("end", () => {
|
|
22
|
-
const stdin = Buffer.concat(chunks);
|
|
23
|
-
if (process.env.OPAQUE_PI_STDIN_CAPTURE) fs.writeFileSync(process.env.OPAQUE_PI_STDIN_CAPTURE, stdin);
|
|
24
|
-
const mode = process.env.OPAQUE_PI_MODE || "ok";
|
|
25
|
-
const log = {
|
|
26
|
-
argv,
|
|
27
|
-
cwd: process.cwd(),
|
|
28
|
-
entries_before: fs.readdirSync(process.cwd()),
|
|
29
|
-
entries_after: fs.readdirSync(process.cwd()),
|
|
30
|
-
};
|
|
31
|
-
if (process.env.OPAQUE_PI_LOG) fs.appendFileSync(process.env.OPAQUE_PI_LOG, JSON.stringify(log) + "\\n");
|
|
32
|
-
if (mode === "break-cleanup" || process.env.OPAQUE_PI_BREAK_CLEANUP === "true") {
|
|
33
|
-
fs.chmodSync(path.dirname(process.cwd()), 0o500);
|
|
34
|
-
}
|
|
35
|
-
if (mode === "empty") process.exit(0);
|
|
36
|
-
if (mode === "hang") { setTimeout(() => process.exit(0), 10_000); return; }
|
|
37
|
-
if (mode === "nonzero") { process.stderr.write("opaque pi failed\\n"); process.exit(7); }
|
|
38
|
-
process.stdout.write(Buffer.from(process.env.OPAQUE_PI_OUTPUT_B64 || "", "base64"));
|
|
39
|
-
});
|
|
40
|
-
`;
|
|
41
|
-
|
|
42
|
-
const PROMPT_BYTES = Buffer.concat([
|
|
43
|
-
Buffer.from("opaque prompt\r\n\u0000", "utf8"),
|
|
44
|
-
Buffer.from([0x01, 0xff, 0xfe, 0x00]),
|
|
45
|
-
]);
|
|
46
|
-
const OUTPUT_BYTES = Buffer.concat([
|
|
47
|
-
Buffer.from("opaque output\r\n\u0000", "utf8"),
|
|
48
|
-
Buffer.from([0x07, 0xff, 0xfe, 0x00]),
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
interface OpaqueHarness {
|
|
52
|
-
directory: string;
|
|
53
|
-
pi: string;
|
|
54
|
-
logPath: string;
|
|
55
|
-
stdinCapturePath: string;
|
|
56
|
-
environment: NodeJS.ProcessEnv;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface OpaquePiLog {
|
|
60
|
-
argv: string[];
|
|
61
|
-
cwd: string;
|
|
62
|
-
entries_before: string[];
|
|
63
|
-
entries_after: string[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function harness(t: test.TestContext, overrides: Record<string, string> = {}): OpaqueHarness {
|
|
67
|
-
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-"));
|
|
68
|
-
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
69
|
-
const pi = join(directory, "pi");
|
|
70
|
-
writeFileSync(pi, FAKE_PI);
|
|
71
|
-
chmodSync(pi, 0o755);
|
|
72
|
-
const logPath = join(directory, "pi.log");
|
|
73
|
-
const stdinCapturePath = join(directory, "stdin.bin");
|
|
74
|
-
return {
|
|
75
|
-
directory,
|
|
76
|
-
pi,
|
|
77
|
-
logPath,
|
|
78
|
-
stdinCapturePath,
|
|
79
|
-
environment: {
|
|
80
|
-
...process.env,
|
|
81
|
-
OPAQUE_PI_LOG: logPath,
|
|
82
|
-
OPAQUE_PI_STDIN_CAPTURE: stdinCapturePath,
|
|
83
|
-
OPAQUE_PI_OUTPUT_B64: OUTPUT_BYTES.toString("base64"),
|
|
84
|
-
...overrides,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function readLog(path: string): OpaquePiLog[] {
|
|
90
|
-
if (!existsSync(path)) return [];
|
|
91
|
-
return readFileSync(path, "utf8")
|
|
92
|
-
.split("\n")
|
|
93
|
-
.filter((line) => line.length > 0)
|
|
94
|
-
.map((line) => JSON.parse(line) as OpaquePiLog);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function rejectsWithTransportError(
|
|
98
|
-
promise: Promise<unknown>,
|
|
99
|
-
kind: (typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE)[keyof typeof OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE],
|
|
100
|
-
): Promise<OpaquePiReviewerTransportError> {
|
|
101
|
-
let caught: OpaquePiReviewerTransportError | undefined;
|
|
102
|
-
await assert.rejects(promise, (error: unknown) => {
|
|
103
|
-
assert.ok(error instanceof OpaquePiReviewerTransportError, `expected OpaquePiReviewerTransportError, received ${String(error)}`);
|
|
104
|
-
caught = error;
|
|
105
|
-
return error.kind === kind;
|
|
106
|
-
});
|
|
107
|
-
return caught!;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
test("the opaque adapter streams arbitrary prompt and stdout bytes verbatim through the fixed Pi subprocess", async (t) => {
|
|
111
|
-
const fixture = harness(t);
|
|
112
|
-
const result = await runOpaquePiReviewer(PROMPT_BYTES, {
|
|
113
|
-
piExecutable: fixture.pi,
|
|
114
|
-
environment: fixture.environment,
|
|
115
|
-
timeoutMs: 10_000,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
assert.equal(result.promptByteLength, PROMPT_BYTES.length);
|
|
119
|
-
assert.equal(result.stdoutByteLength, OUTPUT_BYTES.length);
|
|
120
|
-
assert.deepEqual(result.stdout, OUTPUT_BYTES);
|
|
121
|
-
assert.deepEqual(readFileSync(fixture.stdinCapturePath), PROMPT_BYTES);
|
|
122
|
-
|
|
123
|
-
const calls = readLog(fixture.logPath);
|
|
124
|
-
assert.equal(calls.length, 1);
|
|
125
|
-
assert.deepEqual(calls[0]!.argv, [...OPAQUE_PI_REVIEWER_ARGV]);
|
|
126
|
-
assert.deepEqual(calls[0]!.entries_before, []);
|
|
127
|
-
assert.deepEqual(calls[0]!.entries_after, []);
|
|
128
|
-
assert.notEqual(calls[0]!.cwd, process.cwd());
|
|
129
|
-
assert.equal(existsSync(calls[0]!.cwd), false, "the empty scratch directory is removed after success");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("the opaque adapter returns typed transport errors for launch, nonzero, empty, timeout, and cancellation", async (t) => {
|
|
133
|
-
const fixture = harness(t);
|
|
134
|
-
await rejectsWithTransportError(
|
|
135
|
-
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: join(fixture.directory, "missing-pi"), environment: fixture.environment, timeoutMs: 10_000 }),
|
|
136
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.LAUNCH_FAILED,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const nonzero = await rejectsWithTransportError(
|
|
140
|
-
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero" }, timeoutMs: 10_000 }),
|
|
141
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
|
|
142
|
-
);
|
|
143
|
-
assert.equal(nonzero.exitCode, 7);
|
|
144
|
-
assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
|
|
145
|
-
|
|
146
|
-
await rejectsWithTransportError(
|
|
147
|
-
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "empty" }, timeoutMs: 10_000 }),
|
|
148
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.EMPTY_OUTPUT,
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const timedOut = await rejectsWithTransportError(
|
|
152
|
-
runOpaquePiReviewer(PROMPT_BYTES, { piExecutable: fixture.pi, environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" }, timeoutMs: 300 }),
|
|
153
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
|
|
154
|
-
);
|
|
155
|
-
assert.equal(timedOut.timedOut, true);
|
|
156
|
-
|
|
157
|
-
const controller = new AbortController();
|
|
158
|
-
const cancellation = runOpaquePiReviewer(PROMPT_BYTES, {
|
|
159
|
-
piExecutable: fixture.pi,
|
|
160
|
-
environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang" },
|
|
161
|
-
timeoutMs: 10_000,
|
|
162
|
-
signal: controller.signal,
|
|
163
|
-
});
|
|
164
|
-
setTimeout(() => controller.abort(), 100).unref();
|
|
165
|
-
const cancelled = await rejectsWithTransportError(cancellation, OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CANCELLED);
|
|
166
|
-
assert.equal(cancelled.cancelled, true);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("the opaque adapter leaves no prompt or result file in scratch and reports cleanup failure as transport-only", async (t) => {
|
|
170
|
-
const fixture = harness(t);
|
|
171
|
-
const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-cleanup-"));
|
|
172
|
-
const originalTmpdir = process.env.TMPDIR;
|
|
173
|
-
process.env.TMPDIR = scratchParent;
|
|
174
|
-
try {
|
|
175
|
-
const error = await rejectsWithTransportError(
|
|
176
|
-
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
177
|
-
piExecutable: fixture.pi,
|
|
178
|
-
environment: { ...fixture.environment, OPAQUE_PI_MODE: "break-cleanup" },
|
|
179
|
-
timeoutMs: 10_000,
|
|
180
|
-
}),
|
|
181
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.CLEANUP_FAILED,
|
|
182
|
-
);
|
|
183
|
-
assert.match(error.message, /scratch directory/i);
|
|
184
|
-
} finally {
|
|
185
|
-
if (originalTmpdir === undefined) delete process.env.TMPDIR;
|
|
186
|
-
else process.env.TMPDIR = originalTmpdir;
|
|
187
|
-
chmodSync(scratchParent, 0o700);
|
|
188
|
-
rmSync(scratchParent, { recursive: true, force: true });
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("the opaque adapter preserves primary nonzero and timeout errors when scratch cleanup also fails", async (t) => {
|
|
193
|
-
const fixture = harness(t);
|
|
194
|
-
const scratchParent = mkdtempSync(join(tmpdir(), "gentle-pi-opaque-reviewer-primary-failure-"));
|
|
195
|
-
const originalTmpdir = process.env.TMPDIR;
|
|
196
|
-
process.env.TMPDIR = scratchParent;
|
|
197
|
-
try {
|
|
198
|
-
const nonzero = await rejectsWithTransportError(
|
|
199
|
-
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
200
|
-
piExecutable: fixture.pi,
|
|
201
|
-
environment: { ...fixture.environment, OPAQUE_PI_MODE: "nonzero", OPAQUE_PI_BREAK_CLEANUP: "true" },
|
|
202
|
-
timeoutMs: 10_000,
|
|
203
|
-
}),
|
|
204
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.NONZERO_EXIT,
|
|
205
|
-
);
|
|
206
|
-
assert.equal(nonzero.exitCode, 7);
|
|
207
|
-
assert.deepEqual(nonzero.stderr, Buffer.from("opaque pi failed\n"));
|
|
208
|
-
chmodSync(scratchParent, 0o700);
|
|
209
|
-
|
|
210
|
-
const timedOut = await rejectsWithTransportError(
|
|
211
|
-
runOpaquePiReviewer(PROMPT_BYTES, {
|
|
212
|
-
piExecutable: fixture.pi,
|
|
213
|
-
environment: { ...fixture.environment, OPAQUE_PI_MODE: "hang", OPAQUE_PI_BREAK_CLEANUP: "true" },
|
|
214
|
-
timeoutMs: 300,
|
|
215
|
-
}),
|
|
216
|
-
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT,
|
|
217
|
-
);
|
|
218
|
-
assert.equal(timedOut.timedOut, true);
|
|
219
|
-
} finally {
|
|
220
|
-
if (originalTmpdir === undefined) delete process.env.TMPDIR;
|
|
221
|
-
else process.env.TMPDIR = originalTmpdir;
|
|
222
|
-
chmodSync(scratchParent, 0o700);
|
|
223
|
-
rmSync(scratchParent, { recursive: true, force: true });
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("the opaque adapter has no review lifecycle imports or identifiers", () => {
|
|
228
|
-
const adapterPath = fileURLToPath(new URL("../lib/opaque-pi-reviewer-adapter.ts", import.meta.url));
|
|
229
|
-
const source = readFileSync(adapterPath, "utf8");
|
|
230
|
-
assert.doesNotMatch(source, /review-integration|gentle-ai|materialize|submit/i);
|
|
231
|
-
for (const identifier of ["lineage", "target", "revision", "receipt", "lens", "order", "subject", "schema", "capture", "submission", "status", "model", "provider", "profile"]) {
|
|
232
|
-
assert.doesNotMatch(source, new RegExp(`\\b${identifier}\\b`, "i"), `adapter must not contain lifecycle identifier ${identifier}`);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// #468 / #519: on Windows a bare `pi` resolves to pi.cmd, pi.ps1, or a POSIX
|
|
237
|
-
// shim, none of which Node can spawn with shell:false (EINVAL or ENOENT). The
|
|
238
|
-
// relay already runs inside Pi, so the host's own JavaScript entry is spawned
|
|
239
|
-
// through process.execPath instead. Other platforms keep today's exact shape.
|
|
240
|
-
test("the Pi launch shape spawns the host entry through process.execPath on win32 and stays byte-identical elsewhere", () => {
|
|
241
|
-
const host = {
|
|
242
|
-
execPath: "C:\\Program Files\\nodejs\\node.exe",
|
|
243
|
-
entry: "C:\\Users\\dev\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\bundle\\cli.js",
|
|
244
|
-
};
|
|
245
|
-
const windows = resolvePiLaunch(undefined, "win32", host);
|
|
246
|
-
assert.deepEqual(windows, { file: host.execPath, arguments: [host.entry, ...OPAQUE_PI_REVIEWER_ARGV] });
|
|
247
|
-
assert.notEqual(windows.file, "pi");
|
|
248
|
-
assert.equal(windows.arguments.includes("pi"), false);
|
|
249
|
-
for (const platform of ["linux", "darwin", "freebsd"] as const) {
|
|
250
|
-
assert.deepEqual(resolvePiLaunch(undefined, platform, host), { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
|
|
251
|
-
assert.deepEqual(resolvePiLaunch("/opt/pi/bin/pi", platform, host), { file: "/opt/pi/bin/pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] }, platform);
|
|
252
|
-
}
|
|
253
|
-
assert.deepEqual(resolvePiLaunch("C:\\tools\\pi.exe", "win32", host), { file: "C:\\tools\\pi.exe", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
|
|
254
|
-
for (const entry of [undefined, "", "cli.js", "dist/bundle/cli.js"]) {
|
|
255
|
-
assert.throws(() => resolvePiLaunch(undefined, "win32", { execPath: host.execPath, entry }), /host entry/, `entry ${JSON.stringify(entry)} must fail closed instead of spawning a bare pi`);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("the default Pi launch resolves from the running host process", () => {
|
|
260
|
-
const launch = resolvePiLaunch(undefined);
|
|
261
|
-
if (process.platform === "win32") {
|
|
262
|
-
assert.deepEqual(launch, { file: process.execPath, arguments: [process.argv[1]!, ...OPAQUE_PI_REVIEWER_ARGV] });
|
|
263
|
-
} else {
|
|
264
|
-
assert.deepEqual(launch, { file: "pi", arguments: [...OPAQUE_PI_REVIEWER_ARGV] });
|
|
265
|
-
}
|
|
266
|
-
});
|