gentle-pi 4.2.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 +340 -138
- package/assets/agents/gentle-ai-explore.md +1 -0
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -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 +163 -0
- package/assets/orchestrator-delegation.md +25 -176
- package/assets/orchestrator-tracking.md +1 -1
- package/assets/orchestrator-verification.md +14 -8
- package/assets/orchestrator-writer.md +5 -1
- package/assets/orchestrator.md +11 -11
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +237 -139
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
- package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
- package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/assets/diagrams/agent-orchestration.svg +54 -15
- package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
- package/docs/assets/diagrams/measured-routing.svg +49 -0
- package/docs/assets/diagrams/native-review.svg +52 -12
- package/docs/assets/diagrams/odd-workflow.svg +128 -66
- package/docs/assets/diagrams/permission-modes.svg +51 -0
- package/docs/assets/features/audio-notifications.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/bundled-install.md +65 -30
- package/docs/delegated-verification.md +16 -8
- package/docs/gentle-agents-activity.md +16 -6
- package/docs/gentle-music.md +61 -0
- package/docs/gentle-shell-uninstall.md +8 -1
- package/docs/gentle-shell.md +80 -9
- package/docs/install-wizard.md +37 -6
- package/docs/permission-modes.md +8 -3
- package/docs/readme-reference.md +120 -28
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +38 -10
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/codegraph-tools.ts +68 -6
- package/extensions/gentle-agents.ts +270 -48
- package/extensions/gentle-ai.ts +490 -113
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-herdr-activity.ts +2 -3
- package/extensions/gentle-music.ts +289 -0
- package/extensions/gentle-shell.ts +359 -85
- package/extensions/gentle-todo.ts +43 -9
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/quiet-tools.ts +1 -1
- package/extensions/session-scratch.ts +384 -0
- package/extensions/skill-registry.ts +63 -8
- package/extensions/startup-banner.ts +2 -3
- package/lib/agent-home.ts +8 -3
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +60 -1
- package/lib/agents-runner.ts +107 -9
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-view.ts +3 -1
- 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/bounded-writer-admission.ts +51 -0
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +2 -4
- package/lib/commit-slice-signal.ts +100 -0
- package/lib/context-backstop.ts +120 -0
- package/lib/delivered-test-lint.ts +80 -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/destructive-command-guard.ts +120 -9
- package/lib/external-editor.ts +31 -0
- package/lib/gauge-theme.ts +146 -0
- package/lib/gentle-ai-binary.ts +28 -7
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +146 -36
- package/lib/gentle-shell-import-pi.ts +233 -0
- package/lib/gentle-shell-launcher.ts +154 -15
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +51 -2
- package/lib/herdr-activity.ts +24 -0
- 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 +20 -5
- 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-discovery.ts +5 -1
- package/lib/orchestrator-presence.ts +87 -8
- package/lib/orchestrator-scope.ts +31 -3
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/provider-contract-bundle.ts +99 -53
- package/lib/review-candidate-view-owner.ts +29 -19
- package/lib/review-candidate-view.ts +36 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/review-risk-assessment.ts +43 -38
- package/lib/review-session-standing-permission.ts +3 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +30 -1
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +523 -0
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +29 -28
- package/lib/shell-changes-view.ts +80 -13
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-gauge.ts +49 -6
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +26 -6
- package/lib/shell-todo.ts +24 -5
- package/lib/shell-usage-view.ts +5 -1
- package/lib/shell-usage.ts +7 -7
- package/lib/verifier-admission.ts +30 -0
- 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/lib/yolo-temp-scratch.ts +69 -0
- package/package.json +1 -2
- package/runtime/gentle-ai-binary.mjs +28 -7
- package/runtime/gentle-shell-bundled.mjs +145 -35
- package/runtime/gentle-shell-import-pi.mjs +234 -0
- package/runtime/gentle-shell-launcher.mjs +152 -13
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +51 -2
- package/runtime/native-review-cli.mjs +19 -4
- package/runtime/review-risk-assessment.mjs +43 -38
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/bundled-engram-migration.mjs +119 -0
- 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-server.mjs +14 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +94 -19
- package/scripts/pi-update.mjs +34 -6
- package/scripts/run-test-suite.mjs +5 -0
- package/scripts/verify-package-files.mjs +43 -13
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/skills/gentle-ai/SKILL.md +3 -3
- package/skills/work-unit-commits/SKILL.md +5 -3
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agent-risk-escalation.test.ts +37 -10
- 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 +485 -7
- 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 +142 -0
- package/tests/bounded-writer-admission.test.ts +11 -3
- package/tests/bundled-install.test.ts +3 -3
- package/tests/card-content-policy.test.ts +19 -9
- package/tests/child-safety.test.ts +39 -0
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/codegraph-tools.test.ts +123 -2
- package/tests/command-palette.test.ts +15 -5
- package/tests/commit-slice-signal.test.ts +162 -0
- package/tests/context-backstop.test.ts +225 -0
- package/tests/delegate-for-reason-contract.test.ts +92 -24
- package/tests/delivered-test-lint.test.ts +105 -0
- 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/destructive-command-guard.test.ts +85 -1
- 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 +143 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
- package/tests/gauge-theme.test.ts +120 -0
- package/tests/gentle-agents.test.ts +645 -21
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-child-guards.test.ts +61 -0
- package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
- 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-bin.test.ts +163 -0
- package/tests/gentle-shell-bundled.test.ts +392 -176
- package/tests/gentle-shell-import-pi.test.ts +262 -0
- package/tests/gentle-shell-launcher.test.ts +107 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +89 -0
- package/tests/gentle-shell.test.ts +225 -17
- package/tests/gentle-todo.test.ts +190 -0
- package/tests/headless-delivery-question.test.ts +58 -0
- package/tests/herdr-activity.test.ts +34 -1
- 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 +10 -2
- package/tests/installer-windows-bootstrap.test.ts +94 -7
- 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 +17 -4
- package/tests/native-review-parity.test.ts +23 -2
- package/tests/no-self-pause-contract.test.ts +5 -2
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-integration.test.ts +5 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
- package/tests/odd-routing-contract.test.ts +115 -30
- package/tests/orchestrator-budget.test.ts +22 -8
- package/tests/orchestrator-discovery.test.ts +29 -0
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +3 -1
- package/tests/orchestrator-scope.test.ts +80 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/orchestrator-status-publication-contract.test.ts +12 -8
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +24 -20
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update-launcher.test.ts +21 -2
- package/tests/pi-update.test.ts +99 -4
- package/tests/primary-session-prompt.test.ts +101 -0
- package/tests/provider-contract-bundle.test.ts +118 -0
- package/tests/provider-contract-mirror.test.ts +24 -0
- package/tests/quiet-tool-rendering.test.ts +23 -0
- package/tests/rdd-aware-verification-contract.test.ts +29 -11
- package/tests/rdd-status-line.test.ts +79 -0
- package/tests/review-agent-end-preflight.test.ts +6 -0
- package/tests/review-candidate-view.test.ts +49 -0
- package/tests/review-contract-prompt.test.ts +5 -2
- package/tests/review-controller-native-routing.test.ts +14 -1
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +147 -3
- 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 +258 -0
- package/tests/review-owner-identity.test.ts +132 -0
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/review-risk-assessment.test.ts +163 -76
- package/tests/review-session-standing-permission-controller.test.ts +14 -1
- package/tests/review-session-standing-permission.test.ts +27 -1
- package/tests/run-test-suite.test.ts +15 -0
- package/tests/runtime-harness.mjs +16 -6
- package/tests/session-changes-shell.test.ts +1 -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-profile-freeze.test.ts +23 -0
- package/tests/session-scratch.test.ts +1011 -0
- package/tests/session-subject-routing-contract.test.ts +2 -1
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +21 -2
- package/tests/shell-changes-view.test.ts +137 -1
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-gauge.test.ts +42 -0
- 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/small-task-inline-contract.test.ts +37 -0
- package/tests/standalone-config-home.test.ts +324 -0
- package/tests/startup-banner.test.ts +5 -4
- package/tests/startup-home-repo.test.ts +34 -0
- package/tests/support/orchestrator-modules.ts +1 -0
- package/tests/task-size-routing-contract.test.ts +70 -10
- package/tests/test-scratch-contract.test.ts +102 -0
- package/tests/test-strength-contract.test.ts +36 -1
- package/tests/tool-batching-contract.test.ts +64 -0
- package/tests/verification-proportionality.test.ts +46 -0
- package/tests/verifier-admission.test.ts +154 -0
- package/tests/verify-agent-contract.test.ts +8 -8
- 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 +30 -2
- package/tests/writer-edit-surface-scope.test.ts +3 -0
- package/tests/writer-feature-document-admission.test.ts +156 -0
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +19 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
- package/tests/yolo-temp-scratch.test.ts +71 -0
- package/scripts/mirror-odd-routing.mjs +0 -242
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -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", () => {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isSmallWriterProfile,
|
|
19
19
|
resolveWriterProfile,
|
|
20
20
|
verificationPlan,
|
|
21
|
+
verificationRisk,
|
|
21
22
|
REVIEW_ASSESSMENT_SCHEMA,
|
|
22
23
|
VERIFICATION_TIER,
|
|
23
24
|
RDD_LINE,
|
|
@@ -389,9 +390,11 @@ test("verificationPlan: an omitted nativeReviewOutcome under rdd on defaults to
|
|
|
389
390
|
}
|
|
390
391
|
});
|
|
391
392
|
|
|
392
|
-
|
|
393
|
+
// Changed with verification by named risk: the small-model bias is gone, so a
|
|
394
|
+
// small writer on a medium plan no longer gets an independent verifier.
|
|
395
|
+
test("verificationPlan: on+closed+medium+small -> no verifier (the writer's model never adds one)", () => {
|
|
393
396
|
const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.MEDIUM, writerProfile: WRITER_PROFILE.SMALL, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.CLOSED });
|
|
394
|
-
assert.equal(plan.independentVerifier,
|
|
397
|
+
assert.equal(plan.independentVerifier, false);
|
|
395
398
|
assert.equal(plan.writerSelfVerification, true);
|
|
396
399
|
});
|
|
397
400
|
|
|
@@ -401,9 +404,12 @@ test("verificationPlan: on+declined+medium+large -> no verifier (medium, large)"
|
|
|
401
404
|
assert.equal(plan.writerSelfVerification, true);
|
|
402
405
|
});
|
|
403
406
|
|
|
404
|
-
|
|
407
|
+
// Changed with verification by named risk: no small-model bias, so a declined
|
|
408
|
+
// review over a medium plan keeps the writer's self-verification only.
|
|
409
|
+
test("verificationPlan: on+declined+medium+small -> no verifier", () => {
|
|
405
410
|
const plan = verificationPlan({ rddLine: RDD_LINE.ON, risk: VERIFICATION_TIER.MEDIUM, writerProfile: WRITER_PROFILE.SMALL, nativeReviewOutcome: NATIVE_REVIEW_OUTCOME.DECLINED });
|
|
406
|
-
assert.equal(plan.independentVerifier,
|
|
411
|
+
assert.equal(plan.independentVerifier, false);
|
|
412
|
+
assert.equal(plan.writerSelfVerification, true);
|
|
407
413
|
});
|
|
408
414
|
|
|
409
415
|
test("verificationPlan: on+declined+high -> verifier", () => {
|
|
@@ -468,30 +474,62 @@ for (const rddLine of [RDD_LINE.OFF, RDD_LINE.UNKNOWN]) {
|
|
|
468
474
|
}
|
|
469
475
|
});
|
|
470
476
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
477
|
+
// Changed with verification by named risk: the small-model bias is gone, so
|
|
478
|
+
// medium never adds a verifier, whatever the writer profile.
|
|
479
|
+
test(`verificationPlan: rdd ${rddLine}, medium risk -> writer self-verification only, for every writer profile`, () => {
|
|
480
|
+
for (const writerProfile of PROFILES) {
|
|
481
|
+
const plan = verificationPlan({ rddLine, risk: VERIFICATION_TIER.MEDIUM, writerProfile });
|
|
482
|
+
assert.equal(plan.writerSelfVerification, true, `profile ${writerProfile}`);
|
|
483
|
+
assert.equal(plan.structuralReadbackOnly, false);
|
|
484
|
+
assert.equal(plan.independentVerifier, false, `profile ${writerProfile}`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
476
487
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
488
|
+
// The plan risk is high only through an agent escalation (verificationRisk),
|
|
489
|
+
// so high still means writer self-verification plus one independent verifier.
|
|
490
|
+
test(`verificationPlan: rdd ${rddLine}, high (escalated) risk -> writer self-verification plus independent verifier`, () => {
|
|
491
|
+
for (const writerProfile of PROFILES) {
|
|
492
|
+
const plan = verificationPlan({ rddLine, risk: VERIFICATION_TIER.HIGH, writerProfile });
|
|
493
|
+
assert.equal(plan.writerSelfVerification, true, `rdd ${rddLine}, profile ${writerProfile}`);
|
|
494
|
+
assert.equal(plan.structuralReadbackOnly, false);
|
|
495
|
+
assert.equal(plan.independentVerifier, true, `rdd ${rddLine}, profile ${writerProfile}`);
|
|
496
|
+
assert.match(plan.reason, /escalat/);
|
|
497
|
+
}
|
|
481
498
|
});
|
|
482
499
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
500
|
+
// Changed with verification by named risk: a failed or unrecognized
|
|
501
|
+
// assessment informs only; it is no longer treated as high.
|
|
502
|
+
test(`verificationPlan: rdd ${rddLine}, unassessable risk -> writer self-verification only, no verifier`, () => {
|
|
503
|
+
for (const writerProfile of PROFILES) {
|
|
504
|
+
const plan = verificationPlan({ rddLine, risk: VERIFICATION_TIER.UNASSESSABLE, writerProfile });
|
|
505
|
+
assert.equal(plan.writerSelfVerification, true, `rdd ${rddLine}, profile ${writerProfile}`);
|
|
506
|
+
assert.equal(plan.structuralReadbackOnly, false);
|
|
507
|
+
assert.equal(plan.independentVerifier, false, `rdd ${rddLine}, profile ${writerProfile}`);
|
|
491
508
|
}
|
|
492
509
|
});
|
|
493
510
|
}
|
|
494
511
|
|
|
512
|
+
test("verificationPlan: the writer's model never changes the plan, for every rdd line, risk, and outcome", () => {
|
|
513
|
+
for (const rddLine of RDD_LINES) {
|
|
514
|
+
for (const risk of RISKS) {
|
|
515
|
+
for (const nativeReviewOutcome of Object.values(NATIVE_REVIEW_OUTCOME)) {
|
|
516
|
+
const small = verificationPlan({ rddLine, risk, writerProfile: WRITER_PROFILE.SMALL, nativeReviewOutcome });
|
|
517
|
+
const large = verificationPlan({ rddLine, risk, writerProfile: WRITER_PROFILE.LARGE, nativeReviewOutcome });
|
|
518
|
+
assert.deepEqual(small, large, `rdd ${rddLine}, risk ${risk}, outcome ${nativeReviewOutcome}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("verificationRisk: only an escalation makes the plan high; a native high or unassessable tier informs as medium", () => {
|
|
525
|
+
const escalation = { item: 2 as const, reason: "weakens the permission check" };
|
|
526
|
+
for (const nativeRisk of RISKS) assert.equal(verificationRisk(nativeRisk, escalation), VERIFICATION_TIER.HIGH, `escalated ${nativeRisk}`);
|
|
527
|
+
assert.equal(verificationRisk(VERIFICATION_TIER.PASSIVE, undefined), VERIFICATION_TIER.PASSIVE);
|
|
528
|
+
assert.equal(verificationRisk(VERIFICATION_TIER.MEDIUM, undefined), VERIFICATION_TIER.MEDIUM);
|
|
529
|
+
assert.equal(verificationRisk(VERIFICATION_TIER.HIGH, undefined), VERIFICATION_TIER.MEDIUM, "a native high alone never adds a verifier");
|
|
530
|
+
assert.equal(verificationRisk(VERIFICATION_TIER.UNASSESSABLE, undefined), VERIFICATION_TIER.MEDIUM, "a failed assessment alone never adds a verifier");
|
|
531
|
+
});
|
|
532
|
+
|
|
495
533
|
test("verificationPlan: an unknown rdd line never lowers a tier relative to off", () => {
|
|
496
534
|
for (const risk of RISKS) {
|
|
497
535
|
for (const writerProfile of PROFILES) {
|
|
@@ -567,12 +605,14 @@ test("resolveWriterProfile: an unknown or omitted profile fails closed to small,
|
|
|
567
605
|
});
|
|
568
606
|
|
|
569
607
|
// ---------------------------------------------------------------------------
|
|
570
|
-
// Tool-level
|
|
608
|
+
// Tool-level unassessable path: the `gentle_review` tool's `assess` operation
|
|
571
609
|
// (`extensions/gentle-ai.ts`, gentle-pi#662) must treat a native CLI without
|
|
572
610
|
// the `assess` verb (an older binary) or a rejected `assess` call (a process
|
|
573
|
-
// failure) the same way -- risk "unassessable", which
|
|
574
|
-
//
|
|
575
|
-
//
|
|
611
|
+
// failure) the same way -- native risk "unassessable", which informs only:
|
|
612
|
+
// the plan is medium and adds no independent verifier unless the agent
|
|
613
|
+
// escalates a named high-risk item. `assess` is exposed as a `gentle_review`
|
|
614
|
+
// operation, not a dedicated tool, so the fixed `gentle_*` tool registry stays
|
|
615
|
+
// unchanged.
|
|
576
616
|
|
|
577
617
|
function reviewControllerTool(nativeReviewCli: Partial<NativeReviewCli> | null): { execute: (id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ content: readonly { type: string; text: string }[]; details: unknown }> } {
|
|
578
618
|
const tools = new Map<string, any>();
|
|
@@ -591,27 +631,37 @@ function reviewControllerTool(nativeReviewCli: Partial<NativeReviewCli> | null):
|
|
|
591
631
|
|
|
592
632
|
const ctx = { cwd: process.cwd() } as ExtensionContext;
|
|
593
633
|
|
|
594
|
-
|
|
634
|
+
// The only input that makes the plan high: an agent escalation naming a Task
|
|
635
|
+
// Size high-risk item. Tests whose subject is "closure or a decline never
|
|
636
|
+
// removes the verifier the plan requires" pass it so a verifier is required.
|
|
637
|
+
const ESCALATE = { escalate: { item: 3, reason: "changes a published export" } } as const;
|
|
638
|
+
|
|
639
|
+
// Changed with verification by named risk: an unassessable candidate no longer
|
|
640
|
+
// verifies like high; the plan is medium and the native tier stays visible.
|
|
641
|
+
test("gentle_review assess: an older binary without the assess verb informs only (no verifier without escalate)", async () => {
|
|
595
642
|
const nativeReviewCli: Partial<NativeReviewCli> = {
|
|
596
643
|
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "off", cloneLocal: "off", effective: "off", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
|
|
597
644
|
// assess intentionally absent: pre-gentle-ai#4295 binary.
|
|
598
645
|
};
|
|
599
646
|
const tool = reviewControllerTool(nativeReviewCli);
|
|
600
647
|
const result = await tool.execute("call-1", { operation: "assess" }, undefined, undefined, ctx);
|
|
601
|
-
const details = result.details as { risk: string; rddLine: string; plan: { writerSelfVerification: boolean; independentVerifier: boolean; structuralReadbackOnly: boolean } };
|
|
602
|
-
assert.equal(details.
|
|
648
|
+
const details = result.details as { risk: string; nativeRisk: string; rddLine: string; plan: { writerSelfVerification: boolean; independentVerifier: boolean; structuralReadbackOnly: boolean } };
|
|
649
|
+
assert.equal(details.nativeRisk, VERIFICATION_TIER.UNASSESSABLE);
|
|
650
|
+
assert.equal(details.risk, VERIFICATION_TIER.MEDIUM);
|
|
603
651
|
assert.equal(details.rddLine, "off");
|
|
604
652
|
assert.equal(details.plan.writerSelfVerification, true);
|
|
605
|
-
assert.equal(details.plan.independentVerifier,
|
|
653
|
+
assert.equal(details.plan.independentVerifier, false, "a failed assessment alone never adds a verifier");
|
|
606
654
|
assert.equal(details.plan.structuralReadbackOnly, false);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
assert.equal(details
|
|
611
|
-
assert.equal(
|
|
655
|
+
assert.equal(JSON.parse(result.content[0].text).nativeRisk, VERIFICATION_TIER.UNASSESSABLE);
|
|
656
|
+
|
|
657
|
+
const escalated = await tool.execute("call-1b", { operation: "assess", input: JSON.stringify(ESCALATE) }, undefined, undefined, ctx);
|
|
658
|
+
assert.equal((escalated.details as { risk: string }).risk, VERIFICATION_TIER.HIGH);
|
|
659
|
+
assert.equal((escalated.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, true, "an escalation over an unassessable candidate adds the verifier");
|
|
612
660
|
});
|
|
613
661
|
|
|
614
|
-
|
|
662
|
+
// Changed with verification by named risk: a rejected assess call informs
|
|
663
|
+
// only; the RDD status fail-closed half of this test is unchanged.
|
|
664
|
+
test("gentle_review assess: a rejected assess call informs only, and RDD status read failure fails closed to unknown", async () => {
|
|
615
665
|
const nativeReviewCli: Partial<NativeReviewCli> = {
|
|
616
666
|
reviewMode: async () => {
|
|
617
667
|
throw new Error("native review mode is unavailable");
|
|
@@ -622,12 +672,13 @@ test("gentle_review assess: a rejected assess call fails closed to high, and RDD
|
|
|
622
672
|
};
|
|
623
673
|
const tool = reviewControllerTool(nativeReviewCli);
|
|
624
674
|
const result = await tool.execute("call-2", { operation: "assess" }, undefined, undefined, ctx);
|
|
625
|
-
const details = result.details as { risk: string; rddLine: string; reasons: readonly { code: string }[]; plan: { independentVerifier: boolean; writerSelfVerification: boolean } };
|
|
626
|
-
assert.equal(details.
|
|
675
|
+
const details = result.details as { risk: string; nativeRisk: string; rddLine: string; reasons: readonly { code: string }[]; plan: { independentVerifier: boolean; writerSelfVerification: boolean } };
|
|
676
|
+
assert.equal(details.nativeRisk, VERIFICATION_TIER.UNASSESSABLE);
|
|
677
|
+
assert.equal(details.risk, VERIFICATION_TIER.MEDIUM);
|
|
627
678
|
assert.equal(details.rddLine, "unknown", "an unresolved RDD status must fail closed to unknown, never on or off");
|
|
628
679
|
assert.equal(details.reasons[0]?.code, "native-assess-unavailable");
|
|
629
680
|
assert.equal(details.plan.writerSelfVerification, true);
|
|
630
|
-
assert.equal(details.plan.independentVerifier,
|
|
681
|
+
assert.equal(details.plan.independentVerifier, false);
|
|
631
682
|
});
|
|
632
683
|
|
|
633
684
|
test("gentle_review assess: a successful native assessment is reflected directly in the returned plan", async () => {
|
|
@@ -658,7 +709,10 @@ test("gentle_review assess: baseRef without committedOnly is rejected", async ()
|
|
|
658
709
|
);
|
|
659
710
|
});
|
|
660
711
|
|
|
661
|
-
|
|
712
|
+
// Changed with verification by named risk: the writer profile is still
|
|
713
|
+
// resolved and reported (omitted fails closed to small, gemini is large), but
|
|
714
|
+
// it no longer adds a verifier to a medium plan.
|
|
715
|
+
test("gentle_review assess: writerModelId/writerEffort in input select the reported writer profile, which never changes the plan", async () => {
|
|
662
716
|
const nativeReviewCli: Partial<NativeReviewCli> = {
|
|
663
717
|
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "off", cloneLocal: "off", effective: "off", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
|
|
664
718
|
assess: async () => ({
|
|
@@ -672,12 +726,11 @@ test("gentle_review assess: writerModelId/writerEffort in input select the write
|
|
|
672
726
|
};
|
|
673
727
|
const tool = reviewControllerTool(nativeReviewCli);
|
|
674
728
|
|
|
675
|
-
// Omitted profile + medium + off ->
|
|
676
|
-
//
|
|
677
|
-
// profile must never resolve as permissively as a known large model).
|
|
729
|
+
// Omitted profile + medium + off -> reported as small (fail closed), and
|
|
730
|
+
// still no independent verifier: the model never adds one.
|
|
678
731
|
const omitted = await tool.execute("call-5", { operation: "assess" }, undefined, undefined, ctx);
|
|
679
732
|
assert.equal((omitted.details as { writerProfile: string }).writerProfile, "small");
|
|
680
|
-
assert.equal((omitted.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier,
|
|
733
|
+
assert.equal((omitted.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, false, "an omitted writer profile never adds a verifier");
|
|
681
734
|
|
|
682
735
|
// Explicit large profile + medium + off -> independent verifier false.
|
|
683
736
|
const explicitLarge = await tool.execute(
|
|
@@ -688,10 +741,12 @@ test("gentle_review assess: writerModelId/writerEffort in input select the write
|
|
|
688
741
|
ctx,
|
|
689
742
|
);
|
|
690
743
|
assert.equal((explicitLarge.details as { writerProfile: string }).writerProfile, "large");
|
|
691
|
-
assert.equal((explicitLarge.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, false
|
|
744
|
+
assert.equal((explicitLarge.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, false);
|
|
692
745
|
|
|
693
746
|
const small = await tool.execute("call-7", { operation: "assess", input: JSON.stringify({ writerEffort: "low" }) }, undefined, undefined, ctx);
|
|
694
|
-
assert.equal((small.details as {
|
|
747
|
+
assert.equal((small.details as { writerProfile: string }).writerProfile, "small");
|
|
748
|
+
assert.equal((small.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, false, "a low-effort writer profile never adds a verifier");
|
|
749
|
+
assert.deepEqual((small.details as { plan: unknown }).plan, (explicitLarge.details as { plan: unknown }).plan, "the writer's model never changes the plan");
|
|
695
750
|
|
|
696
751
|
// A gemini model id must resolve large, never small on the bare "mini" substring.
|
|
697
752
|
const gemini = await tool.execute("call-8", { operation: "assess", input: JSON.stringify({ writerModelId: "gemini-2.5-pro", writerEffort: "high" }) }, undefined, undefined, ctx);
|
|
@@ -699,6 +754,9 @@ test("gentle_review assess: writerModelId/writerEffort in input select the write
|
|
|
699
754
|
assert.equal((gemini.details as { plan: { independentVerifier: boolean } }).plan.independentVerifier, false);
|
|
700
755
|
});
|
|
701
756
|
|
|
757
|
+
// Changed with verification by named risk: a native high without escalate is
|
|
758
|
+
// a medium plan (no verifier); the declined fallback still applies, and an
|
|
759
|
+
// escalation still gets its verifier.
|
|
702
760
|
test("gentle_review assess: an explicit nativeReviewOutcome:\"declined\" input falls back to the risk-gated plan even when RDD is on (gentle-pi#668)", async () => {
|
|
703
761
|
const nativeReviewCli: Partial<NativeReviewCli> = {
|
|
704
762
|
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "on", cloneLocal: "", effective: "on", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
|
|
@@ -713,13 +771,19 @@ test("gentle_review assess: an explicit nativeReviewOutcome:\"declined\" input f
|
|
|
713
771
|
};
|
|
714
772
|
const tool = reviewControllerTool(nativeReviewCli);
|
|
715
773
|
const result = await tool.execute("call-9", { operation: "assess", input: JSON.stringify({ nativeReviewOutcome: "declined" }) }, undefined, undefined, ctx);
|
|
716
|
-
const details = result.details as { risk: string; rddLine: string; outcome_source: string; plan: { writerSelfVerification: boolean; independentVerifier: boolean; structuralReadbackOnly: boolean } };
|
|
717
|
-
assert.equal(details.
|
|
774
|
+
const details = result.details as { risk: string; nativeRisk: string; rddLine: string; outcome_source: string; plan: { writerSelfVerification: boolean; independentVerifier: boolean; structuralReadbackOnly: boolean } };
|
|
775
|
+
assert.equal(details.nativeRisk, "high");
|
|
776
|
+
assert.equal(details.risk, "medium", "a native high without escalate informs only");
|
|
718
777
|
assert.equal(details.rddLine, "on", "the rendered RDD line still reads on -- only the verification plan falls back");
|
|
719
778
|
assert.equal(details.outcome_source, "explicit");
|
|
720
779
|
assert.equal(details.plan.writerSelfVerification, true);
|
|
721
|
-
assert.equal(details.plan.independentVerifier,
|
|
780
|
+
assert.equal(details.plan.independentVerifier, false, "a native high without escalate adds no verifier");
|
|
722
781
|
assert.equal(details.plan.structuralReadbackOnly, false);
|
|
782
|
+
|
|
783
|
+
const escalated = await tool.execute("call-9b", { operation: "assess", input: JSON.stringify({ nativeReviewOutcome: "declined", ...ESCALATE }) }, undefined, undefined, ctx);
|
|
784
|
+
const escalatedDetails = escalated.details as { risk: string; plan: { independentVerifier: boolean } };
|
|
785
|
+
assert.equal(escalatedDetails.risk, "high");
|
|
786
|
+
assert.equal(escalatedDetails.plan.independentVerifier, true, "a declined review keeps the verifier an escalation requires");
|
|
723
787
|
});
|
|
724
788
|
|
|
725
789
|
test("gentle_review assess: an unrecognized nativeReviewOutcome value is rejected", async () => {
|
|
@@ -763,16 +827,19 @@ test("gentle_review assess: derivation is bound to the exact candidate recorded,
|
|
|
763
827
|
__testing.clearNativeReviewOutcomeMemoForTesting();
|
|
764
828
|
let current = "target-a";
|
|
765
829
|
const tool = reviewControllerTool(assessOnNativeCli(() => current));
|
|
830
|
+
// Verification by named risk: the plan requires a verifier only through an
|
|
831
|
+
// escalation, so each call escalates to keep "never removes it" observable.
|
|
832
|
+
const escalated = { operation: "assess", input: JSON.stringify(ESCALATE) };
|
|
766
833
|
|
|
767
834
|
// Nothing recorded for candidate A yet -> unknown, risk-gated.
|
|
768
|
-
const before = (await tool.execute("call-11",
|
|
835
|
+
const before = (await tool.execute("call-11", escalated, undefined, undefined, ctx)).details as { nativeReviewOutcome: string; outcome_source: string; plan: { independentVerifier: boolean } };
|
|
769
836
|
assert.equal(before.nativeReviewOutcome, "unknown");
|
|
770
837
|
assert.equal(before.outcome_source, "unknown");
|
|
771
838
|
assert.equal(before.plan.independentVerifier, true);
|
|
772
839
|
|
|
773
840
|
// Candidate A recorded declined -> assess for A derives it.
|
|
774
841
|
__testing.recordNativeReviewOutcome(ctx.cwd, "target-a", "declined");
|
|
775
|
-
const forA = (await tool.execute("call-12",
|
|
842
|
+
const forA = (await tool.execute("call-12", escalated, undefined, undefined, ctx)).details as { nativeReviewOutcome: string; outcome_source: string; plan: { independentVerifier: boolean } };
|
|
776
843
|
assert.equal(forA.nativeReviewOutcome, "declined");
|
|
777
844
|
assert.equal(forA.outcome_source, "derived");
|
|
778
845
|
assert.equal(forA.plan.independentVerifier, true, "a derived decline re-enables the independent verifier for the matching candidate");
|
|
@@ -781,14 +848,14 @@ test("gentle_review assess: derivation is bound to the exact candidate recorded,
|
|
|
781
848
|
// and since closed is never written, an acknowledged A can never leak a
|
|
782
849
|
// closed derivation into B either.
|
|
783
850
|
current = "target-b";
|
|
784
|
-
const forB = (await tool.execute("call-13",
|
|
851
|
+
const forB = (await tool.execute("call-13", escalated, undefined, undefined, ctx)).details as { nativeReviewOutcome: string; outcome_source: string; plan: { independentVerifier: boolean } };
|
|
785
852
|
assert.equal(forB.nativeReviewOutcome, "unknown", "candidate B must never inherit candidate A's recorded outcome");
|
|
786
853
|
assert.equal(forB.outcome_source, "unknown");
|
|
787
854
|
assert.equal(forB.plan.independentVerifier, true);
|
|
788
855
|
|
|
789
856
|
// gentle-pi#1175: a caller-declared closed is only a claim. This older
|
|
790
857
|
// assessment carries no candidate.consumed, so it fails closed to unknown.
|
|
791
|
-
const closed = (await tool.execute("call-14", { operation: "assess", input: JSON.stringify({ nativeReviewOutcome: "closed" }) }, undefined, undefined, ctx)).details as { nativeReviewOutcome: string; outcome_source: string; plan: { independentVerifier: boolean; writerSelfVerification: boolean } };
|
|
858
|
+
const closed = (await tool.execute("call-14", { operation: "assess", input: JSON.stringify({ nativeReviewOutcome: "closed", ...ESCALATE }) }, undefined, undefined, ctx)).details as { nativeReviewOutcome: string; outcome_source: string; plan: { independentVerifier: boolean; writerSelfVerification: boolean } };
|
|
792
859
|
assert.equal(closed.nativeReviewOutcome, "unknown");
|
|
793
860
|
assert.equal(closed.outcome_source, "unknown");
|
|
794
861
|
assert.equal(closed.plan.writerSelfVerification, true);
|
|
@@ -836,6 +903,7 @@ function closureCli(options: ClosureCliOptions): Partial<NativeReviewCli> {
|
|
|
836
903
|
|
|
837
904
|
interface AssessDetails {
|
|
838
905
|
risk: string;
|
|
906
|
+
nativeRisk: string;
|
|
839
907
|
nativeReviewOutcome: string;
|
|
840
908
|
outcome_source: string;
|
|
841
909
|
writerProfile: string;
|
|
@@ -882,8 +950,11 @@ test("gentle_review assess: already-reviewed predecessor remains medium and neve
|
|
|
882
950
|
}
|
|
883
951
|
});
|
|
884
952
|
|
|
953
|
+
// Verification by named risk: the closure tests below that check "closure or a
|
|
954
|
+
// decline never removes the verifier" escalate, because only an escalation
|
|
955
|
+
// makes the plan require one.
|
|
885
956
|
test("gentle_review assess: native consumed true derives closed for this candidate", async () => {
|
|
886
|
-
const details = await assessWith(closureCli({ consumed: true }));
|
|
957
|
+
const details = await assessWith(closureCli({ consumed: true }), ESCALATE);
|
|
887
958
|
assert.equal(details.nativeReviewOutcome, "closed");
|
|
888
959
|
assert.equal(details.outcome_source, "derived");
|
|
889
960
|
assert.equal(details.plan.writerSelfVerification, true);
|
|
@@ -892,7 +963,7 @@ test("gentle_review assess: native consumed true derives closed for this candida
|
|
|
892
963
|
|
|
893
964
|
test("gentle_review assess: a caller-declared closed without native consumed evidence fails closed to unknown", async () => {
|
|
894
965
|
for (const consumed of [false, undefined]) {
|
|
895
|
-
const details = await assessWith(closureCli({ consumed }), { nativeReviewOutcome: "closed" });
|
|
966
|
+
const details = await assessWith(closureCli({ consumed }), { nativeReviewOutcome: "closed", ...ESCALATE });
|
|
896
967
|
assert.equal(details.nativeReviewOutcome, "unknown", `consumed ${String(consumed)}`);
|
|
897
968
|
assert.equal(details.outcome_source, "unknown");
|
|
898
969
|
assert.equal(details.plan.independentVerifier, true);
|
|
@@ -900,7 +971,7 @@ test("gentle_review assess: a caller-declared closed without native consumed evi
|
|
|
900
971
|
});
|
|
901
972
|
|
|
902
973
|
test("gentle_review assess: a caller-declared closed corroborated by native consumed stays closed", async () => {
|
|
903
|
-
const details = await assessWith(closureCli({ consumed: true }), { nativeReviewOutcome: "closed" });
|
|
974
|
+
const details = await assessWith(closureCli({ consumed: true }), { nativeReviewOutcome: "closed", ...ESCALATE });
|
|
904
975
|
assert.equal(details.nativeReviewOutcome, "closed");
|
|
905
976
|
assert.equal(details.outcome_source, "derived", "closure is attributed to the native evidence, not to the caller's claim");
|
|
906
977
|
assert.equal(details.plan.independentVerifier, true, "closure never removes the risk-tier verifier");
|
|
@@ -908,7 +979,7 @@ test("gentle_review assess: a caller-declared closed corroborated by native cons
|
|
|
908
979
|
|
|
909
980
|
test("gentle_review assess: explicit declined, unavailable, or unknown beats native consumed (only ever raises the bar)", async () => {
|
|
910
981
|
for (const outcome of ["declined", "unavailable", "unknown"]) {
|
|
911
|
-
const details = await assessWith(closureCli({ consumed: true }), { nativeReviewOutcome: outcome });
|
|
982
|
+
const details = await assessWith(closureCli({ consumed: true }), { nativeReviewOutcome: outcome, ...ESCALATE });
|
|
912
983
|
assert.equal(details.nativeReviewOutcome, outcome);
|
|
913
984
|
assert.equal(details.outcome_source, "explicit");
|
|
914
985
|
assert.equal(details.plan.independentVerifier, true, `${outcome} must keep the risk-gated verifier`);
|
|
@@ -920,7 +991,7 @@ test("gentle_review assess: a recorded decline for the same candidate beats nati
|
|
|
920
991
|
__testing.clearNativeReviewOutcomeMemoForTesting();
|
|
921
992
|
__testing.recordNativeReviewOutcome(ctx.cwd, "target-a", "declined");
|
|
922
993
|
const cli = closureCli({ consumed: true, targetIdentity: () => "target-a" });
|
|
923
|
-
for (const input of [
|
|
994
|
+
for (const input of [ESCALATE, { nativeReviewOutcome: "closed", ...ESCALATE }]) {
|
|
924
995
|
const details = await assessWith(cli, input);
|
|
925
996
|
assert.equal(details.nativeReviewOutcome, "declined", JSON.stringify(input));
|
|
926
997
|
assert.equal(details.outcome_source, "derived");
|
|
@@ -936,29 +1007,33 @@ test("gentle_review assess: a different candidate never inherits closure or a re
|
|
|
936
1007
|
const closedB = await assessWith(closureCli({ consumed: true, targetIdentity: () => "target-b" }));
|
|
937
1008
|
assert.equal(closedB.nativeReviewOutcome, "closed");
|
|
938
1009
|
// Candidate C follows B, but its own assessment is not consumed: B's closure never carries over.
|
|
939
|
-
const openC = await assessWith(closureCli({ consumed: false, targetIdentity: () => "target-c" }));
|
|
1010
|
+
const openC = await assessWith(closureCli({ consumed: false, targetIdentity: () => "target-c" }), ESCALATE);
|
|
940
1011
|
assert.equal(openC.nativeReviewOutcome, "unknown");
|
|
941
1012
|
assert.equal(openC.outcome_source, "unknown");
|
|
942
1013
|
assert.equal(openC.plan.independentVerifier, true);
|
|
943
1014
|
});
|
|
944
1015
|
|
|
1016
|
+
// Changed with verification by named risk: an unassessable candidate is a
|
|
1017
|
+
// medium plan with no verifier unless escalated; it is still never closed.
|
|
945
1018
|
test("gentle_review assess: an unassessable candidate is never closed", async () => {
|
|
946
1019
|
for (const input of [undefined, { nativeReviewOutcome: "closed" }]) {
|
|
947
1020
|
const details = await assessWith(closureCli({ unassessable: true }), input);
|
|
948
|
-
assert.equal(details.
|
|
1021
|
+
assert.equal(details.nativeRisk, VERIFICATION_TIER.UNASSESSABLE);
|
|
1022
|
+
assert.equal(details.risk, VERIFICATION_TIER.MEDIUM);
|
|
949
1023
|
assert.equal(details.nativeReviewOutcome, "unknown");
|
|
950
|
-
assert.equal(details.plan.independentVerifier,
|
|
1024
|
+
assert.equal(details.plan.independentVerifier, false);
|
|
951
1025
|
}
|
|
952
1026
|
});
|
|
953
1027
|
|
|
954
1028
|
test("gentle_review assess: derived closure only changes the RDD on-path; RDD off plans are unchanged", async () => {
|
|
955
1029
|
for (const risk of ["passive", "medium", "high"] as const) {
|
|
956
1030
|
for (const consumed of [true, false]) {
|
|
1031
|
+
// The plan follows verificationRisk (native high without escalate is medium).
|
|
957
1032
|
const off = await assessWith(closureCli({ rdd: "off", risk, consumed }), { writerModelId: "claude-sonnet-5", writerEffort: "high" });
|
|
958
|
-
const expectedOff = verificationPlan({ rddLine: RDD_LINE.OFF, risk, writerProfile: WRITER_PROFILE.LARGE });
|
|
1033
|
+
const expectedOff = verificationPlan({ rddLine: RDD_LINE.OFF, risk: verificationRisk(risk, undefined), writerProfile: WRITER_PROFILE.LARGE });
|
|
959
1034
|
assert.deepEqual(off.plan, { ...expectedOff }, `off ${risk} consumed ${consumed}`);
|
|
960
1035
|
const on = await assessWith(closureCli({ rdd: "on", risk, consumed }), { writerModelId: "claude-sonnet-5", writerEffort: "high" });
|
|
961
|
-
const expectedOn = verificationPlan({ rddLine: RDD_LINE.ON, risk, writerProfile: WRITER_PROFILE.LARGE, nativeReviewOutcome: consumed ? NATIVE_REVIEW_OUTCOME.CLOSED : NATIVE_REVIEW_OUTCOME.UNKNOWN });
|
|
1036
|
+
const expectedOn = verificationPlan({ rddLine: RDD_LINE.ON, risk: verificationRisk(risk, undefined), writerProfile: WRITER_PROFILE.LARGE, nativeReviewOutcome: consumed ? NATIVE_REVIEW_OUTCOME.CLOSED : NATIVE_REVIEW_OUTCOME.UNKNOWN });
|
|
962
1037
|
assert.deepEqual(on.plan, { ...expectedOn }, `on ${risk} consumed ${consumed}`);
|
|
963
1038
|
}
|
|
964
1039
|
}
|
|
@@ -993,13 +1068,16 @@ function workspaceRoot(): string {
|
|
|
993
1068
|
|
|
994
1069
|
const mediumOffCli = (): Partial<NativeReviewCli> => closureCli({ rdd: "off", risk: "medium" });
|
|
995
1070
|
|
|
1071
|
+
// Changed with verification by named risk (here and in the next two tests):
|
|
1072
|
+
// the runtime writer profile still wins and is reported, but a small profile
|
|
1073
|
+
// no longer adds a verifier to a medium plan.
|
|
996
1074
|
test("gentle_review assess: the runtime writer profile wins over contradicting caller input", async () => {
|
|
997
1075
|
const small = receiptContext();
|
|
998
1076
|
small.record({ toolCallId: "w1", writerModelId: "openai/gpt-5-mini", writerEffort: "high" });
|
|
999
1077
|
const claimedLarge = await assessWith(mediumOffCli(), { writerModelId: "claude-sonnet-5", writerEffort: "high" }, small.context);
|
|
1000
1078
|
assert.equal(claimedLarge.writerProfile, "small");
|
|
1001
1079
|
assert.equal(claimedLarge.writerProfileSource, "runtime");
|
|
1002
|
-
assert.equal(claimedLarge.plan.independentVerifier,
|
|
1080
|
+
assert.equal(claimedLarge.plan.independentVerifier, false, "the reported profile never adds a verifier");
|
|
1003
1081
|
|
|
1004
1082
|
const large = receiptContext();
|
|
1005
1083
|
large.record({ toolCallId: "w1", writerModelId: "google/gemini-2.5-pro", writerEffort: "high" });
|
|
@@ -1022,7 +1100,7 @@ test("gentle_review assess: any small or unknown pending runtime writer makes th
|
|
|
1022
1100
|
const details = await assessWith(mediumOffCli(), { writerModelId: "claude-sonnet-5", writerEffort: "high" }, fixture.context);
|
|
1023
1101
|
assert.equal(details.writerProfile, "small", entry.name);
|
|
1024
1102
|
assert.equal(details.writerProfileSource, "runtime", entry.name);
|
|
1025
|
-
assert.equal(details.plan.independentVerifier,
|
|
1103
|
+
assert.equal(details.plan.independentVerifier, false, entry.name);
|
|
1026
1104
|
}
|
|
1027
1105
|
|
|
1028
1106
|
const known = receiptContext();
|
|
@@ -1048,7 +1126,7 @@ test("gentle_review assess: without pending runtime receipts the caller input pa
|
|
|
1048
1126
|
const fallback = await assessWith(mediumOffCli(), undefined, fixture.context);
|
|
1049
1127
|
assert.equal(fallback.writerProfile, "small");
|
|
1050
1128
|
assert.equal(fallback.writerProfileSource, "fallback");
|
|
1051
|
-
assert.equal(fallback.plan.independentVerifier,
|
|
1129
|
+
assert.equal(fallback.plan.independentVerifier, false);
|
|
1052
1130
|
|
|
1053
1131
|
// No session at all behaves exactly the same way.
|
|
1054
1132
|
const noSession = await assessWith(mediumOffCli());
|
|
@@ -1093,14 +1171,17 @@ function nativeClient(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
|
1093
1171
|
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024);
|
|
1094
1172
|
}
|
|
1095
1173
|
|
|
1096
|
-
|
|
1174
|
+
// Changed with verification by named risk: the refusal is reported as native
|
|
1175
|
+
// risk unassessable, which informs only (medium plan, no verifier).
|
|
1176
|
+
test("gentle_review assess: stderr-only refusal preserves safe actionable diagnostics and reports unassessable", async () => {
|
|
1097
1177
|
const queue = queuedAdapter([{ stdout: "", stderr: "untracked scope declaration required; use --untracked-scope=exclude or select; token=hidden-secret", exitCode: 1 }]);
|
|
1098
1178
|
const client = nativeClient(queue.adapter);
|
|
1099
1179
|
const tool = reviewControllerTool({ assess: client.assess.bind(client) });
|
|
1100
1180
|
const result = await tool.execute("refusal", { operation: "assess" }, undefined, undefined, ctx);
|
|
1101
|
-
const details = result.details as { risk: string; reasons: { code: string; detail: string }[]; plan: { independentVerifier: boolean } };
|
|
1102
|
-
assert.equal(details.
|
|
1103
|
-
assert.equal(details.
|
|
1181
|
+
const details = result.details as { risk: string; nativeRisk: string; reasons: { code: string; detail: string }[]; plan: { independentVerifier: boolean } };
|
|
1182
|
+
assert.equal(details.nativeRisk, "unassessable");
|
|
1183
|
+
assert.equal(details.risk, "medium");
|
|
1184
|
+
assert.equal(details.plan.independentVerifier, false);
|
|
1104
1185
|
assert.equal(details.reasons[0].code, NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT);
|
|
1105
1186
|
assert.match(details.reasons[0].detail, /untracked scope declaration required/);
|
|
1106
1187
|
assert.match(details.reasons[0].detail, /--untracked-scope=exclude/);
|
|
@@ -1133,8 +1214,9 @@ for (const [shape, sensitive] of [
|
|
|
1133
1214
|
const queue = queuedAdapter([{ stdout: "", stderr: `${sensitive}\nuntracked scope declaration required; use --untracked-scope=exclude or select`, exitCode: 1 }]);
|
|
1134
1215
|
const client = nativeClient(queue.adapter);
|
|
1135
1216
|
const result = await reviewControllerTool({ assess: client.assess.bind(client) }).execute("private-shape", { operation: "assess" }, undefined, undefined, ctx);
|
|
1136
|
-
|
|
1137
|
-
|
|
1217
|
+
// The failed assessment shows as nativeRisk; the plan risk is medium.
|
|
1218
|
+
const details = result.details as { nativeRisk: string; reasons: { code: string; detail: string }[] };
|
|
1219
|
+
assert.equal(details.nativeRisk, "unassessable");
|
|
1138
1220
|
assert.equal(details.reasons[0].code, NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT);
|
|
1139
1221
|
assert.doesNotMatch(JSON.stringify(result), /hidden|private_project| project|file\.ts|\/private\//);
|
|
1140
1222
|
assert.match(details.reasons[0].detail, /untracked scope declaration required; use --untracked-scope=exclude or select/);
|
|
@@ -1189,8 +1271,9 @@ test("facade assess leaves absent declarations to native and preserves stale-inv
|
|
|
1189
1271
|
const client = nativeClient(queue.adapter);
|
|
1190
1272
|
const tool = reviewControllerTool({ assess: client.assess.bind(client) });
|
|
1191
1273
|
const result = await tool.execute("stale", { operation: "assess", input: JSON.stringify(selection) }, undefined, undefined, ctx);
|
|
1192
|
-
|
|
1193
|
-
|
|
1274
|
+
// The failed assessment shows as nativeRisk; the plan risk is medium.
|
|
1275
|
+
const details = result.details as { nativeRisk: string; reasons: { detail: string }[] };
|
|
1276
|
+
assert.equal(details.nativeRisk, "unassessable");
|
|
1194
1277
|
assert.match(details.reasons[0].detail, /inventory changed/);
|
|
1195
1278
|
if (!("untrackedScope" in selection)) assert.deepEqual(queue.calls[0].arguments, ["review", "assess", "--cwd", process.cwd(), "--json"]);
|
|
1196
1279
|
}
|
|
@@ -1252,7 +1335,10 @@ test("gentle_review assess: consumed, reviewDue, reviewDueReason, and nextTransi
|
|
|
1252
1335
|
const client = nativeClient(queue.adapter);
|
|
1253
1336
|
const result = await reviewControllerTool({ assess: client.assess.bind(client) }).execute("new-envelope", { operation: "assess" }, undefined, undefined, ctx);
|
|
1254
1337
|
const details = result.details as Record<string, unknown> & { candidate: Record<string, unknown> };
|
|
1255
|
-
|
|
1338
|
+
// Verification by named risk: the native high shows as nativeRisk; the
|
|
1339
|
+
// plan risk is medium without an escalation. The projection is unchanged.
|
|
1340
|
+
assert.equal(details.nativeRisk, "high");
|
|
1341
|
+
assert.equal(details.risk, "medium");
|
|
1256
1342
|
assert.equal(details.candidate.consumed, false);
|
|
1257
1343
|
assert.equal(details.reviewDue, true);
|
|
1258
1344
|
assert.equal(details.reviewDueReason, "high_risk");
|
|
@@ -1275,8 +1361,9 @@ test("gentle_review assess: an older native envelope projects no consumed/review
|
|
|
1275
1361
|
|
|
1276
1362
|
test("gentle_review assess: an unassessable projection omits reason.path instead of emitting an empty string", async () => {
|
|
1277
1363
|
const result = await reviewControllerTool({}).execute("no-assess", { operation: "assess" }, undefined, undefined, ctx);
|
|
1278
|
-
|
|
1279
|
-
|
|
1364
|
+
// Verification by named risk: the failed assessment shows as nativeRisk.
|
|
1365
|
+
const details = result.details as { nativeRisk: string; reasons: Record<string, unknown>[] };
|
|
1366
|
+
assert.equal(details.nativeRisk, VERIFICATION_TIER.UNASSESSABLE);
|
|
1280
1367
|
assert.equal(details.reasons.length, 1);
|
|
1281
1368
|
assert.equal(Object.hasOwn(details.reasons[0], "path"), false);
|
|
1282
1369
|
assert.equal(typeof details.reasons[0].detail, "string");
|