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
|
@@ -10,9 +10,9 @@ import { localProfilePinPath, repoProfileDeclarationPath, writeProfilePinSync }
|
|
|
10
10
|
import { createProfile, emptyProfilesFile, profilesFilePath, writeProfilesFileSync } from "../lib/agent-profiles.ts";
|
|
11
11
|
import type { AgentModelConfig } from "../lib/model-routing-authority.ts";
|
|
12
12
|
import { GENTLE_AI_DEV_BINARY_ENV, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
|
|
13
|
-
import type
|
|
13
|
+
import { NativeReviewIntegrationError, type NativeReviewCli, type NativeReviewUnachievableLensCaptureArtifact, type NativeReviewUnachievableLensCaptureRequest } from "../lib/native-review-cli.ts";
|
|
14
14
|
import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_PI_TIMEOUT_ENV, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
|
|
15
|
-
import { decodeReviewNextTransitionV3, decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
15
|
+
import { decodeReviewFailureV2, decodeReviewNextTransitionV3, decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
16
16
|
import type { InProcessReviewerRegistry } from "../lib/inprocess-reviewer.ts";
|
|
17
17
|
|
|
18
18
|
// One-slot capture routing: the host relay runs only when the selected
|
|
@@ -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])]);
|
|
@@ -444,6 +445,63 @@ async function runCaptureGroup(cwd: string, harness: RoutingHarness, lineageId:
|
|
|
444
445
|
|
|
445
446
|
function prepared(request: ReviewHostRelayRequest) { return { request, promptByteLength: 64, resultByteLength: 32 }; }
|
|
446
447
|
|
|
448
|
+
for (const failAt of [0, 1, 2]) {
|
|
449
|
+
for (const typed of [true, false]) {
|
|
450
|
+
test(`grouped STATUS failure preserves ${typed ? "decoded diagnostics" : "untyped behavior"} at call ${failAt + 1}`, async (t) => {
|
|
451
|
+
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
452
|
+
const cwd = repository(t), lineageId = "relay-group-status-failure", inputs = groupInputs(lineageId);
|
|
453
|
+
const failure = decodeReviewFailureV2({
|
|
454
|
+
schema: "gentle-ai.review-integration.failure/v2", contract: "gentle-ai.review-integration/v2",
|
|
455
|
+
operation: "review.status", phase: "native_running", code: "native_operation_timeout",
|
|
456
|
+
message: "The negotiated review operation exceeded its aggregate time budget.",
|
|
457
|
+
mutation_outcome: "not_started", authority_applicability: "current_target", retry_safe: false,
|
|
458
|
+
replayability: "manual_action_required", required_inputs: [], next_action: "explicit-maintainer-action",
|
|
459
|
+
cause: "STATUS budget exhausted", context: { binding_revision: { expected: SHA, current: PHASE_REVISION } },
|
|
460
|
+
});
|
|
461
|
+
const error = typed ? new NativeReviewIntegrationError(failure) : Object.assign(new Error(failure.message), {
|
|
462
|
+
failureEnvelope: { raw: { secret: "must not be projected" } }, stdout: "private stdout", stderr: "private stderr",
|
|
463
|
+
});
|
|
464
|
+
const harness = nativeHarness([]);
|
|
465
|
+
let calls = 0, preparations = 0, submissions = 0;
|
|
466
|
+
harness.native.targetStatus = async () => {
|
|
467
|
+
const index = calls++;
|
|
468
|
+
if (index === failAt) throw error;
|
|
469
|
+
return finalizeStatus(lineageId, index === 0 ? inputs : inputs.slice(index - 1));
|
|
470
|
+
};
|
|
471
|
+
__testing.setReviewHostRelayGroupRunnersForTesting(async (requests) => {
|
|
472
|
+
preparations += 1;
|
|
473
|
+
return requests.map(prepared);
|
|
474
|
+
}, async (result) => {
|
|
475
|
+
submissions += 1;
|
|
476
|
+
return { promptByteLength: result.promptByteLength, resultByteLength: result.resultByteLength, submission: "{}" };
|
|
477
|
+
});
|
|
478
|
+
const result = await runCaptureGroup(cwd, harness, lineageId, inputs);
|
|
479
|
+
assert.equal(result.outcome, "native-status-failed");
|
|
480
|
+
assert.equal(result.status, "blocked");
|
|
481
|
+
assert.equal(result.reason, failure.message);
|
|
482
|
+
assert.equal(calls, failAt + 1, "no STATUS retry");
|
|
483
|
+
assert.equal(preparations, failAt === 0 ? 0 : 1);
|
|
484
|
+
assert.equal(submissions, Math.max(0, failAt - 1), "no submission after STATUS failure");
|
|
485
|
+
assert.equal(result.mutation_performed, submissions > 0);
|
|
486
|
+
assert.equal(result.mutation_outcome, submissions > 0 ? "partial" : "none");
|
|
487
|
+
if (failAt > 0) {
|
|
488
|
+
const progress = result.host_relay as { reviewers: unknown[] };
|
|
489
|
+
assert.equal(progress.reviewers.length, submissions);
|
|
490
|
+
}
|
|
491
|
+
if (typed) {
|
|
492
|
+
assert.deepEqual(result.native_failure, failure.raw);
|
|
493
|
+
assert.equal(result.replayability, failure.replayability);
|
|
494
|
+
assert.equal(result.next_action, failure.nextAction);
|
|
495
|
+
} else {
|
|
496
|
+
assert.equal(result.native_failure, undefined);
|
|
497
|
+
assert.equal(result.replayability, undefined);
|
|
498
|
+
assert.equal(result.next_action, undefined);
|
|
499
|
+
assert.doesNotMatch(JSON.stringify(result), /must not be projected|private stdout|private stderr/);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
447
505
|
test("grouped capture forecasts once, reaches a four-reviewer barrier, and reconciles unclosed submissions", async (t) => {
|
|
448
506
|
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
449
507
|
const cwd = repository(t), lineageId = "relay-group", inputs = groupInputs(lineageId), lenses = inputs.map((input) => input.arguments.find((argument) => argument.name === "lens")!.value);
|
|
@@ -1343,3 +1401,89 @@ test("a pinned profile missing a required group role is refused typed before any
|
|
|
1343
1401
|
assert.equal(modelLookups, 0, "the typed refusal fires before any model resolution");
|
|
1344
1402
|
assert.equal(authAttempts, 0, "the typed refusal fires before any provider auth");
|
|
1345
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) {
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import type { AssistantMessage, JsonObject, Tool, ToolCall } from "@earendil-works/pi-ai";
|
|
8
|
+
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
9
|
+
|
|
10
|
+
// gentle-pi S11 T1: with receipt-driven development off, the orchestrator only
|
|
11
|
+
// ever calls gentle_review ASSESS, so the parent request carries a short ASSESS
|
|
12
|
+
// guideline and leaves the capture, capture-group, and scope declarations out.
|
|
13
|
+
// With RDD on, the request is exactly what the registered tools declare. Both
|
|
14
|
+
// paths run through a real Pi SDK session and inspect the provider request.
|
|
15
|
+
|
|
16
|
+
const HIDDEN_WHEN_OFF = ["gentle_review_capture", "gentle_review_capture_group", "gentle_review_scope"] as const;
|
|
17
|
+
|
|
18
|
+
interface RecordedRequest { systemPrompt: string; tools: Tool[] }
|
|
19
|
+
interface SessionRun {
|
|
20
|
+
requests: RecordedRequest[];
|
|
21
|
+
registered: Map<string, { description: string; promptGuidelines?: readonly string[] }>;
|
|
22
|
+
assessResults: Array<{ isError: boolean; details: unknown }>;
|
|
23
|
+
errors: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RddMode = "on" | "off" | "throws";
|
|
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
|
+
|
|
34
|
+
function nativeCli(mode: () => RddMode): NativeReviewCli {
|
|
35
|
+
return {
|
|
36
|
+
reviewMode: async () => {
|
|
37
|
+
const effective = mode();
|
|
38
|
+
if (effective === "throws") throw new Error("native review mode is unavailable");
|
|
39
|
+
return { operation: "status", scope: "clone", status: { global: effective, cloneLocal: "", effective, source: "global" } };
|
|
40
|
+
},
|
|
41
|
+
assess: async () => ({
|
|
42
|
+
schema: "gentle-ai.review-assessment/v1",
|
|
43
|
+
risk: "high",
|
|
44
|
+
reviewDue: true,
|
|
45
|
+
reviewDueReason: "high_risk",
|
|
46
|
+
nextTransition: NATIVE_CONTINUATION,
|
|
47
|
+
reasons: [],
|
|
48
|
+
changedPaths: 1,
|
|
49
|
+
changedLines: 3,
|
|
50
|
+
candidate: { kind: "current-changes", baseRef: undefined },
|
|
51
|
+
}),
|
|
52
|
+
} as unknown as NativeReviewCli;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runSession(
|
|
56
|
+
root: string,
|
|
57
|
+
label: string,
|
|
58
|
+
bindings: Record<string, string>,
|
|
59
|
+
mode: () => RddMode,
|
|
60
|
+
beforeSecondPrompt: () => void = () => {},
|
|
61
|
+
): Promise<SessionRun> {
|
|
62
|
+
const sdk = await import("@earendil-works/pi-coding-agent");
|
|
63
|
+
const ai = await import("@earendil-works/pi-ai");
|
|
64
|
+
const { createGentleAiExtension } = await import("../extensions/gentle-ai.ts");
|
|
65
|
+
const cwd = join(root, `repo-${label}`);
|
|
66
|
+
mkdirSync(cwd);
|
|
67
|
+
const gitEnv = { PATH: process.env.PATH, HOME: root, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: join(root, "gitconfig") };
|
|
68
|
+
writeFileSync(gitEnv.GIT_CONFIG_GLOBAL, "");
|
|
69
|
+
execFileSync("git", ["init", "--initial-branch=fixture"], { cwd, env: gitEnv, stdio: "ignore" });
|
|
70
|
+
execFileSync("git", ["-c", "user.name=SDK Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--allow-empty", "-m", "fixture"], { cwd, env: gitEnv, stdio: "ignore" });
|
|
71
|
+
const run: SessionRun = { requests: [], registered: new Map(), assessResults: [], errors: [] };
|
|
72
|
+
let next: { name: string; arguments: JsonObject } | undefined;
|
|
73
|
+
let calls = 0;
|
|
74
|
+
const model = { id: "driver", name: "Local driver", provider: "loadout-fixture", api: "loadout-fixture", baseUrl: "http://invalid.local", reasoning: false, input: ["text"], contextWindow: 1_000_000, maxTokens: 1024, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } } as const;
|
|
75
|
+
const manager = sdk.SessionManager.inMemory(cwd);
|
|
76
|
+
const settings = sdk.SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false }, cacheWarming: "off", packages: [] });
|
|
77
|
+
const runtime = await sdk.ModelRuntime.create({ credentials: new ai.InMemoryCredentialStore(), modelsPath: null, modelsStorePath: join(root, `models-${label}.json`), refreshOnCreate: false, allowModelNetwork: false });
|
|
78
|
+
const loader = new sdk.DefaultResourceLoader({
|
|
79
|
+
cwd, agentDir: bindings.PI_CODING_AGENT_DIR, settingsManager: settings,
|
|
80
|
+
noExtensions: true, noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true,
|
|
81
|
+
extensionFactories: [(pi) => {
|
|
82
|
+
const registerTool = pi.registerTool.bind(pi);
|
|
83
|
+
pi.registerTool = ((definition: { name: string; description: string; promptGuidelines?: readonly string[] }) => {
|
|
84
|
+
run.registered.set(definition.name, definition);
|
|
85
|
+
return registerTool(definition as never);
|
|
86
|
+
}) as typeof pi.registerTool;
|
|
87
|
+
createGentleAiExtension({
|
|
88
|
+
nativeReviewCli: nativeCli(mode),
|
|
89
|
+
candidateViews: null,
|
|
90
|
+
processEnv: { ...bindings, GENTLE_AI_TELEMETRY: "0" },
|
|
91
|
+
resolveTelemetryTriggerBinary: () => { throw new Error("fixture"); },
|
|
92
|
+
})(pi);
|
|
93
|
+
pi.on("tool_result", (event) => {
|
|
94
|
+
if (event.toolName === "gentle_review") run.assessResults.push({ isError: event.isError, details: event.details });
|
|
95
|
+
});
|
|
96
|
+
pi.registerProvider("loadout-fixture", {
|
|
97
|
+
api: "loadout-fixture", apiKey: "fixture-only", baseUrl: "http://invalid.local",
|
|
98
|
+
models: [{ id: model.id, name: model.name, reasoning: false, input: ["text"], cost: model.cost, contextWindow: model.contextWindow, maxTokens: model.maxTokens }],
|
|
99
|
+
streamSimple(_model, context) {
|
|
100
|
+
run.requests.push({ systemPrompt: ai.getCurrentSystemPrompt(context.messages), tools: ai.getCurrentTools(context.messages) });
|
|
101
|
+
const stream = ai.createAssistantMessageEventStream();
|
|
102
|
+
const request = next;
|
|
103
|
+
next = undefined;
|
|
104
|
+
calls += 1;
|
|
105
|
+
const message: AssistantMessage = { role: "assistant", api: model.api, provider: model.provider, model: model.id, content: [], stopReason: "pending" as never, timestamp: Date.now(), usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } };
|
|
106
|
+
queueMicrotask(() => {
|
|
107
|
+
stream.push({ type: "start", partial: message });
|
|
108
|
+
if (request) {
|
|
109
|
+
const toolCall: ToolCall = { type: "toolCall", id: `call-${calls}`, ...request };
|
|
110
|
+
message.content.push(toolCall);
|
|
111
|
+
stream.push({ type: "toolcall_start", contentIndex: 0, partial: message });
|
|
112
|
+
stream.push({ type: "toolcall_delta", contentIndex: 0, delta: JSON.stringify(request.arguments), partial: message });
|
|
113
|
+
stream.push({ type: "toolcall_end", contentIndex: 0, toolCall, partial: message });
|
|
114
|
+
}
|
|
115
|
+
message.stopReason = request ? "toolUse" : "stop";
|
|
116
|
+
stream.push({ type: "done", reason: message.stopReason, message });
|
|
117
|
+
stream.end();
|
|
118
|
+
});
|
|
119
|
+
return stream;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}],
|
|
123
|
+
});
|
|
124
|
+
await loader.reload();
|
|
125
|
+
assert.deepEqual(loader.getExtensions().errors, []);
|
|
126
|
+
const { session } = await sdk.createAgentSession({
|
|
127
|
+
cwd, agentDir: bindings.PI_CODING_AGENT_DIR, modelRuntime: runtime, model: model as never, thinkingLevel: "off", resourceLoader: loader,
|
|
128
|
+
tools: ["read", "gentle_review", ...HIDDEN_WHEN_OFF], sessionManager: manager, settingsManager: settings,
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await session.bindExtensions({ mode: "json", onError: (error) => run.errors.push(error.error) });
|
|
132
|
+
next = { name: "gentle_review", arguments: { operation: "assess", input: { escalate: { item: 4, reason: "Fixture process concurrency boundary" } } } };
|
|
133
|
+
await session.prompt("Assess the current change.", { source: "interactive" });
|
|
134
|
+
beforeSecondPrompt();
|
|
135
|
+
await session.prompt("Report the plan.", { source: "interactive" });
|
|
136
|
+
} finally {
|
|
137
|
+
await session.abort();
|
|
138
|
+
session.dispose();
|
|
139
|
+
}
|
|
140
|
+
return run;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const LEAN_GUIDELINE = /gentle_review only for \{"operation":"assess"\}/;
|
|
144
|
+
|
|
145
|
+
function fixture(t: { after: (fn: () => void) => void }): { root: string; bindings: Record<string, string> } {
|
|
146
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-review-loadout-")));
|
|
147
|
+
const profile = join(root, "profile");
|
|
148
|
+
mkdirSync(profile);
|
|
149
|
+
const bindings = {
|
|
150
|
+
PI_CODING_AGENT_DIR: profile, PI_CODING_AGENT_SESSION_DIR: join(root, "sessions"),
|
|
151
|
+
GENTLE_PI_AGENT_HOME: profile, GENTLE_PI_CONFIG_HOME: join(root, "config"),
|
|
152
|
+
};
|
|
153
|
+
const previous = Object.fromEntries(Object.keys(bindings).map((key) => [key, process.env[key]]));
|
|
154
|
+
Object.assign(process.env, bindings);
|
|
155
|
+
t.after(() => {
|
|
156
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
157
|
+
if (value === undefined) delete process.env[key];
|
|
158
|
+
else process.env[key] = value;
|
|
159
|
+
}
|
|
160
|
+
rmSync(root, { recursive: true, force: true });
|
|
161
|
+
});
|
|
162
|
+
return { root, bindings };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function registeredGuidelines(run: SessionRun): string[] {
|
|
166
|
+
const guidelines = ["gentle_review", ...HIDDEN_WHEN_OFF].flatMap((name) => run.registered.get(name)?.promptGuidelines ?? []);
|
|
167
|
+
assert.ok(guidelines.length > 0);
|
|
168
|
+
return guidelines;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertLeanRequest(request: RecordedRequest, run: SessionRun): void {
|
|
172
|
+
assert.match(request.systemPrompt, /Receipt-driven development: off /);
|
|
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/);
|
|
179
|
+
for (const guideline of registeredGuidelines(run)) assert.equal(request.systemPrompt.includes(guideline), false, `RDD off must leave out: ${guideline.slice(0, 80)}`);
|
|
180
|
+
const names = request.tools.map((tool) => tool.name);
|
|
181
|
+
assert.ok(names.includes("gentle_review"));
|
|
182
|
+
for (const hidden of HIDDEN_WHEN_OFF) assert.equal(names.includes(hidden), false, `${hidden} is not declared while RDD is off`);
|
|
183
|
+
assert.doesNotMatch(request.tools.find((tool) => tool.name === "gentle_review")!.description, /gentle_review_capture/);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function assertRegisteredRequest(request: RecordedRequest, run: SessionRun): void {
|
|
187
|
+
for (const guideline of registeredGuidelines(run)) assert.ok(request.systemPrompt.includes(guideline), `the registered loadout keeps: ${guideline.slice(0, 80)}`);
|
|
188
|
+
assert.doesNotMatch(request.systemPrompt, LEAN_GUIDELINE);
|
|
189
|
+
for (const name of ["gentle_review", ...HIDDEN_WHEN_OFF]) {
|
|
190
|
+
const declared = request.tools.find((tool) => tool.name === name);
|
|
191
|
+
assert.ok(declared, `${name} is declared`);
|
|
192
|
+
assert.equal(declared.description, run.registered.get(name)?.description);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test("RDD off: the parent request carries only the lean ASSESS loadout; RDD on: the registered review loadout", { timeout: 30_000 }, async (t) => {
|
|
197
|
+
const { root, bindings } = fixture(t);
|
|
198
|
+
const off = await runSession(root, "off", bindings, () => "off");
|
|
199
|
+
const on = await runSession(root, "on", bindings, () => "on");
|
|
200
|
+
assert.deepEqual(off.errors, []);
|
|
201
|
+
assert.deepEqual(on.errors, []);
|
|
202
|
+
assert.equal(off.requests.length >= 3, true, "assess turn, tool result turn, follow-up turn");
|
|
203
|
+
|
|
204
|
+
for (const request of off.requests) assertLeanRequest(request, off);
|
|
205
|
+
// The prefix is stable across turns of the same session.
|
|
206
|
+
assert.equal(new Set(off.requests.map((request) => request.systemPrompt)).size, 1);
|
|
207
|
+
assert.equal(new Set(off.requests.map((request) => JSON.stringify(request.tools))).size, 1);
|
|
208
|
+
// ASSESS stays callable and returns its plan.
|
|
209
|
+
assert.equal(off.assessResults.length, 1);
|
|
210
|
+
assert.equal(off.assessResults[0].isError, false);
|
|
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);
|
|
219
|
+
|
|
220
|
+
for (const request of on.requests) {
|
|
221
|
+
assert.match(request.systemPrompt, /Receipt-driven development: on /);
|
|
222
|
+
assertRegisteredRequest(request, on);
|
|
223
|
+
}
|
|
224
|
+
assert.equal(on.assessResults.length, 1);
|
|
225
|
+
assert.equal(on.assessResults[0].isError, false);
|
|
226
|
+
|
|
227
|
+
const bytes = (request: RecordedRequest) => ({
|
|
228
|
+
systemPrompt: Buffer.byteLength(request.systemPrompt),
|
|
229
|
+
tools: Buffer.byteLength(JSON.stringify(request.tools)),
|
|
230
|
+
});
|
|
231
|
+
t.diagnostic(`RDD off request bytes: ${JSON.stringify(bytes(off.requests[0]))}`);
|
|
232
|
+
t.diagnostic(`RDD on request bytes: ${JSON.stringify(bytes(on.requests[0]))}`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("an unknown RDD line keeps the registered loadout; turning RDD on mid-session restores it at the next run", { timeout: 30_000 }, async (t) => {
|
|
236
|
+
const { root, bindings } = fixture(t);
|
|
237
|
+
const { __testing } = await import("../extensions/gentle-ai.ts");
|
|
238
|
+
__testing.clearRddStatusMemoForTesting();
|
|
239
|
+
t.after(() => __testing.clearRddStatusMemoForTesting());
|
|
240
|
+
|
|
241
|
+
const unknown = await runSession(root, "unknown", bindings, () => "throws");
|
|
242
|
+
assert.deepEqual(unknown.errors, []);
|
|
243
|
+
for (const request of unknown.requests) {
|
|
244
|
+
assert.match(request.systemPrompt, /Receipt-driven development: unknown/);
|
|
245
|
+
assertRegisteredRequest(request, unknown);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let mode: RddMode = "off";
|
|
249
|
+
const flipped = await runSession(root, "flip", bindings, () => mode, () => {
|
|
250
|
+
mode = "on";
|
|
251
|
+
__testing.clearRddStatusMemoForTesting();
|
|
252
|
+
});
|
|
253
|
+
assert.deepEqual(flipped.errors, []);
|
|
254
|
+
const last = flipped.requests.at(-1)!;
|
|
255
|
+
for (const request of flipped.requests.slice(0, -1)) assertLeanRequest(request, flipped);
|
|
256
|
+
assert.match(last.systemPrompt, /Receipt-driven development: on /);
|
|
257
|
+
assertRegisteredRequest(last, flipped);
|
|
258
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import childProcess from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { createCandidateOwner, prepareCandidateOwnerParent } from "../lib/review-candidate-view-owner.ts";
|
|
9
|
+
|
|
10
|
+
function fixture(t: test.TestContext) {
|
|
11
|
+
fs.mkdirSync(resolve("tmp"), { recursive: true });
|
|
12
|
+
const base = fs.realpathSync(fs.mkdtempSync(join(resolve("tmp"), "owner-identity-")));
|
|
13
|
+
const commonDir = join(base, "common");
|
|
14
|
+
const parent = join(commonDir, "gentle-ai", "candidate-views");
|
|
15
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
16
|
+
const boundary = [commonDir, dirname(parent), parent];
|
|
17
|
+
let user = "S-1-5-21-1-2-3-1001";
|
|
18
|
+
let identityFailure = "";
|
|
19
|
+
let replacedMarker = false;
|
|
20
|
+
const probes = { user: 0, administrator: 0, owner: [] as string[], dacl: [] as string[], enforce: [] as string[] };
|
|
21
|
+
const native = fs.realpathSync.native;
|
|
22
|
+
t.mock.method(fs.realpathSync, "native", (path: string) =>
|
|
23
|
+
path.startsWith("\\\\?\\GLOBALROOT\\SystemRoot\\System32") ? path : native(path));
|
|
24
|
+
t.mock.method(childProcess, "execFileSync", (command: string, args: string[], options: any) => {
|
|
25
|
+
const name = command.split(/[\\/]/).at(-1);
|
|
26
|
+
if (name === "whoami.exe") {
|
|
27
|
+
probes.user++;
|
|
28
|
+
return identityFailure === "user" ? "unavailable" : user;
|
|
29
|
+
}
|
|
30
|
+
if (name === "icacls.exe") {
|
|
31
|
+
probes.dacl.push(args[0]);
|
|
32
|
+
const directory = fs.lstatSync(args[0]).isDirectory();
|
|
33
|
+
const flags = directory ? "OICI" : "ID";
|
|
34
|
+
const dacl = `D:${directory ? "P" : ""}(A;${flags};FA;;;${user})(A;${flags};FA;;;SY)(A;${flags};FA;;;BA)`;
|
|
35
|
+
fs.writeFileSync(join(options.cwd, args[2]), dacl, "utf16le");
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
if (name === "powershell.exe") {
|
|
39
|
+
const script = Buffer.from(args.at(-1)!, "base64").toString("utf16le");
|
|
40
|
+
if (script.includes("RawSecurityDescriptor")) {
|
|
41
|
+
probes.administrator++;
|
|
42
|
+
return identityFailure === "administrator" ? "unavailable" : "S-1-5-21-1-2-3-500";
|
|
43
|
+
}
|
|
44
|
+
if (script.includes("SetAccessControl")) {
|
|
45
|
+
probes.enforce.push(options.env.GENTLE_PI_CANDIDATE_ACL_PATH);
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
const path = options.env.GENTLE_PI_CANDIDATE_OWNER_PATH;
|
|
49
|
+
probes.owner.push(path);
|
|
50
|
+
return replacedMarker && path.endsWith(".owner.json") ? "S-1-5-21-9-8-7-1002" : user;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Unexpected subprocess: ${name}`);
|
|
53
|
+
});
|
|
54
|
+
syncBuiltinESMExports();
|
|
55
|
+
t.after(() => {
|
|
56
|
+
t.mock.restoreAll();
|
|
57
|
+
syncBuiltinESMExports();
|
|
58
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
return { commonDir, parent, boundary, probes,
|
|
61
|
+
changeUser: () => { user = "S-1-5-21-4-5-6-1002"; },
|
|
62
|
+
failIdentity: (kind: string) => { identityFailure = kind; },
|
|
63
|
+
replaceMarker: () => { replacedMarker = true; } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test("Windows creation reuses identity only within one operation and preserves every filesystem probe", (t) => {
|
|
67
|
+
const f = fixture(t);
|
|
68
|
+
for (let operation = 1; operation <= 2; operation++) {
|
|
69
|
+
if (operation === 2) f.changeUser();
|
|
70
|
+
const root = join(f.parent, randomUUID());
|
|
71
|
+
const owner = createCandidateOwner(f.commonDir, root, "win32");
|
|
72
|
+
const marker = `${root}.owner.json`;
|
|
73
|
+
assert.equal(owner.root, root);
|
|
74
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(marker, "utf8")), owner);
|
|
75
|
+
assert.equal(fs.existsSync(root), false);
|
|
76
|
+
assert.equal(f.probes.user, operation, "one fresh user SID per creation");
|
|
77
|
+
assert.equal(f.probes.administrator, operation, "one fresh administrator SID per creation");
|
|
78
|
+
assert.deepEqual(f.probes.owner.slice((operation - 1) * 4), [...f.boundary, marker]);
|
|
79
|
+
assert.deepEqual(f.probes.dacl.slice((operation - 1) * 4), [...f.boundary, marker]);
|
|
80
|
+
assert.deepEqual(f.probes.enforce, []);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Windows preparation retains fresh pre-write and post-write owner/DACL checks", (t) => {
|
|
85
|
+
const f = fixture(t);
|
|
86
|
+
assert.equal(prepareCandidateOwnerParent(f.commonDir, "win32"), f.parent);
|
|
87
|
+
assert.equal(f.probes.user, 1);
|
|
88
|
+
assert.equal(f.probes.administrator, 1);
|
|
89
|
+
assert.deepEqual(f.probes.owner, [...f.boundary, ...f.boundary.flatMap(path => [path, path])]);
|
|
90
|
+
assert.deepEqual(f.probes.dacl, f.boundary);
|
|
91
|
+
assert.deepEqual(f.probes.enforce, f.boundary);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
for (const kind of ["user", "administrator"]) test(`Windows creation fails closed on fresh ${kind} identity failure`, (t) => {
|
|
95
|
+
const f = fixture(t);
|
|
96
|
+
const first = createCandidateOwner(f.commonDir, join(f.parent, randomUUID()), "win32");
|
|
97
|
+
const marker = `${first.root}.owner.json`;
|
|
98
|
+
const before = fs.readFileSync(marker, "utf8");
|
|
99
|
+
const stored = fs.readdirSync(f.parent);
|
|
100
|
+
f.failIdentity(kind);
|
|
101
|
+
const root = join(f.parent, randomUUID());
|
|
102
|
+
assert.throws(() => createCandidateOwner(f.commonDir, root, "win32"), /SID is unavailable/);
|
|
103
|
+
assert.equal(fs.existsSync(root), false);
|
|
104
|
+
assert.equal(fs.existsSync(`${root}.owner.json`), false);
|
|
105
|
+
assert.equal(fs.readFileSync(marker, "utf8"), before);
|
|
106
|
+
assert.deepEqual(fs.readdirSync(f.parent), stored);
|
|
107
|
+
assert.equal(f.probes.owner.length, 4);
|
|
108
|
+
assert.equal(f.probes.dacl.length, 4);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Windows creation rollback rechecks a replaced marker owner before removal", (t) => {
|
|
112
|
+
const f = fixture(t);
|
|
113
|
+
const write = fs.writeFileSync;
|
|
114
|
+
const root = join(f.parent, randomUUID());
|
|
115
|
+
const marker = `${root}.owner.json`;
|
|
116
|
+
t.mock.method(fs, "writeFileSync", (...args: any[]) => {
|
|
117
|
+
if (typeof args[0] === "number") {
|
|
118
|
+
f.replaceMarker();
|
|
119
|
+
throw new Error("fixture write failure");
|
|
120
|
+
}
|
|
121
|
+
return (write as any)(...args);
|
|
122
|
+
});
|
|
123
|
+
syncBuiltinESMExports();
|
|
124
|
+
assert.throws(() => createCandidateOwner(f.commonDir, root, "win32"), /fixture write failure/);
|
|
125
|
+
assert.equal(f.probes.user, 1);
|
|
126
|
+
assert.equal(f.probes.administrator, 1);
|
|
127
|
+
assert.deepEqual(f.probes.owner, [...f.boundary, marker, marker]);
|
|
128
|
+
assert.deepEqual(f.probes.dacl, [...f.boundary, marker]);
|
|
129
|
+
assert.equal(fs.existsSync(marker), true, "unproven marker survives rollback");
|
|
130
|
+
assert.equal(fs.readFileSync(marker, "utf8"), "");
|
|
131
|
+
assert.equal(fs.existsSync(root), false);
|
|
132
|
+
});
|