gentle-pi 4.3.0 → 4.4.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 +8 -5
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +101 -151
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell.md +77 -7
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +19 -15
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +173 -38
- package/extensions/gentle-ai.ts +214 -69
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +332 -73
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +123 -49
- package/lib/gentle-shell-launcher.ts +9 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +4 -3
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +11 -8
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +122 -48
- package/runtime/gentle-shell-launcher.mjs +9 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +7 -7
- package/scripts/installer-preflight.mjs +5 -3
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +34 -6
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +3 -3
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +1 -1
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +365 -20
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +292 -13
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +335 -0
- package/tests/gentle-shell-bundled.test.ts +301 -248
- package/tests/gentle-shell-launcher.test.ts +28 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +183 -8
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +99 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +14 -2
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +50 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
package/tests/pi-update.test.ts
CHANGED
|
@@ -11,8 +11,12 @@ const CLI = 'dist/bundle/cli.js';
|
|
|
11
11
|
// peer is a sibling of the Shell package; a separate global Pi links to the
|
|
12
12
|
// same store entry. npm nests the peer under the Shell package.
|
|
13
13
|
function fixture({windows = false, npm = false, runtime = 'adjacent', groups = 'both', peerAfter = '1.2.0', shellVersion = '4.1.0',
|
|
14
|
-
badManifest = false, failed = false, mismatch = false, linked = false, readback = true, unrelatedPeer = false, shim = ''
|
|
14
|
+
badManifest = false, failed = false, mismatch = false, linked = false, readback = true, unrelatedPeer = false, shim = '',
|
|
15
|
+
artifact = true, configHome = '', installFailure = undefined as undefined | {stdout?: string, stderrTail?: string}} = {}) {
|
|
15
16
|
const base = windows ? 'C:/pnpm' : '/pnpm';
|
|
17
|
+
const home = windows ? 'C:/Users/u' : '/home/u';
|
|
18
|
+
// The tarball `gentle-shell upgrade --channel main` packs, named after its version.
|
|
19
|
+
const tarball = `${configHome || `${home}/.pi/gentle-ai`}/main/packages/gentle-pi-${shellVersion}.tgz`;
|
|
16
20
|
const storePi = (version: string) => `${base}/store/links/@earendil-works/pi-coding-agent/${version}/h/node_modules/${PI}`;
|
|
17
21
|
const shellModules = npm ? `${base}/npm/lib/node_modules` : `${base}/store/links/@/gentle-pi/4.1.0/h1/node_modules`;
|
|
18
22
|
const shell = `${shellModules}/gentle-pi`;
|
|
@@ -31,6 +35,7 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
31
35
|
const mutations: string[][] = [];
|
|
32
36
|
const out: string[] = [];
|
|
33
37
|
const cwds: unknown[] = [];
|
|
38
|
+
const runOptions: unknown[] = [];
|
|
34
39
|
const entry = (pkg: string, modules: string, extra = {}) => ({[pkg]: {path: `${modules}/${pkg}`, ...extra}});
|
|
35
40
|
const shellEntry = entry('gentle-pi', g1, linked ? {link: true} : {});
|
|
36
41
|
const piEntry = entry(PI, g2, linked ? {resolved: 'file:/src/pi'} : {});
|
|
@@ -41,7 +46,7 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
41
46
|
return npm ? group : [group];
|
|
42
47
|
};
|
|
43
48
|
const exists = (n: string) => [g1, g2, g3, shellModules, shell, storePi(state.peer), storePi(state.group), storePi('1.1.0'), nestedPeer]
|
|
44
|
-
.some(root => n === root || n.startsWith(root + '/'));
|
|
49
|
+
.some(root => n === root || n.startsWith(root + '/')) || (artifact && n === tarball);
|
|
45
50
|
const realpath = async (value: string) => {
|
|
46
51
|
const n = value.replaceAll('\\', '/');
|
|
47
52
|
if (n === shimFile) return n;
|
|
@@ -69,11 +74,14 @@ function fixture({windows = false, npm = false, runtime = 'adjacent', groups = '
|
|
|
69
74
|
cwds.push(options?.cwd);
|
|
70
75
|
if (args.includes('list') || args.includes('ls')) return {code: failed ? 1 : 0, stdout: readback || mutations.length === 0 ? JSON.stringify(listing()) : '{'};
|
|
71
76
|
mutations.push(args);
|
|
72
|
-
|
|
77
|
+
runOptions.push(options);
|
|
78
|
+
if (installFailure) return {code: 1, stdout: installFailure.stdout ?? '', ...('stderrTail' in installFailure ? {stderrTail: installFailure.stderrTail} : {})};
|
|
79
|
+
if (args.some(arg => arg.startsWith('gentle-pi@') || arg.endsWith('.tgz'))) state.peer = peerAfter;
|
|
73
80
|
if (args.some(arg => arg.startsWith(PI + '@'))) state.group = peerAfter;
|
|
74
81
|
return {code: 0};
|
|
75
82
|
};
|
|
76
|
-
return {mutations, out, cwds, state, shell, selectedCli, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
83
|
+
return {mutations, out, cwds, runOptions, state, shell, selectedCli, tarball, input: {platform: windows ? 'win32' : 'linux', packageRoot: `${g1}/gentle-pi`, currentVersion: '1.1.0', cwd: '/neutral',
|
|
84
|
+
home, env: configHome ? {GENTLE_PI_CONFIG_HOME: configHome} : {},
|
|
77
85
|
runtime: runtime === 'adjacent' ? {kind: 'bundled', command: 'node', args: [selectedCli]} : {kind: runtime, command: shim ? shimFile : selectedCli, args: []},
|
|
78
86
|
out: (line: string) => out.push(line),
|
|
79
87
|
adapters: {which: async (name: string) => name === (npm ? 'npm' : 'pnpm') ? `/fake/${name}` : null, fs: {realpath, readFile}, run}}};
|
|
@@ -117,6 +125,60 @@ test('npm nests the peer under Shell and reinstalls Shell for it', async () => {
|
|
|
117
125
|
assert.deepEqual(f.out, ['npm installed gentle-pi@4.1.0; the selected Pi is now 1.2.0 (was 1.1.0).']);
|
|
118
126
|
});
|
|
119
127
|
|
|
128
|
+
// #2324: main-channel Shell versions are never published; only the tarball
|
|
129
|
+
// `gentle-shell upgrade --channel main` packed can reinstall that exact build.
|
|
130
|
+
const MAIN = '4.2.0-main.fb5e731119a7';
|
|
131
|
+
const native = (path: string, windows: boolean) => windows ? path.replaceAll('/', '\\') : path;
|
|
132
|
+
|
|
133
|
+
for (const windows of [false, true]) test(`a main-channel Shell is reinstalled from its recorded tarball, never from the registry (windows: ${windows})`, async () => {
|
|
134
|
+
const f = fixture({windows, shellVersion: MAIN});
|
|
135
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
136
|
+
assert.deepEqual(f.mutations, [['add', '-g', native(f.tarball, windows), '--allow-build=gentle-pi']]);
|
|
137
|
+
assert.deepEqual(f.out, [`pnpm installed ${native(f.tarball, windows)}; the selected Pi is now 1.2.0 (was 1.1.0).`]);
|
|
138
|
+
assert.equal(f.state.group, '1.1.0');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('npm reinstalls a main-channel Shell from its recorded tarball', async () => {
|
|
142
|
+
const f = fixture({npm: true, shellVersion: MAIN});
|
|
143
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
144
|
+
assert.deepEqual(f.mutations, [['install', '-g', f.tarball]]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a main-channel tarball is found under GENTLE_PI_CONFIG_HOME', async () => {
|
|
148
|
+
const f = fixture({shellVersion: MAIN, configHome: '/custom/config'});
|
|
149
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
150
|
+
assert.deepEqual(f.mutations, [['add', '-g', '/custom/config/main/packages/gentle-pi-4.2.0-main.fb5e731119a7.tgz', '--allow-build=gentle-pi']]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a missing main-channel tarball names the rebuild command and installs nothing', async () => {
|
|
154
|
+
const f = fixture({shellVersion: MAIN, artifact: false});
|
|
155
|
+
await assert.rejects(updateSelectedPi(f.input), {message: `gentle-shell update: the main-channel build ${f.tarball} is missing, and this unpublished build cannot be reinstalled from the registry; run 'gentle-shell upgrade --channel release' and then 'gentle-shell upgrade --channel main' to rebuild it. No installation changed.`});
|
|
156
|
+
assert.deepEqual(f.mutations, []);
|
|
157
|
+
assert.deepEqual(f.out, []);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// A relative config home names different folders for `upgrade` and `update`
|
|
161
|
+
// run from different directories, and npm/pnpm resolve it from the neutral cwd.
|
|
162
|
+
test('a relative GENTLE_PI_CONFIG_HOME is refused for a main-channel Shell without mutation', async () => {
|
|
163
|
+
const f = fixture({shellVersion: MAIN, configHome: 'relative/config'});
|
|
164
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership cannot locate the main-channel build without an absolute config home/);
|
|
165
|
+
assert.deepEqual(f.mutations, []);
|
|
166
|
+
assert.deepEqual(f.out, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('a main-channel Shell without a home directory is refused without mutation', async () => {
|
|
170
|
+
const f = fixture({shellVersion: MAIN});
|
|
171
|
+
delete (f.input as {home?: string}).home;
|
|
172
|
+
await assert.rejects(updateSelectedPi(f.input), /ownership cannot locate the main-channel build without an absolute config home/);
|
|
173
|
+
assert.deepEqual(f.mutations, []);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
for (const runtime of ['path', 'env']) test(`a main-channel Shell never redirects the update of a selected ${runtime} Pi`, async () => {
|
|
177
|
+
const f = fixture({runtime, shellVersion: MAIN, artifact: false});
|
|
178
|
+
assert.equal(await updateSelectedPi(f.input), 0);
|
|
179
|
+
assert.deepEqual(f.mutations, [['add', '-g', `${PI}@latest`]]);
|
|
180
|
+
});
|
|
181
|
+
|
|
120
182
|
test('an npm PATH runtime updates the global Pi without its scripts', async () => {
|
|
121
183
|
const f = fixture({npm: true, runtime: 'path'});
|
|
122
184
|
assert.equal(await updateSelectedPi(f.input), 0);
|
|
@@ -184,3 +246,36 @@ test('an unrelated selected override cannot authorize the adjacent Shell group',
|
|
|
184
246
|
await assert.rejects(updateSelectedPi(f.input), /ownership/);
|
|
185
247
|
assert.deepEqual(f.mutations, []);
|
|
186
248
|
});
|
|
249
|
+
|
|
250
|
+
// #2371: a failed install shows the package manager it ran and one sanitized
|
|
251
|
+
// line of its own output; pnpm 11 prints ERR_PNPM_* on stdout, npm on stderr.
|
|
252
|
+
test('a failed pnpm install names the pnpm it ran and its ERR_PNPM line from stdout', async () => {
|
|
253
|
+
const f = fixture({shellVersion: MAIN, installFailure: {stderrTail: '',
|
|
254
|
+
stdout: `Progress: resolved 1\n\u001b[31m[ERR_PNPM_INVALID_VERSION_UNION]\u001b[39m Invalid versions union. Found: "gentle-pi@file:/home/u/x.tgz". Use exact versions only.\nDone\n`}});
|
|
255
|
+
await assert.rejects(updateSelectedPi(f.input), {message: `gentle-shell update: pnpm (/fake/pnpm) could not install ${f.tarball}\n[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union. Found: "gentle-pi@file:~/x.tgz". Use exact versions only.`});
|
|
256
|
+
assert.deepEqual(f.runOptions, [{deadlineMs: 20 * 60_000, cwd: '/neutral', stderrTail: 4096}]);
|
|
257
|
+
assert.deepEqual(f.out, []);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('a failed npm install shows its stderr error line before stdout', async () => {
|
|
261
|
+
const f = fixture({npm: true, installFailure: {stdout: 'unrelated', stderrTail: 'npm error code EACCES\nnpm error Error: EACCES: permission denied\nnpm error A complete log is in /home/u/.npm/x.log\n'}});
|
|
262
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: npm (/fake/npm) could not install gentle-pi@4.1.0\nnpm error Error: EACCES: permission denied'});
|
|
263
|
+
assert.deepEqual(f.out, []);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('a failed install without output keeps the single line, naming the command it ran', async () => {
|
|
267
|
+
const f = fixture({installFailure: {}});
|
|
268
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0'});
|
|
269
|
+
assert.deepEqual(f.mutations, [['add', '-g', 'gentle-pi@4.1.0', '--allow-build=gentle-pi']]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('a stderr warning never hides the ERR_PNPM line pnpm printed on stdout', async () => {
|
|
273
|
+
const f = fixture({installFailure: {stderrTail: '(node:1) [DEP0040] DeprecationWarning: The `punycode` module is deprecated.\n',
|
|
274
|
+
stdout: '[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union.\n'}});
|
|
275
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0\n[ERR_PNPM_INVALID_VERSION_UNION] Invalid versions union.'});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('without any error line the last stderr line is shown', async () => {
|
|
279
|
+
const f = fixture({installFailure: {stderrTail: 'killed\n', stdout: 'Progress: resolved 1\n'}});
|
|
280
|
+
await assert.rejects(updateSelectedPi(f.input), {message: 'gentle-shell update: pnpm (/fake/pnpm) could not install gentle-pi@4.1.0\nkilled'});
|
|
281
|
+
});
|
|
@@ -59,7 +59,7 @@ const HIGH_TIER_ROW = "| high | only through `escalate` naming a Task Size high-
|
|
|
59
59
|
const UNASSESSABLE_TIER_ROW = "| native high, unknown, or assess failed | informs only: writer self-verification stands; escalate when a named high-risk item applies |";
|
|
60
60
|
// odd/tasks/verifier-admission.md S6: the sentence names the runtime admission of the verifier launch.
|
|
61
61
|
const NAMED_RISK_SENTENCE =
|
|
62
|
-
"The independent verifier runs only when the change touches a named Task Size high-risk item, passed as `escalate` (
|
|
62
|
+
"The independent verifier runs only when the change touches a named Task Size high-risk item, passed as `escalate` (runtime admits its launch on a `Risk: item N` line or sentence, a feature-document task line, or past the context backstop); never for size, a native tier alone, a failed assessment, or the model or effort that wrote the change. An unknown `Receipt-driven development:` line never lowers a tier below `off`.";
|
|
63
63
|
const SPOT_CHECK_SENTENCE =
|
|
64
64
|
"The parent spot check (re-running one reported command before delivery) stays required in every tier.";
|
|
65
65
|
test("trigger 5 (Verification rule) routes every RDD line through assess and the risk tier", () => {
|
|
@@ -405,7 +405,8 @@ test("a v9 host-mediated refuter slot routes through the host relay with its fix
|
|
|
405
405
|
// review-refuter entry is refused typed, naming that exact key, never a
|
|
406
406
|
// mid-relay transport failure.
|
|
407
407
|
test("a missing review-refuter routing model is refused typed, naming the key, before any relay launch", async (t) => {
|
|
408
|
-
const cwd =
|
|
408
|
+
const cwd = pinFixture(t);
|
|
409
|
+
isolatedAgentHome(t);
|
|
409
410
|
const lineageId = "relay-role-missing-model";
|
|
410
411
|
const input = hostMediatedRefuterCollectInput(lineageId);
|
|
411
412
|
const harness = nativeHarness([finalizeStatus(lineageId, [input])]);
|
|
@@ -1400,3 +1401,89 @@ test("a pinned profile missing a required group role is refused typed before any
|
|
|
1400
1401
|
assert.equal(modelLookups, 0, "the typed refusal fires before any model resolution");
|
|
1401
1402
|
assert.equal(authAttempts, 0, "the typed refusal fires before any provider auth");
|
|
1402
1403
|
});
|
|
1404
|
+
|
|
1405
|
+
// gentle-pi#2271: review-refuter and review-validator are provider review
|
|
1406
|
+
// roles, not installable agents, so nothing seeds their routing entries and
|
|
1407
|
+
// profiles never carry them. When the routing snapshot in effect (saved
|
|
1408
|
+
// routing, pin, or session binding) has no model for one of them, the relay
|
|
1409
|
+
// uses Pi's persisted default model, as the routing panel already promises.
|
|
1410
|
+
// Lens reviewers keep their fail-closed rule.
|
|
1411
|
+
function isolatedAgentHome(t: test.TestContext, settings?: Record<string, unknown>): void {
|
|
1412
|
+
const agentHome = mkdtempSync(join(tmpdir(), "gentle-pi-relay-agent-home-"));
|
|
1413
|
+
const previous = process.env.GENTLE_PI_AGENT_HOME;
|
|
1414
|
+
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
1415
|
+
t.after(() => {
|
|
1416
|
+
if (previous === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
1417
|
+
else process.env.GENTLE_PI_AGENT_HOME = previous;
|
|
1418
|
+
rmSync(agentHome, { recursive: true, force: true });
|
|
1419
|
+
});
|
|
1420
|
+
if (settings !== undefined) writeFileSync(join(agentHome, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const PI_DEFAULT_SETTINGS = { defaultProvider: "pidefault", defaultModel: "default-model", defaultThinkingLevel: "medium" };
|
|
1424
|
+
|
|
1425
|
+
async function relayedRefuterRequest(t: test.TestContext, cwd: string, lineageId: string): Promise<ReviewHostRelayRequest[]> {
|
|
1426
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
1427
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [hostMediatedRefuterCollectInput(lineageId)])]);
|
|
1428
|
+
const relayed: ReviewHostRelayRequest[] = [];
|
|
1429
|
+
__testing.setReviewHostRelayRunnerForTesting(async (request) => {
|
|
1430
|
+
relayed.push(request);
|
|
1431
|
+
return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
|
|
1432
|
+
});
|
|
1433
|
+
await runCapture(cwd, harness, lineageId);
|
|
1434
|
+
return relayed;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
test("a refuter slot with no routing entry uses Pi's persisted default model and effort", async (t) => {
|
|
1438
|
+
const cwd = pinFixture(t);
|
|
1439
|
+
isolatedAgentHome(t, PI_DEFAULT_SETTINGS);
|
|
1440
|
+
|
|
1441
|
+
const relayed = await relayedRefuterRequest(t, cwd, "relay-role-default");
|
|
1442
|
+
|
|
1443
|
+
assert.equal(relayed.length, 1);
|
|
1444
|
+
assert.equal(relayed[0]!.routingKey, "review-refuter");
|
|
1445
|
+
assert.equal(relayed[0]!.selection, "pidefault/default-model");
|
|
1446
|
+
assert.equal(relayed[0]!.thinking, "medium");
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
test("a pinned profile that omits the refuter role uses Pi's persisted default model instead of refusing", async (t) => {
|
|
1450
|
+
const cwd = pinFixture(t);
|
|
1451
|
+
isolatedAgentHome(t, PI_DEFAULT_SETTINGS);
|
|
1452
|
+
pinLocal(cwd, "pinned-complete");
|
|
1453
|
+
|
|
1454
|
+
const relayed = await relayedRefuterRequest(t, cwd, "relay-role-pin-default");
|
|
1455
|
+
|
|
1456
|
+
assert.equal(relayed.length, 1);
|
|
1457
|
+
assert.equal(relayed[0]!.selection, "pidefault/default-model");
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
test("a configured review-refuter routing entry still wins over Pi's persisted default", async (t) => {
|
|
1461
|
+
const cwd = pinFixture(t);
|
|
1462
|
+
isolatedAgentHome(t, PI_DEFAULT_SETTINGS);
|
|
1463
|
+
writeFileSync(join(process.env.GENTLE_PI_CONFIG_HOME!, "models.json"), `${JSON.stringify({ ...GLOBAL_MODELS, "review-refuter": { model: "global/refuter-model", thinking: "high" } }, null, 2)}\n`);
|
|
1464
|
+
|
|
1465
|
+
const relayed = await relayedRefuterRequest(t, cwd, "relay-role-configured");
|
|
1466
|
+
|
|
1467
|
+
assert.equal(relayed.length, 1);
|
|
1468
|
+
assert.equal(relayed[0]!.selection, "global/refuter-model");
|
|
1469
|
+
assert.equal(relayed[0]!.thinking, "high");
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
test("a lens slot with no routing entry never borrows Pi's persisted default model", async (t) => {
|
|
1473
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
1474
|
+
const cwd = pinFixture(t);
|
|
1475
|
+
isolatedAgentHome(t, PI_DEFAULT_SETTINGS);
|
|
1476
|
+
pinLocal(cwd, "pinned-partial");
|
|
1477
|
+
const lineageId = "relay-lens-no-default";
|
|
1478
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-reliability", 0)])]);
|
|
1479
|
+
const relayed: ReviewHostRelayRequest[] = [];
|
|
1480
|
+
__testing.setReviewHostRelayRunnerForTesting(async (request) => {
|
|
1481
|
+
relayed.push(request);
|
|
1482
|
+
return { promptByteLength: 64, resultByteLength: 32, submission: '{"admission_decision":"completed"}' };
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
await runCapture(cwd, harness, lineageId);
|
|
1486
|
+
|
|
1487
|
+
assert.equal(relayed.length, 1);
|
|
1488
|
+
assert.equal(relayed[0]!.selection, undefined);
|
|
1489
|
+
});
|
|
@@ -539,7 +539,11 @@ test("every in-process reviewer refusal maps to its typed relay failure code, ca
|
|
|
539
539
|
{ code: INPROCESS_REVIEWER_FAILURE.AUTH_UNAVAILABLE, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_AUTH_UNAVAILABLE },
|
|
540
540
|
{ code: INPROCESS_REVIEWER_FAILURE.THINKING_INVALID, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_THINKING_INVALID },
|
|
541
541
|
{ code: INPROCESS_REVIEWER_FAILURE.TOOL_CALL_ATTEMPTED, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL },
|
|
542
|
-
{ code: INPROCESS_REVIEWER_FAILURE.EMPTY_OUTPUT, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, evidence: {
|
|
542
|
+
{ code: INPROCESS_REVIEWER_FAILURE.EMPTY_OUTPUT, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, evidence: {
|
|
543
|
+
reviewerModel: "nan/deepseek-v4-flash", stopReason: "length",
|
|
544
|
+
usage: { input: 12, output: 64, reasoning: 64, cacheRead: 0, cacheWrite: 0, totalTokens: 76 },
|
|
545
|
+
content: { blocks: 1, textBlocks: 0, thinkingBlocks: 1, textChars: 0, thinkingChars: 16 },
|
|
546
|
+
} },
|
|
543
547
|
{ code: INPROCESS_REVIEWER_FAILURE.OUTPUT_TOO_LARGE, kind: REVIEW_HOST_RELAY_FAILURE.REVIEWER_OUTPUT_TOO_LARGE },
|
|
544
548
|
{ code: INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED, kind: REVIEW_HOST_RELAY_FAILURE.PI_FAILED },
|
|
545
549
|
];
|
|
@@ -550,7 +554,10 @@ test("every in-process reviewer refusal maps to its typed relay failure code, ca
|
|
|
550
554
|
assert.equal(error.message, message);
|
|
551
555
|
assert.equal(error.mutationOutcome, "none");
|
|
552
556
|
assert.equal(existsSync(fixture.submitCapturePath), false, `${code} must never reach submission`);
|
|
553
|
-
if (evidence !== undefined)
|
|
557
|
+
if (evidence !== undefined) {
|
|
558
|
+
assert.deepEqual(error.reviewerEvidence, evidence, `${code} must carry its outcome evidence`);
|
|
559
|
+
assert.doesNotMatch(JSON.stringify(error.reviewerEvidence), /errorMessage|thinkingSignature|rawStopReason|authorization|providerPayload/);
|
|
560
|
+
}
|
|
554
561
|
}
|
|
555
562
|
});
|
|
556
563
|
|
|
@@ -53,7 +53,7 @@ test("public Anthropic non-strict adapter preserves the complete review tool dec
|
|
|
53
53
|
assert.deepEqual(controller.required, ["operation"]);
|
|
54
54
|
assert.deepEqual(Object.keys(controller.properties).sort(), ["operation", "lineageId", "selectionBinding", "intendedUntracked", "untrackedScope", "changeName", "idempotencyKey", "transition", "input", "outputPath", "inputPath", "operationId", "lineageIds", "workspaceRoot"].sort());
|
|
55
55
|
assert.ok(controller.properties.input.anyOf.some((schema: any) => schema.type === "object"));
|
|
56
|
-
assert.match(controller.properties.input.description, /START\/ASSESS
|
|
56
|
+
assert.match(controller.properties.input.description, /START\/ASSESS\/answer-consent/);
|
|
57
57
|
assert.deepEqual(controller.properties.operation.enum, tools.get("gentle_review").parameters.properties.operation.enum);
|
|
58
58
|
for (const name of ["gentle_review_capture", "gentle_review_capture_group"]) {
|
|
59
59
|
const schema = emitted.get(name);
|
|
@@ -66,7 +66,7 @@ test("public Anthropic non-strict adapter preserves the complete review tool dec
|
|
|
66
66
|
let nativeCalls = 0;
|
|
67
67
|
const native = new Proxy({}, { get() { return async () => { nativeCalls++; }; } }) as NativeReviewCli;
|
|
68
68
|
for (const operation of ["start", "assess", "answer-consent", "inspect", "reset", "recover"]) {
|
|
69
|
-
const inputs = operation === "start" || operation === "assess" ? [null] : [null, { consentBinding: "opaque", answer: "granted" }];
|
|
69
|
+
const inputs = operation === "start" || operation === "assess" || operation === "answer-consent" ? [null] : [null, { consentBinding: "opaque", answer: "granted" }];
|
|
70
70
|
for (const input of inputs) {
|
|
71
71
|
const args = validateToolArguments({ ...tools.get("gentle_review"), parameters: controller }, { type: "toolCall", id: "provider-shell", name: "gentle_review", arguments: { operation, input } });
|
|
72
72
|
assert.deepEqual(args.input, input, "provider-shell validation accepts and retains this input");
|
|
@@ -89,6 +89,23 @@ test("registered review schemas admit START/ASSESS objects and ordered capture o
|
|
|
89
89
|
assert.deepEqual(__testing.parseReviewCaptureGroupParameters(args).collectBindings, [JSON.stringify(binding), ordered[1]]);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test("answer-consent admits JSON object input and coerces it cleanly (#2375)", async () => {
|
|
93
|
+
const input = { consentBinding: "bound-1", answer: "granted" };
|
|
94
|
+
assert.deepEqual(validate("gentle_review", { operation: "answer-consent", input }).input, input);
|
|
95
|
+
assert.equal(
|
|
96
|
+
__testing.parseReviewControllerParameters(validate("gentle_review", { operation: "answer-consent", input })).input,
|
|
97
|
+
JSON.stringify(input),
|
|
98
|
+
);
|
|
99
|
+
const native = { targetStatus: async () => ({ targetIdentity: "t1" }) } as unknown as NativeReviewCli;
|
|
100
|
+
const outcome = await __testing.executeReviewControllerOperation(
|
|
101
|
+
validate("gentle_review", { operation: "answer-consent", input }),
|
|
102
|
+
process.cwd(),
|
|
103
|
+
native,
|
|
104
|
+
);
|
|
105
|
+
assert.equal(outcome.operation, "answer-consent");
|
|
106
|
+
assert.match(String(outcome.outcome), /consent-binding-stale/);
|
|
107
|
+
});
|
|
108
|
+
|
|
92
109
|
test("validated ASSESS objects reach the mocked native facade with parsed fields", async () => {
|
|
93
110
|
const requests: unknown[] = [];
|
|
94
111
|
const native = { assess: async (request: unknown) => { requests.push(request); throw new Error("mock assessment unavailable"); } } as unknown as NativeReviewCli;
|
|
@@ -132,7 +149,7 @@ test("actual validation rejects arrays, null and coerced primitives rather than
|
|
|
132
149
|
});
|
|
133
150
|
|
|
134
151
|
test("all other controller operations keep object input fail-closed at the facade boundary (#1698)", async () => {
|
|
135
|
-
const operations = (tools.get("gentle_review").parameters.properties.operation.enum as string[]).filter((operation) => operation !== "start" && operation !== "assess" && operation !== "select-intended-untracked");
|
|
152
|
+
const operations = (tools.get("gentle_review").parameters.properties.operation.enum as string[]).filter((operation) => operation !== "start" && operation !== "assess" && operation !== "answer-consent" && operation !== "select-intended-untracked");
|
|
136
153
|
let calls = 0;
|
|
137
154
|
const native = new Proxy({}, { get() { return async () => { calls++; throw new Error("unexpected native execution"); }; } }) as NativeReviewCli;
|
|
138
155
|
for (const operation of operations) {
|
|
@@ -25,6 +25,12 @@ interface SessionRun {
|
|
|
25
25
|
|
|
26
26
|
type RddMode = "on" | "off" | "throws";
|
|
27
27
|
|
|
28
|
+
const NATIVE_CONTINUATION = {
|
|
29
|
+
operation: "review.status",
|
|
30
|
+
command: "gentle-ai review status --agent pi --next-transition",
|
|
31
|
+
arguments: [{ name: "--agent", value: "pi" }, { name: "--next-transition" }],
|
|
32
|
+
};
|
|
33
|
+
|
|
28
34
|
function nativeCli(mode: () => RddMode): NativeReviewCli {
|
|
29
35
|
return {
|
|
30
36
|
reviewMode: async () => {
|
|
@@ -34,7 +40,10 @@ function nativeCli(mode: () => RddMode): NativeReviewCli {
|
|
|
34
40
|
},
|
|
35
41
|
assess: async () => ({
|
|
36
42
|
schema: "gentle-ai.review-assessment/v1",
|
|
37
|
-
risk: "
|
|
43
|
+
risk: "high",
|
|
44
|
+
reviewDue: true,
|
|
45
|
+
reviewDueReason: "high_risk",
|
|
46
|
+
nextTransition: NATIVE_CONTINUATION,
|
|
38
47
|
reasons: [],
|
|
39
48
|
changedPaths: 1,
|
|
40
49
|
changedLines: 3,
|
|
@@ -120,7 +129,7 @@ async function runSession(
|
|
|
120
129
|
});
|
|
121
130
|
try {
|
|
122
131
|
await session.bindExtensions({ mode: "json", onError: (error) => run.errors.push(error.error) });
|
|
123
|
-
next = { name: "gentle_review", arguments: { operation: "assess" } };
|
|
132
|
+
next = { name: "gentle_review", arguments: { operation: "assess", input: { escalate: { item: 4, reason: "Fixture process concurrency boundary" } } } };
|
|
124
133
|
await session.prompt("Assess the current change.", { source: "interactive" });
|
|
125
134
|
beforeSecondPrompt();
|
|
126
135
|
await session.prompt("Report the plan.", { source: "interactive" });
|
|
@@ -162,6 +171,11 @@ function registeredGuidelines(run: SessionRun): string[] {
|
|
|
162
171
|
function assertLeanRequest(request: RecordedRequest, run: SessionRun): void {
|
|
163
172
|
assert.match(request.systemPrompt, /Receipt-driven development: off /);
|
|
164
173
|
assert.match(request.systemPrompt, LEAN_GUIDELINE);
|
|
174
|
+
assert.match(request.systemPrompt, /relay it unchanged but do not execute it while RDD is off/);
|
|
175
|
+
assert.match(request.systemPrompt, /assessment metadata does not authorize STATUS, another review lifecycle operation, or enabling RDD/);
|
|
176
|
+
assert.doesNotMatch(request.systemPrompt, /relay it unchanged and follow it/);
|
|
177
|
+
assert.match(request.systemPrompt, /Continue the returned verification plan/);
|
|
178
|
+
assert.match(request.systemPrompt, /tests, builds, and functional checks still run/);
|
|
165
179
|
for (const guideline of registeredGuidelines(run)) assert.equal(request.systemPrompt.includes(guideline), false, `RDD off must leave out: ${guideline.slice(0, 80)}`);
|
|
166
180
|
const names = request.tools.map((tool) => tool.name);
|
|
167
181
|
assert.ok(names.includes("gentle_review"));
|
|
@@ -195,6 +209,13 @@ test("RDD off: the parent request carries only the lean ASSESS loadout; RDD on:
|
|
|
195
209
|
assert.equal(off.assessResults.length, 1);
|
|
196
210
|
assert.equal(off.assessResults[0].isError, false);
|
|
197
211
|
assert.equal((off.assessResults[0].details as { schema?: string }).schema, "gentle-pi.review-assessment-plan/v1");
|
|
212
|
+
const plan = off.assessResults[0].details as Record<string, unknown>;
|
|
213
|
+
assert.equal(plan.rddLine, "off");
|
|
214
|
+
assert.equal(plan.reviewDue, true);
|
|
215
|
+
assert.equal(plan.reviewDueReason, "high_risk");
|
|
216
|
+
assert.deepEqual(plan.nextTransition, NATIVE_CONTINUATION);
|
|
217
|
+
assert.equal((plan.plan as Record<string, unknown>).writerSelfVerification, true);
|
|
218
|
+
assert.equal((plan.plan as Record<string, unknown>).independentVerifier, true);
|
|
198
219
|
|
|
199
220
|
for (const request of on.requests) {
|
|
200
221
|
assert.match(request.systemPrompt, /Receipt-driven development: on /);
|
|
@@ -7,6 +7,7 @@ import test from "node:test";
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
9
9
|
import { REVIEW_HOST_RELAY_FAILURE, ReviewHostRelayError } from "../lib/review-host-relay.ts";
|
|
10
|
+
import { runInProcessReviewer } from "../lib/inprocess-reviewer.ts";
|
|
10
11
|
import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
11
12
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
12
13
|
import type { ReviewArtifactSubjectV2, ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
@@ -375,6 +376,31 @@ test("a relayed empty-output failure carries the child's own evidence in the fai
|
|
|
375
376
|
assert.equal(failure.reviewer?.toolCallAttempted, true);
|
|
376
377
|
});
|
|
377
378
|
|
|
379
|
+
test("public empty-output report preserves only safe in-process completion facts", async (t) => {
|
|
380
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
381
|
+
const cwd = repository(t);
|
|
382
|
+
const { native } = transportAwareNative();
|
|
383
|
+
const outcome = await runInProcessReviewer({ selection: "nan/deepseek-v4-flash", prompt: Buffer.from("offline"), timeoutMs: 1_000, routingKey: "review-risk" }, {
|
|
384
|
+
registry: {
|
|
385
|
+
find: () => ({ provider: "nan", id: "deepseek-v4-flash" }) as never,
|
|
386
|
+
getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "PRIVATE AUTH" }),
|
|
387
|
+
},
|
|
388
|
+
complete: async () => ({ content: [{ type: "thinking", thinking: "PRIVATE THINKING" }], stopReason: "length", errorMessage: "PRIVATE ERROR", usage: { output: 64, reasoning: 64, unknown: "PRIVATE USAGE" } }) as never,
|
|
389
|
+
});
|
|
390
|
+
assert.equal(outcome.kind, "refused");
|
|
391
|
+
if (outcome.kind !== "refused") return;
|
|
392
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
393
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, "pi", outcome.message, { reviewerEvidence: outcome.evidence });
|
|
394
|
+
});
|
|
395
|
+
const result = await runCapture(cwd, native, "safe-evidence-lineage");
|
|
396
|
+
assert.equal(result.outcome, "pi-host-relay-transport-failure");
|
|
397
|
+
assert.deepEqual((result.failure as { reviewer: unknown }).reviewer, {
|
|
398
|
+
reviewerModel: "nan/deepseek-v4-flash", stopReason: "length", usage: { output: 64, reasoning: 64 },
|
|
399
|
+
content: { blocks: 1, textBlocks: 0, thinkingBlocks: 1, textChars: 0, thinkingChars: 16 },
|
|
400
|
+
});
|
|
401
|
+
assert.doesNotMatch(JSON.stringify(result), /PRIVATE|errorMessage|unknown|authorization/);
|
|
402
|
+
});
|
|
403
|
+
|
|
378
404
|
test("capture forecasts the reviewer model run once and spends nothing until it is acknowledged", async (t) => {
|
|
379
405
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
380
406
|
const cwd = repository(t);
|
|
@@ -5,7 +5,7 @@ import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProf
|
|
|
5
5
|
function fixture() {
|
|
6
6
|
let sessionId = "parent";
|
|
7
7
|
const entries: Array<{ type: string; customType: string; data: unknown }> = [];
|
|
8
|
-
const session = { getSessionId: () => sessionId, getBranch: () => entries };
|
|
8
|
+
const session = { getSessionId: () => sessionId, getBranch: () => entries, getLeafId: () => `leaf-${entries.length}`, getEntryCount: () => entries.length };
|
|
9
9
|
const host = { appendEntry: (customType: string, data: unknown) => { entries.push({ type: "custom", customType, data }); } };
|
|
10
10
|
const write = (toolCallId: string, root = "/repo") => recordReviewMutation(host, session, root, { source: "direct", toolName: "write", toolCallId });
|
|
11
11
|
return { host, session, entries, write, switchSession: (id: string) => { sessionId = id; } };
|
|
@@ -61,6 +61,36 @@ test("malformed, unknown-version, and unmatched consumption entries do not estab
|
|
|
61
61
|
assert.equal(pendingReviewMutation(f.session, "/repo"), captured);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("the ledger scan is memoized until the next append and hosts without leaf identity stay uncached", () => {
|
|
65
|
+
let scans = 0;
|
|
66
|
+
const entries: Array<{ type: string; customType: string; data: unknown }> = [];
|
|
67
|
+
const host = { appendEntry: (customType: string, data: unknown) => { entries.push({ type: "custom", customType, data }); } };
|
|
68
|
+
const branch = () => { scans++; return entries; };
|
|
69
|
+
const session = {
|
|
70
|
+
getSessionId: () => "parent",
|
|
71
|
+
getBranch: branch,
|
|
72
|
+
getLeafId: () => `leaf-${entries.length}`,
|
|
73
|
+
getEntryCount: () => entries.length,
|
|
74
|
+
};
|
|
75
|
+
recordReviewMutation(host, session, "/repo", { source: "direct", toolName: "write", toolCallId: "a" });
|
|
76
|
+
const captured = pendingReviewMutation(session, "/repo");
|
|
77
|
+
assert.ok(captured);
|
|
78
|
+
const afterFirstScan = scans;
|
|
79
|
+
assert.equal(pendingReviewMutation(session, "/repo"), captured);
|
|
80
|
+
assert.deepEqual(pendingReviewMutationProfiles(session, "/repo"), [{}]);
|
|
81
|
+
assert.equal(scans, afterFirstScan, "an unchanged (leafId, entryCount) identity must not rescan the branch");
|
|
82
|
+
recordReviewMutation(host, session, "/repo", { source: "direct", toolName: "edit", toolCallId: "b" });
|
|
83
|
+
const afterAppend = scans;
|
|
84
|
+
const next = pendingReviewMutation(session, "/repo");
|
|
85
|
+
assert.ok(next && next !== captured, "an append invalidates the memo, so the next call sees the new mutation");
|
|
86
|
+
assert.equal(scans, afterAppend + 1);
|
|
87
|
+
const uncached = { getSessionId: () => "parent", getBranch: branch };
|
|
88
|
+
const beforeUncached = scans;
|
|
89
|
+
assert.ok(pendingReviewMutation(uncached, "/repo"));
|
|
90
|
+
assert.ok(pendingReviewMutation(uncached, "/repo"));
|
|
91
|
+
assert.equal(scans, beforeUncached + 2, "a host without leaf identity stays uncached");
|
|
92
|
+
});
|
|
93
|
+
|
|
64
94
|
// gentle-pi#1175 (T2): mutation receipts optionally carry the writer profile
|
|
65
95
|
// the runtime resolved, so ASSESS never has to trust a model re-declaration.
|
|
66
96
|
test("mutation receipts validate with or without runtime writer profile fields, never with empty ones", () => {
|
|
@@ -4,6 +4,7 @@ import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { SessionChanges, readChangeSnapshot, sessionChangeRoots, SESSION_CHANGE_ENTRY, MAX_CHANGE_BYTES } from "../lib/session-changes.ts";
|
|
7
|
+
import { emptyChanges } from "../lib/shell-changes.ts";
|
|
7
8
|
import { SESSION_WORKTREE_ENTRY } from "../lib/session-worktree-registry.ts";
|
|
8
9
|
|
|
9
10
|
const text = (text: string) => ({ kind: "text" as const, text });
|
|
@@ -12,7 +13,7 @@ const evidence = (id = "a", root = "/repo", path = "file.ts", before = text("old
|
|
|
12
13
|
|
|
13
14
|
test("session changes starts empty without reading the worktree or its untracked contents", () => {
|
|
14
15
|
const changes = new SessionChanges("session");
|
|
15
|
-
assert.deepEqual(changes.model, {
|
|
16
|
+
assert.deepEqual(changes.model, { ...emptyChanges(), digest: emptyChanges().digest });
|
|
16
17
|
assert.deepEqual(changes.worktrees, []);
|
|
17
18
|
});
|
|
18
19
|
|